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