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
TypeScript
/**
* 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