prae-cli 0.7.1 → 0.8.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-wMPkjcIC.js";
3
- import "./views-UODslKwu.js";
4
- import { t as main } from "./cli-mahLCjoD.js";
3
+ import "./views-CCwVXpo2.js";
4
+ import { t as main } from "./cli-UAE968If.js";
5
5
  import "./src-Ciggx9ja.js";
6
6
 
7
7
  //#region src/bin.ts
@@ -0,0 +1,99 @@
1
+ import { d as canonical, i as fingerprint } from "./src-Ciggx9ja.js";
2
+ import { createPrivateKey, createPublicKey, sign, verify } from "node:crypto";
3
+
4
+ //#region src/checkpoint.ts
5
+ /** What the signature covers: the facts, never the timestamp or the name of the key. */
6
+ function signable(cp) {
7
+ return Buffer.from(canonical({
8
+ ledger: cp.ledger,
9
+ entries: cp.entries,
10
+ head: cp.head
11
+ }));
12
+ }
13
+ function makeCheckpoint(rows, ledger, privateKeyPem, at = (/* @__PURE__ */ new Date()).toISOString()) {
14
+ const head = rows.at(-1);
15
+ const cp = {
16
+ prae: "checkpoint/1",
17
+ at,
18
+ ledger,
19
+ entries: rows.length,
20
+ head: head === void 0 ? {
21
+ seq: -1,
22
+ hash: ""
23
+ } : {
24
+ seq: head.seq,
25
+ hash: head.hash
26
+ }
27
+ };
28
+ if (privateKeyPem === void 0) return cp;
29
+ const key = createPrivateKey(privateKeyPem);
30
+ const publicKey = createPublicKey(key).export({
31
+ type: "spki",
32
+ format: "pem"
33
+ }).toString();
34
+ return {
35
+ ...cp,
36
+ fingerprint: fingerprint(publicKey),
37
+ sig: sign(null, signable(cp), key).toString("base64")
38
+ };
39
+ }
40
+ /**
41
+ * The ledger as it is now, against the checkpoint as it was then. Rows may
42
+ * have been appended since; nothing before the head may differ.
43
+ */
44
+ function checkCheckpoint(rows, cp, publicKeyPem) {
45
+ const lines = [];
46
+ let ok = true;
47
+ if (cp.prae !== "checkpoint/1") return {
48
+ ok: false,
49
+ lines: [`not a checkpoint: prae is ${String(cp.prae)}`]
50
+ };
51
+ if (publicKeyPem !== void 0) if (cp.sig === void 0) {
52
+ ok = false;
53
+ lines.push("checkpoint is not signed; a key was given to check it against");
54
+ } else {
55
+ let good = false;
56
+ try {
57
+ good = verify(null, signable(cp), createPublicKey(publicKeyPem), Buffer.from(cp.sig, "base64"));
58
+ } catch {
59
+ good = false;
60
+ }
61
+ if (good) lines.push(`checkpoint signature verifies against ${fingerprint(publicKeyPem)}`);
62
+ else {
63
+ ok = false;
64
+ lines.push(`checkpoint signature FAILS against ${fingerprint(publicKeyPem)} — the checkpoint itself has been altered or was signed by another key`);
65
+ }
66
+ }
67
+ if (rows.length < cp.entries) {
68
+ ok = false;
69
+ lines.push(`ledger holds ${rows.length} rows; the checkpoint of ${cp.at} recorded ${cp.entries} — ${cp.entries - rows.length} row${cp.entries - rows.length === 1 ? "" : "s"} removed since`);
70
+ return {
71
+ ok,
72
+ lines
73
+ };
74
+ }
75
+ if (cp.entries === 0) {
76
+ lines.push(`checkpoint of ${cp.at} recorded an empty ledger; ${rows.length} rows since`);
77
+ return {
78
+ ok,
79
+ lines
80
+ };
81
+ }
82
+ const then = rows.find((row) => row.seq === cp.head.seq);
83
+ if (then === void 0 || then.hash !== cp.head.hash) {
84
+ ok = false;
85
+ lines.push(`row ${cp.head.seq} does not match the checkpoint head (${cp.head.hash.slice(0, 16)}…) — the ledger was rewritten since ${cp.at}`);
86
+ return {
87
+ ok,
88
+ lines
89
+ };
90
+ }
91
+ lines.push(`checkpoint of ${cp.at} holds: ${cp.entries} rows then, ${rows.length} now, head ${cp.head.hash.slice(0, 16)}… intact`);
92
+ return {
93
+ ok,
94
+ lines
95
+ };
96
+ }
97
+
98
+ //#endregion
99
+ export { checkCheckpoint, makeCheckpoint };
@@ -1,5 +1,5 @@
1
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";
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-CCwVXpo2.js";
3
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
4
  import { existsSync, readFileSync, readdirSync, statSync, watch } from "node:fs";
5
5
  import { mkdir, readFile, writeFile } from "node:fs/promises";
@@ -972,7 +972,6 @@ function verifyRecord(record, tamper, repair = false) {
972
972
  signatures
973
973
  };
974
974
  }
975
- const OWASP = "owasp-llm-top-10-2026";
976
975
  /**
977
976
  * Evidence toward each control, from the pack and the ledger as they are
978
977
  * now — the rows the controls package defines, so the console, the exporter and
@@ -1001,15 +1000,16 @@ function complianceOf(record) {
1001
1000
  verification: verifyEntries(text),
1002
1001
  generatedAt: (/* @__PURE__ */ new Date()).toISOString()
1003
1002
  };
1004
- const owasp = FRAMEWORKS[OWASP]?.controls ?? {};
1005
1003
  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];
1004
+ const found = Object.entries(FRAMEWORKS).find(([, f]) => control.id.startsWith(`${f.short} · `));
1005
+ const code = found === void 0 ? void 0 : control.id.split(" · ")[1];
1006
+ const named = found === void 0 || code === void 0 ? void 0 : found[1].controls[code];
1008
1007
  return {
1009
1008
  id: code ?? control.id,
1010
1009
  ...named === void 0 ? {} : { name: named.name },
1011
1010
  asks: control.asks,
1012
- framework: named === void 0 ? "obligation" : OWASP,
1011
+ framework: found === void 0 ? "obligation" : found[0],
1012
+ ...found === void 0 ? {} : { frameworkName: found[1].name },
1013
1013
  ...control.evidence(data)
1014
1014
  };
1015
1015
  }) };
