@zanii/blackbox 0.0.0-stage → 0.2.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 (135) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +109 -2
  3. package/dist/agents/index.d.ts +34 -0
  4. package/dist/agents/index.js +73 -0
  5. package/dist/analysis/detectors.d.ts +36 -0
  6. package/dist/analysis/detectors.js +339 -0
  7. package/dist/analysis/faults.d.ts +9 -0
  8. package/dist/analysis/faults.js +250 -0
  9. package/dist/analysis/index.d.ts +68 -0
  10. package/dist/analysis/index.js +388 -0
  11. package/dist/analysis/landing.d.ts +25 -0
  12. package/dist/analysis/landing.js +225 -0
  13. package/dist/analysis/memory.d.ts +13 -0
  14. package/dist/analysis/memory.js +33 -0
  15. package/dist/analysis/waste.d.ts +29 -0
  16. package/dist/analysis/waste.js +79 -0
  17. package/dist/approvals/index.d.ts +11 -0
  18. package/dist/approvals/index.js +27 -0
  19. package/dist/approvals/warnings.d.ts +2 -0
  20. package/dist/approvals/warnings.js +28 -0
  21. package/dist/attest/index.d.ts +17 -0
  22. package/dist/attest/index.js +106 -0
  23. package/dist/authority/index.d.ts +24 -0
  24. package/dist/authority/index.js +77 -0
  25. package/dist/billing/index.d.ts +99 -0
  26. package/dist/billing/index.js +174 -0
  27. package/dist/cli.d.ts +2 -0
  28. package/dist/cli.js +1057 -0
  29. package/dist/client/index.d.ts +146 -0
  30. package/dist/client/index.js +210 -0
  31. package/dist/compliance/index.d.ts +41 -0
  32. package/dist/compliance/index.js +96 -0
  33. package/dist/cost/index.d.ts +133 -0
  34. package/dist/cost/index.js +293 -0
  35. package/dist/data/index.d.ts +191 -0
  36. package/dist/data/index.js +762 -0
  37. package/dist/directives/index.d.ts +35 -0
  38. package/dist/directives/index.js +80 -0
  39. package/dist/drills/index.d.ts +43 -0
  40. package/dist/drills/index.js +101 -0
  41. package/dist/duty/index.d.ts +21 -0
  42. package/dist/duty/index.js +68 -0
  43. package/dist/fleet/index.d.ts +141 -0
  44. package/dist/fleet/index.js +454 -0
  45. package/dist/hooks/ai-sdk.d.ts +42 -0
  46. package/dist/hooks/ai-sdk.js +62 -0
  47. package/dist/hooks/claude-agent-sdk.d.ts +14 -0
  48. package/dist/hooks/claude-agent-sdk.js +70 -0
  49. package/dist/hooks/index.d.ts +7 -0
  50. package/dist/hooks/index.js +10 -0
  51. package/dist/hooks/langchain-agent.d.ts +69 -0
  52. package/dist/hooks/langchain-agent.js +163 -0
  53. package/dist/hooks/langchain.d.ts +41 -0
  54. package/dist/hooks/langchain.js +216 -0
  55. package/dist/hooks/langgraph-checkpoint.d.ts +12 -0
  56. package/dist/hooks/langgraph-checkpoint.js +73 -0
  57. package/dist/hooks/memory.d.ts +17 -0
  58. package/dist/hooks/memory.js +64 -0
  59. package/dist/hooks/openai-agents.d.ts +6 -0
  60. package/dist/hooks/openai-agents.js +40 -0
  61. package/dist/hooks/protect.d.ts +7 -0
  62. package/dist/hooks/protect.js +39 -0
  63. package/dist/hooks/providers.d.ts +16 -0
  64. package/dist/hooks/providers.js +149 -0
  65. package/dist/hooks/shared.d.ts +11 -0
  66. package/dist/hooks/shared.js +39 -0
  67. package/dist/index.d.ts +46 -0
  68. package/dist/index.js +48 -0
  69. package/dist/investigate/index.d.ts +66 -0
  70. package/dist/investigate/index.js +119 -0
  71. package/dist/mcp-server/index.d.ts +85 -0
  72. package/dist/mcp-server/index.js +216 -0
  73. package/dist/mcp-wrap/index.d.ts +17 -0
  74. package/dist/mcp-wrap/index.js +170 -0
  75. package/dist/money/index.d.ts +114 -0
  76. package/dist/money/index.js +622 -0
  77. package/dist/occurrence/index.d.ts +108 -0
  78. package/dist/occurrence/index.js +168 -0
  79. package/dist/ocsf/index.d.ts +22 -0
  80. package/dist/ocsf/index.js +168 -0
  81. package/dist/otlp/index.d.ts +24 -0
  82. package/dist/otlp/index.js +143 -0
  83. package/dist/packs/index.d.ts +48 -0
  84. package/dist/packs/index.js +343 -0
  85. package/dist/policy/delta.d.ts +11 -0
  86. package/dist/policy/delta.js +39 -0
  87. package/dist/policy/drafts.d.ts +34 -0
  88. package/dist/policy/drafts.js +129 -0
  89. package/dist/policy/index.d.ts +47 -0
  90. package/dist/policy/index.js +154 -0
  91. package/dist/precog/index.d.ts +96 -0
  92. package/dist/precog/index.js +167 -0
  93. package/dist/precog/intervention.d.ts +22 -0
  94. package/dist/precog/intervention.js +44 -0
  95. package/dist/precog/normal.d.ts +31 -0
  96. package/dist/precog/normal.js +89 -0
  97. package/dist/preflight/index.d.ts +11 -0
  98. package/dist/preflight/index.js +19 -0
  99. package/dist/ratings/index.d.ts +21 -0
  100. package/dist/ratings/index.js +48 -0
  101. package/dist/reconcile/claude-code.d.ts +19 -0
  102. package/dist/reconcile/claude-code.js +220 -0
  103. package/dist/reconcile/codex.d.ts +5 -0
  104. package/dist/reconcile/codex.js +191 -0
  105. package/dist/reconcile/index.d.ts +19 -0
  106. package/dist/reconcile/index.js +50 -0
  107. package/dist/reconcile/record.d.ts +49 -0
  108. package/dist/reconcile/record.js +225 -0
  109. package/dist/reconcile/shared.d.ts +65 -0
  110. package/dist/reconcile/shared.js +113 -0
  111. package/dist/replay/index.d.ts +11 -0
  112. package/dist/replay/index.js +64 -0
  113. package/dist/replay/repair.d.ts +10 -0
  114. package/dist/replay/repair.js +62 -0
  115. package/dist/session/drain.d.ts +13 -0
  116. package/dist/session/drain.js +35 -0
  117. package/dist/session/index.d.ts +275 -0
  118. package/dist/session/index.js +681 -0
  119. package/dist/undo/index.d.ts +45 -0
  120. package/dist/undo/index.js +212 -0
  121. package/dist/verify/anchor.d.ts +54 -0
  122. package/dist/verify/anchor.js +77 -0
  123. package/dist/verify/chain.d.ts +27 -0
  124. package/dist/verify/chain.js +105 -0
  125. package/dist/verify/envelope.d.ts +28 -0
  126. package/dist/verify/envelope.js +55 -0
  127. package/dist/verify/index.d.ts +3 -0
  128. package/dist/verify/index.js +3 -0
  129. package/dist/version.d.ts +1 -0
  130. package/dist/version.js +2 -0
  131. package/dist/weather/index.d.ts +24 -0
  132. package/dist/weather/index.js +45 -0
  133. package/dist/workspace-receipt/index.d.ts +15 -0
  134. package/dist/workspace-receipt/index.js +121 -0
  135. package/package.json +56 -3
