Include full contents of all nested repositories
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
60
letsbe-orchestrator/app/schemas/__init__.py
Normal file
60
letsbe-orchestrator/app/schemas/__init__.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""Pydantic schemas for API request/response validation."""
|
||||
|
||||
from app.schemas.common import HealthResponse, InstanceMetaResponse
|
||||
from app.schemas.tenant import TenantCreate, TenantResponse
|
||||
from app.schemas.task import (
|
||||
TaskCreate,
|
||||
TaskResponse,
|
||||
TaskUpdate,
|
||||
)
|
||||
from app.schemas.agent import (
|
||||
AgentRegisterRequest,
|
||||
AgentRegisterResponse,
|
||||
AgentHeartbeatResponse,
|
||||
AgentResponse,
|
||||
LocalAgentRegisterRequest,
|
||||
LocalAgentRegisterResponse,
|
||||
)
|
||||
from app.schemas.tasks_extended import (
|
||||
FileWritePayload,
|
||||
EnvUpdatePayload,
|
||||
DockerReloadPayload,
|
||||
CompositeSubTask,
|
||||
CompositePayload,
|
||||
)
|
||||
from app.schemas.env import (
|
||||
EnvInspectRequest,
|
||||
EnvUpdateRequest,
|
||||
)
|
||||
from app.schemas.file import FileInspectRequest
|
||||
|
||||
__all__ = [
|
||||
# Common
|
||||
"HealthResponse",
|
||||
"InstanceMetaResponse",
|
||||
# Tenant
|
||||
"TenantCreate",
|
||||
"TenantResponse",
|
||||
# Task
|
||||
"TaskCreate",
|
||||
"TaskResponse",
|
||||
"TaskUpdate",
|
||||
# Task Payloads (for documentation/reference)
|
||||
"FileWritePayload",
|
||||
"EnvUpdatePayload",
|
||||
"DockerReloadPayload",
|
||||
"CompositeSubTask",
|
||||
"CompositePayload",
|
||||
# Agent
|
||||
"AgentRegisterRequest",
|
||||
"AgentRegisterResponse",
|
||||
"AgentHeartbeatResponse",
|
||||
"AgentResponse",
|
||||
"LocalAgentRegisterRequest",
|
||||
"LocalAgentRegisterResponse",
|
||||
# Env Management
|
||||
"EnvInspectRequest",
|
||||
"EnvUpdateRequest",
|
||||
# File Management
|
||||
"FileInspectRequest",
|
||||
]
|
||||
111
letsbe-orchestrator/app/schemas/agent.py
Normal file
111
letsbe-orchestrator/app/schemas/agent.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""Agent schemas for API validation."""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class AgentRegisterRequest(BaseModel):
|
||||
"""Schema for agent registration request (new secure flow)."""
|
||||
|
||||
hostname: str = Field(..., min_length=1, max_length=255)
|
||||
version: str = Field(..., min_length=1, max_length=50)
|
||||
metadata: dict[str, Any] | None = None
|
||||
registration_token: str = Field(
|
||||
...,
|
||||
min_length=1,
|
||||
description="Registration token issued by the orchestrator",
|
||||
)
|
||||
|
||||
|
||||
class AgentRegisterRequestLegacy(BaseModel):
|
||||
"""Schema for legacy agent registration request (deprecated).
|
||||
|
||||
This schema is kept for backward compatibility during migration.
|
||||
New agents should use AgentRegisterRequest with registration_token.
|
||||
"""
|
||||
|
||||
hostname: str = Field(..., min_length=1, max_length=255)
|
||||
version: str = Field(..., min_length=1, max_length=50)
|
||||
metadata: dict[str, Any] | None = None
|
||||
tenant_id: uuid.UUID | None = Field(
|
||||
default=None,
|
||||
description="Tenant UUID to associate the agent with (DEPRECATED)",
|
||||
)
|
||||
|
||||
|
||||
class AgentRegisterResponse(BaseModel):
|
||||
"""Schema for agent registration response."""
|
||||
|
||||
agent_id: uuid.UUID
|
||||
agent_secret: str = Field(
|
||||
...,
|
||||
description="Agent secret for authentication. Store securely - shown only once.",
|
||||
)
|
||||
tenant_id: uuid.UUID = Field(
|
||||
...,
|
||||
description="Tenant this agent is associated with",
|
||||
)
|
||||
|
||||
|
||||
class LocalAgentRegisterRequest(BaseModel):
|
||||
"""Schema for LOCAL_MODE agent registration request.
|
||||
|
||||
Unlike AgentRegisterRequest, this does NOT include registration_token
|
||||
because LOCAL_MODE uses X-Local-Agent-Key header authentication.
|
||||
"""
|
||||
|
||||
hostname: str = Field(..., min_length=1, max_length=255)
|
||||
version: str = Field(..., min_length=1, max_length=50)
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class LocalAgentRegisterResponse(BaseModel):
|
||||
"""Schema for LOCAL_MODE agent registration response (idempotent).
|
||||
|
||||
This endpoint is idempotent:
|
||||
- First registration: returns agent_id, agent_secret, already_registered=False
|
||||
- Subsequent calls: returns agent_id, NO secret, already_registered=True
|
||||
- With rotate=True: deletes existing, returns new credentials
|
||||
"""
|
||||
|
||||
agent_id: uuid.UUID
|
||||
tenant_id: uuid.UUID
|
||||
agent_secret: str | None = Field(
|
||||
default=None,
|
||||
description="Agent secret. Only returned on first registration or rotation.",
|
||||
)
|
||||
already_registered: bool = Field(
|
||||
default=False,
|
||||
description="True if returning existing agent (no new secret).",
|
||||
)
|
||||
|
||||
|
||||
class AgentRegisterResponseLegacy(BaseModel):
|
||||
"""Schema for legacy agent registration response (deprecated)."""
|
||||
|
||||
agent_id: uuid.UUID
|
||||
token: str
|
||||
|
||||
|
||||
class AgentHeartbeatResponse(BaseModel):
|
||||
"""Schema for agent heartbeat response."""
|
||||
|
||||
status: str = "ok"
|
||||
|
||||
|
||||
class AgentResponse(BaseModel):
|
||||
"""Schema for agent response."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: uuid.UUID
|
||||
tenant_id: uuid.UUID | None
|
||||
name: str
|
||||
version: str
|
||||
status: str
|
||||
last_heartbeat: datetime | None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
39
letsbe-orchestrator/app/schemas/common.py
Normal file
39
letsbe-orchestrator/app/schemas/common.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""Common schemas used across the API."""
|
||||
|
||||
from typing import Generic, Optional, TypeVar
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
"""Health check response schema."""
|
||||
|
||||
status: str
|
||||
version: str
|
||||
|
||||
|
||||
class InstanceMetaResponse(BaseModel):
|
||||
"""
|
||||
Instance metadata response.
|
||||
|
||||
This endpoint is stable even before tenant bootstrap completes.
|
||||
Used for diagnostics and instance identification.
|
||||
"""
|
||||
|
||||
instance_id: Optional[str] = None
|
||||
local_mode: bool
|
||||
version: str
|
||||
tenant_id: Optional[str] = None
|
||||
bootstrap_status: dict
|
||||
|
||||
|
||||
class PaginatedResponse(BaseModel, Generic[T]):
|
||||
"""Generic paginated response wrapper."""
|
||||
|
||||
items: list[T]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
total_pages: int
|
||||
32
letsbe-orchestrator/app/schemas/env.py
Normal file
32
letsbe-orchestrator/app/schemas/env.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""Pydantic schemas for env management endpoints."""
|
||||
|
||||
import uuid
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class EnvInspectRequest(BaseModel):
|
||||
"""Request body for env inspect endpoint."""
|
||||
|
||||
tenant_id: uuid.UUID = Field(..., description="UUID of the tenant")
|
||||
path: str = Field(
|
||||
..., min_length=1, description="Path to .env file (e.g., /opt/letsbe/env/chatwoot.env)"
|
||||
)
|
||||
keys: list[str] | None = Field(
|
||||
None, description="Optional list of specific keys to inspect"
|
||||
)
|
||||
|
||||
|
||||
class EnvUpdateRequest(BaseModel):
|
||||
"""Request body for env update endpoint."""
|
||||
|
||||
tenant_id: uuid.UUID = Field(..., description="UUID of the tenant")
|
||||
path: str = Field(
|
||||
..., min_length=1, description="Path to .env file (e.g., /opt/letsbe/env/chatwoot.env)"
|
||||
)
|
||||
updates: dict[str, str] | None = Field(
|
||||
None, description="Key-value pairs to set or update"
|
||||
)
|
||||
remove_keys: list[str] | None = Field(
|
||||
None, description="Keys to remove from the env file"
|
||||
)
|
||||
37
letsbe-orchestrator/app/schemas/event.py
Normal file
37
letsbe-orchestrator/app/schemas/event.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Event schemas for API validation."""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class EventCreate(BaseModel):
|
||||
"""Schema for creating a new event."""
|
||||
|
||||
tenant_id: uuid.UUID
|
||||
task_id: uuid.UUID | None = None
|
||||
event_type: str = Field(
|
||||
...,
|
||||
min_length=1,
|
||||
max_length=100,
|
||||
description="Event type identifier (e.g. agent.registered, task.completed)",
|
||||
)
|
||||
payload: dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="Event-specific payload data",
|
||||
)
|
||||
|
||||
|
||||
class EventResponse(BaseModel):
|
||||
"""Schema for event response."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: uuid.UUID
|
||||
tenant_id: uuid.UUID
|
||||
task_id: uuid.UUID | None
|
||||
event_type: str
|
||||
payload: dict[str, Any]
|
||||
created_at: datetime
|
||||
20
letsbe-orchestrator/app/schemas/file.py
Normal file
20
letsbe-orchestrator/app/schemas/file.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""Pydantic schemas for file management endpoints."""
|
||||
|
||||
import uuid
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class FileInspectRequest(BaseModel):
|
||||
"""Request body for FILE_INSPECT tasks."""
|
||||
|
||||
tenant_id: uuid.UUID = Field(..., description="UUID of the tenant")
|
||||
path: str = Field(
|
||||
..., min_length=1, description="Absolute path to the file to inspect"
|
||||
)
|
||||
max_bytes: int | None = Field(
|
||||
4096,
|
||||
ge=1,
|
||||
le=1048576,
|
||||
description="Max bytes to read from file (default 4096, max 1MB)",
|
||||
)
|
||||
63
letsbe-orchestrator/app/schemas/registration_token.py
Normal file
63
letsbe-orchestrator/app/schemas/registration_token.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""Registration token schemas for API validation."""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class RegistrationTokenCreate(BaseModel):
|
||||
"""Schema for creating a new registration token."""
|
||||
|
||||
description: str | None = Field(
|
||||
default=None,
|
||||
max_length=255,
|
||||
description="Human-readable description for this token",
|
||||
)
|
||||
max_uses: int = Field(
|
||||
default=1,
|
||||
ge=0,
|
||||
description="Maximum number of times this token can be used (0 = unlimited)",
|
||||
)
|
||||
expires_in_hours: int | None = Field(
|
||||
default=None,
|
||||
ge=1,
|
||||
le=8760, # Max 1 year
|
||||
description="Number of hours until this token expires (optional)",
|
||||
)
|
||||
|
||||
|
||||
class RegistrationTokenResponse(BaseModel):
|
||||
"""Schema for registration token response (without plaintext token)."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: uuid.UUID
|
||||
tenant_id: uuid.UUID
|
||||
description: str | None
|
||||
max_uses: int
|
||||
use_count: int
|
||||
expires_at: datetime | None
|
||||
revoked: bool
|
||||
created_at: datetime
|
||||
created_by: str | None
|
||||
|
||||
|
||||
class RegistrationTokenCreatedResponse(RegistrationTokenResponse):
|
||||
"""Schema for registration token creation response.
|
||||
|
||||
This is the only time the plaintext token is returned to the client.
|
||||
It must be securely stored as it cannot be retrieved again.
|
||||
"""
|
||||
|
||||
token: str = Field(
|
||||
...,
|
||||
description="The plaintext registration token. Store this securely - it cannot be retrieved again.",
|
||||
)
|
||||
|
||||
|
||||
class RegistrationTokenList(BaseModel):
|
||||
"""Schema for listing registration tokens."""
|
||||
|
||||
tokens: list[RegistrationTokenResponse]
|
||||
total: int
|
||||
70
letsbe-orchestrator/app/schemas/task.py
Normal file
70
letsbe-orchestrator/app/schemas/task.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""Task schemas for API validation."""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from app.models.task import TaskStatus
|
||||
|
||||
|
||||
class TaskCreate(BaseModel):
|
||||
"""
|
||||
Schema for creating a new task.
|
||||
|
||||
Supported task types and their expected payloads:
|
||||
|
||||
**FILE_WRITE** - Write content to a file
|
||||
payload: {"path": "/absolute/path", "content": "file content"}
|
||||
|
||||
**ENV_UPDATE** - Update key/value pairs in a .env file
|
||||
payload: {"path": "/path/to/.env", "updates": {"KEY": "value"}}
|
||||
|
||||
**DOCKER_RELOAD** - Reload a Docker Compose stack
|
||||
payload: {"compose_dir": "/path/to/compose/dir"}
|
||||
|
||||
**COMPOSITE** - Execute a sequence of sub-tasks
|
||||
payload: {"sequence": [{"task": "FILE_WRITE", "payload": {...}}, ...]}
|
||||
|
||||
Legacy types (still supported):
|
||||
- provision_server, configure_keycloak, configure_minio, etc.
|
||||
|
||||
Note: Payload validation is performed agent-side. The orchestrator
|
||||
accepts any dict payload to allow flexibility and forward compatibility.
|
||||
"""
|
||||
|
||||
tenant_id: uuid.UUID
|
||||
type: str = Field(
|
||||
...,
|
||||
min_length=1,
|
||||
max_length=100,
|
||||
description="Task type (FILE_WRITE, ENV_UPDATE, DOCKER_RELOAD, COMPOSITE, etc.)",
|
||||
)
|
||||
payload: dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="Task-specific payload (see docstring for formats)",
|
||||
)
|
||||
|
||||
|
||||
class TaskUpdate(BaseModel):
|
||||
"""Schema for updating a task (status and result only)."""
|
||||
|
||||
status: TaskStatus | None = None
|
||||
result: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class TaskResponse(BaseModel):
|
||||
"""Schema for task response."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: uuid.UUID
|
||||
tenant_id: uuid.UUID
|
||||
agent_id: uuid.UUID | None
|
||||
type: str
|
||||
payload: dict[str, Any]
|
||||
status: str
|
||||
result: dict[str, Any] | None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
73
letsbe-orchestrator/app/schemas/tasks_extended.py
Normal file
73
letsbe-orchestrator/app/schemas/tasks_extended.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""Extended task payload schemas for SysAdmin Agent automation.
|
||||
|
||||
These schemas define the expected payload structure for each task type.
|
||||
Validation is performed agent-side; the orchestrator accepts any dict payload.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class FileWritePayload(BaseModel):
|
||||
"""
|
||||
Payload for FILE_WRITE task type.
|
||||
|
||||
Instructs the agent to write content to a file at the specified path.
|
||||
"""
|
||||
|
||||
path: str = Field(..., description="Absolute path to the target file")
|
||||
content: str = Field(..., description="Content to write to the file")
|
||||
|
||||
|
||||
class EnvUpdatePayload(BaseModel):
|
||||
"""
|
||||
Payload for ENV_UPDATE task type.
|
||||
|
||||
Instructs the agent to update key/value pairs in an .env file.
|
||||
Existing keys are updated; new keys are appended.
|
||||
"""
|
||||
|
||||
path: str = Field(..., description="Absolute path to the .env file")
|
||||
updates: dict[str, str] = Field(
|
||||
..., description="Key-value pairs to update or add"
|
||||
)
|
||||
|
||||
|
||||
class DockerReloadPayload(BaseModel):
|
||||
"""
|
||||
Payload for DOCKER_RELOAD task type.
|
||||
|
||||
Instructs the agent to reload a Docker Compose stack.
|
||||
Equivalent to: docker compose down && docker compose up -d
|
||||
"""
|
||||
|
||||
compose_dir: str = Field(
|
||||
..., description="Directory containing docker-compose.yml"
|
||||
)
|
||||
|
||||
|
||||
class CompositeSubTask(BaseModel):
|
||||
"""
|
||||
A single sub-task within a COMPOSITE task.
|
||||
|
||||
Represents one step in a multi-step automation sequence.
|
||||
"""
|
||||
|
||||
task: str = Field(..., description="Task type (e.g., FILE_WRITE, ENV_UPDATE)")
|
||||
payload: dict[str, Any] = Field(
|
||||
default_factory=dict, description="Payload for this sub-task"
|
||||
)
|
||||
|
||||
|
||||
class CompositePayload(BaseModel):
|
||||
"""
|
||||
Payload for COMPOSITE task type.
|
||||
|
||||
Instructs the agent to execute a sequence of sub-tasks in order.
|
||||
If any sub-task fails, the sequence stops and the composite task fails.
|
||||
"""
|
||||
|
||||
sequence: list[CompositeSubTask] = Field(
|
||||
..., description="Ordered list of sub-tasks to execute"
|
||||
)
|
||||
45
letsbe-orchestrator/app/schemas/tenant.py
Normal file
45
letsbe-orchestrator/app/schemas/tenant.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""Tenant schemas for API validation."""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.tenant import Tenant
|
||||
|
||||
|
||||
class TenantCreate(BaseModel):
|
||||
"""Schema for creating a new tenant."""
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=255)
|
||||
domain: str | None = Field(None, max_length=255)
|
||||
|
||||
|
||||
class TenantResponse(BaseModel):
|
||||
"""Schema for tenant response."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: uuid.UUID
|
||||
name: str
|
||||
domain: str | None
|
||||
has_dashboard_token: bool = Field(
|
||||
default=False,
|
||||
description="Whether a dashboard token has been configured",
|
||||
)
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
@classmethod
|
||||
def from_orm_with_token_check(cls, tenant: "Tenant") -> "TenantResponse":
|
||||
"""Create response with dashboard token check."""
|
||||
return cls(
|
||||
id=tenant.id,
|
||||
name=tenant.name,
|
||||
domain=tenant.domain,
|
||||
has_dashboard_token=tenant.dashboard_token_hash is not None,
|
||||
created_at=tenant.created_at,
|
||||
updated_at=tenant.updated_at,
|
||||
)
|
||||
Reference in New Issue
Block a user