@zanii/blackbox 0.2.0 → 0.4.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 (94) hide show
  1. package/README.md +26 -1
  2. package/dist/a2a/index.d.ts +77 -0
  3. package/dist/a2a/index.js +305 -0
  4. package/dist/agents/index.d.ts +8 -0
  5. package/dist/analysis/accuracy.d.ts +24 -0
  6. package/dist/analysis/accuracy.js +45 -0
  7. package/dist/analysis/credential.d.ts +101 -0
  8. package/dist/analysis/credential.js +142 -0
  9. package/dist/analysis/faults.js +115 -0
  10. package/dist/analysis/grounding.d.ts +122 -0
  11. package/dist/analysis/grounding.js +445 -0
  12. package/dist/analysis/hallucination.d.ts +32 -0
  13. package/dist/analysis/hallucination.js +357 -0
  14. package/dist/analysis/index.d.ts +23 -0
  15. package/dist/analysis/index.js +93 -0
  16. package/dist/analysis/memory.d.ts +8 -0
  17. package/dist/analysis/memory.js +35 -8
  18. package/dist/analysis/reference.d.ts +49 -0
  19. package/dist/analysis/reference.js +164 -0
  20. package/dist/analysis/taxonomy.d.ts +18 -0
  21. package/dist/analysis/taxonomy.js +66 -0
  22. package/dist/approvals/index.d.ts +23 -0
  23. package/dist/approvals/index.js +48 -0
  24. package/dist/archive/index.d.ts +39 -0
  25. package/dist/archive/index.js +96 -0
  26. package/dist/archive/parquet.d.ts +2 -0
  27. package/dist/archive/parquet.js +185 -0
  28. package/dist/badge/index.d.ts +16 -0
  29. package/dist/badge/index.js +48 -0
  30. package/dist/bom/index.d.ts +14 -0
  31. package/dist/bom/index.js +152 -0
  32. package/dist/cli.js +114 -10
  33. package/dist/compliance/art12.d.ts +35 -0
  34. package/dist/compliance/art12.js +190 -0
  35. package/dist/compliance/index.d.ts +36 -2
  36. package/dist/compliance/index.js +78 -11
  37. package/dist/compliance/zanii.d.ts +29 -0
  38. package/dist/compliance/zanii.js +84 -0
  39. package/dist/constitution/index.d.ts +57 -0
  40. package/dist/constitution/index.js +131 -0
  41. package/dist/cv/index.d.ts +39 -0
  42. package/dist/cv/index.js +108 -0
  43. package/dist/disclosure/index.d.ts +31 -0
  44. package/dist/disclosure/index.js +113 -0
  45. package/dist/encryption/index.d.ts +9 -0
  46. package/dist/encryption/index.js +31 -0
  47. package/dist/evidence/index.d.ts +60 -0
  48. package/dist/evidence/index.js +151 -0
  49. package/dist/federation/index.d.ts +35 -0
  50. package/dist/federation/index.js +102 -0
  51. package/dist/finance/index.d.ts +126 -0
  52. package/dist/finance/index.js +320 -0
  53. package/dist/fleet/index.js +9 -0
  54. package/dist/gov/index.d.ts +108 -0
  55. package/dist/gov/index.js +225 -0
  56. package/dist/health/index.d.ts +120 -0
  57. package/dist/health/index.js +233 -0
  58. package/dist/index.d.ts +34 -5
  59. package/dist/index.js +34 -5
  60. package/dist/memory/index.d.ts +36 -0
  61. package/dist/memory/index.js +85 -0
  62. package/dist/occurrence/index.d.ts +11 -0
  63. package/dist/occurrence/index.js +18 -0
  64. package/dist/ocsf/index.d.ts +1 -1
  65. package/dist/ocsf/index.js +36 -3
  66. package/dist/otlp/index.d.ts +8 -1
  67. package/dist/otlp/index.js +258 -1
  68. package/dist/packs/index.js +44 -4
  69. package/dist/policy/delta.js +7 -1
  70. package/dist/policy/index.d.ts +40 -6
  71. package/dist/policy/index.js +186 -8
  72. package/dist/policy/zanii.d.ts +31 -0
  73. package/dist/policy/zanii.js +87 -0
  74. package/dist/pq/index.d.ts +23 -0
  75. package/dist/pq/index.js +104 -0
  76. package/dist/search/index.d.ts +23 -0
  77. package/dist/search/index.js +69 -0
  78. package/dist/session/index.d.ts +107 -1
  79. package/dist/session/index.js +189 -11
  80. package/dist/sla/index.d.ts +61 -0
  81. package/dist/sla/index.js +197 -0
  82. package/dist/succession/index.d.ts +50 -0
  83. package/dist/succession/index.js +123 -0
  84. package/dist/timestamp/index.d.ts +24 -0
  85. package/dist/timestamp/index.js +274 -0
  86. package/dist/tokens/index.d.ts +6 -0
  87. package/dist/tokens/index.js +46 -0
  88. package/dist/transparency/index.d.ts +188 -0
  89. package/dist/transparency/index.js +712 -0
  90. package/dist/version.d.ts +1 -1
  91. package/dist/version.js +1 -1
  92. package/dist/walls/index.d.ts +31 -0
  93. package/dist/walls/index.js +119 -0
  94. package/package.json +1 -1
