@zhijiewang/openharness 2.8.0 → 2.9.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 +11 -0
- package/dist/harness/hooks.d.ts +14 -0
- package/dist/harness/hooks.js +47 -4
- 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 +6 -6
- 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Permission types — tool permission context and risk-based gating.
|
|
3
3
|
*/
|
|
4
|
-
import { analyzeBashCommand } from "../utils/bash-safety.js";
|
|
4
|
+
import { analyzeBashCommand, isReadOnlyBashCommand, splitCommands, stripProcessWrappers, } from "../utils/bash-safety.js";
|
|
5
5
|
/** Tools auto-approved in acceptEdits mode */
|
|
6
6
|
const EDIT_SAFE_TOOLS = new Set([
|
|
7
7
|
"FileRead",
|
|
@@ -46,42 +46,94 @@ function matchArgGlob(pattern, value) {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
/** Find the first matching tool permission rule */
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Priority of a rule action for "most restrictive wins" tie-breaking across
|
|
51
|
+
* subcommand matches: deny > ask > allow. Rules that don't apply return -1.
|
|
52
|
+
*/
|
|
53
|
+
function actionPriority(action) {
|
|
54
|
+
return action === "deny" ? 2 : action === "ask" ? 1 : 0;
|
|
55
|
+
}
|
|
56
|
+
function matchesSingleRule(r, toolName, toolInput) {
|
|
57
|
+
const { toolName: specToolName, argPattern } = parseToolSpecifier(r.tool);
|
|
58
|
+
if (!matchToolPattern(specToolName, toolName))
|
|
59
|
+
return false;
|
|
60
|
+
if (argPattern && toolInput) {
|
|
61
|
+
const input = toolInput;
|
|
62
|
+
if (toolName === "Bash" && typeof input.command === "string") {
|
|
63
|
+
return matchArgGlob(argPattern, input.command);
|
|
64
|
+
}
|
|
65
|
+
if (["Edit", "Write", "Read"].includes(toolName) && typeof input.file_path === "string") {
|
|
66
|
+
return matchArgGlob(argPattern, input.file_path);
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if (r.pattern && toolInput && toolName === "Bash") {
|
|
71
|
+
const command = toolInput?.command;
|
|
72
|
+
if (typeof command === "string") {
|
|
73
|
+
try {
|
|
74
|
+
return new RegExp(r.pattern).test(command);
|
|
63
75
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return matchArgGlob(argPattern, input.file_path);
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
67
78
|
}
|
|
68
|
-
return false; // Has pattern but no matching field
|
|
69
79
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Match permission rules against a Bash command.
|
|
86
|
+
*
|
|
87
|
+
* For compound commands (`cmd1 && cmd2`, `a | b`, `x; y`), evaluate rules
|
|
88
|
+
* against each sub-command independently with "most restrictive wins"
|
|
89
|
+
* semantics: any sub-command matching a `deny` rule returns deny; else any
|
|
90
|
+
* `ask` match returns ask; else any `allow` match returns allow; else no
|
|
91
|
+
* match. Process wrappers (`timeout 10 cmd`, `nice -n 5 cmd`) are stripped
|
|
92
|
+
* before matching so the real underlying command is what gets checked.
|
|
93
|
+
*
|
|
94
|
+
* Security note: this closes a common class of bypasses like
|
|
95
|
+
* `git log && rm -rf /` where a naive full-line match would hit the `git log`
|
|
96
|
+
* allow rule and skip the deny on `rm`.
|
|
97
|
+
*/
|
|
98
|
+
function findBashRule(rules, command) {
|
|
99
|
+
const subs = splitCommands(command).map(stripProcessWrappers).filter(Boolean);
|
|
100
|
+
if (subs.length === 0)
|
|
101
|
+
return undefined;
|
|
102
|
+
let best;
|
|
103
|
+
let bestPriority = -1;
|
|
104
|
+
for (const sub of subs) {
|
|
105
|
+
const subInput = { command: sub };
|
|
106
|
+
for (const r of rules) {
|
|
107
|
+
if (!matchesSingleRule(r, "Bash", subInput))
|
|
108
|
+
continue;
|
|
109
|
+
const p = actionPriority(r.action);
|
|
110
|
+
if (p > bestPriority) {
|
|
111
|
+
best = r;
|
|
112
|
+
bestPriority = p;
|
|
113
|
+
if (p === 2)
|
|
114
|
+
return best; // deny short-circuits
|
|
80
115
|
}
|
|
81
|
-
return false;
|
|
82
116
|
}
|
|
83
|
-
|
|
84
|
-
|
|
117
|
+
}
|
|
118
|
+
return best;
|
|
119
|
+
}
|
|
120
|
+
function findToolRule(rules, toolName, toolInput) {
|
|
121
|
+
if (!rules || rules.length === 0)
|
|
122
|
+
return undefined;
|
|
123
|
+
// Bash: always route through the compound-aware matcher. For single commands
|
|
124
|
+
// this just strips wrappers and does a normal rule search; for compound
|
|
125
|
+
// commands it evaluates each sub-command with most-restrictive-wins.
|
|
126
|
+
if (toolName === "Bash" && toolInput) {
|
|
127
|
+
const command = toolInput?.command;
|
|
128
|
+
if (typeof command === "string") {
|
|
129
|
+
const compoundMatch = findBashRule(rules, command);
|
|
130
|
+
if (compoundMatch)
|
|
131
|
+
return compoundMatch;
|
|
132
|
+
// Compound matcher returned nothing (single command or no match).
|
|
133
|
+
// Fall through to the default single-rule find below.
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return rules.find((r) => matchesSingleRule(r, toolName, toolInput));
|
|
85
137
|
}
|
|
86
138
|
/** Cached tool permission rules — set by the REPL at startup */
|
|
87
139
|
let toolPermissionRules;
|
|
@@ -103,23 +155,33 @@ export function checkPermission(mode, riskLevel, isReadOnly, toolName, toolInput
|
|
|
103
155
|
}
|
|
104
156
|
// Bash command safety analysis — detect destructive patterns
|
|
105
157
|
let effectiveRisk = riskLevel;
|
|
158
|
+
let bashReadOnly = false;
|
|
106
159
|
if (toolName === "Bash" && toolInput) {
|
|
107
160
|
const command = toolInput?.command;
|
|
108
161
|
if (typeof command === "string") {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
162
|
+
// Read-only allowlist short-circuit (Claude Code parity). A pure
|
|
163
|
+
// inspection pipeline like `ls | head` or `git status` should not
|
|
164
|
+
// prompt the user in any permission mode that gates read-only work.
|
|
165
|
+
if (isReadOnlyBashCommand(command)) {
|
|
166
|
+
bashReadOnly = true;
|
|
167
|
+
effectiveRisk = "low";
|
|
112
168
|
}
|
|
113
|
-
else
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
169
|
+
else {
|
|
170
|
+
const analysis = analyzeBashCommand(command);
|
|
171
|
+
if (analysis.level === "dangerous") {
|
|
172
|
+
effectiveRisk = "high";
|
|
173
|
+
}
|
|
174
|
+
else if (analysis.level === "moderate" && effectiveRisk !== "high") {
|
|
175
|
+
effectiveRisk = "medium";
|
|
176
|
+
}
|
|
177
|
+
else if (analysis.level === "safe") {
|
|
178
|
+
effectiveRisk = "medium"; // bash is never fully "low" risk
|
|
179
|
+
}
|
|
118
180
|
}
|
|
119
181
|
}
|
|
120
182
|
}
|
|
121
|
-
// Always allow low-risk read-only
|
|
122
|
-
if (effectiveRisk === "low" && isReadOnly) {
|
|
183
|
+
// Always allow low-risk read-only (now includes Bash commands matching the allowlist)
|
|
184
|
+
if (effectiveRisk === "low" && (isReadOnly || bashReadOnly)) {
|
|
123
185
|
return { allowed: true, reason: "auto-approved", riskLevel: effectiveRisk };
|
|
124
186
|
}
|
|
125
187
|
// bypassPermissions — approve everything unconditionally (CI/testing only)
|
|
@@ -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.9.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
|
],
|