feat: Initial Hub implementation

Complete LetsBe Hub service for license management and telemetry:

- Client and Instance CRUD APIs
- License key generation and validation (lb_inst_ format)
- Hub API key generation (hk_ format) for telemetry auth
- Instance activation endpoint
- Telemetry collection with privacy-first redactor
- Key rotation and suspend/reactivate functionality
- Alembic migrations for PostgreSQL
- Docker Compose deployment ready
- Comprehensive test suite

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-22 14:09:32 +01:00
commit adc02e176b
39 changed files with 2968 additions and 0 deletions

21
app/schemas/__init__.py Normal file
View File

@@ -0,0 +1,21 @@
"""Hub API schemas."""
from app.schemas.client import ClientCreate, ClientResponse, ClientUpdate
from app.schemas.instance import (
ActivationError,
ActivationRequest,
ActivationResponse,
InstanceCreate,
InstanceResponse,
)
__all__ = [
"ClientCreate",
"ClientResponse",
"ClientUpdate",
"InstanceCreate",
"InstanceResponse",
"ActivationRequest",
"ActivationResponse",
"ActivationError",
]

38
app/schemas/client.py Normal file
View File

@@ -0,0 +1,38 @@
"""Client schemas for API serialization."""
from datetime import datetime
from typing import Optional
from uuid import UUID
from pydantic import BaseModel, ConfigDict, EmailStr, Field
class ClientCreate(BaseModel):
"""Schema for creating a new client."""
name: str = Field(..., min_length=1, max_length=255, description="Client/company name")
contact_email: Optional[EmailStr] = Field(None, description="Primary contact email")
billing_plan: str = Field("free", description="Billing plan")
class ClientUpdate(BaseModel):
"""Schema for updating a client."""
name: Optional[str] = Field(None, min_length=1, max_length=255)
contact_email: Optional[EmailStr] = None
billing_plan: Optional[str] = None
status: Optional[str] = Field(None, pattern="^(active|suspended|archived)$")
class ClientResponse(BaseModel):
"""Schema for client API responses."""
model_config = ConfigDict(from_attributes=True)
id: UUID
name: str
contact_email: Optional[str]
billing_plan: str
status: str
created_at: datetime
updated_at: datetime

127
app/schemas/instance.py Normal file
View File

@@ -0,0 +1,127 @@
"""Instance schemas for API serialization."""
from datetime import datetime
from typing import Optional
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field
class InstanceCreate(BaseModel):
"""Schema for creating a new instance."""
instance_id: str = Field(
...,
min_length=1,
max_length=255,
description="Unique instance identifier (e.g., 'acme-orchestrator')",
)
region: Optional[str] = Field(None, max_length=50, description="Deployment region")
license_expires_at: Optional[datetime] = Field(
None,
description="License expiry date (None = perpetual)",
)
class InstanceResponse(BaseModel):
"""Schema for instance API responses.
Note: license_key and hub_api_key are ONLY returned on creation.
"""
model_config = ConfigDict(from_attributes=True)
id: UUID
instance_id: str
client_id: UUID
# License info
license_key: Optional[str] = Field(
None,
description="ONLY returned on creation - store securely!",
)
license_key_prefix: str
license_status: str
license_issued_at: datetime
license_expires_at: Optional[datetime]
# Hub API key
hub_api_key: Optional[str] = Field(
None,
description="ONLY returned on creation - store securely!",
)
# Activation state
activated_at: Optional[datetime]
last_activation_at: Optional[datetime]
activation_count: int
# Metadata
region: Optional[str]
version: Optional[str]
last_seen_at: Optional[datetime]
status: str
created_at: datetime
updated_at: datetime
class InstanceBriefResponse(BaseModel):
"""Brief instance response for listings (no secrets)."""
model_config = ConfigDict(from_attributes=True)
id: UUID
instance_id: str
client_id: UUID
license_key_prefix: str
license_status: str
license_expires_at: Optional[datetime]
activated_at: Optional[datetime]
activation_count: int
region: Optional[str]
status: str
created_at: datetime
# === ACTIVATION SCHEMAS ===
class ActivationRequest(BaseModel):
"""
Activation request from a client instance.
PRIVACY: This schema ONLY accepts:
- license_key (credential for validation)
- instance_id (identifier)
It NEVER accepts sensitive data fields.
"""
license_key: str = Field(..., description="License key (lb_inst_...)")
instance_id: str = Field(..., description="Instance identifier")
class ActivationResponse(BaseModel):
"""Response to a successful activation."""
status: str = Field("ok", description="Activation status")
instance_id: str
hub_api_key: str = Field(
...,
description="API key for telemetry auth (or 'USE_EXISTING')",
)
config: dict = Field(
default_factory=dict,
description="Optional configuration from Hub",
)
class ActivationError(BaseModel):
"""Error response for failed activation."""
error: str = Field(..., description="Human-readable error message")
code: str = Field(
...,
description="Error code: invalid_license, expired, suspended, instance_not_found",
)

