@schemavaults/sandbox-management-sdk 0.0.0-stage → 0.1.1

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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -2
  3. package/dist/commands.d.ts +16 -0
  4. package/dist/commands.js +99 -0
  5. package/dist/dbh/index.d.ts +4 -0
  6. package/dist/dbh/index.js +2 -0
  7. package/dist/dbh/migration.d.ts +18 -0
  8. package/dist/dbh/migration.js +41 -0
  9. package/dist/dbh/store.d.ts +42 -0
  10. package/dist/dbh/store.js +105 -0
  11. package/dist/docker/egress-proxy/config.d.ts +20 -0
  12. package/dist/docker/egress-proxy/config.js +29 -0
  13. package/dist/docker/egress-proxy/main.d.ts +1 -0
  14. package/dist/docker/egress-proxy/main.js +19 -0
  15. package/dist/docker/egress-proxy/policy.d.ts +49 -0
  16. package/dist/docker/egress-proxy/policy.js +89 -0
  17. package/dist/docker/egress-proxy/server.d.ts +25 -0
  18. package/dist/docker/egress-proxy/server.js +225 -0
  19. package/dist/docker/egress.d.ts +41 -0
  20. package/dist/docker/egress.js +67 -0
  21. package/dist/docker/index.d.ts +3 -0
  22. package/dist/docker/index.js +2 -0
  23. package/dist/docker/provider.d.ts +101 -0
  24. package/dist/docker/provider.js +373 -0
  25. package/dist/docker/tar.d.ts +20 -0
  26. package/dist/docker/tar.js +124 -0
  27. package/dist/errors.d.ts +40 -0
  28. package/dist/errors.js +67 -0
  29. package/dist/in-memory-store.d.ts +11 -0
  30. package/dist/in-memory-store.js +40 -0
  31. package/dist/index.d.ts +12 -0
  32. package/dist/index.js +8 -0
  33. package/dist/internal/base64.d.ts +7 -0
  34. package/dist/internal/base64.js +20 -0
  35. package/dist/internal/output.d.ts +6 -0
  36. package/dist/internal/output.js +25 -0
  37. package/dist/internal/posix-path.d.ts +8 -0
  38. package/dist/internal/posix-path.js +38 -0
  39. package/dist/manager.d.ts +126 -0
  40. package/dist/manager.js +477 -0
  41. package/dist/network.d.ts +27 -0
  42. package/dist/network.js +128 -0
  43. package/dist/provider.d.ts +77 -0
  44. package/dist/provider.js +1 -0
  45. package/dist/shell-main-process.d.ts +27 -0
  46. package/dist/shell-main-process.js +94 -0
  47. package/dist/store.d.ts +36 -0
  48. package/dist/store.js +19 -0
  49. package/dist/testing/index.d.ts +3 -0
  50. package/dist/testing/index.js +2 -0
  51. package/dist/testing/mock-provider.d.ts +99 -0
  52. package/dist/testing/mock-provider.js +181 -0
  53. package/dist/types.d.ts +158 -0
  54. package/dist/types.js +21 -0
  55. package/dist/vercel/index.d.ts +2 -0
  56. package/dist/vercel/index.js +1 -0
  57. package/dist/vercel/provider.d.ts +125 -0
  58. package/dist/vercel/provider.js +190 -0
  59. package/dist/web-storage/index.d.ts +2 -0
  60. package/dist/web-storage/index.js +1 -0
  61. package/dist/web-storage/store.d.ts +53 -0
  62. package/dist/web-storage/store.js +118 -0
  63. package/package.json +117 -4
