immune-brain 3.3.0 → 3.4.0

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