prae-cli 0.6.2 → 0.7.1

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/lib/bin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import "./evaluate-CkYySl7T.js";
3
- import "./views-CAD9738P.js";
4
- import { t as main } from "./cli-BUgU-LpN.js";
2
+ import "./evaluate-wMPkjcIC.js";
3
+ import "./views-UODslKwu.js";
4
+ import { t as main } from "./cli-mahLCjoD.js";
5
5
  import "./src-Ciggx9ja.js";
6
6
 
7
7
  //#region src/bin.ts
@@ -1,7 +1,7 @@
1
- import { t as evaluate } from "./evaluate-CkYySl7T.js";
2
- import { a as renderPacks, c as renderShell, d as cite, i as renderOverview, l as VERDICT_LABEL$1, n as renderApprovals, o as esc, r as renderCompliance, s as renderNav, t as navFor, u as FRAMEWORKS } from "./views-CAD9738P.js";
3
- import { m as seal, t as Ledger } from "./src-Ciggx9ja.js";
4
- import { existsSync, readFileSync } from "node:fs";
1
+ import { t as evaluate } from "./evaluate-wMPkjcIC.js";
2
+ import { a as renderPacks, c as renderNav, d as FRAMEWORKS, f as cite, i as renderOverview, l as renderShell, n as renderApprovals, o as CONTROLS, r as renderCompliance, s as esc, t as navFor, u as VERDICT_LABEL$1 } from "./views-UODslKwu.js";
3
+ import { f as chain, i as fingerprint, l as verifySignature, m as seal, n as readEntries, r as verifyEntries, t as Ledger, u as GENESIS } from "./src-Ciggx9ja.js";
4
+ import { existsSync, readFileSync, readdirSync, statSync, watch } from "node:fs";
5
5
  import { mkdir, readFile, writeFile } from "node:fs/promises";
6
6
  import { dirname, extname, join, normalize, resolve, sep } from "node:path";
7
7
  import { homedir } from "node:os";
