ai-sdk-sandbox-sbx 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,54 @@
1
+ import { PROBE } from './sandbox-scripts.js';
2
+ import { SbxNetworkSandboxSession } from './sbx-network-sandbox-session.js';
3
+ import { SbxSandboxNotFoundError } from './sbx-sandbox-not-found-error.js';
4
+ import { assertEnvNames } from './sbx-sandbox-session.js';
5
+ const ENV_LINE = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
6
+ /** Where a credential lives: `ANTHROPIC_API_KEY`, `GH_TOKEN`…, but not `MCP_SENTINEL_TOKEN_NAME`. */
7
+ const CREDENTIAL_NAME = /(?:API_KEY|TOKEN)$/;
8
+ /** A credential Docker Sandboxes leaves for its own proxy to fill in. */
9
+ const isProxyManaged = (variable, value) => CREDENTIAL_NAME.test(variable) &&
10
+ (value === 'proxy-managed' || value.includes('sbxproxymanaged'));
11
+ /** The sandboxes `sbx` knows of, by name and, for cloud ones, by `sbx_*` id too. */
12
+ export async function listSandboxes(cli, abortSignal) {
13
+ const listed = JSON.parse(await cli.check(['ls', '--json'], { abortSignal }));
14
+ const sandboxes = Array.isArray(listed) ? listed : (listed.sandboxes ?? []);
15
+ return new Set(sandboxes.flatMap(({ name, id }) => [name, id].filter((key) => key !== undefined)));
16
+ }
17
+ /** Throws {@link SbxSandboxNotFoundError} unless a sandbox named `name` exists. */
18
+ export async function assertSandboxExists(cli, name, abortSignal) {
19
+ if (!(await listSandboxes(cli, abortSignal)).has(name))
20
+ throw new SbxSandboxNotFoundError(name);
21
+ }
22
+ /**
23
+ * Runs each of `setup` in the sandbox `name`, in order, stopping at the first failure. They run as
24
+ * the sandbox user: `sbx --cloud exec` refuses `--user`, and the kits give that user `sudo`.
25
+ */
26
+ export async function runSetup(cli, name, { setup, abortSignal }) {
27
+ for (const command of setup) {
28
+ await cli.check(['exec', name, 'sh', '-c', command], { abortSignal });
29
+ }
30
+ }
31
+ /**
32
+ * A session on the existing sandbox `name`. Its working directory and its environment are read from
33
+ * the live sandbox: they belong to its image, not to this package.
34
+ */
35
+ export async function openSandbox(cli, name, settings) {
36
+ const { clearEnv = [], keepProxyManagedEnv = false } = settings;
37
+ assertEnvNames(clearEnv);
38
+ const [workingDirectory = '', ...env] = (await cli.check(['exec', name, 'sh', '-c', PROBE], { abortSignal: settings.abortSignal })).split('\n');
39
+ const proxyManaged = keepProxyManagedEnv
40
+ ? []
41
+ : env.flatMap((line) => {
42
+ const [, variable, value] = ENV_LINE.exec(line) ?? [];
43
+ return variable !== undefined && value !== undefined && isProxyManaged(variable, value)
44
+ ? [variable]
45
+ : [];
46
+ });
47
+ return new SbxNetworkSandboxSession({
48
+ cli,
49
+ name,
50
+ workingDirectory: workingDirectory.trim(),
51
+ processes: new Set(),
52
+ clearEnv: [...new Set([...proxyManaged, ...clearEnv])],
53
+ }, settings.ports ?? [], settings.brokerCredentials ?? true);
54
+ }
@@ -0,0 +1,30 @@
1
+ import type { SbxCli } from './sbx-cli.js';
2
+ /** A sandbox port made reachable from this process. */
3
+ export interface PublishedPort {
4
+ /** Where it is reached: `http://127.0.0.1:<port>` locally, a public `https://` URL in the cloud. */
5
+ readonly url: string;
6
+ /** What `sbx ports --unpublish` takes to withdraw it. */
7
+ readonly binding: string;
8
+ }
9
+ /** How a sandbox port becomes reachable: on this host's loopback, or through the cloud. */
10
+ export interface PortPublisher {
11
+ publish(port: number): Promise<PublishedPort>;
12
+ unpublish(published: PublishedPort): Promise<void>;
13
+ }
14
+ /**
15
+ * Local sandboxes: each port is published on this host's loopback only, on a free port picked here
16
+ * rather than by `sbx`, since an ephemeral binding comes back under another port once unpublished.
17
+ */
18
+ export declare function loopbackPorts(cli: SbxCli, sandbox: string): PortPublisher;
19
+ /**
20
+ * Cloud sandboxes: the control plane exposes the port and assigns it a public URL, read back from
21
+ * the port listing, or from what publishing printed when the listing does not carry it.
22
+ */
23
+ export declare function cloudPorts(cli: SbxCli, sandbox: string): PortPublisher;
24
+ /** The URL to reach `base` with `protocol`, keeping it secure when the base is. */
25
+ export declare function endpointUrl(base: string, protocol: 'http' | 'https' | 'ws'): string;
26
+ /**
27
+ * The URL of `port` in a port listing, whatever its exact shape: the first object that names the
28
+ * port in a `*port*` field and carries an `http(s)` URL.
29
+ */
30
+ export declare function findPortUrl(listing: unknown, port: number): string | undefined;
@@ -0,0 +1,75 @@
1
+ import { freeLoopbackPort } from './free-loopback-port.js';
2
+ const URL_PATTERN = /^https?:\/\//;
3
+ /**
4
+ * Local sandboxes: each port is published on this host's loopback only, on a free port picked here
5
+ * rather than by `sbx`, since an ephemeral binding comes back under another port once unpublished.
6
+ */
7
+ export function loopbackPorts(cli, sandbox) {
8
+ return {
9
+ publish: async (port) => {
10
+ const hostPort = await freeLoopbackPort();
11
+ const binding = `127.0.0.1:${hostPort}:${port}`;
12
+ await cli.check(['ports', sandbox, '--publish', binding]);
13
+ return { url: `http://127.0.0.1:${hostPort}`, binding };
14
+ },
15
+ unpublish: async ({ binding }) => {
16
+ await cli.run(['ports', sandbox, '--unpublish', binding]);
17
+ },
18
+ };
19
+ }
20
+ /**
21
+ * Cloud sandboxes: the control plane exposes the port and assigns it a public URL, read back from
22
+ * the port listing, or from what publishing printed when the listing does not carry it.
23
+ */
24
+ export function cloudPorts(cli, sandbox) {
25
+ return {
26
+ publish: async (port) => {
27
+ const printed = await cli.check(['ports', sandbox, '--publish', String(port)]);
28
+ const listed = await cli.check(['ports', sandbox, '--json']);
29
+ const url = findPortUrl(parseJson(listed), port) ?? printed.match(/https?:\/\/\S+/)?.[0];
30
+ if (url === undefined) {
31
+ throw new Error(`sbx exposed port ${port} of the cloud sandbox "${sandbox}" without saying at which URL.`);
32
+ }
33
+ return { url: url.replace(/\/$/, ''), binding: String(port) };
34
+ },
35
+ unpublish: async ({ binding }) => {
36
+ await cli.run(['ports', sandbox, '--unpublish', binding]);
37
+ },
38
+ };
39
+ }
40
+ /** The URL to reach `base` with `protocol`, keeping it secure when the base is. */
41
+ export function endpointUrl(base, protocol) {
42
+ const secure = base.startsWith('https:');
43
+ const scheme = protocol === 'ws' ? (secure ? 'wss' : 'ws') : secure ? 'https' : 'http';
44
+ return base.replace(/^https?/, scheme);
45
+ }
46
+ function parseJson(text) {
47
+ try {
48
+ return JSON.parse(text);
49
+ }
50
+ catch {
51
+ return undefined;
52
+ }
53
+ }
54
+ /**
55
+ * The URL of `port` in a port listing, whatever its exact shape: the first object that names the
56
+ * port in a `*port*` field and carries an `http(s)` URL.
57
+ */
58
+ export function findPortUrl(listing, port) {
59
+ if (Array.isArray(listing)) {
60
+ for (const item of listing) {
61
+ const url = findPortUrl(item, port);
62
+ if (url !== undefined)
63
+ return url;
64
+ }
65
+ return undefined;
66
+ }
67
+ if (typeof listing !== 'object' || listing === null)
68
+ return undefined;
69
+ const entries = Object.entries(listing);
70
+ const namesPort = entries.some(([key, value]) => /port/i.test(key) && Number(value) === port);
71
+ const url = entries.find(([, value]) => typeof value === 'string' && URL_PATTERN.test(value));
72
+ if (namesPort && url)
73
+ return url[1];
74
+ return findPortUrl(entries.map(([, value]) => value).filter((value) => typeof value === 'object'), port);
75
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The shell snippets run inside the sandbox. Each takes its inputs as positional arguments
3
+ * (`sh -c SCRIPT $0 $1 …`), never spliced into the script, so no path or command is ever parsed
4
+ * as shell code it was not meant to be.
5
+ */
6
+ /** Where a spawned process leaves its pid, so it can be stopped from outside. */
7
+ export declare const PROCESS_DIR = "/tmp/.ai-sdk-sbx-processes";
8
+ /**
9
+ * Runs `$1` under a shell whose pid is recorded in `$0` first. Killing the `sbx exec` client does
10
+ * not reach the process inside the sandbox, so this pid is the only handle on it.
11
+ */
12
+ export declare const TRACKED = "mkdir -p /tmp/.ai-sdk-sbx-processes && echo $$ > \"$0\" && sh -c \"$1\"; code=$?; rm -f \"$0\"; exit $code";
13
+ /** Stops the process recorded in `$0` and everything it started, children first. */
14
+ export declare const KILL_TREE: string;
15
+ /**
16
+ * Stops every process recorded in {@link PROCESS_DIR}, whichever session started it, sparing the
17
+ * shells running this very script, which `sbx exec` started like any other.
18
+ */
19
+ export declare const KILL_ALL: string;
20
+ /** Exit status of {@link READ} for a path that does not exist. */
21
+ export declare const READ_MISSING = 44;
22
+ /** Exit status of {@link READ} for a path that is a directory. */
23
+ export declare const READ_DIRECTORY = 45;
24
+ /** `cat` the file `$0`, with exit statuses of its own for a missing path and for a directory. */
25
+ export declare const READ = "[ -d \"$0\" ] && exit 45; [ -e \"$0\" ] || exit 44; exec cat -- \"$0\"";
26
+ /** Writes stdin to the file `$0`, creating its parent directories. */
27
+ export declare const WRITE = "mkdir -p \"$(dirname \"$0\")\" && cat > \"$0\"";
28
+ /** The sandbox's working directory on the first line, then its environment. */
29
+ export declare const PROBE = "pwd; env";
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The shell snippets run inside the sandbox. Each takes its inputs as positional arguments
3
+ * (`sh -c SCRIPT $0 $1 …`), never spliced into the script, so no path or command is ever parsed
4
+ * as shell code it was not meant to be.
5
+ */
6
+ /** Where a spawned process leaves its pid, so it can be stopped from outside. */
7
+ export const PROCESS_DIR = '/tmp/.ai-sdk-sbx-processes';
8
+ /**
9
+ * Runs `$1` under a shell whose pid is recorded in `$0` first. Killing the `sbx exec` client does
10
+ * not reach the process inside the sandbox, so this pid is the only handle on it.
11
+ */
12
+ export const TRACKED = `mkdir -p ${PROCESS_DIR} && echo $$ > "$0" && sh -c "$1"; code=$?; rm -f "$0"; exit $code`;
13
+ /** Stops the process recorded in `$0` and everything it started, children first. */
14
+ export const KILL_TREE = 'kill_tree() { for child in $(pgrep -P "$1"); do kill_tree "$child"; done; kill -TERM "$1" 2>/dev/null; }; ' +
15
+ 'pid=$(cat "$0" 2>/dev/null) && kill_tree "$pid"; rm -f "$0"';
16
+ /**
17
+ * Stops every process recorded in {@link PROCESS_DIR}, whichever session started it, sparing the
18
+ * shells running this very script, which `sbx exec` started like any other.
19
+ */
20
+ export const KILL_ALL = [
21
+ 'kill_tree() { for child in $(pgrep -P "$1"); do kill_tree "$child"; done; kill -TERM "$1" 2>/dev/null; }',
22
+ 'mine=" $$ "; p=$$',
23
+ 'while [ "$p" -gt 1 ] 2>/dev/null; do p=$(ps -o ppid= -p "$p" | tr -d " "); mine="$mine$p "; done',
24
+ `for file in ${PROCESS_DIR}/*; do`,
25
+ ' [ -f "$file" ] || continue',
26
+ ' pid=$(cat "$file" 2>/dev/null) || continue',
27
+ ' case "$mine" in *" $pid "*) continue ;; esac',
28
+ ' kill_tree "$pid"; rm -f "$file"',
29
+ 'done',
30
+ 'true',
31
+ ].join('\n');
32
+ /** Exit status of {@link READ} for a path that does not exist. */
33
+ export const READ_MISSING = 44;
34
+ /** Exit status of {@link READ} for a path that is a directory. */
35
+ export const READ_DIRECTORY = 45;
36
+ /** `cat` the file `$0`, with exit statuses of its own for a missing path and for a directory. */
37
+ export const READ = `[ -d "$0" ] && exit ${READ_DIRECTORY}; [ -e "$0" ] || exit ${READ_MISSING}; exec cat -- "$0"`;
38
+ /** Writes stdin to the file `$0`, creating its parent directories. */
39
+ export const WRITE = 'mkdir -p "$(dirname "$0")" && cat > "$0"';
40
+ /** The sandbox's working directory on the first line, then its environment. */
41
+ export const PROBE = 'pwd; env';
@@ -0,0 +1,26 @@
1
+ import type { HarnessV1SandboxTemplate } from '@ai-sdk/harness';
2
+ import type { SbxCli } from './sbx-cli.js';
3
+ /** The name templates are saved under: an image repository locally, a template name prefix in the cloud. */
4
+ export declare const TEMPLATE_REPOSITORY = "ai-sdk-harness-template";
5
+ /** What a template is built from: the harness's recipe, and the base it is laid on. */
6
+ export interface TemplateSource {
7
+ template: HarnessV1SandboxTemplate;
8
+ agent: string;
9
+ image?: string;
10
+ setup: readonly string[];
11
+ }
12
+ /**
13
+ * The image of a sandbox prepared by `source.template`, built the first time and reused afterwards:
14
+ * one sandbox is created, set up, prepared and saved with `sbx template save`, then removed. Every
15
+ * later sandbox starts from that image, with the harness already installed. A local template lives
16
+ * in this host's image store, a cloud one in the cloud registry, where saving takes minutes.
17
+ *
18
+ * The reference derives from the template's identity and from the base it is laid on, so a new
19
+ * harness version or another kit gets an image of its own.
20
+ */
21
+ export declare function ensureTemplateImage(cli: SbxCli, source: TemplateSource, abortSignal?: AbortSignal): Promise<string>;
22
+ /**
23
+ * The template's reference: `docker.io/library/ai-sdk-harness-template:<digest>` locally, the name
24
+ * `ai-sdk-harness-template-<digest>` in the cloud.
25
+ */
26
+ export declare function templateReference({ template, agent, image, setup }: TemplateSource, cloud?: boolean): string;
@@ -0,0 +1,74 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { openSandbox, runSetup } from './open-sandbox.js';
3
+ /** The name templates are saved under: an image repository locally, a template name prefix in the cloud. */
4
+ export const TEMPLATE_REPOSITORY = 'ai-sdk-harness-template';
5
+ /** Where `sbx template save` puts a local template: the sandbox runtime's own image store. */
6
+ const LOCAL_STORE = 'docker.io/library/';
7
+ /** Images being built by this process, by reference: one build per reference at a time. */
8
+ const building = new Map();
9
+ /**
10
+ * The image of a sandbox prepared by `source.template`, built the first time and reused afterwards:
11
+ * one sandbox is created, set up, prepared and saved with `sbx template save`, then removed. Every
12
+ * later sandbox starts from that image, with the harness already installed. A local template lives
13
+ * in this host's image store, a cloud one in the cloud registry, where saving takes minutes.
14
+ *
15
+ * The reference derives from the template's identity and from the base it is laid on, so a new
16
+ * harness version or another kit gets an image of its own.
17
+ */
18
+ export async function ensureTemplateImage(cli, source, abortSignal) {
19
+ const reference = templateReference(source, cli.cloud);
20
+ let image = building.get(reference);
21
+ if (image === undefined) {
22
+ image = imageExists(cli, reference, abortSignal).then((exists) => exists ? reference : buildImage({ cli, source, reference, abortSignal }));
23
+ building.set(reference, image);
24
+ // A failed build is not remembered: the next sandbox tries again.
25
+ image.catch(() => building.delete(reference));
26
+ }
27
+ return image;
28
+ }
29
+ /**
30
+ * The template's reference: `docker.io/library/ai-sdk-harness-template:<digest>` locally, the name
31
+ * `ai-sdk-harness-template-<digest>` in the cloud.
32
+ */
33
+ export function templateReference({ template, agent, image, setup }, cloud = false) {
34
+ const digest = createHash('sha256')
35
+ .update(JSON.stringify([1, template.identity, agent, image ?? null, setup]))
36
+ .digest('hex')
37
+ .slice(0, 24);
38
+ return cloud
39
+ ? `${TEMPLATE_REPOSITORY}-${digest}`
40
+ : `${LOCAL_STORE}${TEMPLATE_REPOSITORY}:${digest}`;
41
+ }
42
+ /** Whether `needle` is one of the strings anywhere in `value`. */
43
+ function containsString(value, needle) {
44
+ if (typeof value === 'string')
45
+ return value === needle;
46
+ if (typeof value !== 'object' || value === null)
47
+ return false;
48
+ return Object.values(value).some((item) => containsString(item, needle));
49
+ }
50
+ async function imageExists(cli, reference, abortSignal) {
51
+ const listed = JSON.parse(await cli.check(['template', 'ls', '--json'], { abortSignal }));
52
+ if (cli.cloud)
53
+ return containsString(listed, reference);
54
+ return (listed.images ?? []).some(({ repository, tag }) => `${repository}:${tag}` === reference);
55
+ }
56
+ async function buildImage({ cli, source: { template, agent, image, setup }, reference, abortSignal, }) {
57
+ const builder = `${TEMPLATE_REPOSITORY}-${reference.slice(-12)}-${randomBytes(3).toString('hex')}`;
58
+ await cli.check(['create', '--name', builder, '--quiet', ...(image ? ['--template', image] : []), agent], { abortSignal });
59
+ try {
60
+ await runSetup(cli, builder, { setup, abortSignal });
61
+ const session = await openSandbox(cli, builder, { abortSignal });
62
+ await template.prepare({ session: session.restricted(), abortSignal });
63
+ // A local `sbx template save` refuses a running sandbox; a cloud one snapshots it running.
64
+ if (!cli.cloud)
65
+ await cli.check(['stop', builder], { abortSignal });
66
+ await cli.check(['template', 'save', builder, reference.replace(LOCAL_STORE, '')], {
67
+ abortSignal,
68
+ });
69
+ return reference;
70
+ }
71
+ finally {
72
+ await cli.run(['rm', '--force', builder]);
73
+ }
74
+ }
@@ -0,0 +1,42 @@
1
+ import type { ChildProcessWithoutNullStreams } from 'node:child_process';
2
+ /** What a finished `sbx` invocation left behind. */
3
+ export interface SbxResult {
4
+ exitCode: number;
5
+ /** Kept as bytes: `sbx exec … cat` hands back a file, which need not be text. */
6
+ stdout: Buffer;
7
+ stderr: string;
8
+ }
9
+ export interface SbxCallOptions {
10
+ /** Written to the command's stdin, which is then closed. */
11
+ stdin?: ReadableStream<Uint8Array> | Uint8Array;
12
+ /**
13
+ * Variables the `sbx` client itself sees, on top of this process's environment. `sbx exec -e NAME`
14
+ * without a value forwards the client's own `NAME` into the sandbox, so a value never has to
15
+ * appear on a command line.
16
+ */
17
+ env?: Record<string, string>;
18
+ abortSignal?: AbortSignal;
19
+ }
20
+ /** Why `signal` was aborted, as an error to reject with. */
21
+ export declare const abortReason: (signal: AbortSignal) => Error;
22
+ /**
23
+ * The `sbx` command line of Docker Sandboxes, run as a child process: the only way this package
24
+ * reaches a sandbox. Every argument is passed as-is, never through a host shell.
25
+ */
26
+ export declare class SbxCli {
27
+ readonly binary: string;
28
+ readonly cloud: boolean;
29
+ /**
30
+ * @param binary The `sbx` binary.
31
+ * @param cloud Address Docker Sandboxes Cloud rather than this host's sandboxes: every command
32
+ * goes out as `sbx --cloud …`.
33
+ */
34
+ constructor(binary?: string, cloud?: boolean);
35
+ /** Starts `sbx` and hands back the running process, streams untouched. */
36
+ start(args: readonly string[], options?: SbxCallOptions): ChildProcessWithoutNullStreams;
37
+ /** Runs `sbx` to completion, whatever its exit code. */
38
+ run(args: readonly string[], options?: SbxCallOptions): Promise<SbxResult>;
39
+ /** Runs `sbx` and returns its standard output as text, throwing {@link SbxError} on failure. */
40
+ check(args: readonly string[], options?: SbxCallOptions): Promise<string>;
41
+ private notFound;
42
+ }
@@ -0,0 +1,77 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { constants } from 'node:os';
3
+ import { Readable } from 'node:stream';
4
+ import { SbxError } from './sbx-error.js';
5
+ /** Why `signal` was aborted, as an error to reject with. */
6
+ export const abortReason = (signal) => {
7
+ const reason = signal.reason;
8
+ return reason instanceof Error ? reason : new DOMException('Aborted', 'AbortError');
9
+ };
10
+ const exitCodeOf = (code, signal) => code ?? (signal === null ? 0 : 128 + (constants.signals[signal] ?? 0));
11
+ /**
12
+ * The `sbx` command line of Docker Sandboxes, run as a child process: the only way this package
13
+ * reaches a sandbox. Every argument is passed as-is, never through a host shell.
14
+ */
15
+ export class SbxCli {
16
+ binary;
17
+ cloud;
18
+ /**
19
+ * @param binary The `sbx` binary.
20
+ * @param cloud Address Docker Sandboxes Cloud rather than this host's sandboxes: every command
21
+ * goes out as `sbx --cloud …`.
22
+ */
23
+ constructor(binary = 'sbx', cloud = false) {
24
+ this.binary = binary;
25
+ this.cloud = cloud;
26
+ }
27
+ /** Starts `sbx` and hands back the running process, streams untouched. */
28
+ start(args, options = {}) {
29
+ const child = spawn(this.binary, this.cloud ? ['--cloud', ...args] : args, {
30
+ env: { ...process.env, ...options.env },
31
+ signal: options.abortSignal,
32
+ stdio: 'pipe',
33
+ });
34
+ // A command that exits before reading its stdin must not crash this process on EPIPE.
35
+ child.stdin.on('error', () => undefined);
36
+ const { stdin } = options;
37
+ if (stdin === undefined) {
38
+ child.stdin.end();
39
+ }
40
+ else if (stdin instanceof Uint8Array) {
41
+ child.stdin.end(stdin);
42
+ }
43
+ else {
44
+ Readable.fromWeb(stdin).pipe(child.stdin);
45
+ }
46
+ return child;
47
+ }
48
+ /** Runs `sbx` to completion, whatever its exit code. */
49
+ run(args, options = {}) {
50
+ if (options.abortSignal?.aborted)
51
+ return Promise.reject(abortReason(options.abortSignal));
52
+ const child = this.start(args, options);
53
+ const stdout = [];
54
+ const stderr = [];
55
+ child.stdout.on('data', (chunk) => stdout.push(chunk));
56
+ child.stderr.on('data', (chunk) => stderr.push(chunk));
57
+ return new Promise((resolve, reject) => {
58
+ child.once('error', (error) => reject(error.code === 'ENOENT' ? this.notFound() : error));
59
+ child.once('close', (code, signal) => resolve({
60
+ exitCode: exitCodeOf(code, signal),
61
+ stdout: Buffer.concat(stdout),
62
+ stderr: Buffer.concat(stderr).toString(),
63
+ }));
64
+ });
65
+ }
66
+ /** Runs `sbx` and returns its standard output as text, throwing {@link SbxError} on failure. */
67
+ async check(args, options = {}) {
68
+ const result = await this.run(args, options);
69
+ if (result.exitCode !== 0)
70
+ throw new SbxError(args, result);
71
+ return result.stdout.toString();
72
+ }
73
+ notFound() {
74
+ return new Error(`The Docker Sandboxes CLI \`${this.binary}\` was not found. Install it ` +
75
+ '(https://docs.docker.com/ai/sandboxes/get-started/) or point the `binary` option at it.');
76
+ }
77
+ }
@@ -0,0 +1,11 @@
1
+ /** An `sbx` command exited with a non-zero status. */
2
+ export declare class SbxError extends Error {
3
+ /** The `sbx` sub-command and its arguments, as they were passed. */
4
+ readonly args: readonly string[];
5
+ readonly exitCode: number;
6
+ readonly stderr: string;
7
+ constructor(args: readonly string[], result: {
8
+ exitCode: number;
9
+ stderr: string;
10
+ });
11
+ }
@@ -0,0 +1,14 @@
1
+ /** An `sbx` command exited with a non-zero status. */
2
+ export class SbxError extends Error {
3
+ /** The `sbx` sub-command and its arguments, as they were passed. */
4
+ args;
5
+ exitCode;
6
+ stderr;
7
+ constructor(args, result) {
8
+ super(`\`sbx ${args[0] ?? ''}\` exited with ${result.exitCode}: ${result.stderr.trim()}`);
9
+ this.name = 'SbxError';
10
+ this.args = args;
11
+ this.exitCode = result.exitCode;
12
+ this.stderr = result.stderr;
13
+ }
14
+ }
@@ -0,0 +1,74 @@
1
+ import type { HarnessV1NetworkSandboxSession, HarnessV1PortEndpoint, HarnessV1RequestTransformation } from '@ai-sdk/harness';
2
+ import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
3
+ import type { SbxSandboxHandle } from './sbx-sandbox-session.js';
4
+ import { SbxSandboxSession } from './sbx-sandbox-session.js';
5
+ export { SBX_SANDBOX_PROVIDER_ID } from './credential-broker.js';
6
+ type Protocol = 'http' | 'https' | 'ws';
7
+ /**
8
+ * The whole of a Docker Sandbox, as `HarnessAgent.createSession({ sandboxSession })` expects it:
9
+ * files and processes, plus the ports a harness bridge listens on and the credentials the sandbox
10
+ * must never see.
11
+ *
12
+ * - **Ports** are published the first time the harness asks for them: on this host's loopback for
13
+ * a local sandbox, at the public URL the control plane assigns for a cloud one.
14
+ * - **Credentials** stay out of the sandbox. Each request transformation the harness asks for
15
+ * becomes a custom secret of the Docker Sandboxes proxy, scoped to this sandbox: the value only
16
+ * exists on the host and in the proxy.
17
+ *
18
+ * Obtain one from `createSbxNetworkSandboxSession()` or `resumeSbxNetworkSandboxSession()`.
19
+ */
20
+ export declare class SbxNetworkSandboxSession extends SbxSandboxSession implements HarnessV1NetworkSandboxSession {
21
+ readonly id: string;
22
+ readonly defaultWorkingDirectory: string;
23
+ /** Absent when brokering is off: the harness then forwards the real credential instead. */
24
+ readonly addRequestTransformations?: (transformations: ReadonlyArray<HarnessV1RequestTransformation>) => Promise<void>;
25
+ /** Absent when brokering is off, like {@link addRequestTransformations}. */
26
+ readonly setRequestTransformations?: (transformations: ReadonlyArray<HarnessV1RequestTransformation>) => Promise<void>;
27
+ /** Whether the sandbox runs in Docker Sandboxes Cloud rather than on this host. */
28
+ readonly cloud: boolean;
29
+ private exposed;
30
+ private readonly broker;
31
+ private readonly publisher;
32
+ /** Sandbox port → where it is published. */
33
+ private readonly published;
34
+ constructor(handle: SbxSandboxHandle, ports: readonly number[], brokerCredentials: boolean);
35
+ /** Ports the sandbox exposes, resolvable with {@link getPortEndpoint}. */
36
+ get ports(): readonly number[];
37
+ restricted: () => SandboxSession;
38
+ getPortEndpoint: ({ port, protocol, }: {
39
+ port: number;
40
+ protocol?: Protocol;
41
+ }) => Promise<HarnessV1PortEndpoint>;
42
+ /** @deprecated Use {@link getPortEndpoint}. */
43
+ getPortUrl: (options: {
44
+ port: number;
45
+ protocol?: Protocol;
46
+ }) => Promise<string>;
47
+ /** Replaces the exposed ports; a port left out is unpublished. */
48
+ setPorts: (ports: ReadonlyArray<number>) => Promise<void>;
49
+ /**
50
+ * Hands the sandbox back without stopping it: the processes this session started are stopped,
51
+ * its ports unpublished and its secrets withdrawn from the proxy. What an application that
52
+ * keeps one sandbox across harness sessions calls between them.
53
+ */
54
+ release: () => Promise<void>;
55
+ /**
56
+ * Stops every process any session started in this sandbox, including those a previous run of the
57
+ * application left behind, such as a harness bridge still holding its port. Call it right after
58
+ * resuming a sandbox no other process uses.
59
+ */
60
+ killAllProcesses: () => Promise<void>;
61
+ /**
62
+ * Extends the time-to-live of a cloud sandbox by `duration` (`30m`, `2h`), within the 24 hours
63
+ * after its creation the platform allows. Local sandboxes have no time-to-live.
64
+ */
65
+ extendTtl: (duration: string) => Promise<void>;
66
+ /**
67
+ * Stops the sandbox, keeping it for the next `sbx exec`: a local microVM keeps its filesystem, a
68
+ * cloud one is suspended with its memory. Idempotent.
69
+ */
70
+ stop: () => Promise<void>;
71
+ /** Removes the sandbox, with its filesystem and the secrets scoped to it. Idempotent. */
72
+ destroy: () => Promise<void>;
73
+ private unpublish;
74
+ }