@shidesheng0218/agentguard 0.8.0 → 0.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.
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ WIRE_VERIFY_CORRECTIVE,
4
+ analyzeCall,
5
+ callsSince,
6
+ captureCheckpoint,
7
+ castVetoVote,
8
+ claudeHooksInstalled,
9
+ collectVetoContext,
10
+ countBlocks,
11
+ extractFile,
12
+ findClaims,
13
+ fingerprint,
14
+ guardHome,
15
+ hasEvidence,
16
+ hashOutput,
17
+ loadConfig,
18
+ openDb,
19
+ outputSampleOf,
20
+ recordBlock,
21
+ recordCall,
22
+ recordEvent
23
+ } from "./chunk-SACTXNYK.js";
24
+
25
+ // src/run/claude.ts
26
+ import { spawn } from "child_process";
27
+ import fs from "fs";
28
+ import path from "path";
29
+ import readline from "readline";
30
+ async function runClaudeSupervised(opts) {
31
+ const cfg = opts.config ?? loadConfig(void 0, "claude");
32
+ const runId = `run-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${process.pid}`;
33
+ const logDir = path.join(guardHome(), "runs", runId);
34
+ fs.mkdirSync(logDir, { recursive: true });
35
+ const logPath = path.join(logDir, "stream.jsonl");
36
+ const startedAt = Date.now();
37
+ const report = {
38
+ runId,
39
+ command: [...opts.command ?? ["claude"], "-p", "<prompt>", "--output-format", "stream-json"],
40
+ startedAt: new Date(startedAt).toISOString(),
41
+ finishedAt: "",
42
+ durationMs: 0,
43
+ turns: 0,
44
+ steps: 0,
45
+ toolCalls: 0,
46
+ stepRetries: [],
47
+ blocks: [],
48
+ steers: [],
49
+ approvals: { approved: 0, rejected: 0 },
50
+ tokenUsage: { input_other: 0, output: 0, input_cache_read: 0, input_cache_creation: 0 },
51
+ finalStatus: "",
52
+ endReason: "finished",
53
+ resumes: 0,
54
+ verifyRounds: 0,
55
+ vetoes: 0,
56
+ thinkingDominance: 0,
57
+ reportPath: path.join(logDir, "report.json"),
58
+ logPath
59
+ };
60
+ const bin = (opts.command ?? ["claude"])[0];
61
+ const binArgs = (opts.command ?? ["claude"]).slice(1);
62
+ const permissionMode = opts.approval === "approve" ? "bypassPermissions" : "auto";
63
+ const hooksLive = claudeHooksInstalled();
64
+ let sessionId = "";
65
+ let attempt = 0;
66
+ let currentPrompt = opts.prompt;
67
+ let timedOut = false;
68
+ const sid = () => sessionId || runId;
69
+ function appendLog(line) {
70
+ try {
71
+ fs.appendFileSync(logPath, line + "\n");
72
+ } catch {
73
+ }
74
+ }
75
+ function onToolResult(call, outputText, isError) {
76
+ report.toolCalls++;
77
+ recordCall({
78
+ sessionId: sid(),
79
+ toolName: call.name,
80
+ argsHash: fingerprint(call.name, call.input),
81
+ argsJson: JSON.stringify(call.input ?? {}).slice(0, 2048),
82
+ outputHash: hashOutput(outputText),
83
+ outputSample: outputSampleOf(outputText),
84
+ filePath: extractFile(call.input),
85
+ status: isError ? "failure" : "ok"
86
+ });
87
+ if (!hooksLive) {
88
+ const since = Date.now() - 30 * 6e4;
89
+ const analysis = analyzeCall(callsSince(sid(), since), { tool: call.name, argsHash: fingerprint(call.name, call.input), args: call.input }, cfg);
90
+ const block = analysis.findings.find((f) => f.severity === "block");
91
+ if (block) {
92
+ recordBlock(sid(), call.name, block.kind);
93
+ report.blocks.push({ tool: call.name, kind: block.kind, message: block.message, ts: Date.now() });
94
+ }
95
+ }
96
+ }
97
+ function runOnce(prompt) {
98
+ return new Promise((resolve) => {
99
+ const pending = /* @__PURE__ */ new Map();
100
+ const args = [
101
+ ...binArgs,
102
+ "-p",
103
+ prompt,
104
+ "--output-format",
105
+ "stream-json",
106
+ "--verbose",
107
+ "--max-turns",
108
+ String(opts.maxSteps),
109
+ "--permission-mode",
110
+ permissionMode
111
+ ];
112
+ if (attempt > 1 && sessionId) args.push("--resume", sessionId);
113
+ const child = spawn(bin, args, { cwd: opts.cwd, env: { ...process.env, ...opts.env } });
114
+ const rl = readline.createInterface({ input: child.stdout });
115
+ let result = null;
116
+ let sawInit = false;
117
+ const killTimer = setTimeout(() => {
118
+ timedOut = true;
119
+ report.endReason = "timeout";
120
+ child.kill("SIGINT");
121
+ setTimeout(() => child.kill("SIGKILL"), 5e3).unref();
122
+ }, opts.maxMinutes * 6e4);
123
+ killTimer.unref();
124
+ rl.on("line", (line) => {
125
+ appendLog(line);
126
+ let msg;
127
+ try {
128
+ msg = JSON.parse(line);
129
+ } catch {
130
+ return;
131
+ }
132
+ switch (msg.type) {
133
+ case "system":
134
+ if (msg.subtype === "init" && msg.session_id) {
135
+ sessionId = msg.session_id;
136
+ if (!sawInit) recordEvent(runId, "run_start", { prompt: opts.prompt.slice(0, 200), harness: "claude" });
137
+ sawInit = true;
138
+ }
139
+ break;
140
+ case "assistant": {
141
+ report.steps++;
142
+ for (const block of msg.message?.content ?? []) {
143
+ if (block["type"] === "tool_use") {
144
+ pending.set(String(block["id"]), { name: String(block["name"]), input: block["input"] });
145
+ }
146
+ }
147
+ break;
148
+ }
149
+ case "user": {
150
+ for (const block of msg.message?.content ?? []) {
151
+ if (block["type"] === "tool_result") {
152
+ const call = pending.get(String(block["tool_use_id"]));
153
+ const content = block["content"];
154
+ const text = typeof content === "string" ? content : Array.isArray(content) ? content.map((c) => c && typeof c === "object" ? String(c["text"] ?? "") : "").join("\n") : JSON.stringify(content ?? "");
155
+ if (call) {
156
+ pending.delete(String(block["tool_use_id"]));
157
+ onToolResult(call, text, Boolean(block["is_error"]));
158
+ }
159
+ }
160
+ }
161
+ break;
162
+ }
163
+ case "result": {
164
+ result = msg;
165
+ report.turns += msg.num_turns ?? 0;
166
+ if (msg.usage) {
167
+ report.tokenUsage.input_other += msg.usage["input_tokens"] ?? 0;
168
+ report.tokenUsage.output += msg.usage["output_tokens"] ?? 0;
169
+ report.tokenUsage.input_cache_read += msg.usage["cache_read_input_tokens"] ?? 0;
170
+ report.tokenUsage.input_cache_creation += msg.usage["cache_creation_input_tokens"] ?? 0;
171
+ }
172
+ if (typeof msg.total_cost_usd === "number") recordEvent(sid(), "cost", { usd: msg.total_cost_usd });
173
+ if (msg.api_error_status) report.stepRetries.push({ n: 0, error_type: "api_error", status_code: msg.api_error_status });
174
+ break;
175
+ }
176
+ default:
177
+ break;
178
+ }
179
+ });
180
+ const killWatch = setInterval(() => {
181
+ const n = countBlocks(sid(), startedAt - cfg.policy.blockWindowMinutes * 6e4);
182
+ if (cfg.policy.killSwitch && n >= cfg.policy.maxBlocksPerSession) {
183
+ report.endReason = "kill-switch";
184
+ child.kill("SIGINT");
185
+ setTimeout(() => child.kill("SIGKILL"), 3e3).unref();
186
+ clearInterval(killWatch);
187
+ }
188
+ }, 1e3);
189
+ killWatch.unref();
190
+ child.on("error", (err) => {
191
+ report.finalStatus = /ENOENT/.test(err.message) ? `${err.message} \u2014 is Claude Code installed and on PATH?` : err.message;
192
+ resolve(null);
193
+ });
194
+ child.on("close", () => {
195
+ clearTimeout(killTimer);
196
+ clearInterval(killWatch);
197
+ resolve(result);
198
+ });
199
+ });
200
+ }
201
+ openDb();
202
+ while (true) {
203
+ attempt++;
204
+ const result = await runOnce(currentPrompt);
205
+ if (!result) {
206
+ if (report.finalStatus === "") report.finalStatus = "no result (process ended without a result message)";
207
+ if (report.endReason === "finished") report.endReason = timedOut ? "timeout" : "error";
208
+ break;
209
+ }
210
+ report.finalStatus = `${result.subtype ?? "?"}${result.is_error ? " (is_error)" : ""}`;
211
+ const maxTurnsHit = result.subtype === "error_max_turns";
212
+ if (maxTurnsHit && attempt <= opts.autoResume) {
213
+ report.resumes++;
214
+ report.endReason = "max_steps";
215
+ const brief = captureCheckpoint(sid(), "auto-resume", Date.now(), cfg);
216
+ currentPrompt = (brief ? `You were stopped at the turn limit. Observed state so far:
217
+
218
+ ${brief.brief}
219
+
220
+ ` : "") + "You reached the turn limit. Continue from where you stopped \u2014 do not repeat work already done.";
221
+ continue;
222
+ }
223
+ if (!result.is_error && result.subtype === "success") {
224
+ const text = result.result ?? "";
225
+ if (cfg.verify.enabled && report.verifyRounds < opts.maxVerifyRounds) {
226
+ const claims = findClaims(text, cfg);
227
+ if (claims.length > 0 && !hasEvidence(sid(), cfg)) {
228
+ if (cfg.verify.veto.enabled) {
229
+ const vote = await castVetoVote({ ...collectVetoContext(sid(), cfg), claims, goal: opts.prompt }, cfg.verify.veto, { ...process.env, ...opts.env });
230
+ if (vote.vetoed) {
231
+ report.vetoes++;
232
+ recordEvent(sid(), "veto", { claims: claims.length });
233
+ break;
234
+ }
235
+ }
236
+ report.verifyRounds++;
237
+ report.endReason = "verify";
238
+ recordEvent(sid(), "verify_gate", { claims: claims.length });
239
+ captureCheckpoint(sid(), "verify-gate", Date.now(), cfg);
240
+ currentPrompt = WIRE_VERIFY_CORRECTIVE;
241
+ continue;
242
+ }
243
+ }
244
+ if (report.endReason === "verify") report.endReason = "finished";
245
+ break;
246
+ }
247
+ if (report.endReason === "finished" || report.endReason === "verify") report.endReason = result.is_error ? "error" : "finished";
248
+ break;
249
+ }
250
+ if (report.endReason === "kill-switch") captureCheckpoint(sid(), "kill-switch", Date.now(), cfg);
251
+ report.durationMs = Date.now() - startedAt;
252
+ report.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
253
+ recordEvent(runId, "run_end", { endReason: report.endReason, blocks: report.blocks.length, harness: "claude" });
254
+ try {
255
+ fs.writeFileSync(report.reportPath, JSON.stringify(report, null, 2), "utf8");
256
+ } catch {
257
+ }
258
+ return report;
259
+ }
260
+ export {
261
+ runClaudeSupervised
262
+ };