agent-embassy 3.1.0 → 4.0.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 (112) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/CONTRIBUTING.md +92 -202
  3. package/README.md +157 -155
  4. package/SECURITY.md +191 -348
  5. package/dist/src/gateway/broker-check.d.ts +26 -0
  6. package/dist/src/gateway/broker-check.js +89 -0
  7. package/dist/src/gateway/broker-check.js.map +1 -0
  8. package/dist/src/gateway/broker-control.d.ts +72 -0
  9. package/dist/src/gateway/broker-control.js +164 -0
  10. package/dist/src/gateway/broker-control.js.map +1 -0
  11. package/dist/src/gateway/broker.d.ts +117 -0
  12. package/dist/src/gateway/broker.js +186 -0
  13. package/dist/src/gateway/broker.js.map +1 -0
  14. package/dist/src/gateway/claude-peer.d.ts +1 -124
  15. package/dist/src/gateway/claude-peer.js +61 -1052
  16. package/dist/src/gateway/claude-peer.js.map +1 -1
  17. package/dist/src/gateway/codex-stateless-transport.d.ts +0 -12
  18. package/dist/src/gateway/codex-stateless-transport.js +8 -142
  19. package/dist/src/gateway/codex-stateless-transport.js.map +1 -1
  20. package/dist/src/gateway/config.d.ts +2 -6
  21. package/dist/src/gateway/config.js +17 -27
  22. package/dist/src/gateway/config.js.map +1 -1
  23. package/dist/src/gateway/coordinator.d.ts +57 -0
  24. package/dist/src/gateway/coordinator.js +217 -0
  25. package/dist/src/gateway/coordinator.js.map +1 -0
  26. package/dist/src/gateway/core-cli.d.ts +15 -0
  27. package/dist/src/gateway/core-cli.js +282 -0
  28. package/dist/src/gateway/core-cli.js.map +1 -0
  29. package/dist/src/gateway/core-service-command.d.ts +16 -0
  30. package/dist/src/gateway/core-service-command.js +71 -0
  31. package/dist/src/gateway/core-service-command.js.map +1 -0
  32. package/dist/src/gateway/endpoint-directory.d.ts +33 -0
  33. package/dist/src/gateway/endpoint-directory.js +223 -0
  34. package/dist/src/gateway/endpoint-directory.js.map +1 -0
  35. package/dist/src/gateway/federation.d.ts +89 -0
  36. package/dist/src/gateway/federation.js +632 -0
  37. package/dist/src/gateway/federation.js.map +1 -0
  38. package/dist/src/gateway/ledger-codec.d.ts +3 -0
  39. package/dist/src/gateway/ledger-codec.js +159 -0
  40. package/dist/src/gateway/ledger-codec.js.map +1 -0
  41. package/dist/src/gateway/ledger.d.ts +138 -0
  42. package/dist/src/gateway/ledger.js +319 -0
  43. package/dist/src/gateway/ledger.js.map +1 -0
  44. package/dist/src/gateway/local-control.d.ts +23 -0
  45. package/dist/src/gateway/local-control.js +489 -0
  46. package/dist/src/gateway/local-control.js.map +1 -0
  47. package/dist/src/gateway/native-destinations.d.ts +34 -0
  48. package/dist/src/gateway/native-destinations.js +272 -0
  49. package/dist/src/gateway/native-destinations.js.map +1 -0
  50. package/dist/src/gateway/owned-state.d.ts +57 -0
  51. package/dist/src/gateway/owned-state.js +398 -0
  52. package/dist/src/gateway/owned-state.js.map +1 -0
  53. package/dist/src/gateway/provenance-envelope.d.ts +3 -3
  54. package/dist/src/gateway/provenance-envelope.js +3 -23
  55. package/dist/src/gateway/provenance-envelope.js.map +1 -1
  56. package/dist/src/gateway/runtime.d.ts +41 -0
  57. package/dist/src/gateway/runtime.js +175 -0
  58. package/dist/src/gateway/runtime.js.map +1 -0
  59. package/docs/CONFIGURATION.md +178 -266
  60. package/docs/DELIVERY.md +133 -82
  61. package/docs/GATEWAY-ARCHITECTURE.md +276 -949
  62. package/package.json +5 -5
  63. package/skills/embassy-peer/SKILL.md +38 -196
  64. package/skills/embassy-peer/agents/openai.yaml +2 -2
  65. package/dist/src/gateway/claude-helper-protocol.d.ts +0 -117
  66. package/dist/src/gateway/claude-helper-protocol.js +0 -122
  67. package/dist/src/gateway/claude-helper-protocol.js.map +0 -1
  68. package/dist/src/gateway/claude-helper-supervisor.d.ts +0 -87
  69. package/dist/src/gateway/claude-helper-supervisor.js +0 -374
  70. package/dist/src/gateway/claude-helper-supervisor.js.map +0 -1
  71. package/dist/src/gateway/claude-helper.d.ts +0 -1
  72. package/dist/src/gateway/claude-helper.js +0 -252
  73. package/dist/src/gateway/claude-helper.js.map +0 -1
  74. package/dist/src/gateway/cli.d.ts +0 -56
  75. package/dist/src/gateway/cli.js +0 -1532
  76. package/dist/src/gateway/cli.js.map +0 -1
  77. package/dist/src/gateway/codex-socket-holder.d.ts +0 -26
  78. package/dist/src/gateway/codex-socket-holder.js +0 -76
  79. package/dist/src/gateway/codex-socket-holder.js.map +0 -1
  80. package/dist/src/gateway/control.d.ts +0 -279
  81. package/dist/src/gateway/control.js +0 -933
  82. package/dist/src/gateway/control.js.map +0 -1
  83. package/dist/src/gateway/peer-client.d.ts +0 -58
  84. package/dist/src/gateway/peer-client.js +0 -217
  85. package/dist/src/gateway/peer-client.js.map +0 -1
  86. package/dist/src/gateway/peer-mailbox.d.ts +0 -78
  87. package/dist/src/gateway/peer-mailbox.js +0 -168
  88. package/dist/src/gateway/peer-mailbox.js.map +0 -1
  89. package/dist/src/gateway/peer-protocol.d.ts +0 -110
  90. package/dist/src/gateway/peer-protocol.js +0 -79
  91. package/dist/src/gateway/peer-protocol.js.map +0 -1
  92. package/dist/src/gateway/peer-stdio.d.ts +0 -23
  93. package/dist/src/gateway/peer-stdio.js +0 -100
  94. package/dist/src/gateway/peer-stdio.js.map +0 -1
  95. package/dist/src/gateway/providers.d.ts +0 -164
  96. package/dist/src/gateway/providers.js +0 -975
  97. package/dist/src/gateway/providers.js.map +0 -1
  98. package/dist/src/gateway/server.d.ts +0 -46
  99. package/dist/src/gateway/server.js +0 -216
  100. package/dist/src/gateway/server.js.map +0 -1
  101. package/dist/src/gateway/service.d.ts +0 -371
  102. package/dist/src/gateway/service.js +0 -2335
  103. package/dist/src/gateway/service.js.map +0 -1
  104. package/dist/src/gateway/status-view.d.ts +0 -119
  105. package/dist/src/gateway/status-view.js +0 -484
  106. package/dist/src/gateway/status-view.js.map +0 -1
  107. package/dist/src/gateway/store.d.ts +0 -188
  108. package/dist/src/gateway/store.js +0 -2360
  109. package/dist/src/gateway/store.js.map +0 -1
  110. package/dist/src/gateway/types.d.ts +0 -541
  111. package/dist/src/gateway/types.js +0 -332
  112. package/dist/src/gateway/types.js.map +0 -1
@@ -1,23 +1,14 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
- import { chmod, constants as fsConstants, lstat, open, opendir, realpath, rename, unlink, writeFile, } from "node:fs/promises";
3
+ import { constants as fsConstants, lstat, open, opendir, realpath, } from "node:fs/promises";
4
4
  import net from "node:net";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { TextDecoder } from "node:util";
8
8
  import { BridgeError } from "../errors.js";
9
- import { KeyedMutex } from "../mutex.js";
10
9
  export const CLAUDE_PEER_COMPATIBILITY = Object.freeze({ peerProtocol: 1 });
11
- const EMBASSY_ADVERTISEMENT_VERSION = 1;
12
- const EMBASSY_SOURCE_NAME_PATTERN = /^(?:codex-|peer-)/;
13
10
  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;
14
11
  const ALIAS_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
15
- const PRIVATE_ARTIFACT_TOKEN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
16
- /** Bounded notices the gateway writes into a live Claude session; the framing tags around them are the wire contract. */
17
- const CLAUDE_PEER_NOTICE_COPY = {
18
- stall: "The local gateway is still waiting to deliver the preceding message. Run `embassy status` for details. Queued mail reaches a busy recipient when its turn ends.",
19
- diagnostic: "The local gateway could not deliver the preceding message. Run `embassy status` for details. Queued mail reaches a busy recipient when its turn ends.",
20
- };
21
12
  const REGISTRY_FILE_PATTERN = /^([1-9][0-9]{0,9})\.json$/;
