vigiles 5.2.0 → 7.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 (78) hide show
  1. package/README.md +99 -48
  2. package/dist/action-gate.js +1 -1
  3. package/dist/adapters/claude-code/agent-runtime.d.ts +64 -4
  4. package/dist/adapters/claude-code/agent-runtime.js +131 -17
  5. package/dist/adapters/claude-code/dialect.d.ts +34 -0
  6. package/dist/adapters/claude-code/dialect.js +46 -33
  7. package/dist/adapters/claude-code/effect-region.js +1 -1
  8. package/dist/adapters/claude-code/skill-runtime.d.ts +1 -1
  9. package/dist/adapters/claude-code/skill-runtime.js +1 -9
  10. package/dist/adapters/claude-code/typed-spec.d.ts +58 -0
  11. package/dist/adapters/claude-code/typed-spec.js +55 -0
  12. package/dist/adapters/codex/hook-protocol.js +3 -0
  13. package/dist/adapters/codex/mock-model.js +1 -1
  14. package/dist/claude-code.d.ts +1 -0
  15. package/dist/claude-code.js +8 -1
  16. package/dist/cli-commands.d.ts +19 -0
  17. package/dist/cli-commands.js +51 -0
  18. package/dist/cli.js +735 -76
  19. package/dist/core/bash-effects.d.ts +12 -0
  20. package/dist/core/bash-effects.js +31 -0
  21. package/dist/core/capability-diff.d.ts +46 -0
  22. package/dist/core/capability-diff.js +97 -0
  23. package/dist/core/compile.d.ts +1 -1
  24. package/dist/core/compile.js +14 -0
  25. package/dist/core/generate-harness.d.ts +187 -0
  26. package/dist/core/generate-harness.js +337 -0
  27. package/dist/core/guards.d.ts +126 -0
  28. package/dist/core/guards.js +309 -0
  29. package/dist/core/harness-driver.d.ts +1 -1
  30. package/dist/core/hook-program.d.ts +459 -0
  31. package/dist/core/hook-program.js +468 -0
  32. package/dist/core/hook-protocol.d.ts +7 -0
  33. package/dist/core/hook-providers.d.ts +138 -0
  34. package/dist/core/hook-providers.js +155 -0
  35. package/dist/core/hook-spec.d.ts +74 -0
  36. package/dist/core/hook-spec.js +130 -0
  37. package/dist/core/inline.js +1 -1
  38. package/dist/core/mcp-tool.d.ts +12 -0
  39. package/dist/core/mcp-tool.js +20 -0
  40. package/dist/core/mcp.d.ts +13 -0
  41. package/dist/core/mcp.js +67 -0
  42. package/dist/core/spec.d.ts +290 -8
  43. package/dist/core/spec.js +118 -3
  44. package/dist/core/types.d.ts +8 -0
  45. package/dist/dialect-drift.d.ts +65 -0
  46. package/dist/dialect-drift.js +216 -0
  47. package/dist/eval.d.ts +40 -5
  48. package/dist/eval.js +59 -5
  49. package/dist/guardrail-check.d.ts +85 -0
  50. package/dist/guardrail-check.js +152 -0
  51. package/dist/harness-assert.d.ts +10 -0
  52. package/dist/harness-assert.js +30 -0
  53. package/dist/hook-install.d.ts +43 -0
  54. package/dist/hook-install.js +91 -0
  55. package/dist/hook.d.ts +52 -0
  56. package/dist/hook.js +98 -0
  57. package/dist/leaderboard.d.ts +6 -0
  58. package/dist/leaderboard.js +43 -1
  59. package/dist/linting.d.ts +9 -5
  60. package/dist/linting.js +17 -5
  61. package/dist/optimize.js +1 -1
  62. package/dist/scaffold-test.d.ts +28 -0
  63. package/dist/scaffold-test.js +134 -15
  64. package/dist/scan-behavioral.d.ts +60 -0
  65. package/dist/scan-behavioral.js +239 -1
  66. package/dist/scan.d.ts +14 -0
  67. package/dist/scan.js +33 -1
  68. package/dist/score-explainer.js +1 -1
  69. package/dist/self-command-refs.d.ts +21 -0
  70. package/dist/self-command-refs.js +125 -0
  71. package/dist/testing.d.ts +5 -3
  72. package/dist/testing.js +37 -23
  73. package/dist/tool-intercept.d.ts +4 -4
  74. package/dist/tool-intercept.js +5 -5
  75. package/dist/unit.d.ts +2 -0
  76. package/dist/unit.js +8 -1
  77. package/hooks/refs-nudge.sh +1 -1
  78. package/package.json +5 -3
@@ -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 {