105
app/schemas/telemetry.py Normal file
View File

@@ -0,0 +1,105 @@
"""Telemetry schemas for orchestrator metrics collection.
PRIVACY GUARANTEE: These schemas use extra="forbid" to reject
unknown fields, preventing accidental PII leaks.
"""
from datetime import datetime
from typing import Optional
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field
# === Nested Metrics Schemas ===
class AgentMetrics(BaseModel):
"""Agent status counts."""
model_config = ConfigDict(extra="forbid")
online_count: int = Field(ge=0, description="Agents currently online")
offline_count: int = Field(ge=0, description="Agents currently offline")
total_count: int = Field(ge=0, description="Total registered agents")
class TaskTypeMetrics(BaseModel):
"""Per-task-type metrics."""
model_config = ConfigDict(extra="forbid")
count: int = Field(ge=0, description="Number of tasks of this type")
avg_duration_ms: Optional[float] = Field(
None,
ge=0,
description="Average duration in milliseconds",
)
class TaskMetrics(BaseModel):
"""Task execution metrics."""
model_config = ConfigDict(extra="forbid")
by_status: dict[str, int] = Field(
default_factory=dict,
description="Task counts by status (completed, failed, running, pending)",
)
by_type: dict[str, TaskTypeMetrics] = Field(
default_factory=dict,
description="Task metrics by type (SHELL, FILE_WRITE, etc.)",
)
class ServerMetrics(BaseModel):
"""Server metrics."""
model_config = ConfigDict(extra="forbid")
total_count: int = Field(ge=0, description="Total registered servers")
class TelemetryMetrics(BaseModel):
"""Top-level metrics container."""
model_config = ConfigDict(extra="forbid")
agents: AgentMetrics
tasks: TaskMetrics
servers: ServerMetrics
# === Request/Response Schemas ===
class TelemetryPayload(BaseModel):
"""
Telemetry payload from an orchestrator instance.
PRIVACY: This schema deliberately uses extra="forbid" to reject
any fields not explicitly defined. This prevents accidental
transmission of PII or sensitive data.
De-duplication: The Hub uses (instance_id, window_start) as a
unique constraint to handle duplicate submissions.
"""
model_config = ConfigDict(extra="forbid")
instance_id: UUID = Field(..., description="Instance UUID (must match path)")
window_start: datetime = Field(..., description="Start of telemetry window")
window_end: datetime = Field(..., description="End of telemetry window")
uptime_seconds: int = Field(ge=0, description="Orchestrator uptime in seconds")
metrics: TelemetryMetrics = Field(..., description="Aggregated metrics")
class TelemetryResponse(BaseModel):
"""Response to telemetry submission."""
received: bool = Field(True, description="Whether telemetry was accepted")
next_interval_seconds: int = Field(
60,
description="Suggested interval for next submission",
)
message: Optional[str] = Field(None, description="Optional status message")