@agentwares/agentguard 0.1.6 → 0.1.9

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +223 -16
  3. package/dist/audit-2YCMHMCP.js +17 -0
  4. package/dist/audit-2YCMHMCP.js.map +1 -0
  5. package/dist/chunk-3O6KWT3E.js +345 -0
  6. package/dist/chunk-3O6KWT3E.js.map +1 -0
  7. package/dist/chunk-5WRI5ZAA.js +31 -0
  8. package/dist/chunk-5WRI5ZAA.js.map +1 -0
  9. package/dist/chunk-6S7LYVOC.js +1033 -0
  10. package/dist/chunk-6S7LYVOC.js.map +1 -0
  11. package/dist/chunk-6TUQD666.js +828 -0
  12. package/dist/chunk-6TUQD666.js.map +1 -0
  13. package/dist/chunk-BKXXTWIS.js +119 -0
  14. package/dist/chunk-BKXXTWIS.js.map +1 -0
  15. package/dist/chunk-D66U3PVB.js +529 -0
  16. package/dist/chunk-D66U3PVB.js.map +1 -0
  17. package/dist/chunk-HPYZ3JDX.js +206 -0
  18. package/dist/chunk-HPYZ3JDX.js.map +1 -0
  19. package/dist/chunk-KRKZGAJ3.js +769 -0
  20. package/dist/chunk-KRKZGAJ3.js.map +1 -0
  21. package/dist/chunk-SUXQTT3X.js +1268 -0
  22. package/dist/chunk-SUXQTT3X.js.map +1 -0
  23. package/dist/chunk-TW5KJREH.js +190 -0
  24. package/dist/chunk-TW5KJREH.js.map +1 -0
  25. package/dist/chunk-VQWQ5TOM.js +113 -0
  26. package/dist/chunk-VQWQ5TOM.js.map +1 -0
  27. package/dist/chunk-X4M7YWQA.js +270 -0
  28. package/dist/chunk-X4M7YWQA.js.map +1 -0
  29. package/dist/chunk-ZSLWPKXI.js +1144 -0
  30. package/dist/chunk-ZSLWPKXI.js.map +1 -0
  31. package/dist/cli.js +8 -4725
  32. package/dist/cli.js.map +1 -1
  33. package/dist/fixtures/crm-server.js +6 -1272
  34. package/dist/fixtures/crm-server.js.map +1 -1
  35. package/dist/fixtures/demo-agent.js +4 -115
  36. package/dist/fixtures/demo-agent.js.map +1 -1
  37. package/dist/index.d.ts +76 -10
  38. package/dist/index.js +246 -5087
  39. package/dist/index.js.map +1 -1
  40. package/dist/init-CWROGV5I.js +15 -0
  41. package/dist/init-CWROGV5I.js.map +1 -0
  42. package/dist/proxy-3Z5YEZ4R.js +154 -0
  43. package/dist/proxy-3Z5YEZ4R.js.map +1 -0
  44. package/dist/tools-3LLDG3XQ.js +15 -0
  45. package/dist/tools-3LLDG3XQ.js.map +1 -0
  46. package/llms.txt +4 -0
  47. package/package.json +9 -3
  48. package/server.json +2 -2