22
13
  const SOCKET_FILE_PATTERN = /^([1-9][0-9]{0,9})\.sock$/;
23
14
  const MAX_PID = 2_147_483_647;
@@ -39,31 +30,15 @@ export const claudePeerRejectionCodes = [
39
30
  "SELF_TARGET",
40
31
  "SESSION_ID_COLLISION",
41
32
  ];
42
- export const claudePeerInboundStallReasons = [
43
- "ROUTE_BUSY",
44
- "ROUTE_UNAVAILABLE",
45
- "AWAITING_EXTERNAL_APPROVAL",
46
- ];
47
- const MAX_CLAUDE_STALL_QUEUED_MS = 3_600_000;
48
- const CAPACITY_SETTLEMENT_MAX_ATTEMPTS = 3;
49
- const CAPACITY_SETTLEMENT_RETRY_MS = 25;
50
33
  function isObject(value) {
51
34
  return typeof value === "object" && value !== null && !Array.isArray(value);
52
35
  }
53
- function hasExactKeys(value, required, optional = []) {
54
- const permitted = new Set([...required, ...optional]);
55
- return (required.every((key) => Object.hasOwn(value, key)) &&
56
- Object.keys(value).every((key) => permitted.has(key)));
57
- }
58
36
  function byteLength(value) {
59
37
  return Buffer.byteLength(value, "utf8");
60
38
  }
61
39
  function isBoundedString(value, maxBytes) {
62
40
  return typeof value === "string" && byteLength(value) <= maxBytes;
63
41
  }