@@ -1187,9 +1187,23 @@ const USAGE = `⌜▪⌟ prae — operator policy for agent harnesses
1187
1187
  the exit code is non-zero if any invariant no longer holds, so a
1188
1188
  control pack can be defended in CI. --pack overrides the suite's own.
1189
1189
 
1190
- prae verify <ledger.jsonl> [--key <public.pem>]
1190
+ prae verify <ledger.jsonl> [--key <public.pem>] [--checkpoint <file>]
1191
1191
  Re-derive every hash in a decision log and report any break. With
1192
- --key, also verify each row's signature against that pinned key.
1192
+ --key, also verify each row's signature against that pinned key. With
1193
+ --checkpoint, also check that nothing before that checkpoint's head
1194
+ has been removed or rewritten since it was taken.
1195
+
1196
+ prae checkpoint <ledger.jsonl> [--key <private.pem>] [--out <file>]
1197
+ Record the ledger's length and head, signed, to keep somewhere the
1198
+ ledger's machine cannot write. A chain proves no row inside the file
1199
+ was touched; a checkpoint proves the file was not cut at the end.
1200
+
1201
+ prae export <ledger.jsonl> --format ocsf [--out <file>]
1202
+ [--hec <url> --hec-token <token>]
1203
+ The record as OCSF 1.5.0 events (NDJSON) for a SIEM or data lake:
1204
+ refusals, holds and withheld outputs as Detection Findings, allowed
1205
+ calls as API Activity, every event carrying the row's seal. With --hec,
1206
+ post them to a Splunk HTTP Event Collector instead of writing a file.
1193
1207
 
1194
1208
  prae keygen [--out <dir>]
1195
1209
  Generate an Ed25519 keypair for signing ledger rows. Prints the
@@ -1270,7 +1284,7 @@ async function check(path) {
1270
1284
  * from the findings, so there is nothing here to be careful about printing.
1271
1285
  */
1272
1286
  async function scanFile(path, packPath, json) {
1273
- const { scan } = await import("./src-Vnr0fcpK.js");
1287
+ const { scan } = await import("./src-CwBEFhww.js");
1274
1288
  let content;
1275
1289
  try {
1276
1290
  content = await readFile(path, "utf8");
@@ -1310,7 +1324,7 @@ async function scanFile(path, packPath, json) {
1310
1324
  * not come up inspecting nothing.
1311
1325
  */
1312
1326
  async function mcp(root, packPath, ledgerPath) {
1313
- const { serveMcp } = await import("./mcp-DDcejhwQ.js");
1327
+ const { serveMcp } = await import("./mcp-doNYU-1C.js");
1314
1328
  const { resolve: resolve$1 } = await import("node:path");
1315
1329
  const bundled = [join(import.meta.dirname, "secret-catcher.prae.json"), join(import.meta.dirname, "..", "..", "..", "examples", "secret-catcher.prae.json")].find((candidate) => existsSync(candidate));
1316
1330
  const chosen = packPath ?? bundled;
@@ -1414,7 +1428,7 @@ async function receipt(path, seq, keyPath, out) {
1414
1428
  * file and sets the exit code, so scripts can branch on it.
1415
1429
  */
1416
1430
  async function verifyReceiptCmd(path) {
1417
- const { checkReceipt } = await import("./receipt-check-DrDE3Wwi.js");
1431
+ const { checkReceipt } = await import("./receipt-check-s3Aryyz-.js");
1418
1432
  let parsed;
1419
1433
  try {
1420
1434
  parsed = JSON.parse(await readFile(path, "utf8"));
@@ -1445,7 +1459,7 @@ async function test(suitePath, packOverride, asJson) {
1445
1459
  } catch (error) {
1446
1460
  return fail(`${suitePath} is not valid JSON: ${error instanceof Error ? error.message : error}`);
1447
1461
  }
1448
- const { parseSuite, renderSuite, runSuite } = await import("./test-run-DOaCKD1t.js");
1462
+ const { parseSuite, renderSuite, runSuite } = await import("./test-run-XNIRHKOu.js");
1449
1463
  const suite = parseSuite(parsed);
1450
1464
  if (!suite.ok) {
1451
1465
  console.error(`prae: ${suitePath} is not a valid test suite`);
@@ -1456,7 +1470,7 @@ async function test(suitePath, packOverride, asJson) {
1456
1470
  if (chosen === void 0) return fail("test needs a pack: set `pack` in the suite or pass --pack");
1457
1471
  const pack = await loadPack(chosen);
1458
1472
  const { capabilityForTool } = await import("capmark");
1459
- const { scan } = await import("./src-Vnr0fcpK.js");
1473
+ const { scan } = await import("./src-CwBEFhww.js");
1460
1474
  const inspect = pack.rules.some((rule) => rule.when.secrets !== void 0) ? (path) => {
1461
1475
  try {
1462
1476
  return scan(readFileSync(path, "utf8"));
@@ -1472,7 +1486,7 @@ async function test(suitePath, packOverride, asJson) {
1472
1486
  else for (const line of renderSuite(result)) console.log(line);
1473
1487
  if (!result.ok) process.exit(1);
1474
1488
  }
1475
- async function verify(path, keyPath) {
1489
+ async function verify(path, keyPath, checkpointPath) {
1476
1490
  let ledger;
1477
1491
  try {
1478
1492
  ledger = Ledger.open(path);
@@ -1484,6 +1498,13 @@ async function verify(path, keyPath) {
1484
1498
  const entries = ledger.read();
1485
1499
  const last = entries.at(-1);
1486
1500
  console.log(`${result.entries} entries · chain intact${last === void 0 ? "" : ` · seal ${seal(last)}`}`);
1501
+ if (checkpointPath !== void 0) {
1502
+ const { checkCheckpoint } = await import("./checkpoint-DiznOVpS.js");
1503
+ const publicKey = keyPath === void 0 ? void 0 : readFileSync(keyPath, "utf8");
1504
+ const verdict = checkCheckpoint(entries, JSON.parse(readFileSync(checkpointPath, "utf8")), publicKey);
1505
+ for (const line of verdict.lines) (verdict.ok ? console.log : console.error)(`checkpoint: ${line}`);
1506
+ if (!verdict.ok) process.exitCode = 1;
1507
+ }
1487
1508
  if (keyPath !== void 0) {
1488
1509
  const { verifySignature: verifySignature$1, fingerprint: fingerprint$1 } = await import("./src-C9L6NBvu.js");
1489
1510
  const { readFileSync: readFileSync$1 } = await import("node:fs");
@@ -1563,7 +1584,7 @@ async function report(path, out, packPath) {
1563
1584
  * that step is a proposal a human approves, and it starts from this list.
1564
1585
  */
1565
1586
  async function instructions(from, asJson) {
1566
- const { ingest } = await import("./src-CraUX64d.js");
1587
+ const { ingest } = await import("./src-d4c_n6wd.js");
1567
1588
  const result = ingest(from);
1568
1589
  if (asJson) {
1569
1590
  console.log(JSON.stringify(result, null, 2));
@@ -1599,7 +1620,7 @@ async function instructions(from, asJson) {
1599
1620
  * on purpose rather than something that happened to their policy.
1600
1621
  */
1601
1622
  async function propose(from, options) {
1602
- const [{ ingest }, { extract }] = await Promise.all([import("./src-CraUX64d.js"), import("./src-CQ0UmFUK.js")]);
1623
+ const [{ ingest }, { extract }] = await Promise.all([import("./src-d4c_n6wd.js"), import("./src-CBGZVvt7.js")]);
1603
1624
  const read = ingest(from);
1604
1625
  if (read.files.length === 0) {
1605
1626
  console.log(`No instruction files found at or above ${from}.`);
@@ -1659,9 +1680,9 @@ async function consoleOut(path, argv) {
1659
1680
  const instructionsDir = flag(argv, "instructions");
1660
1681
  if (instructionsDir !== void 0) {
1661
1682
  const [{ ingest }, { extract }, { renderInstructions }] = await Promise.all([
1662
- import("./src-CraUX64d.js"),
1663
- import("./src-CQ0UmFUK.js"),
1664
- import("./instructions-C104EUaE.js")
1683
+ import("./src-d4c_n6wd.js"),
1684
+ import("./src-CBGZVvt7.js"),
1685
+ import("./instructions-DulxSqFd.js")
1665
1686
  ]);
1666
1687
  const read = ingest(instructionsDir);
1667
1688
  instructionsView = renderInstructions(data, read, extract(read.statements));
@@ -1759,6 +1780,60 @@ async function init(argv) {
1759
1780
  return fail(error instanceof Error ? error.message : String(error));
1760
1781
  }
1761
1782
  }
1783
+ async function checkpoint(path, keyPath, out) {
1784
+ const { makeCheckpoint } = await import("./checkpoint-DiznOVpS.js");
1785
+ let ledger;
1786
+ try {
1787
+ ledger = Ledger.open(path);
1788
+ } catch (error) {
1789
+ return fail(error instanceof Error ? error.message : String(error));
1790
+ }
1791
+ const chain$1 = ledger.verify();
1792
+ if (!chain$1.ok) return fail(`chain broken at line ${chain$1.problems[0]?.line}: ${chain$1.problems[0]?.message} — a checkpoint of a broken chain would certify the break`);
1793
+ const privateKey = keyPath === void 0 ? void 0 : readFileSync(keyPath, "utf8");
1794
+ const cp = makeCheckpoint(ledger.read(), path.split("/").at(-1) ?? path, privateKey);
1795
+ const text = `${JSON.stringify(cp, null, 2)}\n`;
1796
+ if (out === void 0) console.log(text);
1797
+ else {
1798
+ await mkdir(dirname(out), { recursive: true });
1799
+ await writeFile(out, text);
1800
+ console.log(`checkpoint ${out} · ${cp.entries} rows · head ${cp.head.hash.slice(0, 16)}…${cp.sig === void 0 ? " · unsigned" : ` · signed ${cp.fingerprint}`}`);
1801
+ console.log("Keep it where this machine cannot write. `prae verify --checkpoint` later detects rows removed or rewritten since.");
1802
+ }
1803
+ }
1804
+ async function exportLedger(path, options) {
1805
+ const { toOcsf, ndjson, sendToHec } = await import("./ocsf-DhVGbBUw.js");
1806
+ const { createRequire } = await import("node:module");
1807
+ const { version } = createRequire(import.meta.url)("../package.json");
1808
+ let ledger;
1809
+ try {
1810
+ ledger = Ledger.open(path);
1811
+ } catch (error) {
1812
+ return fail(error instanceof Error ? error.message : String(error));
1813
+ }
1814
+ const events = toOcsf(ledger.read(), {
1815
+ product: {
1816
+ name: "prae-gate",
1817
+ version
1818
+ },
1819
+ ledger: path
1820
+ });
1821
+ if (options.hec !== void 0) {
1822
+ if (options.token === void 0) fail("--hec needs --hec-token");
1823
+ const sent = await sendToHec(events, options.hec, options.token);
1824
+ if (sent.status < 200 || sent.status >= 300) return fail(`HEC answered ${sent.status}: ${sent.body.slice(0, 200)}`);
1825
+ console.log(`${sent.sent} events → ${options.hec} (${sent.status})`);
1826
+ return;
1827
+ }
1828
+ const text = ndjson(events);
1829
+ if (options.out === void 0) {
1830
+ process.stdout.write(text);
1831
+ return;
1832
+ }
1833
+ await mkdir(dirname(options.out), { recursive: true });
1834
+ await writeFile(options.out, text);
1835
+ console.log(`${events.length} OCSF events → ${options.out} · seals carried under unmapped; the signature covers the ledger row, not the event`);
1836
+ }
1762
1837
  async function main(argv) {
1763
1838
  const [command, ...rest] = argv;
1764
1839
  switch (command) {
@@ -1798,7 +1873,23 @@ async function main(argv) {
1798
1873
  case "verify": {
1799
1874
  const path = rest[0];
1800
1875
  if (path === void 0) fail("verify needs a ledger file");
1801
- return verify(path, flag(rest, "key"));
1876
+ return verify(path, flag(rest, "key"), flag(rest, "checkpoint"));
1877
+ }
1878
+ case "checkpoint": {
1879
+ const path = rest[0];
1880
+ if (path === void 0) fail("checkpoint needs a ledger file");
1881
+ return checkpoint(path, flag(rest, "key"), flag(rest, "out"));
1882
+ }
1883
+ case "export": {
1884
+ const path = rest[0];
1885
+ if (path === void 0) fail("export needs a ledger file");
1886
+ const format = flag(rest, "format") ?? "ocsf";
1887
+ if (format !== "ocsf") fail(`unknown format ${format}; only ocsf today`);
1888
+ return exportLedger(path, {
1889
+ out: flag(rest, "out"),
1890
+ hec: flag(rest, "hec"),
1891
+ token: flag(rest, "hec-token")
1892
+ });
1802
1893
  }
1803
1894
  case "verify-receipt": {
1804
1895
  const path = rest[0];
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
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";
2
+ import "./views-CCwVXpo2.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-UAE968If.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 renderNav, l as renderShell, s as esc, t as navFor } from "./views-UODslKwu.js";
1
+ import { c as renderNav, l as renderShell, s as esc, t as navFor } from "./views-CCwVXpo2.js";
2
2
 
3
3
  //#region src/console/instructions.ts
4
4
  const STYLE = `
