@zanii/blackbox 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +26 -1
  2. package/dist/a2a/index.d.ts +77 -0
  3. package/dist/a2a/index.js +305 -0
  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.d.ts +18 -0
  21. package/dist/analysis/taxonomy.js +66 -0
  22. package/dist/approvals/index.d.ts +23 -0
  23. package/dist/approvals/index.js +48 -0
  24. package/dist/archive/index.d.ts +39 -0
  25. package/dist/archive/index.js +96 -0
  26. package/dist/archive/parquet.d.ts +2 -0
  27. package/dist/archive/parquet.js +185 -0
  28. package/dist/badge/index.d.ts +16 -0
  29. package/dist/badge/index.js +48 -0
  30. package/dist/bom/index.d.ts +14 -0
  31. package/dist/bom/index.js +152 -0
  32. package/dist/cli.js +114 -10
  33. package/dist/compliance/art12.d.ts +35 -0
  34. package/dist/compliance/art12.js +190 -0
  35. package/dist/compliance/index.d.ts +36 -2
  36. package/dist/compliance/index.js +78 -11
  37. package/dist/compliance/zanii.d.ts +29 -0
  38. package/dist/compliance/zanii.js +84 -0
  39. package/dist/constitution/index.d.ts +57 -0
  40. package/dist/constitution/index.js +131 -0
  41. package/dist/cv/index.d.ts +39 -0
  42. package/dist/cv/index.js +108 -0
  43. package/dist/disclosure/index.d.ts +31 -0
  44. package/dist/disclosure/index.js +113 -0
  45. package/dist/encryption/index.d.ts +9 -0
  46. package/dist/encryption/index.js +31 -0
  47. package/dist/evidence/index.d.ts +60 -0
  48. package/dist/evidence/index.js +151 -0
  49. package/dist/federation/index.d.ts +35 -0
  50. package/dist/federation/index.js +102 -0
  51. package/dist/finance/index.d.ts +126 -0
  52. package/dist/finance/index.js +320 -0
  53. package/dist/fleet/index.js +9 -0
  54. package/dist/gov/index.d.ts +108 -0
  55. package/dist/gov/index.js +225 -0
  56. package/dist/health/index.d.ts +120 -0
  57. package/dist/health/index.js +233 -0
  58. package/dist/index.d.ts +34 -5
  59. package/dist/index.js +34 -5
  60. package/dist/memory/index.d.ts +36 -0
  61. package/dist/memory/index.js +85 -0
  62. package/dist/occurrence/index.d.ts +11 -0
  63. package/dist/occurrence/index.js +18 -0
  64. package/dist/ocsf/index.d.ts +1 -1
  65. package/dist/ocsf/index.js +36 -3
  66. package/dist/otlp/index.d.ts +8 -1
  67. package/dist/otlp/index.js +258 -1
  68. package/dist/packs/index.js +44 -4
  69. package/dist/policy/delta.js +7 -1
  70. package/dist/policy/index.d.ts +40 -6
  71. package/dist/policy/index.js +186 -8
  72. package/dist/policy/zanii.d.ts +31 -0
  73. package/dist/policy/zanii.js +87 -0
  74. package/dist/pq/index.d.ts +23 -0
  75. package/dist/pq/index.js +104 -0
  76. package/dist/search/index.d.ts +23 -0
  77. package/dist/search/index.js +69 -0
  78. package/dist/session/index.d.ts +107 -1
  79. package/dist/session/index.js +189 -11
  80. package/dist/sla/index.d.ts +61 -0
  81. package/dist/sla/index.js +197 -0
  82. package/dist/succession/index.d.ts +50 -0
  83. package/dist/succession/index.js +123 -0
  84. package/dist/timestamp/index.d.ts +24 -0
  85. package/dist/timestamp/index.js +274 -0
  86. package/dist/tokens/index.d.ts +6 -0
  87. package/dist/tokens/index.js +46 -0
  88. package/dist/transparency/index.d.ts +188 -0
  89. package/dist/transparency/index.js +712 -0
  90. package/dist/version.d.ts +1 -1
  91. package/dist/version.js +1 -1
  92. package/dist/walls/index.d.ts +31 -0
  93. package/dist/walls/index.js +119 -0
  94. package/package.json +1 -1
