agent-embassy 1.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 (145) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/CONTRIBUTING.md +122 -0
  3. package/LICENSE +21 -0
  4. package/README.md +185 -0
  5. package/README.zh-CN.md +185 -0
  6. package/SECURITY.md +224 -0
  7. package/assets/live-dashboard/app.css +1610 -0
  8. package/assets/mark-seal.svg +7 -0
  9. package/assets/mark.svg +7 -0
  10. package/assets/social-preview.png +0 -0
  11. package/assets/vendor/react/LICENSE +21 -0
  12. package/assets/vendor/react/react-dom.production.min.js +267 -0
  13. package/assets/vendor/react/react.production.min.js +31 -0
  14. package/dist/src/errors.d.ts +5 -0
  15. package/dist/src/errors.js +11 -0
  16. package/dist/src/errors.js.map +1 -0
  17. package/dist/src/gateway/claude-compatibility-scratch.d.ts +18 -0
  18. package/dist/src/gateway/claude-compatibility-scratch.js +158 -0
  19. package/dist/src/gateway/claude-compatibility-scratch.js.map +1 -0
  20. package/dist/src/gateway/claude-helper-client.d.ts +43 -0
  21. package/dist/src/gateway/claude-helper-client.js +212 -0
  22. package/dist/src/gateway/claude-helper-client.js.map +1 -0
  23. package/dist/src/gateway/claude-helper-protocol.d.ts +172 -0
  24. package/dist/src/gateway/claude-helper-protocol.js +373 -0
  25. package/dist/src/gateway/claude-helper-protocol.js.map +1 -0
  26. package/dist/src/gateway/claude-helper-supervisor.d.ts +74 -0
  27. package/dist/src/gateway/claude-helper-supervisor.js +497 -0
  28. package/dist/src/gateway/claude-helper-supervisor.js.map +1 -0
  29. package/dist/src/gateway/claude-helper.d.ts +1 -0
  30. package/dist/src/gateway/claude-helper.js +269 -0
  31. package/dist/src/gateway/claude-helper.js.map +1 -0
  32. package/dist/src/gateway/claude-peer.d.ts +273 -0
  33. package/dist/src/gateway/claude-peer.js +2359 -0
  34. package/dist/src/gateway/claude-peer.js.map +1 -0
  35. package/dist/src/gateway/claude-runtime.d.ts +40 -0
  36. package/dist/src/gateway/claude-runtime.js +302 -0
  37. package/dist/src/gateway/claude-runtime.js.map +1 -0
  38. package/dist/src/gateway/cli-copy.d.ts +8 -0
  39. package/dist/src/gateway/cli-copy.en.d.ts +13 -0
  40. package/dist/src/gateway/cli-copy.en.js +43 -0
  41. package/dist/src/gateway/cli-copy.en.js.map +1 -0
  42. package/dist/src/gateway/cli-copy.js +19 -0
  43. package/dist/src/gateway/cli-copy.js.map +1 -0
  44. package/dist/src/gateway/cli-copy.zh-CN.d.ts +13 -0
  45. package/dist/src/gateway/cli-copy.zh-CN.js +43 -0
  46. package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -0
  47. package/dist/src/gateway/cli.d.ts +44 -0
  48. package/dist/src/gateway/cli.js +969 -0
  49. package/dist/src/gateway/cli.js.map +1 -0
  50. package/dist/src/gateway/codex-app-server.d.ts +310 -0
  51. package/dist/src/gateway/codex-app-server.js +1746 -0
  52. package/dist/src/gateway/codex-app-server.js.map +1 -0
  53. package/dist/src/gateway/codex-local-transport.d.ts +81 -0
  54. package/dist/src/gateway/codex-local-transport.js +617 -0
  55. package/dist/src/gateway/codex-local-transport.js.map +1 -0
  56. package/dist/src/gateway/codex-registration-generation.d.ts +5 -0
  57. package/dist/src/gateway/codex-registration-generation.js +19 -0
  58. package/dist/src/gateway/codex-registration-generation.js.map +1 -0
  59. package/dist/src/gateway/codex-registration-succession.d.ts +209 -0
  60. package/dist/src/gateway/codex-registration-succession.js +588 -0
  61. package/dist/src/gateway/codex-registration-succession.js.map +1 -0
  62. package/dist/src/gateway/compatibility.d.ts +70 -0
  63. package/dist/src/gateway/compatibility.js +252 -0
  64. package/dist/src/gateway/compatibility.js.map +1 -0
  65. package/dist/src/gateway/config.d.ts +24 -0
  66. package/dist/src/gateway/config.js +130 -0
  67. package/dist/src/gateway/config.js.map +1 -0
  68. package/dist/src/gateway/control.d.ts +308 -0
  69. package/dist/src/gateway/control.js +1573 -0
  70. package/dist/src/gateway/control.js.map +1 -0
  71. package/dist/src/gateway/dashboard-copy.d.ts +7 -0
  72. package/dist/src/gateway/dashboard-copy.en.d.ts +506 -0
  73. package/dist/src/gateway/dashboard-copy.en.js +507 -0
  74. package/dist/src/gateway/dashboard-copy.en.js.map +1 -0
  75. package/dist/src/gateway/dashboard-copy.js +516 -0
  76. package/dist/src/gateway/dashboard-copy.js.map +1 -0
  77. package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +506 -0
  78. package/dist/src/gateway/dashboard-copy.zh-CN.js +507 -0
  79. package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -0
  80. package/dist/src/gateway/dashboard-model.d.ts +207 -0
  81. package/dist/src/gateway/dashboard-model.js +720 -0
  82. package/dist/src/gateway/dashboard-model.js.map +1 -0
  83. package/dist/src/gateway/dashboard.d.ts +26 -0
  84. package/dist/src/gateway/dashboard.js +941 -0
  85. package/dist/src/gateway/dashboard.js.map +1 -0
  86. package/dist/src/gateway/delivery-machine.d.ts +235 -0
  87. package/dist/src/gateway/delivery-machine.js +540 -0
  88. package/dist/src/gateway/delivery-machine.js.map +1 -0
  89. package/dist/src/gateway/instance-lease.d.ts +21 -0
  90. package/dist/src/gateway/instance-lease.js +549 -0
  91. package/dist/src/gateway/instance-lease.js.map +1 -0
  92. package/dist/src/gateway/live-dashboard-app/app.js +2914 -0
  93. package/dist/src/gateway/live-dashboard-assets.d.ts +10 -0
  94. package/dist/src/gateway/live-dashboard-assets.js +73 -0
  95. package/dist/src/gateway/live-dashboard-assets.js.map +1 -0
  96. package/dist/src/gateway/live-dashboard-bootstrap.d.ts +36 -0
  97. package/dist/src/gateway/live-dashboard-bootstrap.js +176 -0
  98. package/dist/src/gateway/live-dashboard-bootstrap.js.map +1 -0
  99. package/dist/src/gateway/live-dashboard-command.d.ts +54 -0
  100. package/dist/src/gateway/live-dashboard-command.js +324 -0
  101. package/dist/src/gateway/live-dashboard-command.js.map +1 -0
  102. package/dist/src/gateway/live-dashboard-http.d.ts +41 -0
  103. package/dist/src/gateway/live-dashboard-http.js +445 -0
  104. package/dist/src/gateway/live-dashboard-http.js.map +1 -0
  105. package/dist/src/gateway/live-dashboard-protocol.d.ts +38 -0
  106. package/dist/src/gateway/live-dashboard-protocol.js +156 -0
  107. package/dist/src/gateway/live-dashboard-protocol.js.map +1 -0
  108. package/dist/src/gateway/live-dashboard-server.d.ts +33 -0
  109. package/dist/src/gateway/live-dashboard-server.js +130 -0
  110. package/dist/src/gateway/live-dashboard-server.js.map +1 -0
  111. package/dist/src/gateway/live-dashboard-stream.d.ts +46 -0
  112. package/dist/src/gateway/live-dashboard-stream.js +233 -0
  113. package/dist/src/gateway/live-dashboard-stream.js.map +1 -0
  114. package/dist/src/gateway/live-dashboard.d.ts +31 -0
  115. package/dist/src/gateway/live-dashboard.js +190 -0
  116. package/dist/src/gateway/live-dashboard.js.map +1 -0
  117. package/dist/src/gateway/locale.d.ts +4 -0
  118. package/dist/src/gateway/locale.js +10 -0
  119. package/dist/src/gateway/locale.js.map +1 -0
  120. package/dist/src/gateway/progress-watch-machine.d.ts +97 -0
  121. package/dist/src/gateway/progress-watch-machine.js +172 -0
  122. package/dist/src/gateway/progress-watch-machine.js.map +1 -0
  123. package/dist/src/gateway/providers.d.ts +287 -0
  124. package/dist/src/gateway/providers.js +2451 -0
  125. package/dist/src/gateway/providers.js.map +1 -0
  126. package/dist/src/gateway/server.d.ts +57 -0
  127. package/dist/src/gateway/server.js +264 -0
  128. package/dist/src/gateway/server.js.map +1 -0
  129. package/dist/src/gateway/service.d.ts +449 -0
  130. package/dist/src/gateway/service.js +4831 -0
  131. package/dist/src/gateway/service.js.map +1 -0
  132. package/dist/src/gateway/store.d.ts +340 -0
  133. package/dist/src/gateway/store.js +4047 -0
  134. package/dist/src/gateway/store.js.map +1 -0
  135. package/dist/src/gateway/types.d.ts +514 -0
  136. package/dist/src/gateway/types.js +414 -0
  137. package/dist/src/gateway/types.js.map +1 -0
  138. package/dist/src/mutex.d.ts +4 -0
  139. package/dist/src/mutex.js +23 -0
  140. package/dist/src/mutex.js.map +1 -0
  141. package/docs/DESIGN.md +134 -0
  142. package/docs/GATEWAY-ARCHITECTURE.md +858 -0
  143. package/package.json +80 -0
  144. package/skills/embassy-peer/SKILL.md +198 -0
  145. package/skills/embassy-peer/agents/openai.yaml +4 -0
