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.
Files changed (32) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +3 -3
  3. package/package.json +5 -3
  4. package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
  5. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +19 -73
  6. package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +1 -2
  7. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +2 -2
  8. package/plugins/immune-brain/dist/BASELINE.md +1 -1
  9. package/plugins/immune-brain/dist/claude/mcp-server.mjs +317 -316
  10. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +1 -1
  11. package/plugins/immune-brain/dist/imm-brainstorm.md +1 -1
  12. package/plugins/immune-brain/dist/imm-loop.md +15 -10
  13. package/plugins/immune-brain/dist/imm-planner.md +22 -17
  14. package/plugins/immune-brain/hooks/hooks.json +0 -10
  15. package/plugins/immune-brain/runtime/assurance/coordinator.ts +0 -8
  16. package/plugins/immune-brain/runtime/claude/capability.ts +4 -10
  17. package/plugins/immune-brain/runtime/claude/interaction.ts +55 -22
  18. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +63 -73
  19. package/plugins/immune-brain/runtime/claude/mcp_server.ts +233 -55
  20. package/plugins/immune-brain/runtime/claude/review_host.ts +2 -138
  21. package/plugins/immune-brain/runtime/kernel/application.ts +0 -1
  22. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +1 -7
  23. package/plugins/immune-brain/runtime/kernel/canary_application.ts +0 -7
  24. package/plugins/immune-brain/runtime/kernel/completion.ts +1 -3
  25. package/plugins/immune-brain/runtime/kernel/reducer.ts +0 -31
  26. package/plugins/immune-brain/runtime/kernel/types.ts +0 -2
  27. package/plugins/immune-brain/runtime/kernel/validation.ts +1 -3
  28. package/plugins/immune-brain/runtime/plugin_version.ts +2 -0
  29. package/plugins/immune-brain/skills/BASELINE.md +1 -1
  30. package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +4 -0
  31. package/plugins/immune-brain/skills/imm-loop/SKILL.md +4 -0
  32. 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.199";
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,57 +38,60 @@ 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
- const rawPermissionMode = env.CLAUDE_CODE_PERMISSION_MODE;
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
 
44
+ // plugins/immune-brain/runtime/plugin_version.ts
45
+ var PLUGIN_VERSION = "3.4.0";
46
+
55
47
  // plugins/immune-brain/runtime/claude/interaction.ts
56
48
  import { createHash, randomUUID } from "node:crypto";
57
49
  var PRIVILEGED_OPERATIONS = [
58
50
  "enroll",
59
51
  "request_authorization",
60
52
  "approve_breaking_intent_revision",
61
- "stop",
62
- "repair_authority_state"
53
+ "stop"
63
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
+ }
64
74
  function isPrivilegedOperation(operation) {
65
75
  return PRIVILEGED_OPERATIONS.includes(operation);
66
76
  }
67
77
  function privilegedAnnotations() {
68
- return {
69
- destructiveHint: true,
70
- "anthropic/requiresUserInteraction": true
71
- };
78
+ return { destructiveHint: true };
72
79
  }
73
80
  function evaluateNativeGate(input) {
74
81
  if (!isPrivilegedOperation(input.operation))
75
82
  return { ok: true };
76
83
  if (!input.interactive)
77
- return { ok: false, reason: "non-interactive execution cannot mint authority" };
78
- const mode = parsePermissionMode(input.permissionMode);
79
- if (!mode)
80
- return { ok: false, reason: `unsupported permission mode ${String(input.permissionMode)}` };
81
- if (mode === "dontAsk")
82
- return { ok: false, reason: "dontAsk cannot mint authority" };
83
- if (!input.requiresUserInteraction) {
84
- return { ok: false, reason: "privileged operation requires anthropic/requiresUserInteraction" };
85
- }
86
- if (input.decision === "deny")
87
- 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") };
88
87
  if (input.decision === "cancel")
89
- return { ok: false, reason: "native interaction cancelled" };
88
+ return { ok: false, error: new NativeAuthorityError("user_cancelled", "native interaction cancelled") };
90
89
  if (input.decision !== "accept")
91
- return { ok: false, reason: "native interaction missing" };
90
+ return { ok: false, error: new NativeAuthorityError("interaction_not_opened", "native interaction returned no decision") };
92
91
  return { ok: true };
93
92
  }