@@ -0,0 +1,48 @@
1
+ // Type ratings (spec/type-ratings.md): airworthiness per label and model, and flying unrated.
2
+ // Pure; mirrors sdks/python/src/zanii_blackbox/ratings.py; pinned by spec/vectors/type-ratings.json.
3
+ import { sessionCost } from "../cost/index.js";
4
+ import { airworthiness, DEFAULT_AIRWORTHINESS, } from "../fleet/index.js";
5
+ /** spec/type-ratings.md §1. `items` newest first. */
6
+ export function typeRatings(items, criteria = DEFAULT_AIRWORTHINESS) {
7
+ const pairs = new Map();
8
+ for (const i of items)
9
+ if (i.summary.closed)
10
+ for (const model of new Set(i.models)) {
11
+ const label = i.summary.label ?? "(no label)";
12
+ pairs.set(JSON.stringify([label, model]), { label, model });
13
+ }
14
+ const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
15
+ return [...pairs.values()]
16
+ .sort((a, b) => cmp(a.label, b.label) || cmp(a.model, b.model))
17
+ .map(({ label, model }) => ({
18
+ ...airworthiness(label, items.filter((i) => i.models.includes(model)).map((i) => i.summary), criteria),
19
+ model,
20
+ }));
21
+ }
22
+ /** spec/type-ratings.md §2. `severity` is "warning" when the server requires type ratings. */
23
+ export function typeUnrated(lines, prices, ratings, severity = "advisory") {
24
+ const open = lines[0]
25
+ ? JSON.parse(lines[0])
26
+ : null;
27
+ if (open?.kind !== "session.open")
28
+ return [];
29
+ const label = typeof open.meta.label === "string" ? open.meta.label : "(no label)";
30
+ const mine = ratings.filter((r) => r.label === label);
31
+ if (!mine.some((r) => r.verdict === "airworthy"))
32
+ return [];
33
+ const out = [];
34
+ const seen = new Set();
35
+ for (const c of sessionCost(lines, prices).calls) {
36
+ if (!c.model || seen.has(c.model))
37
+ continue;
38
+ seen.add(c.model);
39
+ if (!mine.some((r) => r.model === c.model && r.verdict === "airworthy"))
40
+ out.push({
41
+ code: "TYPE_UNRATED",
42
+ source: "type_rating",
43
+ severity,
44
+ ref: { seq: c.seq, model: c.model },
45
+ });
46
+ }
47
+ return out;
48
+ }
@@ -0,0 +1,19 @@
1
+ import type { Call, Receipt } from "./record.ts";
2
+ import { type Finding, type LocalLog } from "./shared.ts";
3
+ export declare const CLAUDE_CODE_TYPES: ReadonlySet<string>;
4
+ export interface Outcome {
5
+ findings: Finding[];
6
+ expected: Call[];
7
+ matched: {
8
+ calls: number;
9
+ tool_results: number;
10
+ };
11
+ notes: string[];
12
+ agentSessions: string[];
13
+ /** When receipts are required (§7). */
14
+ workspace?: {
15
+ receipts: number;
16
+ changed_by: string[];
17
+ };
18
+ }
19
+ export declare function reconcileClaudeCode(calls: Call[], log: LocalLog, agentSessions: string[], allReceipts?: Receipt[], requireReceipts?: boolean): Outcome;
@@ -0,0 +1,220 @@
1
+ // Claude Code transcripts vs the record (spec/reconcile.md §3). Mirrors reconcile/claude_code.py.
2
+ import { canonical, isObj, normText } from "./shared.js";
3
+ const KNOWN = new Set([
4
+ "user",
5
+ "assistant",
6
+ "system",
7
+ "attachment",
8
+ "summary",
9
+ "queue-operation",
10
+ "last-prompt",
11
+ "mode",
12
+ "permission-mode",
13
+ "ai-title",
14
+ "custom-title",
15
+ "atis-latch",
16
+ "cost-state",
17
+ "file-history-snapshot",
18
+ "file-history-delta",
19
+ ]);
20
+ export const CLAUDE_CODE_TYPES = KNOWN;
21
+ export function reconcileClaudeCode(calls, log, agentSessions, allReceipts = [], requireReceipts = false) {
22
+ const sessions = agentSessions.length > 0 ? agentSessions : localSessions(log);
23
+ const findings = [];
24
+ const notes = [];
25
+ const matched = { calls: 0, tool_results: 0 };
26
+ // The local side: assistant blocks by message id, tool results by tool_use_id.
27
+ const messages = new Map();
28
+ const toolResults = new Map();
29
+ for (const r of log.records) {
30
+ const message = isObj(r.json.message) ? r.json.message : null;
31
+ const content = message && Array.isArray(message.content) ? message.content.filter(isObj) : [];
32
+ if (r.json.type === "assistant" && message && typeof message.id === "string") {
33
+ if (message.model === "<synthetic>")
34
+ continue;
35
+ let m = messages.get(message.id);
36
+ if (!m) {
37
+ m = { blocks: new Map(), file: r.file, line: r.line };
38
+ messages.set(message.id, m);
39
+ }
40
+ let index = typeof r.json.apiBlockIndex === "number" ? r.json.apiBlockIndex : m.blocks.size;
41
+ for (const block of content)
42
+ m.blocks.set(index++, block);
43
+ }
44
+ else if (r.json.type === "user") {
45
+ for (const block of content)
46
+ if (block.type === "tool_result" &&
47
+ typeof block.tool_use_id === "string" &&
48
+ !toolResults.has(block.tool_use_id))
49
+ toolResults.set(block.tool_use_id, block);
50
+ }
51
+ }
52
+ const expected = calls.filter((c) => {
53
+ if (!c.complete || c.status !== 200 || !c.request)
54
+ return false;
55
+ const tools = c.request.tools;
56
+ if (!Array.isArray(tools) || tools.length === 0)
57
+ return false;
58
+ return sessions.length === 0 || sessions.includes(c.headers["x-claude-code-session-id"] ?? "");
59
+ });
60
+ let redacted = 0;
61
+ // Steps 1–2: every expected response is in the transcript, block for block.
62
+ for (const call of [...expected].sort((a, b) => a.endSeq - b.endSeq)) {
63
+ if (call.redacted)
64
+ redacted++;
65
+ const id = call.messageId;
66
+ if (!id) {
67
+ notes.push(`call at seq ${call.requestSeq} has no message id`);
68
+ continue;
69
+ }
70
+ const local = messages.get(id);
71
+ if (!local) {
72
+ findings.push({ code: "MISSING_LOCAL", ref: { message_id: id }, gateway_seq: call.endSeq });
73
+ continue;
74
+ }
75
+ let clean = true;
76
+ for (const [i, block] of [...call.blocks].sort(([a], [b]) => a - b)) {
77
+ if (block.type !== "text" && block.type !== "thinking" && block.type !== "tool_use")
78
+ continue;
79
+ const mine = local.blocks.get(i);
80
+ if (!mine) {
81
+ if (block.type !== "thinking") {
82
+ findings.push({
83
+ code: "MISSING_LOCAL",
84
+ ref: { message_id: id, block: i },
85
+ gateway_seq: call.endSeq,
86
+ });
87
+ clean = false;
88
+ }
89
+ continue;
90
+ }
91
+ if (mine.type !== block.type || (!call.redacted && !sameBlock(block, mine))) {
92
+ findings.push({
93
+ code: "ALTERED",
94
+ ref: { message_id: id, block: i },
95
+ gateway_seq: call.endSeq,
96
+ });
97
+ clean = false;
98
+ }
99
+ }
100
+ if (clean)
101
+ matched.calls++;
102
+ }
103
+ // Steps 3–4: every tool result the model was given, from the first request that carried it.
104
+ const given = new Map();
105
+ for (const call of [...expected].sort((a, b) => a.requestSeq - b.requestSeq)) {
106
+ const msgs = Array.isArray(call.request?.messages) ? call.request.messages.filter(isObj) : [];
107
+ for (const m of msgs) {
108
+ if (!Array.isArray(m.content))
109
+ continue;
110
+ for (const block of m.content.filter(isObj))
111
+ if (block.type === "tool_result" &&
112
+ typeof block.tool_use_id === "string" &&
113
+ !given.has(block.tool_use_id))
114
+ given.set(block.tool_use_id, { block, call });
115
+ }
116
+ }
117
+ for (const [toolUseId, { block, call }] of given) {
118
+ const mine = toolResults.get(toolUseId);
119
+ if (!mine) {
120
+ findings.push({
121
+ code: "MISSING_LOCAL",
122
+ ref: { tool_use_id: toolUseId },
123
+ gateway_seq: call.requestSeq,
124
+ });
125
+ continue;
126
+ }
127
+ if (call.requestRedacted) {
128
+ matched.tool_results++;
129
+ continue;
130
+ }
131
+ const recorded = normText(block.content);
132
+ const same = (recorded === normText(mine.content) || log.offloaded.has(recorded)) &&
133
+ Boolean(block.is_error) === Boolean(mine.is_error);
134
+ if (same)
135
+ matched.tool_results++;
136
+ else
137
+ findings.push({
138
+ code: "ALTERED",
139
+ ref: { tool_use_id: toolUseId },
140
+ gateway_seq: call.requestSeq,
141
+ });
142
+ }
143
+ // Step 5: model output the record never saw.
144
+ const seen = new Set(calls.map((c) => c.messageId).filter((m) => m !== null));
145
+ for (const [id, m] of messages)
146
+ if (!seen.has(id))
147
+ findings.push({
148
+ code: "EXTRA_LOCAL",
149
+ ref: { message_id: id },
150
+ local: { file: m.file, line: m.line },
151
+ });
152
+ if (redacted > 0)
153
+ notes.push(`redacted: ${redacted} call(s) not compared for content`);
154
+ // Steps 6–7: workspace receipts (§7).
155
+ const receipts = allReceipts.filter((r) => sessions.length === 0 || (r.agentSession !== null && sessions.includes(r.agentSession)));
156
+ if (!requireReceipts && receipts.length === 0)
157
+ return { findings, expected, matched, notes, agentSessions: sessions };
158
+ const has = (id, phase) => receipts.some((r) => r.toolUseId === id && r.phase === phase);
159
+ for (const call of [...expected].sort((a, b) => a.endSeq - b.endSeq))
160
+ for (const [, block] of [...call.blocks].sort(([a], [b]) => a - b)) {
161
+ if (block.type !== "tool_use" || typeof block.id !== "string")
162
+ continue;
163
+ if (!has(block.id, "pre"))
164
+ findings.push({
165
+ code: "RECEIPT_MISSING",
166
+ ref: { tool_use_id: block.id, phase: "pre" },
167
+ gateway_seq: call.endSeq,
168
+ });
169
+ const result = given.get(block.id);
170
+ if (result && !result.block.is_error && !has(block.id, "post"))
171
+ findings.push({
172
+ code: "RECEIPT_MISSING",
173
+ ref: { tool_use_id: block.id, phase: "post" },
174
+ gateway_seq: call.endSeq,
175
+ });
176
+ }
177
+ const withTree = receipts.filter((r) => r.tree !== null);
178
+ withTree.forEach((b, i) => {
179
+ const a = withTree[i - 1];
180
+ if (a && a.phase === "post" && b.phase === "pre" && a.tree !== b.tree)
181
+ findings.push({
182
+ code: "UNATTRIBUTED_CHANGE",
183
+ ref: { after: a.toolUseId, before: b.toolUseId },
184
+ gateway_seq: b.seq,
185
+ });
186
+ });
187
+ const changedBy = [];
188
+ for (const post of withTree.filter((r) => r.phase === "post")) {
189
+ const pre = withTree.find((r) => r.phase === "pre" && r.toolUseId === post.toolUseId);
190
+ if (pre && pre.tree !== post.tree && !changedBy.includes(post.toolUseId))
191
+ changedBy.push(post.toolUseId);
192
+ }
193
+ return {
194
+ findings,
195
+ expected,
196
+ matched,
197
+ notes,
198
+ agentSessions: sessions,
199
+ workspace: { receipts: receipts.length, changed_by: changedBy },
200
+ };
201
+ }
202
+ function sameBlock(recorded, local) {
203
+ if (recorded.type === "text")
204
+ return (recorded.text ?? "") === local.text;
205
+ if (recorded.type === "thinking") {
206
+ const a = recorded.thinking ?? "";
207
+ const b = typeof local.thinking === "string" ? local.thinking : "";
208
+ return a === "" || b === "" || a === b;
209
+ }
210
+ return (recorded.id === local.id &&
211
+ recorded.name === local.name &&
212
+ canonical(recorded.input ?? {}) === canonical(local.input ?? {}));
213
+ }
214
+ function localSessions(log) {
215
+ const ids = new Set();
216
+ for (const r of log.records)
217
+ if (typeof r.json.sessionId === "string")
218
+ ids.add(r.json.sessionId);
219
+ return [...ids].sort();
220
+ }
@@ -0,0 +1,5 @@
1
+ import type { Outcome } from "./claude-code.ts";
2
+ import type { Call } from "./record.ts";
3
+ import { type LocalLog } from "./shared.ts";
4
+ export declare const CODEX_TYPES: ReadonlySet<string>;
5
+ export declare function reconcileCodex(calls: Call[], log: LocalLog, agentSessions: string[]): Outcome;
@@ -0,0 +1,191 @@
1
+ // Codex rollouts vs the record (spec/reconcile.md §3). Mirrors reconcile/codex.py.
2
+ import { canonical, isObj } from "./shared.js";
3
+ export const CODEX_TYPES = new Set([
4
+ "session_meta",
5
+ "turn_context",
6
+ "response_item",
7
+ "event_msg",
8
+ "token_usage_record",
9
+ "world_state",
10
+ "compacted",
11
+ ]);
12
+ const OUTPUT_TYPES = new Set(["message", "function_call", "custom_tool_call"]);
13
+ const TOOL_OUTPUT_TYPES = new Set(["function_call_output", "custom_tool_call_output"]);
14
+ export function reconcileCodex(calls, log, agentSessions) {
15
+ const findings = [];
16
+ const notes = [];
17
+ const matched = { calls: 0, tool_results: 0 };
18
+ const payload = (r) => (isObj(r.json.payload) ? r.json.payload : {});
19
+ // Step 1: ordinals count 0, 1, 2 … in each file.
20
+ const last = new Map();
21
+ for (const r of log.records) {
22
+ if (typeof r.json.ordinal !== "number")
23
+ continue;
24
+ const prev = last.get(r.file) ?? -1;
25
+ if (r.json.ordinal !== prev + 1)
26
+ findings.push({
27
+ code: "LOCAL_GAP",
28
+ ref: { after: prev, next: r.json.ordinal },
29
+ local: { file: r.file, line: r.line },
30
+ });
31
+ last.set(r.file, r.json.ordinal);
32
+ }
33
+ const sessions = agentSessions.length > 0
34
+ ? agentSessions
35
+ : [
36
+ ...new Set(log.records
37
+ .filter((r) => r.json.type === "session_meta")
38
+ .map((r) => String(payload(r).id ?? ""))),
39
+ ]
40
+ .filter(Boolean)
41
+ .sort();
42
+ const responses = new Set();
43
+ const items = new Map();
44
+ const byCallId = new Map();
45
+ const outputs = new Map();
46
+ for (const r of log.records) {
47
+ const p = payload(r);
48
+ if (r.json.type === "token_usage_record" && typeof p.response_id === "string")
49
+ responses.add(p.response_id);
50
+ if (r.json.type !== "response_item")
51
+ continue;
52
+ const type = String(p.type ?? "");
53
+ if (typeof p.id === "string" && !items.has(p.id))
54
+ items.set(p.id, p);
55
+ if (OUTPUT_TYPES.has(type) && typeof p.call_id === "string" && !byCallId.has(p.call_id))
56
+ byCallId.set(p.call_id, p);
57
+ if (TOOL_OUTPUT_TYPES.has(type) &&
58
+ typeof p.call_id === "string" &&
59
+ !outputs.has(`${type}:${p.call_id}`))
60
+ outputs.set(`${type}:${p.call_id}`, p);
61
+ }
62
+ const expected = calls.filter((c) => {
63
+ if (!c.complete ||
64
+ c.status !== 200 ||
65
+ !c.request ||
66
+ !c.path.split("?")[0]?.endsWith("/responses"))
67
+ return false;
68
+ const metadata = isObj(c.request.client_metadata) ? c.request.client_metadata : {};
69
+ if (metadata.request_kind !== undefined && metadata.request_kind !== "turn")
70
+ return false;
71
+ const session = c.request.prompt_cache_key ?? metadata.session_id;
72
+ return sessions.length === 0 || sessions.includes(String(session ?? ""));
73
+ });
74
+ let redacted = 0;
75
+ // Steps 2–3: every expected response and its output items are in the rollout.
76
+ for (const call of [...expected].sort((a, b) => a.endSeq - b.endSeq)) {
77
+ if (call.redacted)
78
+ redacted++;
79
+ let clean = true;
80
+ const id = call.responseId;
81
+ if (!id)
82
+ notes.push(`call at seq ${call.requestSeq} has no response id`);
83
+ else if (!responses.has(id)) {
84
+ findings.push({ code: "MISSING_LOCAL", ref: { response_id: id }, gateway_seq: call.endSeq });
85
+ clean = false;
86
+ }
87
+ for (const item of call.items) {
88
+ const type = String(item.type ?? "");
89
+ if (!OUTPUT_TYPES.has(type) || typeof item.id !== "string")
90
+ continue;
91
+ const mine = items.get(item.id) ??
92
+ (typeof item.call_id === "string" ? byCallId.get(item.call_id) : undefined);
93
+ if (!mine) {
94
+ findings.push({
95
+ code: "MISSING_LOCAL",
96
+ ref: { item_id: item.id },
97
+ gateway_seq: call.endSeq,
98
+ });
99
+ clean = false;
100
+ }
101
+ else if (!call.redacted && itemContent(item) !== itemContent(mine)) {
102
+ findings.push({ code: "ALTERED", ref: { item_id: item.id }, gateway_seq: call.endSeq });
103
+ clean = false;
104
+ }
105
+ }
106
+ if (clean)
107
+ matched.calls++;
108
+ }
109
+ // Step 4: every tool output the model was given, from the first request that carried it.
110
+ const given = new Map();
111
+ for (const call of [...expected].sort((a, b) => a.requestSeq - b.requestSeq)) {
112
+ const input = Array.isArray(call.request?.input) ? call.request.input.filter(isObj) : [];
113
+ for (const item of input) {
114
+ const type = String(item.type ?? "");
115
+ if (TOOL_OUTPUT_TYPES.has(type) &&
116
+ typeof item.call_id === "string" &&
117
+ !given.has(`${type}:${item.call_id}`))
118
+ given.set(`${type}:${item.call_id}`, { item, call });
119
+ }
120
+ }
121
+ for (const [key, { item, call }] of given) {
122
+ const callId = String(item.call_id);
123
+ const mine = outputs.get(key);
124
+ if (!mine)
125
+ findings.push({
126
+ code: "MISSING_LOCAL",
127
+ ref: { call_id: callId },
128
+ gateway_seq: call.requestSeq,
129
+ });
130
+ else if (!call.requestRedacted && outputText(item.output) !== outputText(mine.output))
131
+ findings.push({ code: "ALTERED", ref: { call_id: callId }, gateway_seq: call.requestSeq });
132
+ else
133
+ matched.tool_results++;
134
+ }
135
+ // Step 5: model output the record never saw.
136
+ const seenResponses = new Set(calls.map((c) => c.responseId).filter((r) => r !== null));
137
+ const seenItems = new Set();
138
+ const seenCallIds = new Set();
139
+ for (const c of calls)
140
+ for (const item of c.items) {
141
+ if (typeof item.id === "string")
142
+ seenItems.add(item.id);
143
+ if (typeof item.call_id === "string")
144
+ seenCallIds.add(item.call_id);
145
+ }
146
+ const reported = new Set();
147
+ for (const r of log.records) {
148
+ const p = payload(r);
149
+ const local = { file: r.file, line: r.line };
150
+ if (r.json.type === "token_usage_record" && typeof p.response_id === "string") {
151
+ if (!seenResponses.has(p.response_id) && !reported.has(`r:${p.response_id}`)) {
152
+ reported.add(`r:${p.response_id}`);
153
+ findings.push({ code: "EXTRA_LOCAL", ref: { response_id: p.response_id }, local });
154
+ }
155
+ continue;
156
+ }
157
+ if (r.json.type !== "response_item")
158
+ continue;
159
+ const type = String(p.type ?? "");
160
+ const modelOutput = (type === "message" && p.role === "assistant") ||
161
+ type === "function_call" ||
162
+ type === "custom_tool_call";
163
+ if (!modelOutput)
164
+ continue;
165
+ const known = (typeof p.id === "string" && seenItems.has(p.id)) ||
166
+ (typeof p.call_id === "string" && seenCallIds.has(p.call_id));
167
+ const key = String(p.id ?? p.call_id ?? "");
168
+ if (!known && key && !reported.has(`i:${key}`)) {
169
+ reported.add(`i:${key}`);
170
+ findings.push({ code: "EXTRA_LOCAL", ref: { item_id: key }, local });
171
+ }
172
+ }
173
+ if (redacted > 0)
174
+ notes.push(`redacted: ${redacted} call(s) not compared for content`);
175
+ return { findings, expected, matched, notes, agentSessions: sessions };
176
+ }
177
+ function itemContent(item) {
178
+ if (item.type === "message") {
179
+ const content = Array.isArray(item.content) ? item.content.filter(isObj) : [];
180
+ return content
181
+ .filter((c) => c.type === "output_text")
182
+ .map((c) => (typeof c.text === "string" ? c.text : ""))
183
+ .join("");
184
+ }
185
+ if (item.type === "function_call")
186
+ return `${String(item.name)}\u0000${outputText(item.arguments)}`;
187
+ return `${String(item.name)}\u0000${outputText(item.input)}`;
188
+ }
189
+ function outputText(value) {
190
+ return typeof value === "string" ? value : canonical(value ?? null);
191
+ }
@@ -0,0 +1,19 @@
1
+ import { type Bundle } from "../verify/index.ts";
2
+ import { type Bodies } from "./record.ts";
3
+ import { type Harness, type LocalLog, type Report } from "./shared.ts";
4
+ export type { Finding, FindingCode, Harness, LocalLog, Report } from "./shared.ts";
5
+ export { loadLocal } from "./shared.ts";
6
+ export declare class RecordNotVerified extends Error {
7
+ }
8
+ export interface ReconcileOptions {
9
+ harness: Harness;
10
+ bundle: Bundle;
11
+ bodies: Bodies;
12
+ /** The local log: a path (read with loadLocal), or one already loaded. */
13
+ local: string | LocalLog;
14
+ agentSessions?: string[];
15
+ /** Every tool call must have its workspace receipts (§7), even if the record holds none. */
16
+ requireReceipts?: boolean;
17
+ }
18
+ /** Throws RecordNotVerified if the bundle doesn't verify: there'd be nothing to trust. */
19
+ export declare function reconcile(options: ReconcileOptions): Promise<Report>;
@@ -0,0 +1,50 @@
1
+ // Reconciliation (spec/reconcile.md): the agent's own log vs the verified record.
2
+ // Mirrors sdks/python/src/zanii_blackbox/reconcile/.
3
+ import { verifyBundle } from "../verify/index.js";
4
+ import { CLAUDE_CODE_TYPES, reconcileClaudeCode } from "./claude-code.js";
5
+ import { CODEX_TYPES, reconcileCodex } from "./codex.js";
6
+ import { callsOf, receiptsOf } from "./record.js";
7
+ import { loadLocal, unknownTypes } from "./shared.js";
8
+ export { loadLocal } from "./shared.js";
9
+ export class RecordNotVerified extends Error {
10
+ }
11
+ /** Throws RecordNotVerified if the bundle doesn't verify: there'd be nothing to trust. */
12
+ export async function reconcile(options) {
13
+ // Audit K3: the bodies it reads are checked against the chain first.
14
+ const verified = await verifyBundle(options.bundle, {
15
+ body: (h) => options.bodies(h) ?? undefined,
16
+ });
17
+ if (!verified.ok) {
18
+ const why = verified.chain.ok ? (verified.identity.message ?? "identity") : verified.chain.code;
19
+ throw new RecordNotVerified(`the record does not verify (${why})`);
20
+ }
21
+ const log = typeof options.local === "string" ? loadLocal(options.local, options.harness) : options.local;
22
+ const calls = callsOf(options.bundle.lines, options.bodies);
23
+ const sessions = options.agentSessions ?? [];
24
+ const outcome = options.harness === "claude-code"
25
+ ? reconcileClaudeCode(calls, log, sessions, receiptsOf(options.bundle.lines), options.requireReceipts ?? false)
26
+ : reconcileCodex(calls, log, sessions);
27
+ const known = options.harness === "claude-code" ? CLAUDE_CODE_TYPES : CODEX_TYPES;
28
+ return {
29
+ v: 1,
30
+ harness: options.harness,
31
+ ok: outcome.findings.length === 0,
32
+ record: {
33
+ session_id: options.bundle.session.session_id,
34
+ events: options.bundle.lines.length,
35
+ calls: calls.length,
36
+ calls_expected: outcome.expected.length,
37
+ calls_not_expected: calls.length - outcome.expected.length,
38
+ },
39
+ local: {
40
+ files: log.files.length,
41
+ records: log.records.length,
42
+ agent_sessions: outcome.agentSessions,
43
+ },
44
+ matched: outcome.matched,
45
+ findings: outcome.findings,
46
+ ...(outcome.workspace ? { workspace: outcome.workspace } : {}),
47
+ notes: log.files.length === 0 ? ["no local log", ...outcome.notes] : outcome.notes,
48
+ warnings: [...log.warnings, ...unknownTypes(log.records, known)],
49
+ };
50
+ }
@@ -0,0 +1,49 @@
1
+ export type Obj = Record<string, unknown>;
2
+ export interface Block {
3
+ type: string;
4
+ text?: string;
5
+ thinking?: string;
6
+ id?: string;
7
+ name?: string;
8
+ input?: unknown;
9
+ }
10
+ export interface Call {
11
+ requestSeq: number;
12
+ /** seq of the llm.response / llm.incomplete; the request's when there is none. */
13
+ endSeq: number;
14
+ path: string;
15
+ headers: Record<string, string>;
16
+ request: Obj | null;
17
+ complete: boolean;
18
+ status: number | null;
19
+ redacted: boolean;
20
+ requestRedacted: boolean;
21
+ messageId: string | null;
22
+ responseId: string | null;
23
+ model: string | null;
24
+ /** The response's flattened usage (spec/api.md §3), when recorded. */
25
+ usage: Record<string, unknown> | null;
26
+ /** The configured provider id the request went to (spec/cost.md §1: provider-scoped prices). */
27
+ provider: string | null;
28
+ /** The provider's service tier, when the response names one (spec/cost.md §1). */
29
+ serviceTier: string | null;
30
+ /** A cost the provider itself reported (OpenRouter's `usage.cost`), in micro-USD. */
31
+ reportedMicroUsd: number | null;
32
+ /** Content blocks, by index: Anthropic's, or a Chat Completions message mapped onto them. */
33
+ blocks: Map<number, Block>;
34
+ /** OpenAI Responses output items, in order. */
35
+ items: Obj[];
36
+ }
37
+ export type Bodies = (hash: string) => Uint8Array | undefined;
38
+ export declare function parseJson(text: string): unknown;
39
+ /** The calls in a verified record, in request order. */
40
+ export declare function callsOf(lines: readonly string[], bodies: Bodies): Call[];
41
+ export interface Receipt {
42
+ seq: number;
43
+ phase: "pre" | "post";
44
+ toolUseId: string;
45
+ agentSession: string | null;
46
+ tree: string | null;
47
+ }
48
+ /** The workspace receipts in a record (spec/reconcile.md §7), in seq order. */
49
+ export declare function receiptsOf(lines: readonly string[]): Receipt[];