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