@zhijiewang/openharness 2.32.0 → 2.33.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/dist/harness/config.d.ts
CHANGED
|
@@ -214,6 +214,34 @@ export type OhConfig = {
|
|
|
214
214
|
rateLimit?: number;
|
|
215
215
|
allowedTools?: string[];
|
|
216
216
|
};
|
|
217
|
+
/**
|
|
218
|
+
* Opt-in OS-level sandbox via `@anthropic-ai/sandbox-runtime` (an optional
|
|
219
|
+
* dependency). When `enabled: true`, BashTool wraps every command in
|
|
220
|
+
* bubblewrap (Linux) or sandbox-exec (macOS) plus a domain-allowlist
|
|
221
|
+
* network proxy. Windows isn't supported by the package — the wrap is a
|
|
222
|
+
* silent passthrough there. Off by default; users opt in via config or the
|
|
223
|
+
* `--sandbox` CLI flag.
|
|
224
|
+
*
|
|
225
|
+
* `network.allowedDomains` is the proxy allowlist (e.g. `["github.com",
|
|
226
|
+
* "registry.npmjs.org"]`); `deniedDomains` blocks specific hosts before
|
|
227
|
+
* the allowlist applies. `filesystem.allowWrite` defaults to `[cwd]` —
|
|
228
|
+
* the sandbox can write to the project tree but nowhere else.
|
|
229
|
+
*
|
|
230
|
+
* See `src/harness/sandbox-runtime.ts` and SECURITY.md for the full
|
|
231
|
+
* threat-model boundary.
|
|
232
|
+
*/
|
|
233
|
+
sandbox?: {
|
|
234
|
+
enabled?: boolean;
|
|
235
|
+
network?: {
|
|
236
|
+
allowedDomains?: string[];
|
|
237
|
+
deniedDomains?: string[];
|
|
238
|
+
};
|
|
239
|
+
filesystem?: {
|
|
240
|
+
allowWrite?: string[];
|
|
241
|
+
denyWrite?: string[];
|
|
242
|
+
denyRead?: string[];
|
|
243
|
+
};
|
|
244
|
+
};
|
|
217
245
|
/**
|
|
218
246
|
* Environment variables injected into child processes spawned by the harness —
|
|
219
247
|
* Bash/Monitor/PowerShell tool executions and MCP server subprocesses. Useful
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OS-level sandbox integration via the optional `@anthropic-ai/sandbox-runtime`
|
|
3
|
+
* package. The package wraps a shell command in bubblewrap (Linux) or
|
|
4
|
+
* sandbox-exec (macOS) plus a network proxy that filters by domain allowlist.
|
|
5
|
+
*
|
|
6
|
+
* Boundaries:
|
|
7
|
+
* - **Linux + macOS**: real sandboxing via the package's static API.
|
|
8
|
+
* - **Windows**: not supported by the package — every wrap call returns null
|
|
9
|
+
* (graceful passthrough; tools spawn unsandboxed). Documented in SECURITY.md.
|
|
10
|
+
* - **Package not installed**: same passthrough behavior — installs cleanly
|
|
11
|
+
* without the optional dep on any platform.
|
|
12
|
+
*
|
|
13
|
+
* Lifecycle:
|
|
14
|
+
* - Initialized once per process on the first wrap request.
|
|
15
|
+
* - One `SandboxManager.initialize` covers all subsequent wrap calls.
|
|
16
|
+
* - No reset — the package documents auto-cleanup on process exit.
|
|
17
|
+
*
|
|
18
|
+
* Opt-in: callers pass `{ enabled: true }` (typically derived from
|
|
19
|
+
* `OhConfig.sandbox.enabled` or the `--sandbox` CLI flag). The default is
|
|
20
|
+
* off so existing users see no behavior change.
|
|
21
|
+
*/
|
|
22
|
+
import type { OhConfig } from "./config.js";
|
|
23
|
+
export type SandboxConfig = NonNullable<OhConfig["sandbox"]>;
|
|
24
|
+
/**
|
|
25
|
+
* Returns true on Linux/macOS where sandboxing is supported. Windows is
|
|
26
|
+
* unsupported by the underlying package, so we short-circuit there to avoid
|
|
27
|
+
* a misleading "tried to load and failed" log.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isSandboxAvailable(): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Wrap a shell command for sandboxed execution.
|
|
32
|
+
*
|
|
33
|
+
* Returns the wrapped command (a single shell string suitable for
|
|
34
|
+
* `spawn(cmd, { shell: "/bin/bash" })`) when sandboxing is enabled and
|
|
35
|
+
* available. Returns null in every other case — Windows, missing package,
|
|
36
|
+
* disabled config, init failure — so the caller falls through to the
|
|
37
|
+
* unsandboxed code path unchanged.
|
|
38
|
+
*/
|
|
39
|
+
export declare function wrapForSandbox(command: string, config: SandboxConfig): Promise<string | null>;
|
|
40
|
+
/**
|
|
41
|
+
* Test-only: reset the cached init promise so unit tests can re-init with
|
|
42
|
+
* different configs.
|
|
43
|
+
*
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
export declare function _resetSandboxForTest(): void;
|
|
47
|
+
//# sourceMappingURL=sandbox-runtime.d.ts.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OS-level sandbox integration via the optional `@anthropic-ai/sandbox-runtime`
|
|
3
|
+
* package. The package wraps a shell command in bubblewrap (Linux) or
|
|
4
|
+
* sandbox-exec (macOS) plus a network proxy that filters by domain allowlist.
|
|
5
|
+
*
|
|
6
|
+
* Boundaries:
|
|
7
|
+
* - **Linux + macOS**: real sandboxing via the package's static API.
|
|
8
|
+
* - **Windows**: not supported by the package — every wrap call returns null
|
|
9
|
+
* (graceful passthrough; tools spawn unsandboxed). Documented in SECURITY.md.
|
|
10
|
+
* - **Package not installed**: same passthrough behavior — installs cleanly
|
|
11
|
+
* without the optional dep on any platform.
|
|
12
|
+
*
|
|
13
|
+
* Lifecycle:
|
|
14
|
+
* - Initialized once per process on the first wrap request.
|
|
15
|
+
* - One `SandboxManager.initialize` covers all subsequent wrap calls.
|
|
16
|
+
* - No reset — the package documents auto-cleanup on process exit.
|
|
17
|
+
*
|
|
18
|
+
* Opt-in: callers pass `{ enabled: true }` (typically derived from
|
|
19
|
+
* `OhConfig.sandbox.enabled` or the `--sandbox` CLI flag). The default is
|
|
20
|
+
* off so existing users see no behavior change.
|
|
21
|
+
*/
|
|
22
|
+
// Cached, lazy-initialized handle. We deliberately don't expose this — callers
|
|
23
|
+
// only see `wrapForSandbox` / `isSandboxAvailable` / `resetSandboxForTest`.
|
|
24
|
+
let _initPromise = null;
|
|
25
|
+
/**
|
|
26
|
+
* Returns true on Linux/macOS where sandboxing is supported. Windows is
|
|
27
|
+
* unsupported by the underlying package, so we short-circuit there to avoid
|
|
28
|
+
* a misleading "tried to load and failed" log.
|
|
29
|
+
*/
|
|
30
|
+
export function isSandboxAvailable() {
|
|
31
|
+
return process.platform === "linux" || process.platform === "darwin";
|
|
32
|
+
}
|
|
33
|
+
async function loadAndInitialize(config) {
|
|
34
|
+
if (!isSandboxAvailable())
|
|
35
|
+
return null;
|
|
36
|
+
let mod;
|
|
37
|
+
try {
|
|
38
|
+
mod = (await import("@anthropic-ai/sandbox-runtime"));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Optional dep not installed — graceful passthrough.
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
await mod.SandboxManager.initialize({
|
|
46
|
+
network: {
|
|
47
|
+
allowedDomains: config.network?.allowedDomains ?? [],
|
|
48
|
+
deniedDomains: config.network?.deniedDomains ?? [],
|
|
49
|
+
},
|
|
50
|
+
filesystem: {
|
|
51
|
+
allowWrite: config.filesystem?.allowWrite ?? [process.cwd()],
|
|
52
|
+
denyWrite: config.filesystem?.denyWrite ?? [],
|
|
53
|
+
denyRead: config.filesystem?.denyRead ?? [],
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Init can fail when bubblewrap / sandbox-exec aren't installed, or when
|
|
59
|
+
// the user's profile rejects the proxy ports. Falling back to passthrough
|
|
60
|
+
// is correct — opting in promised "use sandbox if you can," not "fail
|
|
61
|
+
// closed" — that's a separate `requireSandbox` mode for a future revision.
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return mod;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Wrap a shell command for sandboxed execution.
|
|
68
|
+
*
|
|
69
|
+
* Returns the wrapped command (a single shell string suitable for
|
|
70
|
+
* `spawn(cmd, { shell: "/bin/bash" })`) when sandboxing is enabled and
|
|
71
|
+
* available. Returns null in every other case — Windows, missing package,
|
|
72
|
+
* disabled config, init failure — so the caller falls through to the
|
|
73
|
+
* unsandboxed code path unchanged.
|
|
74
|
+
*/
|
|
75
|
+
export async function wrapForSandbox(command, config) {
|
|
76
|
+
if (!config.enabled)
|
|
77
|
+
return null;
|
|
78
|
+
if (!_initPromise) {
|
|
79
|
+
_initPromise = loadAndInitialize(config);
|
|
80
|
+
}
|
|
81
|
+
const mod = await _initPromise;
|
|
82
|
+
if (!mod)
|
|
83
|
+
return null;
|
|
84
|
+
try {
|
|
85
|
+
return await mod.SandboxManager.wrapWithSandbox(command);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Test-only: reset the cached init promise so unit tests can re-init with
|
|
93
|
+
* different configs.
|
|
94
|
+
*
|
|
95
|
+
* @internal
|
|
96
|
+
*/
|
|
97
|
+
export function _resetSandboxForTest() {
|
|
98
|
+
_initPromise = null;
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=sandbox-runtime.js.map
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { readOhConfig } from "../../harness/config.js";
|
|
4
|
+
import { wrapForSandbox } from "../../harness/sandbox-runtime.js";
|
|
3
5
|
import { safeEnv } from "../../utils/safe-env.js";
|
|
4
6
|
const inputSchema = z.object({
|
|
5
7
|
command: z.string(),
|
|
@@ -21,12 +23,30 @@ export const BashTool = {
|
|
|
21
23
|
isConcurrencySafe() {
|
|
22
24
|
return false;
|
|
23
25
|
},
|
|
24
|
-
call(input, context) {
|
|
26
|
+
async call(input, context) {
|
|
25
27
|
// input.timeout is in seconds; convert to ms. Default 120s.
|
|
26
28
|
const timeoutMs = Math.min((input.timeout ?? 120) * 1000, MAX_TIMEOUT);
|
|
27
29
|
const isWin = process.platform === "win32";
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
// Optional OS-level sandbox via @anthropic-ai/sandbox-runtime. Returns null
|
|
31
|
+
// when disabled / on Windows / when the optional dep isn't installed —
|
|
32
|
+
// caller falls back to the existing unsandboxed spawn unchanged.
|
|
33
|
+
const sandboxCfg = readOhConfig()?.sandbox;
|
|
34
|
+
const wrappedCommand = sandboxCfg ? await wrapForSandbox(input.command, sandboxCfg) : null;
|
|
35
|
+
let shell;
|
|
36
|
+
let shellArgs;
|
|
37
|
+
let extraSpawnOpts = {};
|
|
38
|
+
if (wrappedCommand) {
|
|
39
|
+
// sandbox-runtime returns a shell-string. Pin the shell to /bin/bash so
|
|
40
|
+
// the surrounding command syntax (heredocs, $((...)) etc.) keeps working
|
|
41
|
+
// — `shell: true` would default to /bin/sh on Linux.
|
|
42
|
+
shell = wrappedCommand;
|
|
43
|
+
shellArgs = [];
|
|
44
|
+
extraSpawnOpts = { shell: "/bin/bash" };
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
shell = isWin ? "cmd.exe" : "/bin/bash";
|
|
48
|
+
shellArgs = isWin ? ["/c", input.command] : ["-c", input.command];
|
|
49
|
+
}
|
|
30
50
|
// Background execution: spawn and return immediately
|
|
31
51
|
if (input.run_in_background) {
|
|
32
52
|
const bgId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
@@ -35,9 +55,13 @@ export const BashTool = {
|
|
|
35
55
|
env: safeEnv(),
|
|
36
56
|
stdio: ["ignore", "pipe", "pipe"],
|
|
37
57
|
detached: false,
|
|
58
|
+
...extraSpawnOpts,
|
|
38
59
|
});
|
|
39
60
|
let stdout = "";
|
|
40
61
|
let stderr = "";
|
|
62
|
+
// stdio is fixed to ["ignore", "pipe", "pipe"] above, so stdout/stderr
|
|
63
|
+
// are always streams. Adding `...extraSpawnOpts` widens the spawn
|
|
64
|
+
// overload's return type to potentially-null pipes; assert non-null.
|
|
41
65
|
proc.stdout.on("data", (chunk) => {
|
|
42
66
|
stdout += chunk.toString();
|
|
43
67
|
});
|
|
@@ -76,11 +100,15 @@ export const BashTool = {
|
|
|
76
100
|
cwd: context.workingDir,
|
|
77
101
|
env: safeEnv(),
|
|
78
102
|
stdio: ["ignore", "pipe", "pipe"],
|
|
103
|
+
...extraSpawnOpts,
|
|
79
104
|
});
|
|
80
105
|
const timer = setTimeout(() => {
|
|
81
106
|
killed = true;
|
|
82
107
|
proc.kill("SIGTERM");
|
|
83
108
|
}, timeoutMs);
|
|
109
|
+
// stdio: ["ignore", "pipe", "pipe"] is set above — pipes are always
|
|
110
|
+
// present here; the spread of extraSpawnOpts just widens the return
|
|
111
|
+
// type. Non-null asserts are safe.
|
|
84
112
|
proc.stdout.on("data", (chunk) => {
|
|
85
113
|
const text = chunk.toString();
|
|
86
114
|
stdout += text;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhijiewang/openharness",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.33.0",
|
|
4
4
|
"description": "Open-source terminal coding agent. Works with any LLM.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
},
|
|
92
92
|
"homepage": "https://github.com/zhijiewong/openharness#readme",
|
|
93
93
|
"optionalDependencies": {
|
|
94
|
+
"@anthropic-ai/sandbox-runtime": "^0.0.49",
|
|
94
95
|
"@napi-rs/keyring": "^1.2.0",
|
|
95
96
|
"sharp": "^0.34.5"
|
|
96
97
|
}
|