UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

463 lines 17.4 kB
/** * Sandbox Registry * * Manages registered agentic-sandbox instances. Each sandbox registers * with `aiwg serve` via POST /api/sandboxes/register and then pushes * real-time events over WebSocket at /ws/sandbox/:sandboxId. * * This is the AIWG side of the bidirectional integration: * - aiwg#731 = registration API (this file) * - sandbox#132 = outbound registration (pushes events here) * * @issue #731 * @see #710 — epic * @see #732 — HITL relay (consumes hitl.input_required events) * @see #733 — operator controls (proxies to sandbox HTTP) */ export interface SandboxRegistration { /** Unique sandbox ID (assigned on register, session-scoped) */ id: string; /** Display name chosen by sandbox operator */ name: string; /** * Stable instance identity (UUIDv7) persisted by the sandbox across restarts. * Used for upsert-on-reconnect so the same physical sandbox never creates * duplicate entries regardless of how many times it re-registers. */ instanceId?: string; /** gRPC endpoint for agent communication */ grpcEndpoint: string; /** WebSocket endpoint for PTY streaming */ wsEndpoint: string; /** HTTP REST API endpoint */ httpEndpoint: string; /** Capabilities this sandbox advertises */ capabilities: string[]; /** Sandbox software version */ version: string; /** Auth token for this sandbox (returned at registration) */ token: string; /** When this sandbox_id was first assigned */ registeredAt: string; /** When the most recent registration arrived (updated on every upsert) */ lastRegisteredAt: string; /** Last event received from the sandbox */ lastEventAt: string; /** Whether the event push WebSocket is connected */ connected: boolean; /** When the event push WebSocket last disconnected (undefined if never disconnected) */ disconnectedAt?: string; /** Live agent inventory (updated by sandbox events) */ agents: Map<string, SandboxAgent>; /** Sandbox-level artifact inventory reported at registration time (#906) */ sandboxInventory?: AgentInventory; /** WebSocket protocol capabilities advertised at registration time (#912) */ wsCapabilities?: SandboxCapabilities; } export interface SandboxAgent { agentId: string; status: 'starting' | 'provisioning' | 'ready' | 'busy' | 'error' | 'disconnected'; loadout?: string; /** Framework list — name, providers, and optional version/content_hash (#910) */ aiwgFrameworks?: Array<{ name: string; providers: string[]; version?: string; content_hash?: string; }>; connectedAt?: string; lastHeartbeat?: string; /** Live session count — incremented/decremented by session.start/session.end events. * Approximate; `sessions` is authoritative when present (drift-resistant). */ sessionCount?: number; /** * Authoritative session inventory pushed by the sandbox via the * `agent.sessions` event (#1151, sandbox#192). Replaces wholesale on each * event so a missed start/end can't desync the count. * * Undefined when the sandbox is on a build that doesn't emit * `agent.sessions` yet — UI should fall back to "no badge" rather than * rendering "0 sessions" for unknown state. */ sessions?: SessionInfo[]; /** Agent/command/skill manifest inventory — populated by agent.inventory_updated events (#906) */ inventory?: AgentInventory; /** Latest metrics snapshot — populated by agent.metrics_updated events (#911) */ latestMetrics?: AgentMetrics; /** Rolling metrics history (last METRICS_HISTORY_MAX samples) — for sparklines (#911) */ metricsHistory?: AgentMetricsSample[]; /** Current provisioning step — populated by agent.provisioning_step events (#911) */ provisioningStep?: ProvisioningStep; /** True if provisioning has stalled (no progress for 30s+) (#911) */ provisioningStalled?: boolean; /** * Stable agent instance identity — UUIDv7 generated on first start, persisted by the sandbox. * Survives restarts, reprovisions, and agentId changes (#917). */ instanceId?: string; /** * Human-readable stable name assigned by the operator (e.g. "security-01"). * Persisted in ~/.config/aiwg/sandbox-agents.json (#917). */ logicalName?: string; } /** * One live session record on an agent. Pushed by the sandbox in the * `agent.sessions` event (sandbox#192) as a full inventory replace. * * Field shape mirrors the dashboard's WS `session_list` reply so the UI * can render the same data on either surface. */ export interface SessionInfo { session_id: string; session_name: string; session_type: 'interactive' | 'headless' | 'background'; command: string; /** Unix epoch seconds — kept as raw seconds so the consumer can format * it in the local timezone. */ created_at_secs: number; /** True when the sandbox has a screen-state snapshot for this session * available via `GET /api/v1/sessions/:id/screen` (#913). */ has_screen: boolean; } /** * Protocol capabilities advertised by the sandbox on registration. * Enables AIWG to select the correct WS message path for each sandbox. */ export interface SandboxCapabilities { /** Numeric protocol version (e.g. 2) */ ws_protocol_version: number; /** Client message types this sandbox accepts */ supported_client_messages: string[]; /** Server message types this sandbox emits */ supported_server_messages: string[]; /** Named feature flags (e.g. "replay_buffer", "role_control", "seq_tracking") */ features: string[]; } /** One record per known agent instance in the persistent store. */ interface AgentIdentityRecord { instanceId: string; logicalName?: string; /** Last known agentId — may change across reprovisions */ lastAgentId?: string; /** Last known sandboxId */ lastSandboxId?: string; lastSeenAt: string; } /** * Resolve the identity-store path, honoring `roots.sandbox_identity` * in `.aiwg/storage.config` when set. Sync read because the sandbox * registry constructor is sync and load happens at construction time. * * Backend support: only `fs` (or absent config) is currently supported * for the identity store — non-fs backends would require an async * adapter call which doesn't fit the sync constructor. Throws a clear * error if the user has configured a non-fs backend for this subsystem. * * Exported for testing — production callers omit `projectRootOverride` * to use `process.cwd()`. * * @issue #969 */ export declare function resolveIdentityStorePath(projectRootOverride?: string): string; /** Convenience helper — returns true if the sandbox advertises a feature flag. */ export declare function sandboxHasFeature(reg: { wsCapabilities?: SandboxCapabilities; }, feature: string): boolean; /** Returns true if the sandbox supports a given client message type. */ export declare function sandboxSupports(reg: { wsCapabilities?: SandboxCapabilities; }, msg: string): boolean; export interface AgentMetrics { cpu_percent: number; memory_used_bytes: number; memory_total_bytes: number; uptime_seconds: number; load_avg_1m?: number; disk_used_bytes?: number; disk_total_bytes?: number; /** Unix timestamp (ms) when metrics were sampled */ ts: number; } /** Compact sample stored in the rolling history ring. */ export interface AgentMetricsSample { cpu_percent: number; memory_percent: number; ts: number; } /** Maximum number of samples kept per agent (#911). ~5 min at 5s interval. */ export declare const METRICS_HISTORY_MAX = 60; export interface ProvisioningStep { step: string; step_index?: number; total_steps?: number; elapsed_seconds?: number; ts: string; } export interface AgentManifestSummary { name: string; description: string; model?: string; category: string; platform: string; /** SHA-256 of agent manifest file — for change detection */ content_hash: string; } export interface CommandManifestSummary { name: string; description: string; platform: string; content_hash: string; } export interface SkillManifestSummary { name: string; description: string; platform: string; content_hash: string; } export interface AgentInventory { agents: AgentManifestSummary[]; commands: CommandManifestSummary[]; skills: SkillManifestSummary[]; last_updated: string; } /** * Events pushed from agentic-sandbox to aiwg serve over WebSocket. * Matches the protocol defined in aiwg#731 / sandbox#132. */ export type SandboxEventType = 'agent.connected' | 'agent.disconnected' | 'agent.provisioning' | 'agent.ready' | 'session.start' | 'session.end' | 'hitl.input_required' | 'hitl.responded' | 'hitl.timed_out' | 'agent.inventory_updated' | 'task.submitted' | 'task.started' | 'task.progressed' | 'task.completed' | 'task.failed' | 'agent.metrics_updated' | 'agent.provisioning_step' | 'agent.provisioning_stalled' | 'framework.update_available' | 'session.screen_updated' | 'agent.sessions' | 'aiwg.log'; export interface SandboxEvent { type: SandboxEventType; sandboxId: string; agentId: string; timestamp: string; loadout?: string; aiwgFrameworks?: Array<{ name: string; providers: string[]; }>; step?: string; progress?: unknown; sessionId?: string; /** PTY/exec command — present on session.start events */ command?: string; /** Exit code — present on session.end events */ exitCode?: number; task?: string; hitlId?: string; prompt?: string; context?: string; expiresAt?: string; agentInventory?: AgentManifestSummary[]; commandInventory?: CommandManifestSummary[]; skillInventory?: SkillManifestSummary[]; taskId?: string; outputChunk?: string; taskError?: string; metrics?: AgentMetrics; stepIndex?: number; totalSteps?: number; elapsedSeconds?: number; stalledForSeconds?: number; framework?: string; currentVersion?: string; availableVersion?: string; daysBehind?: number; screenHash?: string; changedLines?: number[]; level?: string; message?: string; /** Stable agent instance UUIDv7 — set on agent.connected events */ agentInstanceId?: string; /** Operator-assigned logical name — set on agent.connected events */ agentLogicalName?: string; /** Full session inventory — set on agent.sessions events. The sandbox * replaces the agent's session list wholesale on each event so missed * session.start / session.end deltas can't desync the count. */ sessions?: SessionInfo[]; } export interface HitlRequest { id: string; sandboxId: string; agentId: string; sessionId: string; timestamp: string; prompt: string; context: string; expiresAt?: string; } export interface RegisterRequest { name: string; /** Stable UUIDv7 generated on first start, persisted across restarts */ instance_id?: string; grpc_endpoint: string; ws_endpoint: string; http_endpoint: string; capabilities?: string[]; version?: string; /** Agent manifest summaries for all deployed agents (#906) */ agent_inventory?: AgentManifestSummary[]; /** Command manifest summaries for all deployed commands (#906) */ command_inventory?: CommandManifestSummary[]; /** Skill manifest summaries for all deployed skills (#906) */ skill_inventory?: SkillManifestSummary[]; /** WebSocket protocol capabilities — enables negotiation on connect (#912) */ ws_capabilities?: SandboxCapabilities; } export interface RegisterResponse { sandbox_id: string; token: string; } /** * Normalize a raw sandbox event payload into the camelCase + dot-notation * shape `handleEvent` expects. Accepts payloads in either the legacy * snake_case or the newer dot/camelCase shape and is idempotent on the * latter, so it is safe to pipe every inbound event through this helper. */ export declare function normalizeSandboxEvent(raw: unknown): SandboxEvent; export declare class SandboxRegistry { private sandboxes; private hitlRequests; private listeners; /** instance_id → sandbox_id (stable reverse-lookup for sandbox upsert) */ private byInstanceId; /** instance_id → last registration timestamp (ms, for debounce) */ private lastRegistrationTime; /** Timer for HITL expiry checks (#908) */ private expiryTimer; /** agentInstanceId → { sandboxId, agentId } — cross-restart lookup (#917) */ private byAgentInstanceId; /** logicalName → { sandboxId, agentId } — human-readable alias lookup (#917) */ private byLogicalName; /** Persistent store: agentInstanceId → identity record (#917) */ private identityStore; constructor(); private checkHitlExpiry; /** * Register a sandbox instance. * * When the request includes a stable `instance_id`: * - **Debounce**: if a registration for the same instance_id arrived within * DEBOUNCE_MS, return the existing sandbox_id + token without touching state. * - **Upsert**: if outside the debounce window, update the existing entry's * endpoints, version, and lastRegisteredAt in-place. The sandbox_id and token * are preserved so in-flight WS connections stay authenticated. * * When no instance_id is provided, a new entry is always created (legacy behaviour). */ register(req: RegisterRequest): RegisterResponse; /** * Remove all disconnected sandboxes from the registry. * Forces re-registration on next sandbox startup. * Returns the number of entries removed. */ clearOffline(): number; /** * Deregister a sandbox (on shutdown or explicit delete). */ deregister(id: string): boolean; /** * Get a sandbox by ID. */ get(id: string): SandboxRegistration | undefined; /** * Validate the auth token for a sandbox. */ authenticate(id: string, token: string): boolean; /** * Mark the event push WebSocket as connected/disconnected. */ setConnected(id: string, connected: boolean): void; /** * List all registered sandboxes (serializable). */ list(): SandboxSummary[]; /** * Get a sandbox summary by ID (serializable). */ getSummary(id: string): SandboxSummary | undefined; /** * Process an event pushed from a sandbox. * Updates internal agent inventory and notifies listeners. */ handleEvent(event: SandboxEvent): void; /** * Emit a synthetic event to all listeners without modifying internal state. * Used by serve.ts to push server-side events (e.g. hitl.responded) to the browser. */ emit(event: SandboxEvent): void; /** * Get all agents across all sandboxes. */ allAgents(): Array<SandboxAgent & { sandboxId: string; sandboxName: string; }>; /** * Resolve an agent reference by agentId → instanceId → logicalName (#917). * Returns the sandbox registration and agent, or undefined if not found. */ resolveAgent(ref: string): { sandbox: SandboxRegistration; agent: SandboxAgent; } | undefined; /** * Assign a stable logical name to an agent (#917). * Persists the mapping to ~/.config/aiwg/sandbox-agents.json. */ aliasAgent(sandboxId: string, agentId: string, logicalName: string): boolean; /** * List known agent identities from the persistent store (#917). */ knownAgentIdentities(): AgentIdentityRecord[]; /** * List pending HITL requests. */ pendingHitl(): HitlRequest[]; /** * Get a specific HITL request. */ getHitl(hitlId: string): HitlRequest | undefined; /** * Remove a HITL request (after response or dismissal). */ resolveHitl(hitlId: string): HitlRequest | undefined; /** * Subscribe to sandbox events (returns unsubscribe fn). */ subscribe(listener: (event: SandboxEvent) => void): () => void; /** * Total registered sandbox count. */ get size(): number; /** * Shut down — clear all state and stop background timers. */ shutdown(): void; } export interface SandboxSummary { id: string; /** Stable instance identity — canonical UI identifier, prefix for display */ instanceId?: string; name: string; grpcEndpoint: string; wsEndpoint: string; httpEndpoint: string; capabilities: string[]; version: string; registeredAt: string; lastRegisteredAt: string; lastEventAt: string; connected: boolean; /** ISO timestamp of last disconnect — present when connected is false */ disconnectedAt?: string; agentCount: number; agents: Array<SandboxAgent>; /** Sandbox-level artifact inventory reported at registration time (#906) */ sandboxInventory?: AgentInventory; /** WebSocket protocol capabilities advertised at registration (#912) */ wsCapabilities?: SandboxCapabilities; } export declare const sandboxRegistry: SandboxRegistry; export {}; //# sourceMappingURL=sandbox-registry.d.ts.map