immune-brain 3.3.0 → 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 +1 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/dist/BASELINE.md +1 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +300 -241
- package/plugins/immune-brain/dist/imm-brainstorm.md +1 -1
- package/plugins/immune-brain/dist/imm-loop.md +8 -7
- package/plugins/immune-brain/dist/imm-planner.md +22 -17
- package/plugins/immune-brain/hooks/hooks.json +0 -10
- package/plugins/immune-brain/runtime/claude/capability.ts +4 -10
- package/plugins/immune-brain/runtime/claude/interaction.ts +55 -21
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +59 -50
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +222 -48
- package/plugins/immune-brain/runtime/claude/review_host.ts +2 -138
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- 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
|
@@ -2,11 +2,12 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
3
|
|
|
4
4
|
// plugins/immune-brain/runtime/claude/mcp_server.ts
|
|
5
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
5
6
|
import { createInterface } from "node:readline";
|
|
6
7
|
import { stdin, stdout } from "node:process";
|
|
7
8
|
|
|
8
9
|
// plugins/immune-brain/runtime/claude/capability.ts
|
|
9
|
-
var MIN_CLAUDE_CODE_VERSION = "2.1.
|
|
10
|
+
var MIN_CLAUDE_CODE_VERSION = "2.1.236";
|
|
10
11
|
var HOST_ID = "claude-code";
|
|
11
12
|
var CORE_CONTRACT = "assurance_kernel/host_adapter/claude-code/v1";
|
|
12
13
|
function parseSemver(value) {
|
|
@@ -22,11 +23,6 @@ function compareSemver(left, right) {
|
|
|
22
23
|
throw new Error(`invalid semver: ${!a ? left : right}`);
|
|
23
24
|
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
|
|
24
25
|
}
|
|
25
|
-
function parsePermissionMode(raw) {
|
|
26
|
-
if (raw === "manual" || raw === "acceptEdits" || raw === "auto" || raw === "bypassPermissions" || raw === "dontAsk")
|
|
27
|
-
return raw;
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
26
|
function probeHost(env = process.env, platform = process.platform, hostVersion) {
|
|
31
27
|
const version = hostVersion ?? env.CLAUDE_CODE_VERSION ?? env.CLAUDE_CLI_VERSION;
|
|
32
28
|
if (!version)
|
|
@@ -42,18 +38,11 @@ function probeHost(env = process.env, platform = process.platform, hostVersion)
|
|
|
42
38
|
if (platform !== "darwin" && platform !== "linux") {
|
|
43
39
|
return { ok: false, reason: `unsupported platform ${platform}; native Windows is out of scope` };
|
|
44
40
|
}
|
|
45
|
-
|
|
46
|
-
if (rawPermissionMode !== undefined && rawPermissionMode !== "") {
|
|
47
|
-
const permissionMode = parsePermissionMode(rawPermissionMode);
|
|
48
|
-
if (!permissionMode)
|
|
49
|
-
return { ok: false, reason: `unsupported permission mode ${rawPermissionMode}` };
|
|
50
|
-
return { ok: true, version, permissionMode, platform };
|
|
51
|
-
}
|
|
52
|
-
return { ok: true, version, permissionMode: "manual", platform };
|
|
41
|
+
return { ok: true, version, platform };
|
|
53
42
|
}
|
|
54
43
|
|
|
55
44
|
// plugins/immune-brain/runtime/plugin_version.ts
|
|
56
|
-
var PLUGIN_VERSION = "3.
|
|
45
|
+
var PLUGIN_VERSION = "3.4.0";
|
|
57
46
|
|
|
58
47
|
// plugins/immune-brain/runtime/claude/interaction.ts
|
|
59
48
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -63,38 +52,46 @@ var PRIVILEGED_OPERATIONS = [
|
|
|
63
52
|
"approve_breaking_intent_revision",
|
|
64
53
|
"stop"
|
|
65
54
|
];
|
|
55
|
+
var RECOVERY_ACTIONS = {
|
|
56
|
+
interaction_not_opened: "retry through a fresh native gate in the current Host",
|
|
57
|
+
user_denied: "wait for a fresh literal-user request",
|
|
58
|
+
user_cancelled: "wait for a fresh literal-user request",
|
|
59
|
+
correlation_missing: "retry through a fresh native gate in the current Host",
|
|
60
|
+
unsupported_host: "upgrade to a supported Claude Code version and retry in the current Host",
|
|
61
|
+
workspace_changed: "review the current workspace and retry through a fresh native gate"
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
class NativeAuthorityError extends Error {
|
|
65
|
+
reasonCode;
|
|
66
|
+
recoveryAction;
|
|
67
|
+
constructor(reasonCode, detail, recoveryAction = RECOVERY_ACTIONS[reasonCode]) {
|
|
68
|
+
super(`${reasonCode}: ${detail}; recovery: ${recoveryAction}`);
|
|
69
|
+
this.reasonCode = reasonCode;
|
|
70
|
+
this.recoveryAction = recoveryAction;
|
|
71
|
+
this.name = "NativeAuthorityError";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
66
74
|
function isPrivilegedOperation(operation) {
|
|
67
75
|
return PRIVILEGED_OPERATIONS.includes(operation);
|
|
68
76
|
}
|
|
69
77
|
function privilegedAnnotations() {
|
|
70
|
-
return {
|
|
71
|
-
destructiveHint: true,
|
|
72
|
-
"anthropic/requiresUserInteraction": true
|
|
73
|
-
};
|
|
78
|
+
return { destructiveHint: true };
|
|
74
79
|
}
|
|
75
80
|
function evaluateNativeGate(input) {
|
|
76
81
|
if (!isPrivilegedOperation(input.operation))
|
|
77
82
|
return { ok: true };
|
|
78
83
|
if (!input.interactive)
|
|
79
|
-
return { ok: false,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return { ok: false, reason: `unsupported permission mode ${String(input.permissionMode)}` };
|
|
83
|
-
if (mode === "dontAsk")
|
|
84
|
-
return { ok: false, reason: "dontAsk cannot mint authority" };
|
|
85
|
-
if (!input.requiresUserInteraction) {
|
|
86
|
-
return { ok: false, reason: "privileged operation requires anthropic/requiresUserInteraction" };
|
|
87
|
-
}
|
|
88
|
-
if (input.decision === "deny")
|
|
89
|
-
return { ok: false, reason: "native interaction denied" };
|
|
84
|
+
return { ok: false, error: new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable") };
|
|
85
|
+
if (input.decision === "decline")
|
|
86
|
+
return { ok: false, error: new NativeAuthorityError("user_denied", "native interaction declined") };
|
|
90
87
|
if (input.decision === "cancel")
|
|
91
|
-
return { ok: false,
|
|
88
|
+
return { ok: false, error: new NativeAuthorityError("user_cancelled", "native interaction cancelled") };
|
|
92
89
|
if (input.decision !== "accept")
|
|
93
|
-
return { ok: false,
|
|
90
|
+
return { ok: false, error: new NativeAuthorityError("interaction_not_opened", "native interaction returned no decision") };
|
|
94
91
|
return { ok: true };
|
|
95
92
|
}
|
|
96
93
|
function confirmationRef(input) {
|
|
97
|
-
return `claude-confirm-${createHash("sha256").update(`${input.
|
|
94
|
+
return `claude-confirm-${createHash("sha256").update(`${input.connectionId}\x00${input.toolCallId}\x00${input.requestId}\x00${input.operation}\x00${input.taskId}\x00${input.intentRevision ?? ""}\x00${input.intentContentHash ?? ""}\x00${input.bindingDigest ?? ""}`).digest("hex").slice(0, 16)}`;
|
|
98
95
|
}
|
|
99
96
|
function enrollmentNonce() {
|
|
100
97
|
return randomUUID();
|
|
@@ -111,7 +108,6 @@ var AGENT_TOOL = "Agent";
|
|
|
111
108
|
|
|
112
109
|
class MemoryHookEventLog {
|
|
113
110
|
events = [];
|
|
114
|
-
consumed = [];
|
|
115
111
|
append(event) {
|
|
116
112
|
this.events.push(event);
|
|
117
113
|
return true;
|
|
@@ -132,18 +128,8 @@ class MemoryHookEventLog {
|
|
|
132
128
|
this.events.splice(i, 1);
|
|
133
129
|
}
|
|
134
130
|
}
|
|
135
|
-
consumeElicitation(sessionId, toolCallId) {
|
|
136
|
-
this.consumed.push(`${sessionId}\x00${toolCallId}`);
|
|
137
|
-
this.events.push({ type: "ElicitationConsumed", sessionId, toolCallId });
|
|
138
|
-
return true;
|
|
139
|
-
}
|
|
140
|
-
consumedKeys() {
|
|
141
|
-
return [...this.consumed];
|
|
142
|
-
}
|
|
143
131
|
}
|
|
144
132
|
var CACHE_DIR = "immune-brain-claude";
|
|
145
|
-
var CONSUMED_FILE = "consumed.jsonl";
|
|
146
|
-
var CONSUMED_DIR = "consumed";
|
|
147
133
|
function sessionHash(sessionId) {
|
|
148
134
|
return createHash2("sha256").update(sessionId).digest("hex");
|
|
149
135
|
}
|
|
@@ -176,27 +162,6 @@ function ensurePrivateDir(dir) {
|
|
|
176
162
|
return false;
|
|
177
163
|
}
|
|
178
164
|
}
|
|
179
|
-
function claimAtomicKey(dir, keyName) {
|
|
180
|
-
const claimsDir = join(dir, CONSUMED_DIR);
|
|
181
|
-
if (!ensurePrivateDir(claimsDir))
|
|
182
|
-
return false;
|
|
183
|
-
const claimPath = join(claimsDir, `${keyName}.claim`);
|
|
184
|
-
const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW | constants.O_NONBLOCK;
|
|
185
|
-
let fd;
|
|
186
|
-
try {
|
|
187
|
-
fd = openSync(claimPath, flags, 384);
|
|
188
|
-
} catch (error) {
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
191
|
-
try {
|
|
192
|
-
const stat = fstatSync(fd);
|
|
193
|
-
if (!stat.isFile() || !ownedByUs(stat) || (stat.mode & 511) !== 384)
|
|
194
|
-
return false;
|
|
195
|
-
return true;
|
|
196
|
-
} finally {
|
|
197
|
-
closeSync(fd);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
165
|
function appendPrivate(path, dir, line) {
|
|
201
166
|
if (!ensurePrivateDir(dir))
|
|
202
167
|
return false;
|
|
@@ -257,7 +222,7 @@ class FileHookEventLog {
|
|
|
257
222
|
if (!ensurePrivateDir(dir))
|
|
258
223
|
return [];
|
|
259
224
|
try {
|
|
260
|
-
return readdirSync(dir).filter((name) => name.endsWith(".jsonl")
|
|
225
|
+
return readdirSync(dir).filter((name) => name.endsWith(".jsonl")).flatMap((name) => this.readFile(join(dir, name)));
|
|
261
226
|
} catch {
|
|
262
227
|
return [];
|
|
263
228
|
}
|
|
@@ -296,36 +261,6 @@ class FileHookEventLog {
|
|
|
296
261
|
rmSync(path, { force: true });
|
|
297
262
|
} catch {}
|
|
298
263
|
}
|
|
299
|
-
consumeElicitation(sessionId, toolCallId) {
|
|
300
|
-
const dir = cacheDir(this.root);
|
|
301
|
-
const claimKey = createHash2("sha256").update(`${sessionId}\x00${toolCallId}`).digest("hex");
|
|
302
|
-
const keyClaimed = claimAtomicKey(dir, claimKey);
|
|
303
|
-
if (!keyClaimed)
|
|
304
|
-
return false;
|
|
305
|
-
if (!appendPrivate(join(dir, CONSUMED_FILE), dir, `${JSON.stringify({ sessionId, toolCallId })}
|
|
306
|
-
`)) {
|
|
307
|
-
try {
|
|
308
|
-
rmSync(join(dir, CONSUMED_DIR, `${claimKey}.claim`), { force: true });
|
|
309
|
-
} catch {}
|
|
310
|
-
return false;
|
|
311
|
-
}
|
|
312
|
-
if (!this.consumedKeys().includes(`${sessionId}\x00${toolCallId}`))
|
|
313
|
-
return false;
|
|
314
|
-
return this.append({ type: "ElicitationConsumed", sessionId, toolCallId });
|
|
315
|
-
}
|
|
316
|
-
consumedKeys() {
|
|
317
|
-
if (!ensurePrivateDir(cacheDir(this.root)))
|
|
318
|
-
return [];
|
|
319
|
-
const text = readPrivate(join(cacheDir(this.root), CONSUMED_FILE));
|
|
320
|
-
if (!text)
|
|
321
|
-
return [];
|
|
322
|
-
try {
|
|
323
|
-
return text.split(`
|
|
324
|
-
`).filter(Boolean).map((line) => JSON.parse(line)).map((item) => `${item.sessionId}\x00${item.toolCallId}`);
|
|
325
|
-
} catch {
|
|
326
|
-
return [];
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
264
|
}
|
|
330
265
|
function bindsStart(event, pending) {
|
|
331
266
|
if (event.taskId && event.taskId !== pending.request.taskId)
|
|
@@ -349,11 +284,9 @@ class ClaudeReviewHost {
|
|
|
349
284
|
host = "claude-code";
|
|
350
285
|
pending = new Map;
|
|
351
286
|
appliedBySession = new Map;
|
|
352
|
-
consumedElicitations = new Set;
|
|
353
287
|
constructor(log = new MemoryHookEventLog) {
|
|
354
288
|
this.log = log;
|
|
355
289
|
}
|
|
356
|
-
confirmations = new Map;
|
|
357
290
|
prepareReview(request) {
|
|
358
291
|
const initialCursors = new Map;
|
|
359
292
|
const sessionCursors = new Map;
|
|
@@ -383,8 +316,6 @@ ${request.prompt}`,
|
|
|
383
316
|
this.log.append(event);
|
|
384
317
|
}
|
|
385
318
|
drain() {
|
|
386
|
-
for (const key of this.log.consumedKeys())
|
|
387
|
-
this.consumedElicitations.add(key);
|
|
388
319
|
for (const sessionId of this.log.sessions()) {
|
|
389
320
|
const events = this.log.list(sessionId);
|
|
390
321
|
let start = this.appliedBySession.get(sessionId) ?? 0;
|
|
@@ -393,34 +324,10 @@ ${request.prompt}`,
|
|
|
393
324
|
let ended = false;
|
|
394
325
|
for (let i = start;i < events.length; i++) {
|
|
395
326
|
const event = events[i];
|
|
396
|
-
if (event.type === "ElicitationResult") {
|
|
397
|
-
const key = `${event.sessionId}\x00${event.toolCallId}`;
|
|
398
|
-
if (this.consumedElicitations.has(key))
|
|
399
|
-
continue;
|
|
400
|
-
if (this.confirmations.has(key)) {
|
|
401
|
-
if (this.log.consumeElicitation(event.sessionId, event.toolCallId)) {
|
|
402
|
-
this.confirmations.delete(key);
|
|
403
|
-
this.consumedElicitations.add(key);
|
|
404
|
-
}
|
|
405
|
-
continue;
|
|
406
|
-
}
|
|
407
|
-
this.confirmations.set(key, event.decision);
|
|
408
|
-
continue;
|
|
409
|
-
}
|
|
410
|
-
if (event.type === "ElicitationConsumed") {
|
|
411
|
-
const key = `${event.sessionId}\x00${event.toolCallId}`;
|
|
412
|
-
this.consumedElicitations.add(key);
|
|
413
|
-
this.confirmations.delete(key);
|
|
414
|
-
continue;
|
|
415
|
-
}
|
|
416
327
|
if (event.type === "SessionEnd") {
|
|
417
328
|
this.log.clear(event.sessionId);
|
|
418
329
|
ended = true;
|
|
419
330
|
this.appliedBySession.delete(event.sessionId);
|
|
420
|
-
for (const key of this.confirmations.keys()) {
|
|
421
|
-
if (key.startsWith(`${event.sessionId}\x00`))
|
|
422
|
-
this.confirmations.delete(key);
|
|
423
|
-
}
|
|
424
331
|
for (const [id, state] of this.pending) {
|
|
425
332
|
if (state.startEvent?.sessionId === event.sessionId || state.postEvent?.sessionId === event.sessionId || state.stopEvent?.sessionId === event.sessionId) {
|
|
426
333
|
this.pending.delete(id);
|
|
@@ -457,37 +364,6 @@ ${request.prompt}`,
|
|
|
457
364
|
}
|
|
458
365
|
}
|
|
459
366
|
}
|
|
460
|
-
peekConfirmation(sessionId, toolCallId) {
|
|
461
|
-
this.drain();
|
|
462
|
-
return this.confirmations.has(`${sessionId}\x00${toolCallId}`);
|
|
463
|
-
}
|
|
464
|
-
takeConfirmation(sessionId, toolCallId) {
|
|
465
|
-
this.drain();
|
|
466
|
-
const key = `${sessionId}\x00${toolCallId}`;
|
|
467
|
-
const decision = this.confirmations.get(key);
|
|
468
|
-
if (!decision)
|
|
469
|
-
return;
|
|
470
|
-
if (!this.log.consumeElicitation(sessionId, toolCallId)) {
|
|
471
|
-
this.confirmations.delete(key);
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
this.confirmations.delete(key);
|
|
475
|
-
this.consumedElicitations.add(key);
|
|
476
|
-
this.drain();
|
|
477
|
-
return decision;
|
|
478
|
-
}
|
|
479
|
-
sessionOfElicitation(toolCallId) {
|
|
480
|
-
this.drain();
|
|
481
|
-
const sessions = [];
|
|
482
|
-
for (const key of this.confirmations.keys()) {
|
|
483
|
-
const sep = key.lastIndexOf("\x00");
|
|
484
|
-
if (sep >= 0 && key.slice(sep + 1) === toolCallId)
|
|
485
|
-
sessions.push(key.slice(0, sep));
|
|
486
|
-
}
|
|
487
|
-
if (sessions.length !== 1)
|
|
488
|
-
return;
|
|
489
|
-
return sessions[0];
|
|
490
|
-
}
|
|
491
367
|
applyReviewEvent(event, state) {
|
|
492
368
|
if (state.error)
|
|
493
369
|
return;
|
|
@@ -620,8 +496,6 @@ function parseHookStdin(raw) {
|
|
|
620
496
|
return null;
|
|
621
497
|
}
|
|
622
498
|
const hookType = String(payload.hook_event_name ?? payload.type ?? "");
|
|
623
|
-
if (hookType === "ElicitationResult" && (typeof payload.session_id !== "string" || !payload.session_id))
|
|
624
|
-
return null;
|
|
625
499
|
const sessionId = String(payload.session_id ?? payload.sessionId ?? process.env.CLAUDE_SESSION_ID ?? "");
|
|
626
500
|
if (!sessionId)
|
|
627
501
|
return null;
|
|
@@ -713,17 +587,6 @@ function parseHookStdin(raw) {
|
|
|
713
587
|
}
|
|
714
588
|
if (hookType === "SessionEnd")
|
|
715
589
|
return { type: "SessionEnd", sessionId };
|
|
716
|
-
if (hookType === "ElicitationResult") {
|
|
717
|
-
const rawToolCallId = payload.tool_use_id ?? payload.toolCallId;
|
|
718
|
-
if (typeof rawToolCallId !== "string" || !rawToolCallId)
|
|
719
|
-
return null;
|
|
720
|
-
const toolCallId = rawToolCallId;
|
|
721
|
-
const raw2 = payload.decision ?? payload.result ?? payload.action;
|
|
722
|
-
const decision = raw2 === "accept" ? "accept" : raw2 === "deny" ? "deny" : raw2 === "cancel" ? "cancel" : null;
|
|
723
|
-
if (!toolCallId || !decision)
|
|
724
|
-
return null;
|
|
725
|
-
return { type: "ElicitationResult", sessionId, toolCallId, decision };
|
|
726
|
-
}
|
|
727
590
|
return null;
|
|
728
591
|
}
|
|
729
592
|
|
|
@@ -6717,6 +6580,10 @@ async function mintCapability(registry, input) {
|
|
|
6717
6580
|
};
|
|
6718
6581
|
return registry.issue(binding);
|
|
6719
6582
|
}
|
|
6583
|
+
function throwIfCancelled(signal) {
|
|
6584
|
+
if (signal?.aborted)
|
|
6585
|
+
throw new NativeAuthorityError("user_cancelled", "Tool call was cancelled");
|
|
6586
|
+
}
|
|
6720
6587
|
|
|
6721
6588
|
class ClaudeRuntime {
|
|
6722
6589
|
host;
|
|
@@ -6724,8 +6591,7 @@ class ClaudeRuntime {
|
|
|
6724
6591
|
cwd;
|
|
6725
6592
|
env;
|
|
6726
6593
|
interactive;
|
|
6727
|
-
|
|
6728
|
-
decisions;
|
|
6594
|
+
requestConfirmation;
|
|
6729
6595
|
hostVersion;
|
|
6730
6596
|
mutationRegistry = null;
|
|
6731
6597
|
enrollmentRegistry = createEnrollmentAuthorityRegistry();
|
|
@@ -6734,8 +6600,7 @@ class ClaudeRuntime {
|
|
|
6734
6600
|
this.cwd = options.cwd;
|
|
6735
6601
|
this.env = options.env ?? process.env;
|
|
6736
6602
|
this.interactive = options.interactive ?? true;
|
|
6737
|
-
this.
|
|
6738
|
-
this.decisions = options.decisions ?? new Map;
|
|
6603
|
+
this.requestConfirmation = options.requestConfirmation;
|
|
6739
6604
|
this.host = options.host ?? new ClaudeReviewHost(new FileHookEventLog);
|
|
6740
6605
|
if (options.ports) {
|
|
6741
6606
|
this.coordinator = new AssuranceCoordinator({ ...options.ports, host: this.host });
|
|
@@ -6749,6 +6614,9 @@ class ClaudeRuntime {
|
|
|
6749
6614
|
bindHostVersion(version) {
|
|
6750
6615
|
this.hostVersion = version || undefined;
|
|
6751
6616
|
}
|
|
6617
|
+
bindNativeConfirmation(port) {
|
|
6618
|
+
this.requestConfirmation = port;
|
|
6619
|
+
}
|
|
6752
6620
|
async shutdown() {
|
|
6753
6621
|
await this.coordinator.onSessionShutdown();
|
|
6754
6622
|
}
|
|
@@ -6784,32 +6652,28 @@ class ClaudeRuntime {
|
|
|
6784
6652
|
this.app ??= createCanaryApplication(this.mutationRegistry);
|
|
6785
6653
|
return { registry: this.mutationRegistry, app: this.app };
|
|
6786
6654
|
}
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
if (configured === "deny" || configured === "cancel")
|
|
6790
|
-
this.gate(operation, meta);
|
|
6791
|
-
}
|
|
6792
|
-
gate(operation, meta, binding = {}) {
|
|
6655
|
+
async gate(operation, meta, binding = {}) {
|
|
6656
|
+
throwIfCancelled(meta.signal);
|
|
6793
6657
|
const probe = probeHost(this.env, process.platform, this.hostVersion);
|
|
6794
6658
|
if (!probe.ok)
|
|
6795
|
-
throw new
|
|
6796
|
-
const
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
});
|
|
6659
|
+
throw new NativeAuthorityError("unsupported_host", probe.reason);
|
|
6660
|
+
const interactive = meta.interactive ?? this.interactive;
|
|
6661
|
+
if (!interactive)
|
|
6662
|
+
throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
|
|
6663
|
+
if (!isPrivilegedOperation(operation))
|
|
6664
|
+
throw new Error(`unsupported native operation ${operation}`);
|
|
6665
|
+
if (!this.requestConfirmation)
|
|
6666
|
+
throw new NativeAuthorityError("interaction_not_opened", "native confirmation port is unavailable");
|
|
6667
|
+
const result = await this.requestConfirmation({ operation, taskId: meta.taskId, toolCallId: meta.toolCallId, signal: meta.signal, ...binding });
|
|
6668
|
+
throwIfCancelled(meta.signal);
|
|
6669
|
+
const gate = evaluateNativeGate({ operation, interactive, decision: result.decision });
|
|
6807
6670
|
if (!gate.ok)
|
|
6808
|
-
throw
|
|
6671
|
+
throw gate.error;
|
|
6809
6672
|
return {
|
|
6810
6673
|
confirmation_ref: confirmationRef({
|
|
6811
|
-
|
|
6674
|
+
connectionId: meta.sessionId,
|
|
6812
6675
|
toolCallId: meta.toolCallId,
|
|
6676
|
+
requestId: result.requestId,
|
|
6813
6677
|
operation,
|
|
6814
6678
|
taskId: meta.taskId,
|
|
6815
6679
|
...binding
|
|
@@ -6820,19 +6684,21 @@ class ClaudeRuntime {
|
|
|
6820
6684
|
return projectAssurance(this.cwd, taskId, diffSnapshotOf);
|
|
6821
6685
|
}
|
|
6822
6686
|
async enroll(taskId, meta) {
|
|
6823
|
-
this.rejectBeforePreparation("enroll", { ...meta, taskId });
|
|
6824
6687
|
const now = new Date().toISOString();
|
|
6825
6688
|
const preparation = await preparePiCanary(this.cwd, { task_id: taskId, now });
|
|
6826
|
-
const
|
|
6689
|
+
const intent = await readTaskIntent(this.cwd, taskId);
|
|
6690
|
+
const gate = await this.gate("enroll", { ...meta, taskId }, {
|
|
6691
|
+
risk: intent.intent.risk,
|
|
6827
6692
|
intentRevision: preparation.intent?.revision,
|
|
6828
6693
|
intentContentHash: preparation.intent?.content_hash,
|
|
6829
6694
|
bindingDigest: preparation.digest
|
|
6830
6695
|
});
|
|
6831
6696
|
const { unchanged } = await revalidatePiCanary(this.cwd, { task_id: taskId, now }, preparation);
|
|
6832
6697
|
if (!unchanged)
|
|
6833
|
-
throw new
|
|
6698
|
+
throw new NativeAuthorityError("workspace_changed", "workspace changed after native confirmation");
|
|
6834
6699
|
if (!preparation.intent)
|
|
6835
6700
|
throw new Error("enrollment requires a readable TaskIntent");
|
|
6701
|
+
throwIfCancelled(meta.signal);
|
|
6836
6702
|
const nonce = enrollmentNonce();
|
|
6837
6703
|
const binding = {
|
|
6838
6704
|
task_id: taskId,
|
|
@@ -6877,7 +6743,6 @@ class ClaudeRuntime {
|
|
|
6877
6743
|
}
|
|
6878
6744
|
if (!isPrivilegedOperation(operation) && operation !== "request_authorization")
|
|
6879
6745
|
throw new Error(`unsupported privileged operation ${operation}`);
|
|
6880
|
-
this.rejectBeforePreparation(operation, { ...meta, taskId });
|
|
6881
6746
|
let op = operation;
|
|
6882
6747
|
let decisionOp;
|
|
6883
6748
|
const projection = await this.status(taskId);
|
|
@@ -6934,16 +6799,22 @@ class ClaudeRuntime {
|
|
|
6934
6799
|
`);
|
|
6935
6800
|
execFileSync4("git", ["add", "--", priorIntent.intent_ref.path], { cwd: this.cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
6936
6801
|
const preparedRecord = await readTaskRecord(this.cwd, taskId);
|
|
6937
|
-
if (!preparedRecord.record)
|
|
6938
|
-
throw new
|
|
6802
|
+
if (!preparedRecord.record) {
|
|
6803
|
+
throw new NativeAuthorityError("workspace_changed", "TaskRecord changed before the breaking revision digest");
|
|
6804
|
+
}
|
|
6939
6805
|
preparedDiffHash = diffHashOf(this.cwd, preparedRecord.record);
|
|
6940
6806
|
}
|
|
6941
6807
|
const preparedProjection = await this.status(taskId);
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6808
|
+
try {
|
|
6809
|
+
assertProjectionBinding(projection, preparedProjection, Boolean(nextIntent));
|
|
6810
|
+
if (preparedProjection.projection.diff_hash !== preparedDiffHash) {
|
|
6811
|
+
throw new Error("workspace changed while preparing the authority digest");
|
|
6812
|
+
}
|
|
6813
|
+
} catch (error) {
|
|
6814
|
+
throw new NativeAuthorityError("workspace_changed", error instanceof Error ? error.message : String(error));
|
|
6945
6815
|
}
|
|
6946
|
-
gate = this.gate(operation, { ...meta, taskId }, {
|
|
6816
|
+
gate = await this.gate(operation, { ...meta, taskId }, {
|
|
6817
|
+
risk: projection.projection.risk,
|
|
6947
6818
|
intentRevision: nextIntent?.revision ?? projection.projection.intent_revision,
|
|
6948
6819
|
intentContentHash: nextIntentHash ?? projection.projection.intent_content_hash,
|
|
6949
6820
|
bindingDigest: `${preparedDiffHash}:${nextIntentHash ?? ""}`
|
|
@@ -6961,11 +6832,16 @@ class ClaudeRuntime {
|
|
|
6961
6832
|
const confirmation = gate.confirmation_ref;
|
|
6962
6833
|
try {
|
|
6963
6834
|
const capabilityProjection = await this.status(taskId);
|
|
6964
|
-
|
|
6835
|
+
try {
|
|
6836
|
+
assertProjectionBinding(projection, capabilityProjection, Boolean(nextIntent));
|
|
6837
|
+
} catch (error) {
|
|
6838
|
+
throw new NativeAuthorityError("workspace_changed", error instanceof Error ? error.message : String(error));
|
|
6839
|
+
}
|
|
6965
6840
|
const operationDiffHash = capabilityProjection.projection.diff_hash;
|
|
6966
6841
|
if (nextIntent && operationDiffHash !== preparedDiffHash) {
|
|
6967
|
-
throw new
|
|
6842
|
+
throw new NativeAuthorityError("workspace_changed", "workspace changed after native confirmation");
|
|
6968
6843
|
}
|
|
6844
|
+
throwIfCancelled(meta.signal);
|
|
6969
6845
|
const capability = await mintCapability(registry, {
|
|
6970
6846
|
authority_kind: "user",
|
|
6971
6847
|
task_id: taskId,
|
|
@@ -6981,6 +6857,7 @@ class ClaudeRuntime {
|
|
|
6981
6857
|
...op === "resolve_user_decision" && decisionOp ? decisionOp : {},
|
|
6982
6858
|
...op === "stop" ? { reason: extra.reason ?? "user stop" } : {}
|
|
6983
6859
|
});
|
|
6860
|
+
throwIfCancelled(meta.signal);
|
|
6984
6861
|
const result = app.execute({
|
|
6985
6862
|
root: this.cwd,
|
|
6986
6863
|
task_id: taskId,
|
|
@@ -7123,6 +7000,7 @@ class ClaudeRuntime {
|
|
|
7123
7000
|
}
|
|
7124
7001
|
|
|
7125
7002
|
// plugins/immune-brain/runtime/claude/mcp_server.ts
|
|
7003
|
+
var MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
7126
7004
|
var TOOLS = [
|
|
7127
7005
|
{ name: "status", description: "Read the Kernel Assurance Projection for an exact task.", privileged: false },
|
|
7128
7006
|
{ name: "enroll", description: "Enroll a Git-tracked TaskIntent after native confirmation.", privileged: true },
|
|
@@ -7150,6 +7028,9 @@ function listMcpTools() {
|
|
|
7150
7028
|
annotations: tool.privileged ? privilegedAnnotations() : { readOnlyHint: tool.name === "status" }
|
|
7151
7029
|
}));
|
|
7152
7030
|
}
|
|
7031
|
+
function supportsElicitationProtocol(value) {
|
|
7032
|
+
return typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value) && value >= MCP_PROTOCOL_VERSION;
|
|
7033
|
+
}
|
|
7153
7034
|
function createMcpRuntime(options = {}) {
|
|
7154
7035
|
const host = options.host ?? new ClaudeReviewHost(new FileHookEventLog);
|
|
7155
7036
|
const runtime = new ClaudeRuntime({
|
|
@@ -7158,19 +7039,24 @@ function createMcpRuntime(options = {}) {
|
|
|
7158
7039
|
host,
|
|
7159
7040
|
ports: options.ports,
|
|
7160
7041
|
interactive: options.interactive,
|
|
7161
|
-
|
|
7042
|
+
requestConfirmation: options.requestConfirmation
|
|
7162
7043
|
});
|
|
7163
7044
|
let negotiatedVersion;
|
|
7164
7045
|
let negotiatedInteractive = false;
|
|
7046
|
+
const connectionId = randomUUID6();
|
|
7165
7047
|
return {
|
|
7166
7048
|
runtime,
|
|
7167
7049
|
host,
|
|
7050
|
+
connectionId,
|
|
7168
7051
|
listTools: listMcpTools,
|
|
7169
7052
|
bindClientHandshake(identity) {
|
|
7170
7053
|
negotiatedVersion = identity.version || undefined;
|
|
7171
|
-
negotiatedInteractive = identity.interactive;
|
|
7054
|
+
negotiatedInteractive = identity.interactive && supportsElicitationProtocol(identity.protocolVersion);
|
|
7172
7055
|
runtime.bindHostVersion(negotiatedVersion);
|
|
7173
7056
|
},
|
|
7057
|
+
bindNativeConfirmation(port) {
|
|
7058
|
+
runtime.bindNativeConfirmation(port);
|
|
7059
|
+
},
|
|
7174
7060
|
sessionInteractive: () => negotiatedInteractive,
|
|
7175
7061
|
async callTool(name, args, meta = {}) {
|
|
7176
7062
|
const taskId = String(args.task_id ?? "");
|
|
@@ -7181,20 +7067,19 @@ function createMcpRuntime(options = {}) {
|
|
|
7181
7067
|
if (name === "status")
|
|
7182
7068
|
return { plugin_version: PLUGIN_VERSION, ...await runtime.status(taskId) };
|
|
7183
7069
|
if (!negotiatedVersion)
|
|
7184
|
-
throw new
|
|
7185
|
-
if (!negotiatedInteractive && name !== "repair_authority_state")
|
|
7186
|
-
throw new
|
|
7070
|
+
throw new NativeAuthorityError("unsupported_host", "Claude Code version is unavailable");
|
|
7071
|
+
if (!negotiatedInteractive && name !== "repair_authority_state") {
|
|
7072
|
+
throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
|
|
7073
|
+
}
|
|
7187
7074
|
const probe = probeHost(options.env ?? process.env, process.platform, negotiatedVersion);
|
|
7188
7075
|
if (!probe.ok)
|
|
7189
|
-
throw new
|
|
7076
|
+
throw new NativeAuthorityError("unsupported_host", probe.reason);
|
|
7190
7077
|
const toolMeta = {
|
|
7191
|
-
sessionId: meta.sessionId ??
|
|
7078
|
+
sessionId: meta.sessionId ?? connectionId,
|
|
7192
7079
|
toolCallId: meta.toolCallId ?? `call-${name}`,
|
|
7193
7080
|
taskId,
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
interactive: meta.interactive ?? options.interactive ?? false,
|
|
7197
|
-
decision: meta.decision
|
|
7081
|
+
interactive: meta.interactive ?? options.interactive ?? negotiatedInteractive,
|
|
7082
|
+
signal: meta.signal
|
|
7198
7083
|
};
|
|
7199
7084
|
if (name === "enroll")
|
|
7200
7085
|
return runtime.enroll(taskId, toolMeta);
|
|
@@ -7211,19 +7096,25 @@ function createMcpRuntime(options = {}) {
|
|
|
7211
7096
|
throw new Error(`unknown tool ${name}`);
|
|
7212
7097
|
},
|
|
7213
7098
|
observe: (event) => host.observe(event),
|
|
7214
|
-
sessionOfElicitation: (toolCallId) => host.sessionOfElicitation(toolCallId),
|
|
7215
7099
|
shutdown: () => runtime.shutdown(),
|
|
7216
7100
|
aborts: new Map
|
|
7217
7101
|
};
|
|
7218
7102
|
}
|
|
7219
|
-
function hostCallIdentity(meta
|
|
7103
|
+
function hostCallIdentity(meta) {
|
|
7220
7104
|
if (!meta)
|
|
7221
7105
|
return;
|
|
7222
7106
|
const toolCallId = meta["claudecode/toolUseId"];
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7107
|
+
return typeof toolCallId === "string" && toolCallId ? { toolCallId } : undefined;
|
|
7108
|
+
}
|
|
7109
|
+
function rpcError(error) {
|
|
7110
|
+
if (error instanceof NativeAuthorityError) {
|
|
7111
|
+
return {
|
|
7112
|
+
code: -32000,
|
|
7113
|
+
message: error.message,
|
|
7114
|
+
data: { reason_code: error.reasonCode, recovery_action: error.recoveryAction }
|
|
7115
|
+
};
|
|
7116
|
+
}
|
|
7117
|
+
return { code: -32000, message: error instanceof Error ? error.message : String(error) };
|
|
7227
7118
|
}
|
|
7228
7119
|
function encodeMessage(message) {
|
|
7229
7120
|
return Buffer.from(`${JSON.stringify(message)}
|
|
@@ -7236,13 +7127,14 @@ async function handleJsonRpc(message, mcp = createMcpRuntime()) {
|
|
|
7236
7127
|
const elicitation = params.capabilities?.elicitation;
|
|
7237
7128
|
mcp.bindClientHandshake({
|
|
7238
7129
|
version: trustedClient && typeof params.clientInfo?.version === "string" ? params.clientInfo.version : "",
|
|
7130
|
+
protocolVersion: typeof params.protocolVersion === "string" ? params.protocolVersion : undefined,
|
|
7239
7131
|
interactive: trustedClient && elicitation !== null && typeof elicitation === "object" && !Array.isArray(elicitation)
|
|
7240
7132
|
});
|
|
7241
7133
|
return {
|
|
7242
7134
|
jsonrpc: "2.0",
|
|
7243
7135
|
id: message.id ?? null,
|
|
7244
7136
|
result: {
|
|
7245
|
-
protocolVersion:
|
|
7137
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
7246
7138
|
serverInfo: {
|
|
7247
7139
|
name: HOST_ID,
|
|
7248
7140
|
version: PLUGIN_VERSION,
|
|
@@ -7273,13 +7165,13 @@ async function handleJsonRpc(message, mcp = createMcpRuntime()) {
|
|
|
7273
7165
|
const name = String(params.name ?? "");
|
|
7274
7166
|
const interactive = mcp.sessionInteractive();
|
|
7275
7167
|
if (isPrivilegedOperation(name) && !interactive) {
|
|
7276
|
-
throw new
|
|
7168
|
+
throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
|
|
7277
7169
|
}
|
|
7278
|
-
const identity = hostCallIdentity(params._meta
|
|
7170
|
+
const identity = hostCallIdentity(params._meta);
|
|
7279
7171
|
if (isPrivilegedOperation(name) && !identity) {
|
|
7280
|
-
throw new
|
|
7172
|
+
throw new NativeAuthorityError("correlation_missing", "canonical claudecode/toolUseId metadata is missing");
|
|
7281
7173
|
}
|
|
7282
|
-
const sessionId =
|
|
7174
|
+
const sessionId = mcp.connectionId;
|
|
7283
7175
|
const toolCallId = identity?.toolCallId ?? String(message.id ?? "stdio");
|
|
7284
7176
|
const result = await mcp.callTool(name, params.arguments ?? {}, {
|
|
7285
7177
|
sessionId,
|
|
@@ -7294,11 +7186,10 @@ async function handleJsonRpc(message, mcp = createMcpRuntime()) {
|
|
|
7294
7186
|
result: { content: [{ type: "text", text: JSON.stringify(result) }] }
|
|
7295
7187
|
};
|
|
7296
7188
|
} catch (error) {
|
|
7297
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
7298
7189
|
return {
|
|
7299
7190
|
jsonrpc: "2.0",
|
|
7300
7191
|
id: message.id ?? null,
|
|
7301
|
-
error:
|
|
7192
|
+
error: rpcError(error)
|
|
7302
7193
|
};
|
|
7303
7194
|
} finally {
|
|
7304
7195
|
mcp.aborts.delete(requestId);
|
|
@@ -7310,9 +7201,75 @@ async function handleJsonRpc(message, mcp = createMcpRuntime()) {
|
|
|
7310
7201
|
}
|
|
7311
7202
|
async function writeReply(output, reply) {
|
|
7312
7203
|
const payload = encodeMessage(reply);
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7204
|
+
await new Promise((resolve7, reject) => {
|
|
7205
|
+
if (!output.writable) {
|
|
7206
|
+
reject(new Error("output stream is not writable"));
|
|
7207
|
+
return;
|
|
7208
|
+
}
|
|
7209
|
+
let settled = false;
|
|
7210
|
+
const cleanup = () => {
|
|
7211
|
+
output.off("drain", onDrain);
|
|
7212
|
+
output.off("error", onError);
|
|
7213
|
+
output.off("close", onClose);
|
|
7214
|
+
};
|
|
7215
|
+
const onDrain = () => {
|
|
7216
|
+
if (settled)
|
|
7217
|
+
return;
|
|
7218
|
+
settled = true;
|
|
7219
|
+
cleanup();
|
|
7220
|
+
resolve7();
|
|
7221
|
+
};
|
|
7222
|
+
const onError = (error) => {
|
|
7223
|
+
if (settled)
|
|
7224
|
+
return;
|
|
7225
|
+
settled = true;
|
|
7226
|
+
cleanup();
|
|
7227
|
+
reject(error);
|
|
7228
|
+
};
|
|
7229
|
+
const onClose = () => {
|
|
7230
|
+
if (settled)
|
|
7231
|
+
return;
|
|
7232
|
+
settled = true;
|
|
7233
|
+
cleanup();
|
|
7234
|
+
reject(new Error("output stream closed before write drained"));
|
|
7235
|
+
};
|
|
7236
|
+
output.once("error", onError);
|
|
7237
|
+
output.once("close", onClose);
|
|
7238
|
+
const ok = output.write(payload, (error) => {
|
|
7239
|
+
if (settled)
|
|
7240
|
+
return;
|
|
7241
|
+
if (error) {
|
|
7242
|
+
settled = true;
|
|
7243
|
+
cleanup();
|
|
7244
|
+
reject(error);
|
|
7245
|
+
}
|
|
7246
|
+
});
|
|
7247
|
+
if (ok) {
|
|
7248
|
+
settled = true;
|
|
7249
|
+
cleanup();
|
|
7250
|
+
resolve7();
|
|
7251
|
+
return;
|
|
7252
|
+
}
|
|
7253
|
+
output.once("drain", onDrain);
|
|
7254
|
+
});
|
|
7255
|
+
}
|
|
7256
|
+
function elicitationParams(input) {
|
|
7257
|
+
const details = [
|
|
7258
|
+
`Operation: ${input.operation}`,
|
|
7259
|
+
`Task: ${input.taskId}`,
|
|
7260
|
+
input.risk ? `Risk: ${input.risk}` : null,
|
|
7261
|
+
input.intentRevision !== undefined ? `Intent revision: ${input.intentRevision}` : null,
|
|
7262
|
+
input.intentContentHash ? `Intent hash: ${input.intentContentHash}` : null,
|
|
7263
|
+
input.bindingDigest ? `Binding digest: ${input.bindingDigest}` : null
|
|
7264
|
+
].filter(Boolean);
|
|
7265
|
+
return {
|
|
7266
|
+
mode: "form",
|
|
7267
|
+
message: `Authorize this exact Immune-Brain operation?
|
|
7268
|
+
|
|
7269
|
+
${details.join(`
|
|
7270
|
+
`)}`,
|
|
7271
|
+
requestedSchema: { type: "object", properties: {} }
|
|
7272
|
+
};
|
|
7316
7273
|
}
|
|
7317
7274
|
async function serveStdio(options = {}) {
|
|
7318
7275
|
const input = options.input ?? stdin;
|
|
@@ -7323,16 +7280,88 @@ async function serveStdio(options = {}) {
|
|
|
7323
7280
|
});
|
|
7324
7281
|
let buffer = Buffer.alloc(0);
|
|
7325
7282
|
let accepting = true;
|
|
7283
|
+
let requestSequence = 0;
|
|
7326
7284
|
const inFlight = new Set;
|
|
7285
|
+
const pending = new Map;
|
|
7327
7286
|
let chain = Promise.resolve();
|
|
7287
|
+
const rejectPending = (error) => {
|
|
7288
|
+
for (const item of pending.values())
|
|
7289
|
+
item.reject(error);
|
|
7290
|
+
pending.clear();
|
|
7291
|
+
};
|
|
7292
|
+
mcp.bindNativeConfirmation(async (input2) => {
|
|
7293
|
+
if (!accepting)
|
|
7294
|
+
throw new NativeAuthorityError("interaction_not_opened", "MCP connection is closed");
|
|
7295
|
+
if (input2.signal?.aborted)
|
|
7296
|
+
throw new NativeAuthorityError("user_cancelled", "Tool call was cancelled");
|
|
7297
|
+
const requestId = `immune-brain:elicitation:${mcp.connectionId}:${++requestSequence}`;
|
|
7298
|
+
let abortListener;
|
|
7299
|
+
const response = new Promise((resolve7, reject) => {
|
|
7300
|
+
pending.set(requestId, { resolve: resolve7, reject });
|
|
7301
|
+
abortListener = () => {
|
|
7302
|
+
pending.delete(requestId);
|
|
7303
|
+
reject(new NativeAuthorityError("user_cancelled", "Tool call was cancelled"));
|
|
7304
|
+
};
|
|
7305
|
+
input2.signal?.addEventListener("abort", abortListener, { once: true });
|
|
7306
|
+
});
|
|
7307
|
+
try {
|
|
7308
|
+
await writeReply(output, {
|
|
7309
|
+
jsonrpc: "2.0",
|
|
7310
|
+
id: requestId,
|
|
7311
|
+
method: "elicitation/create",
|
|
7312
|
+
params: elicitationParams(input2)
|
|
7313
|
+
});
|
|
7314
|
+
const reply = await response;
|
|
7315
|
+
if (reply.error) {
|
|
7316
|
+
if (reply.error.code === -32601) {
|
|
7317
|
+
throw new NativeAuthorityError("unsupported_host", "Claude Code rejected MCP elicitation/create");
|
|
7318
|
+
}
|
|
7319
|
+
throw new NativeAuthorityError("correlation_missing", `MCP elicitation failed: ${reply.error.message}`);
|
|
7320
|
+
}
|
|
7321
|
+
const result = reply.result;
|
|
7322
|
+
const action = typeof result === "object" && result !== null && !Array.isArray(result) ? result.action : undefined;
|
|
7323
|
+
if (action !== "accept" && action !== "decline" && action !== "cancel") {
|
|
7324
|
+
throw new NativeAuthorityError("correlation_missing", "MCP elicitation returned an invalid action");
|
|
7325
|
+
}
|
|
7326
|
+
if (action === "accept") {
|
|
7327
|
+
const content = result.content;
|
|
7328
|
+
if (typeof content !== "object" || content === null || Array.isArray(content) || Object.keys(content).length !== 0) {
|
|
7329
|
+
throw new NativeAuthorityError("correlation_missing", "MCP elicitation accept content did not match the requested schema");
|
|
7330
|
+
}
|
|
7331
|
+
}
|
|
7332
|
+
return { decision: action, requestId };
|
|
7333
|
+
} finally {
|
|
7334
|
+
pending.delete(requestId);
|
|
7335
|
+
if (abortListener)
|
|
7336
|
+
input2.signal?.removeEventListener("abort", abortListener);
|
|
7337
|
+
}
|
|
7338
|
+
});
|
|
7328
7339
|
const runCall = (parsed) => {
|
|
7329
7340
|
const task = handleJsonRpc(parsed, mcp).then(async (reply) => {
|
|
7330
7341
|
if (reply)
|
|
7331
7342
|
await writeReply(output, reply);
|
|
7343
|
+
}).catch(() => {
|
|
7344
|
+
executeShutdown(1);
|
|
7332
7345
|
});
|
|
7333
7346
|
inFlight.add(task);
|
|
7334
7347
|
task.finally(() => inFlight.delete(task));
|
|
7335
7348
|
};
|
|
7349
|
+
const routeResponse = (obj) => {
|
|
7350
|
+
const id = typeof obj.id === "string" ? obj.id : "";
|
|
7351
|
+
const waiter = id ? pending.get(id) : undefined;
|
|
7352
|
+
if (waiter) {
|
|
7353
|
+
pending.delete(id);
|
|
7354
|
+
const hasResult = Object.hasOwn(obj, "result");
|
|
7355
|
+
const hasError = Object.hasOwn(obj, "error");
|
|
7356
|
+
if (obj.jsonrpc !== "2.0" || obj.method !== undefined || hasResult === hasError) {
|
|
7357
|
+
waiter.reject(new NativeAuthorityError("correlation_missing", "malformed MCP elicitation response"));
|
|
7358
|
+
} else {
|
|
7359
|
+
waiter.resolve(obj);
|
|
7360
|
+
}
|
|
7361
|
+
return true;
|
|
7362
|
+
}
|
|
7363
|
+
return obj.method === undefined && obj.id !== undefined;
|
|
7364
|
+
};
|
|
7336
7365
|
const drainStdio = async () => {
|
|
7337
7366
|
while (true) {
|
|
7338
7367
|
const newline = buffer.indexOf(`
|
|
@@ -7355,10 +7384,14 @@ async function serveStdio(options = {}) {
|
|
|
7355
7384
|
continue;
|
|
7356
7385
|
}
|
|
7357
7386
|
const obj = parsed;
|
|
7387
|
+
if (routeResponse(obj))
|
|
7388
|
+
continue;
|
|
7358
7389
|
if (obj.jsonrpc !== "2.0") {
|
|
7359
7390
|
await writeReply(output, { jsonrpc: "2.0", id: null, error: { code: -32600, message: "Invalid Request: jsonrpc must be '2.0'" } });
|
|
7360
7391
|
continue;
|
|
7361
7392
|
}
|
|
7393
|
+
if (routeResponse(obj))
|
|
7394
|
+
continue;
|
|
7362
7395
|
if (typeof obj.method !== "string" || !obj.method) {
|
|
7363
7396
|
const id = obj.id !== undefined && (typeof obj.id === "string" || typeof obj.id === "number") ? obj.id : null;
|
|
7364
7397
|
await writeReply(output, { jsonrpc: "2.0", id, error: { code: -32600, message: "Invalid Request: missing method" } });
|
|
@@ -7375,11 +7408,15 @@ async function serveStdio(options = {}) {
|
|
|
7375
7408
|
const rpc = parsed;
|
|
7376
7409
|
if (rpc.method === "notifications/cancelled") {
|
|
7377
7410
|
const requestId = rpc.params?.requestId;
|
|
7378
|
-
if (requestId
|
|
7411
|
+
if (typeof requestId === "string" && pending.has(requestId)) {
|
|
7412
|
+
pending.get(requestId)?.reject(new NativeAuthorityError("user_cancelled", "native interaction was cancelled"));
|
|
7413
|
+
pending.delete(requestId);
|
|
7414
|
+
} else if (requestId !== undefined) {
|
|
7379
7415
|
mcp.aborts.get(requestId)?.abort(new Error("notifications/cancelled"));
|
|
7416
|
+
}
|
|
7380
7417
|
continue;
|
|
7381
7418
|
}
|
|
7382
|
-
if (
|
|
7419
|
+
if (rpc.method === "tools/call") {
|
|
7383
7420
|
if (!accepting) {
|
|
7384
7421
|
if (rpc.id !== undefined)
|
|
7385
7422
|
await writeReply(output, { jsonrpc: "2.0", id: rpc.id, error: { code: -32000, message: "stdio closed" } });
|
|
@@ -7393,6 +7430,7 @@ async function serveStdio(options = {}) {
|
|
|
7393
7430
|
await writeReply(output, reply);
|
|
7394
7431
|
}
|
|
7395
7432
|
};
|
|
7433
|
+
let resolveStdio = () => {};
|
|
7396
7434
|
let shutdownPromise = null;
|
|
7397
7435
|
const executeShutdown = (code = 0) => {
|
|
7398
7436
|
if (shutdownPromise)
|
|
@@ -7401,31 +7439,49 @@ async function serveStdio(options = {}) {
|
|
|
7401
7439
|
accepting = false;
|
|
7402
7440
|
for (const ac of mcp.aborts.values())
|
|
7403
7441
|
ac.abort(new Error("stdio closed"));
|
|
7404
|
-
|
|
7442
|
+
rejectPending(new NativeAuthorityError("user_cancelled", "MCP connection closed during native interaction"));
|
|
7443
|
+
await Promise.race([
|
|
7444
|
+
Promise.allSettled([...inFlight]),
|
|
7445
|
+
new Promise((resolve7) => setTimeout(resolve7, 200))
|
|
7446
|
+
]);
|
|
7405
7447
|
await mcp.shutdown();
|
|
7406
7448
|
if (output.writable && typeof output.end === "function") {
|
|
7407
7449
|
await new Promise((cb) => output.end(() => cb()));
|
|
7408
7450
|
}
|
|
7409
7451
|
process.exitCode = code;
|
|
7410
7452
|
exit(code);
|
|
7453
|
+
resolveStdio();
|
|
7411
7454
|
})();
|
|
7412
7455
|
return shutdownPromise;
|
|
7413
7456
|
};
|
|
7414
7457
|
await new Promise((resolve7) => {
|
|
7458
|
+
resolveStdio = resolve7;
|
|
7459
|
+
output.on("error", () => {
|
|
7460
|
+
executeShutdown(1);
|
|
7461
|
+
});
|
|
7462
|
+
output.on("close", () => {
|
|
7463
|
+
executeShutdown(0);
|
|
7464
|
+
});
|
|
7415
7465
|
input.on("data", (chunk) => {
|
|
7466
|
+
if (!accepting)
|
|
7467
|
+
return;
|
|
7416
7468
|
chain = chain.then(async () => {
|
|
7469
|
+
if (!accepting)
|
|
7470
|
+
return;
|
|
7417
7471
|
buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
|
|
7418
7472
|
await drainStdio();
|
|
7473
|
+
}).catch(() => {
|
|
7474
|
+
executeShutdown(1);
|
|
7419
7475
|
});
|
|
7420
7476
|
});
|
|
7421
7477
|
input.on("end", () => {
|
|
7422
|
-
chain.then(() => executeShutdown(0))
|
|
7478
|
+
chain.then(() => executeShutdown(0));
|
|
7423
7479
|
});
|
|
7424
7480
|
input.on("close", () => {
|
|
7425
|
-
chain.then(() => executeShutdown(0))
|
|
7481
|
+
chain.then(() => executeShutdown(0));
|
|
7426
7482
|
});
|
|
7427
7483
|
input.on("error", () => {
|
|
7428
|
-
|
|
7484
|
+
executeShutdown(1);
|
|
7429
7485
|
});
|
|
7430
7486
|
});
|
|
7431
7487
|
}
|
|
@@ -7448,9 +7504,12 @@ if (entry.endsWith("mcp_server.ts") || entry.endsWith("mcp-server.mjs")) {
|
|
|
7448
7504
|
serveStdio();
|
|
7449
7505
|
}
|
|
7450
7506
|
export {
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
handleJsonRpc,
|
|
7507
|
+
supportsElicitationProtocol,
|
|
7508
|
+
serveStdio,
|
|
7454
7509
|
listMcpTools,
|
|
7455
|
-
|
|
7510
|
+
handleJsonRpc,
|
|
7511
|
+
elicitationParams,
|
|
7512
|
+
createMcpRuntime,
|
|
7513
|
+
TOOLS,
|
|
7514
|
+
MCP_PROTOCOL_VERSION
|
|
7456
7515
|
};
|