@@ -1,5 +1,5 @@
1
1
  import { t as evaluate } from "./evaluate-wMPkjcIC.js";
2
- import { a as scan, n as MAX_SCAN_BYTES } from "./src-Br1tGPtS.js";
2
+ import { a as scan, n as MAX_SCAN_BYTES } from "./src-B25SHWcz.js";
3
3
  import { readFile, readdir, stat } from "node:fs/promises";
4
4
  import { resolve, sep } from "node:path";
5
5
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -0,0 +1,211 @@
1
+ import { basename } from "node:path";
2
+
3
+ //#region src/ocsf.ts
4
+ const OCSF_VERSION = "1.5.0";
5
+ const DETECTION_FINDING = 2004;
6
+ const API_ACTIVITY = 6003;
7
+ /** OCSF `severity_id`. */
8
+ const SEVERITY = {
9
+ informational: 1,
10
+ low: 2,
11
+ medium: 3,
12
+ high: 4
13
+ };
14
+ /** OCSF `action_id`: what was done about it, in the schema's four words. */
15
+ const ACTION = {
16
+ allowed: 1,
17
+ denied: 2,
18
+ observed: 3
19
+ };
20
+ /** OCSF API Activity `activity_id`. */
21
+ const API_ACTIVITY_OF = {
22
+ read: 2,
23
+ list_dir: 2,
24
+ read_file: 2,
25
+ web_fetch: 2,
26
+ write: 3,
27
+ edit: 3,
28
+ str_replace_editor: 3
29
+ };
30
+ function isEvent(row) {
31
+ return row.tool.startsWith("prae:");
32
+ }
33
+ const isUrl = (subject) => /^https?:\/\//.test(subject);
34
+ /** The file or URL the call was aimed at, as an OCSF evidence artifact. */
35
+ function evidences(row) {
36
+ const subject = row.subject;
37
+ if (subject === void 0 || subject === "" || isEvent(row)) return [];
38
+ if (isUrl(subject)) return [{ url: {
39
+ url_string: subject,
40
+ hostname: subject.replace(/^https?:\/\//, "").split("/")[0] ?? ""
41
+ } }];
42
+ if (row.command !== void 0) return [];
43
+ return [{ file: {
44
+ name: basename(subject),
45
+ path: subject,
46
+ type_id: 1
47
+ } }];
48
+ }
49
+ /** The same target, as the resource an API Activity touched. */
50
+ function resources(row) {
51
+ const subject = row.subject;
52
+ if (subject === void 0 || subject === "") return [];
53
+ if (row.command !== void 0) return [{
54
+ name: row.command.program,
55
+ type: "program"
56
+ }];
57
+ return [{
58
+ name: subject,
59
+ type: isUrl(subject) ? "url" : "file"
60
+ }];
61
+ }
62
+ /** Everything the SIEM has no field for, kept verbatim. The seal lives here. */
63
+ function unmapped(row, disposition) {
64
+ return {
65
+ seq: row.seq,
66
+ disposition,
67
+ seal: row.hash,
68
+ prev: row.prev,
69
+ ...row.sig === void 0 ? {} : { sig: row.sig },
70
+ verdict: row.verdict,
71
+ ...row.rule === void 0 ? {} : { rule: row.rule },
72
+ ...row.capability === void 0 ? {} : { capability: row.capability },
73
+ ...row.phase === void 0 ? {} : { phase: row.phase },
74
+ ...row.shadow === void 0 ? {} : { shadow: row.shadow },
75
+ ...row.subject === void 0 ? {} : { subject: row.subject },
76
+ ...row.command === void 0 ? {} : { command: row.command },
77
+ ...row.secrets === void 0 ? {} : { secrets: row.secrets.map((s) => ({
78
+ type: s.type,
79
+ rule: s.rule
80
+ })) },
81
+ ...row.micros === void 0 ? {} : { micros: row.micros },
82
+ pack: row.pack,
83
+ trace: row.trace,
84
+ evidence: "PRAE ledger row. The seal and signature cover the original row, not this event; verify with `prae verify` or a receipt."
85
+ };
86
+ }
87
+ function metadata(row, options) {
88
+ const log = options.ledger === void 0 ? void 0 : basename(options.ledger);
89
+ return {
90
+ product: {
91
+ name: options.product.name,
92
+ vendor_name: "PRAE",
93
+ version: options.product.version
94
+ },
95
+ version: OCSF_VERSION,
96
+ profiles: ["security_control"],
97
+ uid: `${row.session ?? log ?? "ledger"}#${row.seq}`,
98
+ ...row.session === void 0 ? {} : { correlation_uid: row.session },
99
+ ...log === void 0 ? {} : { log_name: log },
100
+ logged_time: Date.parse(row.at)
101
+ };
102
+ }
103
+ /** What happened, in one line a SOC analyst reads first. */
104
+ function message(row) {
105
+ const what = row.command?.program ?? row.subject ?? row.tool;
106
+ if (isEvent(row)) return `${row.rule ?? row.tool}: ${row.reason ?? what}`;
107
+ const verb = row.verdict === "deny" ? row.phase === "output" ? "ran; output withheld from the model" : "refused before execution" : row.verdict === "ask" ? "held for approval" : row.rule === void 0 ? "allowed" : "allowed and recorded";
108
+ const shadow = row.shadow ? " (shadow mode)" : "";
109
+ const rule = row.rule === void 0 ? "" : ` · ${row.rule}`;
110
+ return `${row.tool} ${what}: ${verb}${shadow}${rule}`;
111
+ }
112
+ function finding(row, options) {
113
+ const event = isEvent(row);
114
+ const severity = event ? SEVERITY.informational : row.verdict === "deny" ? SEVERITY.high : row.verdict === "ask" || row.verdict === "correct" ? SEVERITY.medium : SEVERITY.low;
115
+ const disposition = event ? "logged" : row.shadow ? "logged" : row.verdict === "deny" ? row.phase === "output" ? "withheld" : "blocked" : row.verdict === "ask" ? "held" : "logged";
116
+ const action = disposition === "blocked" || disposition === "withheld" ? ACTION.denied : ACTION.observed;
117
+ const activity = 1;
118
+ return {
119
+ class_uid: DETECTION_FINDING,
120
+ category_uid: 2,
121
+ activity_id: activity,
122
+ type_uid: DETECTION_FINDING * 100 + activity,
123
+ time: Date.parse(row.at),
124
+ severity_id: severity,
125
+ status_id: 1,
126
+ action_id: action,
127
+ is_alert: !event && row.verdict === "deny" && row.shadow !== true,
128
+ message: message(row),
129
+ metadata: metadata(row, options),
130
+ finding_info: {
131
+ uid: `${row.session ?? "ledger"}#${row.seq}`,
132
+ title: event ? `PRAE ${row.rule ?? row.tool}` : `PRAE ${row.verdict}${row.phase === "output" ? " (output)" : ""}${row.rule === void 0 ? "" : ` · ${row.rule}`}`,
133
+ ...row.reason === void 0 ? {} : { desc: row.reason },
134
+ analytic: {
135
+ name: row.rule ?? "baseline",
136
+ type_id: 1,
137
+ ...row.pack === void 0 ? {} : { version: `${row.pack.name} ${row.pack.version}` }
138
+ },
139
+ ...row.capability === void 0 ? {} : { types: [row.capability] }
140
+ },
141
+ evidences: evidences(row),
142
+ unmapped: unmapped(row, disposition)
143
+ };
144
+ }
145
+ function apiActivity(row, options) {
146
+ const activity = API_ACTIVITY_OF[row.tool] ?? 99;
147
+ return {
148
+ class_uid: API_ACTIVITY,
149
+ category_uid: 6,
150
+ activity_id: activity,
151
+ type_uid: API_ACTIVITY * 100 + activity,
152
+ time: Date.parse(row.at),
153
+ severity_id: SEVERITY.informational,
154
+ status_id: 1,
155
+ action_id: ACTION.allowed,
156
+ message: message(row),
157
+ metadata: metadata(row, options),
158
+ actor: {
159
+ app_name: "dsh",
160
+ ...row.session === void 0 ? {} : { session: { uid: row.session } }
161
+ },
162
+ api: {
163
+ operation: row.tool,
164
+ request: { uid: `${row.seq}` },
165
+ response: {
166
+ code: 0,
167
+ message: "allowed"
168
+ }
169
+ },
170
+ src_endpoint: { name: "agent harness" },
171
+ resources: resources(row),
172
+ unmapped: unmapped(row, "allowed")
173
+ };
174
+ }
175
+ /** One OCSF event per ledger row, in file order. */
176
+ function toOcsf(rows, options) {
177
+ return rows.map((row) => isEvent(row) || row.verdict !== "allow" || row.rule !== void 0 ? finding(row, options) : apiActivity(row, options));
178
+ }
179
+ /** Newline-delimited JSON, the form every collector takes. */
180
+ function ndjson(events) {
181
+ return `${events.map((event) => JSON.stringify(event)).join("\n")}\n`;
182
+ }
183
+ /**
184
+ * Splunk HTTP Event Collector: one request, one JSON object per line, each
185
+ * wrapping the event with its time. Sentinel and Security Lake take the
186
+ * NDJSON file through their own ingestion; only HEC has a wire this simple.
187
+ */
188
+ async function sendToHec(events, url, token, fetchImpl = fetch) {
189
+ const endpoint = `${url.replace(/\/$/, "")}/services/collector/event`;
190
+ const body = events.map((event) => JSON.stringify({
191
+ time: Number(event.time) / 1e3,
192
+ sourcetype: "ocsf:prae",
193
+ event
194
+ })).join("\n");
195
+ const response = await fetchImpl(endpoint, {
196
+ method: "POST",
197
+ headers: {
198
+ authorization: `Splunk ${token}`,
199
+ "content-type": "application/json"
200
+ },
201
+ body
202
+ });
203
+ return {
204
+ sent: events.length,
205
+ status: response.status,
206
+ body: await response.text()
207
+ };
208
+ }
209
+
210
+ //#endregion
211
+ export { OCSF_VERSION, ndjson, sendToHec, toOcsf };
package/lib/playground.js CHANGED
@@ -491,52 +491,100 @@
491
491
 
492
492
  //#endregion
493
493
  //#region ../kernel/src/frameworks.ts
494
- const FRAMEWORKS = { "owasp-llm-top-10-2026": {
495
- name: "OWASP Top 10 for LLM Applications — 2026",
496
- short: "OWASP LLM 2026",
497
- controls: {
498
- LLM01: {
499
- name: "Prompt Injection",
500
- summary: "Instructions in a prompt or in retrieved content alter what the model does."
501
- },
502
- LLM02: {
503
- name: "Sensitive Information Disclosure",
504
- summary: "Credentials, personal data or proprietary content reach the model or its output."
505
- },
506
- LLM03: {
507
- name: "Excessive Agency",
508
- summary: "The model can take actions, or holds permissions, beyond what its task needs."
509
- },
510
- LLM04: {
511
- name: "Supply Chain",
512
- summary: "A compromised model, dataset, plugin or dependency enters the application."
513
- },
514
- LLM05: {
515
- name: "Data and Model Poisoning",
516
- summary: "Training, fine-tuning or embedding data is manipulated to change behaviour."
517
- },
518
- LLM06: {
519
- name: "Unbounded Consumption",
520
- summary: "The application permits resource use without a limit, exhausting budget or service."
521
- },
522
- LLM07: {
523
- name: "Misinformation",
524
- summary: "The model produces false or misleading content that is acted on as if true."
525
- },
526
- LLM08: {
527
- name: "Hidden Context Exposure",
528
- summary: "System prompts, hidden instructions or internal context are revealed."
529
- },
530
- LLM09: {
531
- name: "Vector and Embedding Weaknesses",
532
- summary: "Retrieval stores are manipulated or leak across tenants and permissions."
533
- },
534
- LLM10: {
535
- name: "Improper Output Handling",
536
- summary: "Model output is passed to downstream systems without validation."
494
+ const FRAMEWORKS = {
495
+ "owasp-llm-top-10-2026": {
496
+ name: "OWASP Top 10 for LLM Applications — 2026",
497
+ short: "OWASP LLM 2026",
498
+ controls: {
499
+ LLM01: {
500
+ name: "Prompt Injection",
501
+ summary: "Instructions in a prompt or in retrieved content alter what the model does."
502
+ },
503
+ LLM02: {
504
+ name: "Sensitive Information Disclosure",
505
+ summary: "Credentials, personal data or proprietary content reach the model or its output."
506
+ },
507
+ LLM03: {
508
+ name: "Excessive Agency",
509
+ summary: "The model can take actions, or holds permissions, beyond what its task needs."
510
+ },
511
+ LLM04: {
512
+ name: "Supply Chain",
513
+ summary: "A compromised model, dataset, plugin or dependency enters the application."
514
+ },
515
+ LLM05: {
516
+ name: "Data and Model Poisoning",
517
+ summary: "Training, fine-tuning or embedding data is manipulated to change behaviour."
518
+ },
519
+ LLM06: {
520
+ name: "Unbounded Consumption",
521
+ summary: "The application permits resource use without a limit, exhausting budget or service."
522
+ },
523
+ LLM07: {
524
+ name: "Misinformation",
525
+ summary: "The model produces false or misleading content that is acted on as if true."
526
+ },
527
+ LLM08: {
528
+ name: "Hidden Context Exposure",
529
+ summary: "System prompts, hidden instructions or internal context are revealed."
530
+ },
531
+ LLM09: {
532
+ name: "Vector and Embedding Weaknesses",
533
+ summary: "Retrieval stores are manipulated or leak across tenants and permissions."
534
+ },
535
+ LLM10: {
536
+ name: "Improper Output Handling",
537
+ summary: "Model output is passed to downstream systems without validation."
538
+ }
539
+ }
540
+ },
541
+ "owasp-agentic-top-10-2026": {
542
+ name: "OWASP Top 10 for Agentic Applications — 2026",
543
+ short: "OWASP Agentic 2026",
544
+ controls: {
545
+ ASI01: {
546
+ name: "Agent Goal Hijack",
547
+ summary: "Untrusted input redirects what the agent is trying to do, turning its objectives against its owner."
548
+ },
549
+ ASI02: {
550
+ name: "Tool Misuse and Exploitation",
551
+ summary: "A legitimate tool the agent already holds is turned to an attacker's ends."
552
+ },
553
+ ASI03: {
554
+ name: "Identity and Privilege Abuse",
555
+ summary: "The agent's own authority, credentials or delegated access is inherited or escalated."
556
+ },
557
+ ASI04: {
558
+ name: "Agentic Supply Chain",
559
+ summary: "A poisoned tool, model, plugin, skill or dependency is trusted before the agent runs."
560
+ },
561
+ ASI05: {
562
+ name: "Unexpected Code Execution",
563
+ summary: "The agent's output reaches something that runs it: a shell, an interpreter, a parser."
564
+ },
565
+ ASI06: {
566
+ name: "Memory and Context Poisoning",
567
+ summary: "Corrupted memory or retrieved context persists across sessions and steers future reasoning."
568
+ },
569
+ ASI07: {
570
+ name: "Insecure Inter-Agent Communication",
571
+ summary: "One agent trusts another's messages by default, so a compromised agent can steer its peers."
572
+ },
573
+ ASI08: {
574
+ name: "Cascading Failures",
575
+ summary: "A single fault propagates across agents, tools and workflows into system-wide impact."
576
+ },
577
+ ASI09: {
578
+ name: "Human-Agent Trust Exploitation",
579
+ summary: "Persuasive or misleading agent output moves a person to an unsafe action or approval."
580
+ },
581
+ ASI10: {
582
+ name: "Rogue Agents",
583
+ summary: "A compromised, misaligned or drifting agent keeps operating in unintended ways."
584
+ }
537
585
  }
538
586
  }
539
- } };
587
+ };
540
588
 
541
589
  //#endregion
542
590
  //#region ../kernel/src/pack.ts
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "prae": "0.1",
3
3
  "pack": "prae-baseline",
4
- "version": "1.1.0",
4
+ "version": "1.2.0",
5
5
  "rules": [
6
6
  {
7
7
  "id": "BASE-001",
@@ -17,7 +17,8 @@
17
17
  "because": "piping a download into an interpreter runs code nobody has read"
18
18
  },
19
19
  "frameworks": {
20
- "owasp-llm-top-10-2026": ["LLM03"]
20
+ "owasp-llm-top-10-2026": ["LLM03"],
21
+ "owasp-agentic-top-10-2026": ["ASI05", "ASI02"]
21
22
  }
22
23
  },
23
24
  {
@@ -35,7 +36,8 @@
35
36
  "because": "a recursive force-delete is not reversible and needs a human"
36
37
  },
37
38
  "frameworks": {
38
- "owasp-llm-top-10-2026": ["LLM03"]
39
+ "owasp-llm-top-10-2026": ["LLM03"],
40
+ "owasp-agentic-top-10-2026": ["ASI02"]
39
41
  }
40
42
  },
41
43
  {
@@ -51,7 +53,8 @@
51
53
  "because": "escalating privileges needs a human"
52
54
  },
53
55
  "frameworks": {
54
- "owasp-llm-top-10-2026": ["LLM03"]
56
+ "owasp-llm-top-10-2026": ["LLM03"],
57
+ "owasp-agentic-top-10-2026": ["ASI03"]
55
58
  }
56
59
  },
57
60
  {
@@ -73,7 +76,8 @@
73
76
  "because": "block-device and filesystem tools are not granted to agents"
74
77
  },
75
78
  "frameworks": {
76
- "owasp-llm-top-10-2026": ["LLM03"]
79
+ "owasp-llm-top-10-2026": ["LLM03"],
80
+ "owasp-agentic-top-10-2026": ["ASI02"]
77
81
  }
78
82
  },