@@ -0,0 +1,828 @@
1
+ import {
2
+ CLIENT_ALIASES,
3
+ hookSettingsFiles,
4
+ hooksCommand
5
+ } from "./chunk-3O6KWT3E.js";
6
+ import {
7
+ PROXY_SERVER_NAME,
8
+ describeConfigPath,
9
+ detectClientConfigs,
10
+ isGeminiConfig,
11
+ mergeServerIntoConfig
12
+ } from "./chunk-TW5KJREH.js";
13
+ import {
14
+ DEFAULT_POLICY_FILE,
15
+ approvalUrlFor,
16
+ createFileGuard,
17
+ defaultIo,
18
+ describeError,
19
+ flagBool,
20
+ flagList,
21
+ flagNumber,
22
+ flagString,
23
+ homeFrom,
24
+ loadPolicyFor,
25
+ parseArgs,
26
+ policyPathFrom,
27
+ readRegistration,
28
+ version
29
+ } from "./chunk-SUXQTT3X.js";
30
+
31
+ // src/commands/connect.ts
32
+ import { existsSync } from "fs";
33
+ import { relative } from "path";
34
+ var DEFAULT_PROXY_URL = "https://agentwares-agentguard-proxy.vercel.app";
35
+ function fail(e) {
36
+ throw e;
37
+ }
38
+ async function connectCommand(args, io, fetchImpl = fetch) {
39
+ const key = args.positionals[0] ?? io.env.AGENTGUARD_KEY;
40
+ if (!key) {
41
+ io.err("usage: agentguard connect <key> [--write] [--client <path>] [--url <proxy-url>]");
42
+ io.err(" the key is on your dashboard; it starts with agk_");
43
+ return 2;
44
+ }
45
+ const base = (flagString(args, "url") ?? io.env.AGENTGUARD_PROXY_URL ?? DEFAULT_PROXY_URL).replace(/\/+$/, "");
46
+ let res;
47
+ try {
48
+ res = await fetchImpl(`${base}/connect`, {
49
+ method: "POST",
50
+ headers: { "content-type": "application/json" },
51
+ body: JSON.stringify({ key })
52
+ });
53
+ } catch (err) {
54
+ fail({
55
+ code: "PROXY_UNREACHABLE",
56
+ cause: `could not reach ${base}: ${err instanceof Error ? err.message : String(err)}`,
57
+ fix: "check the network, or pass --url if you run your own proxy",
58
+ retryable: true
59
+ });
60
+ }
61
+ const text = await res.text();
62
+ let body;
63
+ try {
64
+ body = JSON.parse(text);
65
+ } catch {
66
+ fail({
67
+ code: "PROXY_BAD_RESPONSE",
68
+ cause: `${base}/connect returned ${res.status} that was not JSON: ${text.slice(0, 200)}`,
69
+ fix: "check --url points at an agentguard proxy, not at the dashboard",
70
+ retryable: false
71
+ });
72
+ }
73
+ if (!res.ok) {
74
+ const e = body;
75
+ fail({
76
+ code: e.code ?? "CONNECT_REFUSED",
77
+ cause: e.cause ?? `${base}/connect returned ${res.status}`,
78
+ fix: e.fix ?? "check the key on your dashboard; revoked keys stop working immediately",
79
+ retryable: e.retryable ?? false
80
+ });
81
+ }
82
+ const conn = body;
83
+ if (flagBool(args, "json")) {
84
+ io.out(JSON.stringify(conn, null, 2));
85
+ return 0;
86
+ }
87
+ const rel = (p) => p.startsWith(io.cwd + "/") ? relative(io.cwd, p) : p;
88
+ const entry = conn.mcpServers[PROXY_SERVER_NAME] ?? {
89
+ type: "http",
90
+ url: conn.mcpUrl,
91
+ headers: conn.headers
92
+ };
93
+ if (!flagBool(args, "write")) {
94
+ io.out(JSON.stringify({ mcpServers: { [PROXY_SERVER_NAME]: entry } }, null, 2));
95
+ } else {
96
+ const explicit = flagString(args, "client");
97
+ const configs = explicit ? [describeConfigPath(explicit)] : detectClientConfigs(io.cwd, homeFrom(io), io.env);
98
+ const projectLevel = configs.filter((c) => c.path.startsWith(io.cwd + "/"));
99
+ let chosen;
100
+ if (explicit || flagBool(args, "all")) chosen = configs;
101
+ else chosen = projectLevel.slice(0, 1);
102
+ if (chosen.length === 0) {
103
+ io.err(
104
+ "no MCP config found here (.mcp.json, .cursor/mcp.json, .vscode/mcp.json). Create one, or paste this into your client:"
105
+ );
106
+ io.out(JSON.stringify({ mcpServers: { [PROXY_SERVER_NAME]: entry } }, null, 2));
107
+ return 1;
108
+ }
109
+ for (const c of chosen) {
110
+ if (!existsSync(c.path)) {
111
+ io.err(`no MCP config at ${c.path}`);
112
+ return 1;
113
+ }
114
+ const forClient = isGeminiConfig(c) ? { httpUrl: entry.url, ...entry.headers ? { headers: entry.headers } : {} } : entry;
115
+ const { backup, replaced } = mergeServerIntoConfig(c, PROXY_SERVER_NAME, forClient);
116
+ io.out(
117
+ `${replaced ? "updated" : "wrote "} ${rel(c.path)} (${c.label}) \u2192 ${PROXY_SERVER_NAME} = ${conn.mcpUrl}`
118
+ );
119
+ io.out(`backup ${rel(backup)}`);
120
+ }
121
+ for (const c of configs.filter((c2) => !chosen.includes(c2)))
122
+ io.out(
123
+ `found ${c.path} (${c.label}, user-level) \u2014 not touched; pass --client ${JSON.stringify(c.path)} to write it too`
124
+ );
125
+ }
126
+ const band = conn.band ? ` band=${conn.band.tier} (${conn.band.quantity} calls/mo)` : "";
127
+ io.out(`
128
+ proxy ${conn.slug} (${conn.proxyId}) mode=${conn.mode}${band}`);
129
+ if (conn.upstreams?.length) io.out(`upstreams: ${conn.upstreams.join(", ")}`);
130
+ if (conn.dashboardUrl) io.out(`dashboard: ${conn.dashboardUrl}`);
131
+ if (conn.claudeCode) io.out(`claude code: ${conn.claudeCode}`);
132
+ if (!flagBool(args, "write"))
133
+ io.out(`
134
+ run again with --write to add this to your MCP client config`);
135
+ return 0;
136
+ }
137
+
138
+ // src/commands/control.ts
139
+ async function pokeHttp(io, reg, path, body) {
140
+ if (!reg) return;
141
+ try {
142
+ const res = await fetch(`${reg.baseUrl}${path}`, {
143
+ method: "POST",
144
+ headers: { Authorization: `Bearer ${reg.token}`, "Content-Type": "application/json" },
145
+ body: body === void 0 ? void 0 : JSON.stringify(body),
146
+ signal: AbortSignal.timeout(2e3)
147
+ });
148
+ if (!res.ok)
149
+ io.err(
150
+ `running proxy at ${reg.baseUrl} answered ${res.status} (the file switch still applies)`
151
+ );
152
+ } catch {
153
+ io.err(
154
+ `no proxy answering at ${reg.baseUrl} (stale ${reg.pid}?) \u2014 the file switch still applies`
155
+ );
156
+ }
157
+ }
158
+ async function killCommand(args, io) {
159
+ const loaded = loadPolicyFor(args, io, { allowMissingEnv: true });
160
+ const { kill } = createFileGuard(loaded, { alerts: false, env: io.env });
161
+ const reason = args.positionals.join(" ") || flagString(args, "reason") || `agentguard kill by ${io.env.USER ?? "operator"}`;
162
+ kill.kill(reason);
163
+ await pokeHttp(io, readRegistration(loaded), "/kill", { reason });
164
+ io.out(
165
+ `KILLED: ${kill.path} written \u2014 every tool call through this policy now returns KILLED. Clear with: agentguard resume`
166
+ );
167
+ return 0;
168
+ }
169
+ async function resumeCommand(args, io) {
170
+ const loaded = loadPolicyFor(args, io, { allowMissingEnv: true });
171
+ const { kill } = createFileGuard(loaded, { alerts: false, env: io.env });
172
+ kill.resume();
173
+ await pokeHttp(io, readRegistration(loaded), "/resume");
174
+ io.out(`resumed: ${kill.path} removed`);
175
+ if (io.env[loaded.policy.kill.env])
176
+ io.err(
177
+ `note: ${loaded.policy.kill.env} is set in this shell's environment; proxies started from it stay killed`
178
+ );
179
+ return 0;
180
+ }
181
+ async function decideCommand(decision, args, io) {
182
+ const id = args.positionals[0];
183
+ if (!id) {
184
+ io.err(
185
+ `usage: agentguard ${decision === "approved" ? "approve" : "deny"} <approval-id> [--by name] [--note text]`
186
+ );
187
+ return 1;
188
+ }
189
+ const loaded = loadPolicyFor(args, io, { allowMissingEnv: true });
190
+ const { guard } = createFileGuard(loaded, { alerts: false, env: io.env });
191
+ const record = await guard.decide(
192
+ id,
193
+ decision,
194
+ flagString(args, "by") ?? io.env.USER ?? "cli",
195
+ flagString(args, "note")
196
+ );
197
+ if (!record) {
198
+ io.err(`no approval ${id} \u2014 pending ones: agentguard approvals`);
199
+ return 1;
200
+ }
201
+ io.out(
202
+ `${record.status}: ${record.tool} (${record.id}, run ${record.run_id})${record.status === "approved" ? " \u2014 the agent may now retry the identical call once" : ""}`
203
+ );
204
+ return 0;
205
+ }
206
+ async function approvalsCommand(args, io) {
207
+ const loaded = loadPolicyFor(args, io, { allowMissingEnv: true });
208
+ const { guard } = createFileGuard(loaded, { alerts: false, env: io.env });
209
+ const list = await guard.approvals.list(flagBool(args, "all") ? void 0 : "pending");
210
+ if (flagBool(args, "json")) {
211
+ io.out(JSON.stringify(list, null, 2));
212
+ return 0;
213
+ }
214
+ if (list.length === 0) {
215
+ io.out(flagBool(args, "all") ? "no approvals recorded" : "no pending approvals");
216
+ return 0;
217
+ }
218
+ for (const r of list) {
219
+ io.out(
220
+ `${r.id} ${r.status.padEnd(9)} ${r.tool} run ${r.run_id}${r.agent ? ` agent ${r.agent}` : ""} expires ${r.expires_at}`
221
+ );
222
+ io.out(` args: ${JSON.stringify(r.args).slice(0, 300)}`);
223
+ if (r.status === "pending")
224
+ io.out(` \u2192 agentguard approve ${r.id} | agentguard deny ${r.id}`);
225
+ }
226
+ return 0;
227
+ }
228
+
229
+ // src/commands/hook.ts
230
+ import { existsSync as existsSync3 } from "fs";
231
+ import { tmpdir } from "os";
232
+ import { dirname, join, resolve } from "path";
233
+ import {
234
+ assessHookAction,
235
+ detectHookClient,
236
+ hookActionFromInput
237
+ } from "@agentwares/agentguard-core";
238
+
239
+ // src/hooks/transcript-file.ts
240
+ import { closeSync, existsSync as existsSync2, openSync, readSync, statSync } from "fs";
241
+ import { scanTranscript } from "@agentwares/agentguard-core";
242
+ var STEPS = [1 << 20, 8 << 20, 64 << 20];
243
+ function readTail(path, bytes, size) {
244
+ const length = Math.min(bytes, size);
245
+ const fd = openSync(path, "r");
246
+ try {
247
+ const buf = Buffer.alloc(length);
248
+ readSync(fd, buf, 0, length, size - length);
249
+ return buf.toString("utf8");
250
+ } finally {
251
+ closeSync(fd);
252
+ }
253
+ }
254
+ function humanTurnFromTranscript(path, opts = {}) {
255
+ if (!path) return { missing: "the hook input carried no transcript_path" };
256
+ if (!existsSync2(path)) return { missing: `no transcript at ${path}` };
257
+ let size;
258
+ try {
259
+ size = statSync(path).size;
260
+ } catch (err) {
261
+ return {
262
+ missing: `transcript unreadable: ${err instanceof Error ? err.message : String(err)}`
263
+ };
264
+ }
265
+ let scan;
266
+ for (const step of STEPS) {
267
+ try {
268
+ scan = scanTranscript(readTail(path, step, size), opts);
269
+ } catch (err) {
270
+ return {
271
+ missing: `transcript unreadable: ${err instanceof Error ? err.message : String(err)}`
272
+ };
273
+ }
274
+ if (scan.turn || step >= size) break;
275
+ }
276
+ if (scan?.turn) return { turn: scan.turn, scan };
277
+ return {
278
+ missing: size > STEPS[STEPS.length - 1] ? "none in the last 64 MB of the transcript" : void 0,
279
+ scan
280
+ };
281
+ }
282
+
283
+ // src/commands/hook.ts
284
+ import { loadPolicyFile } from "@agentwares/agentguard-core/node";
285
+ var RUN_PREFIX = {
286
+ "claude-code": "cc",
287
+ codex: "codex",
288
+ "gemini-cli": "gemini"
289
+ };
290
+ function findPolicyUpwards(start) {
291
+ let dir = resolve(start);
292
+ for (let i = 0; i < 64; i += 1) {
293
+ const candidate = join(dir, DEFAULT_POLICY_FILE);
294
+ if (existsSync3(candidate)) return candidate;
295
+ const parent = dirname(dir);
296
+ if (parent === dir) return void 0;
297
+ dir = parent;
298
+ }
299
+ return void 0;
300
+ }
301
+ function projectDir(io, inputCwd) {
302
+ return io.env.CLAUDE_PROJECT_DIR || io.env.GEMINI_PROJECT_DIR || inputCwd || void 0;
303
+ }
304
+ function resolveHookPolicy(args, io, inputCwd) {
305
+ const flag = flagString(args, "config");
306
+ if (flag) return { path: resolve(io.cwd, flag), found: "flag" };
307
+ if (io.env.AGENTGUARD_CONFIG)
308
+ return { path: resolve(io.cwd, io.env.AGENTGUARD_CONFIG), found: "env" };
309
+ const start = projectDir(io, inputCwd) ?? io.cwd;
310
+ const near = findPolicyUpwards(start);
311
+ if (near) return { path: near, found: "project" };
312
+ const fallback = flagString(args, "fallback-config");
313
+ if (fallback && existsSync3(resolve(io.cwd, fallback)))
314
+ return { path: resolve(io.cwd, fallback), found: "fallback" };
315
+ return void 0;
316
+ }
317
+ function out(io, value) {
318
+ io.out(JSON.stringify(value));
319
+ }
320
+ function warn(io, message) {
321
+ out(io, { systemMessage: message });
322
+ }
323
+ function respond(io, client, result, summary, approveCommand) {
324
+ if (result.decision === "deny" && result.error) {
325
+ const e = result.error;
326
+ const reason = JSON.stringify(e);
327
+ const id = result.approvalId ? `, approval ${result.approvalId}` : "";
328
+ const approve = e.code === "APPROVAL_REQUIRED" && result.approvalId ? ` To let this exact command run once: ${approveCommand(result.approvalId)}` : "";
329
+ const systemMessage = `agentguard refused "${summary.slice(0, 120)}" (${e.code}${id}): ${e.cause}${approve}`;
330
+ if (client === "gemini-cli") out(io, { decision: "deny", reason, systemMessage });
331
+ else
332
+ out(io, {
333
+ hookSpecificOutput: {
334
+ hookEventName: "PreToolUse",
335
+ permissionDecision: "deny",
336
+ permissionDecisionReason: reason
337
+ },
338
+ systemMessage
339
+ });
340
+ return;
341
+ }
342
+ if (result.would) {
343
+ out(io, {
344
+ systemMessage: `agentguard (hooks.mode: dry-run) would have stopped "${summary.slice(0, 120)}" with ${result.would.code}: ${result.would.cause} \u2014 it ran; nothing is blocked until hooks.mode is enforce`
345
+ });
346
+ }
347
+ }
348
+ async function hookCommand(args, io) {
349
+ const event = args.positionals[0];
350
+ if (event !== "pre-tool-use") {
351
+ io.err(
352
+ "usage: agentguard hook pre-tool-use [--client claude|codex|gemini] [--config agentguard.yaml] [--fallback-config path]\n run by a coding agent's pre-execution hook, with the hook's JSON on stdin; `agentguard hooks install` sets it up"
353
+ );
354
+ return 1;
355
+ }
356
+ const flagClient = flagString(args, "client");
357
+ let client = flagClient && CLIENT_ALIASES[flagClient] || "claude-code";
358
+ try {
359
+ const raw = await io.stdin?.() ?? "";
360
+ const input = JSON.parse(raw);
361
+ if (!flagClient) client = detectHookClient(input);
362
+ const inputCwd = typeof input.cwd === "string" ? input.cwd : void 0;
363
+ const policyRef = resolveHookPolicy(args, io, inputCwd);
364
+ if (!policyRef) return 0;
365
+ const loaded = loadPolicyFile(policyRef.path, { env: io.env, allowMissingEnv: true });
366
+ const action = hookActionFromInput(client, input, inputCwd ?? io.cwd);
367
+ if (!action) return 0;
368
+ const home = homeFrom(io);
369
+ const workspace = projectDir(io, void 0) ?? (policyRef.found === "project" ? dirname(policyRef.path) : void 0) ?? action.cwd;
370
+ const tmpDirs = [
371
+ tmpdir(),
372
+ io.env.TMPDIR,
373
+ typeof input.scratchpad_dir === "string" ? input.scratchpad_dir : void 0,
374
+ "/tmp",
375
+ "/private/tmp",
376
+ "/var/folders",
377
+ "/private/var/folders"
378
+ ].filter((d) => Boolean(d));
379
+ const ctx = {
380
+ workspace: resolve(workspace),
381
+ home,
382
+ tmpDirs,
383
+ stateDir: loaded.stateDir,
384
+ guardFiles: [loaded.path, ...hookSettingsFiles({ cwd: resolve(workspace), home })]
385
+ };
386
+ const assessment = assessHookAction(loaded.policy, action, ctx);
387
+ const reg = readRegistration(loaded);
388
+ const { guard } = createFileGuard(loaded, {
389
+ env: io.env,
390
+ approvalUrl: approvalUrlFor(reg),
391
+ log: (line) => io.err(line)
392
+ });
393
+ const configHint = policyRef.found === "project" && dirname(policyRef.path) === resolve(workspace) ? "" : ` --config ${JSON.stringify(policyRef.path)}`;
394
+ const approveCommand = (id) => `npx @agentwares/agentguard approve ${id}${configHint}`;
395
+ const result = await guard.preflight({
396
+ action,
397
+ assessment,
398
+ runId: `${RUN_PREFIX[client]}_${action.sessionId}`,
399
+ humanTurn: () => client === "claude-code" ? humanTurnFromTranscript(action.transcriptPath, {
400
+ requireOrigin: loaded.policy.hooks.irreversible.require_origin
401
+ }) : {
402
+ missing: `${client} transcripts are not a verified format for this check, so an irreversible command needs \`agentguard approve\``
403
+ },
404
+ approveCommand
405
+ });
406
+ respond(io, client, result, assessment.summary, approveCommand);
407
+ return 0;
408
+ } catch (err) {
409
+ warn(
410
+ io,
411
+ `agentguard hook error \u2014 this call was NOT checked: ${describeError(err).replace(/\s+/g, " ").slice(0, 400)}`
412
+ );
413
+ return 0;
414
+ }
415
+ }
416
+
417
+ // src/commands/keys.ts
418
+ import { readFileSync, writeFileSync } from "fs";
419
+ import YAML, { isMap, isSeq, YAMLSeq } from "yaml";
420
+ import { generateAgentKey, hashAgentKey } from "@agentwares/agentguard-core";
421
+ async function keyCommand(args, io) {
422
+ const [sub, name] = args.positionals;
423
+ const path = policyPathFrom(args, io);
424
+ const doc = YAML.parseDocument(readFileSync(path, "utf8"));
425
+ let agents = doc.get("agents");
426
+ if (!isSeq(agents)) {
427
+ agents = new YAMLSeq();
428
+ doc.set("agents", agents);
429
+ }
430
+ const seq = agents;
431
+ const items = seq.items.filter(isMap);
432
+ if (sub === "list" || sub === void 0) {
433
+ if (items.length === 0)
434
+ io.out(
435
+ "no agents in agentguard.yaml \u2014 create one: agentguard key create <name> --allow 'crm_get_*'"
436
+ );
437
+ for (const a of items) {
438
+ const allow2 = a.get("allow");
439
+ io.out(
440
+ `${String(a.get("name")).padEnd(20)} key: ${a.get("key_hash") ? "set" : "none (stdio --agent only)"} allow: ${isSeq(allow2) ? allow2.items.map(String).join(", ") : "all"}`
441
+ );
442
+ }
443
+ return 0;
444
+ }
445
+ if (!name) {
446
+ io.err(
447
+ "usage: agentguard key create <agent> [--allow pattern]... [--deny pattern]... [--writes n] [--spend n] [--deletes n] [--emails n] [--tool-calls n] [--mode dry-run|enforce] [--upstream name]..."
448
+ );
449
+ return 1;
450
+ }
451
+ if (sub === "revoke") {
452
+ const idx = items.findIndex((a) => a.get("name") === name);
453
+ if (idx < 0) {
454
+ io.err(`no agent "${name}"`);
455
+ return 1;
456
+ }
457
+ seq.items.splice(seq.items.indexOf(items[idx]), 1);
458
+ writeFileSync(path, doc.toString());
459
+ io.out(`revoked ${name}: removed from agentguard.yaml (running proxies pick it up on restart)`);
460
+ return 0;
461
+ }
462
+ if (sub !== "create") {
463
+ io.err(`unknown subcommand "${sub}" \u2014 use create, list or revoke`);
464
+ return 1;
465
+ }
466
+ const key = generateAgentKey();
467
+ const keyHash = await hashAgentKey(key);
468
+ const existing = items.find((a) => a.get("name") === name);
469
+ const entry = { name, key_hash: keyHash };
470
+ const allow = flagList(args, "allow");
471
+ const deny = flagList(args, "deny");
472
+ if (allow.length) entry.allow = allow;
473
+ if (deny.length) entry.deny = deny;
474
+ const upstreams = flagList(args, "upstream");
475
+ if (upstreams.length) entry.upstreams = upstreams;
476
+ const mode = flagString(args, "mode");
477
+ if (mode) entry.mode = mode;
478
+ const perRun = {};
479
+ for (const [flag, counter] of [
480
+ ["writes", "writes"],
481
+ ["spend", "spend_usd"],
482
+ ["deletes", "deletes"],
483
+ ["emails", "emails"],
484
+ ["tool-calls", "tool_calls"]
485
+ ]) {
486
+ const n = flagNumber(args, flag);
487
+ if (n !== void 0) perRun[counter] = n;
488
+ }
489
+ if (Object.keys(perRun).length) entry.caps = { per_run: perRun };
490
+ if (existing) {
491
+ for (const [k, v] of Object.entries(entry)) existing.set(k, v);
492
+ } else {
493
+ seq.add(doc.createNode(entry));
494
+ }
495
+ writeFileSync(path, doc.toString());
496
+ io.err(
497
+ `${existing ? "rotated" : "created"} agent "${name}" in ${path}${allow.length ? ` (allow: ${allow.join(", ")})` : " (allow: all tools)"}`
498
+ );
499
+ io.err(
500
+ "the key is shown once; give it to the agent as `Authorization: Bearer <key>` (HTTP) \u2014 over stdio use `agentguard proxy --agent " + name + "`"
501
+ );
502
+ io.out(key);
503
+ return 0;
504
+ }
505
+
506
+ // src/commands/permission-diff.ts
507
+ import { execFileSync } from "child_process";
508
+ import {
509
+ diffPermissionFile,
510
+ renderPermissionDiffMarkdown
511
+ } from "@agentwares/agentguard-core";
512
+ var DEFAULT_PERMISSION_PATHS = [
513
+ "agentguard.yaml",
514
+ "agentguard.yml",
515
+ ".claude/settings.json",
516
+ ".claude/settings.local.json",
517
+ ".mcp.json",
518
+ "mcp.json",
519
+ ".cursor/mcp.json",
520
+ ".vscode/mcp.json"
521
+ ];
522
+ var PERMISSION_FILE = /(^|\/)(agentguard[^/]*\.ya?ml|\.claude\/settings(\.local)?\.json|[^/]*mcp[^/]*\.json|claude_desktop_config\.json)$/;
523
+ function git(cwd, argv) {
524
+ try {
525
+ return execFileSync("git", argv, {
526
+ cwd,
527
+ encoding: "utf8",
528
+ stdio: ["ignore", "pipe", "ignore"]
529
+ });
530
+ } catch {
531
+ return void 0;
532
+ }
533
+ }
534
+ function showAt(cwd, ref, path) {
535
+ return git(cwd, ["show", `${ref}:${path}`]);
536
+ }
537
+ function changedPermissionFiles(cwd, base, head) {
538
+ const out2 = git(cwd, ["diff", "--name-only", base, head]) ?? "";
539
+ return out2.split("\n").map((l) => l.trim()).filter((l) => l && PERMISSION_FILE.test(l));
540
+ }
541
+ function resolveBase(cwd, explicit) {
542
+ if (explicit) return explicit;
543
+ for (const candidate of ["origin/main", "main", "origin/master", "master"]) {
544
+ if (git(cwd, ["rev-parse", "--verify", "--quiet", candidate]) !== void 0) return candidate;
545
+ }
546
+ return "HEAD~1";
547
+ }
548
+ function runPermissionDiff(cwd, opts) {
549
+ const base = resolveBase(cwd, opts.base);
550
+ const head = opts.head ?? "HEAD";
551
+ const files = [
552
+ .../* @__PURE__ */ new Set([
553
+ ...opts.paths ?? DEFAULT_PERMISSION_PATHS,
554
+ ...changedPermissionFiles(cwd, base, head)
555
+ ])
556
+ ];
557
+ const findings = [];
558
+ const touched = [];
559
+ for (const file of files) {
560
+ const before = showAt(cwd, base, file);
561
+ const after = showAt(cwd, head, file);
562
+ if (before === void 0 && after === void 0) continue;
563
+ if (before === after) continue;
564
+ touched.push(file);
565
+ findings.push(...diffPermissionFile(file, before, after));
566
+ }
567
+ return {
568
+ base,
569
+ head,
570
+ files: touched,
571
+ findings,
572
+ markdown: renderPermissionDiffMarkdown(findings, { title: opts.title })
573
+ };
574
+ }
575
+ async function permissionDiffCommand(args, io) {
576
+ const paths = flagList(args, "paths");
577
+ const result = runPermissionDiff(io.cwd, {
578
+ base: flagString(args, "base"),
579
+ head: flagString(args, "head"),
580
+ paths: paths.length ? paths : void 0
581
+ });
582
+ const format = flagString(args, "format") ?? (flagBool(args, "json") ? "json" : "markdown");
583
+ if (format === "json") io.out(JSON.stringify(result, null, 2));
584
+ else io.out(result.markdown);
585
+ const widen = result.findings.filter((f) => f.severity === "widen").length;
586
+ if (flagBool(args, "fail-on-widen") && widen > 0) return 1;
587
+ return 0;
588
+ }
589
+
590
+ // src/commands/report.ts
591
+ import { resolve as resolve2 } from "path";
592
+ import {
593
+ buildReport,
594
+ latestRunId,
595
+ listRuns,
596
+ renderMutationDiff,
597
+ renderReportMarkdown
598
+ } from "@agentwares/agentguard-core";
599
+ import { readAuditFile, verifyAuditFile } from "@agentwares/agentguard-core/node";
600
+ function pickRun(args, entries) {
601
+ if (flagBool(args, "all")) return void 0;
602
+ return flagString(args, "run") ?? latestRunId(entries);
603
+ }
604
+ async function reportCommand(args, io) {
605
+ const loaded = loadPolicyFor(args, io, { allowMissingEnv: true });
606
+ const entries = readAuditFile(loaded.auditPath);
607
+ const runId = pickRun(args, entries);
608
+ const report = buildReport(entries, { runId });
609
+ if (flagBool(args, "json")) {
610
+ io.out(
611
+ JSON.stringify(
612
+ {
613
+ ...report,
614
+ mutations: report.mutations.map((m) => ({
615
+ seq: m.seq,
616
+ ts: m.ts,
617
+ tool: m.tool,
618
+ mutation: m.mutation
619
+ }))
620
+ },
621
+ null,
622
+ 2
623
+ )
624
+ );
625
+ return 0;
626
+ }
627
+ io.out(renderReportMarkdown(report));
628
+ const chain = await verifyAuditFile(loaded.auditPath);
629
+ io.out(
630
+ chain.ok ? `Audit chain: ${chain.entries} entries, verified (${loaded.auditPath}).` : `Audit chain BROKEN at entry ${chain.brokenAt}: ${chain.reason}`
631
+ );
632
+ if (runId && listRuns(entries).length > 1) io.out(`Other runs: agentguard report --all`);
633
+ return 0;
634
+ }
635
+ async function diffCommand(args, io) {
636
+ const loaded = loadPolicyFor(args, io, { allowMissingEnv: true });
637
+ const entries = readAuditFile(loaded.auditPath);
638
+ const runId = pickRun(args, entries);
639
+ io.out(renderMutationDiff(entries, { runId }).trimEnd());
640
+ return 0;
641
+ }
642
+ async function verifyCommand(args, io) {
643
+ let path = args.positionals[0];
644
+ if (!path) path = loadPolicyFor(args, io, { allowMissingEnv: true }).auditPath;
645
+ path = resolve2(io.cwd, path);
646
+ const result = await verifyAuditFile(path);
647
+ if (flagBool(args, "json")) io.out(JSON.stringify({ path, ...result }));
648
+ else if (result.ok)
649
+ io.out(
650
+ `ok: ${result.entries} entries, chain intact, head ${result.head?.slice(0, 16)}\u2026 (${path})`
651
+ );
652
+ else io.out(`BROKEN at entry ${result.brokenAt}: ${result.reason} (${path})`);
653
+ return result.ok ? 0 : 1;
654
+ }
655
+ async function statusCommand(args, io) {
656
+ const loaded = loadPolicyFor(args, io, { allowMissingEnv: true });
657
+ const { guard, kill } = createFileGuard(loaded, { alerts: false, env: io.env });
658
+ const entries = readAuditFile(loaded.auditPath);
659
+ const runId = flagString(args, "run") ?? latestRunId(entries) ?? "(none yet)";
660
+ const status = await guard.status(runId);
661
+ const killed = kill.check();
662
+ const reg = readRegistration(loaded);
663
+ const pending = await guard.approvals.list("pending");
664
+ if (flagBool(args, "json")) {
665
+ io.out(
666
+ JSON.stringify(
667
+ {
668
+ policy: loaded.path,
669
+ mode: loaded.policy.mode,
670
+ runId,
671
+ ...status,
672
+ killed,
673
+ pendingApprovals: pending.length,
674
+ http: reg ? { url: reg.url, pid: reg.pid } : void 0
675
+ },
676
+ null,
677
+ 2
678
+ )
679
+ );
680
+ return 0;
681
+ }
682
+ io.out(
683
+ `policy ${loaded.path} (mode: ${loaded.policy.mode}, ${loaded.policy.upstreams.length} upstreams)`
684
+ );
685
+ io.out(
686
+ `kill ${killed.killed ? `ON \u2014 ${killed.reason} (agentguard resume to clear)` : "off"}`
687
+ );
688
+ io.out(
689
+ `http ${reg ? `${reg.url} (pid ${reg.pid})` : "not running (agentguard proxy --http)"}`
690
+ );
691
+ io.out(`audit ${entries.length} entries in ${loaded.auditPath}`);
692
+ io.out(
693
+ `pending ${pending.length} approval${pending.length === 1 ? "" : "s"}${pending.length ? ` \u2014 agentguard approvals` : ""}`
694
+ );
695
+ io.out(`run ${runId}`);
696
+ const fmt = (scope) => Object.entries(status.caps[scope]).map(([k, limit]) => `${k} ${status.usage[scope][k] ?? 0}/${limit}`).join(" ") || "no caps";
697
+ io.out(` per_run ${fmt("per_run")}`);
698
+ io.out(` per_day ${fmt("per_day")}`);
699
+ return 0;
700
+ }
701
+
702
+ // src/cli.ts
703
+ var proxyCommand = async (args, io) => (await import("./proxy-3Z5YEZ4R.js")).proxyCommand(args, io);
704
+ var initCommand = async (args, io) => (await import("./init-CWROGV5I.js")).initCommand(args, io);
705
+ var toolsCommand = async (args, io) => (await import("./tools-3LLDG3XQ.js")).toolsCommand(args, io);
706
+ var auditCommand = async (args, io) => (await import("./audit-2YCMHMCP.js")).auditCommand(args, io);
707
+ var HELP = `agentguard \u2014 MCP policy proxy for agents that touch production
708
+
709
+ npx @agentwares/agentguard init read your MCP config, write agentguard.yaml (dry-run), route every server through the proxy
710
+ agentguard report [--run id|--all] what this run did / would have destroyed / spent, where it was halted
711
+ agentguard diff [--run id] record-by-record mutation diff from a dry run
712
+ agentguard verify [audit.jsonl] prove the hash-chained audit log was not edited
713
+ agentguard connect <key> [--write] point this machine at your hosted proxy (paid tiers)
714
+
715
+ agentguard hooks install [--project|--user] [--client claude|codex|gemini|all]
716
+ check a coding agent's own shell commands and file edits against agentguard.yaml (dry-run first);
717
+ irreversible commands (push, merge, publish, send, rm outside the repo) only on a turn a person typed
718
+ agentguard hooks uninstall | status
719
+ agentguard hook pre-tool-use what the installed hook runs (reads the harness's JSON on stdin)
720
+
721
+ agentguard audit [--since 7d|30d|all] [--client claude|codex|all] [--json]
722
+ which rules in CLAUDE.md / AGENTS.md / GEMINI.md / .cursor/rules your coding agents broke,
723
+ from this machine's Claude Code and Codex sessions in this repo (nothing is sent anywhere)
724
+ agentguard audit --enforce [--preview]
725
+ write the checkable rules into agentguard.yaml as hook rules, in dry-run, and install the hook
726
+
727
+ agentguard proxy [--http --port 8788] [--agent name] [--run-id id] [--mode dry-run|enforce]
728
+ agentguard tools [--json] the tools your agent will see, with class and why
729
+ agentguard status [--run id] counters vs caps, kill state, pending approvals
730
+
731
+ agentguard kill [reason] halt every run now (file + running HTTP proxy); agentguard resume
732
+ agentguard approvals [--all] pending approvals; agentguard approve <id> | deny <id> [--note text]
733
+ agentguard key create <agent> [--allow 'crm_get_*']... [--deny p] [--writes n] [--spend n] [--mode m]
734
+ agentguard key list | revoke <agent>
735
+ agentguard permission-diff [--base ref] [--head ref] [--paths a,b] [--format markdown|json] [--fail-on-widen]
736
+
737
+ Options: --config <agentguard.yaml> (or AGENTGUARD_CONFIG), --json, --help, --version
738
+ Docs: https://github.com/agentwares/agentguard/tree/main/apps/agentguard-cli`;
739
+ async function main(argv, io = defaultIo()) {
740
+ const args = parseArgs(argv);
741
+ if (args.flags.version) {
742
+ io.out(version());
743
+ return 0;
744
+ }
745
+ if (args.command === void 0 && !args.flags.help && io.tty === false) {
746
+ try {
747
+ return await proxyCommand(args, io);
748
+ } catch (err) {
749
+ io.err(describeError(err));
750
+ return 1;
751
+ }
752
+ }
753
+ if (args.flags.help || args.command === void 0 || args.command === "help") {
754
+ io.out(HELP);
755
+ return args.command === void 0 && !args.flags.help ? 1 : 0;
756
+ }
757
+ try {
758
+ switch (args.command) {
759
+ case "init":
760
+ return await initCommand(args, io);
761
+ case "proxy":
762
+ case "serve":
763
+ return await proxyCommand(args, io);
764
+ case "report":
765
+ return await reportCommand(args, io);
766
+ case "diff":
767
+ return await diffCommand(args, io);
768
+ case "verify":
769
+ return await verifyCommand(args, io);
770
+ case "connect":
771
+ return await connectCommand(args, io);
772
+ case "status":
773
+ return await statusCommand(args, io);
774
+ case "tools":
775
+ return await toolsCommand(args, io);
776
+ case "kill":
777
+ return await killCommand(args, io);
778
+ case "resume":
779
+ return await resumeCommand(args, io);
780
+ case "approve":
781
+ return await decideCommand("approved", args, io);
782
+ case "deny":
783
+ return await decideCommand("denied", args, io);
784
+ case "approvals":
785
+ return await approvalsCommand(args, io);
786
+ case "key":
787
+ case "keys":
788
+ return await keyCommand(args, io);
789
+ case "permission-diff":
790
+ return await permissionDiffCommand(args, io);
791
+ case "hook":
792
+ return await hookCommand(args, io);
793
+ case "hooks":
794
+ return await hooksCommand(args, io);
795
+ case "audit":
796
+ return await auditCommand(args, io);
797
+ default:
798
+ io.err(`unknown command "${args.command}"
799
+ `);
800
+ io.err(HELP);
801
+ return 1;
802
+ }
803
+ } catch (err) {
804
+ io.err(describeError(err));
805
+ return 1;
806
+ }
807
+ }
808
+ var isMain = process.argv[1] !== void 0 && /(^|\/)(cli\.[cm]?[jt]s|agentguard)$/.test(process.argv[1]);
809
+ if (isMain) {
810
+ main(process.argv.slice(2)).then(
811
+ (code) => {
812
+ if (code !== 0) process.exitCode = code;
813
+ },
814
+ (err) => {
815
+ process.stderr.write(`${describeError(err)}
816
+ `);
817
+ process.exitCode = 1;
818
+ }
819
+ );
820
+ }
821
+
822
+ export {
823
+ DEFAULT_PERMISSION_PATHS,
824
+ runPermissionDiff,
825
+ HELP,
826
+ main
827
+ };
828
+ //# sourceMappingURL=chunk-6TUQD666.js.map