94
93
  function confirmationRef(input) {
95
- return `claude-confirm-${createHash("sha256").update(`${input.sessionId}\x00${input.toolCallId}\x00${input.operation}\x00${input.taskId}\x00${input.intentRevision ?? ""}\x00${input.intentContentHash ?? ""}\x00${input.bindingDigest ?? ""}`).digest("hex").slice(0, 16)}`;
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)}`;
96
95
  }
97
96
  function enrollmentNonce() {
98
97
  return randomUUID();
@@ -109,7 +108,6 @@ var AGENT_TOOL = "Agent";
109
108
 
110
109
  class MemoryHookEventLog {
111
110
  events = [];
112
- consumed = [];
113
111
  append(event) {
114
112
  this.events.push(event);
115
113
  return true;
@@ -130,18 +128,8 @@ class MemoryHookEventLog {
130
128
  this.events.splice(i, 1);
131
129
  }
132
130
  }
133
- consumeElicitation(sessionId, toolCallId) {
134
- this.consumed.push(`${sessionId}\x00${toolCallId}`);
135
- this.events.push({ type: "ElicitationConsumed", sessionId, toolCallId });
136
- return true;
137
- }
138
- consumedKeys() {
139
- return [...this.consumed];
140
- }
141
131
  }
142
132
  var CACHE_DIR = "immune-brain-claude";
143
- var CONSUMED_FILE = "consumed.jsonl";
144
- var CONSUMED_DIR = "consumed";
145
133
  function sessionHash(sessionId) {
146
134
  return createHash2("sha256").update(sessionId).digest("hex");
147
135
  }
@@ -174,27 +162,6 @@ function ensurePrivateDir(dir) {
174
162
  return false;
175
163
  }
176
164
  }
177
- function claimAtomicKey(dir, keyName) {
178
- const claimsDir = join(dir, CONSUMED_DIR);
179
- if (!ensurePrivateDir(claimsDir))
180
- return false;
181
- const claimPath = join(claimsDir, `${keyName}.claim`);
182
- const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW | constants.O_NONBLOCK;
183
- let fd;
184
- try {
185
- fd = openSync(claimPath, flags, 384);
186
- } catch (error) {
187
- return false;
188
- }
189
- try {
190
- const stat = fstatSync(fd);
191
- if (!stat.isFile() || !ownedByUs(stat) || (stat.mode & 511) !== 384)
192
- return false;
193
- return true;
194
- } finally {
195
- closeSync(fd);
196
- }
197
- }
198
165
  function appendPrivate(path, dir, line) {
199
166
  if (!ensurePrivateDir(dir))
200
167
  return false;
@@ -255,7 +222,7 @@ class FileHookEventLog {
255
222
  if (!ensurePrivateDir(dir))
256
223
  return [];
257
224
  try {
258
- return readdirSync(dir).filter((name) => name.endsWith(".jsonl") && name !== CONSUMED_FILE).flatMap((name) => this.readFile(join(dir, name)));
225
+ return readdirSync(dir).filter((name) => name.endsWith(".jsonl")).flatMap((name) => this.readFile(join(dir, name)));
259
226
  } catch {
260
227
  return [];
261
228
  }
@@ -294,36 +261,6 @@ class FileHookEventLog {
294
261
  rmSync(path, { force: true });
295
262
  } catch {}
296
263
  }
297
- consumeElicitation(sessionId, toolCallId) {
298
- const dir = cacheDir(this.root);
299
- const claimKey = createHash2("sha256").update(`${sessionId}\x00${toolCallId}`).digest("hex");
300
- const keyClaimed = claimAtomicKey(dir, claimKey);
301
- if (!keyClaimed)
302
- return false;
303
- if (!appendPrivate(join(dir, CONSUMED_FILE), dir, `${JSON.stringify({ sessionId, toolCallId })}
304
- `)) {
305
- try {
306
- rmSync(join(dir, CONSUMED_DIR, `${claimKey}.claim`), { force: true });
307
- } catch {}
308
- return false;
309
- }
310
- if (!this.consumedKeys().includes(`${sessionId}\x00${toolCallId}`))
311
- return false;
312
- return this.append({ type: "ElicitationConsumed", sessionId, toolCallId });
313
- }
314
- consumedKeys() {
315
- if (!ensurePrivateDir(cacheDir(this.root)))
316
- return [];
317
- const text = readPrivate(join(cacheDir(this.root), CONSUMED_FILE));
318
- if (!text)
319
- return [];
320
- try {
321
- return text.split(`
322
- `).filter(Boolean).map((line) => JSON.parse(line)).map((item) => `${item.sessionId}\x00${item.toolCallId}`);
323
- } catch {
324
- return [];
325
- }
326
- }
327
264
  }
328
265
  function bindsStart(event, pending) {
329
266
  if (event.taskId && event.taskId !== pending.request.taskId)
@@ -347,11 +284,9 @@ class ClaudeReviewHost {
347
284
  host = "claude-code";
348
285
  pending = new Map;
349
286
  appliedBySession = new Map;
350
- consumedElicitations = new Set;
351
287
  constructor(log = new MemoryHookEventLog) {
352
288
  this.log = log;
353
289
  }
354
- confirmations = new Map;
355
290
  prepareReview(request) {
356
291
  const initialCursors = new Map;
357
292
  const sessionCursors = new Map;
@@ -381,8 +316,6 @@ ${request.prompt}`,
381
316
  this.log.append(event);
382
317
  }
383
318
  drain() {
384
- for (const key of this.log.consumedKeys())
385
- this.consumedElicitations.add(key);
386
319
  for (const sessionId of this.log.sessions()) {
387
320
  const events = this.log.list(sessionId);
388
321
  let start = this.appliedBySession.get(sessionId) ?? 0;
@@ -391,34 +324,10 @@ ${request.prompt}`,
391
324
  let ended = false;
392
325
  for (let i = start;i < events.length; i++) {
393
326
  const event = events[i];
394
- if (event.type === "ElicitationResult") {
395
- const key = `${event.sessionId}\x00${event.toolCallId}`;
396
- if (this.consumedElicitations.has(key))
397
- continue;
398
- if (this.confirmations.has(key)) {
399
- if (this.log.consumeElicitation(event.sessionId, event.toolCallId)) {
400
- this.confirmations.delete(key);
401
- this.consumedElicitations.add(key);
402
- }
403
- continue;
404
- }
405
- this.confirmations.set(key, event.decision);
406
- continue;
407
- }
408
- if (event.type === "ElicitationConsumed") {
409
- const key = `${event.sessionId}\x00${event.toolCallId}`;
410
- this.consumedElicitations.add(key);
411
- this.confirmations.delete(key);
412
- continue;
413
- }
414
327
  if (event.type === "SessionEnd") {
415
328
  this.log.clear(event.sessionId);
416
329
  ended = true;
417
330
  this.appliedBySession.delete(event.sessionId);
418
- for (const key of this.confirmations.keys()) {
419
- if (key.startsWith(`${event.sessionId}\x00`))
420
- this.confirmations.delete(key);
421
- }
422
331
  for (const [id, state] of this.pending) {
423
332
  if (state.startEvent?.sessionId === event.sessionId || state.postEvent?.sessionId === event.sessionId || state.stopEvent?.sessionId === event.sessionId) {
424
333
  this.pending.delete(id);
@@ -455,37 +364,6 @@ ${request.prompt}`,
455
364
  }
456
365
  }
457
366
  }
