@anvia/sandbox 1.1.1 → 1.1.3
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 +16 -0
- package/dist/{chunk-4D2GWLEH.js → chunk-D5XZQ2ES.js} +1 -1
- package/dist/chunk-D5XZQ2ES.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +82 -3
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-4D2GWLEH.js.map +0 -1
package/README.md
CHANGED
|
@@ -32,11 +32,27 @@ 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
|
+
With `exec.commands.mode: "allow"`, both `exec_command` and `start_process` reject known shell
|
|
36
|
+
executables by default, including path-qualified names such as `/bin/sh`. To permit an allowlisted
|
|
37
|
+
shell, set `exec.commands.allowShellInterpreters` to the boolean `true`. Omitted or `false` keeps the
|
|
38
|
+
guard enabled; non-boolean values such as `"false"` are rejected when creating tools. This guard
|
|
39
|
+
checks executable names only: allowlisted runtimes such as Node.js or Python can still launch other
|
|
40
|
+
commands, so the command policy does not restrict what those programs can execute.
|
|
41
|
+
|
|
35
42
|
`resources.sharedMemoryMb` maps to a private Docker `/dev/shm` size. A security configuration may use
|
|
36
43
|
explicit `dropCapabilities` and `addCapabilities` arrays, plus
|
|
37
44
|
`seccompProfile: { type: "path", path }` with an absolute host path. These options are used by
|
|
38
45
|
`@anvia/browser` to keep Chromium's own process sandbox enabled.
|
|
39
46
|
|
|
47
|
+
`createSandbox()` accepts `containerRuntime` — the runtime name registered in the Docker daemon (for
|
|
48
|
+
example `runsc` for gVisor). The runtime must already be registered (`runsc install` followed by a
|
|
49
|
+
daemon restart); creation fails with a `runtime_not_found` error otherwise. `resumeSandbox()` keeps
|
|
50
|
+
the container's original runtime. Under gVisor, workspace files persist through
|
|
51
|
+
`stop()`/`resumeSandbox()` because they live on a Docker volume, but changes to the container's
|
|
52
|
+
writable rootfs outside the workspace do not survive a resume, and `security.seccompProfile` is not
|
|
53
|
+
enforced the way it is on the default runtime because the gVisor sentry mediates application
|
|
54
|
+
syscalls itself.
|
|
55
|
+
|
|
40
56
|
Studio does not discover sandboxes through tool metadata. Register a read-only inspector explicitly:
|
|
41
57
|
|
|
42
58
|
```ts
|
|
@@ -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 ${args.join(\" \")}`,\n \"docker_command_failed\",\n result,\n );\n }\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":"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","names":[]}
|
package/dist/cli.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -42,6 +42,11 @@ type CreateDockerSandboxOptions = Readonly<{
|
|
|
42
42
|
workdir?: string;
|
|
43
43
|
workspace: DockerSandboxWorkspace;
|
|
44
44
|
network: DockerSandboxNetwork;
|
|
45
|
+
/** Docker runtime used to run the container, as registered in the daemon
|
|
46
|
+
* (for example "runsc" for gVisor). The runtime must already be registered;
|
|
47
|
+
* createSandbox() fails with `runtime_not_found` otherwise. Omitted means
|
|
48
|
+
* the daemon default. resumeSandbox() keeps the container's runtime. */
|
|
49
|
+
containerRuntime?: string;
|
|
45
50
|
files?: Readonly<Record<string, string | Uint8Array>>;
|
|
46
51
|
directories?: readonly string[];
|
|
47
52
|
env?: Readonly<Record<string, string>>;
|
|
@@ -208,6 +213,7 @@ type DockerSandboxInspector = Readonly<{
|
|
|
208
213
|
id: string;
|
|
209
214
|
provider: "docker";
|
|
210
215
|
workdir: string;
|
|
216
|
+
containerRuntime: string;
|
|
211
217
|
listFiles?: DockerSandboxRuntime["listFiles"];
|
|
212
218
|
readFile?: DockerSandboxRuntime["readFile"];
|
|
213
219
|
publishedPorts?: readonly DockerSandboxPublishedPort[];
|
|
@@ -229,6 +235,7 @@ type DockerSandboxToolName = "exec_command" | "read_file" | "write_file" | "list
|
|
|
229
235
|
type DockerSandboxCommandPolicy = Readonly<{
|
|
230
236
|
mode: "allow";
|
|
231
237
|
values: readonly string[];
|
|
238
|
+
allowShellInterpreters?: boolean;
|
|
232
239
|
}> | Readonly<{
|
|
233
240
|
mode: "block";
|
|
234
241
|
values: readonly string[];
|
|
@@ -270,10 +277,11 @@ declare class DockerSandboxClient {
|
|
|
270
277
|
private assertImageExists;
|
|
271
278
|
private assertContainerDoesNotExist;
|
|
272
279
|
private assertVolumeExists;
|
|
280
|
+
private assertRuntimeAvailable;
|
|
273
281
|
private cliOptions;
|
|
274
282
|
}
|
|
275
283
|
|
|
276
|
-
type DockerSandboxErrorCode = "docker_unavailable" | "docker_command_failed" | "image_not_found" | "volume_not_found" | "sandbox_not_found" | "invalid_state" | "invalid_path" | "timeout" | "file_too_large" | "tool_policy" | "port" | "process";
|
|
284
|
+
type DockerSandboxErrorCode = "docker_unavailable" | "docker_command_failed" | "image_not_found" | "runtime_not_found" | "volume_not_found" | "sandbox_not_found" | "invalid_state" | "invalid_path" | "timeout" | "file_too_large" | "tool_policy" | "port" | "process";
|
|
277
285
|
declare class DockerSandboxError extends Error {
|
|
278
286
|
readonly code: DockerSandboxErrorCode;
|
|
279
287
|
readonly details?: unknown | undefined;
|
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-D5XZQ2ES.js";
|
|
7
7
|
|
|
8
8
|
// src/docker-sandbox.ts
|
|
9
9
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -610,6 +610,7 @@ var labels = {
|
|
|
610
610
|
workspaceType: `${labelPrefix}workspace.type`,
|
|
611
611
|
workspaceVolume: `${labelPrefix}workspace.volume`,
|
|
612
612
|
networkMode: `${labelPrefix}network.mode`,
|
|
613
|
+
containerRuntime: `${labelPrefix}container-runtime`,
|
|
613
614
|
commandTimeoutMs: `${labelPrefix}runtime.command-timeout-ms`,
|
|
614
615
|
maxOutputBytes: `${labelPrefix}runtime.max-output-bytes`,
|
|
615
616
|
maxFileBytes: `${labelPrefix}runtime.max-file-bytes`,
|
|
@@ -650,6 +651,7 @@ var DockerSandboxClient = class {
|
|
|
650
651
|
assertSandboxId(id);
|
|
651
652
|
const containerName = containerNameFor(id);
|
|
652
653
|
const workdir = options.workdir ?? defaultWorkdir;
|
|
654
|
+
const containerRuntime = options.containerRuntime ?? "default";
|
|
653
655
|
const runtime = resolveRuntimeLimits(options.runtime);
|
|
654
656
|
const workspace = copyWorkspace(options.workspace);
|
|
655
657
|
const network = copyNetwork(options.network);
|
|
@@ -662,6 +664,7 @@ var DockerSandboxClient = class {
|
|
|
662
664
|
if (workspace.type === "docker-volume") {
|
|
663
665
|
await this.assertVolumeExists(workspace.name, options.abortSignal);
|
|
664
666
|
}
|
|
667
|
+
await this.assertRuntimeAvailable(containerRuntime, options.abortSignal);
|
|
665
668
|
let containerCreated = false;
|
|
666
669
|
let volumeCreated = false;
|
|
667
670
|
try {
|
|
@@ -678,6 +681,7 @@ var DockerSandboxClient = class {
|
|
|
678
681
|
containerName,
|
|
679
682
|
image: options.image,
|
|
680
683
|
workdir,
|
|
684
|
+
containerRuntime,
|
|
681
685
|
workspace,
|
|
682
686
|
volumeName,
|
|
683
687
|
env,
|
|
@@ -704,6 +708,7 @@ var DockerSandboxClient = class {
|
|
|
704
708
|
id,
|
|
705
709
|
containerName,
|
|
706
710
|
workdir,
|
|
711
|
+
containerRuntime,
|
|
707
712
|
workspace,
|
|
708
713
|
volumeName,
|
|
709
714
|
ownsVolume,
|
|
@@ -818,6 +823,37 @@ var DockerSandboxClient = class {
|
|
|
818
823
|
result
|
|
819
824
|
);
|
|
820
825
|
}
|
|
826
|
+
async assertRuntimeAvailable(name, abortSignal) {
|
|
827
|
+
if (name === "default") return;
|
|
828
|
+
const result = await runDockerCli(["info", "--format", "{{json .Runtimes}}"], {
|
|
829
|
+
...this.cliOptions(abortSignal),
|
|
830
|
+
maxOutputBytes: defaultMaxOutputBytes
|
|
831
|
+
});
|
|
832
|
+
if (result.exitCode !== 0) {
|
|
833
|
+
throw new DockerSandboxError(
|
|
834
|
+
"Unable to inspect Docker runtimes.",
|
|
835
|
+
"docker_command_failed",
|
|
836
|
+
result
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
let runtimes;
|
|
840
|
+
try {
|
|
841
|
+
runtimes = JSON.parse(decodeUtf8(result.stdout));
|
|
842
|
+
} catch (error) {
|
|
843
|
+
throw new DockerSandboxError(
|
|
844
|
+
"Docker returned invalid runtime metadata.",
|
|
845
|
+
"docker_command_failed",
|
|
846
|
+
void 0,
|
|
847
|
+
{ cause: error }
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
if (!isRecord(runtimes) || !(name in runtimes)) {
|
|
851
|
+
throw new DockerSandboxError(
|
|
852
|
+
`Docker runtime is not registered in the daemon: ${JSON.stringify(name)}. Install the runtime (for gVisor: runsc install) and restart the Docker daemon.`,
|
|
853
|
+
"runtime_not_found"
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
821
857
|
cliOptions(abortSignal) {
|
|
822
858
|
return { dockerPath: this.dockerPath, signal: abortSignal };
|
|
823
859
|
}
|
|
@@ -853,7 +889,8 @@ var DockerSandboxHandle = class {
|
|
|
853
889
|
let inspector = {
|
|
854
890
|
id: this.id,
|
|
855
891
|
provider: "docker",
|
|
856
|
-
workdir: this.configuration.workdir
|
|
892
|
+
workdir: this.configuration.workdir,
|
|
893
|
+
containerRuntime: this.configuration.containerRuntime
|
|
857
894
|
};
|
|
858
895
|
if (options.files === true) {
|
|
859
896
|
inspector = {
|
|
@@ -1370,6 +1407,7 @@ function createRunArgs(options) {
|
|
|
1370
1407
|
[labels.workspaceType]: options.workspace.type,
|
|
1371
1408
|
[labels.workspaceVolume]: options.volumeName,
|
|
1372
1409
|
[labels.networkMode]: options.network.mode,
|
|
1410
|
+
[labels.containerRuntime]: options.containerRuntime,
|
|
1373
1411
|
[labels.commandTimeoutMs]: `${options.runtime.commandTimeoutMs}`,
|
|
1374
1412
|
[labels.maxOutputBytes]: `${options.runtime.maxOutputBytes}`,
|
|
1375
1413
|
[labels.maxFileBytes]: `${options.runtime.maxFileBytes}`,
|
|
@@ -1385,6 +1423,9 @@ function createRunArgs(options) {
|
|
|
1385
1423
|
"-w",
|
|
1386
1424
|
options.workdir
|
|
1387
1425
|
];
|
|
1426
|
+
if (options.containerRuntime !== "default") {
|
|
1427
|
+
args.push("--runtime", options.containerRuntime);
|
|
1428
|
+
}
|
|
1388
1429
|
for (const [key, value] of Object.entries({ ...options.userLabels, ...runtimeLabels })) {
|
|
1389
1430
|
args.push("--label", `${key}=${value}`);
|
|
1390
1431
|
}
|
|
@@ -1464,6 +1505,9 @@ function validateCreateOptions(options) {
|
|
|
1464
1505
|
if (key.startsWith(labelPrefix)) throw new TypeError(`Docker label is reserved: ${key}`);
|
|
1465
1506
|
}
|
|
1466
1507
|
if (options.user !== void 0) assertNonEmptyString(options.user, "user");
|
|
1508
|
+
if (options.containerRuntime !== void 0) {
|
|
1509
|
+
assertNonEmptyString(options.containerRuntime, "containerRuntime");
|
|
1510
|
+
}
|
|
1467
1511
|
if (options.directories !== void 0 && !Array.isArray(options.directories)) {
|
|
1468
1512
|
throw new TypeError("directories must be an array.");
|
|
1469
1513
|
}
|
|
@@ -1606,6 +1650,9 @@ function snapshotCreateOptions(options) {
|
|
|
1606
1650
|
};
|
|
1607
1651
|
if (options.id !== void 0) snapshot = { ...snapshot, id: options.id };
|
|
1608
1652
|
if (options.workdir !== void 0) snapshot = { ...snapshot, workdir: options.workdir };
|
|
1653
|
+
if (options.containerRuntime !== void 0) {
|
|
1654
|
+
snapshot = { ...snapshot, containerRuntime: options.containerRuntime };
|
|
1655
|
+
}
|
|
1609
1656
|
if (options.files !== void 0) snapshot = { ...snapshot, files: Object.freeze(files) };
|
|
1610
1657
|
if (options.directories !== void 0) {
|
|
1611
1658
|
snapshot = { ...snapshot, directories: Object.freeze([...options.directories]) };
|
|
@@ -1664,6 +1711,9 @@ function configurationFromInspection(id, containerName, inspection) {
|
|
|
1664
1711
|
const workspaceType = requiredLabel(containerLabels, labels.workspaceType);
|
|
1665
1712
|
const volumeName = requiredLabel(containerLabels, labels.workspaceVolume);
|
|
1666
1713
|
const networkMode = requiredLabel(containerLabels, labels.networkMode);
|
|
1714
|
+
const labeledRuntime = containerLabels[labels.containerRuntime];
|
|
1715
|
+
const hostRuntime = inspection.HostConfig?.Runtime;
|
|
1716
|
+
const containerRuntime = labeledRuntime !== void 0 && labeledRuntime !== "" ? labeledRuntime : hostRuntime !== void 0 && hostRuntime !== "" ? hostRuntime : "default";
|
|
1667
1717
|
if (networkMode !== "none" && networkMode !== "bridge") invalidInspection("network mode");
|
|
1668
1718
|
const workspace = workspaceType === "ephemeral" ? { type: "ephemeral" } : workspaceType === "docker-volume" ? { type: "docker-volume", name: volumeName } : invalidInspection("workspace type");
|
|
1669
1719
|
const runtime = {
|
|
@@ -1675,6 +1725,7 @@ function configurationFromInspection(id, containerName, inspection) {
|
|
|
1675
1725
|
return {
|
|
1676
1726
|
id,
|
|
1677
1727
|
containerName,
|
|
1728
|
+
containerRuntime,
|
|
1678
1729
|
workdir,
|
|
1679
1730
|
workspace,
|
|
1680
1731
|
volumeName,
|
|
@@ -1926,6 +1977,22 @@ function isRecord(value) {
|
|
|
1926
1977
|
// src/tools.ts
|
|
1927
1978
|
import { createTool } from "@anvia/core/tool";
|
|
1928
1979
|
import { z } from "zod";
|
|
1980
|
+
|
|
1981
|
+
// src/types.ts
|
|
1982
|
+
var shellInterpreters = [
|
|
1983
|
+
"sh",
|
|
1984
|
+
"bash",
|
|
1985
|
+
"zsh",
|
|
1986
|
+
"ksh",
|
|
1987
|
+
"dash",
|
|
1988
|
+
"ash",
|
|
1989
|
+
"busybox",
|
|
1990
|
+
"fish",
|
|
1991
|
+
"csh",
|
|
1992
|
+
"tcsh"
|
|
1993
|
+
];
|
|
1994
|
+
|
|
1995
|
+
// src/tools.ts
|
|
1929
1996
|
var allToolNames = [
|
|
1930
1997
|
"exec_command",
|
|
1931
1998
|
"read_file",
|
|
@@ -2361,7 +2428,8 @@ function snapshotFactoryOptions(options) {
|
|
|
2361
2428
|
const toolNames = Object.freeze([...options.tools]);
|
|
2362
2429
|
const commands = options.exec?.commands === void 0 ? void 0 : Object.freeze({
|
|
2363
2430
|
mode: options.exec.commands.mode,
|
|
2364
|
-
values: Object.freeze([...options.exec.commands.values])
|
|
2431
|
+
values: Object.freeze([...options.exec.commands.values]),
|
|
2432
|
+
...options.exec.commands.mode === "allow" && options.exec.commands.allowShellInterpreters !== void 0 ? { allowShellInterpreters: options.exec.commands.allowShellInterpreters } : {}
|
|
2365
2433
|
});
|
|
2366
2434
|
let snapshot = {
|
|
2367
2435
|
sandbox: options.sandbox,
|
|
@@ -2389,6 +2457,9 @@ function validateCommandPolicy(policy) {
|
|
|
2389
2457
|
if (policy.mode !== "allow" && policy.mode !== "block") {
|
|
2390
2458
|
throw toolPolicyError("exec.commands must use mode allow or block.");
|
|
2391
2459
|
}
|
|
2460
|
+
if (policy.mode === "allow" && policy.allowShellInterpreters !== void 0 && typeof policy.allowShellInterpreters !== "boolean") {
|
|
2461
|
+
throw toolPolicyError("exec.commands.allowShellInterpreters must be a boolean.");
|
|
2462
|
+
}
|
|
2392
2463
|
if (!Array.isArray(policy.values))
|
|
2393
2464
|
throw toolPolicyError("exec.commands.values must be an array.");
|
|
2394
2465
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -2407,6 +2478,14 @@ function assertCommandAllowed(command, policy) {
|
|
|
2407
2478
|
if (policy.mode === "allow" && !included || policy.mode === "block" && included) {
|
|
2408
2479
|
throw toolPolicyError(`Command is rejected by sandbox tool policy: ${command}`);
|
|
2409
2480
|
}
|
|
2481
|
+
if (policy.mode === "allow" && policy.allowShellInterpreters !== true) {
|
|
2482
|
+
const commandBasename = command.split("/").pop() ?? command;
|
|
2483
|
+
if (shellInterpreters.includes(commandBasename)) {
|
|
2484
|
+
throw toolPolicyError(
|
|
2485
|
+
`Command is rejected by sandbox tool policy: ${command} (shell interpreter not allowed)`
|
|
2486
|
+
);
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2410
2489
|
}
|
|
2411
2490
|
function assertTimeoutAllowed(timeoutMs, maxTimeoutMs) {
|
|
2412
2491
|
if (timeoutMs !== void 0 && maxTimeoutMs !== void 0 && timeoutMs > maxTimeoutMs) {
|