prae-cli 0.6.0 → 0.6.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/README.md +8 -0
- package/lib/bin.js +3 -3
- package/lib/{cli-Cr0U-PFD.js → cli-BUgU-LpN.js} +58 -17
- package/lib/index.js +3 -3
- package/lib/{instructions-D4VOn2lk.js → instructions-oGCdVxHM.js} +1 -1
- package/lib/playground.js +83 -1
- package/lib/{receipt-check-Dql0uqxw.js → receipt-check-DrDE3Wwi.js} +1 -1
- package/lib/secret-catcher.prae.json +18 -3
- package/lib/{src-Kubj3MTX.js → src-C9L6NBvu.js} +1 -1
- package/lib/{views-DalPkBZZ.js → views-CAD9738P.js} +157 -10
- package/package.json +2 -2
- /package/lib/{src-CXU0_W1P.js → src-Ciggx9ja.js} +0 -0
package/README.md
CHANGED
|
@@ -90,6 +90,14 @@ next to the result rather than in a footnote: a removed, edited, or reordered
|
|
|
90
90
|
row breaks the chain; a rewrite of the whole file by someone holding this code
|
|
91
91
|
does not.
|
|
92
92
|
|
|
93
|
+
### `prae report <ledger.jsonl> [--out <file.html>] [--pack <pack.json>]`
|
|
94
|
+
|
|
95
|
+
Render the decision record as a printable evidence report. With `--pack`,
|
|
96
|
+
each clause also names the framework risk the rule cites — OWASP Top 10 for
|
|
97
|
+
LLM Applications 2026 today — so a reviewer reads risk → control → decision →
|
|
98
|
+
seal in one row. Without the pack the clause id stands alone rather than a
|
|
99
|
+
name guessed from it.
|
|
100
|
+
|
|
93
101
|
### `prae console <ledger.jsonl> --out <dir> [--pack <file>]`
|
|
94
102
|
|
|
95
103
|
Write the control plane as linked pages: Overview, Policy packs, Playground,
|
package/lib/bin.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "./evaluate-CkYySl7T.js";
|
|
3
|
-
import
|
|
4
|
-
import "./
|
|
5
|
-
import "./
|
|
3
|
+
import "./views-CAD9738P.js";
|
|
4
|
+
import { t as main } from "./cli-BUgU-LpN.js";
|
|
5
|
+
import "./src-Ciggx9ja.js";
|
|
6
6
|
|
|
7
7
|
//#region src/bin.ts
|
|
8
8
|
main(process.argv.slice(2)).catch((error) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as evaluate } from "./evaluate-CkYySl7T.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
4
|
import { existsSync, readFileSync } 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";
|
|
@@ -302,7 +302,8 @@ function readRule(check$1, path, value) {
|
|
|
302
302
|
"id",
|
|
303
303
|
"title",
|
|
304
304
|
"when",
|
|
305
|
-
"effect"
|
|
305
|
+
"effect",
|
|
306
|
+
"frameworks"
|
|
306
307
|
]);
|
|
307
308
|
const id = check$1.text(`${path}.id`, value.id);
|
|
308
309
|
const effect = readEffect(check$1, `${path}.effect`, value.effect);
|
|
@@ -318,8 +319,40 @@ function readRule(check$1, path, value) {
|
|
|
318
319
|
const title = check$1.text(`${path}.title`, value.title);
|
|
319
320
|
if (title !== void 0) rule.title = title;
|
|
320
321
|
}
|
|
322
|
+
if (value.frameworks !== void 0) {
|
|
323
|
+
const frameworks = readFrameworks(check$1, `${path}.frameworks`, value.frameworks);
|
|
324
|
+
if (frameworks !== void 0) rule.frameworks = frameworks;
|
|
325
|
+
}
|
|
321
326
|
return rule;
|
|
322
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* A citation names a framework edition and controls the registry knows. A
|
|
330
|
+
* typo here would be a rule that claims a mapping no view can render, so it
|
|
331
|
+
* is a pack error rather than an ignored key — the same line the rest of the
|
|
332
|
+
* parser takes.
|
|
333
|
+
*/
|
|
334
|
+
function readFrameworks(check$1, path, value) {
|
|
335
|
+
if (!isObject(value)) {
|
|
336
|
+
check$1.fail(path, "expected an object keyed by framework id");
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const out = {};
|
|
340
|
+
for (const [framework, controls] of Object.entries(value)) {
|
|
341
|
+
const known = FRAMEWORKS[framework];
|
|
342
|
+
if (known === void 0) {
|
|
343
|
+
check$1.fail(`${path}.${framework}`, `unknown framework; expected one of ${Object.keys(FRAMEWORKS).map((k) => `\`${k}\``).join(", ")}`);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (!Array.isArray(controls) || !controls.every((c) => typeof c === "string")) {
|
|
347
|
+
check$1.fail(`${path}.${framework}`, "expected an array of control ids");
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const ids = controls;
|
|
351
|
+
for (const control of ids) if (known.controls[control] === void 0) check$1.fail(`${path}.${framework}`, `unknown control \`${control}\` in ${known.name}`);
|
|
352
|
+
if (ids.length > 0) out[framework] = [...new Set(ids)];
|
|
353
|
+
}
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
323
356
|
/** Parse and validate a pack. Never throws; every problem lands in `errors`. */
|
|
324
357
|
function parsePack(source) {
|
|
325
358
|
let raw;
|
|
@@ -404,14 +437,16 @@ function ago(iso, now) {
|
|
|
404
437
|
const s = seconds % 60;
|
|
405
438
|
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
|
406
439
|
}
|
|
407
|
-
function row(entry) {
|
|
440
|
+
function row(entry, pack) {
|
|
408
441
|
const call = entry.subject === void 0 ? entry.tool : `${entry.tool} ${entry.subject}`;
|
|
409
442
|
const actor = entry.actor ?? entry.agent ?? "—";
|
|
443
|
+
const rule = pack?.rules.find((r) => r.id === entry.rule);
|
|
444
|
+
const cited = rule === void 0 ? "" : cite(rule).map((c) => `<br><span style="color:var(--secondary)">${esc(c.control)} ${esc(c.controlName)}</span>`).join("");
|
|
410
445
|
return ` <div class="grid row">
|
|
411
446
|
<div class="seq">${entry.seq}</div>
|
|
412
447
|
<div class="call">${esc(call)}</div>
|
|
413
448
|
<div class="actor">${esc(actor)}</div>
|
|
414
|
-
<div class="clause">${esc(entry.rule ?? "—")}</div>
|
|
449
|
+
<div class="clause">${esc(entry.rule ?? "—")}${cited}</div>
|
|
415
450
|
<div class="verdict v-${esc(entry.verdict)}">${esc(VERDICT_LABEL$1[entry.verdict] ?? entry.verdict)}</div>
|
|
416
451
|
<div class="seal">${esc(seal(entry))}</div>
|
|
417
452
|
</div>`;
|
|
@@ -452,7 +487,7 @@ function renderLedgerView(entries, verification, options) {
|
|
|
452
487
|
const table = ` <div class="grid thead">
|
|
453
488
|
<div>SEQ</div><div>CALL</div><div>ACTOR</div><div>CLAUSE</div><div>VERDICT</div><div>SEAL</div>
|
|
454
489
|
</div>
|
|
455
|
-
${entries.length === 0 ? " <div class=\"empty\">No decisions recorded yet.</div>" : [...entries].reverse().map(row).join("\n")}`;
|
|
490
|
+
${entries.length === 0 ? " <div class=\"empty\">No decisions recorded yet.</div>" : [...entries].reverse().map((entry) => row(entry, options.pack)).join("\n")}`;
|
|
456
491
|
const first = held.at(-1);
|
|
457
492
|
const awaiting = first === void 0 ? "" : ` <section class="await">
|
|
458
493
|
<div class="k">AWAITING HUMAN · 1 OF ${held.length}</div>
|
|
@@ -874,8 +909,11 @@ const USAGE = `prae — operator policy for agent harnesses
|
|
|
874
909
|
Check one receipt with nothing but the file: recompute its hash, verify
|
|
875
910
|
its signature, and print the key fingerprint to compare against your own.
|
|
876
911
|
|
|
877
|
-
prae report <ledger.jsonl> [--out <file.html>]
|
|
878
|
-
Render the decision record as a printable evidence report.
|
|
912
|
+
prae report <ledger.jsonl> [--out <file.html>] [--pack <pack.json>]
|
|
913
|
+
Render the decision record as a printable evidence report. With
|
|
914
|
+
--pack, each clause also names the framework risk it addresses
|
|
915
|
+
(OWASP LLM Top 10 2026), so a reviewer can trace risk → control →
|
|
916
|
+
decision → seal.
|
|
879
917
|
|
|
880
918
|
prae console <ledger.jsonl> --out <dir> [--pack <file>] [--serve [port]]
|
|
881
919
|
[--instructions <dir>]
|
|
@@ -925,6 +963,7 @@ async function check(path) {
|
|
|
925
963
|
for (const rule of pack.rules) {
|
|
926
964
|
const label = rule.title ?? rule.effect.because;
|
|
927
965
|
console.log(` ${rule.id.padEnd(10)} ${rule.effect.kind.toUpperCase().padEnd(8)} ${label}`);
|
|
966
|
+
for (const c of cite(rule)) console.log(` ${"".padEnd(10)} ${"".padEnd(8)} ↳ ${c.frameworkShort} · ${c.control} ${c.controlName}`);
|
|
928
967
|
}
|
|
929
968
|
}
|
|
930
969
|
/**
|
|
@@ -982,7 +1021,7 @@ async function mcp(root, packPath, ledgerPath) {
|
|
|
982
1021
|
const chosen = packPath ?? bundled;
|
|
983
1022
|
if (chosen === void 0) return fail("mcp needs --pack <file>; no bundled secret-catcher found");
|
|
984
1023
|
const pack = await loadPack(chosen);
|
|
985
|
-
const { Ledger: Ledger$1 } = await import("./src-
|
|
1024
|
+
const { Ledger: Ledger$1 } = await import("./src-C9L6NBvu.js");
|
|
986
1025
|
const ledger = ledgerPath === void 0 ? void 0 : Ledger$1.open(ledgerPath);
|
|
987
1026
|
console.error(`prae mcp: serving ${resolve$1(root)} under ${pack.pack} ${pack.version}` + (ledgerPath === void 0 ? " (no ledger)" : ` · ledger ${ledgerPath}`));
|
|
988
1027
|
await serveMcp({
|
|
@@ -1034,7 +1073,7 @@ async function explain(path, tool, pairs) {
|
|
|
1034
1073
|
* a key that arrived beside it is checked against nothing.
|
|
1035
1074
|
*/
|
|
1036
1075
|
async function keygen(dir) {
|
|
1037
|
-
const { generateKeypair, fingerprint } = await import("./src-
|
|
1076
|
+
const { generateKeypair, fingerprint } = await import("./src-C9L6NBvu.js");
|
|
1038
1077
|
const { writeFileSync: writeFileSync$1 } = await import("node:fs");
|
|
1039
1078
|
const keys = generateKeypair();
|
|
1040
1079
|
const priv = join(dir, "prae-signing.pem");
|
|
@@ -1050,7 +1089,7 @@ async function keygen(dir) {
|
|
|
1050
1089
|
console.log("private key with `signingKey` to sign every row.");
|
|
1051
1090
|
}
|
|
1052
1091
|
async function receipt(path, seq, keyPath, out) {
|
|
1053
|
-
const { Ledger: Ledger$1, makeReceipt, fingerprint } = await import("./src-
|
|
1092
|
+
const { Ledger: Ledger$1, makeReceipt, fingerprint } = await import("./src-C9L6NBvu.js");
|
|
1054
1093
|
const { readFileSync: readFileSync$1 } = await import("node:fs");
|
|
1055
1094
|
const { createPublicKey } = await import("node:crypto");
|
|
1056
1095
|
const privateKey = readFileSync$1(keyPath, "utf8");
|
|
@@ -1080,7 +1119,7 @@ async function receipt(path, seq, keyPath, out) {
|
|
|
1080
1119
|
* file and sets the exit code, so scripts can branch on it.
|
|
1081
1120
|
*/
|
|
1082
1121
|
async function verifyReceiptCmd(path) {
|
|
1083
|
-
const { checkReceipt } = await import("./receipt-check-
|
|
1122
|
+
const { checkReceipt } = await import("./receipt-check-DrDE3Wwi.js");
|
|
1084
1123
|
let parsed;
|
|
1085
1124
|
try {
|
|
1086
1125
|
parsed = JSON.parse(await readFile(path, "utf8"));
|
|
@@ -1151,7 +1190,7 @@ async function verify(path, keyPath) {
|
|
|
1151
1190
|
const last = entries.at(-1);
|
|
1152
1191
|
console.log(`${result.entries} entries · chain intact${last === void 0 ? "" : ` · seal ${seal(last)}`}`);
|
|
1153
1192
|
if (keyPath !== void 0) {
|
|
1154
|
-
const { verifySignature, fingerprint } = await import("./src-
|
|
1193
|
+
const { verifySignature, fingerprint } = await import("./src-C9L6NBvu.js");
|
|
1155
1194
|
const { readFileSync: readFileSync$1 } = await import("node:fs");
|
|
1156
1195
|
const publicKey = readFileSync$1(keyPath, "utf8");
|
|
1157
1196
|
let signed = 0;
|
|
@@ -1190,18 +1229,20 @@ function flag(argv, name) {
|
|
|
1190
1229
|
* has to go and check separately. A report that looks authoritative while
|
|
1191
1230
|
* quietly sitting on a broken chain is worse than no report.
|
|
1192
1231
|
*/
|
|
1193
|
-
async function report(path, out) {
|
|
1232
|
+
async function report(path, out, packPath) {
|
|
1194
1233
|
let ledger;
|
|
1195
1234
|
try {
|
|
1196
1235
|
ledger = Ledger.open(path);
|
|
1197
1236
|
} catch (error) {
|
|
1198
1237
|
return fail(error instanceof Error ? error.message : String(error));
|
|
1199
1238
|
}
|
|
1239
|
+
const pack = packPath === void 0 ? void 0 : await loadPack(packPath);
|
|
1200
1240
|
const entries = ledger.read();
|
|
1201
1241
|
const verification = ledger.verify();
|
|
1202
1242
|
const html = renderLedgerView(entries, verification, {
|
|
1203
1243
|
ledgerPath: path,
|
|
1204
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1244
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1245
|
+
...pack === void 0 ? {} : { pack }
|
|
1205
1246
|
});
|
|
1206
1247
|
if (out === void 0) {
|
|
1207
1248
|
process.stdout.write(html);
|
|
@@ -1325,7 +1366,7 @@ async function consoleOut(path, argv) {
|
|
|
1325
1366
|
const [{ ingest }, { extract }, { renderInstructions }] = await Promise.all([
|
|
1326
1367
|
import("./src-CraUX64d.js"),
|
|
1327
1368
|
import("./src-CQ0UmFUK.js"),
|
|
1328
|
-
import("./instructions-
|
|
1369
|
+
import("./instructions-oGCdVxHM.js")
|
|
1329
1370
|
]);
|
|
1330
1371
|
const read = ingest(instructionsDir);
|
|
1331
1372
|
instructionsView = renderInstructions(data, read, extract(read.statements));
|
|
@@ -1460,7 +1501,7 @@ async function main(argv) {
|
|
|
1460
1501
|
case "report": {
|
|
1461
1502
|
const path = rest[0];
|
|
1462
1503
|
if (path === void 0) fail("report needs a ledger file");
|
|
1463
|
-
return report(path, flag(rest, "out"));
|
|
1504
|
+
return report(path, flag(rest, "out"), flag(rest, "pack"));
|
|
1464
1505
|
}
|
|
1465
1506
|
case "console": {
|
|
1466
1507
|
const path = rest[0];
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "./evaluate-CkYySl7T.js";
|
|
2
|
-
import
|
|
3
|
-
import "./
|
|
4
|
-
import "./
|
|
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";
|
|
4
|
+
import "./src-Ciggx9ja.js";
|
|
5
5
|
|
|
6
6
|
export { applyWrite, defaultProfile, dshHome, main, planWrite, praeRow, profileAt };
|
package/lib/playground.js
CHANGED
|
@@ -466,6 +466,55 @@
|
|
|
466
466
|
};
|
|
467
467
|
}
|
|
468
468
|
|
|
469
|
+
//#endregion
|
|
470
|
+
//#region ../kernel/src/frameworks.ts
|
|
471
|
+
const FRAMEWORKS = { "owasp-llm-top-10-2026": {
|
|
472
|
+
name: "OWASP Top 10 for LLM Applications — 2026",
|
|
473
|
+
short: "OWASP LLM 2026",
|
|
474
|
+
controls: {
|
|
475
|
+
LLM01: {
|
|
476
|
+
name: "Prompt Injection",
|
|
477
|
+
summary: "Instructions in a prompt or in retrieved content alter what the model does."
|
|
478
|
+
},
|
|
479
|
+
LLM02: {
|
|
480
|
+
name: "Sensitive Information Disclosure",
|
|
481
|
+
summary: "Credentials, personal data or proprietary content reach the model or its output."
|
|
482
|
+
},
|
|
483
|
+
LLM03: {
|
|
484
|
+
name: "Excessive Agency",
|
|
485
|
+
summary: "The model can take actions, or holds permissions, beyond what its task needs."
|
|
486
|
+
},
|
|
487
|
+
LLM04: {
|
|
488
|
+
name: "Supply Chain",
|
|
489
|
+
summary: "A compromised model, dataset, plugin or dependency enters the application."
|
|
490
|
+
},
|
|
491
|
+
LLM05: {
|
|
492
|
+
name: "Data and Model Poisoning",
|
|
493
|
+
summary: "Training, fine-tuning or embedding data is manipulated to change behaviour."
|
|
494
|
+
},
|
|
495
|
+
LLM06: {
|
|
496
|
+
name: "Unbounded Consumption",
|
|
497
|
+
summary: "The application permits resource use without a limit, exhausting budget or service."
|
|
498
|
+
},
|
|
499
|
+
LLM07: {
|
|
500
|
+
name: "Misinformation",
|
|
501
|
+
summary: "The model produces false or misleading content that is acted on as if true."
|
|
502
|
+
},
|
|
503
|
+
LLM08: {
|
|
504
|
+
name: "Hidden Context Exposure",
|
|
505
|
+
summary: "System prompts, hidden instructions or internal context are revealed."
|
|
506
|
+
},
|
|
507
|
+
LLM09: {
|
|
508
|
+
name: "Vector and Embedding Weaknesses",
|
|
509
|
+
summary: "Retrieval stores are manipulated or leak across tenants and permissions."
|
|
510
|
+
},
|
|
511
|
+
LLM10: {
|
|
512
|
+
name: "Improper Output Handling",
|
|
513
|
+
summary: "Model output is passed to downstream systems without validation."
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
} };
|
|
517
|
+
|
|
469
518
|
//#endregion
|
|
470
519
|
//#region ../kernel/src/pack.ts
|
|
471
520
|
function isObject(value) {
|
|
@@ -761,7 +810,8 @@
|
|
|
761
810
|
"id",
|
|
762
811
|
"title",
|
|
763
812
|
"when",
|
|
764
|
-
"effect"
|
|
813
|
+
"effect",
|
|
814
|
+
"frameworks"
|
|
765
815
|
]);
|
|
766
816
|
const id = check.text(`${path}.id`, value.id);
|
|
767
817
|
const effect = readEffect(check, `${path}.effect`, value.effect);
|
|
@@ -777,8 +827,40 @@
|
|
|
777
827
|
const title = check.text(`${path}.title`, value.title);
|
|
778
828
|
if (title !== void 0) rule.title = title;
|
|
779
829
|
}
|
|
830
|
+
if (value.frameworks !== void 0) {
|
|
831
|
+
const frameworks = readFrameworks(check, `${path}.frameworks`, value.frameworks);
|
|
832
|
+
if (frameworks !== void 0) rule.frameworks = frameworks;
|
|
833
|
+
}
|
|
780
834
|
return rule;
|
|
781
835
|
}
|
|
836
|
+
/**
|
|
837
|
+
* A citation names a framework edition and controls the registry knows. A
|
|
838
|
+
* typo here would be a rule that claims a mapping no view can render, so it
|
|
839
|
+
* is a pack error rather than an ignored key — the same line the rest of the
|
|
840
|
+
* parser takes.
|
|
841
|
+
*/
|
|
842
|
+
function readFrameworks(check, path, value) {
|
|
843
|
+
if (!isObject(value)) {
|
|
844
|
+
check.fail(path, "expected an object keyed by framework id");
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
const out = {};
|
|
848
|
+
for (const [framework, controls] of Object.entries(value)) {
|
|
849
|
+
const known = FRAMEWORKS[framework];
|
|
850
|
+
if (known === void 0) {
|
|
851
|
+
check.fail(`${path}.${framework}`, `unknown framework; expected one of ${Object.keys(FRAMEWORKS).map((k) => `\`${k}\``).join(", ")}`);
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
if (!Array.isArray(controls) || !controls.every((c) => typeof c === "string")) {
|
|
855
|
+
check.fail(`${path}.${framework}`, "expected an array of control ids");
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
const ids = controls;
|
|
859
|
+
for (const control of ids) if (known.controls[control] === void 0) check.fail(`${path}.${framework}`, `unknown control \`${control}\` in ${known.name}`);
|
|
860
|
+
if (ids.length > 0) out[framework] = [...new Set(ids)];
|
|
861
|
+
}
|
|
862
|
+
return out;
|
|
863
|
+
}
|
|
782
864
|
/** Parse and validate a pack. Never throws; every problem lands in `errors`. */
|
|
783
865
|
function parsePack(source) {
|
|
784
866
|
let raw;
|
|
@@ -17,11 +17,16 @@
|
|
|
17
17
|
"./**/*.sample"
|
|
18
18
|
]
|
|
19
19
|
},
|
|
20
|
-
"secrets": {
|
|
20
|
+
"secrets": {
|
|
21
|
+
"minConfidence": 0.8
|
|
22
|
+
}
|
|
21
23
|
},
|
|
22
24
|
"effect": {
|
|
23
25
|
"kind": "deny",
|
|
24
26
|
"because": "the requested file contains a protected credential; the contents were not returned"
|
|
27
|
+
},
|
|
28
|
+
"frameworks": {
|
|
29
|
+
"owasp-llm-top-10-2026": ["LLM02"]
|
|
25
30
|
}
|
|
26
31
|
},
|
|
27
32
|
{
|
|
@@ -29,17 +34,27 @@
|
|
|
29
34
|
"title": "Private keys are refused everywhere",
|
|
30
35
|
"when": {
|
|
31
36
|
"capability": "fs:read",
|
|
32
|
-
"secrets": {
|
|
37
|
+
"secrets": {
|
|
38
|
+
"types": ["private-key"]
|
|
39
|
+
}
|
|
33
40
|
},
|
|
34
41
|
"effect": {
|
|
35
42
|
"kind": "deny",
|
|
36
43
|
"because": "the requested file contains a private key; the contents were not returned"
|
|
44
|
+
},
|
|
45
|
+
"frameworks": {
|
|
46
|
+
"owasp-llm-top-10-2026": ["LLM02"]
|
|
37
47
|
}
|
|
38
48
|
},
|
|
39
49
|
{
|
|
40
50
|
"id": "SEC-003",
|
|
41
51
|
"title": "Anything credential-shaped is at least recorded",
|
|
42
|
-
"when": {
|
|
52
|
+
"when": {
|
|
53
|
+
"capability": "fs:read",
|
|
54
|
+
"secrets": {
|
|
55
|
+
"minConfidence": 0.5
|
|
56
|
+
}
|
|
57
|
+
},
|
|
43
58
|
"effect": {
|
|
44
59
|
"kind": "warn",
|
|
45
60
|
"because": "a possible credential was noticed in this file; where no stronger rule fired, the read proceeded and this row is the record"
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as generateKeypair, c as verifyReceipt, d as canonical, f as chain, i as fingerprint, l as verifySignature, m as seal, n as readEntries, o as makeReceipt, p as hashEntry, r as verifyEntries, s as signEntry, t as Ledger, u as GENESIS } from "./src-
|
|
1
|
+
import { a as generateKeypair, c as verifyReceipt, d as canonical, f as chain, i as fingerprint, l as verifySignature, m as seal, n as readEntries, o as makeReceipt, p as hashEntry, r as verifyEntries, s as signEntry, t as Ledger, u as GENESIS } from "./src-Ciggx9ja.js";
|
|
2
2
|
|
|
3
3
|
export { GENESIS, Ledger, canonical, chain, fingerprint, generateKeypair, hashEntry, makeReceipt, readEntries, seal, signEntry, verifyEntries, verifyReceipt, verifySignature };
|
|
@@ -1,3 +1,72 @@
|
|
|
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."
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} };
|
|
48
|
+
/**
|
|
49
|
+
* Every framework control a rule cites, resolved to names.
|
|
50
|
+
*
|
|
51
|
+
* A pack that parsed cites only ids the registry knows, so the lookups here
|
|
52
|
+
* cannot miss; the fallbacks exist for a `Rule` built by hand in code.
|
|
53
|
+
*/
|
|
54
|
+
function cite(rule) {
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const [framework, controls] of Object.entries(rule.frameworks ?? {})) {
|
|
57
|
+
const known = FRAMEWORKS[framework];
|
|
58
|
+
for (const control of controls) out.push({
|
|
59
|
+
framework,
|
|
60
|
+
frameworkName: known?.name ?? framework,
|
|
61
|
+
frameworkShort: known?.short ?? framework,
|
|
62
|
+
control,
|
|
63
|
+
controlName: known?.controls[control]?.name ?? control
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
//#endregion
|
|
1
70
|
//#region src/console/logo.ts
|
|
2
71
|
/**
|
|
3
72
|
* The mark, inlined.
|
|
@@ -629,7 +698,7 @@ function renderNav(items, active) {
|
|
|
629
698
|
* the obligation, and a tool that told them the answer would be selling the
|
|
630
699
|
* one thing it cannot supply.
|
|
631
700
|
*/
|
|
632
|
-
const
|
|
701
|
+
const OBLIGATIONS = [
|
|
633
702
|
{
|
|
634
703
|
id: "EU AI Act art. 12",
|
|
635
704
|
asks: "Automatic recording of events over the lifetime of the system, to a degree that enables tracing.",
|
|
@@ -671,6 +740,71 @@ const CONTROLS = [
|
|
|
671
740
|
})
|
|
672
741
|
}
|
|
673
742
|
];
|
|
743
|
+
const OWASP = "owasp-llm-top-10-2026";
|
|
744
|
+
/** Rules in the pack that cite one control of one framework. */
|
|
745
|
+
function citing(pack, framework, control) {
|
|
746
|
+
return (pack?.rules ?? []).filter((rule) => (rule.frameworks?.[framework] ?? []).includes(control));
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Why a runtime execution gate is not the control for a risk no rule cites.
|
|
750
|
+
*
|
|
751
|
+
* Said per risk rather than as one generic line, because "out of scope" with
|
|
752
|
+
* no reason reads as an excuse. Each of these is a boundary a reader can
|
|
753
|
+
* check: the risk lives somewhere a tool-call gate does not stand.
|
|
754
|
+
*/
|
|
755
|
+
const NOT_THIS_BOUNDARY = {
|
|
756
|
+
LLM04: "Where a model or dependency came from is settled before an agent runs; the gate sees the call, not the provenance.",
|
|
757
|
+
LLM05: "Training and fine-tuning data sit outside the execution boundary this gate stands on.",
|
|
758
|
+
LLM07: "Whether an answer is true is not a property of a tool call.",
|
|
759
|
+
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.",
|
|
760
|
+
LLM09: "Retrieval stores are upstream of the call the gate judges.",
|
|
761
|
+
LLM10: "What downstream code does with model output is that code's boundary, not this one's."
|
|
762
|
+
};
|
|
763
|
+
function owaspRow(control) {
|
|
764
|
+
const framework = FRAMEWORKS[OWASP];
|
|
765
|
+
const risk = framework?.controls[control];
|
|
766
|
+
const name = risk?.name ?? control;
|
|
767
|
+
return {
|
|
768
|
+
id: `${framework?.short ?? OWASP} · ${control}`,
|
|
769
|
+
asks: `${name}. ${risk?.summary ?? ""}`.trim(),
|
|
770
|
+
evidence: (data) => {
|
|
771
|
+
if (data.pack === void 0) return {
|
|
772
|
+
state: "absent",
|
|
773
|
+
note: "No pack supplied to this view."
|
|
774
|
+
};
|
|
775
|
+
if (control === "LLM01") {
|
|
776
|
+
const bounding = citing(data.pack, OWASP, "LLM03");
|
|
777
|
+
return {
|
|
778
|
+
state: "partial",
|
|
779
|
+
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.`
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
const rules = citing(data.pack, OWASP, control);
|
|
783
|
+
if (rules.length === 0) return {
|
|
784
|
+
state: "absent",
|
|
785
|
+
note: NOT_THIS_BOUNDARY[control] ?? "No rule in this pack cites this risk."
|
|
786
|
+
};
|
|
787
|
+
const ids = new Set(rules.map((rule) => rule.id));
|
|
788
|
+
const decisions = data.entries.filter((entry) => entry.rule !== void 0 && ids.has(entry.rule));
|
|
789
|
+
const listed = rules.map((rule) => rule.id).join(", ");
|
|
790
|
+
return decisions.length > 0 ? {
|
|
791
|
+
state: "covered",
|
|
792
|
+
note: `${rules.length} control${rules.length === 1 ? "" : "s"} mapped (${listed}) · ${decisions.length} sealed decision${decisions.length === 1 ? "" : "s"} under them in this record.`
|
|
793
|
+
} : {
|
|
794
|
+
state: "partial",
|
|
795
|
+
note: `${rules.length} control${rules.length === 1 ? "" : "s"} mapped (${listed}); none has decided a call in this record yet, so the mapping is declared, not exercised.`
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
const OWASP_ROWS = Object.keys(FRAMEWORKS[OWASP]?.controls ?? {}).map(owaspRow);
|
|
801
|
+
/**
|
|
802
|
+
* The obligations first, then the OWASP risks. Two kinds of row in one table
|
|
803
|
+
* on purpose: an auditor reads down one list and finds the audit obligations
|
|
804
|
+
* the ledger evidences and the application risks the pack's rules address,
|
|
805
|
+
* each stated in its framework's own words.
|
|
806
|
+
*/
|
|
807
|
+
const CONTROLS = [...OBLIGATIONS, ...OWASP_ROWS];
|
|
674
808
|
|
|
675
809
|
//#endregion
|
|
676
810
|
//#region src/console/views.ts
|
|
@@ -789,11 +923,17 @@ function renderPacks(data) {
|
|
|
789
923
|
});
|
|
790
924
|
const baseline = pack.baseline?.capabilities;
|
|
791
925
|
const caps = baseline === void 0 ? `<p style="padding:0 30px 22px;font-size:14px;color:var(--secondary);max-width:76ch">This pack sets no capability ceiling. It governs through its rules alone, and withholds nothing an author declares.</p>` : `<div class="caps">${baseline.map((c) => `<span class="cap">${esc(c)}</span>`).join("")}</div>`;
|
|
792
|
-
const
|
|
926
|
+
const cited = /* @__PURE__ */ new Map();
|
|
927
|
+
for (const rule of pack.rules) for (const c of cite(rule)) cited.set(c.framework, c.frameworkName);
|
|
928
|
+
const frameworks = cited.size === 0 ? "" : `<div class="caps">${[...cited.values()].map((name) => `<span class="cap">${esc(name)}</span>`).join("")}</div>`;
|
|
929
|
+
const rows = pack.rules.map((rule) => {
|
|
930
|
+
const tags = cite(rule).map((c) => `<br><span style="color:var(--secondary)">${esc(c.frameworkShort)} · ${esc(c.control)} ${esc(c.controlName)}</span>`).join("");
|
|
931
|
+
return ` <div class="grid rules row">
|
|
793
932
|
<div class="seq">${esc(rule.id)}</div>
|
|
794
|
-
<div class="call">${esc(rule.title ?? rule.effect.because)}<br><span style="color:var(--muted)">${esc(matcherOf(rule))}</span
|
|
933
|
+
<div class="call">${esc(rule.title ?? rule.effect.because)}<br><span style="color:var(--muted)">${esc(matcherOf(rule))}</span>${tags}</div>
|
|
795
934
|
<div class="effect e-${esc(rule.effect.kind)}">${esc(rule.effect.kind.toUpperCase())}</div>
|
|
796
|
-
</div
|
|
935
|
+
</div>`;
|
|
936
|
+
}).join("\n");
|
|
797
937
|
const body = ` <section class="sect">
|
|
798
938
|
<h2>${esc(pack.pack)} <span style="font-family:var(--proof);font-size:14px;color:var(--muted)">${esc(pack.version)}</span></h2>
|
|
799
939
|
<p>The operator baseline is intersected with whatever a plugin's own
|
|
@@ -801,6 +941,7 @@ function renderPacks(data) {
|
|
|
801
941
|
grant a capability an author never asked for.</p>
|
|
802
942
|
</section>
|
|
803
943
|
${caps}
|
|
944
|
+
${frameworks}
|
|
804
945
|
<div class="grid rules thead">
|
|
805
946
|
<div>CLAUSE</div><div>RULE</div><div>EFFECT</div>
|
|
806
947
|
</div>
|
|
@@ -840,11 +981,17 @@ ${CONTROLS.map((control) => {
|
|
|
840
981
|
</div>`;
|
|
841
982
|
}).join("\n")}
|
|
842
983
|
<section class="sect" style="padding-bottom:26px">
|
|
843
|
-
<h2>
|
|
844
|
-
<p>
|
|
845
|
-
harness records what was decided and never who decided
|
|
846
|
-
approval needs an identity anchor outside this machine,
|
|
847
|
-
control-plane concern this build does not claim.</p>
|
|
984
|
+
<h2>Reading the states</h2>
|
|
985
|
+
<p>For the audit obligations, several rows stop at <em>partial</em> for
|
|
986
|
+
one reason: the harness records what was decided and never who decided
|
|
987
|
+
it. Attributable approval needs an identity anchor outside this machine,
|
|
988
|
+
which is a control-plane concern this build does not claim.</p>
|
|
989
|
+
<p>For the OWASP risks, <em>evidence</em> means rules in this pack cite
|
|
990
|
+
the risk and have decided calls in this record; <em>partial</em> means
|
|
991
|
+
they cite it and have not yet, or — for prompt injection — that the gate
|
|
992
|
+
bounds the risk without claiming to detect it; <em>none</em> means no
|
|
993
|
+
rule cites it, with the reason a tool-call gate is not where that risk
|
|
994
|
+
is controlled.</p>
|
|
848
995
|
</section>`;
|
|
849
996
|
return renderShell({
|
|
850
997
|
title: "Compliance map",
|
|
@@ -926,4 +1073,4 @@ ${history}`;
|
|
|
926
1073
|
}
|
|
927
1074
|
|
|
928
1075
|
//#endregion
|
|
929
|
-
export { renderPacks as a, renderShell as c, renderOverview as i, VERDICT_LABEL as l, renderApprovals as n, esc as o, renderCompliance as r, renderNav as s, navFor as t };
|
|
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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prae-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.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",
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
"typescript": "^5.9.2",
|
|
42
42
|
"vitest": "^3.2.4",
|
|
43
43
|
"@prae/controls": "0.1.0",
|
|
44
|
+
"@prae/extract": "0.1.0",
|
|
44
45
|
"@prae/inspect": "0.1.0",
|
|
45
46
|
"@prae/instructions": "0.1.0",
|
|
46
|
-
"@prae/extract": "0.1.0",
|
|
47
47
|
"@prae/kernel": "0.1.0",
|
|
48
48
|
"@prae/ledger": "0.1.0",
|
|
49
49
|
"@prae/tokens": "0.1.0"
|
|
File without changes
|