@@ -0,0 +1,185 @@
1
+ // A session archive as one Parquet file (spec/archive.md §7, L7): the events of the archived sessions
2
+ // as typed columns, for the analytics tools that read Parquet. A minimal writer: one row group, one
3
+ // uncompressed PLAIN page per column, REQUIRED columns, written with Thrift's compact protocol; no
4
+ // dependency. Deterministic, so both SDKs write the same bytes (spec/vectors/parquet.json). The
5
+ // JSONL files stay the record; this is a copy for reading at scale. Mirrors parquet.py.
6
+ import { canonical } from "../reconcile/shared.js";
7
+ const MAGIC = new TextEncoder().encode("PAR1");
8
+ const CREATED_BY = "zanii-blackbox parquet v1";
9
+ // Parquet's enums (parquet.thrift)
10
+ const BYTE_ARRAY = 6;
11
+ const INT64 = 2;
12
+ const REQUIRED = 0;
13
+ const UTF8 = 0;
14
+ const PLAIN = 0;
15
+ const RLE = 3;
16
+ const UNCOMPRESSED = 0;
17
+ const DATA_PAGE = 0;
18
+ // Thrift compact types
19
+ const I32 = 5;
20
+ const I64 = 6;
21
+ const BINARY = 8;
22
+ const LIST = 9;
23
+ const STRUCT = 12;
24
+ const COLUMNS = [
25
+ ["session_id", BYTE_ARRAY],
26
+ ["seq", INT64],
27
+ ["ts", BYTE_ARRAY],
28
+ ["kind", BYTE_ARRAY],
29
+ ["body_hash", BYTE_ARRAY],
30
+ ["meta", BYTE_ARRAY],
31
+ ];
32
+ const enc = new TextEncoder();
33
+ const varint = (n) => {
34
+ const out = [];
35
+ let v = n;
36
+ for (;;) {
37
+ const b = Number(v & 0x7fn);
38
+ v >>= 7n;
39
+ if (v)
40
+ out.push(b | 0x80);
41
+ else {
42
+ out.push(b);
43
+ return out;
44
+ }
45
+ }
46
+ };
47
+ const zigzag = (n) => {
48
+ const v = BigInt(n);
49
+ return BigInt.asUintN(64, (v << 1n) ^ (v >> 63n));
50
+ };
51
+ const concat = (parts) => {
52
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
53
+ let at = 0;
54
+ for (const p of parts) {
55
+ out.set(p, at);
56
+ at += p.length;
57
+ }
58
+ return out;
59
+ };
60
+ /** Thrift compact protocol: a struct's fields, in increasing id order. */
61
+ class Struct {
62
+ parts = [];
63
+ last = 0;
64
+ head(fid, type) {
65
+ const delta = fid - this.last;
66
+ if (delta > 0 && delta <= 15)
67
+ this.parts.push([(delta << 4) | type]);
68
+ else
69
+ this.parts.push([type], varint(zigzag(fid)));
70
+ this.last = fid;
71
+ }
72
+ i32(fid, v) {
73
+ this.head(fid, I32);
74
+ this.parts.push(varint(zigzag(v)));
75
+ return this;
76
+ }
77
+ i64(fid, v) {
78
+ this.head(fid, I64);
79
+ this.parts.push(varint(zigzag(v)));
80
+ return this;
81
+ }
82
+ string(fid, v) {
83
+ const b = enc.encode(v);
84
+ this.head(fid, BINARY);
85
+ this.parts.push(varint(BigInt(b.length)), b);
86
+ return this;
87
+ }
88
+ struct(fid, v) {
89
+ this.head(fid, STRUCT);
90
+ this.parts.push(v.done());
91
+ return this;
92
+ }
93
+ list(fid, type, items) {
94
+ this.head(fid, LIST);
95
+ const n = items.length;
96
+ this.parts.push(n < 15 ? [(n << 4) | type] : [0xf0 | type, ...varint(BigInt(n))]);
97
+ this.parts.push(...items);
98
+ return this;
99
+ }
100
+ done() {
101
+ return concat([...this.parts, [0]]);
102
+ }
103
+ }
104
+ const i32Item = (v) => new Uint8Array(varint(zigzag(v)));
105
+ const strItem = (v) => {
106
+ const b = enc.encode(v);
107
+ return concat([varint(BigInt(b.length)), b]);
108
+ };
109
+ const u32 = (n) => {
110
+ const b = new Uint8Array(4);
111
+ new DataView(b.buffer).setUint32(0, n, true);
112
+ return b;
113
+ };
114
+ function plain(kind, values) {
115
+ if (kind === INT64) {
116
+ const b = new Uint8Array(values.length * 8);
117
+ const view = new DataView(b.buffer);
118
+ values.forEach((v, i) => {
119
+ view.setBigInt64(i * 8, BigInt(Number(v)), true);
120
+ });
121
+ return b;
122
+ }
123
+ return concat(values.flatMap((v) => {
124
+ const s = enc.encode(String(v));
125
+ return [u32(s.length), s];
126
+ }));
127
+ }
128
+ /** The archived events (session lines, in archive order) as one Parquet file. */
129
+ export function archiveParquet(lines) {
130
+ const rows = lines.map((l) => JSON.parse(l));
131
+ const values = {
132
+ session_id: rows.map((r) => r.session_id ?? ""),
133
+ seq: rows.map((r) => r.seq ?? 0),
134
+ ts: rows.map((r) => r.ts ?? ""),
135
+ kind: rows.map((r) => r.kind ?? ""),
136
+ body_hash: rows.map((r) => r.body_hash ?? ""),
137
+ meta: rows.map((r) => canonical(r.meta ?? {})),
138
+ };
139
+ const parts = [MAGIC];
140
+ let offset = MAGIC.length;
141
+ const chunks = [];
142
+ let total = 0;
143
+ for (const [name, kind] of COLUMNS) {
144
+ const data = plain(kind, values[name]);
145
+ const page = new Struct().i32(1, rows.length).i32(2, PLAIN).i32(3, RLE).i32(4, RLE);
146
+ const header = new Struct()
147
+ .i32(1, DATA_PAGE)
148
+ .i32(2, data.length)
149
+ .i32(3, data.length)
150
+ .struct(5, page)
151
+ .done();
152
+ const at = offset;
153
+ parts.push(header, data);
154
+ const size = header.length + data.length;
155
+ offset += size;
156
+ total += size;
157
+ const meta = new Struct()
158
+ .i32(1, kind)
159
+ .list(2, I32, [i32Item(PLAIN), i32Item(RLE)])
160
+ .list(3, BINARY, [strItem(name)])
161
+ .i32(4, UNCOMPRESSED)
162
+ .i64(5, rows.length)
163
+ .i64(6, size)
164
+ .i64(7, size)
165
+ .i64(9, at);
166
+ chunks.push(new Struct().i64(2, at).struct(3, meta).done());
167
+ }
168
+ const schema = [new Struct().string(4, "schema").i32(5, COLUMNS.length).done()];
169
+ for (const [name, kind] of COLUMNS) {
170
+ const el = new Struct().i32(1, kind).i32(3, REQUIRED).string(4, name);
171
+ if (kind === BYTE_ARRAY)
172
+ el.i32(6, UTF8);
173
+ schema.push(el.done());
174
+ }
175
+ const group = new Struct().list(1, STRUCT, chunks).i64(2, total).i64(3, rows.length).done();
176
+ const footer = new Struct()
177
+ .i32(1, 1)
178
+ .list(2, STRUCT, schema)
179
+ .i64(3, rows.length)
180
+ .list(4, STRUCT, [group])
181
+ .string(6, CREATED_BY)
182
+ .done();
183
+ parts.push(footer, u32(footer.length), MAGIC);
184
+ return concat(parts);
185
+ }
@@ -0,0 +1,16 @@
1
+ /** Zanii's flat badge as an SVG string; every value is escaped. */
2
+ export declare function badgeSvg(o: {
3
+ message: string;
4
+ label?: string;
5
+ color?: string;
6
+ }): string;
7
+ export declare const RECORD_STATES: readonly ["anchored", "verified", "recording", "broken"];
8
+ export type RecordState = (typeof RECORD_STATES)[number];
9
+ /** A session's record as a badge: anchored or verified (green), recording (blue), broken (red). */
10
+ export declare function recordBadge(state: RecordState): string;
11
+ /** The state from what GET /v1/sessions/:id says: the chain, whether it's closed, the anchors. */
12
+ export declare function recordState(o: {
13
+ ok: boolean;
14
+ closed: boolean;
15
+ anchorStatus: string;
16
+ }): RecordState;
@@ -0,0 +1,48 @@
1
+ // Record badges (spec/anchoring.md §11): Zanii's zero-dependency shields-style SVG (@zanii/embed),
2
+ // for a session's record state. Pure; mirrors sdks/python/src/zanii_blackbox/badge.py; pinned by
3
+ // spec/vectors/badge.json.
4
+ const COLORS = {
5
+ green: "#2ea44f",
6
+ red: "#d1242f",
7
+ gray: "#6a737d",
8
+ blue: "#0969da",
9
+ black: "#1f2328",
10
+ };
11
+ const ESCAPE = {
12
+ "&": "&amp;",
13
+ "<": "&lt;",
14
+ ">": "&gt;",
15
+ '"': "&quot;",
16
+ "'": "&#39;",
17
+ };
18
+ const esc = (s) => s.replace(/[&<>"']/g, (c) => ESCAPE[c]);
19
+ // ~6.5px a character, so the badge fits its text without font metrics (Zanii's estimate)
20
+ const width = (s) => Math.round([...s].length * 6.5) + 10;
21
+ /** Zanii's flat badge as an SVG string; every value is escaped. */
22
+ export function badgeSvg(o) {
23
+ const label = o.label ?? "zanii";
24
+ const fill = COLORS[o.color ?? "green"] ?? o.color ?? COLORS.green;
25
+ const lw = width(label);
26
+ const mw = width(o.message);
27
+ return (`<svg xmlns="http://www.w3.org/2000/svg" width="${lw + mw}" height="20" role="img" aria-label="${esc(label)}: ${esc(o.message)}">` +
28
+ `<rect width="${lw}" height="20" fill="#555"/>` +
29
+ `<rect x="${lw}" width="${mw}" height="20" fill="${esc(String(fill))}"/>` +
30
+ '<g fill="#fff" font-family="Verdana,DejaVu Sans,sans-serif" font-size="11" text-anchor="middle">' +
31
+ `<text x="${lw / 2}" y="14">${esc(label)}</text>` +
32
+ `<text x="${lw + mw / 2}" y="14">${esc(o.message)}</text>` +
33
+ "</g></svg>");
34
+ }
35
+ export const RECORD_STATES = ["anchored", "verified", "recording", "broken"];
36
+ /** A session's record as a badge: anchored or verified (green), recording (blue), broken (red). */
37
+ export function recordBadge(state) {
38
+ const color = state === "broken" ? "red" : state === "recording" ? "blue" : "green";
39
+ return badgeSvg({ label: "blackbox", message: state, color });
40
+ }
41
+ /** The state from what GET /v1/sessions/:id says: the chain, whether it's closed, the anchors. */
42
+ export function recordState(o) {
43
+ if (!o.ok)
44
+ return "broken";
45
+ if (o.anchorStatus === "complete")
46
+ return "anchored";
47
+ return o.closed ? "verified" : "recording";
48
+ }
@@ -0,0 +1,14 @@
1
+ export interface AgentIdentity {
2
+ id: string;
3
+ version?: string;
4
+ /** A SPIFFE ID for the workload the agent runs as. */
5
+ workload?: string;
6
+ /** The user or agent it acts for. */
7
+ on_behalf_of?: string;
8
+ /** The SHA-256 of its A2A Agent Card, `sha256:<hex>`. */
9
+ card_digest?: string;
10
+ }
11
+ /** spec/bom.md §1: an error message, or undefined for a valid `agent`. */
12
+ export declare function checkAgent(value: unknown): string | undefined;
13
+ /** spec/bom.md §2: a CycloneDX 1.6 JSON ML-BOM of one session's record. */
14
+ export declare function sessionBom(lines: readonly string[]): Record<string, unknown>;
@@ -0,0 +1,152 @@
1
+ // Agent identity and the agent's bill of materials (spec/bom.md): the `agent` a session is opened
2
+ // with, and a CycloneDX 1.6 ML-BOM of what one session actually used (models, providers, tool
3
+ // servers), made from its record. Deterministic: the same record gives the same bytes.
4
+ // Mirrors sdks/python/src/zanii_blackbox/bom.py.
5
+ import { createHash } from "node:crypto";
6
+ import { hashLine } from "../verify/envelope.js";
7
+ const AGENT_KEYS = new Set(["id", "version", "workload", "on_behalf_of", "card_digest"]);
8
+ const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
9
+ /** spec/bom.md §1: an error message, or undefined for a valid `agent`. */
10
+ export function checkAgent(value) {
11
+ if (!isObject(value))
12
+ return "agent must be an object";
13
+ const extra = Object.keys(value).find((k) => !AGENT_KEYS.has(k));
14
+ if (extra !== undefined)
15
+ return `agent.${extra} isn't a field`;
16
+ const { id, version, workload, on_behalf_of: onBehalfOf, card_digest: card } = value;
17
+ if (typeof id !== "string" || !/^[A-Za-z0-9._:@/-]{1,128}$/.test(id))
18
+ return "agent.id must be 1-128 of [A-Za-z0-9._:@/-]";
19
+ if (version !== undefined &&
20
+ (typeof version !== "string" || !/^[A-Za-z0-9._+-]{1,64}$/.test(version)))
21
+ return "agent.version must be 1-64 of [A-Za-z0-9._+-]";
22
+ if (workload !== undefined &&
23
+ (typeof workload !== "string" ||
24
+ !/^spiffe:\/\/[^\s/]+(\/\S*)?$/.test(workload) ||
25
+ workload.length > 2048))
26
+ return "agent.workload must be a SPIFFE ID (spiffe://trust-domain/path)";
27
+ if (onBehalfOf !== undefined &&
28
+ (typeof onBehalfOf !== "string" ||
29
+ onBehalfOf.length < 1 ||
30
+ onBehalfOf.length > 256 ||
31
+ [...onBehalfOf].some((c) => c < " " || c === "\u007f")))
32
+ return "agent.on_behalf_of must be 1-256 characters, no control characters";
33
+ if (card !== undefined && (typeof card !== "string" || !/^sha256:[0-9a-f]{64}$/.test(card)))
34
+ return "agent.card_digest must be sha256:<64 hex>";
35
+ return undefined;
36
+ }
37
+ const props = (pairs) => pairs
38
+ .filter((p) => typeof p[1] === "string" || typeof p[1] === "number")
39
+ .map(([name, value]) => ({ name, value: String(value) }));
40
+ /** The principal and actor the gateway proved from a delegated token, if any. */
41
+ function verifiedOf(agent) {
42
+ const v = agent.verified;
43
+ if (!isObject(v) || typeof v.iss !== "string" || typeof v.sub !== "string")
44
+ return {};
45
+ return { principal: `${v.iss}#${v.sub}`, ...(typeof v.act === "string" ? { actor: v.act } : {}) };
46
+ }
47
+ /** A UUID (version 8, RFC 9562) from the record's identity: the same record, the same serial. */
48
+ function serialOf(sessionId, count, head) {
49
+ const h = createHash("sha256").update(`${sessionId}\n${count}\n${head}`).digest();
50
+ h[6] = (h[6] & 0x0f) | 0x80;
51
+ h[8] = (h[8] & 0x3f) | 0x80;
52
+ const x = h.subarray(0, 16).toString("hex");
53
+ return `urn:uuid:${x.slice(0, 8)}-${x.slice(8, 12)}-${x.slice(12, 16)}-${x.slice(16, 20)}-${x.slice(20)}`;
54
+ }
55
+ /** spec/bom.md §2: a CycloneDX 1.6 JSON ML-BOM of one session's record. */
56
+ export function sessionBom(lines) {
57
+ const events = lines.map((l) => JSON.parse(l));
58
+ const first = events[0];
59
+ const last = lines.at(-1);
60
+ if (!first || last === undefined)
61
+ throw new Error("a BOM needs a record");
62
+ const sid = first.session_id;
63
+ const open = events.find((e) => e.kind === "session.open");
64
+ const agent = (isObject(open?.meta.agent) ? open.meta.agent : {});
65
+ const label = typeof open?.meta.label === "string" ? open.meta.label : undefined;
66
+ const providerOf = new Map();
67
+ const providers = new Map();
68
+ const models = new Map();
69
+ const servers = new Map();
70
+ for (const e of events) {
71
+ const m = e.meta;
72
+ if (e.kind === "llm.request" && typeof m.provider === "string") {
73
+ providerOf.set(e.seq, m.provider);
74
+ providers.set(m.provider, (providers.get(m.provider) ?? 0) + 1);
75
+ }
76
+ else if (e.kind === "llm.response" && typeof m.model === "string") {
77
+ const provider = providerOf.get(m.request_seq) ?? "unknown";
78
+ const key = `${provider}/${m.model}`;
79
+ const had = models.get(key) ?? { provider, model: m.model, calls: 0 };
80
+ had.calls++;
81
+ models.set(key, had);
82
+ }
83
+ else if (e.kind === "tool.call" && typeof m.server === "string") {
84
+ const tools = servers.get(m.server) ?? new Set();
85
+ if (typeof m.tool === "string")
86
+ tools.add(m.tool);
87
+ servers.set(m.server, tools);
88
+ }
89
+ }
90
+ const byKey = (m) => [...m.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
91
+ const components = byKey(models).map(([key, v]) => ({
92
+ type: "machine-learning-model",
93
+ "bom-ref": `model:${key}`,
94
+ name: v.model,
95
+ publisher: v.provider,
96
+ properties: props([["zanii:calls", v.calls]]),
97
+ }));
98
+ const services = [
99
+ ...byKey(providers).map(([id, calls]) => ({
100
+ "bom-ref": `provider:${id}`,
101
+ name: id,
102
+ group: "llm-provider",
103
+ properties: props([["zanii:calls", calls]]),
104
+ })),
105
+ ...byKey(servers).map(([id, tools]) => ({
106
+ "bom-ref": `tools:${id}`,
107
+ name: id,
108
+ group: "tool-server",
109
+ properties: [...tools].sort().map((t) => ({ name: "zanii:tool", value: t })),
110
+ })),
111
+ ];
112
+ const refs = [...components.map((c) => c["bom-ref"]), ...services.map((s) => s["bom-ref"])];
113
+ return {
114
+ bomFormat: "CycloneDX",
115
+ specVersion: "1.6",
116
+ serialNumber: serialOf(sid, lines.length, hashLine(last)),
117
+ version: 1,
118
+ metadata: {
119
+ timestamp: events.at(-1).ts,
120
+ tools: { components: [{ type: "application", name: "zanii-blackbox" }] },
121
+ component: {
122
+ type: "application",
123
+ "bom-ref": "agent",
124
+ name: agent.id ?? label ?? sid,
125
+ ...(agent.version ? { version: agent.version } : {}),
126
+ properties: props([
127
+ ["zanii:session_id", sid],
128
+ ["zanii:workload", agent.workload],
129
+ // spec/bom.md §1.2: set by the gateway from a JWT-SVID it checked
130
+ ["zanii:workload_verified", workloadProof(agent)],
131
+ ["zanii:on_behalf_of", agent.on_behalf_of],
132
+ ["zanii:card_digest", agent.card_digest],
133
+ // spec/bom.md §1.1: set by the gateway from a delegated token, never by the opener
134
+ ["zanii:verified_principal", verifiedOf(agent).principal],
135
+ ["zanii:verified_actor", verifiedOf(agent).actor],
136
+ ["zanii:record_head", hashLine(last)],
137
+ ]),
138
+ },
139
+ },
140
+ components,
141
+ services,
142
+ dependencies: [{ ref: "agent", dependsOn: refs }],
143
+ };
144
+ }
145
+ /** spec/bom.md §1.2: `jwt-svid:<trust domain>` when the gateway checked the workload's SVID. */
146
+ function workloadProof(agent) {
147
+ const v = agent
148
+ .workload_verified;
149
+ return typeof v?.method === "string" && typeof v.trust_domain === "string"
150
+ ? `${v.method}:${v.trust_domain}`
151
+ : undefined;
152
+ }
package/dist/cli.js CHANGED
@@ -37,6 +37,10 @@ const USAGE = `usage (spec/cli.md):
37
37
  blackbox analyze <bundle.json> [--bodies <dir>] [--prices <file>]
38
38
  blackbox verify-certificate <certificate.json> [--did <did:key:…>]
39
39
  blackbox investigate <bundle.json> [--bodies <dir>] [--directives <file>] [--identity <file>] [--proofs] [--out <prefix>]
40
+ blackbox evidence-pack <bundle.json> [--proofs] [--identity <file>] [--lang en|ar|both] [--out <prefix>]
41
+ blackbox sign-constitution <draft.json> --identity <owner.json> [--out <file>]
42
+ blackbox policy-from-zanii <zanii-policy.json> [--out <file>]
43
+ blackbox policy-to-zanii <policy.json> [--out <file>]
40
44
  blackbox statement --provider anthropic|openai --from <YYYY-MM-DD> --to <YYYY-MM-DD> [--out <file>]
41
45
  blackbox statement --provider csv --file <f> --model-column <name> --amount-column <name> --unit usd|cents|micro_usd|aed [--delimiter <c>] --from <d> --to <d> [--out <file>]
42
46
  blackbox statement --provider custom --connector <file> --from <d> --to <d> [--out <file>]
@@ -88,6 +92,12 @@ async function main(argv) {
88
92
  return preflightCommand(rest);
89
93
  if (command === "investigate")
90
94
  return investigateCommand(rest);
95
+ if (command === "evidence-pack")
96
+ return evidencePackCommand(rest);
97
+ if (command === "sign-constitution")
98
+ return signConstitutionCommand(rest);
99
+ if (command === "policy-from-zanii" || command === "policy-to-zanii")
100
+ return policyZaniiCommand(command, rest);
91
101
  if (command === "statement")
92
102
  return statementCommand(rest);
93
103
  if (command === "export-all")
@@ -607,6 +617,107 @@ async function certificateCommand(rest) {
607
617
  return result.ok ? 0 : 1;
608
618
  }
609
619
  /** spec/investigations.md §2: an independent investigator's signed report, offline. */
620
+ /** A signing identity file `{did, private_key}` (the gateway's identity file works as is). */
621
+ async function readIdentity(path) {
622
+ const { didFromSeed } = await import("./fleet/index.js");
623
+ const file = JSON.parse(readFileSync(path, "utf8"));
624
+ if (typeof file.private_key !== "string" || !/^[0-9a-f]{64}$/.test(file.private_key))
625
+ throw new Error("identity file: private_key must be 64 hex characters");
626
+ const seed = Buffer.from(file.private_key, "hex");
627
+ if (file.did !== didFromSeed(seed))
628
+ throw new Error("identity file: did doesn't match private_key");
629
+ return { did: file.did, seed };
630
+ }
631
+ /** spec/policy.md §8: a Zanii policy file as a Blackbox one, or back; skipped rules are named. */
632
+ async function policyZaniiCommand(command, rest) {
633
+ const { flags, positional } = parse(rest, []);
634
+ if (positional.length !== 1)
635
+ return usage();
636
+ const { fromZaniiPolicy, toZaniiPolicy } = await import("./policy/zanii.js");
637
+ try {
638
+ const input = JSON.parse(readFileSync(positional[0], "utf8"));
639
+ const r = command === "policy-from-zanii" ? fromZaniiPolicy(input) : toZaniiPolicy(input);
640
+ const out = flags.out || (command === "policy-from-zanii" ? "policy.json" : "zanii-policy.json");
641
+ writeFileSync(out, `${JSON.stringify(r.policy, null, 2)}\n`);
642
+ process.stdout.write(`${JSON.stringify({ ok: true, file: out, rules: r.policy.rules.length, skipped: r.skipped })}\n`);
643
+ return 0;
644
+ }
645
+ catch (error) {
646
+ process.stdout.write(`${JSON.stringify({ ok: false, error: error.message })}\n`);
647
+ return 2;
648
+ }
649
+ }
650
+ /** spec/governance.md §1: signs a draft rulebook `{name, rules, default?}` as the identity's DID. */
651
+ async function signConstitutionCommand(rest) {
652
+ const { flags, positional } = parse(rest, []);
653
+ if (positional.length !== 1 || !flags.identity)
654
+ return usage();
655
+ const { constitutionHash, createConstitution } = await import("./constitution/index.js");
656
+ try {
657
+ const draft = JSON.parse(readFileSync(positional[0], "utf8"));
658
+ const identity = await readIdentity(flags.identity);
659
+ const c = createConstitution({
660
+ issuer: identity.did,
661
+ name: String(draft.name ?? ""),
662
+ rules: (Array.isArray(draft.rules) ? draft.rules : []),
663
+ defaultEffect: (draft.default ?? "allow"),
664
+ createdAt: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
665
+ issuerPrivateKey: identity.seed,
666
+ });
667
+ const out = flags.out || "constitution.json";
668
+ writeFileSync(out, `${JSON.stringify(c, null, 2)}\n`);
669
+ process.stdout.write(`${JSON.stringify({ ok: true, file: out, manifest_hash: constitutionHash(c), issuer: c.issuer })}\n`);
670
+ return 0;
671
+ }
672
+ catch (error) {
673
+ process.stdout.write(`${JSON.stringify({ ok: false, error: error.message })}\n`);
674
+ return 2;
675
+ }
676
+ }
677
+ /** spec/investigations.md §7: the court evidence pack, `<prefix>.json` (signed) and `<prefix>.md`. */
678
+ async function evidencePackCommand(rest) {
679
+ const { flags, positional } = parse(rest, ["--proofs"]);
680
+ const lang = flags.lang ?? "both";
681
+ if (positional.length !== 1 || !["en", "ar", "both"].includes(lang))
682
+ return usage();
683
+ const fail = (error) => {
684
+ process.stdout.write(`${JSON.stringify({ ok: false, error: String(error) })}\n`);
685
+ return 2;
686
+ };
687
+ const { evidencePack, renderEvidencePack } = await import("./evidence/index.js");
688
+ const { signCertificate } = await import("./fleet/index.js");
689
+ let raw;
690
+ let bundle;
691
+ let identity;
692
+ try {
693
+ raw = readFileSync(positional[0]);
694
+ bundle = JSON.parse(raw.toString("utf8"));
695
+ if (!Array.isArray(bundle.lines) || bundle.lines.length === 0)
696
+ throw new Error("the bundle has no lines");
697
+ if (flags.identity)
698
+ identity = await readIdentity(flags.identity);
699
+ }
700
+ catch (error) {
701
+ return fail(error instanceof Error ? error.message : error);
702
+ }
703
+ const proofs = flags.proofs === "1";
704
+ const pack = evidencePack(bundle, await verifyBundle(bundle, { proofs }), {
705
+ generated_at: new Date().toISOString(),
706
+ bundle_sha256: `sha256:${createHash("sha256").update(raw).digest("hex")}`,
707
+ proofs: proofs ? "checked" : "not_checked",
708
+ });
709
+ const final = identity ? signCertificate(pack, identity.did, identity.seed) : pack;
710
+ const prefix = flags.out || `evidence-${pack.session_id}`;
711
+ try {
712
+ writeFileSync(`${prefix}.json`, `${JSON.stringify(final)}\n`);
713
+ writeFileSync(`${prefix}.md`, renderEvidencePack(pack, lang));
714
+ }
715
+ catch (error) {
716
+ return fail(error instanceof Error ? error.message : error);
717
+ }
718
+ process.stdout.write(`${JSON.stringify({ ok: pack.verified, json: `${prefix}.json`, md: `${prefix}.md`, signed_by: identity?.did ?? null })}\n`);
719
+ return pack.verified ? 0 : 1;
720
+ }
610
721
  async function investigateCommand(rest) {
611
722
  const { flags, positional } = parse(rest, ["--proofs"]);
612
723
  if (positional.length !== 1)
@@ -617,7 +728,7 @@ async function investigateCommand(rest) {
617
728
  };
618
729
  const { investigation, renderInvestigation } = await import("./investigate/index.js");
619
730
  const { loadPrices, sessionCost } = await import("./cost/index.js");
620
- const { didFromSeed, sessionSummary, signCertificate } = await import("./fleet/index.js");
731
+ const { sessionSummary, signCertificate } = await import("./fleet/index.js");
621
732
  let raw;
622
733
  let bundle;
623
734
  let directives;
@@ -637,15 +748,8 @@ async function investigateCommand(rest) {
637
748
  throw new Error("--directives must hold a list of directives");
638
749
  directives = list;
639
750
  }
640
- if (flags.identity) {
641
- const file = JSON.parse(readFileSync(flags.identity, "utf8"));
642
- if (typeof file.private_key !== "string" || !/^[0-9a-f]{64}$/.test(file.private_key))
643
- throw new Error("identity file: private_key must be 64 hex characters");
644
- const seed = Buffer.from(file.private_key, "hex");
645
- if (file.did !== didFromSeed(seed))
646
- throw new Error("identity file: did doesn't match private_key");
647
- identity = { did: file.did, seed };
648
- }
751
+ if (flags.identity)
752
+ identity = await readIdentity(flags.identity);
649
753
  }
650
754
  catch (error) {
651
755
  return fail(error instanceof Error ? error.message : error);
@@ -0,0 +1,35 @@
1
+ type Evidence = Record<string, number | string | boolean | null | string[] | Record<string, number>>;
2
+ export interface Art12Check {
3
+ id: string;
4
+ article: string;
5
+ requirement: string;
6
+ /** False: the paragraph covers only Annex III point 1(a) systems (remote biometric identification). */
7
+ applies: boolean;
8
+ /** True or false where the record can show it; null where a person must judge the evidence. */
9
+ ok: boolean | null;
10
+ evidence: Evidence;
11
+ }
12
+ export interface Art12Report {
13
+ session_id: string;
14
+ regulation: "Regulation (EU) 2024/1689";
15
+ annex_iii_1a: boolean;
16
+ /** Every applicable check that can pass, passes. */
17
+ ok: boolean;
18
+ checks: Art12Check[];
19
+ notes: string;
20
+ }
21
+ /** Art. 19(1) and 26(6): "at least six months". Counted as 183 days, the longest six months. */
22
+ export declare const ART12_MIN_RETENTION_DAYS = 183;
23
+ /**
24
+ * Checks one session's record against Article 12. `verified`: whether its chain verifies (the caller
25
+ * runs the check). `retentionDays`: the deployment's retention, null when records are kept forever.
26
+ * `annexIII1a`: the system is a remote biometric identification system, so 12(3) applies.
27
+ */
28
+ export declare function art12Check(record: {
29
+ lines: readonly string[];
30
+ verified: boolean;
31
+ }, options: {
32
+ retentionDays: number | null;
33
+ annexIII1a?: boolean;
34
+ }): Art12Report;
35
+ export {};