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.
- package/CHANGELOG.md +46 -0
- package/LICENSE +202 -0
- package/README.md +200 -0
- package/SECURITY.md +34 -0
- package/bin/attenu-guard.js +4 -0
- package/dist/cjs/adapters/langgraph.d.ts +174 -0
- package/dist/cjs/adapters/langgraph.d.ts.map +1 -0
- package/dist/cjs/adapters/langgraph.js +242 -0
- package/dist/cjs/adapters/langgraph.js.map +1 -0
- package/dist/cjs/audit.d.ts +82 -0
- package/dist/cjs/audit.d.ts.map +1 -0
- package/dist/cjs/audit.js +195 -0
- package/dist/cjs/audit.js.map +1 -0
- package/dist/cjs/authority.d.ts +114 -0
- package/dist/cjs/authority.d.ts.map +1 -0
- package/dist/cjs/authority.js +238 -0
- package/dist/cjs/authority.js.map +1 -0
- package/dist/cjs/canonical.d.ts +91 -0
- package/dist/cjs/canonical.d.ts.map +1 -0
- package/dist/cjs/canonical.js +372 -0
- package/dist/cjs/canonical.js.map +1 -0
- package/dist/cjs/ceilings.d.ts +211 -0
- package/dist/cjs/ceilings.d.ts.map +1 -0
- package/dist/cjs/ceilings.js +476 -0
- package/dist/cjs/ceilings.js.map +1 -0
- package/dist/cjs/chain.d.ts +106 -0
- package/dist/cjs/chain.d.ts.map +1 -0
- package/dist/cjs/chain.js +0 -0
- package/dist/cjs/chain.js.map +1 -0
- package/dist/cjs/cli.d.ts +16 -0
- package/dist/cjs/cli.d.ts.map +1 -0
- package/dist/cjs/cli.js +100 -0
- package/dist/cjs/cli.js.map +1 -0
- package/dist/cjs/evidence.d.ts +163 -0
- package/dist/cjs/evidence.d.ts.map +1 -0
- package/dist/cjs/evidence.js +409 -0
- package/dist/cjs/evidence.js.map +1 -0
- package/dist/cjs/guard.d.ts +186 -0
- package/dist/cjs/guard.d.ts.map +1 -0
- package/dist/cjs/guard.js +432 -0
- package/dist/cjs/guard.js.map +1 -0
- package/dist/cjs/index.d.ts +52 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +99 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/reasons.d.ts +93 -0
- package/dist/cjs/reasons.d.ts.map +1 -0
- package/dist/cjs/reasons.js +139 -0
- package/dist/cjs/reasons.js.map +1 -0
- package/dist/cjs/strikes.d.ts +24 -0
- package/dist/cjs/strikes.d.ts.map +1 -0
- package/dist/cjs/strikes.js +30 -0
- package/dist/cjs/strikes.js.map +1 -0
- package/dist/cjs/wire.d.ts +83 -0
- package/dist/cjs/wire.d.ts.map +1 -0
- package/dist/cjs/wire.js +165 -0
- package/dist/cjs/wire.js.map +1 -0
- package/dist/esm/adapters/langgraph.d.ts +174 -0
- package/dist/esm/adapters/langgraph.d.ts.map +1 -0
- package/dist/esm/adapters/langgraph.js +199 -0
- package/dist/esm/adapters/langgraph.js.map +1 -0
- package/dist/esm/audit.d.ts +82 -0
- package/dist/esm/audit.d.ts.map +1 -0
- package/dist/esm/audit.js +189 -0
- package/dist/esm/audit.js.map +1 -0
- package/dist/esm/authority.d.ts +114 -0
- package/dist/esm/authority.d.ts.map +1 -0
- package/dist/esm/authority.js +233 -0
- package/dist/esm/authority.js.map +1 -0
- package/dist/esm/canonical.d.ts +91 -0
- package/dist/esm/canonical.d.ts.map +1 -0
- package/dist/esm/canonical.js +359 -0
- package/dist/esm/canonical.js.map +1 -0
- package/dist/esm/ceilings.d.ts +211 -0
- package/dist/esm/ceilings.d.ts.map +1 -0
- package/dist/esm/ceilings.js +460 -0
- package/dist/esm/ceilings.js.map +1 -0
- package/dist/esm/chain.d.ts +106 -0
- package/dist/esm/chain.d.ts.map +1 -0
- package/dist/esm/chain.js +0 -0
- package/dist/esm/chain.js.map +1 -0
- package/dist/esm/cli.d.ts +16 -0
- package/dist/esm/cli.d.ts.map +1 -0
- package/dist/esm/cli.js +97 -0
- package/dist/esm/cli.js.map +1 -0
- package/dist/esm/evidence.d.ts +163 -0
- package/dist/esm/evidence.d.ts.map +1 -0
- package/dist/esm/evidence.js +398 -0
- package/dist/esm/evidence.js.map +1 -0
- package/dist/esm/guard.d.ts +186 -0
- package/dist/esm/guard.d.ts.map +1 -0
- package/dist/esm/guard.js +427 -0
- package/dist/esm/guard.js.map +1 -0
- package/dist/esm/index.d.ts +52 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +42 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/reasons.d.ts +93 -0
- package/dist/esm/reasons.d.ts.map +1 -0
- package/dist/esm/reasons.js +134 -0
- package/dist/esm/reasons.js.map +1 -0
- package/dist/esm/strikes.d.ts +24 -0
- package/dist/esm/strikes.d.ts.map +1 -0
- package/dist/esm/strikes.js +26 -0
- package/dist/esm/strikes.js.map +1 -0
- package/dist/esm/wire.d.ts +83 -0
- package/dist/esm/wire.d.ts.map +1 -0
- package/dist/esm/wire.js +159 -0
- package/dist/esm/wire.js.map +1 -0
- package/package.json +89 -0
package/dist/cjs/cli.js
ADDED
|
@@ -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":"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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,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
|