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