@@ -0,0 +1,357 @@
1
+ // Tool hallucinations, checked exactly (spec/findings.md §8, H1): the agent calling a tool that
2
+ // doesn't exist, with arguments its schema refuses, with a value it never saw; saying it worked
3
+ // right after an error; quoting a tool it never called. Pure functions over the record, no model.
4
+ // Mirrors sdks/python/src/zanii_blackbox/analysis/hallucination.py; pinned by
5
+ // spec/vectors/hallucination.json.
6
+ import { callsOf } from "../reconcile/record.js";
7
+ import { canonical, isObj } from "../reconcile/shared.js";
8
+ import { toolCallsOf, toolResultsOf } from "./detectors.js";
9
+ const decoder = new TextDecoder();
10
+ const f = (code, ref, detail) => ({
11
+ code,
12
+ source: "hallucination",
13
+ severity: "warning",
14
+ ref,
15
+ detail,
16
+ });
17
+ /** JSON, or the last JSON-RPC message of an SSE body. */
18
+ function rpcOf(bytes) {
19
+ if (!bytes)
20
+ return undefined;
21
+ const text = decoder.decode(bytes);
22
+ const tries = [
23
+ text,
24
+ ...text
25
+ .split("\n")
26
+ .flatMap((l) => (l.startsWith("data:") ? [l.slice(5)] : []))
27
+ .reverse(),
28
+ ];
29
+ for (const t of tries) {
30
+ try {
31
+ const v = JSON.parse(t);
32
+ if (isObj(v))
33
+ return v;
34
+ }
35
+ catch { }
36
+ }
37
+ return undefined;
38
+ }
39
+ // ---------------------------------------------------------------- schemas (§8.2)
40
+ /**
41
+ * The first way `value` breaks `schema`, or null. ponytail: the JSON Schema subset tools use
42
+ * (type, required, properties, additionalProperties: false, enum, items), not $ref or anyOf.
43
+ */
44
+ export function schemaProblem(schema, value, path = "$") {
45
+ if (!isObj(schema))
46
+ return null;
47
+ if (Array.isArray(schema.enum) && !schema.enum.some((e) => canonical(e) === canonical(value)))
48
+ return `enum:${path}`;
49
+ const types = typeof schema.type === "string" ? [schema.type] : Array.isArray(schema.type) ? schema.type : [];
50
+ if (types.length > 0 && !types.some((t) => typeMatches(t, value)))
51
+ return `type:${path}`;
52
+ if (isObj(value)) {
53
+ const props = isObj(schema.properties) ? schema.properties : {};
54
+ for (const r of Array.isArray(schema.required) ? schema.required : [])
55
+ if (typeof r === "string" && !(r in value))
56
+ return `missing:${path}.${r}`;
57
+ for (const [k, v] of Object.entries(value)) {
58
+ if (k in props) {
59
+ const p = schemaProblem(props[k], v, `${path}.${k}`);
60
+ if (p)
61
+ return p;
62
+ }
63
+ else if (schema.additionalProperties === false)
64
+ return `extra:${path}.${k}`;
65
+ }
66
+ }
67
+ if (Array.isArray(value) && isObj(schema.items))
68
+ for (const [i, v] of value.entries()) {
69
+ const p = schemaProblem(schema.items, v, `${path}[${i}]`);
70
+ if (p)
71
+ return p;
72
+ }
73
+ return null;
74
+ }
75
+ function typeMatches(type, v) {
76
+ switch (type) {
77
+ case "string":
78
+ return typeof v === "string";
79
+ case "number":
80
+ return typeof v === "number";
81
+ case "integer":
82
+ return typeof v === "number" && Number.isInteger(v);
83
+ case "boolean":
84
+ return typeof v === "boolean";
85
+ case "array":
86
+ return Array.isArray(v);
87
+ case "object":
88
+ return isObj(v);
89
+ case "null":
90
+ return v === null;
91
+ default:
92
+ return true;
93
+ }
94
+ }
95
+ /** A model request's declared tools: name → input schema (Anthropic, Chat Completions, Responses). */
96
+ function declaredTools(request) {
97
+ const tools = Array.isArray(request?.tools) ? request.tools.filter(isObj) : [];
98
+ const out = new Map();
99
+ for (const t of tools) {
100
+ const fn = isObj(t.function) ? t.function : t;
101
+ if (typeof fn.name === "string")
102
+ out.set(fn.name, fn.input_schema ?? fn.parameters ?? t.input_schema ?? null);
103
+ }
104
+ return out.size > 0 ? out : null;
105
+ }
106
+ // ---------------------------------------------------------------- values the agent never saw (§8.3)
107
+ const ARABIC_DIGITS = /[٠-٩۰-۹]/g;
108
+ /** Arabic-Indic digits as Western ones. */
109
+ export const western = (s) => s.replace(ARABIC_DIGITS, (d) => String((d.charCodeAt(0) & 0xf) % 10));
110
+ /** Lower case, Western digits, and no separators, so `784-1990 1234567` matches `78419901234567`. */
111
+ export function normaliseValue(s) {
112
+ return western(s)
113
+ .toLowerCase()
114
+ .replace(/[\s\-_./()+:,]/g, "");
115
+ }
116
+ const DATE = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$|^[0-9]{2}-[0-9]{2}-[0-9]{4}$/;
117
+ const VALUE_KINDS = [
118
+ ["email", /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g],
119
+ ["iban", /\b[A-Z]{2}[0-9]{2}(?:[ ]?[A-Z0-9]){11,30}\b/g],
120
+ ["uuid", /\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b/g],
121
+ ["reference", /\b[A-Za-z]{2,10}[-_]?[0-9]{3,}[A-Za-z0-9]*\b/g],
122
+ ["number", /(?<![A-Za-z0-9])\+?[0-9][0-9 -]{4,}[0-9](?![A-Za-z0-9])/g],
123
+ ];
124
+ /** The identifying values in a string (Western digits) and where they are, skipping `taken` spans. */
125
+ export function identifierSpans(s, taken = []) {
126
+ const out = [];
127
+ for (const [kind, re] of VALUE_KINDS)
128
+ for (const m of s.matchAll(re)) {
129
+ const at = m.index ?? 0;
130
+ const end = at + m[0].length;
131
+ if (taken.some(([a, b]) => at < b && end > a))
132
+ continue;
133
+ const norm = normaliseValue(m[0]);
134
+ // a date is something an agent makes up legitimately (today, a deadline): not an identifier
135
+ if (kind === "number" && (norm.replace(/[^0-9]/g, "").length < 6 || DATE.test(m[0])))
136
+ continue;
137
+ taken.push([at, end]);
138
+ out.push({ kind, value: norm, at, end });
139
+ }
140
+ return out;
141
+ }
142
+ /** Fields an agent fills in itself (detectors' volatile keys): their values are made, not recalled. */
143
+ const GENERATED = new Set([
144
+ "timestamp",
145
+ "ts",
146
+ "time",
147
+ "date",
148
+ "request_id",
149
+ "requestId",
150
+ "nonce",
151
+ "uuid",
152
+ "trace_id",
153
+ "idempotency_key",
154
+ "idempotencyKey",
155
+ ]);
156
+ /** The identifying values in tool arguments' strings: emails, IBANs, UUIDs, references, long
157
+ * numbers. ponytail: JSON numbers are skipped (counts, sizes, epoch times), as are generated fields. */
158
+ export function identifiersIn(args, path = "$") {
159
+ const out = [];
160
+ const walk = (v, p) => {
161
+ if (out.length >= 50)
162
+ return;
163
+ if (typeof v === "string")
164
+ for (const x of identifierSpans(western(v)))
165
+ out.push({ kind: x.kind, path: p, value: x.value });
166
+ if (Array.isArray(v))
167
+ for (const [i, x] of v.entries())
168
+ walk(x, `${p}[${i}]`);
169
+ else if (isObj(v))
170
+ for (const k of Object.keys(v).sort())
171
+ if (!GENERATED.has(k))
172
+ walk(v[k], `${p}.${k}`);
173
+ };
174
+ walk(args, path);
175
+ return out;
176
+ }
177
+ /** `1,250.50` → `1250.5`: no separators, leading or trailing zeros. */
178
+ export function canonicalNumber(s) {
179
+ const [whole = "0", frac = ""] = s.replaceAll(",", "").split(".");
180
+ const w = whole.replace(/^0+(?=[0-9])/, "");
181
+ const f = frac.replace(/0+$/, "");
182
+ return f ? `${w}.${f}` : w;
183
+ }
184
+ // ---------------------------------------------------------------- answers (§8.4, §8.5)
185
+ /** The answer's text: Anthropic and Chat Completions text blocks, Responses message items. */
186
+ export function answerText(c) {
187
+ const parts = [];
188
+ for (const [, b] of [...c.blocks].sort(([x], [y]) => x - y))
189
+ if (b.type === "text" && typeof b.text === "string")
190
+ parts.push(b.text);
191
+ for (const item of c.items)
192
+ if (item.type === "message" && Array.isArray(item.content))
193
+ for (const p of item.content.filter(isObj))
194
+ if (typeof p.text === "string")
195
+ parts.push(p.text);
196
+ return parts.join("\n");
197
+ }
198
+ const SUCCESS = /\b(?:successfully|succeeded|completed|all set|has been (?:sent|created|updated|processed|booked|transferred|refunded|cancell?ed|saved|deleted|submitted|approved|paid)|have (?:sent|created|updated|processed|booked|transferred|refunded|cancell?ed|saved|deleted|submitted|paid)|is (?:done|complete))\b|تم |بنجاح|اكتمل/i;
199
+ const FAILURE = /\b(?:error|failed|fail|unable|couldn't|could not|can't|cannot|didn't|did not|not able|problem|issue)\b|خطأ|فشل|تعذر|لم /i;
200
+ const CITES = "(?:called|ran|used|queried|checked|according to|the results? (?:of|from)|returned by|output of)";
201
+ /** Right before a citation: it's not a claim that the tool ran ("I haven't called", "I could use"). */
202
+ const NEGATED = /(?:not|n't|never|will|can|could|would|should|shall|may|might|to|if|before)\s*$/i;
203
+ function escapeRe(s) {
204
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
205
+ }
206
+ // ---------------------------------------------------------------- the findings
207
+ /** spec/findings.md §8: UNKNOWN_TOOL, TOOL_ARGS_INVALID, FABRICATED_VALUE, SUCCESS_AFTER_ERROR,
208
+ * PHANTOM_RESULT, in record order. */
209
+ export function toolHallucinations(lines, bodies) {
210
+ const calls = callsOf(lines, bodies);
211
+ const modelCalls = toolCallsOf(calls);
212
+ const results = toolResultsOf(calls, lines);
213
+ const events = lines.map((l) => JSON.parse(l));
214
+ const out = [];
215
+ const push = (at, finding) => out.push({ ...finding, at });
216
+ // the model side: each tool call against the request that produced it
217
+ const byEnd = new Map(calls.map((c) => [c.endSeq, c]));
218
+ const seenBy = new Map();
219
+ for (const t of modelCalls) {
220
+ const c = byEnd.get(t.seq);
221
+ if (!c)
222
+ continue;
223
+ const declared = declaredTools(c.request);
224
+ if (declared && !declared.has(t.tool)) {
225
+ push(t.seq, unknownTool(t.seq, t.tool, "model"));
226
+ continue;
227
+ }
228
+ const schema = declared?.get(t.tool);
229
+ const problem = typeof t.args === "string" ? "not_json" : schemaProblem(schema, t.args ?? {});
230
+ if (problem)
231
+ push(t.seq, argsInvalid(t.seq, t.tool, "model", problem));
232
+ let seen = seenBy.get(t.seq);
233
+ if (seen === undefined) {
234
+ seen = normaliseValue(c.request ? canonical(c.request) : "");
235
+ seenBy.set(t.seq, seen);
236
+ }
237
+ for (const v of identifiersIn(t.args))
238
+ if (!seen.includes(v.value))
239
+ push(t.seq, fabricated(t.seq, t.tool, "model", v));
240
+ }
241
+ // the gateway side: MCP calls against the server's listing and what came before
242
+ const listed = new Map();
243
+ const called = new Map(); // tool → first seq it was called at
244
+ // ponytail: the latest model request holds the conversation so far; tool results are added once
245
+ let request = "";
246
+ let toolText = "";
247
+ let sawModel = false;
248
+ for (const t of modelCalls)
249
+ if (!called.has(t.tool))
250
+ called.set(t.tool, t.seq);
251
+ for (const e of events) {
252
+ const m = e.meta;
253
+ const text = () => {
254
+ const b = bodies(e.body_hash);
255
+ return b ? decoder.decode(b) : "";
256
+ };
257
+ if (e.kind === "llm.request") {
258
+ sawModel = true;
259
+ request = normaliseValue(text());
260
+ }
261
+ else if (e.kind === "tool.result" && typeof m.server === "string" && isObj(m.tool_defs)) {
262
+ const tools = rpcOf(bodies(e.body_hash))?.result;
263
+ const schemas = new Map();
264
+ for (const name of Object.keys(m.tool_defs))
265
+ schemas.set(name, null);
266
+ if (isObj(tools) && Array.isArray(tools.tools))
267
+ for (const t of tools.tools.filter(isObj))
268
+ if (typeof t.name === "string")
269
+ schemas.set(t.name, t.inputSchema ?? null);
270
+ listed.set(m.server, schemas);
271
+ toolText += `\n${normaliseValue(text())}`;
272
+ }
273
+ else if (e.kind === "tool.result" || (e.kind === "sdk.event" && m.type === "tool.result")) {
274
+ toolText += `\n${normaliseValue(text())}`;
275
+ }
276
+ else if (e.kind === "sdk.event" && m.type === "tool.call" && typeof m.name === "string") {
277
+ if (!called.has(m.name))
278
+ called.set(m.name, e.seq);
279
+ }
280
+ else if (e.kind === "tool.call" && m.method === "tools/call" && typeof m.tool === "string") {
281
+ if (!called.has(m.tool))
282
+ called.set(m.tool, e.seq);
283
+ if (m.policy?.action === "deny")
284
+ continue;
285
+ const server = typeof m.server === "string" ? m.server : "";
286
+ const schemas = listed.get(server);
287
+ if (schemas && !schemas.has(m.tool)) {
288
+ push(e.seq, unknownTool(e.seq, m.tool, "mcp", server));
289
+ continue;
290
+ }
291
+ const params = rpcOf(bodies(e.body_hash))?.params;
292
+ const args = isObj(params) ? (params.arguments ?? {}) : {};
293
+ const problem = schemaProblem(schemas?.get(m.tool), args);
294
+ if (problem)
295
+ push(e.seq, argsInvalid(e.seq, m.tool, "mcp", problem, server));
296
+ // ponytail: without a recorded model request there's no input to compare with (SDK-only runs)
297
+ if (sawModel)
298
+ for (const v of identifiersIn(args))
299
+ if (!request.includes(v.value) && !toolText.includes(v.value))
300
+ push(e.seq, fabricated(e.seq, m.tool, "mcp", v, server));
301
+ }
302
+ }
303
+ // answers: success right after an error, and tools quoted that never ran
304
+ const known = new Set();
305
+ for (const c of [...calls].sort((a, b) => a.endSeq - b.endSeq)) {
306
+ for (const name of declaredTools(c.request)?.keys() ?? [])
307
+ known.add(name);
308
+ for (const [, schemas] of listed)
309
+ for (const name of schemas.keys())
310
+ known.add(name);
311
+ if (!c.complete || modelCalls.some((t) => t.seq === c.endSeq))
312
+ continue;
313
+ const text = answerText(c);
314
+ if (!text)
315
+ continue;
316
+ const last = results.filter((r) => r.seq <= c.requestSeq).at(-1);
317
+ if (last?.error && SUCCESS.test(text) && !FAILURE.test(text))
318
+ push(c.endSeq, {
319
+ code: "SUCCESS_AFTER_ERROR",
320
+ source: "hallucination",
321
+ severity: "warning",
322
+ ref: { seq: c.endSeq, error_seq: last.seq },
323
+ detail: "The agent said it worked right after its last tool call failed, with nothing succeeding in between.",
324
+ });
325
+ for (const name of [...known].sort()) {
326
+ if (name.length < 4)
327
+ continue;
328
+ const first = called.get(name);
329
+ if (first !== undefined && first < c.endSeq)
330
+ continue;
331
+ const cite = new RegExp(`${CITES}\\W+(?:the\\W+)?\`?${escapeRe(name)}\\b`, "i").exec(text);
332
+ if (cite && !NEGATED.test(text.slice(Math.max(0, cite.index - 16), cite.index)))
333
+ push(c.endSeq, {
334
+ code: "PHANTOM_RESULT",
335
+ source: "hallucination",
336
+ severity: "warning",
337
+ ref: { seq: c.endSeq, tool: name },
338
+ detail: `The answer cites ${name}, which the agent never called before it: the result it quotes didn't come from that tool.`,
339
+ });
340
+ }
341
+ }
342
+ return out.sort((a, b) => a.at - b.at).map(({ at: _, ...rest }) => rest);
343
+ }
344
+ function unknownTool(seq, tool, via, server) {
345
+ return f("UNKNOWN_TOOL", { seq, tool, via, ...(server ? { server } : {}) }, via === "model"
346
+ ? `The model called ${tool}, which wasn't among the tools it was given.`
347
+ : `The agent called ${tool}, which the server ${server} never listed.`);
348
+ }
349
+ function argsInvalid(seq, tool, via, problem, server) {
350
+ return {
351
+ ...f("TOOL_ARGS_INVALID", { seq, tool, via, problem, ...(server ? { server } : {}) }, `The arguments to ${tool} don't fit its schema (${problem}).`),
352
+ severity: "caution",
353
+ };
354
+ }
355
+ function fabricated(seq, tool, via, v, server) {
356
+ return f("FABRICATED_VALUE", { seq, tool, via, kind: v.kind, path: v.path, ...(server ? { server } : {}) }, `The ${v.kind} at ${v.path} in the call to ${tool} appears nowhere the agent could have read it: no input or earlier tool result holds it.`);
357
+ }
@@ -3,7 +3,13 @@ import { type DutyLimits } from "../duty/index.ts";
3
3
  import { type CompiledPolicy } from "../policy/index.ts";
4
4
  import { type PrecogModel } from "../precog/index.ts";
5
5
  import type { Envelope, Json } from "../verify/index.ts";
6
+ import type { ReferencePack } from "./reference.ts";
7
+ export { type Accuracy, detectorAccuracy, type Label, wilsonLow } from "./accuracy.ts";
8
+ export { type AnswerClaims, answerClaims, answerCredential, type ClaimEntry, verifyAnswerCredential, } from "./credential.ts";
9
+ export { answerRisk, answersOf, canonicalNumber, Evidence, type Fact, factsIn, type GroundedClaim, groundingFindings, groundingRequest, HOLD_TRIGGERS, judgeFindings, judgeRequest, modelGroundingFindings, parseGroundingAnswer, parseJudgeAnswer, } from "./grounding.ts";
10
+ export { answerText, identifiersIn, normaliseValue, schemaProblem, toolHallucinations, } from "./hallucination.ts";
6
11
  export { type ClaimVerdict, type CriterionResult, checkFlightPlan, type Landing, landing, } from "./landing.ts";
12
+ export { checkAgainstPacks, loadReferencePacks, packsFor, type ReferenceFact, type ReferencePack, type ReferenceVerdict, referenceContext, } from "./reference.ts";
7
13
  export { OUTCOMES, type Outcome, type OutcomeRollup, rollup, type WasteReason, type WasteReport, waste, } from "./waste.ts";
8
14
  export type Severity = "advisory" | "caution" | "warning";
9
15
  export interface Finding {
@@ -27,6 +33,10 @@ export interface AnalyzeOptions {
27
33
  policy?: CompiledPolicy;
28
34
  /** spec/precog.md §3: the early-failure model, for PRECOG_RISK (needs `prices`). */
29
35
  precog?: PrecogModel;
36
+ /** spec/governance.md §5: the walls model answers are checked against (WALL_CROSSED). */
37
+ walls?: readonly string[];
38
+ /** spec/findings.md §10: the reference packs answers are checked against (the session's tenant's). */
39
+ reference?: readonly ReferencePack[];
30
40
  /** Epoch ms; +Infinity = judge everything (a session being closed). Default: now. */
31
41
  now?: number;
32
42
  graceMs?: number;
@@ -41,6 +51,12 @@ export interface Ev extends Envelope {
41
51
  export declare function eventsOf(lines: readonly string[], bodies: AnalyzeOptions["bodies"]): Ev[];
42
52
  /** Every analysis, in spec order. */
43
53
  export declare function analyze(lines: readonly string[], options: AnalyzeOptions): Finding[];
54
+ /**
55
+ * spec/approvals.md §2: the agent ran an approved tool with other arguments than the person saw.
56
+ * An SDK `tool.call` that names its `approval_id` carries the hash of what it ran (`args_sha256`);
57
+ * the `approval_request` carries the hash of what was asked. A difference is a finding.
58
+ */
59
+ export declare function approvalArgsChanged(events: Ev[]): Finding[];
44
60
  /** spec/attestation.md §3: a re-run that printed something else than the model was given. */
45
61
  export declare function attestationFindings(events: readonly Ev[]): Finding[];
46
62
  /** spec/fleet.md §3: the agent's own near-miss reports. */
@@ -59,6 +75,13 @@ export declare function checkpointReplay(events: Ev[]): Finding[];
59
75
  export declare function orphanedToolCalls(events: Ev[], now: number): Finding[];
60
76
  /** N4 (idea R5): a LangGraph run in `exit` durability that ran tools. Once per session. */
61
77
  export declare function exitDurability(events: Ev[]): Finding[];
78
+ /**
79
+ * spec/findings.md §1a: a tool's definition changed during the session (the MCP "rug pull"). Every
80
+ * definition the gateway saw is an observation: each `tools/list` answer's `tool_defs`, and each
81
+ * `tools/call`'s `tool_def` (the definition it was judged by). Per server and tool, the first one
82
+ * is the reference; a different one later is a finding, and becomes the new reference.
83
+ */
84
+ export declare function toolChanged(events: Ev[]): Finding[];
62
85
  /** N6 (idea S3): the gateway refused a request that carried the session's own token. */
63
86
  export declare function tokenMisuse(events: Ev[]): Finding[];
64
87
  /** Stage 2 R3: a `verify` check that passed, then failed later in the run. */
@@ -3,15 +3,25 @@
3
3
  import { approvalFindings } from "../approvals/index.js";
4
4
  import { automationSurprise } from "../authority/index.js";
5
5
  import { dutyFindings } from "../duty/index.js";
6
+ import { financeFindings } from "../finance/index.js";
7
+ import { healthFindings } from "../health/index.js";
6
8
  import { policyFindings } from "../policy/index.js";
7
9
  import { precogFindings } from "../precog/index.js";
8
10
  import { preflightFindings } from "../preflight/index.js";
9
11
  import { canonical } from "../reconcile/shared.js";
10
12
  import { undoFindings } from "../undo/index.js";
13
+ import { wallFindings } from "../walls/index.js";
11
14
  import { detectors } from "./detectors.js";
15
+ import { groundingFindings, judgeFindings } from "./grounding.js";
16
+ import { toolHallucinations } from "./hallucination.js";
12
17
  import { flightPlan } from "./landing.js";
13
18
  import { memoryFindings } from "./memory.js";
19
+ export { detectorAccuracy, wilsonLow } from "./accuracy.js";
20
+ export { answerClaims, answerCredential, verifyAnswerCredential, } from "./credential.js";
21
+ export { answerRisk, answersOf, canonicalNumber, Evidence, factsIn, groundingFindings, groundingRequest, HOLD_TRIGGERS, judgeFindings, judgeRequest, modelGroundingFindings, parseGroundingAnswer, parseJudgeAnswer, } from "./grounding.js";
22
+ export { answerText, identifiersIn, normaliseValue, schemaProblem, toolHallucinations, } from "./hallucination.js";
14
23
  export { checkFlightPlan, landing, } from "./landing.js";
24
+ export { checkAgainstPacks, loadReferencePacks, packsFor, referenceContext, } from "./reference.js";
15
25
  export { OUTCOMES, rollup, waste, } from "./waste.js";
16
26
  const decoder = new TextDecoder();
17
27
  /** Parses the lines once, attaching SDK event bodies. */
@@ -45,8 +55,12 @@ export function analyze(lines, options) {
45
55
  ...checkpointReplay(events),
46
56
  ...orphanedToolCalls(events, now),
47
57
  ...tokenMisuse(events),
58
+ ...toolChanged(events),
48
59
  ...verifyRegression(events),
49
60
  ...falseSuccess(events, now),
61
+ ...toolHallucinations(lines, options.bodies),
62
+ ...groundingFindings(lines, options.bodies, options.reference ?? []),
63
+ ...judgeFindings(lines, options.bodies),
50
64
  ...exitDurability(events),
51
65
  ...detectors(lines, {
52
66
  bodies: options.bodies,
@@ -56,12 +70,16 @@ export function analyze(lines, options) {
56
70
  }),
57
71
  ...flightPlan(events, lines, options.bodies),
58
72
  ...(options.policy ? policyFindings(lines, options.bodies, options.policy) : []),
73
+ ...(options.walls?.length ? wallFindings(lines, options.bodies, options.walls) : []),
59
74
  ...nearMisses(events),
60
75
  ...memoryFindings(events),
61
76
  ...attestationFindings(events),
62
77
  ...automationSurprise(lines),
63
78
  ...undoFindings(lines, options.bodies),
64
79
  ...approvalFindings(lines),
80
+ ...financeFindings(lines, options.bodies, now === Number.POSITIVE_INFINITY),
81
+ ...healthFindings(lines, options.bodies, now === Number.POSITIVE_INFINITY),
82
+ ...approvalArgsChanged(events),
65
83
  ...preflightFindings(lines),
66
84
  ...dutyFindings(lines, options.prices, options.duty),
67
85
  ...(options.precog && options.prices
@@ -69,6 +87,38 @@ export function analyze(lines, options) {
69
87
  : []),
70
88
  ];
71
89
  }
90
+ /**
91
+ * spec/approvals.md §2: the agent ran an approved tool with other arguments than the person saw.
92
+ * An SDK `tool.call` that names its `approval_id` carries the hash of what it ran (`args_sha256`);
93
+ * the `approval_request` carries the hash of what was asked. A difference is a finding.
94
+ */
95
+ export function approvalArgsChanged(events) {
96
+ const asked = new Map();
97
+ const out = [];
98
+ for (const e of events) {
99
+ if (e.kind === "control" &&
100
+ e.meta.action === "approval_request" &&
101
+ typeof e.meta.approval_id === "string" &&
102
+ typeof e.meta.args_sha256 === "string")
103
+ asked.set(e.meta.approval_id, e.meta.args_sha256);
104
+ if (e.kind !== "sdk.event" || e.meta.type !== "tool.call")
105
+ continue;
106
+ const id = e.data?.approval_id;
107
+ const ran = e.data?.args_sha256;
108
+ if (typeof id !== "string" || typeof ran !== "string")
109
+ continue;
110
+ const want = asked.get(id);
111
+ if (want !== undefined && want !== ran)
112
+ out.push({
113
+ code: "APPROVAL_ARGS_CHANGED",
114
+ source: "approval",
115
+ severity: "warning",
116
+ ref: { sdk_seq: e.meta.sdk_seq, approval_id: id },
117
+ detail: "The agent ran an approved tool with other arguments than the person approved.",
118
+ });
119
+ }
120
+ return out;
121
+ }
72
122
  /** spec/attestation.md §3: a re-run that printed something else than the model was given. */
73
123
  export function attestationFindings(events) {
74
124
  return events
@@ -308,6 +358,49 @@ export function exitDurability(events) {
308
358
  },
309
359
  ];
310
360
  }
361
+ /**
362
+ * spec/findings.md §1a: a tool's definition changed during the session (the MCP "rug pull"). Every
363
+ * definition the gateway saw is an observation: each `tools/list` answer's `tool_defs`, and each
364
+ * `tools/call`'s `tool_def` (the definition it was judged by). Per server and tool, the first one
365
+ * is the reference; a different one later is a finding, and becomes the new reference.
366
+ */
367
+ export function toolChanged(events) {
368
+ const seen = new Map();
369
+ const out = [];
370
+ const observe = (seq, server, tool, def) => {
371
+ const key = `${server}\u0000${tool}`;
372
+ const before = seen.get(key);
373
+ seen.set(key, def);
374
+ if (before !== undefined && before !== def)
375
+ out.push({
376
+ code: "TOOL_CHANGED",
377
+ source: "detectors",
378
+ severity: "warning",
379
+ ref: { seq, server, tool, before, after: def },
380
+ detail: "A tool's description, schema or annotations changed after the agent first saw it: approvals and policy were judged on the old definition.",
381
+ });
382
+ };
383
+ for (const e of events) {
384
+ const server = e.meta.server;
385
+ if (typeof server !== "string")
386
+ continue;
387
+ const defs = e.meta.tool_defs;
388
+ if (e.kind === "tool.result" &&
389
+ typeof defs === "object" &&
390
+ defs !== null &&
391
+ !Array.isArray(defs))
392
+ for (const tool of Object.keys(defs).sort()) {
393
+ const def = defs[tool];
394
+ if (typeof def === "string")
395
+ observe(e.seq, server, tool, def);
396
+ }
397
+ if (e.kind === "tool.call" &&
398
+ typeof e.meta.tool === "string" &&
399
+ typeof e.meta.tool_def === "string")
400
+ observe(e.seq, server, e.meta.tool, e.meta.tool_def);
401
+ }
402
+ return out;
403
+ }
311
404
  /** N6 (idea S3): the gateway refused a request that carried the session's own token. */
312
405
  export function tokenMisuse(events) {
313
406
  return events
@@ -8,6 +8,14 @@ export declare function memoryXrayOf(events: readonly Ev[], revokedElsewhere?: r
8
8
  seq: number;
9
9
  memory_id: string;
10
10
  }[];
11
+ chain?: {
12
+ ok: boolean;
13
+ length: number;
14
+ broken: Array<{
15
+ seq: number;
16
+ reason: string;
17
+ }>;
18
+ };
11
19
  };
12
20
  /** The findings part (this record's own revocations only). */
13
21
  export declare function memoryFindings(events: readonly Ev[]): Finding[];
@@ -1,16 +1,21 @@
1
1
  // The Memory X-ray (spec/agents.md §2). Mirrors analysis/memory.py.
2
+ import { memoryChain } from "../memory/index.js";
2
3
  /** spec/agents.md §2, over parsed events (sdk.event bodies attached). */
3
4
  export function memoryXrayOf(events, revokedElsewhere = []) {
4
5
  const revoked = new Set(revokedElsewhere);
5
6
  const stale = [];
7
+ const entries = [];
6
8
  let writes = 0;
7
9
  let reads = 0;
8
10
  for (const e of events) {
9
11
  if (e.kind !== "sdk.event")
10
12
  continue;
11
13
  const d = e.data ?? {};
12
- if (e.meta.type === "memory.write")
14
+ if (e.meta.type === "memory.write") {
13
15
  writes++;
16
+ if (d.entry && typeof d.entry === "object" && !Array.isArray(d.entry))
17
+ entries.push([e.seq, d.entry]);
18
+ }
14
19
  else if (e.meta.type === "memory.revoke" && typeof d.memory_id === "string")
15
20
  revoked.add(d.memory_id);
16
21
  else if (e.meta.type === "memory.read") {
@@ -20,14 +25,36 @@ export function memoryXrayOf(events, revokedElsewhere = []) {
20
25
  stale.push({ seq: e.seq, memory_id: id });
21
26
  }
22
27
  }
23
- return { writes, reads, revoked: [...revoked].sort(), stale_reads: stale };
28
+ const out = { writes, reads, revoked: [...revoked].sort(), stale_reads: stale };
29
+ if (entries.length > 0) {
30
+ // spec/agents.md §4: the Zanii memory chain the writes carry
31
+ const chain = memoryChain(entries.map(([, p]) => p));
32
+ out.chain = {
33
+ ok: chain.ok,
34
+ length: chain.length,
35
+ broken: chain.broken.map((b) => ({
36
+ seq: entries[b.index][0],
37
+ reason: b.reason,
38
+ })),
39
+ };
40
+ }
41
+ return out;
24
42
  }
25
43
  /** The findings part (this record's own revocations only). */
26
44
  export function memoryFindings(events) {
27
- return memoryXrayOf(events).stale_reads.map((r) => ({
28
- code: "REVOKED_MEMORY_READ",
29
- source: "memory",
30
- severity: "warning",
31
- ref: { seq: r.seq, memory_id: r.memory_id },
32
- }));
45
+ const xray = memoryXrayOf(events);
46
+ return [
47
+ ...xray.stale_reads.map((r) => ({
48
+ code: "REVOKED_MEMORY_READ",
49
+ source: "memory",
50
+ severity: "warning",
51
+ ref: { seq: r.seq, memory_id: r.memory_id },
52
+ })),
53
+ ...(xray.chain?.broken ?? []).map((b) => ({
54
+ code: "MEMORY_CHAIN_BROKEN",
55
+ source: "memory",
56
+ severity: "warning",
57
+ ref: { seq: b.seq, reason: b.reason },
58
+ })),
59
+ ];
33
60
  }