attenu-guard 0.1.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.
Files changed (112) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/LICENSE +202 -0
  3. package/README.md +200 -0
  4. package/SECURITY.md +34 -0
  5. package/bin/attenu-guard.js +4 -0
  6. package/dist/cjs/adapters/langgraph.d.ts +174 -0
  7. package/dist/cjs/adapters/langgraph.d.ts.map +1 -0
  8. package/dist/cjs/adapters/langgraph.js +242 -0
  9. package/dist/cjs/adapters/langgraph.js.map +1 -0
  10. package/dist/cjs/audit.d.ts +82 -0
  11. package/dist/cjs/audit.d.ts.map +1 -0
  12. package/dist/cjs/audit.js +195 -0
  13. package/dist/cjs/audit.js.map +1 -0
  14. package/dist/cjs/authority.d.ts +114 -0
  15. package/dist/cjs/authority.d.ts.map +1 -0
  16. package/dist/cjs/authority.js +238 -0
  17. package/dist/cjs/authority.js.map +1 -0
  18. package/dist/cjs/canonical.d.ts +91 -0
  19. package/dist/cjs/canonical.d.ts.map +1 -0
  20. package/dist/cjs/canonical.js +372 -0
  21. package/dist/cjs/canonical.js.map +1 -0
  22. package/dist/cjs/ceilings.d.ts +211 -0
  23. package/dist/cjs/ceilings.d.ts.map +1 -0
  24. package/dist/cjs/ceilings.js +476 -0
  25. package/dist/cjs/ceilings.js.map +1 -0
  26. package/dist/cjs/chain.d.ts +106 -0
  27. package/dist/cjs/chain.d.ts.map +1 -0
  28. package/dist/cjs/chain.js +0 -0
  29. package/dist/cjs/chain.js.map +1 -0
  30. package/dist/cjs/cli.d.ts +16 -0
  31. package/dist/cjs/cli.d.ts.map +1 -0
  32. package/dist/cjs/cli.js +100 -0
  33. package/dist/cjs/cli.js.map +1 -0
  34. package/dist/cjs/evidence.d.ts +163 -0
  35. package/dist/cjs/evidence.d.ts.map +1 -0
  36. package/dist/cjs/evidence.js +409 -0
  37. package/dist/cjs/evidence.js.map +1 -0
  38. package/dist/cjs/guard.d.ts +186 -0
  39. package/dist/cjs/guard.d.ts.map +1 -0
  40. package/dist/cjs/guard.js +432 -0
  41. package/dist/cjs/guard.js.map +1 -0
  42. package/dist/cjs/index.d.ts +52 -0
  43. package/dist/cjs/index.d.ts.map +1 -0
  44. package/dist/cjs/index.js +99 -0
  45. package/dist/cjs/index.js.map +1 -0
  46. package/dist/cjs/package.json +3 -0
  47. package/dist/cjs/reasons.d.ts +93 -0
  48. package/dist/cjs/reasons.d.ts.map +1 -0
  49. package/dist/cjs/reasons.js +139 -0
  50. package/dist/cjs/reasons.js.map +1 -0
  51. package/dist/cjs/strikes.d.ts +24 -0
  52. package/dist/cjs/strikes.d.ts.map +1 -0
  53. package/dist/cjs/strikes.js +30 -0
  54. package/dist/cjs/strikes.js.map +1 -0
  55. package/dist/cjs/wire.d.ts +83 -0
  56. package/dist/cjs/wire.d.ts.map +1 -0
  57. package/dist/cjs/wire.js +165 -0
  58. package/dist/cjs/wire.js.map +1 -0
  59. package/dist/esm/adapters/langgraph.d.ts +174 -0
  60. package/dist/esm/adapters/langgraph.d.ts.map +1 -0
  61. package/dist/esm/adapters/langgraph.js +199 -0
  62. package/dist/esm/adapters/langgraph.js.map +1 -0
  63. package/dist/esm/audit.d.ts +82 -0
  64. package/dist/esm/audit.d.ts.map +1 -0
  65. package/dist/esm/audit.js +189 -0
  66. package/dist/esm/audit.js.map +1 -0
  67. package/dist/esm/authority.d.ts +114 -0
  68. package/dist/esm/authority.d.ts.map +1 -0
  69. package/dist/esm/authority.js +233 -0
  70. package/dist/esm/authority.js.map +1 -0
  71. package/dist/esm/canonical.d.ts +91 -0
  72. package/dist/esm/canonical.d.ts.map +1 -0
  73. package/dist/esm/canonical.js +359 -0
  74. package/dist/esm/canonical.js.map +1 -0
  75. package/dist/esm/ceilings.d.ts +211 -0
  76. package/dist/esm/ceilings.d.ts.map +1 -0
  77. package/dist/esm/ceilings.js +460 -0
  78. package/dist/esm/ceilings.js.map +1 -0
  79. package/dist/esm/chain.d.ts +106 -0
  80. package/dist/esm/chain.d.ts.map +1 -0
  81. package/dist/esm/chain.js +0 -0
  82. package/dist/esm/chain.js.map +1 -0
  83. package/dist/esm/cli.d.ts +16 -0
  84. package/dist/esm/cli.d.ts.map +1 -0
  85. package/dist/esm/cli.js +97 -0
  86. package/dist/esm/cli.js.map +1 -0
  87. package/dist/esm/evidence.d.ts +163 -0
  88. package/dist/esm/evidence.d.ts.map +1 -0
  89. package/dist/esm/evidence.js +398 -0
  90. package/dist/esm/evidence.js.map +1 -0
  91. package/dist/esm/guard.d.ts +186 -0
  92. package/dist/esm/guard.d.ts.map +1 -0
  93. package/dist/esm/guard.js +427 -0
  94. package/dist/esm/guard.js.map +1 -0
  95. package/dist/esm/index.d.ts +52 -0
  96. package/dist/esm/index.d.ts.map +1 -0
  97. package/dist/esm/index.js +42 -0
  98. package/dist/esm/index.js.map +1 -0
  99. package/dist/esm/package.json +3 -0
  100. package/dist/esm/reasons.d.ts +93 -0
  101. package/dist/esm/reasons.d.ts.map +1 -0
  102. package/dist/esm/reasons.js +134 -0
  103. package/dist/esm/reasons.js.map +1 -0
  104. package/dist/esm/strikes.d.ts +24 -0
  105. package/dist/esm/strikes.d.ts.map +1 -0
  106. package/dist/esm/strikes.js +26 -0
  107. package/dist/esm/strikes.js.map +1 -0
  108. package/dist/esm/wire.d.ts +83 -0
  109. package/dist/esm/wire.d.ts.map +1 -0
  110. package/dist/esm/wire.js +159 -0
  111. package/dist/esm/wire.js.map +1 -0
  112. package/package.json +89 -0
