From 664d563827af637ffe48a4e83d963910ff136f8f Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Fri, 6 Mar 2026 00:22:19 -0800 Subject: [PATCH] fix: add OpenAPI annotations for process endpoints and fix config persistence race Add summary/description to all process management endpoint specs and the not_found error type. Fix hydrateSessionConfigOptions to re-read from persistence after the network call, and sync mode-category configOptions on session/update current_mode_update events. Co-Authored-By: Claude Opus 4.6 --- docs/openapi.json | 27 +++++ sdks/typescript/src/client.ts | 39 +++++-- sdks/typescript/src/generated/openapi.ts | 140 ++++++++++++++++++++++- 3 files changed, 194 insertions(+), 12 deletions(-) diff --git a/docs/openapi.json b/docs/openapi.json index d600fda..d6272b7 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -954,6 +954,8 @@ "tags": [ "v1" ], + "summary": "List all managed processes.", + "description": "Returns a list of all processes (running and exited) currently tracked\nby the runtime, sorted by process ID.", "operationId": "get_v1_processes", "responses": { "200": { @@ -982,6 +984,8 @@ "tags": [ "v1" ], + "summary": "Create a long-lived managed process.", + "description": "Spawns a new process with the given command and arguments. Supports both\npipe-based and PTY (tty) modes. Returns the process descriptor on success.", "operationId": "post_v1_processes", "requestBody": { "content": { @@ -1042,6 +1046,8 @@ "tags": [ "v1" ], + "summary": "Get process runtime configuration.", + "description": "Returns the current runtime configuration for the process management API,\nincluding limits for concurrency, timeouts, and buffer sizes.", "operationId": "get_v1_processes_config", "responses": { "200": { @@ -1070,6 +1076,8 @@ "tags": [ "v1" ], + "summary": "Update process runtime configuration.", + "description": "Replaces the runtime configuration for the process management API.\nValidates that all values are non-zero and clamps default timeout to max.", "operationId": "post_v1_processes_config", "requestBody": { "content": { @@ -1120,6 +1128,8 @@ "tags": [ "v1" ], + "summary": "Run a one-shot command.", + "description": "Executes a command to completion and returns its stdout, stderr, exit code,\nand duration. Supports configurable timeout and output size limits.", "operationId": "post_v1_processes_run", "requestBody": { "content": { @@ -1170,6 +1180,8 @@ "tags": [ "v1" ], + "summary": "Get a single process by ID.", + "description": "Returns the current state of a managed process including its status,\nPID, exit code, and creation/exit timestamps.", "operationId": "get_v1_process", "parameters": [ { @@ -1219,6 +1231,8 @@ "tags": [ "v1" ], + "summary": "Delete a process record.", + "description": "Removes a stopped process from the runtime. Returns 409 if the process\nis still running; stop or kill it first.", "operationId": "delete_v1_process", "parameters": [ { @@ -1273,6 +1287,8 @@ "tags": [ "v1" ], + "summary": "Write input to a process.", + "description": "Sends data to a process's stdin (pipe mode) or PTY writer (tty mode).\nData can be encoded as base64, utf8, or text. Returns 413 if the decoded\npayload exceeds the configured `maxInputBytesPerRequest` limit.", "operationId": "post_v1_process_input", "parameters": [ { @@ -1354,6 +1370,8 @@ "tags": [ "v1" ], + "summary": "Send SIGKILL to a process.", + "description": "Sends SIGKILL to the process and optionally waits up to `waitMs`\nmilliseconds for the process to exit before returning.", "operationId": "post_v1_process_kill", "parameters": [ { @@ -1417,6 +1435,8 @@ "tags": [ "v1" ], + "summary": "Fetch process logs.", + "description": "Returns buffered log entries for a process. Supports filtering by stream\ntype, tail count, and sequence-based resumption. When `follow=true`,\nreturns an SSE stream that replays buffered entries then streams live output.", "operationId": "get_v1_process_logs", "parameters": [ { @@ -1515,6 +1535,8 @@ "tags": [ "v1" ], + "summary": "Send SIGTERM to a process.", + "description": "Sends SIGTERM to the process and optionally waits up to `waitMs`\nmilliseconds for the process to exit before returning.", "operationId": "post_v1_process_stop", "parameters": [ { @@ -1578,6 +1600,8 @@ "tags": [ "v1" ], + "summary": "Resize a process terminal.", + "description": "Sets the PTY window size (columns and rows) for a tty-mode process and\nsends SIGWINCH so the child process can adapt.", "operationId": "post_v1_process_terminal_resize", "parameters": [ { @@ -1659,6 +1683,8 @@ "tags": [ "v1" ], + "summary": "Open an interactive WebSocket terminal session.", + "description": "Upgrades the connection to a WebSocket for bidirectional PTY I/O. Accepts\n`access_token` query param for browser-based auth (WebSocket API cannot\nsend custom headers). Streams raw PTY output as binary frames and accepts\nJSON control frames for input, resize, and close.", "operationId": "get_v1_process_terminal_ws", "parameters": [ { @@ -2013,6 +2039,7 @@ "permission_denied", "not_acceptable", "unsupported_media_type", + "not_found", "session_not_found", "session_already_exists", "mode_not_supported", diff --git a/sdks/typescript/src/client.ts b/sdks/typescript/src/client.ts index 52dfb33..cd849c9 100644 --- a/sdks/typescript/src/client.ts +++ b/sdks/typescript/src/client.ts @@ -897,7 +897,7 @@ export class SandboxAgent { async getSessionConfigOptions(sessionId: string): Promise { const record = await this.requireSessionRecord(sessionId); - const hydrated = await this.hydrateSessionConfigOptions(record); + const hydrated = await this.hydrateSessionConfigOptions(record.id, record); return cloneConfigOptions(hydrated.configOptions) ?? []; } @@ -937,13 +937,19 @@ export class SandboxAgent { return this.setSessionConfigOption(sessionId, option.id, resolvedValue); } - private async hydrateSessionConfigOptions(record: SessionRecord): Promise { - if (record.configOptions !== undefined) { - return record; + private async hydrateSessionConfigOptions(sessionId: string, snapshot: SessionRecord): Promise { + if (snapshot.configOptions !== undefined) { + return snapshot; } - const info = await this.getAgent(record.agent, { config: true }); + const info = await this.getAgent(snapshot.agent, { config: true }); const configOptions = normalizeSessionConfigOptions(info.configOptions) ?? []; + // Re-read the record from persistence so we merge against the latest + // state, not a stale snapshot captured before the network await. + const record = await this.persist.getSession(sessionId); + if (!record) { + return { ...snapshot, configOptions }; + } const updated: SessionRecord = { ...record, configOptions, @@ -1048,14 +1054,25 @@ export class SandboxAgent { if (!modeId) { return; } + const updates: Partial = {}; const nextModes = applyCurrentMode(record.modes, modeId); - if (!nextModes) { - return; + if (nextModes) { + updates.modes = nextModes; + } + // Keep configOptions mode-category currentValue in sync with the new + // mode, mirroring the reverse sync in the set_config_option path above. + if (record.configOptions) { + const modeOption = findConfigOptionByCategory(record.configOptions, "mode"); + if (modeOption) { + const updated = applyConfigOptionValue(record.configOptions, modeOption.id, modeId); + if (updated) { + updates.configOptions = updated; + } + } + } + if (Object.keys(updates).length > 0) { + await this.persist.updateSession({ ...record, ...updates }); } - await this.persist.updateSession({ - ...record, - modes: nextModes, - }); } } diff --git a/sdks/typescript/src/generated/openapi.ts b/sdks/typescript/src/generated/openapi.ts index a89d796..18374fb 100644 --- a/sdks/typescript/src/generated/openapi.ts +++ b/sdks/typescript/src/generated/openapi.ts @@ -58,36 +58,105 @@ export interface paths { get: operations["get_v1_health"]; }; "/v1/processes": { + /** + * List all managed processes. + * @description Returns a list of all processes (running and exited) currently tracked + * by the runtime, sorted by process ID. + */ get: operations["get_v1_processes"]; + /** + * Create a long-lived managed process. + * @description Spawns a new process with the given command and arguments. Supports both + * pipe-based and PTY (tty) modes. Returns the process descriptor on success. + */ post: operations["post_v1_processes"]; }; "/v1/processes/config": { + /** + * Get process runtime configuration. + * @description Returns the current runtime configuration for the process management API, + * including limits for concurrency, timeouts, and buffer sizes. + */ get: operations["get_v1_processes_config"]; + /** + * Update process runtime configuration. + * @description Replaces the runtime configuration for the process management API. + * Validates that all values are non-zero and clamps default timeout to max. + */ post: operations["post_v1_processes_config"]; }; "/v1/processes/run": { + /** + * Run a one-shot command. + * @description Executes a command to completion and returns its stdout, stderr, exit code, + * and duration. Supports configurable timeout and output size limits. + */ post: operations["post_v1_processes_run"]; }; "/v1/processes/{id}": { + /** + * Get a single process by ID. + * @description Returns the current state of a managed process including its status, + * PID, exit code, and creation/exit timestamps. + */ get: operations["get_v1_process"]; + /** + * Delete a process record. + * @description Removes a stopped process from the runtime. Returns 409 if the process + * is still running; stop or kill it first. + */ delete: operations["delete_v1_process"]; }; "/v1/processes/{id}/input": { + /** + * Write input to a process. + * @description Sends data to a process's stdin (pipe mode) or PTY writer (tty mode). + * Data can be encoded as base64, utf8, or text. Returns 413 if the decoded + * payload exceeds the configured `maxInputBytesPerRequest` limit. + */ post: operations["post_v1_process_input"]; }; "/v1/processes/{id}/kill": { + /** + * Send SIGKILL to a process. + * @description Sends SIGKILL to the process and optionally waits up to `waitMs` + * milliseconds for the process to exit before returning. + */ post: operations["post_v1_process_kill"]; }; "/v1/processes/{id}/logs": { + /** + * Fetch process logs. + * @description Returns buffered log entries for a process. Supports filtering by stream + * type, tail count, and sequence-based resumption. When `follow=true`, + * returns an SSE stream that replays buffered entries then streams live output. + */ get: operations["get_v1_process_logs"]; }; "/v1/processes/{id}/stop": { + /** + * Send SIGTERM to a process. + * @description Sends SIGTERM to the process and optionally waits up to `waitMs` + * milliseconds for the process to exit before returning. + */ post: operations["post_v1_process_stop"]; }; "/v1/processes/{id}/terminal/resize": { + /** + * Resize a process terminal. + * @description Sets the PTY window size (columns and rows) for a tty-mode process and + * sends SIGWINCH so the child process can adapt. + */ post: operations["post_v1_process_terminal_resize"]; }; "/v1/processes/{id}/terminal/ws": { + /** + * Open an interactive WebSocket terminal session. + * @description Upgrades the connection to a WebSocket for bidirectional PTY I/O. Accepts + * `access_token` query param for browser-based auth (WebSocket API cannot + * send custom headers). Streams raw PTY output as binary frames and accepts + * JSON control frames for input, resize, and close. + */ get: operations["get_v1_process_terminal_ws"]; }; } @@ -166,7 +235,7 @@ export interface components { agents: components["schemas"]["AgentInfo"][]; }; /** @enum {string} */ - ErrorType: "invalid_request" | "conflict" | "unsupported_agent" | "agent_not_installed" | "install_failed" | "agent_process_exited" | "token_invalid" | "permission_denied" | "not_acceptable" | "unsupported_media_type" | "session_not_found" | "session_already_exists" | "mode_not_supported" | "stream_error" | "timeout"; + ErrorType: "invalid_request" | "conflict" | "unsupported_agent" | "agent_not_installed" | "install_failed" | "agent_process_exited" | "token_invalid" | "permission_denied" | "not_acceptable" | "unsupported_media_type" | "not_found" | "session_not_found" | "session_already_exists" | "mode_not_supported" | "stream_error" | "timeout"; FsActionResponse: { path: string; }; @@ -891,6 +960,11 @@ export interface operations { }; }; }; + /** + * List all managed processes. + * @description Returns a list of all processes (running and exited) currently tracked + * by the runtime, sorted by process ID. + */ get_v1_processes: { responses: { /** @description List processes */ @@ -907,6 +981,11 @@ export interface operations { }; }; }; + /** + * Create a long-lived managed process. + * @description Spawns a new process with the given command and arguments. Supports both + * pipe-based and PTY (tty) modes. Returns the process descriptor on success. + */ post_v1_processes: { requestBody: { content: { @@ -940,6 +1019,11 @@ export interface operations { }; }; }; + /** + * Get process runtime configuration. + * @description Returns the current runtime configuration for the process management API, + * including limits for concurrency, timeouts, and buffer sizes. + */ get_v1_processes_config: { responses: { /** @description Current runtime process config */ @@ -956,6 +1040,11 @@ export interface operations { }; }; }; + /** + * Update process runtime configuration. + * @description Replaces the runtime configuration for the process management API. + * Validates that all values are non-zero and clamps default timeout to max. + */ post_v1_processes_config: { requestBody: { content: { @@ -983,6 +1072,11 @@ export interface operations { }; }; }; + /** + * Run a one-shot command. + * @description Executes a command to completion and returns its stdout, stderr, exit code, + * and duration. Supports configurable timeout and output size limits. + */ post_v1_processes_run: { requestBody: { content: { @@ -1010,6 +1104,11 @@ export interface operations { }; }; }; + /** + * Get a single process by ID. + * @description Returns the current state of a managed process including its status, + * PID, exit code, and creation/exit timestamps. + */ get_v1_process: { parameters: { path: { @@ -1038,6 +1137,11 @@ export interface operations { }; }; }; + /** + * Delete a process record. + * @description Removes a stopped process from the runtime. Returns 409 if the process + * is still running; stop or kill it first. + */ delete_v1_process: { parameters: { path: { @@ -1070,6 +1174,12 @@ export interface operations { }; }; }; + /** + * Write input to a process. + * @description Sends data to a process's stdin (pipe mode) or PTY writer (tty mode). + * Data can be encoded as base64, utf8, or text. Returns 413 if the decoded + * payload exceeds the configured `maxInputBytesPerRequest` limit. + */ post_v1_process_input: { parameters: { path: { @@ -1115,6 +1225,11 @@ export interface operations { }; }; }; + /** + * Send SIGKILL to a process. + * @description Sends SIGKILL to the process and optionally waits up to `waitMs` + * milliseconds for the process to exit before returning. + */ post_v1_process_kill: { parameters: { query?: { @@ -1147,6 +1262,12 @@ export interface operations { }; }; }; + /** + * Fetch process logs. + * @description Returns buffered log entries for a process. Supports filtering by stream + * type, tail count, and sequence-based resumption. When `follow=true`, + * returns an SSE stream that replays buffered entries then streams live output. + */ get_v1_process_logs: { parameters: { query?: { @@ -1185,6 +1306,11 @@ export interface operations { }; }; }; + /** + * Send SIGTERM to a process. + * @description Sends SIGTERM to the process and optionally waits up to `waitMs` + * milliseconds for the process to exit before returning. + */ post_v1_process_stop: { parameters: { query?: { @@ -1217,6 +1343,11 @@ export interface operations { }; }; }; + /** + * Resize a process terminal. + * @description Sets the PTY window size (columns and rows) for a tty-mode process and + * sends SIGWINCH so the child process can adapt. + */ post_v1_process_terminal_resize: { parameters: { path: { @@ -1262,6 +1393,13 @@ export interface operations { }; }; }; + /** + * Open an interactive WebSocket terminal session. + * @description Upgrades the connection to a WebSocket for bidirectional PTY I/O. Accepts + * `access_token` query param for browser-based auth (WebSocket API cannot + * send custom headers). Streams raw PTY output as binary frames and accepts + * JSON control frames for input, resize, and close. + */ get_v1_process_terminal_ws: { parameters: { query?: {