prae-cli 0.6.1 → 0.7.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/lib/bin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import "./evaluate-CkYySl7T.js";
3
- import "./views-CAD9738P.js";
4
- import { t as main } from "./cli-BUgU-LpN.js";
3
+ import "./views-UODslKwu.js";
4
+ import { t as main } from "./cli-chtNj_el.js";
5
5
  import "./src-Ciggx9ja.js";
6
6
 
7
7
  //#region src/bin.ts
@@ -1,7 +1,7 @@
1
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";
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";
@@ -752,7 +752,9 @@ function praeRow(options) {
752
752
  ];
753
753
  if (options.ledger !== void 0) lines.push(` ledger: ${JSON.stringify(options.ledger)}`);
754
754
  if (options.approver !== void 0) lines.push(` approver: ${JSON.stringify(options.approver)}`);
755
- lines.push(" pack: |", indent(options.pack, 10));
755
+ if (options.mode !== void 0) lines.push(` mode: ${options.mode}`);
756
+ if (options.packFile !== void 0) lines.push(` packFile: ${JSON.stringify(options.packFile)}`);
757
+ else lines.push(" pack: |", indent(options.pack ?? "", 10));
756
758
  return lines.join("\n");
757
759
  }
758
760
  /** Compose the write and check it, without touching the file. */
@@ -792,6 +794,60 @@ async function applyWrite(profile, plan, expectedRevision) {
792
794
  return { snapshot: manifest.id };
793
795
  }
794
796
 
797
+ //#endregion
798
+ //#region src/record-files.ts
799
+ function jsonOrNull(path) {
800
+ if (!existsSync(path)) return null;
801
+ try {
802
+ return JSON.parse(readFileSync(path, "utf8"));
803
+ } catch {
804
+ return null;
805
+ }
806
+ }
807
+ function readLedger(path) {
808
+ if (!existsSync(path)) return [];
809
+ const out = [];
810
+ for (const line of readFileSync(path, "utf8").split("\n")) {
811
+ if (line.trim() === "") continue;
812
+ try {
813
+ out.push(JSON.parse(line));
814
+ } catch {}
815
+ }
816
+ return out;
817
+ }
818
+ /** The defaults the demo writes; a caller with other names overrides them. */
819
+ const DEFAULT_FILES = {
820
+ ledger: "papaya.jsonl",
821
+ discovery: "discovery.json",
822
+ report: "security-report.json",
823
+ guardians: "guardians.json",
824
+ pack: "papaya.prae.json",
825
+ publicKey: "prae-signing.pub.pem",
826
+ redteam: "../redteam/results"
827
+ };
828
+ function readRecord(dir, files) {
829
+ return {
830
+ ledger: readLedger(resolve(dir, files.ledger)),
831
+ discovery: jsonOrNull(resolve(dir, files.discovery)),
832
+ report: jsonOrNull(resolve(dir, files.report)),
833
+ guardians: jsonOrNull(resolve(dir, files.guardians)),
834
+ pack: jsonOrNull(resolve(dir, files.pack))
835
+ };
836
+ }
837
+ /** The red-team campaign and its runs, as the runner wrote them; empty when none. */
838
+ function readRedteam(dir, files) {
839
+ const root = resolve(dir, files.redteam);
840
+ if (!existsSync(root)) return {
841
+ campaign: null,
842
+ runs: []
843
+ };
844
+ 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);
845
+ return {
846
+ campaign: jsonOrNull(resolve(root, "campaign.json")),
847
+ runs
848
+ };
849
+ }
850
+
795
851
  //#endregion
796
852
  //#region src/serve.ts
