"""Pydantic request/response models for the platform SDK REST API.""" from __future__ import annotations from datetime import datetime from typing import Any, Literal from pydantic import BaseModel, Field, SecretStr class TaskConstraintsModel(BaseModel): driver_type: str | None = None capability_tags: list[str] = Field(default_factory=list) target_host_id: str | None = Field(default=None, min_length=1) target_device_id: str | None = Field(default=None, min_length=1) class TaskSubmissionRequest(BaseModel): goal: str | None = None workflow_definition_id: str | None = None constraints: TaskConstraintsModel = Field(default_factory=TaskConstraintsModel) class TaskSubmissionResponse(BaseModel): task_id: str class TaskStatusResponse(BaseModel): id: str status: str goal: str | None = None workflow_definition_id: str | None = None assigned_device_id: str | None = None assigned_host_id: str | None = None attempt_count: int = 0 lease_expires_at: datetime | None = None failure_reason: str | None = None target_host_id: str | None = None target_device_id: str | None = None progress_step_index: int | None = None progress_step_status: str | None = None progress_summary: str | None = None progress_updated_at: datetime | None = None class TaskListItem(BaseModel): id: str status: str goal: str | None = None workflow_definition_id: str | None = None assigned_device_id: str | None = None assigned_host_id: str | None = None attempt_count: int = 0 failure_reason: str | None = None target_host_id: str | None = None target_device_id: str | None = None created_at: datetime progress_step_index: int | None = None progress_step_status: str | None = None progress_summary: str | None = None progress_updated_at: datetime | None = None class TaskListResponse(BaseModel): items: list[TaskListItem] total: int limit: int offset: int class TaskAttemptResponse(BaseModel): task_id: str attempt: int lease_id: str host_id: str device_id: str status: str lease_expires_at: datetime created_at: datetime completed_at: datetime | None = None failure_reason: str | None = None terminal_result: dict[str, Any] | None = None class DeviceResponse(BaseModel): device_id: str host_id: str driver_type: str status: str capability_tags: list[str] = Field(default_factory=list) class HostResponse(BaseModel): host_id: str address: str | None = None last_seen_at: str planner_transport: Literal["direct", "cloud"] = "direct" class PluginRegistrationRequest(BaseModel): name: str version: str entry_point_kind: Literal["driver", "tool", "skill"] target: str class PluginResponse(BaseModel): name: str version: str entry_point_kind: str target: str wired: bool class LoginRequest(BaseModel): username: str = Field(min_length=1, max_length=64) password: str = Field(min_length=1, max_length=256) class PasswordChangeRequest(BaseModel): current_password: str = Field(min_length=1, max_length=256) new_password: str = Field(min_length=1, max_length=256) class UserResponse(BaseModel): id: str username: str display_name: str role: Literal["viewer", "operator", "admin"] enabled: bool must_change_password: bool scopes: list[str] created_at: datetime updated_at: datetime last_login_at: datetime | None = None class UserListResponse(BaseModel): items: list[UserResponse] limit: int offset: int class UserCreateRequest(BaseModel): username: str = Field(min_length=1, max_length=64) display_name: str = Field(min_length=1, max_length=120) role: Literal["viewer", "operator", "admin"] password: str = Field(min_length=1, max_length=256) class UserUpdateRequest(BaseModel): display_name: str | None = Field(default=None, min_length=1, max_length=120) role: Literal["viewer", "operator", "admin"] | None = None enabled: bool | None = None class PasswordResetRequest(BaseModel): password: str = Field(min_length=1, max_length=256) class ErrorResponse(BaseModel): detail: str class DeviceTargetModel(BaseModel): host_id: str = Field(min_length=1) device_id: str = Field(min_length=1) class UserSubmissionPolicyRequest(BaseModel): submission_enabled: bool = True allowed_host_ids: list[str] | None = None allowed_device_targets: list[DeviceTargetModel] | None = None expected_revision: int | None = Field(default=None, ge=0) class UserSubmissionPolicyResponse(BaseModel): user_id: str revision: int submission_enabled: bool allowed_host_ids: list[str] | None = None allowed_device_targets: list[DeviceTargetModel] | None = None updated_at: datetime class HostGovernancePolicyRequest(BaseModel): self_submission_enabled: bool = True max_active_tasks: int | None = Field(default=None, ge=1) daily_token_budget: int | None = Field(default=None, ge=1) expected_revision: int | None = Field(default=None, ge=0) class HostGovernancePolicyResponse(BaseModel): host_id: str revision: int self_submission_enabled: bool max_active_tasks: int | None = None daily_token_budget: int | None = None updated_at: datetime class HostTokenUsageSummaryResponse(BaseModel): host_id: str usage_day: str daily_token_budget: int | None = None used_tokens: int reserved_tokens: int remaining_tokens: int | None = None class TokenUsageEventResponse(BaseModel): id: str host_id: str usage_day: str task_id: str | None = None attempt: int | None = None provider: str model: str input_tokens: int | None = None output_tokens: int | None = None total_tokens: int occurred_at: datetime class LlmProviderProfileCreateRequest(BaseModel): name: str = Field(min_length=1, max_length=120) provider_type: Literal["anthropic", "openai-compatible"] model: str = Field(min_length=1, max_length=200) base_url: str | None = Field(default=None, max_length=500) timeout_seconds: float = Field(gt=0, le=120) api_key: SecretStr class LlmProviderProfileUpdateRequest(BaseModel): name: str | None = Field(default=None, min_length=1, max_length=120) provider_type: Literal["anthropic", "openai-compatible"] | None = None model: str | None = Field(default=None, min_length=1, max_length=200) base_url: str | None = Field(default=None, max_length=500) timeout_seconds: float | None = Field(default=None, gt=0, le=120) enabled: bool | None = None api_key: SecretStr | None = None expected_revision: int | None = Field(default=None, ge=1) class LlmProviderProfileActivateRequest(BaseModel): expected_settings_revision: int | None = Field(default=None, ge=0) class LlmProviderProfileResponse(BaseModel): id: str name: str provider_type: Literal["anthropic", "openai-compatible"] model: str base_url: str | None = None timeout_seconds: float enabled: bool revision: int has_api_key: bool key_last_rotated_at: datetime created_at: datetime updated_at: datetime active: bool class LlmProviderSettingsResponse(BaseModel): active_profile_id: str | None = None revision: int updated_at: datetime | None = None class LlmProviderProfileListResponse(BaseModel): settings: LlmProviderSettingsResponse items: list[LlmProviderProfileResponse] class TaskPlannerDecisionItem(BaseModel): step_index: int attempt: int system_prompt: str user_prompt: str tool_name: str arguments: dict[str, Any] = Field(default_factory=dict) rationale: str | None = None thinking: str | None = None purpose: str | None = None expected_outcome: str | None = None created_at: datetime class TaskPlannerDecisionListResponse(BaseModel): items: list[TaskPlannerDecisionItem] # ---------------------------------------------------------------------- # Cloud-managed skills # ---------------------------------------------------------------------- class CloudSkillSummary(BaseModel): id: str name: str kind: Literal["knowledge", "flow_template"] description: str tags: list[str] = Field(default_factory=list) revision: int created_at: datetime updated_at: datetime class CloudSkillCreateRequest(BaseModel): name: str = Field(min_length=1, max_length=200) kind: Literal["knowledge", "flow_template"] description: str = "" tags: list[str] = Field(default_factory=list) content: str = "" steps: list[dict[str, Any]] = Field(default_factory=list) parameters: dict[str, dict[str, Any]] = Field(default_factory=dict) class CloudSkillUpdateRequest(CloudSkillCreateRequest): pass class CloudSkillResponse(CloudSkillSummary): content: str = "" steps: list[dict[str, Any]] = Field(default_factory=list) parameters: dict[str, dict[str, Any]] = Field(default_factory=dict) class CloudSkillListResponse(BaseModel): items: list[CloudSkillResponse] class CloudSkillEntitlementListResponse(BaseModel): skill_id: str host_ids: list[str] = Field(default_factory=list) class CloudSkillSyncResponse(BaseModel): skills: list[CloudSkillResponse] removed_ids: list[str] = Field(default_factory=list) latest_version: int is_full_replace: bool class HostSkillInventoryRequest(BaseModel): """Agent-reported read-only local-skill inventory (metadata only).""" skills: list[dict[str, Any]] = Field(default_factory=list) class HostSkillInventoryResponse(BaseModel): host_id: str payload: list[dict[str, Any]] = Field(default_factory=list) reported_at: datetime | None = None