immune-brain 3.2.2 → 3.4.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 +2 -2
- package/README.zh-CN.md +3 -3
- package/package.json +5 -3
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +19 -73
- package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +1 -2
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +2 -2
- package/plugins/immune-brain/dist/BASELINE.md +1 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +317 -316
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +1 -1
- package/plugins/immune-brain/dist/imm-brainstorm.md +1 -1
- package/plugins/immune-brain/dist/imm-loop.md +15 -10
- package/plugins/immune-brain/dist/imm-planner.md +22 -17
- package/plugins/immune-brain/hooks/hooks.json +0 -10
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +0 -8
- package/plugins/immune-brain/runtime/claude/capability.ts +4 -10
- package/plugins/immune-brain/runtime/claude/interaction.ts +55 -22
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +63 -73
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +233 -55
- package/plugins/immune-brain/runtime/claude/review_host.ts +2 -138
- package/plugins/immune-brain/runtime/kernel/application.ts +0 -1
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +1 -7
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +0 -7
- package/plugins/immune-brain/runtime/kernel/completion.ts +1 -3
- package/plugins/immune-brain/runtime/kernel/reducer.ts +0 -31
- package/plugins/immune-brain/runtime/kernel/types.ts +0 -2
- package/plugins/immune-brain/runtime/kernel/validation.ts +1 -3
- package/plugins/immune-brain/runtime/plugin_version.ts +2 -0
- package/plugins/immune-brain/skills/BASELINE.md +1 -1
- package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +4 -0
- package/plugins/immune-brain/skills/imm-loop/SKILL.md +4 -0
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +6 -0
|
@@ -1,12 +1,22 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { createInterface } from "node:readline";
|
|
2
3
|
import { stdin, stdout } from "node:process";
|
|
3
4
|
import type { Readable, Writable } from "node:stream";
|
|
4
5
|
import { CORE_CONTRACT, HOST_ID, MIN_CLAUDE_CODE_VERSION, probeHost } from "./capability";
|
|
5
|
-
import {
|
|
6
|
+
import { PLUGIN_VERSION } from "../plugin_version";
|
|
7
|
+
import {
|
|
8
|
+
isPrivilegedOperation,
|
|
9
|
+
NativeAuthorityError,
|
|
10
|
+
privilegedAnnotations,
|
|
11
|
+
type NativeConfirmationInput,
|
|
12
|
+
type NativeConfirmationPort,
|
|
13
|
+
} from "./interaction";
|
|
6
14
|
import { ClaudeReviewHost, FileHookEventLog, parseHookStdin } from "./review_host";
|
|
7
15
|
import { ClaudeRuntime, type ToolMeta } from "./kernel_ports";
|
|
8
16
|
import type { AssuranceCoordinatorPorts } from "../assurance/coordinator";
|
|
9
17
|
|
|
18
|
+
export const MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
19
|
+
|
|
10
20
|
export const TOOLS = [
|
|
11
21
|
{ name: "status", description: "Read the Kernel Assurance Projection for an exact task.", privileged: false },
|
|
12
22
|
{ name: "enroll", description: "Enroll a Git-tracked TaskIntent after native confirmation.", privileged: true },
|
|
@@ -15,7 +25,7 @@ export const TOOLS = [
|
|
|
15
25
|
{ name: "request_authorization", description: "Apply exact literal-user authorization.", privileged: true },
|
|
16
26
|
{ name: "approve_breaking_intent_revision", description: "Approve a breaking TaskIntent revision.", privileged: true },
|
|
17
27
|
{ name: "stop", description: "Stop the active task with literal-user authority.", privileged: true },
|
|
18
|
-
{ name: "repair_authority_state", description: "Repair a recoverable stale backend claim.", privileged:
|
|
28
|
+
{ name: "repair_authority_state", description: "Repair a proven recoverable stale backend claim.", privileged: false },
|
|
19
29
|
] as const;
|
|
20
30
|
|
|
21
31
|
export function listMcpTools() {
|
|
@@ -36,13 +46,17 @@ export function listMcpTools() {
|
|
|
36
46
|
}));
|
|
37
47
|
}
|
|
38
48
|
|
|
49
|
+
export function supportsElicitationProtocol(value: unknown): boolean {
|
|
50
|
+
return typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value) && value >= MCP_PROTOCOL_VERSION;
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
export interface McpRuntimeOptions {
|
|
40
54
|
cwd?: string;
|
|
41
55
|
env?: Record<string, string | undefined>;
|
|
42
56
|
ports?: AssuranceCoordinatorPorts;
|
|
43
57
|
host?: ClaudeReviewHost;
|
|
44
58
|
interactive?: boolean;
|
|
45
|
-
|
|
59
|
+
requestConfirmation?: NativeConfirmationPort;
|
|
46
60
|
}
|
|
47
61
|
|
|
48
62
|
export function createMcpRuntime(options: McpRuntimeOptions = {}) {
|
|
@@ -53,21 +67,26 @@ export function createMcpRuntime(options: McpRuntimeOptions = {}) {
|
|
|
53
67
|
host,
|
|
54
68
|
ports: options.ports,
|
|
55
69
|
interactive: options.interactive,
|
|
56
|
-
|
|
70
|
+
requestConfirmation: options.requestConfirmation,
|
|
57
71
|
});
|
|
58
72
|
// Trusted Host evidence negotiated on this JSON-RPC connection. Absent
|
|
59
73
|
// handshake evidence means unversioned and non-interactive: fail closed.
|
|
60
74
|
let negotiatedVersion: string | undefined;
|
|
61
75
|
let negotiatedInteractive = false;
|
|
76
|
+
const connectionId = randomUUID();
|
|
62
77
|
return {
|
|
63
78
|
runtime,
|
|
64
79
|
host,
|
|
80
|
+
connectionId,
|
|
65
81
|
listTools: listMcpTools,
|
|
66
|
-
bindClientHandshake(identity: { version: string; interactive: boolean }) {
|
|
82
|
+
bindClientHandshake(identity: { version: string; interactive: boolean; protocolVersion?: string }) {
|
|
67
83
|
negotiatedVersion = identity.version || undefined;
|
|
68
|
-
negotiatedInteractive = identity.interactive;
|
|
84
|
+
negotiatedInteractive = identity.interactive && supportsElicitationProtocol(identity.protocolVersion);
|
|
69
85
|
runtime.bindHostVersion(negotiatedVersion);
|
|
70
86
|
},
|
|
87
|
+
bindNativeConfirmation(port: NativeConfirmationPort) {
|
|
88
|
+
runtime.bindNativeConfirmation(port);
|
|
89
|
+
},
|
|
71
90
|
sessionInteractive: () => negotiatedInteractive,
|
|
72
91
|
async callTool(name: string, args: Record<string, unknown>, meta: Partial<ToolMeta> = {}) {
|
|
73
92
|
const taskId = String(args.task_id ?? "");
|
|
@@ -75,26 +94,24 @@ export function createMcpRuntime(options: McpRuntimeOptions = {}) {
|
|
|
75
94
|
if ("native_decision" in args) throw new Error("native_decision cannot be supplied in tool arguments");
|
|
76
95
|
// Read-only status must stay usable without trusted Host evidence, so it
|
|
77
96
|
// is dispatched before the capability probe rejects unversioned hosts.
|
|
78
|
-
if (name === "status") return runtime.status(taskId);
|
|
97
|
+
if (name === "status") return { plugin_version: PLUGIN_VERSION, ...(await runtime.status(taskId)) };
|
|
79
98
|
// The environment version fallback is disabled on this connection:
|
|
80
99
|
// only a version bound from the trusted initialize handshake may
|
|
81
100
|
// reach authority-mutating tools.
|
|
82
|
-
if (!negotiatedVersion) throw new
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
101
|
+
if (!negotiatedVersion) throw new NativeAuthorityError("unsupported_host", "Claude Code version is unavailable");
|
|
102
|
+
// Automatic stale-claim repair is deterministic and needs no native
|
|
103
|
+
// interaction; other mutations retain the Host capability requirement.
|
|
104
|
+
if (!negotiatedInteractive && name !== "repair_authority_state") {
|
|
105
|
+
throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
|
|
106
|
+
}
|
|
88
107
|
const probe = probeHost(options.env ?? process.env, process.platform, negotiatedVersion);
|
|
89
|
-
if (!probe.ok) throw new
|
|
108
|
+
if (!probe.ok) throw new NativeAuthorityError("unsupported_host", probe.reason);
|
|
90
109
|
const toolMeta: ToolMeta = {
|
|
91
|
-
sessionId: meta.sessionId ??
|
|
110
|
+
sessionId: meta.sessionId ?? connectionId,
|
|
92
111
|
toolCallId: meta.toolCallId ?? `call-${name}`,
|
|
93
112
|
taskId,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
interactive: meta.interactive ?? options.interactive ?? false,
|
|
97
|
-
decision: meta.decision,
|
|
113
|
+
interactive: meta.interactive ?? options.interactive ?? negotiatedInteractive,
|
|
114
|
+
signal: meta.signal,
|
|
98
115
|
};
|
|
99
116
|
if (name === "enroll") return runtime.enroll(taskId, toolMeta);
|
|
100
117
|
if (name === "advance_assurance") return runtime.advance(taskId, toolMeta.signal);
|
|
@@ -108,7 +125,6 @@ export function createMcpRuntime(options: McpRuntimeOptions = {}) {
|
|
|
108
125
|
throw new Error(`unknown tool ${name}`);
|
|
109
126
|
},
|
|
110
127
|
observe: (event: Parameters<ClaudeReviewHost["observe"]>[0]) => host.observe(event),
|
|
111
|
-
sessionOfElicitation: (toolCallId: string) => host.sessionOfElicitation(toolCallId),
|
|
112
128
|
shutdown: () => runtime.shutdown(),
|
|
113
129
|
aborts: new Map<string | number, AbortController>(),
|
|
114
130
|
};
|
|
@@ -120,25 +136,24 @@ interface JsonRpc {
|
|
|
120
136
|
method?: string;
|
|
121
137
|
params?: unknown;
|
|
122
138
|
result?: unknown;
|
|
123
|
-
error?: { code: number; message: string };
|
|
139
|
+
error?: { code: number; message: string; data?: Record<string, unknown> };
|
|
124
140
|
}
|
|
125
141
|
|
|
126
|
-
function hostCallIdentity(
|
|
127
|
-
meta: Record<string, unknown> | undefined,
|
|
128
|
-
resolveSession?: (toolCallId: string) => string | undefined,
|
|
129
|
-
): { sessionId: string; toolCallId: string } | undefined {
|
|
142
|
+
function hostCallIdentity(meta: Record<string, unknown> | undefined): { toolCallId: string } | undefined {
|
|
130
143
|
if (!meta) return undefined;
|
|
131
|
-
// Real Claude Code wire correlation metadata carries ONLY the namespaced
|
|
132
|
-
// claudecode/toolUseId key; any other correlation key (tool_use_id,
|
|
133
|
-
// toolUseId, toolCallId) is noncanonical and fails closed. The Host session
|
|
134
|
-
// binding is always derived from the sole unconsumed ElicitationResult
|
|
135
|
-
// record for that exact call; no record, or an ambiguous record, means no
|
|
136
|
-
// native interaction: fail closed. A supplied session_id is never trusted
|
|
137
|
-
// and can never bypass ambiguity resolution.
|
|
138
144
|
const toolCallId = meta["claudecode/toolUseId"];
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
return typeof toolCallId === "string" && toolCallId ? { toolCallId } : undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function rpcError(error: unknown): { code: number; message: string; data?: Record<string, unknown> } {
|
|
149
|
+
if (error instanceof NativeAuthorityError) {
|
|
150
|
+
return {
|
|
151
|
+
code: -32000,
|
|
152
|
+
message: error.message,
|
|
153
|
+
data: { reason_code: error.reasonCode, recovery_action: error.recoveryAction },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return { code: -32000, message: error instanceof Error ? error.message : String(error) };
|
|
142
157
|
}
|
|
143
158
|
|
|
144
159
|
function encodeMessage(message: JsonRpc): Buffer {
|
|
@@ -148,6 +163,7 @@ function encodeMessage(message: JsonRpc): Buffer {
|
|
|
148
163
|
export async function handleJsonRpc(message: JsonRpc, mcp = createMcpRuntime()): Promise<JsonRpc | null> {
|
|
149
164
|
if (message.method === "initialize") {
|
|
150
165
|
const params = (message.params ?? {}) as {
|
|
166
|
+
protocolVersion?: unknown;
|
|
151
167
|
clientInfo?: { name?: unknown; version?: unknown };
|
|
152
168
|
capabilities?: Record<string, unknown>;
|
|
153
169
|
};
|
|
@@ -158,17 +174,20 @@ export async function handleJsonRpc(message: JsonRpc, mcp = createMcpRuntime()):
|
|
|
158
174
|
const elicitation = params.capabilities?.elicitation;
|
|
159
175
|
mcp.bindClientHandshake({
|
|
160
176
|
version: trustedClient && typeof params.clientInfo?.version === "string" ? params.clientInfo.version : "",
|
|
161
|
-
|
|
162
|
-
// the native interactions privileged authority requires; only a
|
|
163
|
-
// valid capability object counts, not any non-null value.
|
|
177
|
+
protocolVersion: typeof params.protocolVersion === "string" ? params.protocolVersion : undefined,
|
|
164
178
|
interactive: trustedClient && elicitation !== null && typeof elicitation === "object" && !Array.isArray(elicitation),
|
|
165
179
|
});
|
|
166
180
|
return {
|
|
167
181
|
jsonrpc: "2.0",
|
|
168
182
|
id: message.id ?? null,
|
|
169
183
|
result: {
|
|
170
|
-
protocolVersion:
|
|
171
|
-
serverInfo: {
|
|
184
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
185
|
+
serverInfo: {
|
|
186
|
+
name: HOST_ID,
|
|
187
|
+
version: PLUGIN_VERSION,
|
|
188
|
+
contract: CORE_CONTRACT,
|
|
189
|
+
minimumHostVersion: MIN_CLAUDE_CODE_VERSION,
|
|
190
|
+
},
|
|
172
191
|
capabilities: { tools: {} },
|
|
173
192
|
},
|
|
174
193
|
};
|
|
@@ -191,13 +210,13 @@ export async function handleJsonRpc(message: JsonRpc, mcp = createMcpRuntime()):
|
|
|
191
210
|
const name = String(params.name ?? "");
|
|
192
211
|
const interactive = mcp.sessionInteractive();
|
|
193
212
|
if (isPrivilegedOperation(name) && !interactive) {
|
|
194
|
-
throw new
|
|
213
|
+
throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
|
|
195
214
|
}
|
|
196
|
-
const identity = hostCallIdentity(params._meta
|
|
215
|
+
const identity = hostCallIdentity(params._meta);
|
|
197
216
|
if (isPrivilegedOperation(name) && !identity) {
|
|
198
|
-
throw new
|
|
217
|
+
throw new NativeAuthorityError("correlation_missing", "canonical claudecode/toolUseId metadata is missing");
|
|
199
218
|
}
|
|
200
|
-
const sessionId =
|
|
219
|
+
const sessionId = mcp.connectionId;
|
|
201
220
|
const toolCallId = identity?.toolCallId ?? String(message.id ?? "stdio");
|
|
202
221
|
const result = await mcp.callTool(name, params.arguments ?? {}, {
|
|
203
222
|
sessionId,
|
|
@@ -212,11 +231,10 @@ export async function handleJsonRpc(message: JsonRpc, mcp = createMcpRuntime()):
|
|
|
212
231
|
result: { content: [{ type: "text", text: JSON.stringify(result) }] },
|
|
213
232
|
};
|
|
214
233
|
} catch (error) {
|
|
215
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
216
234
|
return {
|
|
217
235
|
jsonrpc: "2.0",
|
|
218
236
|
id: message.id ?? null,
|
|
219
|
-
error:
|
|
237
|
+
error: rpcError(error),
|
|
220
238
|
};
|
|
221
239
|
} finally {
|
|
222
240
|
mcp.aborts.delete(requestId);
|
|
@@ -228,8 +246,69 @@ export async function handleJsonRpc(message: JsonRpc, mcp = createMcpRuntime()):
|
|
|
228
246
|
|
|
229
247
|
async function writeReply(output: Writable, reply: JsonRpc): Promise<void> {
|
|
230
248
|
const payload = encodeMessage(reply);
|
|
231
|
-
|
|
232
|
-
|
|
249
|
+
await new Promise<void>((resolve, reject) => {
|
|
250
|
+
if (!output.writable) {
|
|
251
|
+
reject(new Error("output stream is not writable"));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
let settled = false;
|
|
255
|
+
const cleanup = () => {
|
|
256
|
+
output.off("drain", onDrain);
|
|
257
|
+
output.off("error", onError);
|
|
258
|
+
output.off("close", onClose);
|
|
259
|
+
};
|
|
260
|
+
const onDrain = () => {
|
|
261
|
+
if (settled) return;
|
|
262
|
+
settled = true;
|
|
263
|
+
cleanup();
|
|
264
|
+
resolve();
|
|
265
|
+
};
|
|
266
|
+
const onError = (error: Error) => {
|
|
267
|
+
if (settled) return;
|
|
268
|
+
settled = true;
|
|
269
|
+
cleanup();
|
|
270
|
+
reject(error);
|
|
271
|
+
};
|
|
272
|
+
const onClose = () => {
|
|
273
|
+
if (settled) return;
|
|
274
|
+
settled = true;
|
|
275
|
+
cleanup();
|
|
276
|
+
reject(new Error("output stream closed before write drained"));
|
|
277
|
+
};
|
|
278
|
+
output.once("error", onError);
|
|
279
|
+
output.once("close", onClose);
|
|
280
|
+
const ok = output.write(payload, (error) => {
|
|
281
|
+
if (settled) return;
|
|
282
|
+
if (error) {
|
|
283
|
+
settled = true;
|
|
284
|
+
cleanup();
|
|
285
|
+
reject(error);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
if (ok) {
|
|
289
|
+
settled = true;
|
|
290
|
+
cleanup();
|
|
291
|
+
resolve();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
output.once("drain", onDrain);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function elicitationParams(input: NativeConfirmationInput) {
|
|
299
|
+
const details = [
|
|
300
|
+
`Operation: ${input.operation}`,
|
|
301
|
+
`Task: ${input.taskId}`,
|
|
302
|
+
input.risk ? `Risk: ${input.risk}` : null,
|
|
303
|
+
input.intentRevision !== undefined ? `Intent revision: ${input.intentRevision}` : null,
|
|
304
|
+
input.intentContentHash ? `Intent hash: ${input.intentContentHash}` : null,
|
|
305
|
+
input.bindingDigest ? `Binding digest: ${input.bindingDigest}` : null,
|
|
306
|
+
].filter(Boolean);
|
|
307
|
+
return {
|
|
308
|
+
mode: "form",
|
|
309
|
+
message: `Authorize this exact Immune-Brain operation?\n\n${details.join("\n")}`,
|
|
310
|
+
requestedSchema: { type: "object", properties: {} },
|
|
311
|
+
};
|
|
233
312
|
}
|
|
234
313
|
|
|
235
314
|
export async function serveStdio(options: {
|
|
@@ -244,17 +323,93 @@ export async function serveStdio(options: {
|
|
|
244
323
|
const exit = options.exit ?? ((code: number) => { process.exit(code); });
|
|
245
324
|
let buffer = Buffer.alloc(0);
|
|
246
325
|
let accepting = true;
|
|
326
|
+
let requestSequence = 0;
|
|
247
327
|
const inFlight = new Set<Promise<void>>();
|
|
328
|
+
const pending = new Map<string, {
|
|
329
|
+
resolve: (response: JsonRpc) => void;
|
|
330
|
+
reject: (error: Error) => void;
|
|
331
|
+
}>();
|
|
248
332
|
let chain = Promise.resolve();
|
|
249
333
|
|
|
334
|
+
const rejectPending = (error: Error): void => {
|
|
335
|
+
for (const item of pending.values()) item.reject(error);
|
|
336
|
+
pending.clear();
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
mcp.bindNativeConfirmation(async (input) => {
|
|
340
|
+
if (!accepting) throw new NativeAuthorityError("interaction_not_opened", "MCP connection is closed");
|
|
341
|
+
if (input.signal?.aborted) throw new NativeAuthorityError("user_cancelled", "Tool call was cancelled");
|
|
342
|
+
const requestId = `immune-brain:elicitation:${mcp.connectionId}:${++requestSequence}`;
|
|
343
|
+
let abortListener: (() => void) | undefined;
|
|
344
|
+
const response = new Promise<JsonRpc>((resolve, reject) => {
|
|
345
|
+
pending.set(requestId, { resolve, reject });
|
|
346
|
+
abortListener = () => {
|
|
347
|
+
pending.delete(requestId);
|
|
348
|
+
reject(new NativeAuthorityError("user_cancelled", "Tool call was cancelled"));
|
|
349
|
+
};
|
|
350
|
+
input.signal?.addEventListener("abort", abortListener, { once: true });
|
|
351
|
+
});
|
|
352
|
+
try {
|
|
353
|
+
await writeReply(output, {
|
|
354
|
+
jsonrpc: "2.0",
|
|
355
|
+
id: requestId,
|
|
356
|
+
method: "elicitation/create",
|
|
357
|
+
params: elicitationParams(input),
|
|
358
|
+
});
|
|
359
|
+
const reply = await response;
|
|
360
|
+
if (reply.error) {
|
|
361
|
+
if (reply.error.code === -32601) {
|
|
362
|
+
throw new NativeAuthorityError("unsupported_host", "Claude Code rejected MCP elicitation/create");
|
|
363
|
+
}
|
|
364
|
+
throw new NativeAuthorityError("correlation_missing", `MCP elicitation failed: ${reply.error.message}`);
|
|
365
|
+
}
|
|
366
|
+
const result = reply.result;
|
|
367
|
+
const action = typeof result === "object" && result !== null && !Array.isArray(result)
|
|
368
|
+
? (result as { action?: unknown }).action
|
|
369
|
+
: undefined;
|
|
370
|
+
if (action !== "accept" && action !== "decline" && action !== "cancel") {
|
|
371
|
+
throw new NativeAuthorityError("correlation_missing", "MCP elicitation returned an invalid action");
|
|
372
|
+
}
|
|
373
|
+
if (action === "accept") {
|
|
374
|
+
const content = (result as { content?: unknown }).content;
|
|
375
|
+
if (typeof content !== "object" || content === null || Array.isArray(content) || Object.keys(content).length !== 0) {
|
|
376
|
+
throw new NativeAuthorityError("correlation_missing", "MCP elicitation accept content did not match the requested schema");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return { decision: action, requestId };
|
|
380
|
+
} finally {
|
|
381
|
+
pending.delete(requestId);
|
|
382
|
+
if (abortListener) input.signal?.removeEventListener("abort", abortListener);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
250
386
|
const runCall = (parsed: JsonRpc): void => {
|
|
251
387
|
const task = handleJsonRpc(parsed, mcp).then(async (reply) => {
|
|
252
388
|
if (reply) await writeReply(output, reply);
|
|
389
|
+
}).catch(() => {
|
|
390
|
+
void executeShutdown(1);
|
|
253
391
|
});
|
|
254
392
|
inFlight.add(task);
|
|
255
393
|
void task.finally(() => inFlight.delete(task));
|
|
256
394
|
};
|
|
257
395
|
|
|
396
|
+
const routeResponse = (obj: Record<string, unknown>): boolean => {
|
|
397
|
+
const id = typeof obj.id === "string" ? obj.id : "";
|
|
398
|
+
const waiter = id ? pending.get(id) : undefined;
|
|
399
|
+
if (waiter) {
|
|
400
|
+
pending.delete(id);
|
|
401
|
+
const hasResult = Object.hasOwn(obj, "result");
|
|
402
|
+
const hasError = Object.hasOwn(obj, "error");
|
|
403
|
+
if (obj.jsonrpc !== "2.0" || obj.method !== undefined || hasResult === hasError) {
|
|
404
|
+
waiter.reject(new NativeAuthorityError("correlation_missing", "malformed MCP elicitation response"));
|
|
405
|
+
} else {
|
|
406
|
+
waiter.resolve(obj as unknown as JsonRpc);
|
|
407
|
+
}
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
return obj.method === undefined && obj.id !== undefined;
|
|
411
|
+
};
|
|
412
|
+
|
|
258
413
|
const drainStdio = async (): Promise<void> => {
|
|
259
414
|
while (true) {
|
|
260
415
|
const newline = buffer.indexOf("\n");
|
|
@@ -274,10 +429,12 @@ export async function serveStdio(options: {
|
|
|
274
429
|
continue;
|
|
275
430
|
}
|
|
276
431
|
const obj = parsed as Record<string, unknown>;
|
|
432
|
+
if (routeResponse(obj)) continue;
|
|
277
433
|
if (obj.jsonrpc !== "2.0") {
|
|
278
434
|
await writeReply(output, { jsonrpc: "2.0", id: null, error: { code: -32600, message: "Invalid Request: jsonrpc must be '2.0'" } });
|
|
279
435
|
continue;
|
|
280
436
|
}
|
|
437
|
+
if (routeResponse(obj)) continue;
|
|
281
438
|
if (typeof obj.method !== "string" || !obj.method) {
|
|
282
439
|
const id = obj.id !== undefined && (typeof obj.id === "string" || typeof obj.id === "number") ? obj.id : null;
|
|
283
440
|
await writeReply(output, { jsonrpc: "2.0", id, error: { code: -32600, message: "Invalid Request: missing method" } });
|
|
@@ -287,7 +444,6 @@ export async function serveStdio(options: {
|
|
|
287
444
|
await writeReply(output, { jsonrpc: "2.0", id: null, error: { code: -32600, message: "Invalid Request: invalid id type" } });
|
|
288
445
|
continue;
|
|
289
446
|
}
|
|
290
|
-
// tools/call is a request requiring a reply; notification-form tools/call (no id) is an invalid request
|
|
291
447
|
if (obj.method === "tools/call" && obj.id === undefined) {
|
|
292
448
|
await writeReply(output, { jsonrpc: "2.0", id: null, error: { code: -32600, message: "Invalid Request: tools/call requires an id" } });
|
|
293
449
|
continue;
|
|
@@ -295,10 +451,15 @@ export async function serveStdio(options: {
|
|
|
295
451
|
const rpc = parsed as JsonRpc;
|
|
296
452
|
if (rpc.method === "notifications/cancelled") {
|
|
297
453
|
const requestId = (rpc.params as { requestId?: string | number } | undefined)?.requestId;
|
|
298
|
-
if (requestId
|
|
454
|
+
if (typeof requestId === "string" && pending.has(requestId)) {
|
|
455
|
+
pending.get(requestId)?.reject(new NativeAuthorityError("user_cancelled", "native interaction was cancelled"));
|
|
456
|
+
pending.delete(requestId);
|
|
457
|
+
} else if (requestId !== undefined) {
|
|
458
|
+
mcp.aborts.get(requestId)?.abort(new Error("notifications/cancelled"));
|
|
459
|
+
}
|
|
299
460
|
continue;
|
|
300
461
|
}
|
|
301
|
-
if (
|
|
462
|
+
if (rpc.method === "tools/call") {
|
|
302
463
|
if (!accepting) {
|
|
303
464
|
if (rpc.id !== undefined) await writeReply(output, { jsonrpc: "2.0", id: rpc.id, error: { code: -32000, message: "stdio closed" } });
|
|
304
465
|
continue;
|
|
@@ -311,38 +472,55 @@ export async function serveStdio(options: {
|
|
|
311
472
|
}
|
|
312
473
|
};
|
|
313
474
|
|
|
475
|
+
let resolveStdio = () => {};
|
|
314
476
|
let shutdownPromise: Promise<void> | null = null;
|
|
315
477
|
const executeShutdown = (code = 0): Promise<void> => {
|
|
316
478
|
if (shutdownPromise) return shutdownPromise;
|
|
317
479
|
shutdownPromise = (async () => {
|
|
318
480
|
accepting = false;
|
|
319
481
|
for (const ac of mcp.aborts.values()) ac.abort(new Error("stdio closed"));
|
|
320
|
-
|
|
482
|
+
rejectPending(new NativeAuthorityError("user_cancelled", "MCP connection closed during native interaction"));
|
|
483
|
+
await Promise.race([
|
|
484
|
+
Promise.allSettled([...inFlight]),
|
|
485
|
+
new Promise<void>((resolve) => setTimeout(resolve, 200)),
|
|
486
|
+
]);
|
|
321
487
|
await mcp.shutdown();
|
|
322
488
|
if (output.writable && typeof output.end === "function") {
|
|
323
489
|
await new Promise<void>((cb) => output.end(() => cb()));
|
|
324
490
|
}
|
|
325
491
|
process.exitCode = code;
|
|
326
492
|
exit(code);
|
|
493
|
+
resolveStdio();
|
|
327
494
|
})();
|
|
328
495
|
return shutdownPromise;
|
|
329
496
|
};
|
|
330
497
|
|
|
331
498
|
await new Promise<void>((resolve) => {
|
|
499
|
+
resolveStdio = resolve;
|
|
500
|
+
output.on("error", () => {
|
|
501
|
+
void executeShutdown(1);
|
|
502
|
+
});
|
|
503
|
+
output.on("close", () => {
|
|
504
|
+
void executeShutdown(0);
|
|
505
|
+
});
|
|
332
506
|
input.on("data", (chunk: Buffer | string) => {
|
|
507
|
+
if (!accepting) return;
|
|
333
508
|
chain = chain.then(async () => {
|
|
509
|
+
if (!accepting) return;
|
|
334
510
|
buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
|
|
335
511
|
await drainStdio();
|
|
512
|
+
}).catch(() => {
|
|
513
|
+
void executeShutdown(1);
|
|
336
514
|
});
|
|
337
515
|
});
|
|
338
516
|
input.on("end", () => {
|
|
339
|
-
void chain.then(() => executeShutdown(0))
|
|
517
|
+
void chain.then(() => executeShutdown(0));
|
|
340
518
|
});
|
|
341
519
|
input.on("close", () => {
|
|
342
|
-
void chain.then(() => executeShutdown(0))
|
|
520
|
+
void chain.then(() => executeShutdown(0));
|
|
343
521
|
});
|
|
344
522
|
input.on("error", () => {
|
|
345
|
-
void
|
|
523
|
+
void executeShutdown(1);
|
|
346
524
|
});
|
|
347
525
|
});
|
|
348
526
|
}
|