@@ -0,0 +1,97 @@
1
+ /**
2
+ * attenu-guard — command-line tool.
3
+ *
4
+ * attenu-guard verify <log.jsonl | bundle.json> [--hs256-key HEX | --pubkey HEX] [--kid KID]
5
+ *
6
+ * A `.jsonl` audit log is checked for hash-chain integrity. A bundle (the
7
+ * output of `exportBundle`, or of the Python library's `export_bundle`) is
8
+ * checked for integrity, monotonicity (child ⊆ parent) and containment from the
9
+ * bundle alone; the signed anchor is verified when a key is given and reported
10
+ * as "not checked" otherwise.
11
+ *
12
+ * Exit codes: 0 = ok, 2 = a check failed, 1 = usage. The output lines match the
13
+ * Python CLI's, so either implementation can stand in for the other in a script.
14
+ */
15
+ import { readFileSync } from "node:fs";
16
+ import { AuditLog } from "./audit.js";
17
+ import { parseBundle, verifyBundle } from "./evidence.js";
18
+ import { Ed25519Verifier, HS256TestSigner } from "./wire.js";
19
+ const USAGE = `attenu-guard — command-line tool.
20
+
21
+ attenu-guard verify <log.jsonl | bundle.json> [--hs256-key HEX | --pubkey HEX] [--kid KID]
22
+ verify a hash-chained audit log, or an evidence bundle
23
+ (integrity · child ⊆ parent · containment;
24
+ --hs256-key/--pubkey checks the anchor)
25
+ `;
26
+ /** Render a boolean the way Python does, so the two CLIs print the same line. */
27
+ function py(value) {
28
+ return value ? "True" : "False";
29
+ }
30
+ function verify(args) {
31
+ let path = null;
32
+ let keyHex = null;
33
+ let pubHex = null;
34
+ let kid = null;
35
+ for (let i = 0; i < args.length; i++) {
36
+ const a = args[i];
37
+ if (a === "--hs256-key")
38
+ keyHex = args[++i] ?? null;
39
+ else if (a === "--pubkey")
40
+ pubHex = args[++i] ?? null;
41
+ else if (a === "--kid")
42
+ kid = args[++i] ?? null;
43
+ else if (path === null)
44
+ path = a;
45
+ }
46
+ if (path === null) {
47
+ process.stdout.write(USAGE);
48
+ return 1;
49
+ }
50
+ const text = readFileSync(path, "utf8");
51
+ let bundle = null;
52
+ try {
53
+ // A bundle is ONE JSON object; a ledger is JSON Lines.
54
+ const parsed = parseBundle(text);
55
+ bundle =
56
+ parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && "entries" in parsed
57
+ ? parsed
58
+ : null;
59
+ }
60
+ catch {
61
+ bundle = null;
62
+ }
63
+ if (bundle !== null) {
64
+ const anchorKid = bundle.anchor?.["kid"] ?? undefined;
65
+ let signer = null;
66
+ if (keyHex)
67
+ signer = new HS256TestSigner(Buffer.from(keyHex, "hex"), kid ?? anchorKid ?? "k1");
68
+ else if (pubHex) {
69
+ signer = new Ed25519Verifier(Buffer.from(pubHex, "hex"), kid ?? anchorKid ?? "k1");
70
+ }
71
+ const rep = verifyBundle(bundle, signer);
72
+ const c = rep.checks;
73
+ process.stdout.write(`integrity=${py(c.integrity)} monotonicity=${py(c.monotonicity)} ` +
74
+ `containment=${py(c.containment)} anchor=${c.anchor} ` +
75
+ `nodes=${rep.nodes} actions_checked=${rep.actions_checked}\n`);
76
+ for (const f of rep.failures)
77
+ process.stdout.write(` - ${f}\n`);
78
+ process.stdout.write(rep.ok ? "OK\n" : "FAILED\n");
79
+ return rep.ok ? 0 : 2;
80
+ }
81
+ const entries = AuditLog.parseLines(text);
82
+ const [ok, reason] = AuditLog.verify(entries);
83
+ process.stdout.write(ok ? "OK\n" : `TAMPERED — ${reason}\n`);
84
+ return ok ? 0 : 2;
85
+ }
86
+ export function main(argv = process.argv.slice(2)) {
87
+ if (argv.length === 0) {
88
+ process.stdout.write(USAGE);
89
+ return 1;
90
+ }
91
+ const [cmd, ...rest] = argv;
92
+ if (cmd === "verify" && rest.length > 0)
93
+ return verify(rest);
94
+ process.stdout.write(USAGE);
95
+ return 1;
96
+ }
97
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,163 @@
1
+ /**
2
+ * evidence.ts — the offline bundle exporter and verifier.
3
+ *
4
+ * A delegation ledger is only worth as much as an auditor's ability to check it
5
+ * WITHOUT the engine that produced it. A bundle is self-contained — the
6
+ * hash-chained ledger plus a signed anchor — and three invariants are checked
7
+ * from that bundle ALONE:
8
+ *
9
+ * integrity the hash chain reproduces AND matches the out-of-band signed
10
+ * anchor. A consistent full rewrite, which the chain check
11
+ * alone cannot catch, fails here: the anchor's head is the
12
+ * fixed point.
13
+ * monotonicity every delegation is child ⊆ parent — the granted authority on
14
+ * each `spawn` is narrower than the parent node's authority.
15
+ * containment every `allow` action's scope was within the acting node's
16
+ * authority: no action was authorized outside what the node held.
17
+ *
18
+ * const bundle = exportBundle(guard.auditLog(), signer);
19
+ * const report = verifyBundle(bundle, signer);
20
+ *
21
+ * No engine state is consulted — the bundle is the whole input, which is the
22
+ * point. This is byte-compatible with the Python library's
23
+ * `attenu_guard.evidence`.
24
+ */
25
+ import { type Json } from "./canonical.js";
26
+ import { AuditLog, type Anchor, type LedgerEntry } from "./audit.js";
27
+ import type { Signer } from "./wire.js";
28
+ /**
29
+ * The COMPLETE set of top-level ledger field names the library emits. Custody
30
+ * guarantee: an exported bundle may carry ONLY these — an unknown field is
31
+ * exactly where a raw tool argument would be smuggled, so it is a leak, not a
32
+ * curiosity. `exportBundle({strict: true})` throws on any field outside this set.
33
+ *
34
+ * `task` is free text (a delegated prompt) and `context` is an object; both are
35
+ * redactable for transport.
36
+ */
37
+ export declare const LEDGER_FIELDS: ReadonlySet<string>;
38
+ /**
39
+ * Thrown by `exportBundle({strict: true})` when a bundle would carry a field or
40
+ * context key outside the allow-list — potential customer data the custody
41
+ * contract says must not leave the premises.
42
+ */
43
+ export declare class EvidenceLeakError extends Error {
44
+ constructor(message: string);
45
+ }
46
+ export interface RedactionViolation {
47
+ event_index: number;
48
+ event: Json;
49
+ field?: string;
50
+ context_key?: string;
51
+ }
52
+ export interface RedactionReport {
53
+ ok: boolean;
54
+ violations: RedactionViolation[];
55
+ }
56
+ export interface Bundle {
57
+ v: number;
58
+ chain_id: string;
59
+ entries: LedgerEntry[];
60
+ anchor: Anchor;
61
+ redaction: RedactionReport;
62
+ note: string;
63
+ }
64
+ /**
65
+ * Every top-level field must be in `LEDGER_FIELDS`; if `contextAllowlist` is
66
+ * given, every context key must be in it. `task` free text is allowed
67
+ * structurally but is redacted by `exportBundle({redactTask: true})` for
68
+ * transport — its raw value is the caller's to keep, not this library's.
69
+ */
70
+ export declare function redactionReport(entries: readonly LedgerEntry[], contextAllowlist?: Iterable<string> | null): RedactionReport;
71
+ /** A signed commitment to the head of `entries` — mirrors `AuditLog.anchor`. */
72
+ export declare function anchorFor(entries: readonly LedgerEntry[], signer: Signer, ts?: number | string): Anchor;
73
+ export interface ExportOptions {
74
+ ts?: number | string;
75
+ contextAllowlist?: Iterable<string> | null;
76
+ /** Replace free-text `task` fields with a length-and-hash marker. */
77
+ redactTask?: boolean;
78
+ /** Throw `EvidenceLeakError` on any field outside the allow-list. */
79
+ strict?: boolean;
80
+ }
81
+ /**
82
+ * A self-contained evidence bundle: the full ledger plus a signed anchor over
83
+ * its head.
84
+ *
85
+ * With `redactTask`, free-text `task` fields are replaced by a length-and-hash
86
+ * marker BEFORE the anchor is computed, so the transported bundle carries no
87
+ * raw prompt text yet still verifies — redaction is not tampering, because it
88
+ * only ever removes. With `strict`, the bundle is checked against
89
+ * `LEDGER_FIELDS` (and `contextAllowlist` if given) and an `EvidenceLeakError`
90
+ * is thrown on any field outside it.
91
+ */
92
+ export declare function exportBundle(auditLog: AuditLog | readonly LedgerEntry[], signer: Signer, options?: ExportOptions): Bundle;
93
+ export interface GraphNode {
94
+ agent: Json;
95
+ task: Json;
96
+ parent: Json;
97
+ scopes: string[];
98
+ allows: number;
99
+ denies: number;
100
+ revoked: boolean;
101
+ complete: boolean;
102
+ denials_by_disposition: Record<string, number>;
103
+ }
104
+ export interface DelegationGraph {
105
+ chain_id: Json;
106
+ nodes: Record<string, GraphNode>;
107
+ edges: {
108
+ parent: string;
109
+ child: string;
110
+ }[];
111
+ }
112
+ /**
113
+ * A view of the chain from the bundle: each node with its agent, task,
114
+ * authority, parent and per-node action counts — what a reviewer or a UI
115
+ * renders. Derived from the ledger alone.
116
+ */
117
+ export declare function delegationGraph(bundle: Partial<Bundle>): DelegationGraph;
118
+ export interface DenialRow {
119
+ node: Json;
120
+ agent: Json;
121
+ tool: Json;
122
+ scope: Json;
123
+ disposition: Json;
124
+ reason: Json;
125
+ count: number;
126
+ first_seq: number;
127
+ last_seq: number;
128
+ }
129
+ /**
130
+ * Deny events grouped by (node, tool, scope, disposition) — the rows a decisions
131
+ * queue renders: "should this agent be allowed to <tool>?", with how often it
132
+ * asked and why it was refused. A pure fold over the ledger, ordered by first
133
+ * occurrence.
134
+ */
135
+ export declare function denials(bundle: Partial<Bundle>): DenialRow[];
136
+ export interface VerifyChecks {
137
+ integrity: boolean;
138
+ monotonicity: boolean;
139
+ containment: boolean;
140
+ anchor: "not checked" | "verified" | "FAILED";
141
+ }
142
+ export interface VerifyReport {
143
+ ok: boolean;
144
+ checks: VerifyChecks;
145
+ failures: string[];
146
+ nodes: number;
147
+ actions_checked: number;
148
+ chain_id: Json;
149
+ }
150
+ /**
151
+ * Verify integrity, monotonicity and containment from the bundle alone.
152
+ *
153
+ * `signer` is the verifier for the bundle's signed anchor — a public key, or
154
+ * the test signer. Without one the hash chain, monotonicity and containment are
155
+ * still checked but the anchor signature is NOT; the report says so
156
+ * (`checks.anchor === "not checked"`), and `ok` then means "consistent,
157
+ * unverified by key": a consistent full rewrite by someone holding the key
158
+ * cannot be excluded without the key.
159
+ */
160
+ export declare function verifyBundle(bundle: Partial<Bundle>, signer?: Signer | null): VerifyReport;
161
+ /** Read a bundle from JSON text, keeping every number's original literal. */
162
+ export declare function parseBundle(text: string): Bundle;
163
+ //# sourceMappingURL=evidence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evidence.d.ts","sourceRoot":"","sources":["../../src/evidence.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,398 @@
1
+ /**
2
+ * evidence.ts — the offline bundle exporter and verifier.
3
+ *
4
+ * A delegation ledger is only worth as much as an auditor's ability to check it
5
+ * WITHOUT the engine that produced it. A bundle is self-contained — the
6
+ * hash-chained ledger plus a signed anchor — and three invariants are checked
7
+ * from that bundle ALONE:
8
+ *
9
+ * integrity the hash chain reproduces AND matches the out-of-band signed
10
+ * anchor. A consistent full rewrite, which the chain check
11
+ * alone cannot catch, fails here: the anchor's head is the
12
+ * fixed point.
13
+ * monotonicity every delegation is child ⊆ parent — the granted authority on
14
+ * each `spawn` is narrower than the parent node's authority.
15
+ * containment every `allow` action's scope was within the acting node's
16
+ * authority: no action was authorized outside what the node held.
17
+ *
18
+ * const bundle = exportBundle(guard.auditLog(), signer);
19
+ * const report = verifyBundle(bundle, signer);
20
+ *
21
+ * No engine state is consulted — the bundle is the whole input, which is the
22
+ * point. This is byte-compatible with the Python library's
23
+ * `attenu_guard.evidence`.
24
+ */
25
+ import { canonicalBytes, compareCodePoints, parseJson, toPlain, } from "./canonical.js";
26
+ import { createHash } from "node:crypto";
27
+ import { AuditLog, SCHEMA_VERSION, chainIdOf, hashEntry, GENESIS } from "./audit.js";
28
+ import { Authority } from "./authority.js";
29
+ /**
30
+ * The COMPLETE set of top-level ledger field names the library emits. Custody
31
+ * guarantee: an exported bundle may carry ONLY these — an unknown field is
32
+ * exactly where a raw tool argument would be smuggled, so it is a leak, not a
33
+ * curiosity. `exportBundle({strict: true})` throws on any field outside this set.
34
+ *
35
+ * `task` is free text (a delegated prompt) and `context` is an object; both are
36
+ * redactable for transport.
37
+ */
38
+ export const LEDGER_FIELDS = new Set([
39
+ "v",
40
+ "seq",
41
+ "ts",
42
+ "event",
43
+ "prev_hash",
44
+ "hash",
45
+ "chain_id",
46
+ "node",
47
+ "parent",
48
+ "agent",
49
+ "task",
50
+ "scope",
51
+ "tool",
52
+ "context",
53
+ "reason",
54
+ "reasons",
55
+ "authority",
56
+ "requested",
57
+ "granted",
58
+ "target",
59
+ "revoked",
60
+ "strikes",
61
+ "mode",
62
+ "disposition",
63
+ ]);
64
+ /**
65
+ * Thrown by `exportBundle({strict: true})` when a bundle would carry a field or
66
+ * context key outside the allow-list — potential customer data the custody
67
+ * contract says must not leave the premises.
68
+ */
69
+ export class EvidenceLeakError extends Error {
70
+ constructor(message) {
71
+ super(message);
72
+ this.name = "EvidenceLeakError";
73
+ }
74
+ }
75
+ function redactTask(t) {
76
+ if (t === undefined || t === null || t === "" || t === 0 || t === false)
77
+ return t;
78
+ const s = String(toPlain(t));
79
+ const h = createHash("sha256").update(Buffer.from(s, "utf8")).digest("hex").slice(0, 12);
80
+ return `redacted:len=${s.length}:h=${h}`;
81
+ }
82
+ /**
83
+ * Every top-level field must be in `LEDGER_FIELDS`; if `contextAllowlist` is
84
+ * given, every context key must be in it. `task` free text is allowed
85
+ * structurally but is redacted by `exportBundle({redactTask: true})` for
86
+ * transport — its raw value is the caller's to keep, not this library's.
87
+ */
88
+ export function redactionReport(entries, contextAllowlist) {
89
+ const allow = contextAllowlist === undefined || contextAllowlist === null ? null : new Set(contextAllowlist);
90
+ const violations = [];
91
+ entries.forEach((e, i) => {
92
+ for (const f of Object.keys(e)) {
93
+ if (!LEDGER_FIELDS.has(f)) {
94
+ violations.push({ event_index: i, event: toPlain(e["event"]), field: f });
95
+ }
96
+ }
97
+ if (allow !== null) {
98
+ const ctx = (e["context"] ?? {});
99
+ if (ctx !== null && typeof ctx === "object" && !Array.isArray(ctx)) {
100
+ for (const k of Object.keys(ctx)) {
101
+ if (!allow.has(k)) {
102
+ violations.push({ event_index: i, event: toPlain(e["event"]), context_key: k });
103
+ }
104
+ }
105
+ }
106
+ }
107
+ });
108
+ return { ok: violations.length === 0, violations };
109
+ }
110
+ /** A signed commitment to the head of `entries` — mirrors `AuditLog.anchor`. */
111
+ export function anchorFor(entries, signer, ts = 0) {
112
+ let seq;
113
+ let head;
114
+ if (entries.length === 0) {
115
+ seq = -1;
116
+ head = "GENESIS";
117
+ }
118
+ else {
119
+ const last = entries[entries.length - 1];
120
+ const rawSeq = toPlain(last["seq"]);
121
+ seq = typeof rawSeq === "number" ? rawSeq : entries.length - 1;
122
+ head = last["hash"];
123
+ }
124
+ const body = { v: SCHEMA_VERSION, chain_id: chainIdOf(entries), seq, head, ts };
125
+ return { ...body, kid: signer.kid ?? null, sig: signer.sign(canonicalBytes(body)).toString("hex") };
126
+ }
127
+ /**
128
+ * A self-contained evidence bundle: the full ledger plus a signed anchor over
129
+ * its head.
130
+ *
131
+ * With `redactTask`, free-text `task` fields are replaced by a length-and-hash
132
+ * marker BEFORE the anchor is computed, so the transported bundle carries no
133
+ * raw prompt text yet still verifies — redaction is not tampering, because it
134
+ * only ever removes. With `strict`, the bundle is checked against
135
+ * `LEDGER_FIELDS` (and `contextAllowlist` if given) and an `EvidenceLeakError`
136
+ * is thrown on any field outside it.
137
+ */
138
+ export function exportBundle(auditLog, signer, options = {}) {
139
+ const source = auditLog instanceof AuditLog ? auditLog.entries : auditLog;
140
+ const entries = source.map((e) => ({ ...e }));
141
+ if (options.redactTask) {
142
+ for (const e of entries) {
143
+ if ("task" in e)
144
+ e["task"] = redactTask(e["task"]);
145
+ }
146
+ // Re-hash the chain so the redacted form is what the anchor covers.
147
+ let prev = GENESIS;
148
+ for (const e of entries) {
149
+ e["prev_hash"] = prev;
150
+ const payload = {};
151
+ for (const [k, v] of Object.entries(e))
152
+ if (k !== "hash")
153
+ payload[k] = v;
154
+ e["hash"] = hashEntry(prev, payload);
155
+ prev = e["hash"];
156
+ }
157
+ }
158
+ const report = redactionReport(entries, options.contextAllowlist ?? null);
159
+ if (options.strict && !report.ok) {
160
+ throw new EvidenceLeakError(`${report.violations.length} field(s) outside the ledger allow-list: ` +
161
+ JSON.stringify(report.violations.slice(0, 5)));
162
+ }
163
+ const anchor = anchorFor(entries, signer, options.ts ?? 0);
164
+ anchor.verified = AuditLog.verifyAnchor(entries, anchor, signer)[0];
165
+ return {
166
+ v: SCHEMA_VERSION,
167
+ chain_id: chainIdOf(entries),
168
+ entries,
169
+ anchor,
170
+ redaction: report,
171
+ note: "offline-verifiable: attenu_guard.evidence.verify_bundle(bundle, signer)",
172
+ };
173
+ }
174
+ /**
175
+ * A ledger field, or `null` when it is absent. Python's `dict.get` yields `None`
176
+ * for a missing key and that `None` reaches the caller as a value; JavaScript
177
+ * would hand back `undefined`, which is not the same thing to a `deepEqual` or a
178
+ * JSON serialiser. Every field read for a report goes through here.
179
+ */
180
+ function orNull(value) {
181
+ const plain = toPlain(value);
182
+ return plain === undefined ? null : plain;
183
+ }
184
+ /**
185
+ * `node -> Authority` and `node -> parent`, reconstructed from `root` and
186
+ * `spawn` events alone. No engine state.
187
+ */
188
+ function nodeAuthorities(entries) {
189
+ const auth = new Map();
190
+ const parent = new Map();
191
+ const failures = [];
192
+ for (const e of entries) {
193
+ const ev = toPlain(e["event"]);
194
+ const node = toPlain(e["node"]);
195
+ if (ev === "root") {
196
+ try {
197
+ auth.set(node, Authority.fromWire(e["authority"] ?? null));
198
+ }
199
+ catch (exc) {
200
+ failures.push(`root ${node}: unreadable authority (${exc.message})`);
201
+ }
202
+ }
203
+ else if (ev === "spawn") {
204
+ parent.set(node, toPlain(e["parent"]) ?? null);
205
+ try {
206
+ auth.set(node, Authority.fromWire(e["granted"] ?? null));
207
+ }
208
+ catch (exc) {
209
+ failures.push(`spawn ${node}: unreadable granted (${exc.message})`);
210
+ }
211
+ }
212
+ }
213
+ return { auth, parent, failures };
214
+ }
215
+ /**
216
+ * A view of the chain from the bundle: each node with its agent, task,
217
+ * authority, parent and per-node action counts — what a reviewer or a UI
218
+ * renders. Derived from the ledger alone.
219
+ */
220
+ export function delegationGraph(bundle) {
221
+ const entries = bundle.entries ?? [];
222
+ const { auth, parent } = nodeAuthorities(entries);
223
+ const meta = {};
224
+ for (const e of entries) {
225
+ const ev = toPlain(e["event"]);
226
+ const n = toPlain(e["node"]);
227
+ if (ev === "root" || ev === "spawn") {
228
+ const a = auth.get(n);
229
+ meta[n] = {
230
+ agent: orNull(e["agent"]),
231
+ task: orNull(e["task"]),
232
+ parent: orNull(e["parent"]),
233
+ scopes: a ? Array.from(a.scopes).sort(compareCodePoints) : [],
234
+ allows: 0,
235
+ denies: 0,
236
+ revoked: false,
237
+ complete: false,
238
+ denials_by_disposition: {},
239
+ };
240
+ }
241
+ else if (ev === "allow" && meta[n]) {
242
+ meta[n].allows += 1;
243
+ }
244
+ else if (ev === "deny" && meta[n]) {
245
+ meta[n].denies += 1;
246
+ // A deny without a disposition is named by its reason.
247
+ const d = (toPlain(e["disposition"]) ?? toPlain(e["reason"]) ?? "unstated");
248
+ meta[n].denials_by_disposition[d] = (meta[n].denials_by_disposition[d] ?? 0) + 1;
249
+ }
250
+ else if (ev === "done" && meta[n]) {
251
+ meta[n].complete = true;
252
+ }
253
+ else if (ev === "kill") {
254
+ for (const r of toPlain(e["revoked"]) ?? []) {
255
+ if (meta[r])
256
+ meta[r].revoked = true;
257
+ }
258
+ }
259
+ }
260
+ const edges = [];
261
+ for (const [child, p] of parent) {
262
+ if (p)
263
+ edges.push({ parent: p, child });
264
+ }
265
+ return { chain_id: orNull(bundle.chain_id), nodes: meta, edges };
266
+ }
267
+ /**
268
+ * Deny events grouped by (node, tool, scope, disposition) — the rows a decisions
269
+ * queue renders: "should this agent be allowed to <tool>?", with how often it
270
+ * asked and why it was refused. A pure fold over the ledger, ordered by first
271
+ * occurrence.
272
+ */
273
+ export function denials(bundle) {
274
+ const entries = bundle.entries ?? [];
275
+ const agentOf = new Map();
276
+ for (const e of entries) {
277
+ const ev = toPlain(e["event"]);
278
+ if (ev === "root" || ev === "spawn") {
279
+ agentOf.set(toPlain(e["node"]), orNull(e["agent"]));
280
+ }
281
+ }
282
+ const rows = new Map();
283
+ for (const e of entries) {
284
+ if (toPlain(e["event"]) !== "deny")
285
+ continue;
286
+ const node = orNull(e["node"]);
287
+ const key = JSON.stringify([node, orNull(e["tool"]), orNull(e["scope"]), orNull(e["disposition"])]);
288
+ const seq = toPlain(e["seq"]);
289
+ const existing = rows.get(key);
290
+ if (existing === undefined) {
291
+ rows.set(key, {
292
+ node,
293
+ agent: agentOf.get(node) ?? null,
294
+ tool: orNull(e["tool"]),
295
+ scope: orNull(e["scope"]),
296
+ disposition: orNull(e["disposition"]),
297
+ reason: orNull(e["reason"]),
298
+ count: 1,
299
+ first_seq: seq,
300
+ last_seq: seq,
301
+ });
302
+ }
303
+ else {
304
+ existing.count += 1;
305
+ existing.last_seq = seq;
306
+ }
307
+ }
308
+ return Array.from(rows.values()).sort((a, b) => a.first_seq - b.first_seq);
309
+ }
310
+ /**
311
+ * Verify integrity, monotonicity and containment from the bundle alone.
312
+ *
313
+ * `signer` is the verifier for the bundle's signed anchor — a public key, or
314
+ * the test signer. Without one the hash chain, monotonicity and containment are
315
+ * still checked but the anchor signature is NOT; the report says so
316
+ * (`checks.anchor === "not checked"`), and `ok` then means "consistent,
317
+ * unverified by key": a consistent full rewrite by someone holding the key
318
+ * cannot be excluded without the key.
319
+ */
320
+ export function verifyBundle(bundle, signer = null) {
321
+ const entries = bundle.entries ?? [];
322
+ const anchor = (bundle.anchor ?? {});
323
+ const checks = {
324
+ integrity: false,
325
+ monotonicity: false,
326
+ containment: false,
327
+ anchor: "not checked",
328
+ };
329
+ const failures = [];
330
+ // (1) integrity: the hash chain, plus the signed anchor when a key is given.
331
+ const [okChain, err] = AuditLog.verify(entries);
332
+ if (!okChain)
333
+ failures.push(`integrity: ${err}`);
334
+ if (signer !== null) {
335
+ const [okAnchor, aerr] = AuditLog.verifyAnchor(entries, anchor, signer);
336
+ checks.anchor = okAnchor ? "verified" : "FAILED";
337
+ if (!okAnchor)
338
+ failures.push(`integrity(anchor): ${aerr}`);
339
+ checks.integrity = okChain && okAnchor;
340
+ }
341
+ else {
342
+ checks.integrity = okChain;
343
+ }
344
+ const { auth, parent, failures: afail } = nodeAuthorities(entries);
345
+ failures.push(...afail);
346
+ // (2) monotonicity: every child ⊆ its parent.
347
+ let mono = true;
348
+ for (const [node, pid] of parent) {
349
+ if (pid === null || !auth.has(pid) || !auth.has(node))
350
+ continue;
351
+ const child = auth.get(node);
352
+ const p = auth.get(pid);
353
+ const extra = Array.from(child.scopes).filter((s) => !p.scopes.has(s));
354
+ if (!child.isNarrowerThan(p) && extra.length > 0) {
355
+ mono = false;
356
+ failures.push(`monotonicity: ${node} not ⊆ parent ${pid} (child scopes ` +
357
+ `[${extra.sort(compareCodePoints).map((s) => `'${s}'`).join(", ")}] not held by parent)`);
358
+ }
359
+ }
360
+ checks.monotonicity = mono && afail.length === 0;
361
+ // (3) containment: every allowed action's scope within the acting node's authority.
362
+ let contained = true;
363
+ let actions = 0;
364
+ for (const e of entries) {
365
+ if (toPlain(e["event"]) !== "allow")
366
+ continue;
367
+ actions += 1;
368
+ const node = toPlain(e["node"]);
369
+ const scope = toPlain(e["scope"]);
370
+ const ctx = toPlain(e["context"]) ?? {};
371
+ const a = auth.get(node);
372
+ if (a === undefined) {
373
+ contained = false;
374
+ failures.push(`containment: allow on unknown node ${node}`);
375
+ continue;
376
+ }
377
+ if (!a.permits(scope, ctx).allowed) {
378
+ contained = false;
379
+ failures.push(`containment: allow of '${scope}' on ${node} outside its authority ` +
380
+ `[${Array.from(a.scopes).sort(compareCodePoints).map((s) => `'${s}'`).join(", ")}]`);
381
+ }
382
+ }
383
+ checks.containment = contained;
384
+ const ok = checks.integrity && checks.monotonicity && checks.containment && failures.length === 0;
385
+ return {
386
+ ok,
387
+ checks,
388
+ failures,
389
+ nodes: auth.size,
390
+ actions_checked: actions,
391
+ chain_id: orNull(bundle.chain_id),
392
+ };
393
+ }
394
+ /** Read a bundle from JSON text, keeping every number's original literal. */
395
+ export function parseBundle(text) {
396
+ return parseJson(text);
397
+ }
398
+ //# sourceMappingURL=evidence.js.map