79
83
  {
@@ -98,7 +102,8 @@
98
102
  "because": "secrets and credential stores are out of scope for agents"
99
103
  },
100
104
  "frameworks": {
101
- "owasp-llm-top-10-2026": ["LLM02"]
105
+ "owasp-llm-top-10-2026": ["LLM02"],
106
+ "owasp-agentic-top-10-2026": ["ASI03"]
102
107
  }
103
108
  },
104
109
  {
@@ -115,7 +120,8 @@
115
120
  "because": "writing outside the working tree needs a human"
116
121
  },
117
122
  "frameworks": {
118
- "owasp-llm-top-10-2026": ["LLM03"]
123
+ "owasp-llm-top-10-2026": ["LLM03"],
124
+ "owasp-agentic-top-10-2026": ["ASI02"]
119
125
  }
120
126
  },
121
127
  {
@@ -132,7 +138,8 @@
132
138
  "because": "rewriting shared history needs a human"
133
139
  },
134
140
  "frameworks": {
135
- "owasp-llm-top-10-2026": ["LLM03"]
141
+ "owasp-llm-top-10-2026": ["LLM03"],
142
+ "owasp-agentic-top-10-2026": ["ASI02"]
136
143
  }
137
144
  },
138
145
  {
@@ -149,7 +156,8 @@
149
156
  "because": "publishing is outward-facing and needs a human"
150
157
  },
