@zanii/blackbox 0.3.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 (82) hide show
  1. package/README.md +26 -1
  2. package/dist/a2a/index.d.ts +27 -0
  3. package/dist/a2a/index.js +104 -1
  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.js +1 -0
  21. package/dist/approvals/index.d.ts +23 -0
  22. package/dist/approvals/index.js +48 -0
  23. package/dist/archive/parquet.d.ts +2 -0
  24. package/dist/archive/parquet.js +185 -0
  25. package/dist/badge/index.d.ts +16 -0
  26. package/dist/badge/index.js +48 -0
  27. package/dist/bom/index.js +20 -0
  28. package/dist/cli.js +114 -10
  29. package/dist/compliance/art12.js +36 -9
  30. package/dist/compliance/index.d.ts +36 -2
  31. package/dist/compliance/index.js +78 -11
  32. package/dist/compliance/zanii.d.ts +29 -0
  33. package/dist/compliance/zanii.js +84 -0
  34. package/dist/constitution/index.d.ts +57 -0
  35. package/dist/constitution/index.js +131 -0
  36. package/dist/cv/index.d.ts +39 -0
  37. package/dist/cv/index.js +108 -0
  38. package/dist/disclosure/index.d.ts +31 -0
  39. package/dist/disclosure/index.js +113 -0
  40. package/dist/encryption/index.d.ts +9 -0
  41. package/dist/encryption/index.js +31 -0
  42. package/dist/evidence/index.d.ts +60 -0
  43. package/dist/evidence/index.js +151 -0
  44. package/dist/federation/index.d.ts +35 -0
  45. package/dist/federation/index.js +102 -0
  46. package/dist/finance/index.d.ts +126 -0
  47. package/dist/finance/index.js +320 -0
  48. package/dist/fleet/index.js +9 -0
  49. package/dist/gov/index.d.ts +108 -0
  50. package/dist/gov/index.js +225 -0
  51. package/dist/health/index.d.ts +120 -0
  52. package/dist/health/index.js +233 -0
  53. package/dist/index.d.ts +28 -6
  54. package/dist/index.js +28 -6
  55. package/dist/memory/index.d.ts +36 -0
  56. package/dist/memory/index.js +85 -0
  57. package/dist/occurrence/index.d.ts +11 -0
  58. package/dist/occurrence/index.js +18 -0
  59. package/dist/otlp/index.js +28 -1
  60. package/dist/packs/index.js +44 -4
  61. package/dist/policy/delta.js +7 -1
  62. package/dist/policy/index.d.ts +23 -6
  63. package/dist/policy/index.js +151 -8
  64. package/dist/policy/zanii.d.ts +31 -0
  65. package/dist/policy/zanii.js +87 -0
  66. package/dist/pq/index.d.ts +23 -0
  67. package/dist/pq/index.js +104 -0
  68. package/dist/search/index.d.ts +23 -0
  69. package/dist/search/index.js +69 -0
  70. package/dist/session/index.d.ts +89 -1
  71. package/dist/session/index.js +143 -11
  72. package/dist/sla/index.d.ts +61 -0
  73. package/dist/sla/index.js +197 -0
  74. package/dist/succession/index.d.ts +50 -0
  75. package/dist/succession/index.js +123 -0
  76. package/dist/tokens/index.d.ts +6 -0
  77. package/dist/tokens/index.js +46 -0
  78. package/dist/version.d.ts +1 -1
  79. package/dist/version.js +1 -1
  80. package/dist/walls/index.d.ts +31 -0
  81. package/dist/walls/index.js +119 -0
  82. package/package.json +1 -1
