@a-dray/aglib 0.1.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.
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/dist/agent.d.ts +158 -0
- package/dist/agent.js +2 -0
- package/dist/agent.js.map +1 -0
- package/dist/content.d.ts +45 -0
- package/dist/content.js +11 -0
- package/dist/content.js.map +1 -0
- package/dist/harness/adapters/acp/index.d.ts +92 -0
- package/dist/harness/adapters/acp/index.js +479 -0
- package/dist/harness/adapters/acp/index.js.map +1 -0
- package/dist/harness/adapters/acp/rpc.d.ts +23 -0
- package/dist/harness/adapters/acp/rpc.js +103 -0
- package/dist/harness/adapters/acp/rpc.js.map +1 -0
- package/dist/harness/adapters/native/compaction.d.ts +16 -0
- package/dist/harness/adapters/native/compaction.js +55 -0
- package/dist/harness/adapters/native/compaction.js.map +1 -0
- package/dist/harness/adapters/native/index.d.ts +2 -0
- package/dist/harness/adapters/native/index.js +2 -0
- package/dist/harness/adapters/native/index.js.map +1 -0
- package/dist/harness/adapters/native/loop.d.ts +25 -0
- package/dist/harness/adapters/native/loop.js +153 -0
- package/dist/harness/adapters/native/loop.js.map +1 -0
- package/dist/harness/harness.d.ts +129 -0
- package/dist/harness/harness.js +2 -0
- package/dist/harness/harness.js.map +1 -0
- package/dist/harness/index.d.ts +3 -0
- package/dist/harness/index.js +2 -0
- package/dist/harness/index.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/json.d.ts +4 -0
- package/dist/json.js +2 -0
- package/dist/json.js.map +1 -0
- package/dist/model/adapters/anthropic/index.d.ts +34 -0
- package/dist/model/adapters/anthropic/index.js +271 -0
- package/dist/model/adapters/anthropic/index.js.map +1 -0
- package/dist/model/adapters/fake/index.d.ts +27 -0
- package/dist/model/adapters/fake/index.js +37 -0
- package/dist/model/adapters/fake/index.js.map +1 -0
- package/dist/model/adapters/openai-compatible/index.d.ts +29 -0
- package/dist/model/adapters/openai-compatible/index.js +226 -0
- package/dist/model/adapters/openai-compatible/index.js.map +1 -0
- package/dist/model/conformance.d.ts +210 -0
- package/dist/model/conformance.js +427 -0
- package/dist/model/conformance.js.map +1 -0
- package/dist/model/index.d.ts +2 -0
- package/dist/model/index.js +2 -0
- package/dist/model/index.js.map +1 -0
- package/dist/model/model.d.ts +85 -0
- package/dist/model/model.js +8 -0
- package/dist/model/model.js.map +1 -0
- package/dist/result.d.ts +23 -0
- package/dist/result.js +3 -0
- package/dist/result.js.map +1 -0
- package/dist/run.d.ts +10 -0
- package/dist/run.js +477 -0
- package/dist/run.js.map +1 -0
- package/dist/sandbox/adapters/docker.d.ts +22 -0
- package/dist/sandbox/adapters/docker.js +443 -0
- package/dist/sandbox/adapters/docker.js.map +1 -0
- package/dist/sandbox/adapters/local.d.ts +11 -0
- package/dist/sandbox/adapters/local.js +284 -0
- package/dist/sandbox/adapters/local.js.map +1 -0
- package/dist/sandbox/conformance.d.ts +73 -0
- package/dist/sandbox/conformance.js +309 -0
- package/dist/sandbox/conformance.js.map +1 -0
- package/dist/sandbox/index.d.ts +1 -0
- package/dist/sandbox/index.js +2 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/sandbox/sandbox.d.ts +172 -0
- package/dist/sandbox/sandbox.js +2 -0
- package/dist/sandbox/sandbox.js.map +1 -0
- package/dist/session/entry.d.ts +196 -0
- package/dist/session/entry.js +2 -0
- package/dist/session/entry.js.map +1 -0
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +2 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/log.d.ts +20 -0
- package/dist/session/log.js +24 -0
- package/dist/session/log.js.map +1 -0
- package/dist/session/messages.d.ts +42 -0
- package/dist/session/messages.js +99 -0
- package/dist/session/messages.js.map +1 -0
- package/dist/store/adapters/sqlite.d.ts +20 -0
- package/dist/store/adapters/sqlite.js +365 -0
- package/dist/store/adapters/sqlite.js.map +1 -0
- package/dist/store/conformance.d.ts +66 -0
- package/dist/store/conformance.js +580 -0
- package/dist/store/conformance.js.map +1 -0
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.js +2 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/store.d.ts +244 -0
- package/dist/store/store.js +2 -0
- package/dist/store/store.js.map +1 -0
- package/dist/tools/execute.d.ts +24 -0
- package/dist/tools/execute.js +105 -0
- package/dist/tools/execute.js.map +1 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/tool.d.ts +106 -0
- package/dist/tools/tool.js +34 -0
- package/dist/tools/tool.js.map +1 -0
- package/package.json +119 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docker.js","sourceRoot":"","sources":["../../../src/sandbox/adapters/docker.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A machine on this computer, in a container.\n *\n * The port says isolation is requested and either enforced or refused, and\n * until this existed nothing in the package could answer the first half: the\n * local provider runs with the host's authority and says so, which makes the\n * contract honest and leaves it unimplementable. A caller that wanted the thing\n * the port describes had to go and write it, and every one of them wrote the\n * same file.\n *\n * Docker rather than a hosted provider because it needs no account, no vendor\n * SDK and no network: the daemon is on the machine, the client is a program,\n * and the whole adapter is argument arrays. That keeps the package's one\n * dependency one, and it means the isolation a caller asks for is testable on a\n * laptop rather than only on somebody's bill.\n *\n * A container is also what makes a remote agent reachable. The host answers on\n * `host.docker.internal` from inside, so a process started in here can call the\n * application that started it without a tunnel to the open internet.\n */\nimport { spawn as spawnProcess } from \"node:child_process\";\nimport { Readable } from \"node:stream\";\nimport type { CommandResult, Sandbox, SandboxError, SandboxProcess, SandboxProvider } from \"../sandbox.js\";\nimport { err, ok, type Result } from \"../../result.js\";\n\n/** Docker's own client. Its absence is the one failure worth naming plainly. */\nconst DOCKER = \"docker\";\n\n/** The address the host answers on from inside a container. Docker Desktop\n * publishes it; `host-gateway` is what makes it true on a plain Linux daemon\n * too, so one URL works either place. Not exported: nothing composes it yet,\n * and a name with no consumer is surface with a cost. */\nconst HOST_ADDRESS = \"host.docker.internal\";\n\n/**\n * A command may produce useful output; no command may consume the host by\n * streaming forever. Collected output past this — the two streams together, and\n * a file read through `cat` with them — is refused rather than truncated\n * silently, because a caller that asked for a file and got most of one has no\n * way to tell.\n *\n * Refusing ends this client and not the command: what `docker exec` started\n * keeps running in the container, where the container's own lifetime reaps it.\n */\nconst MAX_OUTPUT_BYTES = 1_000_000;\n\nexport interface DockerSandboxProviderOptions {\n /** The image every container is made from. */\n image: string;\n /**\n * How long a container lives before it reaps itself, in seconds. Defaults to\n * an hour. It exists because a process that died without closing would\n * otherwise leave a machine that outlives the laptop's next reboot.\n */\n lifetimeSeconds?: number;\n /** Where relative paths resolve, and the containers' working directory. Defaults to `/home/user`. */\n root?: string;\n /** Deadline for one `docker` call, in milliseconds. Composed with the caller's signal. */\n requestTimeoutMs?: number;\n}\n\ntype Ran = { stdout: string; stderr: string; code: number };\n\n/** Everything this adapter reports. `retryable` follows the code rather than\n * being decided call by call: a daemon that is not there may be there in a\n * moment, and a denied path never will be. */\nfunction failure(code: SandboxError[\"code\"], message: string): Result<never, SandboxError> {\n return err({ code, message, retryable: code === \"unavailable\" || code === \"timeout\" });\n}\n\n/** The one place an abort becomes a code: a deadline reads `timeout`, and\n * anything else is the caller changing its mind. */\nfunction aborted(signal: AbortSignal | undefined, reason?: unknown): Result<never, SandboxError> {\n const cause = (signal?.aborted ? signal.reason : undefined) ?? reason;\n const timedOut = typeof cause === \"object\" && cause !== null && (cause as { name?: unknown }).name === \"TimeoutError\";\n return timedOut\n ? failure(\"timeout\", \"Sandbox operation timed out\")\n : failure(\"cancelled\", \"Sandbox operation cancelled\");\n}\n\n/**\n * One `docker` invocation.\n *\n * Nothing goes through a shell, so a path or an environment value is an\n * argument and never something to quote. `node:child_process` rather than\n * `Bun.spawn` because there is no reason for this adapter to be less portable\n * than the daemon it talks to.\n *\n * An abort or a deadline ends this client only. A command it started is still\n * the container's, and runs there until the container's lifetime reaps it.\n */\nfunction docker(input: {\n argv: readonly string[];\n stdin?: string;\n environment?: Readonly<Record<string, string | undefined>>;\n signal?: AbortSignal;\n timeoutMs: number;\n}): Promise<Result<Ran, SandboxError>> {\n return new Promise((settle) => {\n const deadline = AbortSignal.timeout(input.timeoutMs);\n const signal = input.signal ? AbortSignal.any([input.signal, deadline]) : deadline;\n let child;\n try {\n child = spawnProcess(DOCKER, [...input.argv], {\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n ...(input.environment ? { env: input.environment } : {}),\n signal,\n });\n } catch (error) {\n settle(failure(\"unavailable\", error instanceof Error ? error.message : String(error)));\n return;\n }\n const stdout: Buffer[] = [];\n const stderr: Buffer[] = [];\n let bytes = 0;\n let exceeded = false;\n const collect = (into: Buffer[], chunk: Buffer) => {\n if (exceeded) return;\n const remaining = MAX_OUTPUT_BYTES - bytes;\n if (remaining > 0) {\n const kept = chunk.subarray(0, remaining);\n into.push(kept);\n bytes += kept.byteLength;\n }\n if (bytes >= MAX_OUTPUT_BYTES) { exceeded = true; child.kill(); }\n };\n child.stdout.on(\"data\", (chunk: Buffer) => collect(stdout, chunk));\n child.stderr.on(\"data\", (chunk: Buffer) => collect(stderr, chunk));\n child.stdin.on(\"error\", () => { /* the child exited before it read its input */ });\n child.stdin.end(input.stdin ?? \"\");\n child.on(\"error\", (error) => {\n if (signal.aborted) { settle(aborted(input.signal, signal.reason)); return; }\n settle(failure(\n \"unavailable\",\n (error as { code?: string }).code === \"ENOENT\" ? \"docker is not on PATH\" : error.message,\n ));\n });\n child.on(\"close\", (code) => {\n if (signal.aborted) { settle(aborted(input.signal, signal.reason)); return; }\n if (exceeded) { settle(failure(\"failed\", `Output exceeded ${MAX_OUTPUT_BYTES} bytes`)); return; }\n settle(ok({\n stdout: Buffer.concat(stdout).toString(\"utf8\"),\n stderr: Buffer.concat(stderr).toString(\"utf8\"),\n code: code ?? 1,\n }));\n });\n });\n}\n\n/** The client failing means the daemon is unreachable or the container is gone;\n * either way the sandbox is not there to be used. */\nconst unavailable = (ran: Ran): Result<never, SandboxError> =>\n failure(\"unavailable\", ran.stderr.trim() || ran.stdout.trim() || `docker exited ${ran.code}`);\n\n/** What the daemon writes when the container is not there to run anything in.\n * It writes it to the same stderr the command has, so a match is a question to\n * put to the daemon and never an answer on its own. */\nconst looksGone = (stderr: string): boolean => /No such container|is not running/i.test(stderr);\n\n/** Asks the daemon whether a container is there and running. */\nconst inspectRunning = (id: string, config: Settled, signal?: AbortSignal): Promise<Result<Ran, SandboxError>> =>\n docker({\n argv: [\"inspect\", \"--format\", \"{{.State.Running}}\", id],\n timeoutMs: config.requestTimeoutMs,\n ...(signal ? { signal } : {}),\n });\n\n/** How that answer reads. A daemon that could not be asked is not a container\n * that is running. */\nconst running = (state: Result<Ran, SandboxError>): boolean =>\n state.ok && state.value.code === 0 && state.value.stdout.trim() === \"true\";\n\n/**\n * Resolves `path` against `root`, and answers only when it stayed inside.\n *\n * POSIX by hand: these are paths in the container, not on the calling machine,\n * so `node:path` would resolve them against the wrong filesystem's rules.\n *\n * Lexical, and only lexical: a symlink under the root that points out of it is\n * followed by the filesystem this cannot see. Confining paths to the root is a\n * guard against slips, and the container is the boundary that holds.\n */\nfunction within(root: string, path: string): string | undefined {\n const base = root.replace(/\\/+$/, \"\");\n const segments: string[] = [];\n for (const segment of (path.startsWith(\"/\") ? path : `${base}/${path}`).split(\"/\")) {\n if (segment === \"\" || segment === \".\") continue;\n if (segment === \"..\") {\n if (segments.pop() === undefined) return undefined;\n continue;\n }\n segments.push(segment);\n }\n const resolved = `/${segments.join(\"/\")}`;\n return resolved === base || resolved.startsWith(`${base}/`) ? resolved : undefined;\n}\n\n/** Single-quoted for `/bin/sh`, so a directory name with a space or a quote in\n * it is a directory name and not the start of another command. */\nconst quoted = (value: string): string => `'${value.split(\"'\").join(\"'\\\\''\")}'`;\n\n/**\n * The `--env NAME` flags for an environment, or a refusal.\n *\n * Named rather than valued: the value travels in this client's own environment,\n * so nothing a model typed is ever pasted into an argument or published in the\n * host's process table, where an agent's credentials would be readable by\n * anything on the machine. A name that is itself an assignment would defeat\n * that — `docker` would take it whole and set a variable the caller never\n * named, dropping the value it did — so it is refused rather than passed.\n */\nfunction envFlags(env: Readonly<Record<string, string>> | undefined): Result<readonly string[], SandboxError> {\n const flags: string[] = [];\n for (const name of Object.keys(env ?? {})) {\n if (name === \"\" || name.includes(\"=\")) {\n return failure(\"failed\", `'${name}' is not the name of an environment variable`);\n }\n flags.push(\"--env\", name);\n }\n return ok(flags);\n}\n\ntype Settled = Required<DockerSandboxProviderOptions>;\n\n/**\n * @param base Set for every call, under each call's own. Carries `create`'s\n * `env` and its `secrets` together, because to a container they are the same\n * thing — see the `secrets: \"plain\"` this reports.\n */\nfunction dockerSandbox(id: string, config: Settled, base: Readonly<Record<string, string>>): Sandbox {\n let closed = false;\n const gone = (): Result<never, SandboxError> => failure(\"unavailable\", \"Sandbox is closed\");\n\n const call = (\n argv: readonly string[],\n request: { signal?: AbortSignal },\n stdin?: string,\n environment?: Readonly<Record<string, string | undefined>>,\n ) => docker({\n argv,\n timeoutMs: config.requestTimeoutMs,\n ...(request.signal ? { signal: request.signal } : {}),\n ...(stdin === undefined ? {} : { stdin }),\n ...(environment ? { environment } : {}),\n });\n\n return {\n id,\n root: config.root,\n // The container is the containment: a shell the model talked into\n // misbehaving holds nothing this process has.\n isolation: \"container\",\n // A container has no egress this adapter controls, so a value it is given\n // is a value it holds. The names travel as `--env NAME` and the values in\n // this client's own environment, so nothing is published in the host's\n // process table — but inside the box it is readable, and that is what\n // \"plain\" says.\n secrets: \"plain\",\n\n async exec(request): Promise<Result<CommandResult, SandboxError>> {\n if (closed) return gone();\n const cwd = request.cwd ? within(config.root, request.cwd) : config.root;\n if (!cwd) return failure(\"denied\", \"Working directory is outside the sandbox root\");\n if (request.signal?.aborted) return aborted(request.signal);\n const environment = { ...base, ...request.env };\n const named = envFlags(environment);\n if (!named.ok) return err(named.error);\n // The directory is made rather than required — `docker exec -w` refuses\n // one that is not there yet, and a command's cwd is the model's to name.\n // The command follows on its own line, exactly as written, so there is no\n // second round of quoting for it to be mangled by.\n const script = `mkdir -p ${quoted(cwd)} && cd ${quoted(cwd)} || exit 1\\n${request.command}`;\n const ran = await call(\n [\"exec\", ...named.value, \"--interactive\", id, \"/bin/sh\", \"-c\", script],\n request,\n undefined,\n named.value.length ? { ...process.env, ...environment } : undefined,\n );\n if (!ran.ok) return err(ran.error);\n // A command's own non-zero exit is an outcome. Docker's own 125/126/127\n // are indistinguishable from a command that exited the same way, so only\n // a container that is gone is a failure — and the daemon is asked whether\n // it is, because the text saying so arrives on the stderr the command\n // writes to: a command that printed it could otherwise report its own\n // sandbox as gone, and be believed and retried.\n if (looksGone(ran.value.stderr) && !running(await inspectRunning(id, config, request.signal))) {\n return unavailable(ran.value);\n }\n return ok({ stdout: ran.value.stdout, stderr: ran.value.stderr, exitCode: ran.value.code });\n },\n\n async spawn(request): Promise<Result<SandboxProcess, SandboxError>> {\n if (closed) return gone();\n const cwd = request.cwd ? within(config.root, request.cwd) : config.root;\n if (!cwd) return failure(\"denied\", \"Working directory is outside the sandbox root\");\n const [program, ...rest] = request.command;\n if (!program) return failure(\"failed\", \"spawn was given no command\");\n const environment = { ...base, ...request.env };\n const named = envFlags(environment);\n if (!named.ok) return err(named.error);\n let child;\n try {\n child = spawnProcess(\n DOCKER,\n // `--workdir` where `exec` makes the directory first: docker refuses\n // to start a process in a path that is not there, and refuses it as\n // the process's own exit rather than as this call's result. A caller\n // that means to start something somewhere new makes it with `exec`.\n [\"exec\", \"--interactive\", ...named.value, \"--workdir\", cwd, id, program, ...rest],\n {\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n ...(named.value.length ? { env: { ...process.env, ...environment } } : {}),\n },\n );\n } catch (error) {\n return failure(\"unavailable\", error instanceof Error ? error.message : String(error));\n }\n child.stdin.on(\"error\", () => { /* the process ended before it read its input */ });\n // A launch that fails does so after this call returns, and an `error`\n // event with no listener is an uncaught exception that takes the host\n // process down. No `close` follows it either, so `exited` would never\n // settle. Waiting for the launch makes it the typed failure the port\n // already has a place for, and the listener stays attached afterwards so\n // that a later one is not fatal either.\n const failed = await new Promise<SandboxError | undefined>((settle) => {\n child.once(\"spawn\", () => settle(undefined));\n child.once(\"error\", (error: Error) => settle({\n code: \"unavailable\",\n message: (error as { code?: string }).code === \"ENOENT\" ? \"docker is not on PATH\" : error.message,\n retryable: true,\n }));\n });\n if (failed) return err(failed);\n return ok({\n write: (chunk: string) => { child.stdin.write(chunk); },\n stdout: Readable.toWeb(child.stdout) as ReadableStream<Uint8Array>,\n stderr: Readable.toWeb(child.stderr) as ReadableStream<Uint8Array>,\n exited: new Promise<number>((settle) => { child.on(\"close\", (code) => settle(code ?? 1)); }),\n kill: () => { child.kill(); },\n });\n },\n\n async readFile(request) {\n if (closed) return gone();\n const path = within(config.root, request.path);\n if (!path) return failure(\"denied\", \"Path is outside the sandbox root\");\n if (request.signal?.aborted) return aborted(request.signal);\n const ran = await call([\"exec\", id, \"cat\", path], request);\n if (!ran.ok) return err(ran.error);\n if (ran.value.code !== 0) {\n return /No such file or directory/i.test(ran.value.stderr)\n ? failure(\"not-found\", `No file at ${request.path}`)\n : failure(\"failed\", ran.value.stderr.trim() || `cat exited ${ran.value.code}`);\n }\n return ok(ran.value.stdout);\n },\n\n async writeFile(request) {\n if (closed) return gone();\n const path = within(config.root, request.path);\n if (!path) return failure(\"denied\", \"Path is outside the sandbox root\");\n if (request.signal?.aborted) return aborted(request.signal);\n const ran = await call(\n [\"exec\", \"--interactive\", id, \"/bin/sh\", \"-c\", 'mkdir -p \"$(dirname \"$1\")\" && cat > \"$1\"', \"sh\", path],\n request,\n request.content,\n );\n if (!ran.ok) return err(ran.error);\n if (ran.value.code !== 0) {\n return failure(\"failed\", ran.value.stderr.trim() || `write exited ${ran.value.code}`);\n }\n return ok(undefined);\n },\n\n async close() {\n if (closed) return ok(undefined);\n const ran = await docker({ argv: [\"rm\", \"--force\", id], timeoutMs: config.requestTimeoutMs });\n if (!ran.ok) return err(ran.error);\n // A container already gone is what closing asked for.\n if (ran.value.code !== 0 && !/No such container/i.test(ran.value.stderr)) return unavailable(ran.value);\n // Marked only once the machine is actually gone. Marking it before the\n // daemon answered would tell a caller retrying a reported refusal that\n // the container it is still paying for had been released.\n closed = true;\n return ok(undefined);\n },\n };\n}\n\n/**\n * Containers from the local daemon.\n *\n * Each runs nothing but a sleep long enough to serve a session and no longer,\n * so the container reaps itself on the same ceiling a hosted provider enforces.\n */\nexport function createDockerSandboxProvider(options: DockerSandboxProviderOptions): SandboxProvider {\n const config: Settled = {\n root: \"/home/user\",\n lifetimeSeconds: 60 * 60,\n requestTimeoutMs: 60_000,\n ...options,\n };\n\n return {\n id: \"docker\",\n\n async create(request) {\n // Merged here and not later: to a container these are one environment,\n // and keeping two would be two names for what every call has to combine\n // anyway. What the caller gains by having said `secrets` is the honest\n // `\"plain\"` this sandbox reports back.\n const base = {\n ...request.env,\n ...Object.fromEntries((request.secrets ?? []).map((secret) => [secret.env, secret.value])),\n };\n // Refused rather than ignored. A caller that named an allowlist and got\n // an unrestricted container would have no way to find that out.\n if (request.network.mode === \"web-allowlist\") {\n return failure(\"unsupported\", \"Docker is not configured for a web allowlist; use deny-all or unrestricted.\");\n }\n if (request.signal?.aborted) return aborted(request.signal);\n\n // Reattaching must find a machine, not make one: a container that has\n // stopped is one whose files are already gone.\n if (request.reconnect !== undefined) {\n const found = await inspectRunning(request.reconnect, config, request.signal);\n // A daemon that could not be asked is not the same answer as a machine\n // that is not there, and only the second one is `not-found`.\n if (!found.ok) return err(found.error);\n if (!running(found)) return failure(\"not-found\", `No sandbox ${request.reconnect}`);\n return ok(dockerSandbox(request.reconnect, config, base));\n }\n\n const ran = await docker({\n argv: [\n \"run\", \"--detach\", \"--rm\",\n // What the host answers to from inside. Docker Desktop resolves it\n // anyway; the mapping is what makes a plain Linux daemon agree.\n \"--add-host\", `${HOST_ADDRESS}:host-gateway`,\n ...(request.network.mode === \"deny-all\" ? [\"--network\", \"none\"] : []),\n \"--workdir\", config.root,\n \"--env\", `HOME=${config.root}`,\n config.image,\n \"sleep\", String(config.lifetimeSeconds),\n ],\n timeoutMs: config.requestTimeoutMs,\n ...(request.signal ? { signal: request.signal } : {}),\n });\n if (!ran.ok) return err(ran.error);\n if (ran.value.code !== 0) return unavailable(ran.value);\n const id = ran.value.stdout.trim();\n if (!id) return failure(\"failed\", \"docker run named no container\");\n // `docker run --detach` answers as soon as the container is created, and\n // every relative path is resolved against the root. One command from\n // inside is what says the machine is there to run them.\n const prepared = await docker({\n argv: [\"exec\", id, \"mkdir\", \"-p\", config.root],\n timeoutMs: config.requestTimeoutMs,\n ...(request.signal ? { signal: request.signal } : {}),\n });\n if (!prepared.ok || prepared.value.code !== 0) {\n // The container exists whether or not it can be used, and this is the\n // last place its id is held: not removing it here leaks a machine that\n // nothing can reach and only its own lifetime ends.\n await docker({ argv: [\"rm\", \"--force\", id], timeoutMs: config.requestTimeoutMs });\n return prepared.ok\n ? failure(\"failed\", prepared.value.stderr.trim() || `preparing ${config.root} exited ${prepared.value.code}`)\n : err(prepared.error);\n }\n return ok(dockerSandbox(id, config, base));\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SandboxProvider } from "../sandbox.js";
|
|
2
|
+
/**
|
|
3
|
+
* Commands and files on the host, with the host's authority.
|
|
4
|
+
*
|
|
5
|
+
* This is not a sandbox and does not pretend to be one. Asking it for
|
|
6
|
+
* `isolation: "required"` returns `unsupported` — the caller finds out by
|
|
7
|
+
* failing rather than by reading a boolean it was free to ignore.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createLocalSandboxProvider(options?: {
|
|
10
|
+
root?: string;
|
|
11
|
+
}): SandboxProvider;
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { err, ok } from "../../result.js";
|
|
2
|
+
import { spawn as spawnProcess } from "node:child_process";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
import { readFile, writeFile, mkdir, realpath } from "node:fs/promises";
|
|
5
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
|
+
/**
|
|
7
|
+
* Commands and files on the host, with the host's authority.
|
|
8
|
+
*
|
|
9
|
+
* This is not a sandbox and does not pretend to be one. Asking it for
|
|
10
|
+
* `isolation: "required"` returns `unsupported` — the caller finds out by
|
|
11
|
+
* failing rather than by reading a boolean it was free to ignore.
|
|
12
|
+
*/
|
|
13
|
+
export function createLocalSandboxProvider(options = {}) {
|
|
14
|
+
return {
|
|
15
|
+
id: "local",
|
|
16
|
+
async create(input) {
|
|
17
|
+
if (input.isolation === "required") {
|
|
18
|
+
return err({
|
|
19
|
+
code: "unsupported",
|
|
20
|
+
message: "The local provider runs on the host and cannot isolate. Choose a sandbox provider.",
|
|
21
|
+
retryable: false,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
if (input.network.mode !== "unrestricted") {
|
|
25
|
+
return err({
|
|
26
|
+
code: "unsupported",
|
|
27
|
+
message: `The local provider cannot enforce network mode '${input.network.mode}'.`,
|
|
28
|
+
retryable: false,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
// Where a sandbox lives is a property of the provider, not of one
|
|
32
|
+
// request: the port's `create` names no root, so one provider answers for
|
|
33
|
+
// one directory and a second directory is a second provider.
|
|
34
|
+
return createLocalSandbox({
|
|
35
|
+
root: options.root ?? process.cwd(),
|
|
36
|
+
// Nothing here can substitute anything: a process on the host reads
|
|
37
|
+
// its own environment, and there is no egress to intercept. The two
|
|
38
|
+
// arrive merged, and the sandbox says `secrets: "plain"` so a caller
|
|
39
|
+
// that minds can decline.
|
|
40
|
+
env: {
|
|
41
|
+
...input.env,
|
|
42
|
+
...Object.fromEntries((input.secrets ?? []).map((secret) => [secret.env, secret.value])),
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Not exported. A sandbox is reached through its provider, like every other
|
|
50
|
+
* one — a second constructor beside `createLocalSandboxProvider` was a second
|
|
51
|
+
* way to make the same thing, and the one that skipped the port.
|
|
52
|
+
*/
|
|
53
|
+
async function createLocalSandbox(input) {
|
|
54
|
+
const root = resolve(input.root);
|
|
55
|
+
await mkdir(root, { recursive: true });
|
|
56
|
+
let closed = false;
|
|
57
|
+
/**
|
|
58
|
+
* A released sandbox is gone. Nothing was provisioned here, so closing frees
|
|
59
|
+
* nothing — but a caller that closed and then ran a command has a bug, and
|
|
60
|
+
* every other provider would have told it so.
|
|
61
|
+
*/
|
|
62
|
+
const gone = () => err({ code: "unavailable", message: "Sandbox is closed", retryable: false });
|
|
63
|
+
/**
|
|
64
|
+
* Whether `full` left `base`, reading the two paths and nothing else.
|
|
65
|
+
*
|
|
66
|
+
* A path that left is `..`, begins with a `..` segment, or has no relation to
|
|
67
|
+
* the base at all. A *name* beginning with two dots is a file in it, and a
|
|
68
|
+
* bare `startsWith("..")` refused those too.
|
|
69
|
+
*/
|
|
70
|
+
const escapes = (base, full) => {
|
|
71
|
+
const inside = relative(base, full);
|
|
72
|
+
return inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute(inside);
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* The path a call would really touch.
|
|
76
|
+
*
|
|
77
|
+
* `realpath` fails on something that is not there yet, which `writeFile` is
|
|
78
|
+
* entitled to be given — so the nearest existing ancestor is resolved and the
|
|
79
|
+
* rest re-joined. That is enough: only an existing directory can be the
|
|
80
|
+
* symlink, and a name that does not exist cannot redirect anything.
|
|
81
|
+
*/
|
|
82
|
+
const settled = async (full) => {
|
|
83
|
+
const parts = [];
|
|
84
|
+
let at = full;
|
|
85
|
+
for (;;) {
|
|
86
|
+
try {
|
|
87
|
+
return resolve(await realpath(at), ...parts.reverse());
|
|
88
|
+
}
|
|
89
|
+
catch { /* not there yet — try its parent */ }
|
|
90
|
+
const up = dirname(at);
|
|
91
|
+
if (up === at)
|
|
92
|
+
return full;
|
|
93
|
+
parts.push(basename(at));
|
|
94
|
+
at = up;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const realRoot = await realpath(root);
|
|
98
|
+
/**
|
|
99
|
+
* Confines paths to the chosen root. Not isolation — a guard against slips —
|
|
100
|
+
* but it does consult the filesystem, because a lexical check alone is not
|
|
101
|
+
* even that.
|
|
102
|
+
*
|
|
103
|
+
* A symlink under the root pointing out of it is followed by every call that
|
|
104
|
+
* matters, and `readFile`/`writeFile` are exactly the calls an application
|
|
105
|
+
* hands to a model. Lexically `notes/link/passwd` never leaves the root; on
|
|
106
|
+
* disk it was `/etc/passwd`. So the lexical test runs first, because it is
|
|
107
|
+
* free and catches `../` before any syscall, and the resolved one decides.
|
|
108
|
+
*/
|
|
109
|
+
const within = async (path) => {
|
|
110
|
+
const full = isAbsolute(path) ? resolve(path) : resolve(join(root, path));
|
|
111
|
+
const denied = err({
|
|
112
|
+
code: "denied", message: `Path escapes the sandbox root: ${path}`, retryable: false,
|
|
113
|
+
});
|
|
114
|
+
if (escapes(root, full))
|
|
115
|
+
return denied;
|
|
116
|
+
return escapes(realRoot, await settled(full)) ? denied : ok(full);
|
|
117
|
+
};
|
|
118
|
+
return ok({
|
|
119
|
+
id: `local:${root}`,
|
|
120
|
+
root,
|
|
121
|
+
isolation: "none",
|
|
122
|
+
// The host's own environment is readable by anything running in it.
|
|
123
|
+
secrets: "plain",
|
|
124
|
+
async exec({ command, cwd, env, signal }) {
|
|
125
|
+
if (closed)
|
|
126
|
+
return gone();
|
|
127
|
+
const target = cwd ? await within(cwd) : ok(root);
|
|
128
|
+
if (!target.ok)
|
|
129
|
+
return target;
|
|
130
|
+
// Checked before anything is started: a caller that has already given up
|
|
131
|
+
// gets `cancelled` rather than the command it no longer wants.
|
|
132
|
+
if (signal?.aborted)
|
|
133
|
+
return err(abortOrFail(undefined, signal));
|
|
134
|
+
try {
|
|
135
|
+
const child = spawnProcess("/bin/sh", ["-lc", command], {
|
|
136
|
+
cwd: target.value,
|
|
137
|
+
env: { ...process.env, ...input.env, ...env },
|
|
138
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
139
|
+
});
|
|
140
|
+
const collected = new Promise((settle, fail) => {
|
|
141
|
+
const out = [];
|
|
142
|
+
const errs = [];
|
|
143
|
+
child.stdout.on("data", (chunk) => out.push(chunk));
|
|
144
|
+
child.stderr.on("data", (chunk) => errs.push(chunk));
|
|
145
|
+
child.on("error", fail);
|
|
146
|
+
child.on("close", (code) => settle([
|
|
147
|
+
Buffer.concat(out).toString("utf8"),
|
|
148
|
+
Buffer.concat(errs).toString("utf8"),
|
|
149
|
+
code ?? 1,
|
|
150
|
+
]));
|
|
151
|
+
});
|
|
152
|
+
// Raced, not awaited. Killing the shell does not settle this promptly:
|
|
153
|
+
// the orphan it spawned holds the pipes open, so a `sleep 5` cancelled
|
|
154
|
+
// at 200ms answered five seconds later — and by then the exit code
|
|
155
|
+
// reads as an ordinary signal death rather than a cancellation.
|
|
156
|
+
const finished = signal
|
|
157
|
+
? await Promise.race([
|
|
158
|
+
collected.then((done) => ({ done })),
|
|
159
|
+
new Promise((settle) => {
|
|
160
|
+
signal.addEventListener("abort", () => settle({}), { once: true });
|
|
161
|
+
}),
|
|
162
|
+
])
|
|
163
|
+
: { done: await collected };
|
|
164
|
+
if (!finished.done) {
|
|
165
|
+
// The shell, not everything under it: a command that spawned its own
|
|
166
|
+
// child leaves that child to finish. The same limit the container
|
|
167
|
+
// adapter has, for the same reason — this is a process, not a group.
|
|
168
|
+
child.kill();
|
|
169
|
+
return err(abortOrFail(undefined, signal));
|
|
170
|
+
}
|
|
171
|
+
const [stdout, stderr, exitCode] = finished.done;
|
|
172
|
+
// A non-zero exit is an outcome the caller reasons about, not an error.
|
|
173
|
+
return ok({ stdout, stderr, exitCode });
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
return err(abortOrFail(error, signal));
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
async spawn({ command, cwd, env }) {
|
|
180
|
+
if (closed)
|
|
181
|
+
return gone();
|
|
182
|
+
const target = cwd ? await within(cwd) : ok(root);
|
|
183
|
+
if (!target.ok)
|
|
184
|
+
return target;
|
|
185
|
+
const [executable, ...rest] = command;
|
|
186
|
+
if (!executable) {
|
|
187
|
+
return err({ code: "failed", message: "spawn needs a command", retryable: false });
|
|
188
|
+
}
|
|
189
|
+
const child = spawnProcess(executable, rest, {
|
|
190
|
+
cwd: target.value,
|
|
191
|
+
env: { ...process.env, ...input.env, ...env },
|
|
192
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
193
|
+
});
|
|
194
|
+
// A launch failure arrives asynchronously, and an unhandled `error` event
|
|
195
|
+
// is an uncaught exception — which in a service is the whole process, not
|
|
196
|
+
// the sandbox. Awaited here so it comes back typed, and the listener stays
|
|
197
|
+
// on so a later one is not fatal either.
|
|
198
|
+
const launched = await new Promise((settle) => {
|
|
199
|
+
child.once("spawn", () => settle(undefined));
|
|
200
|
+
child.once("error", (error) => settle({
|
|
201
|
+
code: "unavailable",
|
|
202
|
+
message: error.code === "ENOENT" ? `No such command: ${executable}` : error.message,
|
|
203
|
+
retryable: true,
|
|
204
|
+
}));
|
|
205
|
+
});
|
|
206
|
+
if (launched)
|
|
207
|
+
return err(launched);
|
|
208
|
+
child.on("error", () => { });
|
|
209
|
+
child.stdin.on("error", () => { });
|
|
210
|
+
return ok({
|
|
211
|
+
write: (chunk) => { child.stdin.write(chunk); },
|
|
212
|
+
stdout: Readable.toWeb(child.stdout),
|
|
213
|
+
stderr: Readable.toWeb(child.stderr),
|
|
214
|
+
// `exit` fires when the process ends; `close` waits for its stdio to
|
|
215
|
+
// close too, which a caller that never reads `stdout` may not cause —
|
|
216
|
+
// `Readable.toWeb` leaves the pipe unread until somebody pulls from it.
|
|
217
|
+
// Whichever arrives first answers, so a killed process is reported as
|
|
218
|
+
// gone whether or not anyone was listening to it.
|
|
219
|
+
exited: new Promise((settle) => {
|
|
220
|
+
child.on("exit", (code) => settle(code ?? 1));
|
|
221
|
+
child.on("close", (code) => settle(code ?? 1));
|
|
222
|
+
}),
|
|
223
|
+
kill: () => {
|
|
224
|
+
child.kill();
|
|
225
|
+
// SIGTERM is a request. This port says the process can be killed, so
|
|
226
|
+
// a process that declines to handle it does not get to leave `exited`
|
|
227
|
+
// pending for as long as it likes. Cleared on exit, and unreferenced,
|
|
228
|
+
// so a prompt death leaves nothing holding the loop open.
|
|
229
|
+
const escalate = setTimeout(() => child.kill("SIGKILL"), 2_000);
|
|
230
|
+
escalate.unref?.();
|
|
231
|
+
child.once("exit", () => clearTimeout(escalate));
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
},
|
|
235
|
+
async readFile({ path, signal }) {
|
|
236
|
+
if (closed)
|
|
237
|
+
return gone();
|
|
238
|
+
const target = await within(path);
|
|
239
|
+
if (!target.ok)
|
|
240
|
+
return target;
|
|
241
|
+
if (signal?.aborted)
|
|
242
|
+
return err(abortOrFail(undefined, signal));
|
|
243
|
+
try {
|
|
244
|
+
return ok(await readFile(target.value, "utf8"));
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
return err(fileError(error, path));
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
async writeFile({ path, content, signal }) {
|
|
251
|
+
if (closed)
|
|
252
|
+
return gone();
|
|
253
|
+
const target = await within(path);
|
|
254
|
+
if (!target.ok)
|
|
255
|
+
return target;
|
|
256
|
+
if (signal?.aborted)
|
|
257
|
+
return err(abortOrFail(undefined, signal));
|
|
258
|
+
try {
|
|
259
|
+
await mkdir(join(target.value, ".."), { recursive: true });
|
|
260
|
+
await writeFile(target.value, content, "utf8");
|
|
261
|
+
return ok(undefined);
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
return err(fileError(error, path));
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
async close() { closed = true; return ok(undefined); /* nothing was provisioned to release */ },
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
function abortOrFail(error, signal) {
|
|
271
|
+
if (signal?.aborted) {
|
|
272
|
+
const timedOut = signal.reason?.name === "TimeoutError";
|
|
273
|
+
// A deadline may be met on a second attempt with a longer one; a caller
|
|
274
|
+
// that changed its mind has nothing to act on. The container adapter agrees.
|
|
275
|
+
return timedOut
|
|
276
|
+
? { code: "timeout", message: "Command timed out", retryable: true }
|
|
277
|
+
: { code: "cancelled", message: "Command cancelled", retryable: false };
|
|
278
|
+
}
|
|
279
|
+
return { code: "failed", message: error instanceof Error ? error.message : String(error), retryable: false };
|
|
280
|
+
}
|
|
281
|
+
const fileError = (error, path) => error.code === "ENOENT"
|
|
282
|
+
? { code: "not-found", message: `No such file: ${path}`, retryable: false }
|
|
283
|
+
: { code: "failed", message: error instanceof Error ? error.message : String(error), retryable: false };
|
|
284
|
+
//# sourceMappingURL=local.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local.js","sourceRoot":"","sources":["../../../src/sandbox/adapters/local.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Sandbox, SandboxError, SandboxProcess, SandboxProvider } from \"../sandbox.js\";\nimport { err, ok, type Result } from \"../../result.js\";\nimport { spawn as spawnProcess } from \"node:child_process\";\nimport { Readable } from \"node:stream\";\nimport { readFile, writeFile, mkdir, realpath } from \"node:fs/promises\";\nimport { basename, dirname, isAbsolute, join, relative, resolve, sep } from \"node:path\";\n\n/**\n * Commands and files on the host, with the host's authority.\n *\n * This is not a sandbox and does not pretend to be one. Asking it for\n * `isolation: \"required\"` returns `unsupported` — the caller finds out by\n * failing rather than by reading a boolean it was free to ignore.\n */\nexport function createLocalSandboxProvider(options: { root?: string } = {}): SandboxProvider {\n return {\n id: \"local\",\n async create(input) {\n if (input.isolation === \"required\") {\n return err<SandboxError>({\n code: \"unsupported\",\n message: \"The local provider runs on the host and cannot isolate. Choose a sandbox provider.\",\n retryable: false,\n });\n }\n if (input.network.mode !== \"unrestricted\") {\n return err<SandboxError>({\n code: \"unsupported\",\n message: `The local provider cannot enforce network mode '${input.network.mode}'.`,\n retryable: false,\n });\n }\n // Where a sandbox lives is a property of the provider, not of one\n // request: the port's `create` names no root, so one provider answers for\n // one directory and a second directory is a second provider.\n return createLocalSandbox({\n root: options.root ?? process.cwd(),\n // Nothing here can substitute anything: a process on the host reads\n // its own environment, and there is no egress to intercept. The two\n // arrive merged, and the sandbox says `secrets: \"plain\"` so a caller\n // that minds can decline.\n env: {\n ...input.env,\n ...Object.fromEntries((input.secrets ?? []).map((secret) => [secret.env, secret.value])),\n },\n });\n },\n };\n}\n\n/**\n * Not exported. A sandbox is reached through its provider, like every other\n * one — a second constructor beside `createLocalSandboxProvider` was a second\n * way to make the same thing, and the one that skipped the port.\n */\nasync function createLocalSandbox(\n input: { root: string; env: Readonly<Record<string, string>> },\n): Promise<Result<Sandbox, SandboxError>> {\n const root = resolve(input.root);\n await mkdir(root, { recursive: true });\n let closed = false;\n\n /**\n * A released sandbox is gone. Nothing was provisioned here, so closing frees\n * nothing — but a caller that closed and then ran a command has a bug, and\n * every other provider would have told it so.\n */\n const gone = (): Result<never, SandboxError> =>\n err({ code: \"unavailable\", message: \"Sandbox is closed\", retryable: false });\n\n /**\n * Whether `full` left `base`, reading the two paths and nothing else.\n *\n * A path that left is `..`, begins with a `..` segment, or has no relation to\n * the base at all. A *name* beginning with two dots is a file in it, and a\n * bare `startsWith(\"..\")` refused those too.\n */\n const escapes = (base: string, full: string): boolean => {\n const inside = relative(base, full);\n return inside === \"..\" || inside.startsWith(`..${sep}`) || isAbsolute(inside);\n };\n\n /**\n * The path a call would really touch.\n *\n * `realpath` fails on something that is not there yet, which `writeFile` is\n * entitled to be given — so the nearest existing ancestor is resolved and the\n * rest re-joined. That is enough: only an existing directory can be the\n * symlink, and a name that does not exist cannot redirect anything.\n */\n const settled = async (full: string): Promise<string> => {\n const parts: string[] = [];\n let at = full;\n for (;;) {\n try { return resolve(await realpath(at), ...parts.reverse()); }\n catch { /* not there yet — try its parent */ }\n const up = dirname(at);\n if (up === at) return full;\n parts.push(basename(at));\n at = up;\n }\n };\n\n const realRoot = await realpath(root);\n\n /**\n * Confines paths to the chosen root. Not isolation — a guard against slips —\n * but it does consult the filesystem, because a lexical check alone is not\n * even that.\n *\n * A symlink under the root pointing out of it is followed by every call that\n * matters, and `readFile`/`writeFile` are exactly the calls an application\n * hands to a model. Lexically `notes/link/passwd` never leaves the root; on\n * disk it was `/etc/passwd`. So the lexical test runs first, because it is\n * free and catches `../` before any syscall, and the resolved one decides.\n */\n const within = async (path: string): Promise<Result<string, SandboxError>> => {\n const full = isAbsolute(path) ? resolve(path) : resolve(join(root, path));\n const denied = err<SandboxError>({\n code: \"denied\", message: `Path escapes the sandbox root: ${path}`, retryable: false,\n });\n if (escapes(root, full)) return denied;\n return escapes(realRoot, await settled(full)) ? denied : ok(full);\n };\n\n return ok({\n id: `local:${root}`,\n root,\n isolation: \"none\",\n // The host's own environment is readable by anything running in it.\n secrets: \"plain\",\n\n async exec({ command, cwd, env, signal }) {\n if (closed) return gone();\n const target = cwd ? await within(cwd) : ok(root);\n if (!target.ok) return target;\n // Checked before anything is started: a caller that has already given up\n // gets `cancelled` rather than the command it no longer wants.\n if (signal?.aborted) return err(abortOrFail(undefined, signal));\n try {\n const child = spawnProcess(\"/bin/sh\", [\"-lc\", command], {\n cwd: target.value,\n env: { ...process.env, ...input.env, ...env },\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n const collected = new Promise<[string, string, number]>((settle, fail) => {\n const out: Buffer[] = [];\n const errs: Buffer[] = [];\n child.stdout.on(\"data\", (chunk: Buffer) => out.push(chunk));\n child.stderr.on(\"data\", (chunk: Buffer) => errs.push(chunk));\n child.on(\"error\", fail);\n child.on(\"close\", (code) => settle([\n Buffer.concat(out).toString(\"utf8\"),\n Buffer.concat(errs).toString(\"utf8\"),\n code ?? 1,\n ]));\n });\n // Raced, not awaited. Killing the shell does not settle this promptly:\n // the orphan it spawned holds the pipes open, so a `sleep 5` cancelled\n // at 200ms answered five seconds later — and by then the exit code\n // reads as an ordinary signal death rather than a cancellation.\n const finished = signal\n ? await Promise.race([\n collected.then((done) => ({ done })),\n new Promise<{ done?: undefined }>((settle) => {\n signal.addEventListener(\"abort\", () => settle({}), { once: true });\n }),\n ])\n : { done: await collected };\n if (!finished.done) {\n // The shell, not everything under it: a command that spawned its own\n // child leaves that child to finish. The same limit the container\n // adapter has, for the same reason — this is a process, not a group.\n child.kill();\n return err(abortOrFail(undefined, signal));\n }\n const [stdout, stderr, exitCode] = finished.done;\n // A non-zero exit is an outcome the caller reasons about, not an error.\n return ok({ stdout, stderr, exitCode });\n } catch (error) {\n return err(abortOrFail(error, signal));\n }\n },\n\n async spawn({ command, cwd, env }) {\n if (closed) return gone();\n const target = cwd ? await within(cwd) : ok(root);\n if (!target.ok) return target;\n const [executable, ...rest] = command;\n if (!executable) {\n return err<SandboxError>({ code: \"failed\", message: \"spawn needs a command\", retryable: false });\n }\n const child = spawnProcess(executable, rest, {\n cwd: target.value,\n env: { ...process.env, ...input.env, ...env },\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n // A launch failure arrives asynchronously, and an unhandled `error` event\n // is an uncaught exception — which in a service is the whole process, not\n // the sandbox. Awaited here so it comes back typed, and the listener stays\n // on so a later one is not fatal either.\n const launched = await new Promise<SandboxError | undefined>((settle) => {\n child.once(\"spawn\", () => settle(undefined));\n child.once(\"error\", (error: NodeJS.ErrnoException) => settle({\n code: \"unavailable\",\n message: error.code === \"ENOENT\" ? `No such command: ${executable}` : error.message,\n retryable: true,\n }));\n });\n if (launched) return err(launched);\n child.on(\"error\", () => { /* reported once; a later one is not fatal */ });\n child.stdin.on(\"error\", () => { /* the process ended before it read its input */ });\n return ok<SandboxProcess>({\n write: (chunk) => { child.stdin.write(chunk); },\n stdout: Readable.toWeb(child.stdout) as ReadableStream<Uint8Array>,\n stderr: Readable.toWeb(child.stderr) as ReadableStream<Uint8Array>,\n // `exit` fires when the process ends; `close` waits for its stdio to\n // close too, which a caller that never reads `stdout` may not cause —\n // `Readable.toWeb` leaves the pipe unread until somebody pulls from it.\n // Whichever arrives first answers, so a killed process is reported as\n // gone whether or not anyone was listening to it.\n exited: new Promise<number>((settle) => {\n child.on(\"exit\", (code) => settle(code ?? 1));\n child.on(\"close\", (code) => settle(code ?? 1));\n }),\n kill: () => {\n child.kill();\n // SIGTERM is a request. This port says the process can be killed, so\n // a process that declines to handle it does not get to leave `exited`\n // pending for as long as it likes. Cleared on exit, and unreferenced,\n // so a prompt death leaves nothing holding the loop open.\n const escalate = setTimeout(() => child.kill(\"SIGKILL\"), 2_000);\n escalate.unref?.();\n child.once(\"exit\", () => clearTimeout(escalate));\n },\n });\n },\n\n async readFile({ path, signal }) {\n if (closed) return gone();\n const target = await within(path);\n if (!target.ok) return target;\n if (signal?.aborted) return err(abortOrFail(undefined, signal));\n try { return ok(await readFile(target.value, \"utf8\")); }\n catch (error) { return err(fileError(error, path)); }\n },\n\n async writeFile({ path, content, signal }) {\n if (closed) return gone();\n const target = await within(path);\n if (!target.ok) return target;\n if (signal?.aborted) return err(abortOrFail(undefined, signal));\n try {\n await mkdir(join(target.value, \"..\"), { recursive: true });\n await writeFile(target.value, content, \"utf8\");\n return ok(undefined);\n } catch (error) { return err(fileError(error, path)); }\n },\n\n async close() { closed = true; return ok(undefined); /* nothing was provisioned to release */ },\n });\n}\n\nfunction abortOrFail(error: unknown, signal?: AbortSignal): SandboxError {\n if (signal?.aborted) {\n const timedOut = (signal.reason as Error | undefined)?.name === \"TimeoutError\";\n // A deadline may be met on a second attempt with a longer one; a caller\n // that changed its mind has nothing to act on. The container adapter agrees.\n return timedOut\n ? { code: \"timeout\", message: \"Command timed out\", retryable: true }\n : { code: \"cancelled\", message: \"Command cancelled\", retryable: false };\n }\n return { code: \"failed\", message: error instanceof Error ? error.message : String(error), retryable: false };\n}\n\nconst fileError = (error: unknown, path: string): SandboxError =>\n (error as { code?: string }).code === \"ENOENT\"\n ? { code: \"not-found\", message: `No such file: ${path}`, retryable: false }\n : { code: \"failed\", message: error instanceof Error ? error.message : String(error), retryable: false };\n"]}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What any implementation of the sandbox port must do.
|
|
3
|
+
*
|
|
4
|
+
* The port's central claim is that isolation is requested and either enforced
|
|
5
|
+
* or refused. A provider is the only thing that can honour it, and a provider
|
|
6
|
+
* that overstates what it enforces is worse than one that refuses: the caller
|
|
7
|
+
* asked a question precisely so it would not have to trust an adjective.
|
|
8
|
+
*
|
|
9
|
+
* Two implementations ship, a directory on the host and a container, and they
|
|
10
|
+
* are the same seam only if they answer the same way — down to whether a
|
|
11
|
+
* command's own non-zero exit is an outcome or a failure, whether a missing
|
|
12
|
+
* file is `not-found` or an empty string, and whether a path leaving the root
|
|
13
|
+
* is denied or quietly resolved. Where that differs it differs in production,
|
|
14
|
+
* on the provider nobody develops against.
|
|
15
|
+
*
|
|
16
|
+
* Inert on purpose. Each case is a name and a function that throws, so the
|
|
17
|
+
* suite drags no test framework into the package:
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* for (const item of defineSandboxConformance(subject)) test(item.name, item.run);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import type { NetworkPolicy, Sandbox, SandboxProvider } from "./sandbox.js";
|
|
24
|
+
/** One case: a name, and a function that throws when the contract is broken. */
|
|
25
|
+
export interface ConformanceCase {
|
|
26
|
+
name: string;
|
|
27
|
+
run(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export interface SandboxUnderTest {
|
|
30
|
+
/**
|
|
31
|
+
* The provider under test. A function, not a value, because constructing one
|
|
32
|
+
* may need a credential this run does not have — and a suite that is about to
|
|
33
|
+
* skip every case must not have thrown to find that out.
|
|
34
|
+
*/
|
|
35
|
+
provider(): SandboxProvider;
|
|
36
|
+
/**
|
|
37
|
+
* What this provider enforces when isolation is required.
|
|
38
|
+
*
|
|
39
|
+
* `"none"` is a claim, not an omission: a provider that runs on the host must
|
|
40
|
+
* refuse `isolation: "required"` rather than hand back something labelled
|
|
41
|
+
* unisolated, and this suite holds it to that.
|
|
42
|
+
*/
|
|
43
|
+
isolation: Sandbox["isolation"];
|
|
44
|
+
/** The outbound posture every case is created with. */
|
|
45
|
+
network: NetworkPolicy;
|
|
46
|
+
/**
|
|
47
|
+
* Postures this provider does not enforce. Each must be refused before
|
|
48
|
+
* anything is provisioned — a policy silently ignored is the one failure a
|
|
49
|
+
* caller cannot detect from the outside.
|
|
50
|
+
*/
|
|
51
|
+
refuses?: readonly NetworkPolicy[];
|
|
52
|
+
/**
|
|
53
|
+
* Whether a command's two output streams arrive apart.
|
|
54
|
+
*
|
|
55
|
+
* Declared rather than assumed, because not every backend has them: a hosted
|
|
56
|
+
* box may expose one combined stream, and an adapter over it puts everything
|
|
57
|
+
* in `stdout` and leaves `stderr` empty rather than inventing a split. That
|
|
58
|
+
* is a real difference to an application parsing stderr, so it is said out
|
|
59
|
+
* loud here instead of discovered. Defaults to `"separate"`.
|
|
60
|
+
*/
|
|
61
|
+
streams?: "separate" | "combined";
|
|
62
|
+
/**
|
|
63
|
+
* What this provider does with a `Secret`.
|
|
64
|
+
*
|
|
65
|
+
* `"substituted"` is a strong claim — the value never enters the box — so the
|
|
66
|
+
* suite checks that a process there genuinely cannot read it. `"plain"` is
|
|
67
|
+
* checked the other way: the value has to actually arrive, because a provider
|
|
68
|
+
* that quietly dropped it would leave every agent unauthenticated with
|
|
69
|
+
* nothing saying why. Defaults to `"plain"`.
|
|
70
|
+
*/
|
|
71
|
+
secrets?: Sandbox["secrets"];
|
|
72
|
+
}
|
|
73
|
+
export declare function defineSandboxConformance(subject: SandboxUnderTest): readonly ConformanceCase[];
|