151
158
  "frameworks": {
152
- "owasp-llm-top-10-2026": ["LLM03"]
159
+ "owasp-llm-top-10-2026": ["LLM03"],
160
+ "owasp-agentic-top-10-2026": ["ASI02"]
153
161
  }
154
162
  }
155
163
  ]
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "prae": "0.1",
3
3
  "pack": "secret-catcher",
4
- "version": "1.0.0",
4
+ "version": "1.1.0",
5
5
  "rules": [
6
6
  {
7
7
  "id": "SEC-001",
@@ -26,7 +26,8 @@
26
26
  "because": "the requested file contains a protected credential; the contents were not returned"
27
27
  },
28
28
  "frameworks": {
29
- "owasp-llm-top-10-2026": ["LLM02"]
29
+ "owasp-llm-top-10-2026": ["LLM02"],
30
+ "owasp-agentic-top-10-2026": ["ASI03"]
30
31
  }
31
32
  },
32
33
  {
@@ -43,7 +44,8 @@
43
44
  "because": "the requested file contains a private key; the contents were not returned"
44
45
  },
45
46
  "frameworks": {
46
- "owasp-llm-top-10-2026": ["LLM02"]
47
+ "owasp-llm-top-10-2026": ["LLM02"],
48
+ "owasp-agentic-top-10-2026": ["ASI03"]
47
49
  }
