prae-cli 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/bin.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "./evaluate-CkYySl7T.js";
|
|
3
|
-
import "./views-
|
|
4
|
-
import { t as main } from "./cli-
|
|
3
|
+
import "./views-UODslKwu.js";
|
|
4
|
+
import { t as main } from "./cli-chtNj_el.js";
|
|
5
5
|
import "./src-Ciggx9ja.js";
|
|
6
6
|
|
|
7
7
|
//#region src/bin.ts
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as evaluate } from "./evaluate-CkYySl7T.js";
|
|
2
|
-
import { a as renderPacks, c as
|
|
3
|
-
import { m as seal, t as Ledger } from "./src-Ciggx9ja.js";
|
|
4
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { a as renderPacks, c as renderNav, d as FRAMEWORKS, f as cite, i as renderOverview, l as renderShell, n as renderApprovals, o as CONTROLS, r as renderCompliance, s as esc, t as navFor, u as VERDICT_LABEL$1 } from "./views-UODslKwu.js";
|
|
3
|
+
import { f as chain, i as fingerprint, l as verifySignature, m as seal, n as readEntries, r as verifyEntries, t as Ledger, u as GENESIS } from "./src-Ciggx9ja.js";
|
|
4
|
+
import { existsSync, readFileSync, readdirSync, statSync, watch } from "node:fs";
|
|
5
5
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
6
|
import { dirname, extname, join, normalize, resolve, sep } from "node:path";
|
|
7
7
|
import { homedir } from "node:os";
|
|
@@ -752,7 +752,9 @@ function praeRow(options) {
|
|
|
752
752
|
];
|
|
753
753
|
if (options.ledger !== void 0) lines.push(` ledger: ${JSON.stringify(options.ledger)}`);
|
|
754
754
|
if (options.approver !== void 0) lines.push(` approver: ${JSON.stringify(options.approver)}`);
|
|
755
|
-
lines.push(
|
|
755
|
+
if (options.mode !== void 0) lines.push(` mode: ${options.mode}`);
|
|
756
|
+
if (options.packFile !== void 0) lines.push(` packFile: ${JSON.stringify(options.packFile)}`);
|
|
757
|
+
else lines.push(" pack: |", indent(options.pack ?? "", 10));
|
|
756
758
|
return lines.join("\n");
|
|
757
759
|
}
|
|
758
760
|
/** Compose the write and check it, without touching the file. */
|
|
@@ -792,6 +794,60 @@ async function applyWrite(profile, plan, expectedRevision) {
|
|
|
792
794
|
return { snapshot: manifest.id };
|
|
793
795
|
}
|
|
794
796
|
|
|
797
|
+
//#endregion
|
|
798
|
+
//#region src/record-files.ts
|
|
799
|
+
function jsonOrNull(path) {
|
|
800
|
+
if (!existsSync(path)) return null;
|
|
801
|
+
try {
|
|
802
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
803
|
+
} catch {
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
function readLedger(path) {
|
|
808
|
+
if (!existsSync(path)) return [];
|
|
809
|
+
const out = [];
|
|
810
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
811
|
+
if (line.trim() === "") continue;
|
|
812
|
+
try {
|
|
813
|
+
out.push(JSON.parse(line));
|
|
814
|
+
} catch {}
|
|
815
|
+
}
|
|
816
|
+
return out;
|
|
817
|
+
}
|
|
818
|
+
/** The defaults the demo writes; a caller with other names overrides them. */
|
|
819
|
+
const DEFAULT_FILES = {
|
|
820
|
+
ledger: "papaya.jsonl",
|
|
821
|
+
discovery: "discovery.json",
|
|
822
|
+
report: "security-report.json",
|
|
823
|
+
guardians: "guardians.json",
|
|
824
|
+
pack: "papaya.prae.json",
|
|
825
|
+
publicKey: "prae-signing.pub.pem",
|
|
826
|
+
redteam: "../redteam/results"
|
|
827
|
+
};
|
|
828
|
+
function readRecord(dir, files) {
|
|
829
|
+
return {
|
|
830
|
+
ledger: readLedger(resolve(dir, files.ledger)),
|
|
831
|
+
discovery: jsonOrNull(resolve(dir, files.discovery)),
|
|
832
|
+
report: jsonOrNull(resolve(dir, files.report)),
|
|
833
|
+
guardians: jsonOrNull(resolve(dir, files.guardians)),
|
|
834
|
+
pack: jsonOrNull(resolve(dir, files.pack))
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
/** The red-team campaign and its runs, as the runner wrote them; empty when none. */
|
|
838
|
+
function readRedteam(dir, files) {
|
|
839
|
+
const root = resolve(dir, files.redteam);
|
|
840
|
+
if (!existsSync(root)) return {
|
|
841
|
+
campaign: null,
|
|
842
|
+
runs: []
|
|
843
|
+
};
|
|
844
|
+
const runs = readdirSync(root).filter((n) => n.endsWith(".json")).sort().filter((n) => n !== "campaign.json").map((n) => jsonOrNull(resolve(root, n))).filter((r) => r !== null);
|
|
845
|
+
return {
|
|
846
|
+
campaign: jsonOrNull(resolve(root, "campaign.json")),
|
|
847
|
+
runs
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
795
851
|
//#endregion
|
|
796
852
|
//#region src/serve.ts
|
|
797
853
|
const TYPES = {
|
|
@@ -823,8 +879,233 @@ function safePath(root, urlPath) {
|
|
|
823
879
|
if (target !== base && !target.startsWith(base + sep)) return void 0;
|
|
824
880
|
return target;
|
|
825
881
|
}
|
|
826
|
-
/**
|
|
827
|
-
|
|
882
|
+
/**
|
|
883
|
+
* Verify the ledger the way `prae verify` does — chain, then signatures
|
|
884
|
+
* against the public key beside it. With `tamper`, one row's refusal is
|
|
885
|
+
* changed to an allow in memory first and the altered text is what gets
|
|
886
|
+
* verified: the forged-copy demonstration, without writing a file. With
|
|
887
|
+
* `repair` as well, every hash is recomputed over the altered rows — what
|
|
888
|
+
* an attacker would do to mend the chain — and the signatures, made over the
|
|
889
|
+
* original hashes by a key they do not hold, are what fails. Nothing on disk
|
|
890
|
+
* changes.
|
|
891
|
+
*/
|
|
892
|
+
function verifyRecord(record, tamper, repair = false) {
|
|
893
|
+
const { dir, files } = record;
|
|
894
|
+
const path = resolve(dir, files.ledger);
|
|
895
|
+
if (!existsSync(path)) return {
|
|
896
|
+
ok: false,
|
|
897
|
+
entries: 0,
|
|
898
|
+
problems: [{
|
|
899
|
+
line: 0,
|
|
900
|
+
message: "no ledger"
|
|
901
|
+
}],
|
|
902
|
+
tampered: null,
|
|
903
|
+
repaired: false,
|
|
904
|
+
signatures: null
|
|
905
|
+
};
|
|
906
|
+
let text = readFileSync(path, "utf8");
|
|
907
|
+
let tampered = null;
|
|
908
|
+
if (tamper !== void 0) {
|
|
909
|
+
const lines = text.trimEnd().split("\n");
|
|
910
|
+
const idx = lines.findIndex((line) => {
|
|
911
|
+
try {
|
|
912
|
+
return JSON.parse(line).seq === tamper;
|
|
913
|
+
} catch {
|
|
914
|
+
return false;
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
if (idx >= 0) {
|
|
918
|
+
const row$1 = JSON.parse(lines[idx]);
|
|
919
|
+
row$1.verdict = "allow";
|
|
920
|
+
row$1.reason = "looks fine to me";
|
|
921
|
+
lines[idx] = JSON.stringify(row$1);
|
|
922
|
+
if (repair) {
|
|
923
|
+
let prev = GENESIS;
|
|
924
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
925
|
+
const { seq, prev: _prev, hash: _hash, sig,...rest } = JSON.parse(lines[i]);
|
|
926
|
+
const entry = chain(rest, seq, prev);
|
|
927
|
+
lines[i] = JSON.stringify(sig === void 0 ? entry : {
|
|
928
|
+
...entry,
|
|
929
|
+
sig
|
|
930
|
+
});
|
|
931
|
+
prev = entry.hash;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
text = `${lines.join("\n")}\n`;
|
|
935
|
+
tampered = tamper;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
const verified = verifyEntries(text);
|
|
939
|
+
const keyPath = resolve(dir, files.publicKey);
|
|
940
|
+
let signatures = null;
|
|
941
|
+
if (existsSync(keyPath)) {
|
|
942
|
+
const publicKey = readFileSync(keyPath, "utf8");
|
|
943
|
+
let entries = [];
|
|
944
|
+
try {
|
|
945
|
+
entries = readEntries(text);
|
|
946
|
+
} catch {
|
|
947
|
+
entries = [];
|
|
948
|
+
}
|
|
949
|
+
const signed = entries.filter((e) => e.sig !== void 0);
|
|
950
|
+
signatures = {
|
|
951
|
+
checked: signed.length,
|
|
952
|
+
failed: signed.filter((e) => !verifySignature(e, e.sig, publicKey)).map((e) => e.seq),
|
|
953
|
+
fingerprint: fingerprint(publicKey)
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
return {
|
|
957
|
+
ok: verified.ok,
|
|
958
|
+
entries: verified.entries,
|
|
959
|
+
problems: [...verified.problems],
|
|
960
|
+
tampered,
|
|
961
|
+
repaired: tampered !== null && repair,
|
|
962
|
+
signatures
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
const OWASP = "owasp-llm-top-10-2026";
|
|
966
|
+
/**
|
|
967
|
+
* Evidence toward each control, from the pack and the ledger as they are
|
|
968
|
+
* now — the rows the controls package defines, so the console, the exporter and
|
|
969
|
+
* the old pages cannot disagree about what a profile shows. Framed as
|
|
970
|
+
* evidence, never as compliance: an auditor decides.
|
|
971
|
+
*/
|
|
972
|
+
function complianceOf(record) {
|
|
973
|
+
const { dir, files } = record;
|
|
974
|
+
const ledgerPath = resolve(dir, files.ledger);
|
|
975
|
+
const text = existsSync(ledgerPath) ? readFileSync(ledgerPath, "utf8") : "";
|
|
976
|
+
let entries = [];
|
|
977
|
+
try {
|
|
978
|
+
entries = readEntries(text);
|
|
979
|
+
} catch {
|
|
980
|
+
entries = [];
|
|
981
|
+
}
|
|
982
|
+
const packPath = resolve(dir, files.pack);
|
|
983
|
+
let pack;
|
|
984
|
+
if (existsSync(packPath)) {
|
|
985
|
+
const parsed = parsePack(readFileSync(packPath, "utf8"));
|
|
986
|
+
if (parsed.ok) pack = parsed.pack;
|
|
987
|
+
}
|
|
988
|
+
const data = {
|
|
989
|
+
...pack === void 0 ? {} : { pack },
|
|
990
|
+
entries,
|
|
991
|
+
verification: verifyEntries(text),
|
|
992
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
993
|
+
};
|
|
994
|
+
const owasp = FRAMEWORKS[OWASP]?.controls ?? {};
|
|
995
|
+
return { controls: CONTROLS.map((control) => {
|
|
996
|
+
const code = /LLM\d\d/.exec(control.id)?.[0];
|
|
997
|
+
const named = code === void 0 ? void 0 : owasp[code];
|
|
998
|
+
return {
|
|
999
|
+
id: code ?? control.id,
|
|
1000
|
+
...named === void 0 ? {} : { name: named.name },
|
|
1001
|
+
asks: control.asks,
|
|
1002
|
+
framework: named === void 0 ? "obligation" : OWASP,
|
|
1003
|
+
...control.evidence(data)
|
|
1004
|
+
};
|
|
1005
|
+
}) };
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* The record over HTTP, for the Command Center (apps/console): the same
|
|
1009
|
+
* files `prae console` renders, as JSON, plus one event stream that says
|
|
1010
|
+
* when the ledger moved. Read-only, from files on disk, nothing computed
|
|
1011
|
+
* here — the console derives everything it shows from these bodies.
|
|
1012
|
+
*
|
|
1013
|
+
* Returns false when the URL is not an API route, so a caller can fall
|
|
1014
|
+
* through to static files. Shared with the console's Vite dev server so the
|
|
1015
|
+
* two answer identically.
|
|
1016
|
+
*/
|
|
1017
|
+
function handleApi(req, res, record) {
|
|
1018
|
+
const route = (req.url ?? "").split("?")[0] ?? "";
|
|
1019
|
+
if (!route.startsWith("/api/")) return false;
|
|
1020
|
+
const json = (body, status = 200) => {
|
|
1021
|
+
res.statusCode = status;
|
|
1022
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
1023
|
+
res.setHeader("cache-control", "no-store");
|
|
1024
|
+
res.end(JSON.stringify(body));
|
|
1025
|
+
};
|
|
1026
|
+
const { dir, files } = record;
|
|
1027
|
+
if (route === "/api/events") {
|
|
1028
|
+
res.writeHead(200, {
|
|
1029
|
+
"content-type": "text/event-stream",
|
|
1030
|
+
"cache-control": "no-store",
|
|
1031
|
+
connection: "keep-alive"
|
|
1032
|
+
});
|
|
1033
|
+
res.write(": connected\n\n");
|
|
1034
|
+
let sent = readRecord(dir, files).ledger.length;
|
|
1035
|
+
const push = () => {
|
|
1036
|
+
const rows = readRecord(dir, files).ledger;
|
|
1037
|
+
if (rows.length < sent) sent = 0;
|
|
1038
|
+
const fresh = rows.slice(sent);
|
|
1039
|
+
sent = rows.length;
|
|
1040
|
+
if (fresh.length > 0) res.write(`data: ${JSON.stringify(fresh)}\n\n`);
|
|
1041
|
+
else res.write(": keepalive\n\n");
|
|
1042
|
+
};
|
|
1043
|
+
const watcher = existsSync(dir) ? watch(dir, { persistent: false }, () => push()) : void 0;
|
|
1044
|
+
const keep = setInterval(push, 2e3);
|
|
1045
|
+
res.on("close", () => {
|
|
1046
|
+
clearInterval(keep);
|
|
1047
|
+
watcher?.close();
|
|
1048
|
+
});
|
|
1049
|
+
return true;
|
|
1050
|
+
}
|
|
1051
|
+
if (route === "/api/verify") {
|
|
1052
|
+
const raw = new URL(req.url ?? "/", "http://localhost").searchParams.get("tamper");
|
|
1053
|
+
const tamper = raw === null ? void 0 : Number(raw);
|
|
1054
|
+
const repair = new URL(req.url ?? "/", "http://localhost").searchParams.get("repair") === "1";
|
|
1055
|
+
json(verifyRecord(record, Number.isInteger(tamper) ? tamper : void 0, repair));
|
|
1056
|
+
return true;
|
|
1057
|
+
}
|
|
1058
|
+
if (route === "/api/compliance") {
|
|
1059
|
+
json(complianceOf(record));
|
|
1060
|
+
return true;
|
|
1061
|
+
}
|
|
1062
|
+
if (route === "/api/redteam") {
|
|
1063
|
+
json(readRedteam(dir, files));
|
|
1064
|
+
return true;
|
|
1065
|
+
}
|
|
1066
|
+
const snapshot = readRecord(dir, files);
|
|
1067
|
+
switch (route) {
|
|
1068
|
+
case "/api/record":
|
|
1069
|
+
json(snapshot);
|
|
1070
|
+
break;
|
|
1071
|
+
case "/api/ledger":
|
|
1072
|
+
json(snapshot.ledger);
|
|
1073
|
+
break;
|
|
1074
|
+
case "/api/discovery":
|
|
1075
|
+
json(snapshot.discovery);
|
|
1076
|
+
break;
|
|
1077
|
+
case "/api/report":
|
|
1078
|
+
json(snapshot.report);
|
|
1079
|
+
break;
|
|
1080
|
+
case "/api/guardians":
|
|
1081
|
+
json(snapshot.guardians);
|
|
1082
|
+
break;
|
|
1083
|
+
case "/api/pack":
|
|
1084
|
+
json(snapshot.pack);
|
|
1085
|
+
break;
|
|
1086
|
+
case "/api/status":
|
|
1087
|
+
json({
|
|
1088
|
+
dir,
|
|
1089
|
+
files: Object.fromEntries(Object.entries(files).map(([name, file]) => {
|
|
1090
|
+
const path = resolve(dir, file);
|
|
1091
|
+
if (!existsSync(path)) return [name, null];
|
|
1092
|
+
const stat$1 = statSync(path);
|
|
1093
|
+
return [name, {
|
|
1094
|
+
size: stat$1.size,
|
|
1095
|
+
mtime: stat$1.mtimeMs
|
|
1096
|
+
}];
|
|
1097
|
+
}))
|
|
1098
|
+
});
|
|
1099
|
+
break;
|
|
1100
|
+
default: json({ error: "not found" }, 404);
|
|
1101
|
+
}
|
|
1102
|
+
return true;
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Start the reader. Resolves once it is listening. With a record source,
|
|
1106
|
+
* `/api/*` answers from it; without one, only the rendered pages are served.
|
|
1107
|
+
*/
|
|
1108
|
+
function serveConsole(root, port, record) {
|
|
828
1109
|
const server = createServer((req, res) => {
|
|
829
1110
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
830
1111
|
res.statusCode = 405;
|
|
@@ -832,6 +1113,7 @@ function serveConsole(root, port) {
|
|
|
832
1113
|
res.end();
|
|
833
1114
|
return;
|
|
834
1115
|
}
|
|
1116
|
+
if (record !== void 0 && handleApi(req, res, record)) return;
|
|
835
1117
|
const target = safePath(root, req.url ?? "/");
|
|
836
1118
|
if (target === void 0) {
|
|
837
1119
|
res.statusCode = 403;
|
|
@@ -852,9 +1134,10 @@ function serveConsole(root, port) {
|
|
|
852
1134
|
return new Promise((ok, fail$1) => {
|
|
853
1135
|
server.once("error", fail$1);
|
|
854
1136
|
server.listen(port, "127.0.0.1", () => {
|
|
1137
|
+
const address = server.address();
|
|
855
1138
|
ok({
|
|
856
1139
|
server,
|
|
857
|
-
url: `http://127.0.0.1:${port}/`
|
|
1140
|
+
url: `http://127.0.0.1:${typeof address === "object" && address !== null ? address.port : port}/`
|
|
858
1141
|
});
|
|
859
1142
|
});
|
|
860
1143
|
});
|
|
@@ -869,7 +1152,7 @@ const VERDICT_LABEL = {
|
|
|
869
1152
|
deny: "DENIED",
|
|
870
1153
|
correct: "CORRECTED"
|
|
871
1154
|
};
|
|
872
|
-
const USAGE =
|
|
1155
|
+
const USAGE = `⌜▪⌟ prae — operator policy for agent harnesses
|
|
873
1156
|
|
|
874
1157
|
prae check <pack.json>
|
|
875
1158
|
Validate a policy pack and summarise what it enforces.
|
|
@@ -931,9 +1214,11 @@ const USAGE = `prae — operator policy for agent harnesses
|
|
|
931
1214
|
|
|
932
1215
|
prae init [--pack <file>] [--profile <dir>] [--ledger <file>]
|
|
933
1216
|
(no --pack mounts the built-in baseline of 8 high-risk rules)
|
|
934
|
-
[--approver <name>] [--apply]
|
|
1217
|
+
[--approver <name>] [--link] [--shadow] [--apply]
|
|
935
1218
|
Compose the overlay row that mounts PRAE with a pack. Prints the
|
|
936
|
-
diff; writes only with --apply.
|
|
1219
|
+
diff; writes only with --apply. --link points the profile at the
|
|
1220
|
+
pack file instead of copying it in, so edits hot-reload; --shadow
|
|
1221
|
+
mounts in shadow mode: every verdict sealed, every call let through.
|
|
937
1222
|
`;
|
|
938
1223
|
function fail(message) {
|
|
939
1224
|
console.error(`prae: ${message}`);
|
|
@@ -1073,7 +1358,7 @@ async function explain(path, tool, pairs) {
|
|
|
1073
1358
|
* a key that arrived beside it is checked against nothing.
|
|
1074
1359
|
*/
|
|
1075
1360
|
async function keygen(dir) {
|
|
1076
|
-
const { generateKeypair, fingerprint } = await import("./src-C9L6NBvu.js");
|
|
1361
|
+
const { generateKeypair, fingerprint: fingerprint$1 } = await import("./src-C9L6NBvu.js");
|
|
1077
1362
|
const { writeFileSync: writeFileSync$1 } = await import("node:fs");
|
|
1078
1363
|
const keys = generateKeypair();
|
|
1079
1364
|
const priv = join(dir, "prae-signing.pem");
|
|
@@ -1082,14 +1367,14 @@ async function keygen(dir) {
|
|
|
1082
1367
|
writeFileSync$1(pub, keys.publicKey);
|
|
1083
1368
|
console.log(`private ${priv} (0600 — this signs; guard it)`);
|
|
1084
1369
|
console.log(`public ${pub}`);
|
|
1085
|
-
console.log(`fingerprint ${fingerprint(keys.publicKey)}`);
|
|
1370
|
+
console.log(`fingerprint ${fingerprint$1(keys.publicKey)}`);
|
|
1086
1371
|
console.log("");
|
|
1087
1372
|
console.log("Give the PUBLIC key and fingerprint to whoever will verify");
|
|
1088
1373
|
console.log("receipts, through a channel they trust. Point the plugin at the");
|
|
1089
1374
|
console.log("private key with `signingKey` to sign every row.");
|
|
1090
1375
|
}
|
|
1091
1376
|
async function receipt(path, seq, keyPath, out) {
|
|
1092
|
-
const { Ledger: Ledger$1, makeReceipt, fingerprint } = await import("./src-C9L6NBvu.js");
|
|
1377
|
+
const { Ledger: Ledger$1, makeReceipt, fingerprint: fingerprint$1 } = await import("./src-C9L6NBvu.js");
|
|
1093
1378
|
const { readFileSync: readFileSync$1 } = await import("node:fs");
|
|
1094
1379
|
const { createPublicKey } = await import("node:crypto");
|
|
1095
1380
|
const privateKey = readFileSync$1(keyPath, "utf8");
|
|
@@ -1106,7 +1391,7 @@ async function receipt(path, seq, keyPath, out) {
|
|
|
1106
1391
|
await writeFile(out, text);
|
|
1107
1392
|
console.log(`${out} · seq ${seq} · ${entry.verdict} ${entry.tool} ${entry.subject ?? ""}`);
|
|
1108
1393
|
}
|
|
1109
|
-
console.log(`fingerprint ${fingerprint(publicKey)}`);
|
|
1394
|
+
console.log(`fingerprint ${fingerprint$1(publicKey)}`);
|
|
1110
1395
|
console.log("The verifier compares that fingerprint against the one they");
|
|
1111
1396
|
console.log("pinned — a receipt is only as good as the key they trust.");
|
|
1112
1397
|
}
|
|
@@ -1190,7 +1475,7 @@ async function verify(path, keyPath) {
|
|
|
1190
1475
|
const last = entries.at(-1);
|
|
1191
1476
|
console.log(`${result.entries} entries · chain intact${last === void 0 ? "" : ` · seal ${seal(last)}`}`);
|
|
1192
1477
|
if (keyPath !== void 0) {
|
|
1193
|
-
const { verifySignature, fingerprint } = await import("./src-C9L6NBvu.js");
|
|
1478
|
+
const { verifySignature: verifySignature$1, fingerprint: fingerprint$1 } = await import("./src-C9L6NBvu.js");
|
|
1194
1479
|
const { readFileSync: readFileSync$1 } = await import("node:fs");
|
|
1195
1480
|
const publicKey = readFileSync$1(keyPath, "utf8");
|
|
1196
1481
|
let signed = 0;
|
|
@@ -1198,13 +1483,13 @@ async function verify(path, keyPath) {
|
|
|
1198
1483
|
for (const entry of entries) {
|
|
1199
1484
|
if (entry.sig === void 0) continue;
|
|
1200
1485
|
signed += 1;
|
|
1201
|
-
if (!verifySignature(entry, entry.sig, publicKey)) bad.push(entry.seq);
|
|
1486
|
+
if (!verifySignature$1(entry, entry.sig, publicKey)) bad.push(entry.seq);
|
|
1202
1487
|
}
|
|
1203
1488
|
if (bad.length > 0) {
|
|
1204
|
-
console.error(`signatures: ${bad.length} of ${signed} FAIL against ${fingerprint(publicKey)} — rows ${bad.join(", ")}`);
|
|
1489
|
+
console.error(`signatures: ${bad.length} of ${signed} FAIL against ${fingerprint$1(publicKey)} — rows ${bad.join(", ")}`);
|
|
1205
1490
|
process.exitCode = 1;
|
|
1206
1491
|
} else if (signed === 0) console.log("signatures: none present — this ledger is not signed");
|
|
1207
|
-
else console.log(`signatures: ${signed}/${entries.length} rows verify against ${fingerprint(publicKey)}`);
|
|
1492
|
+
else console.log(`signatures: ${signed}/${entries.length} rows verify against ${fingerprint$1(publicKey)}`);
|
|
1208
1493
|
}
|
|
1209
1494
|
console.log("tamper-evident: a removed, edited, or reordered row breaks this chain.");
|
|
1210
1495
|
console.log("It does not prove against a rewrite of the whole file by someone holding this code.");
|
|
@@ -1366,7 +1651,7 @@ async function consoleOut(path, argv) {
|
|
|
1366
1651
|
const [{ ingest }, { extract }, { renderInstructions }] = await Promise.all([
|
|
1367
1652
|
import("./src-CraUX64d.js"),
|
|
1368
1653
|
import("./src-CQ0UmFUK.js"),
|
|
1369
|
-
import("./instructions-
|
|
1654
|
+
import("./instructions-C104EUaE.js")
|
|
1370
1655
|
]);
|
|
1371
1656
|
const read = ingest(instructionsDir);
|
|
1372
1657
|
instructionsView = renderInstructions(data, read, extract(read.statements));
|
|
@@ -1395,8 +1680,16 @@ async function consoleOut(path, argv) {
|
|
|
1395
1680
|
const portFlag = flag(argv, "serve");
|
|
1396
1681
|
const port = portFlag === void 0 ? 4180 : Number(portFlag);
|
|
1397
1682
|
if (!Number.isInteger(port) || port < 1 || port > 65535) fail(`--serve takes a port, got \`${portFlag}\``);
|
|
1398
|
-
const { url } = await serveConsole(out, port
|
|
1683
|
+
const { url } = await serveConsole(out, port, {
|
|
1684
|
+
dir: dirname(resolve(path)),
|
|
1685
|
+
files: {
|
|
1686
|
+
...DEFAULT_FILES,
|
|
1687
|
+
ledger: resolve(path),
|
|
1688
|
+
...packPath === void 0 ? {} : { pack: resolve(packPath) }
|
|
1689
|
+
}
|
|
1690
|
+
});
|
|
1399
1691
|
console.log(`\nserving ${out} at ${url}`);
|
|
1692
|
+
console.log(`record at ${url}api/record · events at ${url}api/events`);
|
|
1400
1693
|
console.log("loopback only, read-only. ctrl-c to stop.");
|
|
1401
1694
|
}
|
|
1402
1695
|
async function init(argv) {
|
|
@@ -1413,8 +1706,11 @@ async function init(argv) {
|
|
|
1413
1706
|
const profile = profileDir === void 0 ? defaultProfile() : profileAt(profileDir);
|
|
1414
1707
|
const ledger = flag(argv, "ledger");
|
|
1415
1708
|
const approver = flag(argv, "approver");
|
|
1709
|
+
const link = argv.includes("--link");
|
|
1710
|
+
const shadow = argv.includes("--shadow");
|
|
1416
1711
|
const row$1 = praeRow({
|
|
1417
|
-
pack: source,
|
|
1712
|
+
...link ? { packFile: resolve(packPath) } : { pack: source },
|
|
1713
|
+
...shadow ? { mode: "shadow" } : {},
|
|
1418
1714
|
...ledger === void 0 ? {} : { ledger },
|
|
1419
1715
|
...approver === void 0 ? {} : { approver }
|
|
1420
1716
|
});
|
|
@@ -1426,7 +1722,8 @@ async function init(argv) {
|
|
|
1426
1722
|
}
|
|
1427
1723
|
console.log(`profile ${profile.dir}`);
|
|
1428
1724
|
console.log(`overlay ${plan.patchPath}`);
|
|
1429
|
-
console.log(`pack ${pack.pack} ${pack.version} · ${pack.rules.length} rules`);
|
|
1725
|
+
console.log(`pack ${pack.pack} ${pack.version} · ${pack.rules.length} rules${link ? ` · linked, edits hot-reload (${resolve(packPath)})` : ""}`);
|
|
1726
|
+
if (shadow) console.log("mode shadow · every verdict sealed, every call proceeds");
|
|
1430
1727
|
if (ledger !== void 0) console.log(`ledger ${ledger}`);
|
|
1431
1728
|
if (plan.findings.length > 0) {
|
|
1432
1729
|
console.log("");
|
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
1
|
import "./evaluate-CkYySl7T.js";
|
|
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-
|
|
2
|
+
import "./views-UODslKwu.js";
|
|
3
|
+
import { a as planWrite, i as dshHome, n as applyWrite, o as praeRow, r as defaultProfile, s as profileAt, t as main } from "./cli-chtNj_el.js";
|
|
4
4
|
import "./src-Ciggx9ja.js";
|
|
5
5
|
|
|
6
6
|
export { applyWrite, defaultProfile, dshHome, main, planWrite, praeRow, profileAt };
|
|
@@ -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.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",
|