vigiles 6.0.0 → 8.0.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.
Files changed (92) hide show
  1. package/README.md +189 -88
  2. package/dist/action-gate.js +1 -1
  3. package/dist/adapters/claude-code/agent-runtime.d.ts +46 -11
  4. package/dist/adapters/claude-code/agent-runtime.js +95 -24
  5. package/dist/adapters/claude-code/effect-region.js +1 -1
  6. package/dist/adapters/claude-code/skill-runtime.d.ts +1 -1
  7. package/dist/adapters/claude-code/skill-runtime.js +1 -1
  8. package/dist/adapters/codex/hook-protocol.js +3 -0
  9. package/dist/adapters/codex/mock-model.js +1 -1
  10. package/dist/cli-commands.d.ts +19 -0
  11. package/dist/cli-commands.js +47 -0
  12. package/dist/cli.d.ts +1 -1
  13. package/dist/cli.js +1054 -201
  14. package/dist/core/adopt.d.ts +65 -0
  15. package/dist/core/adopt.js +199 -0
  16. package/dist/core/bash-effects.d.ts +12 -0
  17. package/dist/core/bash-effects.js +31 -0
  18. package/dist/core/capability-diff.d.ts +46 -0
  19. package/dist/core/capability-diff.js +97 -0
  20. package/dist/core/compose.d.ts +1 -1
  21. package/dist/core/compose.js +1 -1
  22. package/dist/core/evolve.d.ts +4 -0
  23. package/dist/core/evolve.js +4 -0
  24. package/dist/core/frontmatter.d.ts +8 -7
  25. package/dist/core/frontmatter.js +8 -7
  26. package/dist/core/generate-harness.d.ts +1 -1
  27. package/dist/core/generate-harness.js +3 -3
  28. package/dist/core/generate-schema.js +1 -1
  29. package/dist/core/guards.d.ts +126 -0
  30. package/dist/core/guards.js +309 -0
  31. package/dist/core/harness-driver.d.ts +1 -1
  32. package/dist/core/hook-program.d.ts +459 -0
  33. package/dist/core/hook-program.js +468 -0
  34. package/dist/core/hook-protocol.d.ts +7 -0
  35. package/dist/core/hook-providers.d.ts +138 -0
  36. package/dist/core/hook-providers.js +155 -0
  37. package/dist/core/hook-spec.d.ts +74 -0
  38. package/dist/core/hook-spec.js +130 -0
  39. package/dist/core/inline.d.ts +6 -6
  40. package/dist/core/inline.js +7 -7
  41. package/dist/core/integrity.d.ts +31 -0
  42. package/dist/core/integrity.js +45 -0
  43. package/dist/core/mcp-tool.d.ts +12 -0
  44. package/dist/core/mcp-tool.js +20 -0
  45. package/dist/core/mcp.d.ts +13 -0
  46. package/dist/core/mcp.js +67 -0
  47. package/dist/core/orphans.js +1 -1
  48. package/dist/core/spec.d.ts +40 -2
  49. package/dist/core/spec.js +16 -1
  50. package/dist/core/types.d.ts +37 -5
  51. package/dist/core/validate.js +26 -26
  52. package/dist/dialect-drift.d.ts +65 -0
  53. package/dist/dialect-drift.js +216 -0
  54. package/dist/eval.d.ts +40 -5
  55. package/dist/eval.js +59 -5
  56. package/dist/guardrail-check.d.ts +85 -0
  57. package/dist/guardrail-check.js +152 -0
  58. package/dist/harness-assert.d.ts +10 -0
  59. package/dist/harness-assert.js +30 -0
  60. package/dist/hook-install.d.ts +43 -0
  61. package/dist/hook-install.js +91 -0
  62. package/dist/hook.d.ts +52 -0
  63. package/dist/hook.js +98 -0
  64. package/dist/leaderboard.d.ts +6 -0
  65. package/dist/leaderboard.js +43 -1
  66. package/dist/linting.d.ts +9 -5
  67. package/dist/linting.js +17 -5
  68. package/dist/optimize.js +1 -1
  69. package/dist/scaffold-test.js +21 -7
  70. package/dist/scan-behavioral.d.ts +60 -0
  71. package/dist/scan-behavioral.js +239 -1
  72. package/dist/scan-trigger-suggest.d.ts +54 -0
  73. package/dist/scan-trigger-suggest.js +70 -0
  74. package/dist/scan.d.ts +31 -1
  75. package/dist/scan.js +65 -3
  76. package/dist/score-explainer.js +1 -1
  77. package/dist/self-command-refs.d.ts +21 -0
  78. package/dist/self-command-refs.js +125 -0
  79. package/dist/setup-plan.d.ts +59 -1
  80. package/dist/setup-plan.js +103 -5
  81. package/dist/testing.d.ts +5 -3
  82. package/dist/testing.js +37 -23
  83. package/dist/tool-intercept.d.ts +4 -4
  84. package/dist/tool-intercept.js +5 -5
  85. package/dist/unit.d.ts +2 -0
  86. package/dist/unit.js +8 -1
  87. package/hooks/post-edit.sh +1 -1
  88. package/hooks/refs-nudge.sh +1 -1
  89. package/package.json +5 -3
  90. package/skills/adopt-spec/SKILL.md +7 -7
  91. package/skills/linter-docs/eslint.md +1 -1
  92. package/skills/strengthen/SKILL.md +1 -1
