agent-embassy 1.7.0 → 1.8.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 (118) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/CONTRIBUTING.md +51 -47
  3. package/README.md +26 -9
  4. package/README.zh-CN.md +22 -9
  5. package/SECURITY.md +48 -35
  6. package/dist/src/gateway/acp-client.d.ts +12 -19
  7. package/dist/src/gateway/acp-client.js +90 -65
  8. package/dist/src/gateway/acp-client.js.map +1 -1
  9. package/dist/src/gateway/acp-provider.d.ts +11 -10
  10. package/dist/src/gateway/acp-provider.js +165 -53
  11. package/dist/src/gateway/acp-provider.js.map +1 -1
  12. package/dist/src/gateway/claude-helper-client.d.ts +2 -43
  13. package/dist/src/gateway/claude-helper-client.js +1 -211
  14. package/dist/src/gateway/claude-helper-client.js.map +1 -1
  15. package/dist/src/gateway/claude-helper-protocol.d.ts +16 -74
  16. package/dist/src/gateway/claude-helper-protocol.js +99 -378
  17. package/dist/src/gateway/claude-helper-protocol.js.map +1 -1
  18. package/dist/src/gateway/claude-helper-supervisor.d.ts +56 -44
  19. package/dist/src/gateway/claude-helper-supervisor.js +265 -456
  20. package/dist/src/gateway/claude-helper-supervisor.js.map +1 -1
  21. package/dist/src/gateway/claude-helper.js +184 -210
  22. package/dist/src/gateway/claude-helper.js.map +1 -1
  23. package/dist/src/gateway/claude-peer.d.ts +20 -87
  24. package/dist/src/gateway/claude-peer.js +310 -1009
  25. package/dist/src/gateway/claude-peer.js.map +1 -1
  26. package/dist/src/gateway/claude-runtime.d.ts +13 -15
  27. package/dist/src/gateway/claude-runtime.js +14 -168
  28. package/dist/src/gateway/claude-runtime.js.map +1 -1
  29. package/dist/src/gateway/cli-copy.en.d.ts +2 -2
  30. package/dist/src/gateway/cli-copy.en.js +4 -2
  31. package/dist/src/gateway/cli-copy.en.js.map +1 -1
  32. package/dist/src/gateway/cli-copy.zh-CN.d.ts +2 -2
  33. package/dist/src/gateway/cli-copy.zh-CN.js +4 -2
  34. package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
  35. package/dist/src/gateway/cli.d.ts +7 -2
  36. package/dist/src/gateway/cli.js +277 -575
  37. package/dist/src/gateway/cli.js.map +1 -1
  38. package/dist/src/gateway/codex-app-server.d.ts +5 -227
  39. package/dist/src/gateway/codex-app-server.js +18 -1463
  40. package/dist/src/gateway/codex-app-server.js.map +1 -1
  41. package/dist/src/gateway/codex-doctor.d.ts +36 -0
  42. package/dist/src/gateway/codex-doctor.js +127 -0
  43. package/dist/src/gateway/codex-doctor.js.map +1 -0
  44. package/dist/src/gateway/codex-local-transport.d.ts +6 -6
  45. package/dist/src/gateway/codex-local-transport.js +13 -10
  46. package/dist/src/gateway/codex-local-transport.js.map +1 -1
  47. package/dist/src/gateway/codex-stateless-transport.d.ts +114 -0
  48. package/dist/src/gateway/codex-stateless-transport.js +1108 -0
  49. package/dist/src/gateway/codex-stateless-transport.js.map +1 -0
  50. package/dist/src/gateway/config.d.ts +2 -9
  51. package/dist/src/gateway/config.js +62 -100
  52. package/dist/src/gateway/config.js.map +1 -1
  53. package/dist/src/gateway/control.d.ts +36 -90
  54. package/dist/src/gateway/control.js +465 -1123
  55. package/dist/src/gateway/control.js.map +1 -1
  56. package/dist/src/gateway/dashboard-copy.d.ts +1 -1
  57. package/dist/src/gateway/dashboard-copy.en.d.ts +8 -26
  58. package/dist/src/gateway/dashboard-copy.en.js +19 -37
  59. package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
  60. package/dist/src/gateway/dashboard-copy.js +8 -26
  61. package/dist/src/gateway/dashboard-copy.js.map +1 -1
  62. package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +8 -26
  63. package/dist/src/gateway/dashboard-copy.zh-CN.js +19 -37
  64. package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
  65. package/dist/src/gateway/dashboard-model.d.ts +8 -22
  66. package/dist/src/gateway/dashboard-model.js +44 -130
  67. package/dist/src/gateway/dashboard-model.js.map +1 -1
  68. package/dist/src/gateway/dashboard.d.ts +0 -6
  69. package/dist/src/gateway/dashboard.js +3 -7
  70. package/dist/src/gateway/dashboard.js.map +1 -1
  71. package/dist/src/gateway/live-dashboard-app/app.js +20 -34
  72. package/dist/src/gateway/live-dashboard-command.js +2 -2
  73. package/dist/src/gateway/live-dashboard-command.js.map +1 -1
  74. package/dist/src/gateway/live-dashboard-http.d.ts +1 -1
  75. package/dist/src/gateway/live-dashboard-http.js +2 -2
  76. package/dist/src/gateway/live-dashboard-http.js.map +1 -1
  77. package/dist/src/gateway/progress-watch-machine.d.ts +1 -37
  78. package/dist/src/gateway/progress-watch-machine.js +4 -15
  79. package/dist/src/gateway/progress-watch-machine.js.map +1 -1
  80. package/dist/src/gateway/providers.d.ts +58 -191
  81. package/dist/src/gateway/providers.js +637 -2434
  82. package/dist/src/gateway/providers.js.map +1 -1
  83. package/dist/src/gateway/server.d.ts +16 -25
  84. package/dist/src/gateway/server.js +165 -240
  85. package/dist/src/gateway/server.js.map +1 -1
  86. package/dist/src/gateway/service.d.ts +149 -427
  87. package/dist/src/gateway/service.js +1639 -5662
  88. package/dist/src/gateway/service.js.map +1 -1
  89. package/dist/src/gateway/state-v2-to-v3.d.ts +23 -0
  90. package/dist/src/gateway/state-v2-to-v3.js +994 -0
  91. package/dist/src/gateway/state-v2-to-v3.js.map +1 -0
  92. package/dist/src/gateway/store.d.ts +54 -309
  93. package/dist/src/gateway/store.js +1560 -3510
  94. package/dist/src/gateway/store.js.map +1 -1
  95. package/dist/src/gateway/types.d.ts +181 -225
  96. package/dist/src/gateway/types.js +115 -251
  97. package/dist/src/gateway/types.js.map +1 -1
  98. package/docs/CONFIGURATION.md +22 -9
  99. package/docs/CONFIGURATION.zh-CN.md +21 -9
  100. package/docs/DASHBOARD.md +10 -10
  101. package/docs/DASHBOARD.zh-CN.md +3 -3
  102. package/docs/DELIVERY.md +4 -4
  103. package/docs/DELIVERY.zh-CN.md +4 -4
  104. package/docs/GATEWAY-ARCHITECTURE.md +158 -187
  105. package/package.json +1 -1
  106. package/skills/embassy-peer/SKILL.md +28 -22
  107. package/dist/src/gateway/codex-registration-generation.d.ts +0 -5
  108. package/dist/src/gateway/codex-registration-generation.js +0 -19
  109. package/dist/src/gateway/codex-registration-generation.js.map +0 -1
  110. package/dist/src/gateway/codex-registration-succession.d.ts +0 -209
  111. package/dist/src/gateway/codex-registration-succession.js +0 -588
  112. package/dist/src/gateway/codex-registration-succession.js.map +0 -1
  113. package/dist/src/gateway/compatibility.d.ts +0 -22
  114. package/dist/src/gateway/compatibility.js +0 -23
  115. package/dist/src/gateway/compatibility.js.map +0 -1
  116. package/dist/src/gateway/delivery-machine.d.ts +0 -235
  117. package/dist/src/gateway/delivery-machine.js +0 -540
  118. package/dist/src/gateway/delivery-machine.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { execFile } from "node:child_process";
2
- import { randomUUID } from "node:crypto";
2
+ import { createHash, randomUUID } from "node:crypto";
3
3
  import { chmod, constants as fsConstants, lstat, open, opendir, realpath, rename, unlink, writeFile, } from "node:fs/promises";
4
4
  import net from "node:net";
5
5
  import os from "node:os";
@@ -7,23 +7,13 @@ import path from "node:path";
7
7
  import { TextDecoder } from "node:util";
8
8
  import { BridgeError } from "../errors.js";
9
9
  import { KeyedMutex } from "../mutex.js";
10
- import { createCodexRegistrationGeneration, isCodexRegistrationGeneration, } from "./codex-registration-generation.js";
11
10
  import { isDashboardLocale } from "./locale.js";
12
- import { isCompatibilityVersionEvidence } from "./compatibility.js";
13
- /**
14
- * This adapter intentionally pins the inspected, implementation-specific
15
- * local peer boundary. The feature is documented in Claude Code, but its
16
- * registry and NDJSON formats are not a stable public integration contract.
17
- */
18
- export const CLAUDE_PEER_COMPATIBILITY = Object.freeze({
19
- claudeCodeVersion: "2.1.227",
20
- peerProtocol: 1,
21
- messageVersion: 1,
22
- });
11
+ export const CLAUDE_PEER_COMPATIBILITY = Object.freeze({ peerProtocol: 1 });
23
12
  const EMBASSY_ADVERTISEMENT_VERSION = 1;
24
13
  const EMBASSY_SOURCE_NAME_PATTERN = /^(?:codex-|dsh-|grok-)/;
25
14
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
26
15
  const ALIAS_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
