@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,85 @@
1
+ /** GETs a gateway path; the CLI supplies it with BLACKBOX_URL + the key. */
2
+ export type Get = (path: string) => Promise<{
3
+ status: number;
4
+ text: string;
5
+ }>;
6
+ export declare const TOOLS: ({
7
+ name: string;
8
+ description: string;
9
+ inputSchema: {
10
+ type: string;
11
+ properties: {
12
+ limit: {
13
+ type: string;
14
+ minimum: number;
15
+ maximum: number;
16
+ default: number;
17
+ };
18
+ };
19
+ required?: never;
20
+ };
21
+ } | {
22
+ name: string;
23
+ description: string;
24
+ inputSchema: {
25
+ type: string;
26
+ properties: {
27
+ session_id: {
28
+ type: string;
29
+ description: string;
30
+ };
31
+ limit?: never;
32
+ lang: {
33
+ type: string;
34
+ enum: string[];
35
+ default: string;
36
+ };
37
+ };
38
+ required: string[];
39
+ };
40
+ } | {
41
+ name: string;
42
+ description: string;
43
+ inputSchema: {
44
+ type: string;
45
+ properties: {};
46
+ };
47
+ } | {
48
+ name: string;
49
+ description: string;
50
+ inputSchema: {
51
+ type: string;
52
+ properties: {
53
+ session_id: {
54
+ type: string;
55
+ description: string;
56
+ };
57
+ after_seq: {
58
+ type: string;
59
+ minimum: number;
60
+ default: number;
61
+ };
62
+ limit: {
63
+ type: string;
64
+ minimum: number;
65
+ maximum: number;
66
+ default: number;
67
+ };
68
+ };
69
+ required: string[];
70
+ };
71
+ })[];
72
+ type Result = {
73
+ content: Array<{
74
+ type: "text";
75
+ text: string;
76
+ }>;
77
+ isError: boolean;
78
+ };
79
+ /** One tools/call. Never throws: failures are `isError` results. */
80
+ export declare function callTool(get: Get, name: unknown, args: unknown): Promise<Result>;
81
+ /** Answers one JSON-RPC line; undefined for a notification. */
82
+ export declare function handleLine(get: Get, line: string): Promise<object | undefined>;
83
+ /** Serves stdin → stdout until stdin closes; one request at a time, in order. */
84
+ export declare function serveMcp(get: Get): Promise<number>;
85
+ export {};
@@ -0,0 +1,216 @@
1
+ // The Blackbox MCP server (spec/mcp-server.md): read-only tools over the gateway's API, on stdio.
2
+ // Mirrors sdks/python/src/zanii_blackbox/mcp_server.py.
3
+ import { createInterface } from "node:readline";
4
+ const SESSION_ID = /^ses_[A-Za-z0-9]{16,64}$/;
5
+ const sid = {
6
+ type: "object",
7
+ properties: { session_id: { type: "string", description: "A session id (ses_…)" } },
8
+ required: ["session_id"],
9
+ };
10
+ const none = { type: "object", properties: {} };
11
+ export const TOOLS = [
12
+ {
13
+ name: "list_sessions",
14
+ description: "The newest agent sessions the flight recorder holds.",
15
+ inputSchema: {
16
+ type: "object",
17
+ properties: { limit: { type: "integer", minimum: 1, maximum: 200, default: 20 } },
18
+ },
19
+ },
20
+ {
21
+ name: "session_summary",
22
+ description: "A session's handoff brief: objective, status, calls, tools, spend, findings, claims.",
23
+ inputSchema: sid,
24
+ },
25
+ {
26
+ name: "verify_session",
27
+ description: "Whether a session's record verifies: the hash chain, its anchors, its final count.",
28
+ inputSchema: sid,
29
+ },
30
+ {
31
+ name: "incident_report",
32
+ description: "A session's incident report in Markdown, in English or Arabic.",
33
+ inputSchema: {
34
+ type: "object",
35
+ properties: {
36
+ ...sid.properties,
37
+ lang: { type: "string", enum: ["en", "ar"], default: "en" },
38
+ },
39
+ required: ["session_id"],
40
+ },
41
+ },
42
+ {
43
+ name: "fleet",
44
+ description: "The open sessions right now: the fleet radar.",
45
+ inputSchema: none,
46
+ },
47
+ {
48
+ name: "collisions",
49
+ description: "Resources that two or more open sessions are changing at once.",
50
+ inputSchema: none,
51
+ },
52
+ {
53
+ name: "logbook",
54
+ description: "Per agent label: outcomes, warnings, near misses, exceedances, spend.",
55
+ inputSchema: none,
56
+ },
57
+ // audit, also found: more of the read API, for an assistant helping with an incident
58
+ {
59
+ name: "session_findings",
60
+ description: "A session's findings: fault codes, severities, where in the record.",
61
+ inputSchema: sid,
62
+ },
63
+ {
64
+ name: "session_cost",
65
+ description: "A session's cost, call by call, in micro-USD and AED.",
66
+ inputSchema: sid,
67
+ },
68
+ {
69
+ name: "session_state",
70
+ description: "A session right now: open or closed, blocked or not, who has the controls.",
71
+ inputSchema: sid,
72
+ },
73
+ {
74
+ name: "session_events",
75
+ description: "A session's recorded events (NDJSON), from after_seq, at most limit of them.",
76
+ inputSchema: {
77
+ type: "object",
78
+ properties: {
79
+ ...sid.properties,
80
+ after_seq: { type: "integer", minimum: -1, default: -1 },
81
+ limit: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
82
+ },
83
+ required: ["session_id"],
84
+ },
85
+ },
86
+ {
87
+ name: "weather",
88
+ description: "Each provider and model's error rate and latency over the last minutes.",
89
+ inputSchema: none,
90
+ },
91
+ ];
92
+ const text = (t, isError = false) => ({
93
+ content: [{ type: "text", text: t }],
94
+ isError,
95
+ });
96
+ /** One tools/call. Never throws: failures are `isError` results. */
97
+ export async function callTool(get, name, args) {
98
+ const a = (typeof args === "object" && args !== null ? args : {});
99
+ const id = a.session_id;
100
+ const needsId = [
101
+ "session_summary",
102
+ "verify_session",
103
+ "incident_report",
104
+ "session_findings",
105
+ "session_cost",
106
+ "session_state",
107
+ "session_events",
108
+ ].includes(String(name));
109
+ if (needsId && !(typeof id === "string" && SESSION_ID.test(id)))
110
+ return text("session_id must be a session id (ses_…)", true);
111
+ let path;
112
+ if (name === "list_sessions") {
113
+ const limit = a.limit ?? 20;
114
+ if (!(Number.isSafeInteger(limit) && limit >= 1 && limit <= 200))
115
+ return text("limit must be an integer 1-200", true);
116
+ path = `/v1/sessions?limit=${limit}`;
117
+ }
118
+ else if (name === "session_summary")
119
+ path = `/v1/sessions/${id}/summary`;
120
+ else if (name === "verify_session")
121
+ path = `/v1/sessions/${id}`;
122
+ else if (name === "incident_report") {
123
+ const lang = a.lang ?? "en";
124
+ if (lang !== "en" && lang !== "ar")
125
+ return text("lang must be en or ar", true);
126
+ path = `/v1/sessions/${id}/incident?lang=${lang}`;
127
+ }
128
+ else if (name === "fleet")
129
+ path = "/v1/fleet";
130
+ else if (name === "collisions")
131
+ path = "/v1/fleet/collisions";
132
+ else if (name === "logbook")
133
+ path = "/v1/logbook";
134
+ else if (name === "session_findings")
135
+ path = `/v1/sessions/${id}/findings`;
136
+ else if (name === "session_cost")
137
+ path = `/v1/sessions/${id}/cost`;
138
+ else if (name === "session_state")
139
+ path = `/v1/sessions/${id}/state`;
140
+ else if (name === "session_events") {
141
+ const after = a.after_seq ?? -1;
142
+ const limit = a.limit ?? 200;
143
+ if (!(Number.isSafeInteger(after) && after >= -1))
144
+ return text("after_seq must be an integer >= -1", true);
145
+ if (!(Number.isSafeInteger(limit) && limit >= 1 && limit <= 1000))
146
+ return text("limit must be an integer 1-1000", true);
147
+ path = `/v1/sessions/${id}/events?after_seq=${after}&limit=${limit}`;
148
+ }
149
+ else if (name === "weather")
150
+ path = "/v1/weather";
151
+ else
152
+ return text(`unknown tool: ${String(name)}`, true);
153
+ let res;
154
+ try {
155
+ res = await get(path);
156
+ }
157
+ catch (error) {
158
+ return text(error instanceof Error ? error.message : String(error), true);
159
+ }
160
+ if (res.status < 200 || res.status > 299)
161
+ return text(`HTTP ${res.status}: ${res.text}`, true);
162
+ if (name === "incident_report" || name === "session_events")
163
+ return text(res.text);
164
+ const doc = JSON.parse(res.text);
165
+ const out = name === "verify_session"
166
+ ? {
167
+ session_id: doc.session_id,
168
+ events: doc.events,
169
+ verification: doc.verification,
170
+ anchor_status: doc.anchor_status,
171
+ }
172
+ : doc;
173
+ return text(JSON.stringify(out, null, 2));
174
+ }
175
+ /** Answers one JSON-RPC line; undefined for a notification. */
176
+ export async function handleLine(get, line) {
177
+ let msg;
178
+ try {
179
+ msg = JSON.parse(line);
180
+ }
181
+ catch {
182
+ return { jsonrpc: "2.0", id: null, error: { code: -32700, message: "parse error" } };
183
+ }
184
+ if (typeof msg !== "object" || msg === null || !("id" in msg))
185
+ return undefined;
186
+ const reply = (result) => ({ jsonrpc: "2.0", id: msg.id, result });
187
+ if (msg.method === "initialize")
188
+ return reply({
189
+ protocolVersion: "2025-06-18",
190
+ capabilities: { tools: {} },
191
+ serverInfo: { name: "zanii-blackbox", version: "1" },
192
+ });
193
+ if (msg.method === "ping")
194
+ return reply({});
195
+ if (msg.method === "tools/list")
196
+ return reply({ tools: TOOLS });
197
+ if (msg.method === "tools/call")
198
+ return reply(await callTool(get, msg.params?.name, msg.params?.arguments));
199
+ return {
200
+ jsonrpc: "2.0",
201
+ id: msg.id,
202
+ error: { code: -32601, message: `method not found: ${String(msg.method)}` },
203
+ };
204
+ }
205
+ /** Serves stdin → stdout until stdin closes; one request at a time, in order. */
206
+ export async function serveMcp(get) {
207
+ const lines = createInterface({ input: process.stdin, crlfDelay: Number.POSITIVE_INFINITY });
208
+ for await (const line of lines) {
209
+ if (line.trim() === "")
210
+ continue;
211
+ const out = await handleLine(get, line);
212
+ if (out)
213
+ process.stdout.write(`${JSON.stringify(out)}\n`);
214
+ }
215
+ return 0;
216
+ }
@@ -0,0 +1,17 @@
1
+ import type { Readable, Writable } from "node:stream";
2
+ export interface McpWrapOptions {
3
+ /** The name recorded as `meta.server`. */
4
+ server: string;
5
+ /** The real MCP server: command and arguments. */
6
+ command: string[];
7
+ /** The gateway, e.g. http://127.0.0.1:8787 */
8
+ gatewayUrl: string;
9
+ /** The session token. Never passed to the MCP server. */
10
+ token: string;
11
+ env?: NodeJS.ProcessEnv;
12
+ stdin?: Readable;
13
+ stdout?: Writable;
14
+ stderr?: Writable;
15
+ }
16
+ /** Resolves with the MCP server's exit code once both sides are done. */
17
+ export declare function mcpWrap(options: McpWrapOptions): Promise<number>;
@@ -0,0 +1,170 @@
1
+ // mcp-wrap (spec/mcp.md §5): runs a local stdio MCP server and records every JSON-RPC line through
2
+ // the gateway before relaying it. Mirrors sdks/python/src/zanii_blackbox/mcp_wrap.py.
3
+ import { spawn } from "node:child_process";
4
+ import { Agent as HttpAgent, request as httpRequest } from "node:http";
5
+ import { Agent as HttpsAgent, request as httpsRequest } from "node:https";
6
+ const NAME = /^[a-z][a-z0-9-]{0,31}$/;
7
+ const NL = Buffer.from("\n");
8
+ /** Resolves with the MCP server's exit code once both sides are done. */
9
+ export async function mcpWrap(options) {
10
+ const { server, command, gatewayUrl, token } = options;
11
+ const stdin = options.stdin ?? process.stdin;
12
+ const stdout = options.stdout ?? process.stdout;
13
+ const stderr = options.stderr ?? process.stderr;
14
+ if (!NAME.test(server))
15
+ throw new Error(`invalid --server name "${server}"`);
16
+ const [file, ...args] = command;
17
+ if (!file)
18
+ throw new Error("no MCP server command given");
19
+ const env = { ...(options.env ?? process.env) };
20
+ // The MCP server is third-party code: it gets none of Blackbox's settings or secrets (the session
21
+ // token, an admin key, provider keys).
22
+ for (const k of Object.keys(env))
23
+ if (k.startsWith("BLACKBOX_"))
24
+ delete env[k];
25
+ // Windows can only start .cmd launchers (npx, uvx) through a shell; quote so arguments survive it.
26
+ const child = process.platform === "win32"
27
+ ? spawn([file, ...args].map(quoteWindows).join(" "), {
28
+ shell: true,
29
+ env,
30
+ stdio: ["pipe", "pipe", "inherit"],
31
+ })
32
+ : spawn(file, args, { env, stdio: ["pipe", "pipe", "inherit"] });
33
+ child.stdin.on("error", () => { }); // the server exited early; its exit code tells the story
34
+ const exited = new Promise((resolve) => {
35
+ child.on("error", (error) => {
36
+ stderr.write(`blackbox mcp-wrap: could not start the MCP server: ${error.message}\n`);
37
+ resolve(127);
38
+ });
39
+ child.on("close", (code) => resolve(code ?? 1));
40
+ });
41
+ const base = new URL(gatewayUrl);
42
+ const agent = base.protocol === "https:"
43
+ ? new HttpsAgent({ keepAlive: true })
44
+ : new HttpAgent({ keepAlive: true });
45
+ const send = base.protocol === "https:" ? httpsRequest : httpRequest;
46
+ const endpoint = (d) => new URL(`${base.pathname.replace(/\/$/, "")}/v1/mcp-stdio/${server}/${d}`, base);
47
+ // L1.4.5: the session's mode (from the gateway's answers) and what we relayed unrecorded.
48
+ let mode = null;
49
+ let missed = 0;
50
+ let missedSince = "";
51
+ const UNREACHABLE = "the gateway could not be reached";
52
+ const record = (d, line) => new Promise((resolve) => {
53
+ const req = send(endpoint(d), {
54
+ method: "POST",
55
+ agent,
56
+ timeout: 30_000,
57
+ headers: {
58
+ authorization: `Bearer ${token}`,
59
+ "content-type": "application/json",
60
+ "content-length": line.length,
61
+ ...(missed > 0
62
+ ? { "x-blackbox-missed": String(missed), "x-blackbox-missed-since": missedSince }
63
+ : {}),
64
+ },
65
+ });
66
+ req.on("response", (res) => {
67
+ const parts = [];
68
+ res.on("data", (d) => parts.push(d));
69
+ res.on("end", () => {
70
+ if (res.statusCode === 204) {
71
+ const m = res.headers["x-blackbox-mode"];
72
+ if (typeof m === "string")
73
+ mode = m;
74
+ missed = 0; // the gateway recorded the gap before this line
75
+ return resolve(null);
76
+ }
77
+ // A call the gateway recorded and refused by policy: relay its JSON-RPC error (spec/policy.md §2).
78
+ if (res.statusCode === 403)
79
+ try {
80
+ const rpc = JSON.parse(Buffer.concat(parts).toString("utf8")).rpc_error;
81
+ if (rpc && typeof rpc === "object")
82
+ return resolve({ rpcError: rpc });
83
+ }
84
+ catch { }
85
+ resolve(`the gateway answered ${res.statusCode}`);
86
+ });
87
+ });
88
+ req.on("timeout", () => req.destroy(new Error("timed out")));
89
+ req.on("error", (error) => resolve(`${UNREACHABLE} (${error.message})`));
90
+ req.end(line);
91
+ });
92
+ const write = (target, data) => new Promise((resolve) => {
93
+ if (target.destroyed || target.writableEnded)
94
+ return resolve();
95
+ target.write(data, () => resolve());
96
+ });
97
+ // One queue for both directions: the record's order is the order lines arrived in.
98
+ let queue = Promise.resolve();
99
+ const relay = (d, line, newline) => {
100
+ queue = queue.then(async () => {
101
+ const target = d === "client" ? child.stdin : stdout;
102
+ const out = newline ? Buffer.concat([line, NL]) : line;
103
+ if (line.toString().trim() === "")
104
+ return write(target, out);
105
+ const failure = await record(d, line);
106
+ if (failure === null)
107
+ return write(target, out);
108
+ // An `available` session keeps working when the gateway is down; the gap is recorded later.
109
+ if (mode === "available" && typeof failure === "string" && failure.startsWith(UNREACHABLE)) {
110
+ if (missed === 0) {
111
+ missedSince = new Date().toISOString();
112
+ stderr.write(`blackbox mcp-wrap: ${failure}; relaying unrecorded (available session)\n`);
113
+ }
114
+ missed++;
115
+ return write(target, out);
116
+ }
117
+ stderr.write(typeof failure === "string"
118
+ ? `blackbox mcp-wrap: a ${d} message was not recorded, so not relayed: ${failure}\n`
119
+ : `blackbox mcp-wrap: a ${d} message was refused by the gateway's policy\n`);
120
+ const reply = refusal(d, line, failure);
121
+ if (reply)
122
+ await write(stdout, reply);
123
+ });
124
+ };
125
+ const done = (d, source) => new Promise((resolve) => {
126
+ let pending = Buffer.alloc(0);
127
+ source.on("data", (chunk) => {
128
+ pending = Buffer.concat([pending, chunk]);
129
+ for (let i = pending.indexOf(0x0a); i >= 0; i = pending.indexOf(0x0a)) {
130
+ relay(d, pending.subarray(0, i), true);
131
+ pending = pending.subarray(i + 1);
132
+ }
133
+ });
134
+ source.on("end", () => {
135
+ if (pending.length > 0)
136
+ relay(d, pending, false);
137
+ resolve();
138
+ });
139
+ });
140
+ void done("client", stdin).then(() => queue.then(() => child.stdin.end()));
141
+ await done("server", child.stdout);
142
+ const code = await exited;
143
+ await queue;
144
+ stdin.destroy(); // the server is gone: stop reading, or the open stdin keeps the process alive
145
+ agent.destroy();
146
+ return code;
147
+ }
148
+ /** What the agent gets instead of a message that couldn't be recorded (spec/mcp.md §5). */
149
+ function refusal(d, line, reason) {
150
+ let message;
151
+ try {
152
+ message = JSON.parse(line.toString("utf8"));
153
+ }
154
+ catch {
155
+ return null;
156
+ }
157
+ if (typeof message !== "object" || message === null || !("id" in message))
158
+ return null;
159
+ const isRequest = "method" in message;
160
+ const isResponse = "result" in message || "error" in message;
161
+ if ((d === "client" && !isRequest) || (d === "server" && !isResponse))
162
+ return null;
163
+ const error = typeof reason === "string"
164
+ ? { code: -32001, message: `blackbox: not recorded, so not relayed (${reason})` }
165
+ : reason.rpcError;
166
+ return Buffer.from(`${JSON.stringify({ jsonrpc: "2.0", id: message.id, error })}\n`);
167
+ }
168
+ function quoteWindows(arg) {
169
+ return /^[\w\-./:=@\\]+$/.test(arg) ? arg : `"${arg.replace(/"/g, '""')}"`;
170
+ }
@@ -0,0 +1,114 @@
1
+ import { type LoadedPrices } from "../cost/index.ts";
2
+ import type { Summary } from "../fleet/index.ts";
3
+ /** `{model: micro_usd}` over the priced calls, sorted by model. */
4
+ export declare function modelSpend(lines: readonly string[], prices: LoadedPrices): Record<string, number>;
5
+ export interface Statement {
6
+ lines: Array<{
7
+ model: string;
8
+ micro_usd: number;
9
+ }>;
10
+ }
11
+ /** Why a statement is invalid, or undefined. */
12
+ export declare function checkStatement(s: unknown): string | undefined;
13
+ export declare function reconcileBilling(ours: Record<string, number>, statement: Statement, tolerancePpm?: number): {
14
+ v: 1;
15
+ tolerance_ppm: number;
16
+ ours_micro_usd: number;
17
+ theirs_micro_usd: number;
18
+ diff_micro_usd: number;
19
+ ok: boolean;
20
+ models: {
21
+ model: string;
22
+ ours: number;
23
+ theirs: number;
24
+ diff: number;
25
+ status: string;
26
+ }[];
27
+ };
28
+ export interface Baseline {
29
+ label: string;
30
+ successes: number;
31
+ median_spend_micro_usd: number;
32
+ p90_spend_micro_usd: number;
33
+ median_model_calls: number;
34
+ median_tool_calls: number;
35
+ }
36
+ /** Per label, over successful sessions only (spec/money.md §3). */
37
+ export declare function baselines(summaries: readonly Summary[]): {
38
+ labels: Baseline[];
39
+ };
40
+ /** Will the remaining budget reach the destination? (spec/money.md §4) */
41
+ export declare function fuel(summary: Summary, budgetMicroUsd: number | null, baseline: Baseline | null): {
42
+ spent_micro_usd: number;
43
+ budget_micro_usd: number | null;
44
+ expected_total_micro_usd: number | null;
45
+ p90_total_micro_usd: number | null;
46
+ verdict: string;
47
+ };
48
+ export type AmountUnit = "usd" | "cents" | "micro_usd" | "aed";
49
+ /** spec/money.md §2a-b: an exact decimal amount (string, or a JSON number's text) to integer
50
+ * micro-USD, rounded half up. `unit` is what the amount counts. */
51
+ export declare function decimalToMicroUsd(value: unknown, unit: AmountUnit): number;
52
+ /** spec/money.md §2a: Anthropic's cost report pages (grouped by description) as a statement. */
53
+ export declare function anthropicStatement(pages: readonly unknown[]): Statement;
54
+ /** spec/money.md §2a: OpenAI's organization costs pages (grouped by line_item) as a statement. */
55
+ export declare function openaiStatement(pages: readonly unknown[]): Statement;
56
+ /** spec/money.md §2b: every value at a path (`a.b[].c`, `[]` alone for a list itself). */
57
+ export declare function resolvePath(value: unknown, path: string): unknown[];
58
+ /** spec/money.md §2b: a CSV export with named columns as a statement. */
59
+ export declare function csvStatement(text: string, opts: {
60
+ model_column: string;
61
+ amount_column: string;
62
+ unit: AmountUnit;
63
+ delimiter?: string;
64
+ }): Statement;
65
+ export interface Connector {
66
+ v: 1;
67
+ name: string;
68
+ request: {
69
+ url: string;
70
+ headers?: Record<string, string>;
71
+ method?: "GET" | "POST";
72
+ body?: string;
73
+ content_type?: string;
74
+ };
75
+ results: string;
76
+ model: string;
77
+ amount: string;
78
+ unit: AmountUnit;
79
+ currency?: {
80
+ path: string;
81
+ equals: string;
82
+ };
83
+ /** spec/money.md §2b: how responses are read; json by default. */
84
+ format?: "json" | "xml";
85
+ next?: {
86
+ cursor: string;
87
+ param: string;
88
+ } | {
89
+ page_param: string;
90
+ first_page?: number;
91
+ size_param?: string;
92
+ size?: number;
93
+ } | {
94
+ offset_param: string;
95
+ size: number;
96
+ size_param?: string;
97
+ };
98
+ }
99
+ /** spec/money.md §2b: why a connector file is invalid, or undefined. */
100
+ export declare function checkConnector(value: unknown): string | undefined;
101
+ /** spec/money.md §2b: pages from a connector's API as a statement. */
102
+ export declare function customStatement(pages: readonly unknown[], connector: Connector): Statement;
103
+ /** spec/money.md §2b: the query parameters for the next request, or null when there are no more. */
104
+ export declare function pageParams(c: Connector, pages: readonly unknown[]): Array<[string, string]> | null;
105
+ /** spec/money.md §2b: XML as `{root: [value]}`. No DTDs, so no external or expanding entities. */
106
+ export declare function parseXml(text: string): Record<string, unknown>;
107
+ /** spec/money.md §2b: the next request a connector makes, or null when there are no more. */
108
+ export declare function requestFor(c: Connector, pages: readonly unknown[], from: string, to: string): {
109
+ method: "GET" | "POST";
110
+ url: string;
111
+ query: Array<[string, string]>;
112
+ body?: string;
113
+ content_type?: string;
114
+ } | null;