@zhijiewang/openharness 2.8.0 → 2.10.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/data/registry.json +262 -0
- package/data/skills/code-review.md +19 -0
- package/data/skills/commit.md +17 -0
- package/data/skills/debug.md +24 -0
- package/data/skills/diagnose.md +24 -0
- package/data/skills/plan.md +25 -0
- package/data/skills/simplify.md +24 -0
- package/data/skills/tdd.md +22 -0
- package/dist/agents/roles.d.ts +12 -2
- package/dist/agents/roles.js +65 -6
- package/dist/commands/ai.js +27 -7
- package/dist/commands/skills.d.ts +1 -1
- package/dist/commands/skills.js +51 -6
- package/dist/components/App.js +7 -1
- package/dist/harness/config.d.ts +24 -0
- package/dist/harness/hooks.d.ts +14 -0
- package/dist/harness/hooks.js +205 -11
- package/dist/harness/marketplace.d.ts +77 -2
- package/dist/harness/marketplace.js +260 -38
- package/dist/harness/memory.d.ts +34 -0
- package/dist/harness/memory.js +96 -0
- package/dist/harness/plugins.d.ts +13 -3
- package/dist/harness/plugins.js +98 -17
- package/dist/harness/session-db.d.ts +8 -1
- package/dist/harness/session-db.js +24 -3
- package/dist/harness/skill-registry.d.ts +26 -2
- package/dist/harness/skill-registry.js +42 -4
- package/dist/tools/AgentTool/index.d.ts +2 -2
- package/dist/tools/DiagnosticsTool/index.d.ts +1 -1
- package/dist/tools/GrepTool/index.d.ts +6 -6
- package/dist/tools/MemoryTool/index.d.ts +4 -4
- package/dist/tools/MonitorTool/index.js +5 -1
- package/dist/types/permissions.js +104 -42
- package/dist/utils/bash-safety.d.ts +19 -0
- package/dist/utils/bash-safety.js +179 -1
- package/dist/utils/safe-env.d.ts +5 -1
- package/dist/utils/safe-env.js +19 -1
- package/package.json +3 -1
|
@@ -9,10 +9,29 @@ export type BashRisk = {
|
|
|
9
9
|
level: "safe" | "moderate" | "dangerous";
|
|
10
10
|
reasons: string[];
|
|
11
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* Strip leading process-wrapper tokens from a command string. Returns the
|
|
14
|
+
* underlying command (tokens + original separator). When the command is
|
|
15
|
+
* `timeout 30 npm test`, returns `npm test`. When no wrapper is present,
|
|
16
|
+
* returns the input unchanged. Conservative: only strips wrappers with at
|
|
17
|
+
* least one remaining token after them.
|
|
18
|
+
*/
|
|
19
|
+
export declare function stripProcessWrappers(cmd: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Return true iff every sub-command in the pipeline/chain is a read-only
|
|
22
|
+
* operation. Any side-effecting sub-command disqualifies the whole command.
|
|
23
|
+
* Respects quotes and command substitution via the existing splitCommands/tokenize.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isReadOnlyBashCommand(command: string): boolean;
|
|
12
26
|
/**
|
|
13
27
|
* Analyze a bash command string for safety risks.
|
|
14
28
|
* Does lightweight structural parsing — splits on pipes, semicolons,
|
|
15
29
|
* and && / || operators to analyze each sub-command.
|
|
16
30
|
*/
|
|
17
31
|
export declare function analyzeBashCommand(command: string): BashRisk;
|
|
32
|
+
/**
|
|
33
|
+
* Split a command string into sub-commands on |, ;, &&, ||
|
|
34
|
+
* Respects quoted strings and command substitutions.
|
|
35
|
+
*/
|
|
36
|
+
export declare function splitCommands(cmd: string): string[];
|
|
18
37
|
//# sourceMappingURL=bash-safety.d.ts.map
|
|
@@ -39,6 +39,184 @@ const INSTALL_COMMANDS = new Set([
|
|
|
39
39
|
]);
|
|
40
40
|
// Commands that send data externally
|
|
41
41
|
const NETWORK_EXFIL = new Set(["curl", "wget", "nc", "ncat", "socat", "ssh", "scp", "rsync"]);
|
|
42
|
+
/**
|
|
43
|
+
* Process-wrapper commands that don't change what runs, just how it runs.
|
|
44
|
+
* These are stripped off the front of a sub-command before permission matching
|
|
45
|
+
* so `timeout 10 rm file` matches the same rule as `rm file`. Mirrors Claude
|
|
46
|
+
* Code's wrapper-stripping for robust permission enforcement.
|
|
47
|
+
*/
|
|
48
|
+
const PROCESS_WRAPPERS = new Set(["timeout", "time", "nice", "nohup", "stdbuf", "ionice", "unbuffer", "env"]);
|
|
49
|
+
/**
|
|
50
|
+
* Strip leading process-wrapper tokens from a command string. Returns the
|
|
51
|
+
* underlying command (tokens + original separator). When the command is
|
|
52
|
+
* `timeout 30 npm test`, returns `npm test`. When no wrapper is present,
|
|
53
|
+
* returns the input unchanged. Conservative: only strips wrappers with at
|
|
54
|
+
* least one remaining token after them.
|
|
55
|
+
*/
|
|
56
|
+
export function stripProcessWrappers(cmd) {
|
|
57
|
+
const trimmed = cmd.trim();
|
|
58
|
+
let tokens = tokenize(trimmed);
|
|
59
|
+
while (tokens.length >= 2 && PROCESS_WRAPPERS.has(tokens[0])) {
|
|
60
|
+
const first = tokens[0];
|
|
61
|
+
let skip = 1;
|
|
62
|
+
// `timeout 30s`, `nice -n 10`, `stdbuf -oL` — skip option-like args
|
|
63
|
+
// belonging to the wrapper itself. Numeric or `-flag value` shapes are swallowed.
|
|
64
|
+
while (skip < tokens.length - 1) {
|
|
65
|
+
const t = tokens[skip];
|
|
66
|
+
if (t.startsWith("-") || /^[0-9.]+[a-zA-Z]?$/.test(t)) {
|
|
67
|
+
skip++;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
tokens = tokens.slice(skip);
|
|
74
|
+
// Safety: if stripping removes everything, fall back to original
|
|
75
|
+
if (tokens.length === 0)
|
|
76
|
+
return trimmed;
|
|
77
|
+
// Detect and continue stripping nested wrappers (e.g., `timeout 5 nice rm`)
|
|
78
|
+
if (!PROCESS_WRAPPERS.has(tokens[0]))
|
|
79
|
+
break;
|
|
80
|
+
// Avoid infinite loops on malformed input
|
|
81
|
+
if (tokens[0] === first)
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
return tokens.join(" ");
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Pure read-only commands. A bash invocation consisting only of these
|
|
88
|
+
* commands (optionally piped/chained with each other) is safe to auto-approve
|
|
89
|
+
* without a permission prompt. Mirrors Claude Code's read-only allowlist so
|
|
90
|
+
* common inspection flows (`ls`, `cat file | head`, `git status`) don't pester
|
|
91
|
+
* the user.
|
|
92
|
+
*
|
|
93
|
+
* Strict criteria: no side effects, no network, no filesystem writes.
|
|
94
|
+
*/
|
|
95
|
+
const READ_ONLY_COMMANDS = new Set([
|
|
96
|
+
"ls",
|
|
97
|
+
"cat",
|
|
98
|
+
"head",
|
|
99
|
+
"tail",
|
|
100
|
+
"grep",
|
|
101
|
+
"egrep",
|
|
102
|
+
"fgrep",
|
|
103
|
+
"find",
|
|
104
|
+
"wc",
|
|
105
|
+
"diff",
|
|
106
|
+
"stat",
|
|
107
|
+
"du",
|
|
108
|
+
"df",
|
|
109
|
+
"pwd",
|
|
110
|
+
"echo",
|
|
111
|
+
"printf",
|
|
112
|
+
"whoami",
|
|
113
|
+
"which",
|
|
114
|
+
"type",
|
|
115
|
+
"file",
|
|
116
|
+
"basename",
|
|
117
|
+
"dirname",
|
|
118
|
+
"realpath",
|
|
119
|
+
"readlink",
|
|
120
|
+
"date",
|
|
121
|
+
"true",
|
|
122
|
+
"false",
|
|
123
|
+
"sort",
|
|
124
|
+
"uniq",
|
|
125
|
+
"cut",
|
|
126
|
+
"tr",
|
|
127
|
+
"sed", // sed without -i is read-only (checked below)
|
|
128
|
+
"awk",
|
|
129
|
+
"column",
|
|
130
|
+
"tee", // tee IS a write, handled below
|
|
131
|
+
"tree",
|
|
132
|
+
"jq",
|
|
133
|
+
"yq",
|
|
134
|
+
"xxd",
|
|
135
|
+
"od",
|
|
136
|
+
"md5sum",
|
|
137
|
+
"sha1sum",
|
|
138
|
+
"sha256sum",
|
|
139
|
+
"env", // reading env; `export` / `env X=Y cmd` is not here
|
|
140
|
+
]);
|
|
141
|
+
// Git subcommands that don't mutate the repo or working tree.
|
|
142
|
+
const READ_ONLY_GIT_SUBCOMMANDS = new Set([
|
|
143
|
+
"status",
|
|
144
|
+
"log",
|
|
145
|
+
"show",
|
|
146
|
+
"diff",
|
|
147
|
+
"blame",
|
|
148
|
+
"branch",
|
|
149
|
+
"tag",
|
|
150
|
+
"describe",
|
|
151
|
+
"rev-parse",
|
|
152
|
+
"rev-list",
|
|
153
|
+
"ls-files",
|
|
154
|
+
"ls-tree",
|
|
155
|
+
"cat-file",
|
|
156
|
+
"config",
|
|
157
|
+
"remote",
|
|
158
|
+
"reflog",
|
|
159
|
+
"stash",
|
|
160
|
+
"for-each-ref",
|
|
161
|
+
"shortlog",
|
|
162
|
+
"grep",
|
|
163
|
+
"bisect",
|
|
164
|
+
"worktree",
|
|
165
|
+
]);
|
|
166
|
+
/**
|
|
167
|
+
* Return true iff every sub-command in the pipeline/chain is a read-only
|
|
168
|
+
* operation. Any side-effecting sub-command disqualifies the whole command.
|
|
169
|
+
* Respects quotes and command substitution via the existing splitCommands/tokenize.
|
|
170
|
+
*/
|
|
171
|
+
export function isReadOnlyBashCommand(command) {
|
|
172
|
+
const trimmed = command.trim();
|
|
173
|
+
if (!trimmed)
|
|
174
|
+
return false;
|
|
175
|
+
// Refuse any redirection that creates/overwrites files ( > >> | tee -a ... ).
|
|
176
|
+
// Append is still a write. `2>&1` alone is fine, but `> file` is not.
|
|
177
|
+
if (/(?<![<&])>+\s*[^&]/.test(trimmed))
|
|
178
|
+
return false;
|
|
179
|
+
const subCommands = splitCommands(trimmed);
|
|
180
|
+
if (subCommands.length === 0)
|
|
181
|
+
return false;
|
|
182
|
+
for (const sub of subCommands) {
|
|
183
|
+
const tokens = tokenize(sub);
|
|
184
|
+
if (tokens.length === 0)
|
|
185
|
+
continue;
|
|
186
|
+
const cmd = tokens[0];
|
|
187
|
+
const args = tokens.slice(1);
|
|
188
|
+
// `git <subcmd>` with read-only subcommand
|
|
189
|
+
if (cmd === "git") {
|
|
190
|
+
const sub = args.find((a) => !a.startsWith("-"));
|
|
191
|
+
if (!sub || !READ_ONLY_GIT_SUBCOMMANDS.has(sub))
|
|
192
|
+
return false;
|
|
193
|
+
// `git stash push`, `git stash pop`, `git stash drop` are writes — refuse.
|
|
194
|
+
if (sub === "stash") {
|
|
195
|
+
const action = args[args.indexOf("stash") + 1];
|
|
196
|
+
if (action && ["push", "pop", "drop", "apply", "clear", "save"].includes(action))
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
// `git branch -d`, `git branch -D` delete branches — refuse.
|
|
200
|
+
if (sub === "branch" && args.some((a) => a === "-d" || a === "-D"))
|
|
201
|
+
return false;
|
|
202
|
+
// `git config --global foo=bar` writes config — only read forms are safe.
|
|
203
|
+
if (sub === "config" &&
|
|
204
|
+
args.some((a) => !a.startsWith("-") && args.indexOf(a) > args.indexOf("config") && a.includes("="))) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
// `sed -i` is in-place edit — writes.
|
|
210
|
+
if (cmd === "sed" && args.some((a) => a === "-i" || a.startsWith("-i")))
|
|
211
|
+
return false;
|
|
212
|
+
// `tee` without `-a` is a write; `tee -a` is also a write. Refuse always.
|
|
213
|
+
if (cmd === "tee")
|
|
214
|
+
return false;
|
|
215
|
+
if (!READ_ONLY_COMMANDS.has(cmd))
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
42
220
|
/**
|
|
43
221
|
* Analyze a bash command string for safety risks.
|
|
44
222
|
* Does lightweight structural parsing — splits on pipes, semicolons,
|
|
@@ -149,7 +327,7 @@ export function analyzeBashCommand(command) {
|
|
|
149
327
|
* Split a command string into sub-commands on |, ;, &&, ||
|
|
150
328
|
* Respects quoted strings and command substitutions.
|
|
151
329
|
*/
|
|
152
|
-
function splitCommands(cmd) {
|
|
330
|
+
export function splitCommands(cmd) {
|
|
153
331
|
const parts = [];
|
|
154
332
|
let current = "";
|
|
155
333
|
let inSingle = false;
|
package/dist/utils/safe-env.d.ts
CHANGED
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
6
|
* Filter process.env to remove credential-containing variables.
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
|
+
* Precedence order (later wins):
|
|
9
|
+
* 1. process.env (filtered)
|
|
10
|
+
* 2. .oh/config.yaml `env:` block (Claude Code parity — inject API keys etc.)
|
|
11
|
+
* 3. `extra` argument (call-site overrides — e.g. per-MCP-server env)
|
|
8
12
|
*/
|
|
9
13
|
export declare function safeEnv(extra?: Record<string, string>): Record<string, string>;
|
|
10
14
|
//# sourceMappingURL=safe-env.d.ts.map
|
package/dist/utils/safe-env.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Safe environment variable filtering.
|
|
3
3
|
* Blocks credential-containing vars from being passed to subprocesses.
|
|
4
4
|
*/
|
|
5
|
+
import { readOhConfig } from "../harness/config.js";
|
|
5
6
|
/** Env var names that should never be passed to subprocesses */
|
|
6
7
|
const BLOCKED_PATTERNS = [
|
|
7
8
|
/^ANTHROPIC_API_KEY$/i,
|
|
@@ -21,7 +22,11 @@ const BLOCKED_PATTERNS = [
|
|
|
21
22
|
];
|
|
22
23
|
/**
|
|
23
24
|
* Filter process.env to remove credential-containing variables.
|
|
24
|
-
*
|
|
25
|
+
*
|
|
26
|
+
* Precedence order (later wins):
|
|
27
|
+
* 1. process.env (filtered)
|
|
28
|
+
* 2. .oh/config.yaml `env:` block (Claude Code parity — inject API keys etc.)
|
|
29
|
+
* 3. `extra` argument (call-site overrides — e.g. per-MCP-server env)
|
|
25
30
|
*/
|
|
26
31
|
export function safeEnv(extra) {
|
|
27
32
|
const env = {};
|
|
@@ -32,6 +37,19 @@ export function safeEnv(extra) {
|
|
|
32
37
|
continue;
|
|
33
38
|
env[key] = value;
|
|
34
39
|
}
|
|
40
|
+
// Layer in config-declared env vars if available.
|
|
41
|
+
try {
|
|
42
|
+
const cfg = readOhConfig();
|
|
43
|
+
if (cfg?.env) {
|
|
44
|
+
for (const [k, v] of Object.entries(cfg.env)) {
|
|
45
|
+
if (typeof v === "string")
|
|
46
|
+
env[k] = v;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* config unavailable — fall through */
|
|
52
|
+
}
|
|
35
53
|
if (extra) {
|
|
36
54
|
Object.assign(env, extra);
|
|
37
55
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhijiewang/openharness",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"description": "Open-source terminal coding agent. Works with any LLM.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
"dist/**/*.d.ts",
|
|
18
18
|
"!dist/**/*.test.*",
|
|
19
19
|
"!dist/**/test-helpers.*",
|
|
20
|
+
"data/skills/**/*.md",
|
|
21
|
+
"data/registry.json",
|
|
20
22
|
"README.md",
|
|
21
23
|
"LICENSE"
|
|
22
24
|
],
|