@@ -0,0 +1,2359 @@
1
+ import { execFile } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { chmod, constants as fsConstants, lstat, open, opendir, realpath, rename, unlink, writeFile, } from "node:fs/promises";
4
+ import net from "node:net";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import { TextDecoder } from "node:util";
8
+ import { BridgeError } from "../errors.js";
9
+ import { KeyedMutex } from "../mutex.js";
10
+ import { createCodexRegistrationGeneration, isCodexRegistrationGeneration, } from "./codex-registration-generation.js";
11
+ import { isDashboardLocale } from "./locale.js";
12
+ import { sharesCompatibilityMajor } 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.226",
20
+ peerProtocol: 1,
21
+ messageVersion: 1,
22
+ });
23
+ /** Live peer records may outlive a same-protocol Claude Code upgrade. */
24
+ export const CLAUDE_PEER_COMPATIBLE_SESSION_VERSIONS = Object.freeze([
25
+ "2.1.224",
26
+ "2.1.225",
27
+ "2.1.226",
28
+ ]);
29
+ 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;
30
+ const ALIAS_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
31
+ const CLAUDE_PEER_NOTICE_COPY = {
32
+ en: {
33
+ stall: "The local gateway is still waiting to deliver the preceding message. Run `embassy status` or inspect the dashboard for details.",
34
+ diagnostic: "The local gateway could not deliver the preceding message. Run `embassy status` or inspect the dashboard for details.",
35
+ },
36
+ "zh-CN": {
37
+ stall: "本地网关仍在等待投递前一条消息。运行 `embassy status` 或查看仪表盘了解详情。",
38
+ diagnostic: "本地网关无法投递前一条消息。运行 `embassy status` 或查看仪表盘了解详情。",
39
+ },
40
+ };
41
+ const REGISTRY_FILE_PATTERN = /^([1-9][0-9]{0,9})\.json$/;
42
+ const SOCKET_FILE_PATTERN = /^([1-9][0-9]{0,9})\.sock$/;
43
+ const MAX_PID = 2_147_483_647;
44
+ export const claudePeerStatuses = [
45
+ "busy",
46
+ "shell",
47
+ "idle",
48
+ "waiting",
49
+ ];
50
+ export const claudePeerKinds = [
51
+ "interactive",
52
+ "bg",
53
+ "daemon",
54
+ "daemon-worker",
55
+ ];
56
+ export const claudePeerRejectionCodes = [
57
+ "ENTRY_LIMIT_EXCEEDED",
58
+ "INVALID_FILE_NAME",
59
+ "REGISTRY_NOT_REGULAR",
60
+ "REGISTRY_OWNER_MISMATCH",
61
+ "REGISTRY_MODE_UNSAFE",
62
+ "REGISTRY_TOO_LARGE",
63
+ "REGISTRY_RACED",
64
+ "REGISTRY_INVALID_JSON",
65
+ "REGISTRY_INVALID_SCHEMA",
66
+ "PID_MISMATCH",
67
+ "PID_NOT_LIVE",
68
+ "PID_OWNER_MISMATCH",
69
+ "SOCKET_OUTSIDE_ROOT",
70
+ "SOCKET_NOT_SOCKET",
71
+ "SOCKET_OWNER_MISMATCH",
72
+ "SOCKET_MODE_UNSAFE",
73
+ "SELF_TARGET",
74
+ "SESSION_ID_COLLISION",
75
+ ];
76
+ export const claudePeerInboundStallReasons = [
77
+ "ROUTE_BUSY",
78
+ "ROUTE_UNAVAILABLE",
79
+ "CODEX_ROUTE_STALE",
80
+ "AWAITING_EXTERNAL_APPROVAL",
81
+ ];
82
+ function pendingReceiptDeadlineStatus(pending) {
83
+ if (pending.writeEvidence === "transport_written" ||
84
+ pending.state === "held") {
85
+ return "unconfirmed";
86
+ }
87
+ return pending.writeEvidence === "transport_uncertain"
88
+ ? "ambiguous"
89
+ : "expired";
90
+ }
91
+ const MAX_CLAUDE_STALL_QUEUED_MS = 3_600_000;
92
+ const CAPACITY_SETTLEMENT_MAX_ATTEMPTS = 3;
93
+ const CAPACITY_SETTLEMENT_RETRY_MS = 25;
94
+ function isObject(value) {
95
+ return typeof value === "object" && value !== null && !Array.isArray(value);
96
+ }
97
+ function hasExactKeys(value, required, optional = []) {
98
+ const permitted = new Set([...required, ...optional]);
99
+ return (required.every((key) => Object.hasOwn(value, key)) &&
100
+ Object.keys(value).every((key) => permitted.has(key)));
101
+ }
102
+ function byteLength(value) {
103
+ return Buffer.byteLength(value, "utf8");
104
+ }
105
+ function isBoundedString(value, maxBytes) {
106
+ return typeof value === "string" && byteLength(value) <= maxBytes;
107
+ }
108
+ function validateContent(content, maxBytes) {
109
+ if (typeof content !== "string" ||
110
+ content.length === 0 ||
111
+ content.includes("\0") ||
112
+ byteLength(content) > maxBytes) {
113
+ throw new BridgeError("INVALID_PEER_CONTENT", "Claude peer content must be a non-empty bounded UTF-8 string without NUL bytes.");
114
+ }
115
+ return content;
116
+ }
117
+ function exactMode(mode) {
118
+ return mode & 0o777;
119
+ }
120
+ function generationOf(stat) {
121
+ return {
122
+ dev: stat.dev,
123
+ ino: stat.ino,
124
+ size: stat.size,
125
+ mtimeMs: stat.mtimeMs,
126
+ };
127
+ }
128
+ function sameFileGeneration(left, right) {
129
+ return (left.dev === right.dev &&
130
+ left.ino === right.ino &&
131
+ left.size === right.size &&
132
+ left.mtimeMs === right.mtimeMs);
133
+ }
134
+ function sameSocketGeneration(left, right) {
135
+ return left.dev === right.dev && left.ino === right.ino;
136
+ }
137
+ function sameDirectoryGeneration(left, right) {
138
+ return left.dev === right.dev && left.ino === right.ino;
139
+ }
140
+ function sameTargetGeneration(left, right) {
141
+ return (left.registryPath === right.registryPath &&
142
+ left.record.pid === right.record.pid &&
143
+ left.record.sessionId === right.record.sessionId &&
144
+ left.record.name === right.record.name &&
145
+ left.record.cwd === right.record.cwd &&
146
+ left.record.kind === right.record.kind &&
147
+ left.record.messagingSocketPath === right.record.messagingSocketPath &&
148
+ left.processGeneration === right.processGeneration &&
149
+ sameSocketGeneration(left.socketGeneration, right.socketGeneration));
150
+ }
151
+ function pathContains(parent, child) {
152
+ const relative = path.relative(parent, child);
153
+ return (relative === "" ||
154
+ (!path.isAbsolute(relative) &&
155
+ relative !== ".." &&
156
+ !relative.startsWith(`..${path.sep}`)));
157
+ }
158
+ function parsePositiveInteger(value) {
159
+ if (!/^[1-9][0-9]*$/.test(value))
160
+ return undefined;
161
+ const parsed = Number(value);
162
+ if (!Number.isSafeInteger(parsed) || parsed > MAX_PID)
163
+ return undefined;
164
+ return parsed;
165
+ }
166
+ function assertAbsoluteConfiguredPath(value, label) {
167
+ if (!path.isAbsolute(value) ||
168
+ value.includes("\0") ||
169
+ path.resolve(value) !== value) {
170
+ throw new BridgeError("INVALID_PEER_PATH", `${label} must be an absolute normalized path.`);
171
+ }
172
+ return value;
173
+ }
174
+ function configuredLimit(value, fallback, minimum, maximum, label) {
175
+ const resolved = value ?? fallback;
176
+ if (!Number.isSafeInteger(resolved) ||
177
+ resolved < minimum ||
178
+ resolved > maximum) {
179
+ throw new BridgeError("INVALID_PEER_LIMIT", `${label} is outside the supported bounded range.`);
180
+ }
181
+ return resolved;
182
+ }
183
+ function boundedStallQueuedForMs(value) {
184
+ if (Number.isNaN(value) || value === Number.NEGATIVE_INFINITY)
185
+ return 0;
186
+ if (value === Number.POSITIVE_INFINITY) {
187
+ return MAX_CLAUDE_STALL_QUEUED_MS;
188
+ }
189
+ return Math.min(MAX_CLAUDE_STALL_QUEUED_MS, Math.max(0, Math.trunc(value)));
190
+ }
191
+ function parseRegistryRecord(value, expectedPid) {
192
+ if (!isObject(value))
193
+ return undefined;
194
+ const required = [
195
+ "pid",
196
+ "sessionId",
197
+ "cwd",
198
+ "startedAt",
199
+ "procStart",
200
+ "version",
201
+ "peerProtocol",
202
+ "kind",
203
+ "entrypoint",
204
+ "messagingSocketPath",
205
+ "name",
206
+ "updatedAt",
207
+ ];
208
+ const optional = ["nameSource", "status", "statusUpdatedAt"];
209
+ if (!hasExactKeys(value, required, optional))
210
+ return undefined;
211
+ if (value.pid !== expectedPid)
212
+ return undefined;
213
+ if (typeof value.sessionId !== "string" ||
214
+ !UUID_PATTERN.test(value.sessionId)) {
215
+ return undefined;
216
+ }
217
+ if (!isBoundedString(value.cwd, 4096) ||
218
+ !path.isAbsolute(value.cwd) ||
219
+ value.cwd.includes("\0")) {
220
+ return undefined;
221
+ }
222
+ if (!Number.isSafeInteger(value.startedAt) ||
223
+ value.startedAt < 0 ||
224
+ !Number.isSafeInteger(value.updatedAt) ||
225
+ value.updatedAt < 0 ||
226
+ (value.statusUpdatedAt !== undefined &&
227
+ (!Number.isSafeInteger(value.statusUpdatedAt) ||
228
+ value.statusUpdatedAt < 0)) ||
229
+ !isBoundedString(value.procStart, 256) ||
230
+ value.procStart.length === 0 ||
231
+ value.procStart.includes("\0") ||
232
+ typeof value.version !== "string" ||
233
+ !sharesCompatibilityMajor(value.version, CLAUDE_PEER_COMPATIBILITY.claudeCodeVersion) ||
234
+ !isBoundedString(value.entrypoint, 64) ||
235
+ !/^[A-Za-z0-9._-]+$/.test(value.entrypoint) ||
236
+ (value.nameSource !== undefined &&
237
+ value.nameSource !== null &&
238
+ (!isBoundedString(value.nameSource, 64) ||
239
+ !/^[A-Za-z0-9._-]+$/.test(value.nameSource)))) {
240
+ return undefined;
241
+ }
242
+ if (value.peerProtocol !== CLAUDE_PEER_COMPATIBILITY.peerProtocol) {
243
+ return undefined;
244
+ }
245
+ if (typeof value.kind !== "string" ||
246
+ !claudePeerKinds.includes(value.kind)) {
247
+ return undefined;
248
+ }
249
+ if (!isBoundedString(value.messagingSocketPath, 4096) ||
250
+ !path.isAbsolute(value.messagingSocketPath) ||
251
+ value.messagingSocketPath.includes("\0")) {
252
+ return undefined;
253
+ }
254
+ if (typeof value.name !== "string" ||
255
+ !ALIAS_PATTERN.test(value.name)) {
256
+ return undefined;
257
+ }
258
+ if (value.status !== undefined &&
259
+ (typeof value.status !== "string" ||
260
+ !claudePeerStatuses.includes(value.status))) {
261
+ return undefined;
262
+ }
263
+ return {
264
+ pid: expectedPid,
265
+ sessionId: value.sessionId.toLowerCase(),
266
+ cwd: value.cwd,
267
+ peerProtocol: 1,
268
+ kind: value.kind,
269
+ messagingSocketPath: value.messagingSocketPath,
270
+ name: value.name,
271
+ // Some reviewed Claude Code print/SDK session records omit status fields
272
+ // while the model turn is active. Treat that live process conservatively
273
+ // as busy.
274
+ status: (value.status ?? "busy"),
275
+ };
276
+ }
277
+ function parseFrame(line, maxFrameBytes) {
278
+ if (line.length === 0 || line.length > maxFrameBytes) {
279
+ throw new BridgeError("INVALID_PEER_FRAME", "Invalid peer frame size.");
280
+ }
281
+ let text;
282
+ try {
283
+ text = new TextDecoder("utf-8", { fatal: true }).decode(line);
284
+ }
285
+ catch {
286
+ throw new BridgeError("INVALID_PEER_UTF8", "Invalid peer frame encoding.");
287
+ }
288
+ let value;
289
+ try {
290
+ value = JSON.parse(text);
291
+ }
292
+ catch {
293
+ throw new BridgeError("INVALID_PEER_FRAME", "Invalid peer frame JSON.");
294
+ }
295
+ if (!isObject(value) || typeof value.type !== "string") {
296
+ throw new BridgeError("INVALID_PEER_FRAME", "Invalid peer frame shape.");
297
+ }
298
+ if (value.type === "user") {
299
+ if (!hasExactKeys(value, ["type", "message"], ["msgV", "msg_id", "priority", "from"]) ||
300
+ !isObject(value.message) ||
301
+ !hasExactKeys(value.message, ["role", "content"]) ||
302
+ value.message.role !== "user") {
303
+ throw new BridgeError("INVALID_PEER_FRAME", "Invalid user frame shape.");
304
+ }
305
+ const canonicalFields = [value.msgV, value.msg_id, value.priority];
306
+ const canonicalCount = canonicalFields.filter((field) => field !== undefined).length;
307
+ if (canonicalCount !== 0 &&
308
+ (canonicalCount !== 3 ||
309
+ value.msgV !== 1 ||
310
+ typeof value.msg_id !== "string" ||
311
+ !UUID_PATTERN.test(value.msg_id) ||
312
+ value.priority !== "next")) {
313
+ throw new BridgeError("INVALID_PEER_FRAME", "Invalid canonical user frame fields.");
314
+ }
315
+ if (value.from !== undefined &&
316
+ (!isBoundedString(value.from, 4096) || !value.from.startsWith("uds:"))) {
317
+ throw new BridgeError("INVALID_PEER_FRAME", "Invalid reply address.");
318
+ }
319
+ const content = validateContent(value.message.content, maxFrameBytes);
320
+ return {
321
+ type: "user",
322
+ content,
323
+ ...(typeof value.msg_id === "string"
324
+ ? { messageId: value.msg_id }
325
+ : {}),
326
+ ...(typeof value.from === "string" ? { from: value.from } : {}),
327
+ };
328
+ }
329
+ if (value.type === "control") {
330
+ if (!hasExactKeys(value, [
331
+ "type",
332
+ "action",
333
+ "status",
334
+ "reason",
335
+ "from",
336
+ "orig_msg_id",
337
+ "msgV",
338
+ "msg_id",
339
+ ]) ||
340
+ value.action !== "peer_message_status" ||
341
+ !["held", "denied", "expired", "delivered"].includes(String(value.status)) ||
342
+ !isBoundedString(value.reason, 1024) ||
343
+ !isBoundedString(value.from, 4096) ||
344
+ !value.from.startsWith("uds:") ||
345
+ typeof value.orig_msg_id !== "string" ||
346
+ !UUID_PATTERN.test(value.orig_msg_id) ||
347
+ value.msgV !== 1 ||
348
+ typeof value.msg_id !== "string" ||
349
+ !UUID_PATTERN.test(value.msg_id)) {
350
+ throw new BridgeError("INVALID_PEER_FRAME", "Invalid peer status frame.");
351
+ }
352
+ return {
353
+ type: "control",
354
+ action: "peer_message_status",
355
+ status: value.status,
356
+ from: value.from,
357
+ originalMessageId: value.orig_msg_id,
358
+ };
359
+ }
360
+ throw new BridgeError("UNSUPPORTED_PEER_FRAME", "Unsupported Claude peer frame type.");
361
+ }
362
+ export function encodeClaudePeerUserFrame(input) {
363
+ if (!UUID_PATTERN.test(input.messageId)) {
364
+ throw new BridgeError("INVALID_PEER_MESSAGE_ID", "The peer message ID must be a UUID.");
365
+ }
366
+ const maxFrameBytes = configuredLimit(input.maxFrameBytes, 64 * 1024, 256, 1024 * 1024, "maxFrameBytes");
367
+ const content = validateContent(input.content, maxFrameBytes);
368
+ if (input.from !== undefined &&
369
+ (!input.from.startsWith("uds:") ||
370
+ !path.isAbsolute(input.from.slice(4)) ||
371
+ input.from.includes("\0"))) {
372
+ throw new BridgeError("INVALID_PEER_REPLY_ADDRESS", "The peer reply address must be an absolute uds address.");
373
+ }
374
+ const frame = {
375
+ msgV: 1,
376
+ msg_id: input.messageId,
377
+ type: "user",
378
+ message: { role: "user", content },
379
+ priority: "next",
380
+ ...(input.from === undefined ? {} : { from: input.from }),
381
+ };
382
+ const encoded = Buffer.from(`${JSON.stringify(frame)}\n`, "utf8");
383
+ if (encoded.length > maxFrameBytes + 1) {
384
+ throw new BridgeError("PEER_FRAME_TOO_LARGE", "The encoded Claude peer frame exceeds the configured limit.");
385
+ }
386
+ return encoded;
387
+ }
388
+ async function defaultProcessInspector(pid) {
389
+ const executable = "/bin/ps";
390
+ return await new Promise((resolve, reject) => {
391
+ execFile(executable, ["-o", "uid=,lstart=", "-p", String(pid)], {
392
+ encoding: "utf8",
393
+ env: { LC_ALL: "C", PATH: "/usr/bin:/bin:/usr/sbin:/sbin" },
394
+ timeout: 1_000,
395
+ maxBuffer: 4_096,
396
+ windowsHide: true,
397
+ }, (error, stdout) => {
398
+ if (error) {
399
+ // A missing/exited process and a bounded ps(1) failure both fail
400
+ // closed as "not live". No stderr or command output is surfaced.
401
+ resolve(undefined);
402
+ return;
403
+ }
404
+ const match = /^\s*([0-9]+)\s+(.+?)\s*$/.exec(stdout);
405
+ if (match === null) {
406
+ resolve(undefined);
407
+ return;
408
+ }
409
+ const uid = Number(match[1]);
410
+ const generation = match[2];
411
+ if (!Number.isSafeInteger(uid) || generation === undefined) {
412
+ resolve(undefined);
413
+ return;
414
+ }
415
+ resolve({ uid, generation });
416
+ });
417
+ });
418
+ }
419
+ async function invokeHook(hook, value) {
420
+ if (hook === undefined)
421
+ return;
422
+ await hook(value);
423
+ }
424
+ async function invokeObservationalHook(hook, value) {
425
+ try {
426
+ await invokeHook(hook, value);
427
+ }
428
+ catch {
429
+ // Status observation must never change a possibly-written transport
430
+ // outcome or invite an unsafe caller retry.
431
+ }
432
+ }
433
+ export class ClaudePeerAdapter {
434
+ #sessionsDir;
435
+ #socketDir;
436
+ #expectedUid;
437
+ #limits;
438
+ #inspectProcess;
439
+ #connect;
440
+ #now;
441
+ #createId;
442
+ #createGeneration;
443
+ #locale;
444
+ #attestedClaudeCodeVersion;
445
+ #deliveryNotices;
446
+ #registryRename;
447
+ #registryOperationHook;
448
+ #userHome;
449
+ #tempRoots;
450
+ #listenerOwner = Object.freeze({});
451
+ #registryMutex = new KeyedMutex();
452
+ #registryPublicationHook;
453
+ #targets = new Map();
454
+ #workspacePolicies = new Map();
455
+ #listeners = new Set();
456
+ constructor(options, testing = {}) {
457
+ if (process.platform === "win32" || process.getuid === undefined) {
458
+ throw new BridgeError("CLAUDE_PEER_PLATFORM_UNSUPPORTED", "Claude peer sockets are supported only on macOS and Linux.");
459
+ }
460
+ if (!sharesCompatibilityMajor(options.attestedClaudeCodeVersion, CLAUDE_PEER_COMPATIBILITY.claudeCodeVersion)) {
461
+ throw new BridgeError("CLAUDE_PEER_VERSION_UNSUPPORTED", `Claude peer compatibility is pinned to Claude Code ${CLAUDE_PEER_COMPATIBILITY.claudeCodeVersion}.`);
462
+ }
463
+ if (options.locale !== undefined && !isDashboardLocale(options.locale)) {
464
+ throw new BridgeError("DASHBOARD_LOCALE_UNSUPPORTED", "The Claude peer notice locale is unsupported.");
465
+ }
466
+ this.#locale = options.locale ?? "en";
467
+ this.#attestedClaudeCodeVersion = options.attestedClaudeCodeVersion;
468
+ if (options.deliveryNotices !== undefined &&
469
+ !["merged", "verbose", "quiet"].includes(options.deliveryNotices)) {
470
+ throw new BridgeError("INVALID_GATEWAY_CONFIGURATION", "The Claude peer delivery notice mode is unsupported.");
471
+ }
472
+ this.#deliveryNotices = options.deliveryNotices ?? "merged";
473
+ this.#sessionsDir = assertAbsoluteConfiguredPath(options.sessionsDir, "sessionsDir");
474
+ this.#socketDir = assertAbsoluteConfiguredPath(options.socketDir, "socketDir");
475
+ this.#expectedUid = process.getuid();
476
+ if (!Number.isSafeInteger(this.#expectedUid) || this.#expectedUid < 0) {
477
+ throw new BridgeError("INVALID_PEER_UID", "expectedUid must be a non-negative integer.");
478
+ }
479
+ this.#limits = {
480
+ maxRegistryEntries: configuredLimit(options.maxRegistryEntries, 256, 1, 4_096, "maxRegistryEntries"),
481
+ maxRegistryBytes: configuredLimit(options.maxRegistryBytes, 16 * 1024, 512, 1024 * 1024, "maxRegistryBytes"),
482
+ maxFrameBytes: configuredLimit(options.maxFrameBytes, 64 * 1024, 256, 1024 * 1024, "maxFrameBytes"),
483
+ targetLeaseMs: configuredLimit(options.targetLeaseMs, 30_000, 100, 5 * 60_000, "targetLeaseMs"),
484
+ connectTimeoutMs: configuredLimit(options.connectTimeoutMs, 2_000, 10, 30_000, "connectTimeoutMs"),
485
+ receiptDeadlineMs: configuredLimit(options.receiptDeadlineMs, 5 * 60_000, 10, 10 * 60_000, "receiptDeadlineMs"),
486
+ maxPendingReceipts: configuredLimit(options.maxPendingReceipts, 256, 1, 4_096, "maxPendingReceipts"),
487
+ maxConnections: configuredLimit(options.maxConnections, 32, 1, 1_024, "maxConnections"),
488
+ connectionIdleMs: configuredLimit(options.connectionIdleMs, 5_000, 10, 60_000, "connectionIdleMs"),
489
+ maxFramesPerConnection: configuredLimit(options.maxFramesPerConnection, 8, 1, 128, "maxFramesPerConnection"),
490
+ };
491
+ this.#inspectProcess = testing.processInspector ?? defaultProcessInspector;
492
+ this.#connect =
493
+ testing.connect ??
494
+ ((socketPath) => net.createConnection({ path: socketPath }));
495
+ this.#now = testing.now ?? Date.now;
496
+ this.#createId = testing.createId ?? randomUUID;
497
+ this.#createGeneration =
498
+ testing.createGeneration ?? createCodexRegistrationGeneration;
499
+ this.#registryRename = testing.registryRename ?? rename;
500
+ this.#registryOperationHook = testing.registryOperationHook;
501
+ this.#userHome = assertAbsoluteConfiguredPath(testing.userHome ?? os.userInfo().homedir, "userHome");
502
+ this.#tempRoots = Object.freeze([
503
+ ...(testing.tempRoots ?? ["/tmp", "/private/tmp", os.tmpdir()]),
504
+ ].map((root) => assertAbsoluteConfiguredPath(root, "tempRoot")));
505
+ this.#registryPublicationHook = testing.registryPublicationHook;
506
+ }
507
+ async #validatePrivateDirectory(directory) {
508
+ const stat = await lstat(directory);
509
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
510
+ throw new BridgeError("UNSAFE_PEER_DIRECTORY", "The Claude peer directory is not an accessible real directory.");
511
+ }
512
+ }
513
+ async #readRegistryFile(registryPath) {
514
+ const before = await lstat(registryPath);
515
+ if (before.isSymbolicLink() || !before.isFile()) {
516
+ throw new BridgeError("REGISTRY_NOT_REGULAR", "Unsafe registry type.");
517
+ }
518
+ if (before.size > this.#limits.maxRegistryBytes) {
519
+ throw new BridgeError("REGISTRY_TOO_LARGE", "Registry is too large.");
520
+ }
521
+ const handle = await open(registryPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
522
+ try {
523
+ const opened = await handle.stat();
524
+ const beforeGeneration = generationOf(before);
525
+ const openedGeneration = generationOf(opened);
526
+ if (!sameFileGeneration(beforeGeneration, openedGeneration)) {
527
+ throw new BridgeError("REGISTRY_RACED", "Registry changed while opening.");
528
+ }
529
+ const buffer = Buffer.alloc(this.#limits.maxRegistryBytes + 1);
530
+ let offset = 0;
531
+ while (offset < buffer.length) {
532
+ const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, null);
533
+ if (bytesRead === 0)
534
+ break;
535
+ offset += bytesRead;
536
+ }
537
+ if (offset > this.#limits.maxRegistryBytes) {
538
+ throw new BridgeError("REGISTRY_TOO_LARGE", "Registry is too large.");
539
+ }
540
+ const after = await lstat(registryPath);
541
+ const afterGeneration = generationOf(after);
542
+ if (!sameFileGeneration(openedGeneration, afterGeneration)) {
543
+ throw new BridgeError("REGISTRY_RACED", "Registry changed while reading.");
544
+ }
545
+ let value;
546
+ try {
547
+ value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, offset)));
548
+ }
549
+ catch {
550
+ throw new BridgeError("REGISTRY_INVALID_JSON", "Registry JSON is invalid.");
551
+ }
552
+ return { value, generation: openedGeneration };
553
+ }
554
+ finally {
555
+ await handle.close();
556
+ }
557
+ }
558
+ async #validateSocket(socketPath, expectedPid) {
559
+ if (path.dirname(socketPath) !== this.#socketDir ||
560
+ path.basename(socketPath) !== `${expectedPid}.sock`) {
561
+ throw new BridgeError("SOCKET_OUTSIDE_ROOT", "Peer socket is outside the exact trusted socket root.");
562
+ }
563
+ const match = SOCKET_FILE_PATTERN.exec(path.basename(socketPath));
564
+ if (match === null || parsePositiveInteger(match[1] ?? "") !== expectedPid) {
565
+ throw new BridgeError("SOCKET_OUTSIDE_ROOT", "Peer socket filename does not match its process.");
566
+ }
567
+ const stat = await lstat(socketPath);
568
+ if (stat.isSymbolicLink() || !stat.isSocket()) {
569
+ throw new BridgeError("SOCKET_NOT_SOCKET", "Peer endpoint is not a socket.");
570
+ }
571
+ return { dev: stat.dev, ino: stat.ino };
572
+ }
573
+ async #bindingFromRegistry(registryPath, expectedPid, _existingTargetId, onParsed) {
574
+ const { value, generation: registryGeneration } = await this.#readRegistryFile(registryPath);
575
+ if (isObject(value) && value.pid !== expectedPid) {
576
+ throw new BridgeError("PID_MISMATCH", "Registry filename and process identifier do not match.");
577
+ }
578
+ const record = parseRegistryRecord(value, expectedPid);
579
+ if (record === undefined) {
580
+ throw new BridgeError("REGISTRY_INVALID_SCHEMA", "Registry schema is incompatible.");
581
+ }
582
+ onParsed?.();
583
+ const processIdentity = await this.#inspectProcess(expectedPid);
584
+ if (processIdentity === undefined) {
585
+ throw new BridgeError("PID_NOT_LIVE", "Registry process is not live.");
586
+ }
587
+ if (processIdentity.uid !== this.#expectedUid) {
588
+ throw new BridgeError("PID_OWNER_MISMATCH", "Registry process owner is unsafe.");
589
+ }
590
+ if (expectedPid === process.pid) {
591
+ throw new BridgeError("SELF_TARGET", "The gateway cannot target itself.");
592
+ }
593
+ const socketGeneration = await this.#validateSocket(record.messagingSocketPath, expectedPid);
594
+ return {
595
+ // Claude's native session UUID is the logical route identity. The
596
+ // process, registry file, and socket below are replaceable transport
597
+ // coordinates for that session and are revalidated before every write.
598
+ targetId: record.sessionId,
599
+ alias: record.name,
600
+ registryPath,
601
+ registryGeneration,
602
+ socketGeneration,
603
+ record,
604
+ processGeneration: processIdentity.generation,
605
+ expiresAt: this.#now() + this.#limits.targetLeaseMs,
606
+ };
607
+ }
608
+ async discover() {
609
+ await Promise.all([
610
+ this.#validatePrivateDirectory(this.#sessionsDir),
611
+ this.#validatePrivateDirectory(this.#socketDir),
612
+ ]);
613
+ const previousTargets = [...this.#targets.values()];
614
+ const nextTargets = new Map();
615
+ const collidedSessionIds = new Set();
616
+ const rejected = {};
617
+ const reject = (code) => {
618
+ rejected[code] = (rejected[code] ?? 0) + 1;
619
+ };
620
+ const entries = [];
621
+ let truncated = false;
622
+ const directory = await opendir(this.#sessionsDir);
623
+ for await (const entry of directory) {
624
+ if (entries.length >= this.#limits.maxRegistryEntries) {
625
+ truncated = true;
626
+ break;
627
+ }
628
+ entries.push(entry);
629
+ }
630
+ if (truncated)
631
+ reject("ENTRY_LIMIT_EXCEEDED");
632
+ const bounded = entries
633
+ .sort((left, right) => left.name.localeCompare(right.name))
634
+ .slice(0, this.#limits.maxRegistryEntries);
635
+ const peers = [];
636
+ let parseableRecords = 0;
637
+ for (const entry of bounded) {
638
+ const match = REGISTRY_FILE_PATTERN.exec(entry.name);
639
+ if (match === null) {
640
+ reject("INVALID_FILE_NAME");
641
+ continue;
642
+ }
643
+ const pid = parsePositiveInteger(match[1] ?? "");
644
+ if (pid === undefined) {
645
+ reject("INVALID_FILE_NAME");
646
+ continue;
647
+ }
648
+ const registryPath = path.join(this.#sessionsDir, entry.name);
649
+ try {
650
+ let binding = await this.#bindingFromRegistry(registryPath, pid, undefined, () => {
651
+ parseableRecords += 1;
652
+ });
653
+ const previous = previousTargets.find((candidate) => sameTargetGeneration(candidate, binding));
654
+ if (previous !== undefined) {
655
+ binding = {
656
+ ...binding,
657
+ targetId: previous.targetId,
658
+ expiresAt: this.#now() + this.#limits.targetLeaseMs,
659
+ };
660
+ }
661
+ if (collidedSessionIds.has(binding.targetId) ||
662
+ nextTargets.has(binding.targetId)) {
663
+ nextTargets.delete(binding.targetId);
664
+ collidedSessionIds.add(binding.targetId);
665
+ const prior = peers.findIndex((candidate) => candidate.targetId === binding.targetId);
666
+ if (prior >= 0)
667
+ peers.splice(prior, 1);
668
+ reject("SESSION_ID_COLLISION");
669
+ continue;
670
+ }
671
+ nextTargets.set(binding.targetId, binding);
672
+ peers.push({
673
+ targetId: binding.targetId,
674
+ alias: binding.alias,
675
+ kind: binding.record.kind,
676
+ status: binding.record.status,
677
+ compatibility: "compatible",
678
+ });
679
+ }
680
+ catch (error) {
681
+ const code = error instanceof BridgeError
682
+ ? error.code
683
+ : "REGISTRY_RACED";
684
+ if (claudePeerRejectionCodes.includes(code)) {
685
+ reject(code);
686
+ }
687
+ else {
688
+ reject("REGISTRY_RACED");
689
+ }
690
+ }
691
+ }
692
+ this.#targets.clear();
693
+ for (const [targetId, binding] of nextTargets) {
694
+ this.#targets.set(targetId, binding);
695
+ }
696
+ for (const targetId of this.#workspacePolicies.keys()) {
697
+ if (!nextTargets.has(targetId))
698
+ this.#workspacePolicies.delete(targetId);
699
+ }
700
+ return {
701
+ peers,
702
+ rejected,
703
+ truncated,
704
+ entriesScanned: bounded.length,
705
+ parseableRecords,
706
+ };
707
+ }
708
+ async #revalidateBinding(binding) {
709
+ await Promise.all([
710
+ this.#validatePrivateDirectory(this.#sessionsDir),
711
+ this.#validatePrivateDirectory(this.#socketDir),
712
+ ]);
713
+ if (binding.expiresAt < this.#now()) {
714
+ this.#targets.delete(binding.targetId);
715
+ this.#workspacePolicies.delete(binding.targetId);
716
+ throw new BridgeError("CLAUDE_PEER_TARGET_STALE", "The Claude peer discovery lease expired; discover again.", true);
717
+ }
718
+ const current = await this.#bindingFromRegistry(binding.registryPath, binding.record.pid, binding.targetId);
719
+ if (!sameTargetGeneration(binding, current)) {
720
+ this.#targets.delete(binding.targetId);
721
+ this.#workspacePolicies.delete(binding.targetId);
722
+ throw new BridgeError("CLAUDE_PEER_TARGET_CHANGED", "The Claude peer generation changed; discover again.", true);
723
+ }
724
+ current.expiresAt = binding.expiresAt;
725
+ this.#targets.set(current.targetId, current);
726
+ return current;
727
+ }
728
+ async #canonicalStateRoot(stateRoot) {
729
+ try {
730
+ const configured = assertAbsoluteConfiguredPath(stateRoot, "controllerStateRoot");
731
+ const before = await lstat(configured);
732
+ if (before.isSymbolicLink() ||
733
+ !before.isDirectory() ||
734
+ before.uid !== this.#expectedUid ||
735
+ exactMode(before.mode) !== 0o700) {
736
+ throw new BridgeError("CLAUDE_PEER_STATE_ROOT_UNSAFE", "The controller state root failed its canonical ownership policy.");
737
+ }
738
+ const canonical = await realpath(configured);
739
+ const after = await lstat(configured);
740
+ if (canonical !== configured ||
741
+ after.isSymbolicLink() ||
742
+ !after.isDirectory() ||
743
+ after.uid !== this.#expectedUid ||
744
+ exactMode(after.mode) !== 0o700 ||
745
+ before.dev !== after.dev ||
746
+ before.ino !== after.ino) {
747
+ throw new BridgeError("CLAUDE_PEER_STATE_ROOT_UNSAFE", "The controller state root changed or is not canonical.");
748
+ }
749
+ return {
750
+ path: canonical,
751
+ generation: { dev: after.dev, ino: after.ino },
752
+ };
753
+ }
754
+ catch (error) {
755
+ if (error instanceof BridgeError)
756
+ throw error;
757
+ throw new BridgeError("CLAUDE_PEER_STATE_ROOT_UNSAFE", "The controller state root could not be safely attested.");
758
+ }
759
+ }
760
+ async #assertBindingWorkspaceDisjoint(binding, _canonicalStateRoot) {
761
+ try {
762
+ const cwd = binding.record.cwd;
763
+ const before = await lstat(cwd);
764
+ if (before.isSymbolicLink() || !before.isDirectory()) {
765
+ throw new BridgeError("CLAUDE_PEER_WORKSPACE_UNSAFE", "The selected Claude workspace failed its directory policy.");
766
+ }
767
+ const canonicalWorkspace = await realpath(cwd);
768
+ const homeBefore = await lstat(this.#userHome);
769
+ const canonicalHome = await realpath(this.#userHome);
770
+ const homeAfter = await lstat(this.#userHome);
771
+ if (canonicalWorkspace !== cwd ||
772
+ homeBefore.isSymbolicLink() ||
773
+ !homeBefore.isDirectory() ||
774
+ canonicalHome !== this.#userHome ||
775
+ homeBefore.uid !== this.#expectedUid ||
776
+ (exactMode(homeBefore.mode) & 0o022) !== 0 ||
777
+ homeAfter.isSymbolicLink() ||
778
+ !homeAfter.isDirectory() ||
779
+ homeAfter.uid !== this.#expectedUid ||
780
+ (exactMode(homeAfter.mode) & 0o022) !== 0 ||
781
+ homeBefore.dev !== homeAfter.dev ||
782
+ homeBefore.ino !== homeAfter.ino) {
783
+ throw new BridgeError("CLAUDE_PEER_WORKSPACE_UNSAFE", "The selected Claude workspace or local home is non-canonical.");
784
+ }
785
+ const filesystemRoot = path.parse(canonicalWorkspace).root;
786
+ if (canonicalWorkspace === filesystemRoot) {
787
+ throw new BridgeError("CLAUDE_PEER_WORKSPACE_BROAD", "The selected Claude workspace is broader than the safe local boundary.");
788
+ }
789
+ if (this.#tempRoots.some((tempRoot) => pathContains(tempRoot, canonicalWorkspace) ||
790
+ pathContains(canonicalWorkspace, tempRoot))) {
791
+ throw new BridgeError("CLAUDE_PEER_WORKSPACE_BROAD", "The selected Claude workspace is inside or contains a temporary root.");
792
+ }
793
+ const after = await lstat(cwd);
794
+ if (after.isSymbolicLink() ||
795
+ !after.isDirectory() ||
796
+ before.uid !== this.#expectedUid ||
797
+ after.uid !== this.#expectedUid ||
798
+ (exactMode(before.mode) & 0o022) !== 0 ||
799
+ (exactMode(after.mode) & 0o022) !== 0 ||
800
+ before.dev !== after.dev ||
801
+ before.ino !== after.ino) {
802
+ throw new BridgeError("CLAUDE_PEER_WORKSPACE_UNSAFE", "The selected Claude workspace changed or failed its owner and mode policy.");
803
+ }
804
+ return {
805
+ path: canonicalWorkspace,
806
+ generation: { dev: after.dev, ino: after.ino },
807
+ };
808
+ }
809
+ catch (error) {
810
+ if (error instanceof BridgeError)
811
+ throw error;
812
+ throw new BridgeError("CLAUDE_PEER_WORKSPACE_UNSAFE", "The selected Claude workspace could not be safely attested.");
813
+ }
814
+ }
815
+ /**
816
+ * Required selection gate. It reveals no workspace data and binds the
817
+ * opaque target to one canonical controller state root for later sends.
818
+ */
819
+ async assertTargetWorkspaceDisjoint(targetId, controllerStateRoot) {
820
+ const target = this.#targets.get(targetId);
821
+ if (target === undefined) {
822
+ throw new BridgeError("CLAUDE_PEER_TARGET_UNKNOWN", "The Claude peer target is unknown; discover it first.", true);
823
+ }
824
+ const binding = await this.#revalidateBinding(target);
825
+ const state = await this.#canonicalStateRoot(controllerStateRoot);
826
+ const workspace = await this.#assertBindingWorkspaceDisjoint(binding, state.path);
827
+ const existing = this.#workspacePolicies.get(targetId);
828
+ if (existing !== undefined) {
829
+ if (existing.stateRoot !== state.path ||
830
+ !sameDirectoryGeneration(existing.stateGeneration, state.generation)) {
831
+ this.#workspacePolicies.delete(targetId);
832
+ throw new BridgeError("CLAUDE_PEER_STATE_ROOT_CHANGED", "The selected Claude target is already bound to another controller state generation.");
833
+ }
834
+ if (existing.workspace !== workspace.path ||
835
+ !sameDirectoryGeneration(existing.workspaceGeneration, workspace.generation)) {
836
+ this.#workspacePolicies.delete(targetId);
837
+ throw new BridgeError("CLAUDE_PEER_WORKSPACE_CHANGED", "The selected Claude workspace generation changed; select it again.", true);
838
+ }
839
+ }
840
+ this.#workspacePolicies.set(targetId, {
841
+ stateRoot: state.path,
842
+ stateGeneration: state.generation,
843
+ workspace: workspace.path,
844
+ workspaceGeneration: workspace.generation,
845
+ });
846
+ }
847
+ async #resolveReplyAddress(address) {
848
+ await Promise.all([
849
+ this.#validatePrivateDirectory(this.#sessionsDir),
850
+ this.#validatePrivateDirectory(this.#socketDir),
851
+ ]);
852
+ if (!address.startsWith("uds:")) {
853
+ throw new BridgeError("UNREGISTERED_REPLY_ADDRESS", "Only local registered UDS reply addresses are accepted.");
854
+ }
855
+ const socketPath = address.slice(4);
856
+ if (path.dirname(socketPath) !== this.#socketDir) {
857
+ throw new BridgeError("UNREGISTERED_REPLY_ADDRESS", "Reply address is outside the trusted socket root.");
858
+ }
859
+ const socketMatch = SOCKET_FILE_PATTERN.exec(path.basename(socketPath));
860
+ const pid = socketMatch === null
861
+ ? undefined
862
+ : parsePositiveInteger(socketMatch[1] ?? "");
863
+ if (pid === undefined) {
864
+ throw new BridgeError("UNREGISTERED_REPLY_ADDRESS", "Reply address is not a registered Claude peer socket.");
865
+ }
866
+ const registryPath = path.join(this.#sessionsDir, `${pid}.json`);
867
+ let binding = await this.#bindingFromRegistry(registryPath, pid);
868
+ if (binding.record.messagingSocketPath !== socketPath) {
869
+ throw new BridgeError("UNREGISTERED_REPLY_ADDRESS", "Reply address does not match its live registry generation.");
870
+ }
871
+ const previous = [...this.#targets.values()].find((candidate) => sameTargetGeneration(candidate, binding));
872
+ if (previous !== undefined) {
873
+ binding = {
874
+ ...binding,
875
+ targetId: previous.targetId,
876
+ expiresAt: this.#now() + this.#limits.targetLeaseMs,
877
+ };
878
+ }
879
+ this.#targets.set(binding.targetId, binding);
880
+ return binding;
881
+ }
882
+ /**
883
+ * Converts a transient CLAUDE_CODE_MESSAGING_SOCKET-style address into the
884
+ * owning logical Claude session UUID. Callers discard the socket address;
885
+ * every later delivery resolves the UUID to fresh transport coordinates.
886
+ */
887
+ async resolveReplyAddress(address) {
888
+ await Promise.all([
889
+ this.#validatePrivateDirectory(this.#sessionsDir),
890
+ this.#validatePrivateDirectory(this.#socketDir),
891
+ ]);
892
+ const binding = await this.#resolveReplyAddress(address);
893
+ return {
894
+ targetId: binding.targetId,
895
+ alias: binding.alias,
896
+ kind: binding.record.kind,
897
+ status: binding.record.status,
898
+ compatibility: "compatible",
899
+ };
900
+ }
901
+ async #listen(options, preparedGeneration) {
902
+ await Promise.all([
903
+ this.#validatePrivateDirectory(this.#sessionsDir),
904
+ this.#validatePrivateDirectory(this.#socketDir),
905
+ ]);
906
+ const generation = preparedGeneration ?? this.#createGeneration();
907
+ if (!isCodexRegistrationGeneration(generation)) {
908
+ throw new BridgeError("INVALID_CODEX_PEER_GENERATION", "A native Codex listener requires one bounded opaque generation.");
909
+ }
910
+ if ([...this.#listeners].some((listener) => !listener.closed && listener.generation === generation)) {
911
+ throw new BridgeError("CODEX_PEER_GENERATION_EXISTS", "A live native Codex listener already owns this exact generation.");
912
+ }
913
+ const listener = await ClaudePeerListener.create({
914
+ sessionsDir: this.#sessionsDir,
915
+ socketDir: this.#socketDir,
916
+ expectedUid: this.#expectedUid,
917
+ limits: this.#limits,
918
+ createId: this.#createId,
919
+ connect: this.#connect,
920
+ now: this.#now,
921
+ locale: this.#locale,
922
+ deliveryNotices: this.#deliveryNotices,
923
+ attestedClaudeCodeVersion: this.#attestedClaudeCodeVersion,
924
+ resolveReplyAddress: async (address) => await this.#resolveReplyAddress(address),
925
+ resolveSessionBinding: async (sessionId) => {
926
+ // Native receipt handles retain only the stable Claude session UUID.
927
+ // Registry names, PIDs, and sockets are replaceable coordinates, so
928
+ // refresh discovery before every receipt or progress write.
929
+ const discovery = await this.discover();
930
+ if (discovery.truncated) {
931
+ throw new BridgeError("CLAUDE_PEER_DISCOVERY_INCOMPLETE", "The Claude peer receipt target cannot be proven from an incomplete registry scan.", true);
932
+ }
933
+ const target = this.#targets.get(sessionId);
934
+ if (target === undefined) {
935
+ throw new BridgeError("CLAUDE_PEER_TARGET_UNKNOWN", "The Claude peer receipt target is no longer discoverable.", true);
936
+ }
937
+ return await this.#revalidateBinding(target);
938
+ },
939
+ revalidateBinding: async (binding) => await this.#revalidateBinding(binding),
940
+ options,
941
+ owner: this.#listenerOwner,
942
+ generation,
943
+ registryRename: this.#registryRename,
944
+ ...(this.#registryOperationHook === undefined
945
+ ? {}
946
+ : { registryOperationHook: this.#registryOperationHook }),
947
+ runRegistryMutation: async (operation) => await this.#registryMutex.run("codex-registry", operation),
948
+ ...(preparedGeneration === undefined ? {} : { preparedGeneration }),
949
+ ...(this.#registryPublicationHook === undefined
950
+ ? {}
951
+ : { registryPublicationHook: this.#registryPublicationHook }),
952
+ onClosed: () => this.#listeners.delete(listener),
953
+ });
954
+ this.#listeners.add(listener);
955
+ return listener;
956
+ }
957
+ async listen(options) {
958
+ return await this.#listen(options);
959
+ }
960
+ async listenPrepared(generation, options) {
961
+ if (!isCodexRegistrationGeneration(generation)) {
962
+ throw new BridgeError("INVALID_CODEX_PEER_GENERATION", "A prepared native Codex listener requires one bounded opaque generation.");
963
+ }
964
+ return await this.#listen(options, generation);
965
+ }
966
+ async send(targetId, content, options = {}) {
967
+ // Session UUIDs survive registry rewrites, renames, and endpoint rotation.
968
+ // Refresh the replaceable transport coordinates before any write begins.
969
+ await this.discover();
970
+ const target = this.#targets.get(targetId);
971
+ if (target === undefined) {
972
+ throw new BridgeError("CLAUDE_PEER_TARGET_UNKNOWN", "The Claude peer target is unknown; discover it first.", true);
973
+ }
974
+ const binding = await this.#revalidateBinding(target);
975
+ const workspacePolicy = this.#workspacePolicies.get(targetId);
976
+ if (workspacePolicy === undefined) {
977
+ throw new BridgeError("CLAUDE_PEER_WORKSPACE_UNATTESTED", "The Claude peer workspace must pass selection validation before send.");
978
+ }
979
+ try {
980
+ const state = await this.#canonicalStateRoot(workspacePolicy.stateRoot);
981
+ const workspace = await this.#assertBindingWorkspaceDisjoint(binding, state.path);
982
+ if (!sameDirectoryGeneration(workspacePolicy.stateGeneration, state.generation)) {
983
+ throw new BridgeError("CLAUDE_PEER_STATE_ROOT_CHANGED", "The controller state root generation changed; select the target again.", true);
984
+ }
985
+ if (workspacePolicy.workspace !== workspace.path ||
986
+ !sameDirectoryGeneration(workspacePolicy.workspaceGeneration, workspace.generation)) {
987
+ throw new BridgeError("CLAUDE_PEER_WORKSPACE_CHANGED", "The selected Claude workspace generation changed; select it again.", true);
988
+ }
989
+ }
990
+ catch (error) {
991
+ this.#workspacePolicies.delete(targetId);
992
+ throw error;
993
+ }
994
+ const messageId = this.#createId();
995
+ if (!UUID_PATTERN.test(messageId)) {
996
+ throw new BridgeError("INVALID_PEER_MESSAGE_ID", "The configured ID source did not produce a UUID.");
997
+ }
998
+ const listener = options.listener;
999
+ if (listener !== undefined && !this.#listeners.has(listener)) {
1000
+ throw new BridgeError("CLAUDE_PEER_LISTENER_FOREIGN", "The reply listener is not owned by this adapter.");
1001
+ }
1002
+ const frame = encodeClaudePeerUserFrame({
1003
+ messageId,
1004
+ content,
1005
+ ...(listener === undefined ? {} : { from: listener.address }),
1006
+ maxFrameBytes: this.#limits.maxFrameBytes,
1007
+ });
1008
+ if (options.receiptDeadlineAt !== undefined &&
1009
+ (!Number.isSafeInteger(options.receiptDeadlineAt) ||
1010
+ options.receiptDeadlineAt < 0)) {
1011
+ throw new BridgeError("INVALID_PEER_RECEIPT_DEADLINE", "The Claude peer receipt deadline must be an epoch-millisecond timestamp.");
1012
+ }
1013
+ if (options.receiptDeadlineAt !== undefined &&
1014
+ options.receiptDeadlineAt <= this.#now()) {
1015
+ throw new BridgeError("CLAUDE_PEER_MESSAGE_EXPIRED", "The Claude peer message deadline elapsed before any socket write.", true);
1016
+ }
1017
+ if (listener !== undefined) {
1018
+ listener.track(messageId, binding, options.receiptDeadlineAt);
1019
+ }
1020
+ await invokeObservationalHook(options.onTransportStatus, {
1021
+ messageId,
1022
+ status: "connecting",
1023
+ });
1024
+ if (options.receiptDeadlineAt !== undefined &&
1025
+ options.receiptDeadlineAt <= this.#now()) {
1026
+ listener?.untrack(messageId);
1027
+ await invokeObservationalHook(options.onTransportStatus, {
1028
+ messageId,
1029
+ status: "not_written",
1030
+ });
1031
+ throw new BridgeError("CLAUDE_PEER_MESSAGE_EXPIRED", "The Claude peer message deadline elapsed before any socket write.", true);
1032
+ }
1033
+ let written = false;
1034
+ let writeStarted = false;
1035
+ try {
1036
+ await new Promise((resolve, reject) => {
1037
+ const socket = this.#connect(binding.record.messagingSocketPath);
1038
+ const timeoutMs = Math.max(1, Math.min(this.#limits.connectTimeoutMs, options.receiptDeadlineAt === undefined
1039
+ ? this.#limits.connectTimeoutMs
1040
+ : options.receiptDeadlineAt - this.#now()));
1041
+ const timer = setTimeout(() => {
1042
+ socket.destroy();
1043
+ const deadlineElapsed = options.receiptDeadlineAt !== undefined &&
1044
+ this.#now() >= options.receiptDeadlineAt;
1045
+ reject(new BridgeError(writeStarted
1046
+ ? "CLAUDE_PEER_WRITE_AMBIGUOUS"
1047
+ : deadlineElapsed
1048
+ ? "CLAUDE_PEER_MESSAGE_EXPIRED"
1049
+ : "CLAUDE_PEER_CONNECT_TIMEOUT", writeStarted
1050
+ ? "The Claude peer write began but did not finish in time; do not retry automatically."
1051
+ : deadlineElapsed
1052
+ ? "The Claude peer message deadline elapsed before any socket write."
1053
+ : "The Claude peer socket did not accept the write in time.", !writeStarted));
1054
+ }, timeoutMs);
1055
+ timer.unref();
1056
+ socket.once("error", (error) => {
1057
+ clearTimeout(timer);
1058
+ reject(error);
1059
+ });
1060
+ socket.once("connect", () => {
1061
+ if (options.receiptDeadlineAt !== undefined &&
1062
+ options.receiptDeadlineAt <= this.#now()) {
1063
+ clearTimeout(timer);
1064
+ socket.destroy();
1065
+ reject(new BridgeError("CLAUDE_PEER_MESSAGE_EXPIRED", "The Claude peer message deadline elapsed before any socket write.", true));
1066
+ return;
1067
+ }
1068
+ writeStarted = true;
1069
+ listener?.recordTransportOutcome(messageId, "transport_uncertain");
1070
+ void invokeObservationalHook(options.onTransportStatus, {
1071
+ messageId,
1072
+ status: "write_started",
1073
+ });
1074
+ try {
1075
+ socket.end(frame, () => {
1076
+ written = true;
1077
+ clearTimeout(timer);
1078
+ socket.destroy();
1079
+ resolve();
1080
+ });
1081
+ }
1082
+ catch (error) {
1083
+ clearTimeout(timer);
1084
+ socket.destroy();
1085
+ reject(error);
1086
+ }
1087
+ });
1088
+ });
1089
+ }
1090
+ catch (error) {
1091
+ if (listener !== undefined) {
1092
+ if (writeStarted) {
1093
+ listener.recordTransportOutcome(messageId, "transport_uncertain");
1094
+ }
1095
+ else {
1096
+ listener.untrack(messageId);
1097
+ }
1098
+ }
1099
+ await invokeObservationalHook(options.onTransportStatus, {
1100
+ messageId,
1101
+ status: writeStarted ? "ambiguous" : "not_written",
1102
+ });
1103
+ if (error instanceof BridgeError)
1104
+ throw error;
1105
+ if (writeStarted) {
1106
+ throw new BridgeError("CLAUDE_PEER_WRITE_AMBIGUOUS", "The Claude peer write began but its outcome is ambiguous; do not retry automatically.");
1107
+ }
1108
+ throw new BridgeError("CLAUDE_PEER_WRITE_FAILED", "The Claude peer message was not confirmed written.", true);
1109
+ }
1110
+ if (!written) {
1111
+ listener?.recordTransportOutcome(messageId, "transport_uncertain");
1112
+ throw new BridgeError("CLAUDE_PEER_WRITE_AMBIGUOUS", "The Claude peer write outcome is ambiguous; do not retry automatically.");
1113
+ }
1114
+ listener?.recordTransportOutcome(messageId, "transport_written");
1115
+ await invokeObservationalHook(options.onTransportStatus, {
1116
+ messageId,
1117
+ status: "transport_written",
1118
+ });
1119
+ return {
1120
+ messageId,
1121
+ transportStatus: "transport_written",
1122
+ receiptStatus: listener === undefined ? "unavailable" : "pending",
1123
+ };
1124
+ }
1125
+ async close() {
1126
+ const results = await Promise.allSettled([...this.#listeners].map(async (listener) => listener.close()));
1127
+ this.#targets.clear();
1128
+ this.#workspacePolicies.clear();
1129
+ const failed = results.find((result) => result.status === "rejected");
1130
+ if (failed !== undefined)
1131
+ throw failed.reason;
1132
+ }
1133
+ }
1134
+ export class ClaudePeerListener {
1135
+ address;
1136
+ generation;
1137
+ #sessionsDir;
1138
+ #sessionsGeneration;
1139
+ #expectedUid;
1140
+ #socketPath;
1141
+ #socketGeneration;
1142
+ #server;
1143
+ #limits;
1144
+ #createId;
1145
+ #connect;
1146
+ #now;
1147
+ #locale;
1148
+ #deliveryNotices;
1149
+ #attestedClaudeCodeVersion;
1150
+ #resolveReplyAddress;
1151
+ #resolveSessionBinding;
1152
+ #revalidateBinding;
1153
+ #options;
1154
+ #owner;
1155
+ #registryRename;
1156
+ #registryOperationHook;
1157
+ #runRegistryMutation;
1158
+ #preparedGeneration;
1159
+ #registryPublicationHook;
1160
+ #onClosed;
1161
+ #connections = new Set();
1162
+ #pending = new Map();
1163
+ #inboundReceipts = new Map();
1164
+ // One bounded exception slot owns an overflow message only long enough to
1165
+ // return its terminal capacity rejection. It never forwards content to the
1166
+ // service, retries only proven pre-write failures, and is released after a
1167
+ // successful, ambiguous, non-retryable, or exhausted write.
1168
+ #capacitySettlement;
1169
+ #queuedFrames = 0;
1170
+ #inboundQuiesced = false;
1171
+ #inboundQuiesceWaiters = new Set();
1172
+ #advertisedRecord;
1173
+ #advertisedGeneration;
1174
+ #preparedRecord;
1175
+ #publicationAttempted = false;
1176
+ #publicationSourceGeneration;
1177
+ #publicationConfirmed = false;
1178
+ #activationGranted = false;
1179
+ #closing = false;
1180
+ #closed = false;
1181
+ #closeOperation;
1182
+ constructor(options, server, socketPath, generation, sessionsGeneration) {
1183
+ this.address = `uds:${socketPath}`;
1184
+ this.generation = options.generation;
1185
+ this.#sessionsDir = options.sessionsDir;
1186
+ this.#sessionsGeneration = sessionsGeneration;
1187
+ this.#expectedUid = options.expectedUid;
1188
+ this.#socketPath = socketPath;
1189
+ this.#socketGeneration = generation;
1190
+ this.#server = server;
1191
+ this.#limits = options.limits;
1192
+ this.#createId = options.createId;
1193
+ this.#connect = options.connect;
1194
+ this.#now = options.now;
1195
+ this.#locale = options.locale;
1196
+ this.#deliveryNotices = options.deliveryNotices;
1197
+ this.#attestedClaudeCodeVersion = options.attestedClaudeCodeVersion;
1198
+ this.#resolveReplyAddress = options.resolveReplyAddress;
1199
+ this.#resolveSessionBinding = options.resolveSessionBinding;
1200
+ this.#revalidateBinding = options.revalidateBinding;
1201
+ this.#options = options.options;
1202
+ this.#owner = options.owner;
1203
+ this.#registryRename = options.registryRename;
1204
+ this.#registryOperationHook = options.registryOperationHook;
1205
+ this.#runRegistryMutation = options.runRegistryMutation;
1206
+ this.#preparedGeneration = options.preparedGeneration;
1207
+ this.#registryPublicationHook = options.registryPublicationHook;
1208
+ this.#onClosed = options.onClosed;
1209
+ this.#inboundQuiesced = options.preparedGeneration !== undefined;
1210
+ this.#activationGranted = options.preparedGeneration === undefined;
1211
+ }
1212
+ static async create(options) {
1213
+ // One gateway process owns one native peer socket. It may advertise one
1214
+ // registered Codex task through Claude's native session registry.
1215
+ const socketPath = path.join(options.socketDir, options.preparedGeneration === undefined
1216
+ ? `${process.pid}.sock`
1217
+ : `${process.pid}.${options.preparedGeneration}.sock`);
1218
+ try {
1219
+ await lstat(socketPath);
1220
+ throw new BridgeError("CLAUDE_PEER_CALLBACK_EXISTS", "The gateway callback path already exists; it will not be unlinked.");
1221
+ }
1222
+ catch (error) {
1223
+ if (error instanceof BridgeError ||
1224
+ error.code !== "ENOENT") {
1225
+ throw error;
1226
+ }
1227
+ }
1228
+ let listener;
1229
+ const server = net.createServer((socket) => {
1230
+ if (listener === undefined) {
1231
+ socket.destroy();
1232
+ return;
1233
+ }
1234
+ listener.#accept(socket);
1235
+ });
1236
+ server.on("error", () => {
1237
+ if (listener !== undefined)
1238
+ void listener.#notice("CALLBACK_ERROR");
1239
+ });
1240
+ await new Promise((resolve, reject) => {
1241
+ const onError = (error) => reject(error);
1242
+ server.once("error", onError);
1243
+ server.listen(socketPath, () => {
1244
+ server.off("error", onError);
1245
+ resolve();
1246
+ });
1247
+ });
1248
+ let createdGeneration;
1249
+ try {
1250
+ const created = await lstat(socketPath);
1251
+ if (!created.isSocket() ||
1252
+ created.isSymbolicLink()) {
1253
+ throw new BridgeError("CLAUDE_PEER_CALLBACK_UNSAFE", "The newly bound callback path did not satisfy its type and ownership policy.");
1254
+ }
1255
+ createdGeneration = { dev: created.dev, ino: created.ino };
1256
+ if (created.uid !== options.expectedUid) {
1257
+ throw new BridgeError("CLAUDE_PEER_CALLBACK_UNSAFE", "The newly bound callback path did not satisfy its type and ownership policy.");
1258
+ }
1259
+ await chmod(socketPath, 0o600);
1260
+ const attested = await lstat(socketPath);
1261
+ if (!attested.isSocket() ||
1262
+ attested.isSymbolicLink() ||
1263
+ attested.uid !== options.expectedUid ||
1264
+ exactMode(attested.mode) !== 0o600 ||
1265
+ !sameSocketGeneration({ dev: attested.dev, ino: attested.ino }, createdGeneration)) {
1266
+ throw new BridgeError("CLAUDE_PEER_CALLBACK_UNSAFE", "The gateway callback socket did not satisfy its ownership policy.");
1267
+ }
1268
+ const sessions = await lstat(options.sessionsDir);
1269
+ if (sessions.isSymbolicLink() ||
1270
+ !sessions.isDirectory() ||
1271
+ sessions.uid !== options.expectedUid ||
1272
+ exactMode(sessions.mode) !== 0o700) {
1273
+ throw new BridgeError("UNSAFE_PEER_DIRECTORY", "The native peer registry directory failed its ownership policy.");
1274
+ }
1275
+ listener = new ClaudePeerListener(options, server, socketPath, createdGeneration, { dev: sessions.dev, ino: sessions.ino });
1276
+ return listener;
1277
+ }
1278
+ catch (error) {
1279
+ let stillOwned = false;
1280
+ if (createdGeneration !== undefined) {
1281
+ try {
1282
+ const current = await lstat(socketPath);
1283
+ stillOwned =
1284
+ current.isSocket() &&
1285
+ sameSocketGeneration({ dev: current.dev, ino: current.ino }, createdGeneration);
1286
+ }
1287
+ catch {
1288
+ // A missing or unreadable path cannot be safely unlinked by
1289
+ // net.Server.close(), which deletes by pathname.
1290
+ }
1291
+ }
1292
+ if (stillOwned) {
1293
+ await new Promise((resolve) => server.close(() => resolve()));
1294
+ }
1295
+ else {
1296
+ server.unref();
1297
+ }
1298
+ throw error;
1299
+ }
1300
+ }
1301
+ get closed() {
1302
+ return this.#closing || this.#closed;
1303
+ }
1304
+ #isClosingOrClosed() {
1305
+ return this.#closing || this.#closed;
1306
+ }
1307
+ #registryPath() {
1308
+ return path.join(this.#sessionsDir, `${process.pid}.json`);
1309
+ }
1310
+ #registryTemporaryPath() {
1311
+ return path.join(this.#sessionsDir, `.${process.pid}.${this.generation}.registry.tmp`);
1312
+ }
1313
+ #serializeRecord(record) {
1314
+ return `${JSON.stringify(record)}\n`;
1315
+ }
1316
+ #recordBelongsToThisListener(record) {
1317
+ return (record.pid === process.pid &&
1318
+ UUID_PATTERN.test(record.sessionId) &&
1319
+ record.messagingSocketPath === this.#socketPath &&
1320
+ record.version === this.#attestedClaudeCodeVersion &&
1321
+ record.peerProtocol === CLAUDE_PEER_COMPATIBILITY.peerProtocol &&
1322
+ record.kind === "interactive" &&
1323
+ record.entrypoint === "cli" &&
1324
+ record.name.startsWith("codex-") &&
1325
+ ALIAS_PATTERN.test(record.name));
1326
+ }
1327
+ async #assertRegistryDirectory() {
1328
+ const current = await lstat(this.#sessionsDir);
1329
+ if (current.isSymbolicLink() ||
1330
+ !current.isDirectory() ||
1331
+ current.uid !== this.#expectedUid ||
1332
+ exactMode(current.mode) !== 0o700 ||
1333
+ !sameDirectoryGeneration({ dev: current.dev, ino: current.ino }, this.#sessionsGeneration)) {
1334
+ throw new BridgeError("UNSAFE_PEER_DIRECTORY", "The native peer registry directory changed or failed its ownership policy.");
1335
+ }
1336
+ }
1337
+ async #readRegistryExact() {
1338
+ await this.#assertRegistryDirectory();
1339
+ const registryPath = this.#registryPath();
1340
+ let before;
1341
+ try {
1342
+ before = await lstat(registryPath);
1343
+ }
1344
+ catch (error) {
1345
+ if (error.code === "ENOENT")
1346
+ return undefined;
1347
+ throw error;
1348
+ }
1349
+ if (before.isSymbolicLink() ||
1350
+ !before.isFile() ||
1351
+ before.uid !== this.#expectedUid ||
1352
+ exactMode(before.mode) !== 0o600 ||
1353
+ before.size > this.#limits.maxRegistryBytes) {
1354
+ throw new BridgeError("REGISTRY_RACED", "The native Codex registry record failed its exact ownership policy.");
1355
+ }
1356
+ const beforeGeneration = generationOf(before);
1357
+ const handle = await open(registryPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
1358
+ try {
1359
+ const opened = await handle.stat();
1360
+ const openedGeneration = generationOf(opened);
1361
+ if (!opened.isFile() ||
1362
+ opened.uid !== this.#expectedUid ||
1363
+ exactMode(opened.mode) !== 0o600 ||
1364
+ !sameFileGeneration(beforeGeneration, openedGeneration)) {
1365
+ throw new BridgeError("REGISTRY_RACED", "The native Codex registry record changed while opening.");
1366
+ }
1367
+ const serialized = await handle.readFile({ encoding: "utf8" });
1368
+ if (byteLength(serialized) > this.#limits.maxRegistryBytes) {
1369
+ throw new BridgeError("REGISTRY_TOO_LARGE", "The native Codex registry record exceeded its bound.");
1370
+ }
1371
+ const after = await lstat(registryPath);
1372
+ if (after.isSymbolicLink() ||
1373
+ !after.isFile() ||
1374
+ after.uid !== this.#expectedUid ||
1375
+ exactMode(after.mode) !== 0o600 ||
1376
+ !sameFileGeneration(openedGeneration, generationOf(after))) {
1377
+ throw new BridgeError("REGISTRY_RACED", "The native Codex registry record changed while reading.");
1378
+ }
1379
+ return { serialized, generation: openedGeneration };
1380
+ }
1381
+ finally {
1382
+ await handle.close();
1383
+ }
1384
+ }
1385
+ async #recordMatches(record, generation) {
1386
+ try {
1387
+ const current = await this.#readRegistryExact();
1388
+ if (current === undefined ||
1389
+ current.serialized !== this.#serializeRecord(record) ||
1390
+ (generation !== undefined &&
1391
+ !sameFileGeneration(current.generation, generation))) {
1392
+ return { matches: false };
1393
+ }
1394
+ return { matches: true, generation: current.generation };
1395
+ }
1396
+ catch {
1397
+ return { matches: false };
1398
+ }
1399
+ }
1400
+ async #createRegistryTemporary(record) {
1401
+ await this.#assertRegistryDirectory();
1402
+ const temporaryPath = this.#registryTemporaryPath();
1403
+ await writeFile(temporaryPath, this.#serializeRecord(record), {
1404
+ encoding: "utf8",
1405
+ flag: "wx",
1406
+ mode: 0o600,
1407
+ });
1408
+ let generation;
1409
+ try {
1410
+ const created = await lstat(temporaryPath);
1411
+ if (created.isSymbolicLink() ||
1412
+ !created.isFile() ||
1413
+ created.uid !== this.#expectedUid ||
1414
+ exactMode(created.mode) !== 0o600) {
1415
+ throw new BridgeError("REGISTRY_RACED", "The native Codex temporary registry record is unsafe.");
1416
+ }
1417
+ generation = generationOf(created);
1418
+ const handle = await open(temporaryPath, fsConstants.O_RDWR | fsConstants.O_NOFOLLOW);
1419
+ try {
1420
+ const openedStat = await handle.stat();
1421
+ const opened = generationOf(openedStat);
1422
+ if (!openedStat.isFile() ||
1423
+ openedStat.uid !== this.#expectedUid ||
1424
+ exactMode(openedStat.mode) !== 0o600 ||
1425
+ !sameFileGeneration(generation, opened)) {
1426
+ throw new BridgeError("REGISTRY_RACED", "The native Codex temporary registry record changed while opening.");
1427
+ }
1428
+ await handle.sync();
1429
+ }
1430
+ finally {
1431
+ await handle.close();
1432
+ }
1433
+ return { path: temporaryPath, generation };
1434
+ }
1435
+ catch (error) {
1436
+ if (generation !== undefined) {
1437
+ await this.#cleanupOwnedFile(temporaryPath, generation).catch(() => undefined);
1438
+ }
1439
+ throw error;
1440
+ }
1441
+ }
1442
+ async #cleanupOwnedFile(candidate, generation) {
1443
+ try {
1444
+ const current = await lstat(candidate);
1445
+ if (current.isFile() &&
1446
+ !current.isSymbolicLink() &&
1447
+ current.uid === this.#expectedUid &&
1448
+ exactMode(current.mode) === 0o600 &&
1449
+ sameFileGeneration(generationOf(current), generation)) {
1450
+ await unlink(candidate);
1451
+ }
1452
+ }
1453
+ catch (error) {
1454
+ if (error.code !== "ENOENT")
1455
+ throw error;
1456
+ }
1457
+ }
1458
+ async #syncRegistryDirectory() {
1459
+ await this.#assertRegistryDirectory();
1460
+ const handle = await open(this.#sessionsDir, fsConstants.O_RDONLY);
1461
+ try {
1462
+ const opened = await handle.stat();
1463
+ if (!opened.isDirectory() ||
1464
+ opened.uid !== this.#expectedUid ||
1465
+ exactMode(opened.mode) !== 0o700 ||
1466
+ !sameDirectoryGeneration({ dev: opened.dev, ino: opened.ino }, this.#sessionsGeneration)) {
1467
+ throw new BridgeError("UNSAFE_PEER_DIRECTORY", "The native peer registry directory changed while syncing.");
1468
+ }
1469
+ await handle.sync();
1470
+ }
1471
+ finally {
1472
+ await handle.close();
1473
+ }
1474
+ }
1475
+ #newAdvertisedRecord(name, cwd) {
1476
+ if (!name.startsWith("codex-") || !ALIAS_PATTERN.test(name)) {
1477
+ throw new BridgeError("INVALID_CODEX_PEER_NAME", "A native Codex peer name must start with codex-.");
1478
+ }
1479
+ if (!path.isAbsolute(cwd) || cwd.includes("\0")) {
1480
+ throw new BridgeError("INVALID_CODEX_PEER_CWD", "A native Codex peer requires an absolute working directory.");
1481
+ }
1482
+ const sessionId = this.#createId();
1483
+ if (!UUID_PATTERN.test(sessionId)) {
1484
+ throw new BridgeError("INVALID_CODEX_PEER_SESSION_ID", "The configured ID source did not produce a native peer session UUID.");
1485
+ }
1486
+ const now = Date.now();
1487
+ return {
1488
+ pid: process.pid,
1489
+ sessionId,
1490
+ cwd,
1491
+ startedAt: now,
1492
+ procStart: new Date(now - process.uptime() * 1_000).toString(),
1493
+ version: this.#attestedClaudeCodeVersion,
1494
+ peerProtocol: CLAUDE_PEER_COMPATIBILITY.peerProtocol,
1495
+ kind: "interactive",
1496
+ entrypoint: "cli",
1497
+ messagingSocketPath: this.#socketPath,
1498
+ name,
1499
+ status: "idle",
1500
+ updatedAt: now,
1501
+ statusUpdatedAt: now,
1502
+ };
1503
+ }
1504
+ async #mutateRegistry(operation, mutation) {
1505
+ return await this.#runRegistryMutation(async () => {
1506
+ await this.#registryOperationHook?.({
1507
+ operation,
1508
+ phase: "entered",
1509
+ generation: this.generation,
1510
+ });
1511
+ try {
1512
+ return await mutation();
1513
+ }
1514
+ finally {
1515
+ await this.#registryOperationHook?.({
1516
+ operation,
1517
+ phase: "exited",
1518
+ generation: this.generation,
1519
+ });
1520
+ }
1521
+ });
1522
+ }
1523
+ async advertise(name, cwd) {
1524
+ await this.#mutateRegistry("advertise", async () => {
1525
+ if (this.#isClosingOrClosed()) {
1526
+ throw new BridgeError("CLAUDE_PEER_LISTENER_CLOSED", "The Claude peer callback listener is closed.");
1527
+ }
1528
+ if (this.#preparedGeneration !== undefined) {
1529
+ throw new BridgeError("CODEX_PEER_PREPARED_NOT_ACTIVE", "A prepared native Codex listener must use atomic succession publication.");
1530
+ }
1531
+ if (this.#advertisedRecord !== undefined &&
1532
+ this.#advertisedRecord.name !== name) {
1533
+ throw new BridgeError("CODEX_PEER_ALREADY_ADVERTISED", "This gateway process already advertises another Codex peer.");
1534
+ }
1535
+ if (this.#advertisedRecord?.name === name)
1536
+ return;
1537
+ const record = this.#newAdvertisedRecord(name, cwd);
1538
+ await this.#assertRegistryDirectory();
1539
+ await writeFile(this.#registryPath(), this.#serializeRecord(record), {
1540
+ encoding: "utf8",
1541
+ flag: "wx",
1542
+ mode: 0o600,
1543
+ });
1544
+ const exact = await this.#recordMatches(record);
1545
+ if (!exact.matches) {
1546
+ throw new BridgeError("REGISTRY_RACED", "The native Codex registry record could not be exactly confirmed.");
1547
+ }
1548
+ this.#advertisedRecord = record;
1549
+ this.#advertisedGeneration = exact.generation;
1550
+ });
1551
+ }
1552
+ async #classifyPublication(current, record) {
1553
+ const sourceGeneration = this.#publicationSourceGeneration;
1554
+ if (sourceGeneration !== undefined) {
1555
+ const published = await this.#recordMatches(record, sourceGeneration);
1556
+ if (published.matches) {
1557
+ this.#advertisedRecord = record;
1558
+ this.#advertisedGeneration = published.generation;
1559
+ this.#publicationConfirmed = true;
1560
+ return "published";
1561
+ }
1562
+ }
1563
+ return (await this.#oldPublicationStillPresent(current))
1564
+ ? "not_published"
1565
+ : "unknown";
1566
+ }
1567
+ async #oldPublicationStillPresent(current) {
1568
+ const oldRecord = current.#advertisedRecord;
1569
+ const oldGeneration = current.#advertisedGeneration;
1570
+ if (oldRecord === undefined || oldGeneration === undefined)
1571
+ return false;
1572
+ return (await current.#recordMatches(oldRecord, oldGeneration)).matches;
1573
+ }
1574
+ async publishReplacing(current, name, cwd) {
1575
+ return await this.#mutateRegistry("publish", async () => await this.#publishReplacingLocked(current, name, cwd));
1576
+ }
1577
+ async #publishReplacingLocked(current, name, cwd) {
1578
+ if (this.#isClosingOrClosed() ||
1579
+ current.#isClosingOrClosed() ||
1580
+ this === current ||
1581
+ this.#preparedGeneration === undefined ||
1582
+ this.generation === current.generation ||
1583
+ !current.#activationGranted ||
1584
+ this.#owner !== current.#owner ||
1585
+ this.#sessionsDir !== current.#sessionsDir ||
1586
+ this.#expectedUid !== current.#expectedUid ||
1587
+ !sameDirectoryGeneration(this.#sessionsGeneration, current.#sessionsGeneration)) {
1588
+ throw new BridgeError("CODEX_PEER_SUCCESSION_INVALID", "Native Codex listener succession requires two live generations from one adapter owner.");
1589
+ }
1590
+ if (!this.#inboundQuiesced || !current.#inboundQuiesced) {
1591
+ throw new BridgeError("CODEX_PEER_SUCCESSION_NOT_QUIESCED", "Both native Codex listener generations must be inbound-quiesced before publication.");
1592
+ }
1593
+ if (current.#advertisedRecord === undefined ||
1594
+ current.#advertisedGeneration === undefined ||
1595
+ !current.#recordBelongsToThisListener(current.#advertisedRecord)) {
1596
+ throw new BridgeError("CODEX_PEER_SUCCESSION_CURRENT_UNOWNED", "The current native Codex listener does not own an exact advertised record.");
1597
+ }
1598
+ if (name === current.#advertisedRecord.name) {
1599
+ throw new BridgeError("CODEX_PEER_SUCCESSION_ALIAS_UNCHANGED", "Native Codex succession requires a distinct alias.");
1600
+ }
1601
+ if (this.#preparedRecord === undefined) {
1602
+ this.#preparedRecord = this.#newAdvertisedRecord(name, cwd);
1603
+ }
1604
+ else if (this.#preparedRecord.name !== name ||
1605
+ this.#preparedRecord.cwd !== cwd) {
1606
+ throw new BridgeError("CODEX_PEER_SUCCESSION_CHANGED", "A prepared native Codex publication cannot change identity after preparation.");
1607
+ }
1608
+ const record = this.#preparedRecord;
1609
+ if (this.#publicationAttempted) {
1610
+ return await this.#classifyPublication(current, record);
1611
+ }
1612
+ let temporary;
1613
+ let renameInvoked = false;
1614
+ try {
1615
+ temporary = await this.#createRegistryTemporary(record);
1616
+ this.#publicationSourceGeneration = temporary.generation;
1617
+ await this.#registryPublicationHook?.("before_rename");
1618
+ if (this.#isClosingOrClosed() ||
1619
+ current.#isClosingOrClosed() ||
1620
+ !this.#inboundQuiesced ||
1621
+ !current.#inboundQuiesced) {
1622
+ return (await this.#oldPublicationStillPresent(current))
1623
+ ? "not_published"
1624
+ : "unknown";
1625
+ }
1626
+ const currentExact = await current.#recordMatches(current.#advertisedRecord, current.#advertisedGeneration);
1627
+ if (!currentExact.matches)
1628
+ return "unknown";
1629
+ renameInvoked = true;
1630
+ this.#publicationAttempted = true;
1631
+ await this.#registryRename(temporary.path, this.#registryPath());
1632
+ temporary = undefined;
1633
+ await this.#registryPublicationHook?.("after_rename");
1634
+ await this.#syncRegistryDirectory();
1635
+ return await this.#classifyPublication(current, record);
1636
+ }
1637
+ catch {
1638
+ if (!renameInvoked) {
1639
+ return (await this.#oldPublicationStillPresent(current))
1640
+ ? "not_published"
1641
+ : "unknown";
1642
+ }
1643
+ return await this.#classifyPublication(current, record);
1644
+ }
1645
+ finally {
1646
+ if (temporary !== undefined) {
1647
+ await this.#cleanupOwnedFile(temporary.path, temporary.generation).catch(() => undefined);
1648
+ }
1649
+ }
1650
+ }
1651
+ async updateAdvertisedStatus(status) {
1652
+ await this.#mutateRegistry("status", async () => await this.#updateAdvertisedStatusLocked(status));
1653
+ }
1654
+ async #updateAdvertisedStatusLocked(status) {
1655
+ if (this.#isClosingOrClosed() ||
1656
+ this.#advertisedRecord === undefined ||
1657
+ this.#advertisedRecord.status === status) {
1658
+ return;
1659
+ }
1660
+ const currentExact = await this.#recordMatches(this.#advertisedRecord, this.#advertisedGeneration);
1661
+ if (!currentExact.matches) {
1662
+ this.#advertisedRecord = undefined;
1663
+ this.#advertisedGeneration = undefined;
1664
+ return;
1665
+ }
1666
+ const now = Date.now();
1667
+ const record = {
1668
+ ...this.#advertisedRecord,
1669
+ status,
1670
+ updatedAt: now,
1671
+ statusUpdatedAt: now,
1672
+ };
1673
+ const temporary = await this.#createRegistryTemporary(record);
1674
+ try {
1675
+ const stillCurrent = await this.#recordMatches(this.#advertisedRecord, this.#advertisedGeneration);
1676
+ if (!stillCurrent.matches)
1677
+ return;
1678
+ await this.#registryRename(temporary.path, this.#registryPath());
1679
+ await this.#syncRegistryDirectory();
1680
+ const exact = await this.#recordMatches(record, temporary.generation);
1681
+ if (!exact.matches) {
1682
+ throw new BridgeError("REGISTRY_RACED", "The updated native Codex registry record could not be exactly confirmed.");
1683
+ }
1684
+ this.#advertisedRecord = record;
1685
+ this.#advertisedGeneration = exact.generation;
1686
+ }
1687
+ finally {
1688
+ await this.#cleanupOwnedFile(temporary.path, temporary.generation).catch(() => undefined);
1689
+ }
1690
+ }
1691
+ async unadvertise(name) {
1692
+ await this.#mutateRegistry("unadvertise", async () => await this.#unadvertiseLocked(name));
1693
+ }
1694
+ async #unadvertiseLocked(name) {
1695
+ if (this.#advertisedRecord === undefined ||
1696
+ (name !== undefined && name !== this.#advertisedRecord.name)) {
1697
+ return;
1698
+ }
1699
+ const record = this.#advertisedRecord;
1700
+ const generation = this.#advertisedGeneration;
1701
+ this.#advertisedRecord = undefined;
1702
+ this.#advertisedGeneration = undefined;
1703
+ if (generation === undefined)
1704
+ return;
1705
+ const exact = await this.#recordMatches(record, generation);
1706
+ if (!exact.matches)
1707
+ return;
1708
+ await unlink(this.#registryPath()).catch((error) => {
1709
+ if (error.code !== "ENOENT")
1710
+ throw error;
1711
+ });
1712
+ }
1713
+ grantSuccessionActivation() {
1714
+ if (this.#isClosingOrClosed()) {
1715
+ throw new BridgeError("CLAUDE_PEER_LISTENER_CLOSED", "The Claude peer callback listener is closed.");
1716
+ }
1717
+ if (this.#preparedGeneration === undefined ||
1718
+ !this.#publicationAttempted ||
1719
+ !this.#publicationConfirmed ||
1720
+ this.#publicationSourceGeneration === undefined ||
1721
+ this.#advertisedRecord === undefined ||
1722
+ this.#advertisedGeneration === undefined ||
1723
+ !sameFileGeneration(this.#publicationSourceGeneration, this.#advertisedGeneration)) {
1724
+ throw new BridgeError("CODEX_PEER_PREPARED_NOT_ACTIVE", "A prepared native Codex listener requires exact confirmed publication before activation can be granted.");
1725
+ }
1726
+ this.#activationGranted = true;
1727
+ }
1728
+ resumeInbound() {
1729
+ if (this.#isClosingOrClosed()) {
1730
+ throw new BridgeError("CLAUDE_PEER_LISTENER_CLOSED", "The Claude peer callback listener is closed.");
1731
+ }
1732
+ if (!this.#activationGranted) {
1733
+ throw new BridgeError("CODEX_PEER_PREPARED_NOT_ACTIVE", "A prepared native Codex listener cannot resume before durable succession activation.");
1734
+ }
1735
+ this.#inboundQuiesced = false;
1736
+ }
1737
+ async acknowledge(receiptHandle, status, diagnostic) {
1738
+ if (this.#isClosingOrClosed()) {
1739
+ throw new BridgeError("CLAUDE_PEER_LISTENER_CLOSED", "The Claude peer callback listener is closed.");
1740
+ }
1741
+ const receipt = this.#inboundReceipts.get(receiptHandle);
1742
+ if (receipt === undefined) {
1743
+ throw new BridgeError("CLAUDE_PEER_RECEIPT_UNKNOWN", "The native Claude peer receipt is unknown or already settled.");
1744
+ }
1745
+ if (diagnostic !== undefined &&
1746
+ (status !== "expired" ||
1747
+ !/^[A-Z][A-Z0-9_]{0,63}$/.test(diagnostic.code))) {
1748
+ throw new BridgeError("INVALID_PEER_DELIVERY_DIAGNOSTIC", "A peer delivery diagnostic requires an expired receipt and a safe code.");
1749
+ }
1750
+ try {
1751
+ const result = await this.#writeInboundStatus(receipt, status, diagnostic);
1752
+ if (status !== "held")
1753
+ this.#inboundReceipts.delete(receiptHandle);
1754
+ return result;
1755
+ }
1756
+ catch (error) {
1757
+ // A terminal write that may have started must never be replayed. A
1758
+ // proven pre-write failure retains the handle for a bounded caller retry.
1759
+ if (status !== "held" &&
1760
+ error instanceof BridgeError &&
1761
+ !error.recoverable) {
1762
+ this.#inboundReceipts.delete(receiptHandle);
1763
+ }
1764
+ throw error;
1765
+ }
1766
+ }
1767
+ /**
1768
+ * Writes one nonterminal gateway progress frame without synthesizing a
1769
+ * native peer_message_status transition. The receipt remains available for
1770
+ * its later exact terminal acknowledgement or explicit release.
1771
+ */
1772
+ async notifyInboundProgress(receiptHandle, progress) {
1773
+ if (this.#isClosingOrClosed()) {
1774
+ throw new BridgeError("CLAUDE_PEER_LISTENER_CLOSED", "The Claude peer callback listener is closed.");
1775
+ }
1776
+ const receipt = this.#inboundReceipts.get(receiptHandle);
1777
+ if (receipt === undefined) {
1778
+ throw new BridgeError("CLAUDE_PEER_RECEIPT_UNKNOWN", "The native Claude peer receipt is unknown or already settled.");
1779
+ }
1780
+ if (!isObject(progress) ||
1781
+ !hasExactKeys(progress, ["kind", "reason", "queuedForMs"]) ||
1782
+ progress.kind !== "stall" ||
1783
+ !claudePeerInboundStallReasons.includes(progress.reason) ||
1784
+ typeof progress.queuedForMs !== "number") {
1785
+ throw new BridgeError("INVALID_PEER_PROGRESS", "Claude peer progress must be one bounded gateway stall notice.");
1786
+ }
1787
+ if (receipt.stallNotification !== "available") {
1788
+ throw new BridgeError("CLAUDE_PEER_PROGRESS_ALREADY_NOTIFIED", "The native Claude peer receipt already has a stall notification.");
1789
+ }
1790
+ if (this.#deliveryNotices === "quiet") {
1791
+ receipt.stallNotification = "settled";
1792
+ return { transportStatus: "suppressed" };
1793
+ }
1794
+ const queuedForMs = boundedStallQueuedForMs(progress.queuedForMs);
1795
+ const openingTag = `<gateway-delivery-stall terminal="false" reason="${progress.reason}" ` +
1796
+ `queued-for-ms="${queuedForMs}">`;
1797
+ const detailedContent = [
1798
+ openingTag,
1799
+ CLAUDE_PEER_NOTICE_COPY[this.#locale].stall,
1800
+ "</gateway-delivery-stall>",
1801
+ ].join("\n");
1802
+ const messageId = this.#createId();
1803
+ let progressFrame;
1804
+ try {
1805
+ progressFrame = encodeClaudePeerUserFrame({
1806
+ messageId,
1807
+ content: detailedContent,
1808
+ maxFrameBytes: this.#limits.maxFrameBytes,
1809
+ });
1810
+ }
1811
+ catch (error) {
1812
+ if (!(error instanceof BridgeError) || error.code !== "PEER_FRAME_TOO_LARGE") {
1813
+ throw error;
1814
+ }
1815
+ progressFrame = encodeClaudePeerUserFrame({
1816
+ messageId,
1817
+ content: `${openingTag.slice(0, -1)}/>`,
1818
+ maxFrameBytes: this.#limits.maxFrameBytes,
1819
+ });
1820
+ }
1821
+ receipt.stallNotification = "writing";
1822
+ try {
1823
+ const result = await this.#writeInboundPayload(receipt, progressFrame);
1824
+ receipt.stallNotification = "settled";
1825
+ return result;
1826
+ }
1827
+ catch (error) {
1828
+ // A proven pre-write failure can be retried; an ambiguous write counts
1829
+ // as the one allowed notice so the peer is never spammed by a replay.
1830
+ receipt.stallNotification =
1831
+ error instanceof BridgeError && error.recoverable
1832
+ ? "available"
1833
+ : "settled";
1834
+ throw error;
1835
+ }
1836
+ }
1837
+ /**
1838
+ * Releases a listener-owned native receipt capability without writing to the
1839
+ * peer. The service uses this after a terminal notification becomes
1840
+ * definitively undeliverable so bounded receipt capacity cannot leak.
1841
+ */
1842
+ releaseInboundReceipt(receiptHandle) {
1843
+ return this.#inboundReceipts.delete(receiptHandle);
1844
+ }
1845
+ /**
1846
+ * Stop admitting new user-message frames while keeping the listener alive
1847
+ * for terminal receipt writes. The promise joins every frame already
1848
+ * admitted, so controller shutdown can drain service receipt work before
1849
+ * closing the provider socket.
1850
+ */
1851
+ async quiesceInbound() {
1852
+ this.#inboundQuiesced = true;
1853
+ if (this.#queuedFrames === 0)
1854
+ return;
1855
+ await new Promise((resolve) => {
1856
+ this.#inboundQuiesceWaiters.add(resolve);
1857
+ if (this.#queuedFrames === 0) {
1858
+ this.#inboundQuiesceWaiters.delete(resolve);
1859
+ resolve();
1860
+ }
1861
+ });
1862
+ }
1863
+ async #writeInboundStatus(receipt, status, diagnostic) {
1864
+ const statusFrame = Buffer.from(`${JSON.stringify({
1865
+ type: "control",
1866
+ action: "peer_message_status",
1867
+ status,
1868
+ reason: diagnostic?.code ?? status,
1869
+ from: this.address,
1870
+ orig_msg_id: receipt.originalMessageId,
1871
+ msgV: 1,
1872
+ msg_id: this.#createId(),
1873
+ })}\n`, "utf8");
1874
+ const diagnosticFrame = diagnostic === undefined || this.#deliveryNotices !== "verbose"
1875
+ ? undefined
1876
+ : encodeClaudePeerUserFrame({
1877
+ messageId: this.#createId(),
1878
+ content: [
1879
+ `<gateway-delivery-diagnostic status="expired" code="${diagnostic.code}">`,
1880
+ CLAUDE_PEER_NOTICE_COPY[this.#locale].diagnostic,
1881
+ "</gateway-delivery-diagnostic>",
1882
+ ].join("\n"),
1883
+ maxFrameBytes: this.#limits.maxFrameBytes,
1884
+ });
1885
+ const payload = diagnosticFrame === undefined
1886
+ ? statusFrame
1887
+ : Buffer.concat([statusFrame, diagnosticFrame]);
1888
+ return await this.#writeInboundPayload(receipt, payload);
1889
+ }
1890
+ async #writeInboundPayload(receipt, payload) {
1891
+ let writeStarted = false;
1892
+ try {
1893
+ const binding = await this.#resolveSessionBinding(receipt.sourceSessionId);
1894
+ await new Promise((resolve, reject) => {
1895
+ const socket = this.#connect(binding.record.messagingSocketPath);
1896
+ let settled = false;
1897
+ const finish = (error) => {
1898
+ if (settled)
1899
+ return;
1900
+ settled = true;
1901
+ socket.destroy();
1902
+ if (error === undefined)
1903
+ resolve();
1904
+ else
1905
+ reject(error);
1906
+ };
1907
+ socket.setTimeout(this.#limits.connectTimeoutMs, () => finish(new Error("peer status timeout")));
1908
+ socket.once("error", finish);
1909
+ socket.once("connect", () => {
1910
+ writeStarted = true;
1911
+ try {
1912
+ socket.end(payload, () => finish());
1913
+ }
1914
+ catch (error) {
1915
+ finish(error);
1916
+ }
1917
+ });
1918
+ });
1919
+ }
1920
+ catch (error) {
1921
+ if (writeStarted) {
1922
+ throw new BridgeError("CLAUDE_PEER_RECEIPT_WRITE_AMBIGUOUS", "The Claude peer receipt write began but its outcome is ambiguous; do not retry automatically.");
1923
+ }
1924
+ if (error instanceof BridgeError && !error.recoverable)
1925
+ throw error;
1926
+ throw new BridgeError("CLAUDE_PEER_RECEIPT_NOT_WRITTEN", "The Claude peer receipt was not confirmed written.", true);
1927
+ }
1928
+ return { transportStatus: "transport_written" };
1929
+ }
1930
+ track(messageId, binding, receiptDeadlineAt) {
1931
+ if (this.#isClosingOrClosed()) {
1932
+ throw new BridgeError("CLAUDE_PEER_LISTENER_CLOSED", "The Claude peer callback listener is closed.");
1933
+ }
1934
+ if (this.#pending.size >= this.#limits.maxPendingReceipts) {
1935
+ throw new BridgeError("CLAUDE_PEER_RECEIPT_LIMIT", "The bounded Claude peer receipt table is full.", true);
1936
+ }
1937
+ if (this.#pending.has(messageId)) {
1938
+ throw new BridgeError("CLAUDE_PEER_MESSAGE_ID_COLLISION", "The peer message ID collided with an outstanding receipt.");
1939
+ }
1940
+ const now = this.#now();
1941
+ const deadlineAt = receiptDeadlineAt ?? now + this.#limits.receiptDeadlineMs;
1942
+ const timer = setTimeout(() => {
1943
+ const pending = this.#pending.get(messageId);
1944
+ if (pending === undefined)
1945
+ return;
1946
+ this.#pending.delete(messageId);
1947
+ void this.#emitReceipt({
1948
+ messageId,
1949
+ status: pendingReceiptDeadlineStatus(pending),
1950
+ trust: "untrusted_same_uid_peer",
1951
+ });
1952
+ }, Math.max(1, deadlineAt - now));
1953
+ timer.unref();
1954
+ this.#pending.set(messageId, {
1955
+ binding,
1956
+ state: "pending",
1957
+ writeEvidence: "none",
1958
+ deadlineAt,
1959
+ timer,
1960
+ });
1961
+ }
1962
+ recordTransportOutcome(messageId, outcome) {
1963
+ const pending = this.#pending.get(messageId);
1964
+ if (pending === undefined)
1965
+ return;
1966
+ if (pending.writeEvidence !== "transport_written" ||
1967
+ outcome === "transport_written") {
1968
+ pending.writeEvidence = outcome;
1969
+ }
1970
+ }
1971
+ untrack(messageId) {
1972
+ const pending = this.#pending.get(messageId);
1973
+ if (pending === undefined)
1974
+ return;
1975
+ clearTimeout(pending.timer);
1976
+ this.#pending.delete(messageId);
1977
+ }
1978
+ #accept(socket) {
1979
+ if (this.#isClosingOrClosed() ||
1980
+ this.#connections.size >= this.#limits.maxConnections) {
1981
+ socket.destroy();
1982
+ void this.#notice("CONNECTION_LIMIT");
1983
+ return;
1984
+ }
1985
+ this.#connections.add(socket);
1986
+ socket.setTimeout(this.#limits.connectionIdleMs);
1987
+ let buffered = Buffer.alloc(0);
1988
+ let frames = 0;
1989
+ let chain = Promise.resolve();
1990
+ let rejected = false;
1991
+ const reject = (code) => {
1992
+ if (rejected)
1993
+ return;
1994
+ rejected = true;
1995
+ socket.destroy();
1996
+ void this.#notice(code);
1997
+ };
1998
+ socket.on("timeout", () => reject("CONNECTION_TIMEOUT"));
1999
+ socket.on("error", () => undefined);
2000
+ socket.on("close", () => this.#connections.delete(socket));
2001
+ socket.on("data", (chunk) => {
2002
+ if (rejected)
2003
+ return;
2004
+ buffered = Buffer.concat([buffered, chunk]);
2005
+ if (buffered.length > this.#limits.maxFrameBytes + 1) {
2006
+ const newline = buffered.indexOf(0x0a);
2007
+ if (newline < 0 || newline > this.#limits.maxFrameBytes) {
2008
+ reject("FRAME_TOO_LARGE");
2009
+ return;
2010
+ }
2011
+ }
2012
+ while (!rejected) {
2013
+ const newline = buffered.indexOf(0x0a);
2014
+ if (newline < 0)
2015
+ break;
2016
+ if (newline > this.#limits.maxFrameBytes) {
2017
+ reject("FRAME_TOO_LARGE");
2018
+ break;
2019
+ }
2020
+ const line = buffered.subarray(0, newline);
2021
+ buffered = buffered.subarray(newline + 1);
2022
+ frames += 1;
2023
+ if (frames > this.#limits.maxFramesPerConnection) {
2024
+ reject("INVALID_FRAME");
2025
+ break;
2026
+ }
2027
+ let frame;
2028
+ try {
2029
+ frame = parseFrame(line, this.#limits.maxFrameBytes);
2030
+ }
2031
+ catch (error) {
2032
+ if (error instanceof BridgeError &&
2033
+ error.code === "INVALID_PEER_UTF8") {
2034
+ reject("INVALID_UTF8");
2035
+ }
2036
+ else if (error instanceof BridgeError &&
2037
+ error.code === "UNSUPPORTED_PEER_FRAME") {
2038
+ reject("UNSUPPORTED_FRAME");
2039
+ }
2040
+ else {
2041
+ reject("INVALID_FRAME");
2042
+ }
2043
+ break;
2044
+ }
2045
+ if (this.#queuedFrames >=
2046
+ this.#limits.maxConnections * this.#limits.maxFramesPerConnection) {
2047
+ reject("CONNECTION_LIMIT");
2048
+ break;
2049
+ }
2050
+ this.#queuedFrames += 1;
2051
+ chain = chain
2052
+ .then(async () => {
2053
+ if (rejected)
2054
+ return;
2055
+ await this.#handleFrame(frame, () => {
2056
+ if (rejected)
2057
+ return;
2058
+ rejected = true;
2059
+ socket.destroy();
2060
+ });
2061
+ })
2062
+ .catch(async () => this.#notice("CALLBACK_ERROR"))
2063
+ .finally(() => {
2064
+ this.#queuedFrames -= 1;
2065
+ if (this.#queuedFrames === 0) {
2066
+ for (const resolve of this.#inboundQuiesceWaiters)
2067
+ resolve();
2068
+ this.#inboundQuiesceWaiters.clear();
2069
+ }
2070
+ });
2071
+ }
2072
+ if (!rejected && buffered.length > this.#limits.maxFrameBytes) {
2073
+ reject("FRAME_TOO_LARGE");
2074
+ }
2075
+ });
2076
+ socket.on("end", () => {
2077
+ if (!rejected && buffered.length !== 0)
2078
+ reject("INVALID_FRAME");
2079
+ });
2080
+ }
2081
+ async #handleFrame(frame, rejectTransport) {
2082
+ if (this.#isClosingOrClosed())
2083
+ return;
2084
+ if (frame.type === "control") {
2085
+ await this.#handleControl(frame);
2086
+ return;
2087
+ }
2088
+ if (this.#inboundQuiesced) {
2089
+ rejectTransport();
2090
+ return;
2091
+ }
2092
+ let binding;
2093
+ if (frame.from !== undefined) {
2094
+ try {
2095
+ binding = await this.#resolveReplyAddress(frame.from);
2096
+ }
2097
+ catch {
2098
+ await this.#notice("UNREGISTERED_REPLY_ADDRESS");
2099
+ return;
2100
+ }
2101
+ }
2102
+ const inboundId = this.#createId();
2103
+ if (binding !== undefined && frame.messageId !== undefined) {
2104
+ if (this.#inboundReceipts.size >= this.#limits.maxPendingReceipts) {
2105
+ await this.#notice("RECEIPT_LIMIT");
2106
+ if (this.#capacitySettlement !== undefined) {
2107
+ // Both the configured receipt table and its single terminal-only
2108
+ // overflow slot are occupied. Close this transport rather than
2109
+ // making another native send appear accepted without settlement.
2110
+ rejectTransport();
2111
+ return;
2112
+ }
2113
+ const settlement = {
2114
+ receipt: {
2115
+ sourceSessionId: binding.targetId,
2116
+ originalMessageId: frame.messageId,
2117
+ stallNotification: "settled",
2118
+ },
2119
+ attempts: 0,
2120
+ };
2121
+ this.#capacitySettlement = settlement;
2122
+ await this.#attemptCapacitySettlement(settlement);
2123
+ return;
2124
+ }
2125
+ this.#inboundReceipts.set(inboundId, {
2126
+ sourceSessionId: binding.targetId,
2127
+ originalMessageId: frame.messageId,
2128
+ stallNotification: "available",
2129
+ });
2130
+ }
2131
+ await invokeHook(this.#options.onMessage, {
2132
+ inboundId,
2133
+ content: frame.content,
2134
+ ...(binding === undefined
2135
+ ? {}
2136
+ : {
2137
+ sourceTargetId: binding.targetId,
2138
+ sourceAlias: binding.alias,
2139
+ ...(frame.messageId === undefined
2140
+ ? {}
2141
+ : { receiptHandle: inboundId }),
2142
+ }),
2143
+ replySupported: binding !== undefined,
2144
+ trust: binding === undefined
2145
+ ? "untrusted_anonymous_local_peer"
2146
+ : "untrusted_same_uid_peer",
2147
+ });
2148
+ }
2149
+ async #attemptCapacitySettlement(settlement) {
2150
+ if (this.#isClosingOrClosed() || this.#capacitySettlement !== settlement) {
2151
+ return;
2152
+ }
2153
+ settlement.attempts += 1;
2154
+ try {
2155
+ await this.#writeInboundStatus(settlement.receipt, "expired", { code: "GATEWAY_RECEIPT_CAPACITY" });
2156
+ if (this.#capacitySettlement === settlement) {
2157
+ this.#capacitySettlement = undefined;
2158
+ }
2159
+ }
2160
+ catch (error) {
2161
+ if (this.#capacitySettlement !== settlement)
2162
+ return;
2163
+ const retryable = error instanceof BridgeError &&
2164
+ error.recoverable &&
2165
+ !this.#isClosingOrClosed();
2166
+ if (!retryable ||
2167
+ settlement.attempts >= CAPACITY_SETTLEMENT_MAX_ATTEMPTS) {
2168
+ // An ambiguous write must never be replayed. Exhausted clean
2169
+ // pre-write failures are released with a local protocol notice rather
2170
+ // than leaking the bounded overflow slot or an unhandled rejection.
2171
+ this.#capacitySettlement = undefined;
2172
+ await this.#notice("CALLBACK_ERROR");
2173
+ return;
2174
+ }
2175
+ settlement.retryTimer = setTimeout(() => {
2176
+ delete settlement.retryTimer;
2177
+ void this.#attemptCapacitySettlement(settlement);
2178
+ }, CAPACITY_SETTLEMENT_RETRY_MS);
2179
+ settlement.retryTimer.unref();
2180
+ }
2181
+ }
2182
+ async #handleControl(frame) {
2183
+ const pending = this.#pending.get(frame.originalMessageId);
2184
+ if (pending === undefined) {
2185
+ await this.#notice("UNKNOWN_RECEIPT");
2186
+ return;
2187
+ }
2188
+ let currentBinding;
2189
+ try {
2190
+ // Receipt authority is the stable Claude session UUID, not the
2191
+ // short-lived send-time registry/socket lease. Re-discover the exact
2192
+ // session on every receipt so legitimate held/terminal frames remain
2193
+ // valid for the full message deadline and across socket rotation.
2194
+ currentBinding = await this.#resolveSessionBinding(pending.binding.targetId);
2195
+ }
2196
+ catch {
2197
+ await this.#notice("UNKNOWN_RECEIPT");
2198
+ return;
2199
+ }
2200
+ if (frame.from !== `uds:${currentBinding.record.messagingSocketPath}`) {
2201
+ await this.#notice("UNKNOWN_RECEIPT");
2202
+ return;
2203
+ }
2204
+ pending.binding = currentBinding;
2205
+ if (this.#now() >= pending.deadlineAt) {
2206
+ clearTimeout(pending.timer);
2207
+ this.#pending.delete(frame.originalMessageId);
2208
+ await this.#emitReceipt({
2209
+ messageId: frame.originalMessageId,
2210
+ status: pendingReceiptDeadlineStatus(pending),
2211
+ trust: "untrusted_same_uid_peer",
2212
+ });
2213
+ return;
2214
+ }
2215
+ if (frame.status === "held") {
2216
+ if (pending.state !== "pending") {
2217
+ await this.#notice("INVALID_RECEIPT_TRANSITION");
2218
+ return;
2219
+ }
2220
+ pending.state = "held";
2221
+ pending.writeEvidence = "transport_written";
2222
+ await this.#emitReceipt({
2223
+ messageId: frame.originalMessageId,
2224
+ status: "held",
2225
+ trust: "untrusted_same_uid_peer",
2226
+ });
2227
+ return;
2228
+ }
2229
+ clearTimeout(pending.timer);
2230
+ this.#pending.delete(frame.originalMessageId);
2231
+ await this.#emitReceipt({
2232
+ messageId: frame.originalMessageId,
2233
+ status: frame.status === "delivered"
2234
+ ? "released"
2235
+ : frame.status,
2236
+ trust: "untrusted_same_uid_peer",
2237
+ });
2238
+ }
2239
+ async #emitReceipt(event) {
2240
+ try {
2241
+ await invokeHook(this.#options.onReceipt, event);
2242
+ }
2243
+ catch {
2244
+ await this.#notice("CALLBACK_ERROR");
2245
+ }
2246
+ }
2247
+ async #notice(code) {
2248
+ try {
2249
+ await invokeHook(this.#options.onProtocolNotice, { code });
2250
+ }
2251
+ catch {
2252
+ // Protocol callbacks are observational. Never feed callback failures
2253
+ // back into the socket or expose raw frame data.
2254
+ }
2255
+ }
2256
+ close() {
2257
+ if (this.#closeOperation !== undefined)
2258
+ return this.#closeOperation;
2259
+ if (this.#closed)
2260
+ return Promise.resolve();
2261
+ this.#closing = true;
2262
+ this.#inboundQuiesced = true;
2263
+ const operation = this.#performClose();
2264
+ this.#closeOperation = operation;
2265
+ return operation;
2266
+ }
2267
+ async #performClose() {
2268
+ const errors = [];
2269
+ try {
2270
+ await this.unadvertise();
2271
+ }
2272
+ catch (error) {
2273
+ errors.push(error);
2274
+ }
2275
+ const unsettledReceipts = [...this.#pending.entries()];
2276
+ for (const pending of this.#pending.values())
2277
+ clearTimeout(pending.timer);
2278
+ this.#pending.clear();
2279
+ this.#inboundReceipts.clear();
2280
+ if (this.#capacitySettlement?.retryTimer !== undefined) {
2281
+ clearTimeout(this.#capacitySettlement.retryTimer);
2282
+ }
2283
+ this.#capacitySettlement = undefined;
2284
+ const receiptResults = await Promise.allSettled(unsettledReceipts.map(async ([messageId, pending]) => this.#emitReceipt({
2285
+ messageId,
2286
+ status: pendingReceiptDeadlineStatus(pending) === "unconfirmed"
2287
+ ? "unconfirmed"
2288
+ : "ambiguous",
2289
+ trust: "untrusted_same_uid_peer",
2290
+ })));
2291
+ for (const result of receiptResults) {
2292
+ if (result.status === "rejected")
2293
+ errors.push(result.reason);
2294
+ }
2295
+ for (const socket of this.#connections)
2296
+ socket.destroy();
2297
+ this.#connections.clear();
2298
+ let callbackPathOwned = false;
2299
+ try {
2300
+ const beforeClose = await lstat(this.#socketPath);
2301
+ if (!beforeClose.isSocket() ||
2302
+ !sameSocketGeneration({ dev: beforeClose.dev, ino: beforeClose.ino }, this.#socketGeneration)) {
2303
+ // Node's net.Server.close() unlinks its original pathname even when
2304
+ // another same-user object replaced it. Do not call it after an
2305
+ // observed generation change. The now-unreachable descriptor is
2306
+ // unref'd and left for process exit rather than deleting foreign data.
2307
+ this.#server.unref();
2308
+ errors.push(new BridgeError("CLAUDE_PEER_CALLBACK_CHANGED", "The callback path changed; foreign replacement was preserved."));
2309
+ }
2310
+ else {
2311
+ callbackPathOwned = true;
2312
+ }
2313
+ }
2314
+ catch (error) {
2315
+ if (error.code === "ENOENT") {
2316
+ this.#server.unref();
2317
+ errors.push(new BridgeError("CLAUDE_PEER_CALLBACK_CHANGED", "The callback path disappeared; cleanup failed closed."));
2318
+ }
2319
+ else {
2320
+ this.#server.unref();
2321
+ errors.push(error);
2322
+ }
2323
+ }
2324
+ if (callbackPathOwned) {
2325
+ try {
2326
+ await new Promise((resolve, reject) => this.#server.close((error) => error === undefined ? resolve() : reject(error)));
2327
+ }
2328
+ catch (error) {
2329
+ errors.push(error);
2330
+ }
2331
+ try {
2332
+ const stat = await lstat(this.#socketPath);
2333
+ if (stat.isSocket() &&
2334
+ sameSocketGeneration({ dev: stat.dev, ino: stat.ino }, this.#socketGeneration)) {
2335
+ await unlink(this.#socketPath);
2336
+ }
2337
+ }
2338
+ catch (error) {
2339
+ if (error.code !== "ENOENT") {
2340
+ errors.push(error);
2341
+ }
2342
+ }
2343
+ }
2344
+ this.#closing = false;
2345
+ this.#closed = true;
2346
+ try {
2347
+ this.#onClosed();
2348
+ }
2349
+ catch (error) {
2350
+ errors.push(error);
2351
+ }
2352
+ if (errors.length === 1)
2353
+ throw errors[0];
2354
+ if (errors.length > 1) {
2355
+ throw new AggregateError(errors, "The Claude peer listener closed with multiple cleanup failures.");
2356
+ }
2357
+ }
2358
+ }
2359
+ //# sourceMappingURL=claude-peer.js.map