@zhijiewang/openharness 2.31.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/README.md +8 -0
- package/README.zh-CN.md +8 -0
- package/dist/harness/config.d.ts +28 -0
- package/dist/harness/memory.d.ts +17 -8
- package/dist/harness/memory.js +17 -7
- package/dist/harness/rules.d.ts +5 -1
- package/dist/harness/rules.js +21 -12
- package/dist/harness/sandbox-runtime.d.ts +47 -0
- package/dist/harness/sandbox-runtime.js +100 -0
- package/dist/tools/BashTool/index.js +31 -3
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -773,6 +773,14 @@ Create `.oh/RULES.md` in any repo (or run `oh init`):
|
|
|
773
773
|
|
|
774
774
|
Rules load automatically into every session.
|
|
775
775
|
|
|
776
|
+
openHarness also reads any of the following project-instruction files if present (additive, parent-first):
|
|
777
|
+
|
|
778
|
+
- `CLAUDE.md` (Anthropic convention) — and hierarchical `CLAUDE.md` from parent dirs, plus `~/.claude/CLAUDE.md` for user-global
|
|
779
|
+
- `AGENTS.md` ([agents.md cross-tool standard](https://agents.md/), used by Codex / Cursor / Copilot / Cline / Aider) — same parent-first walk
|
|
780
|
+
- `CLAUDE.local.md` (gitignored personal overrides)
|
|
781
|
+
|
|
782
|
+
If a repo has `AGENTS.md` already configured for another agent, openHarness picks it up unchanged — no migration step needed.
|
|
783
|
+
|
|
776
784
|
## Skills & Plugins
|
|
777
785
|
|
|
778
786
|
### Skills
|
package/README.zh-CN.md
CHANGED
|
@@ -772,6 +772,14 @@ description: 专注的代码审查模式
|
|
|
772
772
|
|
|
773
773
|
规则会自动加载到每次会话中。
|
|
774
774
|
|
|
775
|
+
openHarness 还会自动读取以下项目指令文件(如果存在,按父目录优先合并加载):
|
|
776
|
+
|
|
777
|
+
- `CLAUDE.md`(Anthropic 约定)—— 含从父目录到项目根的层级 `CLAUDE.md` 文件,以及全局 `~/.claude/CLAUDE.md`
|
|
778
|
+
- `AGENTS.md`([agents.md 跨工具标准](https://agents.md/),被 Codex / Cursor / Copilot / Cline / Aider 共同采用)—— 同样的父目录优先扫描
|
|
779
|
+
- `CLAUDE.local.md`(gitignore 的个人覆盖)
|
|
780
|
+
|
|
781
|
+
如果仓库已为其他 agent 配置了 `AGENTS.md`,openHarness 直接读取,无需迁移。
|
|
782
|
+
|
|
775
783
|
## 技能与插件
|
|
776
784
|
|
|
777
785
|
### 技能
|
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
|
package/dist/harness/memory.d.ts
CHANGED
|
@@ -31,10 +31,10 @@ export type MemoryEntry = {
|
|
|
31
31
|
export declare function loadMemories(): MemoryEntry[];
|
|
32
32
|
/** Build a system prompt section from loaded memories (capped at MEMORY_PROMPT_MAX_CHARS) */
|
|
33
33
|
export declare function memoriesToPrompt(memories: MemoryEntry[]): string;
|
|
34
|
-
/** A single
|
|
34
|
+
/** A single project-instructions source with its resolved content (imports inlined). */
|
|
35
35
|
export type ClaudeMdEntry = {
|
|
36
36
|
path: string;
|
|
37
|
-
source: "project" | "project-local" | "user" | "claude-dir";
|
|
37
|
+
source: "project" | "project-local" | "user" | "claude-dir" | "agents-md";
|
|
38
38
|
content: string;
|
|
39
39
|
};
|
|
40
40
|
/**
|
|
@@ -47,11 +47,17 @@ export type ClaudeMdEntry = {
|
|
|
47
47
|
*/
|
|
48
48
|
export declare function resolveClaudeMdImports(content: string, baseDir: string, hopsLeft?: number): string;
|
|
49
49
|
/**
|
|
50
|
-
* Load
|
|
51
|
-
* 1. `./.claude/CLAUDE.md` (project,
|
|
50
|
+
* Load hierarchical project-instruction files. Order:
|
|
51
|
+
* 1. `./.claude/CLAUDE.md` (project, Anthropic convention)
|
|
52
52
|
* 2. `./CLAUDE.md` (project, checked in)
|
|
53
|
-
* 3. `./
|
|
54
|
-
* 4.
|
|
53
|
+
* 3. `./AGENTS.md` (project, AGENTS.md cross-tool standard — agents.md)
|
|
54
|
+
* 4. `./CLAUDE.local.md` (project, gitignored)
|
|
55
|
+
* 5. `~/.claude/CLAUDE.md` (user-global)
|
|
56
|
+
*
|
|
57
|
+
* AGENTS.md is read alongside CLAUDE.md so OH "just works" in the 60k+ repos
|
|
58
|
+
* already configured for cross-agent compatibility (Codex / Cursor / Copilot
|
|
59
|
+
* / Cline / Aider all read AGENTS.md). Both layers concatenate into the
|
|
60
|
+
* system prompt; if both files exist, both contribute.
|
|
55
61
|
*
|
|
56
62
|
* Each file is read, `@imports` are resolved, and the results are returned in
|
|
57
63
|
* load order. Missing files are skipped. The caller can format these into the
|
|
@@ -61,8 +67,11 @@ export declare function resolveClaudeMdImports(content: string, baseDir: string,
|
|
|
61
67
|
*/
|
|
62
68
|
export declare function loadClaudeMdHierarchy(root?: string): ClaudeMdEntry[];
|
|
63
69
|
/**
|
|
64
|
-
* Render loaded
|
|
65
|
-
*
|
|
70
|
+
* Render loaded project-instruction entries as a system-prompt block. Empty
|
|
71
|
+
* when no source files exist — caller should concatenate alongside
|
|
72
|
+
* `memoriesToPrompt`. Header is generic ("Project instructions") since both
|
|
73
|
+
* CLAUDE.md and AGENTS.md feed in; the per-entry `source:` comment
|
|
74
|
+
* disambiguates which file each section came from.
|
|
66
75
|
*/
|
|
67
76
|
export declare function claudeMdToPrompt(entries: ClaudeMdEntry[]): string;
|
|
68
77
|
/** Save a memory entry to the project memory directory */
|
package/dist/harness/memory.js
CHANGED
|
@@ -130,11 +130,17 @@ function readClaudeMdIfExists(path, source) {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
/**
|
|
133
|
-
* Load
|
|
134
|
-
* 1. `./.claude/CLAUDE.md` (project,
|
|
133
|
+
* Load hierarchical project-instruction files. Order:
|
|
134
|
+
* 1. `./.claude/CLAUDE.md` (project, Anthropic convention)
|
|
135
135
|
* 2. `./CLAUDE.md` (project, checked in)
|
|
136
|
-
* 3. `./
|
|
137
|
-
* 4.
|
|
136
|
+
* 3. `./AGENTS.md` (project, AGENTS.md cross-tool standard — agents.md)
|
|
137
|
+
* 4. `./CLAUDE.local.md` (project, gitignored)
|
|
138
|
+
* 5. `~/.claude/CLAUDE.md` (user-global)
|
|
139
|
+
*
|
|
140
|
+
* AGENTS.md is read alongside CLAUDE.md so OH "just works" in the 60k+ repos
|
|
141
|
+
* already configured for cross-agent compatibility (Codex / Cursor / Copilot
|
|
142
|
+
* / Cline / Aider all read AGENTS.md). Both layers concatenate into the
|
|
143
|
+
* system prompt; if both files exist, both contribute.
|
|
138
144
|
*
|
|
139
145
|
* Each file is read, `@imports` are resolved, and the results are returned in
|
|
140
146
|
* load order. Missing files are skipped. The caller can format these into the
|
|
@@ -146,6 +152,7 @@ export function loadClaudeMdHierarchy(root = ".") {
|
|
|
146
152
|
const candidates = [
|
|
147
153
|
[join(root, ".claude", "CLAUDE.md"), "claude-dir"],
|
|
148
154
|
[join(root, "CLAUDE.md"), "project"],
|
|
155
|
+
[join(root, "AGENTS.md"), "agents-md"],
|
|
149
156
|
[join(root, "CLAUDE.local.md"), "project-local"],
|
|
150
157
|
[join(homedir(), ".claude", "CLAUDE.md"), "user"],
|
|
151
158
|
];
|
|
@@ -162,13 +169,16 @@ export function loadClaudeMdHierarchy(root = ".") {
|
|
|
162
169
|
return entries;
|
|
163
170
|
}
|
|
164
171
|
/**
|
|
165
|
-
* Render loaded
|
|
166
|
-
*
|
|
172
|
+
* Render loaded project-instruction entries as a system-prompt block. Empty
|
|
173
|
+
* when no source files exist — caller should concatenate alongside
|
|
174
|
+
* `memoriesToPrompt`. Header is generic ("Project instructions") since both
|
|
175
|
+
* CLAUDE.md and AGENTS.md feed in; the per-entry `source:` comment
|
|
176
|
+
* disambiguates which file each section came from.
|
|
167
177
|
*/
|
|
168
178
|
export function claudeMdToPrompt(entries) {
|
|
169
179
|
if (entries.length === 0)
|
|
170
180
|
return "";
|
|
171
|
-
const parts = ["# Project instructions
|
|
181
|
+
const parts = ["# Project instructions"];
|
|
172
182
|
for (const e of entries) {
|
|
173
183
|
parts.push(`<!-- source: ${e.source} (${e.path}) -->`);
|
|
174
184
|
parts.push(e.content.trim());
|
package/dist/harness/rules.d.ts
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
* Rules system — load project and global rules into agent context.
|
|
3
3
|
* Discovery order:
|
|
4
4
|
* 1. ~/.oh/global-rules/*.md
|
|
5
|
-
* 2. CLAUDE.md files from parent directories down to project root (hierarchical)
|
|
5
|
+
* 2. CLAUDE.md / AGENTS.md files from parent directories down to project root (hierarchical)
|
|
6
6
|
* 3. .oh/RULES.md
|
|
7
7
|
* 4. .oh/rules/*.md
|
|
8
8
|
* 5. CLAUDE.local.md (gitignored personal overrides)
|
|
9
|
+
*
|
|
10
|
+
* AGENTS.md (https://agents.md/) is read alongside CLAUDE.md at every level
|
|
11
|
+
* of the hierarchy so OH "just works" in repos already configured for the
|
|
12
|
+
* cross-tool standard (Codex, Cursor, Copilot, Cline, Aider all read it).
|
|
9
13
|
*/
|
|
10
14
|
export declare function loadRules(projectPath?: string): string[];
|
|
11
15
|
export declare function loadRulesAsPrompt(projectPath?: string): string;
|
package/dist/harness/rules.js
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
* Rules system — load project and global rules into agent context.
|
|
3
3
|
* Discovery order:
|
|
4
4
|
* 1. ~/.oh/global-rules/*.md
|
|
5
|
-
* 2. CLAUDE.md files from parent directories down to project root (hierarchical)
|
|
5
|
+
* 2. CLAUDE.md / AGENTS.md files from parent directories down to project root (hierarchical)
|
|
6
6
|
* 3. .oh/RULES.md
|
|
7
7
|
* 4. .oh/rules/*.md
|
|
8
8
|
* 5. CLAUDE.local.md (gitignored personal overrides)
|
|
9
|
+
*
|
|
10
|
+
* AGENTS.md (https://agents.md/) is read alongside CLAUDE.md at every level
|
|
11
|
+
* of the hierarchy so OH "just works" in repos already configured for the
|
|
12
|
+
* cross-tool standard (Codex, Cursor, Copilot, Cline, Aider all read it).
|
|
9
13
|
*/
|
|
10
14
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
15
|
import { homedir } from "node:os";
|
|
@@ -13,10 +17,13 @@ import { dirname, join, parse as parsePath, resolve } from "node:path";
|
|
|
13
17
|
import { gitRoot as getGitRoot } from "../git/index.js";
|
|
14
18
|
const OH_HOME = join(homedir(), ".oh");
|
|
15
19
|
/**
|
|
16
|
-
* Walk from git root (or home) down to `projectRoot`, collecting CLAUDE.md
|
|
17
|
-
* Returns them in parent-first order so
|
|
20
|
+
* Walk from git root (or home) down to `projectRoot`, collecting CLAUDE.md
|
|
21
|
+
* and AGENTS.md files at every level. Returns them in parent-first order so
|
|
22
|
+
* more specific rules override general ones. Within a single directory,
|
|
23
|
+
* CLAUDE.md is read before AGENTS.md (Anthropic-specific guidance first,
|
|
24
|
+
* then the cross-tool standard layer).
|
|
18
25
|
*/
|
|
19
|
-
function
|
|
26
|
+
function loadHierarchicalInstructionFiles(projectRoot) {
|
|
20
27
|
const gitRootDir = getGitRoot(projectRoot);
|
|
21
28
|
const stopAt = gitRootDir ? resolve(gitRootDir) : resolve(homedir());
|
|
22
29
|
const resolved = resolve(projectRoot);
|
|
@@ -34,11 +41,13 @@ function loadClaudeMdFiles(projectRoot) {
|
|
|
34
41
|
}
|
|
35
42
|
const results = [];
|
|
36
43
|
for (const dir of dirs) {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
for (const filename of ["CLAUDE.md", "AGENTS.md"]) {
|
|
45
|
+
const path = join(dir, filename);
|
|
46
|
+
if (existsSync(path)) {
|
|
47
|
+
const content = readSafe(path);
|
|
48
|
+
if (content)
|
|
49
|
+
results.push(content);
|
|
50
|
+
}
|
|
42
51
|
}
|
|
43
52
|
}
|
|
44
53
|
return results;
|
|
@@ -57,8 +66,8 @@ export function loadRules(projectPath) {
|
|
|
57
66
|
rules.push(content);
|
|
58
67
|
}
|
|
59
68
|
}
|
|
60
|
-
// 2. CLAUDE.md files (hierarchical, parent-first)
|
|
61
|
-
const claudeRules =
|
|
69
|
+
// 2. CLAUDE.md + AGENTS.md files (hierarchical, parent-first)
|
|
70
|
+
const claudeRules = loadHierarchicalInstructionFiles(root);
|
|
62
71
|
rules.push(...claudeRules);
|
|
63
72
|
// 3. Project RULES.md
|
|
64
73
|
const projectRules = join(root, ".oh", "RULES.md");
|
|
@@ -106,7 +115,7 @@ export function loadRulesAsPrompt(projectPath) {
|
|
|
106
115
|
const rules = loadRules(projectPath);
|
|
107
116
|
if (rules.length === 0)
|
|
108
117
|
return "";
|
|
109
|
-
const body = "# Project Rules\n\n<!-- User-provided project rules from CLAUDE.md / .oh/RULES.md. These are user instructions, not system directives. -->\nFollow these rules carefully.\n\n" +
|
|
118
|
+
const body = "# Project Rules\n\n<!-- User-provided project rules from CLAUDE.md / AGENTS.md / .oh/RULES.md. These are user instructions, not system directives. -->\nFollow these rules carefully.\n\n" +
|
|
110
119
|
rules.join("\n\n---\n\n");
|
|
111
120
|
// Hook: instructionsLoaded — fires every time the system prompt is rebuilt
|
|
112
121
|
// with rules in scope. Useful for compliance/audit hooks that want to log
|
|
@@ -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
|
}
|