458
- peekConfirmation(sessionId, toolCallId) {
459
- this.drain();
460
- return this.confirmations.has(`${sessionId}\x00${toolCallId}`);
461
- }
462
- takeConfirmation(sessionId, toolCallId) {
463
- this.drain();
464
- const key = `${sessionId}\x00${toolCallId}`;
465
- const decision = this.confirmations.get(key);
466
- if (!decision)
467
- return;
468
- if (!this.log.consumeElicitation(sessionId, toolCallId)) {
469
- this.confirmations.delete(key);
470
- return;
471
- }
472
- this.confirmations.delete(key);
473
- this.consumedElicitations.add(key);
474
- this.drain();
475
- return decision;
476
- }
477
- sessionOfElicitation(toolCallId) {
478
- this.drain();
479
- const sessions = [];
480
- for (const key of this.confirmations.keys()) {
481
- const sep = key.lastIndexOf("\x00");
482
- if (sep >= 0 && key.slice(sep + 1) === toolCallId)
483
- sessions.push(key.slice(0, sep));
484
- }
485
- if (sessions.length !== 1)
486
- return;
487
- return sessions[0];
488
- }
489
367
  applyReviewEvent(event, state) {
490
368
  if (state.error)
491
369
  return;
@@ -618,8 +496,6 @@ function parseHookStdin(raw) {
618
496
  return null;
619
497
  }
620
498
  const hookType = String(payload.hook_event_name ?? payload.type ?? "");
621
- if (hookType === "ElicitationResult" && (typeof payload.session_id !== "string" || !payload.session_id))
622
- return null;
623
499
  const sessionId = String(payload.session_id ?? payload.sessionId ?? process.env.CLAUDE_SESSION_ID ?? "");
624
500
  if (!sessionId)
625
501
  return null;
@@ -711,17 +587,6 @@ function parseHookStdin(raw) {
711
587
  }
712
588
  if (hookType === "SessionEnd")
713
589
  return { type: "SessionEnd", sessionId };
714
- if (hookType === "ElicitationResult") {
715
- const rawToolCallId = payload.tool_use_id ?? payload.toolCallId;
716
- if (typeof rawToolCallId !== "string" || !rawToolCallId)
717
- return null;
718
- const toolCallId = rawToolCallId;
719
- const raw2 = payload.decision ?? payload.result ?? payload.action;
720
- const decision = raw2 === "accept" ? "accept" : raw2 === "deny" ? "deny" : raw2 === "cancel" ? "cancel" : null;
721
- if (!toolCallId || !decision)
722
- return null;
723
- return { type: "ElicitationResult", sessionId, toolCallId, decision };
724
- }
725
590
  return null;
726
591
  }
727
592
 
@@ -1484,8 +1349,6 @@ class AssuranceCoordinator {
1484
1349
  return this.unknownAfterCommit(taskId, "qa", operationId, boundedAssuranceError(error));
1485
1350
  }
1486
1351
  }
1487
- if (projection.projection.next_obligation === "authorize_user")
1488
- return { state: "awaiting_user", operation: "record-user-approval", operation_id: operationId };
1489
1352
  if (projection.projection.next_obligation !== "run_qa" && projection.projection.next_obligation !== "run_review")
1490
1353
  return { state: "blocked", reason: `Kernel requires ${projection.projection.next_obligation}` };
1491
1354
  const qaAlreadySettled = projection.projection.next_obligation === "run_review";
@@ -1583,8 +1446,6 @@ class AssuranceCoordinator {
1583
1446
  return this.unknownAfterCommit(taskId, "qa", operationId, boundedAssuranceError(error));
1584
1447
  }
1585
1448
  }
1586
- if (fresh.projection.next_obligation === "authorize_user")
1587
- return { state: "awaiting_user", operation: "record-user-approval", operation_id: operationId };
1588
1449
  if (fresh.projection.next_obligation !== "run_review") {
1589
1450
  if (authorityCommitted && aborted())
1590
1451
  return this.unknownAfterCommit(taskId, "qa", operationId, "QA settlement projection did not require Review after cancellation");
@@ -1747,8 +1608,6 @@ class AssuranceCoordinator {
1747
1608
  return this.unknownAfterCommit(taskId, "review", reservation.operationId, boundedAssuranceError(error));
1748
1609
  }
1749
1610
  }
1750
- if (settled.projection.next_obligation === "authorize_user")
1751
- return { state: "awaiting_user", operation: "record-user-approval", operation_id: reservation.operationId };
1752
1611
  return { state: "blocked", reason: `Kernel requires ${settled.projection.next_obligation} after Review` };
1753
1612
  }
