@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,388 @@
1
+ // Analyses that produce findings (spec/findings.md): pure functions over a session's record, run
2
+ // offline or by the server on the live record. Mirrors sdks/python/src/zanii_blackbox/analysis/.
3
+ import { approvalFindings } from "../approvals/index.js";
4
+ import { automationSurprise } from "../authority/index.js";
5
+ import { dutyFindings } from "../duty/index.js";
6
+ import { policyFindings } from "../policy/index.js";
7
+ import { precogFindings } from "../precog/index.js";
8
+ import { preflightFindings } from "../preflight/index.js";
9
+ import { canonical } from "../reconcile/shared.js";
10
+ import { undoFindings } from "../undo/index.js";
11
+ import { detectors } from "./detectors.js";
12
+ import { flightPlan } from "./landing.js";
13
+ import { memoryFindings } from "./memory.js";
14
+ export { checkFlightPlan, landing, } from "./landing.js";
15
+ export { OUTCOMES, rollup, waste, } from "./waste.js";
16
+ const decoder = new TextDecoder();
17
+ /** Parses the lines once, attaching SDK event bodies. */
18
+ export function eventsOf(lines, bodies) {
19
+ // N4 (idea R6): a re-sent SDK event (meta.duplicate_of) stays on the record but counts once.
20
+ return lines.flatMap((line) => {
21
+ const e = JSON.parse(line);
22
+ if (e.kind === "sdk.event" && e.meta.duplicate_of !== undefined)
23
+ return [];
24
+ if (e.kind === "sdk.event") {
25
+ const b = bodies(e.body_hash);
26
+ try {
27
+ const parsed = b === undefined ? {} : JSON.parse(decoder.decode(b));
28
+ e.data =
29
+ typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : {};
30
+ }
31
+ catch {
32
+ e.data = {};
33
+ }
34
+ }
35
+ return [e];
36
+ });
37
+ }
38
+ /** Every analysis, in spec order. */
39
+ export function analyze(lines, options) {
40
+ const events = eventsOf(lines, options.bodies);
41
+ const now = options.now ?? Date.now();
42
+ const graceMs = options.graceMs ?? 60_000;
43
+ return [
44
+ ...dualWitness(events, now, graceMs),
45
+ ...checkpointReplay(events),
46
+ ...orphanedToolCalls(events, now),
47
+ ...tokenMisuse(events),
48
+ ...verifyRegression(events),
49
+ ...falseSuccess(events, now),
50
+ ...exitDurability(events),
51
+ ...detectors(lines, {
52
+ bodies: options.bodies,
53
+ now,
54
+ ...(options.prices ? { prices: options.prices } : {}),
55
+ ...(options.budgetMicroUsd ? { budgetMicroUsd: options.budgetMicroUsd } : {}),
56
+ }),
57
+ ...flightPlan(events, lines, options.bodies),
58
+ ...(options.policy ? policyFindings(lines, options.bodies, options.policy) : []),
59
+ ...nearMisses(events),
60
+ ...memoryFindings(events),
61
+ ...attestationFindings(events),
62
+ ...automationSurprise(lines),
63
+ ...undoFindings(lines, options.bodies),
64
+ ...approvalFindings(lines),
65
+ ...preflightFindings(lines),
66
+ ...dutyFindings(lines, options.prices, options.duty),
67
+ ...(options.precog && options.prices
68
+ ? precogFindings(lines, options.bodies, options.prices, options.precog)
69
+ : []),
70
+ ];
71
+ }
72
+ /** spec/attestation.md §3: a re-run that printed something else than the model was given. */
73
+ export function attestationFindings(events) {
74
+ return events
75
+ .filter((e) => e.kind === "sdk.event" &&
76
+ e.meta.type === "attestation" &&
77
+ e.data?.match === false &&
78
+ e.data.error === undefined)
79
+ .map((e) => ({
80
+ code: "TOOL_OUTPUT_MISMATCH",
81
+ source: "attestation",
82
+ severity: "warning",
83
+ ref: { seq: e.seq },
84
+ }));
85
+ }
86
+ /** spec/fleet.md §3: the agent's own near-miss reports. */
87
+ export function nearMisses(events) {
88
+ return events
89
+ .filter((e) => e.kind === "sdk.event" && e.meta.type === "near_miss")
90
+ .map((e) => ({
91
+ code: "NEAR_MISS",
92
+ source: "agent",
93
+ severity: "advisory",
94
+ ref: { seq: e.seq },
95
+ }));
96
+ }
97
+ /** A stable key for dedup: code + ref with sorted keys. */
98
+ export function findingKey(f) {
99
+ return `${f.code}:${JSON.stringify(Object.keys(f.ref)
100
+ .sort()
101
+ .map((k) => [k, f.ref[k]]))}`;
102
+ }
103
+ // ---------------------------------------------------------------- dual witness (spec/findings.md §2)
104
+ const ID_KEYS = ["message_id", "request_id", "response_id", "completion_id"];
105
+ function idsOf(source) {
106
+ if (!source)
107
+ return [];
108
+ return ID_KEYS.map((k) => source[k]).filter((v) => typeof v === "string" && v !== "");
109
+ }
110
+ export function dualWitness(events, now, graceMs) {
111
+ const old = (e) => now === Number.POSITIVE_INFINITY || Date.parse(e.ts) <= now - graceMs;
112
+ const gateway = events.filter((e) => e.kind === "llm.response" || e.kind === "llm.incomplete");
113
+ const sdk = events.filter((e) => e.kind === "sdk.event" && e.meta.type === "llm.call");
114
+ const gatewayIds = new Set(gateway.flatMap((e) => idsOf(e.meta)));
115
+ const sdkIds = new Set(sdk.flatMap((e) => idsOf(e.data)));
116
+ const findings = [];
117
+ for (const e of sdk) {
118
+ const ids = idsOf(e.data);
119
+ if (ids.length === 0 || !old(e) || ids.some((id) => gatewayIds.has(id)))
120
+ continue;
121
+ findings.push({
122
+ code: "BYPASS",
123
+ source: "dual_witness",
124
+ severity: "warning",
125
+ ref: { sdk_seq: e.meta.sdk_seq },
126
+ detail: "The SDK reported a model call the gateway never saw: the agent called a model around the gateway.",
127
+ });
128
+ }
129
+ // L2.3.2: a session opened with `sdk: true` expects the SDK from the start, even if it never reports.
130
+ const expected = events[0]?.kind === "session.open" && events[0].meta.sdk === true;
131
+ const first = sdk[0];
132
+ if (first || expected) {
133
+ let run = [];
134
+ for (const e of gateway) {
135
+ if ((first && e.seq < first.seq) || !old(e))
136
+ continue;
137
+ if (idsOf(e.meta).some((id) => sdkIds.has(id)))
138
+ run = [];
139
+ else
140
+ run.push(e);
141
+ if (run.length === 3) {
142
+ findings.push({
143
+ code: "SDK_SILENT",
144
+ source: "dual_witness",
145
+ severity: "caution",
146
+ ref: { after_seq: run[0].seq },
147
+ detail: first
148
+ ? "The gateway saw 3 model calls in a row the SDK didn't report: the SDK was disabled or removed."
149
+ : "The session expected an SDK, but the gateway saw 3 model calls and the SDK reported none.",
150
+ });
151
+ break;
152
+ }
153
+ }
154
+ }
155
+ return findings;
156
+ }
157
+ // ---------------------------------------------------------------- checkpoint replay (spec/findings.md §3)
158
+ export function checkpointReplay(events) {
159
+ const str = (v) => (typeof v === "string" && v !== "" ? v : undefined);
160
+ const failed = new Set();
161
+ const written = [];
162
+ const interrupts = [];
163
+ for (const e of events) {
164
+ if (e.kind !== "sdk.event" || !e.data)
165
+ continue;
166
+ if (e.meta.type === "tool.result" && e.data.ok === false && str(e.data.run_id))
167
+ failed.add(e.data.run_id);
168
+ // N5 (idea R2): a checkpointer's record of a task's writes (hooks/langgraph-checkpoint)
169
+ if (e.meta.type === "checkpoint.writes" && str(e.data.task_id))
170
+ written.push({ seq: e.seq, taskId: e.data.task_id });
171
+ if (e.meta.type === "interrupt")
172
+ interrupts.push({
173
+ seq: e.seq,
174
+ thread: str(e.data.thread_id),
175
+ node: str(e.data.node),
176
+ taskId: str(e.data.task_id),
177
+ });
178
+ }
179
+ const calls = [];
180
+ const findings = [];
181
+ for (const e of events) {
182
+ if (e.kind !== "sdk.event" || e.meta.type !== "tool.call" || !e.data)
183
+ continue;
184
+ const thread = str(e.data.thread_id);
185
+ const root = str(e.data.root_run_id);
186
+ const node = str(e.data.node);
187
+ const taskId = str(e.data.task_id);
188
+ if (!thread || !root || (!node && !taskId))
189
+ continue;
190
+ const call = {
191
+ seq: e.seq,
192
+ sdkSeq: e.meta.sdk_seq,
193
+ name: String(e.meta.name ?? ""),
194
+ args: canonical(e.data.args ?? null),
195
+ thread,
196
+ root,
197
+ ...(node ? { node } : {}),
198
+ ...(taskId ? { taskId } : {}),
199
+ ...(str(e.data.run_id) ? { runId: e.data.run_id } : {}),
200
+ };
201
+ const same = (c) => c.name === call.name &&
202
+ c.args === call.args &&
203
+ c.thread === call.thread &&
204
+ c.root !== call.root &&
205
+ (c.taskId && call.taskId
206
+ ? c.taskId === call.taskId
207
+ : c.node !== undefined && c.node === call.node) &&
208
+ !(c.runId && failed.has(c.runId));
209
+ const first = calls.find(same);
210
+ if (first) {
211
+ const paused = interrupts.some((i) => i.seq > first.seq &&
212
+ i.seq < call.seq &&
213
+ i.thread === first.thread &&
214
+ (first.taskId && i.taskId ? i.taskId === first.taskId : i.node === first.node));
215
+ findings.push({
216
+ code: "CHECKPOINT_REPLAY",
217
+ source: "detectors",
218
+ severity: "warning",
219
+ ref: {
220
+ sdk_seq: call.sdkSeq,
221
+ first_sdk_seq: first.sdkSeq,
222
+ cause: paused ? "pre_interrupt_side_effect" : "resume",
223
+ // Task ids are deterministic: a checkpointed write of the same task between the two
224
+ // calls makes it the same task run again, not a guess.
225
+ ...(first.taskId &&
226
+ written.some((w) => w.taskId === first.taskId && w.seq > first.seq && w.seq < call.seq)
227
+ ? { certain: true }
228
+ : {}),
229
+ },
230
+ detail: paused
231
+ ? "A node ran this tool, paused at interrupt(), and resuming re-ran the node from the top: the side effect happened twice."
232
+ : "A resumed LangGraph run re-executed a tool call an earlier run already made (a repeated side effect).",
233
+ });
234
+ }
235
+ calls.push(call);
236
+ }
237
+ return findings;
238
+ }
239
+ // ---------------------------------------------------------------- orphaned tool calls (spec/findings.md §3)
240
+ /** N4 (idea R9): a tool call with no result, judged at close (`now` is +Infinity): a crash
241
+ * mid-call. */
242
+ export function orphanedToolCalls(events, now) {
243
+ if (now !== Number.POSITIVE_INFINITY)
244
+ return [];
245
+ const finished = new Set();
246
+ const answered = new Set();
247
+ const refused = new Set();
248
+ for (const e of events) {
249
+ if (e.kind === "sdk.event" &&
250
+ e.meta.type === "tool.result" &&
251
+ typeof e.data?.run_id === "string")
252
+ finished.add(e.data.run_id);
253
+ if (e.kind === "tool.result" && typeof e.meta.call_seq === "number")
254
+ answered.add(e.meta.call_seq);
255
+ if (e.kind === "control" &&
256
+ e.meta.action === "approval" &&
257
+ e.meta.decision !== "approve" &&
258
+ typeof e.meta.approval_id === "string")
259
+ refused.add(e.meta.approval_id);
260
+ }
261
+ const out = [];
262
+ for (const e of events) {
263
+ const runId = e.data?.run_id;
264
+ if (e.kind === "sdk.event" && e.meta.type === "tool.call" && typeof runId === "string") {
265
+ if (!finished.has(runId))
266
+ out.push({
267
+ code: "ORPHANED_TOOL_CALL",
268
+ source: "detectors",
269
+ severity: "caution",
270
+ ref: { sdk_seq: e.meta.sdk_seq, run_id: runId },
271
+ detail: "A tool call started and never finished: the agent or the tool stopped mid-call. Check what it did before resuming.",
272
+ });
273
+ continue;
274
+ }
275
+ if (e.kind !== "tool.call" || e.meta.method !== "tools/call" || e.meta.transport === "stdio")
276
+ continue;
277
+ const p = e.meta.policy;
278
+ const held = typeof p?.approval_id === "string" && refused.has(p.approval_id);
279
+ if (answered.has(e.seq) || p?.action === "deny" || held)
280
+ continue;
281
+ out.push({
282
+ code: "ORPHANED_TOOL_CALL",
283
+ source: "detectors",
284
+ severity: "caution",
285
+ ref: { seq: e.seq },
286
+ detail: "The gateway forwarded a tool call and never recorded its result: it stopped mid-call. Check what the tool did before resuming.",
287
+ });
288
+ }
289
+ return out;
290
+ }
291
+ /** N4 (idea R5): a LangGraph run in `exit` durability that ran tools. Once per session. */
292
+ export function exitDurability(events) {
293
+ const cfg = events.find((e) => e.kind === "sdk.event" && e.meta.type === "run.config" && e.data?.durability === "exit");
294
+ if (!cfg)
295
+ return [];
296
+ const tool = events.find((e) => e.seq > cfg.seq &&
297
+ ((e.kind === "sdk.event" && e.meta.type === "tool.call") ||
298
+ (e.kind === "tool.call" && e.meta.method === "tools/call")));
299
+ if (!tool)
300
+ return [];
301
+ return [
302
+ {
303
+ code: "EXIT_DURABILITY",
304
+ source: "detectors",
305
+ severity: "caution",
306
+ ref: { sdk_seq: cfg.meta.sdk_seq, first_tool_seq: tool.seq },
307
+ detail: "The run saves its state only when it exits (durability exit) and ran tools: a crash loses the record of what already ran, and a resume runs it again.",
308
+ },
309
+ ];
310
+ }
311
+ /** N6 (idea S3): the gateway refused a request that carried the session's own token. */
312
+ export function tokenMisuse(events) {
313
+ return events
314
+ .filter((e) => e.meta.token_in_body === true)
315
+ .map((e) => ({
316
+ code: "TOKEN_MISUSE",
317
+ source: "detectors",
318
+ severity: "warning",
319
+ ref: { seq: e.seq },
320
+ detail: "The agent put its own session token into a request (a prompt or a tool call): it was refused, and the token should be treated as exposed.",
321
+ }));
322
+ }
323
+ // ---------------------------------------------------------------- verification (spec/findings.md §6)
324
+ /** Stage 2 R3: a `verify` check that passed, then failed later in the run. */
325
+ export function verifyRegression(events) {
326
+ const passed = new Map();
327
+ const out = [];
328
+ for (const e of events) {
329
+ if (e.kind !== "sdk.event" || e.meta.type !== "verify" || typeof e.meta.name !== "string")
330
+ continue;
331
+ const check = e.meta.name;
332
+ const seq = e.meta.sdk_seq;
333
+ if (e.data?.ok === true)
334
+ passed.set(check, seq);
335
+ else if (e.data?.ok === false && passed.has(check)) {
336
+ out.push({
337
+ code: "VERIFY_REGRESSION",
338
+ source: "verification",
339
+ severity: "caution",
340
+ ref: { sdk_seq: seq, check, passed_sdk_seq: passed.get(check) },
341
+ detail: "A check that passed earlier now fails: something the agent did since broke it.",
342
+ });
343
+ passed.delete(check);
344
+ }
345
+ }
346
+ return out;
347
+ }
348
+ /** Stage 2 R4, judged at close: a run labelled (or claimed) a success that its own record contradicts:
349
+ * a check's last result failed, or the last tool call failed with nothing succeeding after it. */
350
+ export function falseSuccess(events, now) {
351
+ if (now !== Number.POSITIVE_INFINITY)
352
+ return [];
353
+ const outcome = events.filter((e) => e.kind === "outcome").at(-1)?.meta.outcome;
354
+ const claimed = events.some((e) => e.kind === "sdk.event" && e.meta.type === "claim");
355
+ const by = outcome === "success" ? "outcome" : outcome === undefined && claimed ? "claim" : null;
356
+ if (!by)
357
+ return [];
358
+ const last = new Map();
359
+ let tool = null;
360
+ for (const e of events) {
361
+ if (e.kind === "sdk.event" && e.meta.type === "verify" && typeof e.meta.name === "string")
362
+ last.set(e.meta.name, e);
363
+ const sdkResult = e.kind === "sdk.event" && e.meta.type === "tool.result";
364
+ if (sdkResult && typeof e.data?.ok === "boolean")
365
+ tool = { seq: e.seq, failed: e.data.ok === false };
366
+ if (e.kind === "tool.result" && typeof e.meta.is_error === "boolean")
367
+ tool = { seq: e.seq, failed: e.meta.is_error === true };
368
+ }
369
+ const failing = [...last.values()].find((e) => e.data?.ok === false);
370
+ const finding = (reason, seq, detail) => [
371
+ {
372
+ code: "FALSE_SUCCESS",
373
+ source: "verification",
374
+ severity: "warning",
375
+ ref: { claimed: by, reason, seq },
376
+ detail,
377
+ },
378
+ ];
379
+ if (failing)
380
+ return finding("failing_check", failing.seq, by === "outcome"
381
+ ? "The run is labelled a success, but a check it ran last failed."
382
+ : "The agent claimed it was done, but a check it ran last failed.");
383
+ if (tool?.failed)
384
+ return finding("last_tool_failed", tool.seq, by === "outcome"
385
+ ? "The run is labelled a success, but its last tool call failed and nothing succeeded after it."
386
+ : "The agent claimed it was done, but its last tool call failed and nothing succeeded after it.");
387
+ return [];
388
+ }
@@ -0,0 +1,25 @@
1
+ import type { Json } from "../verify/index.ts";
2
+ import type { Ev, Finding } from "./index.ts";
3
+ export type Result = "pass" | "fail" | "cannot_verify";
4
+ export interface CriterionResult {
5
+ criterion: string;
6
+ predicate: Json;
7
+ result: Result;
8
+ gap?: string;
9
+ }
10
+ export interface ClaimVerdict {
11
+ seq: number;
12
+ summary?: string;
13
+ verdict: "satisfied" | "failed" | "unverified";
14
+ criteria: CriterionResult[];
15
+ }
16
+ export interface Landing {
17
+ plan: {
18
+ [key: string]: Json;
19
+ } | null;
20
+ claims: ClaimVerdict[];
21
+ }
22
+ export declare function landing(events: Ev[], lines: readonly string[], bodies: (h: string) => Uint8Array | undefined): Landing;
23
+ export declare function flightPlan(events: Ev[], lines: readonly string[], bodies: (h: string) => Uint8Array | undefined): Finding[];
24
+ /** Why a flight plan is invalid (spec/findings.md §5), or undefined if it's fine. */
25
+ export declare function checkFlightPlan(plan: unknown): string | undefined;
@@ -0,0 +1,225 @@
1
+ // Flight plan and landing checklist (spec/findings.md §5): a declared plan, deviations from it, and
2
+ // completion claims judged against the gateway's record. Mirrors analysis/landing.py.
3
+ import { checkDuty } from "../duty/index.js";
4
+ import { callsOf } from "../reconcile/record.js";
5
+ import { isObj } from "../reconcile/shared.js";
6
+ import { toolCallsOf, toolResultsOf } from "./detectors.js";
7
+ /** Every tool call in the record, with what its result said (provider tool_use ids; MCP call_seq). */
8
+ function usesOf(events, lines, bodies) {
9
+ const calls = callsOf(lines, bodies);
10
+ const results = new Map();
11
+ for (const r of toolResultsOf(calls, lines))
12
+ if (r.id !== undefined)
13
+ results.set(r.id, r);
14
+ const uses = toolCallsOf(calls).map((t) => {
15
+ const r = t.id === undefined ? undefined : results.get(t.id);
16
+ return r
17
+ ? { seq: t.seq, tool: t.tool, ok: !r.error, resultSeq: r.seq }
18
+ : { seq: t.seq, tool: t.tool };
19
+ });
20
+ const mcp = new Map();
21
+ for (const e of events) {
22
+ if (e.kind === "tool.call" &&
23
+ e.meta.method === "tools/call" &&
24
+ typeof e.meta.tool === "string") {
25
+ const u = { seq: e.seq, tool: e.meta.tool };
26
+ mcp.set(e.seq, u);
27
+ uses.push(u);
28
+ }
29
+ else if (e.kind === "tool.result" && typeof e.meta.call_seq === "number") {
30
+ const u = mcp.get(e.meta.call_seq);
31
+ if (u && u.ok === undefined) {
32
+ u.ok =
33
+ e.meta.rpc_error !== undefined || e.meta.is_error === true
34
+ ? false
35
+ : e.meta.is_error === false
36
+ ? true
37
+ : null;
38
+ u.resultSeq = e.seq;
39
+ }
40
+ }
41
+ }
42
+ return uses.sort((a, b) => a.seq - b.seq);
43
+ }
44
+ function planOf(events) {
45
+ return plansOf(events).plan;
46
+ }
47
+ /** The plan in force, and the SDK filings refused on the way (spec/findings.md §5, L3.4.6): the
48
+ * plan from session.open wins; else the first valid `flight_plan` SDK event. */
49
+ function plansOf(events) {
50
+ const open = events[0];
51
+ if (open?.kind === "session.open" && isObj(open.meta.flight_plan))
52
+ return { plan: open.meta.flight_plan, invalid: [] };
53
+ const invalid = [];
54
+ for (const e of events) {
55
+ if (e.kind !== "sdk.event" || e.meta.type !== "flight_plan")
56
+ continue;
57
+ const error = checkFlightPlan(e.data);
58
+ if (!error)
59
+ return { plan: e.data, invalid };
60
+ invalid.push({ seq: e.seq, error });
61
+ }
62
+ return { plan: null, invalid };
63
+ }
64
+ const listOf = (v) => (Array.isArray(v) ? v : []);
65
+ /** One predicate over the tool calls (and results) recorded before `before`. */
66
+ function judge(p, uses, before) {
67
+ const seen = uses.filter((u) => u.seq < before);
68
+ const one = isObj(p) && Object.keys(p).length === 1 ? Object.entries(p)[0] : null;
69
+ const [key, arg] = one ?? ["", null];
70
+ const named = typeof arg === "string" ? seen.filter((u) => u.tool === arg) : [];
71
+ const out = (result, gap) => ({ predicate: p, result, ...(gap ? { gap } : {}) });
72
+ if (key === "tool_called" && typeof arg === "string")
73
+ return named.length ? out("pass") : out("fail", `${arg} was never called`);
74
+ if (key === "tool_not_called" && typeof arg === "string")
75
+ return named.length
76
+ ? out("fail", `${arg} was called (seq ${named[0].seq})`)
77
+ : out("pass");
78
+ if (key === "max_tool_calls" && typeof arg === "number" && Number.isSafeInteger(arg))
79
+ return seen.length <= arg
80
+ ? out("pass")
81
+ : out("fail", `${seen.length} tool calls, more than ${arg}`);
82
+ if (key === "tool_succeeded" && typeof arg === "string") {
83
+ if (!named.length)
84
+ return out("fail", `${arg} was never called`);
85
+ const judged = named.map((u) => u.resultSeq !== undefined && u.resultSeq < before ? u.ok : undefined);
86
+ if (judged.includes(true))
87
+ return out("pass");
88
+ if (judged.every((ok) => ok === false))
89
+ return out("fail", `every ${arg} call returned an error`);
90
+ return out("cannot_verify", `no recorded result says whether ${arg} worked`);
91
+ }
92
+ return out("cannot_verify", "unsupported predicate");
93
+ }
94
+ export function landing(events, lines, bodies) {
95
+ const plan = planOf(events);
96
+ const uses = usesOf(events, lines, bodies);
97
+ const claims = [];
98
+ for (const e of events) {
99
+ if (e.kind !== "sdk.event" || e.meta.type !== "claim")
100
+ continue;
101
+ const data = e.data ?? {};
102
+ const criteria = [
103
+ ...listOf(plan?.criteria).map((p, i) => ({
104
+ criterion: `plan:${i}`,
105
+ ...judge(p, uses, e.seq),
106
+ })),
107
+ ...listOf(data.asserts).map((p, i) => ({
108
+ criterion: `assert:${i}`,
109
+ ...judge(p, uses, e.seq),
110
+ })),
111
+ ];
112
+ const verdict = criteria.some((c) => c.result === "fail")
113
+ ? "failed"
114
+ : criteria.length === 0 || criteria.some((c) => c.result === "cannot_verify")
115
+ ? "unverified"
116
+ : "satisfied";
117
+ claims.push({
118
+ seq: e.seq,
119
+ ...(typeof data.summary === "string" ? { summary: data.summary } : {}),
120
+ verdict,
121
+ criteria,
122
+ });
123
+ }
124
+ return { plan, claims };
125
+ }
126
+ const f = (code, severity, ref, detail) => ({
127
+ code,
128
+ source: "flight_plan",
129
+ severity,
130
+ ref,
131
+ detail,
132
+ });
133
+ export function flightPlan(events, lines, bodies) {
134
+ const { plan, claims } = landing(events, lines, bodies);
135
+ const out = plansOf(events).invalid.map((x) => f("PLAN_INVALID", "advisory", { seq: x.seq }, `A flight plan filed by the SDK was ignored: ${x.error}.`));
136
+ if (plan) {
137
+ const uses = usesOf(events, lines, bodies);
138
+ const expected = Array.isArray(plan.expected_tools) ? new Set(plan.expected_tools) : null;
139
+ if (expected) {
140
+ const reported = new Set();
141
+ for (const u of uses)
142
+ if (!expected.has(u.tool) && !reported.has(u.tool)) {
143
+ reported.add(u.tool);
144
+ out.push(f("PLAN_DEVIATION", "caution", { tool: u.tool }, `${u.tool} isn't in the flight plan.`));
145
+ }
146
+ }
147
+ listOf(plan.criteria).forEach((p, i) => {
148
+ if (!isObj(p) || !("tool_not_called" in p || "max_tool_calls" in p))
149
+ return;
150
+ const r = judge(p, uses, Number.POSITIVE_INFINITY);
151
+ if (r.result === "fail")
152
+ out.push(f("PLAN_VIOLATION", "warning", { criterion: `plan:${i}` }, `Flight plan broken: ${r.gap}.`));
153
+ });
154
+ // Sterile cockpit (A9): from the first call to `after` until the first call to `until` (if
155
+ // set), only `after`, `until` and the `allow` tools.
156
+ listOf(plan.sterile).forEach((phase, i) => {
157
+ if (!isObj(phase) || typeof phase.after !== "string")
158
+ return;
159
+ const until = typeof phase.until === "string" ? phase.until : undefined;
160
+ const allowed = new Set([phase.after, ...(until ? [until] : []), ...listOf(phase.allow)]);
161
+ const start = uses.find((u) => u.tool === phase.after);
162
+ const end = start && until ? uses.find((u) => u.seq > start.seq && u.tool === until) : undefined;
163
+ const reported = new Set();
164
+ for (const u of uses)
165
+ if (start &&
166
+ u.seq > start.seq &&
167
+ (end === undefined || u.seq < end.seq) &&
168
+ !allowed.has(u.tool) &&
169
+ !reported.has(u.tool)) {
170
+ reported.add(u.tool);
171
+ out.push(f("STERILE_VIOLATION", "warning", { phase: `sterile:${i}`, tool: u.tool }, `${u.tool} was called in the sterile phase after ${phase.after}.`));
172
+ }
173
+ });
174
+ }
175
+ for (const c of claims) {
176
+ if (c.criteria.length === 0)
177
+ out.push(f("CLAIM_UNVERIFIED", "caution", { claim_seq: c.seq, criterion: "none" }, "A completion claim with nothing to check it against."));
178
+ for (const r of c.criteria)
179
+ if (r.result === "fail")
180
+ out.push(f("FALSE_CLAIM", "warning", { claim_seq: c.seq, criterion: r.criterion }, `Claimed done, but ${r.gap}.`));
181
+ else if (r.result === "cannot_verify")
182
+ out.push(f("CLAIM_UNVERIFIED", "caution", { claim_seq: c.seq, criterion: r.criterion }, `Can't verify the claim: ${r.gap}.`));
183
+ }
184
+ return out;
185
+ }
186
+ /** Why a flight plan is invalid (spec/findings.md §5), or undefined if it's fine. */
187
+ export function checkFlightPlan(plan) {
188
+ if (!isObj(plan))
189
+ return "flight_plan must be an object";
190
+ const { objective, expected_tools: tools, criteria } = plan;
191
+ if (typeof objective !== "string" || objective.length === 0 || objective.length > 2000)
192
+ return "flight_plan.objective must be a string of 1–2000 characters";
193
+ if (tools !== undefined &&
194
+ !(Array.isArray(tools) &&
195
+ tools.length <= 100 &&
196
+ tools.every((x) => typeof x === "string" && x.length > 0 && x.length <= 128)))
197
+ return "flight_plan.expected_tools must be at most 100 tool names";
198
+ const value = (v) => typeof v === "string" ? v.length <= 128 : Number.isSafeInteger(v) && v >= 0;
199
+ if (criteria !== undefined &&
200
+ !(Array.isArray(criteria) &&
201
+ criteria.length <= 50 &&
202
+ criteria.every((c) => isObj(c) && Object.keys(c).length === 1 && value(Object.values(c)[0]))))
203
+ return 'flight_plan.criteria must be at most 50 predicates like {"tool_called": "run_tests"}';
204
+ const name = (x) => typeof x === "string" && x.length > 0 && x.length <= 128;
205
+ const { sterile } = plan;
206
+ if (sterile !== undefined &&
207
+ !(Array.isArray(sterile) &&
208
+ sterile.length <= 10 &&
209
+ sterile.every((p) => isObj(p) &&
210
+ name(p.after) &&
211
+ Object.keys(p).every((k) => k === "after" || k === "allow" || k === "until") &&
212
+ (p.until === undefined || name(p.until)) &&
213
+ (p.allow === undefined ||
214
+ (Array.isArray(p.allow) && p.allow.length <= 100 && p.allow.every(name))))))
215
+ return 'flight_plan.sterile must be at most 10 phases like {"after": "deploy", "allow": ["rollback"]}';
216
+ if (plan.duty !== undefined) {
217
+ const bad = checkDuty(plan.duty); // spec/duty.md §1
218
+ if (bad)
219
+ return bad;
220
+ }
221
+ const extra = Object.keys(plan).filter((k) => !["objective", "expected_tools", "criteria", "sterile", "duty"].includes(k));
222
+ if (extra.length)
223
+ return `flight_plan has unknown fields: ${extra.join(", ")}`;
224
+ return undefined;
225
+ }
@@ -0,0 +1,13 @@
1
+ import type { Ev, Finding } from "./index.ts";
2
+ /** spec/agents.md §2, over parsed events (sdk.event bodies attached). */
3
+ export declare function memoryXrayOf(events: readonly Ev[], revokedElsewhere?: readonly string[]): {
4
+ writes: number;
5
+ reads: number;
6
+ revoked: string[];
7
+ stale_reads: {
8
+ seq: number;
9
+ memory_id: string;
10
+ }[];
11
+ };
12
+ /** The findings part (this record's own revocations only). */
13
+ export declare function memoryFindings(events: readonly Ev[]): Finding[];