"""Pydantic schemas for profiles, extensions, and sandbox lifecycle.""" from __future__ import annotations from datetime import datetime from enum import StrEnum from typing import Any, Literal from pydantic import BaseModel, Field class ActorType(StrEnum): ADM = "adm" AGT = "agt" ATM = "atm" class SandboxState(StrEnum): REQUESTED = "requested" PROVISIONING = "provisioning" READY = "ready" ACTIVE = "active" EXPIRED = "expired" FAILED = "failed" DESTROYING = "destroying" DESTROYED = "destroyed" class RouteStrategy(StrEnum): PREFER_SELF_HOSTED = "prefer-self-hosted" LOWEST_COST = "lowest-cost" LOWEST_LATENCY = "lowest-latency" EXPLICIT = "explicit" class Consumer(BaseModel): actor: ActorType project: str session_id: str | None = None run_id: str | None = None class IsolationSpec(BaseModel): level: Literal["container", "microvm", "policy", "process"] = "container" class NetworkSpec(BaseModel): default: Literal["deny", "allow"] = "deny" egress: list[str] = Field(default_factory=list) class WorkspaceSpec(BaseModel): mode: Literal["mirror", "remote-canonical"] = "remote-canonical" access: Literal["none", "ro", "rw"] = "rw" class TtlSpec(BaseModel): default: str = "4h" max: str = "24h" idle_reap: str | None = None class ResourceSpec(BaseModel): cpu: str | None = None memory_mb: int | None = None class SetupSpec(BaseModel): instructions: str = "" secret_refs: list[str] = Field(default_factory=list) class PlacementSpec(BaseModel): prefer: list[str] = Field(default_factory=list) fallback: list[str] = Field(default_factory=list) class ReachabilitySpec(BaseModel): tunnel: str = "ops-bridge" identity: str = "ops-warden" class ProfileMetadata(BaseModel): cost_class: Literal["self-hosted", "saas-metered"] = "self-hosted" latency_class: str = "standard" observability: Literal["none", "canary"] = "none" class RouteSpec(BaseModel): strategy: RouteStrategy = RouteStrategy.EXPLICIT extensions: list[str] = Field(default_factory=list) max_cost_per_hour_usd: float | None = None class MeterQuote(BaseModel): extension_id: str estimated_usd: float unit: Literal["per_hour", "per_session"] = "per_hour" duration_s: int = 3600 class MeterRecord(BaseModel): pricing_model: Literal["self-hosted", "metered"] = "self-hosted" estimate_usd: float | None = None actual_usd: float | None = None duration_s: float | None = None currency: str = "USD" class Profile(BaseModel): id: str version: str extension: str route: RouteSpec | None = None isolation: IsolationSpec = Field(default_factory=IsolationSpec) network: NetworkSpec = Field(default_factory=NetworkSpec) workspace: WorkspaceSpec = Field(default_factory=WorkspaceSpec) scope_default: Literal["session", "agent", "shared"] = "session" ttl: TtlSpec = Field(default_factory=TtlSpec) resources: ResourceSpec = Field(default_factory=ResourceSpec) setup: SetupSpec = Field(default_factory=SetupSpec) placement: PlacementSpec = Field(default_factory=PlacementSpec) reachability: ReachabilitySpec = Field(default_factory=ReachabilitySpec) metadata: ProfileMetadata = Field(default_factory=ProfileMetadata) class ExtensionCapabilities(BaseModel): isolation_levels: list[str] = Field(default_factory=lambda: ["container"]) regions: list[str] = Field(default_factory=list) persistence: bool = False pricing_model: Literal["self-hosted", "metered"] = "self-hosted" class Extension(BaseModel): id: str title: str description: str = "" handler: str capabilities: ExtensionCapabilities = Field(default_factory=ExtensionCapabilities) config: dict[str, Any] = Field(default_factory=dict) class SandboxCreateRequest(BaseModel): profile: str inputs: dict[str, str] = Field(default_factory=dict) consumer: Consumer ttl: str | None = None class Reachability(BaseModel): ssh: str | None = None remote_dir: str | None = None compose_project: str | None = None host: str | None = None endpoint: str | None = None tunnel: str | None = None tunnel_via: str | None = None identity: str | None = None # Local (no-SSH-hop) descriptor — populated for same-host extensions # like ext.bwrap. A consumer execs into the sandbox directly # (e.g. `nsenter --target ...`) rather than over SSH. pid: str | None = None workspace_dir: str | None = None class SandboxStatus(BaseModel): sandbox_id: str profile_id: str extension_id: str state: SandboxState consumer: Consumer host: str | None = None reachability: Reachability | None = None inputs: dict[str, str] = Field(default_factory=dict) ttl: str | None = None expires_at: datetime | None = None error: str | None = None meter: MeterRecord | None = None telemetry: dict | None = None # IntrospectionReport JSON when canary created_at: datetime updated_at: datetime ready_at: datetime | None = None destroyed_at: datetime | None = None class ExtendTtlRequest(BaseModel): duration: str class ExpireActionResult(BaseModel): sandbox_id: str reason: Literal["ttl", "idle"] action: Literal["dry-run", "expired", "destroyed", "failed"] error: str | None = None class SnapshotRestoreRequest(BaseModel): host: str | None = None consumer: Consumer | None = None class SnapshotRecord(BaseModel): snapshot_id: str sandbox_id: str profile_id: str extension_id: str host: str artifact_path: str = "" handle: dict[str, str] = Field(default_factory=dict) inputs: dict[str, str] = Field(default_factory=dict) consumer: Consumer | None = None name: str | None = None size_bytes: int | None = None created_at: datetime