@@ -0,0 +1,77 @@
1
+ import type { ExecOptions, ExecResult, MainProcessInfo, NormalizedCommand, SandboxNetworkPolicyKind, SandboxSpec } from "./types.js";
2
+ /** Identifies a sandbox on its provider. Passed to every provider operation. */
3
+ export interface ProviderSandbox {
4
+ /** SDK sandbox ID. */
5
+ id: string;
6
+ /** The provider's own ID, as returned from `create`. */
7
+ providerRef: string;
8
+ spec: SandboxSpec;
9
+ /** Resolved working directory (`spec.workdir` or the provider default). */
10
+ workdir: string;
11
+ }
12
+ export interface ProviderCreateInput {
13
+ id: string;
14
+ spec: SandboxSpec;
15
+ workdir: string;
16
+ ownerId: string | null;
17
+ labels: Record<string, string>;
18
+ }
19
+ export interface ProviderCreateResult {
20
+ providerRef: string;
21
+ }
22
+ /** What the provider reports about a sandbox. */
23
+ export type ProviderSandboxStatus = "running" | "stopped" | "missing";
24
+ /** A file written by `writeFiles`, with its path resolved to absolute. */
25
+ export interface ResolvedSandboxFile {
26
+ path: string;
27
+ content: Uint8Array;
28
+ mode: number;
29
+ }
30
+ export interface MainLogChunk {
31
+ data: Uint8Array;
32
+ /** Byte offset to pass on the next read. */
33
+ nextOffset: number;
34
+ }
35
+ /**
36
+ * Adapter to a sandbox backend. Providers are stateless with respect to the
37
+ * store: the `SandboxManager` owns status bookkeeping.
38
+ *
39
+ * Operations other than `create`, `wake`, `stop`, `destroy` and `status` must
40
+ * throw `ProviderSandboxStoppedError` if the sandbox is not running, and
41
+ * `ProviderSandboxMissingError` if it no longer exists, so the manager can
42
+ * auto-resume or report it.
43
+ */
44
+ export interface SandboxProvider {
45
+ readonly name: string;
46
+ /** Working directory used when `spec.workdir` is not set. */
47
+ readonly defaultWorkdir: string;
48
+ /**
49
+ * The restrictive `spec.network` policies this provider enforces. The
50
+ * manager refuses to create a sandbox whose policy is not listed, so a
51
+ * provider that leaves this out never silently ignores one. An unset or
52
+ * `"allow-all"` policy is always accepted.
53
+ */
54
+ readonly networkPolicies?: readonly Exclude<SandboxNetworkPolicyKind, "allow-all">[];
55
+ /** Create the sandbox and leave it running with `workdir` present. */
56
+ create(input: ProviderCreateInput): Promise<ProviderCreateResult>;
57
+ /** Start a stopped sandbox. Must be a no-op if it is already running. */
58
+ wake(sandbox: ProviderSandbox): Promise<void>;
59
+ /** Stop the sandbox, keeping its filesystem. Must be a no-op if already stopped. */
60
+ stop(sandbox: ProviderSandbox): Promise<void>;
61
+ /** Delete the sandbox. Must be a no-op if it no longer exists. */
62
+ destroy(sandbox: ProviderSandbox): Promise<void>;
63
+ status(sandbox: ProviderSandbox): Promise<ProviderSandboxStatus>;
64
+ exec(sandbox: ProviderSandbox, command: NormalizedCommand, options?: ExecOptions): Promise<ExecResult>;
65
+ /** Launch `command` detached as the sandbox's main process, logging its output. */
66
+ startMainProcess(sandbox: ProviderSandbox, command: NormalizedCommand): Promise<void>;
67
+ getMainProcess(sandbox: ProviderSandbox): Promise<MainProcessInfo | null>;
68
+ /** Read the main process log from byte `offset`; an empty chunk means no new output. */
69
+ readMainLog(sandbox: ProviderSandbox, offset: number): Promise<MainLogChunk>;
70
+ writeFiles(sandbox: ProviderSandbox, files: ResolvedSandboxFile[]): Promise<void>;
71
+ /** Resolves `null` if the file does not exist. */
72
+ readFile(sandbox: ProviderSandbox, path: string): Promise<Uint8Array | null>;
73
+ /** A URL that reaches `port` inside the sandbox. The port must be in `spec.ports`. */
74
+ getUrl(sandbox: ProviderSandbox, port: number): Promise<string>;
75
+ /** Extend the provider-side timeout, if the provider enforces one. */
76
+ extendTimeout?(sandbox: ProviderSandbox, durationMs: number): Promise<void>;
77
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import type { MainLogChunk } from "./provider.js";
2
+ import type { ExecResult, MainProcessInfo, NormalizedCommand } from "./types.js";
3
+ /**
4
+ * Main-process bookkeeping for providers that only offer "run a command":
5
+ * the launcher records its PID, appends output to a log file and records the
6
+ * exit code, all under this directory inside the sandbox.
7
+ */
8
+ export declare const MAIN_PROCESS_DIR = "/tmp/sandbox-sdk";
9
+ type Exec = (command: NormalizedCommand) => Promise<ExecResult>;
10
+ type SpawnDetached = (command: NormalizedCommand) => Promise<void>;
11
+ /**
12
+ * Launch `command` as the main process: clear the previous run's PID and exit
13
+ * files, spawn the launcher detached, then wait until it has recorded its PID
14
+ * so `getMainProcess` reflects the new process as soon as this resolves.
15
+ */
16
+ export declare function shellStartMainProcess(exec: Exec, spawnDetached: SpawnDetached, command: NormalizedCommand, { timeoutMs, pollIntervalMs }?: {
17
+ timeoutMs?: number;
18
+ pollIntervalMs?: number;
19
+ }): Promise<void>;
20
+ /**
21
+ * A command that runs `command` in the foreground as the main process. The
22
+ * provider must run it detached (it only returns when the process exits).
23
+ */
24
+ export declare function mainProcessLauncher(command: NormalizedCommand): NormalizedCommand;
25
+ export declare function shellGetMainProcess(exec: Exec): Promise<MainProcessInfo | null>;
26
+ export declare function shellReadMainLog(exec: Exec, offset: number, maxBytes?: number): Promise<MainLogChunk>;
27
+ export {};
@@ -0,0 +1,94 @@
1
+ import { toShellCommandLine } from "./commands.js";
2
+ import { SandboxError } from "./errors.js";
3
+ import { decodeBase64 } from "./internal/base64.js";
4
+ /**
5
+ * Main-process bookkeeping for providers that only offer "run a command":
6
+ * the launcher records its PID, appends output to a log file and records the
7
+ * exit code, all under this directory inside the sandbox.
8
+ */
9
+ export const MAIN_PROCESS_DIR = "/tmp/sandbox-sdk";
10
+ const PID_FILE = `${MAIN_PROCESS_DIR}/main.pid`;
11
+ const EXIT_FILE = `${MAIN_PROCESS_DIR}/main.exit`;
12
+ const LOG_FILE = `${MAIN_PROCESS_DIR}/main.log`;
13
+ const DEFAULT_MAX_LOG_READ_BYTES = 1024 * 1024;
14
+ const sh = (script) => ({ cmd: "sh", args: ["-c", script], cwd: "/", env: {} });
15
+ /**
16
+ * Launch `command` as the main process: clear the previous run's PID and exit
17
+ * files, spawn the launcher detached, then wait until it has recorded its PID
18
+ * so `getMainProcess` reflects the new process as soon as this resolves.
19
+ */
20
+ export async function shellStartMainProcess(exec, spawnDetached, command, { timeoutMs = 15_000, pollIntervalMs = 100 } = {}) {
21
+ const reset = await exec(sh(`mkdir -p ${MAIN_PROCESS_DIR} && rm -f ${PID_FILE} ${EXIT_FILE}`));
22
+ if (reset.exitCode !== 0) {
23
+ throw new SandboxError(`Failed to prepare main process: ${reset.stderr.trim()}`);
24
+ }
25
+ await spawnDetached(mainProcessLauncher(command));
26
+ const deadline = Date.now() + timeoutMs;
27
+ while ((await exec(sh(`[ -f ${PID_FILE} ]`))).exitCode !== 0) {
28
+ if (Date.now() >= deadline)
29
+ throw new SandboxError("Main process did not start");
30
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
31
+ }
32
+ }
33
+ /**
34
+ * A command that runs `command` in the foreground as the main process. The
35
+ * provider must run it detached (it only returns when the process exits).
36
+ */
37
+ export function mainProcessLauncher(command) {
38
+ const script = [
39
+ `mkdir -p ${MAIN_PROCESS_DIR}`,
40
+ `rm -f ${EXIT_FILE}`,
41
+ `echo $$ > ${PID_FILE}`,
42
+ `exec >> ${LOG_FILE} 2>&1 < /dev/null`,
43
+ `(${toShellCommandLine(command)})`,
44
+ `echo $? > ${EXIT_FILE}`,
45
+ ].join("\n");
46
+ return sh(script);
47
+ }
48
+ const STATUS_SCRIPT = `[ -f ${PID_FILE} ] || exit 0
49
+ pid=$(cat ${PID_FILE})
50
+ if kill -0 "$pid" 2>/dev/null; then alive=1; else alive=0; fi
51
+ code=$(cat ${EXIT_FILE} 2>/dev/null || true)
52
+ echo "$pid $alive $code"`;
53
+ export async function shellGetMainProcess(exec) {
54
+ const result = await exec(sh(STATUS_SCRIPT));
55
+ if (result.exitCode !== 0) {
56
+ throw new SandboxError(`Failed to read main process status: ${result.stderr.trim()}`);
57
+ }
58
+ const [pidText, aliveText, codeText] = result.stdout.trim().split(/\s+/);
59
+ if (!pidText)
60
+ return null;
61
+ const pid = Number.parseInt(pidText, 10);
62
+ const exitCode = codeText ? Number.parseInt(codeText, 10) : null;
63
+ return {
64
+ pid: Number.isNaN(pid) ? null : pid,
65
+ running: aliveText === "1" && exitCode === null,
66
+ exitCode: exitCode === null || Number.isNaN(exitCode) ? null : exitCode,
67
+ };
68
+ }
69
+ /**
70
+ * Reads the main log from `offset`. The first output line is the effective
71
+ * start offset (reset to 0 if the log was truncated), then base64 data.
72
+ */
73
+ const READ_LOG_SCRIPT = `f=${LOG_FILE}
74
+ [ -f "$f" ] || { echo 0; exit 0; }
75
+ size=$(wc -c < "$f")
76
+ off=$SANDBOX_LOG_OFFSET
77
+ [ "$size" -lt "$off" ] && off=0
78
+ echo "$off"
79
+ tail -c +$((off + 1)) "$f" | head -c $SANDBOX_LOG_MAX_BYTES | base64`;
80
+ export async function shellReadMainLog(exec, offset, maxBytes = DEFAULT_MAX_LOG_READ_BYTES) {
81
+ const result = await exec({
82
+ cmd: "sh",
83
+ args: ["-c", READ_LOG_SCRIPT],
84
+ cwd: "/",
85
+ env: { SANDBOX_LOG_OFFSET: String(offset), SANDBOX_LOG_MAX_BYTES: String(maxBytes) },
86
+ });
87
+ if (result.exitCode !== 0) {
88
+ throw new SandboxError(`Failed to read main process log: ${result.stderr.trim()}`);
89
+ }
90
+ const newline = result.stdout.indexOf("\n");
91
+ const start = Number.parseInt(newline === -1 ? result.stdout : result.stdout.slice(0, newline), 10) || 0;
92
+ const data = newline === -1 ? new Uint8Array() : decodeBase64(result.stdout.slice(newline + 1));
93
+ return { data, nextOffset: start + data.length };
94
+ }
@@ -0,0 +1,36 @@
1
+ import type { SandboxRecord, SandboxStatus } from "./types.js";
2
+ export interface SandboxListFilter {
3
+ ownerId?: string;
4
+ provider?: string;
5
+ status?: SandboxStatus | SandboxStatus[];
6
+ /** Matches records whose labels contain every given key/value pair. */
7
+ labels?: Record<string, string>;
8
+ /** Destroyed sandboxes are excluded unless this is set (or `status` asks for them). */
9
+ includeDestroyed?: boolean;
10
+ limit?: number;
11
+ offset?: number;
12
+ }
13
+ /** Fields the manager may change after insert. `version` and `updatedAt` are managed by the store. */
14
+ export type SandboxRecordPatch = Partial<Pick<SandboxRecord, "status" | "providerRef" | "error" | "expiresAt" | "labels" | "metadata">>;
15
+ /**
16
+ * Persistence for sandbox records. Implementations must make `update` an
17
+ * atomic compare-and-set on `version`, which is what makes concurrent
18
+ * resume/stop calls from different processes safe.
19
+ */
20
+ export interface SandboxStore {
21
+ insert(record: SandboxRecord): Promise<void>;
22
+ get(id: string): Promise<SandboxRecord | null>;
23
+ /** Newest first. */
24
+ list(filter?: SandboxListFilter): Promise<SandboxRecord[]>;
25
+ /**
26
+ * Apply `patch` only if the stored version equals `expectedVersion`; the
27
+ * stored version is then incremented and `updatedAt` set to now.
28
+ * Resolves the updated record, or `null` if the version did not match (or
29
+ * the record does not exist).
30
+ */
31
+ update(id: string, expectedVersion: number, patch: SandboxRecordPatch): Promise<SandboxRecord | null>;
32
+ /** Hard-delete a record. Resolves whether a record was deleted. */
33
+ delete(id: string): Promise<boolean>;
34
+ }
35
+ /** Shared filter semantics, for stores that filter in memory. */
36
+ export declare function matchesFilter(record: SandboxRecord, filter: SandboxListFilter): boolean;
package/dist/store.js ADDED
@@ -0,0 +1,19 @@
1
+ /** Shared filter semantics, for stores that filter in memory. */
2
+ export function matchesFilter(record, filter) {
3
+ if (filter.ownerId !== undefined && record.ownerId !== filter.ownerId)
4
+ return false;
5
+ if (filter.provider !== undefined && record.provider !== filter.provider)
6
+ return false;
7
+ const statuses = filter.status === undefined ? undefined : [].concat(filter.status);
8
+ if (statuses && !statuses.includes(record.status))
9
+ return false;
10
+ if (!statuses && !filter.includeDestroyed && record.status === "destroyed")
11
+ return false;
12
+ if (filter.labels) {
13
+ for (const [key, value] of Object.entries(filter.labels)) {
14
+ if (record.labels[key] !== value)
15
+ return false;
16
+ }
17
+ }
18
+ return true;
19
+ }
@@ -0,0 +1,3 @@
1
+ export { MockSandboxProvider } from "./mock-provider.js";
2
+ export type { MockCall, MockExecContext, MockExecHandler, MockMainProcess, MockOperation, MockSandboxProviderOptions, MockSandboxState, } from "./mock-provider.js";
3
+ export { InMemorySandboxStore } from "../in-memory-store.js";
@@ -0,0 +1,2 @@
1
+ export { MockSandboxProvider } from "./mock-provider.js";
2
+ export { InMemorySandboxStore } from "../in-memory-store.js";
@@ -0,0 +1,99 @@
1
+ import type { MainLogChunk, ProviderCreateInput, ProviderCreateResult, ProviderSandbox, ProviderSandboxStatus, ResolvedSandboxFile, SandboxProvider } from "../provider.js";
2
+ import type { ExecOptions, ExecResult, MainProcessInfo, NormalizedCommand, SandboxNetworkPolicyKind, SandboxSpec } from "../types.js";
3
+ export type MockOperation = "create" | "wake" | "stop" | "destroy" | "exec" | "startMainProcess" | "writeFiles" | "readFile" | "getUrl" | "extendTimeout";
4
+ export interface MockCall {
5
+ operation: MockOperation;
6
+ sandboxId: string;
7
+ command?: NormalizedCommand;
8
+ }
9
+ export interface MockExecContext {
10
+ sandboxId: string;
11
+ command: NormalizedCommand;
12
+ /** `spec.env` merged with the command's env. */
13
+ env: Record<string, string>;
14
+ }
15
+ /** Answers `exec` calls. Returning nothing means exit code 0 with no output. */
16
+ export type MockExecHandler = (context: MockExecContext) => Partial<ExecResult> | void | Promise<Partial<ExecResult> | void>;
17
+ export interface MockMainProcess {
18
+ command: NormalizedCommand;
19
+ pid: number;
20
+ running: boolean;
21
+ exitCode: number | null;
22
+ }
23
+ export interface MockSandboxState {
24
+ sandboxId: string;
25
+ providerRef: string;
26
+ spec: SandboxSpec;
27
+ workdir: string;
28
+ running: boolean;
29
+ files: Map<string, Uint8Array>;
30
+ mainProcess: MockMainProcess | null;
31
+ /** Main process output, appended across restarts like a real log file. */
32
+ log: Uint8Array;
33
+ timeoutExtensionsMs: number[];
34
+ }
35
+ export interface MockSandboxProviderOptions {
36
+ /** Default `"mock"`. */
37
+ name?: string;
38
+ /** Default `"/workspace"`. */
39
+ defaultWorkdir?: string;
40
+ execHandler?: MockExecHandler;
41
+ /** Simulated latency for create / wake / stop / destroy, in ms. Default 0. */
42
+ lifecycleDelayMs?: number;
43
+ /** Called before every operation; throw from it to inject failures. */
44
+ beforeOperation?: (operation: MockOperation, sandboxId: string) => void | Promise<void>;
45
+ /**
46
+ * `spec.network` policies the mock accepts. Default: all of them. The mock
47
+ * only records the policy (in `spec`); narrow this to simulate a provider
48
+ * that cannot enforce one.
49
+ */
50
+ networkPolicies?: readonly Exclude<SandboxNetworkPolicyKind, "allow-all">[];
51
+ }
52
+ /**
53
+ * An in-memory provider for tests. Nothing is executed: `exec` calls are
54
+ * answered by `execHandler` and recorded in `calls`, files live in memory,
55
+ * and stop / wake / external stops are simulated.
56
+ */
57
+ export declare class MockSandboxProvider implements SandboxProvider {
58
+ readonly name: string;
59
+ readonly defaultWorkdir: string;
60
+ readonly networkPolicies: readonly Exclude<SandboxNetworkPolicyKind, "allow-all">[];
61
+ readonly calls: MockCall[];
62
+ private readonly sandboxes;
63
+ private execHandler;
64
+ private readonly lifecycleDelayMs;
65
+ private readonly beforeOperation;
66
+ private nextPid;
67
+ constructor(options?: MockSandboxProviderOptions);
68
+ setExecHandler(handler: MockExecHandler): void;
69
+ /** State of a sandbox by SDK sandbox ID. */
70
+ getState(sandboxId: string): MockSandboxState | undefined;
71
+ /** Calls recorded for one sandbox, optionally of one operation. */
72
+ callsFor(sandboxId: string, operation?: MockOperation): MockCall[];
73
+ /** Simulate the provider stopping a sandbox on its own, e.g. a provider-side timeout. */
74
+ simulateExternalStop(sandboxId: string): void;
75
+ /** Simulate the sandbox being deleted outside the SDK. */
76
+ simulateExternalDelete(sandboxId: string): void;
77
+ /** Append output to the main process log. */
78
+ appendMainLog(sandboxId: string, text: string): void;
79
+ /** Simulate the main process exiting with `exitCode`. */
80
+ exitMainProcess(sandboxId: string, exitCode: number): void;
81
+ create(input: ProviderCreateInput): Promise<ProviderCreateResult>;
82
+ wake(sandbox: ProviderSandbox): Promise<void>;
83
+ stop(sandbox: ProviderSandbox): Promise<void>;
84
+ destroy(sandbox: ProviderSandbox): Promise<void>;
85
+ status(sandbox: ProviderSandbox): Promise<ProviderSandboxStatus>;
86
+ exec(sandbox: ProviderSandbox, command: NormalizedCommand, options?: ExecOptions): Promise<ExecResult>;
87
+ startMainProcess(sandbox: ProviderSandbox, command: NormalizedCommand): Promise<void>;
88
+ getMainProcess(sandbox: ProviderSandbox): Promise<MainProcessInfo | null>;
89
+ readMainLog(sandbox: ProviderSandbox, offset: number): Promise<MainLogChunk>;
90
+ writeFiles(sandbox: ProviderSandbox, files: ResolvedSandboxFile[]): Promise<void>;
91
+ readFile(sandbox: ProviderSandbox, path: string): Promise<Uint8Array | null>;
92
+ getUrl(sandbox: ProviderSandbox, port: number): Promise<string>;
93
+ extendTimeout(sandbox: ProviderSandbox, durationMs: number): Promise<void>;
94
+ private record;
95
+ private delay;
96
+ private lookup;
97
+ private running;
98
+ private mustGetState;
99
+ }
@@ -0,0 +1,181 @@
1
+ import { ProviderSandboxMissingError, ProviderSandboxStoppedError, SandboxError } from "../errors.js";
2
+ /**
3
+ * An in-memory provider for tests. Nothing is executed: `exec` calls are
4
+ * answered by `execHandler` and recorded in `calls`, files live in memory,
5
+ * and stop / wake / external stops are simulated.
6
+ */
7
+ export class MockSandboxProvider {
8
+ name;
9
+ defaultWorkdir;
10
+ networkPolicies;
11
+ calls = [];
12
+ sandboxes = new Map();
13
+ execHandler;
14
+ lifecycleDelayMs;
15
+ beforeOperation;
16
+ nextPid = 100;
17
+ constructor(options = {}) {
18
+ this.name = options.name ?? "mock";
19
+ this.defaultWorkdir = options.defaultWorkdir ?? "/workspace";
20
+ this.execHandler = options.execHandler ?? (() => undefined);
21
+ this.lifecycleDelayMs = options.lifecycleDelayMs ?? 0;
22
+ this.beforeOperation = options.beforeOperation;
23
+ this.networkPolicies = options.networkPolicies ?? ["deny-all", "allowlist"];
24
+ }
25
+ setExecHandler(handler) {
26
+ this.execHandler = handler;
27
+ }
28
+ /** State of a sandbox by SDK sandbox ID. */
29
+ getState(sandboxId) {
30
+ return [...this.sandboxes.values()].find((state) => state.sandboxId === sandboxId);
31
+ }
32
+ /** Calls recorded for one sandbox, optionally of one operation. */
33
+ callsFor(sandboxId, operation) {
34
+ return this.calls.filter((call) => call.sandboxId === sandboxId && (operation === undefined || call.operation === operation));
35
+ }
36
+ /** Simulate the provider stopping a sandbox on its own, e.g. a provider-side timeout. */
37
+ simulateExternalStop(sandboxId) {
38
+ const state = this.mustGetState(sandboxId);
39
+ state.running = false;
40
+ if (state.mainProcess)
41
+ state.mainProcess.running = false;
42
+ }
43
+ /** Simulate the sandbox being deleted outside the SDK. */
44
+ simulateExternalDelete(sandboxId) {
45
+ this.sandboxes.delete(this.mustGetState(sandboxId).providerRef);
46
+ }
47
+ /** Append output to the main process log. */
48
+ appendMainLog(sandboxId, text) {
49
+ const state = this.mustGetState(sandboxId);
50
+ const added = new TextEncoder().encode(text);
51
+ const log = new Uint8Array(state.log.length + added.length);
52
+ log.set(state.log);
53
+ log.set(added, state.log.length);
54
+ state.log = log;
55
+ }
56
+ /** Simulate the main process exiting with `exitCode`. */
57
+ exitMainProcess(sandboxId, exitCode) {
58
+ const state = this.mustGetState(sandboxId);
59
+ if (!state.mainProcess)
60
+ throw new SandboxError(`Sandbox "${sandboxId}" has no main process`);
61
+ state.mainProcess.running = false;
62
+ state.mainProcess.exitCode = exitCode;
63
+ }
64
+ async create(input) {
65
+ await this.record("create", input.id);
66
+ await this.delay();
67
+ const providerRef = `mock-${input.id}`;
68
+ this.sandboxes.set(providerRef, {
69
+ sandboxId: input.id,
70
+ providerRef,
71
+ spec: input.spec,
72
+ workdir: input.workdir,
73
+ running: true,
74
+ files: new Map(),
75
+ mainProcess: null,
76
+ log: new Uint8Array(),
77
+ timeoutExtensionsMs: [],
78
+ });
79
+ return { providerRef };
80
+ }
81
+ async wake(sandbox) {
82
+ await this.record("wake", sandbox.id);
83
+ await this.delay();
84
+ this.lookup(sandbox).running = true;
85
+ }
86
+ async stop(sandbox) {
87
+ await this.record("stop", sandbox.id);
88
+ await this.delay();
89
+ const state = this.lookup(sandbox);
90
+ state.running = false;
91
+ if (state.mainProcess)
92
+ state.mainProcess.running = false;
93
+ }
94
+ async destroy(sandbox) {
95
+ await this.record("destroy", sandbox.id);
96
+ await this.delay();
97
+ this.sandboxes.delete(sandbox.providerRef);
98
+ }
99
+ async status(sandbox) {
100
+ const state = this.sandboxes.get(sandbox.providerRef);
101
+ if (!state)
102
+ return "missing";
103
+ return state.running ? "running" : "stopped";
104
+ }
105
+ async exec(sandbox, command, options = {}) {
106
+ await this.record("exec", sandbox.id, command);
107
+ const state = this.running(sandbox);
108
+ const result = await this.execHandler({
109
+ sandboxId: sandbox.id,
110
+ command,
111
+ env: { ...state.spec.env, ...command.env },
112
+ });
113
+ const stdout = result?.stdout ?? "";
114
+ const stderr = result?.stderr ?? "";
115
+ if (stdout)
116
+ options.onStdout?.(stdout);
117
+ if (stderr)
118
+ options.onStderr?.(stderr);
119
+ return { exitCode: result?.exitCode ?? 0, stdout, stderr };
120
+ }
121
+ async startMainProcess(sandbox, command) {
122
+ await this.record("startMainProcess", sandbox.id, command);
123
+ const state = this.running(sandbox);
124
+ state.mainProcess = { command, pid: this.nextPid++, running: true, exitCode: null };
125
+ }
126
+ async getMainProcess(sandbox) {
127
+ const process = this.running(sandbox).mainProcess;
128
+ return process ? { pid: process.pid, running: process.running, exitCode: process.exitCode } : null;
129
+ }
130
+ async readMainLog(sandbox, offset) {
131
+ const log = this.running(sandbox).log;
132
+ const start = offset > log.length ? 0 : offset;
133
+ const data = log.slice(start);
134
+ return { data, nextOffset: start + data.length };
135
+ }
136
+ async writeFiles(sandbox, files) {
137
+ await this.record("writeFiles", sandbox.id);
138
+ const state = this.running(sandbox);
139
+ for (const file of files)
140
+ state.files.set(file.path, file.content.slice());
141
+ }
142
+ async readFile(sandbox, path) {
143
+ await this.record("readFile", sandbox.id);
144
+ return this.running(sandbox).files.get(path)?.slice() ?? null;
145
+ }
146
+ async getUrl(sandbox, port) {
147
+ await this.record("getUrl", sandbox.id);
148
+ this.running(sandbox);
149
+ return `http://${sandbox.providerRef}.mock.localhost:${port}`;
150
+ }
151
+ async extendTimeout(sandbox, durationMs) {
152
+ await this.record("extendTimeout", sandbox.id);
153
+ this.running(sandbox).timeoutExtensionsMs.push(durationMs);
154
+ }
155
+ async record(operation, sandboxId, command) {
156
+ this.calls.push({ operation, sandboxId, ...(command && { command }) });
157
+ await this.beforeOperation?.(operation, sandboxId);
158
+ }
159
+ async delay() {
160
+ if (this.lifecycleDelayMs > 0)
161
+ await new Promise((resolve) => setTimeout(resolve, this.lifecycleDelayMs));
162
+ }
163
+ lookup(sandbox) {
164
+ const state = this.sandboxes.get(sandbox.providerRef);
165
+ if (!state)
166
+ throw new ProviderSandboxMissingError(sandbox.providerRef);
167
+ return state;
168
+ }
169
+ running(sandbox) {
170
+ const state = this.lookup(sandbox);
171
+ if (!state.running)
172
+ throw new ProviderSandboxStoppedError(sandbox.providerRef);
173
+ return state;
174
+ }
175
+ mustGetState(sandboxId) {
176
+ const state = this.getState(sandboxId);
177
+ if (!state)
178
+ throw new SandboxError(`Mock sandbox "${sandboxId}" does not exist`);
179
+ return state;
180
+ }
181
+ }