@zhijiewang/openharness 2.29.0 → 2.30.1
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 -5
- package/README.zh-CN.md +8 -5
- package/dist/Tool.d.ts +4 -0
- package/dist/commands/ai.js +4 -4
- package/dist/commands/git.js +1 -1
- package/dist/commands/info.js +30 -3
- package/dist/commands/session.js +1 -2
- package/dist/commands/settings.js +1 -1
- package/dist/commands/skills.js +2 -5
- package/dist/components/InitWizard.js +1 -1
- package/dist/harness/config.js +3 -7
- package/dist/harness/plugins.js +1 -1
- package/dist/harness/telemetry.js +18 -12
- package/dist/harness/traces.d.ts +31 -1
- package/dist/harness/traces.js +85 -4
- package/dist/providers/anthropic.js +4 -1
- package/dist/query/index.js +208 -195
- package/dist/query/tools.js +5 -0
- package/dist/query/types.d.ts +3 -0
- package/dist/repl.js +22 -1
- package/dist/services/AgentDispatcher.js +15 -28
- package/dist/services/StreamingToolExecutor.js +102 -11
- package/dist/tools/CronTool/index.d.ts +2 -2
- package/dist/tools/DiagnosticsTool/index.d.ts +1 -1
- package/dist/tools/GrepTool/index.d.ts +2 -2
- package/dist/tools/PowerShellTool/index.js +11 -2
- package/package.json +1 -1
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Tool execution during LLM streaming — concurrent tool execution
|
|
3
3
|
* with permission checks and queue management.
|
|
4
4
|
*/
|
|
5
|
+
import { getAffectedFiles } from "../harness/checkpoints.js";
|
|
6
|
+
import { emitHook, emitHookWithOutcome } from "../harness/hooks.js";
|
|
5
7
|
import { findToolByName } from "../Tool.js";
|
|
6
8
|
import { checkPermission } from "../types/permissions.js";
|
|
7
9
|
const MAX_CONCURRENCY = 10;
|
|
@@ -54,23 +56,69 @@ export class StreamingToolExecutor {
|
|
|
54
56
|
tracked.status = "completed";
|
|
55
57
|
return;
|
|
56
58
|
}
|
|
59
|
+
const argsPreview = JSON.stringify(tracked.toolCall.arguments).slice(0, 1000);
|
|
57
60
|
// Permission check
|
|
58
61
|
const perm = checkPermission(this.permissionMode, tool.riskLevel, tool.isReadOnly(tracked.toolCall.arguments), tool.name, tracked.toolCall.arguments);
|
|
59
|
-
if (!perm.allowed
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
if (!perm.allowed) {
|
|
63
|
+
if (perm.reason === "needs-approval") {
|
|
64
|
+
// Hook: permissionRequest — give configured hooks first say. If they
|
|
65
|
+
// explicitly allow/deny, that wins; otherwise fall through to the
|
|
66
|
+
// interactive prompt or to a fail-closed deny in headless mode.
|
|
67
|
+
const hookOutcome = await emitHookWithOutcome("permissionRequest", {
|
|
68
|
+
toolName: tool.name,
|
|
69
|
+
toolArgs: argsPreview,
|
|
70
|
+
toolInputJson: JSON.stringify(tracked.toolCall.arguments).slice(0, 1000),
|
|
71
|
+
permissionMode: this.permissionMode,
|
|
72
|
+
permissionAction: "ask",
|
|
73
|
+
});
|
|
74
|
+
const denyAndEmit = (source, reason, output) => {
|
|
75
|
+
emitHook("permissionDenied", {
|
|
76
|
+
toolName: tool.name,
|
|
77
|
+
toolArgs: argsPreview,
|
|
78
|
+
permissionMode: this.permissionMode,
|
|
79
|
+
denySource: source,
|
|
80
|
+
denyReason: reason,
|
|
81
|
+
});
|
|
82
|
+
tracked.result = { output, isError: true };
|
|
83
|
+
tracked.status = "completed";
|
|
84
|
+
};
|
|
85
|
+
if (hookOutcome.permissionDecision === "allow") {
|
|
86
|
+
// Hook granted — proceed.
|
|
87
|
+
}
|
|
88
|
+
else if (hookOutcome.permissionDecision === "deny" || !hookOutcome.allowed) {
|
|
89
|
+
const reason = hookOutcome.reason ? `: ${hookOutcome.reason}` : "";
|
|
90
|
+
denyAndEmit("hook", hookOutcome.reason ?? "hook denied", `Permission denied by hook${reason}`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
else if (this.askUser) {
|
|
94
|
+
const { formatToolArgs } = await import("../utils/tool-summary.js");
|
|
95
|
+
const description = formatToolArgs(tool.name, tracked.toolCall.arguments);
|
|
96
|
+
const allowed = await this.askUser(tool.name, description, tool.riskLevel);
|
|
97
|
+
if (!allowed) {
|
|
98
|
+
denyAndEmit("user", "user declined", "Permission denied by user.");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// Headless mode with no hook decision and no interactive prompt.
|
|
104
|
+
denyAndEmit("headless", "no hook decision and no interactive prompt available", "Permission denied: needs-approval (no interactive prompt available; configure a permissionRequest hook to gate this tool)");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
// Auto-mode policy block (deny / acceptEdits / etc) — symmetric event.
|
|
110
|
+
emitHook("permissionDenied", {
|
|
111
|
+
toolName: tool.name,
|
|
112
|
+
toolArgs: argsPreview,
|
|
113
|
+
permissionMode: this.permissionMode,
|
|
114
|
+
denySource: "policy",
|
|
115
|
+
denyReason: perm.reason,
|
|
116
|
+
});
|
|
117
|
+
tracked.result = { output: `Denied: ${perm.reason}`, isError: true };
|
|
65
118
|
tracked.status = "completed";
|
|
66
119
|
return;
|
|
67
120
|
}
|
|
68
121
|
}
|
|
69
|
-
else if (!perm.allowed) {
|
|
70
|
-
tracked.result = { output: `Denied: ${perm.reason}`, isError: true };
|
|
71
|
-
tracked.status = "completed";
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
122
|
// Validate input
|
|
75
123
|
const parsed = tool.inputSchema.safeParse(tracked.toolCall.arguments);
|
|
76
124
|
if (!parsed.success) {
|
|
@@ -84,6 +132,17 @@ export class StreamingToolExecutor {
|
|
|
84
132
|
tracked.status = "completed";
|
|
85
133
|
return;
|
|
86
134
|
}
|
|
135
|
+
// Hook: preToolUse — last gate before execution. A hook that returns
|
|
136
|
+
// false (exit code 1 / { allowed: false }) blocks the call.
|
|
137
|
+
const preAllowed = emitHook("preToolUse", {
|
|
138
|
+
toolName: tool.name,
|
|
139
|
+
toolArgs: argsPreview,
|
|
140
|
+
});
|
|
141
|
+
if (!preAllowed) {
|
|
142
|
+
tracked.result = { output: "Blocked by preToolUse hook.", isError: true };
|
|
143
|
+
tracked.status = "completed";
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
87
146
|
// Execute with per-call context (streaming output chunks + abort signal)
|
|
88
147
|
const callId = tracked.toolCall.id;
|
|
89
148
|
const callContext = {
|
|
@@ -94,8 +153,11 @@ export class StreamingToolExecutor {
|
|
|
94
153
|
this.outputChunks.push({ callId: id, chunk });
|
|
95
154
|
},
|
|
96
155
|
};
|
|
156
|
+
const toolSpanId = callContext.tracer?.startSpan(`tool:${tool.name}`, { riskLevel: tool.riskLevel }, callContext.parentSpanId);
|
|
97
157
|
try {
|
|
98
158
|
tracked.result = await tool.call(parsed.data, callContext);
|
|
159
|
+
if (toolSpanId)
|
|
160
|
+
callContext.tracer?.endSpan(toolSpanId, tracked.result.isError ? "error" : "ok");
|
|
99
161
|
// Verification loop: auto-run lint/typecheck after file-modifying tools
|
|
100
162
|
if (tracked.result && !tracked.result.isError && ["Edit", "Write", "MultiEdit"].includes(tool.name)) {
|
|
101
163
|
try {
|
|
@@ -132,6 +194,35 @@ export class StreamingToolExecutor {
|
|
|
132
194
|
output: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
133
195
|
isError: true,
|
|
134
196
|
};
|
|
197
|
+
if (toolSpanId)
|
|
198
|
+
callContext.tracer?.endSpan(toolSpanId, "error", { error: tracked.result.output });
|
|
199
|
+
}
|
|
200
|
+
// Hook: postToolUse / postToolUseFailure (mutually exclusive — strict CC parity)
|
|
201
|
+
if (tracked.result) {
|
|
202
|
+
const outputPreview = tracked.result.output.slice(0, 1000);
|
|
203
|
+
if (tracked.result.isError) {
|
|
204
|
+
emitHook("postToolUseFailure", {
|
|
205
|
+
toolName: tool.name,
|
|
206
|
+
toolArgs: argsPreview,
|
|
207
|
+
toolOutput: outputPreview,
|
|
208
|
+
toolError: "ReportedError",
|
|
209
|
+
errorMessage: outputPreview,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
emitHook("postToolUse", {
|
|
214
|
+
toolName: tool.name,
|
|
215
|
+
toolArgs: argsPreview,
|
|
216
|
+
toolOutput: outputPreview,
|
|
217
|
+
});
|
|
218
|
+
// Emit fileChanged hook for file-modifying tools
|
|
219
|
+
if (["Edit", "Write", "MultiEdit"].includes(tool.name)) {
|
|
220
|
+
const filePaths = getAffectedFiles(tool.name, parsed.data);
|
|
221
|
+
for (const fp of filePaths) {
|
|
222
|
+
emitHook("fileChanged", { filePath: fp, toolName: tool.name });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
135
226
|
}
|
|
136
227
|
tracked.status = "completed";
|
|
137
228
|
this.processQueue(); // Process next queued tools
|
|
@@ -6,13 +6,13 @@ declare const createSchema: z.ZodObject<{
|
|
|
6
6
|
schedule: z.ZodString;
|
|
7
7
|
prompt: z.ZodString;
|
|
8
8
|
}, "strip", z.ZodTypeAny, {
|
|
9
|
-
action: "create";
|
|
10
9
|
name: string;
|
|
10
|
+
action: "create";
|
|
11
11
|
prompt: string;
|
|
12
12
|
schedule: string;
|
|
13
13
|
}, {
|
|
14
|
-
action: "create";
|
|
15
14
|
name: string;
|
|
15
|
+
action: "create";
|
|
16
16
|
prompt: string;
|
|
17
17
|
schedule: string;
|
|
18
18
|
}>;
|
|
@@ -6,8 +6,8 @@ declare const inputSchema: z.ZodObject<{
|
|
|
6
6
|
line: z.ZodOptional<z.ZodNumber>;
|
|
7
7
|
character: z.ZodOptional<z.ZodNumber>;
|
|
8
8
|
}, "strip", z.ZodTypeAny, {
|
|
9
|
-
action: "diagnostics" | "definition" | "references" | "hover";
|
|
10
9
|
file_path: string;
|
|
10
|
+
action: "diagnostics" | "definition" | "references" | "hover";
|
|
11
11
|
line?: number | undefined;
|
|
12
12
|
character?: number | undefined;
|
|
13
13
|
}, {
|
|
@@ -17,9 +17,9 @@ declare const inputSchema: z.ZodObject<{
|
|
|
17
17
|
"-n": z.ZodOptional<z.ZodBoolean>;
|
|
18
18
|
}, "strip", z.ZodTypeAny, {
|
|
19
19
|
pattern: string;
|
|
20
|
+
path?: string | undefined;
|
|
20
21
|
type?: string | undefined;
|
|
21
22
|
"-i"?: boolean | undefined;
|
|
22
|
-
path?: string | undefined;
|
|
23
23
|
context?: number | undefined;
|
|
24
24
|
glob?: string | undefined;
|
|
25
25
|
offset?: number | undefined;
|
|
@@ -32,9 +32,9 @@ declare const inputSchema: z.ZodObject<{
|
|
|
32
32
|
"-n"?: boolean | undefined;
|
|
33
33
|
}, {
|
|
34
34
|
pattern: string;
|
|
35
|
+
path?: string | undefined;
|
|
35
36
|
type?: string | undefined;
|
|
36
37
|
"-i"?: boolean | undefined;
|
|
37
|
-
path?: string | undefined;
|
|
38
38
|
context?: number | undefined;
|
|
39
39
|
glob?: string | undefined;
|
|
40
40
|
offset?: number | undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
const inputSchema = z.object({
|
|
4
4
|
command: z.string().describe("PowerShell command to execute"),
|
|
@@ -21,7 +21,16 @@ export const PowerShellTool = {
|
|
|
21
21
|
}
|
|
22
22
|
const timeout = input.timeout ?? 120_000;
|
|
23
23
|
try {
|
|
24
|
-
|
|
24
|
+
// execFileSync(file, args[]) spawns powershell.exe directly without a
|
|
25
|
+
// cmd.exe wrapper, so cmd.exe metachars (& | < > ^ %VAR%) are inert.
|
|
26
|
+
// The user's command is passed as a single -Command arg; PowerShell
|
|
27
|
+
// parses it as PowerShell, not as a doubly-parsed shell string.
|
|
28
|
+
const output = execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", input.command], {
|
|
29
|
+
encoding: "utf-8",
|
|
30
|
+
timeout,
|
|
31
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
32
|
+
windowsHide: true,
|
|
33
|
+
});
|
|
25
34
|
return { output: output.trim(), isError: false };
|
|
26
35
|
}
|
|
27
36
|
catch (err) {
|