prae-cli 0.2.1 → 0.4.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.
- package/README.md +16 -0
- package/lib/bin.js +4 -2
- package/lib/{cli-6hDu8-nF.js → cli-BvAYSK5w.js} +219 -595
- package/lib/evaluate-BilZez15.js +419 -0
- package/lib/index.js +4 -2
- package/lib/{instructions-ijuj4IP1.js → instructions-D4VOn2lk.js} +1 -1
- package/lib/mcp-BH5I4Eyu.js +208 -0
- package/lib/playground.js +38 -1
- package/lib/secret-catcher.prae.json +49 -0
- package/lib/src-Br1tGPtS.js +182 -0
- package/lib/src-CAUNqZJ1.js +225 -0
- package/lib/src-CBK_quVO.js +3 -0
- package/lib/src-Vnr0fcpK.js +3 -0
- package/package.json +5 -3
- /package/lib/{src-BrZXeEez.js → src-CCeafjdm.js} +0 -0
- /package/lib/{src-Dd2_8m2g.js → src-aU5WW02C.js} +0 -0
- /package/lib/{views-BU0sOciR.js → views-DalPkBZZ.js} +0 -0
|
@@ -1,425 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { t as evaluate } from "./evaluate-BilZez15.js";
|
|
2
|
+
import { p as seal, t as Ledger } from "./src-CAUNqZJ1.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";
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
5
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
-
import {
|
|
5
|
-
import { createHash } from "node:crypto";
|
|
6
|
+
import { extname, join, normalize, resolve, sep } from "node:path";
|
|
6
7
|
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
|
|
549
|
-
if (list
|
|
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
|
|
553
|
-
if (list
|
|
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
|
|
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
|
|
247
|
+
if (one !== void 0) list.push(one);
|
|
632
248
|
});
|
|
633
|
-
out.arg = list
|
|
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) {
|
|
@@ -774,165 +394,6 @@ function parsePack(source) {
|
|
|
774
394
|
};
|
|
775
395
|
}
|
|
776
396
|
|
|
777
|
-
//#endregion
|
|
778
|
-
//#region ../ledger/src/entry.ts
|
|
779
|
-
/** The hash a chain starts from. */
|
|
780
|
-
const GENESIS = "0".repeat(64);
|
|
781
|
-
/**
|
|
782
|
-
* Deterministic serialization: object keys sorted at every depth.
|
|
783
|
-
*
|
|
784
|
-
* A hash over `JSON.stringify(entry)` would depend on key insertion order,
|
|
785
|
-
* which means a re-serialized entry could hash differently while saying
|
|
786
|
-
* exactly the same thing — and a verifier that reports tampering on a
|
|
787
|
-
* faithful copy trains its operators to ignore it.
|
|
788
|
-
*/
|
|
789
|
-
function canonical(value) {
|
|
790
|
-
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
791
|
-
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
792
|
-
return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(",")}}`;
|
|
793
|
-
}
|
|
794
|
-
/** Hash one entry's content together with its position and predecessor. */
|
|
795
|
-
function hashEntry(record, seq, prev) {
|
|
796
|
-
return createHash("sha256").update(canonical({
|
|
797
|
-
...record,
|
|
798
|
-
seq,
|
|
799
|
-
prev
|
|
800
|
-
})).digest("hex");
|
|
801
|
-
}
|
|
802
|
-
/** Chain a record onto a predecessor. */
|
|
803
|
-
function chain(record, seq, prev) {
|
|
804
|
-
return {
|
|
805
|
-
...record,
|
|
806
|
-
seq,
|
|
807
|
-
prev,
|
|
808
|
-
hash: hashEntry(record, seq, prev)
|
|
809
|
-
};
|
|
810
|
-
}
|
|
811
|
-
/** The short form an auditor quotes from a screenshot. */
|
|
812
|
-
function seal(entry) {
|
|
813
|
-
return `${entry.hash.slice(0, 4)}…${entry.hash.slice(-4)}`;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
//#endregion
|
|
817
|
-
//#region ../ledger/src/ledger.ts
|
|
818
|
-
/**
|
|
819
|
-
* Read a ledger held as text.
|
|
820
|
-
*
|
|
821
|
-
* Split out from the class because a ledger is not always a file being
|
|
822
|
-
* appended to: a build step renders one it imported, and a reader pastes one
|
|
823
|
-
* in. Those callers need the same parsing and the same verification the CLI
|
|
824
|
-
* runs, and a second implementation of either is how two answers to "does this
|
|
825
|
-
* chain hold" come to exist.
|
|
826
|
-
*/
|
|
827
|
-
function readEntries(text) {
|
|
828
|
-
return text.split("\n").filter((line) => line.trim() !== "").map((line) => JSON.parse(line));
|
|
829
|
-
}
|
|
830
|
-
/** Re-derive every hash and confirm each entry names its predecessor. */
|
|
831
|
-
function verifyEntries(text) {
|
|
832
|
-
const problems = [];
|
|
833
|
-
let expectedPrev = GENESIS;
|
|
834
|
-
let expectedSeq = 0;
|
|
835
|
-
let count = 0;
|
|
836
|
-
const lines = text.split("\n").filter((line) => line.trim() !== "");
|
|
837
|
-
for (const [index, line] of lines.entries()) {
|
|
838
|
-
const at = index + 1;
|
|
839
|
-
count += 1;
|
|
840
|
-
let entry;
|
|
841
|
-
try {
|
|
842
|
-
entry = JSON.parse(line);
|
|
843
|
-
} catch {
|
|
844
|
-
problems.push({
|
|
845
|
-
line: at,
|
|
846
|
-
message: "not valid JSON"
|
|
847
|
-
});
|
|
848
|
-
return {
|
|
849
|
-
ok: false,
|
|
850
|
-
entries: count,
|
|
851
|
-
problems
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
|
-
if (entry.seq !== expectedSeq) problems.push({
|
|
855
|
-
line: at,
|
|
856
|
-
message: `expected seq ${expectedSeq}, found ${entry.seq}`
|
|
857
|
-
});
|
|
858
|
-
if (entry.prev !== expectedPrev) problems.push({
|
|
859
|
-
line: at,
|
|
860
|
-
message: "does not name the previous entry; a row was removed or reordered"
|
|
861
|
-
});
|
|
862
|
-
const { seq, prev, hash,...record } = entry;
|
|
863
|
-
if (hashEntry(record, seq, prev) !== hash) problems.push({
|
|
864
|
-
line: at,
|
|
865
|
-
message: "contents do not match the sealed hash"
|
|
866
|
-
});
|
|
867
|
-
expectedPrev = entry.hash;
|
|
868
|
-
expectedSeq = entry.seq + 1;
|
|
869
|
-
}
|
|
870
|
-
return {
|
|
871
|
-
ok: problems.length === 0,
|
|
872
|
-
entries: count,
|
|
873
|
-
problems
|
|
874
|
-
};
|
|
875
|
-
}
|
|
876
|
-
var Ledger = class Ledger {
|
|
877
|
-
path;
|
|
878
|
-
seq;
|
|
879
|
-
last;
|
|
880
|
-
constructor(path, seq, last) {
|
|
881
|
-
this.path = path;
|
|
882
|
-
this.seq = seq;
|
|
883
|
-
this.last = last;
|
|
884
|
-
}
|
|
885
|
-
/**
|
|
886
|
-
* Open a ledger, reading the tail to continue an existing chain.
|
|
887
|
-
*
|
|
888
|
-
* A corrupt or truncated final line is a hard error rather than something to
|
|
889
|
-
* skip past: appending onto a chain whose end we could not read would bury
|
|
890
|
-
* the damage under valid-looking entries.
|
|
891
|
-
*/
|
|
892
|
-
static open(path) {
|
|
893
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
894
|
-
let text = "";
|
|
895
|
-
try {
|
|
896
|
-
text = readFileSync(path, "utf8");
|
|
897
|
-
} catch (error) {
|
|
898
|
-
if (error.code !== "ENOENT") throw error;
|
|
899
|
-
writeFileSync(path, "", { flag: "wx" });
|
|
900
|
-
return new Ledger(path, 0, GENESIS);
|
|
901
|
-
}
|
|
902
|
-
const tail = text.split("\n").filter((line) => line.trim() !== "").at(-1);
|
|
903
|
-
if (tail === void 0) return new Ledger(path, 0, GENESIS);
|
|
904
|
-
let entry;
|
|
905
|
-
try {
|
|
906
|
-
entry = JSON.parse(tail);
|
|
907
|
-
} catch {
|
|
908
|
-
throw new Error(`prae ledger: last line of ${path} is not readable; refusing to append onto a chain that cannot be continued`);
|
|
909
|
-
}
|
|
910
|
-
return new Ledger(path, entry.seq + 1, entry.hash);
|
|
911
|
-
}
|
|
912
|
-
/** Append one decision and return the entry as written. */
|
|
913
|
-
append(record) {
|
|
914
|
-
const entry = chain(record, this.seq, this.last);
|
|
915
|
-
appendFileSync(this.path, `${JSON.stringify(entry)}\n`);
|
|
916
|
-
this.seq += 1;
|
|
917
|
-
this.last = entry.hash;
|
|
918
|
-
return entry;
|
|
919
|
-
}
|
|
920
|
-
/** Every entry, in order. */
|
|
921
|
-
read() {
|
|
922
|
-
return readEntries(readFileSync(this.path, "utf8"));
|
|
923
|
-
}
|
|
924
|
-
/**
|
|
925
|
-
* Re-derive every hash and confirm each entry names its predecessor.
|
|
926
|
-
*
|
|
927
|
-
* Catches a deleted row, an edited field, and a reordering. Does not catch a
|
|
928
|
-
* wholesale rewrite of the file by someone holding the same code — see the
|
|
929
|
-
* note in `entry.ts`.
|
|
930
|
-
*/
|
|
931
|
-
verify() {
|
|
932
|
-
return verifyEntries(readFileSync(this.path, "utf8"));
|
|
933
|
-
}
|
|
934
|
-
};
|
|
935
|
-
|
|
936
397
|
//#endregion
|
|
937
398
|
//#region src/console/ledger.ts
|
|
938
399
|
function ago(iso, now) {
|
|
@@ -1381,8 +842,27 @@ const USAGE = `prae — operator policy for agent harnesses
|
|
|
1381
842
|
prae explain <pack.json> <tool> [key=value ...]
|
|
1382
843
|
Judge one call against the pack and print the trace. Nothing is executed.
|
|
1383
844
|
|
|
1384
|
-
prae
|
|
1385
|
-
|
|
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
|
+
|
|
856
|
+
prae verify <ledger.jsonl> [--key <public.pem>]
|
|
857
|
+
Re-derive every hash in a decision log and report any break. With
|
|
858
|
+
--key, also verify each row's signature against that pinned key.
|
|
859
|
+
|
|
860
|
+
prae keygen [--out <dir>]
|
|
861
|
+
Generate an Ed25519 keypair for signing ledger rows. Prints the
|
|
862
|
+
fingerprint a counterparty pins.
|
|
863
|
+
|
|
864
|
+
prae receipt <ledger.jsonl> <seq> --key <private.pem> [--out <file>]
|
|
865
|
+
Export one decision as a self-contained signed receipt.
|
|
1386
866
|
|
|
1387
867
|
prae report <ledger.jsonl> [--out <file.html>]
|
|
1388
868
|
Render the decision record as a printable evidence report.
|
|
@@ -1437,6 +917,70 @@ async function check(path) {
|
|
|
1437
917
|
console.log(` ${rule.id.padEnd(10)} ${rule.effect.kind.toUpperCase().padEnd(8)} ${label}`);
|
|
1438
918
|
}
|
|
1439
919
|
}
|
|
920
|
+
/**
|
|
921
|
+
* `prae scan`: the gate's pre-read inspection, runnable by hand.
|
|
922
|
+
*
|
|
923
|
+
* This is the command a pilot runs first — point it at a file that holds a
|
|
924
|
+
* credential and watch the gate see it without repeating it. Output carries
|
|
925
|
+
* type, rule, line and confidence; the matched text is structurally absent
|
|
926
|
+
* from the findings, so there is nothing here to be careful about printing.
|
|
927
|
+
*/
|
|
928
|
+
async function scanFile(path, packPath, json) {
|
|
929
|
+
const { scan } = await import("./src-Vnr0fcpK.js");
|
|
930
|
+
let content;
|
|
931
|
+
try {
|
|
932
|
+
content = await readFile(path, "utf8");
|
|
933
|
+
} catch {
|
|
934
|
+
return fail(`cannot read ${path}`);
|
|
935
|
+
}
|
|
936
|
+
const findings = scan(content);
|
|
937
|
+
if (json) console.log(JSON.stringify({
|
|
938
|
+
file: path,
|
|
939
|
+
findings
|
|
940
|
+
}, null, 2));
|
|
941
|
+
else if (findings.length === 0) console.log(`${path} — no credentials detected`);
|
|
942
|
+
else {
|
|
943
|
+
console.log(`${path} — ${findings.length} finding(s)\n`);
|
|
944
|
+
for (const f of findings) {
|
|
945
|
+
const score = `${Math.round(f.confidence * 100)}%`;
|
|
946
|
+
console.log(` line ${String(f.line).padEnd(5)} ${f.type.padEnd(18)} ${f.rule.padEnd(22)} ${score}`);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
if (packPath !== void 0) {
|
|
950
|
+
const pack = await loadPack(packPath);
|
|
951
|
+
const decision = evaluate(pack, {
|
|
952
|
+
tool: "read",
|
|
953
|
+
capability: "fs:read",
|
|
954
|
+
args: { path },
|
|
955
|
+
secrets: findings
|
|
956
|
+
});
|
|
957
|
+
const label = VERDICT_LABEL[decision.kind];
|
|
958
|
+
console.log(`\n${pack.pack} ${pack.version} would say: ${label}${decision.rule === void 0 ? "" : ` (${decision.rule})`}`);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* `prae mcp`: mount the gate as a filesystem MCP server on stdio.
|
|
963
|
+
*
|
|
964
|
+
* With no --pack, the bundled secret-catcher mounts — same day-zero logic as
|
|
965
|
+
* `init`'s baseline: a gate whose whole point is content inspection should
|
|
966
|
+
* not come up inspecting nothing.
|
|
967
|
+
*/
|
|
968
|
+
async function mcp(root, packPath, ledgerPath) {
|
|
969
|
+
const { serveMcp } = await import("./mcp-BH5I4Eyu.js");
|
|
970
|
+
const { resolve: resolve$1 } = await import("node:path");
|
|
971
|
+
const bundled = [join(import.meta.dirname, "secret-catcher.prae.json"), join(import.meta.dirname, "..", "..", "..", "examples", "secret-catcher.prae.json")].find((candidate) => existsSync(candidate));
|
|
972
|
+
const chosen = packPath ?? bundled;
|
|
973
|
+
if (chosen === void 0) return fail("mcp needs --pack <file>; no bundled secret-catcher found");
|
|
974
|
+
const pack = await loadPack(chosen);
|
|
975
|
+
const { Ledger: Ledger$1 } = await import("./src-CBK_quVO.js");
|
|
976
|
+
const ledger = ledgerPath === void 0 ? void 0 : Ledger$1.open(ledgerPath);
|
|
977
|
+
console.error(`prae mcp: serving ${resolve$1(root)} under ${pack.pack} ${pack.version}` + (ledgerPath === void 0 ? " (no ledger)" : ` · ledger ${ledgerPath}`));
|
|
978
|
+
await serveMcp({
|
|
979
|
+
pack,
|
|
980
|
+
root: resolve$1(root),
|
|
981
|
+
...ledger === void 0 ? {} : { ledger }
|
|
982
|
+
});
|
|
983
|
+
}
|
|
1440
984
|
/** `key=value` pairs, with numbers and booleans read as themselves. */
|
|
1441
985
|
function parseArgs(pairs) {
|
|
1442
986
|
const args = {};
|
|
@@ -1467,12 +1011,57 @@ async function explain(path, tool, pairs) {
|
|
|
1467
1011
|
if (decision.trace.length === 0) console.log("\n no rule matched this call");
|
|
1468
1012
|
else {
|
|
1469
1013
|
console.log("");
|
|
1470
|
-
for (const step
|
|
1014
|
+
for (const step of decision.trace) console.log(` ${step.rule.padEnd(10)} ${step.effect.padEnd(8)} ${step.because}`);
|
|
1471
1015
|
}
|
|
1472
1016
|
console.log(`\n${pack.pack} ${pack.version} · ${decision.evaluated} rules considered`);
|
|
1473
1017
|
if (capability === void 0) console.log(`\nnote: no capability claims \`${tool}\`, so capability rules did not apply`);
|
|
1474
1018
|
}
|
|
1475
|
-
|
|
1019
|
+
/**
|
|
1020
|
+
* Keys for signing rows.
|
|
1021
|
+
*
|
|
1022
|
+
* Written 0600 and never printed. What is printed is the fingerprint — the
|
|
1023
|
+
* thing a counterparty pins out-of-band, because a signature checked against
|
|
1024
|
+
* a key that arrived beside it is checked against nothing.
|
|
1025
|
+
*/
|
|
1026
|
+
async function keygen(dir) {
|
|
1027
|
+
const { generateKeypair, fingerprint } = await import("./src-CBK_quVO.js");
|
|
1028
|
+
const { writeFileSync: writeFileSync$1 } = await import("node:fs");
|
|
1029
|
+
const keys = generateKeypair();
|
|
1030
|
+
const priv = join(dir, "prae-signing.pem");
|
|
1031
|
+
const pub = join(dir, "prae-signing.pub.pem");
|
|
1032
|
+
writeFileSync$1(priv, keys.privateKey, { mode: 384 });
|
|
1033
|
+
writeFileSync$1(pub, keys.publicKey);
|
|
1034
|
+
console.log(`private ${priv} (0600 — this signs; guard it)`);
|
|
1035
|
+
console.log(`public ${pub}`);
|
|
1036
|
+
console.log(`fingerprint ${fingerprint(keys.publicKey)}`);
|
|
1037
|
+
console.log("");
|
|
1038
|
+
console.log("Give the PUBLIC key and fingerprint to whoever will verify");
|
|
1039
|
+
console.log("receipts, through a channel they trust. Point the plugin at the");
|
|
1040
|
+
console.log("private key with `signingKey` to sign every row.");
|
|
1041
|
+
}
|
|
1042
|
+
async function receipt(path, seq, keyPath, out) {
|
|
1043
|
+
const { Ledger: Ledger$1, makeReceipt, fingerprint } = await import("./src-CBK_quVO.js");
|
|
1044
|
+
const { readFileSync: readFileSync$1 } = await import("node:fs");
|
|
1045
|
+
const { createPublicKey } = await import("node:crypto");
|
|
1046
|
+
const privateKey = readFileSync$1(keyPath, "utf8");
|
|
1047
|
+
const publicKey = createPublicKey(privateKey).export({
|
|
1048
|
+
type: "spki",
|
|
1049
|
+
format: "pem"
|
|
1050
|
+
}).toString();
|
|
1051
|
+
const entry = Ledger$1.open(path).read().find((row$1) => row$1.seq === seq);
|
|
1052
|
+
if (entry === void 0) return fail(`no row with seq ${seq} in ${path}`);
|
|
1053
|
+
const packaged = makeReceipt(entry, privateKey, publicKey);
|
|
1054
|
+
const text = `${JSON.stringify(packaged, null, 2)}\n`;
|
|
1055
|
+
if (out === void 0) console.log(text);
|
|
1056
|
+
else {
|
|
1057
|
+
await writeFile(out, text);
|
|
1058
|
+
console.log(`${out} · seq ${seq} · ${entry.verdict} ${entry.tool} ${entry.subject ?? ""}`);
|
|
1059
|
+
}
|
|
1060
|
+
console.log(`fingerprint ${fingerprint(publicKey)}`);
|
|
1061
|
+
console.log("The verifier compares that fingerprint against the one they");
|
|
1062
|
+
console.log("pinned — a receipt is only as good as the key they trust.");
|
|
1063
|
+
}
|
|
1064
|
+
async function verify(path, keyPath) {
|
|
1476
1065
|
let ledger;
|
|
1477
1066
|
try {
|
|
1478
1067
|
ledger = Ledger.open(path);
|
|
@@ -1481,8 +1070,26 @@ async function verify(path) {
|
|
|
1481
1070
|
}
|
|
1482
1071
|
const result = ledger.verify();
|
|
1483
1072
|
if (result.ok) {
|
|
1484
|
-
const
|
|
1073
|
+
const entries = ledger.read();
|
|
1074
|
+
const last = entries.at(-1);
|
|
1485
1075
|
console.log(`${result.entries} entries · chain intact${last === void 0 ? "" : ` · seal ${seal(last)}`}`);
|
|
1076
|
+
if (keyPath !== void 0) {
|
|
1077
|
+
const { verifySignature, fingerprint } = await import("./src-CBK_quVO.js");
|
|
1078
|
+
const { readFileSync: readFileSync$1 } = await import("node:fs");
|
|
1079
|
+
const publicKey = readFileSync$1(keyPath, "utf8");
|
|
1080
|
+
let signed = 0;
|
|
1081
|
+
const bad = [];
|
|
1082
|
+
for (const entry of entries) {
|
|
1083
|
+
if (entry.sig === void 0) continue;
|
|
1084
|
+
signed += 1;
|
|
1085
|
+
if (!verifySignature(entry, entry.sig, publicKey)) bad.push(entry.seq);
|
|
1086
|
+
}
|
|
1087
|
+
if (bad.length > 0) {
|
|
1088
|
+
console.error(`signatures: ${bad.length} of ${signed} FAIL against ${fingerprint(publicKey)} — rows ${bad.join(", ")}`);
|
|
1089
|
+
process.exitCode = 1;
|
|
1090
|
+
} else if (signed === 0) console.log("signatures: none present — this ledger is not signed");
|
|
1091
|
+
else console.log(`signatures: ${signed}/${entries.length} rows verify against ${fingerprint(publicKey)}`);
|
|
1092
|
+
}
|
|
1486
1093
|
console.log("tamper-evident: a removed, edited, or reordered row breaks this chain.");
|
|
1487
1094
|
console.log("It does not prove against a rewrite of the whole file by someone holding this code.");
|
|
1488
1095
|
return;
|
|
@@ -1543,7 +1150,7 @@ async function report(path, out) {
|
|
|
1543
1150
|
* that step is a proposal a human approves, and it starts from this list.
|
|
1544
1151
|
*/
|
|
1545
1152
|
async function instructions(from, asJson) {
|
|
1546
|
-
const { ingest } = await import("./src-
|
|
1153
|
+
const { ingest } = await import("./src-CCeafjdm.js");
|
|
1547
1154
|
const result = ingest(from);
|
|
1548
1155
|
if (asJson) {
|
|
1549
1156
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -1579,7 +1186,7 @@ async function instructions(from, asJson) {
|
|
|
1579
1186
|
* on purpose rather than something that happened to their policy.
|
|
1580
1187
|
*/
|
|
1581
1188
|
async function propose(from, options) {
|
|
1582
|
-
const [{ ingest }, { extract }] = await Promise.all([import("./src-
|
|
1189
|
+
const [{ ingest }, { extract }] = await Promise.all([import("./src-CCeafjdm.js"), import("./src-aU5WW02C.js")]);
|
|
1583
1190
|
const read = ingest(from);
|
|
1584
1191
|
if (read.files.length === 0) {
|
|
1585
1192
|
console.log(`No instruction files found at or above ${from}.`);
|
|
@@ -1639,9 +1246,9 @@ async function consoleOut(path, argv) {
|
|
|
1639
1246
|
const instructionsDir = flag(argv, "instructions");
|
|
1640
1247
|
if (instructionsDir !== void 0) {
|
|
1641
1248
|
const [{ ingest }, { extract }, { renderInstructions }] = await Promise.all([
|
|
1642
|
-
import("./src-
|
|
1643
|
-
import("./src-
|
|
1644
|
-
import("./instructions-
|
|
1249
|
+
import("./src-CCeafjdm.js"),
|
|
1250
|
+
import("./src-aU5WW02C.js"),
|
|
1251
|
+
import("./instructions-D4VOn2lk.js")
|
|
1645
1252
|
]);
|
|
1646
1253
|
const read = ingest(instructionsDir);
|
|
1647
1254
|
instructionsView = renderInstructions(data, read, extract(read.statements));
|
|
@@ -1740,10 +1347,27 @@ async function main(argv) {
|
|
|
1740
1347
|
if (path === void 0 || tool === void 0) fail("explain needs a pack file and a tool name");
|
|
1741
1348
|
return explain(path, tool, pairs);
|
|
1742
1349
|
}
|
|
1350
|
+
case "scan": {
|
|
1351
|
+
const path = rest[0];
|
|
1352
|
+
if (path === void 0) fail("scan needs a file");
|
|
1353
|
+
return scanFile(path, flag(rest, "pack"), rest.includes("--json"));
|
|
1354
|
+
}
|
|
1355
|
+
case "mcp": {
|
|
1356
|
+
const root = flag(rest, "root");
|
|
1357
|
+
if (root === void 0) fail("mcp needs --root <dir>; serving without a boundary is not a gate");
|
|
1358
|
+
return mcp(root, flag(rest, "pack"), flag(rest, "ledger"));
|
|
1359
|
+
}
|
|
1360
|
+
case "keygen": return keygen(flag(rest, "out") ?? ".");
|
|
1361
|
+
case "receipt": {
|
|
1362
|
+
const [path, seq] = rest;
|
|
1363
|
+
const key = flag(rest, "key");
|
|
1364
|
+
if (path === void 0 || seq === void 0 || key === void 0) fail("receipt needs <ledger.jsonl> <seq> --key <private.pem>");
|
|
1365
|
+
return receipt(path, Number(seq), key, flag(rest, "out"));
|
|
1366
|
+
}
|
|
1743
1367
|
case "verify": {
|
|
1744
1368
|
const path = rest[0];
|
|
1745
1369
|
if (path === void 0) fail("verify needs a ledger file");
|
|
1746
|
-
return verify(path);
|
|
1370
|
+
return verify(path, flag(rest, "key"));
|
|
1747
1371
|
}
|
|
1748
1372
|
case "report": {
|
|
1749
1373
|
const path = rest[0];
|