@@ -0,0 +1,49 @@
1
+ import type { Fact } from "./grounding.ts";
2
+ export interface ReferenceFact {
3
+ id: string;
4
+ /** Terms that name what the fact is about (`SOFA-221`, `refund`, `استرداد`): one of them must be in
5
+ * the answer's sentence for the fact to apply. */
6
+ about: string[];
7
+ kind: "money" | "percent" | "quantity" | "date" | "text";
8
+ /** Canonical (`1250.5`, `20261015`); absent for `text`. */
9
+ value?: string;
10
+ currency?: string;
11
+ unit?: string;
12
+ text?: string;
13
+ source?: string;
14
+ }
15
+ export interface ReferencePack {
16
+ id: string;
17
+ version: string;
18
+ tenant?: string;
19
+ title?: string;
20
+ facts: ReferenceFact[];
21
+ /** `sha256:` of the pack's RFC 8785 JSON as loaded. */
22
+ hash: string;
23
+ }
24
+ /** spec/findings.md §10: the packs in a reference file, checked; throws with the first problem. */
25
+ export declare function loadReferencePacks(bytes: Uint8Array): ReferencePack[];
26
+ /** The packs a session's tenant sees: its own, and those for everyone. */
27
+ export declare function packsFor(packs: readonly ReferencePack[], tenant: string | undefined): ReferencePack[];
28
+ export type ReferenceVerdict = {
29
+ status: "grounded";
30
+ pack: ReferencePack;
31
+ fact: ReferenceFact;
32
+ } | {
33
+ status: "contradicted";
34
+ pack: ReferencePack;
35
+ fact: ReferenceFact;
36
+ } | null;
37
+ /**
38
+ * §10: what the packs say about one fact of an answer. The pack facts that apply are those of the
39
+ * same kind with an `about` term in the fact's sentence. Grounded when one of them has its value;
40
+ * contradicted when some apply and none has it; null when none applies.
41
+ */
42
+ export declare function checkAgainstPacks(text: string, f: Fact, packs: readonly ReferencePack[]): ReferenceVerdict;
43
+ /** §10: the text facts an answer touches (an `about` term anywhere in it), for a grounding service. */
44
+ export declare function referenceContext(text: string, packs: readonly ReferencePack[]): Array<{
45
+ pack: string;
46
+ id: string;
47
+ text: string;
48
+ source?: string;
49
+ }>;
@@ -0,0 +1,164 @@
1
+ // Reference packs (spec/findings.md §10, H4): the facts an organisation owns (prices, fees, rules,
2
+ // deadlines, limits, terms), loaded once and checked against every answer, even when the agent
3
+ // called no tool. Any industry: a pack is data, and a tenant can have its own. Mirrors
4
+ // sdks/python/src/zanii_blackbox/analysis/reference.py; pinned by spec/vectors/reference.json.
5
+ import { createHash } from "node:crypto";
6
+ import { canonical } from "../reconcile/shared.js";
7
+ import { canonicalNumber, normaliseValue, western } from "./hallucination.js";
8
+ const KINDS = new Set(["money", "percent", "quantity", "date", "text"]);
9
+ const NUMERIC = /^[0-9]{1,3}(?:,[0-9]{3})+(?:\.[0-9]+)?$|^[0-9]+(?:\.[0-9]+)?$/;
10
+ const DATE = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
11
+ const str = (v, max) => typeof v === "string" && v.length >= 1 && v.length <= max;
12
+ /** spec/findings.md §10: the packs in a reference file, checked; throws with the first problem. */
13
+ export function loadReferencePacks(bytes) {
14
+ let doc;
15
+ try {
16
+ doc = JSON.parse(new TextDecoder().decode(bytes));
17
+ }
18
+ catch {
19
+ throw new Error("the reference file isn't JSON");
20
+ }
21
+ const packs = doc?.packs;
22
+ if (!Array.isArray(packs) || packs.length < 1 || packs.length > 100)
23
+ throw new Error("packs must be a list of 1-100 packs");
24
+ const ids = new Set();
25
+ return packs.map((p, i) => {
26
+ const at = `packs[${i}]`;
27
+ if (typeof p !== "object" || p === null || Array.isArray(p))
28
+ throw new Error(`${at} must be an object`);
29
+ const o = p;
30
+ if (typeof o.id !== "string" || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(o.id))
31
+ throw new Error(`${at}.id must be 1-64 of a-z 0-9 -`);
32
+ const key = `${o.tenant ?? ""}\u0000${o.id}`;
33
+ if (ids.has(key))
34
+ throw new Error(`${at}.id ${o.id} is there twice`);
35
+ ids.add(key);
36
+ if (!str(o.version, 64))
37
+ throw new Error(`${at}.version must be 1-64 characters`);
38
+ if (o.tenant !== undefined && !str(o.tenant, 128))
39
+ throw new Error(`${at}.tenant must be 1-128 characters`);
40
+ if (o.title !== undefined && !str(o.title, 200))
41
+ throw new Error(`${at}.title must be 1-200 characters`);
42
+ if (!Array.isArray(o.facts) || o.facts.length < 1 || o.facts.length > 10_000)
43
+ throw new Error(`${at}.facts must be a list of 1-10,000 facts`);
44
+ const factIds = new Set();
45
+ const facts = o.facts.map((f, j) => checkFact(f, `${at}.facts[${j}]`, factIds));
46
+ const pack = {
47
+ id: o.id,
48
+ version: o.version,
49
+ ...(o.tenant !== undefined ? { tenant: o.tenant } : {}),
50
+ ...(o.title !== undefined ? { title: o.title } : {}),
51
+ facts,
52
+ };
53
+ return {
54
+ ...pack,
55
+ hash: `sha256:${createHash("sha256").update(canonical(pack)).digest("hex")}`,
56
+ };
57
+ });
58
+ }
59
+ function checkFact(f, at, ids) {
60
+ if (typeof f !== "object" || f === null || Array.isArray(f))
61
+ throw new Error(`${at} must be an object`);
62
+ const o = f;
63
+ if (typeof o.id !== "string" || !/^[A-Za-z0-9._-]{1,128}$/.test(o.id))
64
+ throw new Error(`${at}.id must be 1-128 of A-Z a-z 0-9 . _ -`);
65
+ if (ids.has(o.id))
66
+ throw new Error(`${at}.id ${o.id} is there twice`);
67
+ ids.add(o.id);
68
+ if (!Array.isArray(o.about) ||
69
+ o.about.length < 1 ||
70
+ o.about.length > 20 ||
71
+ !o.about.every((a) => str(a, 100)))
72
+ throw new Error(`${at}.about must be 1-20 terms of 1-100 characters`);
73
+ if (typeof o.kind !== "string" || !KINDS.has(o.kind))
74
+ throw new Error(`${at}.kind must be money, percent, quantity, date or text`);
75
+ for (const k of ["currency", "unit"])
76
+ if (o[k] !== undefined && !str(o[k], 32))
77
+ throw new Error(`${at}.${k} must be 1-32 characters`);
78
+ if (o.text !== undefined && !str(o.text, 2000))
79
+ throw new Error(`${at}.text must be 1-2,000 characters`);
80
+ if (o.source !== undefined && !str(o.source, 500))
81
+ throw new Error(`${at}.source must be 1-500 characters`);
82
+ let value;
83
+ if (o.kind === "text") {
84
+ if (o.text === undefined)
85
+ throw new Error(`${at}: a text fact needs text`);
86
+ }
87
+ else {
88
+ const v = typeof o.value === "number" ? String(o.value) : o.value;
89
+ if (typeof v !== "string")
90
+ throw new Error(`${at}.value is required`);
91
+ if (o.kind === "date") {
92
+ if (!DATE.test(v))
93
+ throw new Error(`${at}.value must be a date (YYYY-MM-DD)`);
94
+ value = normaliseValue(v);
95
+ }
96
+ else {
97
+ if (!NUMERIC.test(v))
98
+ throw new Error(`${at}.value must be a number`);
99
+ value = canonicalNumber(v);
100
+ }
101
+ }
102
+ return {
103
+ id: o.id,
104
+ about: o.about,
105
+ kind: o.kind,
106
+ ...(value !== undefined ? { value } : {}),
107
+ ...(o.currency !== undefined ? { currency: o.currency } : {}),
108
+ ...(o.unit !== undefined ? { unit: o.unit } : {}),
109
+ ...(o.text !== undefined ? { text: o.text } : {}),
110
+ ...(o.source !== undefined ? { source: o.source } : {}),
111
+ };
112
+ }
113
+ /** The packs a session's tenant sees: its own, and those for everyone. */
114
+ export function packsFor(packs, tenant) {
115
+ return packs.filter((p) => p.tenant === undefined || p.tenant === tenant);
116
+ }
117
+ const norm = (s) => western(s).toLowerCase();
118
+ /** The sentence around [at, end) in `text`. */
119
+ function sentenceAt(text, at, end) {
120
+ let a = at;
121
+ while (a > 0 && !".!?؟\n".includes(text[a - 1]))
122
+ a--;
123
+ let b = end;
124
+ // a decimal point between digits doesn't end a sentence
125
+ while (b < text.length &&
126
+ !(".!?؟\n".includes(text[b]) && !/[0-9]/.test(text[b + 1] ?? "")))
127
+ b++;
128
+ return text.slice(a, b);
129
+ }
130
+ /**
131
+ * §10: what the packs say about one fact of an answer. The pack facts that apply are those of the
132
+ * same kind with an `about` term in the fact's sentence. Grounded when one of them has its value;
133
+ * contradicted when some apply and none has it; null when none applies.
134
+ */
135
+ export function checkAgainstPacks(text, f, packs) {
136
+ if (f.kind !== "money" && f.kind !== "percent" && f.kind !== "quantity" && f.kind !== "date")
137
+ return null;
138
+ const sentence = norm(sentenceAt(text, f.at, f.end));
139
+ let first = null;
140
+ for (const pack of packs)
141
+ for (const fact of pack.facts) {
142
+ if (fact.kind !== f.kind || !fact.about.some((t) => sentence.includes(norm(t))))
143
+ continue;
144
+ if (fact.value === f.value)
145
+ return { status: "grounded", pack, fact };
146
+ first ??= { pack, fact };
147
+ }
148
+ return first ? { status: "contradicted", ...first } : null;
149
+ }
150
+ /** §10: the text facts an answer touches (an `about` term anywhere in it), for a grounding service. */
151
+ export function referenceContext(text, packs) {
152
+ const t = norm(text);
153
+ const out = [];
154
+ for (const pack of packs)
155
+ for (const fact of pack.facts)
156
+ if (fact.text !== undefined && fact.about.some((a) => t.includes(norm(a))) && out.length < 50)
157
+ out.push({
158
+ pack: pack.id,
159
+ id: fact.id,
160
+ text: fact.text,
161
+ ...(fact.source ? { source: fact.source } : {}),
162
+ });
163
+ return out;
164
+ }
@@ -52,6 +52,7 @@ export const TAXONOMY = {
52
52
  REVOKED_MEMORY_READ: { owasp: ["ASI06"], atlas: ["AML.T0080.000"] },
53
53
  STERILE_VIOLATION: { owasp: ["ASI02", "ASI10"], atlas: ["AML.T0053"] },
54
54
  TOKEN_MISUSE: { owasp: ["ASI03"], atlas: ["AML.T0091.000", "AML.T0086"] },
55
+ TOOL_CHANGED: { owasp: ["ASI04"], atlas: ["AML.T0110.002"] },
55
56
  TOOL_OUTPUT_MISMATCH: { owasp: ["ASI04", "ASI06"], atlas: ["AML.T0110.002", "AML.T0099"] },
56
57
  TOOL_WITNESS_INVALID: { owasp: ["ASI04"], atlas: ["AML.T0110.002"] },
57
58
  };