@@ -72,9 +72,11 @@ function readCommandMatch(check$1, path, value) {
72
72
  check$1.keys(path, value, [
73
73
  "name",
74
74
  "token",
75
- "pipedTo"
75
+ "anyToken",
76
+ "pipedTo",
77
+ "pipeline"
76
78
  ]);
77
- if (value.name === void 0 && value.token === void 0 && value.pipedTo === void 0) check$1.fail(path, "expected `name`, `token` or `pipedTo`; a command matcher with none matches nothing");
79
+ if (value.name === void 0 && value.token === void 0 && value.anyToken === void 0 && value.pipedTo === void 0 && value.pipeline === void 0) check$1.fail(path, "expected `name`, `token`, `anyToken`, `pipedTo` or `pipeline`; a command matcher with none matches nothing");
78
80
  const out = {};
79
81
  if (value.token !== void 0) {
80
82
  const tokens = check$1.names(`${path}.token`, value.token);
@@ -84,10 +86,18 @@ function readCommandMatch(check$1, path, value) {
84
86
  const names = check$1.names(`${path}.name`, value.name);
85
87
  if (names !== void 0) out.name = names;
86
88
  }
89
+ if (value.anyToken !== void 0) {
90
+ const tokens = check$1.names(`${path}.anyToken`, value.anyToken);
91
+ if (tokens !== void 0) out.anyToken = tokens;
92
+ }
87
93
  if (value.pipedTo !== void 0) {
88
94
  const pipes = check$1.names(`${path}.pipedTo`, value.pipedTo);
89
95
  if (pipes !== void 0) out.pipedTo = pipes;
90
96
  }
97
+ if (value.pipeline !== void 0) {
98
+ const shapes = check$1.names(`${path}.pipeline`, value.pipeline);
99
+ if (shapes !== void 0) out.pipeline = shapes;
100
+ }
91
101
  return out;
92
102
  }
93
103
  function readPathMatch(check$1, path, value) {
@@ -752,7 +762,9 @@ function praeRow(options) {
752
762
  ];
753
763
  if (options.ledger !== void 0) lines.push(` ledger: ${JSON.stringify(options.ledger)}`);
754
764
  if (options.approver !== void 0) lines.push(` approver: ${JSON.stringify(options.approver)}`);
755
- lines.push(" pack: |", indent(options.pack, 10));
765
+ if (options.mode !== void 0) lines.push(` mode: ${options.mode}`);
766
+ if (options.packFile !== void 0) lines.push(` packFile: ${JSON.stringify(options.packFile)}`);
767
+ else lines.push(" pack: |", indent(options.pack ?? "", 10));
756
768
  return lines.join("\n");
757
769
  }
758
770
  /** Compose the write and check it, without touching the file. */
@@ -792,6 +804,60 @@ async function applyWrite(profile, plan, expectedRevision) {
792
804
  return { snapshot: manifest.id };
793
805
  }
794
806
 
807
+ //#endregion
808
+ //#region src/record-files.ts
809
+ function jsonOrNull(path) {
810
+ if (!existsSync(path)) return null;
811
+ try {
812
+ return JSON.parse(readFileSync(path, "utf8"));
813
+ } catch {
814
+ return null;
815
+ }
816
+ }
817
+ function readLedger(path) {
818
+ if (!existsSync(path)) return [];
819
+ const out = [];
820
+ for (const line of readFileSync(path, "utf8").split("\n")) {
821
+ if (line.trim() === "") continue;
822
+ try {
823
+ out.push(JSON.parse(line));
824
+ } catch {}
825
+ }
826
+ return out;
827
+ }
828
+ /** The defaults the demo writes; a caller with other names overrides them. */
829
+ const DEFAULT_FILES = {
830
+ ledger: "papaya.jsonl",
831
+ discovery: "discovery.json",
832
+ report: "security-report.json",
833
+ guardians: "guardians.json",
834
+ pack: "papaya.prae.json",
835
+ publicKey: "prae-signing.pub.pem",
836
+ redteam: "../redteam/results"
837
+ };
838
+ function readRecord(dir, files) {
839
+ return {
840
+ ledger: readLedger(resolve(dir, files.ledger)),
841
+ discovery: jsonOrNull(resolve(dir, files.discovery)),
842
+ report: jsonOrNull(resolve(dir, files.report)),
843
+ guardians: jsonOrNull(resolve(dir, files.guardians)),
844
+ pack: jsonOrNull(resolve(dir, files.pack))
845
+ };
846
+ }
847
+ /** The red-team campaign and its runs, as the runner wrote them; empty when none. */
848
+ function readRedteam(dir, files) {
849
+ const root = resolve(dir, files.redteam);
850
+ if (!existsSync(root)) return {
851
+ campaign: null,
852
+ runs: []
853
+ };
854
+ const runs = readdirSync(root).filter((n) => n.endsWith(".json")).sort().filter((n) => n !== "campaign.json").map((n) => jsonOrNull(resolve(root, n))).filter((r) => r !== null);
855
+ return {
856
+ campaign: jsonOrNull(resolve(root, "campaign.json")),
857
+ runs
858
+ };
859
+ }
860
+
795
861
  //#endregion
796
862
  //#region src/serve.ts
797
863
  const TYPES = {
@@ -823,8 +889,233 @@ function safePath(root, urlPath) {
823
889
  if (target !== base && !target.startsWith(base + sep)) return void 0;
824
890
  return target;
825
891
  }
826
- /** Start the reader. Resolves once it is listening. */
827
- function serveConsole(root, port) {
892
+ /**
893
+ * Verify the ledger the way `prae verify` does — chain, then signatures
894
+ * against the public key beside it. With `tamper`, one row's refusal is
895
+ * changed to an allow in memory first and the altered text is what gets
896
+ * verified: the forged-copy demonstration, without writing a file. With
897
+ * `repair` as well, every hash is recomputed over the altered rows — what
898
+ * an attacker would do to mend the chain — and the signatures, made over the
899
+ * original hashes by a key they do not hold, are what fails. Nothing on disk
900
+ * changes.
901
+ */
902
+ function verifyRecord(record, tamper, repair = false) {
903
+ const { dir, files } = record;
904
+ const path = resolve(dir, files.ledger);
905
+ if (!existsSync(path)) return {
906
+ ok: false,
907
+ entries: 0,
908
+ problems: [{
909
+ line: 0,
910
+ message: "no ledger"
911
+ }],
912
+ tampered: null,
913
+ repaired: false,
914
+ signatures: null
915
+ };
916
+ let text = readFileSync(path, "utf8");
917
+ let tampered = null;
918
+ if (tamper !== void 0) {
919
+ const lines = text.trimEnd().split("\n");
920
+ const idx = lines.findIndex((line) => {
921
+ try {
922
+ return JSON.parse(line).seq === tamper;
923
+ } catch {
924
+ return false;
925
+ }
926
+ });
927
+ if (idx >= 0) {
928
+ const row$1 = JSON.parse(lines[idx]);
929
+ row$1.verdict = "allow";
930
+ row$1.reason = "looks fine to me";
931
+ lines[idx] = JSON.stringify(row$1);
932
+ if (repair) {
933
+ let prev = GENESIS;
934
+ for (let i = 0; i < lines.length; i += 1) {
935
+ const { seq, prev: _prev, hash: _hash, sig,...rest } = JSON.parse(lines[i]);
936
+ const entry = chain(rest, seq, prev);
937
+ lines[i] = JSON.stringify(sig === void 0 ? entry : {
938
+ ...entry,
939
+ sig
940
+ });
941
+ prev = entry.hash;
942
+ }
943
+ }
944
+ text = `${lines.join("\n")}\n`;
945
+ tampered = tamper;
946
+ }
947
+ }
948
+ const verified = verifyEntries(text);
949
+ const keyPath = resolve(dir, files.publicKey);
950
+ let signatures = null;
951
+ if (existsSync(keyPath)) {
952
+ const publicKey = readFileSync(keyPath, "utf8");
953
+ let entries = [];
954
+ try {
955
+ entries = readEntries(text);
956
+ } catch {
957
+ entries = [];
958
+ }
959
+ const signed = entries.filter((e) => e.sig !== void 0);
960
+ signatures = {
961
+ checked: signed.length,
962
+ failed: signed.filter((e) => !verifySignature(e, e.sig, publicKey)).map((e) => e.seq),
963
+ fingerprint: fingerprint(publicKey)
964
+ };
965
+ }
966
+ return {
967
+ ok: verified.ok,
968
+ entries: verified.entries,
969
+ problems: [...verified.problems],
970
+ tampered,
971
+ repaired: tampered !== null && repair,
972
+ signatures
973
+ };
974
+ }
975
+ const OWASP = "owasp-llm-top-10-2026";
976
+ /**
977
+ * Evidence toward each control, from the pack and the ledger as they are
978
+ * now — the rows the controls package defines, so the console, the exporter and
979
+ * the old pages cannot disagree about what a profile shows. Framed as
980
+ * evidence, never as compliance: an auditor decides.
981
+ */
982
+ function complianceOf(record) {
983
+ const { dir, files } = record;
984
+ const ledgerPath = resolve(dir, files.ledger);
985
+ const text = existsSync(ledgerPath) ? readFileSync(ledgerPath, "utf8") : "";
986
+ let entries = [];
987
+ try {
988
+ entries = readEntries(text);
989
+ } catch {
990
+ entries = [];
991
+ }
992
+ const packPath = resolve(dir, files.pack);
993
+ let pack;
994
+ if (existsSync(packPath)) {
995
+ const parsed = parsePack(readFileSync(packPath, "utf8"));
996
+ if (parsed.ok) pack = parsed.pack;
997
+ }
998
+ const data = {
999
+ ...pack === void 0 ? {} : { pack },
1000
+ entries,
1001
+ verification: verifyEntries(text),
1002
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
1003
+ };
1004
+ const owasp = FRAMEWORKS[OWASP]?.controls ?? {};
1005
+ return { controls: CONTROLS.map((control) => {
1006
+ const code = /LLM\d\d/.exec(control.id)?.[0];
1007
+ const named = code === void 0 ? void 0 : owasp[code];
1008
+ return {
1009
+ id: code ?? control.id,
1010
+ ...named === void 0 ? {} : { name: named.name },
1011
+ asks: control.asks,
1012
+ framework: named === void 0 ? "obligation" : OWASP,
1013
+ ...control.evidence(data)
1014
+ };
1015
+ }) };
1016
+ }
1017
+ /**
1018
+ * The record over HTTP, for the Command Center (apps/console): the same
1019
+ * files `prae console` renders, as JSON, plus one event stream that says
1020
+ * when the ledger moved. Read-only, from files on disk, nothing computed
1021
+ * here — the console derives everything it shows from these bodies.
1022
+ *
1023
+ * Returns false when the URL is not an API route, so a caller can fall
1024
+ * through to static files. Shared with the console's Vite dev server so the
1025
+ * two answer identically.
1026
+ */
1027
+ function handleApi(req, res, record) {
1028
+ const route = (req.url ?? "").split("?")[0] ?? "";
1029
+ if (!route.startsWith("/api/")) return false;
1030
+ const json = (body, status = 200) => {
1031
+ res.statusCode = status;
1032
+ res.setHeader("content-type", "application/json; charset=utf-8");
1033
+ res.setHeader("cache-control", "no-store");
1034
+ res.end(JSON.stringify(body));
1035
+ };
1036
+ const { dir, files } = record;
1037
+ if (route === "/api/events") {
1038
+ res.writeHead(200, {
1039
+ "content-type": "text/event-stream",
1040
+ "cache-control": "no-store",
1041
+ connection: "keep-alive"
1042
+ });
1043
+ res.write(": connected\n\n");
1044
+ let sent = readRecord(dir, files).ledger.length;
1045
+ const push = () => {
1046
+ const rows = readRecord(dir, files).ledger;
1047
+ if (rows.length < sent) sent = 0;
1048
+ const fresh = rows.slice(sent);
1049
+ sent = rows.length;
1050
+ if (fresh.length > 0) res.write(`data: ${JSON.stringify(fresh)}\n\n`);
1051
+ else res.write(": keepalive\n\n");
1052
+ };
1053
+ const watcher = existsSync(dir) ? watch(dir, { persistent: false }, () => push()) : void 0;
1054
+ const keep = setInterval(push, 2e3);
1055
+ res.on("close", () => {
1056
+ clearInterval(keep);
1057
+ watcher?.close();
1058
+ });
1059
+ return true;
1060
+ }
1061
+ if (route === "/api/verify") {
1062
+ const raw = new URL(req.url ?? "/", "http://localhost").searchParams.get("tamper");
1063
+ const tamper = raw === null ? void 0 : Number(raw);
1064
+ const repair = new URL(req.url ?? "/", "http://localhost").searchParams.get("repair") === "1";
1065
+ json(verifyRecord(record, Number.isInteger(tamper) ? tamper : void 0, repair));
1066
+ return true;
1067
+ }
1068
+ if (route === "/api/compliance") {
1069
+ json(complianceOf(record));
1070
+ return true;
1071
+ }
1072
+ if (route === "/api/redteam") {
1073
+ json(readRedteam(dir, files));
1074
+ return true;
1075
+ }
1076
+ const snapshot = readRecord(dir, files);
1077
+ switch (route) {
1078
+ case "/api/record":
1079
+ json(snapshot);
1080
+ break;
1081
+ case "/api/ledger":
1082
+ json(snapshot.ledger);
1083
+ break;
1084
+ case "/api/discovery":
1085
+ json(snapshot.discovery);
1086
+ break;
1087
+ case "/api/report":
1088
+ json(snapshot.report);
1089
+ break;
1090
+ case "/api/guardians":
1091
+ json(snapshot.guardians);
1092
+ break;
1093
+ case "/api/pack":
1094
+ json(snapshot.pack);
1095
+ break;
1096
+ case "/api/status":
1097
+ json({
1098
+ dir,
1099
+ files: Object.fromEntries(Object.entries(files).map(([name, file]) => {
1100
+ const path = resolve(dir, file);
1101
+ if (!existsSync(path)) return [name, null];
1102
+ const stat$1 = statSync(path);
1103
+ return [name, {
1104
+ size: stat$1.size,
1105
+ mtime: stat$1.mtimeMs
1106
+ }];
1107
+ }))
1108
+ });
1109
+ break;
1110
+ default: json({ error: "not found" }, 404);
1111
+ }
1112
+ return true;
1113
+ }
1114
+ /**
1115
+ * Start the reader. Resolves once it is listening. With a record source,
1116
+ * `/api/*` answers from it; without one, only the rendered pages are served.
1117
+ */
1118
+ function serveConsole(root, port, record) {
828
1119
  const server = createServer((req, res) => {
829
1120
  if (req.method !== "GET" && req.method !== "HEAD") {
830
1121
  res.statusCode = 405;
@@ -832,6 +1123,7 @@ function serveConsole(root, port) {
832
1123
  res.end();
833
1124
  return;
834
1125
  }
1126
+ if (record !== void 0 && handleApi(req, res, record)) return;
835
1127
  const target = safePath(root, req.url ?? "/");
836
1128
  if (target === void 0) {
837
1129
  res.statusCode = 403;
@@ -852,9 +1144,10 @@ function serveConsole(root, port) {
852
1144
  return new Promise((ok, fail$1) => {
853
1145
  server.once("error", fail$1);
854
1146
  server.listen(port, "127.0.0.1", () => {
1147
+ const address = server.address();
855
1148
  ok({
856
1149
  server,
857
- url: `http://127.0.0.1:${port}/`
1150
+ url: `http://127.0.0.1:${typeof address === "object" && address !== null ? address.port : port}/`
858
1151
  });
859
1152
  });
860
1153
  });
@@ -869,7 +1162,7 @@ const VERDICT_LABEL = {
869
1162
  deny: "DENIED",
870
1163
  correct: "CORRECTED"
871
1164
  };
872
- const USAGE = `prae — operator policy for agent harnesses
1165
+ const USAGE = `⌜▪⌟ prae — operator policy for agent harnesses
873
1166
 
874
1167
  prae check <pack.json>
875
1168
  Validate a policy pack and summarise what it enforces.
@@ -931,9 +1224,11 @@ const USAGE = `prae — operator policy for agent harnesses
931
1224
 
932
1225
  prae init [--pack <file>] [--profile <dir>] [--ledger <file>]
933
1226
  (no --pack mounts the built-in baseline of 8 high-risk rules)
934
- [--approver <name>] [--apply]
1227
+ [--approver <name>] [--link] [--shadow] [--apply]
935
1228
  Compose the overlay row that mounts PRAE with a pack. Prints the
936
- diff; writes only with --apply.
1229
+ diff; writes only with --apply. --link points the profile at the
1230
+ pack file instead of copying it in, so edits hot-reload; --shadow
1231
+ mounts in shadow mode: every verdict sealed, every call let through.
937
1232
  `;
938
1233
  function fail(message) {
939
1234
  console.error(`prae: ${message}`);
@@ -1015,7 +1310,7 @@ async function scanFile(path, packPath, json) {
1015
1310
  * not come up inspecting nothing.
1016
1311
  */
1017
1312
  async function mcp(root, packPath, ledgerPath) {
1018
- const { serveMcp } = await import("./mcp-Bq0DNPPR.js");
1313
+ const { serveMcp } = await import("./mcp-DDcejhwQ.js");
1019
1314
  const { resolve: resolve$1 } = await import("node:path");
1020
1315
  const bundled = [join(import.meta.dirname, "secret-catcher.prae.json"), join(import.meta.dirname, "..", "..", "..", "examples", "secret-catcher.prae.json")].find((candidate) => existsSync(candidate));
1021
1316
  const chosen = packPath ?? bundled;
@@ -1073,7 +1368,7 @@ async function explain(path, tool, pairs) {
1073
1368
  * a key that arrived beside it is checked against nothing.
1074
1369
  */
1075
1370
  async function keygen(dir) {
1076
- const { generateKeypair, fingerprint } = await import("./src-C9L6NBvu.js");
1371
+ const { generateKeypair, fingerprint: fingerprint$1 } = await import("./src-C9L6NBvu.js");
1077
1372
  const { writeFileSync: writeFileSync$1 } = await import("node:fs");
1078
1373
  const keys = generateKeypair();
1079
1374
  const priv = join(dir, "prae-signing.pem");
@@ -1082,14 +1377,14 @@ async function keygen(dir) {
1082
1377
  writeFileSync$1(pub, keys.publicKey);
1083
1378
  console.log(`private ${priv} (0600 — this signs; guard it)`);
1084
1379
  console.log(`public ${pub}`);
1085
- console.log(`fingerprint ${fingerprint(keys.publicKey)}`);
1380
+ console.log(`fingerprint ${fingerprint$1(keys.publicKey)}`);
1086
1381
  console.log("");
1087
1382
  console.log("Give the PUBLIC key and fingerprint to whoever will verify");
1088
1383
  console.log("receipts, through a channel they trust. Point the plugin at the");
1089
1384
  console.log("private key with `signingKey` to sign every row.");
1090
1385
  }
1091
1386
  async function receipt(path, seq, keyPath, out) {
1092
- const { Ledger: Ledger$1, makeReceipt, fingerprint } = await import("./src-C9L6NBvu.js");
1387
+ const { Ledger: Ledger$1, makeReceipt, fingerprint: fingerprint$1 } = await import("./src-C9L6NBvu.js");
1093
1388
  const { readFileSync: readFileSync$1 } = await import("node:fs");
1094
1389
  const { createPublicKey } = await import("node:crypto");
1095
1390
  const privateKey = readFileSync$1(keyPath, "utf8");
@@ -1106,7 +1401,7 @@ async function receipt(path, seq, keyPath, out) {
1106
1401
  await writeFile(out, text);
1107
1402
  console.log(`${out} · seq ${seq} · ${entry.verdict} ${entry.tool} ${entry.subject ?? ""}`);
1108
1403
  }
1109
- console.log(`fingerprint ${fingerprint(publicKey)}`);
1404
+ console.log(`fingerprint ${fingerprint$1(publicKey)}`);
1110
1405
  console.log("The verifier compares that fingerprint against the one they");
1111
1406
  console.log("pinned — a receipt is only as good as the key they trust.");
1112
1407
  }
@@ -1150,7 +1445,7 @@ async function test(suitePath, packOverride, asJson) {
1150
1445
  } catch (error) {
1151
1446
  return fail(`${suitePath} is not valid JSON: ${error instanceof Error ? error.message : error}`);
1152
1447
  }
1153
- const { parseSuite, renderSuite, runSuite } = await import("./test-run-04ubUcSb.js");
1448
+ const { parseSuite, renderSuite, runSuite } = await import("./test-run-DOaCKD1t.js");
1154
1449
  const suite = parseSuite(parsed);
1155
1450
  if (!suite.ok) {
1156
1451
  console.error(`prae: ${suitePath} is not a valid test suite`);
@@ -1190,7 +1485,7 @@ async function verify(path, keyPath) {
1190
1485
  const last = entries.at(-1);
1191
1486
  console.log(`${result.entries} entries · chain intact${last === void 0 ? "" : ` · seal ${seal(last)}`}`);
1192
1487
  if (keyPath !== void 0) {
1193
- const { verifySignature, fingerprint } = await import("./src-C9L6NBvu.js");
1488
+ const { verifySignature: verifySignature$1, fingerprint: fingerprint$1 } = await import("./src-C9L6NBvu.js");
1194
1489
  const { readFileSync: readFileSync$1 } = await import("node:fs");
1195
1490
  const publicKey = readFileSync$1(keyPath, "utf8");
1196
1491
  let signed = 0;
@@ -1198,13 +1493,13 @@ async function verify(path, keyPath) {
1198
1493
  for (const entry of entries) {
1199
1494
  if (entry.sig === void 0) continue;
1200
1495
  signed += 1;
1201
- if (!verifySignature(entry, entry.sig, publicKey)) bad.push(entry.seq);
1496
+ if (!verifySignature$1(entry, entry.sig, publicKey)) bad.push(entry.seq);
1202
1497
  }
1203
1498
  if (bad.length > 0) {
1204
- console.error(`signatures: ${bad.length} of ${signed} FAIL against ${fingerprint(publicKey)} — rows ${bad.join(", ")}`);
1499
+ console.error(`signatures: ${bad.length} of ${signed} FAIL against ${fingerprint$1(publicKey)} — rows ${bad.join(", ")}`);
1205
1500
  process.exitCode = 1;
1206
1501
  } else if (signed === 0) console.log("signatures: none present — this ledger is not signed");
1207
- else console.log(`signatures: ${signed}/${entries.length} rows verify against ${fingerprint(publicKey)}`);
1502
+ else console.log(`signatures: ${signed}/${entries.length} rows verify against ${fingerprint$1(publicKey)}`);
1208
1503
  }
1209
1504
  console.log("tamper-evident: a removed, edited, or reordered row breaks this chain.");
1210
1505
  console.log("It does not prove against a rewrite of the whole file by someone holding this code.");
@@ -1366,7 +1661,7 @@ async function consoleOut(path, argv) {
1366
1661
  const [{ ingest }, { extract }, { renderInstructions }] = await Promise.all([
1367
1662
  import("./src-CraUX64d.js"),
1368
1663
  import("./src-CQ0UmFUK.js"),
1369
- import("./instructions-oGCdVxHM.js")
1664
+ import("./instructions-C104EUaE.js")
1370
1665
  ]);
1371
1666
  const read = ingest(instructionsDir);
1372
1667
  instructionsView = renderInstructions(data, read, extract(read.statements));
@@ -1395,8 +1690,16 @@ async function consoleOut(path, argv) {
1395
1690
  const portFlag = flag(argv, "serve");
1396
1691
  const port = portFlag === void 0 ? 4180 : Number(portFlag);
1397
1692
  if (!Number.isInteger(port) || port < 1 || port > 65535) fail(`--serve takes a port, got \`${portFlag}\``);
1398
- const { url } = await serveConsole(out, port);
1693
+ const { url } = await serveConsole(out, port, {
1694
+ dir: dirname(resolve(path)),
1695
+ files: {
1696
+ ...DEFAULT_FILES,
1697
+ ledger: resolve(path),
1698
+ ...packPath === void 0 ? {} : { pack: resolve(packPath) }
1699
+ }
1700
+ });
1399
1701
  console.log(`\nserving ${out} at ${url}`);
1702
+ console.log(`record at ${url}api/record · events at ${url}api/events`);
1400
1703
  console.log("loopback only, read-only. ctrl-c to stop.");
1401
1704
  }
1402
1705
  async function init(argv) {
@@ -1413,8 +1716,11 @@ async function init(argv) {
1413
1716
  const profile = profileDir === void 0 ? defaultProfile() : profileAt(profileDir);
1414
1717
  const ledger = flag(argv, "ledger");
1415
1718
  const approver = flag(argv, "approver");
1719
+ const link = argv.includes("--link");
1720
+ const shadow = argv.includes("--shadow");
1416
1721
  const row$1 = praeRow({
1417
- pack: source,
1722
+ ...link ? { packFile: resolve(packPath) } : { pack: source },
1723
+ ...shadow ? { mode: "shadow" } : {},
1418
1724
  ...ledger === void 0 ? {} : { ledger },
1419
1725
  ...approver === void 0 ? {} : { approver }
1420
1726
  });
@@ -1426,7 +1732,8 @@ async function init(argv) {
1426
1732
  }
1427
1733
  console.log(`profile ${profile.dir}`);
1428
1734
  console.log(`overlay ${plan.patchPath}`);
1429
- console.log(`pack ${pack.pack} ${pack.version} · ${pack.rules.length} rules`);
1735
+ console.log(`pack ${pack.pack} ${pack.version} · ${pack.rules.length} rules${link ? ` · linked, edits hot-reload (${resolve(packPath)})` : ""}`);
1736
+ if (shadow) console.log("mode shadow · every verdict sealed, every call proceeds");
1430
1737
  if (ledger !== void 0) console.log(`ledger ${ledger}`);
1431
1738
  if (plan.findings.length > 0) {
1432
1739
  console.log("");
@@ -123,6 +123,22 @@ function matches$1(pattern, value) {
123
123
  if (pattern.endsWith("*")) return value.startsWith(pattern.slice(0, -1));
124
124
  return value === pattern;
125
125
  }
126
+ /**
127
+ * A glob with `*` anywhere, for pipeline signatures: `curl*|*sh` is the
128
+ * shape of download-and-execute however the two ends are spelled.
129
+ */
130
+ function globMatches(pattern, value) {
131
+ const source = pattern.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*");
132
+ return (/* @__PURE__ */ new RegExp(`^${source}$`)).test(value);
133
+ }
134
+ /**
135
+ * Every wanted token holds in a segment: a plain pattern must match some
136
+ * token, a `!pattern` must match none. `["-rf", "!--dry-run"]` is a real
137
+ * force-delete, not a rehearsal of one.
138
+ */
139
+ function tokensHold(wanted, tokens) {
140
+ return wanted.every((pattern) => pattern.startsWith("!") ? !tokens.some((token) => matches$1(pattern.slice(1), token)) : tokens.some((token) => matches$1(pattern, token)));
141
+ }
126
142
  const list = (v) => v === void 0 ? [] : typeof v === "string" ? [v] : v;
127
143
  /**
128
144
  * Judge one command line against one matcher. Every present field must hold.
@@ -132,17 +148,24 @@ function commandMatches(match, command) {
132
148
  if (parts.length === 0) return false;
133
149
  const names = list(match.name);
134
150
  const args = list(match.token);
151
+ const anyArgs = list(match.anyToken);
135
152
  const pipes = list(match.pipedTo);
153
+ const shapes = list(match.pipeline);
154
+ const argsHold = (segment) => tokensHold(args, segment.tokens) && (anyArgs.length === 0 || anyArgs.some((wanted) => segment.tokens.some((token) => matches$1(wanted, token))));
136
155
  if (names.length > 0) {
137
156
  const hit = parts.filter((segment) => names.some((pattern) => matches$1(pattern, segment.name) || segment.wrappers.some((wrapper) => matches$1(pattern, wrapper))));
138
157
  if (hit.length === 0) return false;
139
- if (args.length > 0 && !hit.some((segment) => args.every((wanted) => segment.tokens.some((token) => matches$1(wanted, token))))) return false;
140
- } else if (args.length > 0) {
141
- if (!parts.some((segment) => args.every((wanted) => segment.tokens.some((token) => matches$1(wanted, token))))) return false;
158
+ if ((args.length > 0 || anyArgs.length > 0) && !hit.some(argsHold)) return false;
159
+ } else if (args.length > 0 || anyArgs.length > 0) {
160
+ if (!parts.some(argsHold)) return false;
142
161
  }
143
162
  if (pipes.length > 0) {
144
163
  if (!parts.slice(1).some((segment) => pipes.some((pattern) => matches$1(pattern, segment.name)))) return false;
145
164
  }
165
+ if (shapes.length > 0) {
166
+ const signature = parts.map((segment) => segment.name).join("|");
167
+ if (!shapes.some((pattern) => globMatches(pattern, signature))) return false;
168
+ }
146
169
  return true;
147
170
  }
148
171
 
package/lib/index.d.ts CHANGED
@@ -32,7 +32,12 @@ declare function defaultProfile(name?: string): Profile;
32
32
  * was in force at the time.
33
33
  */
34
34
  declare function praeRow(options: {
35
- readonly pack: string;
35
+ /** The pack inline, as JSON text. */
36
+ readonly pack?: string;
37
+ /** The pack as a file the gate watches (hot reload); wins over `pack`. */
38
+ readonly packFile?: string;
39
+ /** `shadow` seals every verdict and lets every call through. */
40
+ readonly mode?: 'enforce' | 'shadow';
36
41
  readonly ledger?: string;
37
42
  readonly approver?: string;
38
43
  }): string;
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import "./evaluate-CkYySl7T.js";
2
- import "./views-CAD9738P.js";
3
- import { a as planWrite, i as dshHome, n as applyWrite, o as praeRow, r as defaultProfile, s as profileAt, t as main } from "./cli-BUgU-LpN.js";
1
+ import "./evaluate-wMPkjcIC.js";
2
+ import "./views-UODslKwu.js";
3
+ import { a as planWrite, i as dshHome, n as applyWrite, o as praeRow, r as defaultProfile, s as profileAt, t as main } from "./cli-mahLCjoD.js";
4
4
  import "./src-Ciggx9ja.js";
5
5
 
6
6
  export { applyWrite, defaultProfile, dshHome, main, planWrite, praeRow, profileAt };
@@ -1,4 +1,4 @@
1
- import { c as renderShell, o as esc, s as renderNav, t as navFor } from "./views-CAD9738P.js";
1
+ import { c as renderNav, l as renderShell, s as esc, t as navFor } from "./views-UODslKwu.js";
2
2
 
3
3
  //#region src/console/instructions.ts
4
4
  const STYLE = `
@@ -1,4 +1,4 @@
1
- import { t as evaluate } from "./evaluate-CkYySl7T.js";
1
+ import { t as evaluate } from "./evaluate-wMPkjcIC.js";
2
2
  import { a as scan, n as MAX_SCAN_BYTES } from "./src-Br1tGPtS.js";
3
3
  import { readFile, readdir, stat } from "node:fs/promises";
4
4
  import { resolve, sep } from "node:path";
package/lib/playground.js CHANGED
@@ -126,6 +126,22 @@
126
126
  if (pattern.endsWith("*")) return value.startsWith(pattern.slice(0, -1));
127
127
  return value === pattern;
128
128
  }
129
+ /**
130
+ * A glob with `*` anywhere, for pipeline signatures: `curl*|*sh` is the
131
+ * shape of download-and-execute however the two ends are spelled.
132
+ */
133
+ function globMatches(pattern, value) {
134
+ const source = pattern.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*");
135
+ return (/* @__PURE__ */ new RegExp(`^${source}$`)).test(value);
136
+ }
137
+ /**
138
+ * Every wanted token holds in a segment: a plain pattern must match some
139
+ * token, a `!pattern` must match none. `["-rf", "!--dry-run"]` is a real
140
+ * force-delete, not a rehearsal of one.
141
+ */
142
+ function tokensHold(wanted, tokens) {
143
+ return wanted.every((pattern) => pattern.startsWith("!") ? !tokens.some((token) => matches$1(pattern.slice(1), token)) : tokens.some((token) => matches$1(pattern, token)));
144
+ }
129
145
  const list = (v) => v === void 0 ? [] : typeof v === "string" ? [v] : v;
130
146
  /**
131
147
  * Judge one command line against one matcher. Every present field must hold.
@@ -135,17 +151,24 @@
135
151
  if (parts.length === 0) return false;
136
152
  const names = list(match.name);
137
153
  const args = list(match.token);
154
+ const anyArgs = list(match.anyToken);
138
155
  const pipes = list(match.pipedTo);
156
+ const shapes = list(match.pipeline);
157
+ const argsHold = (segment) => tokensHold(args, segment.tokens) && (anyArgs.length === 0 || anyArgs.some((wanted) => segment.tokens.some((token) => matches$1(wanted, token))));
139
158
  if (names.length > 0) {
140
159
  const hit = parts.filter((segment) => names.some((pattern) => matches$1(pattern, segment.name) || segment.wrappers.some((wrapper) => matches$1(pattern, wrapper))));
141
160
  if (hit.length === 0) return false;
142
- if (args.length > 0 && !hit.some((segment) => args.every((wanted) => segment.tokens.some((token) => matches$1(wanted, token))))) return false;
143
- } else if (args.length > 0) {
144
- if (!parts.some((segment) => args.every((wanted) => segment.tokens.some((token) => matches$1(wanted, token))))) return false;
161
+ if ((args.length > 0 || anyArgs.length > 0) && !hit.some(argsHold)) return false;
162
+ } else if (args.length > 0 || anyArgs.length > 0) {
163
+ if (!parts.some(argsHold)) return false;
145
164
  }
146
165
  if (pipes.length > 0) {
147
166
  if (!parts.slice(1).some((segment) => pipes.some((pattern) => matches$1(pattern, segment.name)))) return false;
148
167
  }
168
+ if (shapes.length > 0) {
169
+ const signature = parts.map((segment) => segment.name).join("|");
170
+ if (!shapes.some((pattern) => globMatches(pattern, signature))) return false;
171
+ }
149
172
  return true;
150
173
  }
151
174
 
@@ -580,9 +603,11 @@
580
603
  check.keys(path, value, [
581
604
  "name",
582
605
  "token",
583
- "pipedTo"
606
+ "anyToken",
607
+ "pipedTo",
608
+ "pipeline"
584
609
  ]);
585
- if (value.name === void 0 && value.token === void 0 && value.pipedTo === void 0) check.fail(path, "expected `name`, `token` or `pipedTo`; a command matcher with none matches nothing");
610
+ if (value.name === void 0 && value.token === void 0 && value.anyToken === void 0 && value.pipedTo === void 0 && value.pipeline === void 0) check.fail(path, "expected `name`, `token`, `anyToken`, `pipedTo` or `pipeline`; a command matcher with none matches nothing");
586
611
  const out = {};
587
612
  if (value.token !== void 0) {
588
613
  const tokens = check.names(`${path}.token`, value.token);
@@ -592,10 +617,18 @@
592
617
  const names = check.names(`${path}.name`, value.name);
593
618
  if (names !== void 0) out.name = names;
594
619
  }
620
+ if (value.anyToken !== void 0) {
621
+ const tokens = check.names(`${path}.anyToken`, value.anyToken);
622
+ if (tokens !== void 0) out.anyToken = tokens;
623
+ }
595
624
  if (value.pipedTo !== void 0) {
596
625
  const pipes = check.names(`${path}.pipedTo`, value.pipedTo);
597
626
  if (pipes !== void 0) out.pipedTo = pipes;
598
627
  }
628
+ if (value.pipeline !== void 0) {
629
+ const shapes = check.names(`${path}.pipeline`, value.pipeline);
630
+ if (shapes !== void 0) out.pipeline = shapes;
631
+ }
599
632
  return out;
600
633
  }
601
634
  function readPathMatch(check, path, value) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "prae": "0.1",
3
3
  "pack": "prae-baseline",
4
- "version": "1.0.0",
4
+ "version": "1.1.0",
5
5
  "rules": [
6
6
  {
7
7
  "id": "BASE-001",
@@ -15,33 +15,65 @@
15
15
  "effect": {
16
16
  "kind": "deny",
17
17
  "because": "piping a download into an interpreter runs code nobody has read"
18
+ },
19
+ "frameworks": {
20
+ "owasp-llm-top-10-2026": ["LLM03"]
18
21
  }
19
22
  },
20
23
  {
21
24
  "id": "BASE-002",
22
25
  "title": "Recursive force-delete is held",
23
- "when": { "command": { "name": ["rm", "shred"], "token": "-rf" } },
26
+ "when": {
27
+ "command": {
28
+ "name": ["rm", "shred"],
29
+ "anyToken": ["-rf", "-fr", "-Rf", "-fR", "-r", "-R"],
30
+ "token": ["!--dry-run"]
31
+ }
32
+ },
24
33
  "effect": {
25
34
  "kind": "ask",
26
35
  "because": "a recursive force-delete is not reversible and needs a human"
36
+ },
37
+ "frameworks": {
38
+ "owasp-llm-top-10-2026": ["LLM03"]
27
39
  }
28
40
  },
29
41
  {
30
42
  "id": "BASE-003",
31
43
  "title": "Privilege escalation is held",
32
- "when": { "command": { "name": ["su", "doas", "pkexec"] } },
44
+ "when": {
45
+ "command": {
46
+ "name": ["su", "doas", "pkexec"]
47
+ }
48
+ },
33
49
  "effect": {
34
50
  "kind": "ask",
35
51
  "because": "escalating privileges needs a human"
52
+ },
53
+ "frameworks": {
54
+ "owasp-llm-top-10-2026": ["LLM03"]
36
55
  }
37
56
  },
38
57
  {
39
58
  "id": "BASE-004",
40
59
  "title": "Filesystem-wide destruction is refused",
41
- "when": { "command": { "name": ["mkfs*", "dd", "diskutil", "fdisk"] } },
60
+ "when": {
61
+ "command": {
62
+ "name": ["mkfs*", "diskutil", "fdisk", "dd"],
63
+ "token": [
64
+ "!of=/dev/null",
65
+ "!of=/dev/zero",
66
+ "!of=/dev/stdout",
67
+ "!of=/dev/stderr"
68
+ ]
69
+ }
70
+ },
42
71
  "effect": {
43
72
  "kind": "deny",
44
73
  "because": "block-device and filesystem tools are not granted to agents"
74
+ },
75
+ "frameworks": {
76
+ "owasp-llm-top-10-2026": ["LLM03"]
45
77
  }
46
78
  },
47
79
  {
@@ -64,24 +96,43 @@
64
96
  "effect": {
65
97
  "kind": "deny",
66
98
  "because": "secrets and credential stores are out of scope for agents"
99
+ },
100
+ "frameworks": {
101
+ "owasp-llm-top-10-2026": ["LLM02"]
67
102
  }
68
103
  },
69
104
  {
70
105
  "id": "BASE-006",
71
106
  "title": "Writes outside the workspace are held",
72
- "when": { "capability": "fs:write", "path": { "outside": ["./**"] } },
107
+ "when": {
108
+ "capability": "fs:write",
109
+ "path": {
110
+ "outside": ["./**"]
111
+ }
112
+ },
73
113
  "effect": {
74
114
  "kind": "ask",
75
115
  "because": "writing outside the working tree needs a human"
116
+ },
117
+ "frameworks": {
118
+ "owasp-llm-top-10-2026": ["LLM03"]
76
119
  }
77
120
  },
78
121
  {
79
122
  "id": "BASE-007",
80
123
  "title": "Force-push is held",
81
- "when": { "command": { "name": "git", "token": "--force*" } },
124
+ "when": {
125
+ "command": {
126
+ "name": "git",
127
+ "token": "--force*"
128
+ }
129
+ },
82
130
  "effect": {
83
131
  "kind": "ask",
84
132
  "because": "rewriting shared history needs a human"
133
+ },
134
+ "frameworks": {
135
+ "owasp-llm-top-10-2026": ["LLM03"]
85
136
  }
86
137
  },
87
138
  {
@@ -96,6 +147,9 @@
96
147
  "effect": {
97
148
  "kind": "ask",
98
149
  "because": "publishing is outward-facing and needs a human"
150
+ },
151
+ "frameworks": {
152
+ "owasp-llm-top-10-2026": ["LLM03"]
99
153
  }
100
154
  }
101
155
  ]
@@ -1,4 +1,4 @@
1
- import { t as evaluate } from "./evaluate-CkYySl7T.js";
1
+ import { t as evaluate } from "./evaluate-wMPkjcIC.js";
2
2
 
3
3
  //#region src/test-run.ts
4
4
  /** The path argument a scenario's call carries, if any. */
@@ -67,17 +67,74 @@ function cite(rule) {
67
67
  }
68
68
 
69
69
  //#endregion
70
- //#region src/console/logo.ts
70
+ //#region ../tokens/src/mark.ts
71
71
  /**
72
- * The mark, inlined.
72
+ * The mark — Guardians — as code.
73
+ *
74
+ * Four corner brackets around a core that none of them touch: the Guardians
75
+ * PRAE assembles around an agent, and the agent, green because what is
76
+ * inside has been verified. The console, the site and the CLI draw it from
77
+ * here, so there is one geometry and no copies to drift. `brand/` holds the
78
+ * same shapes as files, for places that need a file.
73
79
  *
74
- * A P drawn as a door: the product is the moment permission is granted,
75
- * and the glyph is the gate it comes through. Inlined as a data URI rather
76
- * than linked because these pages are read offline and mailed around as
77
- * evidence — a page that fetches an image renders wrong on the machine that
78
- * matters.
80
+ * Grid 32u. Arms 11u long, 5u thick. Core 8u, centred, 5u clear. Radius 0.
79
81
  */
80
- const LOGO = "data:image/png;base64,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";
82
+ const MARK = {
83
+ viewBox: "0 0 32 32",
84
+ brackets: "M2 2H13V7H7V13H2ZM19 2H30V13H25V7H19ZM2 19H7V25H13V30H2ZM25 19H30V30H19V25H25Z",
85
+ corners: [
86
+ {
87
+ d: "M2 2H13V7H7V13H2Z",
88
+ dx: -14,
89
+ dy: -14
90
+ },
91
+ {
92
+ d: "M19 2H30V13H25V7H19Z",
93
+ dx: 14,
94
+ dy: -14
95
+ },
96
+ {
97
+ d: "M25 19H30V30H19V25H25Z",
98
+ dx: 14,
99
+ dy: 14
100
+ },
101
+ {
102
+ d: "M2 19H7V25H13V30H2Z",
103
+ dx: -14,
104
+ dy: 14
105
+ }
106
+ ],
107
+ core: {
108
+ x: 12,
109
+ y: 12,
110
+ width: 8,
111
+ height: 8
112
+ },
113
+ colour: {
114
+ graphite: "#0E1114",
115
+ ink: "#FFFFFF",
116
+ signal: "#98ED49",
117
+ signalOnLight: "#0C6800"
118
+ }
119
+ };
120
+ /** The inner drawing: brackets and, unless absent, the core. */
121
+ function markShapes({ surface = "graphite", core = "verified", mono = false } = {}) {
122
+ const ink = surface === "graphite" ? MARK.colour.ink : MARK.colour.graphite;
123
+ const green = surface === "graphite" ? MARK.colour.signal : MARK.colour.signalOnLight;
124
+ const coreFill = mono || core === "unverified" ? ink : green;
125
+ const { x, y, width, height } = MARK.core;
126
+ const rect = core === "absent" ? "" : `<rect class="prae-core" x="${x}" y="${y}" width="${width}" height="${height}" fill="${coreFill}"/>`;
127
+ return `${MARK.corners.map((c, i) => `<path class="prae-corner" style="--i:${i};--dx:${c.dx};--dy:${c.dy}" d="${c.d}" fill="${ink}"/>`).join("")}${rect}`;
128
+ }
129
+ /** A complete SVG document of the mark. */
130
+ function markSvg(options = {}) {
131
+ const size = options.size ?? 32;
132
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${MARK.viewBox}" width="${size}" height="${size}">${markShapes(options)}</svg>`;
133
+ }
134
+ /** The mark as a data URI, for an `<img>` that must not fetch anything. */
135
+ function markDataUri(options = {}) {
136
+ return `data:image/svg+xml;utf8,${encodeURIComponent(markSvg(options))}`;
137
+ }
81
138
 
82
139
  //#endregion
83
140
  //#region ../tokens/src/palette.ts
@@ -345,6 +402,10 @@ const VERDICT_LABEL = {
345
402
  correct: "CORRECTED"
346
403
  };
347
404
 
405
+ //#endregion
406
+ //#region src/console/logo.ts
407
+ const LOGO = markDataUri({ size: 26 });
408
+
348
409
  //#endregion
349
410
  //#region src/console/shell.ts
350
411
  function esc(value) {
@@ -1073,4 +1134,4 @@ ${history}`;
1073
1134
  }
1074
1135
 
1075
1136
  //#endregion
1076
- export { renderPacks as a, renderShell as c, cite as d, renderOverview as i, VERDICT_LABEL as l, renderApprovals as n, esc as o, renderCompliance as r, renderNav as s, navFor as t, FRAMEWORKS as u };
1137
+ export { renderPacks as a, renderNav as c, FRAMEWORKS as d, cite as f, renderOverview as i, renderShell as l, renderApprovals as n, CONTROLS as o, renderCompliance as r, esc as s, navFor as t, VERDICT_LABEL as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prae-cli",
3
- "version": "0.6.2",
3
+ "version": "0.7.1",
4
4
  "description": "Operator policy for agent harnesses: validate packs, explain a decision, test a pack against security invariants, verify a decision log, and provision a governed DSH profile.",
5
5
  "keywords": [
6
6
  "dsh-plugin",
@@ -40,12 +40,12 @@
40
40
  "tsdown": "^0.15.6",
41
41
  "typescript": "^5.9.2",
42
42
  "vitest": "^3.2.4",
43
- "@prae/controls": "0.1.0",
44
43
  "@prae/extract": "0.1.0",
45
44
  "@prae/inspect": "0.1.0",
46
45
  "@prae/instructions": "0.1.0",
47
46
  "@prae/kernel": "0.1.0",
48
47
  "@prae/ledger": "0.1.0",
48
+ "@prae/controls": "0.1.0",
49
49
  "@prae/tokens": "0.1.0"
50
50
  },
51
51
  "scripts": {