immune-brain 3.3.0 → 3.5.0

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