48
50
  },
49
51
  {
@@ -74,7 +76,8 @@
74
76
  "because": "the command ran and its output contained a protected credential; the output was withheld from the model"
75
77
  },
76
78
  "frameworks": {
77
- "owasp-llm-top-10-2026": ["LLM02"]
79
+ "owasp-llm-top-10-2026": ["LLM02"],
80
+ "owasp-agentic-top-10-2026": ["ASI03"]
78
81
  }
79
82
  }
80
83
  ]
@@ -1,3 +1,3 @@
1
- import { a as scan, i as looksBinary, n as MAX_SCAN_BYTES, r as entropy, t as DETECTORS } from "./src-Br1tGPtS.js";
1
+ import { a as scan, i as looksBinary, n as MAX_SCAN_BYTES, r as entropy, t as DETECTORS } from "./src-B25SHWcz.js";
2
2
 
3
3
  export { DETECTORS, MAX_SCAN_BYTES, entropy, looksBinary, scan };
@@ -1,50 +1,98 @@
1
1
  //#region ../kernel/src/frameworks.ts
2
- const FRAMEWORKS = { "owasp-llm-top-10-2026": {
3
- name: "OWASP Top 10 for LLM Applications — 2026",
4
- short: "OWASP LLM 2026",
5
- controls: {
6
- LLM01: {
7
- name: "Prompt Injection",
8
- summary: "Instructions in a prompt or in retrieved content alter what the model does."
9
- },
10
- LLM02: {
11
- name: "Sensitive Information Disclosure",
12
- summary: "Credentials, personal data or proprietary content reach the model or its output."
13
- },
14
- LLM03: {
15
- name: "Excessive Agency",
16
- summary: "The model can take actions, or holds permissions, beyond what its task needs."
17
- },
18
- LLM04: {
19
- name: "Supply Chain",
20
- summary: "A compromised model, dataset, plugin or dependency enters the application."
21
- },
22
- LLM05: {
23
- name: "Data and Model Poisoning",
24
- summary: "Training, fine-tuning or embedding data is manipulated to change behaviour."
25
- },
26
- LLM06: {
27
- name: "Unbounded Consumption",
28
- summary: "The application permits resource use without a limit, exhausting budget or service."
29
- },
30
- LLM07: {
31
- name: "Misinformation",
32
- summary: "The model produces false or misleading content that is acted on as if true."
33
- },
34
- LLM08: {
35
- name: "Hidden Context Exposure",
36
- summary: "System prompts, hidden instructions or internal context are revealed."
37
- },
38
- LLM09: {
39
- name: "Vector and Embedding Weaknesses",
40
- summary: "Retrieval stores are manipulated or leak across tenants and permissions."
41
- },
42
- LLM10: {
43
- name: "Improper Output Handling",
44
- summary: "Model output is passed to downstream systems without validation."
2
+ const FRAMEWORKS = {
3
+ "owasp-llm-top-10-2026": {
4
+ name: "OWASP Top 10 for LLM Applications — 2026",
5
+ short: "OWASP LLM 2026",
6
+ controls: {
7
+ LLM01: {
8
+ name: "Prompt Injection",
9
+ summary: "Instructions in a prompt or in retrieved content alter what the model does."
10
+ },
11
+ LLM02: {
12
+ name: "Sensitive Information Disclosure",
13
+ summary: "Credentials, personal data or proprietary content reach the model or its output."
14
+ },
15
+ LLM03: {
16
+ name: "Excessive Agency",
17
+ summary: "The model can take actions, or holds permissions, beyond what its task needs."
18
+ },
19
+ LLM04: {
20
+ name: "Supply Chain",
21
+ summary: "A compromised model, dataset, plugin or dependency enters the application."
22
+ },
23
+ LLM05: {
24
+ name: "Data and Model Poisoning",
25
+ summary: "Training, fine-tuning or embedding data is manipulated to change behaviour."
26
+ },
27
+ LLM06: {
28
+ name: "Unbounded Consumption",
29
+ summary: "The application permits resource use without a limit, exhausting budget or service."
30
+ },
31
+ LLM07: {
32
+ name: "Misinformation",
33
+ summary: "The model produces false or misleading content that is acted on as if true."
34
+ },
35
+ LLM08: {
36
+ name: "Hidden Context Exposure",
37
+ summary: "System prompts, hidden instructions or internal context are revealed."
38
+ },
39
+ LLM09: {
40
+ name: "Vector and Embedding Weaknesses",
41
+ summary: "Retrieval stores are manipulated or leak across tenants and permissions."
42
+ },
43
+ LLM10: {
44
+ name: "Improper Output Handling",
45
+ summary: "Model output is passed to downstream systems without validation."
46
+ }
47
+ }
48
+ },
49
+ "owasp-agentic-top-10-2026": {
50
+ name: "OWASP Top 10 for Agentic Applications — 2026",
51
+ short: "OWASP Agentic 2026",
52
+ controls: {
53
+ ASI01: {
54
+ name: "Agent Goal Hijack",
55
+ summary: "Untrusted input redirects what the agent is trying to do, turning its objectives against its owner."
56
+ },
57
+ ASI02: {
58
+ name: "Tool Misuse and Exploitation",
59
+ summary: "A legitimate tool the agent already holds is turned to an attacker's ends."
60
+ },
61
+ ASI03: {
62
+ name: "Identity and Privilege Abuse",
63
+ summary: "The agent's own authority, credentials or delegated access is inherited or escalated."
64
+ },
65
+ ASI04: {
66
+ name: "Agentic Supply Chain",
67
+ summary: "A poisoned tool, model, plugin, skill or dependency is trusted before the agent runs."
68
+ },
69
+ ASI05: {
70
+ name: "Unexpected Code Execution",
71
+ summary: "The agent's output reaches something that runs it: a shell, an interpreter, a parser."
72
+ },
73
+ ASI06: {
74
+ name: "Memory and Context Poisoning",
75
+ summary: "Corrupted memory or retrieved context persists across sessions and steers future reasoning."
76
+ },
77
+ ASI07: {
78
+ name: "Insecure Inter-Agent Communication",
79
+ summary: "One agent trusts another's messages by default, so a compromised agent can steer its peers."
80
+ },
81
+ ASI08: {
82
+ name: "Cascading Failures",
83
+ summary: "A single fault propagates across agents, tools and workflows into system-wide impact."
84
+ },
85
+ ASI09: {
86
+ name: "Human-Agent Trust Exploitation",
87
+ summary: "Persuasive or misleading agent output moves a person to an unsafe action or approval."
88
+ },
89
+ ASI10: {
90
+ name: "Rogue Agents",
91
+ summary: "A compromised, misaligned or drifting agent keeps operating in unintended ways."
92
+ }
45
93
  }
46
94
  }
47
- } };
95
+ };
48
96
  /**
49
97
  * Every framework control a rule cites, resolved to names.
50
98
  *
@@ -802,6 +850,7 @@ const OBLIGATIONS = [
802
850
  }
803
851
  ];
804
852
  const OWASP = "owasp-llm-top-10-2026";
853
+ const AGENTIC = "owasp-agentic-top-10-2026";
805
854
  /** Rules in the pack that cite one control of one framework. */
806
855
  function citing(pack, framework, control) {
807
856
  return (pack?.rules ?? []).filter((rule) => (rule.frameworks?.[framework] ?? []).includes(control));
@@ -819,32 +868,91 @@ const NOT_THIS_BOUNDARY = {
819
868
  LLM07: "Whether an answer is true is not a property of a tool call.",
820
869
  LLM08: "The assembled system prompt is sealed into the ledger as evidence of what the model was told; nothing here stops the model repeating it.",
821
870
  LLM09: "Retrieval stores are upstream of the call the gate judges.",
822
- LLM10: "What downstream code does with model output is that code's boundary, not this one's."
871
+ LLM10: "What downstream code does with model output is that code's boundary, not this one's.",
872
+ ASI04: "Where a tool, skill or plugin came from is settled before the agent runs. A rule on the skill tool (integrity, source) is how a pack refuses to trust one at load time; none in this pack does.",
873
+ ASI06: "Memory and retrieval stores are upstream of the call the gate judges. What the gate can do is seal what the agent was told (PRAE-ASM, PRAE-CTX) so a poisoned context is evidenced, never silent; this record holds no such seal.",
874
+ ASI07: "A message between agents is not a tool call; when it becomes one, it is judged like any other agent's call. The channel itself is outside this boundary.",
875
+ ASI09: "Whether a person was misled is not a property of a tool call. An ask verdict puts a human in the path and seals the outcome; nothing here judges the persuasion."
876
+ };
877
+ /**
878
+ * A risk this gate answers by *not* answering it: nothing here detects an
879
+ * injected instruction or a hijacked goal, and saying otherwise would be the
880
+ * claim a red team disproves first. What the gate does is bound what the
881
+ * subverted agent can execute — the cited controls of another risk doing
882
+ * their job. Said in those words, every time.
883
+ */
884
+ const BOUNDED_BY = {
885
+ LLM01: {
886
+ framework: OWASP,
887
+ control: "LLM03"
888
+ },
889
+ ASI01: {
890
+ framework: AGENTIC,
891
+ control: "ASI02"
892
+ }
893
+ };
894
+ function bounded(data, risk, by) {
895
+ const bounding = citing(data.pack, by.framework, by.control);
896
+ const what = risk === "ASI01" ? "a hijacked goal" : "injected instructions";
897
+ const who = risk === "ASI01" ? "a hijacked agent" : "an injected instruction";
898
+ return {
899
+ state: "partial",
900
+ note: bounding.length === 0 ? `Not mitigated: this gate does not detect ${what}, and no ${by.control} control in this pack bounds what ${who} can execute.` : `Not mitigated: this gate does not detect ${what}. Bounded: ${bounding.length} ${by.control} control${bounding.length === 1 ? "" : "s"} (${bounding.map((r) => r.id).join(", ")}) decide what ${who} can execute.`
901
+ };
902
+ }
903
+ /**
904
+ * Risks whose evidence is in the record rather than in a citing rule.
905
+ *
906
+ * ASI06: a poisoned context cannot be prevented here, but every change to what
907
+ * the agent was told is sealed, so it is evidenced. ASI10: a drifting agent's
908
+ * actions are judged like any other's; what the record shows is whether the
909
+ * gate refused any of them.
910
+ */
911
+ const FROM_THE_RECORD = {
912
+ ASI06: (data) => {
913
+ const seals = data.entries.filter((e) => e.rule === "PRAE-ASM" || e.rule === "PRAE-CTX");
914
+ return seals.length === 0 ? void 0 : {
915
+ state: "partial",
916
+ note: `Evidenced, not prevented: ${seals.length} seal${seals.length === 1 ? "" : "s"} of what the agent was told (PRAE-ASM / PRAE-CTX) in this record; a poisoned context changes the hash and is answerable afterwards.`
917
+ };
918
+ },
919
+ ASI10: (data) => {
920
+ const refused = data.entries.filter((e) => e.verdict === "deny").length;
921
+ const armed = [
922
+ "ASI02",
923
+ "ASI03",
924
+ "ASI08"
925
+ ].flatMap((c) => citing(data.pack, AGENTIC, c));
926
+ if (armed.length === 0) return void 0;
927
+ return {
928
+ state: refused > 0 ? "covered" : "partial",
929
+ note: refused > 0 ? `A drifting agent's actions are judged like any other's: ${armed.length} controls under ASI02/03/08 apply to every call, and ${refused} refusal${refused === 1 ? "" : "s"} in this record show them holding.` : `A drifting agent's actions are judged like any other's: ${armed.length} controls under ASI02/03/08 apply to every call; none has refused one in this record yet. Detecting drift itself is derived from the record, not a rule.`
930
+ };
931
+ }
823
932
  };
824
- function owaspRow(control) {
825
- const framework = FRAMEWORKS[OWASP];
933
+ function frameworkRow(frameworkId, control) {
934
+ const framework = FRAMEWORKS[frameworkId];
826
935
  const risk = framework?.controls[control];
827
936
  const name = risk?.name ?? control;
828
937
  return {
829
- id: `${framework?.short ?? OWASP} · ${control}`,
938
+ id: `${framework?.short ?? frameworkId} · ${control}`,
830
939
  asks: `${name}. ${risk?.summary ?? ""}`.trim(),
831
940
  evidence: (data) => {
832
941
  if (data.pack === void 0) return {
833
942
  state: "absent",
834
943
  note: "No pack supplied to this view."
835
944
  };
836
- if (control === "LLM01") {
837
- const bounding = citing(data.pack, OWASP, "LLM03");
945
+ const by = BOUNDED_BY[control];
946
+ if (by !== void 0) return bounded(data, control, by);
947
+ const rules = citing(data.pack, frameworkId, control);
948
+ if (rules.length === 0) {
949
+ const derived = FROM_THE_RECORD[control]?.(data);
950
+ if (derived !== void 0) return derived;
838
951
  return {
839
- state: "partial",
840
- note: bounding.length === 0 ? "Not mitigated: this gate does not detect injected instructions, and no LLM03 control in this pack bounds what one can execute." : `Not mitigated: this gate does not detect injected instructions. Bounded: ${bounding.length} LLM03 control${bounding.length === 1 ? "" : "s"} (${bounding.map((r) => r.id).join(", ")}) decide what an injected instruction can execute.`
952
+ state: "absent",
953
+ note: NOT_THIS_BOUNDARY[control] ?? "No rule in this pack cites this risk."
841
954
  };
842
955
  }
843
- const rules = citing(data.pack, OWASP, control);
844
- if (rules.length === 0) return {
845
- state: "absent",
846
- note: NOT_THIS_BOUNDARY[control] ?? "No rule in this pack cites this risk."
847
- };
848
956
  const ids = new Set(rules.map((rule) => rule.id));
849
957
  const decisions = data.entries.filter((entry) => entry.rule !== void 0 && ids.has(entry.rule));
850
958
  const listed = rules.map((rule) => rule.id).join(", ");
@@ -858,14 +966,18 @@ function owaspRow(control) {
858
966
  }
859
967
  };
860
968
  }
861
- const OWASP_ROWS = Object.keys(FRAMEWORKS[OWASP]?.controls ?? {}).map(owaspRow);
969
+ const rowsOf = (frameworkId) => Object.keys(FRAMEWORKS[frameworkId]?.controls ?? {}).map((control) => frameworkRow(frameworkId, control));
862
970
  /**
863
- * The obligations first, then the OWASP risks. Two kinds of row in one table
864
- * on purpose: an auditor reads down one list and finds the audit obligations
865
- * the ledger evidences and the application risks the pack's rules address,
866
- * each stated in its framework's own words.
971
+ * The obligations first, then the risks framework by framework. Two kinds of
972
+ * row in one table on purpose: an auditor reads down one list and finds the
973
+ * audit obligations the ledger evidences and the application risks the pack's
974
+ * rules address, each stated in its framework's own words.
867
975
  */
868
- const CONTROLS = [...OBLIGATIONS, ...OWASP_ROWS];
976
+ const CONTROLS = [
977
+ ...OBLIGATIONS,
978
+ ...rowsOf(OWASP),
979
+ ...rowsOf(AGENTIC)
980
+ ];
869
981
 
870
982
  //#endregion
871
983
  //#region src/console/views.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prae-cli",
3
- "version": "0.7.1",
3
+ "version": "0.8.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",
@@ -42,10 +42,10 @@
42
42
  "vitest": "^3.2.4",
43
43
  "@prae/extract": "0.1.0",
44
44
  "@prae/inspect": "0.1.0",
45
+ "@prae/controls": "0.1.0",
45
46
  "@prae/instructions": "0.1.0",
46
47
  "@prae/kernel": "0.1.0",
47
48
  "@prae/ledger": "0.1.0",
48
- "@prae/controls": "0.1.0",
49
49
  "@prae/tokens": "0.1.0"
50
50
  },
51
51
  "scripts": {
File without changes
File without changes
File without changes