@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 +28 -0
- package/dist/{chunk-BRWI4HLK.cjs → chunk-EPANMGH7.cjs} +4 -3
- package/dist/chunk-EPANMGH7.cjs.map +1 -0
- package/dist/{chunk-EEK7S77K.js → chunk-NBNRQN6T.js} +4 -3
- package/dist/chunk-NBNRQN6T.js.map +1 -0
- package/dist/eval.cjs +5 -5
- package/dist/eval.js +1 -1
- package/dist/sandbox/index.cjs +108 -35
- package/dist/sandbox/index.cjs.map +1 -1
- package/dist/sandbox/index.js +97 -24
- package/dist/sandbox/index.js.map +1 -1
- package/dist/sandbox/linux-sandbox.d.cts +24 -4
- package/dist/sandbox/linux-sandbox.d.ts +24 -4
- package/dist/sandbox/seccomp-delivery.d.cts +46 -0
- package/dist/sandbox/seccomp-delivery.d.ts +46 -0
- package/docs/harness-capability-map.md +1 -1
- package/package.json +1 -1
- package/dist/chunk-BRWI4HLK.cjs.map +0 -1
- package/dist/chunk-EEK7S77K.js.map +0 -1
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
|
-
|
|
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-
|
|
172
|
-
//# sourceMappingURL=chunk-
|
|
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
|
-
|
|
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-
|
|
165
|
-
//# sourceMappingURL=chunk-
|
|
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":";;;;;;;AAeO,SAAS,iBAAiB,GAAA,EAAqB;AACpD,EAAA,MAAM,aAAA,GAAgB,GAAA,CAAI,OAAA,CAAQ,IAAA,EAAM,OAAO,CAAA;AAC/C,EAAA,OAAO,CAAA,CAAA,EAAI,aAAA,CAAc,OAAA,CAAQ,mBAAA,EAAqB,QAAQ,CAAC,CAAA,CAAA,CAAA;AACjE;;;ACoDO,IAAM,oBAAA,GAAN,cAAmC,iBAAA,CAAkB;AAAA,EACxC,IAAA,GAAO,sBAAA;AAAA,EACP,IAAA,GAAO,kBAAA;AAAA,EACzB,YAAY,OAAA,EAAiB;AAE3B,IAAA,KAAA,CAAM,SAAS,EAAE,IAAA,EAAM,kBAAA,EAAoB,WAAA,EAAa,OAAO,CAAA;AAAA,EACjE;AACF;AAYO,IAAM,wBAAA,GAAN,cAAuC,iBAAA,CAAkB;AAAA,EAC5C,IAAA,GAAO,0BAAA;AAAA,EACP,IAAA,GAAO,uBAAA;AAAA,EACzB,YAAY,OAAA,EAAiB;AAE3B,IAAA,KAAA,CAAM,SAAS,EAAE,IAAA,EAAM,uBAAA,EAAyB,WAAA,EAAa,OAAO,CAAA;AAAA,EACtE;AACF;AAoBO,IAAe,iBAAf,MAA8B;AAAA,EACzB,MAAA;AAAA,EAEV,WAAA,CAAY,MAAA,GAAwB,EAAC,EAAG;AACtC,IAAA,IAAA,CAAK,MAAA,GAAS;AAAA,MACZ,OAAA,EAAS,OAAO,OAAA,IAAW,MAAA;AAAA,MAC3B,SAAA,EAAW,OAAO,SAAA,IAAa,GAAA;AAAA,MAC/B,cAAA,EAAgB,MAAA,CAAO,cAAA,IAAkB,CAAA,GAAI,IAAA,GAAO,IAAA;AAAA;AAAA,MAEpD,GAAA,EAAK,OAAO,GAAA,IAAO;AAAA,KACrB;AAAA,EACF;AAAA,EAMA,MAAM,SAAS,IAAA,EAA+B;AAC5C,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,OAAA,CAAQ,OAAO,IAAA,CAAK,WAAA,CAAY,IAAI,CAAC,CAAA,CAAE,CAAA;AACjE,IAAA,IAAI,MAAA,CAAO,aAAa,CAAA,EAAG;AACzB,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,iBAAA,EAAoB,MAAA,CAAO,MAAM,CAAA,CAAE,CAAA;AAAA,IACrD;AACA,IAAA,OAAO,MAAA,CAAO,MAAA;AAAA,EAChB;AAAA,EAEA,MAAM,SAAA,CAAU,IAAA,EAAc,OAAA,EAAgC;AAC5D,IAAA,MAAM,IAAA,CAAK,UAAA,CAAW,IAAA,EAAM,OAAO,CAAA;AAAA,EACrC;AAAA,EAEA,MAAM,IAAA,CAAK,OAAA,EAAiB,GAAA,EAAiC;AAC3D,IAAA,MAAM,GAAA,GAAM,GAAA,IAAO,IAAA,CAAK,MAAA,CAAO,OAAA,IAAW,GAAA;AAC1C,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,OAAA;AAAA,MACxB,CAAA,KAAA,EAAQ,KAAK,WAAA,CAAY,GAAG,CAAC,CAAA,OAAA,EAAU,IAAA,CAAK,WAAA,CAAY,OAAO,CAAC,CAAA,oBAAA;AAAA,KAClE;AACA,IAAA,IAAA,CAAK,gBAAA,CAAiB,QAAQ,MAAM,CAAA;AACpC,IAAA,OAAO,MAAA,CAAO,OAAO,IAAA,EAAK,CAAE,MAAM,IAAI,CAAA,CAAE,OAAO,OAAO,CAAA;AAAA,EACxD;AAAA,EAEA,MAAM,IAAA,CAAK,OAAA,EAAiB,IAAA,EAAkC;AAC5D,IAAA,MAAM,SAAS,IAAA,IAAQ,GAAA;AACvB,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,OAAA;AAAA,MACxB,CAAA,SAAA,EAAY,KAAK,WAAA,CAAY,OAAO,CAAC,CAAA,CAAA,EAAI,IAAA,CAAK,WAAA,CAAY,MAAM,CAAC,CAAA,YAAA;AAAA,KACnE;AAEA,IAAA,IAAI,OAAO,QAAA,KAAa,CAAA,EAAG,IAAA,CAAK,gBAAA,CAAiB,QAAQ,MAAM,CAAA;AAC/D,IAAA,OAAO,MAAA,CAAO,OAAO,IAAA,EAAK,CAAE,MAAM,IAAI,CAAA,CAAE,OAAO,OAAO,CAAA;AAAA,EACxD;AAAA,EAEA,MAAM,QAAQ,IAAA,EAAiC;AAC7C,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,OAAA,CAAQ,SAAS,IAAA,CAAK,WAAA,CAAY,IAAI,CAAC,CAAA,CAAE,CAAA;AACnE,IAAA,IAAA,CAAK,gBAAA,CAAiB,WAAW,MAAM,CAAA;AACvC,IAAA,OAAO,MAAA,CAAO,OAAO,IAAA,EAAK,CAAE,MAAM,IAAI,CAAA,CAAE,OAAO,OAAO,CAAA;AAAA,EACxD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcQ,gBAAA,CAAiB,WAAmB,MAAA,EAA6B;AACvE,IAAA,IAAI,MAAA,CAAO,aAAa,CAAA,EAAG;AAC3B,IAAA,MAAM,IAAI,kBAAA;AAAA,MACR,CAAA,EAAG,SAAS,CAAA,cAAA,EAAiB,MAAA,CAAO,MAAA,CAAO,QAAQ,CAAC,CAAA,GAAA,EAAM,MAAA,CAAO,MAAA,CAAO,IAAA,EAAK,IAAK,WAAW,CAAA,mJAAA,CAAA;AAAA,MAG7F,EAAE,MAAM,+BAAA;AAAgC,KAC1C;AAAA,EACF;AAAA,EAEU,eAAe,MAAA,EAAwB;AAC/C,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,MAAA,CAAO,cAAA,IAAkB,IAAI,IAAA,GAAO,IAAA;AACrD,IAAA,IAAI,MAAA,CAAO,UAAA,CAAW,MAAM,CAAA,GAAI,GAAA,EAAK;AACnC,MAAA,OAAO,CAAA,EAAG,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,GAAG,CAAC;AAAA,cAAA,CAAA;AAAA,IAChC;AACA,IAAA,OAAO,MAAA;AAAA,EACT;AAAA,EAEQ,YAAY,GAAA,EAAqB;AACvC,IAAA,OAAO,iBAAiB,GAAG,CAAA;AAAA,EAC7B;AACF;AAgBA,eAAsB,cAAA,CACpB,UACA,GAAA,EACyB;AACzB,EAAA,OAAO,QAAA,YAAoB,cAAA,GAAiB,QAAA,GAAW,QAAA,CAAS,GAAG,CAAA;AACrE;ACxKO,IAAM,YAAA,GAAN,cAA2B,cAAA,CAAe;AAAA,EAC/C,WAAA,CAAY,MAAA,GAAwB,EAAC,EAAG;AACtC,IAAA,KAAA,CAAM,MAAM,CAAA;AAAA,EACd;AAAA,EAEA,MAAM,OAAA,CAAQ,OAAA,EAAiB,IAAA,EAAuD;AACpF,IAAA,MAAM,OAAA,GAAU,IAAA,EAAM,SAAA,IAAa,IAAA,CAAK,OAAO,SAAA,IAAa,GAAA;AAC5D,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,MAAA,CAAO,cAAA,IAAkB,IAAI,IAAA,GAAO,IAAA;AAErD,IAAA,OAAO,IAAI,OAAA,CAAuB,CAAC,OAAA,KAAY;AAC7C,MAAA,MAAM,KAAA,GAAQ,QAAA;AAAA,QACZ,SAAA;AAAA,QACA,CAAC,MAAM,OAAO,CAAA;AAAA,QACd;AAAA,UACE,GAAA,EAAK,KAAK,MAAA,CAAO,OAAA;AAAA,UACjB,OAAA;AAAA,UACA,SAAA,EAAW,GAAA;AAAA,UACX,QAAA,EAAU,OAAA;AAAA;AAAA,UAEV,KAAK,eAAA,CAAgB,EAAE,QAAQ,IAAA,CAAK,MAAA,CAAO,KAAK;AAAA,SAClD;AAAA,QACA,CAAC,KAAA,EAAO,MAAA,EAAQ,MAAA,KAAW;AACzB,UAAA,OAAA,CAAQ,KAAK,WAAA,CAAY,KAAA,EAAO,UAAU,EAAA,EAAI,MAAA,IAAU,EAAE,CAAC,CAAA;AAAA,QAC7D;AAAA,OACF;AAGA,MAAA,KAAA,CAAM,EAAA,CAAG,SAAS,MAAM;AACtB,QAAA,OAAA,CAAQ,EAAE,QAAQ,EAAA,EAAI,MAAA,EAAQ,eAAe,QAAA,EAAU,CAAA,EAAG,QAAA,EAAU,KAAA,EAAO,CAAA;AAAA,MAC7E,CAAC,CAAA;AAAA,IACH,CAAC,CAAA;AAAA,EACH;AAAA,EAEQ,WAAA,CAAY,KAAA,EAAqB,MAAA,EAAgB,MAAA,EAA+B;AACtF,IAAA,MAAM,QAAA,GAAW,KAAA,KAAU,IAAA,IAAQ,QAAA,IAAY,SAAU,KAAA,CAA8B,MAAA;AAKvF,IAAA,MAAM,UAAA,GACJ,KAAA,KAAU,IAAA,IAAS,KAAA,CAA6B,IAAA,KAAS,mCAAA;AAC3D,IAAA,OAAO;AAAA,MACL,QAAQ,IAAA,CAAK,SAAA,CAAU,KAAK,cAAA,CAAe,MAAM,GAAG,UAAU,CAAA;AAAA,MAC9D,QAAQ,IAAA,CAAK,SAAA,CAAU,KAAK,cAAA,CAAe,MAAM,GAAG,UAAU,CAAA;AAAA,MAC9D,QAAA,EAAU,QAAA,GAAW,GAAA,GAAM,KAAA,GAAQ,CAAA,GAAI,CAAA;AAAA,MACvC;AAAA,KACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUQ,SAAA,CAAU,QAAgB,UAAA,EAA6B;AAC7D,IAAA,IAAI,CAAC,YAAY,OAAO,MAAA;AACxB,IAAA,IAAI,MAAA,CAAO,QAAA,CAAS,gBAAgB,CAAA,EAAG,OAAO,MAAA;AAC9C,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,MAAA,CAAO,cAAA,IAAkB,IAAI,IAAA,GAAO,IAAA;AACrD,IAAA,OAAO,OAAO,UAAA,CAAW,MAAM,CAAA,IAAK,GAAA,GAAM,GAAG,MAAM;AAAA,cAAA,CAAA,GAAqB,MAAA;AAAA,EAC1E;AAAA,EAEA,MAAM,UAAA,CAAW,IAAA,EAAc,OAAA,EAAyC;AACtE,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,UAAA,CAAW,GAAG,CAAA,GAAI,IAAA,GAAO,CAAA,EAAG,IAAA,CAAK,MAAA,CAAO,OAAO,CAAA,CAAA,EAAI,IAAI,CAAA,CAAA;AAC7E,IAAA,MAAM,MAAM,OAAA,CAAQ,QAAQ,GAAG,EAAE,SAAA,EAAW,MAAM,CAAA;AAClD,IAAA,MAAMA,SAAA,CAAY,QAAA,EAAU,OAAA,EAAS,OAAO,CAAA;AAAA,EAC9C;AACF","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
|
|
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 =
|
|
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 ${
|
|
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
|
|
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 ${
|
|
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-
|
|
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';
|
package/dist/sandbox/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
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
|
|
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,
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
314
|
+
var LinuxSandbox = class extends chunkEPANMGH7_cjs.LocalSandbox {
|
|
240
315
|
mode;
|
|
241
316
|
network;
|
|
242
317
|
cwd;
|
|
243
318
|
bin;
|
|
244
319
|
env;
|
|
245
|
-
/**
|
|
246
|
-
*
|
|
247
|
-
|
|
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.
|
|
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
|
-
|
|
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 ${
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
|
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
|
|
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
|
|
372
|
-
return networkOpen ? void 0 :
|
|
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
|
-
|
|
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
|
|
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 -- ${
|
|
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 ${
|
|
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 ${
|
|
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
|
|
529
|
+
get: function () { return chunkEPANMGH7_cjs.LocalSandbox; }
|
|
457
530
|
});
|
|
458
531
|
Object.defineProperty(exports, "SandboxBackend", {
|
|
459
532
|
enumerable: true,
|
|
460
|
-
get: function () { return
|
|
533
|
+
get: function () { return chunkEPANMGH7_cjs.SandboxBackend; }
|
|
461
534
|
});
|
|
462
535
|
Object.defineProperty(exports, "SandboxNotAvailableError", {
|
|
463
536
|
enumerable: true,
|
|
464
|
-
get: function () { return
|
|
537
|
+
get: function () { return chunkEPANMGH7_cjs.SandboxNotAvailableError; }
|
|
465
538
|
});
|
|
466
539
|
Object.defineProperty(exports, "SandboxSecurityError", {
|
|
467
540
|
enumerable: true,
|
|
468
|
-
get: function () { return
|
|
541
|
+
get: function () { return chunkEPANMGH7_cjs.SandboxSecurityError; }
|
|
469
542
|
});
|
|
470
543
|
Object.defineProperty(exports, "resolveSandbox", {
|
|
471
544
|
enumerable: true,
|
|
472
|
-
get: function () { return
|
|
545
|
+
get: function () { return chunkEPANMGH7_cjs.resolveSandbox; }
|
|
473
546
|
});
|
|
474
547
|
exports.LinuxSandbox = LinuxSandbox;
|
|
475
548
|
exports.RepoProvisionError = RepoProvisionError;
|