mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-16 13:04:11 +00:00
chore: recover wellington workspace state
This commit is contained in:
parent
5d65013aa5
commit
c294ca85be
366 changed files with 1265 additions and 53395 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "sandbox-agent",
|
||||
"version": "0.3.0",
|
||||
"version": "0.2.1",
|
||||
"description": "Universal API for automatic coding agents in sandboxes. Supports Claude Code, Codex, OpenCode, and Amp.",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
|
|
@ -17,8 +17,8 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@sandbox-agent/cli-shared": "workspace:*",
|
||||
"acp-http-client": "workspace:*"
|
||||
"acp-http-client": "workspace:*",
|
||||
"@sandbox-agent/cli-shared": "workspace:*"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
|
@ -34,12 +34,10 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"openapi-typescript": "^6.7.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vitest": "^3.0.0",
|
||||
"ws": "^8.19.0"
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@sandbox-agent/cli": "workspace:*"
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -57,108 +57,6 @@ export interface paths {
|
|||
"/v1/health": {
|
||||
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"];
|
||||
};
|
||||
}
|
||||
|
||||
export type webhooks = Record<string, never>;
|
||||
|
|
@ -235,7 +133,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" | "not_found" | "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" | "session_not_found" | "session_already_exists" | "mode_not_supported" | "stream_error" | "timeout";
|
||||
FsActionResponse: {
|
||||
path: string;
|
||||
};
|
||||
|
|
@ -332,116 +230,6 @@ export interface components {
|
|||
type: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
ProcessConfig: {
|
||||
/** Format: int64 */
|
||||
defaultRunTimeoutMs: number;
|
||||
maxConcurrentProcesses: number;
|
||||
maxInputBytesPerRequest: number;
|
||||
maxLogBytesPerProcess: number;
|
||||
maxOutputBytes: number;
|
||||
/** Format: int64 */
|
||||
maxRunTimeoutMs: number;
|
||||
};
|
||||
ProcessCreateRequest: {
|
||||
args?: string[];
|
||||
command: string;
|
||||
cwd?: string | null;
|
||||
env?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
interactive?: boolean;
|
||||
tty?: boolean;
|
||||
};
|
||||
ProcessInfo: {
|
||||
args: string[];
|
||||
command: string;
|
||||
/** Format: int64 */
|
||||
createdAtMs: number;
|
||||
cwd?: string | null;
|
||||
/** Format: int32 */
|
||||
exitCode?: number | null;
|
||||
/** Format: int64 */
|
||||
exitedAtMs?: number | null;
|
||||
id: string;
|
||||
interactive: boolean;
|
||||
/** Format: int32 */
|
||||
pid?: number | null;
|
||||
status: components["schemas"]["ProcessState"];
|
||||
tty: boolean;
|
||||
};
|
||||
ProcessInputRequest: {
|
||||
data: string;
|
||||
encoding?: string | null;
|
||||
};
|
||||
ProcessInputResponse: {
|
||||
bytesWritten: number;
|
||||
};
|
||||
ProcessListResponse: {
|
||||
processes: components["schemas"]["ProcessInfo"][];
|
||||
};
|
||||
ProcessLogEntry: {
|
||||
data: string;
|
||||
encoding: string;
|
||||
/** Format: int64 */
|
||||
sequence: number;
|
||||
stream: components["schemas"]["ProcessLogsStream"];
|
||||
/** Format: int64 */
|
||||
timestampMs: number;
|
||||
};
|
||||
ProcessLogsQuery: {
|
||||
follow?: boolean | null;
|
||||
/** Format: int64 */
|
||||
since?: number | null;
|
||||
stream?: components["schemas"]["ProcessLogsStream"] | null;
|
||||
tail?: number | null;
|
||||
};
|
||||
ProcessLogsResponse: {
|
||||
entries: components["schemas"]["ProcessLogEntry"][];
|
||||
processId: string;
|
||||
stream: components["schemas"]["ProcessLogsStream"];
|
||||
};
|
||||
/** @enum {string} */
|
||||
ProcessLogsStream: "stdout" | "stderr" | "combined" | "pty";
|
||||
ProcessRunRequest: {
|
||||
args?: string[];
|
||||
command: string;
|
||||
cwd?: string | null;
|
||||
env?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
maxOutputBytes?: number | null;
|
||||
/** Format: int64 */
|
||||
timeoutMs?: number | null;
|
||||
};
|
||||
ProcessRunResponse: {
|
||||
/** Format: int64 */
|
||||
durationMs: number;
|
||||
/** Format: int32 */
|
||||
exitCode?: number | null;
|
||||
stderr: string;
|
||||
stderrTruncated: boolean;
|
||||
stdout: string;
|
||||
stdoutTruncated: boolean;
|
||||
timedOut: boolean;
|
||||
};
|
||||
ProcessSignalQuery: {
|
||||
/** Format: int64 */
|
||||
waitMs?: number | null;
|
||||
};
|
||||
/** @enum {string} */
|
||||
ProcessState: "running" | "exited";
|
||||
ProcessTerminalResizeRequest: {
|
||||
/** Format: int32 */
|
||||
cols: number;
|
||||
/** Format: int32 */
|
||||
rows: number;
|
||||
};
|
||||
ProcessTerminalResizeResponse: {
|
||||
/** Format: int32 */
|
||||
cols: number;
|
||||
/** Format: int32 */
|
||||
rows: number;
|
||||
};
|
||||
/** @enum {string} */
|
||||
ServerStatus: "running" | "stopped";
|
||||
ServerStatusInfo: {
|
||||
|
|
@ -960,486 +748,4 @@ 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 */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProcessListResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Process API unsupported on this platform */
|
||||
501: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* 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: {
|
||||
"application/json": components["schemas"]["ProcessCreateRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Started process */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProcessInfo"];
|
||||
};
|
||||
};
|
||||
/** @description Invalid request */
|
||||
400: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
/** @description Process limit or state conflict */
|
||||
409: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
/** @description Process API unsupported on this platform */
|
||||
501: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* 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 */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProcessConfig"];
|
||||
};
|
||||
};
|
||||
/** @description Process API unsupported on this platform */
|
||||
501: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* 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: {
|
||||
"application/json": components["schemas"]["ProcessConfig"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Updated runtime process config */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProcessConfig"];
|
||||
};
|
||||
};
|
||||
/** @description Invalid config */
|
||||
400: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
/** @description Process API unsupported on this platform */
|
||||
501: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* 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: {
|
||||
"application/json": components["schemas"]["ProcessRunRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description One-off command result */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProcessRunResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Invalid request */
|
||||
400: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
/** @description Process API unsupported on this platform */
|
||||
501: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* 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: {
|
||||
/** @description Process ID */
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Process details */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProcessInfo"];
|
||||
};
|
||||
};
|
||||
/** @description Unknown process */
|
||||
404: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
/** @description Process API unsupported on this platform */
|
||||
501: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* 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: {
|
||||
/** @description Process ID */
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Process deleted */
|
||||
204: {
|
||||
content: never;
|
||||
};
|
||||
/** @description Unknown process */
|
||||
404: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
/** @description Process is still running */
|
||||
409: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
/** @description Process API unsupported on this platform */
|
||||
501: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* 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: {
|
||||
/** @description Process ID */
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProcessInputRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Input accepted */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProcessInputResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Invalid request */
|
||||
400: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
/** @description Process not writable */
|
||||
409: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
/** @description Input exceeds configured limit */
|
||||
413: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
/** @description Process API unsupported on this platform */
|
||||
501: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* 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?: {
|
||||
/** @description Wait up to N ms for process to exit */
|
||||
waitMs?: number | null;
|
||||
};
|
||||
path: {
|
||||
/** @description Process ID */
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Kill signal sent */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProcessInfo"];
|
||||
};
|
||||
};
|
||||
/** @description Unknown process */
|
||||
404: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
/** @description Process API unsupported on this platform */
|
||||
501: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* 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?: {
|
||||
/** @description stdout|stderr|combined|pty */
|
||||
stream?: components["schemas"]["ProcessLogsStream"] | null;
|
||||
/** @description Tail N entries */
|
||||
tail?: number | null;
|
||||
/** @description Follow via SSE */
|
||||
follow?: boolean | null;
|
||||
/** @description Only entries with sequence greater than this */
|
||||
since?: number | null;
|
||||
};
|
||||
path: {
|
||||
/** @description Process ID */
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Process logs */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProcessLogsResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Unknown process */
|
||||
404: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
/** @description Process API unsupported on this platform */
|
||||
501: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* 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?: {
|
||||
/** @description Wait up to N ms for process to exit */
|
||||
waitMs?: number | null;
|
||||
};
|
||||
path: {
|
||||
/** @description Process ID */
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Stop signal sent */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProcessInfo"];
|
||||
};
|
||||
};
|
||||
/** @description Unknown process */
|
||||
404: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
/** @description Process API unsupported on this platform */
|
||||
501: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* 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: {
|
||||
/** @description Process ID */
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProcessTerminalResizeRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Resize accepted */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProcessTerminalResizeResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Invalid request */
|
||||
400: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
/** @description Unknown process */
|
||||
404: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
/** @description Not a terminal process */
|
||||
409: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
/** @description Process API unsupported on this platform */
|
||||
501: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* 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?: {
|
||||
/** @description Bearer token alternative for WS auth */
|
||||
access_token?: string | null;
|
||||
};
|
||||
path: {
|
||||
/** @description Process ID */
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description WebSocket upgraded */
|
||||
101: {
|
||||
content: never;
|
||||
};
|
||||
/** @description Invalid websocket frame or upgrade request */
|
||||
400: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
/** @description Unknown process */
|
||||
404: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
/** @description Not a terminal process */
|
||||
409: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
/** @description Process API unsupported on this platform */
|
||||
501: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
export {
|
||||
LiveAcpConnection,
|
||||
ProcessTerminalSession,
|
||||
SandboxAgent,
|
||||
SandboxAgentError,
|
||||
Session,
|
||||
UnsupportedSessionCategoryError,
|
||||
UnsupportedSessionConfigOptionError,
|
||||
UnsupportedSessionValueError,
|
||||
} from "./client.ts";
|
||||
|
||||
export { AcpRpcError } from "acp-http-client";
|
||||
|
|
@ -14,14 +10,6 @@ export { AcpRpcError } from "acp-http-client";
|
|||
export { buildInspectorUrl } from "./inspector.ts";
|
||||
|
||||
export type {
|
||||
SandboxAgentHealthWaitOptions,
|
||||
AgentQueryOptions,
|
||||
ProcessLogFollowQuery,
|
||||
ProcessLogListener,
|
||||
ProcessLogSubscription,
|
||||
ProcessTerminalConnectOptions,
|
||||
ProcessTerminalSessionOptions,
|
||||
ProcessTerminalWebSocketUrlOptions,
|
||||
SandboxAgentConnectOptions,
|
||||
SandboxAgentStartOptions,
|
||||
SessionCreateRequest,
|
||||
|
|
@ -41,7 +29,6 @@ export type {
|
|||
AcpServerInfo,
|
||||
AcpServerListResponse,
|
||||
AgentInfo,
|
||||
AgentQuery,
|
||||
AgentInstallRequest,
|
||||
AgentInstallResponse,
|
||||
AgentListResponse,
|
||||
|
|
@ -64,37 +51,11 @@ export type {
|
|||
McpConfigQuery,
|
||||
McpServerConfig,
|
||||
ProblemDetails,
|
||||
ProcessConfig,
|
||||
ProcessCreateRequest,
|
||||
ProcessInfo,
|
||||
ProcessInputRequest,
|
||||
ProcessInputResponse,
|
||||
ProcessListResponse,
|
||||
ProcessLogEntry,
|
||||
ProcessLogsQuery,
|
||||
ProcessLogsResponse,
|
||||
ProcessLogsStream,
|
||||
ProcessRunRequest,
|
||||
ProcessRunResponse,
|
||||
ProcessSignalQuery,
|
||||
ProcessState,
|
||||
ProcessTerminalClientFrame,
|
||||
ProcessTerminalErrorFrame,
|
||||
ProcessTerminalExitFrame,
|
||||
ProcessTerminalReadyFrame,
|
||||
ProcessTerminalResizeRequest,
|
||||
ProcessTerminalResizeResponse,
|
||||
ProcessTerminalServerFrame,
|
||||
SessionEvent,
|
||||
SessionPersistDriver,
|
||||
SessionRecord,
|
||||
SkillsConfig,
|
||||
SkillsConfigQuery,
|
||||
TerminalErrorStatus,
|
||||
TerminalExitStatus,
|
||||
TerminalReadyStatus,
|
||||
TerminalResizePayload,
|
||||
TerminalStatusMessage,
|
||||
} from "./types.ts";
|
||||
|
||||
export type {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,4 @@
|
|||
import type {
|
||||
AnyMessage,
|
||||
NewSessionRequest,
|
||||
SessionConfigOption,
|
||||
SessionModeState,
|
||||
} from "acp-http-client";
|
||||
import type { AnyMessage, NewSessionRequest } from "acp-http-client";
|
||||
import type { components, operations } from "./generated/openapi.ts";
|
||||
|
||||
export type ProblemDetails = components["schemas"]["ProblemDetails"];
|
||||
|
|
@ -11,7 +6,6 @@ export type ProblemDetails = components["schemas"]["ProblemDetails"];
|
|||
export type HealthResponse = JsonResponse<operations["get_v1_health"], 200>;
|
||||
export type AgentListResponse = JsonResponse<operations["get_v1_agents"], 200>;
|
||||
export type AgentInfo = components["schemas"]["AgentInfo"];
|
||||
export type AgentQuery = QueryParams<operations["get_v1_agents"]>;
|
||||
export type AgentInstallRequest = JsonRequestBody<operations["post_v1_agent_install"]>;
|
||||
export type AgentInstallResponse = JsonResponse<operations["post_v1_agent_install"], 200>;
|
||||
|
||||
|
|
@ -37,68 +31,6 @@ export type McpServerConfig = components["schemas"]["McpServerConfig"];
|
|||
export type SkillsConfigQuery = QueryParams<operations["get_v1_config_skills"]>;
|
||||
export type SkillsConfig = components["schemas"]["SkillsConfig"];
|
||||
|
||||
export type ProcessConfig = JsonResponse<operations["get_v1_processes_config"], 200>;
|
||||
export type ProcessCreateRequest = JsonRequestBody<operations["post_v1_processes"]>;
|
||||
export type ProcessInfo = components["schemas"]["ProcessInfo"];
|
||||
export type ProcessInputRequest = JsonRequestBody<operations["post_v1_process_input"]>;
|
||||
export type ProcessInputResponse = JsonResponse<operations["post_v1_process_input"], 200>;
|
||||
export type ProcessListResponse = JsonResponse<operations["get_v1_processes"], 200>;
|
||||
export type ProcessLogEntry = components["schemas"]["ProcessLogEntry"];
|
||||
export type ProcessLogsQuery = QueryParams<operations["get_v1_process_logs"]>;
|
||||
export type ProcessLogsResponse = JsonResponse<operations["get_v1_process_logs"], 200>;
|
||||
export type ProcessLogsStream = components["schemas"]["ProcessLogsStream"];
|
||||
export type ProcessRunRequest = JsonRequestBody<operations["post_v1_processes_run"]>;
|
||||
export type ProcessRunResponse = JsonResponse<operations["post_v1_processes_run"], 200>;
|
||||
export type ProcessSignalQuery = QueryParams<operations["post_v1_process_stop"]>;
|
||||
export type ProcessState = components["schemas"]["ProcessState"];
|
||||
export type ProcessTerminalResizeRequest = JsonRequestBody<operations["post_v1_process_terminal_resize"]>;
|
||||
export type ProcessTerminalResizeResponse = JsonResponse<operations["post_v1_process_terminal_resize"], 200>;
|
||||
|
||||
export type ProcessTerminalClientFrame =
|
||||
| {
|
||||
type: "input";
|
||||
data: string;
|
||||
encoding?: string;
|
||||
}
|
||||
| {
|
||||
type: "resize";
|
||||
cols: number;
|
||||
rows: number;
|
||||
}
|
||||
| {
|
||||
type: "close";
|
||||
};
|
||||
|
||||
export interface ProcessTerminalReadyFrame {
|
||||
type: "ready";
|
||||
processId: string;
|
||||
}
|
||||
|
||||
export interface ProcessTerminalExitFrame {
|
||||
type: "exit";
|
||||
exitCode?: number | null;
|
||||
}
|
||||
|
||||
export interface ProcessTerminalErrorFrame {
|
||||
type: "error";
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type ProcessTerminalServerFrame =
|
||||
| ProcessTerminalReadyFrame
|
||||
| ProcessTerminalExitFrame
|
||||
| ProcessTerminalErrorFrame;
|
||||
|
||||
export type TerminalReadyStatus = ProcessTerminalReadyFrame;
|
||||
export type TerminalExitStatus = ProcessTerminalExitFrame;
|
||||
export type TerminalErrorStatus = ProcessTerminalErrorFrame;
|
||||
export type TerminalStatusMessage = ProcessTerminalServerFrame;
|
||||
|
||||
export interface TerminalResizePayload {
|
||||
cols: number;
|
||||
rows: number;
|
||||
}
|
||||
|
||||
export interface SessionRecord {
|
||||
id: string;
|
||||
agent: string;
|
||||
|
|
@ -107,8 +39,6 @@ export interface SessionRecord {
|
|||
createdAt: number;
|
||||
destroyedAt?: number;
|
||||
sessionInit?: Omit<NewSessionRequest, "_meta">;
|
||||
configOptions?: SessionConfigOption[];
|
||||
modes?: SessionModeState | null;
|
||||
}
|
||||
|
||||
export type SessionEventSender = "client" | "agent";
|
||||
|
|
@ -248,12 +178,6 @@ function cloneSessionRecord(session: SessionRecord): SessionRecord {
|
|||
sessionInit: session.sessionInit
|
||||
? (JSON.parse(JSON.stringify(session.sessionInit)) as SessionRecord["sessionInit"])
|
||||
: undefined,
|
||||
configOptions: session.configOptions
|
||||
? (JSON.parse(JSON.stringify(session.configOptions)) as SessionRecord["configOptions"])
|
||||
: undefined,
|
||||
modes: session.modes
|
||||
? (JSON.parse(JSON.stringify(session.modes)) as SessionRecord["modes"])
|
||||
: session.modes,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,29 +1,18 @@
|
|||
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
function candidateInstallDirs(dataHome: string): string[] {
|
||||
const dirs = [join(dataHome, "sandbox-agent", "bin")];
|
||||
if (process.platform === "darwin") {
|
||||
dirs.push(join(dataHome, "Library", "Application Support", "sandbox-agent", "bin"));
|
||||
} else if (process.platform === "win32") {
|
||||
dirs.push(join(dataHome, "AppData", "Roaming", "sandbox-agent", "bin"));
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
export function prepareMockAgentDataHome(dataHome: string): void {
|
||||
const installDir = join(dataHome, "sandbox-agent", "bin");
|
||||
const processDir = join(installDir, "agent_processes");
|
||||
mkdirSync(processDir, { recursive: true });
|
||||
|
||||
export function prepareMockAgentDataHome(dataHome: string): Record<string, string> {
|
||||
const runtimeEnv: Record<string, string> = {};
|
||||
if (process.platform === "darwin") {
|
||||
runtimeEnv.HOME = dataHome;
|
||||
runtimeEnv.XDG_DATA_HOME = join(dataHome, ".local", "share");
|
||||
} else if (process.platform === "win32") {
|
||||
runtimeEnv.USERPROFILE = dataHome;
|
||||
runtimeEnv.APPDATA = join(dataHome, "AppData", "Roaming");
|
||||
runtimeEnv.LOCALAPPDATA = join(dataHome, "AppData", "Local");
|
||||
} else {
|
||||
runtimeEnv.HOME = dataHome;
|
||||
runtimeEnv.XDG_DATA_HOME = dataHome;
|
||||
}
|
||||
const runner = process.platform === "win32"
|
||||
? join(processDir, "mock-acp.cmd")
|
||||
: join(processDir, "mock-acp");
|
||||
|
||||
const scriptFile = process.platform === "win32"
|
||||
? join(processDir, "mock-acp.js")
|
||||
: runner;
|
||||
|
||||
const nodeScript = String.raw`#!/usr/bin/env node
|
||||
const { createInterface } = require("node:readline");
|
||||
|
|
@ -138,29 +127,14 @@ rl.on("line", (line) => {
|
|||
});
|
||||
`;
|
||||
|
||||
for (const installDir of candidateInstallDirs(dataHome)) {
|
||||
const processDir = join(installDir, "agent_processes");
|
||||
mkdirSync(processDir, { recursive: true });
|
||||
writeFileSync(scriptFile, nodeScript);
|
||||
|
||||
const runner = process.platform === "win32"
|
||||
? join(processDir, "mock-acp.cmd")
|
||||
: join(processDir, "mock-acp");
|
||||
|
||||
const scriptFile = process.platform === "win32"
|
||||
? join(processDir, "mock-acp.js")
|
||||
: runner;
|
||||
|
||||
writeFileSync(scriptFile, nodeScript);
|
||||
|
||||
if (process.platform === "win32") {
|
||||
writeFileSync(runner, `@echo off\r\nnode "${scriptFile}" %*\r\n`);
|
||||
}
|
||||
|
||||
chmodSync(scriptFile, 0o755);
|
||||
if (process.platform === "win32") {
|
||||
chmodSync(runner, 0o755);
|
||||
}
|
||||
if (process.platform === "win32") {
|
||||
writeFileSync(runner, `@echo off\r\nnode "${scriptFile}" %*\r\n`);
|
||||
}
|
||||
|
||||
return runtimeEnv;
|
||||
chmodSync(scriptFile, 0o755);
|
||||
if (process.platform === "win32") {
|
||||
chmodSync(runner, 0o755);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import {
|
|||
} from "../src/index.ts";
|
||||
import { spawnSandboxAgent, isNodeRuntime, type SandboxAgentSpawnHandle } from "../src/spawn.ts";
|
||||
import { prepareMockAgentDataHome } from "./helpers/mock-agent.ts";
|
||||
import WebSocket from "ws";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
|
|
@ -65,91 +64,6 @@ async function waitFor<T>(
|
|||
throw new Error("timed out waiting for condition");
|
||||
}
|
||||
|
||||
async function waitForAsync<T>(
|
||||
fn: () => Promise<T | undefined | null>,
|
||||
timeoutMs = 6000,
|
||||
stepMs = 30,
|
||||
): Promise<T> {
|
||||
const started = Date.now();
|
||||
while (Date.now() - started < timeoutMs) {
|
||||
const value = await fn();
|
||||
if (value !== undefined && value !== null) {
|
||||
return value;
|
||||
}
|
||||
await sleep(stepMs);
|
||||
}
|
||||
throw new Error("timed out waiting for condition");
|
||||
}
|
||||
|
||||
function buildTarArchive(entries: Array<{ name: string; content: string }>): Uint8Array {
|
||||
const blocks: Buffer[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const content = Buffer.from(entry.content, "utf8");
|
||||
const header = Buffer.alloc(512, 0);
|
||||
|
||||
writeTarString(header, 0, 100, entry.name);
|
||||
writeTarOctal(header, 100, 8, 0o644);
|
||||
writeTarOctal(header, 108, 8, 0);
|
||||
writeTarOctal(header, 116, 8, 0);
|
||||
writeTarOctal(header, 124, 12, content.length);
|
||||
writeTarOctal(header, 136, 12, Math.floor(Date.now() / 1000));
|
||||
header.fill(0x20, 148, 156);
|
||||
header[156] = "0".charCodeAt(0);
|
||||
writeTarString(header, 257, 6, "ustar");
|
||||
writeTarString(header, 263, 2, "00");
|
||||
|
||||
let checksum = 0;
|
||||
for (const byte of header) {
|
||||
checksum += byte;
|
||||
}
|
||||
writeTarChecksum(header, checksum);
|
||||
|
||||
blocks.push(header);
|
||||
blocks.push(content);
|
||||
|
||||
const remainder = content.length % 512;
|
||||
if (remainder !== 0) {
|
||||
blocks.push(Buffer.alloc(512 - remainder, 0));
|
||||
}
|
||||
}
|
||||
|
||||
blocks.push(Buffer.alloc(1024, 0));
|
||||
return Buffer.concat(blocks);
|
||||
}
|
||||
|
||||
function writeTarString(buffer: Buffer, offset: number, length: number, value: string): void {
|
||||
const bytes = Buffer.from(value, "utf8");
|
||||
bytes.copy(buffer, offset, 0, Math.min(bytes.length, length));
|
||||
}
|
||||
|
||||
function writeTarOctal(buffer: Buffer, offset: number, length: number, value: number): void {
|
||||
const rendered = value.toString(8).padStart(length - 1, "0");
|
||||
writeTarString(buffer, offset, length, rendered);
|
||||
buffer[offset + length - 1] = 0;
|
||||
}
|
||||
|
||||
function writeTarChecksum(buffer: Buffer, checksum: number): void {
|
||||
const rendered = checksum.toString(8).padStart(6, "0");
|
||||
writeTarString(buffer, 148, 6, rendered);
|
||||
buffer[154] = 0;
|
||||
buffer[155] = 0x20;
|
||||
}
|
||||
|
||||
function decodeProcessLogData(data: string, encoding: string): string {
|
||||
if (encoding === "base64") {
|
||||
return Buffer.from(data, "base64").toString("utf8");
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function nodeCommand(source: string): { command: string; args: string[] } {
|
||||
return {
|
||||
command: process.execPath,
|
||||
args: ["-e", source],
|
||||
};
|
||||
}
|
||||
|
||||
describe("Integration: TypeScript SDK flat session API", () => {
|
||||
let handle: SandboxAgentSpawnHandle;
|
||||
let baseUrl: string;
|
||||
|
|
@ -158,13 +72,15 @@ describe("Integration: TypeScript SDK flat session API", () => {
|
|||
|
||||
beforeAll(async () => {
|
||||
dataHome = mkdtempSync(join(tmpdir(), "sdk-integration-"));
|
||||
const agentEnv = prepareMockAgentDataHome(dataHome);
|
||||
prepareMockAgentDataHome(dataHome);
|
||||
|
||||
handle = await spawnSandboxAgent({
|
||||
enabled: true,
|
||||
log: "silent",
|
||||
timeoutMs: 30000,
|
||||
env: agentEnv,
|
||||
env: {
|
||||
XDG_DATA_HOME: dataHome,
|
||||
},
|
||||
});
|
||||
baseUrl = handle.baseUrl;
|
||||
token = handle.token;
|
||||
|
|
@ -206,9 +122,6 @@ describe("Integration: TypeScript SDK flat session API", () => {
|
|||
const fetched = await sdk.getSession(session.id);
|
||||
expect(fetched?.agent).toBe("mock");
|
||||
|
||||
const acpServers = await sdk.listAcpServers();
|
||||
expect(acpServers.servers.some((server) => server.agent === "mock")).toBe(true);
|
||||
|
||||
const events = await sdk.getEvents({ sessionId: session.id, limit: 100 });
|
||||
expect(events.items.length).toBeGreaterThan(0);
|
||||
expect(events.items.some((event) => event.sender === "client")).toBe(true);
|
||||
|
|
@ -224,64 +137,6 @@ describe("Integration: TypeScript SDK flat session API", () => {
|
|||
await sdk.dispose();
|
||||
});
|
||||
|
||||
it("covers agent query flags and filesystem HTTP helpers", async () => {
|
||||
const sdk = await SandboxAgent.connect({
|
||||
baseUrl,
|
||||
token,
|
||||
});
|
||||
|
||||
const directory = mkdtempSync(join(tmpdir(), "sdk-fs-"));
|
||||
const nestedDir = join(directory, "nested");
|
||||
const filePath = join(directory, "notes.txt");
|
||||
const movedPath = join(directory, "notes-moved.txt");
|
||||
const uploadDir = join(directory, "uploaded");
|
||||
|
||||
try {
|
||||
const listedAgents = await sdk.listAgents({ config: true, noCache: true });
|
||||
expect(listedAgents.agents.some((agent) => agent.id === "mock")).toBe(true);
|
||||
|
||||
const mockAgent = await sdk.getAgent("mock", { config: true, noCache: true });
|
||||
expect(mockAgent.id).toBe("mock");
|
||||
expect(Array.isArray(mockAgent.configOptions)).toBe(true);
|
||||
|
||||
await sdk.mkdirFs({ path: nestedDir });
|
||||
await sdk.writeFsFile({ path: filePath }, "hello from sdk");
|
||||
|
||||
const bytes = await sdk.readFsFile({ path: filePath });
|
||||
expect(new TextDecoder().decode(bytes)).toBe("hello from sdk");
|
||||
|
||||
const stat = await sdk.statFs({ path: filePath });
|
||||
expect(stat.path).toBe(filePath);
|
||||
expect(stat.size).toBe(bytes.byteLength);
|
||||
|
||||
const entries = await sdk.listFsEntries({ path: directory });
|
||||
expect(entries.some((entry) => entry.path === nestedDir)).toBe(true);
|
||||
expect(entries.some((entry) => entry.path === filePath)).toBe(true);
|
||||
|
||||
const moved = await sdk.moveFs({
|
||||
from: filePath,
|
||||
to: movedPath,
|
||||
overwrite: true,
|
||||
});
|
||||
expect(moved.to).toBe(movedPath);
|
||||
|
||||
const uploadResult = await sdk.uploadFsBatch(
|
||||
buildTarArchive([{ name: "batch.txt", content: "batch upload works" }]),
|
||||
{ path: uploadDir },
|
||||
);
|
||||
expect(uploadResult.paths.some((path) => path.endsWith("batch.txt"))).toBe(true);
|
||||
|
||||
const uploaded = await sdk.readFsFile({ path: join(uploadDir, "batch.txt") });
|
||||
expect(new TextDecoder().decode(uploaded)).toBe("batch upload works");
|
||||
|
||||
const deleted = await sdk.deleteFsEntry({ path: movedPath });
|
||||
expect(deleted.path).toBe(movedPath);
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
await sdk.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it("uses custom fetch for both HTTP helpers and ACP session traffic", async () => {
|
||||
const defaultFetch = globalThis.fetch;
|
||||
if (!defaultFetch) {
|
||||
|
|
@ -313,7 +168,7 @@ describe("Integration: TypeScript SDK flat session API", () => {
|
|||
expect(seenPaths.some((path) => path.startsWith("/v1/acp/"))).toBe(true);
|
||||
|
||||
await sdk.dispose();
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
it("requires baseUrl when fetch is not provided", async () => {
|
||||
await expect(SandboxAgent.connect({ token } as any)).rejects.toThrow(
|
||||
|
|
@ -321,111 +176,6 @@ describe("Integration: TypeScript SDK flat session API", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("waits for health before non-ACP HTTP helpers", async () => {
|
||||
const defaultFetch = globalThis.fetch;
|
||||
if (!defaultFetch) {
|
||||
throw new Error("Global fetch is not available in this runtime.");
|
||||
}
|
||||
|
||||
let healthAttempts = 0;
|
||||
const seenPaths: string[] = [];
|
||||
const customFetch: typeof fetch = async (input, init) => {
|
||||
const outgoing = new Request(input, init);
|
||||
const parsed = new URL(outgoing.url);
|
||||
seenPaths.push(parsed.pathname);
|
||||
|
||||
if (parsed.pathname === "/v1/health") {
|
||||
healthAttempts += 1;
|
||||
if (healthAttempts < 3) {
|
||||
return new Response("warming up", { status: 503 });
|
||||
}
|
||||
}
|
||||
|
||||
const forwardedUrl = new URL(`${parsed.pathname}${parsed.search}`, baseUrl);
|
||||
const forwarded = new Request(forwardedUrl.toString(), outgoing);
|
||||
return defaultFetch(forwarded);
|
||||
};
|
||||
|
||||
const sdk = await SandboxAgent.connect({
|
||||
token,
|
||||
fetch: customFetch,
|
||||
});
|
||||
|
||||
const agents = await sdk.listAgents();
|
||||
expect(Array.isArray(agents.agents)).toBe(true);
|
||||
expect(healthAttempts).toBe(3);
|
||||
|
||||
const firstAgentsRequest = seenPaths.indexOf("/v1/agents");
|
||||
expect(firstAgentsRequest).toBeGreaterThanOrEqual(0);
|
||||
expect(seenPaths.slice(0, firstAgentsRequest)).toEqual([
|
||||
"/v1/health",
|
||||
"/v1/health",
|
||||
"/v1/health",
|
||||
]);
|
||||
|
||||
await sdk.dispose();
|
||||
});
|
||||
|
||||
it("surfaces health timeout when a request awaits readiness", async () => {
|
||||
const customFetch: typeof fetch = async (input, init) => {
|
||||
const outgoing = new Request(input, init);
|
||||
const parsed = new URL(outgoing.url);
|
||||
|
||||
if (parsed.pathname === "/v1/health") {
|
||||
return new Response("warming up", { status: 503 });
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected request path during timeout test: ${parsed.pathname}`);
|
||||
};
|
||||
|
||||
const sdk = await SandboxAgent.connect({
|
||||
token,
|
||||
fetch: customFetch,
|
||||
waitForHealth: { timeoutMs: 100 },
|
||||
});
|
||||
|
||||
await expect(sdk.listAgents()).rejects.toThrow("Timed out waiting for sandbox-agent health");
|
||||
await sdk.dispose();
|
||||
});
|
||||
|
||||
it("aborts the shared health wait when connect signal is aborted", async () => {
|
||||
const controller = new AbortController();
|
||||
const customFetch: typeof fetch = async (input, init) => {
|
||||
const outgoing = new Request(input, init);
|
||||
const parsed = new URL(outgoing.url);
|
||||
|
||||
if (parsed.pathname !== "/v1/health") {
|
||||
throw new Error(`Unexpected request path during abort test: ${parsed.pathname}`);
|
||||
}
|
||||
|
||||
return new Promise<Response>((_resolve, reject) => {
|
||||
const onAbort = () => {
|
||||
outgoing.signal.removeEventListener("abort", onAbort);
|
||||
reject(outgoing.signal.reason ?? new DOMException("Connect aborted", "AbortError"));
|
||||
};
|
||||
|
||||
if (outgoing.signal.aborted) {
|
||||
onAbort();
|
||||
return;
|
||||
}
|
||||
|
||||
outgoing.signal.addEventListener("abort", onAbort, { once: true });
|
||||
});
|
||||
};
|
||||
|
||||
const sdk = await SandboxAgent.connect({
|
||||
token,
|
||||
fetch: customFetch,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
const pending = sdk.listAgents();
|
||||
controller.abort(new DOMException("Connect aborted", "AbortError"));
|
||||
|
||||
await expect(pending).rejects.toThrow("Connect aborted");
|
||||
await sdk.dispose();
|
||||
});
|
||||
|
||||
it("restores a session on stale connection by recreating and replaying history on first prompt", async () => {
|
||||
const persist = new InMemorySessionPersistDriver({
|
||||
maxEventsPerSession: 200,
|
||||
|
|
@ -504,127 +254,6 @@ describe("Integration: TypeScript SDK flat session API", () => {
|
|||
await sdk.dispose();
|
||||
});
|
||||
|
||||
it("blocks manual session/cancel and requires destroySession", async () => {
|
||||
const sdk = await SandboxAgent.connect({
|
||||
baseUrl,
|
||||
token,
|
||||
});
|
||||
|
||||
const session = await sdk.createSession({ agent: "mock" });
|
||||
|
||||
await expect(session.send("session/cancel")).rejects.toThrow(
|
||||
"Use destroySession(sessionId) instead.",
|
||||
);
|
||||
await expect(sdk.sendSessionMethod(session.id, "session/cancel", {})).rejects.toThrow(
|
||||
"Use destroySession(sessionId) instead.",
|
||||
);
|
||||
|
||||
const destroyed = await sdk.destroySession(session.id);
|
||||
expect(destroyed.destroyedAt).toBeDefined();
|
||||
|
||||
const reloaded = await sdk.getSession(session.id);
|
||||
expect(reloaded?.destroyedAt).toBeDefined();
|
||||
|
||||
await sdk.dispose();
|
||||
});
|
||||
|
||||
it("supports typed config helpers and createSession preconfiguration", async () => {
|
||||
const sdk = await SandboxAgent.connect({
|
||||
baseUrl,
|
||||
token,
|
||||
});
|
||||
|
||||
const session = await sdk.createSession({
|
||||
agent: "mock",
|
||||
model: "mock",
|
||||
});
|
||||
|
||||
const options = await session.getConfigOptions();
|
||||
expect(options.some((option) => option.category === "model")).toBe(true);
|
||||
|
||||
await expect(session.setModel("unknown-model")).rejects.toThrow("does not support value");
|
||||
|
||||
await sdk.dispose();
|
||||
});
|
||||
|
||||
it("setModel happy path switches to a valid model", async () => {
|
||||
const sdk = await SandboxAgent.connect({
|
||||
baseUrl,
|
||||
token,
|
||||
});
|
||||
|
||||
const session = await sdk.createSession({ agent: "mock" });
|
||||
await session.setModel("mock-fast");
|
||||
|
||||
const options = await session.getConfigOptions();
|
||||
const modelOption = options.find((o) => o.category === "model");
|
||||
expect(modelOption?.currentValue).toBe("mock-fast");
|
||||
|
||||
await sdk.dispose();
|
||||
});
|
||||
|
||||
it("setMode happy path switches to a valid mode", async () => {
|
||||
const sdk = await SandboxAgent.connect({
|
||||
baseUrl,
|
||||
token,
|
||||
});
|
||||
|
||||
const session = await sdk.createSession({ agent: "mock" });
|
||||
await session.setMode("plan");
|
||||
|
||||
const modes = await session.getModes();
|
||||
expect(modes?.currentModeId).toBe("plan");
|
||||
|
||||
await sdk.dispose();
|
||||
});
|
||||
|
||||
it("setThoughtLevel happy path switches to a valid thought level", async () => {
|
||||
const sdk = await SandboxAgent.connect({
|
||||
baseUrl,
|
||||
token,
|
||||
});
|
||||
|
||||
const session = await sdk.createSession({ agent: "mock" });
|
||||
await session.setThoughtLevel("high");
|
||||
|
||||
const options = await session.getConfigOptions();
|
||||
const thoughtOption = options.find((o) => o.category === "thought_level");
|
||||
expect(thoughtOption?.currentValue).toBe("high");
|
||||
|
||||
await sdk.dispose();
|
||||
});
|
||||
|
||||
it("setModel/setMode/setThoughtLevel can be changed multiple times", async () => {
|
||||
const sdk = await SandboxAgent.connect({
|
||||
baseUrl,
|
||||
token,
|
||||
});
|
||||
|
||||
const session = await sdk.createSession({ agent: "mock" });
|
||||
|
||||
// Model: mock → mock-fast → mock
|
||||
await session.setModel("mock-fast");
|
||||
expect((await session.getConfigOptions()).find((o) => o.category === "model")?.currentValue).toBe("mock-fast");
|
||||
await session.setModel("mock");
|
||||
expect((await session.getConfigOptions()).find((o) => o.category === "model")?.currentValue).toBe("mock");
|
||||
|
||||
// Mode: normal → plan → normal
|
||||
await session.setMode("plan");
|
||||
expect((await session.getModes())?.currentModeId).toBe("plan");
|
||||
await session.setMode("normal");
|
||||
expect((await session.getModes())?.currentModeId).toBe("normal");
|
||||
|
||||
// Thought level: low → high → medium → low
|
||||
await session.setThoughtLevel("high");
|
||||
expect((await session.getConfigOptions()).find((o) => o.category === "thought_level")?.currentValue).toBe("high");
|
||||
await session.setThoughtLevel("medium");
|
||||
expect((await session.getConfigOptions()).find((o) => o.category === "thought_level")?.currentValue).toBe("medium");
|
||||
await session.setThoughtLevel("low");
|
||||
expect((await session.getConfigOptions()).find((o) => o.category === "thought_level")?.currentValue).toBe("low");
|
||||
|
||||
await sdk.dispose();
|
||||
});
|
||||
|
||||
it("supports MCP and skills config HTTP helpers", async () => {
|
||||
const sdk = await SandboxAgent.connect({
|
||||
baseUrl,
|
||||
|
|
@ -691,195 +320,4 @@ describe("Integration: TypeScript SDK flat session API", () => {
|
|||
await sdk.dispose();
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("covers process runtime HTTP helpers, log streaming, and terminal websocket access", async () => {
|
||||
const sdk = await SandboxAgent.connect({
|
||||
baseUrl,
|
||||
token,
|
||||
});
|
||||
|
||||
const originalConfig = await sdk.getProcessConfig();
|
||||
const updatedConfig = await sdk.setProcessConfig({
|
||||
...originalConfig,
|
||||
maxOutputBytes: originalConfig.maxOutputBytes + 1,
|
||||
});
|
||||
expect(updatedConfig.maxOutputBytes).toBe(originalConfig.maxOutputBytes + 1);
|
||||
|
||||
const runResult = await sdk.runProcess({
|
||||
...nodeCommand("process.stdout.write('run-stdout'); process.stderr.write('run-stderr');"),
|
||||
timeoutMs: 5_000,
|
||||
});
|
||||
expect(runResult.stdout).toContain("run-stdout");
|
||||
expect(runResult.stderr).toContain("run-stderr");
|
||||
|
||||
let interactiveProcessId: string | undefined;
|
||||
let ttyProcessId: string | undefined;
|
||||
let killProcessId: string | undefined;
|
||||
|
||||
try {
|
||||
const interactiveProcess = await sdk.createProcess({
|
||||
...nodeCommand(`
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdout.write("ready\\n");
|
||||
process.stdin.on("data", (chunk) => {
|
||||
process.stdout.write("echo:" + chunk);
|
||||
});
|
||||
setInterval(() => {}, 1_000);
|
||||
`),
|
||||
interactive: true,
|
||||
});
|
||||
interactiveProcessId = interactiveProcess.id;
|
||||
|
||||
const listed = await sdk.listProcesses();
|
||||
expect(listed.processes.some((process) => process.id === interactiveProcess.id)).toBe(true);
|
||||
|
||||
const fetched = await sdk.getProcess(interactiveProcess.id);
|
||||
expect(fetched.status).toBe("running");
|
||||
|
||||
const initialLogs = await waitForAsync(async () => {
|
||||
const logs = await sdk.getProcessLogs(interactiveProcess.id, { tail: 10 });
|
||||
return logs.entries.some((entry) => decodeProcessLogData(entry.data, entry.encoding).includes("ready"))
|
||||
? logs
|
||||
: undefined;
|
||||
});
|
||||
expect(
|
||||
initialLogs.entries.some((entry) => decodeProcessLogData(entry.data, entry.encoding).includes("ready")),
|
||||
).toBe(true);
|
||||
|
||||
const followedLogs: string[] = [];
|
||||
const subscription = await sdk.followProcessLogs(
|
||||
interactiveProcess.id,
|
||||
(entry) => {
|
||||
followedLogs.push(decodeProcessLogData(entry.data, entry.encoding));
|
||||
},
|
||||
{ tail: 1 },
|
||||
);
|
||||
|
||||
try {
|
||||
const inputResult = await sdk.sendProcessInput(interactiveProcess.id, {
|
||||
data: Buffer.from("hello over stdin\n", "utf8").toString("base64"),
|
||||
encoding: "base64",
|
||||
});
|
||||
expect(inputResult.bytesWritten).toBeGreaterThan(0);
|
||||
|
||||
await waitFor(() => {
|
||||
const joined = followedLogs.join("");
|
||||
return joined.includes("echo:hello over stdin") ? joined : undefined;
|
||||
});
|
||||
} finally {
|
||||
subscription.close();
|
||||
await subscription.closed;
|
||||
}
|
||||
|
||||
const stopped = await sdk.stopProcess(interactiveProcess.id, { waitMs: 5_000 });
|
||||
expect(stopped.status).toBe("exited");
|
||||
|
||||
await sdk.deleteProcess(interactiveProcess.id);
|
||||
interactiveProcessId = undefined;
|
||||
|
||||
const ttyProcess = await sdk.createProcess({
|
||||
...nodeCommand(`
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("data", (chunk) => {
|
||||
process.stdout.write(chunk);
|
||||
});
|
||||
setInterval(() => {}, 1_000);
|
||||
`),
|
||||
interactive: true,
|
||||
tty: true,
|
||||
});
|
||||
ttyProcessId = ttyProcess.id;
|
||||
|
||||
const resized = await sdk.resizeProcessTerminal(ttyProcess.id, {
|
||||
cols: 120,
|
||||
rows: 40,
|
||||
});
|
||||
expect(resized.cols).toBe(120);
|
||||
expect(resized.rows).toBe(40);
|
||||
|
||||
const wsUrl = sdk.buildProcessTerminalWebSocketUrl(ttyProcess.id);
|
||||
expect(wsUrl.startsWith("ws://") || wsUrl.startsWith("wss://")).toBe(true);
|
||||
|
||||
const session = sdk.connectProcessTerminal(ttyProcess.id, {
|
||||
WebSocket: WebSocket as unknown as typeof globalThis.WebSocket,
|
||||
});
|
||||
const readyFrames: string[] = [];
|
||||
const ttyOutput: string[] = [];
|
||||
const exitFrames: Array<number | null | undefined> = [];
|
||||
const terminalErrors: string[] = [];
|
||||
let closeCount = 0;
|
||||
|
||||
session.onReady((frame) => {
|
||||
readyFrames.push(frame.processId);
|
||||
});
|
||||
session.onData((bytes) => {
|
||||
ttyOutput.push(Buffer.from(bytes).toString("utf8"));
|
||||
});
|
||||
session.onExit((frame) => {
|
||||
exitFrames.push(frame.exitCode);
|
||||
});
|
||||
session.onError((error) => {
|
||||
terminalErrors.push(error instanceof Error ? error.message : error.message);
|
||||
});
|
||||
session.onClose(() => {
|
||||
closeCount += 1;
|
||||
});
|
||||
|
||||
await waitFor(() => readyFrames[0]);
|
||||
|
||||
session.sendInput("hello tty\n");
|
||||
|
||||
await waitFor(() => {
|
||||
const joined = ttyOutput.join("");
|
||||
return joined.includes("hello tty") ? joined : undefined;
|
||||
});
|
||||
|
||||
session.close();
|
||||
await session.closed;
|
||||
expect(closeCount).toBeGreaterThan(0);
|
||||
expect(exitFrames).toHaveLength(0);
|
||||
expect(terminalErrors).toEqual([]);
|
||||
|
||||
await waitForAsync(async () => {
|
||||
const processInfo = await sdk.getProcess(ttyProcess.id);
|
||||
return processInfo.status === "running" ? processInfo : undefined;
|
||||
});
|
||||
|
||||
const killedTty = await sdk.killProcess(ttyProcess.id, { waitMs: 5_000 });
|
||||
expect(killedTty.status).toBe("exited");
|
||||
|
||||
await sdk.deleteProcess(ttyProcess.id);
|
||||
ttyProcessId = undefined;
|
||||
|
||||
const killProcess = await sdk.createProcess({
|
||||
...nodeCommand("setInterval(() => {}, 1_000);"),
|
||||
});
|
||||
killProcessId = killProcess.id;
|
||||
|
||||
const killed = await sdk.killProcess(killProcess.id, { waitMs: 5_000 });
|
||||
expect(killed.status).toBe("exited");
|
||||
|
||||
await sdk.deleteProcess(killProcess.id);
|
||||
killProcessId = undefined;
|
||||
} finally {
|
||||
await sdk.setProcessConfig(originalConfig);
|
||||
|
||||
if (interactiveProcessId) {
|
||||
await sdk.killProcess(interactiveProcessId, { waitMs: 5_000 }).catch(() => {});
|
||||
await sdk.deleteProcess(interactiveProcessId).catch(() => {});
|
||||
}
|
||||
|
||||
if (ttyProcessId) {
|
||||
await sdk.killProcess(ttyProcessId, { waitMs: 5_000 }).catch(() => {});
|
||||
await sdk.deleteProcess(ttyProcessId).catch(() => {});
|
||||
}
|
||||
|
||||
if (killProcessId) {
|
||||
await sdk.killProcess(killProcessId, { waitMs: 5_000 }).catch(() => {});
|
||||
await sdk.deleteProcess(killProcessId).catch(() => {});
|
||||
}
|
||||
|
||||
await sdk.dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue