prae-cli 0.3.0 → 0.5.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.
@@ -1,5 +1,6 @@
1
- import { p as seal, t as Ledger } from "./src-DGWTvmRZ.js";
2
- import { a as renderPacks, c as renderShell, i as renderOverview, l as VERDICT_LABEL$1, n as renderApprovals, o as esc, r as renderCompliance, s as renderNav, t as navFor } from "./views-CLLfJPWB.js";
1
+ import { t as evaluate } from "./evaluate-BilZez15.js";
2
+ import { m as seal, t as Ledger } from "./src-CXU0_W1P.js";
3
+ import { a as renderPacks, c as renderShell, i as renderOverview, l as VERDICT_LABEL$1, n as renderApprovals, o as esc, r as renderCompliance, s as renderNav, t as navFor } from "./views-DalPkBZZ.js";
3
4
  import { existsSync, readFileSync } from "node:fs";
4
5
  import { mkdir, readFile, writeFile } from "node:fs/promises";
5
6
  import { extname, join, normalize, resolve, sep } from "node:path";
@@ -7,419 +8,6 @@ import { homedir } from "node:os";
7
8
  import { composePatchFile, preflightOps, revisionOf, takeSnapshot } from "dsh-overlay-check";
8
9
  import { createServer } from "node:http";
9
10
 
10
- //#region ../kernel/src/command.ts
11
- /** Wrappers whose first operand is the program actually being run. */
12
- const WRAPPERS = new Set([
13
- "sudo",
14
- "command",
15
- "env",
16
- "nohup",
17
- "time",
18
- "xargs"
19
- ]);
20
- /** Wrapper options that consume the next token, so it is not the program. */
21
- const OPTION_WITH_VALUE = new Set([
22
- "-u",
23
- "-g",
24
- "-p",
25
- "-h",
26
- "-C",
27
- "-r",
28
- "-t",
29
- "-U",
30
- "-I",
31
- "-n",
32
- "-P",
33
- "-L",
34
- "-s"
35
- ]);
36
- const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
37
- /**
38
- * Split a line into pipeline segments of words, one scan, quote-aware.
39
- *
40
- * Splitting on `|` before parsing quotes is the classic mistake — it turns
41
- * `echo "a | b"` into two segments — so quoting and separators are read
42
- * together.
43
- */
44
- function scan(command) {
45
- const parts = [];
46
- let tokens = [];
47
- let current = "";
48
- let quote;
49
- const endToken = () => {
50
- if (current !== "") tokens.push(current);
51
- current = "";
52
- };
53
- const endSegment = () => {
54
- endToken();
55
- if (tokens.length > 0) parts.push(tokens);
56
- tokens = [];
57
- };
58
- for (let at = 0; at < command.length; at += 1) {
59
- const ch = command[at];
60
- if (quote !== void 0) {
61
- if (ch === quote) quote = void 0;
62
- else current += ch;
63
- continue;
64
- }
65
- if (ch === "'" || ch === "\"") {
66
- quote = ch;
67
- continue;
68
- }
69
- if (ch === ";") {
70
- endSegment();
71
- continue;
72
- }
73
- if (ch === "|" || ch === "&") {
74
- endSegment();
75
- if (command[at + 1] === ch) at += 1;
76
- continue;
77
- }
78
- if (/\s/.test(ch)) {
79
- endToken();
80
- continue;
81
- }
82
- current += ch;
83
- }
84
- endSegment();
85
- return parts;
86
- }
87
- /** Decompose a command line into pipeline segments. */
88
- function segments(command) {
89
- const found = [];
90
- for (const tokens of scan(command)) {
91
- let at = 0;
92
- while (at < tokens.length) {
93
- const token = tokens[at];
94
- if (ASSIGNMENT.test(token)) {
95
- at += 1;
96
- continue;
97
- }
98
- if (WRAPPERS.has(token)) {
99
- at += 1;
100
- while (at < tokens.length) {
101
- const option = tokens[at];
102
- if (!option.startsWith("-")) break;
103
- at += OPTION_WITH_VALUE.has(option) ? 2 : 1;
104
- }
105
- continue;
106
- }
107
- break;
108
- }
109
- const name = tokens[at];
110
- if (name === void 0) continue;
111
- found.push({
112
- name: (name.split("/").pop() ?? name).toLowerCase(),
113
- tokens: tokens.slice(at + 1)
114
- });
115
- }
116
- return found;
117
- }
118
- /** `git*` matches `git`, `github`; bare strings match exactly. */
119
- function matches$1(pattern, value) {
120
- if (pattern.endsWith("*")) return value.startsWith(pattern.slice(0, -1));
121
- return value === pattern;
122
- }
123
- const list = (v) => v === void 0 ? [] : typeof v === "string" ? [v] : v;
124
- /**
125
- * Judge one command line against one matcher. Every present field must hold.
126
- */
127
- function commandMatches(match, command) {
128
- const parts = segments(command);
129
- if (parts.length === 0) return false;
130
- const names = list(match.name);
131
- const args = list(match.token);
132
- const pipes = list(match.pipedTo);
133
- if (names.length > 0) {
134
- const hit = parts.filter((segment) => names.some((pattern) => matches$1(pattern, segment.name)));
135
- if (hit.length === 0) return false;
136
- if (args.length > 0 && !hit.some((segment) => args.every((wanted) => segment.tokens.some((token) => matches$1(wanted, token))))) return false;
137
- } else if (args.length > 0) {
138
- if (!parts.some((segment) => args.every((wanted) => segment.tokens.some((token) => matches$1(wanted, token))))) return false;
139
- }
140
- if (pipes.length > 0) {
141
- if (!parts.slice(1).some((segment) => pipes.some((pattern) => matches$1(pattern, segment.name)))) return false;
142
- }
143
- return true;
144
- }
145
-
146
- //#endregion
147
- //#region ../kernel/src/path.ts
148
- /**
149
- * Path normalization and glob containment, with no filesystem access.
150
- *
151
- * The contract, stated here because getting it wrong is how path policies
152
- * become theatre: **this module compares strings.** It resolves `.` and `..`
153
- * lexically so that `./src/../../etc/passwd` cannot pose as a workspace path,
154
- * but it cannot see a symlink. A link inside an allowed directory pointing out
155
- * of it will pass every check here.
156
- *
157
- * That is not a gap to paper over in the matcher — it is a gap the caller
158
- * closes by resolving before it asks. `dsh-overlay-check` learned this the
159
- * hard way during the plugin-store gate review: lexical containment accepted a
160
- * symlink escape cleanly, and the fix was to resolve both the path and its
161
- * nearest existing ancestor before deciding. Adapters that can touch the
162
- * filesystem must resolve first and pass the resolved path in.
163
- */
164
- /** Split a path into segments, resolving `.` and `..` lexically. */
165
- function normalize$1(path) {
166
- const absolute = path.startsWith("/");
167
- const out = [];
168
- let escaped = false;
169
- for (const raw of path.split(/[/\\]+/)) {
170
- if (raw === "" || raw === ".") continue;
171
- if (raw === "..") {
172
- if (out.length > 0) out.pop();
173
- else escaped = true;
174
- continue;
175
- }
176
- out.push(raw);
177
- }
178
- return {
179
- segments: out,
180
- absolute,
181
- escaped
182
- };
183
- }
184
- /** Turn one glob segment into an anchored regular expression. */
185
- function segmentPattern(segment) {
186
- const body = segment.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*");
187
- return /* @__PURE__ */ new RegExp(`^${body}$`);
188
- }
189
- /**
190
- * Wildcard segment matching, where `**` spans zero or more segments.
191
- *
192
- * Two-pointer with backtracking rather than a recursive walk: the pattern
193
- * comes from a policy pack and the path from a tool call, and a catastrophic
194
- * backtrack in the hot path of every call would be a denial of service handed
195
- * to whoever writes the pack.
196
- */
197
- function matchSegments(path, pattern) {
198
- let i = 0;
199
- let j = 0;
200
- let starPattern = -1;
201
- let starPath = 0;
202
- while (i < path.length) {
203
- const segment = j < pattern.length ? pattern[j] : void 0;
204
- const here = path[i];
205
- if (segment === "**") {
206
- starPattern = j;
207
- starPath = i;
208
- j += 1;
209
- continue;
210
- }
211
- if (segment !== void 0 && here !== void 0 && segmentPattern(segment).test(here)) {
212
- i += 1;
213
- j += 1;
214
- continue;
215
- }
216
- if (starPattern >= 0) {
217
- starPath += 1;
218
- i = starPath;
219
- j = starPattern + 1;
220
- continue;
221
- }
222
- return false;
223
- }
224
- while (j < pattern.length && pattern[j] === "**") j += 1;
225
- return j === pattern.length;
226
- }
227
- /**
228
- * Is `path` contained by `glob`?
229
- *
230
- * A glob naming a directory contains everything under it: `./src` and
231
- * `./src/**` both match `src/app/main.ts`. Writing the shorter form is the
232
- * common intent, and treating it as "this exact directory entry only" is a
233
- * trap that silently grants the whole subtree past the rule.
234
- */
235
- function within(path, glob) {
236
- const target = normalize$1(path);
237
- if (target.escaped) return false;
238
- const trimmed = glob.trim();
239
- if (!(trimmed === "**" || trimmed.startsWith("**/")) && target.absolute !== trimmed.startsWith("/")) return false;
240
- const pattern = normalize$1(glob);
241
- if (matchSegments(target.segments, pattern.segments)) return true;
242
- return matchSegments(target.segments, [...pattern.segments, "**"]);
243
- }
244
- /** Is `path` inside any of these globs? */
245
- function withinAny(path, globs) {
246
- return globs.some((glob) => within(path, glob));
247
- }
248
- /**
249
- * The host of a URL, or undefined when it is not a URL we can read.
250
- *
251
- * Unparseable input returns undefined, and callers treat that as "no host" —
252
- * which for a `notIn` rule means the rule matches. Failing toward the rule is
253
- * the only safe direction: a string we cannot parse is exactly the shape an
254
- * evasion attempt takes.
255
- */
256
- function hostOf(url) {
257
- try {
258
- return new URL(url).hostname.toLowerCase();
259
- } catch {
260
- return;
261
- }
262
- }
263
- /**
264
- * Host list membership, where a leading dot means "this domain or below".
265
- *
266
- * `example.com` matches only that host. `.example.com` also matches
267
- * `api.example.com`, and deliberately still matches `example.com` itself —
268
- * an operator writing the suffix form means the organisation, not a rule that
269
- * covers every subdomain except the one they first thought of.
270
- */
271
- function hostIn(host, list$1) {
272
- return list$1.some((entry) => {
273
- const want = entry.toLowerCase();
274
- if (want.startsWith(".")) return host === want.slice(1) || host.endsWith(want);
275
- return host === want;
276
- });
277
- }
278
-
279
- //#endregion
280
- //#region ../kernel/src/rule.ts
281
- /**
282
- * Strength ordering, used when several rules match one call.
283
- *
284
- * Higher wins. This is the property that makes a pack safe to extend: adding a
285
- * rule can raise the strength of a decision and never lower it, so an operator
286
- * appending to a pack can never accidentally widen what an agent may do.
287
- */
288
- const STRENGTH = {
289
- warn: 1,
290
- correct: 2,
291
- ask: 3,
292
- deny: 4
293
- };
294
-
295
- //#endregion
296
- //#region ../kernel/src/evaluate.ts
297
- function listed(value, want) {
298
- if (want === void 0) return true;
299
- if (value === void 0) return false;
300
- return typeof want === "string" ? value === want : want.includes(value);
301
- }
302
- function argMatches(match, args) {
303
- const has = Object.hasOwn(args, match.name);
304
- if (match.present !== void 0 && match.present !== has) return false;
305
- if (!has) return match.above === void 0 && match.below === void 0 && match.equals === void 0 && match.oneOf === void 0;
306
- const value = args[match.name];
307
- if (match.above !== void 0) {
308
- if (typeof value !== "number" || !(value > match.above)) return false;
309
- }
310
- if (match.below !== void 0) {
311
- if (typeof value !== "number" || !(value < match.below)) return false;
312
- }
313
- if (match.equals !== void 0 && value !== match.equals) return false;
314
- if (match.oneOf !== void 0) {
315
- if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") return false;
316
- if (!match.oneOf.includes(value)) return false;
317
- }
318
- return true;
319
- }
320
- function matches(when, facts) {
321
- if (!listed(facts.tool, when.tool)) return false;
322
- if (!listed(facts.capability, when.capability)) return false;
323
- if (!listed(facts.plugin, when.plugin)) return false;
324
- if (!listed(facts.agent, when.agent)) return false;
325
- if (when.command !== void 0) {
326
- const raw = facts.args.command ?? facts.args.cmd ?? facts.args.script;
327
- if (typeof raw !== "string") return false;
328
- if (!commandMatches(when.command, raw)) return false;
329
- }
330
- if (when.path !== void 0) {
331
- const raw = facts.args[when.path.arg ?? "path"];
332
- if (typeof raw !== "string") return false;
333
- if (when.path.inside !== void 0 && !withinAny(raw, when.path.inside)) return false;
334
- if (when.path.outside !== void 0 && withinAny(raw, when.path.outside)) return false;
335
- }
336
- if (when.host !== void 0) {
337
- const raw = facts.args[when.host.arg ?? "url"];
338
- if (typeof raw !== "string") return false;
339
- const host = hostOf(raw);
340
- if (host === void 0) return when.host.notIn !== void 0;
341
- if (when.host.in !== void 0 && !hostIn(host, when.host.in)) return false;
342
- if (when.host.notIn !== void 0 && hostIn(host, when.host.notIn)) return false;
343
- }
344
- if (when.arg !== void 0) {
345
- for (const one of when.arg) if (!argMatches(one, facts.args)) return false;
346
- }
347
- if (when.count !== void 0) {
348
- const key = facts.capability;
349
- if (key === void 0) return false;
350
- if (!((facts.counts?.[key] ?? 0) > when.count.above)) return false;
351
- }
352
- return true;
353
- }
354
- function step(rule, effect) {
355
- return {
356
- rule: rule.id,
357
- ...rule.title === void 0 ? {} : { title: rule.title },
358
- effect: effect.kind,
359
- because: effect.because
360
- };
361
- }
362
- /**
363
- * Decide one call against one pack.
364
- *
365
- * Every matching rule lands in the trace; the strongest effect decides. Ties
366
- * go to the first rule in pack order, so a pack reads top-down the way its
367
- * author wrote it.
368
- *
369
- * `correct` is the effect the rest of this ecosystem does not have. A rival
370
- * guardrail can allow a call or refuse it; a rule here can also rewrite an
371
- * argument and let it through, recording what it changed. Refusing is not
372
- * always the safe option — an agent denied mid-task retries, and the retry is
373
- * rarely safer than the clamp would have been.
374
- */
375
- function evaluate(pack, facts) {
376
- const hits = [];
377
- for (const rule of pack.rules) if (matches(rule.when, facts)) hits.push({
378
- rule,
379
- effect: rule.effect
380
- });
381
- if (hits.length === 0) return {
382
- kind: "allow",
383
- trace: [],
384
- evaluated: pack.rules.length
385
- };
386
- hits.sort((a, b) => STRENGTH[b.effect.kind] - STRENGTH[a.effect.kind]);
387
- const trace = hits.map((hit) => step(hit.rule, hit.effect));
388
- const evaluated = pack.rules.length;
389
- const strongest = hits[0];
390
- if (strongest === void 0) return {
391
- kind: "allow",
392
- trace: [],
393
- evaluated
394
- };
395
- const winner = strongest.effect.kind;
396
- if (winner === "deny" || winner === "ask") return {
397
- kind: winner,
398
- rule: strongest.rule.id,
399
- reason: strongest.effect.because,
400
- trace,
401
- evaluated
402
- };
403
- if (winner === "correct") {
404
- const args = { ...facts.args };
405
- for (const hit of hits) if (hit.effect.kind === "correct") Object.assign(args, hit.effect.set);
406
- return {
407
- kind: "correct",
408
- rule: strongest.rule.id,
409
- reason: strongest.effect.because,
410
- args,
411
- trace,
412
- evaluated
413
- };
414
- }
415
- return {
416
- kind: "allow",
417
- trace,
418
- evaluated
419
- };
420
- }
421
-
422
- //#endregion
423
11
  //#region ../kernel/src/pack.ts
