@theokit/sdk 5.12.0 → 5.12.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.12.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#773](https://github.com/usetheokit/theokit-sdk/pull/773) [`ecfabd8`](https://github.com/usetheokit/theokit-sdk/commit/ecfabd8639da45573bb4283a2b6b0a6c98daf97a) Thanks [@usetheodev](https://github.com/usetheodev)! - Security: a command confined by `LinuxSandbox` or `interactiveWrapCommand` can no longer weaken the
8
+ seccomp filter that later confined commands load. The filter used to be read from a temp file under
9
+ `/tmp`, which `workspace-write` commands can write, so one command could replace it with an
10
+ allow-all program. Both wraps now pipe the program to bubblewrap from memory, and no file holds it.
11
+
12
+ `wrapCommandForSandbox` takes a new optional `seccompProgram` (the program's bytes, a positive
13
+ multiple of 8; a `TypeError` otherwise). It wins over `seccompPath` when both are given.
14
+
15
+ `seccompPath`, `restrictedSeccompPath` and `seccompPathForArch` are deprecated in favour of
16
+ `seccompProgram`, and keep working. A `seccompPath` that `restrictedSeccompPath` or
17
+ `seccompPathForArch` returned is now piped from the bytes the process wrote, so a sandbox started
18
+ without the filter (a network-open one mounts `/tmp` writable) can replace the file without effect.
19
+ For any other path the wrap binds the filter's directory read-only, or the file where the directory
20
+ is a writable root or contains one. That is only a partial fix: the bind guards the file only from
21
+ the command that loads it, a temp dir nested inside a writable root can still be swapped by renaming
22
+ an ancestor, and where only the file is bound its parent can be renamed. Code that reads the
23
+ helper's file itself is not covered either.
24
+
25
+ The piped wrap passes the program, bubblewrap's arguments and the command to `/bin/sh` as
26
+ positional arguments, so each is shell-escaped once. Every sandbox wrap also writes a backslash that
27
+ precedes a quote, a backslash or the end of a word outside the single quotes, so the wrap parses to
28
+ the same words under fish as under sh. Before, when the PTY's `$SHELL` was fish, a command containing
29
+ `\'` could end the quoting early and run its tail in the host shell, outside bubblewrap.
30
+
3
31
  ## 5.12.0
4
32
 
5
33
  ### Minor Changes
@@ -8,7 +8,8 @@ var path = require('path');
8
8
 
9
9
  // src/sandbox/shell-escape.ts