16
+ const PRIVATE_ARTIFACT_TOKEN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
27
17
  const CLAUDE_PEER_NOTICE_COPY = {
28
18
  en: {
29
19
  stall: "The local gateway is still waiting to deliver the preceding message. Run `embassy status` or inspect the dashboard for details. Queued mail reaches a busy recipient when its turn ends.",
@@ -37,24 +27,12 @@ const CLAUDE_PEER_NOTICE_COPY = {
37
27
  const REGISTRY_FILE_PATTERN = /^([1-9][0-9]{0,9})\.json$/;
38
28
  const SOCKET_FILE_PATTERN = /^([1-9][0-9]{0,9})\.sock$/;
39
29
  const MAX_PID = 2_147_483_647;
40
- export const claudePeerStatuses = [
41
- "busy",
42
- "shell",
43
- "idle",
44
- "waiting",
45
- ];
46
- export const claudePeerKinds = [
47
- "interactive",
48
- "bg",
49
- "daemon",
50
- "daemon-worker",
51
- ];
30
+ export const claudePeerStatuses = ["busy", "shell", "idle", "waiting"];
31
+ export const claudePeerKinds = ["interactive", "bg", "daemon", "daemon-worker"];
52
32
  export const claudePeerRejectionCodes = [
53
33
  "ENTRY_LIMIT_EXCEEDED",
54
34
  "INVALID_FILE_NAME",
55
35
  "REGISTRY_NOT_REGULAR",
56
- "REGISTRY_OWNER_MISMATCH",
57
- "REGISTRY_MODE_UNSAFE",
58
36
  "REGISTRY_TOO_LARGE",
59
37
  "REGISTRY_RACED",
60
38
  "REGISTRY_INVALID_JSON",
@@ -64,26 +42,14 @@ export const claudePeerRejectionCodes = [
64
42
  "PID_OWNER_MISMATCH",
65
43
  "SOCKET_OUTSIDE_ROOT",
66
44
  "SOCKET_NOT_SOCKET",
67
- "SOCKET_OWNER_MISMATCH",
68
- "SOCKET_MODE_UNSAFE",
69
45
  "SELF_TARGET",
70
46
  "SESSION_ID_COLLISION",
71
47
  ];
72
48
  export const claudePeerInboundStallReasons = [
73
49
  "ROUTE_BUSY",
74
50
  "ROUTE_UNAVAILABLE",
75
- "CODEX_ROUTE_STALE",
76
51
  "AWAITING_EXTERNAL_APPROVAL",
77
52
  ];
78
- function pendingReceiptDeadlineStatus(pending) {
79
- if (pending.writeEvidence === "transport_written" ||
80
- pending.state === "held") {
81
- return "unconfirmed";
82
- }
83
- return pending.writeEvidence === "transport_uncertain"
84
- ? "ambiguous"
85
- : "expired";
86
- }
87
53
  const MAX_CLAUDE_STALL_QUEUED_MS = 3_600_000;
88
54
  const CAPACITY_SETTLEMENT_MAX_ATTEMPTS = 3;
89
55
  const CAPACITY_SETTLEMENT_RETRY_MS = 25;
@@ -101,6 +67,9 @@ function byteLength(value) {
101
67
  function isBoundedString(value, maxBytes) {
102
68
  return typeof value === "string" && byteLength(value) <= maxBytes;
103
69
  }
70
+ function isDiagnosticVersion(value) {
71
+ return isBoundedString(value, 64) && value.length > 0 && !value.includes("\0");
72
+ }
104
73
  function validateContent(content, maxBytes) {
105
74
  if (typeof content !== "string" ||
106
75
  content.length === 0 ||
@@ -110,16 +79,9 @@ function validateContent(content, maxBytes) {
110
79
  }
111
80
  return content;
112
81
  }
113
- function exactMode(mode) {
114
- return mode & 0o777;
115
- }
82
+ function exactMode(mode) { return mode & 0o777; }
116
83
  function generationOf(stat) {
117
- return {
118
- dev: stat.dev,
119
- ino: stat.ino,
120
- size: stat.size,
121
- mtimeMs: stat.mtimeMs,
122
- };
84
+ return { dev: stat.dev, ino: stat.ino, size: stat.size, mtimeMs: stat.mtimeMs };
123
85
  }
124
86
  function sameFileGeneration(left, right) {
125
87
  return (left.dev === right.dev &&
@@ -133,17 +95,6 @@ function sameSocketGeneration(left, right) {
133
95
  function sameDirectoryGeneration(left, right) {
134
96
  return left.dev === right.dev && left.ino === right.ino;
135
97
  }
136
- function sameTargetGeneration(left, right) {
137
- return (left.registryPath === right.registryPath &&
138
- left.record.pid === right.record.pid &&
139
- left.record.sessionId === right.record.sessionId &&
140
- left.record.name === right.record.name &&
141
- left.record.cwd === right.record.cwd &&
142
- left.record.kind === right.record.kind &&
143
- left.record.messagingSocketPath === right.record.messagingSocketPath &&
144
- left.processGeneration === right.processGeneration &&
145
- sameSocketGeneration(left.socketGeneration, right.socketGeneration));
146
- }
147
98
  function pathContains(parent, child) {
148
99
  const relative = path.relative(parent, child);
149
100
  return (relative === "" ||
@@ -179,31 +130,20 @@ function configuredLimit(value, fallback, minimum, maximum, label) {
179
130
  function boundedStallQueuedForMs(value) {
180
131
  if (Number.isNaN(value) || value === Number.NEGATIVE_INFINITY)
181
132
  return 0;
182
- if (value === Number.POSITIVE_INFINITY) {
133
+ if (value === Number.POSITIVE_INFINITY)
183
134
  return MAX_CLAUDE_STALL_QUEUED_MS;
184
- }
185
135
  return Math.min(MAX_CLAUDE_STALL_QUEUED_MS, Math.max(0, Math.trunc(value)));
186
136
  }
187
137
  function parseRegistryRecord(value, expectedPid) {
188
138
  if (!isObject(value))
189
139
  return undefined;
190
- const required = [
191
- "pid",
192
- "sessionId",
193
- "cwd",
194
- "startedAt",
195
- "procStart",
196
- "peerProtocol",
197
- "kind",
198
- "entrypoint",
199
- "messagingSocketPath",
200
- "name",
201
- "updatedAt",
202
- ];
140
+ const required = ["pid", "sessionId", "cwd", "startedAt", "procStart",
141
+ "peerProtocol", "kind", "entrypoint", "messagingSocketPath", "name",
142
+ "updatedAt"];
203
143
  if (!required.every((key) => Object.hasOwn(value, key)))
204
144
  return undefined;
205
145
  const embassyAdvertisement = value.embassyAdvertisementVersion === EMBASSY_ADVERTISEMENT_VERSION &&
206
- isCompatibilityVersionEvidence(value.version) &&
146
+ isDiagnosticVersion(value.version) &&
207
147
  typeof value.name === "string" &&
208
148
  EMBASSY_SOURCE_NAME_PATTERN.test(value.name) &&
209
149
  ALIAS_PATTERN.test(value.name) &&
@@ -212,18 +152,13 @@ function parseRegistryRecord(value, expectedPid) {
212
152
  value.peerProtocol === CLAUDE_PEER_COMPATIBILITY.peerProtocol &&
213
153
  value.status !== undefined &&
214
154
  value.statusUpdatedAt !== undefined;
215
- if (value.embassyAdvertisementVersion !== undefined &&
216
- !embassyAdvertisement) {
155
+ if (value.embassyAdvertisementVersion !== undefined && !embassyAdvertisement)
217
156
  return undefined;
218
- }
219
- const versionObserved = value.version === undefined ||
220
- isCompatibilityVersionEvidence(value.version);
157
+ const versionObserved = value.version === undefined || isDiagnosticVersion(value.version);
221
158
  if (value.pid !== expectedPid)
222
159
  return undefined;
223
- if (typeof value.sessionId !== "string" ||
224
- !UUID_PATTERN.test(value.sessionId)) {
160
+ if (typeof value.sessionId !== "string" || !UUID_PATTERN.test(value.sessionId))
225
161
  return undefined;
226
- }
227
162
  if (!isBoundedString(value.cwd, 4096) ||
228
163
  !path.isAbsolute(value.cwd) ||
229
164
  value.cwd.includes("\0")) {
@@ -273,13 +208,9 @@ function parseRegistryRecord(value, expectedPid) {
273
208
  pid: expectedPid,
274
209
  sessionId: value.sessionId.toLowerCase(),
275
210
  cwd: value.cwd,
276
- peerProtocol: 1,
277
211
  kind: value.kind,
278
212
  messagingSocketPath: value.messagingSocketPath,
279
213
  name: value.name,
280
- // Some reviewed Claude Code print/SDK session records omit status fields
281
- // while the model turn is active. Treat that live process conservatively
282
- // as busy.
283
214
  status: (value.status ?? "busy"),
284
215
  embassyAdvertisement,
285
216
  };
@@ -431,15 +362,49 @@ async function invokeHook(hook, value) {
431
362
  return;
432
363
  await hook(value);
433
364
  }
434
- async function invokeObservationalHook(hook, value) {
365
+ function writeSocketPayload(connect, socketPath, payload, timeoutMs, onWriteStart) {
366
+ return new Promise((resolve, reject) => {
367
+ const socket = connect(socketPath);
368
+ let settled = false;
369
+ const timer = setTimeout(() => finish(new Error("peer write timeout")), timeoutMs);
370
+ timer.unref();
371
+ const finish = (error) => {
372
+ if (settled)
373
+ return;
374
+ settled = true;
375
+ clearTimeout(timer);
376
+ socket.destroy();
377
+ error === undefined ? resolve() : reject(error);
378
+ };
379
+ socket.once("error", finish);
380
+ socket.once("connect", () => {
381
+ try {
382
+ onWriteStart();
383
+ socket.end(payload, () => finish());
384
+ }
385
+ catch (error) {
386
+ finish(error);
387
+ }
388
+ });
389
+ });
390
+ }
391
+ async function socketOwnership(socketPath, generation) {
435
392
  try {
436
- await invokeHook(hook, value);
393
+ const stat = await lstat(socketPath);
394
+ return stat.isSocket() &&
395
+ sameSocketGeneration({ dev: stat.dev, ino: stat.ino }, generation)
396
+ ? "owned"
397
+ : "changed";
437
398
  }
438
- catch {
439
- // Status observation must never change a possibly-written transport
440
- // outcome or invite an unsafe caller retry.
399
+ catch (error) {
400
+ if (error.code === "ENOENT")
401
+ return "missing";
402
+ throw error;
441
403
  }
442
404
  }
405
+ function closeServer(server) {
406
+ return new Promise((resolve, reject) => server.close((error) => error === undefined ? resolve() : reject(error)));
407
+ }
443
408
  export class ClaudePeerAdapter {
444
409
  #sessionsDir;
445
410
  #socketDir;
@@ -449,33 +414,27 @@ export class ClaudePeerAdapter {
449
414
  #connect;
450
415
  #now;
451
416
  #createId;
452
- #createGeneration;
417
+ #createArtifactToken;
453
418
  #locale;
454
- #attestedClaudeCodeVersion;
455
419
  #deliveryNotices;
456
420
  #registryRename;
457
421
  #registryOperationHook;
458
422
  #userHome;
459
423
  #tempRoots;
460
- #listenerOwner = Object.freeze({});
461
424
  #registryMutex = new KeyedMutex();
462
- #registryPublicationHook;
463
425
  #postBindHook;
464
426
  #targets = new Map();
465
- #workspacePolicies = new Map();
427
+ #selectedStateRoots = new Map();
466
428
  #listeners = new Set();
429
+ #preparedSends = new Set();
467
430
  constructor(options, testing = {}) {
468
431
  if (process.platform === "win32" || process.getuid === undefined) {
469
432
  throw new BridgeError("CLAUDE_PEER_PLATFORM_UNSUPPORTED", "Claude peer sockets are supported only on macOS and Linux.");
470
433
  }
471
- if (!isCompatibilityVersionEvidence(options.attestedClaudeCodeVersion)) {
472
- throw new BridgeError("INVALID_GATEWAY_CONFIGURATION", "The Claude version metadata is malformed.");
473
- }
474
434
  if (options.locale !== undefined && !isDashboardLocale(options.locale)) {
475
435
  throw new BridgeError("DASHBOARD_LOCALE_UNSUPPORTED", "The Claude peer notice locale is unsupported.");
476
436
  }
477
437
  this.#locale = options.locale ?? "en";
478
- this.#attestedClaudeCodeVersion = options.attestedClaudeCodeVersion;
479
438
  if (options.deliveryNotices !== undefined &&
480
439
  !["merged", "verbose", "quiet"].includes(options.deliveryNotices)) {
481
440
  throw new BridgeError("INVALID_GATEWAY_CONFIGURATION", "The Claude peer delivery notice mode is unsupported.");
@@ -487,17 +446,16 @@ export class ClaudePeerAdapter {
487
446
  if (!Number.isSafeInteger(this.#expectedUid) || this.#expectedUid < 0) {
488
447
  throw new BridgeError("INVALID_PEER_UID", "expectedUid must be a non-negative integer.");
489
448
  }
449
+ const limit = (name, fallback, minimum, maximum) => configuredLimit(options[name], fallback, minimum, maximum, name);
490
450
  this.#limits = {
491
- maxRegistryEntries: configuredLimit(options.maxRegistryEntries, 256, 1, 4_096, "maxRegistryEntries"),
492
- maxRegistryBytes: configuredLimit(options.maxRegistryBytes, 16 * 1024, 512, 1024 * 1024, "maxRegistryBytes"),
493
- maxFrameBytes: configuredLimit(options.maxFrameBytes, 64 * 1024, 256, 1024 * 1024, "maxFrameBytes"),
494
- targetLeaseMs: configuredLimit(options.targetLeaseMs, 30_000, 100, 5 * 60_000, "targetLeaseMs"),
495
- connectTimeoutMs: configuredLimit(options.connectTimeoutMs, 2_000, 10, 30_000, "connectTimeoutMs"),
496
- receiptDeadlineMs: configuredLimit(options.receiptDeadlineMs, 5 * 60_000, 10, 10 * 60_000, "receiptDeadlineMs"),
497
- maxPendingReceipts: configuredLimit(options.maxPendingReceipts, 256, 1, 4_096, "maxPendingReceipts"),
498
- maxConnections: configuredLimit(options.maxConnections, 32, 1, 1_024, "maxConnections"),
499
- connectionIdleMs: configuredLimit(options.connectionIdleMs, 5_000, 10, 60_000, "connectionIdleMs"),
500
- maxFramesPerConnection: configuredLimit(options.maxFramesPerConnection, 8, 1, 128, "maxFramesPerConnection"),
451
+ maxRegistryEntries: limit("maxRegistryEntries", 256, 1, 4_096),
452
+ maxRegistryBytes: limit("maxRegistryBytes", 16 * 1024, 512, 1024 * 1024),
453
+ maxFrameBytes: limit("maxFrameBytes", 64 * 1024, 256, 1024 * 1024),
454
+ connectTimeoutMs: limit("connectTimeoutMs", 2_000, 10, 30_000),
455
+ maxPendingReceipts: limit("maxPendingReceipts", 256, 1, 4_096),
456
+ maxConnections: limit("maxConnections", 32, 1, 1_024),
457
+ connectionIdleMs: limit("connectionIdleMs", 5_000, 10, 60_000),
458
+ maxFramesPerConnection: limit("maxFramesPerConnection", 8, 1, 128),
501
459
  };
502
460
  this.#inspectProcess = testing.processInspector ?? defaultProcessInspector;
503
461
  this.#connect =
@@ -505,31 +463,29 @@ export class ClaudePeerAdapter {
505
463
  ((socketPath) => net.createConnection({ path: socketPath }));
506
464
  this.#now = testing.now ?? Date.now;
507
465
  this.#createId = testing.createId ?? randomUUID;
508
- this.#createGeneration =
509
- testing.createGeneration ?? createCodexRegistrationGeneration;
466
+ this.#createArtifactToken = testing.createArtifactToken ?? randomUUID;
510
467
  this.#registryRename = testing.registryRename ?? rename;
511
468
  this.#registryOperationHook = testing.registryOperationHook;
512
469
  this.#userHome = assertAbsoluteConfiguredPath(testing.userHome ?? os.userInfo().homedir, "userHome");
513
470
  this.#tempRoots = Object.freeze([
514
471
  ...(testing.tempRoots ?? ["/tmp", "/private/tmp", os.tmpdir()]),
515
472
  ].map((root) => assertAbsoluteConfiguredPath(root, "tempRoot")));
516
- this.#registryPublicationHook = testing.registryPublicationHook;
517
473
  this.#postBindHook = testing.postBindHook;
518
474
  }
519
- async #validateRealDirectory(directory) {
520
- const stat = await lstat(directory);
521
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
522
- throw new BridgeError("UNSAFE_PEER_DIRECTORY", "The Claude peer directory is not an accessible real directory.");
523
- }
524
- }
525
- async #validateSessionsDirectory() {
526
- const stat = await lstat(this.#sessionsDir);
475
+ async #validateRoots() {
476
+ const [stat, sockets] = await Promise.all([
477
+ lstat(this.#sessionsDir),
478
+ lstat(this.#socketDir),
479
+ ]);
527
480
  if (stat.isSymbolicLink() ||
528
481
  !stat.isDirectory() ||
529
482
  stat.uid !== this.#expectedUid ||
530
483
  exactMode(stat.mode) !== 0o700) {
531
484
  throw new BridgeError("UNSAFE_PEER_DIRECTORY", "The Claude sessions directory failed its exact owner and mode policy.");
532
485
  }
486
+ if (sockets.isSymbolicLink() || !sockets.isDirectory()) {
487
+ throw new BridgeError("UNSAFE_PEER_DIRECTORY", "The Claude peer socket directory is not an accessible real directory.");
488
+ }
533
489
  }
534
490
  async #readRegistryFile(registryPath) {
535
491
  const before = await lstat(registryPath);
@@ -592,7 +548,7 @@ export class ClaudePeerAdapter {
592
548
  return { dev: stat.dev, ino: stat.ino };
593
549
  }
594
550
  async #bindingFromRegistry(registryPath, expectedPid, _existingTargetId, onParsed) {
595
- const { value, generation: registryGeneration } = await this.#readRegistryFile(registryPath);
551
+ const { value } = await this.#readRegistryFile(registryPath);
596
552
  if (isObject(value) && value.pid !== expectedPid) {
597
553
  throw new BridgeError("PID_MISMATCH", "Registry filename and process identifier do not match.");
598
554
  }
@@ -613,27 +569,18 @@ export class ClaudePeerAdapter {
613
569
  if (processIdentity.uid !== this.#expectedUid) {
614
570
  throw new BridgeError("PID_OWNER_MISMATCH", "Registry process owner is unsafe.");
615
571
  }
616
- const socketGeneration = await this.#validateSocket(record.messagingSocketPath, expectedPid);
572
+ await this.#validateSocket(record.messagingSocketPath, expectedPid);
617
573
  return {
618
574
  // Claude's native session UUID is the logical route identity. The
619
575
  // process, registry file, and socket below are replaceable transport
620
576
  // coordinates for that session and are revalidated before every write.
621
577
  targetId: record.sessionId,
622
578
  alias: record.name,
623
- registryPath,
624
- registryGeneration,
625
- socketGeneration,
626
579
  record,
627
- processGeneration: processIdentity.generation,
628
- expiresAt: this.#now() + this.#limits.targetLeaseMs,
629
580
  };
630
581
  }
631
582
  async discover() {
632
- await Promise.all([
633
- this.#validateSessionsDirectory(),
634
- this.#validateRealDirectory(this.#socketDir),
635
- ]);
636
- const previousTargets = [...this.#targets.values()];
583
+ await this.#validateRoots();
637
584
  const nextTargets = new Map();
638
585
  const collidedSessionIds = new Set();
639
586
  const rejected = {};
@@ -670,17 +617,9 @@ export class ClaudePeerAdapter {
670
617
  }
671
618
  const registryPath = path.join(this.#sessionsDir, entry.name);
672
619
  try {
673
- let binding = await this.#bindingFromRegistry(registryPath, pid, undefined, () => {
620
+ const binding = await this.#bindingFromRegistry(registryPath, pid, undefined, () => {
674
621
  parseableRecords += 1;
675
622
  });
676
- const previous = previousTargets.find((candidate) => sameTargetGeneration(candidate, binding));
677
- if (previous !== undefined) {
678
- binding = {
679
- ...binding,
680
- targetId: previous.targetId,
681
- expiresAt: this.#now() + this.#limits.targetLeaseMs,
682
- };
683
- }
684
623
  if (collidedSessionIds.has(binding.targetId) ||
685
624
  nextTargets.has(binding.targetId)) {
686
625
  nextTargets.delete(binding.targetId);
@@ -718,10 +657,6 @@ export class ClaudePeerAdapter {
718
657
  for (const [targetId, binding] of nextTargets) {
719
658
  this.#targets.set(targetId, binding);
720
659
  }
721
- for (const targetId of this.#workspacePolicies.keys()) {
722
- if (!nextTargets.has(targetId))
723
- this.#workspacePolicies.delete(targetId);
724
- }
725
660
  return {
726
661
  peers,
727
662
  rejected,
@@ -730,111 +665,47 @@ export class ClaudePeerAdapter {
730
665
  parseableRecords,
731
666
  };
732
667
  }
733
- async #revalidateBinding(binding) {
734
- await Promise.all([
735
- this.#validateSessionsDirectory(),
736
- this.#validateRealDirectory(this.#socketDir),
737
- ]);
738
- if (binding.expiresAt < this.#now()) {
739
- this.#targets.delete(binding.targetId);
740
- this.#workspacePolicies.delete(binding.targetId);
741
- throw new BridgeError("CLAUDE_PEER_TARGET_STALE", "The Claude peer discovery lease expired; discover again.", true);
742
- }
743
- const current = await this.#bindingFromRegistry(binding.registryPath, binding.record.pid, binding.targetId);
744
- if (!sameTargetGeneration(binding, current)) {
745
- this.#targets.delete(binding.targetId);
746
- this.#workspacePolicies.delete(binding.targetId);
747
- throw new BridgeError("CLAUDE_PEER_TARGET_CHANGED", "The Claude peer generation changed; discover again.", true);
748
- }
749
- current.expiresAt = binding.expiresAt;
750
- this.#targets.set(current.targetId, current);
751
- return current;
752
- }
753
- async #canonicalStateRoot(stateRoot) {
668
+ async #attestOwnedDirectory(candidate, exactPrivate, code) {
754
669
  try {
755
- const configured = assertAbsoluteConfiguredPath(stateRoot, "controllerStateRoot");
670
+ const configured = assertAbsoluteConfiguredPath(candidate, "peerDirectory");
756
671
  const before = await lstat(configured);
672
+ const canonical = await realpath(configured);
673
+ const after = await lstat(configured);
674
+ const safeMode = (mode) => exactPrivate ? exactMode(mode) === 0o700 : (exactMode(mode) & 0o022) === 0;
757
675
  if (before.isSymbolicLink() ||
758
676
  !before.isDirectory() ||
759
677
  before.uid !== this.#expectedUid ||
760
- exactMode(before.mode) !== 0o700) {
761
- throw new BridgeError("CLAUDE_PEER_STATE_ROOT_UNSAFE", "The controller state root failed its canonical ownership policy.");
762
- }
763
- const canonical = await realpath(configured);
764
- const after = await lstat(configured);
765
- if (canonical !== configured ||
678
+ !safeMode(before.mode) ||
679
+ canonical !== configured ||
766
680
  after.isSymbolicLink() ||
767
681
  !after.isDirectory() ||
768
682
  after.uid !== this.#expectedUid ||
769
- exactMode(after.mode) !== 0o700 ||
683
+ !safeMode(after.mode) ||
770
684
  before.dev !== after.dev ||
771
685
  before.ino !== after.ino) {
772
- throw new BridgeError("CLAUDE_PEER_STATE_ROOT_UNSAFE", "The controller state root changed or is not canonical.");
686
+ throw new BridgeError(code, "The peer directory failed its exact ownership policy.");
773
687
  }
774
- return {
775
- path: canonical,
776
- generation: { dev: after.dev, ino: after.ino },
777
- };
688
+ return canonical;
778
689
  }
779
690
  catch (error) {
780
691
  if (error instanceof BridgeError)
781
692
  throw error;
782
- throw new BridgeError("CLAUDE_PEER_STATE_ROOT_UNSAFE", "The controller state root could not be safely attested.");
693
+ throw new BridgeError(code, "The peer directory could not be safely attested.");
783
694
  }
784
695
  }
785
- async #assertBindingWorkspaceDisjoint(binding, _canonicalStateRoot) {
786
- try {
787
- const cwd = binding.record.cwd;
788
- const before = await lstat(cwd);
789
- if (before.isSymbolicLink() || !before.isDirectory()) {
790
- throw new BridgeError("CLAUDE_PEER_WORKSPACE_UNSAFE", "The selected Claude workspace failed its directory policy.");
791
- }
792
- const canonicalWorkspace = await realpath(cwd);
793
- const homeBefore = await lstat(this.#userHome);
794
- const canonicalHome = await realpath(this.#userHome);
795
- const homeAfter = await lstat(this.#userHome);
796
- if (canonicalWorkspace !== cwd ||
797
- homeBefore.isSymbolicLink() ||
798
- !homeBefore.isDirectory() ||
799
- canonicalHome !== this.#userHome ||
800
- homeBefore.uid !== this.#expectedUid ||
801
- (exactMode(homeBefore.mode) & 0o022) !== 0 ||
802
- homeAfter.isSymbolicLink() ||
803
- !homeAfter.isDirectory() ||
804
- homeAfter.uid !== this.#expectedUid ||
805
- (exactMode(homeAfter.mode) & 0o022) !== 0 ||
806
- homeBefore.dev !== homeAfter.dev ||
807
- homeBefore.ino !== homeAfter.ino) {
808
- throw new BridgeError("CLAUDE_PEER_WORKSPACE_UNSAFE", "The selected Claude workspace or local home is non-canonical.");
809
- }
810
- const filesystemRoot = path.parse(canonicalWorkspace).root;
811
- if (canonicalWorkspace === filesystemRoot) {
812
- throw new BridgeError("CLAUDE_PEER_WORKSPACE_BROAD", "The selected Claude workspace is broader than the safe local boundary.");
813
- }
814
- if (this.#tempRoots.some((tempRoot) => pathContains(tempRoot, canonicalWorkspace) ||
815
- pathContains(canonicalWorkspace, tempRoot))) {
816
- throw new BridgeError("CLAUDE_PEER_WORKSPACE_BROAD", "The selected Claude workspace is inside or contains a temporary root.");
817
- }
818
- const after = await lstat(cwd);
819
- if (after.isSymbolicLink() ||
820
- !after.isDirectory() ||
821
- before.uid !== this.#expectedUid ||
822
- after.uid !== this.#expectedUid ||
823
- (exactMode(before.mode) & 0o022) !== 0 ||
824
- (exactMode(after.mode) & 0o022) !== 0 ||
825
- before.dev !== after.dev ||
826
- before.ino !== after.ino) {
827
- throw new BridgeError("CLAUDE_PEER_WORKSPACE_UNSAFE", "The selected Claude workspace changed or failed its owner and mode policy.");
828
- }
829
- return {
830
- path: canonicalWorkspace,
831
- generation: { dev: after.dev, ino: after.ino },
832
- };
696
+ async #assertBindingWorkspaceDisjoint(binding) {
697
+ const code = "CLAUDE_PEER_WORKSPACE_UNSAFE";
698
+ const configured = assertAbsoluteConfiguredPath(binding.record.cwd, "peerDirectory");
699
+ if (configured === path.parse(configured).root ||
700
+ this.#tempRoots.some((root) => pathContains(root, configured) || pathContains(configured, root))) {
701
+ throw new BridgeError("CLAUDE_PEER_WORKSPACE_BROAD", "The selected Claude workspace is broader than the safe local boundary.");
833
702
  }
834
- catch (error) {
835
- if (error instanceof BridgeError)
836
- throw error;
837
- throw new BridgeError("CLAUDE_PEER_WORKSPACE_UNSAFE", "The selected Claude workspace could not be safely attested.");
703
+ const [workspace] = await Promise.all([
704
+ this.#attestOwnedDirectory(configured, false, code),
705
+ this.#attestOwnedDirectory(this.#userHome, false, code),
706
+ ]);
707
+ if (workspace !== configured) {
708
+ throw new BridgeError(code, "The peer workspace changed during attestation.");
838
709
  }
839
710
  }
840
711
  /**
@@ -846,34 +717,12 @@ export class ClaudePeerAdapter {
846
717
  if (target === undefined) {
847
718
  throw new BridgeError("CLAUDE_PEER_TARGET_UNKNOWN", "The Claude peer target is unknown; discover it first.", true);
848
719
  }
849
- const binding = await this.#revalidateBinding(target);
850
- const state = await this.#canonicalStateRoot(controllerStateRoot);
851
- const workspace = await this.#assertBindingWorkspaceDisjoint(binding, state.path);
852
- const existing = this.#workspacePolicies.get(targetId);
853
- if (existing !== undefined) {
854
- if (existing.stateRoot !== state.path ||
855
- !sameDirectoryGeneration(existing.stateGeneration, state.generation)) {
856
- this.#workspacePolicies.delete(targetId);
857
- throw new BridgeError("CLAUDE_PEER_STATE_ROOT_CHANGED", "The selected Claude target is already bound to another controller state generation.");
858
- }
859
- if (existing.workspace !== workspace.path ||
860
- !sameDirectoryGeneration(existing.workspaceGeneration, workspace.generation)) {
861
- this.#workspacePolicies.delete(targetId);
862
- throw new BridgeError("CLAUDE_PEER_WORKSPACE_CHANGED", "The selected Claude workspace generation changed; select it again.", true);
863
- }
864
- }
865
- this.#workspacePolicies.set(targetId, {
866
- stateRoot: state.path,
867
- stateGeneration: state.generation,
868
- workspace: workspace.path,
869
- workspaceGeneration: workspace.generation,
870
- });
720
+ const stateRoot = await this.#attestOwnedDirectory(controllerStateRoot, true, "CLAUDE_PEER_STATE_ROOT_UNSAFE");
721
+ await this.#assertBindingWorkspaceDisjoint(target);
722
+ this.#selectedStateRoots.set(targetId, stateRoot);
871
723
  }
872
724
  async #resolveReplyAddress(address) {
873
- await Promise.all([
874
- this.#validateSessionsDirectory(),
875
- this.#validateRealDirectory(this.#socketDir),
876
- ]);
725
+ await this.#validateRoots();
877
726
  if (!address.startsWith("uds:")) {
878
727
  throw new BridgeError("UNREGISTERED_REPLY_ADDRESS", "Only local registered UDS reply addresses are accepted.");
879
728
  }
@@ -889,18 +738,10 @@ export class ClaudePeerAdapter {
889
738
  throw new BridgeError("UNREGISTERED_REPLY_ADDRESS", "Reply address is not a registered Claude peer socket.");
890
739
  }
891
740
  const registryPath = path.join(this.#sessionsDir, `${pid}.json`);
892
- let binding = await this.#bindingFromRegistry(registryPath, pid);
741
+ const binding = await this.#bindingFromRegistry(registryPath, pid);
893
742
  if (binding.record.messagingSocketPath !== socketPath) {
894
743
  throw new BridgeError("UNREGISTERED_REPLY_ADDRESS", "Reply address does not match its live registry generation.");
895
744
  }
896
- const previous = [...this.#targets.values()].find((candidate) => sameTargetGeneration(candidate, binding));
897
- if (previous !== undefined) {
898
- binding = {
899
- ...binding,
900
- targetId: previous.targetId,
901
- expiresAt: this.#now() + this.#limits.targetLeaseMs,
902
- };
903
- }
904
745
  this.#targets.set(binding.targetId, binding);
905
746
  return binding;
906
747
  }
@@ -910,10 +751,6 @@ export class ClaudePeerAdapter {
910
751
  * every later delivery resolves the UUID to fresh transport coordinates.
911
752
  */
912
753
  async resolveReplyAddress(address) {
913
- await Promise.all([
914
- this.#validateSessionsDirectory(),
915
- this.#validateRealDirectory(this.#socketDir),
916
- ]);
917
754
  const binding = await this.#resolveReplyAddress(address);
918
755
  return {
919
756
  targetId: binding.targetId,
@@ -923,14 +760,11 @@ export class ClaudePeerAdapter {
923
760
  compatibility: "compatible",
924
761
  };
925
762
  }
926
- async #listen(options, preparedGeneration) {
927
- await Promise.all([
928
- this.#validateSessionsDirectory(),
929
- this.#validateRealDirectory(this.#socketDir),
930
- ]);
931
- const generation = preparedGeneration ?? this.#createGeneration();
932
- if (!isCodexRegistrationGeneration(generation)) {
933
- throw new BridgeError("INVALID_CODEX_PEER_GENERATION", "A native Codex listener requires one bounded opaque generation.");
763
+ async listen(options) {
764
+ await this.#validateRoots();
765
+ const generation = this.#createArtifactToken();
766
+ if (!PRIVATE_ARTIFACT_TOKEN_PATTERN.test(generation)) {
767
+ throw new BridgeError("INVALID_CODEX_PEER_GENERATION", "A native source listener requires one bounded private artifact token.");
934
768
  }
935
769
  if ([...this.#listeners].some((listener) => !listener.closed && listener.generation === generation)) {
936
770
  throw new BridgeError("CODEX_PEER_GENERATION_EXISTS", "A live native Codex listener already owns this exact generation.");
@@ -942,10 +776,8 @@ export class ClaudePeerAdapter {
942
776
  limits: this.#limits,
943
777
  createId: this.#createId,
944
778
  connect: this.#connect,
945
- now: this.#now,
946
779
  locale: this.#locale,
947
780
  deliveryNotices: this.#deliveryNotices,
948
- attestedClaudeCodeVersion: this.#attestedClaudeCodeVersion,
949
781
  resolveReplyAddress: async (address) => await this.#resolveReplyAddress(address),
950
782
  resolveSessionBinding: async (sessionId) => {
951
783
  // Native receipt handles retain only the stable Claude session UUID.
@@ -959,199 +791,122 @@ export class ClaudePeerAdapter {
959
791
  if (target === undefined) {
960
792
  throw new BridgeError("CLAUDE_PEER_TARGET_UNKNOWN", "The Claude peer receipt target is no longer discoverable.", true);
961
793
  }
962
- return await this.#revalidateBinding(target);
794
+ return target;
963
795
  },
964
- revalidateBinding: async (binding) => await this.#revalidateBinding(binding),
965
796
  options,
966
- owner: this.#listenerOwner,
967
797
  generation,
968
798
  registryRename: this.#registryRename,
969
799
  ...(this.#registryOperationHook === undefined
970
800
  ? {}
971
801
  : { registryOperationHook: this.#registryOperationHook }),
972
802
  runRegistryMutation: async (operation) => await this.#registryMutex.run("codex-registry", operation),
973
- ...(preparedGeneration === undefined ? {} : { preparedGeneration }),
974
- ...(this.#registryPublicationHook === undefined
975
- ? {}
976
- : { registryPublicationHook: this.#registryPublicationHook }),
977
803
  postBindHook: this.#postBindHook,
978
804
  onClosed: () => this.#listeners.delete(listener),
979
805
  });
980
806
  this.#listeners.add(listener);
981
807
  return listener;
982
808
  }
983
- async listen(options) {
984
- return await this.#listen(options);
985
- }
986
- async listenPrepared(generation, options) {
987
- if (!isCodexRegistrationGeneration(generation)) {
988
- throw new BridgeError("INVALID_CODEX_PEER_GENERATION", "A prepared native Codex listener requires one bounded opaque generation.");
809
+ async prepareSend(targetId, content, options) {
810
+ if (!Number.isSafeInteger(options.deadlineAt) ||
811
+ options.deadlineAt < 0) {
812
+ throw new BridgeError("INVALID_PEER_MESSAGE_DEADLINE", "The Claude peer message deadline must be an epoch-millisecond timestamp.");
989
813
  }
990
- return await this.#listen(options, generation);
991
- }
992
- async send(targetId, content, options = {}) {
993
- // Session UUIDs survive registry rewrites, renames, and endpoint rotation.
994
- // Refresh the replaceable transport coordinates before any write begins.
814
+ if (options.deadlineAt <= this.#now()) {
815
+ throw new BridgeError("CLAUDE_PEER_MESSAGE_EXPIRED", "The Claude peer message deadline elapsed before preparation.", true);
816
+ }
817
+ // Preparation positively resolves every replaceable coordinate and exact
818
+ // workspace generation. The resulting operation owns only immutable wire
819
+ // bytes and the already-validated socket path; it performs no connection
820
+ // or write until its one-shot perform function is invoked.
995
821
  await this.discover();
996
822
  const target = this.#targets.get(targetId);
997
823
  if (target === undefined) {
998
824
  throw new BridgeError("CLAUDE_PEER_TARGET_UNKNOWN", "The Claude peer target is unknown; discover it first.", true);
999
825
  }
1000
- const binding = await this.#revalidateBinding(target);
1001
- const workspacePolicy = this.#workspacePolicies.get(targetId);
1002
- if (workspacePolicy === undefined) {
826
+ const stateRoot = this.#selectedStateRoots.get(targetId);
827
+ if (stateRoot === undefined) {
1003
828
  throw new BridgeError("CLAUDE_PEER_WORKSPACE_UNATTESTED", "The Claude peer workspace must pass selection validation before send.");
1004
829
  }
1005
830
  try {
1006
- const state = await this.#canonicalStateRoot(workspacePolicy.stateRoot);
1007
- const workspace = await this.#assertBindingWorkspaceDisjoint(binding, state.path);
1008
- if (!sameDirectoryGeneration(workspacePolicy.stateGeneration, state.generation)) {
1009
- throw new BridgeError("CLAUDE_PEER_STATE_ROOT_CHANGED", "The controller state root generation changed; select the target again.", true);
1010
- }
1011
- if (workspacePolicy.workspace !== workspace.path ||
1012
- !sameDirectoryGeneration(workspacePolicy.workspaceGeneration, workspace.generation)) {
1013
- throw new BridgeError("CLAUDE_PEER_WORKSPACE_CHANGED", "The selected Claude workspace generation changed; select it again.", true);
1014
- }
831
+ await this.#attestOwnedDirectory(stateRoot, true, "CLAUDE_PEER_STATE_ROOT_UNSAFE");
832
+ await this.#assertBindingWorkspaceDisjoint(target);
1015
833
  }
1016
834
  catch (error) {
1017
- this.#workspacePolicies.delete(targetId);
835
+ this.#selectedStateRoots.delete(targetId);
1018
836
  throw error;
1019
837
  }
1020
838
  const messageId = this.#createId();
1021
839
  if (!UUID_PATTERN.test(messageId)) {
1022
840
  throw new BridgeError("INVALID_PEER_MESSAGE_ID", "The configured ID source did not produce a UUID.");
1023
841
  }
1024
- const listener = options.listener;
1025
- if (listener !== undefined && !this.#listeners.has(listener)) {
842
+ if (options.replyListener !== undefined &&
843
+ !this.#listeners.has(options.replyListener)) {
1026
844
  throw new BridgeError("CLAUDE_PEER_LISTENER_FOREIGN", "The reply listener is not owned by this adapter.");
1027
845
  }
1028
846
  const frame = encodeClaudePeerUserFrame({
1029
847
  messageId,
1030
848
  content,
1031
- ...(listener === undefined ? {} : { from: listener.address }),
849
+ ...(options.replyListener === undefined
850
+ ? {}
851
+ : { from: options.replyListener.address }),
1032
852
  maxFrameBytes: this.#limits.maxFrameBytes,
1033
853
  });
1034
- if (options.receiptDeadlineAt !== undefined &&
1035
- (!Number.isSafeInteger(options.receiptDeadlineAt) ||
1036
- options.receiptDeadlineAt < 0)) {
1037
- throw new BridgeError("INVALID_PEER_RECEIPT_DEADLINE", "The Claude peer receipt deadline must be an epoch-millisecond timestamp.");
1038
- }
1039
- if (options.receiptDeadlineAt !== undefined &&
1040
- options.receiptDeadlineAt <= this.#now()) {
1041
- throw new BridgeError("CLAUDE_PEER_MESSAGE_EXPIRED", "The Claude peer message deadline elapsed before any socket write.", true);
1042
- }
1043
- if (listener !== undefined) {
1044
- listener.track(messageId, binding, options.receiptDeadlineAt);
1045
- }
1046
- await invokeObservationalHook(options.onTransportStatus, {
1047
- messageId,
1048
- status: "connecting",
1049
- });
1050
- if (options.receiptDeadlineAt !== undefined &&
1051
- options.receiptDeadlineAt <= this.#now()) {
1052
- listener?.untrack(messageId);
1053
- await invokeObservationalHook(options.onTransportStatus, {
1054
- messageId,
1055
- status: "not_written",
1056
- });
1057
- throw new BridgeError("CLAUDE_PEER_MESSAGE_EXPIRED", "The Claude peer message deadline elapsed before any socket write.", true);
1058
- }
1059
- let written = false;
1060
- let writeStarted = false;
1061
- try {
1062
- await new Promise((resolve, reject) => {
1063
- const socket = this.#connect(binding.record.messagingSocketPath);
1064
- const timeoutMs = Math.max(1, Math.min(this.#limits.connectTimeoutMs, options.receiptDeadlineAt === undefined
1065
- ? this.#limits.connectTimeoutMs
1066
- : options.receiptDeadlineAt - this.#now()));
1067
- const timer = setTimeout(() => {
1068
- socket.destroy();
1069
- const deadlineElapsed = options.receiptDeadlineAt !== undefined &&
1070
- this.#now() >= options.receiptDeadlineAt;
1071
- reject(new BridgeError(writeStarted
1072
- ? "CLAUDE_PEER_WRITE_AMBIGUOUS"
1073
- : deadlineElapsed
1074
- ? "CLAUDE_PEER_MESSAGE_EXPIRED"
1075
- : "CLAUDE_PEER_CONNECT_TIMEOUT", writeStarted
1076
- ? "The Claude peer write began but did not finish in time; do not retry automatically."
1077
- : deadlineElapsed
1078
- ? "The Claude peer message deadline elapsed before any socket write."
1079
- : "The Claude peer socket did not accept the write in time.", !writeStarted));
1080
- }, timeoutMs);
1081
- timer.unref();
1082
- socket.once("error", (error) => {
1083
- clearTimeout(timer);
1084
- reject(error);
1085
- });
1086
- socket.once("connect", () => {
1087
- if (options.receiptDeadlineAt !== undefined &&
1088
- options.receiptDeadlineAt <= this.#now()) {
1089
- clearTimeout(timer);
1090
- socket.destroy();
1091
- reject(new BridgeError("CLAUDE_PEER_MESSAGE_EXPIRED", "The Claude peer message deadline elapsed before any socket write.", true));
1092
- return;
1093
- }
1094
- writeStarted = true;
1095
- listener?.recordTransportOutcome(messageId, "transport_uncertain");
1096
- void invokeObservationalHook(options.onTransportStatus, {
1097
- messageId,
1098
- status: "write_started",
1099
- });
1100
- try {
1101
- socket.end(frame, () => {
1102
- written = true;
1103
- clearTimeout(timer);
1104
- socket.destroy();
1105
- resolve();
1106
- });
1107
- }
1108
- catch (error) {
1109
- clearTimeout(timer);
1110
- socket.destroy();
1111
- reject(error);
1112
- }
1113
- });
1114
- });
1115
- }
1116
- catch (error) {
1117
- if (listener !== undefined) {
1118
- if (writeStarted) {
1119
- listener.recordTransportOutcome(messageId, "transport_uncertain");
1120
- }
1121
- else {
1122
- listener.untrack(messageId);
854
+ const socketPath = target.record.messagingSocketPath;
855
+ let state = "prepared";
856
+ const cancel = () => {
857
+ if (state !== "prepared")
858
+ return;
859
+ state = "cancelled";
860
+ this.#preparedSends.delete(cancel);
861
+ };
862
+ this.#preparedSends.add(cancel);
863
+ const perform = () => {
864
+ if (state !== "prepared") {
865
+ return Promise.reject(new BridgeError("CLAUDE_PEER_PREPARATION_CONSUMED", "The prepared Claude peer write was already performed or cancelled."));
866
+ }
867
+ state = "performed";
868
+ this.#preparedSends.delete(cancel);
869
+ if (options.deadlineAt <= this.#now()) {
870
+ return Promise.reject(new BridgeError("CLAUDE_PEER_MESSAGE_EXPIRED", "The Claude peer message deadline elapsed before any socket write.", true));
871
+ }
872
+ let writeStarted = false;
873
+ const write = writeSocketPayload(this.#connect, socketPath, frame, Math.max(1, Math.min(this.#limits.connectTimeoutMs, options.deadlineAt - this.#now())), () => {
874
+ if (options.deadlineAt <= this.#now()) {
875
+ throw new BridgeError("CLAUDE_PEER_MESSAGE_EXPIRED", "The Claude peer message deadline elapsed before any socket write.", true);
1123
876
  }
1124
- }
1125
- await invokeObservationalHook(options.onTransportStatus, {
1126
- messageId,
1127
- status: writeStarted ? "ambiguous" : "not_written",
877
+ writeStarted = true;
1128
878
  });
1129
- if (error instanceof BridgeError)
1130
- throw error;
1131
- if (writeStarted) {
1132
- throw new BridgeError("CLAUDE_PEER_WRITE_AMBIGUOUS", "The Claude peer write began but its outcome is ambiguous; do not retry automatically.");
1133
- }
1134
- throw new BridgeError("CLAUDE_PEER_WRITE_FAILED", "The Claude peer message was not confirmed written.", true);
1135
- }
1136
- if (!written) {
1137
- listener?.recordTransportOutcome(messageId, "transport_uncertain");
1138
- throw new BridgeError("CLAUDE_PEER_WRITE_AMBIGUOUS", "The Claude peer write outcome is ambiguous; do not retry automatically.");
1139
- }
1140
- listener?.recordTransportOutcome(messageId, "transport_written");
1141
- await invokeObservationalHook(options.onTransportStatus, {
879
+ return write.then(() => ({ messageId, transportStatus: "transport_written" }), (error) => {
880
+ if (error instanceof BridgeError)
881
+ throw error;
882
+ const expired = this.#now() >= options.deadlineAt;
883
+ throw new BridgeError(writeStarted
884
+ ? "CLAUDE_PEER_WRITE_AMBIGUOUS"
885
+ : expired
886
+ ? "CLAUDE_PEER_MESSAGE_EXPIRED"
887
+ : error instanceof Error && error.message === "peer write timeout"
888
+ ? "CLAUDE_PEER_CONNECT_TIMEOUT"
889
+ : "CLAUDE_PEER_WRITE_FAILED", writeStarted
890
+ ? "The Claude peer write began but its outcome is ambiguous; do not retry automatically."
891
+ : expired
892
+ ? "The Claude peer message deadline elapsed before any socket write."
893
+ : "The Claude peer message was not confirmed written.", !writeStarted);
894
+ });
895
+ };
896
+ return Object.freeze({
1142
897
  messageId,
1143
- status: "transport_written",
898
+ frameBytes: frame.length,
899
+ sha256: createHash("sha256").update(frame).digest("hex"),
900
+ perform,
901
+ cancel,
1144
902
  });
1145
- return {
1146
- messageId,
1147
- transportStatus: "transport_written",
1148
- receiptStatus: listener === undefined ? "unavailable" : "pending",
1149
- };
1150
903
  }
1151
904
  async close() {
905
+ for (const cancel of [...this.#preparedSends])
906
+ cancel();
1152
907
  const results = await Promise.allSettled([...this.#listeners].map(async (listener) => listener.close()));
1153
908
  this.#targets.clear();
1154
- this.#workspacePolicies.clear();
909
+ this.#selectedStateRoots.clear();
1155
910
  const failed = results.find((result) => result.status === "rejected");
1156
911
  if (failed !== undefined)
1157
912
  throw failed.reason;
@@ -1160,32 +915,12 @@ export class ClaudePeerAdapter {
1160
915
  export class ClaudePeerListener {
1161
916
  address;
1162
917
  generation;
1163
- #sessionsDir;
1164
918
  #sessionsGeneration;
1165
- #expectedUid;
1166
919
  #socketPath;
1167
920
  #socketGeneration;
1168
921
  #server;
1169
- #limits;
1170
- #createId;
1171
- #connect;
1172
- #now;
1173
- #locale;
1174
- #deliveryNotices;
1175
- #attestedClaudeCodeVersion;
1176
- #resolveReplyAddress;
1177
- #resolveSessionBinding;
1178
- #revalidateBinding;
1179
- #options;
1180
- #owner;
1181
- #registryRename;
1182
- #registryOperationHook;
1183
- #runRegistryMutation;
1184
- #preparedGeneration;
1185
- #registryPublicationHook;
1186
- #onClosed;
922
+ #context;
1187
923
  #connections = new Set();
1188
- #pending = new Map();
1189
924
  #inboundReceipts = new Map();
1190
925
  // One bounded exception slot owns an overflow message only long enough to
1191
926
  // return its terminal capacity rejection. It never forwards content to the
@@ -1195,52 +930,23 @@ export class ClaudePeerListener {
1195
930
  #queuedFrames = 0;
1196
931
  #inboundQuiesced = false;
1197
932
  #inboundQuiesceWaiters = new Set();
1198
- #advertisedRecord;
1199
- #advertisedGeneration;
1200
- #preparedRecord;
1201
- #publicationAttempted = false;
1202
- #publicationSourceGeneration;
1203
- #publicationConfirmed = false;
1204
- #activationGranted = false;
933
+ #advertisement;
1205
934
  #closing = false;
1206
935
  #closed = false;
1207
936
  #closeOperation;
1208
937
  constructor(options, server, socketPath, generation, sessionsGeneration) {
1209
938
  this.address = `uds:${socketPath}`;
1210
939
  this.generation = options.generation;
1211
- this.#sessionsDir = options.sessionsDir;
1212
940
  this.#sessionsGeneration = sessionsGeneration;
1213
- this.#expectedUid = options.expectedUid;
1214
941
  this.#socketPath = socketPath;
1215
942
  this.#socketGeneration = generation;
1216
943
  this.#server = server;
1217
- this.#limits = options.limits;
1218
- this.#createId = options.createId;
1219
- this.#connect = options.connect;
1220
- this.#now = options.now;
1221
- this.#locale = options.locale;
1222
- this.#deliveryNotices = options.deliveryNotices;
1223
- this.#attestedClaudeCodeVersion = options.attestedClaudeCodeVersion;
1224
- this.#resolveReplyAddress = options.resolveReplyAddress;
1225
- this.#resolveSessionBinding = options.resolveSessionBinding;
1226
- this.#revalidateBinding = options.revalidateBinding;
1227
- this.#options = options.options;
1228
- this.#owner = options.owner;
1229
- this.#registryRename = options.registryRename;
1230
- this.#registryOperationHook = options.registryOperationHook;
1231
- this.#runRegistryMutation = options.runRegistryMutation;
1232
- this.#preparedGeneration = options.preparedGeneration;
1233
- this.#registryPublicationHook = options.registryPublicationHook;
1234
- this.#onClosed = options.onClosed;
1235
- this.#inboundQuiesced = options.preparedGeneration !== undefined;
1236
- this.#activationGranted = options.preparedGeneration === undefined;
944
+ this.#context = options;
1237
945
  }
1238
946
  static async create(options) {
1239
947
  // One gateway process owns one native peer socket. It may advertise one
1240
948
  // registered Codex task through Claude's native session registry.
1241
- const socketPath = path.join(options.socketDir, options.preparedGeneration === undefined
1242
- ? `${process.pid}.sock`
1243
- : `${process.pid}.${options.preparedGeneration}.sock`);
949
+ const socketPath = path.join(options.socketDir, `${process.pid}.sock`);
1244
950
  try {
1245
951
  await lstat(socketPath);
1246
952
  throw new BridgeError("CLAUDE_PEER_CALLBACK_EXISTS", "The gateway callback path already exists; it will not be unlinked.");
@@ -1303,36 +1009,17 @@ export class ClaudePeerListener {
1303
1009
  return listener;
1304
1010
  }
1305
1011
  catch (error) {
1306
- let stillOwned = false;
1307
- if (createdGeneration !== undefined) {
1308
- try {
1309
- const current = await lstat(socketPath);
1310
- stillOwned =
1311
- current.isSocket() &&
1312
- sameSocketGeneration({ dev: current.dev, ino: current.ino }, createdGeneration);
1313
- }
1314
- catch {
1315
- // A missing or unreadable path cannot be safely unlinked by
1316
- // net.Server.close(), which deletes by pathname.
1317
- }
1318
- }
1319
- let closeConfirmed = false;
1320
- if (stillOwned) {
1321
- try {
1322
- await new Promise((resolve, reject) => server.close((closeError) => closeError === undefined ? resolve() : reject(closeError)));
1323
- try {
1324
- await lstat(socketPath);
1325
- }
1326
- catch (closeError) {
1327
- closeConfirmed =
1328
- closeError.code === "ENOENT";
1329
- }
1012
+ try {
1013
+ if (createdGeneration === undefined ||
1014
+ await socketOwnership(socketPath, createdGeneration) !== "owned") {
1015
+ throw new Error("callback ownership changed");
1330
1016
  }
1331
- catch {
1332
- // An unconfirmed close cannot safely downgrade a callback failure.
1017
+ await closeServer(server);
1018
+ if (await socketOwnership(socketPath, createdGeneration) !== "missing") {
1019
+ throw new Error("callback close unconfirmed");
1333
1020
  }
1334
1021
  }
1335
- if (!closeConfirmed) {
1022
+ catch {
1336
1023
  server.unref();
1337
1024
  throw new BridgeError("CLAUDE_PEER_CALLBACK_UNSAFE", "The newly bound callback socket could not be safely closed.");
1338
1025
  }
@@ -1342,35 +1029,20 @@ export class ClaudePeerListener {
1342
1029
  get closed() {
1343
1030
  return this.#closing || this.#closed;
1344
1031
  }
1345
- #isClosingOrClosed() {
1346
- return this.#closing || this.#closed;
1347
- }
1348
1032
  #registryPath() {
1349
- return path.join(this.#sessionsDir, `${process.pid}.json`);
1033
+ return path.join(this.#context.sessionsDir, `${process.pid}.json`);
1350
1034
  }
1351
1035
  #registryTemporaryPath() {
1352
- return path.join(this.#sessionsDir, `.${process.pid}.${this.generation}.registry.tmp`);
1036
+ return path.join(this.#context.sessionsDir, `.${process.pid}.${this.generation}.registry.tmp`);
1353
1037
  }
1354
1038
  #serializeRecord(record) {
1355
1039
  return `${JSON.stringify(record)}\n`;
1356
1040
  }
1357
- #recordBelongsToThisListener(record) {
1358
- return (record.embassyAdvertisementVersion === EMBASSY_ADVERTISEMENT_VERSION &&
1359
- record.pid === process.pid &&
1360
- UUID_PATTERN.test(record.sessionId) &&
1361
- record.messagingSocketPath === this.#socketPath &&
1362
- record.version === this.#attestedClaudeCodeVersion &&
1363
- record.peerProtocol === CLAUDE_PEER_COMPATIBILITY.peerProtocol &&
1364
- record.kind === "interactive" &&
1365
- record.entrypoint === "cli" &&
1366
- EMBASSY_SOURCE_NAME_PATTERN.test(record.name) &&
1367
- ALIAS_PATTERN.test(record.name));
1368
- }
1369
1041
  async #assertRegistryDirectory() {
1370
- const current = await lstat(this.#sessionsDir);
1042
+ const current = await lstat(this.#context.sessionsDir);
1371
1043
  if (current.isSymbolicLink() ||
1372
1044
  !current.isDirectory() ||
1373
- current.uid !== this.#expectedUid ||
1045
+ current.uid !== this.#context.expectedUid ||
1374
1046
  exactMode(current.mode) !== 0o700 ||
1375
1047
  !sameDirectoryGeneration({ dev: current.dev, ino: current.ino }, this.#sessionsGeneration)) {
1376
1048
  throw new BridgeError("UNSAFE_PEER_DIRECTORY", "The native peer registry directory changed or failed its ownership policy.");
@@ -1379,43 +1051,36 @@ export class ClaudePeerListener {
1379
1051
  async #readRegistryExact() {
1380
1052
  await this.#assertRegistryDirectory();
1381
1053
  const registryPath = this.#registryPath();
1382
- let before;
1054
+ let handle;
1383
1055
  try {
1384
- before = await lstat(registryPath);
1056
+ handle = await open(registryPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
1385
1057
  }
1386
1058
  catch (error) {
1387
1059
  if (error.code === "ENOENT")
1388
1060
  return undefined;
1389
1061
  throw error;
1390
1062
  }
1391
- if (before.isSymbolicLink() ||
1392
- !before.isFile() ||
1393
- before.uid !== this.#expectedUid ||
1394
- exactMode(before.mode) !== 0o600 ||
1395
- before.size > this.#limits.maxRegistryBytes) {
1396
- throw new BridgeError("REGISTRY_RACED", "The native Codex registry record failed its exact ownership policy.");
1397
- }
1398
- const beforeGeneration = generationOf(before);
1399
- const handle = await open(registryPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
1400
1063
  try {
1401
1064
  const opened = await handle.stat();
1402
1065
  const openedGeneration = generationOf(opened);
1403
1066
  if (!opened.isFile() ||
1404
- opened.uid !== this.#expectedUid ||
1067
+ opened.uid !== this.#context.expectedUid ||
1405
1068
  exactMode(opened.mode) !== 0o600 ||
1406
- !sameFileGeneration(beforeGeneration, openedGeneration)) {
1407
- throw new BridgeError("REGISTRY_RACED", "The native Codex registry record changed while opening.");
1069
+ opened.size > this.#context.limits.maxRegistryBytes) {
1070
+ throw new BridgeError("REGISTRY_RACED", "The native Codex registry record failed its exact ownership policy.");
1408
1071
  }
1409
1072
  const serialized = await handle.readFile({ encoding: "utf8" });
1410
- if (byteLength(serialized) > this.#limits.maxRegistryBytes) {
1073
+ if (byteLength(serialized) > this.#context.limits.maxRegistryBytes) {
1411
1074
  throw new BridgeError("REGISTRY_TOO_LARGE", "The native Codex registry record exceeded its bound.");
1412
1075
  }
1413
- const after = await lstat(registryPath);
1414
- if (after.isSymbolicLink() ||
1415
- !after.isFile() ||
1416
- after.uid !== this.#expectedUid ||
1417
- exactMode(after.mode) !== 0o600 ||
1418
- !sameFileGeneration(openedGeneration, generationOf(after))) {
1076
+ const [afterOpen, afterPath] = await Promise.all([
1077
+ handle.stat(),
1078
+ lstat(registryPath),
1079
+ ]);
1080
+ if (afterPath.isSymbolicLink() ||
1081
+ !afterPath.isFile() ||
1082
+ !sameFileGeneration(openedGeneration, generationOf(afterOpen)) ||
1083
+ !sameFileGeneration(openedGeneration, generationOf(afterPath))) {
1419
1084
  throw new BridgeError("REGISTRY_RACED", "The native Codex registry record changed while reading.");
1420
1085
  }
1421
1086
  return { serialized, generation: openedGeneration };
@@ -1447,46 +1112,21 @@ export class ClaudePeerListener {
1447
1112
  flag: "wx",
1448
1113
  mode: 0o600,
1449
1114
  });
1450
- let generation;
1451
- try {
1452
- const created = await lstat(temporaryPath);
1453
- if (created.isSymbolicLink() ||
1454
- !created.isFile() ||
1455
- created.uid !== this.#expectedUid ||
1456
- exactMode(created.mode) !== 0o600) {
1457
- throw new BridgeError("REGISTRY_RACED", "The native Codex temporary registry record is unsafe.");
1458
- }
1459
- generation = generationOf(created);
1460
- const handle = await open(temporaryPath, fsConstants.O_RDWR | fsConstants.O_NOFOLLOW);
1461
- try {
1462
- const openedStat = await handle.stat();
1463
- const opened = generationOf(openedStat);
1464
- if (!openedStat.isFile() ||
1465
- openedStat.uid !== this.#expectedUid ||
1466
- exactMode(openedStat.mode) !== 0o600 ||
1467
- !sameFileGeneration(generation, opened)) {
1468
- throw new BridgeError("REGISTRY_RACED", "The native Codex temporary registry record changed while opening.");
1469
- }
1470
- await handle.sync();
1471
- }
1472
- finally {
1473
- await handle.close();
1474
- }
1475
- return { path: temporaryPath, generation };
1476
- }
1477
- catch (error) {
1478
- if (generation !== undefined) {
1479
- await this.#cleanupOwnedFile(temporaryPath, generation).catch(() => undefined);
1480
- }
1481
- throw error;
1115
+ const created = await lstat(temporaryPath);
1116
+ if (created.isSymbolicLink() ||
1117
+ !created.isFile() ||
1118
+ created.uid !== this.#context.expectedUid ||
1119
+ exactMode(created.mode) !== 0o600) {
1120
+ throw new BridgeError("REGISTRY_RACED", "The native Codex temporary registry record is unsafe.");
1482
1121
  }
1122
+ return { path: temporaryPath, generation: generationOf(created) };
1483
1123
  }
1484
1124
  async #cleanupOwnedFile(candidate, generation) {
1485
1125
  try {
1486
1126
  const current = await lstat(candidate);
1487
1127
  if (current.isFile() &&
1488
1128
  !current.isSymbolicLink() &&
1489
- current.uid === this.#expectedUid &&
1129
+ current.uid === this.#context.expectedUid &&
1490
1130
  exactMode(current.mode) === 0o600 &&
1491
1131
  sameFileGeneration(generationOf(current), generation)) {
1492
1132
  await unlink(candidate);
@@ -1497,23 +1137,6 @@ export class ClaudePeerListener {
1497
1137
  throw error;
1498
1138
  }
1499
1139
  }
1500
- async #syncRegistryDirectory() {
1501
- await this.#assertRegistryDirectory();
1502
- const handle = await open(this.#sessionsDir, fsConstants.O_RDONLY);
1503
- try {
1504
- const opened = await handle.stat();
1505
- if (!opened.isDirectory() ||
1506
- opened.uid !== this.#expectedUid ||
1507
- exactMode(opened.mode) !== 0o700 ||
1508
- !sameDirectoryGeneration({ dev: opened.dev, ino: opened.ino }, this.#sessionsGeneration)) {
1509
- throw new BridgeError("UNSAFE_PEER_DIRECTORY", "The native peer registry directory changed while syncing.");
1510
- }
1511
- await handle.sync();
1512
- }
1513
- finally {
1514
- await handle.close();
1515
- }
1516
- }
1517
1140
  #newAdvertisedRecord(name, cwd) {
1518
1141
  if (!EMBASSY_SOURCE_NAME_PATTERN.test(name) || !ALIAS_PATTERN.test(name)) {
1519
1142
  throw new BridgeError("INVALID_CODEX_PEER_NAME", "A native source peer name must use its provider prefix.");
@@ -1521,7 +1144,7 @@ export class ClaudePeerListener {
1521
1144
  if (!path.isAbsolute(cwd) || cwd.includes("\0")) {
1522
1145
  throw new BridgeError("INVALID_CODEX_PEER_CWD", "A native Codex peer requires an absolute working directory.");
1523
1146
  }
1524
- const sessionId = this.#createId();
1147
+ const sessionId = this.#context.createId();
1525
1148
  if (!UUID_PATTERN.test(sessionId)) {
1526
1149
  throw new BridgeError("INVALID_CODEX_PEER_SESSION_ID", "The configured ID source did not produce a native peer session UUID.");
1527
1150
  }
@@ -1533,7 +1156,7 @@ export class ClaudePeerListener {
1533
1156
  cwd,
1534
1157
  startedAt: now,
1535
1158
  procStart: new Date(now - process.uptime() * 1_000).toString(),
1536
- version: this.#attestedClaudeCodeVersion,
1159
+ version: "unknown",
1537
1160
  peerProtocol: CLAUDE_PEER_COMPATIBILITY.peerProtocol,
1538
1161
  kind: "interactive",
1539
1162
  entrypoint: "cli",
@@ -1545,8 +1168,8 @@ export class ClaudePeerListener {
1545
1168
  };
1546
1169
  }
1547
1170
  async #mutateRegistry(operation, mutation) {
1548
- return await this.#runRegistryMutation(async () => {
1549
- await this.#registryOperationHook?.({
1171
+ return await this.#context.runRegistryMutation(async () => {
1172
+ await this.#context.registryOperationHook?.({
1550
1173
  operation,
1551
1174
  phase: "entered",
1552
1175
  generation: this.generation,
@@ -1555,7 +1178,7 @@ export class ClaudePeerListener {
1555
1178
  return await mutation();
1556
1179
  }
1557
1180
  finally {
1558
- await this.#registryOperationHook?.({
1181
+ await this.#context.registryOperationHook?.({
1559
1182
  operation,
1560
1183
  phase: "exited",
1561
1184
  generation: this.generation,
@@ -1565,17 +1188,13 @@ export class ClaudePeerListener {
1565
1188
  }
1566
1189
  async advertise(name, cwd) {
1567
1190
  await this.#mutateRegistry("advertise", async () => {
1568
- if (this.#isClosingOrClosed()) {
1191
+ if (this.closed) {
1569
1192
  throw new BridgeError("CLAUDE_PEER_LISTENER_CLOSED", "The Claude peer callback listener is closed.");
1570
1193
  }
1571
- if (this.#preparedGeneration !== undefined) {
1572
- throw new BridgeError("CODEX_PEER_PREPARED_NOT_ACTIVE", "A prepared native Codex listener must use atomic succession publication.");
1573
- }
1574
- if (this.#advertisedRecord !== undefined &&
1575
- this.#advertisedRecord.name !== name) {
1194
+ if (this.#advertisement !== undefined && this.#advertisement.record.name !== name) {
1576
1195
  throw new BridgeError("CODEX_PEER_ALREADY_ADVERTISED", "This gateway process already advertises another Codex peer.");
1577
1196
  }
1578
- if (this.#advertisedRecord?.name === name)
1197
+ if (this.#advertisement?.record.name === name)
1579
1198
  return;
1580
1199
  const record = this.#newAdvertisedRecord(name, cwd);
1581
1200
  await this.#assertRegistryDirectory();
@@ -1588,197 +1207,52 @@ export class ClaudePeerListener {
1588
1207
  if (!exact.matches) {
1589
1208
  throw new BridgeError("REGISTRY_RACED", "The native Codex registry record could not be exactly confirmed.");
1590
1209
  }
1591
- this.#advertisedRecord = record;
1592
- this.#advertisedGeneration = exact.generation;
1210
+ this.#advertisement = { record, generation: exact.generation };
1593
1211
  });
1594
1212
  }
1595
- async #classifyPublication(current, record) {
1596
- const sourceGeneration = this.#publicationSourceGeneration;
1597
- if (sourceGeneration !== undefined) {
1598
- const published = await this.#recordMatches(record, sourceGeneration);
1599
- if (published.matches) {
1600
- this.#advertisedRecord = record;
1601
- this.#advertisedGeneration = published.generation;
1602
- this.#publicationConfirmed = true;
1603
- return "published";
1604
- }
1605
- }
1606
- return (await this.#oldPublicationStillPresent(current))
1607
- ? "not_published"
1608
- : "unknown";
1609
- }
1610
- async #oldPublicationStillPresent(current) {
1611
- const oldRecord = current.#advertisedRecord;
1612
- const oldGeneration = current.#advertisedGeneration;
1613
- if (oldRecord === undefined || oldGeneration === undefined)
1614
- return false;
1615
- return (await current.#recordMatches(oldRecord, oldGeneration)).matches;
1616
- }
1617
- async publishReplacing(current, name, cwd) {
1618
- return await this.#mutateRegistry("publish", async () => await this.#publishReplacingLocked(current, name, cwd));
1619
- }
1620
- async #publishReplacingLocked(current, name, cwd) {
1621
- if (this.#isClosingOrClosed() ||
1622
- current.#isClosingOrClosed() ||
1623
- this === current ||
1624
- this.#preparedGeneration === undefined ||
1625
- this.generation === current.generation ||
1626
- !current.#activationGranted ||
1627
- this.#owner !== current.#owner ||
1628
- this.#sessionsDir !== current.#sessionsDir ||
1629
- this.#expectedUid !== current.#expectedUid ||
1630
- !sameDirectoryGeneration(this.#sessionsGeneration, current.#sessionsGeneration)) {
1631
- throw new BridgeError("CODEX_PEER_SUCCESSION_INVALID", "Native Codex listener succession requires two live generations from one adapter owner.");
1632
- }
1633
- if (!this.#inboundQuiesced || !current.#inboundQuiesced) {
1634
- throw new BridgeError("CODEX_PEER_SUCCESSION_NOT_QUIESCED", "Both native Codex listener generations must be inbound-quiesced before publication.");
1635
- }
1636
- if (current.#advertisedRecord === undefined ||
1637
- current.#advertisedGeneration === undefined ||
1638
- !current.#recordBelongsToThisListener(current.#advertisedRecord)) {
1639
- throw new BridgeError("CODEX_PEER_SUCCESSION_CURRENT_UNOWNED", "The current native Codex listener does not own an exact advertised record.");
1640
- }
1641
- if (name === current.#advertisedRecord.name) {
1642
- throw new BridgeError("CODEX_PEER_SUCCESSION_ALIAS_UNCHANGED", "Native Codex succession requires a distinct alias.");
1643
- }
1644
- if (this.#preparedRecord === undefined) {
1645
- this.#preparedRecord = this.#newAdvertisedRecord(name, cwd);
1646
- }
1647
- else if (this.#preparedRecord.name !== name ||
1648
- this.#preparedRecord.cwd !== cwd) {
1649
- throw new BridgeError("CODEX_PEER_SUCCESSION_CHANGED", "A prepared native Codex publication cannot change identity after preparation.");
1650
- }
1651
- const record = this.#preparedRecord;
1652
- if (this.#publicationAttempted) {
1653
- return await this.#classifyPublication(current, record);
1654
- }
1655
- let temporary;
1656
- let renameInvoked = false;
1657
- try {
1658
- temporary = await this.#createRegistryTemporary(record);
1659
- this.#publicationSourceGeneration = temporary.generation;
1660
- await this.#registryPublicationHook?.("before_rename");
1661
- if (this.#isClosingOrClosed() ||
1662
- current.#isClosingOrClosed() ||
1663
- !this.#inboundQuiesced ||
1664
- !current.#inboundQuiesced) {
1665
- return (await this.#oldPublicationStillPresent(current))
1666
- ? "not_published"
1667
- : "unknown";
1213
+ async updateAdvertisedStatus(status) {
1214
+ await this.#mutateRegistry("status", async () => {
1215
+ const owned = this.#advertisement;
1216
+ if (this.closed || owned === undefined || owned.record.status === status)
1217
+ return;
1218
+ if (!(await this.#recordMatches(owned.record, owned.generation)).matches) {
1219
+ this.#advertisement = undefined;
1220
+ return;
1668
1221
  }
1669
- const currentExact = await current.#recordMatches(current.#advertisedRecord, current.#advertisedGeneration);
1670
- if (!currentExact.matches)
1671
- return "unknown";
1672
- renameInvoked = true;
1673
- this.#publicationAttempted = true;
1674
- await this.#registryRename(temporary.path, this.#registryPath());
1675
- temporary = undefined;
1676
- await this.#registryPublicationHook?.("after_rename");
1677
- await this.#syncRegistryDirectory();
1678
- return await this.#classifyPublication(current, record);
1679
- }
1680
- catch {
1681
- if (!renameInvoked) {
1682
- return (await this.#oldPublicationStillPresent(current))
1683
- ? "not_published"
1684
- : "unknown";
1222
+ const now = Date.now();
1223
+ const record = { ...owned.record, status, updatedAt: now, statusUpdatedAt: now };
1224
+ const temporary = await this.#createRegistryTemporary(record);
1225
+ try {
1226
+ if (!(await this.#recordMatches(owned.record, owned.generation)).matches)
1227
+ return;
1228
+ await this.#context.registryRename(temporary.path, this.#registryPath());
1229
+ const exact = await this.#recordMatches(record, temporary.generation);
1230
+ if (!exact.matches) {
1231
+ throw new BridgeError("REGISTRY_RACED", "The updated native Codex registry record could not be exactly confirmed.");
1232
+ }
1233
+ this.#advertisement = { record, generation: exact.generation };
1685
1234
  }
1686
- return await this.#classifyPublication(current, record);
1687
- }
1688
- finally {
1689
- if (temporary !== undefined) {
1235
+ finally {
1690
1236
  await this.#cleanupOwnedFile(temporary.path, temporary.generation).catch(() => undefined);
1691
1237
  }
1692
- }
1693
- }
1694
- async updateAdvertisedStatus(status) {
1695
- await this.#mutateRegistry("status", async () => await this.#updateAdvertisedStatusLocked(status));
1696
- }
1697
- async #updateAdvertisedStatusLocked(status) {
1698
- if (this.#isClosingOrClosed() ||
1699
- this.#advertisedRecord === undefined ||
1700
- this.#advertisedRecord.status === status) {
1701
- return;
1702
- }
1703
- const currentExact = await this.#recordMatches(this.#advertisedRecord, this.#advertisedGeneration);
1704
- if (!currentExact.matches) {
1705
- this.#advertisedRecord = undefined;
1706
- this.#advertisedGeneration = undefined;
1707
- return;
1708
- }
1709
- const now = Date.now();
1710
- const record = {
1711
- ...this.#advertisedRecord,
1712
- status,
1713
- updatedAt: now,
1714
- statusUpdatedAt: now,
1715
- };
1716
- const temporary = await this.#createRegistryTemporary(record);
1717
- try {
1718
- const stillCurrent = await this.#recordMatches(this.#advertisedRecord, this.#advertisedGeneration);
1719
- if (!stillCurrent.matches)
1720
- return;
1721
- await this.#registryRename(temporary.path, this.#registryPath());
1722
- await this.#syncRegistryDirectory();
1723
- const exact = await this.#recordMatches(record, temporary.generation);
1724
- if (!exact.matches) {
1725
- throw new BridgeError("REGISTRY_RACED", "The updated native Codex registry record could not be exactly confirmed.");
1726
- }
1727
- this.#advertisedRecord = record;
1728
- this.#advertisedGeneration = exact.generation;
1729
- }
1730
- finally {
1731
- await this.#cleanupOwnedFile(temporary.path, temporary.generation).catch(() => undefined);
1732
- }
1238
+ });
1733
1239
  }
1734
1240
  async unadvertise(name) {
1735
- await this.#mutateRegistry("unadvertise", async () => await this.#unadvertiseLocked(name));
1736
- }
1737
- async #unadvertiseLocked(name) {
1738
- if (this.#advertisedRecord === undefined ||
1739
- (name !== undefined && name !== this.#advertisedRecord.name)) {
1740
- return;
1741
- }
1742
- const record = this.#advertisedRecord;
1743
- const generation = this.#advertisedGeneration;
1744
- this.#advertisedRecord = undefined;
1745
- this.#advertisedGeneration = undefined;
1746
- if (generation === undefined)
1747
- return;
1748
- const exact = await this.#recordMatches(record, generation);
1749
- if (!exact.matches)
1750
- return;
1751
- await unlink(this.#registryPath()).catch((error) => {
1752
- if (error.code !== "ENOENT")
1753
- throw error;
1241
+ await this.#mutateRegistry("unadvertise", async () => {
1242
+ const owned = this.#advertisement;
1243
+ if (owned === undefined || (name !== undefined && name !== owned.record.name))
1244
+ return;
1245
+ this.#advertisement = undefined;
1246
+ if (!(await this.#recordMatches(owned.record, owned.generation)).matches)
1247
+ return;
1248
+ await unlink(this.#registryPath()).catch((error) => {
1249
+ if (error.code !== "ENOENT")
1250
+ throw error;
1251
+ });
1754
1252
  });
1755
1253
  }
1756
- grantSuccessionActivation() {
1757
- if (this.#isClosingOrClosed()) {
1758
- throw new BridgeError("CLAUDE_PEER_LISTENER_CLOSED", "The Claude peer callback listener is closed.");
1759
- }
1760
- if (this.#preparedGeneration === undefined ||
1761
- !this.#publicationAttempted ||
1762
- !this.#publicationConfirmed ||
1763
- this.#publicationSourceGeneration === undefined ||
1764
- this.#advertisedRecord === undefined ||
1765
- this.#advertisedGeneration === undefined ||
1766
- !sameFileGeneration(this.#publicationSourceGeneration, this.#advertisedGeneration)) {
1767
- throw new BridgeError("CODEX_PEER_PREPARED_NOT_ACTIVE", "A prepared native Codex listener requires exact confirmed publication before activation can be granted.");
1768
- }
1769
- this.#activationGranted = true;
1770
- }
1771
- resumeInbound() {
1772
- if (this.#isClosingOrClosed()) {
1773
- throw new BridgeError("CLAUDE_PEER_LISTENER_CLOSED", "The Claude peer callback listener is closed.");
1774
- }
1775
- if (!this.#activationGranted) {
1776
- throw new BridgeError("CODEX_PEER_PREPARED_NOT_ACTIVE", "A prepared native Codex listener cannot resume before durable succession activation.");
1777
- }
1778
- this.#inboundQuiesced = false;
1779
- }
1780
1254
  async acknowledge(receiptHandle, status, diagnostic) {
1781
- if (this.#isClosingOrClosed()) {
1255
+ if (this.closed) {
1782
1256
  throw new BridgeError("CLAUDE_PEER_LISTENER_CLOSED", "The Claude peer callback listener is closed.");
1783
1257
  }
1784
1258
  const receipt = this.#inboundReceipts.get(receiptHandle);
@@ -1813,7 +1287,7 @@ export class ClaudePeerListener {
1813
1287
  * its later exact terminal acknowledgement or explicit release.
1814
1288
  */
1815
1289
  async notifyInboundProgress(receiptHandle, progress) {
1816
- if (this.#isClosingOrClosed()) {
1290
+ if (this.closed) {
1817
1291
  throw new BridgeError("CLAUDE_PEER_LISTENER_CLOSED", "The Claude peer callback listener is closed.");
1818
1292
  }
1819
1293
  const receipt = this.#inboundReceipts.get(receiptHandle);
@@ -1830,7 +1304,7 @@ export class ClaudePeerListener {
1830
1304
  if (receipt.stallNotification !== "available") {
1831
1305
  throw new BridgeError("CLAUDE_PEER_PROGRESS_ALREADY_NOTIFIED", "The native Claude peer receipt already has a stall notification.");
1832
1306
  }
1833
- if (this.#deliveryNotices === "quiet") {
1307
+ if (this.#context.deliveryNotices === "quiet") {
1834
1308
  receipt.stallNotification = "settled";
1835
1309
  return { transportStatus: "suppressed" };
1836
1310
  }
@@ -1839,16 +1313,16 @@ export class ClaudePeerListener {
1839
1313
  `queued-for-ms="${queuedForMs}">`;
1840
1314
  const detailedContent = [
1841
1315
  openingTag,
1842
- CLAUDE_PEER_NOTICE_COPY[this.#locale].stall,
1316
+ CLAUDE_PEER_NOTICE_COPY[this.#context.locale].stall,
1843
1317
  "</gateway-delivery-stall>",
1844
1318
  ].join("\n");
1845
- const messageId = this.#createId();
1319
+ const messageId = this.#context.createId();
1846
1320
  let progressFrame;
1847
1321
  try {
1848
1322
  progressFrame = encodeClaudePeerUserFrame({
1849
1323
  messageId,
1850
1324
  content: detailedContent,
1851
- maxFrameBytes: this.#limits.maxFrameBytes,
1325
+ maxFrameBytes: this.#context.limits.maxFrameBytes,
1852
1326
  });
1853
1327
  }
1854
1328
  catch (error) {
@@ -1858,7 +1332,7 @@ export class ClaudePeerListener {
1858
1332
  progressFrame = encodeClaudePeerUserFrame({
1859
1333
  messageId,
1860
1334
  content: `${openingTag.slice(0, -1)}/>`,
1861
- maxFrameBytes: this.#limits.maxFrameBytes,
1335
+ maxFrameBytes: this.#context.limits.maxFrameBytes,
1862
1336
  });
1863
1337
  }
1864
1338
  receipt.stallNotification = "writing";
@@ -1912,18 +1386,18 @@ export class ClaudePeerListener {
1912
1386
  from: this.address,
1913
1387
  orig_msg_id: receipt.originalMessageId,
1914
1388
  msgV: 1,
1915
- msg_id: this.#createId(),
1389
+ msg_id: this.#context.createId(),
1916
1390
  })}\n`, "utf8");
1917
- const diagnosticFrame = diagnostic === undefined || this.#deliveryNotices !== "verbose"
1391
+ const diagnosticFrame = diagnostic === undefined || this.#context.deliveryNotices !== "verbose"
1918
1392
  ? undefined
1919
1393
  : encodeClaudePeerUserFrame({
1920
- messageId: this.#createId(),
1394
+ messageId: this.#context.createId(),
1921
1395
  content: [
1922
1396
  `<gateway-delivery-diagnostic status="expired" code="${diagnostic.code}">`,
1923
- CLAUDE_PEER_NOTICE_COPY[this.#locale].diagnostic,
1397
+ CLAUDE_PEER_NOTICE_COPY[this.#context.locale].diagnostic,
1924
1398
  "</gateway-delivery-diagnostic>",
1925
1399
  ].join("\n"),
1926
- maxFrameBytes: this.#limits.maxFrameBytes,
1400
+ maxFrameBytes: this.#context.limits.maxFrameBytes,
1927
1401
  });
1928
1402
  const payload = diagnosticFrame === undefined
1929
1403
  ? statusFrame
@@ -1933,31 +1407,9 @@ export class ClaudePeerListener {
1933
1407
  async #writeInboundPayload(receipt, payload) {
1934
1408
  let writeStarted = false;
1935
1409
  try {
1936
- const binding = await this.#resolveSessionBinding(receipt.sourceSessionId);
1937
- await new Promise((resolve, reject) => {
1938
- const socket = this.#connect(binding.record.messagingSocketPath);
1939
- let settled = false;
1940
- const finish = (error) => {
1941
- if (settled)
1942
- return;
1943
- settled = true;
1944
- socket.destroy();
1945
- if (error === undefined)
1946
- resolve();
1947
- else
1948
- reject(error);
1949
- };
1950
- socket.setTimeout(this.#limits.connectTimeoutMs, () => finish(new Error("peer status timeout")));
1951
- socket.once("error", finish);
1952
- socket.once("connect", () => {
1953
- writeStarted = true;
1954
- try {
1955
- socket.end(payload, () => finish());
1956
- }
1957
- catch (error) {
1958
- finish(error);
1959
- }
1960
- });
1410
+ const binding = await this.#context.resolveSessionBinding(receipt.sourceSessionId);
1411
+ await writeSocketPayload(this.#context.connect, binding.record.messagingSocketPath, payload, this.#context.limits.connectTimeoutMs, () => {
1412
+ writeStarted = true;
1961
1413
  });
1962
1414
  }
1963
1415
  catch (error) {
@@ -1970,63 +1422,15 @@ export class ClaudePeerListener {
1970
1422
  }
1971
1423
  return { transportStatus: "transport_written" };
1972
1424
  }
1973
- track(messageId, binding, receiptDeadlineAt) {
1974
- if (this.#isClosingOrClosed()) {
1975
- throw new BridgeError("CLAUDE_PEER_LISTENER_CLOSED", "The Claude peer callback listener is closed.");
1976
- }
1977
- if (this.#pending.size >= this.#limits.maxPendingReceipts) {
1978
- throw new BridgeError("CLAUDE_PEER_RECEIPT_LIMIT", "The bounded Claude peer receipt table is full.", true);
1979
- }
1980
- if (this.#pending.has(messageId)) {
1981
- throw new BridgeError("CLAUDE_PEER_MESSAGE_ID_COLLISION", "The peer message ID collided with an outstanding receipt.");
1982
- }
1983
- const now = this.#now();
1984
- const deadlineAt = receiptDeadlineAt ?? now + this.#limits.receiptDeadlineMs;
1985
- const timer = setTimeout(() => {
1986
- const pending = this.#pending.get(messageId);
1987
- if (pending === undefined)
1988
- return;
1989
- this.#pending.delete(messageId);
1990
- void this.#emitReceipt({
1991
- messageId,
1992
- status: pendingReceiptDeadlineStatus(pending),
1993
- trust: "untrusted_same_uid_peer",
1994
- });
1995
- }, Math.max(1, deadlineAt - now));
1996
- timer.unref();
1997
- this.#pending.set(messageId, {
1998
- binding,
1999
- state: "pending",
2000
- writeEvidence: "none",
2001
- deadlineAt,
2002
- timer,
2003
- });
2004
- }
2005
- recordTransportOutcome(messageId, outcome) {
2006
- const pending = this.#pending.get(messageId);
2007
- if (pending === undefined)
2008
- return;
2009
- if (pending.writeEvidence !== "transport_written" ||
2010
- outcome === "transport_written") {
2011
- pending.writeEvidence = outcome;
2012
- }
2013
- }
2014
- untrack(messageId) {
2015
- const pending = this.#pending.get(messageId);
2016
- if (pending === undefined)
2017
- return;
2018
- clearTimeout(pending.timer);
2019
- this.#pending.delete(messageId);
2020
- }
2021
1425
  #accept(socket) {
2022
- if (this.#isClosingOrClosed() ||
2023
- this.#connections.size >= this.#limits.maxConnections) {
1426
+ if (this.closed ||
1427
+ this.#connections.size >= this.#context.limits.maxConnections) {
2024
1428
  socket.destroy();
2025
1429
  void this.#notice("CONNECTION_LIMIT");
2026
1430
  return;
2027
1431
  }
2028
1432
  this.#connections.add(socket);
2029
- socket.setTimeout(this.#limits.connectionIdleMs);
1433
+ socket.setTimeout(this.#context.limits.connectionIdleMs);
2030
1434
  let buffered = Buffer.alloc(0);
2031
1435
  let frames = 0;
2032
1436
  let chain = Promise.resolve();
@@ -2045,9 +1449,9 @@ export class ClaudePeerListener {
2045
1449
  if (rejected)
2046
1450
  return;
2047
1451
  buffered = Buffer.concat([buffered, chunk]);
2048
- if (buffered.length > this.#limits.maxFrameBytes + 1) {
1452
+ if (buffered.length > this.#context.limits.maxFrameBytes + 1) {
2049
1453
  const newline = buffered.indexOf(0x0a);
2050
- if (newline < 0 || newline > this.#limits.maxFrameBytes) {
1454
+ if (newline < 0 || newline > this.#context.limits.maxFrameBytes) {
2051
1455
  reject("FRAME_TOO_LARGE");
2052
1456
  return;
2053
1457
  }
@@ -2056,20 +1460,20 @@ export class ClaudePeerListener {
2056
1460
  const newline = buffered.indexOf(0x0a);
2057
1461
  if (newline < 0)
2058
1462
  break;
2059
- if (newline > this.#limits.maxFrameBytes) {
1463
+ if (newline > this.#context.limits.maxFrameBytes) {
2060
1464
  reject("FRAME_TOO_LARGE");
2061
1465
  break;
2062
1466
  }
2063
1467
  const line = buffered.subarray(0, newline);
2064
1468
  buffered = buffered.subarray(newline + 1);
2065
1469
  frames += 1;
2066
- if (frames > this.#limits.maxFramesPerConnection) {
1470
+ if (frames > this.#context.limits.maxFramesPerConnection) {
2067
1471
  reject("INVALID_FRAME");
2068
1472
  break;
2069
1473
  }
2070
1474
  let frame;
2071
1475
  try {
2072
- frame = parseFrame(line, this.#limits.maxFrameBytes);
1476
+ frame = parseFrame(line, this.#context.limits.maxFrameBytes);
2073
1477
  }
2074
1478
  catch (error) {
2075
1479
  if (error instanceof BridgeError &&
@@ -2086,7 +1490,7 @@ export class ClaudePeerListener {
2086
1490
  break;
2087
1491
  }
2088
1492
  if (this.#queuedFrames >=
2089
- this.#limits.maxConnections * this.#limits.maxFramesPerConnection) {
1493
+ this.#context.limits.maxConnections * this.#context.limits.maxFramesPerConnection) {
2090
1494
  reject("CONNECTION_LIMIT");
2091
1495
  break;
2092
1496
  }
@@ -2112,7 +1516,7 @@ export class ClaudePeerListener {
2112
1516
  }
2113
1517
  });
2114
1518
  }
2115
- if (!rejected && buffered.length > this.#limits.maxFrameBytes) {
1519
+ if (!rejected && buffered.length > this.#context.limits.maxFrameBytes) {
2116
1520
  reject("FRAME_TOO_LARGE");
2117
1521
  }
2118
1522
  });
@@ -2122,10 +1526,11 @@ export class ClaudePeerListener {
2122
1526
  });
2123
1527
  }
2124
1528
  async #handleFrame(frame, rejectTransport) {
2125
- if (this.#isClosingOrClosed())
1529
+ if (this.closed)
2126
1530
  return;
2127
1531
  if (frame.type === "control") {
2128
- await this.#handleControl(frame);
1532
+ // Native outbound status is accepted for wire compatibility but is not
1533
+ // settlement authority. A confirmed mailbox write is terminal.
2129
1534
  return;
2130
1535
  }
2131
1536
  if (this.#inboundQuiesced) {
@@ -2135,16 +1540,16 @@ export class ClaudePeerListener {
2135
1540
  let binding;
2136
1541
  if (frame.from !== undefined) {
2137
1542
  try {
2138
- binding = await this.#resolveReplyAddress(frame.from);
1543
+ binding = await this.#context.resolveReplyAddress(frame.from);
2139
1544
  }
2140
1545
  catch {
2141
1546
  await this.#notice("UNREGISTERED_REPLY_ADDRESS");
2142
1547
  return;
2143
1548
  }
2144
1549
  }
2145
- const inboundId = this.#createId();
1550
+ const inboundId = this.#context.createId();
2146
1551
  if (binding !== undefined && frame.messageId !== undefined) {
2147
- if (this.#inboundReceipts.size >= this.#limits.maxPendingReceipts) {
1552
+ if (this.#inboundReceipts.size >= this.#context.limits.maxPendingReceipts) {
2148
1553
  await this.#notice("RECEIPT_LIMIT");
2149
1554
  if (this.#capacitySettlement !== undefined) {
2150
1555
  // Both the configured receipt table and its single terminal-only
@@ -2171,7 +1576,7 @@ export class ClaudePeerListener {
2171
1576
  stallNotification: "available",
2172
1577
  });
2173
1578
  }
2174
- await invokeHook(this.#options.onMessage, {
1579
+ await invokeHook(this.#context.options.onMessage, {
2175
1580
  inboundId,
2176
1581
  content: frame.content,
2177
1582
  ...(binding === undefined
@@ -2190,7 +1595,7 @@ export class ClaudePeerListener {
2190
1595
  });
2191
1596
  }
2192
1597
  async #attemptCapacitySettlement(settlement) {
2193
- if (this.#isClosingOrClosed() || this.#capacitySettlement !== settlement) {
1598
+ if (this.closed || this.#capacitySettlement !== settlement) {
2194
1599
  return;
2195
1600
  }
2196
1601
  settlement.attempts += 1;
@@ -2205,7 +1610,7 @@ export class ClaudePeerListener {
2205
1610
  return;
2206
1611
  const retryable = error instanceof BridgeError &&
2207
1612
  error.recoverable &&
2208
- !this.#isClosingOrClosed();
1613
+ !this.closed;
2209
1614
  if (!retryable ||
2210
1615
  settlement.attempts >= CAPACITY_SETTLEMENT_MAX_ATTEMPTS) {
2211
1616
  // An ambiguous write must never be replayed. Exhausted clean
@@ -2222,74 +1627,9 @@ export class ClaudePeerListener {
2222
1627
  settlement.retryTimer.unref();
2223
1628
  }
2224
1629
  }
2225
- async #handleControl(frame) {
2226
- const pending = this.#pending.get(frame.originalMessageId);
2227
- if (pending === undefined) {
2228
- await this.#notice("UNKNOWN_RECEIPT");
2229
- return;
2230
- }
2231
- let currentBinding;
2232
- try {
2233
- // Receipt authority is the stable Claude session UUID, not the
2234
- // short-lived send-time registry/socket lease. Re-discover the exact
2235
- // session on every receipt so legitimate held/terminal frames remain
2236
- // valid for the full message deadline and across socket rotation.
2237
- currentBinding = await this.#resolveSessionBinding(pending.binding.targetId);
2238
- }
2239
- catch {
2240
- await this.#notice("UNKNOWN_RECEIPT");
2241
- return;
2242
- }
2243
- if (frame.from !== `uds:${currentBinding.record.messagingSocketPath}`) {
2244
- await this.#notice("UNKNOWN_RECEIPT");
2245
- return;
2246
- }
2247
- pending.binding = currentBinding;
2248
- if (this.#now() >= pending.deadlineAt) {
2249
- clearTimeout(pending.timer);
2250
- this.#pending.delete(frame.originalMessageId);
2251
- await this.#emitReceipt({
2252
- messageId: frame.originalMessageId,
2253
- status: pendingReceiptDeadlineStatus(pending),
2254
- trust: "untrusted_same_uid_peer",
2255
- });
2256
- return;
2257
- }
2258
- if (frame.status === "held") {
2259
- if (pending.state !== "pending") {
2260
- await this.#notice("INVALID_RECEIPT_TRANSITION");
2261
- return;
2262
- }
2263
- pending.state = "held";
2264
- pending.writeEvidence = "transport_written";
2265
- await this.#emitReceipt({
2266
- messageId: frame.originalMessageId,
2267
- status: "held",
2268
- trust: "untrusted_same_uid_peer",
2269
- });
2270
- return;
2271
- }
2272
- clearTimeout(pending.timer);
2273
- this.#pending.delete(frame.originalMessageId);
2274
- await this.#emitReceipt({
2275
- messageId: frame.originalMessageId,
2276
- status: frame.status === "delivered"
2277
- ? "released"
2278
- : frame.status,
2279
- trust: "untrusted_same_uid_peer",
2280
- });
2281
- }
2282
- async #emitReceipt(event) {
2283
- try {
2284
- await invokeHook(this.#options.onReceipt, event);
2285
- }
2286
- catch {
2287
- await this.#notice("CALLBACK_ERROR");
2288
- }
2289
- }
2290
1630
  async #notice(code) {
2291
1631
  try {
2292
- await invokeHook(this.#options.onProtocolNotice, { code });
1632
+ await invokeHook(this.#context.options.onProtocolNotice, { code });
2293
1633
  }
2294
1634
  catch {
2295
1635
  // Protocol callbacks are observational. Never feed callback failures
@@ -2315,34 +1655,17 @@ export class ClaudePeerListener {
2315
1655
  catch (error) {
2316
1656
  errors.push(error);
2317
1657
  }
2318
- const unsettledReceipts = [...this.#pending.entries()];
2319
- for (const pending of this.#pending.values())
2320
- clearTimeout(pending.timer);
2321
- this.#pending.clear();
2322
1658
  this.#inboundReceipts.clear();
2323
1659
  if (this.#capacitySettlement?.retryTimer !== undefined) {
2324
1660
  clearTimeout(this.#capacitySettlement.retryTimer);
2325
1661
  }
2326
1662
  this.#capacitySettlement = undefined;
2327
- const receiptResults = await Promise.allSettled(unsettledReceipts.map(async ([messageId, pending]) => this.#emitReceipt({
2328
- messageId,
2329
- status: pendingReceiptDeadlineStatus(pending) === "unconfirmed"
2330
- ? "unconfirmed"
2331
- : "ambiguous",
2332
- trust: "untrusted_same_uid_peer",
2333
- })));
2334
- for (const result of receiptResults) {
2335
- if (result.status === "rejected")
2336
- errors.push(result.reason);
2337
- }
2338
1663
  for (const socket of this.#connections)
2339
1664
  socket.destroy();
2340
1665
  this.#connections.clear();
2341
- let callbackPathOwned = false;
2342
1666
  try {
2343
- const beforeClose = await lstat(this.#socketPath);
2344
- if (!beforeClose.isSocket() ||
2345
- !sameSocketGeneration({ dev: beforeClose.dev, ino: beforeClose.ino }, this.#socketGeneration)) {
1667
+ if (await socketOwnership(this.#socketPath, this.#socketGeneration) !==
1668
+ "owned") {
2346
1669
  // Node's net.Server.close() unlinks its original pathname even when
2347
1670
  // another same-user object replaced it. Do not call it after an
2348
1671
  // observed generation change. The now-unreachable descriptor is
@@ -2351,43 +1674,21 @@ export class ClaudePeerListener {
2351
1674
  errors.push(new BridgeError("CLAUDE_PEER_CALLBACK_CHANGED", "The callback path changed; foreign replacement was preserved."));
2352
1675
  }
2353
1676
  else {
2354
- callbackPathOwned = true;
2355
- }
2356
- }
2357
- catch (error) {
2358
- if (error.code === "ENOENT") {
2359
- this.#server.unref();
2360
- errors.push(new BridgeError("CLAUDE_PEER_CALLBACK_CHANGED", "The callback path disappeared; cleanup failed closed."));
2361
- }
2362
- else {
2363
- this.#server.unref();
2364
- errors.push(error);
2365
- }
2366
- }
2367
- if (callbackPathOwned) {
2368
- try {
2369
- await new Promise((resolve, reject) => this.#server.close((error) => error === undefined ? resolve() : reject(error)));
2370
- }
2371
- catch (error) {
2372
- errors.push(error);
2373
- }
2374
- try {
2375
- const stat = await lstat(this.#socketPath);
2376
- if (stat.isSocket() &&
2377
- sameSocketGeneration({ dev: stat.dev, ino: stat.ino }, this.#socketGeneration)) {
1677
+ await closeServer(this.#server);
1678
+ if (await socketOwnership(this.#socketPath, this.#socketGeneration) ===
1679
+ "owned") {
2378
1680
  await unlink(this.#socketPath);
2379
1681
  }
2380
1682
  }
2381
- catch (error) {
2382
- if (error.code !== "ENOENT") {
2383
- errors.push(error);
2384
- }
2385
- }
1683
+ }
1684
+ catch (error) {
1685
+ this.#server.unref();
1686
+ errors.push(error);
2386
1687
  }
2387
1688
  this.#closing = false;
2388
1689
  this.#closed = true;
2389
1690
  try {
2390
- this.#onClosed();
1691
+ this.#context.onClosed();
2391
1692
  }
2392
1693
  catch (error) {
2393
1694
  errors.push(error);