feat: expand api snapshots and schema tooling

This commit is contained in:
Nathan Flurry 2026-01-26 00:13:17 -08:00
parent ee014b0838
commit 011ca27287
72 changed files with 29480 additions and 1081 deletions

View file

@ -157,6 +157,26 @@
}
}
},
"/v1/sessions": {
"get": {
"tags": [
"sessions"
],
"operationId": "list_sessions",
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SessionListResponse"
}
}
}
}
}
}
},
"/v1/sessions/{session_id}": {
"post": {
"tags": [
@ -1047,6 +1067,65 @@
}
}
},
"SessionInfo": {
"type": "object",
"required": [
"sessionId",
"agent",
"agentMode",
"permissionMode",
"ended",
"eventCount"
],
"properties": {
"agent": {
"type": "string"
},
"agentMode": {
"type": "string"
},
"agentSessionId": {
"type": "string",
"nullable": true
},
"ended": {
"type": "boolean"
},
"eventCount": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"model": {
"type": "string",
"nullable": true
},
"permissionMode": {
"type": "string"
},
"sessionId": {
"type": "string"
},
"variant": {
"type": "string",
"nullable": true
}
}
},
"SessionListResponse": {
"type": "object",
"required": [
"sessions"
],
"properties": {
"sessions": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SessionInfo"
}
}
}
},
"Started": {
"type": "object",
"properties": {

View file

@ -20,8 +20,8 @@
"dist"
],
"scripts": {
"generate:openapi": "cargo check -p sandbox-agent-openapi-gen && cargo run -p sandbox-agent-openapi-gen -- --out src/generated/openapi.json",
"generate:types": "openapi-typescript src/generated/openapi.json -o src/generated/openapi.ts",
"generate:openapi": "cargo check -p sandbox-agent-openapi-gen && cargo run -p sandbox-agent-openapi-gen -- --out ../openapi/openapi.json",
"generate:types": "openapi-typescript ../openapi/openapi.json -o src/generated/openapi.ts",
"generate": "pnpm run generate:openapi && pnpm run generate:types",
"build": "pnpm run generate && tsc -p tsconfig.json"
},

View file

@ -11,14 +11,21 @@ export type AgentInfo = components["schemas"]["AgentInfo"];
export type AgentListResponse = components["schemas"]["AgentListResponse"];
export type CreateSessionRequest = components["schemas"]["CreateSessionRequest"];
export type CreateSessionResponse = components["schemas"]["CreateSessionResponse"];
export type HealthResponse = components["schemas"]["HealthResponse"];
export type MessageRequest = components["schemas"]["MessageRequest"];
export type EventsQuery = components["schemas"]["EventsQuery"];
export type EventsResponse = components["schemas"]["EventsResponse"];
export type PermissionRequest = components["schemas"]["PermissionRequest"];
export type QuestionReplyRequest = components["schemas"]["QuestionReplyRequest"];
export type QuestionRequest = components["schemas"]["QuestionRequest"];
export type PermissionReplyRequest = components["schemas"]["PermissionReplyRequest"];
export type PermissionReply = components["schemas"]["PermissionReply"];
export type ProblemDetails = components["schemas"]["ProblemDetails"];
export type SessionInfo = components["schemas"]["SessionInfo"];
export type SessionListResponse = components["schemas"]["SessionListResponse"];
export type UniversalEvent = components["schemas"]["UniversalEvent"];
export type UniversalMessage = components["schemas"]["UniversalMessage"];
export type UniversalMessagePart = components["schemas"]["UniversalMessagePart"];
const API_PREFIX = "/v1";
@ -58,6 +65,7 @@ type RequestOptions = {
body?: unknown;
headers?: HeadersInit;
accept?: string;
signal?: AbortSignal;
};
export class SandboxDaemonClient {
@ -108,6 +116,10 @@ export class SandboxDaemonClient {
return this.requestJson("GET", `${API_PREFIX}/agents`);
}
async getHealth(): Promise<HealthResponse> {
return this.requestJson("GET", `${API_PREFIX}/health`);
}
async installAgent(agent: string, request: AgentInstallRequest = {}): Promise<void> {
await this.requestJson("POST", `${API_PREFIX}/agents/${encodeURIComponent(agent)}/install`, {
body: request,
@ -124,6 +136,10 @@ export class SandboxDaemonClient {
});
}
async listSessions(): Promise<SessionListResponse> {
return this.requestJson("GET", `${API_PREFIX}/sessions`);
}
async postMessage(sessionId: string, request: MessageRequest): Promise<void> {
await this.requestJson("POST", `${API_PREFIX}/sessions/${encodeURIComponent(sessionId)}/messages`, {
body: request,
@ -136,15 +152,20 @@ export class SandboxDaemonClient {
});
}
async getEventsSse(sessionId: string, query?: EventsQuery): Promise<Response> {
async getEventsSse(sessionId: string, query?: EventsQuery, signal?: AbortSignal): Promise<Response> {
return this.requestRaw("GET", `${API_PREFIX}/sessions/${encodeURIComponent(sessionId)}/events/sse`, {
query,
accept: "text/event-stream",
signal,
});
}
async *streamEvents(sessionId: string, query?: EventsQuery): AsyncGenerator<UniversalEvent, void, void> {
const response = await this.getEventsSse(sessionId, query);
async *streamEvents(
sessionId: string,
query?: EventsQuery,
signal?: AbortSignal,
): AsyncGenerator<UniversalEvent, void, void> {
const response = await this.getEventsSse(sessionId, query, signal);
if (!response.body) {
throw new Error("SSE stream is not readable in this environment.");
}
@ -249,7 +270,7 @@ export class SandboxDaemonClient {
headers.set("Accept", options.accept);
}
const init: RequestInit = { method, headers };
const init: RequestInit = { method, headers, signal: options.signal };
if (options.body !== undefined) {
headers.set("Content-Type", "application/json");
init.body = JSON.stringify(options.body);

View file

@ -14,12 +14,19 @@ export type {
CreateSessionResponse,
EventsQuery,
EventsResponse,
HealthResponse,
MessageRequest,
PermissionRequest,
PermissionReply,
PermissionReplyRequest,
ProblemDetails,
QuestionRequest,
QuestionReplyRequest,
SessionInfo,
SessionListResponse,
UniversalEvent,
UniversalMessage,
UniversalMessagePart,
SandboxDaemonClientOptions,
SandboxDaemonConnectOptions,
} from "./client.js";

View file

@ -50,8 +50,9 @@ export async function spawnSandboxDaemon(
const net = await import("node:net");
const { createRequire } = await import("node:module");
const host = options.host ?? "127.0.0.1";
const port = options.port ?? (await getFreePort(net, host));
const bindHost = options.host ?? "127.0.0.1";
const port = options.port ?? (await getFreePort(net, bindHost));
const connectHost = bindHost === "0.0.0.0" || bindHost === "::" ? "127.0.0.1" : bindHost;
const token = options.token ?? crypto.randomBytes(24).toString("hex");
const timeoutMs = options.timeoutMs ?? 15_000;
const logMode: SandboxDaemonSpawnLogMode = options.log ?? "inherit";
@ -67,7 +68,7 @@ export async function spawnSandboxDaemon(
}
const stdio = logMode === "inherit" ? "inherit" : logMode === "silent" ? "ignore" : "pipe";
const args = ["--host", host, "--port", String(port), "--token", token];
const args = ["--host", bindHost, "--port", String(port), "--token", token];
const child = spawn(binaryPath, args, {
stdio,
env: {
@ -77,8 +78,8 @@ export async function spawnSandboxDaemon(
});
const cleanup = registerProcessCleanup(child);
const baseUrl = `http://${host}:${port}`;
const ready = waitForHealth(baseUrl, fetcher ?? globalThis.fetch, timeoutMs, child);
const baseUrl = `http://${connectHost}:${port}`;
const ready = waitForHealth(baseUrl, fetcher ?? globalThis.fetch, timeoutMs, child, token);
await ready;
@ -161,6 +162,7 @@ async function waitForHealth(
fetcher: typeof fetch | undefined,
timeoutMs: number,
child: ChildProcess,
token: string,
): Promise<void> {
if (!fetcher) {
throw new Error("Fetch API is not available; provide a fetch implementation.");
@ -173,7 +175,9 @@ async function waitForHealth(
throw new Error("sandbox-agent exited before becoming healthy.");
}
try {
const response = await fetcher(`${baseUrl}/v1/health`);
const response = await fetcher(`${baseUrl}/v1/health`, {
headers: { Authorization: `Bearer ${token}` },
});
if (response.ok) {
return;
}