@anvia/sandbox 1.1.7 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -62
- package/dist/index.js +65 -16
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @anvia/sandbox
|
|
2
2
|
|
|
3
|
-
Docker-backed
|
|
3
|
+
Give agents a workspace where they can run commands and work with files. Docker-backed sandboxes keep execution infrastructure explicit and under your application’s control.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add @anvia/sandbox @anvia/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
Requires Node.js 20.12 or later and an accessible Docker daemon. Pull an image before creating a sandbox.
|
|
4
14
|
|
|
5
15
|
```ts
|
|
6
16
|
import { createDockerSandboxTools, DockerSandboxClient } from "@anvia/sandbox";
|
|
@@ -18,67 +28,32 @@ const tools = createDockerSandboxTools({
|
|
|
18
28
|
sandbox: sandbox.runtime,
|
|
19
29
|
tools: ["exec_command", "read_file", "write_file", "list_files"],
|
|
20
30
|
});
|
|
31
|
+
// Pass tools to new Agent({ id, model, tools }).
|
|
21
32
|
```
|
|
22
33
|
|
|
23
|
-
|
|
24
|
-
and `createSandbox()` and `resumeSandbox()` never pull images. `DockerSandbox` owns the container and,
|
|
25
|
-
for an ephemeral workspace, its Docker volume. Dispose it asynchronously or call `destroy()`.
|
|
26
|
-
|
|
27
|
-
`stop()` preserves the container and workspace. `resumeSandbox({ id })` creates a fresh live handle for
|
|
28
|
-
that container; workspace files persist, while the in-memory managed-process registry starts empty. A
|
|
29
|
-
caller-owned `{ type: "docker-volume", name }` workspace is never deleted by the sandbox.
|
|
30
|
-
|
|
31
|
-
Networking is explicit. Use `{ mode: "none" }` or `{ mode: "bridge", ports: [...] }`; published ports
|
|
32
|
-
bind only to `127.0.0.1`. Runtime methods use object arguments, propagate abort signals, and expose
|
|
33
|
-
command and process output as bytes. Tool wrappers decode UTF-8 strictly and return structured values.
|
|
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
|
-
|
|
42
|
-
File paths and command working directories passed to agent tools may be workspace-relative (for
|
|
43
|
-
example `notes/result.txt`) or absolute paths inside the sandbox workdir (for example
|
|
44
|
-
`/workspace/notes/result.txt` with the default workdir). Paths outside the sandbox workdir are
|
|
45
|
-
rejected.
|
|
46
|
-
|
|
47
|
-
With `exec.commands.mode: "allow"`, both `exec_command` and `start_process` reject known shell
|
|
48
|
-
executables by default, including path-qualified names such as `/bin/sh`. To permit an allowlisted
|
|
49
|
-
shell, set `exec.commands.allowShellInterpreters` to the boolean `true`. Omitted or `false` keeps the
|
|
50
|
-
guard enabled; non-boolean values such as `"false"` are rejected when creating tools. This guard
|
|
51
|
-
checks executable names only: allowlisted runtimes such as Node.js or Python can still launch other
|
|
52
|
-
commands, so the command policy does not restrict what those programs can execute.
|
|
53
|
-
|
|
54
|
-
`resources.sharedMemoryMb` maps to a private Docker `/dev/shm` size. A security configuration may use
|
|
55
|
-
explicit `dropCapabilities` and `addCapabilities` arrays, plus
|
|
56
|
-
`seccompProfile: { type: "path", path }` with an absolute host path. These options are used by
|
|
57
|
-
`@anvia/browser` to keep Chromium's own process sandbox enabled.
|
|
58
|
-
|
|
59
|
-
`createSandbox()` accepts `containerRuntime` — the runtime name registered in the Docker daemon (for
|
|
60
|
-
example `runsc` for gVisor). The runtime must already be registered (`runsc install` followed by a
|
|
61
|
-
daemon restart); creation fails with a `runtime_not_found` error otherwise. `resumeSandbox()` keeps
|
|
62
|
-
the container's original runtime. Under gVisor, workspace files persist through
|
|
63
|
-
`stop()`/`resumeSandbox()` because they live on a Docker volume, but changes to the container's
|
|
64
|
-
writable rootfs outside the workspace do not survive a resume, and `security.seccompProfile` is not
|
|
65
|
-
enforced the way it is on the default runtime because the gVisor sentry mediates application
|
|
66
|
-
syscalls itself.
|
|
67
|
-
|
|
68
|
-
Studio does not discover sandboxes through tool metadata. Register a read-only inspector explicitly:
|
|
34
|
+
## What you get
|
|
69
35
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
36
|
+
- Command execution, file access, and managed processes.
|
|
37
|
+
- Ephemeral workspaces or caller-owned Docker volumes.
|
|
38
|
+
- Explicit networking, resource limits, and command policies.
|
|
39
|
+
- Stop/resume lifecycle and automatic disposal.
|
|
40
|
+
- Read-only sandbox inspection in Anvia Studio.
|
|
41
|
+
|
|
42
|
+
`await using` destroys the owned container and ephemeral volume at scope exit. Keep the scope open while agents use its tools. Command policies filter executable names; they do not restrict what an allowed interpreter can execute.
|
|
43
|
+
|
|
44
|
+
## Shell command lines
|
|
45
|
+
|
|
46
|
+
`exec_command` and `start_process` use `/bin/bash -c` for natural command lines when `/bin/bash`
|
|
47
|
+
is executable, falling back to `sh -c` when it is absent. Availability is cached per live sandbox
|
|
48
|
+
runtime handle. Explicit `command` plus `args` remains exact argv; use
|
|
49
|
+
`{ command: "sh", args: ["-c", script] }` to retain the previous POSIX shell behavior.
|
|
50
|
+
|
|
51
|
+
Allow policies must permit the selected executable (`/bin/bash` or `sh`) and set
|
|
52
|
+
`allowShellInterpreters: true`. Allowing only `sh` does not implicitly authorize Bash.
|
|
53
|
+
Block-mode policies continue to reject natural command lines. See the usage guide for details.
|
|
54
|
+
|
|
55
|
+
## Learn more
|
|
81
56
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
57
|
+
- [Usage guide](https://github.com/anvia-hq/anvia/blob/main/docs/packages/tool-sandbox.md)
|
|
58
|
+
- [Anvia](https://github.com/anvia-hq/anvia/blob/main/README.md)
|
|
59
|
+
- [Contributing](https://github.com/anvia-hq/anvia/blob/main/CONTRIBUTING.md)
|
package/dist/index.js
CHANGED
|
@@ -2007,6 +2007,47 @@ function isRecord(value) {
|
|
|
2007
2007
|
import { createTool } from "@anvia/core/tool";
|
|
2008
2008
|
import { z } from "zod";
|
|
2009
2009
|
|
|
2010
|
+
// src/shell.ts
|
|
2011
|
+
var shells = /* @__PURE__ */ new WeakMap();
|
|
2012
|
+
async function sandboxShell(sandbox, abortSignal) {
|
|
2013
|
+
abortSignal?.throwIfAborted();
|
|
2014
|
+
let pending = shells.get(sandbox);
|
|
2015
|
+
if (pending === void 0) {
|
|
2016
|
+
pending = detectShell(sandbox).catch((error) => {
|
|
2017
|
+
shells.delete(sandbox);
|
|
2018
|
+
throw error;
|
|
2019
|
+
});
|
|
2020
|
+
shells.set(sandbox, pending);
|
|
2021
|
+
}
|
|
2022
|
+
if (abortSignal === void 0) return pending;
|
|
2023
|
+
let abort;
|
|
2024
|
+
try {
|
|
2025
|
+
return await Promise.race([
|
|
2026
|
+
pending,
|
|
2027
|
+
new Promise((_, reject) => {
|
|
2028
|
+
abort = () => reject(abortSignal.reason);
|
|
2029
|
+
if (abortSignal.aborted) abort();
|
|
2030
|
+
else abortSignal.addEventListener("abort", abort, { once: true });
|
|
2031
|
+
})
|
|
2032
|
+
]);
|
|
2033
|
+
} finally {
|
|
2034
|
+
if (abort !== void 0) abortSignal.removeEventListener("abort", abort);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
async function detectShell(sandbox) {
|
|
2038
|
+
const result = await sandbox.exec({
|
|
2039
|
+
command: "sh",
|
|
2040
|
+
args: ["-c", "test -x /bin/bash"],
|
|
2041
|
+
timeoutMs: 5e3
|
|
2042
|
+
});
|
|
2043
|
+
if (result.status === "timed_out") {
|
|
2044
|
+
throw new DockerSandboxError("Sandbox shell detection timed out.", "timeout");
|
|
2045
|
+
}
|
|
2046
|
+
if (result.exitCode === 0) return "/bin/bash";
|
|
2047
|
+
if (result.exitCode === 1) return "sh";
|
|
2048
|
+
throw new DockerSandboxError("Could not detect the sandbox shell.", "docker_command_failed");
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2010
2051
|
// src/types.ts
|
|
2011
2052
|
var shellInterpreters = [
|
|
2012
2053
|
"sh",
|
|
@@ -2036,7 +2077,7 @@ var allToolNames = [
|
|
|
2036
2077
|
];
|
|
2037
2078
|
var execCommandInput = z.object({
|
|
2038
2079
|
command: z.string().min(1).describe(
|
|
2039
|
-
"Command to run.
|
|
2080
|
+
"Command to run. Natural command lines use /bin/bash -c when available, otherwise sh -c. Use args for exact argv execution."
|
|
2040
2081
|
),
|
|
2041
2082
|
args: z.array(z.string()).describe("Optional exact arguments. Omit when command contains the complete command line.").optional(),
|
|
2042
2083
|
cwd: z.string().describe("Workspace-relative directory or an absolute directory inside the sandbox workdir.").optional(),
|
|
@@ -2105,8 +2146,8 @@ var publishedPortOutput = z.object({
|
|
|
2105
2146
|
});
|
|
2106
2147
|
var listPortsOutput = z.object({ ports: z.array(publishedPortOutput) });
|
|
2107
2148
|
var startProcessInput = z.object({
|
|
2108
|
-
command:
|
|
2109
|
-
args:
|
|
2149
|
+
command: execCommandInput.shape.command,
|
|
2150
|
+
args: execCommandInput.shape.args,
|
|
2110
2151
|
cwd: z.string().describe("Workspace-relative directory or an absolute directory inside the sandbox workdir.").optional(),
|
|
2111
2152
|
env: z.record(z.string(), z.string()).optional()
|
|
2112
2153
|
});
|
|
@@ -2168,19 +2209,20 @@ function createDockerSandboxTools(options) {
|
|
|
2168
2209
|
function createExecCommandTool(options) {
|
|
2169
2210
|
return createTool({
|
|
2170
2211
|
name: "exec_command",
|
|
2171
|
-
description: "Run a command inside the sandbox,
|
|
2212
|
+
description: "Run a command inside the sandbox. Natural command lines use /bin/bash -c when available, otherwise sh -c; supply args for exact argv execution.",
|
|
2172
2213
|
inputSchema: execCommandInput,
|
|
2173
2214
|
outputSchema: execResultOutput,
|
|
2174
2215
|
execute: async ({ command, args, cwd, env, timeoutMs, input }, context) => {
|
|
2175
|
-
const invocation = resolveExecInvocation(command, args, options.exec?.commands);
|
|
2176
2216
|
const effectiveTimeoutMs = timeoutMs ?? options.exec?.defaultTimeoutMs;
|
|
2177
2217
|
assertTimeoutAllowed(effectiveTimeoutMs, options.exec?.maxTimeoutMs);
|
|
2218
|
+
const normalizedCwd = cwd === void 0 ? void 0 : normalizeToolPath(options.sandbox, cwd, true);
|
|
2219
|
+
const invocation = await resolveExecInvocation(options, command, args, context.abortSignal);
|
|
2178
2220
|
let execOptions = {
|
|
2179
2221
|
command: invocation.command
|
|
2180
2222
|
};
|
|
2181
2223
|
if (invocation.args !== void 0) execOptions = { ...execOptions, args: invocation.args };
|
|
2182
|
-
if (
|
|
2183
|
-
execOptions = { ...execOptions, cwd:
|
|
2224
|
+
if (normalizedCwd !== void 0) {
|
|
2225
|
+
execOptions = { ...execOptions, cwd: normalizedCwd };
|
|
2184
2226
|
}
|
|
2185
2227
|
if (env !== void 0) execOptions = { ...execOptions, env };
|
|
2186
2228
|
if (effectiveTimeoutMs !== void 0) {
|
|
@@ -2275,15 +2317,15 @@ function createListPortsTool(sandbox) {
|
|
|
2275
2317
|
function createStartProcessTool(options) {
|
|
2276
2318
|
return createTool({
|
|
2277
2319
|
name: "start_process",
|
|
2278
|
-
description: "Start a managed long-running process inside the sandbox.",
|
|
2320
|
+
description: "Start a managed long-running process inside the sandbox. Natural command lines use /bin/bash -c when available, otherwise sh -c; supply args for exact argv execution.",
|
|
2279
2321
|
inputSchema: startProcessInput,
|
|
2280
2322
|
outputSchema: processInfoOutput,
|
|
2281
2323
|
execute: async ({ command, args, cwd, env }, context) => {
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
if (
|
|
2286
|
-
startOptions = { ...startOptions, cwd:
|
|
2324
|
+
const normalizedCwd = cwd === void 0 ? void 0 : normalizeToolPath(options.sandbox, cwd, true);
|
|
2325
|
+
const invocation = await resolveExecInvocation(options, command, args, context.abortSignal);
|
|
2326
|
+
let startOptions = { ...invocation };
|
|
2327
|
+
if (normalizedCwd !== void 0) {
|
|
2328
|
+
startOptions = { ...startOptions, cwd: normalizedCwd };
|
|
2287
2329
|
}
|
|
2288
2330
|
if (env !== void 0) startOptions = { ...startOptions, env };
|
|
2289
2331
|
if (context.abortSignal !== void 0) {
|
|
@@ -2538,15 +2580,22 @@ function assertCommandAllowed(command, policy) {
|
|
|
2538
2580
|
}
|
|
2539
2581
|
}
|
|
2540
2582
|
}
|
|
2541
|
-
function resolveExecInvocation(command, args,
|
|
2583
|
+
async function resolveExecInvocation(options, command, args, abortSignal) {
|
|
2584
|
+
abortSignal?.throwIfAborted();
|
|
2585
|
+
const policy = options.exec?.commands;
|
|
2542
2586
|
if (args === void 0 && requiresShell(command)) {
|
|
2543
2587
|
if (policy?.mode === "block") {
|
|
2544
2588
|
throw toolPolicyError(
|
|
2545
2589
|
"Natural command lines cannot be used with block-mode command policies. Use command and args instead."
|
|
2546
2590
|
);
|
|
2547
2591
|
}
|
|
2548
|
-
|
|
2549
|
-
|
|
2592
|
+
if (policy?.mode === "allow" && policy.allowShellInterpreters !== true) {
|
|
2593
|
+
throw toolPolicyError("Natural command lines require allowShellInterpreters: true.");
|
|
2594
|
+
}
|
|
2595
|
+
const shell = await sandboxShell(options.sandbox, abortSignal);
|
|
2596
|
+
abortSignal?.throwIfAborted();
|
|
2597
|
+
assertCommandAllowed(shell, policy);
|
|
2598
|
+
return { command: shell, args: ["-c", command] };
|
|
2550
2599
|
}
|
|
2551
2600
|
assertCommandAllowed(command, policy);
|
|
2552
2601
|
return args === void 0 ? { command } : { command, args };
|