424
12
  function isObject(value) {
425
13
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -545,12 +133,12 @@ function readHostMatch(check$1, path, value) {
545
133
  if (arg !== void 0) out.arg = arg;
546
134
  }
547
135
  if (value.in !== void 0) {
548
- const list$1 = check$1.globs(`${path}.in`, value.in);
549
- if (list$1 !== void 0) out.in = list$1;
136
+ const list = check$1.globs(`${path}.in`, value.in);
137
+ if (list !== void 0) out.in = list;
550
138
  }
551
139
  if (value.notIn !== void 0) {
552
- const list$1 = check$1.globs(`${path}.notIn`, value.notIn);
553
- if (list$1 !== void 0) out.notIn = list$1;
140
+ const list = check$1.globs(`${path}.notIn`, value.notIn);
141
+ if (list !== void 0) out.notIn = list;
554
142
  }
555
143
  return out;
556
144
  }
@@ -585,6 +173,33 @@ function readArgMatch(check$1, path, value) {
585
173
  else out.present = value.present;
586
174
  return out;
587
175
  }
176
+ function readSecretsMatch(check$1, path, value) {
177
+ if (!isObject(value)) {
178
+ check$1.fail(path, "expected an object");
179
+ return;
180
+ }
181
+ check$1.keys(path, value, [
182
+ "types",
183
+ "minConfidence",
184
+ "atLeast"
185
+ ]);
186
+ const out = {};
187
+ if (value.types !== void 0) {
188
+ const types = check$1.globs(`${path}.types`, value.types);
189
+ if (types !== void 0) out.types = types;
190
+ }
191
+ if (value.minConfidence !== void 0) {
192
+ const n = check$1.number(`${path}.minConfidence`, value.minConfidence);
193
+ if (n !== void 0) if (n < 0 || n > 1) check$1.fail(`${path}.minConfidence`, "expected a number between 0 and 1");
194
+ else out.minConfidence = n;
195
+ }
196
+ if (value.atLeast !== void 0) {
197
+ const n = check$1.number(`${path}.atLeast`, value.atLeast);
198
+ if (n !== void 0) if (!Number.isInteger(n) || n < 1) check$1.fail(`${path}.atLeast`, "expected an integer of at least 1");
199
+ else out.atLeast = n;
200
+ }
201
+ return out;
202
+ }
588
203
  function readMatch(check$1, path, value) {
589
204
  if (!isObject(value)) {
590
205
  check$1.fail(path, "expected an object");
@@ -599,7 +214,8 @@ function readMatch(check$1, path, value) {
599
214
  "path",
600
215
  "host",
601
216
  "arg",
602
- "count"
217
+ "count",
218
+ "secrets"
603
219
  ]);
604
220
  const out = {};
605
221
  for (const key of [
@@ -625,12 +241,12 @@ function readMatch(check$1, path, value) {
625
241
  }
626
242
  if (value.arg !== void 0) if (!Array.isArray(value.arg)) check$1.fail(`${path}.arg`, "expected an array of argument matchers");
627
243
  else {
628
- const list$1 = [];
244
+ const list = [];
629
245
  value.arg.forEach((entry, index) => {
630
246
  const one = readArgMatch(check$1, `${path}.arg[${index}]`, entry);
631
- if (one !== void 0) list$1.push(one);
247
+ if (one !== void 0) list.push(one);
632
248
  });
633
- out.arg = list$1;
249
+ out.arg = list;
634
250
  }
635
251
  if (value.count !== void 0) if (!isObject(value.count)) check$1.fail(`${path}.count`, "expected an object");
636
252
  else {
@@ -638,6 +254,10 @@ function readMatch(check$1, path, value) {
638
254
  const above = check$1.number(`${path}.count.above`, value.count.above);
639
255
  if (above !== void 0) out.count = { above };
640
256
  }
257
+ if (value.secrets !== void 0) {
258
+ const match = readSecretsMatch(check$1, `${path}.secrets`, value.secrets);
259
+ if (match !== void 0) out.secrets = match;
260
+ }
641
261
  return out;
642
262
  }
643
263
  function readEffect(check$1, path, value) {
@@ -1222,6 +842,17 @@ const USAGE = `prae — operator policy for agent harnesses
1222
842
  prae explain <pack.json> <tool> [key=value ...]
1223
843
  Judge one call against the pack and print the trace. Nothing is executed.
1224
844
 
845
+ prae scan <file> [--pack <pack.json>] [--json]
846
+ Inspect a file for credentials the way the gate would before a read.
847
+ Findings name the type and the line, never the secret. With --pack,
848
+ also print the verdict the pack would reach.
849
+
850
+ prae mcp --root <dir> [--pack <pack.json>] [--ledger <file>]
851
+ Serve gated filesystem access over MCP stdio. Any MCP-capable agent or
852
+ IDE mounts this as its file tool; every read is inspected before its
853
+ contents are returned, and every decision lands in the ledger.
854
+ (no --pack mounts the bundled secret-catcher pack)
855
+
1225
856
  prae verify <ledger.jsonl> [--key <public.pem>]
1226
857
  Re-derive every hash in a decision log and report any break. With
1227
858
  --key, also verify each row's signature against that pinned key.
@@ -1233,6 +864,10 @@ const USAGE = `prae — operator policy for agent harnesses
1233
864
  prae receipt <ledger.jsonl> <seq> --key <private.pem> [--out <file>]
1234
865
  Export one decision as a self-contained signed receipt.
1235
866
 
867
+ prae verify-receipt <receipt.json>
868
+ Check one receipt with nothing but the file: recompute its hash, verify
869
+ its signature, and print the key fingerprint to compare against your own.
870
+
1236
871
  prae report <ledger.jsonl> [--out <file.html>]
1237
872
  Render the decision record as a printable evidence report.
1238
873
 
@@ -1286,6 +921,70 @@ async function check(path) {
1286
921
  console.log(` ${rule.id.padEnd(10)} ${rule.effect.kind.toUpperCase().padEnd(8)} ${label}`);
1287
922
  }
1288
923
  }
924
+ /**
925
+ * `prae scan`: the gate's pre-read inspection, runnable by hand.
926
+ *
927
+ * This is the command a pilot runs first — point it at a file that holds a
928
+ * credential and watch the gate see it without repeating it. Output carries
929
+ * type, rule, line and confidence; the matched text is structurally absent
930
+ * from the findings, so there is nothing here to be careful about printing.
931
+ */
932
+ async function scanFile(path, packPath, json) {
933
+ const { scan } = await import("./src-Vnr0fcpK.js");
934
+ let content;
935
+ try {
936
+ content = await readFile(path, "utf8");
937
+ } catch {
938
+ return fail(`cannot read ${path}`);
939
+ }
940
+ const findings = scan(content);
941
+ if (json) console.log(JSON.stringify({
942
+ file: path,
943
+ findings
944
+ }, null, 2));
945
+ else if (findings.length === 0) console.log(`${path} — no credentials detected`);
946
+ else {
947
+ console.log(`${path} — ${findings.length} finding(s)\n`);
948
+ for (const f of findings) {
949
+ const score = `${Math.round(f.confidence * 100)}%`;
950
+ console.log(` line ${String(f.line).padEnd(5)} ${f.type.padEnd(18)} ${f.rule.padEnd(22)} ${score}`);
951
+ }
952
+ }
953
+ if (packPath !== void 0) {
954
+ const pack = await loadPack(packPath);
955
+ const decision = evaluate(pack, {
956
+ tool: "read",
957
+ capability: "fs:read",
958
+ args: { path },
959
+ secrets: findings
960
+ });
961
+ const label = VERDICT_LABEL[decision.kind];
962
+ console.log(`\n${pack.pack} ${pack.version} would say: ${label}${decision.rule === void 0 ? "" : ` (${decision.rule})`}`);
963
+ }
964
+ }
965
+ /**
966
+ * `prae mcp`: mount the gate as a filesystem MCP server on stdio.
967
+ *
968
+ * With no --pack, the bundled secret-catcher mounts — same day-zero logic as
969
+ * `init`'s baseline: a gate whose whole point is content inspection should
970
+ * not come up inspecting nothing.
971
+ */
972
+ async function mcp(root, packPath, ledgerPath) {
973
+ const { serveMcp } = await import("./mcp-BH5I4Eyu.js");
974
+ const { resolve: resolve$1 } = await import("node:path");
975
+ const bundled = [join(import.meta.dirname, "secret-catcher.prae.json"), join(import.meta.dirname, "..", "..", "..", "examples", "secret-catcher.prae.json")].find((candidate) => existsSync(candidate));
976
+ const chosen = packPath ?? bundled;
977
+ if (chosen === void 0) return fail("mcp needs --pack <file>; no bundled secret-catcher found");
978
+ const pack = await loadPack(chosen);
979
+ const { Ledger: Ledger$1 } = await import("./src-Kubj3MTX.js");
980
+ const ledger = ledgerPath === void 0 ? void 0 : Ledger$1.open(ledgerPath);
981
+ console.error(`prae mcp: serving ${resolve$1(root)} under ${pack.pack} ${pack.version}` + (ledgerPath === void 0 ? " (no ledger)" : ` · ledger ${ledgerPath}`));
982
+ await serveMcp({
983
+ pack,
984
+ root: resolve$1(root),
985
+ ...ledger === void 0 ? {} : { ledger }
986
+ });
987
+ }
1289
988
  /** `key=value` pairs, with numbers and booleans read as themselves. */
1290
989
  function parseArgs(pairs) {
1291
990
  const args = {};
@@ -1316,7 +1015,7 @@ async function explain(path, tool, pairs) {
1316
1015
  if (decision.trace.length === 0) console.log("\n no rule matched this call");
1317
1016
  else {
1318
1017
  console.log("");
1319
- for (const step$1 of decision.trace) console.log(` ${step$1.rule.padEnd(10)} ${step$1.effect.padEnd(8)} ${step$1.because}`);
1018
+ for (const step of decision.trace) console.log(` ${step.rule.padEnd(10)} ${step.effect.padEnd(8)} ${step.because}`);
1320
1019
  }
1321
1020
  console.log(`\n${pack.pack} ${pack.version} · ${decision.evaluated} rules considered`);
1322
1021
  if (capability === void 0) console.log(`\nnote: no capability claims \`${tool}\`, so capability rules did not apply`);
@@ -1329,7 +1028,7 @@ async function explain(path, tool, pairs) {
1329
1028
  * a key that arrived beside it is checked against nothing.
1330
1029
  */
1331
1030
  async function keygen(dir) {
1332
- const { generateKeypair, fingerprint } = await import("./src-Cinsv7Wq.js");
1031
+ const { generateKeypair, fingerprint } = await import("./src-Kubj3MTX.js");
1333
1032
  const { writeFileSync: writeFileSync$1 } = await import("node:fs");
1334
1033
  const keys = generateKeypair();
1335
1034
  const priv = join(dir, "prae-signing.pem");
@@ -1345,7 +1044,7 @@ async function keygen(dir) {
1345
1044
  console.log("private key with `signingKey` to sign every row.");
1346
1045
  }
1347
1046
  async function receipt(path, seq, keyPath, out) {
1348
- const { Ledger: Ledger$1, makeReceipt, fingerprint } = await import("./src-Cinsv7Wq.js");
1047
+ const { Ledger: Ledger$1, makeReceipt, fingerprint } = await import("./src-Kubj3MTX.js");
1349
1048
  const { readFileSync: readFileSync$1 } = await import("node:fs");
1350
1049
  const { createPublicKey } = await import("node:crypto");
1351
1050
  const privateKey = readFileSync$1(keyPath, "utf8");
@@ -1366,6 +1065,27 @@ async function receipt(path, seq, keyPath, out) {
1366
1065
  console.log("The verifier compares that fingerprint against the one they");
1367
1066
  console.log("pinned — a receipt is only as good as the key they trust.");
1368
1067
  }
1068
+ /**
1069
+ * `prae verify-receipt`: check a single self-contained receipt.
1070
+ *
1071
+ * The counterparty's command. It needs nothing but the receipt file — no
1072
+ * ledger, no PRAE account, no trust in the machine that issued it. The verdict
1073
+ * and its wording live in `receipt-check.ts` as a pure function; this reads the
1074
+ * file and sets the exit code, so scripts can branch on it.
1075
+ */
1076
+ async function verifyReceiptCmd(path) {
1077
+ const { checkReceipt } = await import("./receipt-check-Dql0uqxw.js");
1078
+ let parsed;
1079
+ try {
1080
+ parsed = JSON.parse(await readFile(path, "utf8"));
1081
+ } catch {
1082
+ return fail(`cannot read a receipt from ${path}`);
1083
+ }
1084
+ const result = checkReceipt(parsed);
1085
+ for (const line of result.lines) if (result.ok) console.log(line);
1086
+ else console.error(line);
1087
+ if (!result.ok) process.exit(1);
1088
+ }
1369
1089
  async function verify(path, keyPath) {
1370
1090
  let ledger;
1371
1091
  try {
@@ -1379,7 +1099,7 @@ async function verify(path, keyPath) {
1379
1099
  const last = entries.at(-1);
1380
1100
  console.log(`${result.entries} entries · chain intact${last === void 0 ? "" : ` · seal ${seal(last)}`}`);
1381
1101
  if (keyPath !== void 0) {
1382
- const { verifySignature, fingerprint } = await import("./src-Cinsv7Wq.js");
1102
+ const { verifySignature, fingerprint } = await import("./src-Kubj3MTX.js");
1383
1103
  const { readFileSync: readFileSync$1 } = await import("node:fs");
1384
1104
  const publicKey = readFileSync$1(keyPath, "utf8");
1385
1105
  let signed = 0;
@@ -1455,7 +1175,7 @@ async function report(path, out) {
1455
1175
  * that step is a proposal a human approves, and it starts from this list.
1456
1176
  */
1457
1177
  async function instructions(from, asJson) {
1458
- const { ingest } = await import("./src-2pGbYeGU.js");
1178
+ const { ingest } = await import("./src-C4fnUm9V.js");
1459
1179
  const result = ingest(from);
1460
1180
  if (asJson) {
1461
1181
  console.log(JSON.stringify(result, null, 2));
@@ -1491,7 +1211,7 @@ async function instructions(from, asJson) {
1491
1211
  * on purpose rather than something that happened to their policy.
1492
1212
  */
1493
1213
  async function propose(from, options) {
1494
- const [{ ingest }, { extract }] = await Promise.all([import("./src-2pGbYeGU.js"), import("./src-CuHszf3f.js")]);
1214
+ const [{ ingest }, { extract }] = await Promise.all([import("./src-C4fnUm9V.js"), import("./src-BfYsUDW9.js")]);
1495
1215
  const read = ingest(from);
1496
1216
  if (read.files.length === 0) {
1497
1217
  console.log(`No instruction files found at or above ${from}.`);
@@ -1551,9 +1271,9 @@ async function consoleOut(path, argv) {
1551
1271
  const instructionsDir = flag(argv, "instructions");
1552
1272
  if (instructionsDir !== void 0) {
1553
1273
  const [{ ingest }, { extract }, { renderInstructions }] = await Promise.all([
1554
- import("./src-2pGbYeGU.js"),
1555
- import("./src-CuHszf3f.js"),
1556
- import("./instructions-C6jwClPO.js")
1274
+ import("./src-C4fnUm9V.js"),
1275
+ import("./src-BfYsUDW9.js"),
1276
+ import("./instructions-D4VOn2lk.js")
1557
1277
  ]);
1558
1278
  const read = ingest(instructionsDir);
1559
1279
  instructionsView = renderInstructions(data, read, extract(read.statements));
@@ -1652,6 +1372,16 @@ async function main(argv) {
1652
1372
  if (path === void 0 || tool === void 0) fail("explain needs a pack file and a tool name");
1653
1373
  return explain(path, tool, pairs);
1654
1374
  }
1375
+ case "scan": {
1376
+ const path = rest[0];
1377
+ if (path === void 0) fail("scan needs a file");
1378
+ return scanFile(path, flag(rest, "pack"), rest.includes("--json"));
1379
+ }
1380
+ case "mcp": {
1381
+ const root = flag(rest, "root");
1382
+ if (root === void 0) fail("mcp needs --root <dir>; serving without a boundary is not a gate");
1383
+ return mcp(root, flag(rest, "pack"), flag(rest, "ledger"));
1384
+ }
1655
1385
  case "keygen": return keygen(flag(rest, "out") ?? ".");
1656
1386
  case "receipt": {
1657
1387
  const [path, seq] = rest;
@@ -1664,6 +1394,11 @@ async function main(argv) {
1664
1394
  if (path === void 0) fail("verify needs a ledger file");
1665
1395
  return verify(path, flag(rest, "key"));
1666
1396
  }
1397
+ case "verify-receipt": {
1398
+ const path = rest[0];
1399
+ if (path === void 0) fail("verify-receipt needs a receipt file");
1400
+ return verifyReceiptCmd(path);
1401
+ }
1667
1402
  case "report": {
1668
1403
  const path = rest[0];
1669
1404
  if (path === void 0) fail("report needs a ledger file");