@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.
- package/README.md +26 -1
- package/dist/a2a/index.d.ts +77 -0
- package/dist/a2a/index.js +305 -0
- package/dist/agents/index.d.ts +8 -0
- package/dist/analysis/accuracy.d.ts +24 -0
- package/dist/analysis/accuracy.js +45 -0
- package/dist/analysis/credential.d.ts +101 -0
- package/dist/analysis/credential.js +142 -0
- package/dist/analysis/faults.js +115 -0
- package/dist/analysis/grounding.d.ts +122 -0
- package/dist/analysis/grounding.js +445 -0
- package/dist/analysis/hallucination.d.ts +32 -0
- package/dist/analysis/hallucination.js +357 -0
- package/dist/analysis/index.d.ts +23 -0
- package/dist/analysis/index.js +93 -0
- package/dist/analysis/memory.d.ts +8 -0
- package/dist/analysis/memory.js +35 -8
- package/dist/analysis/reference.d.ts +49 -0
- package/dist/analysis/reference.js +164 -0
- package/dist/analysis/taxonomy.d.ts +18 -0
- package/dist/analysis/taxonomy.js +66 -0
- package/dist/approvals/index.d.ts +23 -0
- package/dist/approvals/index.js +48 -0
- package/dist/archive/index.d.ts +39 -0
- package/dist/archive/index.js +96 -0
- package/dist/archive/parquet.d.ts +2 -0
- package/dist/archive/parquet.js +185 -0
- package/dist/badge/index.d.ts +16 -0
- package/dist/badge/index.js +48 -0
- package/dist/bom/index.d.ts +14 -0
- package/dist/bom/index.js +152 -0
- package/dist/cli.js +114 -10
- package/dist/compliance/art12.d.ts +35 -0
- package/dist/compliance/art12.js +190 -0
- package/dist/compliance/index.d.ts +36 -2
- package/dist/compliance/index.js +78 -11
- package/dist/compliance/zanii.d.ts +29 -0
- package/dist/compliance/zanii.js +84 -0
- package/dist/constitution/index.d.ts +57 -0
- package/dist/constitution/index.js +131 -0
- package/dist/cv/index.d.ts +39 -0
- package/dist/cv/index.js +108 -0
- package/dist/disclosure/index.d.ts +31 -0
- package/dist/disclosure/index.js +113 -0
- package/dist/encryption/index.d.ts +9 -0
- package/dist/encryption/index.js +31 -0
- package/dist/evidence/index.d.ts +60 -0
- package/dist/evidence/index.js +151 -0
- package/dist/federation/index.d.ts +35 -0
- package/dist/federation/index.js +102 -0
- package/dist/finance/index.d.ts +126 -0
- package/dist/finance/index.js +320 -0
- package/dist/fleet/index.js +9 -0
- package/dist/gov/index.d.ts +108 -0
- package/dist/gov/index.js +225 -0
- package/dist/health/index.d.ts +120 -0
- package/dist/health/index.js +233 -0
- package/dist/index.d.ts +34 -5
- package/dist/index.js +34 -5
- package/dist/memory/index.d.ts +36 -0
- package/dist/memory/index.js +85 -0
- package/dist/occurrence/index.d.ts +11 -0
- package/dist/occurrence/index.js +18 -0
- package/dist/ocsf/index.d.ts +1 -1
- package/dist/ocsf/index.js +36 -3
- package/dist/otlp/index.d.ts +8 -1
- package/dist/otlp/index.js +258 -1
- package/dist/packs/index.js +44 -4
- package/dist/policy/delta.js +7 -1
- package/dist/policy/index.d.ts +40 -6
- package/dist/policy/index.js +186 -8
- package/dist/policy/zanii.d.ts +31 -0
- package/dist/policy/zanii.js +87 -0
- package/dist/pq/index.d.ts +23 -0
- package/dist/pq/index.js +104 -0
- package/dist/search/index.d.ts +23 -0
- package/dist/search/index.js +69 -0
- package/dist/session/index.d.ts +107 -1
- package/dist/session/index.js +189 -11
- package/dist/sla/index.d.ts +61 -0
- package/dist/sla/index.js +197 -0
- package/dist/succession/index.d.ts +50 -0
- package/dist/succession/index.js +123 -0
- package/dist/timestamp/index.d.ts +24 -0
- package/dist/timestamp/index.js +274 -0
- package/dist/tokens/index.d.ts +6 -0
- package/dist/tokens/index.js +46 -0
- package/dist/transparency/index.d.ts +188 -0
- package/dist/transparency/index.js +712 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/walls/index.d.ts +31 -0
- package/dist/walls/index.js +119 -0
- 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
|
+
"&": "&",
|
|
13
|
+
"<": "<",
|
|
14
|
+
">": ">",
|
|
15
|
+
'"': """,
|
|
16
|
+
"'": "'",
|
|
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 {
|
|
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
|
-
|
|
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 {};
|