@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,61 @@
1
+ type Obj = Record<string, unknown>;
2
+ type Commitments = {
3
+ min_actions?: number;
4
+ max_actions?: number;
5
+ max_gap_ms?: number;
6
+ };
7
+ /** Zanii's SLA terms. `commitments` holds at least one of min_actions, max_actions, max_gap_ms. */
8
+ export declare function buildSlaBody(o: {
9
+ provider: string;
10
+ client: string;
11
+ scope: string;
12
+ window: {
13
+ start: string;
14
+ end: string;
15
+ };
16
+ commitments: Commitments;
17
+ price?: {
18
+ amount: string;
19
+ currency: string;
20
+ };
21
+ createdAt: string;
22
+ ref?: string;
23
+ }): Obj;
24
+ /** One party signs the terms. Collect the provider's and the client's. */
25
+ export declare const signSla: (body: Obj, did: string, privateKey: Uint8Array) => {
26
+ did: string;
27
+ sig: string;
28
+ };
29
+ export declare const assembleSla: (body: Obj, signatures: Array<{
30
+ did: string;
31
+ sig: string;
32
+ }>) => {
33
+ body: Obj;
34
+ signatures: {
35
+ did: string;
36
+ sig: string;
37
+ }[];
38
+ };
39
+ export declare const slaHash: (sla: Obj) => string;
40
+ /** Zanii's check: sane terms, and BOTH provider and client signed exactly these. */
41
+ export declare function verifySla(sla: unknown): {
42
+ ok: boolean;
43
+ reasons: string[];
44
+ };
45
+ /**
46
+ * Zanii's verdict over the provider's consecutive receipt chain for the window. Breaches:
47
+ * INVALID_RECEIPT, INCOMPLETE_RECORD (a seam: something recorded was left out), TOO_FEW_ACTIONS,
48
+ * TOO_MANY_ACTIONS, MAX_GAP_EXCEEDED. `asOf` (default the window's end) caps the trailing gap.
49
+ */
50
+ export declare function assessCompliance(sla: Obj, receipts: readonly unknown[], asOf?: string): {
51
+ ok: boolean;
52
+ breaches: {
53
+ kind: string;
54
+ detail: string;
55
+ }[];
56
+ metrics: {
57
+ actions: number;
58
+ max_gap_ms: number | null;
59
+ };
60
+ };
61
+ export {};
@@ -0,0 +1,197 @@
1
+ // Service agreements proven from receipts (spec/anchoring.md §14): Zanii's SLA, co-signed terms
2
+ // whose compliance is computed from the provider's consecutive receipt chain, the same verdict for
3
+ // everyone (@zanii/sla). Pure; mirrors sdks/python/src/zanii_blackbox/sla.py; pinned by
4
+ // spec/vectors/sla.json.
5
+ import { canonicalBytes, jcsHash, publicKeyFromDid, receiptHash, scopeCovers, verifyReceipt, } from "@zanii/core";
6
+ import { parseMoney } from "../finance/index.js";
7
+ import { ed25519Sign, ed25519Verify } from "../transparency/index.js";
8
+ const ms = (ts) => {
9
+ if (typeof ts !== "string")
10
+ return null;
11
+ const t = Date.parse(ts);
12
+ return Number.isNaN(t) ? null : t;
13
+ };
14
+ const isCount = (v) => Number.isSafeInteger(v) && v >= 0;
15
+ /** Zanii's SLA terms. `commitments` holds at least one of min_actions, max_actions, max_gap_ms. */
16
+ export function buildSlaBody(o) {
17
+ if (!o.provider || !o.client)
18
+ throw new Error("provider and client are required");
19
+ if (o.provider === o.client)
20
+ throw new Error("provider and client must differ");
21
+ if (!o.scope)
22
+ throw new Error("scope is required (the target pattern the service receipts match)");
23
+ const start = ms(o.window.start);
24
+ const end = ms(o.window.end);
25
+ if (start === null || end === null)
26
+ throw new Error("window start/end must be ISO timestamps");
27
+ if (start >= end)
28
+ throw new Error("window start must be before end");
29
+ if (ms(o.createdAt) === null)
30
+ throw new Error("created_at must be an ISO timestamp");
31
+ const c = { ...o.commitments };
32
+ for (const [k, v] of Object.entries(c))
33
+ if (v !== undefined && v !== null && !isCount(v))
34
+ throw new Error(`commitment ${k} must be a non-negative integer`);
35
+ if (c.min_actions == null && c.max_actions == null && c.max_gap_ms == null)
36
+ throw new Error("an SLA with no commitments commits to nothing");
37
+ if (c.min_actions != null && c.max_actions != null && c.min_actions > c.max_actions)
38
+ throw new Error("min_actions must not exceed max_actions");
39
+ return {
40
+ v: 1,
41
+ type: "sla",
42
+ provider: o.provider,
43
+ client: o.client,
44
+ scope: o.scope,
45
+ window: { start: o.window.start, end: o.window.end },
46
+ commitments: c,
47
+ created_at: o.createdAt,
48
+ ...(o.price ? { price: parseMoney(o.price.amount, o.price.currency) } : {}),
49
+ ...(o.ref !== undefined ? { ref: o.ref } : {}),
50
+ };
51
+ }
52
+ /** One party signs the terms. Collect the provider's and the client's. */
53
+ export const signSla = (body, did, privateKey) => ({
54
+ did,
55
+ sig: `ed25519:${Buffer.from(ed25519Sign(privateKey, canonicalBytes(body))).toString("hex")}`,
56
+ });
57
+ export const assembleSla = (body, signatures) => ({
58
+ body,
59
+ signatures,
60
+ });
61
+ export const slaHash = (sla) => jcsHash(sla);
62
+ const sigValid = (body, did, sig) => {
63
+ const pub = publicKeyFromDid(did);
64
+ const m = typeof sig === "string" ? /^ed25519:([0-9a-f]{128})$/.exec(sig) : null;
65
+ return Boolean(pub && m && ed25519Verify(pub, canonicalBytes(body), Buffer.from(m[1], "hex")));
66
+ };
67
+ /** Zanii's check: sane terms, and BOTH provider and client signed exactly these. */
68
+ export function verifySla(sla) {
69
+ const reasons = [];
70
+ const e = (sla ?? {});
71
+ const b = e.body ?? {};
72
+ if (b.v !== 1 || b.type !== "sla")
73
+ reasons.push("not an sla (v1)");
74
+ const { provider, client } = b;
75
+ if (!provider || !client || provider === client)
76
+ reasons.push("missing/invalid parties");
77
+ if (!b.scope)
78
+ reasons.push("missing scope");
79
+ const w = (b.window ?? {});
80
+ const start = ms(w.start);
81
+ const end = ms(w.end);
82
+ if (start === null || end === null || start >= end)
83
+ reasons.push("invalid window");
84
+ const c = (b.commitments ?? {});
85
+ if (c.min_actions == null && c.max_actions == null && c.max_gap_ms == null)
86
+ reasons.push("no commitments");
87
+ if (reasons.length === 0)
88
+ for (const [party, role] of [
89
+ [provider, "provider"],
90
+ [client, "client"],
91
+ ]) {
92
+ const s = (e.signatures ?? []).find((x) => x.did === party);
93
+ if (!s)
94
+ reasons.push(`missing signature from ${role}`);
95
+ else if (!sigValid(b, String(s.did), s.sig))
96
+ reasons.push(`invalid signature from ${role}`);
97
+ }
98
+ return { ok: reasons.length === 0, reasons };
99
+ }
100
+ /**
101
+ * Zanii's verdict over the provider's consecutive receipt chain for the window. Breaches:
102
+ * INVALID_RECEIPT, INCOMPLETE_RECORD (a seam: something recorded was left out), TOO_FEW_ACTIONS,
103
+ * TOO_MANY_ACTIONS, MAX_GAP_EXCEEDED. `asOf` (default the window's end) caps the trailing gap.
104
+ */
105
+ export function assessCompliance(sla, receipts, asOf) {
106
+ const agreement = verifySla(sla);
107
+ if (!agreement.ok)
108
+ return {
109
+ ok: false,
110
+ breaches: agreement.reasons.map((r) => ({ kind: "INVALID_RECEIPT", detail: `sla: ${r}` })),
111
+ metrics: { actions: 0, max_gap_ms: null },
112
+ };
113
+ const b = sla.body;
114
+ const breaches = [];
115
+ const winStart = ms(b.window.start);
116
+ const winEnd = ms(b.window.end);
117
+ if (asOf !== undefined && ms(asOf) === null)
118
+ throw new Error("as_of must be an ISO timestamp");
119
+ const asOfMs = Math.min(asOf !== undefined ? ms(asOf) : winEnd, winEnd);
120
+ // a stable sort, as Python's sorted
121
+ const sorted = receipts
122
+ .map((r, i) => ({ r, i, t: ms(r.ts) ?? 0 }))
123
+ .sort((x, y) => x.t - y.t || x.i - y.i)
124
+ .map((x) => x.r);
125
+ for (const r of sorted) {
126
+ let check;
127
+ try {
128
+ check = verifyReceipt(r);
129
+ }
130
+ catch (error) {
131
+ check = { ok: false, error: error instanceof Error ? error.message : String(error) };
132
+ }
133
+ if (!check.ok)
134
+ breaches.push({
135
+ kind: "INVALID_RECEIPT",
136
+ detail: check.error || "invalid receipt",
137
+ receipt: receiptHash(r),
138
+ });
139
+ else if (r.agent_id !== b.provider)
140
+ breaches.push({
141
+ kind: "INVALID_RECEIPT",
142
+ detail: `receipt by ${r.agent_id}, not the provider`,
143
+ receipt: receiptHash(r),
144
+ });
145
+ }
146
+ for (let i = 1; i < sorted.length; i++)
147
+ if (sorted[i].prev !== receiptHash(sorted[i - 1]))
148
+ breaches.push({
149
+ kind: "INCOMPLETE_RECORD",
150
+ detail: `chain break between receipts #${i - 1} and #${i} — the slice is not the provider's consecutive record`,
151
+ receipt: receiptHash(sorted[i]),
152
+ });
153
+ const matching = sorted.filter((r) => {
154
+ const t = ms(r.ts);
155
+ return (t !== null && winStart <= t && t <= winEnd && scopeCovers(String(b.scope), r.target || ""));
156
+ });
157
+ const c = b.commitments;
158
+ if (c.min_actions != null && matching.length < c.min_actions)
159
+ breaches.push({
160
+ kind: "TOO_FEW_ACTIONS",
161
+ detail: `${matching.length} matching actions < committed minimum ${c.min_actions}`,
162
+ });
163
+ if (c.max_actions != null && matching.length > c.max_actions)
164
+ breaches.push({
165
+ kind: "TOO_MANY_ACTIONS",
166
+ detail: `${matching.length} matching actions > committed maximum ${c.max_actions}`,
167
+ });
168
+ let maxGap = null;
169
+ if (matching.length > 0) {
170
+ const times = matching.map((r) => ms(r.ts));
171
+ const gaps = [[times[0] - winStart, null]];
172
+ for (let i = 1; i < times.length; i++)
173
+ gaps.push([times[i] - times[i - 1], matching[i - 1]]);
174
+ const last = times[times.length - 1];
175
+ if (asOfMs > last)
176
+ gaps.push([asOfMs - last, matching[matching.length - 1]]);
177
+ maxGap = Math.max(...gaps.map(([g]) => g));
178
+ if (c.max_gap_ms != null)
179
+ for (const [g, after] of gaps)
180
+ if (g > c.max_gap_ms)
181
+ breaches.push({
182
+ kind: "MAX_GAP_EXCEEDED",
183
+ detail: `${g}ms of silence > committed max ${c.max_gap_ms}ms`,
184
+ ...(after ? { receipt: receiptHash(after) } : {}),
185
+ });
186
+ }
187
+ else if (c.max_gap_ms != null && asOfMs - winStart > c.max_gap_ms)
188
+ breaches.push({
189
+ kind: "MAX_GAP_EXCEEDED",
190
+ detail: `no matching actions for ${asOfMs - winStart}ms > committed max ${c.max_gap_ms}ms`,
191
+ });
192
+ return {
193
+ ok: breaches.length === 0,
194
+ breaches,
195
+ metrics: { actions: matching.length, max_gap_ms: maxGap },
196
+ };
197
+ }
@@ -0,0 +1,50 @@
1
+ export type SuccessionReason = "rotation" | "compromise" | "retirement";
2
+ export interface SuccessionBody {
3
+ v: 1;
4
+ type: "succession";
5
+ predecessor: string;
6
+ successor: string;
7
+ owner: string;
8
+ reason: SuccessionReason;
9
+ ts: string;
10
+ compromised_at?: string;
11
+ note?: string;
12
+ }
13
+ export interface Succession {
14
+ body: SuccessionBody;
15
+ signatures: Array<{
16
+ did: string;
17
+ sig: string;
18
+ }>;
19
+ }
20
+ export declare function buildSuccessionBody(input: {
21
+ predecessor: string;
22
+ successor: string;
23
+ owner: string;
24
+ reason: string;
25
+ ts: string;
26
+ compromised_at?: string;
27
+ note?: string;
28
+ }): SuccessionBody;
29
+ /** Signs as the owner (required) or the predecessor (optional: a planned handover). */
30
+ export declare const signSuccession: (body: SuccessionBody, did: string, seed: Uint8Array) => {
31
+ did: string;
32
+ sig: string;
33
+ };
34
+ export declare const assembleSuccession: (body: SuccessionBody, signatures: Succession["signatures"]) => Succession;
35
+ export declare const successionHash: (s: Succession) => string;
36
+ /** Structure sane and the owner signed; `cosigned` when the predecessor countersigned too. */
37
+ export declare function verifySuccession(s: unknown): {
38
+ ok: boolean;
39
+ reasons: string[];
40
+ cosigned: boolean;
41
+ };
42
+ /**
43
+ * Walks the successions back from `did`: every link verifies, links connect, one owner throughout
44
+ * (a change of owner is a sale, not a succession). `lineage` is oldest first.
45
+ */
46
+ export declare function verifyLineage(successions: unknown[], did: string): {
47
+ ok: boolean;
48
+ reasons: string[];
49
+ lineage: string[];
50
+ };
@@ -0,0 +1,123 @@
1
+ // Owner-signed succession of the gateway identity (spec/anchoring.md §7), in Zanii's format:
2
+ // byte-identical bodies, signatures and hashes to `@zanii/succession` / `zanii.succession`
3
+ // (vectors: spec/vectors/succession.json). The Python SDK re-exports zanii.succession.
4
+ //
5
+ // Succession proves continuity of authority (the owner says B follows A), not which code ran.
6
+ // After a compromise the predecessor's signature isn't asked for (the attacker holds that key), and
7
+ // `compromised_at` brackets the window its old receipts can be trusted in.
8
+ import { canonicalBytes, jcsHash, publicKeyFromDid } from "@zanii/core";
9
+ import { ed25519Sign, ed25519Verify } from "../transparency/index.js";
10
+ const REASONS = new Set(["rotation", "compromise", "retirement"]);
11
+ const validTs = (ts) => typeof ts === "string" && !Number.isNaN(Date.parse(ts));
12
+ export function buildSuccessionBody(input) {
13
+ const { predecessor, successor, owner, reason, ts, compromised_at, note } = input;
14
+ if (!predecessor || !successor || !owner)
15
+ throw new Error("predecessor, successor, and owner are required");
16
+ if (predecessor === successor)
17
+ throw new Error("an agent cannot succeed itself");
18
+ if (!REASONS.has(reason))
19
+ throw new Error(`invalid reason '${reason}'`);
20
+ if (!validTs(ts))
21
+ throw new Error("ts must be an ISO timestamp");
22
+ if (reason === "compromise") {
23
+ if (!validTs(compromised_at))
24
+ throw new Error("a 'compromise' succession requires compromised_at");
25
+ }
26
+ else if (compromised_at !== undefined)
27
+ throw new Error("compromised_at is only meaningful with reason 'compromise'");
28
+ return {
29
+ v: 1,
30
+ type: "succession",
31
+ predecessor,
32
+ successor,
33
+ owner,
34
+ reason: reason,
35
+ ts,
36
+ ...(compromised_at !== undefined ? { compromised_at } : {}),
37
+ ...(note !== undefined ? { note } : {}),
38
+ };
39
+ }
40
+ /** Signs as the owner (required) or the predecessor (optional: a planned handover). */
41
+ export const signSuccession = (body, did, seed) => ({
42
+ did,
43
+ sig: `ed25519:${Buffer.from(ed25519Sign(seed, canonicalBytes(body))).toString("hex")}`,
44
+ });
45
+ export const assembleSuccession = (body, signatures) => ({ body, signatures });
46
+ export const successionHash = (s) => jcsHash(s);
47
+ function sigValid(body, did, sig) {
48
+ const pub = publicKeyFromDid(did);
49
+ const m = typeof sig === "string" ? /^ed25519:([0-9a-f]{128})$/.exec(sig) : null;
50
+ if (!pub || !m)
51
+ return false;
52
+ return ed25519Verify(pub, canonicalBytes(body), Buffer.from(m[1], "hex"));
53
+ }
54
+ /** Structure sane and the owner signed; `cosigned` when the predecessor countersigned too. */
55
+ export function verifySuccession(s) {
56
+ const reasons = [];
57
+ const e = (s ?? {});
58
+ const b = (e.body ?? {});
59
+ if (b.v !== 1 || b.type !== "succession")
60
+ reasons.push("not a succession (v1)");
61
+ const { predecessor: pred, successor: succ, owner } = b;
62
+ if (!pred || !succ || !owner || pred === succ)
63
+ reasons.push("missing/invalid parties");
64
+ if (!REASONS.has(String(b.reason)))
65
+ reasons.push("invalid reason");
66
+ if (b.reason === "compromise" && !validTs(b.compromised_at))
67
+ reasons.push("a 'compromise' succession requires a valid compromised_at");
68
+ if (!validTs(b.ts))
69
+ reasons.push("missing/invalid ts");
70
+ let cosigned = false;
71
+ if (reasons.length === 0) {
72
+ const sigs = Array.isArray(e.signatures) ? e.signatures : [];
73
+ const ownerSig = sigs.find((x) => x?.did === owner);
74
+ if (!ownerSig || !sigValid(b, ownerSig.did, ownerSig.sig))
75
+ reasons.push("missing/invalid OWNER signature");
76
+ const predSig = sigs.find((x) => x?.did === pred);
77
+ cosigned = predSig !== undefined && sigValid(b, predSig.did, predSig.sig);
78
+ }
79
+ return { ok: reasons.length === 0, reasons, cosigned };
80
+ }
81
+ /**
82
+ * Walks the successions back from `did`: every link verifies, links connect, one owner throughout
83
+ * (a change of owner is a sale, not a succession). `lineage` is oldest first.
84
+ */
85
+ export function verifyLineage(successions, did) {
86
+ if (successions.length === 0)
87
+ return { ok: true, reasons: [], lineage: [did] };
88
+ const bySuccessor = new Map();
89
+ for (const s of successions) {
90
+ const check = verifySuccession(s);
91
+ if (!check.ok) {
92
+ const label = s?.body?.successor ?? "?";
93
+ return { ok: false, reasons: check.reasons.map((r) => `link ${label}: ${r}`), lineage: [] };
94
+ }
95
+ const link = s;
96
+ if (bySuccessor.has(link.body.successor))
97
+ return {
98
+ ok: false,
99
+ reasons: [`two successions claim successor ${link.body.successor}`],
100
+ lineage: [],
101
+ };
102
+ bySuccessor.set(link.body.successor, link);
103
+ }
104
+ const links = successions;
105
+ const owner = links[0]?.body.owner;
106
+ if (!links.every((s) => s.body.owner === owner))
107
+ return { ok: false, reasons: ["lineage spans multiple owners"], lineage: [] };
108
+ const lineage = [did];
109
+ const seen = new Set([did]);
110
+ let cursor = did;
111
+ for (let link = bySuccessor.get(cursor); link; link = bySuccessor.get(cursor)) {
112
+ const prev = link.body.predecessor;
113
+ if (seen.has(prev))
114
+ return { ok: false, reasons: ["succession cycle detected"], lineage: [] };
115
+ seen.add(prev);
116
+ lineage.unshift(prev);
117
+ cursor = prev;
118
+ }
119
+ const reasons = lineage.length === links.length + 1
120
+ ? []
121
+ : [`${links.length + 1 - lineage.length} succession(s) do not connect to ${did}'s lineage`];
122
+ return { ok: reasons.length === 0, reasons, lineage };
123
+ }
@@ -0,0 +1,6 @@
1
+ /** The token for one value: `<kind_tok:base64url(nonce ‖ AES-256-GCM(value, aad=kind))>`. */
2
+ export declare function tokenFor(kind: string, value: string, key: Uint8Array): string;
3
+ /** A token's value, or null when it isn't one, or isn't this key's. */
4
+ export declare function resolveToken(token: string, key: Uint8Array): string | null;
5
+ /** Every token in `text` this key resolves, put back; any other is left as it is. */
6
+ export declare function resolveTokens(text: string, key: Uint8Array): string;
@@ -0,0 +1,46 @@
1
+ // Keyed redaction tokens (spec/data.md §12, L9): a redacted value as `<kind_tok:…>`, AES-256-GCM
2
+ // under the deployment's token key, so a tool that holds the key resolves it and nothing in between
3
+ // (the model, the gateway's memory) has the value. Deterministic: the nonce is an HMAC of the kind
4
+ // and value, so the same value always reads the same. Mirrors sdks/python/src/zanii_blackbox/
5
+ // tokens.py; pinned by spec/vectors/tokens.json.
6
+ import { createCipheriv, createDecipheriv, createHmac } from "node:crypto";
7
+ const TOKEN = /<([a-z_]{1,32})_tok:([A-Za-z0-9_-]{24,4096})>/g;
8
+ const check = (key) => {
9
+ if (key.length !== 32)
10
+ throw new Error("a token key is 32 bytes");
11
+ };
12
+ /** The token for one value: `<kind_tok:base64url(nonce ‖ AES-256-GCM(value, aad=kind))>`. */
13
+ export function tokenFor(kind, value, key) {
14
+ check(key);
15
+ if (!/^[a-z_]{1,32}$/.test(kind))
16
+ throw new Error("kind must be 1-32 of a-z _");
17
+ const nonce = createHmac("sha256", key).update(`${kind}\0${value}`).digest().subarray(0, 12);
18
+ const cipher = createCipheriv("aes-256-gcm", key, nonce);
19
+ cipher.setAAD(Buffer.from(kind));
20
+ const sealed = Buffer.concat([cipher.update(value, "utf8"), cipher.final(), cipher.getAuthTag()]);
21
+ return `<${kind}_tok:${Buffer.concat([nonce, sealed]).toString("base64url")}>`;
22
+ }
23
+ /** A token's value, or null when it isn't one, or isn't this key's. */
24
+ export function resolveToken(token, key) {
25
+ check(key);
26
+ const m = new RegExp(`^${TOKEN.source}$`).exec(token);
27
+ if (!m)
28
+ return null;
29
+ try {
30
+ const raw = Buffer.from(m[2], "base64url");
31
+ if (raw.length < 12 + 16)
32
+ return null;
33
+ const d = createDecipheriv("aes-256-gcm", key, raw.subarray(0, 12));
34
+ d.setAAD(Buffer.from(m[1]));
35
+ d.setAuthTag(raw.subarray(raw.length - 16));
36
+ return Buffer.concat([d.update(raw.subarray(12, raw.length - 16)), d.final()]).toString("utf8");
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ /** Every token in `text` this key resolves, put back; any other is left as it is. */
43
+ export function resolveTokens(text, key) {
44
+ check(key);
45
+ return text.replace(TOKEN, (t) => resolveToken(t, key) ?? t);
46
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.3.0";
1
+ export declare const VERSION = "0.4.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // The SDK version, on its own so the CLI can print it without loading the whole SDK.
2
- export const VERSION = "0.3.0";
2
+ export const VERSION = "0.4.0";
@@ -0,0 +1,31 @@
1
+ export interface Wall {
2
+ id: string;
3
+ regulator: string;
4
+ scopes: string[];
5
+ denies: string[];
6
+ forbidden: string[];
7
+ note: string;
8
+ }
9
+ export declare const WALLS: Readonly<Record<string, Wall>>;
10
+ /** Zanii's wall manifest hash: the id and its sorted scopes and denies (order-independent). */
11
+ export declare const wallManifestHash: (w: Wall) => string;
12
+ /** Zanii's heuristic: an output crosses the wall when it holds a forbidden phrase. */
13
+ export declare function checkOutput(w: Wall, text: string): {
14
+ ok: boolean;
15
+ violations: string[];
16
+ };
17
+ /**
18
+ * spec/governance.md §5: WALL_CROSSED for each model response that holds a phrase one of the
19
+ * walls forbids; one finding per response and wall, naming the first phrase.
20
+ */
21
+ export declare function wallFindings(lines: readonly string[], bodies: (hash: string) => Uint8Array | undefined, wallIds: readonly string[]): {
22
+ code: string;
23
+ source: string;
24
+ severity: "warning";
25
+ ref: {
26
+ seq: number;
27
+ wall: string;
28
+ phrase: string;
29
+ };
30
+ detail: string;
31
+ }[];
@@ -0,0 +1,119 @@
1
+ // Zanii's UAE vertical walls (spec/governance.md §5): the table from spec/walls/v1.json, embedded
2
+ // (a test checks it matches), and the output check. Mirrors sdks/python/src/zanii_blackbox/walls.py.
3
+ import { jcsHash } from "@zanii/core";
4
+ export const WALLS = {
5
+ "sca-trading": {
6
+ id: "sca-trading",
7
+ regulator: "SCA",
8
+ scopes: ["trading.education.*", "market.read.*"],
9
+ denies: ["trading.execute.*", "trading.advise.*", "trading.recommend.*"],
10
+ forbidden: [
11
+ "buy this",
12
+ "sell this",
13
+ "you should buy",
14
+ "you should sell",
15
+ "i recommend buying",
16
+ "i recommend selling",
17
+ "guaranteed return",
18
+ "guaranteed profit",
19
+ "this will go up",
20
+ "will moon",
21
+ ],
22
+ note: 'education only; NEVER "buy this" — investment advice/execution is a licensed SCA activity',
23
+ },
24
+ "rera-realty": {
25
+ id: "rera-realty",
26
+ regulator: "RERA/DLD",
27
+ scopes: ["realty.inform.*", "realty.read.*"],
28
+ denies: ["realty.broker.*", "realty.list.*"],
29
+ forbidden: ["i can broker", "i will list", "sign the deal"],
30
+ note: "informs; never brokers or lists unpermitted properties",
31
+ },
32
+ "tdra-messaging": {
33
+ id: "tdra-messaging",
34
+ regulator: "TDRA",
35
+ scopes: ["messaging.send.optin.*"],
36
+ denies: ["messaging.send.bulk.*", "messaging.send.nonoptin.*"],
37
+ forbidden: [],
38
+ note: "opt-in only; must announce it is a bot",
39
+ },
40
+ "legal-drafting": {
41
+ id: "legal-drafting",
42
+ regulator: "Legal Affairs",
43
+ scopes: ["legal.draft.*", "legal.read.*"],
44
+ denies: ["legal.opine.*", "legal.advise.*"],
45
+ forbidden: ["my legal opinion is", "you should sue", "this is legally binding advice"],
46
+ note: "drafts; never gives legal opinions",
47
+ },
48
+ "consumer-due": {
49
+ id: "consumer-due",
50
+ regulator: "Consumer Protection / Central Bank",
51
+ scopes: ["collections.remind.*"],
52
+ denies: ["collections.threaten.*"],
53
+ forbidden: ["we will sue you", "legal action today", "you will be arrested"],
54
+ note: "reminds; never threatens; frequency limits",
55
+ },
56
+ "difc-dp": {
57
+ id: "difc-dp",
58
+ regulator: "DIFC (DP Law 2020)",
59
+ scopes: ["data.process.difc.*"],
60
+ denies: ["data.export.nonadequate.*"],
61
+ forbidden: [],
62
+ note: "DIFC free-zone data protection; select per deployment (not federal PDPL)",
63
+ },
64
+ "adgm-dp": {
65
+ id: "adgm-dp",
66
+ regulator: "ADGM (DP Regs 2021)",
67
+ scopes: ["data.process.adgm.*"],
68
+ denies: ["data.export.nonadequate.*"],
69
+ forbidden: [],
70
+ note: "ADGM free-zone data protection; select per deployment (not federal PDPL)",
71
+ },
72
+ };
73
+ /** Zanii's wall manifest hash: the id and its sorted scopes and denies (order-independent). */
74
+ export const wallManifestHash = (w) => jcsHash({ id: w.id, scopes: [...w.scopes].sort(), denies: [...w.denies].sort() });
75
+ /** Zanii's heuristic: an output crosses the wall when it holds a forbidden phrase. */
76
+ export function checkOutput(w, text) {
77
+ const lower = text.toLowerCase();
78
+ const violations = w.forbidden.filter((p) => lower.includes(p));
79
+ return { ok: violations.length === 0, violations };
80
+ }
81
+ const decoder = new TextDecoder();
82
+ /**
83
+ * spec/governance.md §5: WALL_CROSSED for each model response that holds a phrase one of the
84
+ * walls forbids; one finding per response and wall, naming the first phrase.
85
+ */
86
+ export function wallFindings(lines, bodies, wallIds) {
87
+ const walls = wallIds.map((id) => WALLS[id]).filter((w) => w !== undefined);
88
+ const out = [];
89
+ if (walls.length === 0)
90
+ return out;
91
+ // a response's bytes are its chunks' bodies, joined in order (spec/api.md: llm.chunk)
92
+ const chunks = new Map();
93
+ for (const l of lines) {
94
+ const e = JSON.parse(l);
95
+ if (e.kind === "llm.chunk") {
96
+ const body = bodies(e.body_hash);
97
+ const list = chunks.get(e.meta.request_seq) ?? [];
98
+ list.push(body ? decoder.decode(body) : "");
99
+ chunks.set(e.meta.request_seq, list);
100
+ }
101
+ if (e.kind !== "llm.response")
102
+ continue;
103
+ const text = (chunks.get(e.meta.request_seq) ?? []).join("");
104
+ if (!text)
105
+ continue;
106
+ for (const w of walls) {
107
+ const check = checkOutput(w, text);
108
+ if (!check.ok)
109
+ out.push({
110
+ code: "WALL_CROSSED",
111
+ source: "detectors",
112
+ severity: "warning",
113
+ ref: { seq: e.seq, wall: w.id, phrase: check.violations[0] },
114
+ detail: `The model's answer crossed the ${w.regulator} wall: ${w.note}`,
115
+ });
116
+ }
117
+ }
118
+ return out;
119
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zanii/blackbox",
3
3
  "license": "Apache-2.0",
4
- "version": "0.3.0",
4
+ "version": "0.4.0",
5
5
  "description": "The flight recorder for AI agents: sessions, a zero-loss spool, framework hooks, approvals, and offline verification of the gateway's hash-chained record.",
6
6
  "keywords": [
7
7
  "ai-agents",