@zanii/blackbox 0.1.0 → 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.
- package/dist/analysis/faults.js +40 -0
- package/dist/cli.js +38 -0
- package/dist/data/index.d.ts +191 -0
- package/dist/data/index.js +762 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/packs/index.d.ts +8 -0
- package/dist/packs/index.js +128 -2
- package/dist/session/index.d.ts +19 -0
- package/dist/session/index.js +23 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/analysis/faults.js
CHANGED
|
@@ -203,6 +203,46 @@ export const FAULTS = {
|
|
|
203
203
|
en: "Flying a model it isn't rated on",
|
|
204
204
|
ar: "يعمل على نموذج غير مؤهل له",
|
|
205
205
|
},
|
|
206
|
+
DATA_LEFT_REGION: {
|
|
207
|
+
fault: "BBX-9101",
|
|
208
|
+
en: "Personal data sent outside the data region",
|
|
209
|
+
ar: "بيانات شخصية أُرسلت خارج منطقة البيانات",
|
|
210
|
+
},
|
|
211
|
+
DATA_REGION_UNKNOWN: {
|
|
212
|
+
fault: "BBX-9102",
|
|
213
|
+
en: "Personal data sent to a model of unknown region",
|
|
214
|
+
ar: "بيانات شخصية أُرسلت إلى نموذج منطقته غير معروفة",
|
|
215
|
+
},
|
|
216
|
+
DATA_TO_UNAPPROVED_DESTINATION: {
|
|
217
|
+
fault: "BBX-9103",
|
|
218
|
+
en: "Data sent to a destination its rules don't allow",
|
|
219
|
+
ar: "بيانات أُرسلت إلى وجهة لا تسمح بها قواعدها",
|
|
220
|
+
},
|
|
221
|
+
DATA_CANARY_TRIPPED: {
|
|
222
|
+
fault: "BBX-9104",
|
|
223
|
+
en: "A canary value left the gateway",
|
|
224
|
+
ar: "قيمة إنذار خرجت من البوابة",
|
|
225
|
+
},
|
|
226
|
+
CONSENT_WITHDRAWN_USE: {
|
|
227
|
+
fault: "BBX-9105",
|
|
228
|
+
en: "Data used after consent was withdrawn",
|
|
229
|
+
ar: "استُخدمت بيانات بعد سحب الموافقة",
|
|
230
|
+
},
|
|
231
|
+
UNEVIDENCED_SIDE_EFFECT: {
|
|
232
|
+
fault: "BBX-9201",
|
|
233
|
+
en: "A change the touched system gave no proof of",
|
|
234
|
+
ar: "تغيير لم يقدّم النظام المعني دليلاً عليه",
|
|
235
|
+
},
|
|
236
|
+
TOOL_WITNESS_INVALID: {
|
|
237
|
+
fault: "BBX-9202",
|
|
238
|
+
en: "A tool's signature doesn't verify",
|
|
239
|
+
ar: "توقيع الأداة غير صالح",
|
|
240
|
+
},
|
|
241
|
+
EGRESS_OPEN: {
|
|
242
|
+
fault: "BBX-9301",
|
|
243
|
+
en: "The agent can reach the internet around the gateway",
|
|
244
|
+
ar: "يستطيع الوكيل الوصول إلى الإنترنت متجاوزاً البوابة",
|
|
245
|
+
},
|
|
206
246
|
};
|
|
207
247
|
/** The fault for a finding code; unknown codes get a generic entry, never an exception. */
|
|
208
248
|
export function faultOf(code) {
|
package/dist/cli.js
CHANGED
|
@@ -45,6 +45,7 @@ const USAGE = `usage (spec/cli.md):
|
|
|
45
45
|
blackbox mcp (an MCP server on stdio: spec/mcp-server.md)
|
|
46
46
|
blackbox mcp-wrap --server <name> -- <command> [args...]
|
|
47
47
|
blackbox reconcile --harness claude-code|codex --bundle <bundle.json> --bodies <dir> [--agent-session <id>]... [--require-receipts] <path>
|
|
48
|
+
blackbox reconcile-system --connector <connector.json> --log <export.csv|jsonl> [--pack <pack.json>] <session_id>
|
|
48
49
|
blackbox workspace-receipt (a Claude Code PreToolUse / PostToolUse hook; reads the hook JSON on stdin)
|
|
49
50
|
blackbox attest (a Claude Code PostToolUse hook: spec/attestation.md §4)`;
|
|
50
51
|
async function main(argv) {
|
|
@@ -55,6 +56,8 @@ async function main(argv) {
|
|
|
55
56
|
return wrap(rest);
|
|
56
57
|
if (command === "reconcile")
|
|
57
58
|
return reconcileCommand(rest);
|
|
59
|
+
if (command === "reconcile-system")
|
|
60
|
+
return reconcileSystemCommand(rest);
|
|
58
61
|
if (command === "workspace-receipt")
|
|
59
62
|
return receiptCommand();
|
|
60
63
|
if (command === "attest")
|
|
@@ -1007,6 +1010,41 @@ async function drainCommand(rest) {
|
|
|
1007
1010
|
process.stdout.write(`${JSON.stringify({ ok: results.every((r) => r.shipped), sessions: results })}\n`);
|
|
1008
1011
|
return results.every((r) => r.shipped) ? 0 : 2;
|
|
1009
1012
|
}
|
|
1013
|
+
/** spec/data.md §10: the session's record against a touched system's own audit-log export, run
|
|
1014
|
+
* here so the export never leaves this machine. Exits 1 on any finding. */
|
|
1015
|
+
async function reconcileSystemCommand(rest) {
|
|
1016
|
+
const { flags, positional } = parse(rest, []);
|
|
1017
|
+
const id = positional[0] ?? "";
|
|
1018
|
+
if (positional.length !== 1 || !SESSION_ID.test(id) || !flags.connector || !flags.log)
|
|
1019
|
+
return usage();
|
|
1020
|
+
const { checkAuditConnector, parseAuditLog, reconcileSystem, CORE_EVIDENCE } = await import("./data/index.js");
|
|
1021
|
+
const { packData } = await import("./packs/index.js");
|
|
1022
|
+
const connector = JSON.parse(readFileSync(flags.connector, "utf8"));
|
|
1023
|
+
const bad = checkAuditConnector(connector);
|
|
1024
|
+
if (bad)
|
|
1025
|
+
throw new ServerError(`--connector: ${bad}`);
|
|
1026
|
+
const evidence = flags.pack
|
|
1027
|
+
? packData([JSON.parse(readFileSync(flags.pack, "utf8"))]).evidence
|
|
1028
|
+
: CORE_EVIDENCE;
|
|
1029
|
+
const c = connector;
|
|
1030
|
+
const rows = parseAuditLog(readFileSync(flags.log, "utf8"), c);
|
|
1031
|
+
const bundle = (await adminJson(`/v1/sessions/${id}/bundle`));
|
|
1032
|
+
// Only the bodies the comparison reads: tool calls and results, and SDK events.
|
|
1033
|
+
const bodies = new Map();
|
|
1034
|
+
for (const line of bundle.lines) {
|
|
1035
|
+
const e = JSON.parse(line);
|
|
1036
|
+
if (!["tool.call", "tool.result", "sdk.event"].includes(e.kind) || bodies.has(e.body_hash))
|
|
1037
|
+
continue;
|
|
1038
|
+
if (!/^sha256:[0-9a-f]{64}$/.test(e.body_hash))
|
|
1039
|
+
continue;
|
|
1040
|
+
const res = await admin(`/v1/bodies/${e.body_hash}`, undefined, [404, 410]);
|
|
1041
|
+
if (res.status === 200)
|
|
1042
|
+
bodies.set(e.body_hash, Buffer.from(await res.arrayBuffer()));
|
|
1043
|
+
}
|
|
1044
|
+
const report = reconcileSystem(bundle.lines, (h) => bodies.get(h), rows, c, evidence);
|
|
1045
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
1046
|
+
return report.findings.length > 0 ? 1 : 0;
|
|
1047
|
+
}
|
|
1010
1048
|
function usage() {
|
|
1011
1049
|
process.stderr.write(`${USAGE}\n`);
|
|
1012
1050
|
return 2;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
export interface Identifier {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
pattern: string;
|
|
5
|
+
normalize: "digits" | "lower" | "upper" | "exact";
|
|
6
|
+
keep_last?: number;
|
|
7
|
+
check?: "luhn";
|
|
8
|
+
personal: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** spec/data/identifiers-v1.json: always on. */
|
|
11
|
+
export declare const CORE_IDENTIFIERS: readonly Identifier[];
|
|
12
|
+
/** spec/data/core-v1.json: evidence defaults for common writes (§5). */
|
|
13
|
+
export declare const CORE_EVIDENCE: ReadonlyArray<{
|
|
14
|
+
tool: string;
|
|
15
|
+
field: string;
|
|
16
|
+
}>;
|
|
17
|
+
/**
|
|
18
|
+
* §6: the canary values as one identifier, matched literally. Longer values first, so a value that
|
|
19
|
+
* contains another wins. ASCII punctuation is escaped the same way both languages read it.
|
|
20
|
+
*/
|
|
21
|
+
export declare function canaryIdentifier(values: readonly string[]): Identifier;
|
|
22
|
+
export declare function normalizeValue(value: string, how: Identifier["normalize"]): string;
|
|
23
|
+
export declare function luhn(digits: string): boolean;
|
|
24
|
+
export interface Found {
|
|
25
|
+
id: string;
|
|
26
|
+
value: string;
|
|
27
|
+
}
|
|
28
|
+
/** §1: each distinct (id, normalized value) in text order, at most 200. Earlier identifiers win a span. */
|
|
29
|
+
export declare function dataScan(text: string, identifiers: readonly Identifier[]): Found[];
|
|
30
|
+
/** Every match counted (not only distinct ones), for `data.classes`. */
|
|
31
|
+
export declare function dataClasses(text: string, identifiers: readonly Identifier[]): Record<string, number>;
|
|
32
|
+
/** §2: the tenant's fingerprint of one identifier value. */
|
|
33
|
+
export declare function dataFingerprint(key: Uint8Array | string, tenant: string, id: string, value: string): string;
|
|
34
|
+
/** §2: the `data` meta of a body, or undefined when it holds no identifier. */
|
|
35
|
+
export declare function dataMeta(text: string, identifiers: readonly Identifier[], key: Uint8Array | string, tenant: string): {
|
|
36
|
+
classes: Record<string, number>;
|
|
37
|
+
fps: Array<{
|
|
38
|
+
id: string;
|
|
39
|
+
fp: string;
|
|
40
|
+
}>;
|
|
41
|
+
} | undefined;
|
|
42
|
+
export interface Lineage {
|
|
43
|
+
subjects: Array<{
|
|
44
|
+
fp: string;
|
|
45
|
+
id: string;
|
|
46
|
+
first: {
|
|
47
|
+
seq: number;
|
|
48
|
+
kind: string;
|
|
49
|
+
from: string;
|
|
50
|
+
};
|
|
51
|
+
sent: Array<{
|
|
52
|
+
seq: number;
|
|
53
|
+
to: string;
|
|
54
|
+
region?: string;
|
|
55
|
+
}>;
|
|
56
|
+
}>;
|
|
57
|
+
destinations: Array<{
|
|
58
|
+
to: string;
|
|
59
|
+
region?: string;
|
|
60
|
+
classes: Record<string, number>;
|
|
61
|
+
subjects: number;
|
|
62
|
+
}>;
|
|
63
|
+
}
|
|
64
|
+
/** §3: where each subject's data came from and went in one session. */
|
|
65
|
+
export declare function lineage(lines: readonly string[]): Lineage;
|
|
66
|
+
export interface Topology {
|
|
67
|
+
nodes: Array<{
|
|
68
|
+
id: string;
|
|
69
|
+
kind: string;
|
|
70
|
+
region?: string;
|
|
71
|
+
sessions: number;
|
|
72
|
+
}>;
|
|
73
|
+
edges: Array<{
|
|
74
|
+
from: string;
|
|
75
|
+
to: string;
|
|
76
|
+
region?: string;
|
|
77
|
+
classes: Record<string, number>;
|
|
78
|
+
subjects: number;
|
|
79
|
+
sessions: number;
|
|
80
|
+
flags: Array<"outside_region" | "region_unknown">;
|
|
81
|
+
}>;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* §3a: many sessions' lineage as one map: where data is read from (nodes on the left) and sent to,
|
|
85
|
+
* with how many subjects and sessions each flow carried. With `dataRegion`, a flow to a model in
|
|
86
|
+
* another region is flagged `outside_region`, one to a model of no known region `region_unknown`.
|
|
87
|
+
*/
|
|
88
|
+
export declare function dataTopology(sessions: ReadonlyArray<{
|
|
89
|
+
session_id: string;
|
|
90
|
+
lines: readonly string[];
|
|
91
|
+
}>, dataRegion?: string | null): Topology;
|
|
92
|
+
export interface DataRules {
|
|
93
|
+
identifiers: readonly Identifier[];
|
|
94
|
+
residency: boolean;
|
|
95
|
+
restricted: ReadonlyArray<{
|
|
96
|
+
identifiers: readonly string[];
|
|
97
|
+
allow: readonly string[];
|
|
98
|
+
}>;
|
|
99
|
+
evidence: ReadonlyArray<{
|
|
100
|
+
tool: string;
|
|
101
|
+
field: string;
|
|
102
|
+
}>;
|
|
103
|
+
}
|
|
104
|
+
export interface DataFinding {
|
|
105
|
+
code: "DATA_LEFT_REGION" | "DATA_REGION_UNKNOWN" | "DATA_TO_UNAPPROVED_DESTINATION" | "DATA_CANARY_TRIPPED" | "CONSENT_WITHDRAWN_USE" | "EGRESS_OPEN" | "TOOL_WITNESS_INVALID" | "UNEVIDENCED_SIDE_EFFECT";
|
|
106
|
+
severity: "advisory" | "caution" | "warning";
|
|
107
|
+
ref: Record<string, string | number>;
|
|
108
|
+
}
|
|
109
|
+
/** §4, §6, §7, §9: findings over a session's events. */
|
|
110
|
+
export declare function dataFindings(lines: readonly string[], rules: Pick<DataRules, "identifiers" | "residency" | "restricted">, dataRegion: string | null): DataFinding[];
|
|
111
|
+
export declare const hasEvidence: (value: unknown, field: string) => boolean;
|
|
112
|
+
/** §5: successful results of state-changing tools with no evidence of their own. */
|
|
113
|
+
export declare function unevidenced(lines: readonly string[], body: (hash: string) => Uint8Array | undefined, rules: DataRules["evidence"]): DataFinding[];
|
|
114
|
+
/** §9: a tool signs what it returns, so the result is the tool's word, not the agent's. */
|
|
115
|
+
export declare function toolWitness(keypair: {
|
|
116
|
+
did?: string;
|
|
117
|
+
privateKey: Uint8Array;
|
|
118
|
+
}): {
|
|
119
|
+
did: string;
|
|
120
|
+
sign(result: unknown): {
|
|
121
|
+
result: unknown;
|
|
122
|
+
witness: {
|
|
123
|
+
did: string;
|
|
124
|
+
sig: string;
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
/** §9: does `witness` sign `result`? */
|
|
129
|
+
export declare function verifyToolWitness(result: unknown, witness: {
|
|
130
|
+
did?: unknown;
|
|
131
|
+
sig?: unknown;
|
|
132
|
+
}): boolean;
|
|
133
|
+
/** §9: the witness an MCP result or an SDK tool.result body carries, checked; undefined if none. */
|
|
134
|
+
export declare function witnessOf(bodyJson: unknown): {
|
|
135
|
+
did: string;
|
|
136
|
+
ok: boolean;
|
|
137
|
+
} | undefined;
|
|
138
|
+
export interface Connector {
|
|
139
|
+
v: 1;
|
|
140
|
+
id: string;
|
|
141
|
+
format: "csv" | "jsonl";
|
|
142
|
+
fields: {
|
|
143
|
+
at: string;
|
|
144
|
+
action: string;
|
|
145
|
+
object?: string;
|
|
146
|
+
evidence?: string;
|
|
147
|
+
amount?: string;
|
|
148
|
+
actor?: string;
|
|
149
|
+
};
|
|
150
|
+
actions: Record<string, string>;
|
|
151
|
+
/** Which tool arguments hold the object and the amount the call acted on. */
|
|
152
|
+
arguments?: {
|
|
153
|
+
object?: string;
|
|
154
|
+
amount?: string;
|
|
155
|
+
};
|
|
156
|
+
window_seconds?: number;
|
|
157
|
+
}
|
|
158
|
+
export interface AuditRow {
|
|
159
|
+
row: number;
|
|
160
|
+
at: number;
|
|
161
|
+
action: string;
|
|
162
|
+
object?: string;
|
|
163
|
+
evidence?: string;
|
|
164
|
+
amount?: string;
|
|
165
|
+
actor?: string;
|
|
166
|
+
}
|
|
167
|
+
/** §10: why a connector is invalid, or undefined. */
|
|
168
|
+
export declare function checkAuditConnector(value: unknown): string | undefined;
|
|
169
|
+
/** RFC 4180: a header row, then rows; quoted fields may hold commas, quotes ("") and newlines. */
|
|
170
|
+
export declare function parseCsv(text: string): string[][];
|
|
171
|
+
/** §10: the rows of an audit-log export, in file order; rows without a time or action are skipped. */
|
|
172
|
+
export declare function parseAuditLog(text: string, connector: Connector): AuditRow[];
|
|
173
|
+
export interface SystemReport {
|
|
174
|
+
connector: string;
|
|
175
|
+
calls: number;
|
|
176
|
+
rows: number;
|
|
177
|
+
matched: Array<{
|
|
178
|
+
seq: number;
|
|
179
|
+
row: number;
|
|
180
|
+
by: "evidence" | "action";
|
|
181
|
+
}>;
|
|
182
|
+
findings: Array<{
|
|
183
|
+
code: "AGENT_ACTION_NOT_IN_SYSTEM" | "SYSTEM_CHANGE_NOT_IN_RECORD" | "SYSTEM_RECORD_MISMATCH";
|
|
184
|
+
ref: {
|
|
185
|
+
seq?: number;
|
|
186
|
+
row?: number;
|
|
187
|
+
};
|
|
188
|
+
}>;
|
|
189
|
+
}
|
|
190
|
+
/** §10: the session's successful mapped tool calls against the system's own log. */
|
|
191
|
+
export declare function reconcileSystem(lines: readonly string[], body: (hash: string) => Uint8Array | undefined, rows: readonly AuditRow[], connector: Connector, evidence: DataRules["evidence"]): SystemReport;
|