1754
1613
  abandonReview(taskId, reason) {
@@ -3633,7 +3492,6 @@ var ACTION_V2_TYPES = [
3633
3492
  "record_finding",
3634
3493
  "resolve_finding",
3635
3494
  "record_approval",
3636
- "record_user_approval",
3637
3495
  "revise_intent",
3638
3496
  "approve_breaking_intent_revision",
3639
3497
  "request_rework",
@@ -3696,8 +3554,7 @@ function parseTaskAction(raw) {
3696
3554
  };
3697
3555
  break;
3698
3556
  }
3699
- case "record_approval":
3700
- case "record_user_approval": {
3557
+ case "record_approval": {
3701
3558
  rejectUnknown2(value, [...ACTION_BASE_FIELDS, "approval"], "action", violations);
3702
3559
  const approval = parseApprovalV2(value.approval, 0, violations, true);
3703
3560
  if (approval.review_revision && approval.kind !== "review")
@@ -4904,7 +4761,7 @@ function commitTerminalLocked(root, taskId, transaction, tombstone) {
4904
4761
  var REQUIRED_ATTESTATIONS = {
4905
4762
  routine: ["qa"],
4906
4763
  material: ["qa", "review"],
4907
- critical: ["qa", "review", "user"]
4764
+ critical: ["qa", "review"]
4908
4765
  };
4909
4766
  function archiveActivePlanningPath(path) {
4910
4767
  const matched = path.match(/^docs\/(plans|specs)\/([^/]+)$/);
@@ -5009,8 +4866,6 @@ function projectTask(intent, record, currentDiffHash, currentIntentContentHash,
5009
4866
  nextObligation = "run_qa";
5010
4867
  } else if (decision.missing_approval_kinds.includes("review")) {
5011
4868
  nextObligation = "run_review";
5012
- } else if (decision.missing_approval_kinds.includes("user")) {
5013
- nextObligation = "authorize_user";
5014
4869
  } else if (decision.complete) {
5015
4870
  nextObligation = "complete";
5016
4871
  }
@@ -5036,8 +4891,6 @@ function deriveAssuranceAuthorization(input) {
5036
4891
  state: "none",
5037
4892
  blocked: `resolve-user-decision requires exactly one open user decision; found ${input.open_user_decision_count}`
5038
4893
  };
5039
- if (input.next_obligation === "authorize_user")
5040
- return { state: "record_user_approval", blocked: null };
5041
4894
  return { state: "none", blocked: null };
5042
4895
  }
5043
4896
  function emptyProjection() {
@@ -5288,7 +5141,7 @@ function intentRefMatches(intent, ref) {
5288
5141
  return ref.path === `docs/plans/${intent.task_id}.intent.json` && ref.content_hash === canonicalIntentHash(intent);
5289
5142
  }
5290
5143
  function hasPrivilegedKind(action) {
5291
- return action.type === "record_approval" || action.type === "record_user_approval" || action.type === "approve_breaking_intent_revision" || action.type === "request_rework" || action.type === "stop" || action.type === "resolve_user_decision";
5144
+ return action.type === "record_approval" || action.type === "approve_breaking_intent_revision" || action.type === "request_rework" || action.type === "stop" || action.type === "resolve_user_decision";
5292
5145
  }
5293
5146
  function findingsDigestV2(findings) {
5294
5147
  const normalized = findings.map((finding) => ({
@@ -5440,36 +5293,6 @@ function reduceTask(recordRaw, actionRaw, authorityAudit = null, changedPaths) {
5440
5293
  appendHistory(record, action, from, approval.id, authorityAudit);
5441
5294
  break;
5442
5295
  }
5443
- case "record_user_approval": {
5444
- if (record.lifecycle !== "active" || record.artifact_state !== "frozen")
5445
- throw new KernelInvariantError([
5446
- `cannot record user approval while state is ${stateOf(record)}`
5447
- ]);
5448
- const approval = action.approval;
5449
- if (approval.kind !== "user")
5450
- throw new KernelInvariantError([
5451
- "record_user_approval requires kind user"
5452
- ]);
5453
- if (!authorityAudit || authorityAudit.authority_kind !== "user")
5454
- throw new KernelInvariantError([
5455
- "record_user_approval requires user authority"
5456
- ]);
5457
- if (approval.task_revision !== record.intent_snapshot.revision)
5458
- throw new KernelInvariantError(["approval task_revision must equal the current intent revision"]);
5459
- if (approval.intent_content_hash !== record.intent_ref.content_hash)
5460
- throw new KernelInvariantError(["approval intent_content_hash must equal the current intent hash"]);
5461
- if (approval.diff_hash !== diffHash)
5462
- throw new KernelInvariantError(["approval diff_hash must equal the action diff hash"]);
5463
- if (record.attestations.some((item) => item.id === approval.id))
5464
- throw new KernelInvariantError([
5465
- `attestations contains duplicate id ${approval.id}`
5466
- ]);
5467
- if (approval.review_revision)
5468
- throw new KernelInvariantError(["review_revision is only valid on review approvals"]);
5469
- record.attestations.push({ ...approval, acceptance_results: [] });
5470
- appendHistory(record, action, from, approval.id, authorityAudit);
5471
- break;
5472
- }
5473
5296
  case "revise_intent":
5474
5297
  case "approve_breaking_intent_revision": {
5475
5298
  if (record.lifecycle !== "active")
@@ -5706,7 +5529,7 @@ function applyTaskAction(input) {
5706
5529
  "intent token does not match the committed record intent"
5707
5530
  ]);
5708
5531
  }
5709
- const privileged = action.type === "record_approval" || action.type === "record_user_approval" || action.type === "approve_breaking_intent_revision" || action.type === "request_rework" || action.type === "stop" || action.type === "resolve_user_decision";
5532
+ const privileged = action.type === "record_approval" || action.type === "approve_breaking_intent_revision" || action.type === "request_rework" || action.type === "stop" || action.type === "resolve_user_decision";
5710
5533
  const expectedAuthority = privileged ? {
5711
5534
  task_id,
5712
5535
  action,
@@ -5828,8 +5651,6 @@ function capabilityActionFor(input) {
5828
5651
  switch (input.op) {
5829
5652
  case "record_approval":
5830
5653
  return { ...base, approval: input.approval };
5831
- case "record_user_approval":
5832
- return { ...base, approval: input.approval };
5833
5654
  case "request_rework":
5834
5655
  return { ...base, findings: input.findings };
5835
5656
  case "stop":
@@ -6014,10 +5835,6 @@ function createCanaryApplication(registry) {
6014
5835
  capability = operation.capability;
6015
5836
  action = { ...base, type: "record_approval", approval: operation.approval };
6016
5837
  break;
6017
- case "record_user_approval":
6018
- capability = operation.capability;
6019
- action = { ...base, type: "record_user_approval", approval: operation.approval };
6020
- break;
6021
5838
  case "revise_intent":
6022
5839
  action = {
6023
5840
  ...base,
@@ -6763,6 +6580,10 @@ async function mintCapability(registry, input) {
6763
6580
  };
6764
6581
  return registry.issue(binding);
6765
6582
  }
6583
+ function throwIfCancelled(signal) {
6584
+ if (signal?.aborted)
6585
+ throw new NativeAuthorityError("user_cancelled", "Tool call was cancelled");
6586
+ }
6766
6587
 
6767
6588
  class ClaudeRuntime {
6768
6589
  host;
@@ -6770,8 +6591,7 @@ class ClaudeRuntime {
6770
6591
  cwd;
6771
6592
  env;
6772
6593
  interactive;
6773
- permissionMode;
6774
- decisions;
6594
+ requestConfirmation;
6775
6595
  hostVersion;
6776
6596
  mutationRegistry = null;
6777
6597
  enrollmentRegistry = createEnrollmentAuthorityRegistry();
@@ -6780,8 +6600,7 @@ class ClaudeRuntime {
6780
6600
  this.cwd = options.cwd;
6781
6601
  this.env = options.env ?? process.env;
6782
6602
  this.interactive = options.interactive ?? true;
6783
- this.permissionMode = options.permissionMode ?? "manual";
6784
- this.decisions = options.decisions ?? new Map;
6603
+ this.requestConfirmation = options.requestConfirmation;
6785
6604
  this.host = options.host ?? new ClaudeReviewHost(new FileHookEventLog);
6786
6605
  if (options.ports) {
6787
6606
  this.coordinator = new AssuranceCoordinator({ ...options.ports, host: this.host });
@@ -6795,6 +6614,9 @@ class ClaudeRuntime {
6795
6614
  bindHostVersion(version) {
6796
6615
  this.hostVersion = version || undefined;
6797
6616
  }
6617
+ bindNativeConfirmation(port) {
6618
+ this.requestConfirmation = port;
6619
+ }
6798
6620
  async shutdown() {
6799
6621
  await this.coordinator.onSessionShutdown();
6800
6622
  }
@@ -6830,32 +6652,28 @@ class ClaudeRuntime {
6830
6652
  this.app ??= createCanaryApplication(this.mutationRegistry);
6831
6653
  return { registry: this.mutationRegistry, app: this.app };
6832
6654
  }
6833
- rejectBeforePreparation(operation, meta) {
6834
- const configured = this.decisions.get(operation) ?? meta.decision;
6835
- if (configured === "deny" || configured === "cancel")
6836
- this.gate(operation, meta);
6837
- }
6838
- gate(operation, meta, binding = {}) {
6655
+ async gate(operation, meta, binding = {}) {
6656
+ throwIfCancelled(meta.signal);
6839
6657
  const probe = probeHost(this.env, process.platform, this.hostVersion);
6840
6658
  if (!probe.ok)
6841
- throw new Error(probe.reason);
6842
- const requiresUserInteraction = Boolean(meta.requiresUserInteraction);
6843
- const permissionMode = meta.permissionMode ?? this.permissionMode;
6844
- const configuredDecision = this.decisions.get(operation) ?? meta.decision;
6845
- const decision = configuredDecision ?? (isPrivilegedOperation(operation) && Boolean(meta.interactive ?? this.interactive) && requiresUserInteraction && permissionMode !== "dontAsk" ? this.host.takeConfirmation(meta.sessionId, meta.toolCallId) : undefined);
6846
- const gate = evaluateNativeGate({
6847
- operation,
6848
- permissionMode,
6849
- requiresUserInteraction,
6850
- interactive: meta.interactive ?? this.interactive,
6851
- decision
6852
- });
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 });
6853
6670
  if (!gate.ok)
6854
- throw new Error(gate.reason);
6671
+ throw gate.error;
6855
6672
  return {
6856
6673
  confirmation_ref: confirmationRef({
6857
- sessionId: meta.sessionId,
6674
+ connectionId: meta.sessionId,
6858
6675
  toolCallId: meta.toolCallId,
6676
+ requestId: result.requestId,
6859
6677
  operation,
6860
6678
  taskId: meta.taskId,
6861
6679
  ...binding
@@ -6866,19 +6684,21 @@ class ClaudeRuntime {
6866
6684
  return projectAssurance(this.cwd, taskId, diffSnapshotOf);
6867
6685
  }
6868
6686
  async enroll(taskId, meta) {
6869
- this.rejectBeforePreparation("enroll", { ...meta, taskId });
6870
6687
  const now = new Date().toISOString();
6871
6688
  const preparation = await preparePiCanary(this.cwd, { task_id: taskId, now });
6872
- const gate = this.gate("enroll", { ...meta, taskId }, {
6689
+ const intent = await readTaskIntent(this.cwd, taskId);
6690
+ const gate = await this.gate("enroll", { ...meta, taskId }, {
6691
+ risk: intent.intent.risk,
6873
6692
  intentRevision: preparation.intent?.revision,
6874
6693
  intentContentHash: preparation.intent?.content_hash,
6875
6694
  bindingDigest: preparation.digest
6876
6695
  });
6877
6696
  const { unchanged } = await revalidatePiCanary(this.cwd, { task_id: taskId, now }, preparation);
6878
6697
  if (!unchanged)
6879
- throw new Error("Workspace changed after confirmation; enrollment aborted before authority");
6698
+ throw new NativeAuthorityError("workspace_changed", "workspace changed after native confirmation");
6880
6699
  if (!preparation.intent)
6881
6700
  throw new Error("enrollment requires a readable TaskIntent");
6701
+ throwIfCancelled(meta.signal);
6882
6702
  const nonce = enrollmentNonce();
6883
6703
  const binding = {
6884
6704
  task_id: taskId,
@@ -6914,22 +6734,16 @@ class ClaudeRuntime {
6914
6734
  return submitClaudeReview(this.host, this.coordinator, { cwd: this.cwd }, taskId, verdictInput);
6915
6735
  }
6916
6736
  async authorize(taskId, operation, meta, extra = {}) {
6917
- if (!isPrivilegedOperation(operation) && operation !== "request_authorization")
6918
- throw new Error(`unsupported privileged operation ${operation}`);
6919
- this.rejectBeforePreparation(operation, { ...meta, taskId });
6920
6737
  if (operation === "repair_authority_state") {
6921
6738
  const authority = reconcileKernelAuthority(this.cwd, taskId);
6922
6739
  if (authority.state !== "repairable_stale_claim" || authority.owner_task_id !== taskId) {
6923
6740
  throw new Error(authority.diagnostic ?? "authority repair requires a repairable stale claim");
6924
6741
  }
6925
- const gate2 = this.gate(operation, { ...meta, taskId }, { bindingDigest: `repair:${authority.revision}` });
6926
- const current = reconcileKernelAuthority(this.cwd, taskId);
6927
- if (current.state !== authority.state || current.owner_task_id !== authority.owner_task_id || current.revision !== authority.revision) {
6928
- throw new Error("authority changed after native confirmation; repair aborted before capability issuance");
6929
- }
6930
- return repairKernelAuthority(this.cwd, taskId, current.revision);
6742
+ return repairKernelAuthority(this.cwd, taskId, authority.revision);
6931
6743
  }
6932
- let op = operation === "request_authorization" ? "record_user_approval" : operation;
6744
+ if (!isPrivilegedOperation(operation) && operation !== "request_authorization")
6745
+ throw new Error(`unsupported privileged operation ${operation}`);
6746
+ let op = operation;
6933
6747
  let decisionOp;
6934
6748
  const projection = await this.status(taskId);
6935
6749
  if (projection.error || !projection.claim)
@@ -6943,7 +6757,7 @@ class ClaudeRuntime {
6943
6757
  throw new Error(`resolve-user-decision requires exactly one open user decision; found ${open.length}`);
6944
6758
  op = "resolve_user_decision";
6945
6759
  decisionOp = { finding_id: open[0].id, resolution: `resume after literal-user decision: ${open[0].summary}` };
6946
- } else if (readiness.state !== "record_user_approval") {
6760
+ } else {
6947
6761
  throw new Error(readiness.blocked ?? "no unique host-derived authorization operation");
6948
6762
  }
6949
6763
  }
@@ -6985,16 +6799,22 @@ class ClaudeRuntime {
6985
6799
  `);
6986
6800
  execFileSync4("git", ["add", "--", priorIntent.intent_ref.path], { cwd: this.cwd, stdio: ["ignore", "pipe", "pipe"] });
6987
6801
  const preparedRecord = await readTaskRecord(this.cwd, taskId);
6988
- if (!preparedRecord.record)
6989
- throw new Error("TaskRecord changed before the breaking revision digest");
6802
+ if (!preparedRecord.record) {
6803
+ throw new NativeAuthorityError("workspace_changed", "TaskRecord changed before the breaking revision digest");
6804
+ }
6990
6805
  preparedDiffHash = diffHashOf(this.cwd, preparedRecord.record);
6991
6806
  }
6992
6807
  const preparedProjection = await this.status(taskId);
6993
- assertProjectionBinding(projection, preparedProjection, Boolean(nextIntent));
6994
- if (preparedProjection.projection.diff_hash !== preparedDiffHash) {
6995
- throw new Error("Workspace changed while preparing the authority digest");
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));
6996
6815
  }
6997
- gate = this.gate(operation, { ...meta, taskId }, {
6816
+ gate = await this.gate(operation, { ...meta, taskId }, {
6817
+ risk: projection.projection.risk,
6998
6818
  intentRevision: nextIntent?.revision ?? projection.projection.intent_revision,
6999
6819
  intentContentHash: nextIntentHash ?? projection.projection.intent_content_hash,
7000
6820
  bindingDigest: `${preparedDiffHash}:${nextIntentHash ?? ""}`
@@ -7010,23 +6830,18 @@ class ClaudeRuntime {
7010
6830
  }
7011
6831
  const { registry, app } = this.authority();
7012
6832
  const confirmation = gate.confirmation_ref;
7013
- const approval = op === "record_user_approval" ? {
7014
- id: `approval-user-${randomUUID5().slice(0, 8)}`,
7015
- kind: "user",
7016
- authority_role: "user",
7017
- task_revision: projection.projection.intent_revision,
7018
- intent_content_hash: projection.projection.intent_content_hash,
7019
- diff_hash: projection.projection.diff_hash,
7020
- actor_id: actorId,
7021
- summary: "literal user approval"
7022
- } : undefined;
7023
6833
  try {
7024
6834
  const capabilityProjection = await this.status(taskId);
7025
- assertProjectionBinding(projection, capabilityProjection, Boolean(nextIntent));
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
+ }
7026
6840
  const operationDiffHash = capabilityProjection.projection.diff_hash;
7027
6841
  if (nextIntent && operationDiffHash !== preparedDiffHash) {
7028
- throw new Error("Workspace changed after native confirmation; authority aborted before capability issuance");
6842
+ throw new NativeAuthorityError("workspace_changed", "workspace changed after native confirmation");
7029
6843
  }
6844
+ throwIfCancelled(meta.signal);
7030
6845
  const capability = await mintCapability(registry, {
7031
6846
  authority_kind: "user",
7032
6847
  task_id: taskId,
@@ -7038,11 +6853,11 @@ class ClaudeRuntime {
7038
6853
  actor_id: actorId,
7039
6854
  now,
7040
6855
  confirmation_ref: confirmation,
7041
- ...approval ? { approval } : {},
7042
6856
  ...op === "approve_breaking_intent_revision" ? { next_intent: nextIntent, next_intent_ref: nextIntentRef } : {},
7043
6857
  ...op === "resolve_user_decision" && decisionOp ? decisionOp : {},
7044
6858
  ...op === "stop" ? { reason: extra.reason ?? "user stop" } : {}
7045
6859
  });
6860
+ throwIfCancelled(meta.signal);
7046
6861
  const result = app.execute({
7047
6862
  root: this.cwd,
7048
6863
  task_id: taskId,
@@ -7050,7 +6865,6 @@ class ClaudeRuntime {
7050
6865
  op,
7051
6866
  capability,
7052
6867
  actor_id: actorId,
7053
- ...approval ? { approval } : {},
7054
6868
  ...op === "approve_breaking_intent_revision" ? { next_intent: nextIntent, next_intent_ref: nextIntentRef } : {},
7055
6869
  ...op === "resolve_user_decision" && decisionOp ? decisionOp : {},
7056
6870
  ...op === "stop" ? { reason: extra.reason ?? "user stop" } : {}
@@ -7186,6 +7000,7 @@ class ClaudeRuntime {
7186
7000
  }
7187
7001
 
7188
7002
  // plugins/immune-brain/runtime/claude/mcp_server.ts
7003
+ var MCP_PROTOCOL_VERSION = "2025-06-18";
7189
7004
  var TOOLS = [
7190
7005
  { name: "status", description: "Read the Kernel Assurance Projection for an exact task.", privileged: false },
7191
7006
  { name: "enroll", description: "Enroll a Git-tracked TaskIntent after native confirmation.", privileged: true },
@@ -7194,7 +7009,7 @@ var TOOLS = [
7194
7009
  { name: "request_authorization", description: "Apply exact literal-user authorization.", privileged: true },
7195
7010
  { name: "approve_breaking_intent_revision", description: "Approve a breaking TaskIntent revision.", privileged: true },
7196
7011
  { name: "stop", description: "Stop the active task with literal-user authority.", privileged: true },
7197
- { name: "repair_authority_state", description: "Repair a recoverable stale backend claim.", privileged: true }
7012
+ { name: "repair_authority_state", description: "Repair a proven recoverable stale backend claim.", privileged: false }
7198
7013
  ];
7199
7014
  function listMcpTools() {
7200
7015
  return TOOLS.map((tool) => ({
@@ -7213,6 +7028,9 @@ function listMcpTools() {
7213
7028
  annotations: tool.privileged ? privilegedAnnotations() : { readOnlyHint: tool.name === "status" }
7214
7029
  }));
7215
7030
  }
7031
+ function supportsElicitationProtocol(value) {
7032
+ return typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value) && value >= MCP_PROTOCOL_VERSION;
7033
+ }
7216
7034
  function createMcpRuntime(options = {}) {
7217
7035
  const host = options.host ?? new ClaudeReviewHost(new FileHookEventLog);
7218
7036
  const runtime = new ClaudeRuntime({
@@ -7221,19 +7039,24 @@ function createMcpRuntime(options = {}) {
7221
7039
  host,
7222
7040
  ports: options.ports,
7223
7041
  interactive: options.interactive,
7224
- decisions: options.decisions
7042
+ requestConfirmation: options.requestConfirmation
7225
7043
  });
7226
7044
  let negotiatedVersion;
7227
7045
  let negotiatedInteractive = false;
7046
+ const connectionId = randomUUID6();
7228
7047
  return {
7229
7048
  runtime,
7230
7049
  host,
7050
+ connectionId,
7231
7051
  listTools: listMcpTools,
7232
7052
  bindClientHandshake(identity) {
7233
7053
  negotiatedVersion = identity.version || undefined;
7234
- negotiatedInteractive = identity.interactive;
7054
+ negotiatedInteractive = identity.interactive && supportsElicitationProtocol(identity.protocolVersion);
7235
7055
  runtime.bindHostVersion(negotiatedVersion);
7236
7056
  },
7057
+ bindNativeConfirmation(port) {
7058
+ runtime.bindNativeConfirmation(port);
7059
+ },
7237
7060
  sessionInteractive: () => negotiatedInteractive,
7238
7061
  async callTool(name, args, meta = {}) {
7239
7062
  const taskId = String(args.task_id ?? "");
@@ -7242,22 +7065,21 @@ function createMcpRuntime(options = {}) {
7242
7065
  if ("native_decision" in args)
7243
7066
  throw new Error("native_decision cannot be supplied in tool arguments");
7244
7067
  if (name === "status")
7245
- return runtime.status(taskId);
7068
+ return { plugin_version: PLUGIN_VERSION, ...await runtime.status(taskId) };
7246
7069
  if (!negotiatedVersion)
7247
- throw new Error("Claude Code version is unavailable");
7248
- if (!negotiatedInteractive)
7249
- throw new Error("non-interactive host session cannot execute authority tools");
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
+ }
7250
7074
  const probe = probeHost(options.env ?? process.env, process.platform, negotiatedVersion);
7251
7075
  if (!probe.ok)
7252
- throw new Error(probe.reason);
7076
+ throw new NativeAuthorityError("unsupported_host", probe.reason);
7253
7077
  const toolMeta = {
7254
- sessionId: meta.sessionId ?? "session",
7078
+ sessionId: meta.sessionId ?? connectionId,
7255
7079
  toolCallId: meta.toolCallId ?? `call-${name}`,
7256
7080
  taskId,
7257
- requiresUserInteraction: meta.requiresUserInteraction ?? isPrivilegedOperation(name),
7258
- permissionMode: meta.permissionMode ?? probe.permissionMode,
7259
- interactive: meta.interactive ?? options.interactive ?? false,
7260
- decision: meta.decision
7081
+ interactive: meta.interactive ?? options.interactive ?? negotiatedInteractive,
7082
+ signal: meta.signal
7261
7083
  };
7262
7084
  if (name === "enroll")
7263
7085
  return runtime.enroll(taskId, toolMeta);
@@ -7274,19 +7096,25 @@ function createMcpRuntime(options = {}) {
7274
7096
  throw new Error(`unknown tool ${name}`);
7275
7097
  },
7276
7098
  observe: (event) => host.observe(event),
7277
- sessionOfElicitation: (toolCallId) => host.sessionOfElicitation(toolCallId),
7278
7099
  shutdown: () => runtime.shutdown(),
7279
7100
  aborts: new Map
7280
7101
  };
7281
7102
  }
7282
- function hostCallIdentity(meta, resolveSession) {
7103
+ function hostCallIdentity(meta) {
7283
7104
  if (!meta)
7284
7105
  return;
7285
7106
  const toolCallId = meta["claudecode/toolUseId"];
7286
- if (typeof toolCallId !== "string" || !toolCallId)
7287
- return;
7288
- const sessionId = resolveSession?.(toolCallId);
7289
- return sessionId ? { sessionId, toolCallId } : undefined;
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) };
7290
7118
  }
7291
7119
  function encodeMessage(message) {
7292
7120
  return Buffer.from(`${JSON.stringify(message)}
@@ -7299,14 +7127,20 @@ async function handleJsonRpc(message, mcp = createMcpRuntime()) {
7299
7127
  const elicitation = params.capabilities?.elicitation;
7300
7128
  mcp.bindClientHandshake({
7301
7129
  version: trustedClient && typeof params.clientInfo?.version === "string" ? params.clientInfo.version : "",
7130
+ protocolVersion: typeof params.protocolVersion === "string" ? params.protocolVersion : undefined,
7302
7131
  interactive: trustedClient && elicitation !== null && typeof elicitation === "object" && !Array.isArray(elicitation)
7303
7132
  });
7304
7133
  return {
7305
7134
  jsonrpc: "2.0",
7306
7135
  id: message.id ?? null,
7307
7136
  result: {
7308
- protocolVersion: "2024-11-05",
7309
- serverInfo: { name: HOST_ID, version: MIN_CLAUDE_CODE_VERSION, contract: CORE_CONTRACT },
7137
+ protocolVersion: MCP_PROTOCOL_VERSION,
7138
+ serverInfo: {
7139
+ name: HOST_ID,
7140
+ version: PLUGIN_VERSION,
7141
+ contract: CORE_CONTRACT,
7142
+ minimumHostVersion: MIN_CLAUDE_CODE_VERSION
7143
+ },
7310
7144
  capabilities: { tools: {} }
7311
7145
  }
7312
7146
  };
@@ -7331,13 +7165,13 @@ async function handleJsonRpc(message, mcp = createMcpRuntime()) {
7331
7165
  const name = String(params.name ?? "");
7332
7166
  const interactive = mcp.sessionInteractive();
7333
7167
  if (isPrivilegedOperation(name) && !interactive) {
7334
- throw new Error("non-interactive host session cannot mint authority");
7168
+ throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
7335
7169
  }
7336
- const identity = hostCallIdentity(params._meta, (toolCallId2) => mcp.sessionOfElicitation(toolCallId2));
7170
+ const identity = hostCallIdentity(params._meta);
7337
7171
  if (isPrivilegedOperation(name) && !identity) {
7338
- throw new Error("host correlation metadata missing");
7172
+ throw new NativeAuthorityError("correlation_missing", "canonical claudecode/toolUseId metadata is missing");
7339
7173
  }
7340
- const sessionId = identity?.sessionId ?? "stdio";
7174
+ const sessionId = mcp.connectionId;
7341
7175
  const toolCallId = identity?.toolCallId ?? String(message.id ?? "stdio");
7342
7176
  const result = await mcp.callTool(name, params.arguments ?? {}, {
7343
7177
  sessionId,
@@ -7352,11 +7186,10 @@ async function handleJsonRpc(message, mcp = createMcpRuntime()) {
7352
7186
  result: { content: [{ type: "text", text: JSON.stringify(result) }] }
7353
7187
  };
7354
7188
  } catch (error) {
7355
- const reason = error instanceof Error ? error.message : String(error);
7356
7189
  return {
7357
7190
  jsonrpc: "2.0",
7358
7191
  id: message.id ?? null,
7359
- error: { code: -32000, message: reason }
7192
+ error: rpcError(error)
7360
7193
  };
7361
7194
  } finally {
7362
7195
  mcp.aborts.delete(requestId);
@@ -7368,9 +7201,75 @@ async function handleJsonRpc(message, mcp = createMcpRuntime()) {
7368
7201
  }
7369
7202
  async function writeReply(output, reply) {
7370
7203
  const payload = encodeMessage(reply);
7371
- if (output.write(payload))
7372
- return;
7373
- await new Promise((resolve7) => output.once("drain", resolve7));
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
+ };
7374
7273
  }
7375
7274
  async function serveStdio(options = {}) {
7376
7275
  const input = options.input ?? stdin;
@@ -7381,16 +7280,88 @@ async function serveStdio(options = {}) {
7381
7280
  });
7382
7281
  let buffer = Buffer.alloc(0);
7383
7282
  let accepting = true;
7283
+ let requestSequence = 0;
7384
7284
  const inFlight = new Set;
7285
+ const pending = new Map;
7385
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
+ });
7386
7339
  const runCall = (parsed) => {
7387
7340
  const task = handleJsonRpc(parsed, mcp).then(async (reply) => {
7388
7341
  if (reply)
7389
7342
  await writeReply(output, reply);
7343
+ }).catch(() => {
7344
+ executeShutdown(1);
7390
7345
  });
7391
7346
  inFlight.add(task);
7392
7347
  task.finally(() => inFlight.delete(task));
7393
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
+ };
7394
7365
  const drainStdio = async () => {
7395
7366
  while (true) {
7396
7367
  const newline = buffer.indexOf(`
@@ -7413,10 +7384,14 @@ async function serveStdio(options = {}) {
7413
7384
  continue;
7414
7385
  }
7415
7386
  const obj = parsed;
7387
+ if (routeResponse(obj))
7388
+ continue;
7416
7389
  if (obj.jsonrpc !== "2.0") {
7417
7390
  await writeReply(output, { jsonrpc: "2.0", id: null, error: { code: -32600, message: "Invalid Request: jsonrpc must be '2.0'" } });
7418
7391
  continue;
7419
7392
  }
7393
+ if (routeResponse(obj))
7394
+ continue;
7420
7395
  if (typeof obj.method !== "string" || !obj.method) {
7421
7396
  const id = obj.id !== undefined && (typeof obj.id === "string" || typeof obj.id === "number") ? obj.id : null;
7422
7397
  await writeReply(output, { jsonrpc: "2.0", id, error: { code: -32600, message: "Invalid Request: missing method" } });
@@ -7433,11 +7408,15 @@ async function serveStdio(options = {}) {
7433
7408
  const rpc = parsed;
7434
7409
  if (rpc.method === "notifications/cancelled") {
7435
7410
  const requestId = rpc.params?.requestId;
7436
- if (requestId !== undefined)
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) {
7437
7415
  mcp.aborts.get(requestId)?.abort(new Error("notifications/cancelled"));
7416
+ }
7438
7417
  continue;
7439
7418
  }
7440
- if (parsed.method === "tools/call") {
7419
+ if (rpc.method === "tools/call") {
7441
7420
  if (!accepting) {
7442
7421
  if (rpc.id !== undefined)
7443
7422
  await writeReply(output, { jsonrpc: "2.0", id: rpc.id, error: { code: -32000, message: "stdio closed" } });
@@ -7451,6 +7430,7 @@ async function serveStdio(options = {}) {
7451
7430
  await writeReply(output, reply);
7452
7431
  }
7453
7432
  };
7433
+ let resolveStdio = () => {};
7454
7434
  let shutdownPromise = null;
7455
7435
  const executeShutdown = (code = 0) => {
7456
7436
  if (shutdownPromise)
@@ -7459,31 +7439,49 @@ async function serveStdio(options = {}) {
7459
7439
  accepting = false;
7460
7440
  for (const ac of mcp.aborts.values())
7461
7441
  ac.abort(new Error("stdio closed"));
7462
- await Promise.allSettled([...inFlight]);
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
+ ]);
7463
7447
  await mcp.shutdown();
7464
7448
  if (output.writable && typeof output.end === "function") {
7465
7449
  await new Promise((cb) => output.end(() => cb()));
7466
7450
  }
7467
7451
  process.exitCode = code;
7468
7452
  exit(code);
7453
+ resolveStdio();
7469
7454
  })();
7470
7455
  return shutdownPromise;
7471
7456
  };
7472
7457
  await new Promise((resolve7) => {
7458
+ resolveStdio = resolve7;
7459
+ output.on("error", () => {
7460
+ executeShutdown(1);
7461
+ });
7462
+ output.on("close", () => {
7463
+ executeShutdown(0);
7464
+ });
7473
7465
  input.on("data", (chunk) => {
7466
+ if (!accepting)
7467
+ return;
7474
7468
  chain = chain.then(async () => {
7469
+ if (!accepting)
7470
+ return;
7475
7471
  buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
7476
7472
  await drainStdio();
7473
+ }).catch(() => {
7474
+ executeShutdown(1);
7477
7475
  });
7478
7476
  });
7479
7477
  input.on("end", () => {
7480
- chain.then(() => executeShutdown(0)).finally(resolve7);
7478
+ chain.then(() => executeShutdown(0));
7481
7479
  });
7482
7480
  input.on("close", () => {
7483
- chain.then(() => executeShutdown(0)).finally(resolve7);
7481
+ chain.then(() => executeShutdown(0));
7484
7482
  });
7485
7483
  input.on("error", () => {
7486
- chain.then(() => executeShutdown(1)).finally(resolve7);
7484
+ executeShutdown(1);
7487
7485
  });
7488
7486
  });
7489
7487
  }
@@ -7506,9 +7504,12 @@ if (entry.endsWith("mcp_server.ts") || entry.endsWith("mcp-server.mjs")) {
7506
7504
  serveStdio();
7507
7505
  }
7508
7506
  export {
7507
+ supportsElicitationProtocol,
7509
7508
  serveStdio,
7510
7509
  listMcpTools,
7511
7510
  handleJsonRpc,
7511
+ elicitationParams,
7512
7512
  createMcpRuntime,
7513
- TOOLS
7513
+ TOOLS,
7514
+ MCP_PROTOCOL_VERSION
7514
7515
  };