@@ -0,0 +1,126 @@
1
+ /**
2
+ * EXPERIMENTAL prototype — typed, safe-by-construction harness GUARDS.
3
+ *
4
+ * Dogfood finding (real OSS hooks: our pre-edit.sh, superpowers/OMC session-start,
5
+ * OMC keyword-detector): a "hook" today is an arbitrary shell command in
6
+ * settings.json — which is BOTH the enforcement vehicle AND the RCE footgun
7
+ * (CVE-2025-59536: a malicious repo's hook runs before the trust dialog). Arbitrary
8
+ * hook safety is UNDECIDABLE (Rice). So this prototype inverts it: you don't WRITE a
9
+ * hook, you DECLARE a guard from a closed, audited vocabulary, and vigiles GENERATES
10
+ * the hooks block — whose command is vigiles's OWN gate (`vigiles hook-runtime guard`), never
11
+ * user shell. Safe-by-construction, not safe-by-analysis.
12
+ *
13
+ * Covers the real PreToolUse patterns + the new ORDER axis:
14
+ * - block: deny a tool call matching args (reproduces pre-edit.sh's intent)
15
+ * - requireBefore: ORDER — deny a tool call until a prerequisite call has fired
16
+ * (`terraform destroy` only after `terraform plan`; the moat)
17
+ * - confine: deny a path-taking tool whose path escapes an allowlist (rm -rf /)
18
+ *
19
+ * EXPERIMENTAL — not on the public API. But the gate now RUNS end-to-end: the
20
+ * `vigiles hook-runtime guard` CLI subcommand reads the live PreToolUse event, loads the
21
+ * guard set (`.vigiles/guards.json`) + the session ledger (`.vigiles/guard-ledger.json`,
22
+ * the reconstructed prior-call list `requireBefore` needs), runs `decideGuards`, and
23
+ * blocks (exit 2 + reason) or records-the-allowed-call (so the next call sees it). The
24
+ * pure `decideGuards` is the decision; `compileGuards` is the generator; the
25
+ * serialization + ledger below are the IO seam the CLI calls.
26
+ * See research/harness-protocol-flow-moat.md.
27
+ */
28
+ import { type ArgMatcher } from "../arg-match.js";
29
+ /** A tool-call shape: a tool name + an optional argument matcher (the `when`). */
30
+ export interface ToolPattern {
31
+ readonly tool: string;
32
+ readonly when?: ArgMatcher;
33
+ }
34
+ export type Guard = {
35
+ readonly kind: "block";
36
+ readonly target: ToolPattern;
37
+ readonly reason: string;
38
+ } | {
39
+ readonly kind: "requireBefore";
40
+ readonly target: ToolPattern;
41
+ /** The prerequisite call that must have fired earlier this session. */
42
+ readonly prerequisite: ToolPattern;
43
+ readonly reason?: string;
44
+ } | {
45
+ readonly kind: "confine";
46
+ readonly tools: readonly string[];
47
+ /** Dot-path to the path argument (default `file_path`). */
48
+ readonly pathKey?: string;
49
+ /** Allowed path prefixes (a call outside ALL of them is denied). */
50
+ readonly allow: readonly string[];
51
+ readonly reason?: string;
52
+ };
53
+ /** Ergonomic builders for the closed vocabulary. */
54
+ export declare const guard: {
55
+ block: (target: ToolPattern, reason: string) => Guard;
56
+ requireBefore: (target: ToolPattern, prerequisite: ToolPattern, reason?: string) => Guard;
57
+ confine: (tools: readonly string[], allow: readonly string[], reason?: string, pathKey?: string) => Guard;
58
+ };
59
+ /** A tool call as seen at PreToolUse (and as recorded in the prior-call ledger). */
60
+ export interface ToolEvent {
61
+ readonly tool: string;
62
+ readonly input: unknown;
63
+ }
64
+ export interface GuardDecision {
65
+ readonly allow: boolean;
66
+ /** Set on a deny — the message surfaced to the agent. */
67
+ readonly reason?: string;
68
+ }
69
+ /**
70
+ * The pure runtime gate: decide allow/deny for `event`, given the guards and the
71
+ * calls that already fired this session (`prior`, oldest-first — the ledger the
72
+ * `guard-hook` CLI reconstructs from the transcript). First match wins a deny.
73
+ */
74
+ export declare function decideGuards(guards: readonly Guard[], event: ToolEvent, prior?: readonly ToolEvent[]): GuardDecision;
75
+ /** Every tool name a guard set gates (the PreToolUse matcher union). */
76
+ export declare function guardedTools(guards: readonly Guard[]): string[];
77
+ /** A Claude Code settings `hooks` block (the generated artifact). */
78
+ export interface HooksConfig {
79
+ readonly hooks: {
80
+ readonly PreToolUse: readonly {
81
+ readonly matcher: string;
82
+ readonly hooks: readonly {
83
+ readonly type: "command";
84
+ readonly command: string;
85
+ }[];
86
+ }[];
87
+ };
88
+ }
89
+ /**
90
+ * Generate the hooks block for a guard set. The command is vigiles's OWN gate
91
+ * (default `npx vigiles hook-runtime guard`), NOT user shell — so the generated
92
+ * enforcement is safe-by-construction and a repo can't smuggle an arbitrary RCE
93
+ * hook. The gate reads the same guard set + the live event and runs `decideGuards`.
94
+ */
95
+ export declare function compileGuards(guards: readonly Guard[], gateCommand?: string): HooksConfig;
96
+ /** Serialize a guard set for `.vigiles/guards.json` (RegExp matchers preserved). */
97
+ export declare function serializeGuards(guards: readonly Guard[]): string;
98
+ /** Parse a guard set from JSON (tolerant — a malformed guard is skipped). */
99
+ export declare function parseGuards(json: string): Guard[];
100
+ /** Load the declared guard set from `.vigiles/guards.json` (absent → none). */
101
+ export declare function loadGuards(cwd: string): Guard[];
102
+ /**
103
+ * The prior-call ledger — the calls already allowed this session, oldest-first.
104
+ * `requireBefore` reads it to know whether a prerequisite ran. Claude Code doesn't
105
+ * surface call history to a hook, so vigiles records each allowed call itself
106
+ * (mirrors `.vigiles/active-agent.json`).
107
+ */
108
+ export declare function readGuardLedger(cwd: string): ToolEvent[];
109
+ /** Append an allowed call to the session ledger. */
110
+ export declare function recordGuardCall(cwd: string, event: ToolEvent): void;
111
+ /** Parse a PreToolUse event JSON (the hook's stdin) into a {@link ToolEvent}. */
112
+ export declare function parseGuardEvent(rawJson: string): ToolEvent | null;
113
+ /** The outcome of running the gate against one event. */
114
+ export interface GuardHookOutcome {
115
+ readonly decision: GuardDecision;
116
+ /** True iff the allowed call was recorded to the ledger. */
117
+ readonly recorded: boolean;
118
+ }
119
+ /**
120
+ * The runnable gate, decoupled from process/exit so it's testable. Decides the
121
+ * event against the loaded guards + the prior-call ledger; on ALLOW it records the
122
+ * call (so a later `requireBefore` sees it) and on DENY it records nothing (a
123
+ * blocked call never happened). Malformed/absent event → allow, record nothing.
124
+ */
125
+ export declare function runGuardHook(cwd: string, rawJson: string): GuardHookOutcome;
126
+ //# sourceMappingURL=guards.d.ts.map
@@ -0,0 +1,309 @@
1
+ "use strict";
2
+ /**
3
+ * EXPERIMENTAL prototype — typed, safe-by-construction harness GUARDS.
4
+ *
5
+ * Dogfood finding (real OSS hooks: our pre-edit.sh, superpowers/OMC session-start,
6
+ * OMC keyword-detector): a "hook" today is an arbitrary shell command in
7
+ * settings.json — which is BOTH the enforcement vehicle AND the RCE footgun
8
+ * (CVE-2025-59536: a malicious repo's hook runs before the trust dialog). Arbitrary
9
+ * hook safety is UNDECIDABLE (Rice). So this prototype inverts it: you don't WRITE a
10
+ * hook, you DECLARE a guard from a closed, audited vocabulary, and vigiles GENERATES
11
+ * the hooks block — whose command is vigiles's OWN gate (`vigiles hook-runtime guard`), never
12
+ * user shell. Safe-by-construction, not safe-by-analysis.
13
+ *
14
+ * Covers the real PreToolUse patterns + the new ORDER axis:
15
+ * - block: deny a tool call matching args (reproduces pre-edit.sh's intent)
16
+ * - requireBefore: ORDER — deny a tool call until a prerequisite call has fired
17
+ * (`terraform destroy` only after `terraform plan`; the moat)
18
+ * - confine: deny a path-taking tool whose path escapes an allowlist (rm -rf /)
19
+ *
20
+ * EXPERIMENTAL — not on the public API. But the gate now RUNS end-to-end: the
21
+ * `vigiles hook-runtime guard` CLI subcommand reads the live PreToolUse event, loads the
22
+ * guard set (`.vigiles/guards.json`) + the session ledger (`.vigiles/guard-ledger.json`,
23
+ * the reconstructed prior-call list `requireBefore` needs), runs `decideGuards`, and
24
+ * blocks (exit 2 + reason) or records-the-allowed-call (so the next call sees it). The
25
+ * pure `decideGuards` is the decision; `compileGuards` is the generator; the
26
+ * serialization + ledger below are the IO seam the CLI calls.
27
+ * See research/harness-protocol-flow-moat.md.
28
+ */
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.guard = void 0;
31
+ exports.decideGuards = decideGuards;
32
+ exports.guardedTools = guardedTools;
33
+ exports.compileGuards = compileGuards;
34
+ exports.serializeGuards = serializeGuards;
35
+ exports.parseGuards = parseGuards;
36
+ exports.loadGuards = loadGuards;
37
+ exports.readGuardLedger = readGuardLedger;
38
+ exports.recordGuardCall = recordGuardCall;
39
+ exports.parseGuardEvent = parseGuardEvent;
40
+ exports.runGuardHook = runGuardHook;
41
+ const node_fs_1 = require("node:fs");
42
+ const node_path_1 = require("node:path");
43
+ const arg_match_js_1 = require("../arg-match.js");
44
+ /** Ergonomic builders for the closed vocabulary. */
45
+ exports.guard = {
46
+ block: (target, reason) => ({
47
+ kind: "block",
48
+ target,
49
+ reason,
50
+ }),
51
+ requireBefore: (target, prerequisite, reason) => ({ kind: "requireBefore", target, prerequisite, reason }),
52
+ confine: (tools, allow, reason, pathKey) => ({ kind: "confine", tools, allow, reason, pathKey }),
53
+ };
54
+ const ALLOW = { allow: true };
55
+ const deny = (reason) => ({ allow: false, reason });
56
+ const matchesPattern = (e, p) => e.tool === p.tool && (p.when === undefined || (0, arg_match_js_1.matchesArgs)(e.input, p.when));
57
+ /** A path is confined if it sits under at least one allowed prefix. */
58
+ function isConfined(path, allow) {
59
+ const norm = path.replace(/^\.\//, "");
60
+ return allow.some((a) => {
61
+ const base = a.replace(/\/?\*+$/, "").replace(/\/$/, "");
62
+ return base === "" || norm === base || norm.startsWith(base + "/");
63
+ });
64
+ }
65
+ /** Decide a single guard against the event (null = this guard doesn't apply). */
66
+ function decideOne(g, event, prior) {
67
+ switch (g.kind) {
68
+ case "block":
69
+ return matchesPattern(event, g.target) ? deny(g.reason) : null;
70
+ case "requireBefore":
71
+ if (!matchesPattern(event, g.target))
72
+ return null;
73
+ if (prior.some((c) => matchesPattern(c, g.prerequisite)))
74
+ return null;
75
+ return deny(g.reason ??
76
+ `${describePattern(g.target)} requires ${describePattern(g.prerequisite)} first`);
77
+ case "confine": {
78
+ if (!g.tools.includes(event.tool))
79
+ return null;
80
+ const path = event.input?.[g.pathKey ?? "file_path"];
81
+ if (typeof path !== "string" || isConfined(path, g.allow))
82
+ return null;
83
+ return deny(g.reason ??
84
+ `${event.tool} path "${path}" is outside the allowed set [${g.allow.join(", ")}]`);
85
+ }
86
+ }
87
+ }
88
+ /**
89
+ * The pure runtime gate: decide allow/deny for `event`, given the guards and the
90
+ * calls that already fired this session (`prior`, oldest-first — the ledger the
91
+ * `guard-hook` CLI reconstructs from the transcript). First match wins a deny.
92
+ */
93
+ function decideGuards(guards, event, prior = []) {
94
+ for (const g of guards) {
95
+ const d = decideOne(g, event, prior);
96
+ if (d)
97
+ return d;
98
+ }
99
+ return ALLOW;
100
+ }
101
+ const describePattern = (p) => p.when ? `${p.tool}(${(0, arg_match_js_1.describeArgs)(p.when)})` : p.tool;
102
+ /** Every tool name a guard set gates (the PreToolUse matcher union). */
103
+ function guardedTools(guards) {
104
+ const tools = new Set();
105
+ for (const g of guards) {
106
+ if (g.kind === "confine")
107
+ for (const t of g.tools)
108
+ tools.add(t);
109
+ else
110
+ tools.add(g.target.tool);
111
+ }
112
+ return [...tools].sort();
113
+ }
114
+ /**
115
+ * Generate the hooks block for a guard set. The command is vigiles's OWN gate
116
+ * (default `npx vigiles hook-runtime guard`), NOT user shell — so the generated
117
+ * enforcement is safe-by-construction and a repo can't smuggle an arbitrary RCE
118
+ * hook. The gate reads the same guard set + the live event and runs `decideGuards`.
119
+ */
120
+ function compileGuards(guards, gateCommand = "npx vigiles hook-runtime guard") {
121
+ const matcher = guardedTools(guards).join("|");
122
+ return {
123
+ hooks: {
124
+ PreToolUse: [
125
+ { matcher, hooks: [{ type: "command", command: gateCommand }] },
126
+ ],
127
+ },
128
+ };
129
+ }
130
+ function isWireRegex(v) {
131
+ return typeof v === "object" && v !== null && "re" in v;
132
+ }
133
+ function encodeMatcher(m) {
134
+ const out = {};
135
+ for (const [k, v] of Object.entries(m)) {
136
+ out[k] = v instanceof RegExp ? { re: v.source, flags: v.flags } : v;
137
+ }
138
+ return out;
139
+ }
140
+ function decodeMatcher(w) {
141
+ const out = {};
142
+ for (const [k, v] of Object.entries(w)) {
143
+ out[k] = isWireRegex(v) ? new RegExp(v.re, v.flags) : v;
144
+ }
145
+ return out;
146
+ }
147
+ function encodePattern(p) {
148
+ return { tool: p.tool, when: p.when ? encodeMatcher(p.when) : undefined };
149
+ }
150
+ function decodePattern(raw) {
151
+ if (raw === null || typeof raw !== "object")
152
+ return null;
153
+ const o = raw;
154
+ if (typeof o.tool !== "string")
155
+ return null;
156
+ const when = o.when !== null && typeof o.when === "object"
157
+ ? decodeMatcher(o.when)
158
+ : undefined;
159
+ return { tool: o.tool, when };
160
+ }
161
+ /** Serialize a guard set for `.vigiles/guards.json` (RegExp matchers preserved). */
162
+ function serializeGuards(guards) {
163
+ const wire = guards.map((g) => {
164
+ if (g.kind === "confine") {
165
+ return {
166
+ kind: g.kind,
167
+ tools: g.tools,
168
+ allow: g.allow,
169
+ pathKey: g.pathKey,
170
+ reason: g.reason,
171
+ };
172
+ }
173
+ if (g.kind === "requireBefore") {
174
+ return {
175
+ kind: g.kind,
176
+ target: encodePattern(g.target),
177
+ prerequisite: encodePattern(g.prerequisite),
178
+ reason: g.reason,
179
+ };
180
+ }
181
+ return { kind: g.kind, target: encodePattern(g.target), reason: g.reason };
182
+ });
183
+ return JSON.stringify({ guards: wire }, null, 2);
184
+ }
185
+ function parseConfine(o) {
186
+ if (!Array.isArray(o.tools) || !Array.isArray(o.allow))
187
+ return null;
188
+ return {
189
+ kind: "confine",
190
+ tools: o.tools.filter((t) => typeof t === "string"),
191
+ allow: o.allow.filter((a) => typeof a === "string"),
192
+ pathKey: typeof o.pathKey === "string" ? o.pathKey : undefined,
193
+ reason: typeof o.reason === "string" ? o.reason : undefined,
194
+ };
195
+ }
196
+ function parseGuard(raw) {
197
+ if (raw === null || typeof raw !== "object")
198
+ return null;
199
+ const o = raw;
200
+ const reason = typeof o.reason === "string" ? o.reason : undefined;
201
+ if (o.kind === "block") {
202
+ const target = decodePattern(o.target);
203
+ return target && reason ? { kind: "block", target, reason } : null;
204
+ }
205
+ if (o.kind === "requireBefore") {
206
+ const target = decodePattern(o.target);
207
+ const prerequisite = decodePattern(o.prerequisite);
208
+ if (!target || !prerequisite)
209
+ return null;
210
+ return { kind: "requireBefore", target, prerequisite, reason };
211
+ }
212
+ if (o.kind === "confine")
213
+ return parseConfine(o);
214
+ return null;
215
+ }
216
+ /** Parse a guard set from JSON (tolerant — a malformed guard is skipped). */
217
+ function parseGuards(json) {
218
+ let data;
219
+ try {
220
+ data = JSON.parse(json);
221
+ }
222
+ catch {
223
+ return [];
224
+ }
225
+ const list = data?.guards;
226
+ if (!Array.isArray(list))
227
+ return [];
228
+ const out = [];
229
+ for (const item of list) {
230
+ const g = parseGuard(item);
231
+ if (g)
232
+ out.push(g);
233
+ }
234
+ return out;
235
+ }
236
+ // ---------------------------------------------------------------------------
237
+ // Runtime IO — load the guard set, read/append the session ledger
238
+ // ---------------------------------------------------------------------------
239
+ const GUARDS_FILE = ".vigiles/guards.json";
240
+ const LEDGER_FILE = ".vigiles/guard-ledger.json";
241
+ /** Load the declared guard set from `.vigiles/guards.json` (absent → none). */
242
+ function loadGuards(cwd) {
243
+ const p = (0, node_path_1.resolve)(cwd, GUARDS_FILE);
244
+ if (!(0, node_fs_1.existsSync)(p))
245
+ return [];
246
+ return parseGuards((0, node_fs_1.readFileSync)(p, "utf-8"));
247
+ }
248
+ /**
249
+ * The prior-call ledger — the calls already allowed this session, oldest-first.
250
+ * `requireBefore` reads it to know whether a prerequisite ran. Claude Code doesn't
251
+ * surface call history to a hook, so vigiles records each allowed call itself
252
+ * (mirrors `.vigiles/active-agent.json`).
253
+ */
254
+ function readGuardLedger(cwd) {
255
+ const p = (0, node_path_1.resolve)(cwd, LEDGER_FILE);
256
+ if (!(0, node_fs_1.existsSync)(p))
257
+ return [];
258
+ try {
259
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(p, "utf-8"));
260
+ if (!Array.isArray(parsed.calls))
261
+ return [];
262
+ return parsed.calls.filter((c) => c !== null &&
263
+ typeof c === "object" &&
264
+ typeof c.tool === "string");
265
+ }
266
+ catch {
267
+ return [];
268
+ }
269
+ }
270
+ /** Append an allowed call to the session ledger. */
271
+ function recordGuardCall(cwd, event) {
272
+ const p = (0, node_path_1.resolve)(cwd, LEDGER_FILE);
273
+ const calls = readGuardLedger(cwd);
274
+ calls.push({ tool: event.tool, input: event.input });
275
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(p), { recursive: true });
276
+ (0, node_fs_1.writeFileSync)(p, JSON.stringify({ calls }, null, 2));
277
+ }
278
+ /** Parse a PreToolUse event JSON (the hook's stdin) into a {@link ToolEvent}. */
279
+ function parseGuardEvent(rawJson) {
280
+ let parsed;
281
+ try {
282
+ parsed = JSON.parse(rawJson);
283
+ }
284
+ catch {
285
+ return null;
286
+ }
287
+ if (typeof parsed.tool_name !== "string" || !parsed.tool_name)
288
+ return null;
289
+ return { tool: parsed.tool_name, input: parsed.tool_input ?? {} };
290
+ }
291
+ /**
292
+ * The runnable gate, decoupled from process/exit so it's testable. Decides the
293
+ * event against the loaded guards + the prior-call ledger; on ALLOW it records the
294
+ * call (so a later `requireBefore` sees it) and on DENY it records nothing (a
295
+ * blocked call never happened). Malformed/absent event → allow, record nothing.
296
+ */
297
+ function runGuardHook(cwd, rawJson) {
298
+ const event = parseGuardEvent(rawJson);
299
+ if (!event)
300
+ return { decision: ALLOW, recorded: false };
301
+ const guards = loadGuards(cwd);
302
+ const decision = decideGuards(guards, event, readGuardLedger(cwd));
303
+ if (decision.allow) {
304
+ recordGuardCall(cwd, event);
305
+ return { decision, recorded: true };
306
+ }
307
+ return { decision, recorded: false };
308
+ }
309
+ //# sourceMappingURL=guards.js.map
@@ -19,7 +19,7 @@ import type { HarnessRuntime } from "./runtime.js";
19
19
  /**
20
20
  * One scripted assistant turn: a final text answer, or a tool call. The common
21
21
  * shape both harness mocks consume — the Anthropic Messages mock
22
- * (`src/adapters/claude-code/mock-model.ts`) and the OpenAI Responses mock
22
+ * (`src/mock-model.ts`) and the OpenAI Responses mock
23
23
  * (`src/adapters/codex/mock-model.ts`, which uses only `text`).
24
24
  */
25
25
  export interface ModelTurn {