@ctliz/agent-intercom-codex 0.11.0-connect.2
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/.codex-plugin/plugin.json +39 -0
- package/.mcp.json +10 -0
- package/COPYRIGHT +6 -0
- package/LICENSE +661 -0
- package/LICENSE_TRANSITION.md +25 -0
- package/README.md +572 -0
- package/THIRD_PARTY_NOTICES.md +17 -0
- package/assets/logo-generated.png +0 -0
- package/assets/logo.svg +22 -0
- package/boss-control-outbox.ts +195 -0
- package/broker/access-credential.ts +76 -0
- package/broker/access-registry.ts +464 -0
- package/broker/audit.ts +60 -0
- package/broker/authorization.ts +54 -0
- package/broker/boss-adapter.ts +531 -0
- package/broker/boss-control-ledger.ts +306 -0
- package/broker/broker.ts +2327 -0
- package/broker/client.ts +1111 -0
- package/broker/framing.ts +87 -0
- package/broker/ownership.ts +61 -0
- package/broker/paths.ts +161 -0
- package/broker/spawn.ts +453 -0
- package/codex/app-server-client.ts +562 -0
- package/codex/boss-client.ts +34 -0
- package/codex/bridge-config.ts +282 -0
- package/codex/bridge-daemon.ts +913 -0
- package/codex/clipboard.ts +142 -0
- package/codex/coi.ts +802 -0
- package/codex/contact.ts +54 -0
- package/codex/mcp-protocol.ts +248 -0
- package/codex/runtime.ts +488 -0
- package/codex/server.ts +60 -0
- package/codex/team.ts +249 -0
- package/codex/tui-input.ts +174 -0
- package/config.ts +195 -0
- package/dist/bridge-daemon.mjs +3781 -0
- package/dist/broker.mjs +3012 -0
- package/dist/build-info.json +12 -0
- package/dist/codex-server.mjs +2890 -0
- package/dist/coi.mjs +4686 -0
- package/durable-json.ts +25 -0
- package/licenses/MIT-pi-intercom.txt +21 -0
- package/outbound-outbox.ts +116 -0
- package/package.json +89 -0
- package/protocol-v4/contract.ts +27 -0
- package/provider/protected-service.ts +162 -0
- package/provider/provider.mjs +34 -0
- package/scripts/build-info.mjs +58 -0
- package/scripts/build.mjs +55 -0
- package/skills/codex-intercom/SKILL.md +104 -0
- package/tsconfig.json +16 -0
- package/types.ts +225 -0
package/dist/broker.mjs
ADDED
|
@@ -0,0 +1,3012 @@
|
|
|
1
|
+
process.stderr.write("[agent-intercom-build] package=@ctliz/agent-intercom-codex version=0.11.0-connect.2 target=broker sourceSha256=3f81ffd19c83ad2f4db13939ce8dba9eae17d8c4b0c419e405941e0352f9c102\n");
|
|
2
|
+
|
|
3
|
+
// broker/broker.ts
|
|
4
|
+
import net from "net";
|
|
5
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2 } from "fs";
|
|
6
|
+
import { join as join2 } from "path";
|
|
7
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
8
|
+
import { types as nodeUtilTypes2 } from "node:util";
|
|
9
|
+
import { authorize, POLICY_SEMANTICS_HASH, POLICY_SEMANTICS_VERSION } from "@ctliz/agent-intercom-core";
|
|
10
|
+
import { canonicalHash } from "@ctliz/agent-intercom-core/canonical";
|
|
11
|
+
|
|
12
|
+
// broker/framing.ts
|
|
13
|
+
var MAX_FRAME_BYTES = 1024 * 1024;
|
|
14
|
+
function writeMessage(socket, msg) {
|
|
15
|
+
const json = JSON.stringify(msg);
|
|
16
|
+
const payload = Buffer.from(json, "utf-8");
|
|
17
|
+
const header = Buffer.alloc(4);
|
|
18
|
+
header.writeUInt32BE(payload.length, 0);
|
|
19
|
+
socket.write(Buffer.concat([header, payload]));
|
|
20
|
+
}
|
|
21
|
+
function createMessageReader(onMessage, onError, maxFrameBytes = MAX_FRAME_BYTES) {
|
|
22
|
+
let buffer = Buffer.alloc(0);
|
|
23
|
+
function reportMessage(payload) {
|
|
24
|
+
let msg;
|
|
25
|
+
try {
|
|
26
|
+
msg = JSON.parse(payload.toString("utf-8"));
|
|
27
|
+
} catch (error) {
|
|
28
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
29
|
+
onError(new Error(`Failed to parse intercom message: ${message}`, { cause: error }));
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
onMessage(msg);
|
|
34
|
+
return true;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
37
|
+
onError(new Error(`Failed to handle intercom message: ${message}`, { cause: error }));
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return (data) => {
|
|
42
|
+
let remaining = data;
|
|
43
|
+
while (remaining.length > 0) {
|
|
44
|
+
if (buffer.length < 4) {
|
|
45
|
+
const headerBytes = Math.min(4 - buffer.length, remaining.length);
|
|
46
|
+
buffer = Buffer.concat([buffer, remaining.subarray(0, headerBytes)]);
|
|
47
|
+
remaining = remaining.subarray(headerBytes);
|
|
48
|
+
if (buffer.length < 4) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const length = buffer.readUInt32BE(0);
|
|
53
|
+
if (length > maxFrameBytes) {
|
|
54
|
+
buffer = Buffer.alloc(0);
|
|
55
|
+
onError(new Error(`Intercom frame length ${length} exceeds maximum ${maxFrameBytes} bytes`));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const missingPayloadBytes = length - Math.max(0, buffer.length - 4);
|
|
59
|
+
const payloadBytes = Math.min(missingPayloadBytes, remaining.length);
|
|
60
|
+
if (payloadBytes > 0) {
|
|
61
|
+
buffer = Buffer.concat([buffer, remaining.subarray(0, payloadBytes)]);
|
|
62
|
+
remaining = remaining.subarray(payloadBytes);
|
|
63
|
+
}
|
|
64
|
+
if (buffer.length < 4 + length) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const payload = buffer.subarray(4, 4 + length);
|
|
68
|
+
buffer = Buffer.alloc(0);
|
|
69
|
+
if (!reportMessage(payload)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// protocol-v4/contract.ts
|
|
77
|
+
import {
|
|
78
|
+
INTERCOM_PROTOCOL_NAME,
|
|
79
|
+
INTERCOM_PROTOCOL_V4_SEMANTICS_HASH,
|
|
80
|
+
INTERCOM_PROTOCOL_V4_VECTOR_SCHEMA_VERSION,
|
|
81
|
+
INTERCOM_PROTOCOL_V4_VECTORS,
|
|
82
|
+
INTERCOM_PROTOCOL_VERSION,
|
|
83
|
+
INTERCOM_SCOPE_ENV,
|
|
84
|
+
INTERCOM_SCOPE_ID_PATTERN,
|
|
85
|
+
INTERCOM_SCOPE_ID_PATTERN_SOURCE,
|
|
86
|
+
intercomScopeIdFromEnv,
|
|
87
|
+
parseIntercomScopeId,
|
|
88
|
+
sameIntercomScope
|
|
89
|
+
} from "@ctliz/agent-intercom-core/protocol-v4";
|
|
90
|
+
|
|
91
|
+
// broker/paths.ts
|
|
92
|
+
import { chmodSync, mkdirSync, readFileSync } from "fs";
|
|
93
|
+
import { isAbsolute, join, resolve } from "path";
|
|
94
|
+
import { homedir } from "os";
|
|
95
|
+
var INTERCOM_DIR_MODE = 448;
|
|
96
|
+
var INTERCOM_RUNTIME_FILE_MODE = 384;
|
|
97
|
+
var INTERCOM_TCP_HOST = "127.0.0.1";
|
|
98
|
+
var INTERCOM_PROTOCOL_NAME2 = "pi-intercom";
|
|
99
|
+
var INTERCOM_PROTOCOL_VERSION2 = 4;
|
|
100
|
+
function sanitizePipeSegment(value) {
|
|
101
|
+
return value.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "default";
|
|
102
|
+
}
|
|
103
|
+
function getAgentDirPath(env = process.env, homeDir = homedir(), cwd = process.cwd()) {
|
|
104
|
+
const configured = env.PI_CODING_AGENT_DIR?.trim();
|
|
105
|
+
if (!configured) {
|
|
106
|
+
return join(homeDir, ".pi/agent");
|
|
107
|
+
}
|
|
108
|
+
return isAbsolute(configured) ? configured : resolve(cwd, configured);
|
|
109
|
+
}
|
|
110
|
+
function getIntercomDirPath(agentDir = getAgentDirPath()) {
|
|
111
|
+
return join(agentDir, "intercom");
|
|
112
|
+
}
|
|
113
|
+
function shouldUseWindowsTcpTransport(platform = process.platform, env = process.env) {
|
|
114
|
+
if (platform !== "win32") {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const transport = env.PI_INTERCOM_TRANSPORT?.trim().toLowerCase();
|
|
118
|
+
if (transport === "tcp") {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
const legacyOptIn = env.PI_INTERCOM_TCP?.trim().toLowerCase();
|
|
122
|
+
return legacyOptIn === "1" || legacyOptIn === "true";
|
|
123
|
+
}
|
|
124
|
+
function getBrokerPortFilePath(intercomDir = getIntercomDirPath()) {
|
|
125
|
+
return join(intercomDir, "broker.port.json");
|
|
126
|
+
}
|
|
127
|
+
function getBrokerAskStateFilePath(intercomDir = getIntercomDirPath()) {
|
|
128
|
+
return join(intercomDir, "broker-asks.json");
|
|
129
|
+
}
|
|
130
|
+
function getBrokerAccessStateFilePath(intercomDir = getIntercomDirPath()) {
|
|
131
|
+
return join(intercomDir, "broker-access.json");
|
|
132
|
+
}
|
|
133
|
+
function getBrokerAdminCredentialFilePath(intercomDir = getIntercomDirPath()) {
|
|
134
|
+
return join(intercomDir, "broker-admin.json");
|
|
135
|
+
}
|
|
136
|
+
function getBrokerAuditFilePath(intercomDir = getIntercomDirPath()) {
|
|
137
|
+
return join(intercomDir, "broker-audit.jsonl");
|
|
138
|
+
}
|
|
139
|
+
function getRemoteGatewaySocketPath(platform = process.platform, agentDir = getAgentDirPath()) {
|
|
140
|
+
if (platform === "win32") {
|
|
141
|
+
return `\\\\.\\pipe\\pi-intercom-remote-${sanitizePipeSegment(agentDir)}`;
|
|
142
|
+
}
|
|
143
|
+
return join(getIntercomDirPath(agentDir), "remote-gateway.sock");
|
|
144
|
+
}
|
|
145
|
+
function getBrokerSocketPath(platform = process.platform, agentDir = getAgentDirPath()) {
|
|
146
|
+
if (platform === "win32") {
|
|
147
|
+
return `\\\\.\\pipe\\pi-intercom-${sanitizePipeSegment(agentDir)}`;
|
|
148
|
+
}
|
|
149
|
+
return join(getIntercomDirPath(agentDir), "broker.sock");
|
|
150
|
+
}
|
|
151
|
+
function getBrokerListenTarget(platform = process.platform, env = process.env) {
|
|
152
|
+
if (shouldUseWindowsTcpTransport(platform, env)) {
|
|
153
|
+
return { transport: "tcp", host: INTERCOM_TCP_HOST, port: 0 };
|
|
154
|
+
}
|
|
155
|
+
return getBrokerSocketPath(platform, getAgentDirPath(env));
|
|
156
|
+
}
|
|
157
|
+
function ensureIntercomRuntimeDir(intercomDir = getIntercomDirPath(), platform = process.platform) {
|
|
158
|
+
mkdirSync(intercomDir, { recursive: true, mode: INTERCOM_DIR_MODE });
|
|
159
|
+
if (platform !== "win32") {
|
|
160
|
+
chmodSync(intercomDir, INTERCOM_DIR_MODE);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function restrictIntercomRuntimeFile(filePath, platform = process.platform) {
|
|
164
|
+
if (platform !== "win32") {
|
|
165
|
+
chmodSync(filePath, INTERCOM_RUNTIME_FILE_MODE);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// config.ts
|
|
170
|
+
var DEFAULT_ASK_TIMEOUT_MS = 45 * 1e3;
|
|
171
|
+
var MAX_ASK_TIMEOUT_MS = 120 * 1e3;
|
|
172
|
+
function validateAskTimeoutMs(value, name = "timeout_ms") {
|
|
173
|
+
if (!Number.isSafeInteger(value) || typeof value !== "number" || value <= 0) {
|
|
174
|
+
throw new Error(`${name} must be a positive integer number of milliseconds`);
|
|
175
|
+
}
|
|
176
|
+
if (value > MAX_ASK_TIMEOUT_MS) {
|
|
177
|
+
throw new Error(`${name} must be ${MAX_ASK_TIMEOUT_MS} ms or less; use intercom_send plus intercom_pending for longer-running work`);
|
|
178
|
+
}
|
|
179
|
+
return value;
|
|
180
|
+
}
|
|
181
|
+
function getAskTimeoutMs() {
|
|
182
|
+
const raw = process.env.PI_INTERCOM_ASK_TIMEOUT_MS;
|
|
183
|
+
if (raw === void 0 || raw.trim() === "") {
|
|
184
|
+
return DEFAULT_ASK_TIMEOUT_MS;
|
|
185
|
+
}
|
|
186
|
+
const value = Number(raw);
|
|
187
|
+
return validateAskTimeoutMs(value, "PI_INTERCOM_ASK_TIMEOUT_MS");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// durable-json.ts
|
|
191
|
+
import { randomUUID } from "crypto";
|
|
192
|
+
import { closeSync, fsyncSync, openSync, renameSync, writeFileSync } from "fs";
|
|
193
|
+
import { dirname } from "path";
|
|
194
|
+
function writeDurableJson(filePath, value) {
|
|
195
|
+
const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
196
|
+
writeFileSync(temporaryPath, JSON.stringify(value), { encoding: "utf-8", mode: INTERCOM_RUNTIME_FILE_MODE });
|
|
197
|
+
const fileDescriptor = openSync(temporaryPath, "r");
|
|
198
|
+
try {
|
|
199
|
+
fsyncSync(fileDescriptor);
|
|
200
|
+
} finally {
|
|
201
|
+
closeSync(fileDescriptor);
|
|
202
|
+
}
|
|
203
|
+
renameSync(temporaryPath, filePath);
|
|
204
|
+
restrictIntercomRuntimeFile(filePath);
|
|
205
|
+
if (process.platform !== "win32") {
|
|
206
|
+
const directoryDescriptor = openSync(dirname(filePath), "r");
|
|
207
|
+
try {
|
|
208
|
+
fsyncSync(directoryDescriptor);
|
|
209
|
+
} finally {
|
|
210
|
+
closeSync(directoryDescriptor);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// broker/ownership.ts
|
|
216
|
+
import { closeSync as closeSync2, constants, openSync as openSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
217
|
+
function ownerPid(path) {
|
|
218
|
+
try {
|
|
219
|
+
const pid = Number.parseInt(readFileSync2(path, "utf8").trim(), 10);
|
|
220
|
+
return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
|
|
221
|
+
} catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function pidIsAlive(pid) {
|
|
226
|
+
try {
|
|
227
|
+
process.kill(pid, 0);
|
|
228
|
+
return true;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
return error.code !== "ESRCH";
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function acquireBrokerOwnership(path, pid = process.pid) {
|
|
234
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
235
|
+
try {
|
|
236
|
+
const fd = openSync2(path, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, INTERCOM_RUNTIME_FILE_MODE);
|
|
237
|
+
try {
|
|
238
|
+
writeFileSync2(fd, String(pid));
|
|
239
|
+
} finally {
|
|
240
|
+
closeSync2(fd);
|
|
241
|
+
}
|
|
242
|
+
restrictIntercomRuntimeFile(path);
|
|
243
|
+
return;
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if (error.code !== "EEXIST") throw error;
|
|
246
|
+
const existingPid = ownerPid(path);
|
|
247
|
+
if (existingPid !== null && pidIsAlive(existingPid)) {
|
|
248
|
+
throw new Error(`Intercom broker already owned by live process ${existingPid}`);
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
unlinkSync(path);
|
|
252
|
+
} catch (unlinkError) {
|
|
253
|
+
if (unlinkError.code !== "ENOENT") throw unlinkError;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
throw new Error("Could not acquire intercom broker ownership");
|
|
258
|
+
}
|
|
259
|
+
function releaseBrokerOwnership(path, pid = process.pid) {
|
|
260
|
+
if (ownerPid(path) !== pid) return;
|
|
261
|
+
try {
|
|
262
|
+
unlinkSync(path);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
if (error.code !== "ENOENT") throw error;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function hasBrokerOwnership(path, pid = process.pid) {
|
|
268
|
+
return ownerPid(path) === pid;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// broker/access-registry.ts
|
|
272
|
+
import { createHash, randomBytes, randomUUID as randomUUID2, timingSafeEqual } from "crypto";
|
|
273
|
+
import { existsSync, readFileSync as readFileSync3 } from "fs";
|
|
274
|
+
var REMOTE_ACCESS_STATE_VERSION = 2;
|
|
275
|
+
var REMOTE_ACCESS_CREDENTIAL_VERSION = 1;
|
|
276
|
+
var DEFAULT_ENROLLMENT_TTL_MS = 10 * 60 * 1e3;
|
|
277
|
+
var DEFAULT_PRINCIPAL_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
278
|
+
var RemoteAccessError = class extends Error {
|
|
279
|
+
constructor(code, message) {
|
|
280
|
+
super(message);
|
|
281
|
+
this.code = code;
|
|
282
|
+
this.name = "RemoteAccessError";
|
|
283
|
+
}
|
|
284
|
+
code;
|
|
285
|
+
};
|
|
286
|
+
function emptyState() {
|
|
287
|
+
return { version: REMOTE_ACCESS_STATE_VERSION, principals: {}, enrollments: {} };
|
|
288
|
+
}
|
|
289
|
+
function hashSecret(secret) {
|
|
290
|
+
return createHash("sha256").update(secret, "utf8").digest("hex");
|
|
291
|
+
}
|
|
292
|
+
function secretsMatch(secret, expectedHash) {
|
|
293
|
+
const actual = Buffer.from(hashSecret(secret), "hex");
|
|
294
|
+
const expected = Buffer.from(expectedHash, "hex");
|
|
295
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
296
|
+
}
|
|
297
|
+
function newSecret() {
|
|
298
|
+
return randomBytes(32).toString("base64url");
|
|
299
|
+
}
|
|
300
|
+
function requireText(value, field, maxLength = 512) {
|
|
301
|
+
const normalized = value.trim();
|
|
302
|
+
if (!normalized || normalized.length > maxLength || /[\u0000-\u001f\u007f]/.test(normalized)) {
|
|
303
|
+
throw new Error(`Invalid ${field}`);
|
|
304
|
+
}
|
|
305
|
+
return normalized;
|
|
306
|
+
}
|
|
307
|
+
function boundedInteger(value, fallback, field, minimum, maximum) {
|
|
308
|
+
const resolved = value ?? fallback;
|
|
309
|
+
if (!Number.isSafeInteger(resolved) || resolved < minimum || resolved > maximum) throw new Error(`Invalid ${field}`);
|
|
310
|
+
return resolved;
|
|
311
|
+
}
|
|
312
|
+
function parseState(raw) {
|
|
313
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new Error("expected object");
|
|
314
|
+
const state = raw;
|
|
315
|
+
if (state.version !== 1 && state.version !== REMOTE_ACCESS_STATE_VERSION) throw new Error("unsupported version");
|
|
316
|
+
if (typeof state.principals !== "object" || state.principals === null || Array.isArray(state.principals)) throw new Error("invalid principals");
|
|
317
|
+
if (typeof state.enrollments !== "object" || state.enrollments === null || Array.isArray(state.enrollments)) throw new Error("invalid enrollments");
|
|
318
|
+
if (state.adminCredentialHash !== void 0 && typeof state.adminCredentialHash !== "string") throw new Error("invalid admin credential hash");
|
|
319
|
+
const principals = {};
|
|
320
|
+
for (const [id, value] of Object.entries(state.principals)) {
|
|
321
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`invalid principal ${id}`);
|
|
322
|
+
const principal = value;
|
|
323
|
+
if (principal.id !== id || typeof principal.name !== "string" || typeof principal.credentialHash !== "string" || typeof principal.parentSessionId !== "string" || typeof principal.rootSessionId !== "string" || typeof principal.remoteHostId !== "string" || typeof principal.generation !== "number" || principal.state !== "active" && principal.state !== "revoked" || typeof principal.expiresAt !== "number" || typeof principal.createdAt !== "number" || typeof principal.updatedAt !== "number") throw new Error(`invalid principal ${id}`);
|
|
324
|
+
const depth = boundedInteger(principal.depth, 1, "principal depth", 1, 32);
|
|
325
|
+
const maxDepth = boundedInteger(principal.maxDepth, depth, "maximum delegation depth", depth, 32);
|
|
326
|
+
const maxChildren = boundedInteger(principal.maxChildren, 0, "maximum child count", 0, 128);
|
|
327
|
+
principals[id] = {
|
|
328
|
+
...principal,
|
|
329
|
+
policy: "remote-tree",
|
|
330
|
+
canDelegate: principal.canDelegate === true,
|
|
331
|
+
depth,
|
|
332
|
+
maxDepth,
|
|
333
|
+
maxChildren
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const enrollments = {};
|
|
337
|
+
for (const [hash, value] of Object.entries(state.enrollments)) {
|
|
338
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`invalid enrollment ${hash}`);
|
|
339
|
+
const enrollment = value;
|
|
340
|
+
const template = enrollment.template;
|
|
341
|
+
if (!template || typeof template !== "object") throw new Error(`invalid enrollment ${hash}`);
|
|
342
|
+
const depth = boundedInteger(template.depth, 1, "enrollment depth", 1, 32);
|
|
343
|
+
enrollments[hash] = {
|
|
344
|
+
...enrollment,
|
|
345
|
+
template: {
|
|
346
|
+
...template,
|
|
347
|
+
canDelegate: template.canDelegate === true,
|
|
348
|
+
depth,
|
|
349
|
+
maxDepth: boundedInteger(template.maxDepth, depth, "enrollment maximum depth", depth, 32),
|
|
350
|
+
maxChildren: boundedInteger(template.maxChildren, 0, "enrollment maximum children", 0, 128)
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
version: REMOTE_ACCESS_STATE_VERSION,
|
|
356
|
+
...typeof state.adminCredentialHash === "string" ? { adminCredentialHash: state.adminCredentialHash } : {},
|
|
357
|
+
principals,
|
|
358
|
+
enrollments
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
var RemoteAccessRegistry = class {
|
|
362
|
+
constructor(statePath, now = Date.now) {
|
|
363
|
+
this.statePath = statePath;
|
|
364
|
+
this.now = now;
|
|
365
|
+
this.state = this.load();
|
|
366
|
+
}
|
|
367
|
+
statePath;
|
|
368
|
+
now;
|
|
369
|
+
state;
|
|
370
|
+
snapshot() {
|
|
371
|
+
return structuredClone(this.state);
|
|
372
|
+
}
|
|
373
|
+
ensureAdminCredential(credentialPath) {
|
|
374
|
+
if (existsSync(credentialPath)) {
|
|
375
|
+
const parsed = JSON.parse(readFileSync3(credentialPath, "utf8"));
|
|
376
|
+
if (parsed.version === REMOTE_ACCESS_CREDENTIAL_VERSION && typeof parsed.adminToken === "string" && parsed.adminToken.length >= 32) {
|
|
377
|
+
const hash = hashSecret(parsed.adminToken);
|
|
378
|
+
if (this.state.adminCredentialHash !== hash) {
|
|
379
|
+
this.state.adminCredentialHash = hash;
|
|
380
|
+
this.persist();
|
|
381
|
+
}
|
|
382
|
+
restrictIntercomRuntimeFile(credentialPath);
|
|
383
|
+
return parsed.adminToken;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const adminToken = newSecret();
|
|
387
|
+
writeDurableJson(credentialPath, { version: REMOTE_ACCESS_CREDENTIAL_VERSION, adminToken });
|
|
388
|
+
this.state.adminCredentialHash = hashSecret(adminToken);
|
|
389
|
+
this.persist();
|
|
390
|
+
return adminToken;
|
|
391
|
+
}
|
|
392
|
+
authenticateAdmin(adminToken) {
|
|
393
|
+
return typeof this.state.adminCredentialHash === "string" && secretsMatch(adminToken, this.state.adminCredentialHash);
|
|
394
|
+
}
|
|
395
|
+
issueEnrollment(template, ttlMs = DEFAULT_ENROLLMENT_TTL_MS) {
|
|
396
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0 || ttlMs > 24 * 60 * 60 * 1e3) throw new Error("Invalid enrollment TTL");
|
|
397
|
+
const now = this.now();
|
|
398
|
+
const enrollmentToken = newSecret();
|
|
399
|
+
const tokenHash = hashSecret(enrollmentToken);
|
|
400
|
+
const expiresAt = now + ttlMs;
|
|
401
|
+
const principalExpiresAt = template.expiresAt ?? now + DEFAULT_PRINCIPAL_TTL_MS;
|
|
402
|
+
if (!Number.isSafeInteger(principalExpiresAt) || principalExpiresAt <= now) throw new Error("Invalid principal expiry");
|
|
403
|
+
const depth = boundedInteger(template.depth, 1, "principal depth", 1, 32);
|
|
404
|
+
const maxDepth = boundedInteger(template.maxDepth, depth, "maximum delegation depth", depth, 32);
|
|
405
|
+
const maxChildren = boundedInteger(template.maxChildren, 0, "maximum child count", 0, 128);
|
|
406
|
+
const canDelegate = template.canDelegate === true;
|
|
407
|
+
if (canDelegate && (maxDepth <= depth || maxChildren === 0)) throw new Error("Delegating principals require remaining depth and child capacity");
|
|
408
|
+
const normalized = {
|
|
409
|
+
name: requireText(template.name, "principal name", 256),
|
|
410
|
+
parentSessionId: requireText(template.parentSessionId, "parent session ID"),
|
|
411
|
+
rootSessionId: requireText(template.rootSessionId, "root session ID"),
|
|
412
|
+
remoteHostId: requireText(template.remoteHostId, "remote host ID", 256),
|
|
413
|
+
expiresAt: principalExpiresAt,
|
|
414
|
+
canDelegate,
|
|
415
|
+
depth,
|
|
416
|
+
maxDepth,
|
|
417
|
+
maxChildren
|
|
418
|
+
};
|
|
419
|
+
this.pruneExpiredEnrollments(now);
|
|
420
|
+
this.state.enrollments[tokenHash] = { tokenHash, template: normalized, expiresAt, createdAt: now };
|
|
421
|
+
this.persist();
|
|
422
|
+
return { enrollmentToken, expiresAt };
|
|
423
|
+
}
|
|
424
|
+
issueChildEnrollment(parentSessionId, parentGeneration, request, ttlMs = DEFAULT_ENROLLMENT_TTL_MS) {
|
|
425
|
+
const parent = this.validatePrincipal(parentSessionId, parentGeneration);
|
|
426
|
+
if (!parent.canDelegate) throw new RemoteAccessError("INVALID_ENROLLMENT", "Parent principal cannot delegate children");
|
|
427
|
+
const now = this.now();
|
|
428
|
+
const activeChildren = Object.values(this.state.principals).filter(
|
|
429
|
+
(principal) => principal.parentSessionId === parent.id && principal.state === "active" && principal.expiresAt > now
|
|
430
|
+
).length;
|
|
431
|
+
const pendingChildren = Object.values(this.state.enrollments).filter(
|
|
432
|
+
(enrollment) => enrollment.template.parentSessionId === parent.id && enrollment.expiresAt > now
|
|
433
|
+
).length;
|
|
434
|
+
if (activeChildren + pendingChildren >= parent.maxChildren) throw new RemoteAccessError("INVALID_ENROLLMENT", "Parent child limit is exhausted");
|
|
435
|
+
const depth = parent.depth + 1;
|
|
436
|
+
if (depth > parent.maxDepth) throw new RemoteAccessError("INVALID_ENROLLMENT", "Parent delegation depth is exhausted");
|
|
437
|
+
const maxDepth = boundedInteger(request.maxDepth, depth, "child maximum depth", depth, parent.maxDepth);
|
|
438
|
+
const maxChildren = boundedInteger(request.maxChildren, 0, "child maximum count", 0, parent.maxChildren);
|
|
439
|
+
const canDelegate = request.canDelegate === true;
|
|
440
|
+
if (canDelegate && (maxDepth <= depth || maxChildren === 0)) throw new Error("Delegating child requires remaining depth and child capacity");
|
|
441
|
+
const expiresAt = request.expiresAt ?? parent.expiresAt;
|
|
442
|
+
if (!Number.isSafeInteger(expiresAt) || expiresAt <= this.now() || expiresAt > parent.expiresAt) {
|
|
443
|
+
throw new Error("Child expiry must not exceed the parent expiry");
|
|
444
|
+
}
|
|
445
|
+
return this.issueEnrollment({
|
|
446
|
+
name: request.name,
|
|
447
|
+
parentSessionId: parent.id,
|
|
448
|
+
rootSessionId: parent.rootSessionId,
|
|
449
|
+
remoteHostId: parent.remoteHostId,
|
|
450
|
+
expiresAt,
|
|
451
|
+
canDelegate,
|
|
452
|
+
depth,
|
|
453
|
+
maxDepth,
|
|
454
|
+
maxChildren
|
|
455
|
+
}, ttlMs);
|
|
456
|
+
}
|
|
457
|
+
consumeEnrollment(enrollmentToken) {
|
|
458
|
+
const now = this.now();
|
|
459
|
+
const tokenHash = hashSecret(enrollmentToken);
|
|
460
|
+
const enrollment = this.state.enrollments[tokenHash];
|
|
461
|
+
if (!enrollment) {
|
|
462
|
+
console.error("DEBUG: Enrollment token not found, hash:", tokenHash);
|
|
463
|
+
throw new RemoteAccessError("INVALID_ENROLLMENT", "Enrollment credential is invalid or already consumed");
|
|
464
|
+
}
|
|
465
|
+
console.error("DEBUG: Enrollment consumed, hash:", tokenHash);
|
|
466
|
+
delete this.state.enrollments[tokenHash];
|
|
467
|
+
if (enrollment.expiresAt <= now) {
|
|
468
|
+
this.persist();
|
|
469
|
+
throw new RemoteAccessError("INVALID_ENROLLMENT", "Enrollment credential has expired");
|
|
470
|
+
}
|
|
471
|
+
const sessionCredential = newSecret();
|
|
472
|
+
const id = randomUUID2();
|
|
473
|
+
const principal = {
|
|
474
|
+
id,
|
|
475
|
+
name: enrollment.template.name,
|
|
476
|
+
credentialHash: hashSecret(sessionCredential),
|
|
477
|
+
parentSessionId: enrollment.template.parentSessionId,
|
|
478
|
+
rootSessionId: enrollment.template.rootSessionId,
|
|
479
|
+
remoteHostId: enrollment.template.remoteHostId,
|
|
480
|
+
generation: 1,
|
|
481
|
+
policy: "remote-tree",
|
|
482
|
+
canDelegate: enrollment.template.canDelegate === true,
|
|
483
|
+
depth: enrollment.template.depth,
|
|
484
|
+
maxDepth: enrollment.template.maxDepth,
|
|
485
|
+
maxChildren: enrollment.template.maxChildren,
|
|
486
|
+
state: "active",
|
|
487
|
+
expiresAt: enrollment.template.expiresAt,
|
|
488
|
+
createdAt: now,
|
|
489
|
+
updatedAt: now
|
|
490
|
+
};
|
|
491
|
+
this.state.principals[id] = principal;
|
|
492
|
+
this.persist();
|
|
493
|
+
return { principal: structuredClone(principal), sessionCredential };
|
|
494
|
+
}
|
|
495
|
+
authenticateSession(sessionId, generation, sessionCredential) {
|
|
496
|
+
const principal = this.state.principals[sessionId];
|
|
497
|
+
if (!principal || !secretsMatch(sessionCredential, principal.credentialHash)) {
|
|
498
|
+
throw new RemoteAccessError("INVALID_CREDENTIAL", "Session credential is invalid");
|
|
499
|
+
}
|
|
500
|
+
return this.validatePrincipal(sessionId, generation);
|
|
501
|
+
}
|
|
502
|
+
validatePrincipal(sessionId, generation) {
|
|
503
|
+
const principal = this.state.principals[sessionId];
|
|
504
|
+
if (!principal) throw new RemoteAccessError("INVALID_CREDENTIAL", "Remote principal does not exist");
|
|
505
|
+
if (principal.state !== "active") throw new RemoteAccessError("REVOKED_CREDENTIAL", "Session credential is revoked");
|
|
506
|
+
if (principal.expiresAt <= this.now()) throw new RemoteAccessError("EXPIRED_CREDENTIAL", "Session credential has expired");
|
|
507
|
+
if (principal.generation !== generation) throw new RemoteAccessError("STALE_GENERATION", "Session credential generation is stale");
|
|
508
|
+
return structuredClone(principal);
|
|
509
|
+
}
|
|
510
|
+
inspectSubtree(principalId) {
|
|
511
|
+
const result = [];
|
|
512
|
+
const queue = [principalId];
|
|
513
|
+
const seen = /* @__PURE__ */ new Set();
|
|
514
|
+
while (queue.length) {
|
|
515
|
+
const id = queue.shift();
|
|
516
|
+
if (seen.has(id)) continue;
|
|
517
|
+
seen.add(id);
|
|
518
|
+
const principal = this.state.principals[id];
|
|
519
|
+
if (!principal) continue;
|
|
520
|
+
const { credentialHash: _credentialHash, ...metadata } = principal;
|
|
521
|
+
result.push(structuredClone(metadata));
|
|
522
|
+
for (const candidate of Object.values(this.state.principals)) {
|
|
523
|
+
if (candidate.parentSessionId === id) queue.push(candidate.id);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return result;
|
|
527
|
+
}
|
|
528
|
+
adoptSubtree(principalId, newParentSessionId, newRootSessionId) {
|
|
529
|
+
const principal = this.state.principals[principalId];
|
|
530
|
+
if (!principal) throw new Error("Unknown adopted principal");
|
|
531
|
+
if (principalId === newParentSessionId) throw new Error("Adoption would create an ownership cycle");
|
|
532
|
+
let ancestor = this.state.principals[newParentSessionId];
|
|
533
|
+
const seen = /* @__PURE__ */ new Set();
|
|
534
|
+
while (ancestor && !seen.has(ancestor.id)) {
|
|
535
|
+
if (ancestor.id === principalId) throw new Error("Adoption would create an ownership cycle");
|
|
536
|
+
seen.add(ancestor.id);
|
|
537
|
+
ancestor = this.state.principals[ancestor.parentSessionId];
|
|
538
|
+
}
|
|
539
|
+
const ids = this.subtreePrincipalIds(principalId);
|
|
540
|
+
const now = this.now();
|
|
541
|
+
principal.parentSessionId = requireText(newParentSessionId, "new parent session ID");
|
|
542
|
+
for (const id of ids) {
|
|
543
|
+
const changed = this.state.principals[id];
|
|
544
|
+
changed.rootSessionId = requireText(newRootSessionId, "new root session ID");
|
|
545
|
+
changed.generation += 1;
|
|
546
|
+
changed.updatedAt = now;
|
|
547
|
+
}
|
|
548
|
+
const changedIds = new Set(ids);
|
|
549
|
+
for (const [hash, enrollment] of Object.entries(this.state.enrollments)) {
|
|
550
|
+
if (changedIds.has(enrollment.template.parentSessionId)) delete this.state.enrollments[hash];
|
|
551
|
+
}
|
|
552
|
+
this.persist();
|
|
553
|
+
return ids.map((id) => structuredClone(this.state.principals[id]));
|
|
554
|
+
}
|
|
555
|
+
expirePrincipals(now = this.now()) {
|
|
556
|
+
const changed = /* @__PURE__ */ new Map();
|
|
557
|
+
const expiredRoots = Object.values(this.state.principals).filter((principal) => principal.state === "active" && principal.expiresAt <= now).sort((left, right) => left.depth - right.depth);
|
|
558
|
+
for (const principal of expiredRoots) {
|
|
559
|
+
if (changed.has(principal.id)) continue;
|
|
560
|
+
for (const revoked of this.revoke(principal.id)) changed.set(revoked.id, revoked);
|
|
561
|
+
}
|
|
562
|
+
return [...changed.values()];
|
|
563
|
+
}
|
|
564
|
+
revoke(principalId) {
|
|
565
|
+
const now = this.now();
|
|
566
|
+
const queue = [principalId];
|
|
567
|
+
const seen = /* @__PURE__ */ new Set();
|
|
568
|
+
const changed = [];
|
|
569
|
+
while (queue.length) {
|
|
570
|
+
const id = queue.shift();
|
|
571
|
+
if (seen.has(id)) continue;
|
|
572
|
+
seen.add(id);
|
|
573
|
+
const principal = this.state.principals[id];
|
|
574
|
+
if (!principal) continue;
|
|
575
|
+
principal.state = "revoked";
|
|
576
|
+
principal.generation += 1;
|
|
577
|
+
principal.updatedAt = now;
|
|
578
|
+
changed.push(structuredClone(principal));
|
|
579
|
+
for (const candidate of Object.values(this.state.principals)) {
|
|
580
|
+
if (candidate.parentSessionId === id) queue.push(candidate.id);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (changed.length) this.persist();
|
|
584
|
+
return changed;
|
|
585
|
+
}
|
|
586
|
+
subtreePrincipalIds(principalId) {
|
|
587
|
+
const result = [];
|
|
588
|
+
const queue = [principalId];
|
|
589
|
+
const seen = /* @__PURE__ */ new Set();
|
|
590
|
+
while (queue.length) {
|
|
591
|
+
const id = queue.shift();
|
|
592
|
+
if (seen.has(id) || !this.state.principals[id]) continue;
|
|
593
|
+
seen.add(id);
|
|
594
|
+
result.push(id);
|
|
595
|
+
for (const candidate of Object.values(this.state.principals)) {
|
|
596
|
+
if (candidate.parentSessionId === id) queue.push(candidate.id);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return result;
|
|
600
|
+
}
|
|
601
|
+
pruneExpiredEnrollments(now = this.now()) {
|
|
602
|
+
for (const [hash, enrollment] of Object.entries(this.state.enrollments)) {
|
|
603
|
+
if (enrollment.expiresAt <= now) delete this.state.enrollments[hash];
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
load() {
|
|
607
|
+
if (!existsSync(this.statePath)) return emptyState();
|
|
608
|
+
try {
|
|
609
|
+
return parseState(JSON.parse(readFileSync3(this.statePath, "utf8")));
|
|
610
|
+
} catch (error) {
|
|
611
|
+
throw new Error(`Invalid remote access registry at ${this.statePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
persist() {
|
|
615
|
+
writeDurableJson(this.statePath, this.state);
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
// broker/boss-adapter.ts
|
|
620
|
+
import {
|
|
621
|
+
BOSS_CAPABILITY_FEATURE_DIGEST,
|
|
622
|
+
BOSS_CONTROL_ENVELOPE_VERSION,
|
|
623
|
+
BOSS_POLICY_PRINCIPAL_VERSION,
|
|
624
|
+
BOSS_POLICY_SEMANTICS_HASH,
|
|
625
|
+
BOSS_RUN_FEATURE,
|
|
626
|
+
BOSS_RUN_FEATURE_CONTRACT,
|
|
627
|
+
BOSS_RUN_FEATURE_SEMANTICS_HASH,
|
|
628
|
+
BOSS_RUN_FEATURE_VERSION,
|
|
629
|
+
BOSS_RUN_PROTOCOL_FEATURE_CONTRACT_HASH,
|
|
630
|
+
BROKER_FEATURE_ATTESTATION_VERSION,
|
|
631
|
+
INTERCOM_BASE_PROTOCOL_VERSION,
|
|
632
|
+
authorizeFeatureAware,
|
|
633
|
+
brokerFeatureSetHash,
|
|
634
|
+
parseBossControlEnvelope,
|
|
635
|
+
parseBossParticipantBinding,
|
|
636
|
+
parseBossParticipantCredentialEnvelope,
|
|
637
|
+
parseBossRunFeatureContract,
|
|
638
|
+
parseBrokerCapabilityAdvertisement,
|
|
639
|
+
parseParticipantState,
|
|
640
|
+
parseWorkerIdentityV2
|
|
641
|
+
} from "@ctliz/agent-intercom-core/boss";
|
|
642
|
+
import {
|
|
643
|
+
ContractValidationError,
|
|
644
|
+
assertExactKeys,
|
|
645
|
+
assertRecord,
|
|
646
|
+
canonicalJson
|
|
647
|
+
} from "@ctliz/agent-intercom-core/canonical";
|
|
648
|
+
import { types as nodeUtilTypes } from "node:util";
|
|
649
|
+
var BOSS_ADVERTISEMENT_PREDICATES = [
|
|
650
|
+
"protectedProvider",
|
|
651
|
+
"brokerIdentity",
|
|
652
|
+
"credentialRegistry",
|
|
653
|
+
"authorityTransitions",
|
|
654
|
+
"participantHealth"
|
|
655
|
+
];
|
|
656
|
+
var DORMANT_BOSS_ADVERTISEMENT_READINESS = Object.freeze({
|
|
657
|
+
protectedProvider: false,
|
|
658
|
+
brokerIdentity: false,
|
|
659
|
+
credentialRegistry: false,
|
|
660
|
+
authorityTransitions: false,
|
|
661
|
+
participantHealth: false
|
|
662
|
+
});
|
|
663
|
+
var ORDINARY_SESSION_REGISTRATION_KEYS = [
|
|
664
|
+
"cwd",
|
|
665
|
+
"model",
|
|
666
|
+
"pid",
|
|
667
|
+
"startedAt",
|
|
668
|
+
"lastActivity"
|
|
669
|
+
];
|
|
670
|
+
var OPTIONAL_SESSION_REGISTRATION_KEYS = ["name", "status", "runtimeInstanceId"];
|
|
671
|
+
function assertBossCanonicalData(value, path = "$", seen = /* @__PURE__ */ new WeakSet()) {
|
|
672
|
+
if (typeof value !== "object" || value === null) return;
|
|
673
|
+
if (nodeUtilTypes.isProxy(value)) {
|
|
674
|
+
throw new ContractValidationError(path, "proxies are not supported");
|
|
675
|
+
}
|
|
676
|
+
if (seen.has(value)) throw new ContractValidationError(path, "cyclic values are not supported");
|
|
677
|
+
seen.add(value);
|
|
678
|
+
if (Array.isArray(value)) {
|
|
679
|
+
if (Object.getPrototypeOf(value) !== Array.prototype) {
|
|
680
|
+
throw new ContractValidationError(path, "must use the exact Array prototype");
|
|
681
|
+
}
|
|
682
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
683
|
+
const expectedKeys = /* @__PURE__ */ new Set(["length"]);
|
|
684
|
+
for (let index = 0; index < value.length; index += 1) expectedKeys.add(String(index));
|
|
685
|
+
if (ownKeys.length !== expectedKeys.size || ownKeys.some((key) => !expectedKeys.has(key))) {
|
|
686
|
+
throw new ContractValidationError(path, "must be a dense array without symbols or extra properties");
|
|
687
|
+
}
|
|
688
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
689
|
+
if (!Object.hasOwn(value, index)) {
|
|
690
|
+
throw new ContractValidationError(`${path}[${index}]`, "sparse array holes are not supported");
|
|
691
|
+
}
|
|
692
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
693
|
+
if (descriptor === void 0 || !descriptor.enumerable || !Object.hasOwn(descriptor, "value")) {
|
|
694
|
+
throw new ContractValidationError(`${path}[${index}]`, "must be an own enumerable data property");
|
|
695
|
+
}
|
|
696
|
+
assertBossCanonicalData(descriptor.value, `${path}[${index}]`, seen);
|
|
697
|
+
}
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
if (Object.getPrototypeOf(value) !== Object.prototype) {
|
|
701
|
+
throw new ContractValidationError(path, "must use the exact Object prototype");
|
|
702
|
+
}
|
|
703
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
704
|
+
if (typeof key !== "string") throw new ContractValidationError(path, "symbol properties are not supported");
|
|
705
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
706
|
+
if (descriptor === void 0 || !descriptor.enumerable || !Object.hasOwn(descriptor, "value")) {
|
|
707
|
+
throw new ContractValidationError(`${path}.${key}`, "must be an own enumerable data property");
|
|
708
|
+
}
|
|
709
|
+
assertBossCanonicalData(descriptor.value, `${path}.${key}`, seen);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
function parseBossAdvertisementReadiness(value) {
|
|
713
|
+
assertBossCanonicalData(value, "$.readiness");
|
|
714
|
+
assertRecord(value);
|
|
715
|
+
assertExactKeys(value, BOSS_ADVERTISEMENT_PREDICATES);
|
|
716
|
+
const parsed = {};
|
|
717
|
+
for (const predicate of BOSS_ADVERTISEMENT_PREDICATES) {
|
|
718
|
+
const enabled = ownDataValue(value, predicate);
|
|
719
|
+
if (typeof enabled !== "boolean") {
|
|
720
|
+
throw new ContractValidationError(`$.readiness.${predicate}`, "must be a boolean");
|
|
721
|
+
}
|
|
722
|
+
parsed[predicate] = enabled;
|
|
723
|
+
}
|
|
724
|
+
return parsed;
|
|
725
|
+
}
|
|
726
|
+
function missingBossAdvertisementPredicates(readiness = DORMANT_BOSS_ADVERTISEMENT_READINESS) {
|
|
727
|
+
const parsed = parseBossAdvertisementReadiness(readiness);
|
|
728
|
+
return BOSS_ADVERTISEMENT_PREDICATES.filter((predicate) => parsed[predicate] !== true);
|
|
729
|
+
}
|
|
730
|
+
function bossCapabilityAdvertisement(readiness = DORMANT_BOSS_ADVERTISEMENT_READINESS) {
|
|
731
|
+
if (missingBossAdvertisementPredicates(readiness).length > 0) return void 0;
|
|
732
|
+
const features = [{
|
|
733
|
+
version: BROKER_FEATURE_ATTESTATION_VERSION,
|
|
734
|
+
feature: BOSS_RUN_FEATURE,
|
|
735
|
+
featureVersion: BOSS_RUN_FEATURE_VERSION,
|
|
736
|
+
semanticsHash: BOSS_RUN_FEATURE_SEMANTICS_HASH,
|
|
737
|
+
controlEnvelopeVersion: BOSS_CONTROL_ENVELOPE_VERSION,
|
|
738
|
+
capabilityDigest: BOSS_CAPABILITY_FEATURE_DIGEST
|
|
739
|
+
}];
|
|
740
|
+
return parseBrokerCapabilityAdvertisement({
|
|
741
|
+
baseProtocolVersion: INTERCOM_BASE_PROTOCOL_VERSION,
|
|
742
|
+
features,
|
|
743
|
+
protocolFeatureContractHash: BOSS_RUN_PROTOCOL_FEATURE_CONTRACT_HASH,
|
|
744
|
+
featureSetHash: brokerFeatureSetHash(features),
|
|
745
|
+
controlEnvelopeVersion: BOSS_CONTROL_ENVELOPE_VERSION,
|
|
746
|
+
capabilityDigest: BOSS_CAPABILITY_FEATURE_DIGEST
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
function optionalOwnDataValue(value, key) {
|
|
750
|
+
assertRecord(value);
|
|
751
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
752
|
+
if (descriptor === void 0) return void 0;
|
|
753
|
+
if (!descriptor.enumerable || !Object.hasOwn(descriptor, "value")) {
|
|
754
|
+
throw new ContractValidationError(`$.${key}`, "must be an own enumerable data property");
|
|
755
|
+
}
|
|
756
|
+
return descriptor.value;
|
|
757
|
+
}
|
|
758
|
+
function ownDataValue(value, key) {
|
|
759
|
+
const result = optionalOwnDataValue(value, key);
|
|
760
|
+
if (result === void 0) throw new ContractValidationError(`$.${key}`, "is required");
|
|
761
|
+
return result;
|
|
762
|
+
}
|
|
763
|
+
function parseBossParticipantRegistrationMetadata(value) {
|
|
764
|
+
assertBossCanonicalData(value);
|
|
765
|
+
assertRecord(value);
|
|
766
|
+
assertExactKeys(value, ["featureContract", "credential"]);
|
|
767
|
+
const featureContract = parseBossRunFeatureContract(ownDataValue(value, "featureContract"));
|
|
768
|
+
if (featureContract.baseProtocolVersion !== INTERCOM_BASE_PROTOCOL_VERSION || canonicalJson(featureContract) !== canonicalJson(BOSS_RUN_FEATURE_CONTRACT)) {
|
|
769
|
+
throw new ContractValidationError("$.featureContract", "must exactly negotiate boss-run-v1 over base protocol v3");
|
|
770
|
+
}
|
|
771
|
+
const credential = parseBossParticipantCredentialEnvelope(ownDataValue(value, "credential"));
|
|
772
|
+
if (credential.namespace !== featureContract.feature) {
|
|
773
|
+
throw new ContractValidationError("$.credential.namespace", "must match the negotiated feature namespace");
|
|
774
|
+
}
|
|
775
|
+
return { featureContract, credential };
|
|
776
|
+
}
|
|
777
|
+
function exactRegistrationKind(session, value) {
|
|
778
|
+
assertBossCanonicalData(session, "$.session");
|
|
779
|
+
assertRecord(session);
|
|
780
|
+
const boss = optionalOwnDataValue(session, "boss");
|
|
781
|
+
if (boss === void 0) {
|
|
782
|
+
if (value === void 0) return "ordinary";
|
|
783
|
+
throw new ContractValidationError("$.registrationKind", "must be absent when Boss metadata is absent");
|
|
784
|
+
}
|
|
785
|
+
if (value !== "boss") {
|
|
786
|
+
throw new ContractValidationError("$.registrationKind", "must be boss when Boss metadata is present");
|
|
787
|
+
}
|
|
788
|
+
return "boss";
|
|
789
|
+
}
|
|
790
|
+
function parseExactRegistrationFrame(value) {
|
|
791
|
+
assertBossCanonicalData(value, "$.register");
|
|
792
|
+
assertRecord(value);
|
|
793
|
+
const session = ownDataValue(value, "session");
|
|
794
|
+
assertRecord(session);
|
|
795
|
+
const registrationKind = optionalOwnDataValue(value, "registrationKind");
|
|
796
|
+
const kind = exactRegistrationKind(session, registrationKind);
|
|
797
|
+
if (kind === "ordinary") {
|
|
798
|
+
assertExactKeys(value, ["type", "protocol", "version", "session"], ["sessionId", "stateId", "access", "scopeId"]);
|
|
799
|
+
assertExactKeys(session, ORDINARY_SESSION_REGISTRATION_KEYS, OPTIONAL_SESSION_REGISTRATION_KEYS);
|
|
800
|
+
} else {
|
|
801
|
+
assertExactKeys(value, ["type", "registrationKind", "protocol", "version", "session"], ["sessionId", "stateId", "scopeId"]);
|
|
802
|
+
assertExactKeys(session, [...ORDINARY_SESSION_REGISTRATION_KEYS, "boss"], OPTIONAL_SESSION_REGISTRATION_KEYS);
|
|
803
|
+
parseBossParticipantRegistrationMetadata(ownDataValue(session, "boss"));
|
|
804
|
+
}
|
|
805
|
+
if (ownDataValue(value, "type") !== "register") {
|
|
806
|
+
throw new ContractValidationError("$.register.type", "must be register");
|
|
807
|
+
}
|
|
808
|
+
return value;
|
|
809
|
+
}
|
|
810
|
+
function parseBossParticipantBindingMetadata(value, expectedSessionId) {
|
|
811
|
+
assertBossCanonicalData(value);
|
|
812
|
+
assertRecord(value);
|
|
813
|
+
assertExactKeys(
|
|
814
|
+
value,
|
|
815
|
+
["featureContract", "binding", "brokerIdentityVerified"],
|
|
816
|
+
["assignedParticipantIds", "requestingPrincipalId", "workerIdentity", "participantState"]
|
|
817
|
+
);
|
|
818
|
+
const featureContract = parseBossRunFeatureContract(ownDataValue(value, "featureContract"));
|
|
819
|
+
if (featureContract.baseProtocolVersion !== INTERCOM_BASE_PROTOCOL_VERSION || canonicalJson(featureContract) !== canonicalJson(BOSS_RUN_FEATURE_CONTRACT)) {
|
|
820
|
+
throw new ContractValidationError("$.featureContract", "must exactly bind boss-run-v1 over base protocol v3");
|
|
821
|
+
}
|
|
822
|
+
const binding = parseBossParticipantBinding(ownDataValue(value, "binding"));
|
|
823
|
+
if (ownDataValue(value, "brokerIdentityVerified") !== true) {
|
|
824
|
+
throw new ContractValidationError("$.brokerIdentityVerified", "must be true for a broker-owned Boss binding");
|
|
825
|
+
}
|
|
826
|
+
if (expectedSessionId !== void 0 && binding.sessionId !== expectedSessionId) {
|
|
827
|
+
throw new ContractValidationError("$.binding.sessionId", "must match the registered intercom session");
|
|
828
|
+
}
|
|
829
|
+
const rawAssignedParticipantIds = optionalOwnDataValue(value, "assignedParticipantIds");
|
|
830
|
+
let assignedParticipantIds;
|
|
831
|
+
if (rawAssignedParticipantIds !== void 0) {
|
|
832
|
+
if (binding.role !== "manager" || !Array.isArray(rawAssignedParticipantIds) || rawAssignedParticipantIds.some((entry) => typeof entry !== "string" || entry.length === 0) || new Set(rawAssignedParticipantIds).size !== rawAssignedParticipantIds.length) {
|
|
833
|
+
throw new ContractValidationError("$.assignedParticipantIds", "must be a unique participant list present only for a Manager");
|
|
834
|
+
}
|
|
835
|
+
assignedParticipantIds = rawAssignedParticipantIds;
|
|
836
|
+
}
|
|
837
|
+
if (binding.role === "manager" && assignedParticipantIds === void 0) {
|
|
838
|
+
throw new ContractValidationError("$.assignedParticipantIds", "is required for a Manager policy binding");
|
|
839
|
+
}
|
|
840
|
+
const rawRequestingPrincipalId = optionalOwnDataValue(value, "requestingPrincipalId");
|
|
841
|
+
if (binding.role === "council" !== (typeof rawRequestingPrincipalId === "string" && rawRequestingPrincipalId.length > 0)) {
|
|
842
|
+
throw new ContractValidationError("$.requestingPrincipalId", "is required exactly for a Council policy binding");
|
|
843
|
+
}
|
|
844
|
+
const requestingPrincipalId = typeof rawRequestingPrincipalId === "string" ? rawRequestingPrincipalId : void 0;
|
|
845
|
+
const rawWorkerIdentity = optionalOwnDataValue(value, "workerIdentity");
|
|
846
|
+
const rawParticipantState = optionalOwnDataValue(value, "participantState");
|
|
847
|
+
if (rawWorkerIdentity === void 0 !== (rawParticipantState === void 0)) {
|
|
848
|
+
throw new ContractValidationError("$.workerIdentity", "workerIdentity and participantState must be supplied together");
|
|
849
|
+
}
|
|
850
|
+
const workerIdentity = rawWorkerIdentity === void 0 ? void 0 : parseWorkerIdentityV2(rawWorkerIdentity);
|
|
851
|
+
const participantState = rawParticipantState === void 0 ? void 0 : parseParticipantState(rawParticipantState, "$.participantState");
|
|
852
|
+
if (workerIdentity !== void 0 && (!("bossRunId" in workerIdentity) || workerIdentity.bossRunId !== binding.bossRunId || workerIdentity.participantId !== binding.participantId || workerIdentity.bindingEpoch !== binding.bindingEpoch)) throw new ContractValidationError("$.workerIdentity", "must match the broker-owned participant binding");
|
|
853
|
+
return {
|
|
854
|
+
featureContract,
|
|
855
|
+
binding,
|
|
856
|
+
brokerIdentityVerified: true,
|
|
857
|
+
...assignedParticipantIds === void 0 ? {} : { assignedParticipantIds: [...assignedParticipantIds] },
|
|
858
|
+
...requestingPrincipalId === void 0 ? {} : { requestingPrincipalId },
|
|
859
|
+
...workerIdentity === void 0 ? {} : { workerIdentity, participantState }
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
function bossPrincipal(session, metadata) {
|
|
863
|
+
const { binding } = metadata;
|
|
864
|
+
return {
|
|
865
|
+
version: BOSS_POLICY_PRINCIPAL_VERSION,
|
|
866
|
+
principalId: session.id,
|
|
867
|
+
principalClass: "boss-private",
|
|
868
|
+
state: binding.state,
|
|
869
|
+
bossRunId: binding.bossRunId,
|
|
870
|
+
participantId: binding.participantId,
|
|
871
|
+
role: binding.role,
|
|
872
|
+
bindingEpoch: binding.bindingEpoch,
|
|
873
|
+
...binding.assignedManagerParticipantId === void 0 ? {} : { assignedManagerParticipantId: binding.assignedManagerParticipantId },
|
|
874
|
+
...metadata.assignedParticipantIds === void 0 ? {} : { assignedParticipantIds: metadata.assignedParticipantIds },
|
|
875
|
+
...metadata.requestingPrincipalId === void 0 ? {} : { requestingPrincipalId: metadata.requestingPrincipalId }
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
function featurePolicyStateForSessions(sessions) {
|
|
879
|
+
const legacy = { principals: {} };
|
|
880
|
+
const boss = { principals: {} };
|
|
881
|
+
const registrations = {};
|
|
882
|
+
for (const session of sessions) {
|
|
883
|
+
if (session.boss !== void 0) {
|
|
884
|
+
const metadata = parseBossParticipantBindingMetadata(session.boss, session.id);
|
|
885
|
+
boss.principals[session.id] = bossPrincipal(session, metadata);
|
|
886
|
+
registrations[session.id] = {
|
|
887
|
+
principalId: session.id,
|
|
888
|
+
principalClass: "boss-bound",
|
|
889
|
+
state: metadata.binding.state,
|
|
890
|
+
bossRunId: metadata.binding.bossRunId,
|
|
891
|
+
participantId: metadata.binding.participantId,
|
|
892
|
+
bindingEpoch: metadata.binding.bindingEpoch,
|
|
893
|
+
featureContract: metadata.featureContract,
|
|
894
|
+
policySemanticsHash: BOSS_POLICY_SEMANTICS_HASH,
|
|
895
|
+
capabilityDigest: BOSS_CAPABILITY_FEATURE_DIGEST,
|
|
896
|
+
brokerIdentityVerified: metadata.brokerIdentityVerified
|
|
897
|
+
};
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
const principal = session.origin === "remote" ? (() => {
|
|
901
|
+
if (!session.parentSessionId || !session.rootSessionId || !session.generation) {
|
|
902
|
+
throw new Error(`Remote session ${session.id} is missing broker-owned policy metadata`);
|
|
903
|
+
}
|
|
904
|
+
return {
|
|
905
|
+
id: session.id,
|
|
906
|
+
kind: "remote",
|
|
907
|
+
state: "active",
|
|
908
|
+
generation: session.generation,
|
|
909
|
+
policy: "remote-tree",
|
|
910
|
+
parentSessionId: session.parentSessionId,
|
|
911
|
+
rootSessionId: session.rootSessionId
|
|
912
|
+
};
|
|
913
|
+
})() : {
|
|
914
|
+
id: session.id,
|
|
915
|
+
kind: "local",
|
|
916
|
+
state: "active",
|
|
917
|
+
generation: 1,
|
|
918
|
+
policy: "local-public",
|
|
919
|
+
rootSessionId: session.id
|
|
920
|
+
};
|
|
921
|
+
legacy.principals[session.id] = principal;
|
|
922
|
+
registrations[session.id] = { principalId: session.id, principalClass: "ordinary", state: "active" };
|
|
923
|
+
}
|
|
924
|
+
return { legacy, boss, registrations };
|
|
925
|
+
}
|
|
926
|
+
function authorizeBossAwareSessionAction(sessions, actorId, action, targetId, bossContext) {
|
|
927
|
+
const values = Array.from(sessions);
|
|
928
|
+
const state = featurePolicyStateForSessions(values);
|
|
929
|
+
const actor = state.registrations[actorId];
|
|
930
|
+
const target = state.registrations[targetId];
|
|
931
|
+
return authorizeFeatureAware(state, {
|
|
932
|
+
actorId,
|
|
933
|
+
action,
|
|
934
|
+
targetId,
|
|
935
|
+
...actor?.principalClass === "ordinary" && target?.principalClass === "ordinary" ? {
|
|
936
|
+
legacyContext: {
|
|
937
|
+
actorGeneration: state.legacy.principals[actorId]?.generation,
|
|
938
|
+
targetGeneration: state.legacy.principals[targetId]?.generation
|
|
939
|
+
}
|
|
940
|
+
} : bossContext === void 0 ? {} : { bossContext }
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
var BOSS_CONTROL_KIND_BY_TYPE = {
|
|
944
|
+
"boss.assignment.created": "assignment_request",
|
|
945
|
+
"boss.assignment.accepted": "assignment_response",
|
|
946
|
+
"boss.assignment.checkpoint": "assignment_response",
|
|
947
|
+
"boss.assignment.submitted": "assignment_response",
|
|
948
|
+
"boss.assignment.rejected": "assignment_response",
|
|
949
|
+
"boss.assignment.cancelled": "lifecycle",
|
|
950
|
+
"boss.staffing.requested": "staffing",
|
|
951
|
+
"boss.staffing.resolved": "staffing",
|
|
952
|
+
"boss.review.requested": "review_request",
|
|
953
|
+
"boss.review.submitted": "review_result",
|
|
954
|
+
"boss.council.requested": "review_request",
|
|
955
|
+
"boss.council.submitted": "review_result",
|
|
956
|
+
"boss.proof.submitted": "proof",
|
|
957
|
+
"boss.worker.health": "health",
|
|
958
|
+
"boss.worker.blocked": "health",
|
|
959
|
+
"boss.worker.failed": "health",
|
|
960
|
+
"boss.worker.notice": "lifecycle",
|
|
961
|
+
"boss.worker.notice_delivery_failed": "lifecycle",
|
|
962
|
+
"boss.decision.required": "decision"
|
|
963
|
+
};
|
|
964
|
+
function bossControlKind(envelopeValue) {
|
|
965
|
+
assertBossCanonicalData(envelopeValue);
|
|
966
|
+
const envelope = parseBossControlEnvelope(envelopeValue);
|
|
967
|
+
return { envelope, controlKind: BOSS_CONTROL_KIND_BY_TYPE[envelope.type] };
|
|
968
|
+
}
|
|
969
|
+
function hasAuthoritativeBossControlCorrelation() {
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
function exactBossSessionTarget(sessions, requestedSessionId) {
|
|
973
|
+
const target = sessions.get(requestedSessionId);
|
|
974
|
+
return target?.info.id === requestedSessionId ? target : void 0;
|
|
975
|
+
}
|
|
976
|
+
function assertBossControlSender(session, envelopeValue) {
|
|
977
|
+
const { envelope } = bossControlKind(envelopeValue);
|
|
978
|
+
if (session.boss === void 0) {
|
|
979
|
+
throw new ContractValidationError("$.session", "ordinary sessions cannot originate Boss control envelopes");
|
|
980
|
+
}
|
|
981
|
+
const { binding } = parseBossParticipantBindingMetadata(session.boss, session.id);
|
|
982
|
+
if (binding.state !== "active" || envelope.bossRunId !== binding.bossRunId || envelope.participantId !== binding.participantId || envelope.bindingEpoch !== binding.bindingEpoch) {
|
|
983
|
+
throw new ContractValidationError("$.envelope", "does not match the active broker-owned participant binding");
|
|
984
|
+
}
|
|
985
|
+
return envelope;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// broker/authorization.ts
|
|
989
|
+
function authorizeSessionAction(sessions, actorId, action, targetId, bossContext) {
|
|
990
|
+
return authorizeBossAwareSessionAction(sessions, actorId, action, targetId, bossContext);
|
|
991
|
+
}
|
|
992
|
+
function visibleSessions(sessions, actorId) {
|
|
993
|
+
const values = Array.from(sessions);
|
|
994
|
+
return values.filter((target) => authorizeSessionAction(values, actorId, "discover", target.id).allowed);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// broker/audit.ts
|
|
998
|
+
import { closeSync as closeSync3, fsyncSync as fsyncSync2, openSync as openSync3, writeSync } from "fs";
|
|
999
|
+
var BROKER_AUDIT_VERSION = 1;
|
|
1000
|
+
var BrokerAuditLog = class {
|
|
1001
|
+
constructor(path, now = Date.now) {
|
|
1002
|
+
this.path = path;
|
|
1003
|
+
this.now = now;
|
|
1004
|
+
}
|
|
1005
|
+
path;
|
|
1006
|
+
now;
|
|
1007
|
+
record(entry) {
|
|
1008
|
+
const line = `${JSON.stringify({
|
|
1009
|
+
version: BROKER_AUDIT_VERSION,
|
|
1010
|
+
timestamp: this.now(),
|
|
1011
|
+
...entry
|
|
1012
|
+
})}
|
|
1013
|
+
`;
|
|
1014
|
+
const descriptor = openSync3(this.path, "a", INTERCOM_RUNTIME_FILE_MODE);
|
|
1015
|
+
try {
|
|
1016
|
+
writeSync(descriptor, line, void 0, "utf8");
|
|
1017
|
+
fsyncSync2(descriptor);
|
|
1018
|
+
} finally {
|
|
1019
|
+
closeSync3(descriptor);
|
|
1020
|
+
}
|
|
1021
|
+
restrictIntercomRuntimeFile(this.path);
|
|
1022
|
+
}
|
|
1023
|
+
tryRecord(entry) {
|
|
1024
|
+
try {
|
|
1025
|
+
this.record(entry);
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
console.error("Failed to append Agent Intercom broker audit event:", error);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
// broker/boss-control-ledger.ts
|
|
1033
|
+
import { existsSync as existsSync2, readFileSync as readFileSync4, renameSync as renameSync2 } from "node:fs";
|
|
1034
|
+
import { canonicalJson as canonicalJson2 } from "@ctliz/agent-intercom-core/canonical";
|
|
1035
|
+
var BOSS_CONTROL_LEDGER_VERSION = 3;
|
|
1036
|
+
var EXPIRING_BOSS_CONTROL_LEDGER_VERSION = 2;
|
|
1037
|
+
var MAX_BOSS_CONTROL_RESULTS = 2048;
|
|
1038
|
+
var BOSS_CONTROL_FAILURE_CODES = /* @__PURE__ */ new Set([
|
|
1039
|
+
"INVALID_CONTROL",
|
|
1040
|
+
"IDEMPOTENCY_CONFLICT",
|
|
1041
|
+
"SESSION_NOT_FOUND",
|
|
1042
|
+
"POLICY_DENIED",
|
|
1043
|
+
"RECIPIENT_DISCONNECTED",
|
|
1044
|
+
"DELIVERY_TIMEOUT"
|
|
1045
|
+
]);
|
|
1046
|
+
function exactStringKeys(value, required, optional = []) {
|
|
1047
|
+
const keys = Reflect.ownKeys(value);
|
|
1048
|
+
const permitted = /* @__PURE__ */ new Set([...required, ...optional]);
|
|
1049
|
+
return required.every((key) => Object.hasOwn(value, key)) && keys.every((key) => typeof key === "string" && permitted.has(key));
|
|
1050
|
+
}
|
|
1051
|
+
function parseBossControlResult(value) {
|
|
1052
|
+
assertBossCanonicalData(value, "$.bossControlResult");
|
|
1053
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1054
|
+
throw new Error("Boss control result must be an exact plain object");
|
|
1055
|
+
}
|
|
1056
|
+
const result = value;
|
|
1057
|
+
const base = typeof result.requestId === "string" && result.requestId.length > 0 && result.messageId === result.requestId && typeof result.idempotencyKey === "string" && result.idempotencyKey.length > 0;
|
|
1058
|
+
if (!base || result.type !== "boss_control_result") throw new Error("Invalid Boss control result binding");
|
|
1059
|
+
if (result.status === "delivered" && result.delivered === true && typeof result.deliveryId === "string" && result.deliveryId.length > 0 && exactStringKeys(result, ["type", "requestId", "messageId", "idempotencyKey", "status", "delivered", "deliveryId"])) return result;
|
|
1060
|
+
if (result.status === "rejected" && result.delivered === false && typeof result.code === "string" && BOSS_CONTROL_FAILURE_CODES.has(result.code) && typeof result.reason === "string" && result.reason.length > 0 && (!Object.hasOwn(result, "deliveryId") || typeof result.deliveryId === "string" && result.deliveryId.length > 0) && exactStringKeys(
|
|
1061
|
+
result,
|
|
1062
|
+
["type", "requestId", "messageId", "idempotencyKey", "status", "delivered", "code", "reason"],
|
|
1063
|
+
["deliveryId"]
|
|
1064
|
+
)) return result;
|
|
1065
|
+
throw new Error("Invalid Boss control result discriminant");
|
|
1066
|
+
}
|
|
1067
|
+
function rebindBossControlResult(resultValue, messageId) {
|
|
1068
|
+
if (typeof messageId !== "string" || messageId.length === 0) throw new Error("Replay messageId is required");
|
|
1069
|
+
const result = parseBossControlResult(resultValue);
|
|
1070
|
+
return parseBossControlResult({ ...result, requestId: messageId, messageId });
|
|
1071
|
+
}
|
|
1072
|
+
function bossControlReplayFrames(resultValue, messageId) {
|
|
1073
|
+
const result = rebindBossControlResult(resultValue, messageId);
|
|
1074
|
+
if (result.deliveryId === void 0) return [result];
|
|
1075
|
+
return [{
|
|
1076
|
+
type: "boss_control_ack",
|
|
1077
|
+
requestId: messageId,
|
|
1078
|
+
messageId,
|
|
1079
|
+
idempotencyKey: result.idempotencyKey,
|
|
1080
|
+
status: "accepted",
|
|
1081
|
+
deliveryId: result.deliveryId
|
|
1082
|
+
}, result];
|
|
1083
|
+
}
|
|
1084
|
+
function bossControlAcceptedRecoveryFrames(resultValue) {
|
|
1085
|
+
const result = parseBossControlResult(resultValue);
|
|
1086
|
+
if (result.status !== "rejected" || result.deliveryId === void 0) {
|
|
1087
|
+
throw new Error("Accepted Boss recovery requires a delivery-bound rejected result");
|
|
1088
|
+
}
|
|
1089
|
+
return [{
|
|
1090
|
+
type: "boss_control_ack",
|
|
1091
|
+
requestId: result.requestId,
|
|
1092
|
+
messageId: result.messageId,
|
|
1093
|
+
idempotencyKey: result.idempotencyKey,
|
|
1094
|
+
status: "accepted",
|
|
1095
|
+
deliveryId: result.deliveryId
|
|
1096
|
+
}, result];
|
|
1097
|
+
}
|
|
1098
|
+
function parseHash(value, field) {
|
|
1099
|
+
if (typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value)) throw new Error(`Invalid Boss ledger ${field}`);
|
|
1100
|
+
return value;
|
|
1101
|
+
}
|
|
1102
|
+
function parseEntry(value, version) {
|
|
1103
|
+
assertBossCanonicalData(value, "$.entries[]");
|
|
1104
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("Invalid Boss ledger entry");
|
|
1105
|
+
const entry = value;
|
|
1106
|
+
const legacyExpiry = version === EXPIRING_BOSS_CONTROL_LEDGER_VERSION;
|
|
1107
|
+
if (legacyExpiry && (typeof entry.expiresAt !== "number" || !Number.isSafeInteger(entry.expiresAt))) {
|
|
1108
|
+
throw new Error("Invalid Boss ledger expiry");
|
|
1109
|
+
}
|
|
1110
|
+
const base = {
|
|
1111
|
+
scope: parseHash(entry.scope, "scope"),
|
|
1112
|
+
fingerprint: parseHash(entry.fingerprint, "fingerprint")
|
|
1113
|
+
};
|
|
1114
|
+
const baseKeys = legacyExpiry ? ["scope", "fingerprint", "expiresAt", "state"] : ["scope", "fingerprint", "state"];
|
|
1115
|
+
if (entry.state === "accepted" && exactStringKeys(entry, [...baseKeys, "deliveryId"]) && typeof entry.deliveryId === "string" && entry.deliveryId.length > 0) return { ...base, state: "accepted", deliveryId: entry.deliveryId };
|
|
1116
|
+
if (entry.state === "terminal" && exactStringKeys(entry, [...baseKeys, "result"])) {
|
|
1117
|
+
return { ...base, state: "terminal", result: parseBossControlResult(entry.result) };
|
|
1118
|
+
}
|
|
1119
|
+
throw new Error("Invalid Boss ledger state discriminant");
|
|
1120
|
+
}
|
|
1121
|
+
var BossControlResultLedger = class {
|
|
1122
|
+
constructor(path, now = Date.now) {
|
|
1123
|
+
this.path = path;
|
|
1124
|
+
this.now = now;
|
|
1125
|
+
const loaded = this.load();
|
|
1126
|
+
this.state = loaded.state;
|
|
1127
|
+
if (loaded.migrated) this.persist();
|
|
1128
|
+
}
|
|
1129
|
+
path;
|
|
1130
|
+
now;
|
|
1131
|
+
state;
|
|
1132
|
+
lookup(scope, fingerprint) {
|
|
1133
|
+
const entry = this.state.entries.find((candidate) => candidate.scope === scope);
|
|
1134
|
+
if (!entry) return { status: "miss" };
|
|
1135
|
+
if (entry.fingerprint !== fingerprint) return { status: "conflict" };
|
|
1136
|
+
return entry.state === "accepted" ? { status: "accepted", deliveryId: entry.deliveryId } : { status: "replay", result: structuredClone(entry.result) };
|
|
1137
|
+
}
|
|
1138
|
+
recordAccepted(scope, fingerprint, deliveryId) {
|
|
1139
|
+
if (!/^[a-f0-9]{64}$/.test(scope) || !/^[a-f0-9]{64}$/.test(fingerprint) || !deliveryId) {
|
|
1140
|
+
throw new Error("Invalid Boss accepted-state binding");
|
|
1141
|
+
}
|
|
1142
|
+
const existing = this.state.entries.find((entry) => entry.scope === scope);
|
|
1143
|
+
if (existing) {
|
|
1144
|
+
if (existing.fingerprint !== fingerprint || existing.state !== "accepted" || existing.deliveryId !== deliveryId) {
|
|
1145
|
+
throw new Error("Boss idempotency scope is already bound to a different canonical state");
|
|
1146
|
+
}
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
this.reserveCapacity();
|
|
1150
|
+
this.state.entries.push({ scope, fingerprint, state: "accepted", deliveryId });
|
|
1151
|
+
this.persist();
|
|
1152
|
+
}
|
|
1153
|
+
recordTerminal(scope, fingerprint, resultValue) {
|
|
1154
|
+
const result = parseBossControlResult(resultValue);
|
|
1155
|
+
canonicalJson2(result);
|
|
1156
|
+
const existing = this.state.entries.find((entry) => entry.scope === scope);
|
|
1157
|
+
if (existing?.fingerprint !== void 0 && existing.fingerprint !== fingerprint) {
|
|
1158
|
+
throw new Error("Boss idempotency scope is already bound to a different canonical request");
|
|
1159
|
+
}
|
|
1160
|
+
if (existing?.state === "terminal") {
|
|
1161
|
+
const existingStable = { ...existing.result, requestId: "<caller>", messageId: "<caller>" };
|
|
1162
|
+
const resultStable = { ...result, requestId: "<caller>", messageId: "<caller>" };
|
|
1163
|
+
if (canonicalJson2(existingStable) !== canonicalJson2(resultStable)) {
|
|
1164
|
+
throw new Error("Boss idempotency scope is already bound to a different canonical result");
|
|
1165
|
+
}
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
if (result.status === "delivered" || result.deliveryId !== void 0) {
|
|
1169
|
+
if (existing?.state !== "accepted" || existing.deliveryId !== result.deliveryId) {
|
|
1170
|
+
throw new Error("Boss terminal delivery requires the matching durable accepted state");
|
|
1171
|
+
}
|
|
1172
|
+
} else if (existing?.state === "accepted") {
|
|
1173
|
+
throw new Error("A terminal result after acceptance must carry the accepted deliveryId");
|
|
1174
|
+
}
|
|
1175
|
+
const terminal = {
|
|
1176
|
+
scope,
|
|
1177
|
+
fingerprint,
|
|
1178
|
+
state: "terminal",
|
|
1179
|
+
result
|
|
1180
|
+
};
|
|
1181
|
+
if (existing) this.state.entries[this.state.entries.indexOf(existing)] = terminal;
|
|
1182
|
+
else {
|
|
1183
|
+
this.reserveCapacity();
|
|
1184
|
+
this.state.entries.push(terminal);
|
|
1185
|
+
}
|
|
1186
|
+
this.persist();
|
|
1187
|
+
}
|
|
1188
|
+
reserveCapacity() {
|
|
1189
|
+
if (this.state.entries.length >= MAX_BOSS_CONTROL_RESULTS) {
|
|
1190
|
+
throw new Error("Durable Boss control ledger is full");
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
load() {
|
|
1194
|
+
if (!existsSync2(this.path)) {
|
|
1195
|
+
return { state: { version: BOSS_CONTROL_LEDGER_VERSION, entries: [] }, migrated: false };
|
|
1196
|
+
}
|
|
1197
|
+
try {
|
|
1198
|
+
const parsed = JSON.parse(readFileSync4(this.path, "utf8"));
|
|
1199
|
+
assertBossCanonicalData(parsed, "$.bossControlLedger");
|
|
1200
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("expected object");
|
|
1201
|
+
const state = parsed;
|
|
1202
|
+
if (!exactStringKeys(state, ["version", "entries"]) || state.version !== BOSS_CONTROL_LEDGER_VERSION && state.version !== EXPIRING_BOSS_CONTROL_LEDGER_VERSION || !Array.isArray(state.entries)) throw new Error("invalid ledger state");
|
|
1203
|
+
return {
|
|
1204
|
+
state: {
|
|
1205
|
+
version: BOSS_CONTROL_LEDGER_VERSION,
|
|
1206
|
+
entries: state.entries.map((entry) => parseEntry(entry, state.version))
|
|
1207
|
+
},
|
|
1208
|
+
migrated: state.version === EXPIRING_BOSS_CONTROL_LEDGER_VERSION
|
|
1209
|
+
};
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
const corruptPath = `${this.path}.corrupt-${this.now()}`;
|
|
1212
|
+
renameSync2(this.path, corruptPath);
|
|
1213
|
+
restrictIntercomRuntimeFile(corruptPath);
|
|
1214
|
+
throw new Error(`Boss control ledger was corrupt and quarantined at ${corruptPath}`, { cause: error });
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
persist() {
|
|
1218
|
+
writeDurableJson(this.path, this.state);
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1222
|
+
// broker/broker.ts
|
|
1223
|
+
var INTERCOM_DIR = getIntercomDirPath();
|
|
1224
|
+
var LISTEN_TARGET = getBrokerListenTarget();
|
|
1225
|
+
var REMOTE_LISTEN_TARGET = getRemoteGatewaySocketPath();
|
|
1226
|
+
var PID_PATH = join2(INTERCOM_DIR, "broker.pid");
|
|
1227
|
+
var OWNER_PATH = join2(INTERCOM_DIR, "broker.owner");
|
|
1228
|
+
var PORT_PATH = getBrokerPortFilePath(INTERCOM_DIR);
|
|
1229
|
+
var ASK_STATE_PATH = getBrokerAskStateFilePath(INTERCOM_DIR);
|
|
1230
|
+
var ACCESS_STATE_PATH = getBrokerAccessStateFilePath(INTERCOM_DIR);
|
|
1231
|
+
var ADMIN_CREDENTIAL_PATH = getBrokerAdminCredentialFilePath(INTERCOM_DIR);
|
|
1232
|
+
var AUDIT_PATH = getBrokerAuditFilePath(INTERCOM_DIR);
|
|
1233
|
+
var BOSS_CONTROL_LEDGER_PATH = join2(INTERCOM_DIR, "boss-control-results.json");
|
|
1234
|
+
var BROKER_STATE_ID = randomUUID3();
|
|
1235
|
+
var MAX_SESSIONS = 128;
|
|
1236
|
+
var MAX_UNREGISTERED_CONNECTIONS = 32;
|
|
1237
|
+
var REGISTRATION_TIMEOUT_MS = 1e3;
|
|
1238
|
+
var RATE_LIMIT_CAPACITY = 240;
|
|
1239
|
+
var RATE_LIMIT_REFILL_PER_SECOND = 120;
|
|
1240
|
+
var REMOTE_RATE_LIMIT_CAPACITY = 60;
|
|
1241
|
+
var REMOTE_RATE_LIMIT_REFILL_PER_SECOND = 30;
|
|
1242
|
+
var REMOTE_EXPIRY_SWEEP_MS = Math.max(50, Number.parseInt(process.env.PI_INTERCOM_REMOTE_EXPIRY_SWEEP_MS ?? "1000", 10) || 1e3);
|
|
1243
|
+
var PRESENCE_HEARTBEAT_MS = 1e3;
|
|
1244
|
+
var DELIVERY_ACK_TIMEOUT_MS = 8e3;
|
|
1245
|
+
var RECENT_DELIVERY_TTL_MS = 10 * 60 * 1e3;
|
|
1246
|
+
var MAX_PENDING_DELIVERIES = 1024;
|
|
1247
|
+
var MAX_PENDING_DELIVERIES_PER_SESSION = 64;
|
|
1248
|
+
var MAX_PENDING_ASKS_PER_SESSION = 64;
|
|
1249
|
+
var RATE_LIMIT_BYTES_PER_TOKEN = 8 * 1024;
|
|
1250
|
+
var MAX_MESSAGE_TEXT_BYTES = 256 * 1024;
|
|
1251
|
+
var MAX_ATTACHMENT_CONTENT_BYTES = 512 * 1024;
|
|
1252
|
+
var MAX_ATTACHMENTS = 16;
|
|
1253
|
+
var MAX_MESSAGE_ID_LENGTH = 256;
|
|
1254
|
+
var MAX_TARGET_LENGTH = 512;
|
|
1255
|
+
var MAX_SESSION_NAME_LENGTH = 256;
|
|
1256
|
+
var MAX_SESSION_CWD_LENGTH = 4096;
|
|
1257
|
+
var MAX_SESSION_MODEL_LENGTH = 512;
|
|
1258
|
+
var MAX_SESSION_STATUS_LENGTH = 512;
|
|
1259
|
+
var MAX_RUNTIME_INSTANCE_ID_LENGTH = 256;
|
|
1260
|
+
function isAttachment(value) {
|
|
1261
|
+
if (typeof value !== "object" || value === null) {
|
|
1262
|
+
return false;
|
|
1263
|
+
}
|
|
1264
|
+
const attachment = value;
|
|
1265
|
+
if (attachment.type !== "file" && attachment.type !== "snippet" && attachment.type !== "context") {
|
|
1266
|
+
return false;
|
|
1267
|
+
}
|
|
1268
|
+
if (typeof attachment.name !== "string" || attachment.name.length > 256 || typeof attachment.content !== "string" || Buffer.byteLength(attachment.content, "utf-8") > MAX_ATTACHMENT_CONTENT_BYTES) {
|
|
1269
|
+
return false;
|
|
1270
|
+
}
|
|
1271
|
+
return attachment.language === void 0 || typeof attachment.language === "string";
|
|
1272
|
+
}
|
|
1273
|
+
function isMessage(value) {
|
|
1274
|
+
if (typeof value !== "object" || value === null) {
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
const message = value;
|
|
1278
|
+
if (typeof message.id !== "string" || message.id.length === 0 || message.id.length > MAX_MESSAGE_ID_LENGTH || typeof message.timestamp !== "number" || !Number.isFinite(message.timestamp)) {
|
|
1279
|
+
return false;
|
|
1280
|
+
}
|
|
1281
|
+
if (message.replyTo !== void 0 && (typeof message.replyTo !== "string" || message.replyTo.length === 0 || message.replyTo.length > MAX_MESSAGE_ID_LENGTH)) {
|
|
1282
|
+
return false;
|
|
1283
|
+
}
|
|
1284
|
+
if (message.expectsReply !== void 0 && typeof message.expectsReply !== "boolean") {
|
|
1285
|
+
return false;
|
|
1286
|
+
}
|
|
1287
|
+
if (typeof message.content !== "object" || message.content === null) {
|
|
1288
|
+
return false;
|
|
1289
|
+
}
|
|
1290
|
+
const content = message.content;
|
|
1291
|
+
if (typeof content.text !== "string" || Buffer.byteLength(content.text, "utf-8") > MAX_MESSAGE_TEXT_BYTES) {
|
|
1292
|
+
return false;
|
|
1293
|
+
}
|
|
1294
|
+
return content.attachments === void 0 || Array.isArray(content.attachments) && content.attachments.length <= MAX_ATTACHMENTS && content.attachments.every(isAttachment);
|
|
1295
|
+
}
|
|
1296
|
+
function isSessionId(value) {
|
|
1297
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
1298
|
+
}
|
|
1299
|
+
function isSessionRegistration(value) {
|
|
1300
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1301
|
+
return false;
|
|
1302
|
+
}
|
|
1303
|
+
const session = value;
|
|
1304
|
+
if (typeof session.cwd !== "string" || session.cwd.length === 0 || session.cwd.length > MAX_SESSION_CWD_LENGTH || typeof session.model !== "string" || session.model.length === 0 || session.model.length > MAX_SESSION_MODEL_LENGTH || typeof session.pid !== "number" || !Number.isFinite(session.pid) || typeof session.startedAt !== "number" || !Number.isFinite(session.startedAt) || typeof session.lastActivity !== "number" || !Number.isFinite(session.lastActivity)) {
|
|
1305
|
+
return false;
|
|
1306
|
+
}
|
|
1307
|
+
if (session.boss !== void 0) {
|
|
1308
|
+
try {
|
|
1309
|
+
parseBossParticipantRegistrationMetadata(session.boss);
|
|
1310
|
+
} catch {
|
|
1311
|
+
return false;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
if (session.name !== void 0 && (typeof session.name !== "string" || session.name.length > MAX_SESSION_NAME_LENGTH)) {
|
|
1315
|
+
return false;
|
|
1316
|
+
}
|
|
1317
|
+
if (session.runtimeInstanceId !== void 0 && (typeof session.runtimeInstanceId !== "string" || session.runtimeInstanceId.length === 0 || session.runtimeInstanceId.length > MAX_RUNTIME_INSTANCE_ID_LENGTH)) {
|
|
1318
|
+
return false;
|
|
1319
|
+
}
|
|
1320
|
+
return session.status === void 0 || typeof session.status === "string" && session.status.length <= MAX_SESSION_STATUS_LENGTH;
|
|
1321
|
+
}
|
|
1322
|
+
function isSameLocalRuntime(previous, registration) {
|
|
1323
|
+
if (previous.runtimeInstanceId !== void 0 || registration.runtimeInstanceId !== void 0) {
|
|
1324
|
+
return previous.runtimeInstanceId !== void 0 && previous.runtimeInstanceId === registration.runtimeInstanceId;
|
|
1325
|
+
}
|
|
1326
|
+
return previous.info.pid === registration.pid && previous.info.startedAt === registration.startedAt;
|
|
1327
|
+
}
|
|
1328
|
+
var IntercomBroker = class {
|
|
1329
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1330
|
+
askEdges = /* @__PURE__ */ new Map();
|
|
1331
|
+
pendingDeliveries = /* @__PURE__ */ new Map();
|
|
1332
|
+
pendingDeliveryKeys = /* @__PURE__ */ new Map();
|
|
1333
|
+
recentDeliveries = /* @__PURE__ */ new Map();
|
|
1334
|
+
pendingBossControls = /* @__PURE__ */ new Map();
|
|
1335
|
+
pendingBossControlKeys = /* @__PURE__ */ new Map();
|
|
1336
|
+
connections = /* @__PURE__ */ new Set();
|
|
1337
|
+
unregisteredConnections = /* @__PURE__ */ new Set();
|
|
1338
|
+
server;
|
|
1339
|
+
remoteServer = null;
|
|
1340
|
+
shutdownTimer = null;
|
|
1341
|
+
expiryTimer = null;
|
|
1342
|
+
askTimeoutMs = getAskTimeoutMs();
|
|
1343
|
+
accessRegistry;
|
|
1344
|
+
audit;
|
|
1345
|
+
bossControlLedger;
|
|
1346
|
+
constructor() {
|
|
1347
|
+
ensureIntercomRuntimeDir(INTERCOM_DIR);
|
|
1348
|
+
acquireBrokerOwnership(OWNER_PATH);
|
|
1349
|
+
this.accessRegistry = new RemoteAccessRegistry(ACCESS_STATE_PATH);
|
|
1350
|
+
this.audit = new BrokerAuditLog(AUDIT_PATH);
|
|
1351
|
+
this.bossControlLedger = new BossControlResultLedger(BOSS_CONTROL_LEDGER_PATH);
|
|
1352
|
+
this.accessRegistry.ensureAdminCredential(ADMIN_CREDENTIAL_PATH);
|
|
1353
|
+
this.loadAskEdges();
|
|
1354
|
+
if (typeof LISTEN_TARGET === "string" && process.platform !== "win32") {
|
|
1355
|
+
for (const socketPath of [LISTEN_TARGET, REMOTE_LISTEN_TARGET]) {
|
|
1356
|
+
try {
|
|
1357
|
+
unlinkSync2(socketPath);
|
|
1358
|
+
} catch {
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
this.server = net.createServer((socket) => this.handleConnection(socket, "local"));
|
|
1363
|
+
if (process.platform !== "win32" && typeof LISTEN_TARGET === "string") {
|
|
1364
|
+
this.remoteServer = net.createServer((socket) => this.handleConnection(socket, "remote"));
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
start() {
|
|
1368
|
+
let localListening = false;
|
|
1369
|
+
let remoteListening = this.remoteServer === null;
|
|
1370
|
+
const announceWhenReady = () => {
|
|
1371
|
+
if (!localListening || !remoteListening) return;
|
|
1372
|
+
writeFileSync3(PID_PATH, String(process.pid), { mode: INTERCOM_RUNTIME_FILE_MODE });
|
|
1373
|
+
restrictIntercomRuntimeFile(PID_PATH);
|
|
1374
|
+
console.log(`Intercom broker started (pid: ${process.pid}, remote-access-v1)`);
|
|
1375
|
+
};
|
|
1376
|
+
const onLocalListening = () => {
|
|
1377
|
+
if (typeof LISTEN_TARGET === "string") {
|
|
1378
|
+
restrictIntercomRuntimeFile(LISTEN_TARGET);
|
|
1379
|
+
} else {
|
|
1380
|
+
const address = this.server.address();
|
|
1381
|
+
if (!address || typeof address === "string") {
|
|
1382
|
+
throw new Error("Intercom TCP broker started without a TCP address");
|
|
1383
|
+
}
|
|
1384
|
+
const endpoint = {
|
|
1385
|
+
transport: "tcp",
|
|
1386
|
+
host: LISTEN_TARGET.host,
|
|
1387
|
+
port: address.port,
|
|
1388
|
+
stateId: BROKER_STATE_ID
|
|
1389
|
+
};
|
|
1390
|
+
writeFileSync3(PORT_PATH, `${JSON.stringify(endpoint)}
|
|
1391
|
+
`, { mode: INTERCOM_RUNTIME_FILE_MODE });
|
|
1392
|
+
restrictIntercomRuntimeFile(PORT_PATH);
|
|
1393
|
+
}
|
|
1394
|
+
localListening = true;
|
|
1395
|
+
announceWhenReady();
|
|
1396
|
+
};
|
|
1397
|
+
if (typeof LISTEN_TARGET === "string") {
|
|
1398
|
+
this.server.listen(LISTEN_TARGET, onLocalListening);
|
|
1399
|
+
} else {
|
|
1400
|
+
this.server.listen({ host: LISTEN_TARGET.host, port: LISTEN_TARGET.port }, onLocalListening);
|
|
1401
|
+
}
|
|
1402
|
+
if (this.remoteServer) {
|
|
1403
|
+
this.remoteServer.listen(REMOTE_LISTEN_TARGET, () => {
|
|
1404
|
+
restrictIntercomRuntimeFile(REMOTE_LISTEN_TARGET);
|
|
1405
|
+
remoteListening = true;
|
|
1406
|
+
announceWhenReady();
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
this.expiryTimer = setInterval(() => this.reconcileExpiredPrincipals(), REMOTE_EXPIRY_SWEEP_MS);
|
|
1410
|
+
this.expiryTimer.unref?.();
|
|
1411
|
+
process.on("SIGTERM", () => this.shutdown());
|
|
1412
|
+
process.on("SIGINT", () => this.shutdown());
|
|
1413
|
+
}
|
|
1414
|
+
handleConnection(socket, origin) {
|
|
1415
|
+
this.connections.add(socket);
|
|
1416
|
+
let sessionId = null;
|
|
1417
|
+
let registrationTimeout = null;
|
|
1418
|
+
const armRegistrationTimeout = () => {
|
|
1419
|
+
if (registrationTimeout) {
|
|
1420
|
+
clearTimeout(registrationTimeout);
|
|
1421
|
+
}
|
|
1422
|
+
this.unregisteredConnections.delete(socket);
|
|
1423
|
+
this.unregisteredConnections.add(socket);
|
|
1424
|
+
this.evictOldestUnregisteredConnections(socket);
|
|
1425
|
+
registrationTimeout = setTimeout(() => {
|
|
1426
|
+
if (!sessionId) {
|
|
1427
|
+
socket.destroy();
|
|
1428
|
+
}
|
|
1429
|
+
}, REGISTRATION_TIMEOUT_MS);
|
|
1430
|
+
registrationTimeout.unref?.();
|
|
1431
|
+
};
|
|
1432
|
+
const clearRegistrationTimeout = () => {
|
|
1433
|
+
if (registrationTimeout) {
|
|
1434
|
+
clearTimeout(registrationTimeout);
|
|
1435
|
+
registrationTimeout = null;
|
|
1436
|
+
}
|
|
1437
|
+
this.unregisteredConnections.delete(socket);
|
|
1438
|
+
};
|
|
1439
|
+
armRegistrationTimeout();
|
|
1440
|
+
const connection = {
|
|
1441
|
+
socket,
|
|
1442
|
+
origin,
|
|
1443
|
+
tokens: origin === "remote" ? REMOTE_RATE_LIMIT_CAPACITY : RATE_LIMIT_CAPACITY,
|
|
1444
|
+
refillPerSecond: origin === "remote" ? REMOTE_RATE_LIMIT_REFILL_PER_SECOND : RATE_LIMIT_REFILL_PER_SECOND,
|
|
1445
|
+
lastRefillAt: Date.now()
|
|
1446
|
+
};
|
|
1447
|
+
const reader = createMessageReader((msg) => {
|
|
1448
|
+
const byteCost = Math.max(1, Math.ceil(Buffer.byteLength(JSON.stringify(msg), "utf-8") / RATE_LIMIT_BYTES_PER_TOKEN));
|
|
1449
|
+
if (!this.consumeToken(connection, byteCost)) {
|
|
1450
|
+
this.sendError(socket, "RATE_LIMITED", "Intercom broker rate limit exceeded");
|
|
1451
|
+
socket.destroy(new Error("Intercom broker rate limit exceeded"));
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
try {
|
|
1455
|
+
this.handleMessage(socket, origin, msg, sessionId, (id) => {
|
|
1456
|
+
sessionId = id;
|
|
1457
|
+
if (id) {
|
|
1458
|
+
clearRegistrationTimeout();
|
|
1459
|
+
} else {
|
|
1460
|
+
armRegistrationTimeout();
|
|
1461
|
+
}
|
|
1462
|
+
});
|
|
1463
|
+
} catch (error) {
|
|
1464
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1465
|
+
if (reason === "Invalid intercom TCP endpoint credentials") {
|
|
1466
|
+
socket.destroy();
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
this.sendError(socket, "INVALID_REQUEST", reason);
|
|
1470
|
+
socket.end();
|
|
1471
|
+
}
|
|
1472
|
+
}, (error) => {
|
|
1473
|
+
socket.destroy(error);
|
|
1474
|
+
});
|
|
1475
|
+
socket.on("data", reader);
|
|
1476
|
+
socket.on("close", () => {
|
|
1477
|
+
clearRegistrationTimeout();
|
|
1478
|
+
this.connections.delete(socket);
|
|
1479
|
+
if (sessionId) {
|
|
1480
|
+
const existing = this.sessions.get(sessionId);
|
|
1481
|
+
if (existing?.socket === socket) {
|
|
1482
|
+
if (existing.info.origin === "remote") {
|
|
1483
|
+
this.audit.tryRecord({
|
|
1484
|
+
event: "remote_disconnect",
|
|
1485
|
+
outcome: "observed",
|
|
1486
|
+
actorId: sessionId,
|
|
1487
|
+
remoteHostId: existing.info.remoteHostId,
|
|
1488
|
+
generation: existing.info.generation,
|
|
1489
|
+
reason: "SOCKET_CLOSED"
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
this.broadcastLifecycle({ type: "session_left", sessionId }, existing, sessionId);
|
|
1493
|
+
this.sessions.delete(sessionId);
|
|
1494
|
+
this.clearPendingDeliveriesForSession(sessionId, socket);
|
|
1495
|
+
this.deferAskEdgesForSession(sessionId);
|
|
1496
|
+
this.scheduleShutdownCheck();
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
});
|
|
1500
|
+
socket.on("error", (error) => {
|
|
1501
|
+
console.error("Socket error:", error);
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
evictOldestUnregisteredConnections(currentSocket) {
|
|
1505
|
+
while (this.unregisteredConnections.size > MAX_UNREGISTERED_CONNECTIONS) {
|
|
1506
|
+
const [oldest] = this.unregisteredConnections;
|
|
1507
|
+
if (!oldest) {
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
if (oldest === currentSocket && this.unregisteredConnections.size === 1) {
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
this.unregisteredConnections.delete(oldest);
|
|
1514
|
+
oldest.destroy();
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
consumeToken(connection, cost = 1, now = Date.now()) {
|
|
1518
|
+
const elapsedMs = now - connection.lastRefillAt;
|
|
1519
|
+
if (elapsedMs > 0) {
|
|
1520
|
+
connection.tokens = Math.min(
|
|
1521
|
+
connection.origin === "remote" ? REMOTE_RATE_LIMIT_CAPACITY : RATE_LIMIT_CAPACITY,
|
|
1522
|
+
connection.tokens + elapsedMs * connection.refillPerSecond / 1e3
|
|
1523
|
+
);
|
|
1524
|
+
connection.lastRefillAt = now;
|
|
1525
|
+
}
|
|
1526
|
+
if (connection.tokens < cost) {
|
|
1527
|
+
return false;
|
|
1528
|
+
}
|
|
1529
|
+
connection.tokens -= cost;
|
|
1530
|
+
return true;
|
|
1531
|
+
}
|
|
1532
|
+
sendError(socket, code, error) {
|
|
1533
|
+
writeMessage(socket, { type: "error", code, error });
|
|
1534
|
+
}
|
|
1535
|
+
sendDeliveryFailure(socket, messageId, accepted, code, reason) {
|
|
1536
|
+
writeMessage(socket, { type: "delivery_failed", messageId, accepted, code, reason });
|
|
1537
|
+
}
|
|
1538
|
+
sendBossControlFailure(socket, requestId, messageId, idempotencyKey, code, reason, deliveryId, ledgerBinding, acknowledgeAcceptedRecovery = false) {
|
|
1539
|
+
const result = {
|
|
1540
|
+
type: "boss_control_result",
|
|
1541
|
+
requestId,
|
|
1542
|
+
messageId,
|
|
1543
|
+
idempotencyKey,
|
|
1544
|
+
status: "rejected",
|
|
1545
|
+
delivered: false,
|
|
1546
|
+
code,
|
|
1547
|
+
reason,
|
|
1548
|
+
...deliveryId === void 0 ? {} : { deliveryId }
|
|
1549
|
+
};
|
|
1550
|
+
if (ledgerBinding) {
|
|
1551
|
+
this.bossControlLedger.recordTerminal(ledgerBinding.scope, ledgerBinding.fingerprint, result);
|
|
1552
|
+
}
|
|
1553
|
+
if (acknowledgeAcceptedRecovery) {
|
|
1554
|
+
for (const frame of bossControlAcceptedRecoveryFrames(result)) writeMessage(socket, frame);
|
|
1555
|
+
} else {
|
|
1556
|
+
writeMessage(socket, result);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
scheduleShutdownCheck() {
|
|
1560
|
+
if (this.shutdownTimer) return;
|
|
1561
|
+
this.shutdownTimer = setTimeout(() => {
|
|
1562
|
+
this.shutdownTimer = null;
|
|
1563
|
+
if (this.sessions.size === 0) {
|
|
1564
|
+
console.log("No sessions connected, shutting down");
|
|
1565
|
+
this.shutdown();
|
|
1566
|
+
}
|
|
1567
|
+
}, 5e3);
|
|
1568
|
+
}
|
|
1569
|
+
handleMessage(socket, origin, msg, currentId, setId) {
|
|
1570
|
+
if (typeof msg !== "object" || msg === null || nodeUtilTypes2.isProxy(msg)) {
|
|
1571
|
+
throw new Error("Invalid client message");
|
|
1572
|
+
}
|
|
1573
|
+
const typeDescriptor = Object.getOwnPropertyDescriptor(msg, "type");
|
|
1574
|
+
if (typeDescriptor === void 0 || !typeDescriptor.enumerable || !Object.hasOwn(typeDescriptor, "value") || typeof typeDescriptor.value !== "string") throw new Error("Invalid client message");
|
|
1575
|
+
const clientMessage = msg;
|
|
1576
|
+
const requiresEndpointAuth = typeof LISTEN_TARGET !== "string";
|
|
1577
|
+
const hasEndpointAuth = clientMessage.stateId === BROKER_STATE_ID;
|
|
1578
|
+
if (clientMessage.type === "health") {
|
|
1579
|
+
if (typeof clientMessage.requestId !== "string") {
|
|
1580
|
+
throw new Error("Invalid health message");
|
|
1581
|
+
}
|
|
1582
|
+
if (requiresEndpointAuth && !hasEndpointAuth) {
|
|
1583
|
+
throw new Error("Invalid intercom TCP endpoint credentials");
|
|
1584
|
+
}
|
|
1585
|
+
writeMessage(socket, {
|
|
1586
|
+
type: "health_ok",
|
|
1587
|
+
requestId: clientMessage.requestId,
|
|
1588
|
+
protocol: INTERCOM_PROTOCOL_NAME2,
|
|
1589
|
+
version: INTERCOM_PROTOCOL_VERSION2,
|
|
1590
|
+
endpoint: origin,
|
|
1591
|
+
remoteAccess: this.remoteAccessContract(),
|
|
1592
|
+
...bossCapabilityAdvertisement() === void 0 ? {} : { capabilities: bossCapabilityAdvertisement() }
|
|
1593
|
+
});
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
if (clientMessage.type === "access_control") {
|
|
1597
|
+
if (currentId !== null) {
|
|
1598
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote access control requires a short-lived control connection");
|
|
1599
|
+
socket.end();
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
if (origin === "local") this.handleAccessControl(socket, clientMessage);
|
|
1603
|
+
else this.handleRemoteAccessControl(socket, clientMessage);
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
if (requiresEndpointAuth && clientMessage.type === "register" && !hasEndpointAuth) {
|
|
1607
|
+
throw new Error("Invalid intercom TCP endpoint credentials");
|
|
1608
|
+
}
|
|
1609
|
+
if (currentId === null && clientMessage.type !== "register") {
|
|
1610
|
+
throw new Error(`Received ${clientMessage.type} before register`);
|
|
1611
|
+
}
|
|
1612
|
+
if (currentId) {
|
|
1613
|
+
const bound = this.sessions.get(currentId);
|
|
1614
|
+
if (bound?.socket !== socket) {
|
|
1615
|
+
socket.destroy();
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
if (!this.isCurrentPrincipal(currentId)) {
|
|
1619
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote session authorization is no longer valid");
|
|
1620
|
+
socket.destroy();
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
switch (clientMessage.type) {
|
|
1625
|
+
case "register": {
|
|
1626
|
+
try {
|
|
1627
|
+
parseExactRegistrationFrame(clientMessage);
|
|
1628
|
+
} catch (error) {
|
|
1629
|
+
this.sendError(socket, "BOSS_CONTRACT_MISMATCH", error instanceof Error ? error.message : "Registration contract is invalid");
|
|
1630
|
+
socket.end();
|
|
1631
|
+
break;
|
|
1632
|
+
}
|
|
1633
|
+
if (clientMessage.protocol !== INTERCOM_PROTOCOL_NAME2 || clientMessage.version !== INTERCOM_PROTOCOL_VERSION2) {
|
|
1634
|
+
this.sendError(
|
|
1635
|
+
socket,
|
|
1636
|
+
"PROTOCOL_MISMATCH",
|
|
1637
|
+
`Unsupported intercom protocol; expected ${INTERCOM_PROTOCOL_NAME2} v${INTERCOM_PROTOCOL_VERSION2}`
|
|
1638
|
+
);
|
|
1639
|
+
socket.end();
|
|
1640
|
+
break;
|
|
1641
|
+
}
|
|
1642
|
+
let scopeId;
|
|
1643
|
+
try {
|
|
1644
|
+
parseIntercomScopeId(clientMessage.scopeId, "invalid-config");
|
|
1645
|
+
scopeId = clientMessage.scopeId;
|
|
1646
|
+
} catch {
|
|
1647
|
+
this.sendError(socket, "INVALID_REQUEST", "Invalid session configuration");
|
|
1648
|
+
socket.end();
|
|
1649
|
+
break;
|
|
1650
|
+
}
|
|
1651
|
+
if (!isSessionRegistration(clientMessage.session)) {
|
|
1652
|
+
this.sendError(socket, "BOSS_CONTRACT_MISMATCH", "Registration session contract is invalid");
|
|
1653
|
+
socket.end();
|
|
1654
|
+
break;
|
|
1655
|
+
}
|
|
1656
|
+
if (currentId) {
|
|
1657
|
+
throw new Error("Received duplicate register message");
|
|
1658
|
+
}
|
|
1659
|
+
if (clientMessage.session.boss !== void 0) {
|
|
1660
|
+
if (origin !== "local") {
|
|
1661
|
+
this.sendError(socket, "ACCESS_DENIED", "Boss participants require the protected local broker endpoint");
|
|
1662
|
+
} else if (bossCapabilityAdvertisement() === void 0) {
|
|
1663
|
+
this.sendError(socket, "BOSS_FEATURE_UNAVAILABLE", "boss-run-v1 is not advertised by this broker");
|
|
1664
|
+
} else {
|
|
1665
|
+
this.sendError(socket, "BOSS_FEATURE_UNAVAILABLE", "Boss participant credential binding is not installed");
|
|
1666
|
+
}
|
|
1667
|
+
socket.end();
|
|
1668
|
+
break;
|
|
1669
|
+
}
|
|
1670
|
+
let id;
|
|
1671
|
+
let previousLocal;
|
|
1672
|
+
let remotePrincipal;
|
|
1673
|
+
let issuedSessionCredential;
|
|
1674
|
+
let enrollmentConsumed = false;
|
|
1675
|
+
if (origin === "remote") {
|
|
1676
|
+
if (this.sessions.size >= MAX_SESSIONS) {
|
|
1677
|
+
this.sendError(socket, "TOO_MANY_SESSIONS", "Too many registered intercom sessions");
|
|
1678
|
+
socket.destroy();
|
|
1679
|
+
break;
|
|
1680
|
+
}
|
|
1681
|
+
const access = clientMessage.access;
|
|
1682
|
+
if (typeof access !== "object" || access === null || Array.isArray(access)) {
|
|
1683
|
+
this.audit.tryRecord({ event: "remote_registration_denied", outcome: "denied", reason: "MISSING_CREDENTIAL" });
|
|
1684
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote registration requires an access credential");
|
|
1685
|
+
socket.end();
|
|
1686
|
+
break;
|
|
1687
|
+
}
|
|
1688
|
+
const fields = access;
|
|
1689
|
+
try {
|
|
1690
|
+
if (typeof fields.enrollmentToken === "string") {
|
|
1691
|
+
const consumed = this.accessRegistry.consumeEnrollment(fields.enrollmentToken);
|
|
1692
|
+
remotePrincipal = consumed.principal;
|
|
1693
|
+
issuedSessionCredential = consumed.sessionCredential;
|
|
1694
|
+
enrollmentConsumed = true;
|
|
1695
|
+
} else if (typeof fields.sessionCredential === "string" && typeof fields.sessionId === "string" && typeof fields.generation === "number" && Number.isSafeInteger(fields.generation)) {
|
|
1696
|
+
remotePrincipal = this.accessRegistry.authenticateSession(fields.sessionId, fields.generation, fields.sessionCredential);
|
|
1697
|
+
} else {
|
|
1698
|
+
throw new Error("Invalid remote access credential shape");
|
|
1699
|
+
}
|
|
1700
|
+
} catch {
|
|
1701
|
+
this.audit.tryRecord({ event: "remote_registration_denied", outcome: "denied", reason: "INVALID_CREDENTIAL" });
|
|
1702
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote registration credential was rejected");
|
|
1703
|
+
socket.end();
|
|
1704
|
+
break;
|
|
1705
|
+
}
|
|
1706
|
+
id = remotePrincipal.id;
|
|
1707
|
+
if (this.sessions.has(id)) {
|
|
1708
|
+
this.audit.tryRecord({
|
|
1709
|
+
event: "credential_reuse_denied",
|
|
1710
|
+
outcome: "denied",
|
|
1711
|
+
actorId: id,
|
|
1712
|
+
remoteHostId: remotePrincipal.remoteHostId,
|
|
1713
|
+
generation: remotePrincipal.generation,
|
|
1714
|
+
reason: "ALREADY_ACTIVE"
|
|
1715
|
+
});
|
|
1716
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote session credential is already active");
|
|
1717
|
+
socket.end();
|
|
1718
|
+
break;
|
|
1719
|
+
}
|
|
1720
|
+
} else {
|
|
1721
|
+
id = randomUUID3();
|
|
1722
|
+
if (clientMessage.sessionId !== void 0) {
|
|
1723
|
+
if (!isSessionId(clientMessage.sessionId)) {
|
|
1724
|
+
throw new Error("Invalid register sessionId");
|
|
1725
|
+
}
|
|
1726
|
+
id = clientMessage.sessionId;
|
|
1727
|
+
}
|
|
1728
|
+
previousLocal = this.sessions.get(id);
|
|
1729
|
+
if (!previousLocal && this.sessions.size >= MAX_SESSIONS) {
|
|
1730
|
+
this.sendError(socket, "TOO_MANY_SESSIONS", "Too many registered intercom sessions");
|
|
1731
|
+
socket.destroy();
|
|
1732
|
+
break;
|
|
1733
|
+
}
|
|
1734
|
+
if (previousLocal && !isSameLocalRuntime(previousLocal, clientMessage.session)) {
|
|
1735
|
+
this.sendError(
|
|
1736
|
+
socket,
|
|
1737
|
+
"SESSION_ID_IN_USE",
|
|
1738
|
+
`Session ID "${id}" is already active in another local runtime; close the existing session or use a different session ID`
|
|
1739
|
+
);
|
|
1740
|
+
socket.end();
|
|
1741
|
+
break;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
setId(id);
|
|
1745
|
+
const session = clientMessage.session;
|
|
1746
|
+
const info = remotePrincipal ? {
|
|
1747
|
+
id,
|
|
1748
|
+
name: remotePrincipal.name,
|
|
1749
|
+
cwd: session.cwd,
|
|
1750
|
+
model: session.model,
|
|
1751
|
+
pid: session.pid,
|
|
1752
|
+
startedAt: session.startedAt,
|
|
1753
|
+
lastActivity: session.lastActivity,
|
|
1754
|
+
...session.status !== void 0 ? { status: session.status } : {},
|
|
1755
|
+
trustedLocal: false,
|
|
1756
|
+
origin: "remote",
|
|
1757
|
+
remoteHostId: remotePrincipal.remoteHostId,
|
|
1758
|
+
parentSessionId: remotePrincipal.parentSessionId,
|
|
1759
|
+
rootSessionId: remotePrincipal.rootSessionId,
|
|
1760
|
+
generation: remotePrincipal.generation,
|
|
1761
|
+
canDelegate: remotePrincipal.canDelegate,
|
|
1762
|
+
depth: remotePrincipal.depth,
|
|
1763
|
+
maxDepth: remotePrincipal.maxDepth,
|
|
1764
|
+
maxChildren: remotePrincipal.maxChildren
|
|
1765
|
+
} : {
|
|
1766
|
+
id,
|
|
1767
|
+
...session.name !== void 0 ? { name: session.name } : {},
|
|
1768
|
+
cwd: session.cwd,
|
|
1769
|
+
model: session.model,
|
|
1770
|
+
pid: session.pid,
|
|
1771
|
+
startedAt: session.startedAt,
|
|
1772
|
+
lastActivity: session.lastActivity,
|
|
1773
|
+
...session.status !== void 0 ? { status: session.status } : {},
|
|
1774
|
+
trustedLocal: typeof LISTEN_TARGET === "string" && process.platform !== "win32",
|
|
1775
|
+
origin: "local"
|
|
1776
|
+
};
|
|
1777
|
+
if (remotePrincipal) {
|
|
1778
|
+
this.audit.record({
|
|
1779
|
+
event: enrollmentConsumed ? "enrollment_consumed" : "remote_reconnect",
|
|
1780
|
+
outcome: "allowed",
|
|
1781
|
+
actorId: id,
|
|
1782
|
+
targetId: remotePrincipal.parentSessionId,
|
|
1783
|
+
remoteHostId: remotePrincipal.remoteHostId,
|
|
1784
|
+
generation: remotePrincipal.generation
|
|
1785
|
+
});
|
|
1786
|
+
this.audit.record({
|
|
1787
|
+
event: "remote_connect",
|
|
1788
|
+
outcome: "allowed",
|
|
1789
|
+
actorId: id,
|
|
1790
|
+
targetId: remotePrincipal.parentSessionId,
|
|
1791
|
+
remoteHostId: remotePrincipal.remoteHostId,
|
|
1792
|
+
generation: remotePrincipal.generation
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
const connected = {
|
|
1796
|
+
socket,
|
|
1797
|
+
info,
|
|
1798
|
+
...scopeId ? { scopeId } : {},
|
|
1799
|
+
...!remotePrincipal && clientMessage.session.runtimeInstanceId ? { runtimeInstanceId: clientMessage.session.runtimeInstanceId } : {},
|
|
1800
|
+
lastPresenceBroadcastAt: Date.now()
|
|
1801
|
+
};
|
|
1802
|
+
if (previousLocal) {
|
|
1803
|
+
this.broadcastLifecycle({ type: "session_left", sessionId: id }, previousLocal, id);
|
|
1804
|
+
this.clearPendingDeliveriesForSession(id, previousLocal.socket);
|
|
1805
|
+
this.deferAskEdgesForSession(id);
|
|
1806
|
+
}
|
|
1807
|
+
this.sessions.set(id, connected);
|
|
1808
|
+
if (this.shutdownTimer) {
|
|
1809
|
+
clearTimeout(this.shutdownTimer);
|
|
1810
|
+
this.shutdownTimer = null;
|
|
1811
|
+
}
|
|
1812
|
+
writeMessage(socket, {
|
|
1813
|
+
type: "registered",
|
|
1814
|
+
sessionId: id,
|
|
1815
|
+
protocol: INTERCOM_PROTOCOL_NAME2,
|
|
1816
|
+
version: INTERCOM_PROTOCOL_VERSION2,
|
|
1817
|
+
...remotePrincipal ? {
|
|
1818
|
+
remoteAccess: this.remoteAccessContract(),
|
|
1819
|
+
access: {
|
|
1820
|
+
origin: "remote",
|
|
1821
|
+
remoteHostId: remotePrincipal.remoteHostId,
|
|
1822
|
+
parentSessionId: remotePrincipal.parentSessionId,
|
|
1823
|
+
rootSessionId: remotePrincipal.rootSessionId,
|
|
1824
|
+
generation: remotePrincipal.generation,
|
|
1825
|
+
canDelegate: remotePrincipal.canDelegate,
|
|
1826
|
+
depth: remotePrincipal.depth,
|
|
1827
|
+
maxDepth: remotePrincipal.maxDepth,
|
|
1828
|
+
maxChildren: remotePrincipal.maxChildren,
|
|
1829
|
+
...issuedSessionCredential ? { sessionCredential: issuedSessionCredential } : {}
|
|
1830
|
+
}
|
|
1831
|
+
} : {}
|
|
1832
|
+
});
|
|
1833
|
+
this.broadcastLifecycle({ type: "session_joined", session: info }, connected, id);
|
|
1834
|
+
previousLocal?.socket.end();
|
|
1835
|
+
break;
|
|
1836
|
+
}
|
|
1837
|
+
case "unregister": {
|
|
1838
|
+
if (!currentId) {
|
|
1839
|
+
throw new Error("Received unregister before register");
|
|
1840
|
+
}
|
|
1841
|
+
if (clientMessage.preserveAsks !== void 0 && typeof clientMessage.preserveAsks !== "boolean") {
|
|
1842
|
+
throw new Error("Invalid unregister preserveAsks value");
|
|
1843
|
+
}
|
|
1844
|
+
const existing = this.sessions.get(currentId);
|
|
1845
|
+
if (existing?.socket === socket) {
|
|
1846
|
+
if (existing.info.origin === "remote") {
|
|
1847
|
+
this.audit.tryRecord({
|
|
1848
|
+
event: "remote_disconnect",
|
|
1849
|
+
outcome: "observed",
|
|
1850
|
+
actorId: currentId,
|
|
1851
|
+
remoteHostId: existing.info.remoteHostId,
|
|
1852
|
+
generation: existing.info.generation,
|
|
1853
|
+
reason: "UNREGISTERED"
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
this.broadcastLifecycle({ type: "session_left", sessionId: currentId }, existing, currentId);
|
|
1857
|
+
this.sessions.delete(currentId);
|
|
1858
|
+
this.clearPendingDeliveriesForSession(currentId, socket);
|
|
1859
|
+
if (clientMessage.preserveAsks) {
|
|
1860
|
+
this.deferAskEdgesForSession(currentId);
|
|
1861
|
+
} else {
|
|
1862
|
+
this.clearAskEdgesForSession(currentId, "session_disconnected");
|
|
1863
|
+
}
|
|
1864
|
+
this.scheduleShutdownCheck();
|
|
1865
|
+
}
|
|
1866
|
+
setId(null);
|
|
1867
|
+
break;
|
|
1868
|
+
}
|
|
1869
|
+
case "list": {
|
|
1870
|
+
if (typeof clientMessage.requestId !== "string") {
|
|
1871
|
+
throw new Error("Invalid list message");
|
|
1872
|
+
}
|
|
1873
|
+
const actor = this.sessions.get(currentId);
|
|
1874
|
+
const sameScopeSessions = actor ? Array.from(this.sessions.values()).filter((candidate) => sameIntercomScope(actor.scopeId, candidate.scopeId)).map((candidate) => candidate.info) : [];
|
|
1875
|
+
const allSessions = sameScopeSessions;
|
|
1876
|
+
const sessions = visibleSessions(allSessions, currentId);
|
|
1877
|
+
if (actor?.info.origin === "remote" && sessions.length < allSessions.length) {
|
|
1878
|
+
this.audit.tryRecord({
|
|
1879
|
+
event: "remote_visibility_filtered",
|
|
1880
|
+
outcome: "observed",
|
|
1881
|
+
actorId: currentId,
|
|
1882
|
+
remoteHostId: actor.info.remoteHostId,
|
|
1883
|
+
generation: actor.info.generation,
|
|
1884
|
+
visibleCount: sessions.length,
|
|
1885
|
+
hiddenCount: allSessions.length - sessions.length
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
writeMessage(socket, { type: "sessions", requestId: clientMessage.requestId, sessions });
|
|
1889
|
+
break;
|
|
1890
|
+
}
|
|
1891
|
+
case "send": {
|
|
1892
|
+
if (!currentId) {
|
|
1893
|
+
throw new Error("Received send before register");
|
|
1894
|
+
}
|
|
1895
|
+
const message = clientMessage.message;
|
|
1896
|
+
const messageId = typeof message === "object" && message !== null && "id" in message && typeof message.id === "string" && message.id.length > 0 && message.id.length <= MAX_MESSAGE_ID_LENGTH ? message.id : "unknown";
|
|
1897
|
+
if (typeof clientMessage.to !== "string" || clientMessage.to.length === 0 || clientMessage.to.length > MAX_TARGET_LENGTH || !isMessage(message)) {
|
|
1898
|
+
this.sendDeliveryFailure(socket, messageId, false, "INVALID_MESSAGE", "Invalid message format");
|
|
1899
|
+
break;
|
|
1900
|
+
}
|
|
1901
|
+
const action = message.replyTo ? "reply" : message.expectsReply ? "ask" : "send";
|
|
1902
|
+
this.pruneRecentDeliveries();
|
|
1903
|
+
const deliveryKey = this.deliveryKey(currentId, message.id);
|
|
1904
|
+
const fingerprint = JSON.stringify({
|
|
1905
|
+
to: clientMessage.to,
|
|
1906
|
+
replyTo: message.replyTo,
|
|
1907
|
+
expectsReply: message.expectsReply,
|
|
1908
|
+
content: message.content
|
|
1909
|
+
});
|
|
1910
|
+
const recent = this.recentDeliveries.get(deliveryKey);
|
|
1911
|
+
if (recent) {
|
|
1912
|
+
if (recent.fingerprint !== fingerprint) {
|
|
1913
|
+
this.sendDeliveryFailure(socket, message.id, false, "DUPLICATE_MESSAGE_ID", "Message ID was already used with a different payload");
|
|
1914
|
+
break;
|
|
1915
|
+
}
|
|
1916
|
+
const actor = this.sessions.get(currentId);
|
|
1917
|
+
const target = this.sessions.get(recent.to);
|
|
1918
|
+
const authorizationStillValid = Boolean(
|
|
1919
|
+
actor && target && (actor.info.generation ?? 1) === recent.fromGeneration && (target.info.generation ?? 1) === recent.toGeneration && this.isAuthorized(currentId, recent.action, recent.to)
|
|
1920
|
+
);
|
|
1921
|
+
if (recent.retryable || !authorizationStillValid) {
|
|
1922
|
+
this.recentDeliveries.delete(deliveryKey);
|
|
1923
|
+
} else {
|
|
1924
|
+
if (recent.response.type === "delivered") {
|
|
1925
|
+
writeMessage(socket, {
|
|
1926
|
+
type: "delivery_accepted",
|
|
1927
|
+
messageId: message.id,
|
|
1928
|
+
deliveryId: recent.response.deliveryId
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
writeMessage(socket, recent.response);
|
|
1932
|
+
break;
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
const existingDeliveryId = this.pendingDeliveryKeys.get(deliveryKey);
|
|
1936
|
+
if (existingDeliveryId) {
|
|
1937
|
+
const existing = this.pendingDeliveries.get(existingDeliveryId);
|
|
1938
|
+
if (!existing || existing.fingerprint !== fingerprint) {
|
|
1939
|
+
this.sendDeliveryFailure(socket, message.id, false, "DUPLICATE_MESSAGE_ID", "Message ID is already pending with a different payload");
|
|
1940
|
+
break;
|
|
1941
|
+
}
|
|
1942
|
+
const actor = this.sessions.get(existing.from);
|
|
1943
|
+
const target = this.sessions.get(existing.to);
|
|
1944
|
+
if (actor && target && (actor.info.generation ?? 1) === existing.fromGeneration && (target.info.generation ?? 1) === existing.toGeneration && this.isAuthorized(existing.from, existing.action, existing.to)) {
|
|
1945
|
+
writeMessage(socket, { type: "delivery_accepted", messageId: message.id, deliveryId: existing.id });
|
|
1946
|
+
break;
|
|
1947
|
+
}
|
|
1948
|
+
this.failPendingDelivery(existing.id, "SESSION_NOT_FOUND", "Delivery authorization changed while pending");
|
|
1949
|
+
}
|
|
1950
|
+
if (this.pendingDeliveries.size >= MAX_PENDING_DELIVERIES || this.countPendingDeliveriesFrom(currentId) >= MAX_PENDING_DELIVERIES_PER_SESSION) {
|
|
1951
|
+
this.sendDeliveryFailure(socket, message.id, false, "TOO_MANY_PENDING_DELIVERIES", "Too many messages are waiting for receiver acknowledgement");
|
|
1952
|
+
break;
|
|
1953
|
+
}
|
|
1954
|
+
const candidates = this.findSessions(currentId, clientMessage.to);
|
|
1955
|
+
const targets = candidates.filter((target) => this.isAuthorized(currentId, action, target.info.id));
|
|
1956
|
+
if (candidates.length > 0 && targets.length === 0) {
|
|
1957
|
+
const actor = this.sessions.get(currentId);
|
|
1958
|
+
this.audit.tryRecord({
|
|
1959
|
+
event: "remote_delivery_denied",
|
|
1960
|
+
outcome: "denied",
|
|
1961
|
+
actorId: currentId,
|
|
1962
|
+
targetId: candidates.length === 1 ? candidates[0].info.id : void 0,
|
|
1963
|
+
remoteHostId: actor?.info.remoteHostId ?? candidates.find((candidate) => candidate.info.remoteHostId)?.info.remoteHostId,
|
|
1964
|
+
generation: actor?.info.generation,
|
|
1965
|
+
reason: "POLICY_DENIED"
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
if (targets.length === 1) {
|
|
1969
|
+
const fromSession = this.sessions.get(currentId);
|
|
1970
|
+
if (!fromSession || fromSession.socket !== socket) {
|
|
1971
|
+
this.sendDeliveryFailure(socket, message.id, false, "SENDER_NOT_FOUND", "Sender session not found");
|
|
1972
|
+
break;
|
|
1973
|
+
}
|
|
1974
|
+
const target = targets[0];
|
|
1975
|
+
const replyEdge = message.replyTo ? this.askEdges.get(this.askKey(target.info.id, message.replyTo)) : void 0;
|
|
1976
|
+
if (message.replyTo && !replyEdge) {
|
|
1977
|
+
this.sendDeliveryFailure(socket, message.id, false, "INVALID_REPLY_TARGET", "Reply target does not match a pending ask");
|
|
1978
|
+
break;
|
|
1979
|
+
}
|
|
1980
|
+
if (replyEdge && (replyEdge.to !== currentId || replyEdge.from !== target.info.id)) {
|
|
1981
|
+
this.sendDeliveryFailure(socket, message.id, false, "INVALID_REPLY_TARGET", "Reply target does not match the pending ask");
|
|
1982
|
+
break;
|
|
1983
|
+
}
|
|
1984
|
+
if (message.expectsReply) {
|
|
1985
|
+
const existingAsk = Array.from(this.askEdges.values()).find(
|
|
1986
|
+
(edge) => edge.from === currentId && edge.to === target.info.id
|
|
1987
|
+
);
|
|
1988
|
+
if (existingAsk) {
|
|
1989
|
+
this.sendDeliveryFailure(socket, message.id, false, "ASK_ALREADY_PENDING", "Another ask to this session is still unresolved. Wait for its reply or use intercom_send for a non-blocking follow-up.");
|
|
1990
|
+
break;
|
|
1991
|
+
}
|
|
1992
|
+
const reverseEdge = Array.from(this.askEdges.values()).find(
|
|
1993
|
+
(edge) => edge.state === "blocking" && !(message.replyTo === edge.messageId && target.info.id === edge.from) && edge.from === target.info.id && edge.to === currentId
|
|
1994
|
+
);
|
|
1995
|
+
if (reverseEdge) {
|
|
1996
|
+
this.sendDeliveryFailure(socket, message.id, false, "MUTUAL_ASK", "Mutual ask refused: target session is already waiting for a reply from this session.");
|
|
1997
|
+
break;
|
|
1998
|
+
}
|
|
1999
|
+
if (this.countAskEdgesFrom(currentId) >= MAX_PENDING_ASKS_PER_SESSION) {
|
|
2000
|
+
this.sendDeliveryFailure(socket, message.id, false, "TOO_MANY_PENDING_ASKS", "Too many asks are already waiting for replies");
|
|
2001
|
+
break;
|
|
2002
|
+
}
|
|
2003
|
+
this.addAskEdge(message.id, currentId, target.info.id);
|
|
2004
|
+
}
|
|
2005
|
+
const deliveryId = randomUUID3();
|
|
2006
|
+
const timeout = setTimeout(() => {
|
|
2007
|
+
this.failPendingDelivery(deliveryId, "DELIVERY_TIMEOUT", "Recipient did not acknowledge the message in time");
|
|
2008
|
+
}, DELIVERY_ACK_TIMEOUT_MS);
|
|
2009
|
+
timeout.unref?.();
|
|
2010
|
+
const pending = {
|
|
2011
|
+
id: deliveryId,
|
|
2012
|
+
key: deliveryKey,
|
|
2013
|
+
fingerprint,
|
|
2014
|
+
message,
|
|
2015
|
+
from: currentId,
|
|
2016
|
+
to: target.info.id,
|
|
2017
|
+
senderSocket: socket,
|
|
2018
|
+
recipientSocket: target.socket,
|
|
2019
|
+
action,
|
|
2020
|
+
fromGeneration: fromSession.info.generation ?? 1,
|
|
2021
|
+
toGeneration: target.info.generation ?? 1,
|
|
2022
|
+
timeout
|
|
2023
|
+
};
|
|
2024
|
+
this.pendingDeliveries.set(deliveryId, pending);
|
|
2025
|
+
this.pendingDeliveryKeys.set(deliveryKey, deliveryId);
|
|
2026
|
+
writeMessage(socket, { type: "delivery_accepted", messageId: message.id, deliveryId });
|
|
2027
|
+
writeMessage(target.socket, {
|
|
2028
|
+
type: "message",
|
|
2029
|
+
deliveryId,
|
|
2030
|
+
from: fromSession.info,
|
|
2031
|
+
message
|
|
2032
|
+
});
|
|
2033
|
+
break;
|
|
2034
|
+
}
|
|
2035
|
+
if (targets.length > 1) {
|
|
2036
|
+
this.sendDeliveryFailure(socket, message.id, false, "AMBIGUOUS_TARGET", `Multiple sessions named "${clientMessage.to}" are connected. Use the session ID instead.`);
|
|
2037
|
+
break;
|
|
2038
|
+
}
|
|
2039
|
+
this.sendDeliveryFailure(socket, message.id, false, "SESSION_NOT_FOUND", "Session not found");
|
|
2040
|
+
break;
|
|
2041
|
+
}
|
|
2042
|
+
case "boss_control": {
|
|
2043
|
+
if (!currentId) throw new Error("Received boss_control before register");
|
|
2044
|
+
const requestId = clientMessage.requestId;
|
|
2045
|
+
const requestedTarget = clientMessage.to;
|
|
2046
|
+
if (typeof requestId !== "string" || requestId.length === 0 || requestId.length > MAX_MESSAGE_ID_LENGTH || typeof requestedTarget !== "string" || requestedTarget.length === 0 || requestedTarget.length > MAX_TARGET_LENGTH) throw new Error("Invalid boss_control routing metadata");
|
|
2047
|
+
const sender = this.sessions.get(currentId);
|
|
2048
|
+
let envelope;
|
|
2049
|
+
let controlKind;
|
|
2050
|
+
try {
|
|
2051
|
+
if (!sender || sender.socket !== socket) throw new Error("Boss sender session not found");
|
|
2052
|
+
envelope = assertBossControlSender(sender.info, clientMessage.envelope);
|
|
2053
|
+
controlKind = bossControlKind(envelope).controlKind;
|
|
2054
|
+
if (requestId !== envelope.messageId) {
|
|
2055
|
+
throw new Error("requestId must equal the canonical Boss envelope messageId");
|
|
2056
|
+
}
|
|
2057
|
+
} catch (error) {
|
|
2058
|
+
this.sendBossControlFailure(
|
|
2059
|
+
socket,
|
|
2060
|
+
requestId,
|
|
2061
|
+
requestId,
|
|
2062
|
+
requestId,
|
|
2063
|
+
"INVALID_CONTROL",
|
|
2064
|
+
error instanceof Error ? error.message : "Invalid Boss control envelope"
|
|
2065
|
+
);
|
|
2066
|
+
break;
|
|
2067
|
+
}
|
|
2068
|
+
const key = this.bossControlKey(currentId, envelope);
|
|
2069
|
+
const fingerprint = this.bossControlFingerprint(requestedTarget, envelope);
|
|
2070
|
+
const prior = this.bossControlLedger.lookup(key, fingerprint);
|
|
2071
|
+
if (prior.status === "replay") {
|
|
2072
|
+
for (const frame of bossControlReplayFrames(prior.result, envelope.messageId)) writeMessage(socket, frame);
|
|
2073
|
+
break;
|
|
2074
|
+
}
|
|
2075
|
+
if (prior.status === "conflict") {
|
|
2076
|
+
this.sendBossControlFailure(
|
|
2077
|
+
socket,
|
|
2078
|
+
requestId,
|
|
2079
|
+
envelope.messageId,
|
|
2080
|
+
envelope.idempotencyKey,
|
|
2081
|
+
"IDEMPOTENCY_CONFLICT",
|
|
2082
|
+
"Boss idempotency key is durably bound to a different canonical request"
|
|
2083
|
+
);
|
|
2084
|
+
break;
|
|
2085
|
+
}
|
|
2086
|
+
const exactTarget = exactBossSessionTarget(this.sessions, requestedTarget);
|
|
2087
|
+
const correlated = hasAuthoritativeBossControlCorrelation();
|
|
2088
|
+
const target = exactTarget && authorizeSessionAction(
|
|
2089
|
+
Array.from(this.sessions.values(), (session) => session.info),
|
|
2090
|
+
currentId,
|
|
2091
|
+
"control",
|
|
2092
|
+
exactTarget.info.id,
|
|
2093
|
+
{ controlKind, correlated }
|
|
2094
|
+
).allowed ? exactTarget : void 0;
|
|
2095
|
+
if (!target) {
|
|
2096
|
+
const acceptedDeliveryId = prior.status === "accepted" ? prior.deliveryId : void 0;
|
|
2097
|
+
this.sendBossControlFailure(
|
|
2098
|
+
socket,
|
|
2099
|
+
requestId,
|
|
2100
|
+
envelope.messageId,
|
|
2101
|
+
envelope.idempotencyKey,
|
|
2102
|
+
exactTarget ? "POLICY_DENIED" : "SESSION_NOT_FOUND",
|
|
2103
|
+
exactTarget ? "Boss control routing requires authoritative correlation evidence" : "Boss control target session ID was not found",
|
|
2104
|
+
acceptedDeliveryId,
|
|
2105
|
+
{ scope: key, fingerprint },
|
|
2106
|
+
acceptedDeliveryId !== void 0
|
|
2107
|
+
);
|
|
2108
|
+
break;
|
|
2109
|
+
}
|
|
2110
|
+
const existingDeliveryId = this.pendingBossControlKeys.get(key);
|
|
2111
|
+
if (existingDeliveryId) {
|
|
2112
|
+
const existing = this.pendingBossControls.get(existingDeliveryId);
|
|
2113
|
+
if (!existing || existing.fingerprint !== fingerprint) {
|
|
2114
|
+
this.sendBossControlFailure(socket, requestId, envelope.messageId, envelope.idempotencyKey, "IDEMPOTENCY_CONFLICT", "Boss idempotency key is already bound to a different canonical request");
|
|
2115
|
+
break;
|
|
2116
|
+
}
|
|
2117
|
+
if (existing.messageId !== envelope.messageId) {
|
|
2118
|
+
existing.requestId = requestId;
|
|
2119
|
+
existing.messageId = envelope.messageId;
|
|
2120
|
+
existing.envelope = envelope;
|
|
2121
|
+
existing.senderSocket = socket;
|
|
2122
|
+
writeMessage(existing.recipientSocket, { type: "boss_control", deliveryId: existing.deliveryId, from: sender.info, envelope });
|
|
2123
|
+
}
|
|
2124
|
+
writeMessage(socket, {
|
|
2125
|
+
type: "boss_control_ack",
|
|
2126
|
+
requestId,
|
|
2127
|
+
messageId: envelope.messageId,
|
|
2128
|
+
idempotencyKey: envelope.idempotencyKey,
|
|
2129
|
+
status: "accepted",
|
|
2130
|
+
deliveryId: existing.deliveryId
|
|
2131
|
+
});
|
|
2132
|
+
break;
|
|
2133
|
+
}
|
|
2134
|
+
const deliveryId = prior.status === "accepted" ? prior.deliveryId : randomUUID3();
|
|
2135
|
+
if (prior.status === "miss") {
|
|
2136
|
+
this.bossControlLedger.recordAccepted(key, fingerprint, deliveryId);
|
|
2137
|
+
}
|
|
2138
|
+
const timeout = setTimeout(() => {
|
|
2139
|
+
this.failPendingBossControl(deliveryId, "DELIVERY_TIMEOUT", "Recipient did not acknowledge the Boss control envelope in time");
|
|
2140
|
+
}, DELIVERY_ACK_TIMEOUT_MS);
|
|
2141
|
+
timeout.unref?.();
|
|
2142
|
+
this.pendingBossControls.set(deliveryId, {
|
|
2143
|
+
deliveryId,
|
|
2144
|
+
key,
|
|
2145
|
+
fingerprint,
|
|
2146
|
+
requestId,
|
|
2147
|
+
messageId: envelope.messageId,
|
|
2148
|
+
envelope,
|
|
2149
|
+
from: currentId,
|
|
2150
|
+
to: target.info.id,
|
|
2151
|
+
senderSocket: socket,
|
|
2152
|
+
recipientSocket: target.socket,
|
|
2153
|
+
timeout
|
|
2154
|
+
});
|
|
2155
|
+
this.pendingBossControlKeys.set(key, deliveryId);
|
|
2156
|
+
writeMessage(socket, {
|
|
2157
|
+
type: "boss_control_ack",
|
|
2158
|
+
requestId,
|
|
2159
|
+
messageId: envelope.messageId,
|
|
2160
|
+
idempotencyKey: envelope.idempotencyKey,
|
|
2161
|
+
status: "accepted",
|
|
2162
|
+
deliveryId
|
|
2163
|
+
});
|
|
2164
|
+
writeMessage(target.socket, { type: "boss_control", deliveryId, from: sender.info, envelope });
|
|
2165
|
+
break;
|
|
2166
|
+
}
|
|
2167
|
+
case "boss_control_received": {
|
|
2168
|
+
if (!currentId) throw new Error("Received boss_control_received before register");
|
|
2169
|
+
if (typeof clientMessage.deliveryId !== "string" || typeof clientMessage.messageId !== "string" || typeof clientMessage.idempotencyKey !== "string") {
|
|
2170
|
+
throw new Error("Invalid boss_control_received message");
|
|
2171
|
+
}
|
|
2172
|
+
this.acknowledgePendingBossControl(clientMessage.deliveryId, clientMessage.messageId, clientMessage.idempotencyKey, currentId, socket);
|
|
2173
|
+
break;
|
|
2174
|
+
}
|
|
2175
|
+
case "message_received": {
|
|
2176
|
+
if (!currentId) {
|
|
2177
|
+
throw new Error("Received message_received before register");
|
|
2178
|
+
}
|
|
2179
|
+
if (typeof clientMessage.deliveryId !== "string") {
|
|
2180
|
+
throw new Error("Invalid message_received message");
|
|
2181
|
+
}
|
|
2182
|
+
this.acknowledgePendingDelivery(clientMessage.deliveryId, currentId, socket);
|
|
2183
|
+
break;
|
|
2184
|
+
}
|
|
2185
|
+
case "message_rejected": {
|
|
2186
|
+
if (!currentId) {
|
|
2187
|
+
throw new Error("Received message_rejected before register");
|
|
2188
|
+
}
|
|
2189
|
+
if (typeof clientMessage.deliveryId !== "string" || clientMessage.code !== "CONFLICTING_MESSAGE_ID" || typeof clientMessage.reason !== "string" || clientMessage.reason.length > 1024) {
|
|
2190
|
+
throw new Error("Invalid message_rejected message");
|
|
2191
|
+
}
|
|
2192
|
+
const pending = this.pendingDeliveries.get(clientMessage.deliveryId);
|
|
2193
|
+
if (pending?.to === currentId && pending.recipientSocket === socket) {
|
|
2194
|
+
this.failPendingDelivery(clientMessage.deliveryId, clientMessage.code, clientMessage.reason);
|
|
2195
|
+
}
|
|
2196
|
+
break;
|
|
2197
|
+
}
|
|
2198
|
+
case "defer_ask": {
|
|
2199
|
+
if (!currentId) {
|
|
2200
|
+
throw new Error("Received defer_ask before register");
|
|
2201
|
+
}
|
|
2202
|
+
if (typeof clientMessage.messageId !== "string" || clientMessage.messageId.length > MAX_MESSAGE_ID_LENGTH || typeof clientMessage.requestId !== "string" || clientMessage.requestId.length > MAX_MESSAGE_ID_LENGTH) {
|
|
2203
|
+
throw new Error("Invalid defer_ask message");
|
|
2204
|
+
}
|
|
2205
|
+
const session = this.sessions.get(currentId);
|
|
2206
|
+
const edge = this.askEdges.get(this.askKey(currentId, clientMessage.messageId));
|
|
2207
|
+
const applied = Boolean(session?.socket === socket && edge?.from === currentId);
|
|
2208
|
+
if (applied && edge?.state === "blocking") {
|
|
2209
|
+
edge.state = "deferred";
|
|
2210
|
+
this.persistAskEdges();
|
|
2211
|
+
this.notifyAskDeferred(edge);
|
|
2212
|
+
}
|
|
2213
|
+
writeMessage(socket, { type: "ask_control_result", requestId: clientMessage.requestId, action: "defer", messageId: clientMessage.messageId, applied });
|
|
2214
|
+
break;
|
|
2215
|
+
}
|
|
2216
|
+
case "cancel_ask": {
|
|
2217
|
+
if (!currentId) {
|
|
2218
|
+
throw new Error("Received cancel_ask before register");
|
|
2219
|
+
}
|
|
2220
|
+
if (typeof clientMessage.messageId !== "string" || clientMessage.messageId.length > MAX_MESSAGE_ID_LENGTH || typeof clientMessage.requestId !== "string" || clientMessage.requestId.length > MAX_MESSAGE_ID_LENGTH) {
|
|
2221
|
+
throw new Error("Invalid cancel_ask message");
|
|
2222
|
+
}
|
|
2223
|
+
const session = this.sessions.get(currentId);
|
|
2224
|
+
const edgeKey = this.askKey(currentId, clientMessage.messageId);
|
|
2225
|
+
const edge = this.askEdges.get(edgeKey);
|
|
2226
|
+
const applied = Boolean(session?.socket === socket && edge?.from === currentId);
|
|
2227
|
+
if (applied) {
|
|
2228
|
+
this.removeAskEdge(edgeKey, "cancelled", true);
|
|
2229
|
+
}
|
|
2230
|
+
writeMessage(socket, { type: "ask_control_result", requestId: clientMessage.requestId, action: "cancel", messageId: clientMessage.messageId, applied });
|
|
2231
|
+
break;
|
|
2232
|
+
}
|
|
2233
|
+
case "presence": {
|
|
2234
|
+
if (!currentId) {
|
|
2235
|
+
throw new Error("Received presence before register");
|
|
2236
|
+
}
|
|
2237
|
+
const session = this.sessions.get(currentId);
|
|
2238
|
+
if (session?.socket === socket) {
|
|
2239
|
+
let changed = false;
|
|
2240
|
+
if (clientMessage.name !== void 0) {
|
|
2241
|
+
if (typeof clientMessage.name !== "string" || clientMessage.name.length > MAX_SESSION_NAME_LENGTH) {
|
|
2242
|
+
throw new Error("Invalid presence name");
|
|
2243
|
+
}
|
|
2244
|
+
if (session.info.origin !== "remote" && session.info.name !== clientMessage.name) {
|
|
2245
|
+
session.info.name = clientMessage.name;
|
|
2246
|
+
changed = true;
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
if (clientMessage.status !== void 0) {
|
|
2250
|
+
if (typeof clientMessage.status !== "string" || clientMessage.status.length > MAX_SESSION_STATUS_LENGTH) {
|
|
2251
|
+
throw new Error("Invalid presence status");
|
|
2252
|
+
}
|
|
2253
|
+
if (session.info.status !== clientMessage.status) {
|
|
2254
|
+
session.info.status = clientMessage.status;
|
|
2255
|
+
changed = true;
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
if (clientMessage.model !== void 0) {
|
|
2259
|
+
if (typeof clientMessage.model !== "string" || clientMessage.model.length > MAX_SESSION_MODEL_LENGTH) {
|
|
2260
|
+
throw new Error("Invalid presence model");
|
|
2261
|
+
}
|
|
2262
|
+
if (session.info.model !== clientMessage.model) {
|
|
2263
|
+
session.info.model = clientMessage.model;
|
|
2264
|
+
changed = true;
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
const now = Date.now();
|
|
2268
|
+
session.info.lastActivity = now;
|
|
2269
|
+
if (changed || now - session.lastPresenceBroadcastAt >= PRESENCE_HEARTBEAT_MS) {
|
|
2270
|
+
session.lastPresenceBroadcastAt = now;
|
|
2271
|
+
this.broadcastLifecycle({ type: "presence_update", session: session.info }, session, currentId);
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
break;
|
|
2275
|
+
}
|
|
2276
|
+
default:
|
|
2277
|
+
throw new Error(`Unknown client message type: ${clientMessage.type}`);
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
remoteAccessContract() {
|
|
2281
|
+
return {
|
|
2282
|
+
feature: "remote-access-v1",
|
|
2283
|
+
policySemanticsVersion: POLICY_SEMANTICS_VERSION,
|
|
2284
|
+
policySemanticsHash: POLICY_SEMANTICS_HASH
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
handleAccessControl(socket, message) {
|
|
2288
|
+
if (typeof message.requestId !== "string" || message.requestId.length > MAX_MESSAGE_ID_LENGTH || typeof message.adminToken !== "string" || !this.accessRegistry.authenticateAdmin(message.adminToken)) {
|
|
2289
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote access control credential or request was rejected");
|
|
2290
|
+
socket.end();
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
if (message.action === "inspect_tree") {
|
|
2294
|
+
if (typeof message.principalId !== "string" || !isSessionId(message.principalId)) {
|
|
2295
|
+
this.sendError(socket, "INVALID_REQUEST", "Invalid remote principal ID");
|
|
2296
|
+
socket.end();
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2299
|
+
const principals = this.accessRegistry.inspectSubtree(message.principalId).map((principal) => this.principalSummary(principal));
|
|
2300
|
+
this.audit.record({
|
|
2301
|
+
event: "tree_inspected",
|
|
2302
|
+
outcome: "allowed",
|
|
2303
|
+
actorId: "local-admin",
|
|
2304
|
+
targetId: message.principalId,
|
|
2305
|
+
visibleCount: principals.length
|
|
2306
|
+
});
|
|
2307
|
+
writeMessage(socket, { type: "access_control_result", requestId: message.requestId, action: "inspect_tree", principals });
|
|
2308
|
+
socket.end();
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2311
|
+
if (message.action === "adopt_subtree") {
|
|
2312
|
+
if (typeof message.principalId !== "string" || !isSessionId(message.principalId) || typeof message.newParentSessionId !== "string" || !isSessionId(message.newParentSessionId)) {
|
|
2313
|
+
this.sendError(socket, "INVALID_REQUEST", "Invalid adoption request");
|
|
2314
|
+
socket.end();
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
const localParent = this.sessions.get(message.newParentSessionId);
|
|
2318
|
+
const remoteParent = this.accessRegistry.snapshot().principals[message.newParentSessionId];
|
|
2319
|
+
if ((!localParent || localParent.info.origin === "remote") && (!remoteParent || remoteParent.state !== "active")) {
|
|
2320
|
+
this.sendError(socket, "ACCESS_DENIED", "Adoption parent must be an active local or remote principal");
|
|
2321
|
+
socket.end();
|
|
2322
|
+
return;
|
|
2323
|
+
}
|
|
2324
|
+
const newRootSessionId = localParent?.info.origin === "local" ? localParent.info.id : remoteParent.rootSessionId;
|
|
2325
|
+
const priorSessions = Array.from(this.sessions.values(), (session) => session.info);
|
|
2326
|
+
let changed;
|
|
2327
|
+
try {
|
|
2328
|
+
changed = this.accessRegistry.adoptSubtree(message.principalId, message.newParentSessionId, newRootSessionId);
|
|
2329
|
+
} catch {
|
|
2330
|
+
this.sendError(socket, "ACCESS_DENIED", "Adoption would violate the ownership tree");
|
|
2331
|
+
socket.end();
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
2334
|
+
this.disconnectTransitionedPrincipals(changed, priorSessions, "principal_adopted");
|
|
2335
|
+
writeMessage(socket, {
|
|
2336
|
+
type: "access_control_result",
|
|
2337
|
+
requestId: message.requestId,
|
|
2338
|
+
action: "adopt_subtree",
|
|
2339
|
+
principals: changed.map((principal) => this.principalSummary(principal))
|
|
2340
|
+
});
|
|
2341
|
+
socket.end();
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
if (message.action === "revoke_subtree") {
|
|
2345
|
+
if (typeof message.principalId !== "string" || !isSessionId(message.principalId)) {
|
|
2346
|
+
this.sendError(socket, "INVALID_REQUEST", "Invalid remote principal ID");
|
|
2347
|
+
socket.end();
|
|
2348
|
+
return;
|
|
2349
|
+
}
|
|
2350
|
+
const priorSessions = Array.from(this.sessions.values(), (session) => session.info);
|
|
2351
|
+
const changed = this.accessRegistry.revoke(message.principalId);
|
|
2352
|
+
this.disconnectTransitionedPrincipals(changed, priorSessions);
|
|
2353
|
+
writeMessage(socket, {
|
|
2354
|
+
type: "access_control_result",
|
|
2355
|
+
requestId: message.requestId,
|
|
2356
|
+
action: "revoke_subtree",
|
|
2357
|
+
changedPrincipalIds: changed.map((principal) => principal.id)
|
|
2358
|
+
});
|
|
2359
|
+
socket.end();
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2362
|
+
if (message.action !== "issue_enrollment" || typeof message.enrollment !== "object" || message.enrollment === null || Array.isArray(message.enrollment)) {
|
|
2363
|
+
this.sendError(socket, "INVALID_REQUEST", "Unknown remote access control action");
|
|
2364
|
+
socket.end();
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2367
|
+
const enrollment = message.enrollment;
|
|
2368
|
+
if (typeof enrollment.name !== "string" || typeof enrollment.parentSessionId !== "string" || typeof enrollment.rootSessionId !== "string" || typeof enrollment.remoteHostId !== "string" || enrollment.ttlMs !== void 0 && (typeof enrollment.ttlMs !== "number" || !Number.isSafeInteger(enrollment.ttlMs)) || enrollment.expiresAt !== void 0 && (typeof enrollment.expiresAt !== "number" || !Number.isSafeInteger(enrollment.expiresAt)) || enrollment.canDelegate !== void 0 && typeof enrollment.canDelegate !== "boolean" || enrollment.maxDepth !== void 0 && (typeof enrollment.maxDepth !== "number" || !Number.isSafeInteger(enrollment.maxDepth)) || enrollment.maxChildren !== void 0 && (typeof enrollment.maxChildren !== "number" || !Number.isSafeInteger(enrollment.maxChildren))) {
|
|
2369
|
+
this.sendError(socket, "INVALID_REQUEST", "Invalid remote enrollment request");
|
|
2370
|
+
socket.end();
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
const parent = this.sessions.get(enrollment.parentSessionId);
|
|
2374
|
+
if (!parent || parent.info.origin === "remote" || enrollment.rootSessionId !== parent.info.id) {
|
|
2375
|
+
this.sendError(socket, "ACCESS_DENIED", "Enrollment parent must be an active local root session");
|
|
2376
|
+
socket.end();
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
const issued = this.accessRegistry.issueEnrollment({
|
|
2380
|
+
name: enrollment.name,
|
|
2381
|
+
parentSessionId: parent.info.id,
|
|
2382
|
+
rootSessionId: parent.info.id,
|
|
2383
|
+
remoteHostId: enrollment.remoteHostId,
|
|
2384
|
+
...enrollment.expiresAt !== void 0 ? { expiresAt: enrollment.expiresAt } : {},
|
|
2385
|
+
...enrollment.canDelegate !== void 0 ? { canDelegate: enrollment.canDelegate } : {},
|
|
2386
|
+
...enrollment.maxDepth !== void 0 ? { maxDepth: enrollment.maxDepth } : {},
|
|
2387
|
+
...enrollment.maxChildren !== void 0 ? { maxChildren: enrollment.maxChildren } : {}
|
|
2388
|
+
}, enrollment.ttlMs);
|
|
2389
|
+
this.audit.record({
|
|
2390
|
+
event: "enrollment_issued",
|
|
2391
|
+
outcome: "allowed",
|
|
2392
|
+
actorId: parent.info.id,
|
|
2393
|
+
targetId: enrollment.name,
|
|
2394
|
+
remoteHostId: enrollment.remoteHostId,
|
|
2395
|
+
reason: `expires:${issued.expiresAt}`
|
|
2396
|
+
});
|
|
2397
|
+
writeMessage(socket, {
|
|
2398
|
+
type: "access_control_result",
|
|
2399
|
+
requestId: message.requestId,
|
|
2400
|
+
action: "issue_enrollment",
|
|
2401
|
+
enrollmentToken: issued.enrollmentToken,
|
|
2402
|
+
expiresAt: issued.expiresAt
|
|
2403
|
+
});
|
|
2404
|
+
socket.end();
|
|
2405
|
+
}
|
|
2406
|
+
handleRemoteAccessControl(socket, message) {
|
|
2407
|
+
if (typeof message.requestId !== "string" || message.requestId.length > MAX_MESSAGE_ID_LENGTH || typeof message.access !== "object" || message.access === null || Array.isArray(message.access)) {
|
|
2408
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote control request was rejected");
|
|
2409
|
+
socket.end();
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
const access = message.access;
|
|
2413
|
+
if (typeof access.sessionCredential !== "string" || typeof access.sessionId !== "string" || typeof access.generation !== "number" || !Number.isSafeInteger(access.generation)) {
|
|
2414
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote control credential was rejected");
|
|
2415
|
+
socket.end();
|
|
2416
|
+
return;
|
|
2417
|
+
}
|
|
2418
|
+
let parent;
|
|
2419
|
+
try {
|
|
2420
|
+
parent = this.accessRegistry.authenticateSession(access.sessionId, access.generation, access.sessionCredential);
|
|
2421
|
+
} catch {
|
|
2422
|
+
this.audit.tryRecord({ event: "remote_registration_denied", outcome: "denied", reason: "INVALID_CONTROL_CREDENTIAL" });
|
|
2423
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote control credential was rejected");
|
|
2424
|
+
socket.end();
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
const policyState = this.registryPolicyState();
|
|
2428
|
+
if (message.action === "inspect_tree") {
|
|
2429
|
+
const targetId = typeof message.principalId === "string" ? message.principalId : parent.id;
|
|
2430
|
+
const inspection = authorize(policyState, parent.id, "inspect_tree", targetId, { actorGeneration: parent.generation });
|
|
2431
|
+
if (!inspection.allowed) {
|
|
2432
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote tree inspection policy denied the request");
|
|
2433
|
+
socket.end();
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
const principals = this.accessRegistry.inspectSubtree(targetId).filter((candidate) => authorize(policyState, parent.id, "inspect_tree", candidate.id, { actorGeneration: parent.generation }).allowed).map((candidate) => this.principalSummary(candidate));
|
|
2437
|
+
this.audit.record({
|
|
2438
|
+
event: "tree_inspected",
|
|
2439
|
+
outcome: "allowed",
|
|
2440
|
+
actorId: parent.id,
|
|
2441
|
+
targetId,
|
|
2442
|
+
remoteHostId: parent.remoteHostId,
|
|
2443
|
+
generation: parent.generation,
|
|
2444
|
+
visibleCount: principals.length
|
|
2445
|
+
});
|
|
2446
|
+
writeMessage(socket, { type: "access_control_result", requestId: message.requestId, action: "inspect_tree", principals });
|
|
2447
|
+
socket.end();
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
if (message.action !== "issue_child_enrollment" || typeof message.enrollment !== "object" || message.enrollment === null || Array.isArray(message.enrollment)) {
|
|
2451
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote control action was rejected");
|
|
2452
|
+
socket.end();
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
const enrollment = message.enrollment;
|
|
2456
|
+
if (typeof enrollment.name !== "string" || enrollment.ttlMs !== void 0 && (typeof enrollment.ttlMs !== "number" || !Number.isSafeInteger(enrollment.ttlMs)) || enrollment.expiresAt !== void 0 && (typeof enrollment.expiresAt !== "number" || !Number.isSafeInteger(enrollment.expiresAt)) || enrollment.canDelegate !== void 0 && typeof enrollment.canDelegate !== "boolean" || enrollment.maxDepth !== void 0 && (typeof enrollment.maxDepth !== "number" || !Number.isSafeInteger(enrollment.maxDepth)) || enrollment.maxChildren !== void 0 && (typeof enrollment.maxChildren !== "number" || !Number.isSafeInteger(enrollment.maxChildren))) {
|
|
2457
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote delegation request was rejected");
|
|
2458
|
+
socket.end();
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
const delegation = authorize(policyState, parent.id, "delegate_child", parent.id, {
|
|
2462
|
+
actorGeneration: parent.generation,
|
|
2463
|
+
targetGeneration: parent.generation
|
|
2464
|
+
});
|
|
2465
|
+
if (!delegation.allowed) {
|
|
2466
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote delegation policy denied the request");
|
|
2467
|
+
socket.end();
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
let issued;
|
|
2471
|
+
try {
|
|
2472
|
+
issued = this.accessRegistry.issueChildEnrollment(parent.id, parent.generation, {
|
|
2473
|
+
name: enrollment.name,
|
|
2474
|
+
...enrollment.expiresAt !== void 0 ? { expiresAt: enrollment.expiresAt } : {},
|
|
2475
|
+
...enrollment.canDelegate !== void 0 ? { canDelegate: enrollment.canDelegate } : {},
|
|
2476
|
+
...enrollment.maxDepth !== void 0 ? { maxDepth: enrollment.maxDepth } : {},
|
|
2477
|
+
...enrollment.maxChildren !== void 0 ? { maxChildren: enrollment.maxChildren } : {}
|
|
2478
|
+
}, enrollment.ttlMs);
|
|
2479
|
+
} catch {
|
|
2480
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote delegation limits denied the request");
|
|
2481
|
+
socket.end();
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2484
|
+
this.audit.record({
|
|
2485
|
+
event: "enrollment_issued",
|
|
2486
|
+
outcome: "allowed",
|
|
2487
|
+
actorId: parent.id,
|
|
2488
|
+
targetId: enrollment.name,
|
|
2489
|
+
remoteHostId: parent.remoteHostId,
|
|
2490
|
+
generation: parent.generation,
|
|
2491
|
+
reason: `delegated-expires:${issued.expiresAt}`
|
|
2492
|
+
});
|
|
2493
|
+
writeMessage(socket, {
|
|
2494
|
+
type: "access_control_result",
|
|
2495
|
+
requestId: message.requestId,
|
|
2496
|
+
action: "issue_child_enrollment",
|
|
2497
|
+
enrollmentToken: issued.enrollmentToken,
|
|
2498
|
+
expiresAt: issued.expiresAt,
|
|
2499
|
+
parentSessionId: parent.id
|
|
2500
|
+
});
|
|
2501
|
+
socket.end();
|
|
2502
|
+
}
|
|
2503
|
+
registryPolicyState() {
|
|
2504
|
+
const records = this.accessRegistry.snapshot().principals;
|
|
2505
|
+
const principals = {};
|
|
2506
|
+
for (const record of Object.values(records)) {
|
|
2507
|
+
principals[record.id] = {
|
|
2508
|
+
id: record.id,
|
|
2509
|
+
kind: "remote",
|
|
2510
|
+
state: record.state,
|
|
2511
|
+
generation: record.generation,
|
|
2512
|
+
policy: "remote-tree",
|
|
2513
|
+
parentSessionId: record.parentSessionId,
|
|
2514
|
+
rootSessionId: record.rootSessionId
|
|
2515
|
+
};
|
|
2516
|
+
if (!principals[record.rootSessionId]) {
|
|
2517
|
+
principals[record.rootSessionId] = {
|
|
2518
|
+
id: record.rootSessionId,
|
|
2519
|
+
kind: "local",
|
|
2520
|
+
state: "active",
|
|
2521
|
+
generation: 1,
|
|
2522
|
+
policy: "local-public",
|
|
2523
|
+
rootSessionId: record.rootSessionId
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
return { principals };
|
|
2528
|
+
}
|
|
2529
|
+
principalSummary(principal) {
|
|
2530
|
+
return { ...principal, connected: this.sessions.has(principal.id) };
|
|
2531
|
+
}
|
|
2532
|
+
reconcileExpiredPrincipals() {
|
|
2533
|
+
const priorSessions = Array.from(this.sessions.values(), (session) => session.info);
|
|
2534
|
+
const changed = this.accessRegistry.expirePrincipals();
|
|
2535
|
+
if (changed.length > 0) this.disconnectTransitionedPrincipals(changed, priorSessions, "principal_expired");
|
|
2536
|
+
}
|
|
2537
|
+
disconnectTransitionedPrincipals(changed, priorSessions, auditEvent = "principal_revoked") {
|
|
2538
|
+
const changedIds = new Set(changed.map((principal) => principal.id));
|
|
2539
|
+
for (const principal of changed) {
|
|
2540
|
+
const live = this.sessions.get(principal.id);
|
|
2541
|
+
if (!live) {
|
|
2542
|
+
this.audit.record({
|
|
2543
|
+
event: auditEvent,
|
|
2544
|
+
outcome: "allowed",
|
|
2545
|
+
actorId: principal.id,
|
|
2546
|
+
remoteHostId: principal.remoteHostId,
|
|
2547
|
+
generation: principal.generation,
|
|
2548
|
+
reason: "OFFLINE"
|
|
2549
|
+
});
|
|
2550
|
+
continue;
|
|
2551
|
+
}
|
|
2552
|
+
const subject = priorSessions.find((session) => session.id === principal.id) ?? live.info;
|
|
2553
|
+
for (const [recipientId, recipient] of this.sessions) {
|
|
2554
|
+
if (recipientId !== principal.id && !changedIds.has(recipientId) && authorizeSessionAction(priorSessions, recipientId, "discover", principal.id).allowed && sameIntercomScope(recipient.scopeId, live.scopeId)) {
|
|
2555
|
+
writeMessage(recipient.socket, { type: "session_left", sessionId: principal.id });
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
this.clearPendingDeliveriesForSession(principal.id, live.socket);
|
|
2559
|
+
this.clearAskEdgesForSession(principal.id, "authorization_revoked");
|
|
2560
|
+
this.sessions.delete(principal.id);
|
|
2561
|
+
for (const [key, recent] of this.recentDeliveries) {
|
|
2562
|
+
if (recent.from === principal.id || recent.to === principal.id) this.recentDeliveries.delete(key);
|
|
2563
|
+
}
|
|
2564
|
+
this.audit.record({
|
|
2565
|
+
event: auditEvent,
|
|
2566
|
+
outcome: "allowed",
|
|
2567
|
+
actorId: principal.id,
|
|
2568
|
+
targetId: subject.parentSessionId,
|
|
2569
|
+
remoteHostId: principal.remoteHostId,
|
|
2570
|
+
generation: principal.generation,
|
|
2571
|
+
reason: "DISCONNECTED"
|
|
2572
|
+
});
|
|
2573
|
+
live.socket.destroy();
|
|
2574
|
+
}
|
|
2575
|
+
if (changed.length > 0) this.scheduleShutdownCheck();
|
|
2576
|
+
}
|
|
2577
|
+
isCurrentPrincipal(sessionId) {
|
|
2578
|
+
const session = this.sessions.get(sessionId);
|
|
2579
|
+
if (!session) return false;
|
|
2580
|
+
if (session.info.origin !== "remote") return true;
|
|
2581
|
+
try {
|
|
2582
|
+
this.accessRegistry.validatePrincipal(sessionId, session.info.generation ?? 0);
|
|
2583
|
+
return true;
|
|
2584
|
+
} catch {
|
|
2585
|
+
return false;
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
isAuthorized(actorId, action, targetId, bossContext) {
|
|
2589
|
+
if (!this.isCurrentPrincipal(actorId) || !this.isCurrentPrincipal(targetId)) return false;
|
|
2590
|
+
return authorizeSessionAction(
|
|
2591
|
+
Array.from(this.sessions.values(), (session) => session.info),
|
|
2592
|
+
actorId,
|
|
2593
|
+
action,
|
|
2594
|
+
targetId,
|
|
2595
|
+
bossContext
|
|
2596
|
+
).allowed;
|
|
2597
|
+
}
|
|
2598
|
+
broadcastLifecycle(message, subject, exclude) {
|
|
2599
|
+
for (const [id, session] of this.sessions) {
|
|
2600
|
+
if (id !== exclude && sameIntercomScope(session.scopeId, subject.scopeId) && this.isAuthorized(id, "discover", subject.info.id)) {
|
|
2601
|
+
writeMessage(session.socket, message);
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
askKey(fromSessionId, messageId) {
|
|
2606
|
+
return `${fromSessionId}\0${messageId}`;
|
|
2607
|
+
}
|
|
2608
|
+
deliveryKey(fromSessionId, messageId) {
|
|
2609
|
+
return `${fromSessionId}\0${messageId}`;
|
|
2610
|
+
}
|
|
2611
|
+
addAskEdge(messageId, from, to) {
|
|
2612
|
+
const key = this.askKey(from, messageId);
|
|
2613
|
+
const previous = this.askEdges.get(key);
|
|
2614
|
+
if (previous) {
|
|
2615
|
+
clearTimeout(previous.timeout);
|
|
2616
|
+
}
|
|
2617
|
+
const createdAt = Date.now();
|
|
2618
|
+
const expiresAt = createdAt + this.askTimeoutMs;
|
|
2619
|
+
this.askEdges.set(key, {
|
|
2620
|
+
messageId,
|
|
2621
|
+
from,
|
|
2622
|
+
to,
|
|
2623
|
+
createdAt,
|
|
2624
|
+
expiresAt,
|
|
2625
|
+
state: "blocking",
|
|
2626
|
+
timeout: this.scheduleAskExpiry(key, expiresAt)
|
|
2627
|
+
});
|
|
2628
|
+
this.persistAskEdges();
|
|
2629
|
+
}
|
|
2630
|
+
removeAskEdge(key, reason, notifyRecipient = false) {
|
|
2631
|
+
const edge = this.askEdges.get(key);
|
|
2632
|
+
if (!edge) {
|
|
2633
|
+
return;
|
|
2634
|
+
}
|
|
2635
|
+
clearTimeout(edge.timeout);
|
|
2636
|
+
this.askEdges.delete(key);
|
|
2637
|
+
this.persistAskEdges();
|
|
2638
|
+
if (reason && notifyRecipient) {
|
|
2639
|
+
this.notifyAskCancelled(edge.to, edge.messageId, edge.from, reason);
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
notifyAskDeferred(edge) {
|
|
2643
|
+
const recipient = this.sessions.get(edge.to);
|
|
2644
|
+
if (recipient) {
|
|
2645
|
+
writeMessage(recipient.socket, {
|
|
2646
|
+
type: "ask_deferred",
|
|
2647
|
+
messageId: edge.messageId,
|
|
2648
|
+
fromSessionId: edge.from
|
|
2649
|
+
});
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
notifyAskCancelled(sessionId, messageId, fromSessionId, reason) {
|
|
2653
|
+
const session = this.sessions.get(sessionId);
|
|
2654
|
+
if (session) {
|
|
2655
|
+
writeMessage(session.socket, { type: "ask_cancelled", messageId, fromSessionId, reason });
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
clearAskEdgesForSession(sessionId, reason) {
|
|
2659
|
+
let changed = false;
|
|
2660
|
+
for (const [key, edge] of this.askEdges) {
|
|
2661
|
+
if (edge.from === sessionId || edge.to === sessionId) {
|
|
2662
|
+
clearTimeout(edge.timeout);
|
|
2663
|
+
this.askEdges.delete(key);
|
|
2664
|
+
changed = true;
|
|
2665
|
+
if (edge.from === sessionId) {
|
|
2666
|
+
this.notifyAskCancelled(edge.to, edge.messageId, edge.from, reason);
|
|
2667
|
+
} else {
|
|
2668
|
+
this.notifyAskCancelled(edge.from, edge.messageId, edge.to, reason);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
if (changed) {
|
|
2673
|
+
this.persistAskEdges();
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
deferAskEdgesForSession(sessionId) {
|
|
2677
|
+
let changed = false;
|
|
2678
|
+
for (const edge of this.askEdges.values()) {
|
|
2679
|
+
if ((edge.from === sessionId || edge.to === sessionId) && edge.state === "blocking") {
|
|
2680
|
+
edge.state = "deferred";
|
|
2681
|
+
changed = true;
|
|
2682
|
+
if (edge.from === sessionId) {
|
|
2683
|
+
this.notifyAskDeferred(edge);
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
if (changed) {
|
|
2688
|
+
this.persistAskEdges();
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
scheduleAskExpiry(key, expiresAt) {
|
|
2692
|
+
const delay = Math.max(1, Math.min(expiresAt - Date.now(), 2147483647));
|
|
2693
|
+
const timeout = setTimeout(() => {
|
|
2694
|
+
if (expiresAt > Date.now()) {
|
|
2695
|
+
const edge = this.askEdges.get(key);
|
|
2696
|
+
if (edge) {
|
|
2697
|
+
clearTimeout(edge.timeout);
|
|
2698
|
+
edge.timeout = this.scheduleAskExpiry(key, expiresAt);
|
|
2699
|
+
}
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2702
|
+
this.removeAskEdge(key, "expired", true);
|
|
2703
|
+
}, delay);
|
|
2704
|
+
timeout.unref?.();
|
|
2705
|
+
return timeout;
|
|
2706
|
+
}
|
|
2707
|
+
loadAskEdges() {
|
|
2708
|
+
if (!existsSync3(ASK_STATE_PATH)) {
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
2711
|
+
try {
|
|
2712
|
+
const parsed = JSON.parse(readFileSync5(ASK_STATE_PATH, "utf-8"));
|
|
2713
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2714
|
+
throw new Error("expected an object");
|
|
2715
|
+
}
|
|
2716
|
+
const state = parsed;
|
|
2717
|
+
if (state.version !== 1) {
|
|
2718
|
+
throw new Error("unsupported state version");
|
|
2719
|
+
}
|
|
2720
|
+
const edges = state.edges;
|
|
2721
|
+
if (!Array.isArray(edges)) {
|
|
2722
|
+
throw new Error("expected an edges array");
|
|
2723
|
+
}
|
|
2724
|
+
const now = Date.now();
|
|
2725
|
+
for (const candidate of edges) {
|
|
2726
|
+
if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) {
|
|
2727
|
+
continue;
|
|
2728
|
+
}
|
|
2729
|
+
const edge = candidate;
|
|
2730
|
+
if (typeof edge.messageId !== "string" || edge.messageId.length === 0 || edge.messageId.length > MAX_MESSAGE_ID_LENGTH || !isSessionId(edge.from) || !isSessionId(edge.to) || typeof edge.createdAt !== "number" || !Number.isFinite(edge.createdAt) || typeof edge.expiresAt !== "number" || !Number.isFinite(edge.expiresAt) || edge.expiresAt <= now || edge.state !== "blocking" && edge.state !== "deferred") {
|
|
2731
|
+
continue;
|
|
2732
|
+
}
|
|
2733
|
+
const key = this.askKey(edge.from, edge.messageId);
|
|
2734
|
+
this.askEdges.set(key, {
|
|
2735
|
+
messageId: edge.messageId,
|
|
2736
|
+
from: edge.from,
|
|
2737
|
+
to: edge.to,
|
|
2738
|
+
createdAt: edge.createdAt,
|
|
2739
|
+
expiresAt: edge.expiresAt,
|
|
2740
|
+
state: "deferred",
|
|
2741
|
+
timeout: this.scheduleAskExpiry(key, edge.expiresAt)
|
|
2742
|
+
});
|
|
2743
|
+
}
|
|
2744
|
+
this.persistAskEdges();
|
|
2745
|
+
} catch (error) {
|
|
2746
|
+
console.error(`Failed to load persisted ask state at ${ASK_STATE_PATH}:`, error);
|
|
2747
|
+
for (const edge of this.askEdges.values()) {
|
|
2748
|
+
clearTimeout(edge.timeout);
|
|
2749
|
+
}
|
|
2750
|
+
this.askEdges.clear();
|
|
2751
|
+
try {
|
|
2752
|
+
const corruptPath = `${ASK_STATE_PATH}.corrupt-${Date.now()}`;
|
|
2753
|
+
renameSync3(ASK_STATE_PATH, corruptPath);
|
|
2754
|
+
restrictIntercomRuntimeFile(corruptPath);
|
|
2755
|
+
} catch {
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
persistAskEdges() {
|
|
2760
|
+
const edges = Array.from(this.askEdges.values(), (edge) => ({
|
|
2761
|
+
messageId: edge.messageId,
|
|
2762
|
+
from: edge.from,
|
|
2763
|
+
to: edge.to,
|
|
2764
|
+
createdAt: edge.createdAt,
|
|
2765
|
+
expiresAt: edge.expiresAt,
|
|
2766
|
+
state: edge.state
|
|
2767
|
+
}));
|
|
2768
|
+
writeDurableJson(ASK_STATE_PATH, { version: 1, edges });
|
|
2769
|
+
}
|
|
2770
|
+
countAskEdgesFrom(sessionId) {
|
|
2771
|
+
let count = 0;
|
|
2772
|
+
for (const edge of this.askEdges.values()) {
|
|
2773
|
+
if (edge.from === sessionId) {
|
|
2774
|
+
count += 1;
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
return count;
|
|
2778
|
+
}
|
|
2779
|
+
countPendingDeliveriesFrom(sessionId) {
|
|
2780
|
+
let count = 0;
|
|
2781
|
+
for (const delivery of this.pendingDeliveries.values()) {
|
|
2782
|
+
if (delivery.from === sessionId) {
|
|
2783
|
+
count += 1;
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
return count;
|
|
2787
|
+
}
|
|
2788
|
+
bossControlKey(fromSessionId, envelope) {
|
|
2789
|
+
return canonicalHash("agent-intercom-codex/boss-control/idempotency-scope/v1", {
|
|
2790
|
+
fromSessionId,
|
|
2791
|
+
bossRunId: envelope.bossRunId,
|
|
2792
|
+
participantId: envelope.participantId,
|
|
2793
|
+
bindingEpoch: Number(envelope.bindingEpoch),
|
|
2794
|
+
idempotencyKey: envelope.idempotencyKey
|
|
2795
|
+
});
|
|
2796
|
+
}
|
|
2797
|
+
bossControlFingerprint(toSessionId, envelope) {
|
|
2798
|
+
const { messageId: _transportMessageId, ...stableEnvelope } = envelope;
|
|
2799
|
+
return canonicalHash("agent-intercom-codex/boss-control/request/v1", { toSessionId, envelope: stableEnvelope });
|
|
2800
|
+
}
|
|
2801
|
+
acknowledgePendingBossControl(deliveryId, messageId, idempotencyKey, sessionId, socket) {
|
|
2802
|
+
const pending = this.pendingBossControls.get(deliveryId);
|
|
2803
|
+
if (!pending || pending.to !== sessionId || pending.recipientSocket !== socket || pending.messageId !== messageId || pending.envelope.idempotencyKey !== idempotencyKey) return;
|
|
2804
|
+
const sender = this.sessions.get(pending.from);
|
|
2805
|
+
const recipient = this.sessions.get(pending.to);
|
|
2806
|
+
const { controlKind } = bossControlKind(pending.envelope);
|
|
2807
|
+
if (!sender || !recipient || !authorizeSessionAction(
|
|
2808
|
+
Array.from(this.sessions.values(), (session) => session.info),
|
|
2809
|
+
pending.from,
|
|
2810
|
+
"control",
|
|
2811
|
+
pending.to,
|
|
2812
|
+
{ controlKind, correlated: hasAuthoritativeBossControlCorrelation() }
|
|
2813
|
+
).allowed) {
|
|
2814
|
+
this.failPendingBossControl(deliveryId, "POLICY_DENIED", "Boss control authorization changed before acknowledgement");
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2817
|
+
if (sender.socket === pending.senderSocket) {
|
|
2818
|
+
const result = {
|
|
2819
|
+
type: "boss_control_result",
|
|
2820
|
+
requestId: pending.requestId,
|
|
2821
|
+
messageId: pending.messageId,
|
|
2822
|
+
idempotencyKey: pending.envelope.idempotencyKey,
|
|
2823
|
+
status: "delivered",
|
|
2824
|
+
deliveryId,
|
|
2825
|
+
delivered: true
|
|
2826
|
+
};
|
|
2827
|
+
this.bossControlLedger.recordTerminal(pending.key, pending.fingerprint, result);
|
|
2828
|
+
clearTimeout(pending.timeout);
|
|
2829
|
+
this.pendingBossControls.delete(deliveryId);
|
|
2830
|
+
this.pendingBossControlKeys.delete(pending.key);
|
|
2831
|
+
writeMessage(sender.socket, result);
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
failPendingBossControl(deliveryId, code, reason) {
|
|
2835
|
+
const pending = this.pendingBossControls.get(deliveryId);
|
|
2836
|
+
if (!pending) return;
|
|
2837
|
+
const sender = this.sessions.get(pending.from);
|
|
2838
|
+
const result = {
|
|
2839
|
+
type: "boss_control_result",
|
|
2840
|
+
requestId: pending.requestId,
|
|
2841
|
+
messageId: pending.messageId,
|
|
2842
|
+
idempotencyKey: pending.envelope.idempotencyKey,
|
|
2843
|
+
status: "rejected",
|
|
2844
|
+
delivered: false,
|
|
2845
|
+
code,
|
|
2846
|
+
reason,
|
|
2847
|
+
deliveryId
|
|
2848
|
+
};
|
|
2849
|
+
this.bossControlLedger.recordTerminal(pending.key, pending.fingerprint, result);
|
|
2850
|
+
clearTimeout(pending.timeout);
|
|
2851
|
+
this.pendingBossControls.delete(deliveryId);
|
|
2852
|
+
this.pendingBossControlKeys.delete(pending.key);
|
|
2853
|
+
if (sender?.socket === pending.senderSocket) writeMessage(sender.socket, result);
|
|
2854
|
+
}
|
|
2855
|
+
clearPendingBossControlsForSession(sessionId, socket) {
|
|
2856
|
+
for (const pending of Array.from(this.pendingBossControls.values())) {
|
|
2857
|
+
if (pending.to === sessionId && pending.recipientSocket === socket) {
|
|
2858
|
+
this.failPendingBossControl(pending.deliveryId, "RECIPIENT_DISCONNECTED", "Boss control recipient disconnected before acknowledgement");
|
|
2859
|
+
} else if (pending.from === sessionId && pending.senderSocket === socket) {
|
|
2860
|
+
clearTimeout(pending.timeout);
|
|
2861
|
+
this.pendingBossControls.delete(pending.deliveryId);
|
|
2862
|
+
this.pendingBossControlKeys.delete(pending.key);
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
acknowledgePendingDelivery(deliveryId, sessionId, socket) {
|
|
2867
|
+
const pending = this.pendingDeliveries.get(deliveryId);
|
|
2868
|
+
if (!pending || pending.to !== sessionId || pending.recipientSocket !== socket) {
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
const sender = this.sessions.get(pending.from);
|
|
2872
|
+
const recipient = this.sessions.get(pending.to);
|
|
2873
|
+
if (!sender || !recipient || (sender.info.generation ?? 1) !== pending.fromGeneration || (recipient.info.generation ?? 1) !== pending.toGeneration || !this.isAuthorized(pending.from, pending.action, pending.to)) {
|
|
2874
|
+
this.failPendingDelivery(deliveryId, "SESSION_NOT_FOUND", "Delivery authorization changed before acknowledgement");
|
|
2875
|
+
return;
|
|
2876
|
+
}
|
|
2877
|
+
clearTimeout(pending.timeout);
|
|
2878
|
+
this.pendingDeliveries.delete(deliveryId);
|
|
2879
|
+
this.pendingDeliveryKeys.delete(pending.key);
|
|
2880
|
+
if (pending.message.replyTo) {
|
|
2881
|
+
this.removeAskEdge(this.askKey(pending.to, pending.message.replyTo));
|
|
2882
|
+
}
|
|
2883
|
+
const response = { type: "delivered", messageId: pending.message.id, deliveryId };
|
|
2884
|
+
this.recentDeliveries.set(pending.key, {
|
|
2885
|
+
fingerprint: pending.fingerprint,
|
|
2886
|
+
from: pending.from,
|
|
2887
|
+
to: pending.to,
|
|
2888
|
+
action: pending.action,
|
|
2889
|
+
fromGeneration: pending.fromGeneration,
|
|
2890
|
+
toGeneration: pending.toGeneration,
|
|
2891
|
+
retryable: false,
|
|
2892
|
+
response,
|
|
2893
|
+
expiresAt: Date.now() + RECENT_DELIVERY_TTL_MS
|
|
2894
|
+
});
|
|
2895
|
+
if (sender.socket === pending.senderSocket) {
|
|
2896
|
+
writeMessage(sender.socket, response);
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
failPendingDelivery(deliveryId, code, reason) {
|
|
2900
|
+
const pending = this.pendingDeliveries.get(deliveryId);
|
|
2901
|
+
if (!pending) {
|
|
2902
|
+
return;
|
|
2903
|
+
}
|
|
2904
|
+
clearTimeout(pending.timeout);
|
|
2905
|
+
this.pendingDeliveries.delete(deliveryId);
|
|
2906
|
+
this.pendingDeliveryKeys.delete(pending.key);
|
|
2907
|
+
if (pending.message.expectsReply) {
|
|
2908
|
+
this.removeAskEdge(this.askKey(pending.from, pending.message.id), "delivery_failed", true);
|
|
2909
|
+
}
|
|
2910
|
+
const response = {
|
|
2911
|
+
type: "delivery_failed",
|
|
2912
|
+
messageId: pending.message.id,
|
|
2913
|
+
accepted: true,
|
|
2914
|
+
code,
|
|
2915
|
+
reason
|
|
2916
|
+
};
|
|
2917
|
+
this.recentDeliveries.set(pending.key, {
|
|
2918
|
+
fingerprint: pending.fingerprint,
|
|
2919
|
+
from: pending.from,
|
|
2920
|
+
to: pending.to,
|
|
2921
|
+
action: pending.action,
|
|
2922
|
+
fromGeneration: pending.fromGeneration,
|
|
2923
|
+
toGeneration: pending.toGeneration,
|
|
2924
|
+
retryable: true,
|
|
2925
|
+
response,
|
|
2926
|
+
expiresAt: Date.now() + RECENT_DELIVERY_TTL_MS
|
|
2927
|
+
});
|
|
2928
|
+
const sender = this.sessions.get(pending.from);
|
|
2929
|
+
if (sender?.socket === pending.senderSocket) {
|
|
2930
|
+
writeMessage(sender.socket, response);
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
clearPendingDeliveriesForSession(sessionId, socket) {
|
|
2934
|
+
this.clearPendingBossControlsForSession(sessionId, socket);
|
|
2935
|
+
for (const delivery of Array.from(this.pendingDeliveries.values())) {
|
|
2936
|
+
if (delivery.to === sessionId && delivery.recipientSocket === socket) {
|
|
2937
|
+
this.failPendingDelivery(delivery.id, "RECIPIENT_DISCONNECTED", "Recipient disconnected before acknowledging the message");
|
|
2938
|
+
} else if (delivery.from === sessionId && delivery.senderSocket === socket) {
|
|
2939
|
+
this.failPendingDelivery(delivery.id, "SENDER_DISCONNECTED", "Sender disconnected before delivery was acknowledged");
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
pruneRecentDeliveries(now = Date.now()) {
|
|
2944
|
+
for (const [key, delivery] of this.recentDeliveries) {
|
|
2945
|
+
if (delivery.expiresAt <= now) {
|
|
2946
|
+
this.recentDeliveries.delete(key);
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
findSessions(actorId, nameOrId) {
|
|
2951
|
+
const byId = this.sessions.get(nameOrId);
|
|
2952
|
+
if (byId) return [byId];
|
|
2953
|
+
const actor = this.sessions.get(actorId);
|
|
2954
|
+
if (!actor) return [];
|
|
2955
|
+
const sameScope = Array.from(this.sessions.values()).filter(
|
|
2956
|
+
(session) => sameIntercomScope(actor.scopeId, session.scopeId)
|
|
2957
|
+
);
|
|
2958
|
+
const lowerName = nameOrId.toLowerCase();
|
|
2959
|
+
const byName = sameScope.filter((session) => session.info.name?.toLowerCase() === lowerName);
|
|
2960
|
+
if (byName.length > 0) return byName;
|
|
2961
|
+
return sameScope.filter((session) => session.info.id.startsWith(nameOrId));
|
|
2962
|
+
}
|
|
2963
|
+
shutdown() {
|
|
2964
|
+
console.log("Broker shutting down");
|
|
2965
|
+
if (this.expiryTimer) {
|
|
2966
|
+
clearInterval(this.expiryTimer);
|
|
2967
|
+
this.expiryTimer = null;
|
|
2968
|
+
}
|
|
2969
|
+
for (const session of this.sessions.values()) {
|
|
2970
|
+
session.socket.end();
|
|
2971
|
+
}
|
|
2972
|
+
this.sessions.clear();
|
|
2973
|
+
for (const delivery of this.pendingDeliveries.values()) {
|
|
2974
|
+
clearTimeout(delivery.timeout);
|
|
2975
|
+
}
|
|
2976
|
+
this.pendingDeliveries.clear();
|
|
2977
|
+
this.pendingDeliveryKeys.clear();
|
|
2978
|
+
for (const pending of this.pendingBossControls.values()) {
|
|
2979
|
+
clearTimeout(pending.timeout);
|
|
2980
|
+
}
|
|
2981
|
+
this.pendingBossControls.clear();
|
|
2982
|
+
this.pendingBossControlKeys.clear();
|
|
2983
|
+
for (const edge of this.askEdges.values()) {
|
|
2984
|
+
clearTimeout(edge.timeout);
|
|
2985
|
+
}
|
|
2986
|
+
this.askEdges.clear();
|
|
2987
|
+
const ownsBroker = hasBrokerOwnership(OWNER_PATH);
|
|
2988
|
+
if (ownsBroker && typeof LISTEN_TARGET === "string" && process.platform !== "win32") {
|
|
2989
|
+
for (const socketPath of [LISTEN_TARGET, REMOTE_LISTEN_TARGET]) {
|
|
2990
|
+
try {
|
|
2991
|
+
unlinkSync2(socketPath);
|
|
2992
|
+
} catch {
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
if (ownsBroker) {
|
|
2997
|
+
try {
|
|
2998
|
+
unlinkSync2(PORT_PATH);
|
|
2999
|
+
} catch {
|
|
3000
|
+
}
|
|
3001
|
+
try {
|
|
3002
|
+
unlinkSync2(PID_PATH);
|
|
3003
|
+
} catch {
|
|
3004
|
+
}
|
|
3005
|
+
releaseBrokerOwnership(OWNER_PATH);
|
|
3006
|
+
}
|
|
3007
|
+
this.server.close();
|
|
3008
|
+
this.remoteServer?.close();
|
|
3009
|
+
process.exit(0);
|
|
3010
|
+
}
|
|
3011
|
+
};
|
|
3012
|
+
new IntercomBroker().start();
|