@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,343 @@
1
+ // Framework packs (spec/packs.md): compliance and occurrence frameworks as data a deployment
2
+ // switches on. Pure; mirrors sdks/python/src/zanii_blackbox/packs.py; pinned by
3
+ // spec/vectors/packs.json. The `data` part is spec/data.md §4.
4
+ import { CORE_EVIDENCE, CORE_IDENTIFIERS } from "../data/index.js";
5
+ const FACTS = new Set([
6
+ "sessions",
7
+ "events",
8
+ "sessions_verified",
9
+ "sessions_anchored",
10
+ "redactions",
11
+ "findings_warning",
12
+ "bypasses",
13
+ "false_claims",
14
+ "blocks",
15
+ "emergency_stops",
16
+ "resumes_with_note",
17
+ "outcomes",
18
+ "data_region",
19
+ ]);
20
+ const FILLS = new Set(["record", "findings", "redactions", "operator"]);
21
+ const PACK_ID = /^[a-z0-9-]{1,64}$/;
22
+ const FRAMEWORK_ID = /^[a-z0-9_-]{1,64}$/;
23
+ const DATE = /^\d{4}-\d{2}-\d{2}$/;
24
+ const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
25
+ // Lengths in code points, as Python counts them (spec/packs.md §1).
26
+ const text = (v, max) => typeof v === "string" && v.length >= 1 && [...v].length <= max;
27
+ const only = (o, keys) => Object.keys(o).every((k) => keys.includes(k));
28
+ function checkSections(sections, max, at, check) {
29
+ if (!Array.isArray(sections) || sections.length < 1 || sections.length > max)
30
+ return `${at}.sections must hold 1-${max} sections`;
31
+ const ids = new Set();
32
+ for (const [i, s] of sections.entries()) {
33
+ const sat = `${at}.sections[${i}]`;
34
+ if (!isObj(s))
35
+ return `${sat} must be an object`;
36
+ if (typeof s.id !== "string" || !FRAMEWORK_ID.test(s.id))
37
+ return `${sat}.id must be 1-64 of a-z 0-9 _ -`;
38
+ if (ids.has(s.id))
39
+ return `${sat}.id ${s.id} repeats`;
40
+ ids.add(s.id);
41
+ if (!text(s.title, 500))
42
+ return `${sat}.title must be 1-500 characters`;
43
+ const bad = check(s, sat);
44
+ if (bad)
45
+ return bad;
46
+ }
47
+ return undefined;
48
+ }
49
+ function checkPart(part, kind) {
50
+ if (!isObj(part) || !only(part, ["notes", "notes_ar", "frameworks"]))
51
+ return `${kind} must be {notes, frameworks}`;
52
+ if (!text(part.notes, 2000))
53
+ return `${kind}.notes must be 1-2000 characters`;
54
+ if (part.notes_ar !== undefined && !text(part.notes_ar, 2000))
55
+ return `${kind}.notes_ar must be 1-2000 characters`;
56
+ const fws = part.frameworks;
57
+ if (!isObj(fws) || Object.keys(fws).length < 1 || Object.keys(fws).length > 50)
58
+ return `${kind}.frameworks must hold 1-50 frameworks`;
59
+ for (const [id, fw] of Object.entries(fws)) {
60
+ const at = `${kind}.frameworks.${id}`;
61
+ if (!FRAMEWORK_ID.test(id))
62
+ return `${at}: the id must be 1-64 of a-z 0-9 _ -`;
63
+ if (!isObj(fw) || !text(fw.title, 500))
64
+ return `${at}.title must be 1-500 characters`;
65
+ if (kind === "compliance") {
66
+ if (!only(fw, ["title", "sections"]))
67
+ return `${at} may only have title and sections`;
68
+ const bad = checkSections(fw.sections, 50, at, (s, sat) => {
69
+ if (!only(s, ["id", "title", "requirement", "facts"]))
70
+ return `${sat} may only have id, title, requirement, facts`;
71
+ if (!text(s.requirement, 2000))
72
+ return `${sat}.requirement must be 1-2000 characters`;
73
+ if (!Array.isArray(s.facts) || s.facts.length < 1 || s.facts.length > 20)
74
+ return `${sat}.facts must hold 1-20 fact keys`;
75
+ const unknown = s.facts.find((f) => typeof f !== "string" || !FACTS.has(f));
76
+ return unknown === undefined
77
+ ? undefined
78
+ : `${sat}.facts: unknown fact ${JSON.stringify(unknown)}`;
79
+ });
80
+ if (bad)
81
+ return bad;
82
+ }
83
+ else {
84
+ if (!only(fw, ["title", "authority", "deadlines", "sections", "ar"]))
85
+ return `${at} may only have title, authority, deadlines, sections`;
86
+ if (!text(fw.authority, 500))
87
+ return `${at}.authority must be 1-500 characters`;
88
+ if (!Array.isArray(fw.deadlines) ||
89
+ fw.deadlines.length < 1 ||
90
+ fw.deadlines.length > 10 ||
91
+ !fw.deadlines.every((d) => text(d, 500)))
92
+ return `${at}.deadlines must hold 1-10 texts of 1-500 characters`;
93
+ const bad = checkSections(fw.sections, 30, at, (s, sat) => {
94
+ if (!only(s, ["id", "title", "fill"]))
95
+ return `${sat} may only have id, title, fill`;
96
+ return FILLS.has(s.fill)
97
+ ? undefined
98
+ : `${sat}.fill must be record, findings, redactions or operator`;
99
+ });
100
+ if (bad)
101
+ return bad;
102
+ if (fw.ar !== undefined) {
103
+ const badAr = checkArabic(fw.ar, fw, `${at}.ar`);
104
+ if (badAr)
105
+ return badAr;
106
+ }
107
+ }
108
+ }
109
+ return undefined;
110
+ }
111
+ /** spec/packs.md §1: an occurrence framework's Arabic: the same deadlines and sections, translated. */
112
+ function checkArabic(ar, fw, at) {
113
+ if (!isObj(ar) || !only(ar, ["title", "authority", "deadlines", "sections"]))
114
+ return `${at} must be {title, authority, deadlines, sections}`;
115
+ if (!text(ar.title, 500) || !text(ar.authority, 500))
116
+ return `${at}.title and .authority must be 1-500 characters`;
117
+ const deadlines = fw.deadlines;
118
+ if (!Array.isArray(ar.deadlines) ||
119
+ ar.deadlines.length !== deadlines.length ||
120
+ !ar.deadlines.every((d) => text(d, 500)))
121
+ return `${at}.deadlines must translate each deadline`;
122
+ const ids = fw.sections.map((s) => s.id);
123
+ const titles = ar.sections;
124
+ if (!isObj(titles) ||
125
+ Object.keys(titles).length !== ids.length ||
126
+ !ids.every((id) => text(titles[id], 500)))
127
+ return `${at}.sections must translate each section's title, by id`;
128
+ return undefined;
129
+ }
130
+ /** spec/packs.md §1: why a pack is invalid, or undefined. */
131
+ export function checkPack(value) {
132
+ if (!isObj(value))
133
+ return "a pack must be an object";
134
+ // `data` came later (spec/data.md §4); the pinned messages below name the v1 keys only.
135
+ if (!only(value, [
136
+ "data",
137
+ "v",
138
+ "id",
139
+ "title",
140
+ "status",
141
+ "reviewed_by",
142
+ "reviewed_at",
143
+ "compliance",
144
+ "occurrence",
145
+ ]))
146
+ return "a pack may only have v, id, title, status, reviewed_by, reviewed_at, compliance, occurrence";
147
+ if (value.v !== 1)
148
+ return "v must be 1";
149
+ if (typeof value.id !== "string" || !PACK_ID.test(value.id))
150
+ return "id must be 1-64 of a-z 0-9 -";
151
+ if (!text(value.title, 200))
152
+ return "title must be 1-200 characters";
153
+ if (value.status !== "draft" && value.status !== "reviewed")
154
+ return "status must be draft or reviewed";
155
+ if (value.status === "reviewed" &&
156
+ (!text(value.reviewed_by, 200) ||
157
+ typeof value.reviewed_at !== "string" ||
158
+ !DATE.test(value.reviewed_at)))
159
+ return "a reviewed pack needs reviewed_by and reviewed_at (YYYY-MM-DD)";
160
+ if (value.compliance === undefined && value.occurrence === undefined && value.data === undefined)
161
+ return "a pack needs compliance, occurrence or both";
162
+ for (const kind of ["compliance", "occurrence"])
163
+ if (value[kind] !== undefined) {
164
+ const bad = checkPart(value[kind], kind);
165
+ if (bad)
166
+ return bad;
167
+ }
168
+ return value.data === undefined ? undefined : checkData(value.data);
169
+ }
170
+ const IDENTIFIER_ID = /^[a-z0-9_]{1,64}$/;
171
+ const NORMALIZE = new Set(["digits", "lower", "upper", "exact"]);
172
+ // Escapes Python reads over Unicode and JavaScript over ASCII (spec/data.md §1).
173
+ const UNPORTABLE = /\\[dDwWbBsS]/;
174
+ const ALLOW = /^(?:region|model|tool|api):(?:[A-Za-z0-9._-]{1,128}\*?|\*)$/;
175
+ const TOOL = /^[A-Za-z0-9._-]{1,128}$/;
176
+ const FIELD = /^[A-Za-z0-9_.-]{1,64}$/;
177
+ const compiles = (pattern) => {
178
+ try {
179
+ new RegExp(pattern);
180
+ return true;
181
+ }
182
+ catch {
183
+ return false;
184
+ }
185
+ };
186
+ /** spec/data.md §1: why an identifier is invalid, or undefined. */
187
+ export function checkIdentifier(v, at) {
188
+ if (!isObj(v) ||
189
+ !only(v, ["id", "title", "pattern", "normalize", "keep_last", "check", "personal"]))
190
+ return `${at} may only have id, title, pattern, normalize, keep_last, check, personal`;
191
+ if (typeof v.id !== "string" || !IDENTIFIER_ID.test(v.id))
192
+ return `${at}.id must be 1-64 of a-z 0-9 _`;
193
+ if (!text(v.title, 200))
194
+ return `${at}.title must be 1-200 characters`;
195
+ if (typeof v.pattern !== "string" || !text(v.pattern, 500) || !compiles(v.pattern))
196
+ return `${at}.pattern must be a regular expression of 1-500 characters`;
197
+ if (UNPORTABLE.test(v.pattern))
198
+ return `${at}.pattern must not use \\d \\w \\b or \\s`;
199
+ if (!NORMALIZE.has(v.normalize))
200
+ return `${at}.normalize must be digits, lower, upper or exact`;
201
+ if (v.keep_last !== undefined &&
202
+ (!Number.isInteger(v.keep_last) || v.keep_last < 1 || v.keep_last > 30))
203
+ return `${at}.keep_last must be 1-30`;
204
+ if (v.check !== undefined && v.check !== "luhn")
205
+ return `${at}.check must be luhn`;
206
+ if (typeof v.personal !== "boolean")
207
+ return `${at}.personal must be true or false`;
208
+ return undefined;
209
+ }
210
+ function checkData(part) {
211
+ if (!isObj(part) || !only(part, ["notes", "identifiers", "residency", "restricted", "evidence"]))
212
+ return "data may only have notes, identifiers, residency, restricted, evidence";
213
+ if (!text(part.notes, 2000))
214
+ return "data.notes must be 1-2000 characters";
215
+ const ids = part.identifiers ?? [];
216
+ if (!Array.isArray(ids) || ids.length > 50)
217
+ return "data.identifiers must hold 0-50 identifiers";
218
+ const seen = new Set(CORE_IDENTIFIERS.map((i) => i.id));
219
+ for (const [i, v] of ids.entries()) {
220
+ const bad = checkIdentifier(v, `data.identifiers[${i}]`);
221
+ if (bad)
222
+ return bad;
223
+ const id = v.id;
224
+ if (seen.has(id))
225
+ return `data.identifiers[${i}].id ${id} is already defined`;
226
+ seen.add(id);
227
+ }
228
+ if (part.residency !== undefined && typeof part.residency !== "boolean")
229
+ return "data.residency must be true or false";
230
+ const rules = part.restricted ?? [];
231
+ if (!Array.isArray(rules) || rules.length > 50)
232
+ return "data.restricted must hold 0-50 rules";
233
+ for (const [i, r] of rules.entries()) {
234
+ const at = `data.restricted[${i}]`;
235
+ if (!isObj(r) || !only(r, ["identifiers", "allow"]))
236
+ return `${at} must be {identifiers, allow}`;
237
+ if (!Array.isArray(r.identifiers) ||
238
+ r.identifiers.length < 1 ||
239
+ r.identifiers.length > 50 ||
240
+ !r.identifiers.every((x) => typeof x === "string" && seen.has(x)))
241
+ return `${at}.identifiers must hold 1-50 known identifier ids`;
242
+ if (!Array.isArray(r.allow) ||
243
+ r.allow.length < 1 ||
244
+ r.allow.length > 20 ||
245
+ !r.allow.every((x) => typeof x === "string" && ALLOW.test(x)))
246
+ return `${at}.allow must hold 1-20 of region:, model:, tool:, api:`;
247
+ }
248
+ const evidence = part.evidence ?? [];
249
+ if (!Array.isArray(evidence) || evidence.length > 200)
250
+ return "data.evidence must hold 0-200 rules";
251
+ for (const [i, e] of evidence.entries())
252
+ if (!isObj(e) ||
253
+ !only(e, ["tool", "field"]) ||
254
+ typeof e.tool !== "string" ||
255
+ !TOOL.test(e.tool) ||
256
+ typeof e.field !== "string" ||
257
+ !FIELD.test(e.field))
258
+ return `data.evidence[${i}] must be {tool, field}`;
259
+ return undefined;
260
+ }
261
+ /**
262
+ * spec/data.md §4: the data rules of packs merged in order, then the core. Pack identifiers come
263
+ * before the core ones; throws on an invalid pack or an identifier id in two packs.
264
+ */
265
+ export function packData(packs) {
266
+ const identifiers = [];
267
+ const restricted = [];
268
+ const evidence = [];
269
+ const owner = new Map();
270
+ let residency = false;
271
+ for (const p of packs) {
272
+ const bad = checkPack(p);
273
+ const id = isObj(p) && typeof p.id === "string" ? p.id : "?";
274
+ if (bad)
275
+ throw new Error(`pack ${id}: ${bad}`);
276
+ const data = p.data;
277
+ if (!data)
278
+ continue;
279
+ for (const ident of data.identifiers ?? []) {
280
+ const had = owner.get(ident.id);
281
+ if (had)
282
+ throw new Error(`identifier ${ident.id} is in packs ${had} and ${id}`);
283
+ owner.set(ident.id, id);
284
+ identifiers.push(ident);
285
+ }
286
+ residency ||= data.residency === true;
287
+ restricted.push(...(data.restricted ?? []));
288
+ evidence.push(...(data.evidence ?? []));
289
+ }
290
+ identifiers.push(...CORE_IDENTIFIERS);
291
+ return { identifiers, residency, restricted, evidence: [...evidence, ...CORE_EVIDENCE] };
292
+ }
293
+ /** spec/packs.md §2: the core pack, from the built-in frameworks files. */
294
+ export function corePack(compliance, occurrence) {
295
+ return {
296
+ v: 1,
297
+ id: "core",
298
+ title: "Core (built in)",
299
+ status: "draft",
300
+ compliance: { notes: compliance.notes, frameworks: compliance.frameworks },
301
+ occurrence: {
302
+ notes: occurrence.notes,
303
+ ...(occurrence.notes_ar ? { notes_ar: occurrence.notes_ar } : {}),
304
+ frameworks: occurrence.frameworks,
305
+ },
306
+ };
307
+ }
308
+ /** spec/packs.md §2: packs merged in order; throws on an invalid pack or a repeated framework id. */
309
+ export function packFrameworks(packs) {
310
+ const out = {
311
+ compliance: {},
312
+ occurrence: {},
313
+ };
314
+ const seen = new Set();
315
+ for (const p of packs) {
316
+ const bad = checkPack(p);
317
+ const id = isObj(p) && typeof p.id === "string" ? p.id : "?";
318
+ if (bad)
319
+ throw new Error(`pack ${id}: ${bad}`);
320
+ const pack = p;
321
+ if (seen.has(id))
322
+ throw new Error(`pack ${id} is listed twice`);
323
+ seen.add(id);
324
+ for (const kind of ["compliance", "occurrence"]) {
325
+ const part = pack[kind];
326
+ if (!part)
327
+ continue;
328
+ for (const [fid, fw] of Object.entries(part.frameworks)) {
329
+ const had = out[kind][fid];
330
+ if (had)
331
+ throw new Error(`${kind} framework ${fid} is in packs ${had.pack} and ${id}`);
332
+ out[kind][fid] = {
333
+ pack: id,
334
+ status: pack.status,
335
+ notes: part.notes,
336
+ ...(part.notes_ar ? { notes_ar: part.notes_ar } : {}),
337
+ framework: fw,
338
+ };
339
+ }
340
+ }
341
+ }
342
+ return out;
343
+ }
@@ -0,0 +1,11 @@
1
+ import type { Policy } from "./index.ts";
2
+ export interface PolicyChange {
3
+ rule: string;
4
+ change: "added" | "removed" | "weakened" | "strengthened" | "modified" | "moved";
5
+ adds_power: boolean;
6
+ }
7
+ export interface PolicyDelta {
8
+ adds_power: boolean;
9
+ changes: PolicyChange[];
10
+ }
11
+ export declare function policyDelta(current: Policy, proposed: Policy): PolicyDelta;
@@ -0,0 +1,39 @@
1
+ // Policy deltas (spec/policy.md §6, idea C4): does a proposed policy give agents more power than the
2
+ // current one? A change that adds none can be applied without a second person. Conservative: when
3
+ // in doubt, it adds power. Mirrors policy/delta.py.
4
+ /** How much a rule holds back: an enforced allow opens (0), an audit rule does nothing (1), a hold
5
+ * (2) and a deny (3) close. */
6
+ const rank = (r) => r.audit === true ? 1 : r.action === "allow" ? 0 : r.action === "require_approval" ? 2 : 3;
7
+ const matcher = (r) => JSON.stringify([r.tool, r.args_match ?? null, r.ignore_case === true]);
8
+ export function policyDelta(current, proposed) {
9
+ const before = new Map(current.rules.map((r) => [r.id, r]));
10
+ const after = new Map(proposed.rules.map((r) => [r.id, r]));
11
+ const changes = [];
12
+ for (const r of proposed.rules) {
13
+ const old = before.get(r.id);
14
+ if (!old) {
15
+ changes.push({ rule: r.id, change: "added", adds_power: rank(r) < 1 });
16
+ continue;
17
+ }
18
+ if (matcher(old) !== matcher(r))
19
+ changes.push({ rule: r.id, change: "modified", adds_power: rank(old) > 1 || rank(r) < 1 });
20
+ else if (rank(r) !== rank(old))
21
+ changes.push({
22
+ rule: r.id,
23
+ change: rank(r) < rank(old) ? "weakened" : "strengthened",
24
+ adds_power: rank(r) < rank(old),
25
+ });
26
+ }
27
+ for (const r of current.rules)
28
+ if (!after.has(r.id))
29
+ changes.push({ rule: r.id, change: "removed", adds_power: rank(r) > 1 });
30
+ // First match wins, so the order of the rules both keep matters: any move may open something.
31
+ const kept = (rules, other) => rules.filter((r) => other.has(r.id)).map((r) => r.id);
32
+ const was = kept(current.rules, after);
33
+ const now = kept(proposed.rules, before);
34
+ now.forEach((id, i) => {
35
+ if (was[i] !== id)
36
+ changes.push({ rule: id, change: "moved", adds_power: true });
37
+ });
38
+ return { adds_power: changes.some((c) => c.adds_power), changes };
39
+ }
@@ -0,0 +1,34 @@
1
+ import type { Rule } from "./index.ts";
2
+ export interface DraftGroup {
3
+ rule: string;
4
+ tool: string;
5
+ denied: number;
6
+ audited: number;
7
+ held: number;
8
+ approved: number;
9
+ rejected: number;
10
+ timeout: number;
11
+ /** Sessions where a person granted the tool after this rule denied it (spec/approvals.md §4). */
12
+ granted: number;
13
+ sessions: number;
14
+ samples: Array<{
15
+ session_id: string;
16
+ seq: number;
17
+ }>;
18
+ }
19
+ export interface Draft {
20
+ /** `allow`: add `draft` before `rule`; `enforce`: remove `audit` from `rule`. */
21
+ kind: "allow" | "enforce";
22
+ rule: string;
23
+ tool: string;
24
+ confidence_pct: number;
25
+ why: string;
26
+ draft?: Rule;
27
+ }
28
+ export declare function policyDrafts(sessions: ReadonlyArray<{
29
+ session_id: string;
30
+ lines: readonly string[];
31
+ }>): {
32
+ groups: DraftGroup[];
33
+ drafts: Draft[];
34
+ };
@@ -0,0 +1,129 @@
1
+ // Rule drafts from what the policy did (spec/policy.md §5, idea C6): denials, holds, audit hits and
2
+ // grants, grouped by rule and tool, counted, with samples, and drafted into rule changes with a
3
+ // confidence. Deterministic, no model. Mirrors policy/drafts.py.
4
+ const SAMPLES = 3;
5
+ /** The tool's policy name: `mcp__<server>__<tool>` for an MCP call. */
6
+ const toolOf = (meta) => typeof meta.server === "string" && typeof meta.tool === "string"
7
+ ? `mcp__${meta.server}__${meta.tool}`
8
+ : String(meta.tool ?? "?");
9
+ const allowId = (tool) => `allow-${tool
10
+ .toLowerCase()
11
+ .replace(/[^a-z0-9-]+/g, "-")
12
+ .replace(/^-+|-+$/g, "")}`.slice(0, 64);
13
+ export function policyDrafts(sessions) {
14
+ const groups = new Map();
15
+ const group = (rule, tool) => {
16
+ const key = `${rule}\n${tool}`;
17
+ let g = groups.get(key);
18
+ if (!g) {
19
+ g = {
20
+ rule,
21
+ tool,
22
+ denied: 0,
23
+ audited: 0,
24
+ held: 0,
25
+ approved: 0,
26
+ rejected: 0,
27
+ timeout: 0,
28
+ granted: 0,
29
+ sessions: 0,
30
+ samples: [],
31
+ seen: new Set(),
32
+ };
33
+ groups.set(key, g);
34
+ }
35
+ return g;
36
+ };
37
+ const touch = (g, sessionId, seq) => {
38
+ if (!g.seen.has(sessionId)) {
39
+ g.seen.add(sessionId);
40
+ g.sessions++;
41
+ }
42
+ if (g.samples.length < SAMPLES)
43
+ g.samples.push({ session_id: sessionId, seq });
44
+ };
45
+ for (const { session_id, lines } of sessions) {
46
+ const decisions = new Map();
47
+ const held = [];
48
+ const denied = new Set();
49
+ const grants = new Set();
50
+ for (const line of lines) {
51
+ const e = JSON.parse(line);
52
+ const meta = e.meta ?? {};
53
+ if (e.kind === "control") {
54
+ if (meta.action === "approval" && typeof meta.approval_id === "string")
55
+ decisions.set(meta.approval_id, String(meta.decision));
56
+ if (meta.action === "permission" && typeof meta.tool === "string")
57
+ grants.add(meta.tool);
58
+ continue;
59
+ }
60
+ if (e.kind !== "tool.call")
61
+ continue;
62
+ const tool = toolOf(meta);
63
+ const p = meta.policy;
64
+ if (typeof p?.rule === "string" && p.rule !== "supervised") {
65
+ const g = group(p.rule, tool);
66
+ if (p.action === "deny") {
67
+ g.denied++;
68
+ denied.add(g);
69
+ touch(g, session_id, e.seq);
70
+ }
71
+ else if (p.action === "hold" && typeof p.approval_id === "string") {
72
+ g.held++;
73
+ held.push([g, p.approval_id]);
74
+ touch(g, session_id, e.seq);
75
+ }
76
+ }
77
+ const hits = Array.isArray(meta.policy_audit) ? meta.policy_audit : [];
78
+ for (const a of hits)
79
+ if (typeof a.rule === "string") {
80
+ const g = group(a.rule, tool);
81
+ g.audited++;
82
+ touch(g, session_id, e.seq);
83
+ }
84
+ }
85
+ for (const [g, id] of held) {
86
+ const d = decisions.get(id);
87
+ if (d === "approve")
88
+ g.approved++;
89
+ else if (d === "reject")
90
+ g.rejected++;
91
+ else if (d === "timeout")
92
+ g.timeout++;
93
+ }
94
+ for (const g of denied)
95
+ if (grants.has(g.tool))
96
+ g.granted++;
97
+ }
98
+ const out = [...groups.values()]
99
+ .map(({ seen: _, ...g }) => g)
100
+ .sort((a, b) => (a.rule === b.rule ? cmp(a.tool, b.tool) : cmp(a.rule, b.rule)));
101
+ const drafts = [];
102
+ for (const g of out) {
103
+ if (g.audited > 0)
104
+ drafts.push({
105
+ kind: "enforce",
106
+ rule: g.rule,
107
+ tool: g.tool,
108
+ confidence_pct: Math.min(100, g.sessions * 20),
109
+ why: `audit only: it would have acted ${g.audited} times in ${g.sessions} sessions`,
110
+ });
111
+ const byApproval = g.held >= 5 && g.approved === g.held ? Math.min(100, g.approved * 5) : 0;
112
+ const byGrant = g.granted >= 3 ? Math.min(100, g.granted * 20) : 0;
113
+ if (byApproval || byGrant)
114
+ drafts.push({
115
+ kind: "allow",
116
+ rule: g.rule,
117
+ tool: g.tool,
118
+ confidence_pct: Math.max(byApproval, byGrant),
119
+ why: byGrant >= byApproval
120
+ ? `a person granted it in ${g.granted} sessions after ${g.rule} denied it`
121
+ : `a person approved all ${g.approved} held calls`,
122
+ draft: { id: allowId(g.tool), tool: g.tool, action: "allow" },
123
+ });
124
+ }
125
+ drafts.sort((a, b) => b.confidence_pct - a.confidence_pct || cmp(a.rule, b.rule) || cmp(a.tool, b.tool));
126
+ return { groups: out, drafts };
127
+ }
128
+ /** Code-point order, the same in both SDKs. */
129
+ const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
@@ -0,0 +1,47 @@
1
+ import type { Finding } from "../analysis/index.ts";
2
+ import { type Bodies } from "../reconcile/record.ts";
3
+ import { type Compensation } from "../undo/index.ts";
4
+ export interface Rule {
5
+ id: string;
6
+ tool: string;
7
+ args_match?: string;
8
+ ignore_case?: boolean;
9
+ /** spec/approvals.md: `require_approval` holds the call for a second person. */
10
+ action: "deny" | "allow" | "require_approval";
11
+ reason?: string;
12
+ /** N3 (idea C5): record-only. Never decides a call; what it would do is recorded (POLICY_AUDIT). */
13
+ audit?: boolean;
14
+ }
15
+ export interface Policy {
16
+ version: 1;
17
+ rules: Rule[];
18
+ /** spec/undo.md §1: how to reverse each risky tool. */
19
+ compensations?: Compensation[];
20
+ }
21
+ export interface Decision {
22
+ rule: string;
23
+ action: "deny" | "allow" | "require_approval";
24
+ reason?: string;
25
+ }
26
+ interface Compiled {
27
+ rule: Rule;
28
+ tool: RegExp;
29
+ args: RegExp | null;
30
+ }
31
+ /** Parses a policy file; throws on anything invalid (the server refuses to start). */
32
+ export declare function loadPolicy(bytes: Uint8Array): Policy;
33
+ export declare function compilePolicy(policy: Policy): Compiled[];
34
+ /** The first rule that matches this call, or null (allowed). `names`: the tool's names (an MCP tool has two). */
35
+ export declare function decide(compiled: readonly Compiled[], names: readonly string[], args: unknown): Decision | null;
36
+ export type CompiledPolicy = ReturnType<typeof compilePolicy>;
37
+ /** N3 (idea C5): every audit rule this call matches, and what it would do if it were enforced. */
38
+ export declare function audited(compiled: readonly Compiled[], names: readonly string[], args: unknown): Array<{
39
+ rule: string;
40
+ would: Rule["action"];
41
+ }>;
42
+ /**
43
+ * spec/policy.md §2 as findings: POLICY_DENIED for MCP calls the gateway refused (tool.call meta.policy),
44
+ * POLICY_VIOLATION for tools a model response asked for that a deny rule matches.
45
+ */
46
+ export declare function policyFindings(lines: readonly string[], bodies: Bodies, compiled: CompiledPolicy): Finding[];
47
+ export {};