@zanii/blackbox 0.9.0 → 0.11.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.
- package/README.md +13 -0
- package/dist/analysis/grounding.d.ts +10 -1
- package/dist/analysis/grounding.js +327 -6
- package/dist/cli.js +70 -2
- package/dist/client/index.d.ts +12 -0
- package/dist/client/index.js +10 -0
- package/dist/policy/index.d.ts +3 -0
- package/dist/policy/index.js +49 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -83,6 +83,19 @@ verifyAnswerCredential(credential, lines, bodies, packs); // { ok, problems }: t
|
|
|
83
83
|
|
|
84
84
|
The same checks run offline as pure functions: `toolHallucinations`, `groundingFindings`, `factsIn`, `loadReferencePacks`, `answerRisk`, `detectorAccuracy`.
|
|
85
85
|
|
|
86
|
+
## New in 0.11.0
|
|
87
|
+
|
|
88
|
+
- Facts against the record compare currencies and units: `AED 500` in a tool result no longer grounds `USD 500` in the answer, nor `500 g` grounds `500 mg` (`currencyCodes`, `unitOf`). Dates written with an English or Arabic month (`3 March 2026`, `3 مارس 2026`) are facts too.
|
|
89
|
+
- `blackbox policy-hook`: a Claude Code / Codex PreToolUse hook that asks the gateway's policy before a local tool runs, and fails closed.
|
|
90
|
+
- Works with gateway v0.13 (`POST /v1/sessions/:id/policy-check`).
|
|
91
|
+
|
|
92
|
+
## New in 0.10.0
|
|
93
|
+
|
|
94
|
+
- `setTenantPolicy` / `tenantPolicy` / `removeTenantPolicy`: a tenant's own rules, hot on the gateway. `setProviderKey` / `providerKeys` / `removeProviderKey`: a tenant's own model keys, sealed. `cancelRun(sessionId, runId)`: stop one run (calls carrying `x-blackbox-run`).
|
|
95
|
+
- `loadPolicy` refuses an `args_match` that JavaScript and Python would read differently (`portableRegexError`).
|
|
96
|
+
- `blackbox sessions --outcome success|failure|abandoned`.
|
|
97
|
+
- Works with gateway v0.12: alert routing to PagerDuty, Teams, Alertmanager and Slack; the key passphrase from Vault or AWS KMS.
|
|
98
|
+
|
|
86
99
|
## New in 0.9.0
|
|
87
100
|
|
|
88
101
|
- `client(...).createKey(tenantId, {scope: "approve", expires_in_days})`: a key that only decides approvals (and labels findings), and keys that expire.
|
|
@@ -10,7 +10,14 @@ export interface Fact {
|
|
|
10
10
|
at: number;
|
|
11
11
|
end: number;
|
|
12
12
|
currency?: string;
|
|
13
|
+
/** Quantities: the canonical unit (spec/findings.md §9). */
|
|
14
|
+
unit?: string;
|
|
13
15
|
}
|
|
16
|
+
/** spec/findings.md §9: the ISO codes a written currency may be (a symbol or a word names a
|
|
17
|
+
* family: "$" is any dollar). */
|
|
18
|
+
export declare function currencyCodes(written: string): string[];
|
|
19
|
+
/** spec/findings.md §9: a written unit's canonical unit. */
|
|
20
|
+
export declare const unitOf: (written: string) => string;
|
|
14
21
|
/** The facts an answer states: money, percentages, quantities, ISO dates, then identifiers (as H1's). */
|
|
15
22
|
export declare function factsIn(text: string): Fact[];
|
|
16
23
|
/** What the agent was given, to check facts against: the record lines it came from (§11 points at
|
|
@@ -30,7 +37,9 @@ export declare class Evidence {
|
|
|
30
37
|
/**
|
|
31
38
|
* The seqs of the lines that hold a fact, or null. Money, percentages and quantities: the same
|
|
32
39
|
* number, the same in minor units, a percentage as a fraction, or (money) the sum of two of the
|
|
33
|
-
* evidence's numbers.
|
|
40
|
+
* evidence's numbers. A number the evidence writes only with another currency or unit doesn't
|
|
41
|
+
* hold it (AED 500 isn't USD 500; 500 g isn't 500 mg); one it writes bare does. Dates and
|
|
42
|
+
* identifiers: the normalised text (a date in words as its ISO form).
|
|
34
43
|
*/
|
|
35
44
|
support(f: Fact): number[] | null;
|
|
36
45
|
private pairSums;
|
|
@@ -19,9 +19,291 @@ const MONEY = new RegExp(`(?<![A-Za-z])(${CURRENCY})\\s?(${AMOUNT})(?![0-9])|(?<
|
|
|
19
19
|
const PERCENT = new RegExp(`(?<![0-9.,])(${AMOUNT})\\s?(?:%|٪|percent|per cent|بالمائة|في المائة)`, "gi");
|
|
20
20
|
// any measured quantity: a dose, a weight, a distance, a duration ("within 14 days")
|
|
21
21
|
const UNIT = "mg/dL|mmol/L|mcg|µg|mg|kg|g|mL|ml|L|km|cm|mm|m|mi|ft|lbs|lb|oz|°C|°F|kWh|MWh|kW|MW|IU|hours|hour|hrs|hr|h|minutes|minute|mins|min|seconds|second|secs|days|day|weeks|week|months|month|years|year|units|unit|items|pieces|tablets|doses|ملغ|مجم|غرام|كغ|كيلوغرام|مل|لتر|كم|متر|ساعة|ساعات|دقيقة|دقائق|يوم|أيام|أسبوع|أسابيع|شهر|أشهر|سنة|سنوات|وحدة|حبة|جرعة";
|
|
22
|
-
const QUANTITY = new RegExp(`(?<![0-9.,])(${AMOUNT})\\s?(
|
|
22
|
+
const QUANTITY = new RegExp(`(?<![0-9.,])(${AMOUNT})\\s?(${UNIT})(?![A-Za-z\u0600-\u06FF])`, "g");
|
|
23
23
|
const ISO_DATE = /(?<![0-9])[0-9]{4}-[0-9]{2}-[0-9]{2}(?![0-9])/g;
|
|
24
24
|
const NUMBER = new RegExp(`(?<![0-9.,])(?:${AMOUNT})(?![0-9])`, "g");
|
|
25
|
+
// spec/grounding-tables.json, inline (both SDKs carry the same tables)
|
|
26
|
+
const FAMILIES = {
|
|
27
|
+
$: [
|
|
28
|
+
"USD",
|
|
29
|
+
"CAD",
|
|
30
|
+
"AUD",
|
|
31
|
+
"NZD",
|
|
32
|
+
"SGD",
|
|
33
|
+
"HKD",
|
|
34
|
+
"TWD",
|
|
35
|
+
"JMD",
|
|
36
|
+
"BSD",
|
|
37
|
+
"BBD",
|
|
38
|
+
"BZD",
|
|
39
|
+
"BMD",
|
|
40
|
+
"BND",
|
|
41
|
+
"FJD",
|
|
42
|
+
"GYD",
|
|
43
|
+
"KYD",
|
|
44
|
+
"LRD",
|
|
45
|
+
"NAD",
|
|
46
|
+
"SBD",
|
|
47
|
+
"SRD",
|
|
48
|
+
"TTD",
|
|
49
|
+
"XCD",
|
|
50
|
+
],
|
|
51
|
+
dollar: [
|
|
52
|
+
"USD",
|
|
53
|
+
"CAD",
|
|
54
|
+
"AUD",
|
|
55
|
+
"NZD",
|
|
56
|
+
"SGD",
|
|
57
|
+
"HKD",
|
|
58
|
+
"TWD",
|
|
59
|
+
"JMD",
|
|
60
|
+
"BSD",
|
|
61
|
+
"BBD",
|
|
62
|
+
"BZD",
|
|
63
|
+
"BMD",
|
|
64
|
+
"BND",
|
|
65
|
+
"FJD",
|
|
66
|
+
"GYD",
|
|
67
|
+
"KYD",
|
|
68
|
+
"LRD",
|
|
69
|
+
"NAD",
|
|
70
|
+
"SBD",
|
|
71
|
+
"SRD",
|
|
72
|
+
"TTD",
|
|
73
|
+
"XCD",
|
|
74
|
+
],
|
|
75
|
+
دولار: [
|
|
76
|
+
"USD",
|
|
77
|
+
"CAD",
|
|
78
|
+
"AUD",
|
|
79
|
+
"NZD",
|
|
80
|
+
"SGD",
|
|
81
|
+
"HKD",
|
|
82
|
+
"TWD",
|
|
83
|
+
"JMD",
|
|
84
|
+
"BSD",
|
|
85
|
+
"BBD",
|
|
86
|
+
"BZD",
|
|
87
|
+
"BMD",
|
|
88
|
+
"BND",
|
|
89
|
+
"FJD",
|
|
90
|
+
"GYD",
|
|
91
|
+
"KYD",
|
|
92
|
+
"LRD",
|
|
93
|
+
"NAD",
|
|
94
|
+
"SBD",
|
|
95
|
+
"SRD",
|
|
96
|
+
"TTD",
|
|
97
|
+
"XCD",
|
|
98
|
+
],
|
|
99
|
+
"€": ["EUR"],
|
|
100
|
+
euro: ["EUR"],
|
|
101
|
+
يورو: ["EUR"],
|
|
102
|
+
"£": ["GBP", "EGP", "LBP", "SYP", "SDG", "SSP", "GIP", "FKP", "SHP"],
|
|
103
|
+
pound: ["GBP", "EGP", "LBP", "SYP", "SDG", "SSP", "GIP", "FKP", "SHP"],
|
|
104
|
+
جنيه: ["GBP", "EGP", "LBP", "SYP", "SDG", "SSP", "GIP", "FKP", "SHP"],
|
|
105
|
+
"¥": ["JPY", "CNY"],
|
|
106
|
+
yen: ["JPY"],
|
|
107
|
+
yuan: ["CNY"],
|
|
108
|
+
"₹": ["INR"],
|
|
109
|
+
rupee: ["INR", "PKR", "LKR", "NPR", "MUR", "SCR"],
|
|
110
|
+
"₩": ["KRW"],
|
|
111
|
+
"₽": ["RUB"],
|
|
112
|
+
"₺": ["TRY"],
|
|
113
|
+
lira: ["TRY", "LBP", "SYP"],
|
|
114
|
+
"₪": ["ILS"],
|
|
115
|
+
"฿": ["THB"],
|
|
116
|
+
"₫": ["VND"],
|
|
117
|
+
"₦": ["NGN"],
|
|
118
|
+
naira: ["NGN"],
|
|
119
|
+
"₱": ["PHP"],
|
|
120
|
+
dirham: ["AED", "MAD"],
|
|
121
|
+
dh: ["AED", "MAD"],
|
|
122
|
+
dhs: ["AED", "MAD"],
|
|
123
|
+
"د.إ": ["AED"],
|
|
124
|
+
درهم: ["AED", "MAD"],
|
|
125
|
+
دراهم: ["AED", "MAD"],
|
|
126
|
+
riyal: ["SAR", "QAR", "OMR", "YER", "IRR"],
|
|
127
|
+
rial: ["SAR", "QAR", "OMR", "YER", "IRR"],
|
|
128
|
+
ريال: ["SAR", "QAR", "OMR", "YER", "IRR"],
|
|
129
|
+
dinar: ["KWD", "BHD", "JOD", "IQD", "LYD", "TND", "DZD", "RSD", "MKD"],
|
|
130
|
+
دينار: ["KWD", "BHD", "JOD", "IQD", "LYD", "TND", "DZD", "RSD", "MKD"],
|
|
131
|
+
franc: ["CHF", "XAF", "XOF", "XPF", "CDF", "BIF", "DJF", "GNF", "KMF", "RWF"],
|
|
132
|
+
peso: ["MXN", "ARS", "CLP", "COP", "CUP", "DOP", "PHP", "UYU"],
|
|
133
|
+
ringgit: ["MYR"],
|
|
134
|
+
rand: ["ZAR"],
|
|
135
|
+
shilling: ["KES", "UGX", "TZS", "SOS"],
|
|
136
|
+
taka: ["BDT"],
|
|
137
|
+
};
|
|
138
|
+
const UNITS = {
|
|
139
|
+
"mg/dl": "mg/dL",
|
|
140
|
+
"mmol/l": "mmol/L",
|
|
141
|
+
mcg: "mcg",
|
|
142
|
+
µg: "mcg",
|
|
143
|
+
mg: "mg",
|
|
144
|
+
ملغ: "mg",
|
|
145
|
+
مجم: "mg",
|
|
146
|
+
g: "g",
|
|
147
|
+
غرام: "g",
|
|
148
|
+
kg: "kg",
|
|
149
|
+
كغ: "kg",
|
|
150
|
+
كيلوغرام: "kg",
|
|
151
|
+
ml: "ml",
|
|
152
|
+
مل: "ml",
|
|
153
|
+
l: "l",
|
|
154
|
+
لتر: "l",
|
|
155
|
+
km: "km",
|
|
156
|
+
كم: "km",
|
|
157
|
+
cm: "cm",
|
|
158
|
+
mm: "mm",
|
|
159
|
+
m: "m",
|
|
160
|
+
متر: "m",
|
|
161
|
+
mi: "mi",
|
|
162
|
+
ft: "ft",
|
|
163
|
+
lb: "lb",
|
|
164
|
+
lbs: "lb",
|
|
165
|
+
oz: "oz",
|
|
166
|
+
"°c": "°C",
|
|
167
|
+
"°f": "°F",
|
|
168
|
+
kwh: "kWh",
|
|
169
|
+
mwh: "MWh",
|
|
170
|
+
kw: "kW",
|
|
171
|
+
mw: "MW",
|
|
172
|
+
iu: "IU",
|
|
173
|
+
hours: "hour",
|
|
174
|
+
hour: "hour",
|
|
175
|
+
hrs: "hour",
|
|
176
|
+
hr: "hour",
|
|
177
|
+
h: "hour",
|
|
178
|
+
ساعة: "hour",
|
|
179
|
+
ساعات: "hour",
|
|
180
|
+
minutes: "minute",
|
|
181
|
+
minute: "minute",
|
|
182
|
+
mins: "minute",
|
|
183
|
+
min: "minute",
|
|
184
|
+
دقيقة: "minute",
|
|
185
|
+
دقائق: "minute",
|
|
186
|
+
seconds: "second",
|
|
187
|
+
second: "second",
|
|
188
|
+
secs: "second",
|
|
189
|
+
days: "day",
|
|
190
|
+
day: "day",
|
|
191
|
+
يوم: "day",
|
|
192
|
+
أيام: "day",
|
|
193
|
+
weeks: "week",
|
|
194
|
+
week: "week",
|
|
195
|
+
أسبوع: "week",
|
|
196
|
+
أسابيع: "week",
|
|
197
|
+
months: "month",
|
|
198
|
+
month: "month",
|
|
199
|
+
شهر: "month",
|
|
200
|
+
أشهر: "month",
|
|
201
|
+
years: "year",
|
|
202
|
+
year: "year",
|
|
203
|
+
سنة: "year",
|
|
204
|
+
سنوات: "year",
|
|
205
|
+
units: "unit",
|
|
206
|
+
unit: "unit",
|
|
207
|
+
وحدة: "unit",
|
|
208
|
+
items: "item",
|
|
209
|
+
pieces: "piece",
|
|
210
|
+
tablets: "tablet",
|
|
211
|
+
حبة: "tablet",
|
|
212
|
+
doses: "dose",
|
|
213
|
+
جرعة: "dose",
|
|
214
|
+
};
|
|
215
|
+
const MONTHS = {
|
|
216
|
+
january: 1,
|
|
217
|
+
february: 2,
|
|
218
|
+
march: 3,
|
|
219
|
+
april: 4,
|
|
220
|
+
may: 5,
|
|
221
|
+
june: 6,
|
|
222
|
+
july: 7,
|
|
223
|
+
august: 8,
|
|
224
|
+
september: 9,
|
|
225
|
+
october: 10,
|
|
226
|
+
november: 11,
|
|
227
|
+
december: 12,
|
|
228
|
+
jan: 1,
|
|
229
|
+
feb: 2,
|
|
230
|
+
mar: 3,
|
|
231
|
+
apr: 4,
|
|
232
|
+
jun: 6,
|
|
233
|
+
jul: 7,
|
|
234
|
+
aug: 8,
|
|
235
|
+
sept: 9,
|
|
236
|
+
sep: 9,
|
|
237
|
+
oct: 10,
|
|
238
|
+
nov: 11,
|
|
239
|
+
dec: 12,
|
|
240
|
+
يناير: 1,
|
|
241
|
+
فبراير: 2,
|
|
242
|
+
مارس: 3,
|
|
243
|
+
أبريل: 4,
|
|
244
|
+
إبريل: 4,
|
|
245
|
+
ابريل: 4,
|
|
246
|
+
مايو: 5,
|
|
247
|
+
يونيو: 6,
|
|
248
|
+
يوليو: 7,
|
|
249
|
+
أغسطس: 8,
|
|
250
|
+
اغسطس: 8,
|
|
251
|
+
سبتمبر: 9,
|
|
252
|
+
أكتوبر: 10,
|
|
253
|
+
اكتوبر: 10,
|
|
254
|
+
نوفمبر: 11,
|
|
255
|
+
ديسمبر: 12,
|
|
256
|
+
"كانون الثاني": 1,
|
|
257
|
+
شباط: 2,
|
|
258
|
+
آذار: 3,
|
|
259
|
+
نيسان: 4,
|
|
260
|
+
أيار: 5,
|
|
261
|
+
حزيران: 6,
|
|
262
|
+
تموز: 7,
|
|
263
|
+
آب: 8,
|
|
264
|
+
أيلول: 9,
|
|
265
|
+
"تشرين الأول": 10,
|
|
266
|
+
"تشرين الثاني": 11,
|
|
267
|
+
"كانون الأول": 12,
|
|
268
|
+
};
|
|
269
|
+
/** spec/findings.md §9: the ISO codes a written currency may be (a symbol or a word names a
|
|
270
|
+
* family: "$" is any dollar). */
|
|
271
|
+
export function currencyCodes(written) {
|
|
272
|
+
if (/^[A-Z]{3}$/.test(written))
|
|
273
|
+
return [written];
|
|
274
|
+
const w = written.toLowerCase();
|
|
275
|
+
return FAMILIES[w] ?? FAMILIES[w.replace(/s$/, "")] ?? [written];
|
|
276
|
+
}
|
|
277
|
+
/** spec/findings.md §9: a written unit's canonical unit. */
|
|
278
|
+
export const unitOf = (written) => UNITS[written.toLowerCase()] ?? written;
|
|
279
|
+
const MONTH = Object.keys(MONTHS)
|
|
280
|
+
.sort((a, b) => b.length - a.length)
|
|
281
|
+
.join("|");
|
|
282
|
+
const DATE_DMY = new RegExp(`(?<![0-9])([0-9]{1,2})(?:st|nd|rd|th)?\\s+(${MONTH})\\.?,?\\s+([0-9]{4})(?![0-9])`, "gi");
|
|
283
|
+
const DATE_MDY = new RegExp(`(?<![A-Za-z])(${MONTH})\\.?\\s+([0-9]{1,2})(?:st|nd|rd|th)?,?\\s+([0-9]{4})(?![0-9])`, "gi");
|
|
284
|
+
/** Dates written in words (English or Arabic months), as ISO dates with their spans. */
|
|
285
|
+
function wordDates(s) {
|
|
286
|
+
const out = [];
|
|
287
|
+
const add = (d, m, y, at, end) => {
|
|
288
|
+
const month = MONTHS[m.toLowerCase()];
|
|
289
|
+
const day = Number(d);
|
|
290
|
+
const date = new Date(Date.UTC(Number(y), (month ?? 0) - 1, day));
|
|
291
|
+
if (!month || date.getUTCDate() !== day || date.getUTCMonth() !== month - 1)
|
|
292
|
+
return;
|
|
293
|
+
if (out.some((o) => at < o.end && end > o.at))
|
|
294
|
+
return;
|
|
295
|
+
out.push({ iso: `${y}-${String(month).padStart(2, "0")}-${d.padStart(2, "0")}`, at, end });
|
|
296
|
+
};
|
|
297
|
+
for (const m of s.matchAll(DATE_DMY)) {
|
|
298
|
+
const at = m.index ?? 0;
|
|
299
|
+
add(m[1], m[2], m[3], at, at + m[0].length);
|
|
300
|
+
}
|
|
301
|
+
for (const m of s.matchAll(DATE_MDY)) {
|
|
302
|
+
const at = m.index ?? 0;
|
|
303
|
+
add(m[2], m[1], m[3], at, at + m[0].length);
|
|
304
|
+
}
|
|
305
|
+
return out.sort((a, b) => a.at - b.at);
|
|
306
|
+
}
|
|
25
307
|
/** A canonical decimal as an integer of millionths, exactly (sums without floats). */
|
|
26
308
|
function micros(c) {
|
|
27
309
|
const [w = "0", f = ""] = c.split(".");
|
|
@@ -58,12 +340,20 @@ export function factsIn(text) {
|
|
|
58
340
|
}
|
|
59
341
|
for (const m of s.matchAll(QUANTITY)) {
|
|
60
342
|
const at = m.index ?? 0;
|
|
61
|
-
add({
|
|
343
|
+
add({
|
|
344
|
+
kind: "quantity",
|
|
345
|
+
value: canonicalNumber(m[1]),
|
|
346
|
+
at,
|
|
347
|
+
end: at + m[0].length,
|
|
348
|
+
unit: unitOf(m[2]),
|
|
349
|
+
});
|
|
62
350
|
}
|
|
63
351
|
for (const m of s.matchAll(ISO_DATE)) {
|
|
64
352
|
const at = m.index ?? 0;
|
|
65
353
|
add({ kind: "date", value: normaliseValue(m[0]), at, end: at + m[0].length });
|
|
66
354
|
}
|
|
355
|
+
for (const d of wordDates(s))
|
|
356
|
+
add({ kind: "date", value: normaliseValue(d.iso), at: d.at, end: d.end });
|
|
67
357
|
for (const x of identifierSpans(s, taken))
|
|
68
358
|
out.push({ kind: x.kind, value: x.value, at: x.at, end: x.end });
|
|
69
359
|
return out.sort((a, b) => a.at - b.at);
|
|
@@ -80,10 +370,32 @@ export class Evidence {
|
|
|
80
370
|
this.raw = raw;
|
|
81
371
|
this.pieces = (pieces ?? [{ seq: -1, text: raw }]).map((p) => {
|
|
82
372
|
const s = westernText(p.text);
|
|
373
|
+
const tags = new Map();
|
|
374
|
+
let blank = s;
|
|
375
|
+
const tag = (value, t, at, end) => {
|
|
376
|
+
const set = tags.get(value) ?? new Set();
|
|
377
|
+
for (const x of t)
|
|
378
|
+
set.add(x);
|
|
379
|
+
tags.set(value, set);
|
|
380
|
+
blank = blank.slice(0, at) + " ".repeat(end - at) + blank.slice(end);
|
|
381
|
+
};
|
|
382
|
+
for (const m of s.matchAll(MONEY)) {
|
|
383
|
+
const at = m.index ?? 0;
|
|
384
|
+
const codes = currencyCodes((m[1] ?? m[4])).map((c) => `cur:${c}`);
|
|
385
|
+
tag(canonicalNumber((m[2] ?? m[3])), codes, at, at + m[0].length);
|
|
386
|
+
}
|
|
387
|
+
for (const m of s.matchAll(QUANTITY)) {
|
|
388
|
+
const at = m.index ?? 0;
|
|
389
|
+
tag(canonicalNumber(m[1]), [`unit:${unitOf(m[2])}`], at, at + m[0].length);
|
|
390
|
+
}
|
|
391
|
+
const numbersIn = (t) => new Set([...t.matchAll(NUMBER)].map((m) => canonicalNumber(m[0])));
|
|
83
392
|
return {
|
|
84
393
|
seq: p.seq,
|
|
85
|
-
|
|
86
|
-
|
|
394
|
+
// dates in words count as their ISO form too
|
|
395
|
+
text: normaliseValue([s, ...wordDates(s).map((d) => d.iso)].join(" ")),
|
|
396
|
+
numbers: numbersIn(s),
|
|
397
|
+
bare: numbersIn(blank),
|
|
398
|
+
tags,
|
|
87
399
|
};
|
|
88
400
|
});
|
|
89
401
|
this.numbers = new Set(this.pieces.flatMap((p) => [...p.numbers]));
|
|
@@ -95,7 +407,9 @@ export class Evidence {
|
|
|
95
407
|
/**
|
|
96
408
|
* The seqs of the lines that hold a fact, or null. Money, percentages and quantities: the same
|
|
97
409
|
* number, the same in minor units, a percentage as a fraction, or (money) the sum of two of the
|
|
98
|
-
* evidence's numbers.
|
|
410
|
+
* evidence's numbers. A number the evidence writes only with another currency or unit doesn't
|
|
411
|
+
* hold it (AED 500 isn't USD 500; 500 g isn't 500 mg); one it writes bare does. Dates and
|
|
412
|
+
* identifiers: the normalised text (a date in words as its ISO form).
|
|
99
413
|
*/
|
|
100
414
|
support(f) {
|
|
101
415
|
if (f.kind === "money" || f.kind === "percent" || f.kind === "quantity") {
|
|
@@ -104,7 +418,14 @@ export class Evidence {
|
|
|
104
418
|
shift(f.value, 2),
|
|
105
419
|
...(f.kind === "percent" ? [shift(f.value, -2)] : []),
|
|
106
420
|
];
|
|
107
|
-
const
|
|
421
|
+
const mine = f.kind === "money" && f.currency
|
|
422
|
+
? currencyCodes(f.currency).map((c) => `cur:${c}`)
|
|
423
|
+
: f.kind === "quantity" && f.unit
|
|
424
|
+
? [`unit:${f.unit}`]
|
|
425
|
+
: null;
|
|
426
|
+
const fits = (p, w) => p.numbers.has(w) &&
|
|
427
|
+
(mine === null || p.bare.has(w) || mine.some((t) => p.tags.get(w)?.has(t) === true));
|
|
428
|
+
const hit = this.pieces.find((p) => wanted.some((w) => fits(p, w)));
|
|
108
429
|
if (hit)
|
|
109
430
|
return [hit.seq];
|
|
110
431
|
return f.kind === "money" ? (this.pairSums().get(f.value) ?? null) : null;
|
package/dist/cli.js
CHANGED
|
@@ -15,7 +15,7 @@ const USAGE = `usage (spec/cli.md):
|
|
|
15
15
|
blackbox verify <bundle.json> [--proofs] [--bodies <dir>]
|
|
16
16
|
blackbox export <session_id> --out <bundle.json> [--bodies <dir>]
|
|
17
17
|
blackbox export-all --from <date> --to <date> --out <file.ndjson>
|
|
18
|
-
blackbox sessions [--limit N] [--label L] [--open | --closed] [--before <session_id>] [--from <date>] [--to <date>] [--json]
|
|
18
|
+
blackbox sessions [--limit N] [--label L] [--open | --closed] [--outcome success|failure|abandoned] [--before <session_id>] [--from <date>] [--to <date>] [--json]
|
|
19
19
|
blackbox sessions <session_id> [--json]
|
|
20
20
|
blackbox summary <session_id>
|
|
21
21
|
blackbox incident <session_id> [--lang en|ar]
|
|
@@ -52,7 +52,8 @@ const USAGE = `usage (spec/cli.md):
|
|
|
52
52
|
blackbox reconcile --harness claude-code|codex --bundle <bundle.json> --bodies <dir> [--agent-session <id>]... [--require-receipts] <path>
|
|
53
53
|
blackbox reconcile-system --connector <connector.json> --log <export.csv|jsonl> [--pack <pack.json>] <session_id>
|
|
54
54
|
blackbox workspace-receipt (a Claude Code PreToolUse / PostToolUse hook; reads the hook JSON on stdin)
|
|
55
|
-
blackbox attest (a Claude Code PostToolUse hook: spec/attestation.md §4)
|
|
55
|
+
blackbox attest (a Claude Code PostToolUse hook: spec/attestation.md §4)
|
|
56
|
+
blackbox policy-hook (a Claude Code / Codex PreToolUse hook: asks the gateway's policy; fails closed)`;
|
|
56
57
|
async function main(argv) {
|
|
57
58
|
const [command, ...rest] = argv;
|
|
58
59
|
if (command === "mcp")
|
|
@@ -67,6 +68,8 @@ async function main(argv) {
|
|
|
67
68
|
return receiptCommand();
|
|
68
69
|
if (command === "attest")
|
|
69
70
|
return attestCommand();
|
|
71
|
+
if (command === "policy-hook")
|
|
72
|
+
return policyHookCommand();
|
|
70
73
|
if (command === "verify")
|
|
71
74
|
return verifyCommand(rest);
|
|
72
75
|
if (command === "export")
|
|
@@ -215,6 +218,69 @@ async function attestCommand() {
|
|
|
215
218
|
}
|
|
216
219
|
return 0;
|
|
217
220
|
}
|
|
221
|
+
/** spec/policy.md §2a: a PreToolUse hook. Deny: exit 2, the reason on stderr (the agent sees it).
|
|
222
|
+
* Ask: Claude Code's `permissionDecision: "ask"` on stdout. Allow: exit 0, nothing printed. Any
|
|
223
|
+
* failure denies, unless BLACKBOX_HOOK_FAIL_OPEN=1. */
|
|
224
|
+
async function policyHookCommand() {
|
|
225
|
+
const refuse = (why) => {
|
|
226
|
+
if (process.env.BLACKBOX_HOOK_FAIL_OPEN === "1") {
|
|
227
|
+
process.stderr.write(`blackbox policy-hook: ${why}; allowed (BLACKBOX_HOOK_FAIL_OPEN=1)\n`);
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
230
|
+
process.stderr.write(`blackbox policy-hook: ${why}; refused (fails closed)\n`);
|
|
231
|
+
return 2;
|
|
232
|
+
};
|
|
233
|
+
const url = process.env.BLACKBOX_URL;
|
|
234
|
+
const token = process.env.BLACKBOX_TOKEN;
|
|
235
|
+
const sessionId = process.env.BLACKBOX_SESSION_ID;
|
|
236
|
+
if (!url || !token || !sessionId)
|
|
237
|
+
return refuse("BLACKBOX_URL, BLACKBOX_TOKEN and BLACKBOX_SESSION_ID are needed");
|
|
238
|
+
let hook;
|
|
239
|
+
try {
|
|
240
|
+
const parts = [];
|
|
241
|
+
for await (const chunk of process.stdin)
|
|
242
|
+
parts.push(chunk);
|
|
243
|
+
hook = JSON.parse(Buffer.concat(parts).toString("utf8"));
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return refuse("the hook input is not JSON");
|
|
247
|
+
}
|
|
248
|
+
if (typeof hook.tool_name !== "string")
|
|
249
|
+
return refuse("the hook input has no tool_name");
|
|
250
|
+
let answer;
|
|
251
|
+
try {
|
|
252
|
+
const res = await fetch(new URL(`/v1/sessions/${encodeURIComponent(sessionId)}/policy-check`, url), {
|
|
253
|
+
method: "POST",
|
|
254
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
255
|
+
body: JSON.stringify({ tool: hook.tool_name, args: hook.tool_input ?? {} }),
|
|
256
|
+
signal: AbortSignal.timeout(5_000),
|
|
257
|
+
});
|
|
258
|
+
if (!res.ok)
|
|
259
|
+
return refuse(`the gateway answered HTTP ${res.status}`);
|
|
260
|
+
answer = (await res.json());
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
return refuse(`the gateway couldn't be asked (${error instanceof Error ? error.message : error})`);
|
|
264
|
+
}
|
|
265
|
+
const why = `${typeof answer.reason === "string" ? answer.reason : "the policy says so"} (rule ${String(answer.rule)})`;
|
|
266
|
+
if (answer.decision === "deny") {
|
|
267
|
+
process.stderr.write(`Blackbox policy refused ${hook.tool_name}: ${why}\n`);
|
|
268
|
+
return 2;
|
|
269
|
+
}
|
|
270
|
+
if (answer.decision === "ask") {
|
|
271
|
+
process.stdout.write(`${JSON.stringify({
|
|
272
|
+
hookSpecificOutput: {
|
|
273
|
+
hookEventName: "PreToolUse",
|
|
274
|
+
permissionDecision: "ask",
|
|
275
|
+
permissionDecisionReason: `Blackbox policy: ${why}`,
|
|
276
|
+
},
|
|
277
|
+
})}\n`);
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
if (answer.decision === "allow")
|
|
281
|
+
return 0;
|
|
282
|
+
return refuse("the gateway's answer has no decision");
|
|
283
|
+
}
|
|
218
284
|
async function receiptCommand() {
|
|
219
285
|
const url = process.env.BLACKBOX_URL;
|
|
220
286
|
const token = process.env.BLACKBOX_TOKEN;
|
|
@@ -388,6 +454,8 @@ async function sessionsCommand(rest) {
|
|
|
388
454
|
q.set("from", f.from);
|
|
389
455
|
if (f.to !== undefined)
|
|
390
456
|
q.set("to", f.to);
|
|
457
|
+
if (f.outcome !== undefined)
|
|
458
|
+
q.set("outcome", f.outcome);
|
|
391
459
|
if (f.open === "1")
|
|
392
460
|
q.set("status", "open");
|
|
393
461
|
if (f.closed === "1")
|
package/dist/client/index.d.ts
CHANGED
|
@@ -102,6 +102,10 @@ export declare function client(options: ClientOptions): {
|
|
|
102
102
|
emergency: (id: string, o?: {
|
|
103
103
|
note?: string;
|
|
104
104
|
}) => Promise<Json>;
|
|
105
|
+
/** Refuses the session's later calls that name this run (x-blackbox-run), spec/control.md §3a. */
|
|
106
|
+
cancelRun: (id: string, runId: string, o?: {
|
|
107
|
+
note?: string;
|
|
108
|
+
}) => Promise<Json>;
|
|
105
109
|
/** Hands the controls over (spec/authority.md): `human` takes over; `agent`/`supervised` hand back. */
|
|
106
110
|
authority: (id: string, to: "human" | "agent" | "supervised", o?: {
|
|
107
111
|
reason?: string;
|
|
@@ -143,5 +147,13 @@ export declare function client(options: ClientOptions): {
|
|
|
143
147
|
expires_in_days?: number;
|
|
144
148
|
}) => Promise<Json>;
|
|
145
149
|
revokeKey: (tenantId: string, keyId: string) => Promise<Json>;
|
|
150
|
+
/** Operator only: a tenant's own rules (spec/policy.md §7a). */
|
|
151
|
+
tenantPolicy: (tenantId: string) => Promise<Json>;
|
|
152
|
+
setTenantPolicy: (tenantId: string, rules: object[]) => Promise<Json>;
|
|
153
|
+
removeTenantPolicy: (tenantId: string) => Promise<Json>;
|
|
154
|
+
/** Operator only: a tenant's own provider keys, sealed, never shown (spec/tenancy.md §5a). */
|
|
155
|
+
providerKeys: (tenantId: string) => Promise<Json>;
|
|
156
|
+
setProviderKey: (tenantId: string, provider: string, apiKey: string) => Promise<Json>;
|
|
157
|
+
removeProviderKey: (tenantId: string, provider: string) => Promise<Json>;
|
|
146
158
|
};
|
|
147
159
|
export type Client = ReturnType<typeof client>;
|
package/dist/client/index.js
CHANGED
|
@@ -187,6 +187,8 @@ export function client(options) {
|
|
|
187
187
|
pause: (id, o = {}) => json("POST", ses(id, "pause"), compact(o)),
|
|
188
188
|
/** The emergency stop (squawk 7700, spec/control.md). */
|
|
189
189
|
emergency: (id, o = {}) => json("POST", ses(id, "emergency"), compact(o)),
|
|
190
|
+
/** Refuses the session's later calls that name this run (x-blackbox-run), spec/control.md §3a. */
|
|
191
|
+
cancelRun: (id, runId, o = {}) => json("POST", ses(id, `runs/${encodeURIComponent(runId)}/cancel`), compact(o), true),
|
|
190
192
|
/** Hands the controls over (spec/authority.md): `human` takes over; `agent`/`supervised` hand back. */
|
|
191
193
|
authority: (id, to, o = {}) => json("POST", ses(id, "authority"), compact({ to, ...o })),
|
|
192
194
|
/** The pending approvals (spec/approvals.md). */
|
|
@@ -205,6 +207,14 @@ export function client(options) {
|
|
|
205
207
|
updateTenant: (tenantId, o) => json("PATCH", `/v1/tenants/${encodeURIComponent(tenantId)}`, compact(o)),
|
|
206
208
|
createKey: (tenantId, o = {}) => json("POST", `/v1/tenants/${encodeURIComponent(tenantId)}/keys`, compact(o)),
|
|
207
209
|
revokeKey: (tenantId, keyId) => json("POST", `/v1/tenants/${encodeURIComponent(tenantId)}/keys/${encodeURIComponent(keyId)}/revoke`, {}, true),
|
|
210
|
+
/** Operator only: a tenant's own rules (spec/policy.md §7a). */
|
|
211
|
+
tenantPolicy: (tenantId) => json("GET", `/v1/tenants/${encodeURIComponent(tenantId)}/policy`),
|
|
212
|
+
setTenantPolicy: (tenantId, rules) => json("PUT", `/v1/tenants/${encodeURIComponent(tenantId)}/policy`, { policy: { rules } }, true),
|
|
213
|
+
removeTenantPolicy: (tenantId) => json("DELETE", `/v1/tenants/${encodeURIComponent(tenantId)}/policy`, undefined, true),
|
|
214
|
+
/** Operator only: a tenant's own provider keys, sealed, never shown (spec/tenancy.md §5a). */
|
|
215
|
+
providerKeys: (tenantId) => json("GET", `/v1/tenants/${encodeURIComponent(tenantId)}/providers`),
|
|
216
|
+
setProviderKey: (tenantId, provider, apiKey) => json("PUT", `/v1/tenants/${encodeURIComponent(tenantId)}/providers/${encodeURIComponent(provider)}`, { api_key: apiKey }, true),
|
|
217
|
+
removeProviderKey: (tenantId, provider) => json("DELETE", `/v1/tenants/${encodeURIComponent(tenantId)}/providers/${encodeURIComponent(provider)}`, undefined, true),
|
|
208
218
|
};
|
|
209
219
|
return api;
|
|
210
220
|
}
|
package/dist/policy/index.d.ts
CHANGED
|
@@ -59,6 +59,9 @@ interface Compiled {
|
|
|
59
59
|
args: RegExp | null;
|
|
60
60
|
}
|
|
61
61
|
/** Parses a policy file; throws on anything invalid (the server refuses to start). */
|
|
62
|
+
/** spec/policy.md §1 (L6.5.4): null when `args_match` uses only syntax JavaScript and Python
|
|
63
|
+
* share, else what doesn't (vectors: spec/vectors/policy-regex.json). */
|
|
64
|
+
export declare function portableRegexError(p: string): string | null;
|
|
62
65
|
export declare function loadPolicy(bytes: Uint8Array): Policy;
|
|
63
66
|
export declare function compilePolicy(policy: Policy): Compiled[];
|
|
64
67
|
/** spec/api.md: the session's environment, from its `session.open` (the first line). */
|
package/dist/policy/index.js
CHANGED
|
@@ -58,6 +58,52 @@ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
|
58
58
|
/** A tool pattern: `*` is any run of characters, the rest is literal, anchored at both ends. */
|
|
59
59
|
const glob = (pattern) => new RegExp(`^${pattern.split("*").map(escapeRe).join(".*")}$`);
|
|
60
60
|
/** Parses a policy file; throws on anything invalid (the server refuses to start). */
|
|
61
|
+
/** spec/policy.md §1 (L6.5.4): null when `args_match` uses only syntax JavaScript and Python
|
|
62
|
+
* share, else what doesn't (vectors: spec/vectors/policy-regex.json). */
|
|
63
|
+
export function portableRegexError(p) {
|
|
64
|
+
const hex = (s) => /^[0-9A-Fa-f]+$/.test(s);
|
|
65
|
+
let inClass = false;
|
|
66
|
+
for (let i = 0; i < p.length; i++) {
|
|
67
|
+
const c = p[i];
|
|
68
|
+
if (c === "\\") {
|
|
69
|
+
const n = p[i + 1];
|
|
70
|
+
if (n === undefined)
|
|
71
|
+
return "a trailing \\";
|
|
72
|
+
if (n === "x" && !(p.length >= i + 4 && hex(p.slice(i + 2, i + 4))))
|
|
73
|
+
return "\\x needs 2 hex digits";
|
|
74
|
+
if (n === "u" && !(p.length >= i + 6 && hex(p.slice(i + 2, i + 6))))
|
|
75
|
+
return "\\u needs 4 hex digits";
|
|
76
|
+
if (/[A-Za-z]/.test(n) && !"dDwWsSbBnrtfvxu".includes(n))
|
|
77
|
+
return `\\${n}`;
|
|
78
|
+
i++;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (inClass) {
|
|
82
|
+
if (c === "]")
|
|
83
|
+
inClass = false;
|
|
84
|
+
else if (c === "[" && p[i + 1] === ":")
|
|
85
|
+
return "[:";
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (c === "[") {
|
|
89
|
+
if (p[i + 1] === ":")
|
|
90
|
+
return "[:";
|
|
91
|
+
const first = p[i + 1] === "^" ? i + 2 : i + 1;
|
|
92
|
+
if (p[first] === "]")
|
|
93
|
+
return "an empty class";
|
|
94
|
+
inClass = true;
|
|
95
|
+
i = first - 1;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (c === "(" && p[i + 1] === "?" && !":=!".includes(p[i + 2] ?? ""))
|
|
99
|
+
return "(? other than (?: (?= (?!";
|
|
100
|
+
if ("*+?}".includes(c) && p[i + 1] === "+" && !(c === "?" && "*+?}".includes(p[i - 1] ?? "")))
|
|
101
|
+
return "a possessive quantifier";
|
|
102
|
+
if (c === "{" && p[i + 1] === ",")
|
|
103
|
+
return "{,";
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
61
107
|
export function loadPolicy(bytes) {
|
|
62
108
|
const doc = JSON.parse(new TextDecoder().decode(bytes));
|
|
63
109
|
if (doc.version !== 1 || !Array.isArray(doc.rules))
|
|
@@ -77,6 +123,9 @@ export function loadPolicy(bytes) {
|
|
|
77
123
|
if (r.args_match !== undefined) {
|
|
78
124
|
if (typeof r.args_match !== "string" || r.args_match.length > 200)
|
|
79
125
|
throw new Error(`policy ${r.id}: args_match must be a regular expression of ≤ 200 characters`);
|
|
126
|
+
const unportable = portableRegexError(r.args_match);
|
|
127
|
+
if (unportable)
|
|
128
|
+
throw new Error(`policy ${r.id}: args_match uses syntax JavaScript and Python don't share (${unportable})`);
|
|
80
129
|
new RegExp(r.args_match); // throws on bad syntax
|
|
81
130
|
}
|
|
82
131
|
if (r.audit !== undefined && typeof r.audit !== "boolean")
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.11.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// The SDK version, on its own so the CLI can print it without loading the whole SDK.
|
|
2
|
-
export const VERSION = "0.
|
|
2
|
+
export const VERSION = "0.11.0";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zanii/blackbox",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.11.0",
|
|
5
5
|
"description": "The flight recorder for AI agents: sessions, a zero-loss spool, framework hooks, approvals, and offline verification of the gateway's hash-chained record.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"ai-agents",
|