@tanstack/ai-sandbox
Version:
Provider-agnostic sandbox layer for TanStack AI — run harness adapters inside isolated sandboxes (defineSandbox, defineWorkspace, withSandbox) with a uniform SandboxHandle, workspace bootstrap, policy, and resumable lifecycle.
129 lines (128 loc) • 4.85 kB
TypeScript
import { ExecResult, SandboxHandle } from './contracts.js';
/**
* A `WorkspaceBackend` over a live sandbox. With it, `workspaceTools()`
* reads and writes files and runs commands in the sandbox, not on this
* machine.
*
* Who stops the sandbox:
*
* - By default, you. `close()` of the backend stops the jobs that it
* started, and leaves the sandbox running.
* - With `owned: true`, the backend. `close({ reason: 'closed' })` also
* destroys the sandbox. Use it for a sandbox that you create in
* `workspace.bind`, one for each thread.
*
* - Paths are POSIX paths in the sandbox, like `/workspace/src/a.ts`.
* - `exec` gives exit code 124 when `timeoutMs` stops a command, and 1 when
* `signal` stops it. A sandbox with `capabilities.killableProcesses` off
* cannot stop the command, so it continues in the sandbox.
* - `spawn` is there only when the sandbox has
* `capabilities.backgroundProcesses`.
* - `stat` gives `mtimeMs: 0`. The sandbox file system does not give the
* time of the last change. With `lstat`, `stat` never reads the file.
* - The `signal` of a file call goes to the sandbox file system and to the
* commands that the call runs, like `readlink`.
* - `readLines` reads one page of a file in the sandbox with `wc`, `od`,
* and `awk`, so only the page comes out of the sandbox.
* - `realpath` resolves links in the sandbox: `lstat` finds each link, and
* `readlink` in the sandbox reads its target. A sandbox without `lstat`
* cannot find links, so `realpath` throws, and the tools refuse each path.
*
* - `spawn` keeps the newest 1 MiB of the output of a job, and gives each
* chunk to `onOutput` as it comes.
* - `close({ reason: 'moved' })` keeps the jobs and the sandbox, so the
* next machine can resume them.
* - `binding()` gives the provider and the id of the sandbox. The session
* log keeps them, so `reattach` of `workspace` can resume the sandbox on
* another machine.
* - The id of a job is its pid in the sandbox and its start time from
* `ps`. `killJob` stops it, also from a backend over the same sandbox on
* another machine, and only when the process with that pid started at
* that time. A sandbox without a `ps -o lstart` cannot check it, so
* `killJob` rejects for a job that runs.
*
* @param handle - The sandbox, from `provider.create()` or `provider.resume()`.
* @param options - `owned: true` when the backend owns the sandbox. Then
* `close` destroys it. Default: `false`.
*
* @example
* ```ts
* const provider = localProcessSandbox()
* workspaceTools({
* workspace: {
* bind: async () => ({
* ...sandboxWorkspaceBackend(await provider.create({}), { owned: true }),
* root: '/workspace',
* }),
* },
* })
* ```
*/
export declare function sandboxWorkspaceBackend(handle: SandboxHandle, { owned }?: {
owned?: boolean;
}): {
close: (input: {
reason: "closed" | "moved";
}) => Promise<void>;
spawn?: ((command: string, options?: {
cwd?: string;
env?: Record<string, string>;
onOutput?: (text: string) => void;
}) => {
wait: () => Promise<{
exitCode: number;
}>;
kill: () => void;
output: () => string;
jobId?: Promise<string | undefined>;
}) | undefined;
shell: "sh";
binding: () => {
kind: "sandbox";
provider: string;
sandboxId: string;
};
killJob: (jobId: string) => Promise<boolean>;
readFile: (path: string, options?: {
signal?: AbortSignal;
} | undefined) => Promise<Uint8Array<ArrayBufferLike>>;
writeFile: (path: string, data: string | Uint8Array<ArrayBufferLike>, options?: {
signal?: AbortSignal;
} | undefined) => Promise<void>;
remove: (path: string, options?: {
signal?: AbortSignal;
} | undefined) => Promise<void>;
readLines: (path: string, options: {
offset: number;
limit: number;
signal?: AbortSignal;
}) => Promise<{
head: Uint8Array;
size: number;
lines: Array<string>;
total?: number;
}>;
stat: (path: string, options?: {
signal?: AbortSignal;
}) => Promise<{
type: "file" | "dir";
size: number;
mtimeMs: number;
} | undefined>;
realpath: (path: string, options?: {
signal?: AbortSignal;
}) => Promise<string | undefined>;
readdir: (path: string, options?: {
signal?: AbortSignal;
} | undefined) => Promise<{
name: string;
type: "link" | "file" | "dir";
}[]>;
exec: (command: string, options?: {
cwd?: string;
env?: Record<string, string>;
timeoutMs?: number;
signal?: AbortSignal;
onStart?: (jobId: string) => void;
} | undefined) => Promise<ExecResult>;
};