immune-brain 3.3.0 → 3.5.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/.pi-extension/imm-canary-work.ts +84 -1
- package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +124 -6
- package/plugins/immune-brain/dist/BASELINE.md +1 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +307 -242
- 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/assurance/coordinator.ts +7 -1
- 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.5.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
|
|
|
@@ -1517,7 +1380,13 @@ class AssuranceCoordinator {
|
|
|
1517
1380
|
ensureOperationLive();
|
|
1518
1381
|
qaVerdict = await this.ports.runQa(assurance.snapshot, assurance.descriptors, runner, {
|
|
1519
1382
|
signal: operationController.signal,
|
|
1520
|
-
onProgress: (item) => progress("verifying", `QA ${item.index}/${item.total} ${item.acceptance_id} ${item.phase}`, {
|
|
1383
|
+
onProgress: (item) => progress("verifying", `QA ${item.index}/${item.total} ${item.acceptance_id} ${item.phase}`, {
|
|
1384
|
+
current: item.index,
|
|
1385
|
+
total: item.total,
|
|
1386
|
+
acceptance_id: item.acceptance_id,
|
|
1387
|
+
acceptance_phase: item.phase,
|
|
1388
|
+
elapsed_ms: item.elapsed_ms
|
|
1389
|
+
})
|
|
1521
1390
|
});
|
|
1522
1391
|
ensureOperationLive();
|
|
1523
1392
|
const invocation = this.openInvocation(taskId);
|
|
@@ -6717,6 +6586,10 @@ async function mintCapability(registry, input) {
|
|
|
6717
6586
|
};
|
|
6718
6587
|
return registry.issue(binding);
|
|
6719
6588
|
}
|
|
6589
|
+
function throwIfCancelled(signal) {
|
|
6590
|
+
if (signal?.aborted)
|
|
6591
|
+
throw new NativeAuthorityError("user_cancelled", "Tool call was cancelled");
|
|
6592
|
+
}
|
|
6720
6593
|
|
|
6721
6594
|
class ClaudeRuntime {
|
|
6722
6595
|
host;
|
|
@@ -6724,8 +6597,7 @@ class ClaudeRuntime {
|
|
|
6724
6597
|
cwd;
|
|
6725
6598
|
env;
|
|
6726
6599
|
interactive;
|
|
6727
|
-
|
|
6728
|
-
decisions;
|
|
6600
|
+
requestConfirmation;
|
|
6729
6601
|
hostVersion;
|
|
6730
6602
|
mutationRegistry = null;
|
|
6731
6603
|
enrollmentRegistry = createEnrollmentAuthorityRegistry();
|
|
@@ -6734,8 +6606,7 @@ class ClaudeRuntime {
|
|
|
6734
6606
|
this.cwd = options.cwd;
|
|
6735
6607
|
this.env = options.env ?? process.env;
|
|
6736
6608
|
this.interactive = options.interactive ?? true;
|
|
6737
|
-
this.
|
|
6738
|
-
this.decisions = options.decisions ?? new Map;
|
|
6609
|
+
this.requestConfirmation = options.requestConfirmation;
|
|
6739
6610
|
this.host = options.host ?? new ClaudeReviewHost(new FileHookEventLog);
|
|
6740
6611
|
if (options.ports) {
|
|
6741
6612
|
this.coordinator = new AssuranceCoordinator({ ...options.ports, host: this.host });
|
|
@@ -6749,6 +6620,9 @@ class ClaudeRuntime {
|
|
|
6749
6620
|
bindHostVersion(version) {
|
|
6750
6621
|
this.hostVersion = version || undefined;
|
|
6751
6622
|
}
|
|
6623
|
+
bindNativeConfirmation(port) {
|
|
6624
|
+
this.requestConfirmation = port;
|
|
6625
|
+
}
|
|
6752
6626
|
async shutdown() {
|
|
6753
6627
|
await this.coordinator.onSessionShutdown();
|
|
6754
6628
|
}
|
|
@@ -6784,32 +6658,28 @@ class ClaudeRuntime {
|
|
|
6784
6658
|
this.app ??= createCanaryApplication(this.mutationRegistry);
|
|
6785
6659
|
return { registry: this.mutationRegistry, app: this.app };
|
|
6786
6660
|
}
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
if (configured === "deny" || configured === "cancel")
|
|
6790
|
-
this.gate(operation, meta);
|
|
6791
|
-
}
|
|
6792
|
-
gate(operation, meta, binding = {}) {
|
|
6661
|
+
async gate(operation, meta, binding = {}) {
|
|
6662
|
+
throwIfCancelled(meta.signal);
|
|
6793
6663
|
const probe = probeHost(this.env, process.platform, this.hostVersion);
|
|
6794
6664
|
if (!probe.ok)
|
|
6795
|
-
throw new
|
|
6796
|
-
const
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
});
|
|
6665
|
+
throw new NativeAuthorityError("unsupported_host", probe.reason);
|
|
6666
|
+
const interactive = meta.interactive ?? this.interactive;
|
|
6667
|
+
if (!interactive)
|
|
6668
|
+
throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
|
|
6669
|
+
if (!isPrivilegedOperation(operation))
|
|
6670
|
+
throw new Error(`unsupported native operation ${operation}`);
|
|
6671
|
+
if (!this.requestConfirmation)
|
|
6672
|
+
throw new NativeAuthorityError("interaction_not_opened", "native confirmation port is unavailable");
|
|
6673
|
+
const result = await this.requestConfirmation({ operation, taskId: meta.taskId, toolCallId: meta.toolCallId, signal: meta.signal, ...binding });
|
|
6674
|
+
throwIfCancelled(meta.signal);
|
|
6675
|
+
const gate = evaluateNativeGate({ operation, interactive, decision: result.decision });
|
|
6807
6676
|
if (!gate.ok)
|
|
6808
|
-
throw
|
|
6677
|
+
throw gate.error;
|
|
6809
6678
|
return {
|
|
6810
6679
|
confirmation_ref: confirmationRef({
|
|
6811
|
-
|
|
6680
|
+
connectionId: meta.sessionId,
|
|
6812
6681
|
toolCallId: meta.toolCallId,
|
|
6682
|
+
requestId: result.requestId,
|
|
6813
6683
|
operation,
|
|
6814
6684
|
taskId: meta.taskId,
|
|
6815
6685
|
...binding
|
|
@@ -6820,19 +6690,21 @@ class ClaudeRuntime {
|
|
|
6820
6690
|
return projectAssurance(this.cwd, taskId, diffSnapshotOf);
|
|
6821
6691
|
}
|
|
6822
6692
|
async enroll(taskId, meta) {
|
|
6823
|
-
this.rejectBeforePreparation("enroll", { ...meta, taskId });
|
|
6824
6693
|
const now = new Date().toISOString();
|
|
6825
6694
|
const preparation = await preparePiCanary(this.cwd, { task_id: taskId, now });
|
|
6826
|
-
const
|
|
6695
|
+
const intent = await readTaskIntent(this.cwd, taskId);
|
|
6696
|
+
const gate = await this.gate("enroll", { ...meta, taskId }, {
|
|
6697
|
+
risk: intent.intent.risk,
|
|
6827
6698
|
intentRevision: preparation.intent?.revision,
|
|
6828
6699
|
intentContentHash: preparation.intent?.content_hash,
|
|
6829
6700
|
bindingDigest: preparation.digest
|
|
6830
6701
|
});
|
|
6831
6702
|
const { unchanged } = await revalidatePiCanary(this.cwd, { task_id: taskId, now }, preparation);
|
|
6832
6703
|
if (!unchanged)
|
|
6833
|
-
throw new
|
|
6704
|
+
throw new NativeAuthorityError("workspace_changed", "workspace changed after native confirmation");
|
|
6834
6705
|
if (!preparation.intent)
|
|
6835
6706
|
throw new Error("enrollment requires a readable TaskIntent");
|
|
6707
|
+
throwIfCancelled(meta.signal);
|
|
6836
6708
|
const nonce = enrollmentNonce();
|
|
6837
6709
|
const binding = {
|
|
6838
6710
|
task_id: taskId,
|
|
@@ -6877,7 +6749,6 @@ class ClaudeRuntime {
|
|
|
6877
6749
|
}
|
|
6878
6750
|
if (!isPrivilegedOperation(operation) && operation !== "request_authorization")
|
|
6879
6751
|
throw new Error(`unsupported privileged operation ${operation}`);
|
|
6880
|
-
this.rejectBeforePreparation(operation, { ...meta, taskId });
|
|
6881
6752
|
let op = operation;
|
|
6882
6753
|
let decisionOp;
|
|
6883
6754
|
const projection = await this.status(taskId);
|
|
@@ -6934,16 +6805,22 @@ class ClaudeRuntime {
|
|
|
6934
6805
|
`);
|
|
6935
6806
|
execFileSync4("git", ["add", "--", priorIntent.intent_ref.path], { cwd: this.cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
6936
6807
|
const preparedRecord = await readTaskRecord(this.cwd, taskId);
|
|
6937
|
-
if (!preparedRecord.record)
|
|
6938
|
-
throw new
|
|
6808
|
+
if (!preparedRecord.record) {
|
|
6809
|
+
throw new NativeAuthorityError("workspace_changed", "TaskRecord changed before the breaking revision digest");
|
|
6810
|
+
}
|
|
6939
6811
|
preparedDiffHash = diffHashOf(this.cwd, preparedRecord.record);
|
|
6940
6812
|
}
|
|
6941
6813
|
const preparedProjection = await this.status(taskId);
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6814
|
+
try {
|
|
6815
|
+
assertProjectionBinding(projection, preparedProjection, Boolean(nextIntent));
|
|
6816
|
+
if (preparedProjection.projection.diff_hash !== preparedDiffHash) {
|
|
6817
|
+
throw new Error("workspace changed while preparing the authority digest");
|
|
6818
|
+
}
|
|
6819
|
+
} catch (error) {
|
|
6820
|
+
throw new NativeAuthorityError("workspace_changed", error instanceof Error ? error.message : String(error));
|
|
6945
6821
|
}
|
|
6946
|
-
gate = this.gate(operation, { ...meta, taskId }, {
|
|
6822
|
+
gate = await this.gate(operation, { ...meta, taskId }, {
|
|
6823
|
+
risk: projection.projection.risk,
|
|
6947
6824
|
intentRevision: nextIntent?.revision ?? projection.projection.intent_revision,
|
|
6948
6825
|
intentContentHash: nextIntentHash ?? projection.projection.intent_content_hash,
|
|
6949
6826
|
bindingDigest: `${preparedDiffHash}:${nextIntentHash ?? ""}`
|
|
@@ -6961,11 +6838,16 @@ class ClaudeRuntime {
|
|
|
6961
6838
|
const confirmation = gate.confirmation_ref;
|
|
6962
6839
|
try {
|
|
6963
6840
|
const capabilityProjection = await this.status(taskId);
|
|
6964
|
-
|
|
6841
|
+
try {
|
|
6842
|
+
assertProjectionBinding(projection, capabilityProjection, Boolean(nextIntent));
|
|
6843
|
+
} catch (error) {
|
|
6844
|
+
throw new NativeAuthorityError("workspace_changed", error instanceof Error ? error.message : String(error));
|
|
6845
|
+
}
|
|
6965
6846
|
const operationDiffHash = capabilityProjection.projection.diff_hash;
|
|
6966
6847
|
if (nextIntent && operationDiffHash !== preparedDiffHash) {
|
|
6967
|
-
throw new
|
|
6848
|
+
throw new NativeAuthorityError("workspace_changed", "workspace changed after native confirmation");
|
|
6968
6849
|
}
|
|
6850
|
+
throwIfCancelled(meta.signal);
|
|
6969
6851
|
const capability = await mintCapability(registry, {
|
|
6970
6852
|
authority_kind: "user",
|
|
6971
6853
|
task_id: taskId,
|
|
@@ -6981,6 +6863,7 @@ class ClaudeRuntime {
|
|
|
6981
6863
|
...op === "resolve_user_decision" && decisionOp ? decisionOp : {},
|
|
6982
6864
|
...op === "stop" ? { reason: extra.reason ?? "user stop" } : {}
|
|
6983
6865
|
});
|
|
6866
|
+
throwIfCancelled(meta.signal);
|
|
6984
6867
|
const result = app.execute({
|
|
6985
6868
|
root: this.cwd,
|
|
6986
6869
|
task_id: taskId,
|
|
@@ -7123,6 +7006,7 @@ class ClaudeRuntime {
|
|
|
7123
7006
|
}
|
|
7124
7007
|
|
|
7125
7008
|
// plugins/immune-brain/runtime/claude/mcp_server.ts
|
|
7009
|
+
var MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
7126
7010
|
var TOOLS = [
|
|
7127
7011
|
{ name: "status", description: "Read the Kernel Assurance Projection for an exact task.", privileged: false },
|
|
7128
7012
|
{ name: "enroll", description: "Enroll a Git-tracked TaskIntent after native confirmation.", privileged: true },
|
|
@@ -7150,6 +7034,9 @@ function listMcpTools() {
|
|
|
7150
7034
|
annotations: tool.privileged ? privilegedAnnotations() : { readOnlyHint: tool.name === "status" }
|
|
7151
7035
|
}));
|
|
7152
7036
|
}
|
|
7037
|
+
function supportsElicitationProtocol(value) {
|
|
7038
|
+
return typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value) && value >= MCP_PROTOCOL_VERSION;
|
|
7039
|
+
}
|
|
7153
7040
|
function createMcpRuntime(options = {}) {
|
|
7154
7041
|
const host = options.host ?? new ClaudeReviewHost(new FileHookEventLog);
|
|
7155
7042
|
const runtime = new ClaudeRuntime({
|
|
@@ -7158,19 +7045,24 @@ function createMcpRuntime(options = {}) {
|
|
|
7158
7045
|
host,
|
|
7159
7046
|
ports: options.ports,
|
|
7160
7047
|
interactive: options.interactive,
|
|
7161
|
-
|
|
7048
|
+
requestConfirmation: options.requestConfirmation
|
|
7162
7049
|
});
|
|
7163
7050
|
let negotiatedVersion;
|
|
7164
7051
|
let negotiatedInteractive = false;
|
|
7052
|
+
const connectionId = randomUUID6();
|
|
7165
7053
|
return {
|
|
7166
7054
|
runtime,
|
|
7167
7055
|
host,
|
|
7056
|
+
connectionId,
|
|
7168
7057
|
listTools: listMcpTools,
|
|
7169
7058
|
bindClientHandshake(identity) {
|
|
7170
7059
|
negotiatedVersion = identity.version || undefined;
|
|
7171
|
-
negotiatedInteractive = identity.interactive;
|
|
7060
|
+
negotiatedInteractive = identity.interactive && supportsElicitationProtocol(identity.protocolVersion);
|
|
7172
7061
|
runtime.bindHostVersion(negotiatedVersion);
|
|
7173
7062
|
},
|
|
7063
|
+
bindNativeConfirmation(port) {
|
|
7064
|
+
runtime.bindNativeConfirmation(port);
|
|
7065
|
+
},
|
|
7174
7066
|
sessionInteractive: () => negotiatedInteractive,
|
|
7175
7067
|
async callTool(name, args, meta = {}) {
|
|
7176
7068
|
const taskId = String(args.task_id ?? "");
|
|
@@ -7181,20 +7073,19 @@ function createMcpRuntime(options = {}) {
|
|
|
7181
7073
|
if (name === "status")
|
|
7182
7074
|
return { plugin_version: PLUGIN_VERSION, ...await runtime.status(taskId) };
|
|
7183
7075
|
if (!negotiatedVersion)
|
|
7184
|
-
throw new
|
|
7185
|
-
if (!negotiatedInteractive && name !== "repair_authority_state")
|
|
7186
|
-
throw new
|
|
7076
|
+
throw new NativeAuthorityError("unsupported_host", "Claude Code version is unavailable");
|
|
7077
|
+
if (!negotiatedInteractive && name !== "repair_authority_state") {
|
|
7078
|
+
throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
|
|
7079
|
+
}
|
|
7187
7080
|
const probe = probeHost(options.env ?? process.env, process.platform, negotiatedVersion);
|
|
7188
7081
|
if (!probe.ok)
|
|
7189
|
-
throw new
|
|
7082
|
+
throw new NativeAuthorityError("unsupported_host", probe.reason);
|
|
7190
7083
|
const toolMeta = {
|
|
7191
|
-
sessionId: meta.sessionId ??
|
|
7084
|
+
sessionId: meta.sessionId ?? connectionId,
|
|
7192
7085
|
toolCallId: meta.toolCallId ?? `call-${name}`,
|
|
7193
7086
|
taskId,
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
interactive: meta.interactive ?? options.interactive ?? false,
|
|
7197
|
-
decision: meta.decision
|
|
7087
|
+
interactive: meta.interactive ?? options.interactive ?? negotiatedInteractive,
|
|
7088
|
+
signal: meta.signal
|
|
7198
7089
|
};
|
|
7199
7090
|
if (name === "enroll")
|
|
7200
7091
|
return runtime.enroll(taskId, toolMeta);
|
|
@@ -7211,19 +7102,25 @@ function createMcpRuntime(options = {}) {
|
|
|
7211
7102
|
throw new Error(`unknown tool ${name}`);
|
|
7212
7103
|
},
|
|
7213
7104
|
observe: (event) => host.observe(event),
|
|
7214
|
-
sessionOfElicitation: (toolCallId) => host.sessionOfElicitation(toolCallId),
|
|
7215
7105
|
shutdown: () => runtime.shutdown(),
|
|
7216
7106
|
aborts: new Map
|
|
7217
7107
|
};
|
|
7218
7108
|
}
|
|
7219
|
-
function hostCallIdentity(meta
|
|
7109
|
+
function hostCallIdentity(meta) {
|
|
7220
7110
|
if (!meta)
|
|
7221
7111
|
return;
|
|
7222
7112
|
const toolCallId = meta["claudecode/toolUseId"];
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7113
|
+
return typeof toolCallId === "string" && toolCallId ? { toolCallId } : undefined;
|
|
7114
|
+
}
|
|
7115
|
+
function rpcError(error) {
|
|
7116
|
+
if (error instanceof NativeAuthorityError) {
|
|
7117
|
+
return {
|
|
7118
|
+
code: -32000,
|
|
7119
|
+
message: error.message,
|
|
7120
|
+
data: { reason_code: error.reasonCode, recovery_action: error.recoveryAction }
|
|
7121
|
+
};
|
|
7122
|
+
}
|
|
7123
|
+
return { code: -32000, message: error instanceof Error ? error.message : String(error) };
|
|
7227
7124
|
}
|
|
7228
7125
|
function encodeMessage(message) {
|
|
7229
7126
|
return Buffer.from(`${JSON.stringify(message)}
|
|
@@ -7236,13 +7133,14 @@ async function handleJsonRpc(message, mcp = createMcpRuntime()) {
|
|
|
7236
7133
|
const elicitation = params.capabilities?.elicitation;
|
|
7237
7134
|
mcp.bindClientHandshake({
|
|
7238
7135
|
version: trustedClient && typeof params.clientInfo?.version === "string" ? params.clientInfo.version : "",
|
|
7136
|
+
protocolVersion: typeof params.protocolVersion === "string" ? params.protocolVersion : undefined,
|
|
7239
7137
|
interactive: trustedClient && elicitation !== null && typeof elicitation === "object" && !Array.isArray(elicitation)
|
|
7240
7138
|
});
|
|
7241
7139
|
return {
|
|
7242
7140
|
jsonrpc: "2.0",
|
|
7243
7141
|
id: message.id ?? null,
|
|
7244
7142
|
result: {
|
|
7245
|
-
protocolVersion:
|
|
7143
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
7246
7144
|
serverInfo: {
|
|
7247
7145
|
name: HOST_ID,
|
|
7248
7146
|
version: PLUGIN_VERSION,
|
|
@@ -7273,13 +7171,13 @@ async function handleJsonRpc(message, mcp = createMcpRuntime()) {
|
|
|
7273
7171
|
const name = String(params.name ?? "");
|
|
7274
7172
|
const interactive = mcp.sessionInteractive();
|
|
7275
7173
|
if (isPrivilegedOperation(name) && !interactive) {
|
|
7276
|
-
throw new
|
|
7174
|
+
throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
|
|
7277
7175
|
}
|
|
7278
|
-
const identity = hostCallIdentity(params._meta
|
|
7176
|
+
const identity = hostCallIdentity(params._meta);
|
|
7279
7177
|
if (isPrivilegedOperation(name) && !identity) {
|
|
7280
|
-
throw new
|
|
7178
|
+
throw new NativeAuthorityError("correlation_missing", "canonical claudecode/toolUseId metadata is missing");
|
|
7281
7179
|
}
|
|
7282
|
-
const sessionId =
|
|
7180
|
+
const sessionId = mcp.connectionId;
|
|
7283
7181
|
const toolCallId = identity?.toolCallId ?? String(message.id ?? "stdio");
|
|
7284
7182
|
const result = await mcp.callTool(name, params.arguments ?? {}, {
|
|
7285
7183
|
sessionId,
|
|
@@ -7294,11 +7192,10 @@ async function handleJsonRpc(message, mcp = createMcpRuntime()) {
|
|
|
7294
7192
|
result: { content: [{ type: "text", text: JSON.stringify(result) }] }
|
|
7295
7193
|
};
|
|
7296
7194
|
} catch (error) {
|
|
7297
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
7298
7195
|
return {
|
|
7299
7196
|
jsonrpc: "2.0",
|
|
7300
7197
|
id: message.id ?? null,
|
|
7301
|
-
error:
|
|
7198
|
+
error: rpcError(error)
|
|
7302
7199
|
};
|
|
7303
7200
|
} finally {
|
|
7304
7201
|
mcp.aborts.delete(requestId);
|
|
@@ -7310,9 +7207,75 @@ async function handleJsonRpc(message, mcp = createMcpRuntime()) {
|
|
|
7310
7207
|
}
|
|
7311
7208
|
async function writeReply(output, reply) {
|
|
7312
7209
|
const payload = encodeMessage(reply);
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7210
|
+
await new Promise((resolve7, reject) => {
|
|
7211
|
+
if (!output.writable) {
|
|
7212
|
+
reject(new Error("output stream is not writable"));
|
|
7213
|
+
return;
|
|
7214
|
+
}
|
|
7215
|
+
let settled = false;
|
|
7216
|
+
const cleanup = () => {
|
|
7217
|
+
output.off("drain", onDrain);
|
|
7218
|
+
output.off("error", onError);
|
|
7219
|
+
output.off("close", onClose);
|
|
7220
|
+
};
|
|
7221
|
+
const onDrain = () => {
|
|
7222
|
+
if (settled)
|
|
7223
|
+
return;
|
|
7224
|
+
settled = true;
|
|
7225
|
+
cleanup();
|
|
7226
|
+
resolve7();
|
|
7227
|
+
};
|
|
7228
|
+
const onError = (error) => {
|
|
7229
|
+
if (settled)
|
|
7230
|
+
return;
|
|
7231
|
+
settled = true;
|
|
7232
|
+
cleanup();
|
|
7233
|
+
reject(error);
|
|
7234
|
+
};
|
|
7235
|
+
const onClose = () => {
|
|
7236
|
+
if (settled)
|
|
7237
|
+
return;
|
|
7238
|
+
settled = true;
|
|
7239
|
+
cleanup();
|
|
7240
|
+
reject(new Error("output stream closed before write drained"));
|
|
7241
|
+
};
|
|
7242
|
+
output.once("error", onError);
|
|
7243
|
+
output.once("close", onClose);
|
|
7244
|
+
const ok = output.write(payload, (error) => {
|
|
7245
|
+
if (settled)
|
|
7246
|
+
return;
|
|
7247
|
+
if (error) {
|
|
7248
|
+
settled = true;
|
|
7249
|
+
cleanup();
|
|
7250
|
+
reject(error);
|
|
7251
|
+
}
|
|
7252
|
+
});
|
|
7253
|
+
if (ok) {
|
|
7254
|
+
settled = true;
|
|
7255
|
+
cleanup();
|
|
7256
|
+
resolve7();
|
|
7257
|
+
return;
|
|
7258
|
+
}
|
|
7259
|
+
output.once("drain", onDrain);
|
|
7260
|
+
});
|
|
7261
|
+
}
|
|
7262
|
+
function elicitationParams(input) {
|
|
7263
|
+
const details = [
|
|
7264
|
+
`Operation: ${input.operation}`,
|
|
7265
|
+
`Task: ${input.taskId}`,
|
|
7266
|
+
input.risk ? `Risk: ${input.risk}` : null,
|
|
7267
|
+
input.intentRevision !== undefined ? `Intent revision: ${input.intentRevision}` : null,
|
|
7268
|
+
input.intentContentHash ? `Intent hash: ${input.intentContentHash}` : null,
|
|
7269
|
+
input.bindingDigest ? `Binding digest: ${input.bindingDigest}` : null
|
|
7270
|
+
].filter(Boolean);
|
|
7271
|
+
return {
|
|
7272
|
+
mode: "form",
|
|
7273
|
+
message: `Authorize this exact Immune-Brain operation?
|
|
7274
|
+
|
|
7275
|
+
${details.join(`
|
|
7276
|
+
`)}`,
|
|
7277
|
+
requestedSchema: { type: "object", properties: {} }
|
|
7278
|
+
};
|
|
7316
7279
|
}
|
|
7317
7280
|
async function serveStdio(options = {}) {
|
|
7318
7281
|
const input = options.input ?? stdin;
|
|
@@ -7323,16 +7286,88 @@ async function serveStdio(options = {}) {
|
|
|
7323
7286
|
});
|
|
7324
7287
|
let buffer = Buffer.alloc(0);
|
|
7325
7288
|
let accepting = true;
|
|
7289
|
+
let requestSequence = 0;
|
|
7326
7290
|
const inFlight = new Set;
|
|
7291
|
+
const pending = new Map;
|
|
7327
7292
|
let chain = Promise.resolve();
|
|
7293
|
+
const rejectPending = (error) => {
|
|
7294
|
+
for (const item of pending.values())
|
|
7295
|
+
item.reject(error);
|
|
7296
|
+
pending.clear();
|
|
7297
|
+
};
|
|
7298
|
+
mcp.bindNativeConfirmation(async (input2) => {
|
|
7299
|
+
if (!accepting)
|
|
7300
|
+
throw new NativeAuthorityError("interaction_not_opened", "MCP connection is closed");
|
|
7301
|
+
if (input2.signal?.aborted)
|
|
7302
|
+
throw new NativeAuthorityError("user_cancelled", "Tool call was cancelled");
|
|
7303
|
+
const requestId = `immune-brain:elicitation:${mcp.connectionId}:${++requestSequence}`;
|
|
7304
|
+
let abortListener;
|
|
7305
|
+
const response = new Promise((resolve7, reject) => {
|
|
7306
|
+
pending.set(requestId, { resolve: resolve7, reject });
|
|
7307
|
+
abortListener = () => {
|
|
7308
|
+
pending.delete(requestId);
|
|
7309
|
+
reject(new NativeAuthorityError("user_cancelled", "Tool call was cancelled"));
|
|
7310
|
+
};
|
|
7311
|
+
input2.signal?.addEventListener("abort", abortListener, { once: true });
|
|
7312
|
+
});
|
|
7313
|
+
try {
|
|
7314
|
+
await writeReply(output, {
|
|
7315
|
+
jsonrpc: "2.0",
|
|
7316
|
+
id: requestId,
|
|
7317
|
+
method: "elicitation/create",
|
|
7318
|
+
params: elicitationParams(input2)
|
|
7319
|
+
});
|
|
7320
|
+
const reply = await response;
|
|
7321
|
+
if (reply.error) {
|
|
7322
|
+
if (reply.error.code === -32601) {
|
|
7323
|
+
throw new NativeAuthorityError("unsupported_host", "Claude Code rejected MCP elicitation/create");
|
|
7324
|
+
}
|
|
7325
|
+
throw new NativeAuthorityError("correlation_missing", `MCP elicitation failed: ${reply.error.message}`);
|
|
7326
|
+
}
|
|
7327
|
+
const result = reply.result;
|
|
7328
|
+
const action = typeof result === "object" && result !== null && !Array.isArray(result) ? result.action : undefined;
|
|
7329
|
+
if (action !== "accept" && action !== "decline" && action !== "cancel") {
|
|
7330
|
+
throw new NativeAuthorityError("correlation_missing", "MCP elicitation returned an invalid action");
|
|
7331
|
+
}
|
|
7332
|
+
if (action === "accept") {
|
|
7333
|
+
const content = result.content;
|
|
7334
|
+
if (typeof content !== "object" || content === null || Array.isArray(content) || Object.keys(content).length !== 0) {
|
|
7335
|
+
throw new NativeAuthorityError("correlation_missing", "MCP elicitation accept content did not match the requested schema");
|
|
7336
|
+
}
|
|
7337
|
+
}
|
|
7338
|
+
return { decision: action, requestId };
|
|
7339
|
+
} finally {
|
|
7340
|
+
pending.delete(requestId);
|
|
7341
|
+
if (abortListener)
|
|
7342
|
+
input2.signal?.removeEventListener("abort", abortListener);
|
|
7343
|
+
}
|
|
7344
|
+
});
|
|
7328
7345
|
const runCall = (parsed) => {
|
|
7329
7346
|
const task = handleJsonRpc(parsed, mcp).then(async (reply) => {
|
|
7330
7347
|
if (reply)
|
|
7331
7348
|
await writeReply(output, reply);
|
|
7349
|
+
}).catch(() => {
|
|
7350
|
+
executeShutdown(1);
|
|
7332
7351
|
});
|
|
7333
7352
|
inFlight.add(task);
|
|
7334
7353
|
task.finally(() => inFlight.delete(task));
|
|
7335
7354
|
};
|
|
7355
|
+
const routeResponse = (obj) => {
|
|
7356
|
+
const id = typeof obj.id === "string" ? obj.id : "";
|
|
7357
|
+
const waiter = id ? pending.get(id) : undefined;
|
|
7358
|
+
if (waiter) {
|
|
7359
|
+
pending.delete(id);
|
|
7360
|
+
const hasResult = Object.hasOwn(obj, "result");
|
|
7361
|
+
const hasError = Object.hasOwn(obj, "error");
|
|
7362
|
+
if (obj.jsonrpc !== "2.0" || obj.method !== undefined || hasResult === hasError) {
|
|
7363
|
+
waiter.reject(new NativeAuthorityError("correlation_missing", "malformed MCP elicitation response"));
|
|
7364
|
+
} else {
|
|
7365
|
+
waiter.resolve(obj);
|
|
7366
|
+
}
|
|
7367
|
+
return true;
|
|
7368
|
+
}
|
|
7369
|
+
return obj.method === undefined && obj.id !== undefined;
|
|
7370
|
+
};
|
|
7336
7371
|
const drainStdio = async () => {
|
|
7337
7372
|
while (true) {
|
|
7338
7373
|
const newline = buffer.indexOf(`
|
|
@@ -7355,10 +7390,14 @@ async function serveStdio(options = {}) {
|
|
|
7355
7390
|
continue;
|
|
7356
7391
|
}
|
|
7357
7392
|
const obj = parsed;
|
|
7393
|
+
if (routeResponse(obj))
|
|
7394
|
+
continue;
|
|
7358
7395
|
if (obj.jsonrpc !== "2.0") {
|
|
7359
7396
|
await writeReply(output, { jsonrpc: "2.0", id: null, error: { code: -32600, message: "Invalid Request: jsonrpc must be '2.0'" } });
|
|
7360
7397
|
continue;
|
|
7361
7398
|
}
|
|
7399
|
+
if (routeResponse(obj))
|
|
7400
|
+
continue;
|
|
7362
7401
|
if (typeof obj.method !== "string" || !obj.method) {
|
|
7363
7402
|
const id = obj.id !== undefined && (typeof obj.id === "string" || typeof obj.id === "number") ? obj.id : null;
|
|
7364
7403
|
await writeReply(output, { jsonrpc: "2.0", id, error: { code: -32600, message: "Invalid Request: missing method" } });
|
|
@@ -7375,11 +7414,15 @@ async function serveStdio(options = {}) {
|
|
|
7375
7414
|
const rpc = parsed;
|
|
7376
7415
|
if (rpc.method === "notifications/cancelled") {
|
|
7377
7416
|
const requestId = rpc.params?.requestId;
|
|
7378
|
-
if (requestId
|
|
7417
|
+
if (typeof requestId === "string" && pending.has(requestId)) {
|
|
7418
|
+
pending.get(requestId)?.reject(new NativeAuthorityError("user_cancelled", "native interaction was cancelled"));
|
|
7419
|
+
pending.delete(requestId);
|
|
7420
|
+
} else if (requestId !== undefined) {
|
|
7379
7421
|
mcp.aborts.get(requestId)?.abort(new Error("notifications/cancelled"));
|
|
7422
|
+
}
|
|
7380
7423
|
continue;
|
|
7381
7424
|
}
|
|
7382
|
-
if (
|
|
7425
|
+
if (rpc.method === "tools/call") {
|
|
7383
7426
|
if (!accepting) {
|
|
7384
7427
|
if (rpc.id !== undefined)
|
|
7385
7428
|
await writeReply(output, { jsonrpc: "2.0", id: rpc.id, error: { code: -32000, message: "stdio closed" } });
|
|
@@ -7393,6 +7436,7 @@ async function serveStdio(options = {}) {
|
|
|
7393
7436
|
await writeReply(output, reply);
|
|
7394
7437
|
}
|
|
7395
7438
|
};
|
|
7439
|
+
let resolveStdio = () => {};
|
|
7396
7440
|
let shutdownPromise = null;
|
|
7397
7441
|
const executeShutdown = (code = 0) => {
|
|
7398
7442
|
if (shutdownPromise)
|
|
@@ -7401,31 +7445,49 @@ async function serveStdio(options = {}) {
|
|
|
7401
7445
|
accepting = false;
|
|
7402
7446
|
for (const ac of mcp.aborts.values())
|
|
7403
7447
|
ac.abort(new Error("stdio closed"));
|
|
7404
|
-
|
|
7448
|
+
rejectPending(new NativeAuthorityError("user_cancelled", "MCP connection closed during native interaction"));
|
|
7449
|
+
await Promise.race([
|
|
7450
|
+
Promise.allSettled([...inFlight]),
|
|
7451
|
+
new Promise((resolve7) => setTimeout(resolve7, 200))
|
|
7452
|
+
]);
|
|
7405
7453
|
await mcp.shutdown();
|
|
7406
7454
|
if (output.writable && typeof output.end === "function") {
|
|
7407
7455
|
await new Promise((cb) => output.end(() => cb()));
|
|
7408
7456
|
}
|
|
7409
7457
|
process.exitCode = code;
|
|
7410
7458
|
exit(code);
|
|
7459
|
+
resolveStdio();
|
|
7411
7460
|
})();
|
|
7412
7461
|
return shutdownPromise;
|
|
7413
7462
|
};
|
|
7414
7463
|
await new Promise((resolve7) => {
|
|
7464
|
+
resolveStdio = resolve7;
|
|
7465
|
+
output.on("error", () => {
|
|
7466
|
+
executeShutdown(1);
|
|
7467
|
+
});
|
|
7468
|
+
output.on("close", () => {
|
|
7469
|
+
executeShutdown(0);
|
|
7470
|
+
});
|
|
7415
7471
|
input.on("data", (chunk) => {
|
|
7472
|
+
if (!accepting)
|
|
7473
|
+
return;
|
|
7416
7474
|
chain = chain.then(async () => {
|
|
7475
|
+
if (!accepting)
|
|
7476
|
+
return;
|
|
7417
7477
|
buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
|
|
7418
7478
|
await drainStdio();
|
|
7479
|
+
}).catch(() => {
|
|
7480
|
+
executeShutdown(1);
|
|
7419
7481
|
});
|
|
7420
7482
|
});
|
|
7421
7483
|
input.on("end", () => {
|
|
7422
|
-
chain.then(() => executeShutdown(0))
|
|
7484
|
+
chain.then(() => executeShutdown(0));
|
|
7423
7485
|
});
|
|
7424
7486
|
input.on("close", () => {
|
|
7425
|
-
chain.then(() => executeShutdown(0))
|
|
7487
|
+
chain.then(() => executeShutdown(0));
|
|
7426
7488
|
});
|
|
7427
7489
|
input.on("error", () => {
|
|
7428
|
-
|
|
7490
|
+
executeShutdown(1);
|
|
7429
7491
|
});
|
|
7430
7492
|
});
|
|
7431
7493
|
}
|
|
@@ -7448,9 +7510,12 @@ if (entry.endsWith("mcp_server.ts") || entry.endsWith("mcp-server.mjs")) {
|
|
|
7448
7510
|
serveStdio();
|
|
7449
7511
|
}
|
|
7450
7512
|
export {
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
handleJsonRpc,
|
|
7513
|
+
supportsElicitationProtocol,
|
|
7514
|
+
serveStdio,
|
|
7454
7515
|
listMcpTools,
|
|
7455
|
-
|
|
7516
|
+
handleJsonRpc,
|
|
7517
|
+
elicitationParams,
|
|
7518
|
+
createMcpRuntime,
|
|
7519
|
+
TOOLS,
|
|
7520
|
+
MCP_PROTOCOL_VERSION
|
|
7456
7521
|
};
|