64
- function isDiagnosticVersion(value) {
65
- return isBoundedString(value, 64) && value.length > 0 && !value.includes("\0");
66
- }
67
42
  function validateContent(content, maxBytes) {
68
43
  if (typeof content !== "string" ||
69
44
  content.length === 0 ||
@@ -84,10 +59,7 @@ function sameFileGeneration(left, right) {
84
59
  left.mtimeMs === right.mtimeMs);
85
60
  }
86
61
  function sameSocketGeneration(left, right) {
87
- return left.dev === right.dev && left.ino === right.ino;
88
- }
89
- function sameDirectoryGeneration(left, right) {
90
- return left.dev === right.dev && left.ino === right.ino;
62
+ return left.dev === right.dev && left.ino === right.ino && left.ctimeNs === right.ctimeNs;
91
63
  }
92
64
  function pathContains(parent, child) {
93
65
  const relative = path.relative(parent, child);
@@ -121,13 +93,6 @@ function configuredLimit(value, fallback, minimum, maximum, label) {
121
93
  }
122
94
  return resolved;
123
95
  }
124
- function boundedStallQueuedForMs(value) {
125
- if (Number.isNaN(value) || value === Number.NEGATIVE_INFINITY)
126
- return 0;
127
- if (value === Number.POSITIVE_INFINITY)
128
- return MAX_CLAUDE_STALL_QUEUED_MS;
129
- return Math.min(MAX_CLAUDE_STALL_QUEUED_MS, Math.max(0, Math.trunc(value)));
130
- }
131
96
  function parseRegistryRecord(value, expectedPid) {
132
97
  if (!isObject(value))
133
98
  return undefined;
@@ -136,19 +101,6 @@ function parseRegistryRecord(value, expectedPid) {
136
101
  "updatedAt"];
137
102
  if (!required.every((key) => Object.hasOwn(value, key)))
138
103
  return undefined;
139
- const embassyAdvertisement = value.embassyAdvertisementVersion === EMBASSY_ADVERTISEMENT_VERSION &&
140
- isDiagnosticVersion(value.version) &&
141
- typeof value.name === "string" &&
142
- EMBASSY_SOURCE_NAME_PATTERN.test(value.name) &&
143
- ALIAS_PATTERN.test(value.name) &&
144
- value.kind === "interactive" &&
145
- value.entrypoint === "cli" &&
146
- value.peerProtocol === CLAUDE_PEER_COMPATIBILITY.peerProtocol &&
147
- value.status !== undefined &&
148
- value.statusUpdatedAt !== undefined;
149
- if (value.embassyAdvertisementVersion !== undefined && !embassyAdvertisement)
150
- return undefined;
151
- const versionObserved = value.version === undefined || isDiagnosticVersion(value.version);
152
104
  if (value.pid !== expectedPid)
153
105
  return undefined;
154
106
  if (typeof value.sessionId !== "string" || !UUID_PATTERN.test(value.sessionId))
@@ -168,7 +120,6 @@ function parseRegistryRecord(value, expectedPid) {
168
120
  !isBoundedString(value.procStart, 256) ||
169
121
  value.procStart.length === 0 ||
170
122
  value.procStart.includes("\0") ||
171
- !versionObserved ||
172
123
  !isBoundedString(value.entrypoint, 64) ||
173
124
  !/^[A-Za-z0-9._-]+$/.test(value.entrypoint) ||
174
125
  (value.nameSource !== undefined &&
@@ -206,113 +157,20 @@ function parseRegistryRecord(value, expectedPid) {
206
157
  messagingSocketPath: value.messagingSocketPath,
207
158
  name: value.name,
208
159
  status: (value.status ?? "busy"),
209
- embassyAdvertisement,
210
160
  };
211
161
  }
212
- function parseFrame(line, maxFrameBytes) {
213
- if (line.length === 0 || line.length > maxFrameBytes) {
214
- throw new BridgeError("INVALID_PEER_FRAME", "Invalid peer frame size.");
215
- }
216
- let text;
217
- try {
218
- text = new TextDecoder("utf-8", { fatal: true }).decode(line);
219
- }
220
- catch {
221
- throw new BridgeError("INVALID_PEER_UTF8", "Invalid peer frame encoding.");
222
- }
223
- let value;
224
- try {
225
- value = JSON.parse(text);
226
- }
227
- catch {
228
- throw new BridgeError("INVALID_PEER_FRAME", "Invalid peer frame JSON.");
229
- }
230
- if (!isObject(value) || typeof value.type !== "string") {
231
- throw new BridgeError("INVALID_PEER_FRAME", "Invalid peer frame shape.");
232
- }
233
- if (value.type === "user") {
234
- if (!hasExactKeys(value, ["type", "message"], ["msgV", "msg_id", "priority", "from"]) ||
235
- !isObject(value.message) ||
236
- !hasExactKeys(value.message, ["role", "content"]) ||
237
- value.message.role !== "user") {
238
- throw new BridgeError("INVALID_PEER_FRAME", "Invalid user frame shape.");
239
- }
240
- const canonicalFields = [value.msgV, value.msg_id, value.priority];
241
- const canonicalCount = canonicalFields.filter((field) => field !== undefined).length;
242
- if (canonicalCount !== 0 &&
243
- (canonicalCount !== 3 ||
244
- value.msgV !== 1 ||
245
- typeof value.msg_id !== "string" ||
246
- !UUID_PATTERN.test(value.msg_id) ||
247
- value.priority !== "next")) {
248
- throw new BridgeError("INVALID_PEER_FRAME", "Invalid canonical user frame fields.");
249
- }
250
- if (value.from !== undefined &&
251
- (!isBoundedString(value.from, 4096) || !value.from.startsWith("uds:"))) {
252
- throw new BridgeError("INVALID_PEER_FRAME", "Invalid reply address.");
253
- }
254
- const content = validateContent(value.message.content, maxFrameBytes);
255
- return {
256
- type: "user",
257
- content,
258
- ...(typeof value.msg_id === "string"
259
- ? { messageId: value.msg_id }
260
- : {}),
261
- ...(typeof value.from === "string" ? { from: value.from } : {}),
262
- };
263
- }
264
- if (value.type === "control") {
265
- if (!hasExactKeys(value, [
266
- "type",
267
- "action",
268
- "status",
269
- "reason",
270
- "from",
271
- "orig_msg_id",
272
- "msgV",
273
- "msg_id",
274
- ]) ||
275
- value.action !== "peer_message_status" ||
276
- !["held", "denied", "expired", "delivered"].includes(String(value.status)) ||
277
- !isBoundedString(value.reason, 1024) ||
278
- !isBoundedString(value.from, 4096) ||
279
- !value.from.startsWith("uds:") ||
280
- typeof value.orig_msg_id !== "string" ||
281
- !UUID_PATTERN.test(value.orig_msg_id) ||
282
- value.msgV !== 1 ||
283
- typeof value.msg_id !== "string" ||
284
- !UUID_PATTERN.test(value.msg_id)) {
285
- throw new BridgeError("INVALID_PEER_FRAME", "Invalid peer status frame.");
286
- }
287
- return {
288
- type: "control",
289
- action: "peer_message_status",
290
- status: value.status,
291
- from: value.from,
292
- originalMessageId: value.orig_msg_id,
293
- };
294
- }
295
- throw new BridgeError("UNSUPPORTED_PEER_FRAME", "Unsupported Claude peer frame type.");
296
- }
297
162
  export function encodeClaudePeerUserFrame(input) {
298
163
  if (!UUID_PATTERN.test(input.messageId)) {
299
164
  throw new BridgeError("INVALID_PEER_MESSAGE_ID", "The peer message ID must be a UUID.");
300
165
  }
301
166
  const maxFrameBytes = configuredLimit(input.maxFrameBytes, 64 * 1024, 256, 1024 * 1024, "maxFrameBytes");
302
167
  const content = validateContent(input.content, maxFrameBytes);
303
- if (input.from !== undefined &&
304
- (!input.from.startsWith("uds:") ||
305
- !path.isAbsolute(input.from.slice(4)) ||
306
- input.from.includes("\0"))) {
307
- throw new BridgeError("INVALID_PEER_REPLY_ADDRESS", "The peer reply address must be an absolute uds address.");
308
- }
309
168
  const frame = {
310
169
  msgV: 1,
311
170
  msg_id: input.messageId,
312
171
  type: "user",
313
172
  message: { role: "user", content },
314
173
  priority: "next",
315
- ...(input.from === undefined ? {} : { from: input.from }),
316
174
  };
317
175
  const encoded = Buffer.from(`${JSON.stringify(frame)}\n`, "utf8");
318
176
  if (encoded.length > maxFrameBytes + 1) {
@@ -351,12 +209,7 @@ async function defaultProcessInspector(pid) {
351
209
  });
352
210
  });
353
211
  }
354
- async function invokeHook(hook, value) {
355
- if (hook === undefined)
356
- return;
357
- await hook(value);
358
- }
359
- function writeSocketPayload(connect, socketPath, payload, timeoutMs, onWriteStart) {
212
+ function writeSocketPayload(connect, socketPath, payload, timeoutMs, beforeWrite, onWriteStart) {
360
213
  return new Promise((resolve, reject) => {
361
214
  const socket = connect(socketPath);
362
215
  let settled = false;
@@ -371,34 +224,20 @@ function writeSocketPayload(connect, socketPath, payload, timeoutMs, onWriteStar
371
224
  error === undefined ? resolve() : reject(error);
372
225
  };
373
226
  socket.once("error", finish);
374
- socket.once("connect", () => {
227
+ socket.once("connect", () => void (async () => {
375
228
  try {
229
+ await beforeWrite();
230
+ if (settled)
231
+ return;
376
232
  onWriteStart();
377
233
  socket.end(payload, () => finish());
378
234
  }
379
235
  catch (error) {
380
236
  finish(error);
381
237
  }
382
- });
238
+ })());
383
239
  });
384
240
  }
385
- async function socketOwnership(socketPath, generation) {
386
- try {
387
- const stat = await lstat(socketPath);
388
- return stat.isSocket() &&
389
- sameSocketGeneration({ dev: stat.dev, ino: stat.ino }, generation)
390
- ? "owned"
391
- : "changed";
392
- }
393
- catch (error) {
394
- if (error.code === "ENOENT")
395
- return "missing";
396
- throw error;
397
- }
398
- }
399
- function closeServer(server) {
400
- return new Promise((resolve, reject) => server.close((error) => error === undefined ? resolve() : reject(error)));
401
- }
402
241
  export class ClaudePeerAdapter {
403
242
  #sessionsDir;
404
243
  #socketDir;
@@ -408,27 +247,15 @@ export class ClaudePeerAdapter {
408
247
  #connect;
409
248
  #now;
410
249
  #createId;
411
- #createArtifactToken;
412
- #deliveryNotices;
413
- #registryRename;
414
- #registryOperationHook;
415
250
  #userHome;
416
251
  #tempRoots;
417
- #registryMutex = new KeyedMutex();
418
- #postBindHook;
419
252
  #targets = new Map();
420
253
  #selectedStateRoots = new Map();
421
- #listeners = new Set();
422
254
  #preparedSends = new Set();
423
255
  constructor(options, testing = {}) {
424
256
  if (process.platform === "win32" || process.getuid === undefined) {
425
257
  throw new BridgeError("CLAUDE_PEER_PLATFORM_UNSUPPORTED", "Claude peer sockets are supported only on macOS and Linux.");
426
258
  }
427
- if (options.deliveryNotices !== undefined &&
428
- !["merged", "verbose", "quiet"].includes(options.deliveryNotices)) {
429
- throw new BridgeError("INVALID_GATEWAY_CONFIGURATION", "The Claude peer delivery notice mode is unsupported.");
430
- }
431
- this.#deliveryNotices = options.deliveryNotices ?? "merged";
432
259
  this.#sessionsDir = assertAbsoluteConfiguredPath(options.sessionsDir, "sessionsDir");
433
260
  this.#socketDir = assertAbsoluteConfiguredPath(options.socketDir, "socketDir");
434
261
  this.#expectedUid = testing.expectedUid ?? process.getuid();
@@ -441,10 +268,6 @@ export class ClaudePeerAdapter {
441
268
  maxRegistryBytes: limit("maxRegistryBytes", 16 * 1024, 512, 1024 * 1024),
442
269
  maxFrameBytes: limit("maxFrameBytes", 64 * 1024, 256, 1024 * 1024),
443
270
  connectTimeoutMs: limit("connectTimeoutMs", 2_000, 10, 30_000),
444
- maxPendingReceipts: limit("maxPendingReceipts", 256, 1, 4_096),
445
- maxConnections: limit("maxConnections", 32, 1, 1_024),
446
- connectionIdleMs: limit("connectionIdleMs", 5_000, 10, 60_000),
447
- maxFramesPerConnection: limit("maxFramesPerConnection", 8, 1, 128),
448
271
  };
449
272
  this.#inspectProcess = testing.processInspector ?? defaultProcessInspector;
450
273
  this.#connect =
@@ -452,14 +275,10 @@ export class ClaudePeerAdapter {
452
275
  ((socketPath) => net.createConnection({ path: socketPath }));
453
276
  this.#now = testing.now ?? Date.now;
454
277
  this.#createId = testing.createId ?? randomUUID;
455
- this.#createArtifactToken = testing.createArtifactToken ?? randomUUID;
456
- this.#registryRename = testing.registryRename ?? rename;
457
- this.#registryOperationHook = testing.registryOperationHook;
458
278
  this.#userHome = assertAbsoluteConfiguredPath(testing.userHome ?? os.userInfo().homedir, "userHome");
459
279
  this.#tempRoots = Object.freeze([
460
280
  ...(testing.tempRoots ?? ["/tmp", "/private/tmp", os.tmpdir()]),
461
281
  ].map((root) => assertAbsoluteConfiguredPath(root, "tempRoot")));
462
- this.#postBindHook = testing.postBindHook;
463
282
  }
464
283
  async #validateRoots() {
465
284
  const [stat, sockets] = await Promise.all([
@@ -515,7 +334,7 @@ export class ClaudePeerAdapter {
515
334
  catch {
516
335
  throw new BridgeError("REGISTRY_INVALID_JSON", "Registry JSON is invalid.");
517
336
  }
518
- return { value, generation: openedGeneration };
337
+ return value;
519
338
  }
520
339
  finally {
521
340
  await handle.close();
@@ -530,14 +349,14 @@ export class ClaudePeerAdapter {
530
349
  if (match === null || parsePositiveInteger(match[1] ?? "") !== expectedPid) {
531
350
  throw new BridgeError("SOCKET_OUTSIDE_ROOT", "Peer socket filename does not match its process.");
532
351
  }
533
- const stat = await lstat(socketPath);
352
+ const stat = await lstat(socketPath, { bigint: true });
534
353
  if (stat.isSymbolicLink() || !stat.isSocket()) {
535
354
  throw new BridgeError("SOCKET_NOT_SOCKET", "Peer endpoint is not a socket.");
536
355
  }
537
- return { dev: stat.dev, ino: stat.ino };
356
+ return { dev: stat.dev, ino: stat.ino, ctimeNs: stat.ctimeNs };
538
357
  }
539
- async #bindingFromRegistry(registryPath, expectedPid, _existingTargetId, onParsed) {
540
- const { value } = await this.#readRegistryFile(registryPath);
358
+ async #bindingFromRegistry(registryPath, expectedPid, onParsed) {
359
+ const value = await this.#readRegistryFile(registryPath);
541
360
  if (isObject(value) && value.pid !== expectedPid) {
542
361
  throw new BridgeError("PID_MISMATCH", "Registry filename and process identifier do not match.");
543
362
  }
@@ -545,9 +364,7 @@ export class ClaudePeerAdapter {
545
364
  if (record === undefined) {
546
365
  throw new BridgeError("REGISTRY_INVALID_SCHEMA", "Registry schema is incompatible.");
547
366
  }
548
- // Embassy advertisements are addressable by Claude, never Claude delivery
549
- // targets or evidence that a Claude record parsed.
550
- if (record.embassyAdvertisement || expectedPid === process.pid) {
367
+ if (expectedPid === process.pid) {
551
368
  throw new BridgeError("SELF_TARGET", "The gateway cannot target itself.");
552
369
  }
553
370
  onParsed?.();
@@ -558,7 +375,7 @@ export class ClaudePeerAdapter {
558
375
  if (processIdentity.uid !== this.#expectedUid) {
559
376
  throw new BridgeError("PID_OWNER_MISMATCH", "Registry process owner is unsafe.");
560
377
  }
561
- await this.#validateSocket(record.messagingSocketPath, expectedPid);
378
+ const socketGeneration = await this.#validateSocket(record.messagingSocketPath, expectedPid);
562
379
  return {
563
380
  // Claude's native session UUID is the logical route identity. The
564
381
  // process, registry file, and socket below are replaceable transport
@@ -566,8 +383,29 @@ export class ClaudePeerAdapter {
566
383
  targetId: record.sessionId,
567
384
  alias: record.name,
568
385
  record,
386
+ registryPath,
387
+ processGeneration: processIdentity.generation,
388
+ socketGeneration,
569
389
  };
570
390
  }
391
+ async #revalidatePreparedBinding(expected, stateRoot) {
392
+ try {
393
+ await this.#validateRoots();
394
+ const current = await this.#bindingFromRegistry(expected.registryPath, expected.record.pid);
395
+ if (current.targetId !== expected.targetId ||
396
+ current.record.cwd !== expected.record.cwd ||
397
+ current.record.messagingSocketPath !== expected.record.messagingSocketPath ||
398
+ current.processGeneration !== expected.processGeneration ||
399
+ !sameSocketGeneration(current.socketGeneration, expected.socketGeneration)) {
400
+ throw new Error("prepared Claude peer generation changed");
401
+ }
402
+ await this.#attestOwnedDirectory(stateRoot, true, "CLAUDE_PEER_STATE_ROOT_UNSAFE");
403
+ await this.#assertBindingWorkspaceDisjoint(current);
404
+ }
405
+ catch {
406
+ throw new BridgeError("CLAUDE_PEER_TARGET_CHANGED", "The prepared Claude peer generation changed before the write.", true);
407
+ }
408
+ }
571
409
  async discover() {
572
410
  await this.#validateRoots();
573
411
  const nextTargets = new Map();
@@ -606,7 +444,7 @@ export class ClaudePeerAdapter {
606
444
  }
607
445
  const registryPath = path.join(this.#sessionsDir, entry.name);
608
446
  try {
609
- const binding = await this.#bindingFromRegistry(registryPath, pid, undefined, () => {
447
+ const binding = await this.#bindingFromRegistry(registryPath, pid, () => {
610
448
  parseableRecords += 1;
611
449
  });
612
450
  if (collidedSessionIds.has(binding.targetId) ||
@@ -749,51 +587,6 @@ export class ClaudePeerAdapter {
749
587
  compatibility: "compatible",
750
588
  };
751
589
  }
752
- async listen(options) {
753
- await this.#validateRoots();
754
- const generation = this.#createArtifactToken();
755
- if (!PRIVATE_ARTIFACT_TOKEN_PATTERN.test(generation)) {
756
- throw new BridgeError("INVALID_CODEX_PEER_GENERATION", "A native source listener requires one bounded private artifact token.");
757
- }
758
- if ([...this.#listeners].some((listener) => !listener.closed && listener.generation === generation)) {
759
- throw new BridgeError("CODEX_PEER_GENERATION_EXISTS", "A live native Codex listener already owns this exact generation.");
760
- }
761
- const listener = await ClaudePeerListener.create({
762
- sessionsDir: this.#sessionsDir,
763
- socketDir: this.#socketDir,
764
- expectedUid: this.#expectedUid,
765
- limits: this.#limits,
766
- createId: this.#createId,
767
- connect: this.#connect,
768
- deliveryNotices: this.#deliveryNotices,
769
- resolveReplyAddress: async (address) => await this.#resolveReplyAddress(address),
770
- resolveSessionBinding: async (sessionId) => {
771
- // Native receipt handles retain only the stable Claude session UUID.
772
- // Registry names, PIDs, and sockets are replaceable coordinates, so
773
- // refresh discovery before every receipt or progress write.
774
- const discovery = await this.discover();
775
- if (discovery.truncated) {
776
- throw new BridgeError("CLAUDE_PEER_DISCOVERY_INCOMPLETE", "The Claude peer receipt target cannot be proven from an incomplete registry scan.", true);
777
- }
778
- const target = this.#targets.get(sessionId);
779
- if (target === undefined) {
780
- throw new BridgeError("CLAUDE_PEER_TARGET_UNKNOWN", "The Claude peer receipt target is no longer discoverable.", true);
781
- }
782
- return target;
783
- },
784
- options,
785
- generation,
786
- registryRename: this.#registryRename,
787
- ...(this.#registryOperationHook === undefined
788
- ? {}
789
- : { registryOperationHook: this.#registryOperationHook }),
790
- runRegistryMutation: async (operation) => await this.#registryMutex.run("codex-registry", operation),
791
- postBindHook: this.#postBindHook,
792
- onClosed: () => this.#listeners.delete(listener),
793
- });
794
- this.#listeners.add(listener);
795
- return listener;
796
- }
797
590
  async prepareSend(targetId, content, options) {
798
591
  if (!Number.isSafeInteger(options.deadlineAt) ||
799
592
  options.deadlineAt < 0) {
@@ -827,16 +620,9 @@ export class ClaudePeerAdapter {
827
620
  if (!UUID_PATTERN.test(messageId)) {
828
621
  throw new BridgeError("INVALID_PEER_MESSAGE_ID", "The configured ID source did not produce a UUID.");
829
622
  }
830
- if (options.replyListener !== undefined &&
831
- !this.#listeners.has(options.replyListener)) {
832
- throw new BridgeError("CLAUDE_PEER_LISTENER_FOREIGN", "The reply listener is not owned by this adapter.");
833
- }
834
623
  const frame = encodeClaudePeerUserFrame({
835
624
  messageId,
836
625
  content,
837
- ...(options.replyListener === undefined
838
- ? {}
839
- : { from: options.replyListener.address }),
840
626
  maxFrameBytes: this.#limits.maxFrameBytes,
841
627
  });
842
628
  const socketPath = target.record.messagingSocketPath;
@@ -848,37 +634,52 @@ export class ClaudePeerAdapter {
848
634
  this.#preparedSends.delete(cancel);
849
635
  };
850
636
  this.#preparedSends.add(cancel);
851
- const perform = () => {
637
+ const perform = async (authorize) => {
852
638
  if (state !== "prepared") {
853
- return Promise.reject(new BridgeError("CLAUDE_PEER_PREPARATION_CONSUMED", "The prepared Claude peer write was already performed or cancelled."));
639
+ throw new BridgeError("CLAUDE_PEER_PREPARATION_CONSUMED", "The prepared Claude peer write was already performed or cancelled.");
854
640
  }
855
641
  state = "performed";
856
642
  this.#preparedSends.delete(cancel);
857
643
  if (options.deadlineAt <= this.#now()) {
858
- return Promise.reject(new BridgeError("CLAUDE_PEER_MESSAGE_EXPIRED", "The Claude peer message deadline elapsed before any socket write.", true));
644
+ throw new BridgeError("CLAUDE_PEER_MESSAGE_EXPIRED", "The Claude peer message deadline elapsed before any socket write.", true);
859
645
  }
860
646
  let writeStarted = false;
861
- const write = writeSocketPayload(this.#connect, socketPath, frame, Math.max(1, Math.min(this.#limits.connectTimeoutMs, options.deadlineAt - this.#now())), () => {
647
+ let authorizationStarted = false;
648
+ await this.#revalidatePreparedBinding(target, stateRoot);
649
+ const write = writeSocketPayload(this.#connect, socketPath, frame, Math.max(1, Math.min(this.#limits.connectTimeoutMs, options.deadlineAt - this.#now())), async () => {
650
+ await this.#revalidatePreparedBinding(target, stateRoot);
651
+ authorizationStarted = true;
652
+ let authorized;
653
+ try {
654
+ authorized = await authorize();
655
+ }
656
+ catch {
657
+ throw new BridgeError("WRITE_AUTHORIZATION_UNCERTAIN", "The final write authorization outcome is uncertain; do not retry automatically.");
658
+ }
659
+ if (!authorized) {
660
+ throw new BridgeError("WRITE_AUTHORIZATION_DENIED", "The final write authorization was denied before any socket write.", true);
661
+ }
662
+ }, () => {
862
663
  if (options.deadlineAt <= this.#now()) {
863
664
  throw new BridgeError("CLAUDE_PEER_MESSAGE_EXPIRED", "The Claude peer message deadline elapsed before any socket write.", true);
864
665
  }
865
666
  writeStarted = true;
866
667
  });
867
- return write.then(() => ({ messageId, transportStatus: "transport_written" }), (error) => {
668
+ return await write.then(() => ({ messageId, transportStatus: "transport_written" }), (error) => {
868
669
  if (error instanceof BridgeError)
869
670
  throw error;
870
671
  const expired = this.#now() >= options.deadlineAt;
871
- throw new BridgeError(writeStarted
672
+ throw new BridgeError(writeStarted || authorizationStarted
872
673
  ? "CLAUDE_PEER_WRITE_AMBIGUOUS"
873
674
  : expired
874
675
  ? "CLAUDE_PEER_MESSAGE_EXPIRED"
875
676
  : error instanceof Error && error.message === "peer write timeout"
876
677
  ? "CLAUDE_PEER_CONNECT_TIMEOUT"
877
- : "CLAUDE_PEER_WRITE_FAILED", writeStarted
878
- ? "The Claude peer write began but its outcome is ambiguous; do not retry automatically."
678
+ : "CLAUDE_PEER_WRITE_FAILED", writeStarted || authorizationStarted
679
+ ? "The Claude write or final authorization began but its outcome is ambiguous; do not retry automatically."
879
680
  : expired
880
681
  ? "The Claude peer message deadline elapsed before any socket write."
881
- : "The Claude peer message was not confirmed written.", !writeStarted);
682
+ : "The Claude peer message was not confirmed written.", !writeStarted && !authorizationStarted);
882
683
  });
883
684
  };
884
685
  return Object.freeze({
@@ -892,800 +693,8 @@ export class ClaudePeerAdapter {
892
693
  async close() {
893
694
  for (const cancel of [...this.#preparedSends])
894
695
  cancel();
895
- const results = await Promise.allSettled([...this.#listeners].map(async (listener) => listener.close()));
896
696
  this.#targets.clear();
897
697
  this.#selectedStateRoots.clear();
898
- const failed = results.find((result) => result.status === "rejected");
899
- if (failed !== undefined)
900
- throw failed.reason;
901
- }
902
- }
903
- export class ClaudePeerListener {
904
- address;
905
- generation;
906
- #sessionsGeneration;
907
- #socketPath;
908
- #socketGeneration;
909
- #server;
910
- #context;
911
- #connections = new Set();
912
- #inboundReceipts = new Map();
913
- // One bounded exception slot owns an overflow message only long enough to
914
- // return its terminal capacity rejection. It never forwards content to the
915
- // service, retries only proven pre-write failures, and is released after a
916
- // successful, ambiguous, non-retryable, or exhausted write.
917
- #capacitySettlement;
918
- #queuedFrames = 0;
919
- #inboundQuiesced = false;
920
- #inboundQuiesceWaiters = new Set();
921
- #advertisement;
922
- #closing = false;
923
- #closed = false;
924
- #closeOperation;
925
- constructor(options, server, socketPath, generation, sessionsGeneration) {
926
- this.address = `uds:${socketPath}`;
927
- this.generation = options.generation;
928
- this.#sessionsGeneration = sessionsGeneration;
929
- this.#socketPath = socketPath;
930
- this.#socketGeneration = generation;
931
- this.#server = server;
932
- this.#context = options;
933
- }
934
- static async create(options) {
935
- // One gateway process owns one native peer socket. It may advertise one
936
- // registered Codex task through Claude's native session registry.
937
- const socketPath = path.join(options.socketDir, `${process.pid}.sock`);
938
- try {
939
- await lstat(socketPath);
940
- throw new BridgeError("CLAUDE_PEER_CALLBACK_EXISTS", "The gateway callback path already exists; it will not be unlinked.");
941
- }
942
- catch (error) {
943
- if (error instanceof BridgeError ||
944
- error.code !== "ENOENT") {
945
- throw error;
946
- }
947
- }
948
- let listener;
949
- const server = net.createServer((socket) => {
950
- if (listener === undefined) {
951
- socket.destroy();
952
- return;
953
- }
954
- listener.#accept(socket);
955
- });
956
- server.on("error", () => {
957
- if (listener !== undefined)
958
- void listener.#notice("CALLBACK_ERROR");
959
- });
960
- await new Promise((resolve, reject) => {
961
- const onError = (error) => reject(error);
962
- server.once("error", onError);
963
- server.listen(socketPath, () => {
964
- server.off("error", onError);
965
- resolve();
966
- });
967
- });
968
- let createdGeneration;
969
- try {
970
- const created = await lstat(socketPath);
971
- if (!created.isSocket() ||
972
- created.isSymbolicLink()) {
973
- throw new BridgeError("CLAUDE_PEER_CALLBACK_UNSAFE", "The newly bound callback path did not satisfy its type and ownership policy.");
974
- }
975
- createdGeneration = { dev: created.dev, ino: created.ino };
976
- if (created.uid !== options.expectedUid) {
977
- throw new BridgeError("CLAUDE_PEER_CALLBACK_UNSAFE", "The newly bound callback path did not satisfy its type and ownership policy.");
978
- }
979
- await chmod(socketPath, 0o600);
980
- const attested = await lstat(socketPath);
981
- if (!attested.isSocket() ||
982
- attested.isSymbolicLink() ||
983
- attested.uid !== options.expectedUid ||
984
- exactMode(attested.mode) !== 0o600 ||
985
- !sameSocketGeneration({ dev: attested.dev, ino: attested.ino }, createdGeneration)) {
986
- throw new BridgeError("CLAUDE_PEER_CALLBACK_UNSAFE", "The gateway callback socket did not satisfy its ownership policy.");
987
- }
988
- await options.postBindHook?.(socketPath);
989
- const sessions = await lstat(options.sessionsDir);
990
- if (sessions.isSymbolicLink() ||
991
- !sessions.isDirectory() ||
992
- sessions.uid !== options.expectedUid ||
993
- exactMode(sessions.mode) !== 0o700) {
994
- throw new BridgeError("UNSAFE_PEER_DIRECTORY", "The native peer registry directory failed its ownership policy.");
995
- }
996
- listener = new ClaudePeerListener(options, server, socketPath, createdGeneration, { dev: sessions.dev, ino: sessions.ino });
997
- return listener;
998
- }
999
- catch (error) {
1000
- try {
1001
- if (createdGeneration === undefined ||
1002
- await socketOwnership(socketPath, createdGeneration) !== "owned") {
1003
- throw new Error("callback ownership changed");
1004
- }
1005
- await closeServer(server);
1006
- if (await socketOwnership(socketPath, createdGeneration) !== "missing") {
1007
- throw new Error("callback close unconfirmed");
1008
- }
1009
- }
1010
- catch {
1011
- server.unref();
1012
- throw new BridgeError("CLAUDE_PEER_CALLBACK_UNSAFE", "The newly bound callback socket could not be safely closed.");
1013
- }
1014
- throw error;
1015
- }
1016
- }
1017
- get closed() {
1018
- return this.#closing || this.#closed;
1019
- }
1020
- #registryPath() {
1021
- return path.join(this.#context.sessionsDir, `${process.pid}.json`);
1022
- }
1023
- #registryTemporaryPath() {
1024
- return path.join(this.#context.sessionsDir, `.${process.pid}.${this.generation}.registry.tmp`);
1025
- }
1026
- #serializeRecord(record) {
1027
- return `${JSON.stringify(record)}\n`;
1028
- }
1029
- async #assertRegistryDirectory() {
1030
- const current = await lstat(this.#context.sessionsDir);
1031
- if (current.isSymbolicLink() ||
1032
- !current.isDirectory() ||
1033
- current.uid !== this.#context.expectedUid ||
1034
- exactMode(current.mode) !== 0o700 ||
1035
- !sameDirectoryGeneration({ dev: current.dev, ino: current.ino }, this.#sessionsGeneration)) {
1036
- throw new BridgeError("UNSAFE_PEER_DIRECTORY", "The native peer registry directory changed or failed its ownership policy.");
1037
- }
1038
- }
1039
- async #readRegistryExact() {
1040
- await this.#assertRegistryDirectory();
1041
- const registryPath = this.#registryPath();
1042
- let handle;
1043
- try {
1044
- handle = await open(registryPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
1045
- }
1046
- catch (error) {
1047
- if (error.code === "ENOENT")
1048
- return undefined;
1049
- throw error;
1050
- }
1051
- try {
1052
- const opened = await handle.stat();
1053
- const openedGeneration = generationOf(opened);
1054
- if (!opened.isFile() ||
1055
- opened.uid !== this.#context.expectedUid ||
1056
- exactMode(opened.mode) !== 0o600 ||
1057
- opened.size > this.#context.limits.maxRegistryBytes) {
1058
- throw new BridgeError("REGISTRY_RACED", "The native Codex registry record failed its exact ownership policy.");
1059
- }
1060
- const serialized = await handle.readFile({ encoding: "utf8" });
1061
- if (byteLength(serialized) > this.#context.limits.maxRegistryBytes) {
1062
- throw new BridgeError("REGISTRY_TOO_LARGE", "The native Codex registry record exceeded its bound.");
1063
- }
1064
- const [afterOpen, afterPath] = await Promise.all([
1065
- handle.stat(),
1066
- lstat(registryPath),
1067
- ]);
1068
- if (afterPath.isSymbolicLink() ||
1069
- !afterPath.isFile() ||
1070
- !sameFileGeneration(openedGeneration, generationOf(afterOpen)) ||
1071
- !sameFileGeneration(openedGeneration, generationOf(afterPath))) {
1072
- throw new BridgeError("REGISTRY_RACED", "The native Codex registry record changed while reading.");
1073
- }
1074
- return { serialized, generation: openedGeneration };
1075
- }
1076
- finally {
1077
- await handle.close();
1078
- }
1079
- }
1080
- async #recordMatches(record, generation) {
1081
- try {
1082
- const current = await this.#readRegistryExact();
1083
- if (current === undefined ||
1084
- current.serialized !== this.#serializeRecord(record) ||
1085
- (generation !== undefined &&
1086
- !sameFileGeneration(current.generation, generation))) {
1087
- return { matches: false };
1088
- }
1089
- return { matches: true, generation: current.generation };
1090
- }
1091
- catch {
1092
- return { matches: false };
1093
- }
1094
- }
1095
- async #createRegistryTemporary(record) {
1096
- await this.#assertRegistryDirectory();
1097
- const temporaryPath = this.#registryTemporaryPath();
1098
- await writeFile(temporaryPath, this.#serializeRecord(record), {
1099
- encoding: "utf8",
1100
- flag: "wx",
1101
- mode: 0o600,
1102
- });
1103
- const created = await lstat(temporaryPath);
1104
- if (created.isSymbolicLink() ||
1105
- !created.isFile() ||
1106
- created.uid !== this.#context.expectedUid ||
1107
- exactMode(created.mode) !== 0o600) {
1108
- throw new BridgeError("REGISTRY_RACED", "The native Codex temporary registry record is unsafe.");
1109
- }
1110
- return { path: temporaryPath, generation: generationOf(created) };
1111
- }
1112
- async #cleanupOwnedFile(candidate, generation) {
1113
- try {
1114
- const current = await lstat(candidate);
1115
- if (current.isFile() &&
1116
- !current.isSymbolicLink() &&
1117
- current.uid === this.#context.expectedUid &&
1118
- exactMode(current.mode) === 0o600 &&
1119
- sameFileGeneration(generationOf(current), generation)) {
1120
- await unlink(candidate);
1121
- }
1122
- }
1123
- catch (error) {
1124
- if (error.code !== "ENOENT")
1125
- throw error;
1126
- }
1127
- }
1128
- #newAdvertisedRecord(name, cwd) {
1129
- if (!EMBASSY_SOURCE_NAME_PATTERN.test(name) || !ALIAS_PATTERN.test(name)) {
1130
- throw new BridgeError("INVALID_CODEX_PEER_NAME", "A native source peer name must use its provider prefix.");
1131
- }
1132
- if (!path.isAbsolute(cwd) || cwd.includes("\0")) {
1133
- throw new BridgeError("INVALID_CODEX_PEER_CWD", "A native Codex peer requires an absolute working directory.");
1134
- }
1135
- const sessionId = this.#context.createId();
1136
- if (!UUID_PATTERN.test(sessionId)) {
1137
- throw new BridgeError("INVALID_CODEX_PEER_SESSION_ID", "The configured ID source did not produce a native peer session UUID.");
1138
- }
1139
- const now = Date.now();
1140
- return {
1141
- embassyAdvertisementVersion: EMBASSY_ADVERTISEMENT_VERSION,
1142
- pid: process.pid,
1143
- sessionId,
1144
- cwd,
1145
- startedAt: now,
1146
- procStart: new Date(now - process.uptime() * 1_000).toString(),
1147
- version: "unknown",
1148
- peerProtocol: CLAUDE_PEER_COMPATIBILITY.peerProtocol,
1149
- kind: "interactive",
1150
- entrypoint: "cli",
1151
- messagingSocketPath: this.#socketPath,
1152
- name,
1153
- status: "idle",
1154
- updatedAt: now,
1155
- statusUpdatedAt: now,
1156
- };
1157
- }
1158
- async #mutateRegistry(operation, mutation) {
1159
- return await this.#context.runRegistryMutation(async () => {
1160
- await this.#context.registryOperationHook?.({
1161
- operation,
1162
- phase: "entered",
1163
- generation: this.generation,
1164
- });
1165
- try {
1166
- return await mutation();
1167
- }
1168
- finally {
1169
- await this.#context.registryOperationHook?.({
1170
- operation,
1171
- phase: "exited",
1172
- generation: this.generation,
1173
- });
1174
- }
1175
- });
1176
- }
1177
- async advertise(name, cwd) {
1178
- await this.#mutateRegistry("advertise", async () => {
1179
- if (this.closed) {
1180
- throw new BridgeError("CLAUDE_PEER_LISTENER_CLOSED", "The Claude peer callback listener is closed.");
1181
- }
1182
- if (this.#advertisement !== undefined && this.#advertisement.record.name !== name) {
1183
- throw new BridgeError("CODEX_PEER_ALREADY_ADVERTISED", "This gateway process already advertises another Codex peer.");
1184
- }
1185
- if (this.#advertisement?.record.name === name)
1186
- return;
1187
- const record = this.#newAdvertisedRecord(name, cwd);
1188
- await this.#assertRegistryDirectory();
1189
- await writeFile(this.#registryPath(), this.#serializeRecord(record), {
1190
- encoding: "utf8",
1191
- flag: "wx",
1192
- mode: 0o600,
1193
- });
1194
- const exact = await this.#recordMatches(record);
1195
- if (!exact.matches) {
1196
- throw new BridgeError("REGISTRY_RACED", "The native Codex registry record could not be exactly confirmed.");
1197
- }
1198
- this.#advertisement = { record, generation: exact.generation };
1199
- });
1200
- }
1201
- async updateAdvertisedStatus(status) {
1202
- await this.#mutateRegistry("status", async () => {
1203
- const owned = this.#advertisement;
1204
- if (this.closed || owned === undefined || owned.record.status === status)
1205
- return;
1206
- if (!(await this.#recordMatches(owned.record, owned.generation)).matches) {
1207
- this.#advertisement = undefined;
1208
- return;
1209
- }
1210
- const now = Date.now();
1211
- const record = { ...owned.record, status, updatedAt: now, statusUpdatedAt: now };
1212
- const temporary = await this.#createRegistryTemporary(record);
1213
- try {
1214
- if (!(await this.#recordMatches(owned.record, owned.generation)).matches)
1215
- return;
1216
- await this.#context.registryRename(temporary.path, this.#registryPath());
1217
- const exact = await this.#recordMatches(record, temporary.generation);
1218
- if (!exact.matches) {
1219
- throw new BridgeError("REGISTRY_RACED", "The updated native Codex registry record could not be exactly confirmed.");
1220
- }
1221
- this.#advertisement = { record, generation: exact.generation };
1222
- }
1223
- finally {
1224
- await this.#cleanupOwnedFile(temporary.path, temporary.generation).catch(() => undefined);
1225
- }
1226
- });
1227
- }
1228
- async unadvertise(name) {
1229
- await this.#mutateRegistry("unadvertise", async () => {
1230
- const owned = this.#advertisement;
1231
- if (owned === undefined || (name !== undefined && name !== owned.record.name))
1232
- return;
1233
- this.#advertisement = undefined;
1234
- if (!(await this.#recordMatches(owned.record, owned.generation)).matches)
1235
- return;
1236
- await unlink(this.#registryPath()).catch((error) => {
1237
- if (error.code !== "ENOENT")
1238
- throw error;
1239
- });
1240
- });
1241
- }
1242
- async acknowledge(receiptHandle, status, diagnostic) {
1243
- if (this.closed) {
1244
- throw new BridgeError("CLAUDE_PEER_LISTENER_CLOSED", "The Claude peer callback listener is closed.");
1245
- }
1246
- const receipt = this.#inboundReceipts.get(receiptHandle);
1247
- if (receipt === undefined) {
1248
- throw new BridgeError("CLAUDE_PEER_RECEIPT_UNKNOWN", "The native Claude peer receipt is unknown or already settled.");
1249
- }
1250
- if (diagnostic !== undefined &&
1251
- (status !== "expired" ||
1252
- !/^[A-Z][A-Z0-9_]{0,63}$/.test(diagnostic.code))) {
1253
- throw new BridgeError("INVALID_PEER_DELIVERY_DIAGNOSTIC", "A peer delivery diagnostic requires an expired receipt and a safe code.");
1254
- }
1255
- try {
1256
- const result = await this.#writeInboundStatus(receipt, status, diagnostic);
1257
- if (status !== "held")
1258
- this.#inboundReceipts.delete(receiptHandle);
1259
- return result;
1260
- }
1261
- catch (error) {
1262
- // A terminal write that may have started must never be replayed. A
1263
- // proven pre-write failure retains the handle for a bounded caller retry.
1264
- if (status !== "held" &&
1265
- error instanceof BridgeError &&
1266
- !error.recoverable) {
1267
- this.#inboundReceipts.delete(receiptHandle);
1268
- }
1269
- throw error;
1270
- }
1271
- }
1272
- /**
1273
- * Writes one nonterminal gateway progress frame without synthesizing a
1274
- * native peer_message_status transition. The receipt remains available for
1275
- * its later exact terminal acknowledgement or explicit release.
1276
- */
1277
- async notifyInboundProgress(receiptHandle, progress) {
1278
- if (this.closed) {
1279
- throw new BridgeError("CLAUDE_PEER_LISTENER_CLOSED", "The Claude peer callback listener is closed.");
1280
- }
1281
- const receipt = this.#inboundReceipts.get(receiptHandle);
1282
- if (receipt === undefined) {
1283
- throw new BridgeError("CLAUDE_PEER_RECEIPT_UNKNOWN", "The native Claude peer receipt is unknown or already settled.");
1284
- }
1285
- if (!isObject(progress) ||
1286
- !hasExactKeys(progress, ["kind", "reason", "queuedForMs"]) ||
1287
- progress.kind !== "stall" ||
1288
- !claudePeerInboundStallReasons.includes(progress.reason) ||
1289
- typeof progress.queuedForMs !== "number") {
1290
- throw new BridgeError("INVALID_PEER_PROGRESS", "Claude peer progress must be one bounded gateway stall notice.");
1291
- }
1292
- if (receipt.stallNotification !== "available") {
1293
- throw new BridgeError("CLAUDE_PEER_PROGRESS_ALREADY_NOTIFIED", "The native Claude peer receipt already has a stall notification.");
1294
- }
1295
- if (this.#context.deliveryNotices === "quiet") {
1296
- receipt.stallNotification = "settled";
1297
- return { transportStatus: "suppressed" };
1298
- }
1299
- const queuedForMs = boundedStallQueuedForMs(progress.queuedForMs);
1300
- const openingTag = `<gateway-delivery-stall terminal="false" reason="${progress.reason}" ` +
1301
- `queued-for-ms="${queuedForMs}">`;
1302
- const detailedContent = [
1303
- openingTag,
1304
- CLAUDE_PEER_NOTICE_COPY.stall,
1305
- "</gateway-delivery-stall>",
1306
- ].join("\n");
1307
- const messageId = this.#context.createId();
1308
- let progressFrame;
1309
- try {
1310
- progressFrame = encodeClaudePeerUserFrame({
1311
- messageId,
1312
- content: detailedContent,
1313
- maxFrameBytes: this.#context.limits.maxFrameBytes,
1314
- });
1315
- }
1316
- catch (error) {
1317
- if (!(error instanceof BridgeError) || error.code !== "PEER_FRAME_TOO_LARGE") {
1318
- throw error;
1319
- }
1320
- progressFrame = encodeClaudePeerUserFrame({
1321
- messageId,
1322
- content: `${openingTag.slice(0, -1)}/>`,
1323
- maxFrameBytes: this.#context.limits.maxFrameBytes,
1324
- });
1325
- }
1326
- receipt.stallNotification = "writing";
1327
- try {
1328
- const result = await this.#writeInboundPayload(receipt, progressFrame);
1329
- receipt.stallNotification = "settled";
1330
- return result;
1331
- }
1332
- catch (error) {
1333
- // A proven pre-write failure can be retried; an ambiguous write counts
1334
- // as the one allowed notice so the peer is never spammed by a replay.
1335
- receipt.stallNotification =
1336
- error instanceof BridgeError && error.recoverable
1337
- ? "available"
1338
- : "settled";
1339
- throw error;
1340
- }
1341
- }
1342
- /**
1343
- * Releases a listener-owned native receipt capability without writing to the
1344
- * peer. The service uses this after a terminal notification becomes
1345
- * definitively undeliverable so bounded receipt capacity cannot leak.
1346
- */
1347
- releaseInboundReceipt(receiptHandle) {
1348
- return this.#inboundReceipts.delete(receiptHandle);
1349
- }
1350
- /**
1351
- * Stop admitting new user-message frames while keeping the listener alive
1352
- * for terminal receipt writes. The promise joins every frame already
1353
- * admitted, so controller shutdown can drain service receipt work before
1354
- * closing the provider socket.
1355
- */
1356
- async quiesceInbound() {
1357
- this.#inboundQuiesced = true;
1358
- if (this.#queuedFrames === 0)
1359
- return;
1360
- await new Promise((resolve) => {
1361
- this.#inboundQuiesceWaiters.add(resolve);
1362
- if (this.#queuedFrames === 0) {
1363
- this.#inboundQuiesceWaiters.delete(resolve);
1364
- resolve();
1365
- }
1366
- });
1367
- }
1368
- async #writeInboundStatus(receipt, status, diagnostic) {
1369
- const statusFrame = Buffer.from(`${JSON.stringify({
1370
- type: "control",
1371
- action: "peer_message_status",
1372
- status,
1373
- reason: diagnostic?.code ?? status,
1374
- from: this.address,
1375
- orig_msg_id: receipt.originalMessageId,
1376
- msgV: 1,
1377
- msg_id: this.#context.createId(),
1378
- })}\n`, "utf8");
1379
- const diagnosticFrame = diagnostic === undefined || this.#context.deliveryNotices !== "verbose"
1380
- ? undefined
1381
- : encodeClaudePeerUserFrame({
1382
- messageId: this.#context.createId(),
1383
- content: [
1384
- `<gateway-delivery-diagnostic status="expired" code="${diagnostic.code}">`,
1385
- CLAUDE_PEER_NOTICE_COPY.diagnostic,
1386
- "</gateway-delivery-diagnostic>",
1387
- ].join("\n"),
1388
- maxFrameBytes: this.#context.limits.maxFrameBytes,
1389
- });
1390
- const payload = diagnosticFrame === undefined
1391
- ? statusFrame
1392
- : Buffer.concat([statusFrame, diagnosticFrame]);
1393
- return await this.#writeInboundPayload(receipt, payload);
1394
- }
1395
- async #writeInboundPayload(receipt, payload) {
1396
- let writeStarted = false;
1397
- try {
1398
- const binding = await this.#context.resolveSessionBinding(receipt.sourceSessionId);
1399
- await writeSocketPayload(this.#context.connect, binding.record.messagingSocketPath, payload, this.#context.limits.connectTimeoutMs, () => {
1400
- writeStarted = true;
1401
- });
1402
- }
1403
- catch (error) {
1404
- if (writeStarted) {
1405
- throw new BridgeError("CLAUDE_PEER_RECEIPT_WRITE_AMBIGUOUS", "The Claude peer receipt write began but its outcome is ambiguous; do not retry automatically.");
1406
- }
1407
- if (error instanceof BridgeError && !error.recoverable)
1408
- throw error;
1409
- throw new BridgeError("CLAUDE_PEER_RECEIPT_NOT_WRITTEN", "The Claude peer receipt was not confirmed written.", true);
1410
- }
1411
- return { transportStatus: "transport_written" };
1412
- }
1413
- #accept(socket) {
1414
- if (this.closed ||
1415
- this.#connections.size >= this.#context.limits.maxConnections) {
1416
- socket.destroy();
1417
- void this.#notice("CONNECTION_LIMIT");
1418
- return;
1419
- }
1420
- this.#connections.add(socket);
1421
- socket.setTimeout(this.#context.limits.connectionIdleMs);
1422
- let buffered = Buffer.alloc(0);
1423
- let frames = 0;
1424
- let chain = Promise.resolve();
1425
- let rejected = false;
1426
- const reject = (code) => {
1427
- if (rejected)
1428
- return;
1429
- rejected = true;
1430
- socket.destroy();
1431
- void this.#notice(code);
1432
- };
1433
- socket.on("timeout", () => reject("CONNECTION_TIMEOUT"));
1434
- socket.on("error", () => undefined);
1435
- socket.on("close", () => this.#connections.delete(socket));
1436
- socket.on("data", (chunk) => {
1437
- if (rejected)
1438
- return;
1439
- buffered = Buffer.concat([buffered, chunk]);
1440
- if (buffered.length > this.#context.limits.maxFrameBytes + 1) {
1441
- const newline = buffered.indexOf(0x0a);
1442
- if (newline < 0 || newline > this.#context.limits.maxFrameBytes) {
1443
- reject("FRAME_TOO_LARGE");
1444
- return;
1445
- }
1446
- }
1447
- while (!rejected) {
1448
- const newline = buffered.indexOf(0x0a);
1449
- if (newline < 0)
1450
- break;
1451
- if (newline > this.#context.limits.maxFrameBytes) {
1452
- reject("FRAME_TOO_LARGE");
1453
- break;
1454
- }
1455
- const line = buffered.subarray(0, newline);
1456
- buffered = buffered.subarray(newline + 1);
1457
- frames += 1;
1458
- if (frames > this.#context.limits.maxFramesPerConnection) {
1459
- reject("INVALID_FRAME");
1460
- break;
1461
- }
1462
- let frame;
1463
- try {
1464
- frame = parseFrame(line, this.#context.limits.maxFrameBytes);
1465
- }
1466
- catch (error) {
1467
- if (error instanceof BridgeError &&
1468
- error.code === "INVALID_PEER_UTF8") {
1469
- reject("INVALID_UTF8");
1470
- }
1471
- else if (error instanceof BridgeError &&
1472
- error.code === "UNSUPPORTED_PEER_FRAME") {
1473
- reject("UNSUPPORTED_FRAME");
1474
- }
1475
- else {
1476
- reject("INVALID_FRAME");
1477
- }
1478
- break;
1479
- }
1480
- if (this.#queuedFrames >=
1481
- this.#context.limits.maxConnections * this.#context.limits.maxFramesPerConnection) {
1482
- reject("CONNECTION_LIMIT");
1483
- break;
1484
- }
1485
- this.#queuedFrames += 1;
1486
- chain = chain
1487
- .then(async () => {
1488
- if (rejected)
1489
- return;
1490
- await this.#handleFrame(frame, () => {
1491
- if (rejected)
1492
- return;
1493
- rejected = true;
1494
- socket.destroy();
1495
- });
1496
- })
1497
- .catch(async () => this.#notice("CALLBACK_ERROR"))
1498
- .finally(() => {
1499
- this.#queuedFrames -= 1;
1500
- if (this.#queuedFrames === 0) {
1501
- for (const resolve of this.#inboundQuiesceWaiters)
1502
- resolve();
1503
- this.#inboundQuiesceWaiters.clear();
1504
- }
1505
- });
1506
- }
1507
- if (!rejected && buffered.length > this.#context.limits.maxFrameBytes) {
1508
- reject("FRAME_TOO_LARGE");
1509
- }
1510
- });
1511
- socket.on("end", () => {
1512
- if (!rejected && buffered.length !== 0)
1513
- reject("INVALID_FRAME");
1514
- });
1515
- }
1516
- async #handleFrame(frame, rejectTransport) {
1517
- if (this.closed)
1518
- return;
1519
- if (frame.type === "control") {
1520
- // Native outbound status is accepted for wire compatibility but is not
1521
- // settlement authority. A confirmed mailbox write is terminal.
1522
- return;
1523
- }
1524
- if (this.#inboundQuiesced) {
1525
- rejectTransport();
1526
- return;
1527
- }
1528
- let binding;
1529
- if (frame.from !== undefined) {
1530
- try {
1531
- binding = await this.#context.resolveReplyAddress(frame.from);
1532
- }
1533
- catch {
1534
- await this.#notice("UNREGISTERED_REPLY_ADDRESS");
1535
- return;
1536
- }
1537
- }
1538
- const inboundId = this.#context.createId();
1539
- if (binding !== undefined && frame.messageId !== undefined) {
1540
- if (this.#inboundReceipts.size >= this.#context.limits.maxPendingReceipts) {
1541
- await this.#notice("RECEIPT_LIMIT");
1542
- if (this.#capacitySettlement !== undefined) {
1543
- // Both the configured receipt table and its single terminal-only
1544
- // overflow slot are occupied. Close this transport rather than
1545
- // making another native send appear accepted without settlement.
1546
- rejectTransport();
1547
- return;
1548
- }
1549
- const settlement = {
1550
- receipt: {
1551
- sourceSessionId: binding.targetId,
1552
- originalMessageId: frame.messageId,
1553
- stallNotification: "settled",
1554
- },
1555
- attempts: 0,
1556
- };
1557
- this.#capacitySettlement = settlement;
1558
- await this.#attemptCapacitySettlement(settlement);
1559
- return;
1560
- }
1561
- this.#inboundReceipts.set(inboundId, {
1562
- sourceSessionId: binding.targetId,
1563
- originalMessageId: frame.messageId,
1564
- stallNotification: "available",
1565
- });
1566
- }
1567
- await invokeHook(this.#context.options.onMessage, {
1568
- inboundId,
1569
- content: frame.content,
1570
- ...(binding === undefined
1571
- ? {}
1572
- : {
1573
- sourceTargetId: binding.targetId,
1574
- sourceAlias: binding.alias,
1575
- ...(frame.messageId === undefined
1576
- ? {}
1577
- : { receiptHandle: inboundId }),
1578
- }),
1579
- replySupported: binding !== undefined,
1580
- trust: binding === undefined
1581
- ? "untrusted_anonymous_local_peer"
1582
- : "untrusted_same_uid_peer",
1583
- });
1584
- }
1585
- async #attemptCapacitySettlement(settlement) {
1586
- if (this.closed || this.#capacitySettlement !== settlement) {
1587
- return;
1588
- }
1589
- settlement.attempts += 1;
1590
- try {
1591
- await this.#writeInboundStatus(settlement.receipt, "expired", { code: "GATEWAY_RECEIPT_CAPACITY" });
1592
- if (this.#capacitySettlement === settlement) {
1593
- this.#capacitySettlement = undefined;
1594
- }
1595
- }
1596
- catch (error) {
1597
- if (this.#capacitySettlement !== settlement)
1598
- return;
1599
- const retryable = error instanceof BridgeError &&
1600
- error.recoverable &&
1601
- !this.closed;
1602
- if (!retryable ||
1603
- settlement.attempts >= CAPACITY_SETTLEMENT_MAX_ATTEMPTS) {
1604
- // An ambiguous write must never be replayed. Exhausted clean
1605
- // pre-write failures are released with a local protocol notice rather
1606
- // than leaking the bounded overflow slot or an unhandled rejection.
1607
- this.#capacitySettlement = undefined;
1608
- await this.#notice("CALLBACK_ERROR");
1609
- return;
1610
- }
1611
- settlement.retryTimer = setTimeout(() => {
1612
- delete settlement.retryTimer;
1613
- void this.#attemptCapacitySettlement(settlement);
1614
- }, CAPACITY_SETTLEMENT_RETRY_MS);
1615
- settlement.retryTimer.unref();
1616
- }
1617
- }
1618
- async #notice(code) {
1619
- try {
1620
- await invokeHook(this.#context.options.onProtocolNotice, { code });
1621
- }
1622
- catch {
1623
- // Protocol callbacks are observational. Never feed callback failures
1624
- // back into the socket or expose raw frame data.
1625
- }
1626
- }
1627
- close() {
1628
- if (this.#closeOperation !== undefined)
1629
- return this.#closeOperation;
1630
- if (this.#closed)
1631
- return Promise.resolve();
1632
- this.#closing = true;
1633
- this.#inboundQuiesced = true;
1634
- const operation = this.#performClose();
1635
- this.#closeOperation = operation;
1636
- return operation;
1637
- }
1638
- async #performClose() {
1639
- const errors = [];
1640
- try {
1641
- await this.unadvertise();
1642
- }
1643
- catch (error) {
1644
- errors.push(error);
1645
- }
1646
- this.#inboundReceipts.clear();
1647
- if (this.#capacitySettlement?.retryTimer !== undefined) {
1648
- clearTimeout(this.#capacitySettlement.retryTimer);
1649
- }
1650
- this.#capacitySettlement = undefined;
1651
- for (const socket of this.#connections)
1652
- socket.destroy();
1653
- this.#connections.clear();
1654
- try {
1655
- if (await socketOwnership(this.#socketPath, this.#socketGeneration) !==
1656
- "owned") {
1657
- // Node's net.Server.close() unlinks its original pathname even when
1658
- // another same-user object replaced it. Do not call it after an
1659
- // observed generation change. The now-unreachable descriptor is
1660
- // unref'd and left for process exit rather than deleting foreign data.
1661
- this.#server.unref();
1662
- errors.push(new BridgeError("CLAUDE_PEER_CALLBACK_CHANGED", "The callback path changed; foreign replacement was preserved."));
1663
- }
1664
- else {
1665
- await closeServer(this.#server);
1666
- if (await socketOwnership(this.#socketPath, this.#socketGeneration) ===
1667
- "owned") {
1668
- await unlink(this.#socketPath);
1669
- }
1670
- }
1671
- }
1672
- catch (error) {
1673
- this.#server.unref();
1674
- errors.push(error);
1675
- }
1676
- this.#closing = false;
1677
- this.#closed = true;
1678
- try {
1679
- this.#context.onClosed();
1680
- }
1681
- catch (error) {
1682
- errors.push(error);
1683
- }
1684
- if (errors.length === 1)
1685
- throw errors[0];
1686
- if (errors.length > 1) {
1687
- throw new AggregateError(errors, "The Claude peer listener closed with multiple cleanup failures.");
1688
- }
1689
698
  }
1690
699
  }
1691
700
  //# sourceMappingURL=claude-peer.js.map