@zanii/blackbox 0.0.0-stage → 0.1.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/LICENSE +202 -0
- package/README.md +109 -2
- package/dist/agents/index.d.ts +34 -0
- package/dist/agents/index.js +73 -0
- package/dist/analysis/detectors.d.ts +36 -0
- package/dist/analysis/detectors.js +339 -0
- package/dist/analysis/faults.d.ts +9 -0
- package/dist/analysis/faults.js +210 -0
- package/dist/analysis/index.d.ts +68 -0
- package/dist/analysis/index.js +388 -0
- package/dist/analysis/landing.d.ts +25 -0
- package/dist/analysis/landing.js +225 -0
- package/dist/analysis/memory.d.ts +13 -0
- package/dist/analysis/memory.js +33 -0
- package/dist/analysis/waste.d.ts +29 -0
- package/dist/analysis/waste.js +79 -0
- package/dist/approvals/index.d.ts +11 -0
- package/dist/approvals/index.js +27 -0
- package/dist/approvals/warnings.d.ts +2 -0
- package/dist/approvals/warnings.js +28 -0
- package/dist/attest/index.d.ts +17 -0
- package/dist/attest/index.js +106 -0
- package/dist/authority/index.d.ts +24 -0
- package/dist/authority/index.js +77 -0
- package/dist/billing/index.d.ts +99 -0
- package/dist/billing/index.js +174 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1019 -0
- package/dist/client/index.d.ts +146 -0
- package/dist/client/index.js +210 -0
- package/dist/compliance/index.d.ts +41 -0
- package/dist/compliance/index.js +96 -0
- package/dist/cost/index.d.ts +133 -0
- package/dist/cost/index.js +293 -0
- package/dist/directives/index.d.ts +35 -0
- package/dist/directives/index.js +80 -0
- package/dist/drills/index.d.ts +43 -0
- package/dist/drills/index.js +101 -0
- package/dist/duty/index.d.ts +21 -0
- package/dist/duty/index.js +68 -0
- package/dist/fleet/index.d.ts +141 -0
- package/dist/fleet/index.js +454 -0
- package/dist/hooks/ai-sdk.d.ts +42 -0
- package/dist/hooks/ai-sdk.js +62 -0
- package/dist/hooks/claude-agent-sdk.d.ts +14 -0
- package/dist/hooks/claude-agent-sdk.js +70 -0
- package/dist/hooks/index.d.ts +7 -0
- package/dist/hooks/index.js +10 -0
- package/dist/hooks/langchain-agent.d.ts +69 -0
- package/dist/hooks/langchain-agent.js +163 -0
- package/dist/hooks/langchain.d.ts +41 -0
- package/dist/hooks/langchain.js +216 -0
- package/dist/hooks/langgraph-checkpoint.d.ts +12 -0
- package/dist/hooks/langgraph-checkpoint.js +73 -0
- package/dist/hooks/memory.d.ts +17 -0
- package/dist/hooks/memory.js +64 -0
- package/dist/hooks/openai-agents.d.ts +6 -0
- package/dist/hooks/openai-agents.js +40 -0
- package/dist/hooks/protect.d.ts +7 -0
- package/dist/hooks/protect.js +39 -0
- package/dist/hooks/providers.d.ts +16 -0
- package/dist/hooks/providers.js +149 -0
- package/dist/hooks/shared.d.ts +11 -0
- package/dist/hooks/shared.js +39 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +47 -0
- package/dist/investigate/index.d.ts +66 -0
- package/dist/investigate/index.js +119 -0
- package/dist/mcp-server/index.d.ts +85 -0
- package/dist/mcp-server/index.js +216 -0
- package/dist/mcp-wrap/index.d.ts +17 -0
- package/dist/mcp-wrap/index.js +170 -0
- package/dist/money/index.d.ts +114 -0
- package/dist/money/index.js +622 -0
- package/dist/occurrence/index.d.ts +108 -0
- package/dist/occurrence/index.js +168 -0
- package/dist/ocsf/index.d.ts +22 -0
- package/dist/ocsf/index.js +168 -0
- package/dist/otlp/index.d.ts +24 -0
- package/dist/otlp/index.js +143 -0
- package/dist/packs/index.d.ts +40 -0
- package/dist/packs/index.js +217 -0
- package/dist/policy/delta.d.ts +11 -0
- package/dist/policy/delta.js +39 -0
- package/dist/policy/drafts.d.ts +34 -0
- package/dist/policy/drafts.js +129 -0
- package/dist/policy/index.d.ts +47 -0
- package/dist/policy/index.js +154 -0
- package/dist/precog/index.d.ts +96 -0
- package/dist/precog/index.js +167 -0
- package/dist/precog/intervention.d.ts +22 -0
- package/dist/precog/intervention.js +44 -0
- package/dist/precog/normal.d.ts +31 -0
- package/dist/precog/normal.js +89 -0
- package/dist/preflight/index.d.ts +11 -0
- package/dist/preflight/index.js +19 -0
- package/dist/ratings/index.d.ts +21 -0
- package/dist/ratings/index.js +48 -0
- package/dist/reconcile/claude-code.d.ts +19 -0
- package/dist/reconcile/claude-code.js +220 -0
- package/dist/reconcile/codex.d.ts +5 -0
- package/dist/reconcile/codex.js +191 -0
- package/dist/reconcile/index.d.ts +19 -0
- package/dist/reconcile/index.js +50 -0
- package/dist/reconcile/record.d.ts +49 -0
- package/dist/reconcile/record.js +225 -0
- package/dist/reconcile/shared.d.ts +65 -0
- package/dist/reconcile/shared.js +113 -0
- package/dist/replay/index.d.ts +11 -0
- package/dist/replay/index.js +64 -0
- package/dist/replay/repair.d.ts +10 -0
- package/dist/replay/repair.js +62 -0
- package/dist/session/drain.d.ts +13 -0
- package/dist/session/drain.js +35 -0
- package/dist/session/index.d.ts +256 -0
- package/dist/session/index.js +658 -0
- package/dist/undo/index.d.ts +45 -0
- package/dist/undo/index.js +212 -0
- package/dist/verify/anchor.d.ts +54 -0
- package/dist/verify/anchor.js +77 -0
- package/dist/verify/chain.d.ts +27 -0
- package/dist/verify/chain.js +105 -0
- package/dist/verify/envelope.d.ts +28 -0
- package/dist/verify/envelope.js +55 -0
- package/dist/verify/index.d.ts +3 -0
- package/dist/verify/index.js +3 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +2 -0
- package/dist/weather/index.d.ts +24 -0
- package/dist/weather/index.js +45 -0
- package/dist/workspace-receipt/index.d.ts +15 -0
- package/dist/workspace-receipt/index.js +121 -0
- package/package.json +56 -3
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Investigations (spec/investigations.md): the independent investigator's report and its reading
|
|
2
|
+
// copy. Pure; mirrors sdks/python/src/zanii_blackbox/investigate.py; pinned by
|
|
3
|
+
// spec/vectors/investigations.json.
|
|
4
|
+
import { faultOf } from "../analysis/faults.js";
|
|
5
|
+
import { authorityTimeline } from "../authority/index.js";
|
|
6
|
+
import { drillReport } from "../drills/index.js";
|
|
7
|
+
import { signatureOf } from "../fleet/index.js";
|
|
8
|
+
/** spec/investigations.md §1. */
|
|
9
|
+
export function investigation(lines, opts) {
|
|
10
|
+
const { summary, models } = opts;
|
|
11
|
+
const authority = authorityTimeline(lines);
|
|
12
|
+
const spanAt = (seq) => authority.find((s) => seq >= s.from_seq && (s.to_seq === null || seq <= s.to_seq));
|
|
13
|
+
const findings = lines
|
|
14
|
+
.map((l) => JSON.parse(l))
|
|
15
|
+
.filter((e) => e.kind === "finding")
|
|
16
|
+
.map((e) => {
|
|
17
|
+
const code = String(e.meta.code);
|
|
18
|
+
const ref = e.meta.ref;
|
|
19
|
+
const span = spanAt(typeof ref?.seq === "number" ? ref.seq : e.seq);
|
|
20
|
+
return {
|
|
21
|
+
seq: e.seq,
|
|
22
|
+
code,
|
|
23
|
+
fault: faultOf(code).fault,
|
|
24
|
+
severity: String(e.meta.severity),
|
|
25
|
+
source: String(e.meta.source),
|
|
26
|
+
in_control: span?.mode ?? null,
|
|
27
|
+
in_control_by: span?.by ?? null,
|
|
28
|
+
weather: e.meta.weather ?? null,
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
const signature = signatureOf(summary)?.signature;
|
|
32
|
+
const matches = (d) => d.signature === signature &&
|
|
33
|
+
("model" in d.applies_to
|
|
34
|
+
? models.includes(d.applies_to.model)
|
|
35
|
+
: Object.hasOwn(summary.tools, d.applies_to.tool));
|
|
36
|
+
const drill = drillReport(lines);
|
|
37
|
+
return {
|
|
38
|
+
kind: "blackbox.investigation",
|
|
39
|
+
v: 1,
|
|
40
|
+
session_id: summary.session_id,
|
|
41
|
+
label: summary.label,
|
|
42
|
+
started_at: summary.started_at,
|
|
43
|
+
last_event_at: summary.last_event_at,
|
|
44
|
+
events: summary.events,
|
|
45
|
+
outcome: summary.outcome,
|
|
46
|
+
authority,
|
|
47
|
+
findings,
|
|
48
|
+
directives: opts.directives
|
|
49
|
+
? opts.directives.filter(matches).map((d) => ({
|
|
50
|
+
id: d.id,
|
|
51
|
+
applies_to: d.applies_to,
|
|
52
|
+
pattern: d.pattern,
|
|
53
|
+
affected_ppm: d.affected_ppm,
|
|
54
|
+
}))
|
|
55
|
+
: null,
|
|
56
|
+
drill: drill.drill ? drill : null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const cell = (v) => v === null || v === undefined || v === ""
|
|
60
|
+
? "—"
|
|
61
|
+
: String(v)
|
|
62
|
+
.replace(/\r\n|\r|\n/g, " ")
|
|
63
|
+
.replace(/\|/g, "\\|");
|
|
64
|
+
const row = (cells) => `| ${cells.map(cell).join(" | ")} |`;
|
|
65
|
+
const head = (names) => [row(names), `|${names.map(() => "---|").join("")}`];
|
|
66
|
+
/** spec/investigations.md §3: the reading copy. The signed JSON is the report; this isn't. */
|
|
67
|
+
export function renderInvestigation(r) {
|
|
68
|
+
const out = [`# Investigation: ${r.session_id}`, "", ...head(["Fact", "Value"])];
|
|
69
|
+
const v = r.verification;
|
|
70
|
+
out.push(row(["Label", r.label]), row(["Started", r.started_at]), row(["Last event", r.last_event_at]), row(["Events", r.events]), row(["Outcome", r.outcome]), row([
|
|
71
|
+
"Record verified",
|
|
72
|
+
v === undefined
|
|
73
|
+
? "not checked"
|
|
74
|
+
: `${v.ok ? "yes" : "NO"} (identity ${v.identity ? "ok" : "not ok"}, ledger proofs ${v.proofs.replace("_", " ")})`,
|
|
75
|
+
]), row(["Bundle SHA-256", r.bundle_sha256]), row(["Investigator", r.signed ? r.signed.by : "unsigned"]), row(["Investigated at", r.investigated_at]));
|
|
76
|
+
if (v && !v.ok)
|
|
77
|
+
out.push("", "**The record does not verify. Everything below may have been altered.**");
|
|
78
|
+
out.push("", "## Who had the controls", "");
|
|
79
|
+
if (r.authority.length === 0)
|
|
80
|
+
out.push("No authority recorded.");
|
|
81
|
+
else {
|
|
82
|
+
out.push(...head(["From seq", "To seq", "Mode", "By", "Reason"]));
|
|
83
|
+
for (const s of r.authority)
|
|
84
|
+
out.push(row([s.from_seq, s.to_seq ?? "end", s.mode, s.by, s.reason]));
|
|
85
|
+
}
|
|
86
|
+
out.push("", "## Findings", "");
|
|
87
|
+
if (r.findings.length === 0)
|
|
88
|
+
out.push("None recorded.");
|
|
89
|
+
else {
|
|
90
|
+
out.push(...head(["Seq", "Code", "Fault", "Severity", "In control", "Weather"]));
|
|
91
|
+
for (const f of r.findings) {
|
|
92
|
+
const weather = Array.isArray(f.weather)
|
|
93
|
+
? f.weather
|
|
94
|
+
.map((w) => `${w.model}: ${w.status}`)
|
|
95
|
+
.join(", ")
|
|
96
|
+
: null;
|
|
97
|
+
const control = f.in_control === null ? null : `${f.in_control} (${f.in_control_by ?? "—"})`;
|
|
98
|
+
out.push(row([f.seq, f.code, f.fault, f.severity, control, weather]));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
out.push("", "## Matching directives", "");
|
|
102
|
+
if (r.directives === null)
|
|
103
|
+
out.push("No directives were given.");
|
|
104
|
+
else if (r.directives.length === 0)
|
|
105
|
+
out.push("None matched.");
|
|
106
|
+
else {
|
|
107
|
+
out.push(...head(["Directive", "Applies to", "Pattern", "Affected (ppm)"]));
|
|
108
|
+
for (const d of r.directives) {
|
|
109
|
+
const to = "model" in d.applies_to ? `model ${d.applies_to.model}` : `tool ${d.applies_to.tool}`;
|
|
110
|
+
out.push(row([d.id, to, d.pattern.join(", "), d.affected_ppm]));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (r.drill) {
|
|
114
|
+
const d = r.drill;
|
|
115
|
+
out.push("", "## Drill", "", `This session was a drill: ${d.planned.length} faults planned, ${d.injected.length} injected (${d.injected.map((i) => i.kind).join(", ") || "none"}).`, `Recovered: ${d.recovered ? "yes" : "no"}. Findings after the first fault: ${d.findings_after.join(", ") || "none"}.`);
|
|
116
|
+
}
|
|
117
|
+
out.push("", "## What this report proves", "", "If the record verifies, these events are the ones the gateway recorded, in this order, and unchanged since. Its chain is intact and its anchors are signed by the pinned identity; with ledger proofs checked, they were also on the ledger at the times shown.", "It does not prove what the agent intended, or that the providers and tools behaved as their answers say. It also can't show what happened outside the gateway.", "The signature shows who wrote this report. Check it with `blackbox verify-certificate <report>.json --did <investigator>`.", "");
|
|
118
|
+
return out.join("\n");
|
|
119
|
+
}
|
|
@@ -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
|
+
}
|