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:
21
app/schemas/__init__.py
Normal file
21
app/schemas/__init__.py
Normal 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
38
app/schemas/client.py
Normal 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
127
app/schemas/instance.py
Normal 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
105
app/schemas/telemetry.py
Normal 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")
|
||||
Reference in New Issue
Block a user