@anvia/sandbox 1.1.5 → 1.1.7
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/README.md +7 -0
- package/dist/{chunk-D5XZQ2ES.js → chunk-ZMZH4DBC.js} +21 -2
- package/dist/chunk-ZMZH4DBC.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +54 -19
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-D5XZQ2ES.js.map +0 -1
package/README.md
CHANGED
|
@@ -32,6 +32,13 @@ Networking is explicit. Use `{ mode: "none" }` or `{ mode: "bridge", ports: [...
|
|
|
32
32
|
bind only to `127.0.0.1`. Runtime methods use object arguments, propagate abort signals, and expose
|
|
33
33
|
command and process output as bytes. Tool wrappers decode UTF-8 strictly and return structured values.
|
|
34
34
|
|
|
35
|
+
`exec_command` accepts a complete shell command line in `command`, including pipes, redirects, and
|
|
36
|
+
multiline scripts. For exact argv execution without shell parsing, provide the executable in
|
|
37
|
+
`command` and its arguments in `args`. Command policies apply to the implicit `sh` executable when a
|
|
38
|
+
shell command line is used. Natural command lines are rejected with block-mode command policies
|
|
39
|
+
because arbitrary shell syntax cannot be checked safely against an executable block list; use exact
|
|
40
|
+
`command` and `args` input in that configuration.
|
|
41
|
+
|
|
35
42
|
File paths and command working directories passed to agent tools may be workspace-relative (for
|
|
36
43
|
example `notes/result.txt`) or absolute paths inside the sandbox workdir (for example
|
|
37
44
|
`/workspace/notes/result.txt` with the default workdir). Paths outside the sandbox workdir are
|
|
@@ -87,12 +87,31 @@ async function assertDockerCli(args, options) {
|
|
|
87
87
|
const result = await runDockerCli(args, options);
|
|
88
88
|
if (result.exitCode !== 0) {
|
|
89
89
|
throw new DockerSandboxError(
|
|
90
|
-
`Docker command failed: docker ${args.join(" ")}`,
|
|
90
|
+
`Docker command failed: docker ${redactCliArgs(args).join(" ")}`,
|
|
91
91
|
"docker_command_failed",
|
|
92
92
|
result
|
|
93
93
|
);
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
+
var envValueFlags = /* @__PURE__ */ new Set(["--env", "-e"]);
|
|
97
|
+
function redactCliArgs(args) {
|
|
98
|
+
return args.map((arg, index) => {
|
|
99
|
+
const flag = args[index - 1];
|
|
100
|
+
if (flag !== void 0 && envValueFlags.has(flag)) return redactEnvAssignment(arg);
|
|
101
|
+
for (const envFlag of envValueFlags) {
|
|
102
|
+
const prefix = `${envFlag}=`;
|
|
103
|
+
if (arg.startsWith(prefix))
|
|
104
|
+
return `${prefix}${redactEnvAssignment(arg.slice(prefix.length))}`;
|
|
105
|
+
}
|
|
106
|
+
if (arg.startsWith("-e") && !arg.startsWith("-e=") && arg.length > 2)
|
|
107
|
+
return `-e${redactEnvAssignment(arg.slice(2))}`;
|
|
108
|
+
return arg;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function redactEnvAssignment(value) {
|
|
112
|
+
const separator = value.indexOf("=");
|
|
113
|
+
return separator === -1 ? value : `${value.slice(0, separator + 1)}<redacted>`;
|
|
114
|
+
}
|
|
96
115
|
function createOutputCollector(maxBytes, onChunk) {
|
|
97
116
|
const chunks = [];
|
|
98
117
|
let length = 0;
|
|
@@ -131,4 +150,4 @@ export {
|
|
|
131
150
|
assertDockerCli,
|
|
132
151
|
decodeUtf8
|
|
133
152
|
};
|
|
134
|
-
//# sourceMappingURL=chunk-
|
|
153
|
+
//# sourceMappingURL=chunk-ZMZH4DBC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/docker-cli.ts"],"sourcesContent":["export type DockerSandboxErrorCode =\n | \"docker_unavailable\"\n | \"docker_command_failed\"\n | \"image_not_found\"\n | \"runtime_not_found\"\n | \"volume_not_found\"\n | \"sandbox_not_found\"\n | \"invalid_state\"\n | \"invalid_path\"\n | \"timeout\"\n | \"file_too_large\"\n | \"tool_policy\"\n | \"port\"\n | \"process\";\n\nexport class DockerSandboxError extends Error {\n constructor(\n message: string,\n readonly code: DockerSandboxErrorCode,\n readonly details?: unknown,\n options?: ErrorOptions,\n ) {\n super(message, options);\n this.name = \"DockerSandboxError\";\n }\n}\n\nexport function dockerSandboxError(\n message: string,\n code: DockerSandboxErrorCode,\n cause?: unknown,\n details?: unknown,\n): DockerSandboxError {\n return new DockerSandboxError(\n message,\n code,\n details,\n cause === undefined ? undefined : { cause },\n );\n}\n","import { spawn } from \"node:child_process\";\nimport { DockerSandboxError } from \"./errors\";\n\nexport interface DockerCliResult {\n stdout: Uint8Array;\n stderr: Uint8Array;\n exitCode: number;\n durationMs: number;\n timedOut: boolean;\n stdoutTruncated: boolean;\n stderrTruncated: boolean;\n}\n\nexport interface DockerCliOptions {\n dockerPath: string;\n timeoutMs?: number | undefined;\n maxOutputBytes?: number | undefined;\n input?: string | Uint8Array | undefined;\n signal?: AbortSignal | undefined;\n onStdout?: (chunk: Uint8Array) => void;\n onStderr?: (chunk: Uint8Array) => void;\n}\n\nconst defaultMaxOutputBytes = 1024 * 1024;\n\nexport async function runDockerCli(\n args: string[],\n options: DockerCliOptions,\n): Promise<DockerCliResult> {\n const startedAt = Date.now();\n const maxOutputBytes = options.maxOutputBytes ?? defaultMaxOutputBytes;\n const stdout = createOutputCollector(maxOutputBytes, options.onStdout);\n const stderr = createOutputCollector(maxOutputBytes, options.onStderr);\n\n return new Promise((resolve, reject) => {\n const child = spawn(options.dockerPath, args, {\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n\n let timedOut = false;\n let settled = false;\n\n const timeout =\n options.timeoutMs === undefined\n ? undefined\n : setTimeout(() => {\n timedOut = true;\n child.kill(\"SIGKILL\");\n }, options.timeoutMs);\n\n const abort = () => {\n child.kill(\"SIGKILL\");\n };\n\n if (options.signal?.aborted === true) {\n abort();\n } else {\n options.signal?.addEventListener(\"abort\", abort, { once: true });\n }\n\n child.stdout.on(\"data\", (chunk: Buffer) => stdout.accept(chunk));\n child.stderr.on(\"data\", (chunk: Buffer) => stderr.accept(chunk));\n\n child.on(\"error\", (error) => {\n if (settled) {\n return;\n }\n settled = true;\n clearTimeout(timeout);\n options.signal?.removeEventListener(\"abort\", abort);\n\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n reject(\n new DockerSandboxError(\"Docker CLI was not found.\", \"docker_unavailable\", undefined, {\n cause: error,\n }),\n );\n return;\n }\n\n reject(error);\n });\n\n child.on(\"close\", (code) => {\n if (settled) {\n return;\n }\n settled = true;\n clearTimeout(timeout);\n options.signal?.removeEventListener(\"abort\", abort);\n\n if (options.signal?.aborted === true) {\n reject(options.signal.reason ?? new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n\n resolve({\n stdout: stdout.bytes(),\n stderr: stderr.bytes(),\n exitCode: code ?? 1,\n durationMs: Date.now() - startedAt,\n timedOut,\n stdoutTruncated: stdout.truncated,\n stderrTruncated: stderr.truncated,\n });\n });\n\n if (options.input !== undefined) {\n child.stdin.end(options.input);\n } else {\n child.stdin.end();\n }\n });\n}\n\nexport async function assertDockerCli(args: string[], options: DockerCliOptions): Promise<void> {\n const result = await runDockerCli(args, options);\n\n if (result.exitCode !== 0) {\n throw new DockerSandboxError(\n `Docker command failed: docker ${redactCliArgs(args).join(\" \")}`,\n \"docker_command_failed\",\n result,\n );\n }\n}\n\n/** Flags whose following value is a `KEY=VALUE` assignment that may carry a secret. */\nconst envValueFlags = new Set([\"--env\", \"-e\"]);\n\n/**\n * Replace secret values in a Docker argv before it is embedded in an error message, so\n * environment values passed with `--env`/`-e` never reach logs or traces. The variable\n * name is kept visible to keep the failure diagnosable. Both the separated\n * (`--env KEY=VALUE`) and equals-delimited (`--env=KEY=VALUE`) spellings are covered, as\n * well as the attached short-flag spelling (`-eKEY=VALUE`) that Docker's flag parser\n * accepts for the `-e` shorthand.\n */\nfunction redactCliArgs(args: string[]): string[] {\n return args.map((arg, index) => {\n const flag = args[index - 1];\n if (flag !== undefined && envValueFlags.has(flag)) return redactEnvAssignment(arg);\n for (const envFlag of envValueFlags) {\n const prefix = `${envFlag}=`;\n if (arg.startsWith(prefix))\n return `${prefix}${redactEnvAssignment(arg.slice(prefix.length))}`;\n }\n if (arg.startsWith(\"-e\") && !arg.startsWith(\"-e=\") && arg.length > 2)\n return `-e${redactEnvAssignment(arg.slice(2))}`;\n return arg;\n });\n}\n\nfunction redactEnvAssignment(value: string): string {\n const separator = value.indexOf(\"=\");\n return separator === -1 ? value : `${value.slice(0, separator + 1)}<redacted>`;\n}\n\nfunction createOutputCollector(maxBytes: number, onChunk?: (chunk: Uint8Array) => void) {\n const chunks: Buffer[] = [];\n let length = 0;\n let truncated = false;\n\n return {\n get truncated() {\n return truncated;\n },\n accept(chunk: Buffer) {\n onChunk?.(chunk);\n\n if (length >= maxBytes) {\n truncated = true;\n return;\n }\n\n const remaining = maxBytes - length;\n const next = chunk.length > remaining ? chunk.subarray(0, remaining) : chunk;\n chunks.push(next);\n length += next.length;\n\n if (next.length < chunk.length) {\n truncated = true;\n }\n },\n bytes() {\n const bytes = Buffer.concat(chunks, length);\n return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength).slice();\n },\n };\n}\n\nexport function decodeUtf8(bytes: Uint8Array): string {\n return new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes);\n}\n"],"mappings":";AAeO,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC5C,YACE,SACS,MACA,SACT,SACA;AACA,UAAM,SAAS,OAAO;AAJb;AACA;AAIT,SAAK,OAAO;AAAA,EACd;AAAA,EANW;AAAA,EACA;AAMb;;;ACzBA,SAAS,aAAa;AAuBtB,IAAM,wBAAwB,OAAO;AAErC,eAAsB,aACpB,MACA,SAC0B;AAC1B,QAAM,YAAY,KAAK,IAAI;AAC3B,QAAM,iBAAiB,QAAQ,kBAAkB;AACjD,QAAM,SAAS,sBAAsB,gBAAgB,QAAQ,QAAQ;AACrE,QAAM,SAAS,sBAAsB,gBAAgB,QAAQ,QAAQ;AAErE,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,QAAQ,MAAM,QAAQ,YAAY,MAAM;AAAA,MAC5C,OAAO,CAAC,QAAQ,QAAQ,MAAM;AAAA,IAChC,CAAC;AAED,QAAI,WAAW;AACf,QAAI,UAAU;AAEd,UAAM,UACJ,QAAQ,cAAc,SAClB,SACA,WAAW,MAAM;AACf,iBAAW;AACX,YAAM,KAAK,SAAS;AAAA,IACtB,GAAG,QAAQ,SAAS;AAE1B,UAAM,QAAQ,MAAM;AAClB,YAAM,KAAK,SAAS;AAAA,IACtB;AAEA,QAAI,QAAQ,QAAQ,YAAY,MAAM;AACpC,YAAM;AAAA,IACR,OAAO;AACL,cAAQ,QAAQ,iBAAiB,SAAS,OAAO,EAAE,MAAM,KAAK,CAAC;AAAA,IACjE;AAEA,UAAM,OAAO,GAAG,QAAQ,CAAC,UAAkB,OAAO,OAAO,KAAK,CAAC;AAC/D,UAAM,OAAO,GAAG,QAAQ,CAAC,UAAkB,OAAO,OAAO,KAAK,CAAC;AAE/D,UAAM,GAAG,SAAS,CAAC,UAAU;AAC3B,UAAI,SAAS;AACX;AAAA,MACF;AACA,gBAAU;AACV,mBAAa,OAAO;AACpB,cAAQ,QAAQ,oBAAoB,SAAS,KAAK;AAElD,UAAK,MAAgC,SAAS,UAAU;AACtD;AAAA,UACE,IAAI,mBAAmB,6BAA6B,sBAAsB,QAAW;AAAA,YACnF,OAAO;AAAA,UACT,CAAC;AAAA,QACH;AACA;AAAA,MACF;AAEA,aAAO,KAAK;AAAA,IACd,CAAC;AAED,UAAM,GAAG,SAAS,CAAC,SAAS;AAC1B,UAAI,SAAS;AACX;AAAA,MACF;AACA,gBAAU;AACV,mBAAa,OAAO;AACpB,cAAQ,QAAQ,oBAAoB,SAAS,KAAK;AAElD,UAAI,QAAQ,QAAQ,YAAY,MAAM;AACpC,eAAO,QAAQ,OAAO,UAAU,IAAI,aAAa,WAAW,YAAY,CAAC;AACzE;AAAA,MACF;AAEA,cAAQ;AAAA,QACN,QAAQ,OAAO,MAAM;AAAA,QACrB,QAAQ,OAAO,MAAM;AAAA,QACrB,UAAU,QAAQ;AAAA,QAClB,YAAY,KAAK,IAAI,IAAI;AAAA,QACzB;AAAA,QACA,iBAAiB,OAAO;AAAA,QACxB,iBAAiB,OAAO;AAAA,MAC1B,CAAC;AAAA,IACH,CAAC;AAED,QAAI,QAAQ,UAAU,QAAW;AAC/B,YAAM,MAAM,IAAI,QAAQ,KAAK;AAAA,IAC/B,OAAO;AACL,YAAM,MAAM,IAAI;AAAA,IAClB;AAAA,EACF,CAAC;AACH;AAEA,eAAsB,gBAAgB,MAAgB,SAA0C;AAC9F,QAAM,SAAS,MAAM,aAAa,MAAM,OAAO;AAE/C,MAAI,OAAO,aAAa,GAAG;AACzB,UAAM,IAAI;AAAA,MACR,iCAAiC,cAAc,IAAI,EAAE,KAAK,GAAG,CAAC;AAAA,MAC9D;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAGA,IAAM,gBAAgB,oBAAI,IAAI,CAAC,SAAS,IAAI,CAAC;AAU7C,SAAS,cAAc,MAA0B;AAC/C,SAAO,KAAK,IAAI,CAAC,KAAK,UAAU;AAC9B,UAAM,OAAO,KAAK,QAAQ,CAAC;AAC3B,QAAI,SAAS,UAAa,cAAc,IAAI,IAAI,EAAG,QAAO,oBAAoB,GAAG;AACjF,eAAW,WAAW,eAAe;AACnC,YAAM,SAAS,GAAG,OAAO;AACzB,UAAI,IAAI,WAAW,MAAM;AACvB,eAAO,GAAG,MAAM,GAAG,oBAAoB,IAAI,MAAM,OAAO,MAAM,CAAC,CAAC;AAAA,IACpE;AACA,QAAI,IAAI,WAAW,IAAI,KAAK,CAAC,IAAI,WAAW,KAAK,KAAK,IAAI,SAAS;AACjE,aAAO,KAAK,oBAAoB,IAAI,MAAM,CAAC,CAAC,CAAC;AAC/C,WAAO;AAAA,EACT,CAAC;AACH;AAEA,SAAS,oBAAoB,OAAuB;AAClD,QAAM,YAAY,MAAM,QAAQ,GAAG;AACnC,SAAO,cAAc,KAAK,QAAQ,GAAG,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC;AACpE;AAEA,SAAS,sBAAsB,UAAkB,SAAuC;AACtF,QAAM,SAAmB,CAAC;AAC1B,MAAI,SAAS;AACb,MAAI,YAAY;AAEhB,SAAO;AAAA,IACL,IAAI,YAAY;AACd,aAAO;AAAA,IACT;AAAA,IACA,OAAO,OAAe;AACpB,gBAAU,KAAK;AAEf,UAAI,UAAU,UAAU;AACtB,oBAAY;AACZ;AAAA,MACF;AAEA,YAAM,YAAY,WAAW;AAC7B,YAAM,OAAO,MAAM,SAAS,YAAY,MAAM,SAAS,GAAG,SAAS,IAAI;AACvE,aAAO,KAAK,IAAI;AAChB,gBAAU,KAAK;AAEf,UAAI,KAAK,SAAS,MAAM,QAAQ;AAC9B,oBAAY;AAAA,MACd;AAAA,IACF;AAAA,IACA,QAAQ;AACN,YAAM,QAAQ,OAAO,OAAO,QAAQ,MAAM;AAC1C,aAAO,IAAI,WAAW,MAAM,QAAQ,MAAM,YAAY,MAAM,UAAU,EAAE,MAAM;AAAA,IAChF;AAAA,EACF;AACF;AAEO,SAAS,WAAW,OAA2B;AACpD,SAAO,IAAI,YAAY,SAAS,EAAE,OAAO,KAAK,CAAC,EAAE,OAAO,KAAK;AAC/D;","names":[]}
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
assertDockerCli,
|
|
4
4
|
decodeUtf8,
|
|
5
5
|
runDockerCli
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-ZMZH4DBC.js";
|
|
7
7
|
|
|
8
8
|
// src/docker-sandbox.ts
|
|
9
9
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -1209,15 +1209,19 @@ var DockerSandboxRuntimeImpl = class {
|
|
|
1209
1209
|
"1",
|
|
1210
1210
|
"-maxdepth",
|
|
1211
1211
|
"1",
|
|
1212
|
-
"-
|
|
1213
|
-
"
|
|
1212
|
+
"-exec",
|
|
1213
|
+
"stat",
|
|
1214
|
+
"-c",
|
|
1215
|
+
"%n %f %s",
|
|
1216
|
+
"{}",
|
|
1217
|
+
"+"
|
|
1214
1218
|
]
|
|
1215
1219
|
},
|
|
1216
1220
|
abortSignal
|
|
1217
1221
|
);
|
|
1218
1222
|
if (result.status !== "exited" || result.exitCode !== 0) {
|
|
1219
1223
|
throw new DockerSandboxError(
|
|
1220
|
-
"Unable to list sandbox files
|
|
1224
|
+
dockerExecFailureMessage("Unable to list sandbox files", result),
|
|
1221
1225
|
"docker_command_failed",
|
|
1222
1226
|
result
|
|
1223
1227
|
);
|
|
@@ -1820,8 +1824,11 @@ async function assertCopiedRegularFile(temporaryDirectory, target, sandboxPath)
|
|
|
1820
1824
|
"invalid_path"
|
|
1821
1825
|
);
|
|
1822
1826
|
}
|
|
1823
|
-
const resolvedTarget = await
|
|
1824
|
-
|
|
1827
|
+
const [resolvedDirectory, resolvedTarget] = await Promise.all([
|
|
1828
|
+
realpath(temporaryDirectory),
|
|
1829
|
+
realpath(target)
|
|
1830
|
+
]);
|
|
1831
|
+
const relativeTarget = path2.relative(resolvedDirectory, resolvedTarget);
|
|
1825
1832
|
if (relativeTarget === "" || relativeTarget === ".." || relativeTarget.startsWith(`..${path2.sep}`) || path2.isAbsolute(relativeTarget)) {
|
|
1826
1833
|
throw new DockerSandboxError(
|
|
1827
1834
|
`Sandbox file escaped its temporary read boundary: ${sandboxPath}`,
|
|
@@ -1844,13 +1851,14 @@ async function removeVolume(dockerPath, volumeName) {
|
|
|
1844
1851
|
);
|
|
1845
1852
|
}
|
|
1846
1853
|
function parseFindEntry(line, workdir) {
|
|
1847
|
-
const [absolutePath,
|
|
1848
|
-
|
|
1854
|
+
const [absolutePath, rawMode, rawSize] = line.split(" ");
|
|
1855
|
+
const mode = rawMode === void 0 ? Number.NaN : Number.parseInt(rawMode, 16);
|
|
1856
|
+
if (absolutePath === void 0 || rawSize === void 0 || !Number.isSafeInteger(mode)) {
|
|
1849
1857
|
throw new DockerSandboxError("Docker returned invalid file metadata.", "docker_command_failed");
|
|
1850
1858
|
}
|
|
1851
1859
|
const relativePath = path2.posix.relative(workdir, absolutePath);
|
|
1852
1860
|
const size = Number(rawSize);
|
|
1853
|
-
const type =
|
|
1861
|
+
const type = mapFileMode(mode);
|
|
1854
1862
|
let entry = {
|
|
1855
1863
|
path: normalizeSandboxPath(relativePath),
|
|
1856
1864
|
type
|
|
@@ -1860,12 +1868,21 @@ function parseFindEntry(line, workdir) {
|
|
|
1860
1868
|
}
|
|
1861
1869
|
return entry;
|
|
1862
1870
|
}
|
|
1863
|
-
function
|
|
1864
|
-
|
|
1865
|
-
if (
|
|
1866
|
-
if (
|
|
1871
|
+
function mapFileMode(mode) {
|
|
1872
|
+
const type = mode & 61440;
|
|
1873
|
+
if (type === 32768) return "file";
|
|
1874
|
+
if (type === 16384) return "directory";
|
|
1875
|
+
if (type === 40960) return "symlink";
|
|
1867
1876
|
return "other";
|
|
1868
1877
|
}
|
|
1878
|
+
function dockerExecFailureMessage(prefix, result) {
|
|
1879
|
+
const detail = (safeDecode(result.stderr).trim() || safeDecode(result.stdout).trim()).slice(
|
|
1880
|
+
0,
|
|
1881
|
+
500
|
|
1882
|
+
);
|
|
1883
|
+
if (detail.length > 0) return `${prefix}: ${detail}`;
|
|
1884
|
+
return result.status === "exited" ? `${prefix}: command exited with code ${result.exitCode}.` : `${prefix}: command timed out.`;
|
|
1885
|
+
}
|
|
1869
1886
|
function toExecResult(result) {
|
|
1870
1887
|
const common = {
|
|
1871
1888
|
stdout: result.stdout.slice(),
|
|
@@ -2018,8 +2035,10 @@ var allToolNames = [
|
|
|
2018
2035
|
"wait_for_port"
|
|
2019
2036
|
];
|
|
2020
2037
|
var execCommandInput = z.object({
|
|
2021
|
-
command: z.string().min(1)
|
|
2022
|
-
|
|
2038
|
+
command: z.string().min(1).describe(
|
|
2039
|
+
"Command to run. Complete shell command lines are supported; use args for exact argv execution."
|
|
2040
|
+
),
|
|
2041
|
+
args: z.array(z.string()).describe("Optional exact arguments. Omit when command contains the complete command line.").optional(),
|
|
2023
2042
|
cwd: z.string().describe("Workspace-relative directory or an absolute directory inside the sandbox workdir.").optional(),
|
|
2024
2043
|
env: z.record(z.string(), z.string()).optional(),
|
|
2025
2044
|
timeoutMs: z.number().int().positive().max(3e5).optional(),
|
|
@@ -2149,17 +2168,17 @@ function createDockerSandboxTools(options) {
|
|
|
2149
2168
|
function createExecCommandTool(options) {
|
|
2150
2169
|
return createTool({
|
|
2151
2170
|
name: "exec_command",
|
|
2152
|
-
description: "Run
|
|
2171
|
+
description: "Run a command inside the sandbox, including natural shell command lines.",
|
|
2153
2172
|
inputSchema: execCommandInput,
|
|
2154
2173
|
outputSchema: execResultOutput,
|
|
2155
2174
|
execute: async ({ command, args, cwd, env, timeoutMs, input }, context) => {
|
|
2156
|
-
|
|
2175
|
+
const invocation = resolveExecInvocation(command, args, options.exec?.commands);
|
|
2157
2176
|
const effectiveTimeoutMs = timeoutMs ?? options.exec?.defaultTimeoutMs;
|
|
2158
2177
|
assertTimeoutAllowed(effectiveTimeoutMs, options.exec?.maxTimeoutMs);
|
|
2159
2178
|
let execOptions = {
|
|
2160
|
-
command
|
|
2179
|
+
command: invocation.command
|
|
2161
2180
|
};
|
|
2162
|
-
if (args !== void 0) execOptions = { ...execOptions, args };
|
|
2181
|
+
if (invocation.args !== void 0) execOptions = { ...execOptions, args: invocation.args };
|
|
2163
2182
|
if (cwd !== void 0) {
|
|
2164
2183
|
execOptions = { ...execOptions, cwd: normalizeToolPath(options.sandbox, cwd, true) };
|
|
2165
2184
|
}
|
|
@@ -2519,6 +2538,22 @@ function assertCommandAllowed(command, policy) {
|
|
|
2519
2538
|
}
|
|
2520
2539
|
}
|
|
2521
2540
|
}
|
|
2541
|
+
function resolveExecInvocation(command, args, policy) {
|
|
2542
|
+
if (args === void 0 && requiresShell(command)) {
|
|
2543
|
+
if (policy?.mode === "block") {
|
|
2544
|
+
throw toolPolicyError(
|
|
2545
|
+
"Natural command lines cannot be used with block-mode command policies. Use command and args instead."
|
|
2546
|
+
);
|
|
2547
|
+
}
|
|
2548
|
+
assertCommandAllowed("sh", policy);
|
|
2549
|
+
return { command: "sh", args: ["-c", command] };
|
|
2550
|
+
}
|
|
2551
|
+
assertCommandAllowed(command, policy);
|
|
2552
|
+
return args === void 0 ? { command } : { command, args };
|
|
2553
|
+
}
|
|
2554
|
+
function requiresShell(command) {
|
|
2555
|
+
return /[\s|&;<>()$`'"*?[\]{}!#~=]/u.test(command);
|
|
2556
|
+
}
|
|
2522
2557
|
function assertTimeoutAllowed(timeoutMs, maxTimeoutMs) {
|
|
2523
2558
|
if (timeoutMs !== void 0 && maxTimeoutMs !== void 0 && timeoutMs > maxTimeoutMs) {
|
|
2524
2559
|
throw toolPolicyError(`Command timeout exceeds policy (${timeoutMs} > ${maxTimeoutMs}).`);
|