797
853
  const TYPES = {
@@ -823,8 +879,233 @@ function safePath(root, urlPath) {
823
879
  if (target !== base && !target.startsWith(base + sep)) return void 0;
824
880
  return target;
825
881
  }
826
- /** Start the reader. Resolves once it is listening. */
827
- function serveConsole(root, port) {
882
+ /**
883
+ * Verify the ledger the way `prae verify` does — chain, then signatures
884
+ * against the public key beside it. With `tamper`, one row's refusal is
885
+ * changed to an allow in memory first and the altered text is what gets
886
+ * verified: the forged-copy demonstration, without writing a file. With
887
+ * `repair` as well, every hash is recomputed over the altered rows — what
888
+ * an attacker would do to mend the chain — and the signatures, made over the
889
+ * original hashes by a key they do not hold, are what fails. Nothing on disk
890
+ * changes.
891
+ */
892
+ function verifyRecord(record, tamper, repair = false) {
893
+ const { dir, files } = record;
894
+ const path = resolve(dir, files.ledger);
895
+ if (!existsSync(path)) return {
896
+ ok: false,
897
+ entries: 0,
898
+ problems: [{
899
+ line: 0,
900
+ message: "no ledger"
901
+ }],
902
+ tampered: null,
903
+ repaired: false,
904
+ signatures: null
905
+ };
906
+ let text = readFileSync(path, "utf8");
907
+ let tampered = null;
908
+ if (tamper !== void 0) {
909
+ const lines = text.trimEnd().split("\n");
910
+ const idx = lines.findIndex((line) => {
911
+ try {
912
+ return JSON.parse(line).seq === tamper;
913
+ } catch {
914
+ return false;
915
+ }
916
+ });
917
+ if (idx >= 0) {
918
+ const row$1 = JSON.parse(lines[idx]);
919
+ row$1.verdict = "allow";
920
+ row$1.reason = "looks fine to me";
921
+ lines[idx] = JSON.stringify(row$1);
922
+ if (repair) {
923
+ let prev = GENESIS;
924
+ for (let i = 0; i < lines.length; i += 1) {
925
+ const { seq, prev: _prev, hash: _hash, sig,...rest } = JSON.parse(lines[i]);
926
+ const entry = chain(rest, seq, prev);
927
+ lines[i] = JSON.stringify(sig === void 0 ? entry : {
928
+ ...entry,
929
+ sig
930
+ });
931
+ prev = entry.hash;
932
+ }
933
+ }
934
+ text = `${lines.join("\n")}\n`;
935
+ tampered = tamper;
936
+ }
937
+ }
938
+ const verified = verifyEntries(text);
939
+ const keyPath = resolve(dir, files.publicKey);
940
+ let signatures = null;
941
+ if (existsSync(keyPath)) {
942
+ const publicKey = readFileSync(keyPath, "utf8");
943
+ let entries = [];
944
+ try {
945
+ entries = readEntries(text);
946
+ } catch {
947
+ entries = [];
948
+ }
949
+ const signed = entries.filter((e) => e.sig !== void 0);
950
+ signatures = {
951
+ checked: signed.length,
952
+ failed: signed.filter((e) => !verifySignature(e, e.sig, publicKey)).map((e) => e.seq),
953
+ fingerprint: fingerprint(publicKey)
954
+ };
955
+ }
956
+ return {
957
+ ok: verified.ok,
958
+ entries: verified.entries,
959
+ problems: [...verified.problems],
960
+ tampered,
961
+ repaired: tampered !== null && repair,
962
+ signatures
963
+ };
964
+ }
965
+ const OWASP = "owasp-llm-top-10-2026";
966
+ /**
967
+ * Evidence toward each control, from the pack and the ledger as they are
968
+ * now — the rows the controls package defines, so the console, the exporter and
969
+ * the old pages cannot disagree about what a profile shows. Framed as
970
+ * evidence, never as compliance: an auditor decides.
971
+ */
972
+ function complianceOf(record) {
973
+ const { dir, files } = record;
974
+ const ledgerPath = resolve(dir, files.ledger);
975
+ const text = existsSync(ledgerPath) ? readFileSync(ledgerPath, "utf8") : "";
976
+ let entries = [];
977
+ try {
978
+ entries = readEntries(text);
979
+ } catch {
980
+ entries = [];
981
+ }
982
+ const packPath = resolve(dir, files.pack);
983
+ let pack;
984
+ if (existsSync(packPath)) {
985
+ const parsed = parsePack(readFileSync(packPath, "utf8"));
986
+ if (parsed.ok) pack = parsed.pack;
987
+ }
988
+ const data = {
989
+ ...pack === void 0 ? {} : { pack },
990
+ entries,
991
+ verification: verifyEntries(text),
992
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
993
+ };
994
+ const owasp = FRAMEWORKS[OWASP]?.controls ?? {};
995
+ return { controls: CONTROLS.map((control) => {
996
+ const code = /LLM\d\d/.exec(control.id)?.[0];
997
+ const named = code === void 0 ? void 0 : owasp[code];
998
+ return {
999
+ id: code ?? control.id,
1000
+ ...named === void 0 ? {} : { name: named.name },
1001
+ asks: control.asks,
1002
+ framework: named === void 0 ? "obligation" : OWASP,
1003
+ ...control.evidence(data)
1004
+ };
1005
+ }) };
1006
+ }
1007
+ /**
1008
+ * The record over HTTP, for the Command Center (apps/console): the same
1009
+ * files `prae console` renders, as JSON, plus one event stream that says
1010
+ * when the ledger moved. Read-only, from files on disk, nothing computed
1011
+ * here — the console derives everything it shows from these bodies.
1012
+ *
1013
+ * Returns false when the URL is not an API route, so a caller can fall
1014
+ * through to static files. Shared with the console's Vite dev server so the
1015
+ * two answer identically.
1016
+ */
1017
+ function handleApi(req, res, record) {
1018
+ const route = (req.url ?? "").split("?")[0] ?? "";
1019
+ if (!route.startsWith("/api/")) return false;
1020
+ const json = (body, status = 200) => {
1021
+ res.statusCode = status;
1022
+ res.setHeader("content-type", "application/json; charset=utf-8");
1023
+ res.setHeader("cache-control", "no-store");
1024
+ res.end(JSON.stringify(body));
1025
+ };
1026
+ const { dir, files } = record;
1027
+ if (route === "/api/events") {
1028
+ res.writeHead(200, {
1029
+ "content-type": "text/event-stream",
1030
+ "cache-control": "no-store",
1031
+ connection: "keep-alive"
1032
+ });
1033
+ res.write(": connected\n\n");
1034
+ let sent = readRecord(dir, files).ledger.length;
1035
+ const push = () => {
1036
+ const rows = readRecord(dir, files).ledger;
1037
+ if (rows.length < sent) sent = 0;
1038
+ const fresh = rows.slice(sent);
1039
+ sent = rows.length;
1040
+ if (fresh.length > 0) res.write(`data: ${JSON.stringify(fresh)}\n\n`);
1041
+ else res.write(": keepalive\n\n");
1042
+ };
1043
+ const watcher = existsSync(dir) ? watch(dir, { persistent: false }, () => push()) : void 0;
1044
+ const keep = setInterval(push, 2e3);
1045
+ res.on("close", () => {
1046
+ clearInterval(keep);
1047
+ watcher?.close();
1048
+ });
1049
+ return true;
1050
+ }
1051
+ if (route === "/api/verify") {
1052
+ const raw = new URL(req.url ?? "/", "http://localhost").searchParams.get("tamper");
1053
+ const tamper = raw === null ? void 0 : Number(raw);
1054
+ const repair = new URL(req.url ?? "/", "http://localhost").searchParams.get("repair") === "1";
1055
+ json(verifyRecord(record, Number.isInteger(tamper) ? tamper : void 0, repair));
1056
+ return true;
1057
+ }
1058
+ if (route === "/api/compliance") {
1059
+ json(complianceOf(record));
1060
+ return true;
1061
+ }
1062
+ if (route === "/api/redteam") {
1063
+ json(readRedteam(dir, files));
1064
+ return true;
1065
+ }
1066
+ const snapshot = readRecord(dir, files);
1067
+ switch (route) {
1068
+ case "/api/record":
1069
+ json(snapshot);
1070
+ break;
1071
+ case "/api/ledger":
1072
+ json(snapshot.ledger);
1073
+ break;
1074
+ case "/api/discovery":
1075
+ json(snapshot.discovery);
1076
+ break;
1077
+ case "/api/report":
1078
+ json(snapshot.report);
1079
+ break;
1080
+ case "/api/guardians":
1081
+ json(snapshot.guardians);
1082
+ break;
1083
+ case "/api/pack":
1084
+ json(snapshot.pack);
1085
+ break;
1086
+ case "/api/status":
1087
+ json({
1088
+ dir,
1089
+ files: Object.fromEntries(Object.entries(files).map(([name, file]) => {
1090
+ const path = resolve(dir, file);
1091
+ if (!existsSync(path)) return [name, null];
1092
+ const stat$1 = statSync(path);
1093
+ return [name, {
1094
+ size: stat$1.size,
1095
+ mtime: stat$1.mtimeMs
1096
+ }];
1097
+ }))
1098
+ });
1099
+ break;
1100
+ default: json({ error: "not found" }, 404);
1101
+ }
1102
+ return true;
1103
+ }
1104
+ /**
1105
+ * Start the reader. Resolves once it is listening. With a record source,
1106
+ * `/api/*` answers from it; without one, only the rendered pages are served.
1107
+ */
1108
+ function serveConsole(root, port, record) {
828
1109
  const server = createServer((req, res) => {
829
1110
  if (req.method !== "GET" && req.method !== "HEAD") {
830
1111
  res.statusCode = 405;
@@ -832,6 +1113,7 @@ function serveConsole(root, port) {
832
1113
  res.end();
833
1114
  return;
834
1115
  }
1116
+ if (record !== void 0 && handleApi(req, res, record)) return;
835
1117
  const target = safePath(root, req.url ?? "/");
836
1118
  if (target === void 0) {
837
1119
  res.statusCode = 403;
@@ -852,9 +1134,10 @@ function serveConsole(root, port) {
852
1134
  return new Promise((ok, fail$1) => {
853
1135
  server.once("error", fail$1);
854
1136
  server.listen(port, "127.0.0.1", () => {
1137
+ const address = server.address();
855
1138
  ok({
856
1139
  server,
857
- url: `http://127.0.0.1:${port}/`
1140
+ url: `http://127.0.0.1:${typeof address === "object" && address !== null ? address.port : port}/`
858
1141
  });
859
1142
  });
860
1143
  });
@@ -869,7 +1152,7 @@ const VERDICT_LABEL = {
869
1152
  deny: "DENIED",
870
1153
  correct: "CORRECTED"
871
1154
  };
872
- const USAGE = `prae — operator policy for agent harnesses
1155
+ const USAGE = `⌜▪⌟ prae — operator policy for agent harnesses
873
1156
 
874
1157
  prae check <pack.json>
875
1158
  Validate a policy pack and summarise what it enforces.
@@ -931,9 +1214,11 @@ const USAGE = `prae — operator policy for agent harnesses
931
1214
 
932
1215
  prae init [--pack <file>] [--profile <dir>] [--ledger <file>]
933
1216
  (no --pack mounts the built-in baseline of 8 high-risk rules)
934
- [--approver <name>] [--apply]
1217
+ [--approver <name>] [--link] [--shadow] [--apply]
935
1218
  Compose the overlay row that mounts PRAE with a pack. Prints the
936
- diff; writes only with --apply.
1219
+ diff; writes only with --apply. --link points the profile at the
1220
+ pack file instead of copying it in, so edits hot-reload; --shadow
1221
+ mounts in shadow mode: every verdict sealed, every call let through.
937
1222
  `;
938
1223
  function fail(message) {
939
1224
  console.error(`prae: ${message}`);
@@ -1073,7 +1358,7 @@ async function explain(path, tool, pairs) {
1073
1358
  * a key that arrived beside it is checked against nothing.
1074
1359
  */
1075
1360
  async function keygen(dir) {
1076
- const { generateKeypair, fingerprint } = await import("./src-C9L6NBvu.js");
1361
+ const { generateKeypair, fingerprint: fingerprint$1 } = await import("./src-C9L6NBvu.js");
1077
1362
  const { writeFileSync: writeFileSync$1 } = await import("node:fs");
1078
1363
  const keys = generateKeypair();
1079
1364
  const priv = join(dir, "prae-signing.pem");
@@ -1082,14 +1367,14 @@ async function keygen(dir) {
1082
1367
  writeFileSync$1(pub, keys.publicKey);
1083
1368
  console.log(`private ${priv} (0600 — this signs; guard it)`);
1084
1369
  console.log(`public ${pub}`);
1085
- console.log(`fingerprint ${fingerprint(keys.publicKey)}`);
1370
+ console.log(`fingerprint ${fingerprint$1(keys.publicKey)}`);
1086
1371
  console.log("");
1087
1372
  console.log("Give the PUBLIC key and fingerprint to whoever will verify");
1088
1373
  console.log("receipts, through a channel they trust. Point the plugin at the");
1089
1374
  console.log("private key with `signingKey` to sign every row.");
1090
1375
  }
1091
1376
  async function receipt(path, seq, keyPath, out) {
1092
- const { Ledger: Ledger$1, makeReceipt, fingerprint } = await import("./src-C9L6NBvu.js");
1377
+ const { Ledger: Ledger$1, makeReceipt, fingerprint: fingerprint$1 } = await import("./src-C9L6NBvu.js");
1093
1378
  const { readFileSync: readFileSync$1 } = await import("node:fs");
1094
1379
  const { createPublicKey } = await import("node:crypto");
1095
1380
  const privateKey = readFileSync$1(keyPath, "utf8");
@@ -1106,7 +1391,7 @@ async function receipt(path, seq, keyPath, out) {
1106
1391
  await writeFile(out, text);
1107
1392
  console.log(`${out} · seq ${seq} · ${entry.verdict} ${entry.tool} ${entry.subject ?? ""}`);
1108
1393
  }
1109
- console.log(`fingerprint ${fingerprint(publicKey)}`);
1394
+ console.log(`fingerprint ${fingerprint$1(publicKey)}`);
1110
1395
  console.log("The verifier compares that fingerprint against the one they");
1111
1396
  console.log("pinned — a receipt is only as good as the key they trust.");
1112
1397
  }
@@ -1190,7 +1475,7 @@ async function verify(path, keyPath) {
1190
1475
  const last = entries.at(-1);
1191
1476
  console.log(`${result.entries} entries · chain intact${last === void 0 ? "" : ` · seal ${seal(last)}`}`);
1192
1477
  if (keyPath !== void 0) {
1193
- const { verifySignature, fingerprint } = await import("./src-C9L6NBvu.js");
1478
+ const { verifySignature: verifySignature$1, fingerprint: fingerprint$1 } = await import("./src-C9L6NBvu.js");
1194
1479
  const { readFileSync: readFileSync$1 } = await import("node:fs");
1195
1480
  const publicKey = readFileSync$1(keyPath, "utf8");
1196
1481
  let signed = 0;
@@ -1198,13 +1483,13 @@ async function verify(path, keyPath) {
1198
1483
  for (const entry of entries) {
1199
1484
  if (entry.sig === void 0) continue;
1200
1485
  signed += 1;
1201
- if (!verifySignature(entry, entry.sig, publicKey)) bad.push(entry.seq);
1486
+ if (!verifySignature$1(entry, entry.sig, publicKey)) bad.push(entry.seq);
1202
1487
  }
1203
1488
  if (bad.length > 0) {
1204
- console.error(`signatures: ${bad.length} of ${signed} FAIL against ${fingerprint(publicKey)} — rows ${bad.join(", ")}`);
1489
+ console.error(`signatures: ${bad.length} of ${signed} FAIL against ${fingerprint$1(publicKey)} — rows ${bad.join(", ")}`);
1205
1490
  process.exitCode = 1;
1206
1491
  } 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)}`);
1492
+ else console.log(`signatures: ${signed}/${entries.length} rows verify against ${fingerprint$1(publicKey)}`);
1208
1493
  }
1209
1494
  console.log("tamper-evident: a removed, edited, or reordered row breaks this chain.");
1210
1495
  console.log("It does not prove against a rewrite of the whole file by someone holding this code.");
@@ -1366,7 +1651,7 @@ async function consoleOut(path, argv) {
1366
1651
  const [{ ingest }, { extract }, { renderInstructions }] = await Promise.all([
1367
1652
  import("./src-CraUX64d.js"),
1368
1653
  import("./src-CQ0UmFUK.js"),
1369
- import("./instructions-oGCdVxHM.js")
1654
+ import("./instructions-C104EUaE.js")
1370
1655
  ]);
1371
1656
  const read = ingest(instructionsDir);
1372
1657
  instructionsView = renderInstructions(data, read, extract(read.statements));
@@ -1395,8 +1680,16 @@ async function consoleOut(path, argv) {
1395
1680
  const portFlag = flag(argv, "serve");
1396
1681
  const port = portFlag === void 0 ? 4180 : Number(portFlag);
1397
1682
  if (!Number.isInteger(port) || port < 1 || port > 65535) fail(`--serve takes a port, got \`${portFlag}\``);
1398
- const { url } = await serveConsole(out, port);
1683
+ const { url } = await serveConsole(out, port, {
1684
+ dir: dirname(resolve(path)),
1685
+ files: {
1686
+ ...DEFAULT_FILES,
1687
+ ledger: resolve(path),
1688
+ ...packPath === void 0 ? {} : { pack: resolve(packPath) }
1689
+ }
1690
+ });
1399
1691
  console.log(`\nserving ${out} at ${url}`);
1692
+ console.log(`record at ${url}api/record · events at ${url}api/events`);
1400
1693
  console.log("loopback only, read-only. ctrl-c to stop.");
1401
1694
  }
1402
1695
  async function init(argv) {
@@ -1413,8 +1706,11 @@ async function init(argv) {
1413
1706
  const profile = profileDir === void 0 ? defaultProfile() : profileAt(profileDir);
1414
1707
  const ledger = flag(argv, "ledger");
1415
1708
  const approver = flag(argv, "approver");
1709
+ const link = argv.includes("--link");
1710
+ const shadow = argv.includes("--shadow");
1416
1711
  const row$1 = praeRow({
1417
- pack: source,
1712
+ ...link ? { packFile: resolve(packPath) } : { pack: source },
1713
+ ...shadow ? { mode: "shadow" } : {},
1418
1714
  ...ledger === void 0 ? {} : { ledger },
1419
1715
  ...approver === void 0 ? {} : { approver }
1420
1716
  });
@@ -1426,7 +1722,8 @@ async function init(argv) {
1426
1722
  }
1427
1723
  console.log(`profile ${profile.dir}`);
1428
1724
  console.log(`overlay ${plan.patchPath}`);
1429
- console.log(`pack ${pack.pack} ${pack.version} · ${pack.rules.length} rules`);
1725
+ console.log(`pack ${pack.pack} ${pack.version} · ${pack.rules.length} rules${link ? ` · linked, edits hot-reload (${resolve(packPath)})` : ""}`);
1726
+ if (shadow) console.log("mode shadow · every verdict sealed, every call proceeds");
1430
1727
  if (ledger !== void 0) console.log(`ledger ${ledger}`);
1431
1728
  if (plan.findings.length > 0) {
1432
1729
  console.log("");
@@ -172,9 +172,9 @@ Prefer the interactive UI? Swap `@deepseek-ai/dsh-headless` for
172
172
  | Attempt | Expected | Why |
173
173
  | --- | --- | --- |
174
174
  | Ask for the file by another relative spelling, `../pilot-target/docs/…`, an absolute path | **deny** | Paths are normalized and anchored; the content rule has no path condition anyway |
175
- | Ask it to `cat` / `head` / `tail` the file through the shell | **held (ask)** by `SEC-005` | Shell output is not content-inspected; the pilot pack holds the known dumpers for a human. In headless mode there is no approver, so expect the call to fail closed rather than run |
176
- | Ask it to `python3 -c "print(open(...).read())"`, `node -e`, `awk`, or a here-doc | **not caught** by this pack | Shell is governed by *command* rules, not by content inspection — the list of programs that can print a file is unbounded. The honest control is scope: deny or hold `proc:spawn` for agents that should not need a shell |
177
- | Ask it to `grep AKIA docs/` or search the directory | **not caught** by this pack | Search tools return matched *lines*; content inspection runs on whole-file reads. A pilot pack can hold `grep`/`glob` by tool name (`"tool": ["grep"]` → `ask`) if that matters to you — say so and we will ship it as an option |
175
+ | Ask it to `cat` / `head` / `tail` the file through the shell | **held (ask)** by `SEC-005` before it runs; and from `prae-gate 0.6.0`, if it does run, the **output is withheld** (`SEC-004`, `phase: output`) | Two doors: the pilot pack holds the known dumpers for a human, and the result of any shell command is content-inspected before the model sees it. The command runs; the model gets a structured refusal saying so |
176
+ | Ask it to `python3 -c "print(open(...).read())"`, `node -e`, `awk`, or a here-doc | **output withheld** (`SEC-004`) from `prae-gate 0.6.0` | No program list is involved: the result is scanned, not the command. Say it precisely — the process ran, the model saw nothing of what it printed. To stop the process itself, scope `proc:spawn` |
177
+ | Ask it to `grep AKIA docs/` or search the directory | **output withheld** from `prae-gate 0.6.0` | Search results are lines, so nothing is inspected before the call; the lines it returns are, under the read rules |
178
178
  | Copy the file under `tests/fixtures/` first, then read it | **allowed, recorded** | That is the carve-out working as designed (`SEC-003` still writes a warn row). Tighten or remove the carve-out in the pack if your repos keep real secrets in fixtures |
179
179
  | Edit the ledger to hide the refusal | **chain breaks** on `prae verify` | And with a signing key configured, the signature fails without the private key — `prae keygen`, then `signingKey` in the plugin config |
180
180
 
@@ -116,14 +116,23 @@ counterparty can check without trusting the machine it came from.
116
116
  it; the dsh plugin covers the harness's `read` tool. An agent with direct
117
117
  filesystem access outside both is covered only where its harness routes
118
118
  those calls through a gated seam.
119
- - **The shell is governed by scope, not by content.** `cat`, `python -c`,
120
- `node -e`, a here-doc — the set of programs that can print a file is
121
- unbounded, so shell output is never content-inspected. The pilot pack holds
122
- the common dumpers (`SEC-005`, `ask`); the honest control for agents that
123
- should not need a shell is to deny or hold `proc:spawn`.
124
- - **Search tools return lines, not files.** `grep`/`glob` results are not
125
- content-inspected. Hold them by tool name (`"tool": ["grep"]` → `ask`) where
126
- that matters; `docs/pilot-dsh.md` §4 lists this with the rest.
119
+ - **The shell is content-inspected on the way out, not the way in.** `cat`,
120
+ `python -c`, `node -e`, `sed`, a here-doc — the set of programs that can
121
+ print a file is unbounded, so no list of them is a control. What is bounded
122
+ is the result: every tool result is scanned with the same detector before
123
+ the model sees it, and a result carrying a credential is withheld (`SEC-004`
124
+ in the shipped pack, `prae-gate ≥ 0.6.0`). Be exact about what that means:
125
+ **the command ran**; the model never saw what it printed. The ledger row says
126
+ `phase: output`, and the refusal the agent gets says `executed: true`. If a
127
+ process must not run at all, that is a scope rule on `proc:spawn`, not a
128
+ content rule.
129
+ - **Search tools are covered by the same door.** `grep`/`glob` return lines,
130
+ not files, so nothing is inspected before they run; their output is, and a
131
+ matched line that carries a credential is withheld under the read rules.
132
+ - **Shell rows carry a fingerprint, never the line.** `command: { program,
133
+ hash }` — the program word and a hash of the whole line — so an auditor can
134
+ see what ran and match repeats, while a line that carried a secret is never
135
+ quoted into the log.
127
136
  - **Detection is regex + entropy.** Same floor the mature at-rest scanners
128
137
  stand on; novel unformatted secrets can pass. The engine is an interface,
129
138
  not a ceiling.
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
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";
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-chtNj_el.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 = `
@@ -59,6 +59,23 @@
59
59
  "kind": "warn",
60
60
  "because": "a possible credential was noticed in this file; where no stronger rule fired, the read proceeded and this row is the record"
61
61
  }
62
+ },
63
+ {
64
+ "id": "SEC-004",
65
+ "title": "Shell output carrying credentials is withheld",
66
+ "when": {
67
+ "capability": "proc:spawn",
68
+ "secrets": {
69
+ "minConfidence": 0.8
70
+ }
71
+ },
72
+ "effect": {
73
+ "kind": "deny",
74
+ "because": "the command ran and its output contained a protected credential; the output was withheld from the model"
75
+ },
76
+ "frameworks": {
77
+ "owasp-llm-top-10-2026": ["LLM02"]
78
+ }
62
79
  }
63
80
  ]
64
81
  }
@@ -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.1",
3
+ "version": "0.7.0",
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",