10
10
  function shellEscapePosix(arg) {
11
- return `'${arg.replace(/'/g, "'\\''")}'`;
11
+ const quotesEscaped = arg.replace(/'/g, "'\\''");
12
+ return `'${quotesEscaped.replace(/\\(?=\\|'\\''|$)/g, "'\\\\'")}'`;
12
13
  }
13
14
 
14
15
  // src/sandbox/types.ts
@@ -168,5 +169,5 @@ exports.SandboxNotAvailableError = SandboxNotAvailableError;
168
169
  exports.SandboxSecurityError = SandboxSecurityError;
169
170
  exports.resolveSandbox = resolveSandbox;
170
171
  exports.shellEscapePosix = shellEscapePosix;
171
- //# sourceMappingURL=chunk-BRWI4HLK.cjs.map
172
- //# sourceMappingURL=chunk-BRWI4HLK.cjs.map
172
+ //# sourceMappingURL=chunk-EPANMGH7.cjs.map
173
+ //# sourceMappingURL=chunk-EPANMGH7.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sandbox/shell-escape.ts","../src/sandbox/types.ts","../src/sandbox/local-sandbox.ts"],"names":["TheokitAgentError","ConfigurationError","execFile","resolveChildEnv","path","mkdir","dirname","fsWriteFile"],"mappings":"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","file":"chunk-EPANMGH7.cjs","sourcesContent":["/**\n * POSIX shell escaping for values interpolated into a `SandboxBackend.execute`\n * command string. `execute` runs via `/bin/sh -c`, so any untrusted value\n * (repo URL, ref, path) MUST be quoted to prevent command injection.\n *\n * @internal\n */\n\n/**\n * Wrap `arg` in single quotes, escaping embedded single quotes (`'\\''`). A backslash before a quote,\n * a backslash or the end is written outside the quotes too (`'\\\\'`), so the word parses the same\n * under fish, which reads `\\'` and `\\\\` inside single quotes as escapes, as under sh (B-600): the PTY\n * runs the sandbox wrap under the user's `$SHELL`. After the first pass an original quote reads\n * `'\\''`, whose own backslash is always followed by `''`, so the second pass never matches it.\n */\nexport function shellEscapePosix(arg: string): string {\n const quotesEscaped = arg.replace(/'/g, \"'\\\\''\");\n return `'${quotesEscaped.replace(/\\\\(?=\\\\|'\\\\''|$)/g, \"'\\\\\\\\'\")}'`;\n}\n","/**\n * Sandbox backend protocol — pluggable execution environment for agent tools.\n *\n * Per ADR D1: only 2 abstract methods (`execute` + `uploadFile`). All\n * higher-level operations are derived on the base class. New backends\n * (Docker, Firecracker, E2B) only implement those 2 methods.\n *\n * @public\n */\n\nimport { ConfigurationError, TheokitAgentError } from \"../errors.js\";\nimport type { EnvPolicy } from \"../types/env-policy.js\";\nimport { shellEscapePosix } from \"./shell-escape.js\";\n\n/**\n * What a {@link SandboxBackend} returns for one command.\n *\n * `exitCode` is a coarse signal rather than the child's real exit status: the backends in this\n * package report `0` on success, `124` on timeout, and `1` for everything else — including a command\n * that exited with its own non-zero code. Read `timedOut` to tell a killed command from a failed one,\n * and `stderr` for the reason; branching on a specific exit code will not work here.\n *\n * `stdout` and `stderr` are truncated independently at `SandboxConfig.maxOutputBytes`, with a\n * trailing `...(truncated)` marker. Neither is safe to parse as a complete document without checking\n * for it.\n */\nexport interface ExecuteResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n timedOut: boolean;\n}\n\n/**\n * Construction-time settings shared by every {@link SandboxBackend}. All fields are optional and the\n * base constructor fills them in, so `{}` is a valid config: `workDir` `/tmp`, `timeoutMs` 30000,\n * `maxOutputBytes` 5 MiB, `env` `\"inherit-scrubbed\"`.\n *\n * `workDir` is the child process's cwd and the base a relative `uploadFile` path resolves against. It\n * is not a boundary — under `LocalSandbox` nothing stops a command, or an absolute path, from leaving\n * it. Only `LinuxSandbox` makes it enforceable.\n *\n * `timeoutMs` is the default for every call; `execute(command, { timeoutMs })` overrides it per\n * command. On expiry the child is killed and the result carries `timedOut: true` with `exitCode`\n * 124 — the partial output collected so far is still returned.\n */\nexport interface SandboxConfig {\n workDir?: string;\n timeoutMs?: number;\n maxOutputBytes?: number;\n /**\n * #54 — env inherit/scrub policy for the executed command's child process.\n * Defaults to `\"inherit-scrubbed\"` (drop secret-like vars: `*KEY*`, `*SECRET*`,\n * `*TOKEN*`, `*PASSWORD*`, `*_AUTH*`). Pass `\"all\"` to restore full inheritance\n * or `\"core\"` for a minimal safe allowlist.\n */\n env?: EnvPolicy;\n}\n\n/**\n * A backend refused a command on policy grounds — the environment works, and it said no. Carries a\n * stable `code: \"sandbox_security\"` for callers that prefer not to match on the class.\n *\n * Nothing in this package throws it, which is worth knowing before you write a `catch` for it:\n * `LocalSandbox` and `LinuxSandbox` report a refusal through {@link ExecuteResult} (non-zero\n * `exitCode`, reason on `stderr`), and a write blocked under confinement surfaces as a plain `Error`\n * from `uploadFile`. This type exists so a custom backend that must reject a command BEFORE spawning\n * anything — a container or VM runner enforcing its own policy — can do so in the protocol's\n * vocabulary instead of inventing an error of its own.\n */\nexport class SandboxSecurityError extends TheokitAgentError {\n override readonly name = \"SandboxSecurityError\";\n override readonly code = \"sandbox_security\" as const;\n constructor(message: string) {\n // not retryable: a refused command is refused every time; retrying re-asks the same question\n super(message, { code: \"sandbox_security\", isRetryable: false });\n }\n}\n\n/**\n * A backend cannot be used at all: its runtime is missing or unusable. The distinction from\n * {@link SandboxSecurityError} is which side failed — there the sandbox worked and denied the\n * command, here there is no sandbox. Carries `code: \"sandbox_not_available\"`.\n *\n * Nothing in this package throws it either, and that is a position rather than an oversight:\n * `createSandboxBackend` treats a missing bubblewrap as a degradation, warns once, and hands back an\n * unconfined `LocalSandbox`. Throw this instead from a backend that must never silently run outside\n * its isolation.\n */\nexport class SandboxNotAvailableError extends TheokitAgentError {\n override readonly name = \"SandboxNotAvailableError\";\n override readonly code = \"sandbox_not_available\" as const;\n constructor(message: string) {\n // not retryable: a missing runtime does not appear on a second attempt\n super(message, { code: \"sandbox_not_available\", isRetryable: false });\n }\n}\n\n/**\n * The execution capability handed to an agent tool, and the extension point for new environments.\n *\n * A backend implements exactly two methods, `execute` and `uploadFile`. Everything else here —\n * `readFile`, `glob`, `grep`, `listDir` — is derived from `execute` by shelling out `cat`, `find`,\n * `grep` and `ls`, so a subclass that overrides `execute` alone inherits confined reads, searches and\n * listings for free. It does not inherit confined writes: `writeFile` delegates to `uploadFile`, and\n * that is the second override `LinuxSandbox` needs.\n *\n * Choosing one: `LocalSandbox` runs the command on the host with no isolation and suits code you\n * already trust; `LinuxSandbox` adds kernel confinement where bubblewrap is available;\n * `createSandboxBackend` probes and picks between them, and is the right entry point unless you have\n * a reason to pin one. For genuinely untrusted code you want a container or VM backend, which this\n * package does not ship — write it against these two methods.\n *\n * Because the derived helpers assemble shell command lines (arguments escaped for POSIX `sh`), a\n * backend whose `execute` is not a POSIX shell must override them as well.\n */\nexport abstract class SandboxBackend {\n protected config: SandboxConfig;\n\n constructor(config: SandboxConfig = {}) {\n this.config = {\n workDir: config.workDir ?? \"/tmp\",\n timeoutMs: config.timeoutMs ?? 30_000,\n maxOutputBytes: config.maxOutputBytes ?? 5 * 1024 * 1024,\n // #54 — preserve the env policy so backends can scrub secrets.\n env: config.env ?? \"inherit-scrubbed\",\n };\n }\n\n abstract execute(command: string, opts?: { timeoutMs?: number }): Promise<ExecuteResult>;\n\n abstract uploadFile(path: string, content: string | Buffer): Promise<void>;\n\n async readFile(path: string): Promise<string> {\n const result = await this.execute(`cat ${this.shellEscape(path)}`);\n if (result.exitCode !== 0) {\n throw new Error(`readFile failed: ${result.stderr}`);\n }\n return result.stdout;\n }\n\n async writeFile(path: string, content: string): Promise<void> {\n await this.uploadFile(path, content);\n }\n\n async glob(pattern: string, cwd?: string): Promise<string[]> {\n const dir = cwd ?? this.config.workDir ?? \".\";\n const result = await this.execute(\n `find ${this.shellEscape(dir)} -name ${this.shellEscape(pattern)} -type f 2>/dev/null`,\n );\n this.assertCommandRan(\"glob\", result);\n return result.stdout.trim().split(\"\\n\").filter(Boolean);\n }\n\n async grep(pattern: string, path?: string): Promise<string[]> {\n const target = path ?? \".\";\n const result = await this.execute(\n `grep -rn ${this.shellEscape(pattern)} ${this.shellEscape(target)} 2>/dev/null`,\n );\n // `grep` exits 1 for NO MATCH and >= 2 for an error, so a 1 is an answer and is not asserted on.\n if (result.exitCode !== 1) this.assertCommandRan(\"grep\", result);\n return result.stdout.trim().split(\"\\n\").filter(Boolean);\n }\n\n async listDir(path: string): Promise<string[]> {\n const result = await this.execute(`ls -1 ${this.shellEscape(path)}`);\n this.assertCommandRan(\"listDir\", result);\n return result.stdout.trim().split(\"\\n\").filter(Boolean);\n }\n\n /**\n * Turns \"the command did not run\" into an error instead of an empty array.\n *\n * `glob`, `grep` and `listDir` used to `return []` on any non-zero exit, so a search that could not\n * run reported the same thing as a search that found nothing — opposite facts, one normal and one\n * meaning the agent is looking at a filesystem it cannot read. That is the failure mode a backend\n * whose `execute` is not a POSIX shell hits, which this class's own docblock warns about in prose\n * and could not enforce.\n *\n * A genuine no-match is still `[]`: `find` exits 0 with empty output, and `grep`'s exit 1 is\n * checked by its caller before this is reached.\n */\n private assertCommandRan(operation: string, result: ExecuteResult): void {\n if (result.exitCode === 0) return;\n throw new ConfigurationError(\n `${operation} failed (exit ${String(result.exitCode)}): ${result.stderr.trim() || \"no stderr\"}. ` +\n `The derived helpers shell out to POSIX \\`cat\\`/\\`find\\`/\\`grep\\`/\\`ls\\` — a backend whose ` +\n `execute() is not a POSIX shell must override them.`,\n { code: \"sandbox_derived_helper_failed\" },\n );\n }\n\n protected truncateOutput(output: string): string {\n const max = this.config.maxOutputBytes ?? 5 * 1024 * 1024;\n if (Buffer.byteLength(output) > max) {\n return `${output.slice(0, max)}\\n...(truncated)`;\n }\n return output;\n }\n\n private shellEscape(arg: string): string {\n return shellEscapePosix(arg);\n }\n}\n\n/**\n * A backend OR a per-request resolver of one — mirrors `FilesystemProvider` / `InteractiveProvider`.\n * A resolver runs at tool-execution time (request scope), so a multi-tenant / multi-role agent gets a\n * distinct sandbox per request without a shared mutable one. This is how a tool takes execution as an\n * INJECTED capability: the same tool runs on a local sandbox, a container/E2B backend (cluster/web), or\n * any future backend, with no direct `child_process` import.\n *\n * @public\n */\nexport type SandboxProvider<Ctx = unknown> =\n | SandboxBackend\n | ((ctx: Ctx) => SandboxBackend | Promise<SandboxBackend>);\n\n/** Resolve a {@link SandboxProvider} to a concrete backend for `ctx`. */\nexport async function resolveSandbox<Ctx>(\n provider: SandboxProvider<Ctx>,\n ctx: Ctx,\n): Promise<SandboxBackend> {\n return provider instanceof SandboxBackend ? provider : provider(ctx);\n}\n","/**\n * LocalSandbox — subprocess-based execution. **This is NOT an isolation\n * boundary.** It runs the command via `/bin/sh -c` in the SAME OS as the host\n * with the host's filesystem and network fully reachable — it provides NO\n * process, filesystem, or network isolation. Its only safety affordances are:\n * - a wall-clock timeout (kills a runaway command),\n * - an output-size cap (bounds memory), and\n * - env scrubbing (#54): secret-like parent env vars (`*KEY*`/`*SECRET*`/\n * `*TOKEN*`/`*PASSWORD*`/`*_AUTH*`) are dropped from the child by default\n * (`SandboxConfig.env`), so a shell tool cannot exfiltrate host secrets via\n * the environment.\n *\n * For real isolation (untrusted code), use a container/VM backend — NOT this.\n *\n * @public\n */\n\nimport { execFile } from \"node:child_process\";\nimport { writeFile as fsWriteFile, mkdir } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\n\nimport { resolveChildEnv } from \"../internal/runtime/lifecycle/env-policy.js\";\nimport { type ExecuteResult, SandboxBackend, type SandboxConfig } from \"./types.js\";\n\n/**\n * Runs a command with `/bin/sh -c` on the host. NOT an isolation boundary — the\n * module note above states exactly what it does and does not protect.\n *\n * const sandbox = new LocalSandbox({ workDir: \"/srv/repo\", timeoutMs: 10_000 });\n * const { stdout, exitCode, timedOut } = await sandbox.execute(\"ls -1\");\n *\n * Needs a POSIX host with `/bin/sh`. Every helper inherited from\n * {@link SandboxBackend} — `readFile`, `glob`, `grep`, `listDir` — is built on\n * this `execute`, so they inherit that requirement too.\n *\n * How it fails: `execute` NEVER rejects. Every outcome, including a failure to\n * spawn, comes back as an {@link ExecuteResult}; read `timedOut` first\n * (`true` implies `exitCode` 124) and `stderr` for the reason. `uploadFile` DOES\n * reject, with the raw `node:fs` error, on a bad path or missing permission.\n *\n * Traps:\n * - `exitCode` is not the child's exit code. It is 0, 124 (timed out) or 1 — a\n * command that exits 3 is reported as 1, so branching on a specific code does\n * not work here.\n * - Output beyond `maxOutputBytes` (default 5 MiB) makes Node kill the child.\n * The result is `exitCode: 1` and `timedOut: false`, with the cut stream\n * carrying the `...(truncated)` marker described on `ExecuteResult` (#363).\n * Since `exitCode` is 1 either way, that marker is what tells a lost-output\n * result from a command that genuinely failed.\n * - `uploadFile` resolves a relative path against `workDir` but does not contain\n * it: an absolute path, or one containing `..`, writes wherever the process\n * can. `workDir` is a starting directory, not a jail.\n * - Secret-like parent env vars are dropped by default. Passing\n * `env: \"all\"` in {@link SandboxConfig} puts the host's API keys back within\n * reach of any command the agent runs.\n */\nexport class LocalSandbox extends SandboxBackend {\n constructor(config: SandboxConfig = {}) {\n super(config);\n }\n\n async execute(command: string, opts?: { timeoutMs?: number }): Promise<ExecuteResult> {\n const timeout = opts?.timeoutMs ?? this.config.timeoutMs ?? 30_000;\n const max = this.config.maxOutputBytes ?? 5 * 1024 * 1024;\n\n return new Promise<ExecuteResult>((resolve) => {\n const child = execFile(\n \"/bin/sh\",\n [\"-c\", command],\n {\n cwd: this.config.workDir,\n timeout,\n maxBuffer: max,\n encoding: \"utf-8\",\n // #54 — scrub secret-like host env vars from the child by default.\n env: resolveChildEnv({ policy: this.config.env }),\n },\n (error, stdout, stderr) => {\n resolve(this.buildResult(error, stdout ?? \"\", stderr ?? \"\"));\n },\n );\n\n // Safety: if child somehow doesn't callback\n child.on(\"error\", () => {\n resolve({ stdout: \"\", stderr: \"spawn error\", exitCode: 1, timedOut: false });\n });\n });\n }\n\n private buildResult(error: Error | null, stdout: string, stderr: string): ExecuteResult {\n const timedOut = error !== null && \"killed\" in error && (error as { killed: boolean }).killed;\n // #363 — the ONLY reliable signal that output was lost. Node caps the buffer AT `maxBuffer`, so\n // for ASCII output the string comes back exactly at the cap and `truncateOutput`'s `> max` test\n // never fires; a length comparison cannot tell a cut document from a complete one that happens\n // to be that long. The child being killed for overflow can.\n const overflowed =\n error !== null && (error as { code?: unknown }).code === \"ERR_CHILD_PROCESS_STDIO_MAXBUFFER\";\n return {\n stdout: this.markIfCut(this.truncateOutput(stdout), overflowed),\n stderr: this.markIfCut(this.truncateOutput(stderr), overflowed),\n exitCode: timedOut ? 124 : error ? 1 : 0,\n timedOut,\n };\n }\n\n /**\n * Append the `...(truncated)` marker `ExecuteResult` tells callers to branch on, when the child\n * was killed for exceeding `maxOutputBytes` and this stream is the one sitting at the cap.\n *\n * `truncateOutput` has already run, so an output it marked is left alone rather than marked\n * twice. Node does not say WHICH stream overflowed, so the stream at the cap is the one that\n * lost data — the other, being shorter, is complete.\n */\n private markIfCut(output: string, overflowed: boolean): string {\n if (!overflowed) return output;\n if (output.endsWith(\"...(truncated)\")) return output;\n const max = this.config.maxOutputBytes ?? 5 * 1024 * 1024;\n return Buffer.byteLength(output) >= max ? `${output}\\n...(truncated)` : output;\n }\n\n async uploadFile(path: string, content: string | Buffer): Promise<void> {\n const fullPath = path.startsWith(\"/\") ? path : `${this.config.workDir}/${path}`;\n await mkdir(dirname(fullPath), { recursive: true });\n await fsWriteFile(fullPath, content, \"utf-8\");\n }\n}\n"]}
@@ -6,7 +6,8 @@ import { dirname } from 'path';
6
6
 
7
7
  // src/sandbox/shell-escape.ts
8
8
  function shellEscapePosix(arg) {
9
- return `'${arg.replace(/'/g, "'\\''")}'`;
9
+ const quotesEscaped = arg.replace(/'/g, "'\\''");
10
+ return `'${quotesEscaped.replace(/\\(?=\\|'\\''|$)/g, "'\\\\'")}'`;
10
11
  }
11
12
 
12
13
  // src/sandbox/types.ts
@@ -161,5 +162,5 @@ var LocalSandbox = class extends SandboxBackend {
161
162
  };
162
163
 
163
164
  export { LocalSandbox, SandboxBackend, SandboxNotAvailableError, SandboxSecurityError, resolveSandbox, shellEscapePosix };
164
- //# sourceMappingURL=chunk-EEK7S77K.js.map
165
- //# sourceMappingURL=chunk-EEK7S77K.js.map
165
+ //# sourceMappingURL=chunk-NBNRQN6T.js.map
166
+ //# sourceMappingURL=chunk-NBNRQN6T.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sandbox/shell-escape.ts","../src/sandbox/types.ts","../src/sandbox/local-sandbox.ts"],"names":["fsWriteFile"],"mappings":"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","file":"chunk-NBNRQN6T.js","sourcesContent":["/**\n * POSIX shell escaping for values interpolated into a `SandboxBackend.execute`\n * command string. `execute` runs via `/bin/sh -c`, so any untrusted value\n * (repo URL, ref, path) MUST be quoted to prevent command injection.\n *\n * @internal\n */\n\n/**\n * Wrap `arg` in single quotes, escaping embedded single quotes (`'\\''`). A backslash before a quote,\n * a backslash or the end is written outside the quotes too (`'\\\\'`), so the word parses the same\n * under fish, which reads `\\'` and `\\\\` inside single quotes as escapes, as under sh (B-600): the PTY\n * runs the sandbox wrap under the user's `$SHELL`. After the first pass an original quote reads\n * `'\\''`, whose own backslash is always followed by `''`, so the second pass never matches it.\n */\nexport function shellEscapePosix(arg: string): string {\n const quotesEscaped = arg.replace(/'/g, \"'\\\\''\");\n return `'${quotesEscaped.replace(/\\\\(?=\\\\|'\\\\''|$)/g, \"'\\\\\\\\'\")}'`;\n}\n","/**\n * Sandbox backend protocol — pluggable execution environment for agent tools.\n *\n * Per ADR D1: only 2 abstract methods (`execute` + `uploadFile`). All\n * higher-level operations are derived on the base class. New backends\n * (Docker, Firecracker, E2B) only implement those 2 methods.\n *\n * @public\n */\n\nimport { ConfigurationError, TheokitAgentError } from \"../errors.js\";\nimport type { EnvPolicy } from \"../types/env-policy.js\";\nimport { shellEscapePosix } from \"./shell-escape.js\";\n\n/**\n * What a {@link SandboxBackend} returns for one command.\n *\n * `exitCode` is a coarse signal rather than the child's real exit status: the backends in this\n * package report `0` on success, `124` on timeout, and `1` for everything else — including a command\n * that exited with its own non-zero code. Read `timedOut` to tell a killed command from a failed one,\n * and `stderr` for the reason; branching on a specific exit code will not work here.\n *\n * `stdout` and `stderr` are truncated independently at `SandboxConfig.maxOutputBytes`, with a\n * trailing `...(truncated)` marker. Neither is safe to parse as a complete document without checking\n * for it.\n */\nexport interface ExecuteResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n timedOut: boolean;\n}\n\n/**\n * Construction-time settings shared by every {@link SandboxBackend}. All fields are optional and the\n * base constructor fills them in, so `{}` is a valid config: `workDir` `/tmp`, `timeoutMs` 30000,\n * `maxOutputBytes` 5 MiB, `env` `\"inherit-scrubbed\"`.\n *\n * `workDir` is the child process's cwd and the base a relative `uploadFile` path resolves against. It\n * is not a boundary — under `LocalSandbox` nothing stops a command, or an absolute path, from leaving\n * it. Only `LinuxSandbox` makes it enforceable.\n *\n * `timeoutMs` is the default for every call; `execute(command, { timeoutMs })` overrides it per\n * command. On expiry the child is killed and the result carries `timedOut: true` with `exitCode`\n * 124 — the partial output collected so far is still returned.\n */\nexport interface SandboxConfig {\n workDir?: string;\n timeoutMs?: number;\n maxOutputBytes?: number;\n /**\n * #54 — env inherit/scrub policy for the executed command's child process.\n * Defaults to `\"inherit-scrubbed\"` (drop secret-like vars: `*KEY*`, `*SECRET*`,\n * `*TOKEN*`, `*PASSWORD*`, `*_AUTH*`). Pass `\"all\"` to restore full inheritance\n * or `\"core\"` for a minimal safe allowlist.\n */\n env?: EnvPolicy;\n}\n\n/**\n * A backend refused a command on policy grounds — the environment works, and it said no. Carries a\n * stable `code: \"sandbox_security\"` for callers that prefer not to match on the class.\n *\n * Nothing in this package throws it, which is worth knowing before you write a `catch` for it:\n * `LocalSandbox` and `LinuxSandbox` report a refusal through {@link ExecuteResult} (non-zero\n * `exitCode`, reason on `stderr`), and a write blocked under confinement surfaces as a plain `Error`\n * from `uploadFile`. This type exists so a custom backend that must reject a command BEFORE spawning\n * anything — a container or VM runner enforcing its own policy — can do so in the protocol's\n * vocabulary instead of inventing an error of its own.\n */\nexport class SandboxSecurityError extends TheokitAgentError {\n override readonly name = \"SandboxSecurityError\";\n override readonly code = \"sandbox_security\" as const;\n constructor(message: string) {\n // not retryable: a refused command is refused every time; retrying re-asks the same question\n super(message, { code: \"sandbox_security\", isRetryable: false });\n }\n}\n\n/**\n * A backend cannot be used at all: its runtime is missing or unusable. The distinction from\n * {@link SandboxSecurityError} is which side failed — there the sandbox worked and denied the\n * command, here there is no sandbox. Carries `code: \"sandbox_not_available\"`.\n *\n * Nothing in this package throws it either, and that is a position rather than an oversight:\n * `createSandboxBackend` treats a missing bubblewrap as a degradation, warns once, and hands back an\n * unconfined `LocalSandbox`. Throw this instead from a backend that must never silently run outside\n * its isolation.\n */\nexport class SandboxNotAvailableError extends TheokitAgentError {\n override readonly name = \"SandboxNotAvailableError\";\n override readonly code = \"sandbox_not_available\" as const;\n constructor(message: string) {\n // not retryable: a missing runtime does not appear on a second attempt\n super(message, { code: \"sandbox_not_available\", isRetryable: false });\n }\n}\n\n/**\n * The execution capability handed to an agent tool, and the extension point for new environments.\n *\n * A backend implements exactly two methods, `execute` and `uploadFile`. Everything else here —\n * `readFile`, `glob`, `grep`, `listDir` — is derived from `execute` by shelling out `cat`, `find`,\n * `grep` and `ls`, so a subclass that overrides `execute` alone inherits confined reads, searches and\n * listings for free. It does not inherit confined writes: `writeFile` delegates to `uploadFile`, and\n * that is the second override `LinuxSandbox` needs.\n *\n * Choosing one: `LocalSandbox` runs the command on the host with no isolation and suits code you\n * already trust; `LinuxSandbox` adds kernel confinement where bubblewrap is available;\n * `createSandboxBackend` probes and picks between them, and is the right entry point unless you have\n * a reason to pin one. For genuinely untrusted code you want a container or VM backend, which this\n * package does not ship — write it against these two methods.\n *\n * Because the derived helpers assemble shell command lines (arguments escaped for POSIX `sh`), a\n * backend whose `execute` is not a POSIX shell must override them as well.\n */\nexport abstract class SandboxBackend {\n protected config: SandboxConfig;\n\n constructor(config: SandboxConfig = {}) {\n this.config = {\n workDir: config.workDir ?? \"/tmp\",\n timeoutMs: config.timeoutMs ?? 30_000,\n maxOutputBytes: config.maxOutputBytes ?? 5 * 1024 * 1024,\n // #54 — preserve the env policy so backends can scrub secrets.\n env: config.env ?? \"inherit-scrubbed\",\n };\n }\n\n abstract execute(command: string, opts?: { timeoutMs?: number }): Promise<ExecuteResult>;\n\n abstract uploadFile(path: string, content: string | Buffer): Promise<void>;\n\n async readFile(path: string): Promise<string> {\n const result = await this.execute(`cat ${this.shellEscape(path)}`);\n if (result.exitCode !== 0) {\n throw new Error(`readFile failed: ${result.stderr}`);\n }\n return result.stdout;\n }\n\n async writeFile(path: string, content: string): Promise<void> {\n await this.uploadFile(path, content);\n }\n\n async glob(pattern: string, cwd?: string): Promise<string[]> {\n const dir = cwd ?? this.config.workDir ?? \".\";\n const result = await this.execute(\n `find ${this.shellEscape(dir)} -name ${this.shellEscape(pattern)} -type f 2>/dev/null`,\n );\n this.assertCommandRan(\"glob\", result);\n return result.stdout.trim().split(\"\\n\").filter(Boolean);\n }\n\n async grep(pattern: string, path?: string): Promise<string[]> {\n const target = path ?? \".\";\n const result = await this.execute(\n `grep -rn ${this.shellEscape(pattern)} ${this.shellEscape(target)} 2>/dev/null`,\n );\n // `grep` exits 1 for NO MATCH and >= 2 for an error, so a 1 is an answer and is not asserted on.\n if (result.exitCode !== 1) this.assertCommandRan(\"grep\", result);\n return result.stdout.trim().split(\"\\n\").filter(Boolean);\n }\n\n async listDir(path: string): Promise<string[]> {\n const result = await this.execute(`ls -1 ${this.shellEscape(path)}`);\n this.assertCommandRan(\"listDir\", result);\n return result.stdout.trim().split(\"\\n\").filter(Boolean);\n }\n\n /**\n * Turns \"the command did not run\" into an error instead of an empty array.\n *\n * `glob`, `grep` and `listDir` used to `return []` on any non-zero exit, so a search that could not\n * run reported the same thing as a search that found nothing — opposite facts, one normal and one\n * meaning the agent is looking at a filesystem it cannot read. That is the failure mode a backend\n * whose `execute` is not a POSIX shell hits, which this class's own docblock warns about in prose\n * and could not enforce.\n *\n * A genuine no-match is still `[]`: `find` exits 0 with empty output, and `grep`'s exit 1 is\n * checked by its caller before this is reached.\n */\n private assertCommandRan(operation: string, result: ExecuteResult): void {\n if (result.exitCode === 0) return;\n throw new ConfigurationError(\n `${operation} failed (exit ${String(result.exitCode)}): ${result.stderr.trim() || \"no stderr\"}. ` +\n `The derived helpers shell out to POSIX \\`cat\\`/\\`find\\`/\\`grep\\`/\\`ls\\` — a backend whose ` +\n `execute() is not a POSIX shell must override them.`,\n { code: \"sandbox_derived_helper_failed\" },\n );\n }\n\n protected truncateOutput(output: string): string {\n const max = this.config.maxOutputBytes ?? 5 * 1024 * 1024;\n if (Buffer.byteLength(output) > max) {\n return `${output.slice(0, max)}\\n...(truncated)`;\n }\n return output;\n }\n\n private shellEscape(arg: string): string {\n return shellEscapePosix(arg);\n }\n}\n\n/**\n * A backend OR a per-request resolver of one — mirrors `FilesystemProvider` / `InteractiveProvider`.\n * A resolver runs at tool-execution time (request scope), so a multi-tenant / multi-role agent gets a\n * distinct sandbox per request without a shared mutable one. This is how a tool takes execution as an\n * INJECTED capability: the same tool runs on a local sandbox, a container/E2B backend (cluster/web), or\n * any future backend, with no direct `child_process` import.\n *\n * @public\n */\nexport type SandboxProvider<Ctx = unknown> =\n | SandboxBackend\n | ((ctx: Ctx) => SandboxBackend | Promise<SandboxBackend>);\n\n/** Resolve a {@link SandboxProvider} to a concrete backend for `ctx`. */\nexport async function resolveSandbox<Ctx>(\n provider: SandboxProvider<Ctx>,\n ctx: Ctx,\n): Promise<SandboxBackend> {\n return provider instanceof SandboxBackend ? provider : provider(ctx);\n}\n","/**\n * LocalSandbox — subprocess-based execution. **This is NOT an isolation\n * boundary.** It runs the command via `/bin/sh -c` in the SAME OS as the host\n * with the host's filesystem and network fully reachable — it provides NO\n * process, filesystem, or network isolation. Its only safety affordances are:\n * - a wall-clock timeout (kills a runaway command),\n * - an output-size cap (bounds memory), and\n * - env scrubbing (#54): secret-like parent env vars (`*KEY*`/`*SECRET*`/\n * `*TOKEN*`/`*PASSWORD*`/`*_AUTH*`) are dropped from the child by default\n * (`SandboxConfig.env`), so a shell tool cannot exfiltrate host secrets via\n * the environment.\n *\n * For real isolation (untrusted code), use a container/VM backend — NOT this.\n *\n * @public\n */\n\nimport { execFile } from \"node:child_process\";\nimport { writeFile as fsWriteFile, mkdir } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\n\nimport { resolveChildEnv } from \"../internal/runtime/lifecycle/env-policy.js\";\nimport { type ExecuteResult, SandboxBackend, type SandboxConfig } from \"./types.js\";\n\n/**\n * Runs a command with `/bin/sh -c` on the host. NOT an isolation boundary — the\n * module note above states exactly what it does and does not protect.\n *\n * const sandbox = new LocalSandbox({ workDir: \"/srv/repo\", timeoutMs: 10_000 });\n * const { stdout, exitCode, timedOut } = await sandbox.execute(\"ls -1\");\n *\n * Needs a POSIX host with `/bin/sh`. Every helper inherited from\n * {@link SandboxBackend} — `readFile`, `glob`, `grep`, `listDir` — is built on\n * this `execute`, so they inherit that requirement too.\n *\n * How it fails: `execute` NEVER rejects. Every outcome, including a failure to\n * spawn, comes back as an {@link ExecuteResult}; read `timedOut` first\n * (`true` implies `exitCode` 124) and `stderr` for the reason. `uploadFile` DOES\n * reject, with the raw `node:fs` error, on a bad path or missing permission.\n *\n * Traps:\n * - `exitCode` is not the child's exit code. It is 0, 124 (timed out) or 1 — a\n * command that exits 3 is reported as 1, so branching on a specific code does\n * not work here.\n * - Output beyond `maxOutputBytes` (default 5 MiB) makes Node kill the child.\n * The result is `exitCode: 1` and `timedOut: false`, with the cut stream\n * carrying the `...(truncated)` marker described on `ExecuteResult` (#363).\n * Since `exitCode` is 1 either way, that marker is what tells a lost-output\n * result from a command that genuinely failed.\n * - `uploadFile` resolves a relative path against `workDir` but does not contain\n * it: an absolute path, or one containing `..`, writes wherever the process\n * can. `workDir` is a starting directory, not a jail.\n * - Secret-like parent env vars are dropped by default. Passing\n * `env: \"all\"` in {@link SandboxConfig} puts the host's API keys back within\n * reach of any command the agent runs.\n */\nexport class LocalSandbox extends SandboxBackend {\n constructor(config: SandboxConfig = {}) {\n super(config);\n }\n\n async execute(command: string, opts?: { timeoutMs?: number }): Promise<ExecuteResult> {\n const timeout = opts?.timeoutMs ?? this.config.timeoutMs ?? 30_000;\n const max = this.config.maxOutputBytes ?? 5 * 1024 * 1024;\n\n return new Promise<ExecuteResult>((resolve) => {\n const child = execFile(\n \"/bin/sh\",\n [\"-c\", command],\n {\n cwd: this.config.workDir,\n timeout,\n maxBuffer: max,\n encoding: \"utf-8\",\n // #54 — scrub secret-like host env vars from the child by default.\n env: resolveChildEnv({ policy: this.config.env }),\n },\n (error, stdout, stderr) => {\n resolve(this.buildResult(error, stdout ?? \"\", stderr ?? \"\"));\n },\n );\n\n // Safety: if child somehow doesn't callback\n child.on(\"error\", () => {\n resolve({ stdout: \"\", stderr: \"spawn error\", exitCode: 1, timedOut: false });\n });\n });\n }\n\n private buildResult(error: Error | null, stdout: string, stderr: string): ExecuteResult {\n const timedOut = error !== null && \"killed\" in error && (error as { killed: boolean }).killed;\n // #363 — the ONLY reliable signal that output was lost. Node caps the buffer AT `maxBuffer`, so\n // for ASCII output the string comes back exactly at the cap and `truncateOutput`'s `> max` test\n // never fires; a length comparison cannot tell a cut document from a complete one that happens\n // to be that long. The child being killed for overflow can.\n const overflowed =\n error !== null && (error as { code?: unknown }).code === \"ERR_CHILD_PROCESS_STDIO_MAXBUFFER\";\n return {\n stdout: this.markIfCut(this.truncateOutput(stdout), overflowed),\n stderr: this.markIfCut(this.truncateOutput(stderr), overflowed),\n exitCode: timedOut ? 124 : error ? 1 : 0,\n timedOut,\n };\n }\n\n /**\n * Append the `...(truncated)` marker `ExecuteResult` tells callers to branch on, when the child\n * was killed for exceeding `maxOutputBytes` and this stream is the one sitting at the cap.\n *\n * `truncateOutput` has already run, so an output it marked is left alone rather than marked\n * twice. Node does not say WHICH stream overflowed, so the stream at the cap is the one that\n * lost data — the other, being shorter, is complete.\n */\n private markIfCut(output: string, overflowed: boolean): string {\n if (!overflowed) return output;\n if (output.endsWith(\"...(truncated)\")) return output;\n const max = this.config.maxOutputBytes ?? 5 * 1024 * 1024;\n return Buffer.byteLength(output) >= max ? `${output}\\n...(truncated)` : output;\n }\n\n async uploadFile(path: string, content: string | Buffer): Promise<void> {\n const fullPath = path.startsWith(\"/\") ? path : `${this.config.workDir}/${path}`;\n await mkdir(dirname(fullPath), { recursive: true });\n await fsWriteFile(fullPath, content, \"utf-8\");\n }\n}\n"]}
package/dist/eval.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkBRWI4HLK_cjs = require('./chunk-BRWI4HLK.cjs');
3
+ var chunkEPANMGH7_cjs = require('./chunk-EPANMGH7.cjs');
4
4
  var chunkQ6WT5535_cjs = require('./chunk-Q6WT5535.cjs');
5
5
  require('./chunk-L7JPXZ2C.cjs');
6
6
  require('./chunk-YOVZMN4P.cjs');
@@ -631,13 +631,13 @@ function assertEval(run, thresholds) {
631
631
  // src/internal/eval/code-runner.ts
632
632
  var ARTIFACT_PATCH = ".theo-artifact.patch";
633
633
  async function captureArtifact(sandbox, repoDir) {
634
- const dir = chunkBRWI4HLK_cjs.shellEscapePosix(repoDir);
634
+ const dir = chunkEPANMGH7_cjs.shellEscapePosix(repoDir);
635
635
  const diffRes = await sandbox.execute(`git -C ${dir} diff`);
636
636
  const diff = diffRes.stdout;
637
637
  if (diff.length === 0) return { diff: "", applies: false };
638
638
  await sandbox.uploadFile(`${repoDir}/${ARTIFACT_PATCH}`, diff);
639
639
  const check = await sandbox.execute(
640
- `git -C ${dir} apply --check --reverse ${chunkBRWI4HLK_cjs.shellEscapePosix(ARTIFACT_PATCH)}`
640
+ `git -C ${dir} apply --check --reverse ${chunkEPANMGH7_cjs.shellEscapePosix(ARTIFACT_PATCH)}`
641
641
  );
642
642
  return { diff, applies: check.exitCode === 0 };
643
643
  }
@@ -945,7 +945,7 @@ var Scorers = {
945
945
  * that rejects shell metacharacters in `execute` is unsupported for this scorer.
946
946
  */
947
947
  verifyGate(opts) {
948
- const { sandbox = new chunkBRWI4HLK_cjs.LocalSandbox(), repoDir, failToPass, passToPass, command } = opts;
948
+ const { sandbox = new chunkEPANMGH7_cjs.LocalSandbox(), repoDir, failToPass, passToPass, command } = opts;
949
949
  return {
950
950
  name: "verify-gate",
951
951
  score: async () => {
@@ -953,7 +953,7 @@ var Scorers = {
953
953
  if (cmd.length === 0) {
954
954
  return { score: 0, reason: "verify_gate_empty_command" };
955
955
  }
956
- const r = await sandbox.execute(`cd ${chunkBRWI4HLK_cjs.shellEscapePosix(repoDir)} && ${cmd}`);
956
+ const r = await sandbox.execute(`cd ${chunkEPANMGH7_cjs.shellEscapePosix(repoDir)} && ${cmd}`);
957
957
  if (r.exitCode === 0) return { score: 1 };
958
958
  return { score: 0, reason: `exit=${r.exitCode} ${r.stderr.slice(0, 200)}`.trim() };
959
959
  }
package/dist/eval.js CHANGED
@@ -1,4 +1,4 @@
1
- import { shellEscapePosix, LocalSandbox } from './chunk-EEK7S77K.js';
1
+ import { shellEscapePosix, LocalSandbox } from './chunk-NBNRQN6T.js';
2
2
  import { openRouterMemoryEmbeddingProviderAdapter } from './chunk-BJ7BPLQK.js';
3
3
  import './chunk-HWMGR3HA.js';
4
4
  import './chunk-4GUFNDZR.js';
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkBRWI4HLK_cjs = require('../chunk-BRWI4HLK.cjs');
3
+ var chunkEPANMGH7_cjs = require('../chunk-EPANMGH7.cjs');
4
4
  require('../chunk-2ZPEDVLM.cjs');
5
5
  var chunkCEGU5AVD_cjs = require('../chunk-CEGU5AVD.cjs');
6
6
  var chunkRNXXYF22_cjs = require('../chunk-RNXXYF22.cjs');
@@ -185,7 +185,7 @@ function buildSeccompFilter(opts) {
185
185
  const denyIdx = body.length - 1;
186
186
  push(RET_K, SECCOMP_RET_KILL_PROCESS);
187
187
  const killIdx = body.length - 1;
188
- const resolve = (i, t) => {
188
+ const resolve2 = (i, t) => {
189
189
  const abs = t === ALLOW ? allowIdx : t === DENY ? denyIdx : t === KILL ? killIdx : i + 1 + t;
190
190
  const off = abs - (i + 1);
191
191
  if (off < 0 || off > 255) throw new RangeError(`seccomp jump out of range at ${i}: ${off}`);
@@ -193,7 +193,7 @@ function buildSeccompFilter(opts) {
193
193
  };
194
194
  for (const [i, b] of body.entries()) {
195
195
  if (b.code === JEQ_K || b.code === JGE_K) {
196
- insns.push(jmp(b.code, b.k, resolve(i, b.jt), resolve(i, b.jf)));
196
+ insns.push(jmp(b.code, b.k, resolve2(i, b.jt), resolve2(i, b.jf)));
197
197
  } else {
198
198
  insns.push(stmt(b.code, b.k));
199
199
  }
@@ -207,15 +207,90 @@ function buildSeccompFilter(opts) {
207
207
  });
208
208
  return buf;
209
209
  }
210
+ function nonX64SeccompWarning(arch) {
211
+ return `[sandbox] seccomp syscall filter unsupported on ${arch} (x86_64 only in v1) \u2014 running without the filter; bwrap FS/network confinement still applies.`;
212
+ }
213
+ var warnedNonX64Program = false;
214
+ function seccompProgramForArch(arch, warn) {
215
+ if (arch !== "x64") {
216
+ if (!warnedNonX64Program) {
217
+ warnedNonX64Program = true;
218
+ warn(nonX64SeccompWarning(arch));
219
+ }
220
+ return void 0;
221
+ }
222
+ return buildSeccompFilter({ networkRestricted: true });
223
+ }
224
+ var memoizedProgram;
225
+ function restrictedSeccompProgram(warn) {
226
+ if (memoizedProgram === void 0)
227
+ memoizedProgram = seccompProgramForArch(process.arch, warn) ?? null;
228
+ return memoizedProgram ?? void 0;
229
+ }
230
+ function octalPrintfArg(program) {
231
+ let out = "";
232
+ for (const byte of program) out += `\\${byte.toString(8).padStart(3, "0")}`;
233
+ return out;
234
+ }
235
+ function assertSeccompProgramShape(program) {
236
+ if (program.length === 0 || program.length % 8 !== 0) {
237
+ throw new TypeError(
238
+ `seccompProgram must be a positive multiple of 8 bytes, got ${program.length}`
239
+ );
240
+ }
241
+ }
242
+ var PIPE_SCRIPT = '{ p=$1; shift; printf "$p" | exec "$@" 3<&0 0<&4 4<&-; } 4<&0';
243
+ function wrapWithSeccompProgram(bwrapArgv, command, program) {
244
+ assertSeccompProgramShape(program);
245
+ const words = [octalPrintfArg(program), ...bwrapArgv, "--seccomp", "3", "--", "/bin/sh", "-c"];
246
+ return `exec /bin/sh -c ${chunkEPANMGH7_cjs.shellEscapePosix(PIPE_SCRIPT)} sh ${[...words, command].map(chunkEPANMGH7_cjs.shellEscapePosix).join(" ")}`;
247
+ }
248
+ var programsWritten = /* @__PURE__ */ new Map();
249
+ function rememberSeccompFile(path2, program) {
250
+ programsWritten.set(path.resolve(path2), Buffer.from(program));
251
+ }
252
+ function removeDirOnExit(dir) {
253
+ const cleanup = () => fs.rmSync(dir, { recursive: true, force: true });
254
+ process.once("exit", cleanup);
255
+ for (const signal of ["SIGINT", "SIGTERM"]) {
256
+ process.once(signal, () => {
257
+ cleanup();
258
+ if (process.listenerCount(signal) === 0) process.kill(process.pid, signal);
259
+ });
260
+ }
261
+ }
262
+ function wrapWithSeccompPath(bwrapArgv, policy, command, seccompPath) {
263
+ const program = programsWritten.get(path.resolve(seccompPath));
264
+ const bind = program === void 0 ? "--ro-bind" : "--ro-bind-try";
265
+ const flags = [
266
+ ...bwrapArgv.slice(0, -3),
267
+ ...readOnlyBindForSeccompPath(policy.mode, policy.cwd, seccompPath, bind),
268
+ ...bwrapArgv.slice(-3, -1)
269
+ ];
270
+ if (program !== void 0) return wrapWithSeccompProgram(flags, command, program);
271
+ return `${[...flags, "--seccomp", "3", "--"].map(chunkEPANMGH7_cjs.shellEscapePosix).join(" ")} /bin/sh -c ${chunkEPANMGH7_cjs.shellEscapePosix(command)} 3< ${chunkEPANMGH7_cjs.shellEscapePosix(seccompPath)}`;
272
+ }
273
+ function readOnlyBindForSeccompPath(mode, cwd, seccompPath, bind) {
274
+ const file = path.resolve(seccompPath);
275
+ const dir = path.dirname(file);
276
+ const roots = writableRootsFor(mode, path.resolve(cwd)) ?? [];
277
+ const covers = (root) => dir === "/" || root === dir || root.startsWith(`${dir}/`);
278
+ const target = roots.some(covers) ? file : dir;
279
+ return [bind, target, target];
280
+ }
210
281
 
211
282
  // src/sandbox/linux-sandbox.ts
212
283
  function wrapCommandForSandbox(mode, opts, command) {
213
284
  const argv = buildBwrapArgv(mode, { cwd: opts.cwd, network: opts.network, env: opts.env });
214
285
  if (argv === null) return null;
215
286
  const bin = opts.bin ?? "bwrap";
216
- const seccompArgv = opts.seccompPath !== void 0 ? ["--seccomp", "3"] : [];
217
- const base = `${chunkBRWI4HLK_cjs.shellEscapePosix(bin)} ${[...argv.slice(0, -1), ...seccompArgv, "--"].map(chunkBRWI4HLK_cjs.shellEscapePosix).join(" ")} /bin/sh -c ${chunkBRWI4HLK_cjs.shellEscapePosix(command)}`;
218
- return opts.seccompPath !== void 0 ? `${base} 3< ${chunkBRWI4HLK_cjs.shellEscapePosix(opts.seccompPath)}` : base;
287
+ if (opts.seccompProgram !== void 0) {
288
+ return wrapWithSeccompProgram([bin, ...argv.slice(0, -1)], command, opts.seccompProgram);
289
+ }
290
+ if (opts.seccompPath === void 0) {
291
+ return `${[bin, ...argv].map(chunkEPANMGH7_cjs.shellEscapePosix).join(" ")} /bin/sh -c ${chunkEPANMGH7_cjs.shellEscapePosix(command)}`;
292
+ }
293
+ return wrapWithSeccompPath([bin, ...argv], { mode, cwd: opts.cwd }, command, opts.seccompPath);
219
294
  }
220
295
  var ENV_ALLOWLIST = [
221
296
  "PATH",
@@ -236,15 +311,16 @@ function allowlistedEnv(source = process.env) {
236
311
  }
237
312
  return out;
238
313
  }
239
- var LinuxSandbox = class extends chunkBRWI4HLK_cjs.LocalSandbox {
314
+ var LinuxSandbox = class extends chunkEPANMGH7_cjs.LocalSandbox {
240
315
  mode;
241
316
  network;
242
317
  cwd;
243
318
  bin;
244
319
  env;
245
- /** M63 — path to the cBPF seccomp program written host-side; passed to `bwrap --seccomp 3` via a
246
- * shell redirect. `undefined` when the network is unrestricted OR generation failed (honest fallback). */
247
- seccompPath;
320
+ /** B-600 — the cBPF seccomp program, piped to `bwrap --seccomp 3` from memory (never a file a
321
+ * confined command could replace). `undefined` when the network is unrestricted or the arch is not
322
+ * x86_64 (honest fallback). */
323
+ seccompProgram;
248
324
  constructor(config, opts) {
249
325
  super(config);
250
326
  this.mode = opts.mode;
@@ -252,7 +328,7 @@ var LinuxSandbox = class extends chunkBRWI4HLK_cjs.LocalSandbox {
252
328
  this.cwd = config.workDir ?? process.cwd();
253
329
  this.bin = opts.bin ?? "bwrap";
254
330
  this.env = opts.env ?? allowlistedEnv();
255
- this.seccompPath = seccompPathForNetwork(this.network);
331
+ this.seccompProgram = seccompProgramForNetwork(this.network);
256
332
  }
257
333
  /** Extracted for test visibility — delegates to the pure `wrapCommandForSandbox` (M57, single wrap SoT). */
258
334
  wrapCommand(command) {
@@ -263,7 +339,7 @@ var LinuxSandbox = class extends chunkBRWI4HLK_cjs.LocalSandbox {
263
339
  network: this.network,
264
340
  env: this.env,
265
341
  bin: this.bin,
266
- seccompPath: this.seccompPath
342
+ seccompProgram: this.seccompProgram
267
343
  },
268
344
  command
269
345
  );
@@ -286,7 +362,7 @@ var LinuxSandbox = class extends chunkBRWI4HLK_cjs.LocalSandbox {
286
362
  const target = path2.startsWith("/") ? path2 : `${this.cwd}/${path2}`;
287
363
  const b64 = Buffer.from(content).toString("base64");
288
364
  const r = await this.execute(
289
- `mkdir -p ${chunkBRWI4HLK_cjs.shellEscapePosix(path.dirname(target))} && printf %s ${chunkBRWI4HLK_cjs.shellEscapePosix(b64)} | base64 -d > ${chunkBRWI4HLK_cjs.shellEscapePosix(target)}`
365
+ `mkdir -p ${chunkEPANMGH7_cjs.shellEscapePosix(path.dirname(target))} && printf %s ${chunkEPANMGH7_cjs.shellEscapePosix(b64)} | base64 -d > ${chunkEPANMGH7_cjs.shellEscapePosix(target)}`
290
366
  );
291
367
  if (r.exitCode !== 0) {
292
368
  throw new Error(
@@ -305,20 +381,17 @@ function seccompPathForArch(arch, warn) {
305
381
  if (arch !== "x64") {
306
382
  if (!warnedNonX64) {
307
383
  warnedNonX64 = true;
308
- warn(
309
- `[sandbox] seccomp syscall filter unsupported on ${arch} (x86_64 only in v1) \u2014 running without the filter; bwrap FS/network confinement still applies.`
310
- );
384
+ warn(nonX64SeccompWarning(arch));
311
385
  }
312
386
  return void 0;
313
387
  }
314
388
  try {
315
389
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ab-seccomp-"));
316
390
  const path2 = path.join(dir, "filter.bpf");
317
- fs.writeFileSync(path2, buildSeccompFilter({ networkRestricted: true }));
318
- const cleanup = () => fs.rmSync(dir, { recursive: true, force: true });
319
- process.once("exit", cleanup);
320
- process.once("SIGINT", cleanup);
321
- process.once("SIGTERM", cleanup);
391
+ const program = buildSeccompFilter({ networkRestricted: true });
392
+ fs.writeFileSync(path2, program);
393
+ rememberSeccompFile(path2, program);
394
+ removeDirOnExit(dir);
322
395
  return path2;
323
396
  } catch (err) {
324
397
  warn(
@@ -350,7 +423,7 @@ function resolveSandboxPosture(opts) {
350
423
  }
351
424
  function createSandboxBackend(opts) {
352
425
  const config = { workDir: opts.workDir, timeoutMs: opts.timeoutMs };
353
- if (opts.mode === "danger-full-access") return new chunkBRWI4HLK_cjs.LocalSandbox(config);
426
+ if (opts.mode === "danger-full-access") return new chunkEPANMGH7_cjs.LocalSandbox(config);
354
427
  const detection = (opts.detect ?? detectBwrapMemoized)();
355
428
  if (!detection.ok) {
356
429
  if (!warnedUnavailable) {
@@ -360,7 +433,7 @@ function createSandboxBackend(opts) {
360
433
  `[sandbox] OS-level enforcement unavailable (${detection.reason}) \u2014 falling back to tool-level gating only (sandbox_mode=${opts.mode}).`
361
434
  );
362
435
  }
363
- return new chunkBRWI4HLK_cjs.LocalSandbox(config);
436
+ return new chunkEPANMGH7_cjs.LocalSandbox(config);
364
437
  }
365
438
  return new LinuxSandbox(config, { mode: opts.mode, network: opts.network, bin: detection.bin });
366
439
  }
@@ -368,8 +441,8 @@ var interactiveWarned = false;
368
441
  function __resetInteractiveWarnLatchForTests() {
369
442
  interactiveWarned = false;
370
443
  }
371
- function seccompPathForNetwork(networkOpen) {
372
- return networkOpen ? void 0 : restrictedSeccompPath();
444
+ function seccompProgramForNetwork(networkOpen) {
445
+ return networkOpen ? void 0 : restrictedSeccompProgram((m) => console.warn(chunkRNXXYF22_cjs.redactSecrets(m)));
373
446
  }
374
447
  function decideConfinement(opts) {
375
448
  if (opts.mode === "danger-full-access") return null;
@@ -395,7 +468,7 @@ function interactiveWrapCommand(opts) {
395
468
  network: opts.network ?? false,
396
469
  env: allowlistedEnv(),
397
470
  bin,
398
- seccompPath: seccompPathForNetwork(opts.network ?? false)
471
+ seccompProgram: seccompProgramForNetwork(opts.network ?? false)
399
472
  },
400
473
  command
401
474
  );
@@ -417,7 +490,7 @@ var RepoProvisionError = class extends chunkCEGU5AVD_cjs.TheokitAgentError {
417
490
  };
418
491
  var SAFE_INSTANCE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
419
492
  async function provisionRepo(sandboxOrOpts, maybeOpts) {
420
- const sandbox = maybeOpts !== void 0 ? sandboxOrOpts : new chunkBRWI4HLK_cjs.LocalSandbox();
493
+ const sandbox = maybeOpts !== void 0 ? sandboxOrOpts : new chunkEPANMGH7_cjs.LocalSandbox();
421
494
  const opts = maybeOpts ?? sandboxOrOpts;
422
495
  const { repoUrl, ref, instanceId } = opts;
423
496
  if (!SAFE_INSTANCE_ID.test(instanceId)) {
@@ -430,20 +503,20 @@ async function provisionRepo(sandboxOrOpts, maybeOpts) {
430
503
  throw new RepoProvisionError(instanceId, `invalid ref: must not begin with '-' (got ${ref})`);
431
504
  }
432
505
  const clone = await sandbox.execute(
433
- `git -c protocol.ext.allow=never clone --quiet -- ${chunkBRWI4HLK_cjs.shellEscapePosix(repoUrl)} ${chunkBRWI4HLK_cjs.shellEscapePosix(instanceId)}`
506
+ `git -c protocol.ext.allow=never clone --quiet -- ${chunkEPANMGH7_cjs.shellEscapePosix(repoUrl)} ${chunkEPANMGH7_cjs.shellEscapePosix(instanceId)}`
434
507
  );
435
508
  if (clone.exitCode !== 0) {
436
509
  throw new RepoProvisionError(instanceId, `clone failed: ${clone.stderr.trim()}`);
437
510
  }
438
511
  const top = await sandbox.execute(
439
- `git -C ${chunkBRWI4HLK_cjs.shellEscapePosix(instanceId)} rev-parse --show-toplevel`
512
+ `git -C ${chunkEPANMGH7_cjs.shellEscapePosix(instanceId)} rev-parse --show-toplevel`
440
513
  );
441
514
  if (top.exitCode !== 0) {
442
515
  throw new RepoProvisionError(instanceId, `resolve repoDir failed: ${top.stderr.trim()}`);
443
516
  }
444
517
  const repoDir = top.stdout.trim();
445
518
  const checkout = await sandbox.execute(
446
- `git -C ${chunkBRWI4HLK_cjs.shellEscapePosix(repoDir)} checkout --quiet ${chunkBRWI4HLK_cjs.shellEscapePosix(ref)}`
519
+ `git -C ${chunkEPANMGH7_cjs.shellEscapePosix(repoDir)} checkout --quiet ${chunkEPANMGH7_cjs.shellEscapePosix(ref)}`
447
520
  );
448
521
  if (checkout.exitCode !== 0) {
449
522
  throw new RepoProvisionError(instanceId, `checkout ${ref} failed: ${checkout.stderr.trim()}`);
@@ -453,23 +526,23 @@ async function provisionRepo(sandboxOrOpts, maybeOpts) {
453
526
 
454
527
  Object.defineProperty(exports, "LocalSandbox", {
455
528
  enumerable: true,
456
- get: function () { return chunkBRWI4HLK_cjs.LocalSandbox; }
529
+ get: function () { return chunkEPANMGH7_cjs.LocalSandbox; }
457
530
  });
458
531
  Object.defineProperty(exports, "SandboxBackend", {
459
532
  enumerable: true,
460
- get: function () { return chunkBRWI4HLK_cjs.SandboxBackend; }
533
+ get: function () { return chunkEPANMGH7_cjs.SandboxBackend; }
461
534
  });
462
535
  Object.defineProperty(exports, "SandboxNotAvailableError", {
463
536
  enumerable: true,
464
- get: function () { return chunkBRWI4HLK_cjs.SandboxNotAvailableError; }
537
+ get: function () { return chunkEPANMGH7_cjs.SandboxNotAvailableError; }
465
538
  });
466
539
  Object.defineProperty(exports, "SandboxSecurityError", {
467
540
  enumerable: true,
468
- get: function () { return chunkBRWI4HLK_cjs.SandboxSecurityError; }
541
+ get: function () { return chunkEPANMGH7_cjs.SandboxSecurityError; }
469
542
  });
470
543
  Object.defineProperty(exports, "resolveSandbox", {
471
544
  enumerable: true,
472
- get: function () { return chunkBRWI4HLK_cjs.resolveSandbox; }
545
+ get: function () { return chunkEPANMGH7_cjs.resolveSandbox; }
473
546
  });
474
547
  exports.LinuxSandbox = LinuxSandbox;
475
548
  exports.RepoProvisionError = RepoProvisionError;