@@ -1,3 +1,7 @@
1
+ import { createConfirmation } from "@zanii/core";
2
+ /** spec/approvals.md §2: what an approval's arguments are, `sha256:<hex>` of their canonical JSON
3
+ * (keys sorted, no whitespace). The approver sees these; the run must use them. */
4
+ export declare function argsHash(args: unknown): string;
1
5
  export interface ApprovalFinding {
2
6
  code: "APPROVAL_REJECTED" | "APPROVAL_TIMEOUT";
3
7
  source: "approval";
@@ -9,3 +13,22 @@ export interface ApprovalFinding {
9
13
  }
10
14
  /** spec/approvals.md §3: a refused action (advisory), and one nobody confirmed in time (warning). */
11
15
  export declare function approvalFindings(lines: readonly string[]): ApprovalFinding[];
16
+ /**
17
+ * spec/approvals.md §8: a person's yes as a Zanii action_confirmation, signed with the gateway's
18
+ * identity: over the hash of the event approved (the held call, or the approval request), at the
19
+ * decision's time. Deterministic. `error` when there is no approve decision.
20
+ */
21
+ export declare function approvalConfirmation(lines: readonly string[], approvalId: string, gateway: {
22
+ did: string;
23
+ privateKey: Uint8Array;
24
+ }): {
25
+ confirmation: ReturnType<typeof createConfirmation>;
26
+ decision_seq: number;
27
+ action_seq: number;
28
+ by: unknown;
29
+ person?: unknown;
30
+ } | {
31
+ error: "not_found" | "not_approved";
32
+ };
33
+ /** spec/approvals.md §8: Zanii's check of an action_confirmation (the DID it names signed it). */
34
+ export declare const verifyConfirmation: (c: unknown) => boolean;
@@ -1,5 +1,16 @@
1
1
  // The two-person rule (spec/approvals.md): what the approval decisions say. Pure; mirrors
2
2
  // sdks/python/src/zanii_blackbox/approvals.py; pinned by spec/vectors/approvals.json.
3
+ import { createHash } from "node:crypto";
4
+ import { createConfirmation, verifyConfirmation as zaniiVerifyConfirmation } from "@zanii/core";
5
+ import { canonical } from "../reconcile/shared.js";
6
+ import { hashLine } from "../verify/envelope.js";
7
+ /** spec/approvals.md §2: what an approval's arguments are, `sha256:<hex>` of their canonical JSON
8
+ * (keys sorted, no whitespace). The approver sees these; the run must use them. */
9
+ export function argsHash(args) {
10
+ return `sha256:${createHash("sha256")
11
+ .update(canonical(args ?? null))
12
+ .digest("hex")}`;
13
+ }
3
14
  /** spec/approvals.md §3: a refused action (advisory), and one nobody confirmed in time (warning). */
4
15
  export function approvalFindings(lines) {
5
16
  const out = [];
@@ -25,3 +36,40 @@ export function approvalFindings(lines) {
25
36
  }
26
37
  return out;
27
38
  }
39
+ /**
40
+ * spec/approvals.md §8: a person's yes as a Zanii action_confirmation, signed with the gateway's
41
+ * identity: over the hash of the event approved (the held call, or the approval request), at the
42
+ * decision's time. Deterministic. `error` when there is no approve decision.
43
+ */
44
+ export function approvalConfirmation(lines, approvalId, gateway) {
45
+ const events = lines.map((l) => JSON.parse(l));
46
+ const decision = events.find((e) => e.kind === "control" && e.meta.action === "approval" && e.meta.approval_id === approvalId);
47
+ if (!decision)
48
+ return { error: "not_found" };
49
+ if (decision.meta.decision !== "approve")
50
+ return { error: "not_approved" };
51
+ const request = events.find((e) => e.kind === "control" &&
52
+ e.meta.action === "approval_request" &&
53
+ e.meta.approval_id === approvalId);
54
+ const actionSeq = typeof decision.meta.call_seq === "number" ? decision.meta.call_seq : (request?.seq ?? -1);
55
+ const line = lines[actionSeq];
56
+ if (line === undefined)
57
+ return { error: "not_found" };
58
+ const confirmation = createConfirmation({ ownerId: gateway.did, actionHash: hashLine(line), confirmedAt: decision.ts }, gateway.privateKey);
59
+ return {
60
+ confirmation,
61
+ decision_seq: decision.seq,
62
+ action_seq: actionSeq,
63
+ by: decision.meta.by,
64
+ ...(decision.meta.person !== undefined ? { person: decision.meta.person } : {}),
65
+ };
66
+ }
67
+ /** spec/approvals.md §8: Zanii's check of an action_confirmation (the DID it names signed it). */
68
+ export const verifyConfirmation = (c) => {
69
+ try {
70
+ return zaniiVerifyConfirmation(c);
71
+ }
72
+ catch {
73
+ return false;
74
+ }
75
+ };
@@ -0,0 +1,2 @@
1
+ /** The archived events (session lines, in archive order) as one Parquet file. */
2
+ export declare function archiveParquet(lines: readonly string[]): Uint8Array;
@@ -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
+ }
package/dist/bom/index.js CHANGED
@@ -37,6 +37,13 @@ export function checkAgent(value) {
37
37
  const props = (pairs) => pairs
38
38
  .filter((p) => typeof p[1] === "string" || typeof p[1] === "number")
39
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
+ }
40
47
  /** A UUID (version 8, RFC 9562) from the record's identity: the same record, the same serial. */
41
48
  function serialOf(sessionId, count, head) {
42
49
  const h = createHash("sha256").update(`${sessionId}\n${count}\n${head}`).digest();
@@ -119,8 +126,13 @@ export function sessionBom(lines) {
119
126
  properties: props([
120
127
  ["zanii:session_id", sid],
121
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)],
122
131
  ["zanii:on_behalf_of", agent.on_behalf_of],
123
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],
124
136
  ["zanii:record_head", hashLine(last)],
125
137
  ]),
126
138
  },
@@ -130,3 +142,11 @@ export function sessionBom(lines) {
130
142
  dependencies: [{ ref: "agent", dependsOn: refs }],
131
143
  };
132
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
+ }