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