@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/broker/broker.ts
ADDED
|
@@ -0,0 +1,2327 @@
|
|
|
1
|
+
import net from "net";
|
|
2
|
+
import { existsSync, readFileSync, renameSync, writeFileSync, unlinkSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { randomUUID } from "crypto";
|
|
5
|
+
import { types as nodeUtilTypes } from "node:util";
|
|
6
|
+
import { authorize, POLICY_SEMANTICS_HASH, POLICY_SEMANTICS_VERSION, type PolicyAction, type PolicyState } from "@ctliz/agent-intercom-core";
|
|
7
|
+
import type { BossAuthorizationContext, BossControlEnvelope, BossPolicyAction } from "@ctliz/agent-intercom-core/boss";
|
|
8
|
+
import { canonicalHash } from "@ctliz/agent-intercom-core/canonical";
|
|
9
|
+
import { writeMessage, createMessageReader } from "./framing.ts";
|
|
10
|
+
import { parseIntercomScopeId, sameIntercomScope } from "../protocol-v4/contract.ts";
|
|
11
|
+
import {
|
|
12
|
+
ensureIntercomRuntimeDir,
|
|
13
|
+
getBrokerAccessStateFilePath,
|
|
14
|
+
getBrokerAdminCredentialFilePath,
|
|
15
|
+
getBrokerAskStateFilePath,
|
|
16
|
+
getBrokerAuditFilePath,
|
|
17
|
+
getBrokerListenTarget,
|
|
18
|
+
getBrokerPortFilePath,
|
|
19
|
+
getIntercomDirPath,
|
|
20
|
+
getRemoteGatewaySocketPath,
|
|
21
|
+
INTERCOM_PROTOCOL_NAME,
|
|
22
|
+
INTERCOM_PROTOCOL_VERSION,
|
|
23
|
+
INTERCOM_RUNTIME_FILE_MODE,
|
|
24
|
+
restrictIntercomRuntimeFile,
|
|
25
|
+
type BrokerConnectTarget,
|
|
26
|
+
} from "./paths.ts";
|
|
27
|
+
import { getAskTimeoutMs } from "../config.ts";
|
|
28
|
+
import { writeDurableJson } from "../durable-json.ts";
|
|
29
|
+
import { acquireBrokerOwnership, hasBrokerOwnership, releaseBrokerOwnership } from "./ownership.ts";
|
|
30
|
+
import { RemoteAccessRegistry, type RemotePrincipalMetadata, type RemotePrincipalRecord } from "./access-registry.ts";
|
|
31
|
+
import { authorizeSessionAction, visibleSessions } from "./authorization.ts";
|
|
32
|
+
import {
|
|
33
|
+
assertBossControlSender,
|
|
34
|
+
bossCapabilityAdvertisement,
|
|
35
|
+
bossControlKind,
|
|
36
|
+
exactBossSessionTarget,
|
|
37
|
+
hasAuthoritativeBossControlCorrelation,
|
|
38
|
+
parseExactRegistrationFrame,
|
|
39
|
+
parseBossParticipantRegistrationMetadata,
|
|
40
|
+
} from "./boss-adapter.ts";
|
|
41
|
+
import { BrokerAuditLog } from "./audit.ts";
|
|
42
|
+
import {
|
|
43
|
+
BossControlResultLedger,
|
|
44
|
+
bossControlAcceptedRecoveryFrames,
|
|
45
|
+
bossControlReplayFrames,
|
|
46
|
+
type BossControlResult,
|
|
47
|
+
} from "./boss-control-ledger.ts";
|
|
48
|
+
import type {
|
|
49
|
+
AskCancellationReason,
|
|
50
|
+
BrokerErrorCode,
|
|
51
|
+
BrokerMessage,
|
|
52
|
+
DeliveryFailureCode,
|
|
53
|
+
Message,
|
|
54
|
+
Attachment,
|
|
55
|
+
BossControlFailureCode,
|
|
56
|
+
SessionInfo,
|
|
57
|
+
SessionRegistration,
|
|
58
|
+
RemotePrincipalSummary,
|
|
59
|
+
} from "../types.ts";
|
|
60
|
+
|
|
61
|
+
const INTERCOM_DIR = getIntercomDirPath();
|
|
62
|
+
const LISTEN_TARGET = getBrokerListenTarget();
|
|
63
|
+
const REMOTE_LISTEN_TARGET = getRemoteGatewaySocketPath();
|
|
64
|
+
const PID_PATH = join(INTERCOM_DIR, "broker.pid");
|
|
65
|
+
const OWNER_PATH = join(INTERCOM_DIR, "broker.owner");
|
|
66
|
+
const PORT_PATH = getBrokerPortFilePath(INTERCOM_DIR);
|
|
67
|
+
const ASK_STATE_PATH = getBrokerAskStateFilePath(INTERCOM_DIR);
|
|
68
|
+
const ACCESS_STATE_PATH = getBrokerAccessStateFilePath(INTERCOM_DIR);
|
|
69
|
+
const ADMIN_CREDENTIAL_PATH = getBrokerAdminCredentialFilePath(INTERCOM_DIR);
|
|
70
|
+
const AUDIT_PATH = getBrokerAuditFilePath(INTERCOM_DIR);
|
|
71
|
+
const BOSS_CONTROL_LEDGER_PATH = join(INTERCOM_DIR, "boss-control-results.json");
|
|
72
|
+
const BROKER_STATE_ID = randomUUID();
|
|
73
|
+
const MAX_SESSIONS = 128;
|
|
74
|
+
const MAX_UNREGISTERED_CONNECTIONS = 32;
|
|
75
|
+
const REGISTRATION_TIMEOUT_MS = 1000;
|
|
76
|
+
const RATE_LIMIT_CAPACITY = 240;
|
|
77
|
+
const RATE_LIMIT_REFILL_PER_SECOND = 120;
|
|
78
|
+
const REMOTE_RATE_LIMIT_CAPACITY = 60;
|
|
79
|
+
const REMOTE_RATE_LIMIT_REFILL_PER_SECOND = 30;
|
|
80
|
+
const REMOTE_EXPIRY_SWEEP_MS = Math.max(50, Number.parseInt(process.env.PI_INTERCOM_REMOTE_EXPIRY_SWEEP_MS ?? "1000", 10) || 1000);
|
|
81
|
+
const PRESENCE_HEARTBEAT_MS = 1000;
|
|
82
|
+
const DELIVERY_ACK_TIMEOUT_MS = 8000;
|
|
83
|
+
const RECENT_DELIVERY_TTL_MS = 10 * 60 * 1000;
|
|
84
|
+
const MAX_PENDING_DELIVERIES = 1024;
|
|
85
|
+
const MAX_PENDING_DELIVERIES_PER_SESSION = 64;
|
|
86
|
+
const MAX_PENDING_ASKS_PER_SESSION = 64;
|
|
87
|
+
const RATE_LIMIT_BYTES_PER_TOKEN = 8 * 1024;
|
|
88
|
+
const MAX_MESSAGE_TEXT_BYTES = 256 * 1024;
|
|
89
|
+
const MAX_ATTACHMENT_CONTENT_BYTES = 512 * 1024;
|
|
90
|
+
const MAX_ATTACHMENTS = 16;
|
|
91
|
+
const MAX_MESSAGE_ID_LENGTH = 256;
|
|
92
|
+
const MAX_TARGET_LENGTH = 512;
|
|
93
|
+
const MAX_SESSION_NAME_LENGTH = 256;
|
|
94
|
+
const MAX_SESSION_CWD_LENGTH = 4096;
|
|
95
|
+
const MAX_SESSION_MODEL_LENGTH = 512;
|
|
96
|
+
const MAX_SESSION_STATUS_LENGTH = 512;
|
|
97
|
+
const MAX_RUNTIME_INSTANCE_ID_LENGTH = 256;
|
|
98
|
+
|
|
99
|
+
interface ConnectedSession {
|
|
100
|
+
socket: net.Socket;
|
|
101
|
+
info: SessionInfo;
|
|
102
|
+
scopeId?: string;
|
|
103
|
+
runtimeInstanceId?: string;
|
|
104
|
+
lastPresenceBroadcastAt: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type ConnectionOrigin = "local" | "remote";
|
|
108
|
+
|
|
109
|
+
interface ConnectionState {
|
|
110
|
+
socket: net.Socket;
|
|
111
|
+
origin: ConnectionOrigin;
|
|
112
|
+
tokens: number;
|
|
113
|
+
refillPerSecond: number;
|
|
114
|
+
lastRefillAt: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface AskEdge {
|
|
118
|
+
messageId: string;
|
|
119
|
+
from: string;
|
|
120
|
+
to: string;
|
|
121
|
+
createdAt: number;
|
|
122
|
+
expiresAt: number;
|
|
123
|
+
state: "blocking" | "deferred";
|
|
124
|
+
timeout: NodeJS.Timeout;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface PersistedAskEdge {
|
|
128
|
+
messageId: string;
|
|
129
|
+
from: string;
|
|
130
|
+
to: string;
|
|
131
|
+
createdAt: number;
|
|
132
|
+
expiresAt: number;
|
|
133
|
+
state: "blocking" | "deferred";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface PendingDelivery {
|
|
137
|
+
id: string;
|
|
138
|
+
key: string;
|
|
139
|
+
fingerprint: string;
|
|
140
|
+
message: Message;
|
|
141
|
+
from: string;
|
|
142
|
+
to: string;
|
|
143
|
+
senderSocket: net.Socket;
|
|
144
|
+
recipientSocket: net.Socket;
|
|
145
|
+
action: PolicyAction;
|
|
146
|
+
fromGeneration: number;
|
|
147
|
+
toGeneration: number;
|
|
148
|
+
timeout: NodeJS.Timeout;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface PendingBossControl {
|
|
152
|
+
deliveryId: string;
|
|
153
|
+
key: string;
|
|
154
|
+
fingerprint: string;
|
|
155
|
+
requestId: string;
|
|
156
|
+
messageId: string;
|
|
157
|
+
envelope: BossControlEnvelope;
|
|
158
|
+
from: string;
|
|
159
|
+
to: string;
|
|
160
|
+
senderSocket: net.Socket;
|
|
161
|
+
recipientSocket: net.Socket;
|
|
162
|
+
timeout: NodeJS.Timeout;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
interface RecentDelivery {
|
|
166
|
+
fingerprint: string;
|
|
167
|
+
from: string;
|
|
168
|
+
to: string;
|
|
169
|
+
action: PolicyAction;
|
|
170
|
+
fromGeneration: number;
|
|
171
|
+
toGeneration: number;
|
|
172
|
+
retryable: boolean;
|
|
173
|
+
response:
|
|
174
|
+
| { type: "delivered"; messageId: string; deliveryId: string }
|
|
175
|
+
| { type: "delivery_failed"; messageId: string; accepted: boolean; code: DeliveryFailureCode; reason: string };
|
|
176
|
+
expiresAt: number;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function isAttachment(value: unknown): value is Attachment {
|
|
180
|
+
if (typeof value !== "object" || value === null) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const attachment = value as Record<string, unknown>;
|
|
185
|
+
|
|
186
|
+
if (
|
|
187
|
+
attachment.type !== "file"
|
|
188
|
+
&& attachment.type !== "snippet"
|
|
189
|
+
&& attachment.type !== "context"
|
|
190
|
+
) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (
|
|
195
|
+
typeof attachment.name !== "string"
|
|
196
|
+
|| attachment.name.length > 256
|
|
197
|
+
|| typeof attachment.content !== "string"
|
|
198
|
+
|| Buffer.byteLength(attachment.content, "utf-8") > MAX_ATTACHMENT_CONTENT_BYTES
|
|
199
|
+
) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return attachment.language === undefined || typeof attachment.language === "string";
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function isMessage(value: unknown): value is Message {
|
|
207
|
+
if (typeof value !== "object" || value === null) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const message = value as Record<string, unknown>;
|
|
212
|
+
|
|
213
|
+
if (
|
|
214
|
+
typeof message.id !== "string"
|
|
215
|
+
|| message.id.length === 0
|
|
216
|
+
|| message.id.length > MAX_MESSAGE_ID_LENGTH
|
|
217
|
+
|| typeof message.timestamp !== "number"
|
|
218
|
+
|| !Number.isFinite(message.timestamp)
|
|
219
|
+
) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (
|
|
224
|
+
message.replyTo !== undefined
|
|
225
|
+
&& (typeof message.replyTo !== "string" || message.replyTo.length === 0 || message.replyTo.length > MAX_MESSAGE_ID_LENGTH)
|
|
226
|
+
) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (message.expectsReply !== undefined && typeof message.expectsReply !== "boolean") {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (typeof message.content !== "object" || message.content === null) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const content = message.content as Record<string, unknown>;
|
|
239
|
+
if (typeof content.text !== "string" || Buffer.byteLength(content.text, "utf-8") > MAX_MESSAGE_TEXT_BYTES) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return content.attachments === undefined
|
|
244
|
+
|| (
|
|
245
|
+
Array.isArray(content.attachments)
|
|
246
|
+
&& content.attachments.length <= MAX_ATTACHMENTS
|
|
247
|
+
&& content.attachments.every(isAttachment)
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function isSessionId(value: unknown): value is string {
|
|
252
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function isSessionRegistration(value: unknown): value is SessionRegistration {
|
|
256
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const session = value as Record<string, unknown>;
|
|
261
|
+
|
|
262
|
+
if (
|
|
263
|
+
typeof session.cwd !== "string"
|
|
264
|
+
|| session.cwd.length === 0
|
|
265
|
+
|| session.cwd.length > MAX_SESSION_CWD_LENGTH
|
|
266
|
+
|| typeof session.model !== "string"
|
|
267
|
+
|| session.model.length === 0
|
|
268
|
+
|| session.model.length > MAX_SESSION_MODEL_LENGTH
|
|
269
|
+
|| typeof session.pid !== "number"
|
|
270
|
+
|| !Number.isFinite(session.pid)
|
|
271
|
+
|| typeof session.startedAt !== "number"
|
|
272
|
+
|| !Number.isFinite(session.startedAt)
|
|
273
|
+
|| typeof session.lastActivity !== "number"
|
|
274
|
+
|| !Number.isFinite(session.lastActivity)
|
|
275
|
+
) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (session.boss !== undefined) {
|
|
280
|
+
try {
|
|
281
|
+
parseBossParticipantRegistrationMetadata(session.boss);
|
|
282
|
+
} catch {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (session.name !== undefined && (typeof session.name !== "string" || session.name.length > MAX_SESSION_NAME_LENGTH)) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
if (
|
|
291
|
+
session.runtimeInstanceId !== undefined
|
|
292
|
+
&& (
|
|
293
|
+
typeof session.runtimeInstanceId !== "string"
|
|
294
|
+
|| session.runtimeInstanceId.length === 0
|
|
295
|
+
|| session.runtimeInstanceId.length > MAX_RUNTIME_INSTANCE_ID_LENGTH
|
|
296
|
+
)
|
|
297
|
+
) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return session.status === undefined
|
|
302
|
+
|| (typeof session.status === "string" && session.status.length <= MAX_SESSION_STATUS_LENGTH);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function isSameLocalRuntime(previous: ConnectedSession, registration: SessionRegistration): boolean {
|
|
306
|
+
if (previous.runtimeInstanceId !== undefined || registration.runtimeInstanceId !== undefined) {
|
|
307
|
+
return previous.runtimeInstanceId !== undefined
|
|
308
|
+
&& previous.runtimeInstanceId === registration.runtimeInstanceId;
|
|
309
|
+
}
|
|
310
|
+
return previous.info.pid === registration.pid
|
|
311
|
+
&& previous.info.startedAt === registration.startedAt;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
class IntercomBroker {
|
|
315
|
+
private sessions = new Map<string, ConnectedSession>();
|
|
316
|
+
private askEdges = new Map<string, AskEdge>();
|
|
317
|
+
private pendingDeliveries = new Map<string, PendingDelivery>();
|
|
318
|
+
private pendingDeliveryKeys = new Map<string, string>();
|
|
319
|
+
private recentDeliveries = new Map<string, RecentDelivery>();
|
|
320
|
+
private pendingBossControls = new Map<string, PendingBossControl>();
|
|
321
|
+
private pendingBossControlKeys = new Map<string, string>();
|
|
322
|
+
private connections = new Set<net.Socket>();
|
|
323
|
+
private unregisteredConnections = new Set<net.Socket>();
|
|
324
|
+
private server: net.Server;
|
|
325
|
+
private remoteServer: net.Server | null = null;
|
|
326
|
+
private shutdownTimer: NodeJS.Timeout | null = null;
|
|
327
|
+
private expiryTimer: NodeJS.Timeout | null = null;
|
|
328
|
+
private readonly askTimeoutMs = getAskTimeoutMs();
|
|
329
|
+
private readonly accessRegistry: RemoteAccessRegistry;
|
|
330
|
+
private readonly audit: BrokerAuditLog;
|
|
331
|
+
private readonly bossControlLedger: BossControlResultLedger;
|
|
332
|
+
|
|
333
|
+
constructor() {
|
|
334
|
+
ensureIntercomRuntimeDir(INTERCOM_DIR);
|
|
335
|
+
acquireBrokerOwnership(OWNER_PATH);
|
|
336
|
+
this.accessRegistry = new RemoteAccessRegistry(ACCESS_STATE_PATH);
|
|
337
|
+
this.audit = new BrokerAuditLog(AUDIT_PATH);
|
|
338
|
+
this.bossControlLedger = new BossControlResultLedger(BOSS_CONTROL_LEDGER_PATH);
|
|
339
|
+
this.accessRegistry.ensureAdminCredential(ADMIN_CREDENTIAL_PATH);
|
|
340
|
+
this.loadAskEdges();
|
|
341
|
+
if (typeof LISTEN_TARGET === "string" && process.platform !== "win32") {
|
|
342
|
+
for (const socketPath of [LISTEN_TARGET, REMOTE_LISTEN_TARGET]) {
|
|
343
|
+
try {
|
|
344
|
+
unlinkSync(socketPath);
|
|
345
|
+
} catch {
|
|
346
|
+
// A clean startup has no stale socket to remove.
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
this.server = net.createServer((socket) => this.handleConnection(socket, "local"));
|
|
351
|
+
if (process.platform !== "win32" && typeof LISTEN_TARGET === "string") {
|
|
352
|
+
this.remoteServer = net.createServer((socket) => this.handleConnection(socket, "remote"));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
start(): void {
|
|
357
|
+
let localListening = false;
|
|
358
|
+
let remoteListening = this.remoteServer === null;
|
|
359
|
+
const announceWhenReady = () => {
|
|
360
|
+
if (!localListening || !remoteListening) return;
|
|
361
|
+
writeFileSync(PID_PATH, String(process.pid), { mode: INTERCOM_RUNTIME_FILE_MODE });
|
|
362
|
+
restrictIntercomRuntimeFile(PID_PATH);
|
|
363
|
+
console.log(`Intercom broker started (pid: ${process.pid}, remote-access-v1)`);
|
|
364
|
+
};
|
|
365
|
+
const onLocalListening = () => {
|
|
366
|
+
if (typeof LISTEN_TARGET === "string") {
|
|
367
|
+
restrictIntercomRuntimeFile(LISTEN_TARGET);
|
|
368
|
+
} else {
|
|
369
|
+
const address = this.server.address();
|
|
370
|
+
if (!address || typeof address === "string") {
|
|
371
|
+
throw new Error("Intercom TCP broker started without a TCP address");
|
|
372
|
+
}
|
|
373
|
+
const endpoint: BrokerConnectTarget = {
|
|
374
|
+
transport: "tcp",
|
|
375
|
+
host: LISTEN_TARGET.host,
|
|
376
|
+
port: address.port,
|
|
377
|
+
stateId: BROKER_STATE_ID,
|
|
378
|
+
};
|
|
379
|
+
writeFileSync(PORT_PATH, `${JSON.stringify(endpoint)}\n`, { mode: INTERCOM_RUNTIME_FILE_MODE });
|
|
380
|
+
restrictIntercomRuntimeFile(PORT_PATH);
|
|
381
|
+
}
|
|
382
|
+
localListening = true;
|
|
383
|
+
announceWhenReady();
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
if (typeof LISTEN_TARGET === "string") {
|
|
387
|
+
this.server.listen(LISTEN_TARGET, onLocalListening);
|
|
388
|
+
} else {
|
|
389
|
+
this.server.listen({ host: LISTEN_TARGET.host, port: LISTEN_TARGET.port }, onLocalListening);
|
|
390
|
+
}
|
|
391
|
+
if (this.remoteServer) {
|
|
392
|
+
this.remoteServer.listen(REMOTE_LISTEN_TARGET, () => {
|
|
393
|
+
restrictIntercomRuntimeFile(REMOTE_LISTEN_TARGET);
|
|
394
|
+
remoteListening = true;
|
|
395
|
+
announceWhenReady();
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
this.expiryTimer = setInterval(() => this.reconcileExpiredPrincipals(), REMOTE_EXPIRY_SWEEP_MS);
|
|
399
|
+
this.expiryTimer.unref?.();
|
|
400
|
+
process.on("SIGTERM", () => this.shutdown());
|
|
401
|
+
process.on("SIGINT", () => this.shutdown());
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private handleConnection(socket: net.Socket, origin: ConnectionOrigin): void {
|
|
405
|
+
this.connections.add(socket);
|
|
406
|
+
let sessionId: string | null = null;
|
|
407
|
+
let registrationTimeout: NodeJS.Timeout | null = null;
|
|
408
|
+
const armRegistrationTimeout = () => {
|
|
409
|
+
if (registrationTimeout) {
|
|
410
|
+
clearTimeout(registrationTimeout);
|
|
411
|
+
}
|
|
412
|
+
this.unregisteredConnections.delete(socket);
|
|
413
|
+
this.unregisteredConnections.add(socket);
|
|
414
|
+
this.evictOldestUnregisteredConnections(socket);
|
|
415
|
+
registrationTimeout = setTimeout(() => {
|
|
416
|
+
if (!sessionId) {
|
|
417
|
+
socket.destroy();
|
|
418
|
+
}
|
|
419
|
+
}, REGISTRATION_TIMEOUT_MS);
|
|
420
|
+
registrationTimeout.unref?.();
|
|
421
|
+
};
|
|
422
|
+
const clearRegistrationTimeout = () => {
|
|
423
|
+
if (registrationTimeout) {
|
|
424
|
+
clearTimeout(registrationTimeout);
|
|
425
|
+
registrationTimeout = null;
|
|
426
|
+
}
|
|
427
|
+
this.unregisteredConnections.delete(socket);
|
|
428
|
+
};
|
|
429
|
+
armRegistrationTimeout();
|
|
430
|
+
const connection: ConnectionState = {
|
|
431
|
+
socket,
|
|
432
|
+
origin,
|
|
433
|
+
tokens: origin === "remote" ? REMOTE_RATE_LIMIT_CAPACITY : RATE_LIMIT_CAPACITY,
|
|
434
|
+
refillPerSecond: origin === "remote" ? REMOTE_RATE_LIMIT_REFILL_PER_SECOND : RATE_LIMIT_REFILL_PER_SECOND,
|
|
435
|
+
lastRefillAt: Date.now(),
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const reader = createMessageReader((msg) => {
|
|
439
|
+
const byteCost = Math.max(1, Math.ceil(Buffer.byteLength(JSON.stringify(msg), "utf-8") / RATE_LIMIT_BYTES_PER_TOKEN));
|
|
440
|
+
if (!this.consumeToken(connection, byteCost)) {
|
|
441
|
+
this.sendError(socket, "RATE_LIMITED", "Intercom broker rate limit exceeded");
|
|
442
|
+
socket.destroy(new Error("Intercom broker rate limit exceeded"));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
this.handleMessage(socket, origin, msg, sessionId, (id) => {
|
|
447
|
+
sessionId = id;
|
|
448
|
+
if (id) {
|
|
449
|
+
clearRegistrationTimeout();
|
|
450
|
+
} else {
|
|
451
|
+
armRegistrationTimeout();
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
} catch (error) {
|
|
455
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
456
|
+
if (reason === "Invalid intercom TCP endpoint credentials") {
|
|
457
|
+
socket.destroy();
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
this.sendError(socket, "INVALID_REQUEST", reason);
|
|
461
|
+
socket.end();
|
|
462
|
+
}
|
|
463
|
+
}, (error) => {
|
|
464
|
+
socket.destroy(error);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
socket.on("data", reader);
|
|
468
|
+
|
|
469
|
+
socket.on("close", () => {
|
|
470
|
+
clearRegistrationTimeout();
|
|
471
|
+
this.connections.delete(socket);
|
|
472
|
+
if (sessionId) {
|
|
473
|
+
const existing = this.sessions.get(sessionId);
|
|
474
|
+
if (existing?.socket === socket) {
|
|
475
|
+
if (existing.info.origin === "remote") {
|
|
476
|
+
this.audit.tryRecord({
|
|
477
|
+
event: "remote_disconnect",
|
|
478
|
+
outcome: "observed",
|
|
479
|
+
actorId: sessionId,
|
|
480
|
+
remoteHostId: existing.info.remoteHostId,
|
|
481
|
+
generation: existing.info.generation,
|
|
482
|
+
reason: "SOCKET_CLOSED",
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
this.broadcastLifecycle({ type: "session_left", sessionId }, existing, sessionId);
|
|
486
|
+
this.sessions.delete(sessionId);
|
|
487
|
+
this.clearPendingDeliveriesForSession(sessionId, socket);
|
|
488
|
+
this.deferAskEdgesForSession(sessionId);
|
|
489
|
+
this.scheduleShutdownCheck();
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
socket.on("error", (error) => {
|
|
495
|
+
console.error("Socket error:", error);
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
private evictOldestUnregisteredConnections(currentSocket: net.Socket): void {
|
|
500
|
+
while (this.unregisteredConnections.size > MAX_UNREGISTERED_CONNECTIONS) {
|
|
501
|
+
const [oldest] = this.unregisteredConnections;
|
|
502
|
+
if (!oldest) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (oldest === currentSocket && this.unregisteredConnections.size === 1) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
this.unregisteredConnections.delete(oldest);
|
|
509
|
+
oldest.destroy();
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private consumeToken(connection: ConnectionState, cost = 1, now = Date.now()): boolean {
|
|
514
|
+
const elapsedMs = now - connection.lastRefillAt;
|
|
515
|
+
if (elapsedMs > 0) {
|
|
516
|
+
connection.tokens = Math.min(
|
|
517
|
+
connection.origin === "remote" ? REMOTE_RATE_LIMIT_CAPACITY : RATE_LIMIT_CAPACITY,
|
|
518
|
+
connection.tokens + elapsedMs * connection.refillPerSecond / 1000,
|
|
519
|
+
);
|
|
520
|
+
connection.lastRefillAt = now;
|
|
521
|
+
}
|
|
522
|
+
if (connection.tokens < cost) {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
connection.tokens -= cost;
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
private sendError(socket: net.Socket, code: BrokerErrorCode, error: string): void {
|
|
530
|
+
writeMessage(socket, { type: "error", code, error });
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
private sendDeliveryFailure(
|
|
534
|
+
socket: net.Socket,
|
|
535
|
+
messageId: string,
|
|
536
|
+
accepted: boolean,
|
|
537
|
+
code: DeliveryFailureCode,
|
|
538
|
+
reason: string,
|
|
539
|
+
): void {
|
|
540
|
+
writeMessage(socket, { type: "delivery_failed", messageId, accepted, code, reason });
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private sendBossControlFailure(
|
|
544
|
+
socket: net.Socket,
|
|
545
|
+
requestId: string,
|
|
546
|
+
messageId: string,
|
|
547
|
+
idempotencyKey: string,
|
|
548
|
+
code: BossControlFailureCode,
|
|
549
|
+
reason: string,
|
|
550
|
+
deliveryId?: string,
|
|
551
|
+
ledgerBinding?: { scope: string; fingerprint: string },
|
|
552
|
+
acknowledgeAcceptedRecovery = false,
|
|
553
|
+
): void {
|
|
554
|
+
const result: BossControlResult = {
|
|
555
|
+
type: "boss_control_result",
|
|
556
|
+
requestId,
|
|
557
|
+
messageId,
|
|
558
|
+
idempotencyKey,
|
|
559
|
+
status: "rejected",
|
|
560
|
+
delivered: false,
|
|
561
|
+
code,
|
|
562
|
+
reason,
|
|
563
|
+
...(deliveryId === undefined ? {} : { deliveryId }),
|
|
564
|
+
};
|
|
565
|
+
if (ledgerBinding) {
|
|
566
|
+
this.bossControlLedger.recordTerminal(ledgerBinding.scope, ledgerBinding.fingerprint, result);
|
|
567
|
+
}
|
|
568
|
+
if (acknowledgeAcceptedRecovery) {
|
|
569
|
+
for (const frame of bossControlAcceptedRecoveryFrames(result)) writeMessage(socket, frame);
|
|
570
|
+
} else {
|
|
571
|
+
writeMessage(socket, result);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private scheduleShutdownCheck(): void {
|
|
576
|
+
if (this.shutdownTimer) return;
|
|
577
|
+
|
|
578
|
+
this.shutdownTimer = setTimeout(() => {
|
|
579
|
+
this.shutdownTimer = null;
|
|
580
|
+
if (this.sessions.size === 0) {
|
|
581
|
+
console.log("No sessions connected, shutting down");
|
|
582
|
+
this.shutdown();
|
|
583
|
+
}
|
|
584
|
+
}, 5000);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
private handleMessage(
|
|
588
|
+
socket: net.Socket,
|
|
589
|
+
origin: ConnectionOrigin,
|
|
590
|
+
msg: unknown,
|
|
591
|
+
currentId: string | null,
|
|
592
|
+
setId: (id: string | null) => void,
|
|
593
|
+
): void {
|
|
594
|
+
if (typeof msg !== "object" || msg === null || nodeUtilTypes.isProxy(msg)) {
|
|
595
|
+
throw new Error("Invalid client message");
|
|
596
|
+
}
|
|
597
|
+
const typeDescriptor = Object.getOwnPropertyDescriptor(msg, "type");
|
|
598
|
+
if (
|
|
599
|
+
typeDescriptor === undefined
|
|
600
|
+
|| !typeDescriptor.enumerable
|
|
601
|
+
|| !Object.hasOwn(typeDescriptor, "value")
|
|
602
|
+
|| typeof typeDescriptor.value !== "string"
|
|
603
|
+
) throw new Error("Invalid client message");
|
|
604
|
+
|
|
605
|
+
const clientMessage = msg as { type: string } & Record<string, unknown>;
|
|
606
|
+
const requiresEndpointAuth = typeof LISTEN_TARGET !== "string";
|
|
607
|
+
const hasEndpointAuth = clientMessage.stateId === BROKER_STATE_ID;
|
|
608
|
+
|
|
609
|
+
if (clientMessage.type === "health") {
|
|
610
|
+
if (typeof clientMessage.requestId !== "string") {
|
|
611
|
+
throw new Error("Invalid health message");
|
|
612
|
+
}
|
|
613
|
+
if (requiresEndpointAuth && !hasEndpointAuth) {
|
|
614
|
+
throw new Error("Invalid intercom TCP endpoint credentials");
|
|
615
|
+
}
|
|
616
|
+
writeMessage(socket, {
|
|
617
|
+
type: "health_ok",
|
|
618
|
+
requestId: clientMessage.requestId,
|
|
619
|
+
protocol: INTERCOM_PROTOCOL_NAME,
|
|
620
|
+
version: INTERCOM_PROTOCOL_VERSION,
|
|
621
|
+
endpoint: origin,
|
|
622
|
+
remoteAccess: this.remoteAccessContract(),
|
|
623
|
+
...(bossCapabilityAdvertisement() === undefined
|
|
624
|
+
? {}
|
|
625
|
+
: { capabilities: bossCapabilityAdvertisement() }),
|
|
626
|
+
});
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (clientMessage.type === "access_control") {
|
|
631
|
+
if (currentId !== null) {
|
|
632
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote access control requires a short-lived control connection");
|
|
633
|
+
socket.end();
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (origin === "local") this.handleAccessControl(socket, clientMessage);
|
|
637
|
+
else this.handleRemoteAccessControl(socket, clientMessage);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (requiresEndpointAuth && clientMessage.type === "register" && !hasEndpointAuth) {
|
|
642
|
+
throw new Error("Invalid intercom TCP endpoint credentials");
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (currentId === null && clientMessage.type !== "register") {
|
|
646
|
+
throw new Error(`Received ${clientMessage.type} before register`);
|
|
647
|
+
}
|
|
648
|
+
if (currentId) {
|
|
649
|
+
const bound = this.sessions.get(currentId);
|
|
650
|
+
if (bound?.socket !== socket) {
|
|
651
|
+
socket.destroy();
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (!this.isCurrentPrincipal(currentId)) {
|
|
655
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote session authorization is no longer valid");
|
|
656
|
+
socket.destroy();
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
switch (clientMessage.type) {
|
|
662
|
+
case "register": {
|
|
663
|
+
try {
|
|
664
|
+
parseExactRegistrationFrame(clientMessage);
|
|
665
|
+
} catch (error) {
|
|
666
|
+
this.sendError(socket, "BOSS_CONTRACT_MISMATCH", error instanceof Error ? error.message : "Registration contract is invalid");
|
|
667
|
+
socket.end();
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (
|
|
672
|
+
clientMessage.protocol !== INTERCOM_PROTOCOL_NAME
|
|
673
|
+
|| clientMessage.version !== INTERCOM_PROTOCOL_VERSION
|
|
674
|
+
) {
|
|
675
|
+
this.sendError(
|
|
676
|
+
socket,
|
|
677
|
+
"PROTOCOL_MISMATCH",
|
|
678
|
+
`Unsupported intercom protocol; expected ${INTERCOM_PROTOCOL_NAME} v${INTERCOM_PROTOCOL_VERSION}`,
|
|
679
|
+
);
|
|
680
|
+
socket.end();
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
let scopeId: string | undefined;
|
|
684
|
+
try {
|
|
685
|
+
parseIntercomScopeId(clientMessage.scopeId, "invalid-config");
|
|
686
|
+
scopeId = clientMessage.scopeId as string | undefined;
|
|
687
|
+
} catch {
|
|
688
|
+
this.sendError(socket, "INVALID_REQUEST", "Invalid session configuration");
|
|
689
|
+
socket.end();
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
if (!isSessionRegistration(clientMessage.session)) {
|
|
693
|
+
this.sendError(socket, "BOSS_CONTRACT_MISMATCH", "Registration session contract is invalid");
|
|
694
|
+
socket.end();
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (currentId) {
|
|
699
|
+
throw new Error("Received duplicate register message");
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (clientMessage.session.boss !== undefined) {
|
|
703
|
+
// Stage B publishes the exact registration contract without
|
|
704
|
+
// activating it on the legacy user-owned broker. No credential is
|
|
705
|
+
// consumed and no binding is trusted before the protected service
|
|
706
|
+
// predicates make an advertisement possible.
|
|
707
|
+
if (origin !== "local") {
|
|
708
|
+
this.sendError(socket, "ACCESS_DENIED", "Boss participants require the protected local broker endpoint");
|
|
709
|
+
} else if (bossCapabilityAdvertisement() === undefined) {
|
|
710
|
+
this.sendError(socket, "BOSS_FEATURE_UNAVAILABLE", "boss-run-v1 is not advertised by this broker");
|
|
711
|
+
} else {
|
|
712
|
+
this.sendError(socket, "BOSS_FEATURE_UNAVAILABLE", "Boss participant credential binding is not installed");
|
|
713
|
+
}
|
|
714
|
+
socket.end();
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
let id: string;
|
|
719
|
+
let previousLocal: ConnectedSession | undefined;
|
|
720
|
+
let remotePrincipal: RemotePrincipalRecord | undefined;
|
|
721
|
+
let issuedSessionCredential: string | undefined;
|
|
722
|
+
let enrollmentConsumed = false;
|
|
723
|
+
if (origin === "remote") {
|
|
724
|
+
if (this.sessions.size >= MAX_SESSIONS) {
|
|
725
|
+
this.sendError(socket, "TOO_MANY_SESSIONS", "Too many registered intercom sessions");
|
|
726
|
+
socket.destroy();
|
|
727
|
+
break;
|
|
728
|
+
}
|
|
729
|
+
const access = clientMessage.access;
|
|
730
|
+
if (typeof access !== "object" || access === null || Array.isArray(access)) {
|
|
731
|
+
this.audit.tryRecord({ event: "remote_registration_denied", outcome: "denied", reason: "MISSING_CREDENTIAL" });
|
|
732
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote registration requires an access credential");
|
|
733
|
+
socket.end();
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
const fields = access as Record<string, unknown>;
|
|
737
|
+
try {
|
|
738
|
+
if (typeof fields.enrollmentToken === "string") {
|
|
739
|
+
const consumed = this.accessRegistry.consumeEnrollment(fields.enrollmentToken);
|
|
740
|
+
remotePrincipal = consumed.principal;
|
|
741
|
+
issuedSessionCredential = consumed.sessionCredential;
|
|
742
|
+
enrollmentConsumed = true;
|
|
743
|
+
} else if (
|
|
744
|
+
typeof fields.sessionCredential === "string"
|
|
745
|
+
&& typeof fields.sessionId === "string"
|
|
746
|
+
&& typeof fields.generation === "number"
|
|
747
|
+
&& Number.isSafeInteger(fields.generation)
|
|
748
|
+
) {
|
|
749
|
+
remotePrincipal = this.accessRegistry.authenticateSession(fields.sessionId, fields.generation, fields.sessionCredential);
|
|
750
|
+
} else {
|
|
751
|
+
throw new Error("Invalid remote access credential shape");
|
|
752
|
+
}
|
|
753
|
+
} catch {
|
|
754
|
+
this.audit.tryRecord({ event: "remote_registration_denied", outcome: "denied", reason: "INVALID_CREDENTIAL" });
|
|
755
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote registration credential was rejected");
|
|
756
|
+
socket.end();
|
|
757
|
+
break;
|
|
758
|
+
}
|
|
759
|
+
id = remotePrincipal.id;
|
|
760
|
+
if (this.sessions.has(id)) {
|
|
761
|
+
this.audit.tryRecord({
|
|
762
|
+
event: "credential_reuse_denied",
|
|
763
|
+
outcome: "denied",
|
|
764
|
+
actorId: id,
|
|
765
|
+
remoteHostId: remotePrincipal.remoteHostId,
|
|
766
|
+
generation: remotePrincipal.generation,
|
|
767
|
+
reason: "ALREADY_ACTIVE",
|
|
768
|
+
});
|
|
769
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote session credential is already active");
|
|
770
|
+
socket.end();
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
} else {
|
|
774
|
+
id = randomUUID();
|
|
775
|
+
if (clientMessage.sessionId !== undefined) {
|
|
776
|
+
if (!isSessionId(clientMessage.sessionId)) {
|
|
777
|
+
throw new Error("Invalid register sessionId");
|
|
778
|
+
}
|
|
779
|
+
id = clientMessage.sessionId;
|
|
780
|
+
}
|
|
781
|
+
previousLocal = this.sessions.get(id);
|
|
782
|
+
if (!previousLocal && this.sessions.size >= MAX_SESSIONS) {
|
|
783
|
+
this.sendError(socket, "TOO_MANY_SESSIONS", "Too many registered intercom sessions");
|
|
784
|
+
socket.destroy();
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
if (previousLocal && !isSameLocalRuntime(previousLocal, clientMessage.session)) {
|
|
788
|
+
this.sendError(
|
|
789
|
+
socket,
|
|
790
|
+
"SESSION_ID_IN_USE",
|
|
791
|
+
`Session ID "${id}" is already active in another local runtime; close the existing session or use a different session ID`,
|
|
792
|
+
);
|
|
793
|
+
socket.end();
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
setId(id);
|
|
798
|
+
const session = clientMessage.session;
|
|
799
|
+
const info: SessionInfo = remotePrincipal ? {
|
|
800
|
+
id,
|
|
801
|
+
name: remotePrincipal.name,
|
|
802
|
+
cwd: session.cwd,
|
|
803
|
+
model: session.model,
|
|
804
|
+
pid: session.pid,
|
|
805
|
+
startedAt: session.startedAt,
|
|
806
|
+
lastActivity: session.lastActivity,
|
|
807
|
+
...(session.status !== undefined ? { status: session.status } : {}),
|
|
808
|
+
trustedLocal: false,
|
|
809
|
+
origin: "remote",
|
|
810
|
+
remoteHostId: remotePrincipal.remoteHostId,
|
|
811
|
+
parentSessionId: remotePrincipal.parentSessionId,
|
|
812
|
+
rootSessionId: remotePrincipal.rootSessionId,
|
|
813
|
+
generation: remotePrincipal.generation,
|
|
814
|
+
canDelegate: remotePrincipal.canDelegate,
|
|
815
|
+
depth: remotePrincipal.depth,
|
|
816
|
+
maxDepth: remotePrincipal.maxDepth,
|
|
817
|
+
maxChildren: remotePrincipal.maxChildren,
|
|
818
|
+
} : {
|
|
819
|
+
id,
|
|
820
|
+
...(session.name !== undefined ? { name: session.name } : {}),
|
|
821
|
+
cwd: session.cwd,
|
|
822
|
+
model: session.model,
|
|
823
|
+
pid: session.pid,
|
|
824
|
+
startedAt: session.startedAt,
|
|
825
|
+
lastActivity: session.lastActivity,
|
|
826
|
+
...(session.status !== undefined ? { status: session.status } : {}),
|
|
827
|
+
trustedLocal: typeof LISTEN_TARGET === "string" && process.platform !== "win32",
|
|
828
|
+
origin: "local",
|
|
829
|
+
};
|
|
830
|
+
if (remotePrincipal) {
|
|
831
|
+
this.audit.record({
|
|
832
|
+
event: enrollmentConsumed ? "enrollment_consumed" : "remote_reconnect",
|
|
833
|
+
outcome: "allowed",
|
|
834
|
+
actorId: id,
|
|
835
|
+
targetId: remotePrincipal.parentSessionId,
|
|
836
|
+
remoteHostId: remotePrincipal.remoteHostId,
|
|
837
|
+
generation: remotePrincipal.generation,
|
|
838
|
+
});
|
|
839
|
+
this.audit.record({
|
|
840
|
+
event: "remote_connect",
|
|
841
|
+
outcome: "allowed",
|
|
842
|
+
actorId: id,
|
|
843
|
+
targetId: remotePrincipal.parentSessionId,
|
|
844
|
+
remoteHostId: remotePrincipal.remoteHostId,
|
|
845
|
+
generation: remotePrincipal.generation,
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
const connected: ConnectedSession = {
|
|
849
|
+
socket,
|
|
850
|
+
info,
|
|
851
|
+
...(scopeId ? { scopeId } : {}),
|
|
852
|
+
...(!remotePrincipal && clientMessage.session.runtimeInstanceId
|
|
853
|
+
? { runtimeInstanceId: clientMessage.session.runtimeInstanceId }
|
|
854
|
+
: {}),
|
|
855
|
+
lastPresenceBroadcastAt: Date.now(),
|
|
856
|
+
};
|
|
857
|
+
if (previousLocal) {
|
|
858
|
+
// v4 replacement: broadcast left to the old scope's audience BEFORE
|
|
859
|
+
// installing the new binding, so recipients in the old scope observe
|
|
860
|
+
// left before recipients in the new scope observe joined.
|
|
861
|
+
this.broadcastLifecycle({ type: "session_left", sessionId: id }, previousLocal, id);
|
|
862
|
+
this.clearPendingDeliveriesForSession(id, previousLocal.socket);
|
|
863
|
+
this.deferAskEdgesForSession(id);
|
|
864
|
+
}
|
|
865
|
+
this.sessions.set(id, connected);
|
|
866
|
+
|
|
867
|
+
if (this.shutdownTimer) {
|
|
868
|
+
clearTimeout(this.shutdownTimer);
|
|
869
|
+
this.shutdownTimer = null;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
writeMessage(socket, {
|
|
873
|
+
type: "registered",
|
|
874
|
+
sessionId: id,
|
|
875
|
+
protocol: INTERCOM_PROTOCOL_NAME,
|
|
876
|
+
version: INTERCOM_PROTOCOL_VERSION,
|
|
877
|
+
...(remotePrincipal ? {
|
|
878
|
+
remoteAccess: this.remoteAccessContract(),
|
|
879
|
+
access: {
|
|
880
|
+
origin: "remote",
|
|
881
|
+
remoteHostId: remotePrincipal.remoteHostId,
|
|
882
|
+
parentSessionId: remotePrincipal.parentSessionId,
|
|
883
|
+
rootSessionId: remotePrincipal.rootSessionId,
|
|
884
|
+
generation: remotePrincipal.generation,
|
|
885
|
+
canDelegate: remotePrincipal.canDelegate,
|
|
886
|
+
depth: remotePrincipal.depth,
|
|
887
|
+
maxDepth: remotePrincipal.maxDepth,
|
|
888
|
+
maxChildren: remotePrincipal.maxChildren,
|
|
889
|
+
...(issuedSessionCredential ? { sessionCredential: issuedSessionCredential } : {}),
|
|
890
|
+
},
|
|
891
|
+
} : {}),
|
|
892
|
+
});
|
|
893
|
+
this.broadcastLifecycle({ type: "session_joined", session: info }, connected, id);
|
|
894
|
+
// End the old socket only after the new binding is installed and the
|
|
895
|
+
// new joined event has been broadcast. Any late frame from the old
|
|
896
|
+
// socket is discarded by the stale-socket fence upstream.
|
|
897
|
+
previousLocal?.socket.end();
|
|
898
|
+
break;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
case "unregister": {
|
|
902
|
+
if (!currentId) {
|
|
903
|
+
throw new Error("Received unregister before register");
|
|
904
|
+
}
|
|
905
|
+
if (clientMessage.preserveAsks !== undefined && typeof clientMessage.preserveAsks !== "boolean") {
|
|
906
|
+
throw new Error("Invalid unregister preserveAsks value");
|
|
907
|
+
}
|
|
908
|
+
const existing = this.sessions.get(currentId);
|
|
909
|
+
if (existing?.socket === socket) {
|
|
910
|
+
if (existing.info.origin === "remote") {
|
|
911
|
+
this.audit.tryRecord({
|
|
912
|
+
event: "remote_disconnect",
|
|
913
|
+
outcome: "observed",
|
|
914
|
+
actorId: currentId,
|
|
915
|
+
remoteHostId: existing.info.remoteHostId,
|
|
916
|
+
generation: existing.info.generation,
|
|
917
|
+
reason: "UNREGISTERED",
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
this.broadcastLifecycle({ type: "session_left", sessionId: currentId }, existing, currentId);
|
|
921
|
+
this.sessions.delete(currentId);
|
|
922
|
+
this.clearPendingDeliveriesForSession(currentId, socket);
|
|
923
|
+
if (clientMessage.preserveAsks) {
|
|
924
|
+
this.deferAskEdgesForSession(currentId);
|
|
925
|
+
} else {
|
|
926
|
+
this.clearAskEdgesForSession(currentId, "session_disconnected");
|
|
927
|
+
}
|
|
928
|
+
this.scheduleShutdownCheck();
|
|
929
|
+
}
|
|
930
|
+
setId(null);
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
case "list": {
|
|
935
|
+
if (typeof clientMessage.requestId !== "string") {
|
|
936
|
+
throw new Error("Invalid list message");
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const actor = this.sessions.get(currentId!);
|
|
940
|
+
// v4 list scope filter: only return same-scope sessions to the actor.
|
|
941
|
+
const sameScopeSessions = actor
|
|
942
|
+
? Array.from(this.sessions.values())
|
|
943
|
+
.filter((candidate) => sameIntercomScope(actor.scopeId, candidate.scopeId))
|
|
944
|
+
.map((candidate) => candidate.info)
|
|
945
|
+
: [];
|
|
946
|
+
const allSessions = sameScopeSessions;
|
|
947
|
+
const sessions = visibleSessions(allSessions, currentId!);
|
|
948
|
+
if (actor?.info.origin === "remote" && sessions.length < allSessions.length) {
|
|
949
|
+
this.audit.tryRecord({
|
|
950
|
+
event: "remote_visibility_filtered",
|
|
951
|
+
outcome: "observed",
|
|
952
|
+
actorId: currentId!,
|
|
953
|
+
remoteHostId: actor.info.remoteHostId,
|
|
954
|
+
generation: actor.info.generation,
|
|
955
|
+
visibleCount: sessions.length,
|
|
956
|
+
hiddenCount: allSessions.length - sessions.length,
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
writeMessage(socket, { type: "sessions", requestId: clientMessage.requestId, sessions });
|
|
960
|
+
break;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
case "send": {
|
|
964
|
+
if (!currentId) {
|
|
965
|
+
throw new Error("Received send before register");
|
|
966
|
+
}
|
|
967
|
+
const message = clientMessage.message;
|
|
968
|
+
const messageId = typeof message === "object"
|
|
969
|
+
&& message !== null
|
|
970
|
+
&& "id" in message
|
|
971
|
+
&& typeof message.id === "string"
|
|
972
|
+
&& message.id.length > 0
|
|
973
|
+
&& message.id.length <= MAX_MESSAGE_ID_LENGTH
|
|
974
|
+
? message.id
|
|
975
|
+
: "unknown";
|
|
976
|
+
|
|
977
|
+
if (
|
|
978
|
+
typeof clientMessage.to !== "string"
|
|
979
|
+
|| clientMessage.to.length === 0
|
|
980
|
+
|| clientMessage.to.length > MAX_TARGET_LENGTH
|
|
981
|
+
|| !isMessage(message)
|
|
982
|
+
) {
|
|
983
|
+
this.sendDeliveryFailure(socket, messageId, false, "INVALID_MESSAGE", "Invalid message format");
|
|
984
|
+
break;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const action: PolicyAction = message.replyTo ? "reply" : message.expectsReply ? "ask" : "send";
|
|
988
|
+
this.pruneRecentDeliveries();
|
|
989
|
+
const deliveryKey = this.deliveryKey(currentId, message.id);
|
|
990
|
+
const fingerprint = JSON.stringify({
|
|
991
|
+
to: clientMessage.to,
|
|
992
|
+
replyTo: message.replyTo,
|
|
993
|
+
expectsReply: message.expectsReply,
|
|
994
|
+
content: message.content,
|
|
995
|
+
});
|
|
996
|
+
const recent = this.recentDeliveries.get(deliveryKey);
|
|
997
|
+
if (recent) {
|
|
998
|
+
if (recent.fingerprint !== fingerprint) {
|
|
999
|
+
this.sendDeliveryFailure(socket, message.id, false, "DUPLICATE_MESSAGE_ID", "Message ID was already used with a different payload");
|
|
1000
|
+
break;
|
|
1001
|
+
}
|
|
1002
|
+
const actor = this.sessions.get(currentId);
|
|
1003
|
+
const target = this.sessions.get(recent.to);
|
|
1004
|
+
const authorizationStillValid = Boolean(
|
|
1005
|
+
actor
|
|
1006
|
+
&& target
|
|
1007
|
+
&& (actor.info.generation ?? 1) === recent.fromGeneration
|
|
1008
|
+
&& (target.info.generation ?? 1) === recent.toGeneration
|
|
1009
|
+
&& this.isAuthorized(currentId, recent.action, recent.to)
|
|
1010
|
+
);
|
|
1011
|
+
if (recent.retryable || !authorizationStillValid) {
|
|
1012
|
+
this.recentDeliveries.delete(deliveryKey);
|
|
1013
|
+
} else {
|
|
1014
|
+
if (recent.response.type === "delivered") {
|
|
1015
|
+
writeMessage(socket, {
|
|
1016
|
+
type: "delivery_accepted",
|
|
1017
|
+
messageId: message.id,
|
|
1018
|
+
deliveryId: recent.response.deliveryId,
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
writeMessage(socket, recent.response);
|
|
1022
|
+
break;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
const existingDeliveryId = this.pendingDeliveryKeys.get(deliveryKey);
|
|
1026
|
+
if (existingDeliveryId) {
|
|
1027
|
+
const existing = this.pendingDeliveries.get(existingDeliveryId);
|
|
1028
|
+
if (!existing || existing.fingerprint !== fingerprint) {
|
|
1029
|
+
this.sendDeliveryFailure(socket, message.id, false, "DUPLICATE_MESSAGE_ID", "Message ID is already pending with a different payload");
|
|
1030
|
+
break;
|
|
1031
|
+
}
|
|
1032
|
+
const actor = this.sessions.get(existing.from);
|
|
1033
|
+
const target = this.sessions.get(existing.to);
|
|
1034
|
+
if (
|
|
1035
|
+
actor
|
|
1036
|
+
&& target
|
|
1037
|
+
&& (actor.info.generation ?? 1) === existing.fromGeneration
|
|
1038
|
+
&& (target.info.generation ?? 1) === existing.toGeneration
|
|
1039
|
+
&& this.isAuthorized(existing.from, existing.action, existing.to)
|
|
1040
|
+
) {
|
|
1041
|
+
writeMessage(socket, { type: "delivery_accepted", messageId: message.id, deliveryId: existing.id });
|
|
1042
|
+
break;
|
|
1043
|
+
}
|
|
1044
|
+
this.failPendingDelivery(existing.id, "SESSION_NOT_FOUND", "Delivery authorization changed while pending");
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
if (
|
|
1048
|
+
this.pendingDeliveries.size >= MAX_PENDING_DELIVERIES
|
|
1049
|
+
|| this.countPendingDeliveriesFrom(currentId) >= MAX_PENDING_DELIVERIES_PER_SESSION
|
|
1050
|
+
) {
|
|
1051
|
+
this.sendDeliveryFailure(socket, message.id, false, "TOO_MANY_PENDING_DELIVERIES", "Too many messages are waiting for receiver acknowledgement");
|
|
1052
|
+
break;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
const candidates = this.findSessions(currentId, clientMessage.to);
|
|
1056
|
+
const targets = candidates.filter((target) => this.isAuthorized(currentId, action, target.info.id));
|
|
1057
|
+
if (candidates.length > 0 && targets.length === 0) {
|
|
1058
|
+
const actor = this.sessions.get(currentId);
|
|
1059
|
+
this.audit.tryRecord({
|
|
1060
|
+
event: "remote_delivery_denied",
|
|
1061
|
+
outcome: "denied",
|
|
1062
|
+
actorId: currentId,
|
|
1063
|
+
targetId: candidates.length === 1 ? candidates[0].info.id : undefined,
|
|
1064
|
+
remoteHostId: actor?.info.remoteHostId ?? candidates.find((candidate) => candidate.info.remoteHostId)?.info.remoteHostId,
|
|
1065
|
+
generation: actor?.info.generation,
|
|
1066
|
+
reason: "POLICY_DENIED",
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
if (targets.length === 1) {
|
|
1070
|
+
const fromSession = this.sessions.get(currentId);
|
|
1071
|
+
if (!fromSession || fromSession.socket !== socket) {
|
|
1072
|
+
this.sendDeliveryFailure(socket, message.id, false, "SENDER_NOT_FOUND", "Sender session not found");
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
const target = targets[0];
|
|
1076
|
+
const replyEdge = message.replyTo
|
|
1077
|
+
? this.askEdges.get(this.askKey(target.info.id, message.replyTo))
|
|
1078
|
+
: undefined;
|
|
1079
|
+
if (message.replyTo && !replyEdge) {
|
|
1080
|
+
this.sendDeliveryFailure(socket, message.id, false, "INVALID_REPLY_TARGET", "Reply target does not match a pending ask");
|
|
1081
|
+
break;
|
|
1082
|
+
}
|
|
1083
|
+
if (replyEdge && (replyEdge.to !== currentId || replyEdge.from !== target.info.id)) {
|
|
1084
|
+
this.sendDeliveryFailure(socket, message.id, false, "INVALID_REPLY_TARGET", "Reply target does not match the pending ask");
|
|
1085
|
+
break;
|
|
1086
|
+
}
|
|
1087
|
+
if (message.expectsReply) {
|
|
1088
|
+
const existingAsk = Array.from(this.askEdges.values()).find((edge) =>
|
|
1089
|
+
edge.from === currentId && edge.to === target.info.id
|
|
1090
|
+
);
|
|
1091
|
+
if (existingAsk) {
|
|
1092
|
+
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.");
|
|
1093
|
+
break;
|
|
1094
|
+
}
|
|
1095
|
+
const reverseEdge = Array.from(this.askEdges.values()).find((edge) =>
|
|
1096
|
+
edge.state === "blocking"
|
|
1097
|
+
&& !(message.replyTo === edge.messageId && target.info.id === edge.from)
|
|
1098
|
+
&& edge.from === target.info.id
|
|
1099
|
+
&& edge.to === currentId
|
|
1100
|
+
);
|
|
1101
|
+
if (reverseEdge) {
|
|
1102
|
+
this.sendDeliveryFailure(socket, message.id, false, "MUTUAL_ASK", "Mutual ask refused: target session is already waiting for a reply from this session.");
|
|
1103
|
+
break;
|
|
1104
|
+
}
|
|
1105
|
+
if (this.countAskEdgesFrom(currentId) >= MAX_PENDING_ASKS_PER_SESSION) {
|
|
1106
|
+
this.sendDeliveryFailure(socket, message.id, false, "TOO_MANY_PENDING_ASKS", "Too many asks are already waiting for replies");
|
|
1107
|
+
break;
|
|
1108
|
+
}
|
|
1109
|
+
this.addAskEdge(message.id, currentId, target.info.id);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
const deliveryId = randomUUID();
|
|
1113
|
+
const timeout = setTimeout(() => {
|
|
1114
|
+
this.failPendingDelivery(deliveryId, "DELIVERY_TIMEOUT", "Recipient did not acknowledge the message in time");
|
|
1115
|
+
}, DELIVERY_ACK_TIMEOUT_MS);
|
|
1116
|
+
timeout.unref?.();
|
|
1117
|
+
const pending: PendingDelivery = {
|
|
1118
|
+
id: deliveryId,
|
|
1119
|
+
key: deliveryKey,
|
|
1120
|
+
fingerprint,
|
|
1121
|
+
message,
|
|
1122
|
+
from: currentId,
|
|
1123
|
+
to: target.info.id,
|
|
1124
|
+
senderSocket: socket,
|
|
1125
|
+
recipientSocket: target.socket,
|
|
1126
|
+
action,
|
|
1127
|
+
fromGeneration: fromSession.info.generation ?? 1,
|
|
1128
|
+
toGeneration: target.info.generation ?? 1,
|
|
1129
|
+
timeout,
|
|
1130
|
+
};
|
|
1131
|
+
this.pendingDeliveries.set(deliveryId, pending);
|
|
1132
|
+
this.pendingDeliveryKeys.set(deliveryKey, deliveryId);
|
|
1133
|
+
writeMessage(socket, { type: "delivery_accepted", messageId: message.id, deliveryId });
|
|
1134
|
+
writeMessage(target.socket, {
|
|
1135
|
+
type: "message",
|
|
1136
|
+
deliveryId,
|
|
1137
|
+
from: fromSession.info,
|
|
1138
|
+
message,
|
|
1139
|
+
});
|
|
1140
|
+
break;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
if (targets.length > 1) {
|
|
1144
|
+
this.sendDeliveryFailure(socket, message.id, false, "AMBIGUOUS_TARGET", `Multiple sessions named \"${clientMessage.to}\" are connected. Use the session ID instead.`);
|
|
1145
|
+
break;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
this.sendDeliveryFailure(socket, message.id, false, "SESSION_NOT_FOUND", "Session not found");
|
|
1149
|
+
break;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
case "boss_control": {
|
|
1153
|
+
if (!currentId) throw new Error("Received boss_control before register");
|
|
1154
|
+
const requestId = clientMessage.requestId;
|
|
1155
|
+
const requestedTarget = clientMessage.to;
|
|
1156
|
+
if (
|
|
1157
|
+
typeof requestId !== "string"
|
|
1158
|
+
|| requestId.length === 0
|
|
1159
|
+
|| requestId.length > MAX_MESSAGE_ID_LENGTH
|
|
1160
|
+
|| typeof requestedTarget !== "string"
|
|
1161
|
+
|| requestedTarget.length === 0
|
|
1162
|
+
|| requestedTarget.length > MAX_TARGET_LENGTH
|
|
1163
|
+
) throw new Error("Invalid boss_control routing metadata");
|
|
1164
|
+
|
|
1165
|
+
const sender = this.sessions.get(currentId);
|
|
1166
|
+
let envelope: BossControlEnvelope;
|
|
1167
|
+
let controlKind: ReturnType<typeof bossControlKind>["controlKind"];
|
|
1168
|
+
try {
|
|
1169
|
+
if (!sender || sender.socket !== socket) throw new Error("Boss sender session not found");
|
|
1170
|
+
envelope = assertBossControlSender(sender.info, clientMessage.envelope);
|
|
1171
|
+
controlKind = bossControlKind(envelope).controlKind;
|
|
1172
|
+
if (requestId !== envelope.messageId) {
|
|
1173
|
+
throw new Error("requestId must equal the canonical Boss envelope messageId");
|
|
1174
|
+
}
|
|
1175
|
+
} catch (error) {
|
|
1176
|
+
// The envelope may be an outer or nested Proxy rejected by the
|
|
1177
|
+
// adapter preflight. Never inspect it again on the failure path.
|
|
1178
|
+
this.sendBossControlFailure(
|
|
1179
|
+
socket,
|
|
1180
|
+
requestId,
|
|
1181
|
+
requestId,
|
|
1182
|
+
requestId,
|
|
1183
|
+
"INVALID_CONTROL",
|
|
1184
|
+
error instanceof Error ? error.message : "Invalid Boss control envelope",
|
|
1185
|
+
);
|
|
1186
|
+
break;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
const key = this.bossControlKey(currentId, envelope);
|
|
1190
|
+
const fingerprint = this.bossControlFingerprint(requestedTarget, envelope);
|
|
1191
|
+
const prior = this.bossControlLedger.lookup(key, fingerprint);
|
|
1192
|
+
if (prior.status === "replay") {
|
|
1193
|
+
for (const frame of bossControlReplayFrames(prior.result, envelope.messageId)) writeMessage(socket, frame);
|
|
1194
|
+
break;
|
|
1195
|
+
}
|
|
1196
|
+
if (prior.status === "conflict") {
|
|
1197
|
+
this.sendBossControlFailure(
|
|
1198
|
+
socket,
|
|
1199
|
+
requestId,
|
|
1200
|
+
envelope.messageId,
|
|
1201
|
+
envelope.idempotencyKey,
|
|
1202
|
+
"IDEMPOTENCY_CONFLICT",
|
|
1203
|
+
"Boss idempotency key is durably bound to a different canonical request",
|
|
1204
|
+
);
|
|
1205
|
+
break;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// Boss routing is identity-bearing: names and ID prefixes are never
|
|
1209
|
+
// accepted. The protected provider must supply durable causation
|
|
1210
|
+
// evidence before this dormant adapter may claim correlation.
|
|
1211
|
+
const exactTarget = exactBossSessionTarget(this.sessions, requestedTarget);
|
|
1212
|
+
const correlated = hasAuthoritativeBossControlCorrelation();
|
|
1213
|
+
const target = exactTarget && authorizeSessionAction(
|
|
1214
|
+
Array.from(this.sessions.values(), (session) => session.info),
|
|
1215
|
+
currentId,
|
|
1216
|
+
"control",
|
|
1217
|
+
exactTarget.info.id,
|
|
1218
|
+
{ controlKind, correlated },
|
|
1219
|
+
).allowed ? exactTarget : undefined;
|
|
1220
|
+
if (!target) {
|
|
1221
|
+
const acceptedDeliveryId = prior.status === "accepted" ? prior.deliveryId : undefined;
|
|
1222
|
+
this.sendBossControlFailure(
|
|
1223
|
+
socket,
|
|
1224
|
+
requestId,
|
|
1225
|
+
envelope.messageId,
|
|
1226
|
+
envelope.idempotencyKey,
|
|
1227
|
+
exactTarget ? "POLICY_DENIED" : "SESSION_NOT_FOUND",
|
|
1228
|
+
exactTarget
|
|
1229
|
+
? "Boss control routing requires authoritative correlation evidence"
|
|
1230
|
+
: "Boss control target session ID was not found",
|
|
1231
|
+
acceptedDeliveryId,
|
|
1232
|
+
{ scope: key, fingerprint },
|
|
1233
|
+
acceptedDeliveryId !== undefined,
|
|
1234
|
+
);
|
|
1235
|
+
break;
|
|
1236
|
+
}
|
|
1237
|
+
const existingDeliveryId = this.pendingBossControlKeys.get(key);
|
|
1238
|
+
if (existingDeliveryId) {
|
|
1239
|
+
const existing = this.pendingBossControls.get(existingDeliveryId);
|
|
1240
|
+
if (!existing || existing.fingerprint !== fingerprint) {
|
|
1241
|
+
this.sendBossControlFailure(socket, requestId, envelope.messageId, envelope.idempotencyKey, "IDEMPOTENCY_CONFLICT", "Boss idempotency key is already bound to a different canonical request");
|
|
1242
|
+
break;
|
|
1243
|
+
}
|
|
1244
|
+
if (existing.messageId !== envelope.messageId) {
|
|
1245
|
+
existing.requestId = requestId;
|
|
1246
|
+
existing.messageId = envelope.messageId;
|
|
1247
|
+
existing.envelope = envelope;
|
|
1248
|
+
existing.senderSocket = socket;
|
|
1249
|
+
writeMessage(existing.recipientSocket, { type: "boss_control", deliveryId: existing.deliveryId, from: sender.info, envelope });
|
|
1250
|
+
}
|
|
1251
|
+
writeMessage(socket, {
|
|
1252
|
+
type: "boss_control_ack",
|
|
1253
|
+
requestId,
|
|
1254
|
+
messageId: envelope.messageId,
|
|
1255
|
+
idempotencyKey: envelope.idempotencyKey,
|
|
1256
|
+
status: "accepted",
|
|
1257
|
+
deliveryId: existing.deliveryId,
|
|
1258
|
+
});
|
|
1259
|
+
break;
|
|
1260
|
+
}
|
|
1261
|
+
const deliveryId = prior.status === "accepted" ? prior.deliveryId : randomUUID();
|
|
1262
|
+
if (prior.status === "miss") {
|
|
1263
|
+
this.bossControlLedger.recordAccepted(key, fingerprint, deliveryId);
|
|
1264
|
+
}
|
|
1265
|
+
const timeout = setTimeout(() => {
|
|
1266
|
+
this.failPendingBossControl(deliveryId, "DELIVERY_TIMEOUT", "Recipient did not acknowledge the Boss control envelope in time");
|
|
1267
|
+
}, DELIVERY_ACK_TIMEOUT_MS);
|
|
1268
|
+
timeout.unref?.();
|
|
1269
|
+
this.pendingBossControls.set(deliveryId, {
|
|
1270
|
+
deliveryId,
|
|
1271
|
+
key,
|
|
1272
|
+
fingerprint,
|
|
1273
|
+
requestId,
|
|
1274
|
+
messageId: envelope.messageId,
|
|
1275
|
+
envelope,
|
|
1276
|
+
from: currentId,
|
|
1277
|
+
to: target.info.id,
|
|
1278
|
+
senderSocket: socket,
|
|
1279
|
+
recipientSocket: target.socket,
|
|
1280
|
+
timeout,
|
|
1281
|
+
});
|
|
1282
|
+
this.pendingBossControlKeys.set(key, deliveryId);
|
|
1283
|
+
writeMessage(socket, {
|
|
1284
|
+
type: "boss_control_ack",
|
|
1285
|
+
requestId,
|
|
1286
|
+
messageId: envelope.messageId,
|
|
1287
|
+
idempotencyKey: envelope.idempotencyKey,
|
|
1288
|
+
status: "accepted",
|
|
1289
|
+
deliveryId,
|
|
1290
|
+
});
|
|
1291
|
+
writeMessage(target.socket, { type: "boss_control", deliveryId, from: sender.info, envelope });
|
|
1292
|
+
break;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
case "boss_control_received": {
|
|
1296
|
+
if (!currentId) throw new Error("Received boss_control_received before register");
|
|
1297
|
+
if (
|
|
1298
|
+
typeof clientMessage.deliveryId !== "string"
|
|
1299
|
+
|| typeof clientMessage.messageId !== "string"
|
|
1300
|
+
|| typeof clientMessage.idempotencyKey !== "string"
|
|
1301
|
+
) {
|
|
1302
|
+
throw new Error("Invalid boss_control_received message");
|
|
1303
|
+
}
|
|
1304
|
+
this.acknowledgePendingBossControl(clientMessage.deliveryId, clientMessage.messageId, clientMessage.idempotencyKey, currentId, socket);
|
|
1305
|
+
break;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
case "message_received": {
|
|
1309
|
+
if (!currentId) {
|
|
1310
|
+
throw new Error("Received message_received before register");
|
|
1311
|
+
}
|
|
1312
|
+
if (typeof clientMessage.deliveryId !== "string") {
|
|
1313
|
+
throw new Error("Invalid message_received message");
|
|
1314
|
+
}
|
|
1315
|
+
this.acknowledgePendingDelivery(clientMessage.deliveryId, currentId, socket);
|
|
1316
|
+
break;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
case "message_rejected": {
|
|
1320
|
+
if (!currentId) {
|
|
1321
|
+
throw new Error("Received message_rejected before register");
|
|
1322
|
+
}
|
|
1323
|
+
if (
|
|
1324
|
+
typeof clientMessage.deliveryId !== "string"
|
|
1325
|
+
|| clientMessage.code !== "CONFLICTING_MESSAGE_ID"
|
|
1326
|
+
|| typeof clientMessage.reason !== "string"
|
|
1327
|
+
|| clientMessage.reason.length > 1024
|
|
1328
|
+
) {
|
|
1329
|
+
throw new Error("Invalid message_rejected message");
|
|
1330
|
+
}
|
|
1331
|
+
const pending = this.pendingDeliveries.get(clientMessage.deliveryId);
|
|
1332
|
+
if (pending?.to === currentId && pending.recipientSocket === socket) {
|
|
1333
|
+
this.failPendingDelivery(clientMessage.deliveryId, clientMessage.code, clientMessage.reason);
|
|
1334
|
+
}
|
|
1335
|
+
break;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
case "defer_ask": {
|
|
1339
|
+
if (!currentId) {
|
|
1340
|
+
throw new Error("Received defer_ask before register");
|
|
1341
|
+
}
|
|
1342
|
+
if (
|
|
1343
|
+
typeof clientMessage.messageId !== "string"
|
|
1344
|
+
|| clientMessage.messageId.length > MAX_MESSAGE_ID_LENGTH
|
|
1345
|
+
|| typeof clientMessage.requestId !== "string"
|
|
1346
|
+
|| clientMessage.requestId.length > MAX_MESSAGE_ID_LENGTH
|
|
1347
|
+
) {
|
|
1348
|
+
throw new Error("Invalid defer_ask message");
|
|
1349
|
+
}
|
|
1350
|
+
const session = this.sessions.get(currentId);
|
|
1351
|
+
const edge = this.askEdges.get(this.askKey(currentId, clientMessage.messageId));
|
|
1352
|
+
const applied = Boolean(session?.socket === socket && edge?.from === currentId);
|
|
1353
|
+
if (applied && edge?.state === "blocking") {
|
|
1354
|
+
edge.state = "deferred";
|
|
1355
|
+
this.persistAskEdges();
|
|
1356
|
+
this.notifyAskDeferred(edge);
|
|
1357
|
+
}
|
|
1358
|
+
writeMessage(socket, { type: "ask_control_result", requestId: clientMessage.requestId, action: "defer", messageId: clientMessage.messageId, applied });
|
|
1359
|
+
break;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
case "cancel_ask": {
|
|
1363
|
+
if (!currentId) {
|
|
1364
|
+
throw new Error("Received cancel_ask before register");
|
|
1365
|
+
}
|
|
1366
|
+
if (
|
|
1367
|
+
typeof clientMessage.messageId !== "string"
|
|
1368
|
+
|| clientMessage.messageId.length > MAX_MESSAGE_ID_LENGTH
|
|
1369
|
+
|| typeof clientMessage.requestId !== "string"
|
|
1370
|
+
|| clientMessage.requestId.length > MAX_MESSAGE_ID_LENGTH
|
|
1371
|
+
) {
|
|
1372
|
+
throw new Error("Invalid cancel_ask message");
|
|
1373
|
+
}
|
|
1374
|
+
const session = this.sessions.get(currentId);
|
|
1375
|
+
const edgeKey = this.askKey(currentId, clientMessage.messageId);
|
|
1376
|
+
const edge = this.askEdges.get(edgeKey);
|
|
1377
|
+
const applied = Boolean(session?.socket === socket && edge?.from === currentId);
|
|
1378
|
+
if (applied) {
|
|
1379
|
+
this.removeAskEdge(edgeKey, "cancelled", true);
|
|
1380
|
+
}
|
|
1381
|
+
writeMessage(socket, { type: "ask_control_result", requestId: clientMessage.requestId, action: "cancel", messageId: clientMessage.messageId, applied });
|
|
1382
|
+
break;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
case "presence": {
|
|
1386
|
+
if (!currentId) {
|
|
1387
|
+
throw new Error("Received presence before register");
|
|
1388
|
+
}
|
|
1389
|
+
const session = this.sessions.get(currentId);
|
|
1390
|
+
if (session?.socket === socket) {
|
|
1391
|
+
let changed = false;
|
|
1392
|
+
if (clientMessage.name !== undefined) {
|
|
1393
|
+
if (typeof clientMessage.name !== "string" || clientMessage.name.length > MAX_SESSION_NAME_LENGTH) {
|
|
1394
|
+
throw new Error("Invalid presence name");
|
|
1395
|
+
}
|
|
1396
|
+
if (session.info.origin !== "remote" && session.info.name !== clientMessage.name) {
|
|
1397
|
+
session.info.name = clientMessage.name;
|
|
1398
|
+
changed = true;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
if (clientMessage.status !== undefined) {
|
|
1402
|
+
if (typeof clientMessage.status !== "string" || clientMessage.status.length > MAX_SESSION_STATUS_LENGTH) {
|
|
1403
|
+
throw new Error("Invalid presence status");
|
|
1404
|
+
}
|
|
1405
|
+
if (session.info.status !== clientMessage.status) {
|
|
1406
|
+
session.info.status = clientMessage.status;
|
|
1407
|
+
changed = true;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
if (clientMessage.model !== undefined) {
|
|
1411
|
+
if (typeof clientMessage.model !== "string" || clientMessage.model.length > MAX_SESSION_MODEL_LENGTH) {
|
|
1412
|
+
throw new Error("Invalid presence model");
|
|
1413
|
+
}
|
|
1414
|
+
if (session.info.model !== clientMessage.model) {
|
|
1415
|
+
session.info.model = clientMessage.model;
|
|
1416
|
+
changed = true;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
const now = Date.now();
|
|
1420
|
+
session.info.lastActivity = now;
|
|
1421
|
+
if (changed || now - session.lastPresenceBroadcastAt >= PRESENCE_HEARTBEAT_MS) {
|
|
1422
|
+
session.lastPresenceBroadcastAt = now;
|
|
1423
|
+
this.broadcastLifecycle({ type: "presence_update", session: session.info }, session, currentId);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
break;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
default:
|
|
1430
|
+
throw new Error(`Unknown client message type: ${clientMessage.type}`);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
private remoteAccessContract() {
|
|
1435
|
+
return {
|
|
1436
|
+
feature: "remote-access-v1" as const,
|
|
1437
|
+
policySemanticsVersion: POLICY_SEMANTICS_VERSION,
|
|
1438
|
+
policySemanticsHash: POLICY_SEMANTICS_HASH,
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
private handleAccessControl(socket: net.Socket, message: Record<string, unknown>): void {
|
|
1443
|
+
if (
|
|
1444
|
+
typeof message.requestId !== "string"
|
|
1445
|
+
|| message.requestId.length > MAX_MESSAGE_ID_LENGTH
|
|
1446
|
+
|| typeof message.adminToken !== "string"
|
|
1447
|
+
|| !this.accessRegistry.authenticateAdmin(message.adminToken)
|
|
1448
|
+
) {
|
|
1449
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote access control credential or request was rejected");
|
|
1450
|
+
socket.end();
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
if (message.action === "inspect_tree") {
|
|
1454
|
+
if (typeof message.principalId !== "string" || !isSessionId(message.principalId)) {
|
|
1455
|
+
this.sendError(socket, "INVALID_REQUEST", "Invalid remote principal ID");
|
|
1456
|
+
socket.end();
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
const principals = this.accessRegistry.inspectSubtree(message.principalId).map((principal) => this.principalSummary(principal));
|
|
1460
|
+
this.audit.record({
|
|
1461
|
+
event: "tree_inspected",
|
|
1462
|
+
outcome: "allowed",
|
|
1463
|
+
actorId: "local-admin",
|
|
1464
|
+
targetId: message.principalId,
|
|
1465
|
+
visibleCount: principals.length,
|
|
1466
|
+
});
|
|
1467
|
+
writeMessage(socket, { type: "access_control_result", requestId: message.requestId, action: "inspect_tree", principals });
|
|
1468
|
+
socket.end();
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
if (message.action === "adopt_subtree") {
|
|
1472
|
+
if (
|
|
1473
|
+
typeof message.principalId !== "string"
|
|
1474
|
+
|| !isSessionId(message.principalId)
|
|
1475
|
+
|| typeof message.newParentSessionId !== "string"
|
|
1476
|
+
|| !isSessionId(message.newParentSessionId)
|
|
1477
|
+
) {
|
|
1478
|
+
this.sendError(socket, "INVALID_REQUEST", "Invalid adoption request");
|
|
1479
|
+
socket.end();
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
const localParent = this.sessions.get(message.newParentSessionId);
|
|
1483
|
+
const remoteParent = this.accessRegistry.snapshot().principals[message.newParentSessionId];
|
|
1484
|
+
if ((!localParent || localParent.info.origin === "remote") && (!remoteParent || remoteParent.state !== "active")) {
|
|
1485
|
+
this.sendError(socket, "ACCESS_DENIED", "Adoption parent must be an active local or remote principal");
|
|
1486
|
+
socket.end();
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
const newRootSessionId = localParent?.info.origin === "local"
|
|
1490
|
+
? localParent.info.id
|
|
1491
|
+
: remoteParent!.rootSessionId;
|
|
1492
|
+
const priorSessions = Array.from(this.sessions.values(), (session) => session.info);
|
|
1493
|
+
let changed: RemotePrincipalRecord[];
|
|
1494
|
+
try {
|
|
1495
|
+
changed = this.accessRegistry.adoptSubtree(message.principalId, message.newParentSessionId, newRootSessionId);
|
|
1496
|
+
} catch {
|
|
1497
|
+
this.sendError(socket, "ACCESS_DENIED", "Adoption would violate the ownership tree");
|
|
1498
|
+
socket.end();
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
this.disconnectTransitionedPrincipals(changed, priorSessions, "principal_adopted");
|
|
1502
|
+
writeMessage(socket, {
|
|
1503
|
+
type: "access_control_result",
|
|
1504
|
+
requestId: message.requestId,
|
|
1505
|
+
action: "adopt_subtree",
|
|
1506
|
+
principals: changed.map((principal) => this.principalSummary(principal)),
|
|
1507
|
+
});
|
|
1508
|
+
socket.end();
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
if (message.action === "revoke_subtree") {
|
|
1512
|
+
if (typeof message.principalId !== "string" || !isSessionId(message.principalId)) {
|
|
1513
|
+
this.sendError(socket, "INVALID_REQUEST", "Invalid remote principal ID");
|
|
1514
|
+
socket.end();
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
const priorSessions = Array.from(this.sessions.values(), (session) => session.info);
|
|
1518
|
+
const changed = this.accessRegistry.revoke(message.principalId);
|
|
1519
|
+
this.disconnectTransitionedPrincipals(changed, priorSessions);
|
|
1520
|
+
writeMessage(socket, {
|
|
1521
|
+
type: "access_control_result",
|
|
1522
|
+
requestId: message.requestId,
|
|
1523
|
+
action: "revoke_subtree",
|
|
1524
|
+
changedPrincipalIds: changed.map((principal) => principal.id),
|
|
1525
|
+
});
|
|
1526
|
+
socket.end();
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
if (
|
|
1530
|
+
message.action !== "issue_enrollment"
|
|
1531
|
+
|| typeof message.enrollment !== "object"
|
|
1532
|
+
|| message.enrollment === null
|
|
1533
|
+
|| Array.isArray(message.enrollment)
|
|
1534
|
+
) {
|
|
1535
|
+
this.sendError(socket, "INVALID_REQUEST", "Unknown remote access control action");
|
|
1536
|
+
socket.end();
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
const enrollment = message.enrollment as Record<string, unknown>;
|
|
1540
|
+
if (
|
|
1541
|
+
typeof enrollment.name !== "string"
|
|
1542
|
+
|| typeof enrollment.parentSessionId !== "string"
|
|
1543
|
+
|| typeof enrollment.rootSessionId !== "string"
|
|
1544
|
+
|| typeof enrollment.remoteHostId !== "string"
|
|
1545
|
+
|| (enrollment.ttlMs !== undefined && (typeof enrollment.ttlMs !== "number" || !Number.isSafeInteger(enrollment.ttlMs)))
|
|
1546
|
+
|| (enrollment.expiresAt !== undefined && (typeof enrollment.expiresAt !== "number" || !Number.isSafeInteger(enrollment.expiresAt)))
|
|
1547
|
+
|| (enrollment.canDelegate !== undefined && typeof enrollment.canDelegate !== "boolean")
|
|
1548
|
+
|| (enrollment.maxDepth !== undefined && (typeof enrollment.maxDepth !== "number" || !Number.isSafeInteger(enrollment.maxDepth)))
|
|
1549
|
+
|| (enrollment.maxChildren !== undefined && (typeof enrollment.maxChildren !== "number" || !Number.isSafeInteger(enrollment.maxChildren)))
|
|
1550
|
+
) {
|
|
1551
|
+
this.sendError(socket, "INVALID_REQUEST", "Invalid remote enrollment request");
|
|
1552
|
+
socket.end();
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
const parent = this.sessions.get(enrollment.parentSessionId);
|
|
1556
|
+
if (!parent || parent.info.origin === "remote" || enrollment.rootSessionId !== parent.info.id) {
|
|
1557
|
+
this.sendError(socket, "ACCESS_DENIED", "Enrollment parent must be an active local root session");
|
|
1558
|
+
socket.end();
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
const issued = this.accessRegistry.issueEnrollment({
|
|
1562
|
+
name: enrollment.name,
|
|
1563
|
+
parentSessionId: parent.info.id,
|
|
1564
|
+
rootSessionId: parent.info.id,
|
|
1565
|
+
remoteHostId: enrollment.remoteHostId,
|
|
1566
|
+
...(enrollment.expiresAt !== undefined ? { expiresAt: enrollment.expiresAt as number } : {}),
|
|
1567
|
+
...(enrollment.canDelegate !== undefined ? { canDelegate: enrollment.canDelegate as boolean } : {}),
|
|
1568
|
+
...(enrollment.maxDepth !== undefined ? { maxDepth: enrollment.maxDepth as number } : {}),
|
|
1569
|
+
...(enrollment.maxChildren !== undefined ? { maxChildren: enrollment.maxChildren as number } : {}),
|
|
1570
|
+
}, enrollment.ttlMs as number | undefined);
|
|
1571
|
+
this.audit.record({
|
|
1572
|
+
event: "enrollment_issued",
|
|
1573
|
+
outcome: "allowed",
|
|
1574
|
+
actorId: parent.info.id,
|
|
1575
|
+
targetId: enrollment.name,
|
|
1576
|
+
remoteHostId: enrollment.remoteHostId,
|
|
1577
|
+
reason: `expires:${issued.expiresAt}`,
|
|
1578
|
+
});
|
|
1579
|
+
writeMessage(socket, {
|
|
1580
|
+
type: "access_control_result",
|
|
1581
|
+
requestId: message.requestId,
|
|
1582
|
+
action: "issue_enrollment",
|
|
1583
|
+
enrollmentToken: issued.enrollmentToken,
|
|
1584
|
+
expiresAt: issued.expiresAt,
|
|
1585
|
+
});
|
|
1586
|
+
socket.end();
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
private handleRemoteAccessControl(socket: net.Socket, message: Record<string, unknown>): void {
|
|
1590
|
+
if (
|
|
1591
|
+
typeof message.requestId !== "string"
|
|
1592
|
+
|| message.requestId.length > MAX_MESSAGE_ID_LENGTH
|
|
1593
|
+
|| typeof message.access !== "object"
|
|
1594
|
+
|| message.access === null
|
|
1595
|
+
|| Array.isArray(message.access)
|
|
1596
|
+
) {
|
|
1597
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote control request was rejected");
|
|
1598
|
+
socket.end();
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
const access = message.access as Record<string, unknown>;
|
|
1602
|
+
if (
|
|
1603
|
+
typeof access.sessionCredential !== "string"
|
|
1604
|
+
|| typeof access.sessionId !== "string"
|
|
1605
|
+
|| typeof access.generation !== "number"
|
|
1606
|
+
|| !Number.isSafeInteger(access.generation)
|
|
1607
|
+
) {
|
|
1608
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote control credential was rejected");
|
|
1609
|
+
socket.end();
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
let parent: RemotePrincipalRecord;
|
|
1613
|
+
try {
|
|
1614
|
+
parent = this.accessRegistry.authenticateSession(access.sessionId, access.generation, access.sessionCredential);
|
|
1615
|
+
} catch {
|
|
1616
|
+
this.audit.tryRecord({ event: "remote_registration_denied", outcome: "denied", reason: "INVALID_CONTROL_CREDENTIAL" });
|
|
1617
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote control credential was rejected");
|
|
1618
|
+
socket.end();
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
const policyState = this.registryPolicyState();
|
|
1622
|
+
if (message.action === "inspect_tree") {
|
|
1623
|
+
const targetId = typeof message.principalId === "string" ? message.principalId : parent.id;
|
|
1624
|
+
const inspection = authorize(policyState, parent.id, "inspect_tree", targetId, { actorGeneration: parent.generation });
|
|
1625
|
+
if (!inspection.allowed) {
|
|
1626
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote tree inspection policy denied the request");
|
|
1627
|
+
socket.end();
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
const principals = this.accessRegistry.inspectSubtree(targetId)
|
|
1631
|
+
.filter((candidate) => authorize(policyState, parent.id, "inspect_tree", candidate.id, { actorGeneration: parent.generation }).allowed)
|
|
1632
|
+
.map((candidate) => this.principalSummary(candidate));
|
|
1633
|
+
this.audit.record({
|
|
1634
|
+
event: "tree_inspected",
|
|
1635
|
+
outcome: "allowed",
|
|
1636
|
+
actorId: parent.id,
|
|
1637
|
+
targetId,
|
|
1638
|
+
remoteHostId: parent.remoteHostId,
|
|
1639
|
+
generation: parent.generation,
|
|
1640
|
+
visibleCount: principals.length,
|
|
1641
|
+
});
|
|
1642
|
+
writeMessage(socket, { type: "access_control_result", requestId: message.requestId, action: "inspect_tree", principals });
|
|
1643
|
+
socket.end();
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
if (
|
|
1647
|
+
message.action !== "issue_child_enrollment"
|
|
1648
|
+
|| typeof message.enrollment !== "object"
|
|
1649
|
+
|| message.enrollment === null
|
|
1650
|
+
|| Array.isArray(message.enrollment)
|
|
1651
|
+
) {
|
|
1652
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote control action was rejected");
|
|
1653
|
+
socket.end();
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
const enrollment = message.enrollment as Record<string, unknown>;
|
|
1657
|
+
if (
|
|
1658
|
+
typeof enrollment.name !== "string"
|
|
1659
|
+
|| (enrollment.ttlMs !== undefined && (typeof enrollment.ttlMs !== "number" || !Number.isSafeInteger(enrollment.ttlMs)))
|
|
1660
|
+
|| (enrollment.expiresAt !== undefined && (typeof enrollment.expiresAt !== "number" || !Number.isSafeInteger(enrollment.expiresAt)))
|
|
1661
|
+
|| (enrollment.canDelegate !== undefined && typeof enrollment.canDelegate !== "boolean")
|
|
1662
|
+
|| (enrollment.maxDepth !== undefined && (typeof enrollment.maxDepth !== "number" || !Number.isSafeInteger(enrollment.maxDepth)))
|
|
1663
|
+
|| (enrollment.maxChildren !== undefined && (typeof enrollment.maxChildren !== "number" || !Number.isSafeInteger(enrollment.maxChildren)))
|
|
1664
|
+
) {
|
|
1665
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote delegation request was rejected");
|
|
1666
|
+
socket.end();
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
const delegation = authorize(policyState, parent.id, "delegate_child", parent.id, {
|
|
1670
|
+
actorGeneration: parent.generation,
|
|
1671
|
+
targetGeneration: parent.generation,
|
|
1672
|
+
});
|
|
1673
|
+
if (!delegation.allowed) {
|
|
1674
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote delegation policy denied the request");
|
|
1675
|
+
socket.end();
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
let issued;
|
|
1679
|
+
try {
|
|
1680
|
+
issued = this.accessRegistry.issueChildEnrollment(parent.id, parent.generation, {
|
|
1681
|
+
name: enrollment.name,
|
|
1682
|
+
...(enrollment.expiresAt !== undefined ? { expiresAt: enrollment.expiresAt as number } : {}),
|
|
1683
|
+
...(enrollment.canDelegate !== undefined ? { canDelegate: enrollment.canDelegate as boolean } : {}),
|
|
1684
|
+
...(enrollment.maxDepth !== undefined ? { maxDepth: enrollment.maxDepth as number } : {}),
|
|
1685
|
+
...(enrollment.maxChildren !== undefined ? { maxChildren: enrollment.maxChildren as number } : {}),
|
|
1686
|
+
}, enrollment.ttlMs as number | undefined);
|
|
1687
|
+
} catch {
|
|
1688
|
+
this.sendError(socket, "ACCESS_DENIED", "Remote delegation limits denied the request");
|
|
1689
|
+
socket.end();
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
this.audit.record({
|
|
1693
|
+
event: "enrollment_issued",
|
|
1694
|
+
outcome: "allowed",
|
|
1695
|
+
actorId: parent.id,
|
|
1696
|
+
targetId: enrollment.name,
|
|
1697
|
+
remoteHostId: parent.remoteHostId,
|
|
1698
|
+
generation: parent.generation,
|
|
1699
|
+
reason: `delegated-expires:${issued.expiresAt}`,
|
|
1700
|
+
});
|
|
1701
|
+
writeMessage(socket, {
|
|
1702
|
+
type: "access_control_result",
|
|
1703
|
+
requestId: message.requestId,
|
|
1704
|
+
action: "issue_child_enrollment",
|
|
1705
|
+
enrollmentToken: issued.enrollmentToken,
|
|
1706
|
+
expiresAt: issued.expiresAt,
|
|
1707
|
+
parentSessionId: parent.id,
|
|
1708
|
+
});
|
|
1709
|
+
socket.end();
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
private registryPolicyState(): PolicyState {
|
|
1713
|
+
const records = this.accessRegistry.snapshot().principals;
|
|
1714
|
+
const principals: PolicyState["principals"] = {};
|
|
1715
|
+
for (const record of Object.values(records)) {
|
|
1716
|
+
principals[record.id] = {
|
|
1717
|
+
id: record.id,
|
|
1718
|
+
kind: "remote",
|
|
1719
|
+
state: record.state,
|
|
1720
|
+
generation: record.generation,
|
|
1721
|
+
policy: "remote-tree",
|
|
1722
|
+
parentSessionId: record.parentSessionId,
|
|
1723
|
+
rootSessionId: record.rootSessionId,
|
|
1724
|
+
};
|
|
1725
|
+
if (!principals[record.rootSessionId]) {
|
|
1726
|
+
principals[record.rootSessionId] = {
|
|
1727
|
+
id: record.rootSessionId,
|
|
1728
|
+
kind: "local",
|
|
1729
|
+
state: "active",
|
|
1730
|
+
generation: 1,
|
|
1731
|
+
policy: "local-public",
|
|
1732
|
+
rootSessionId: record.rootSessionId,
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
return { principals };
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
private principalSummary(principal: RemotePrincipalMetadata): RemotePrincipalSummary {
|
|
1740
|
+
return { ...principal, connected: this.sessions.has(principal.id) };
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
private reconcileExpiredPrincipals(): void {
|
|
1744
|
+
const priorSessions = Array.from(this.sessions.values(), (session) => session.info);
|
|
1745
|
+
const changed = this.accessRegistry.expirePrincipals();
|
|
1746
|
+
if (changed.length > 0) this.disconnectTransitionedPrincipals(changed, priorSessions, "principal_expired");
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
private disconnectTransitionedPrincipals(
|
|
1750
|
+
changed: RemotePrincipalRecord[],
|
|
1751
|
+
priorSessions: SessionInfo[],
|
|
1752
|
+
auditEvent: "principal_revoked" | "principal_expired" | "principal_adopted" = "principal_revoked",
|
|
1753
|
+
): void {
|
|
1754
|
+
const changedIds = new Set(changed.map((principal) => principal.id));
|
|
1755
|
+
for (const principal of changed) {
|
|
1756
|
+
const live = this.sessions.get(principal.id);
|
|
1757
|
+
if (!live) {
|
|
1758
|
+
this.audit.record({
|
|
1759
|
+
event: auditEvent,
|
|
1760
|
+
outcome: "allowed",
|
|
1761
|
+
actorId: principal.id,
|
|
1762
|
+
remoteHostId: principal.remoteHostId,
|
|
1763
|
+
generation: principal.generation,
|
|
1764
|
+
reason: "OFFLINE",
|
|
1765
|
+
});
|
|
1766
|
+
continue;
|
|
1767
|
+
}
|
|
1768
|
+
const subject = priorSessions.find((session) => session.id === principal.id) ?? live.info;
|
|
1769
|
+
for (const [recipientId, recipient] of this.sessions) {
|
|
1770
|
+
if (
|
|
1771
|
+
recipientId !== principal.id
|
|
1772
|
+
&& !changedIds.has(recipientId)
|
|
1773
|
+
&& authorizeSessionAction(priorSessions, recipientId, "discover", principal.id).allowed
|
|
1774
|
+
&& sameIntercomScope(recipient.scopeId, live.scopeId)
|
|
1775
|
+
) {
|
|
1776
|
+
writeMessage(recipient.socket, { type: "session_left", sessionId: principal.id });
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
this.clearPendingDeliveriesForSession(principal.id, live.socket);
|
|
1780
|
+
this.clearAskEdgesForSession(principal.id, "authorization_revoked");
|
|
1781
|
+
this.sessions.delete(principal.id);
|
|
1782
|
+
for (const [key, recent] of this.recentDeliveries) {
|
|
1783
|
+
if (recent.from === principal.id || recent.to === principal.id) this.recentDeliveries.delete(key);
|
|
1784
|
+
}
|
|
1785
|
+
this.audit.record({
|
|
1786
|
+
event: auditEvent,
|
|
1787
|
+
outcome: "allowed",
|
|
1788
|
+
actorId: principal.id,
|
|
1789
|
+
targetId: subject.parentSessionId,
|
|
1790
|
+
remoteHostId: principal.remoteHostId,
|
|
1791
|
+
generation: principal.generation,
|
|
1792
|
+
reason: "DISCONNECTED",
|
|
1793
|
+
});
|
|
1794
|
+
live.socket.destroy();
|
|
1795
|
+
}
|
|
1796
|
+
if (changed.length > 0) this.scheduleShutdownCheck();
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
private isCurrentPrincipal(sessionId: string): boolean {
|
|
1800
|
+
const session = this.sessions.get(sessionId);
|
|
1801
|
+
if (!session) return false;
|
|
1802
|
+
if (session.info.origin !== "remote") return true;
|
|
1803
|
+
try {
|
|
1804
|
+
this.accessRegistry.validatePrincipal(sessionId, session.info.generation ?? 0);
|
|
1805
|
+
return true;
|
|
1806
|
+
} catch {
|
|
1807
|
+
return false;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
private isAuthorized(
|
|
1812
|
+
actorId: string,
|
|
1813
|
+
action: PolicyAction | BossPolicyAction,
|
|
1814
|
+
targetId: string,
|
|
1815
|
+
bossContext?: BossAuthorizationContext,
|
|
1816
|
+
): boolean {
|
|
1817
|
+
if (!this.isCurrentPrincipal(actorId) || !this.isCurrentPrincipal(targetId)) return false;
|
|
1818
|
+
return authorizeSessionAction(
|
|
1819
|
+
Array.from(this.sessions.values(), (session) => session.info),
|
|
1820
|
+
actorId,
|
|
1821
|
+
action,
|
|
1822
|
+
targetId,
|
|
1823
|
+
bossContext,
|
|
1824
|
+
).allowed;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
private broadcastLifecycle(message: BrokerMessage, subject: ConnectedSession, exclude?: string): void {
|
|
1828
|
+
for (const [id, session] of this.sessions) {
|
|
1829
|
+
if (
|
|
1830
|
+
id !== exclude
|
|
1831
|
+
&& sameIntercomScope(session.scopeId, subject.scopeId)
|
|
1832
|
+
&& this.isAuthorized(id, "discover", subject.info.id)
|
|
1833
|
+
) {
|
|
1834
|
+
writeMessage(session.socket, message);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
private askKey(fromSessionId: string, messageId: string): string {
|
|
1840
|
+
return `${fromSessionId}\u0000${messageId}`;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
private deliveryKey(fromSessionId: string, messageId: string): string {
|
|
1844
|
+
return `${fromSessionId}\u0000${messageId}`;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
private addAskEdge(messageId: string, from: string, to: string): void {
|
|
1848
|
+
const key = this.askKey(from, messageId);
|
|
1849
|
+
const previous = this.askEdges.get(key);
|
|
1850
|
+
if (previous) {
|
|
1851
|
+
clearTimeout(previous.timeout);
|
|
1852
|
+
}
|
|
1853
|
+
const createdAt = Date.now();
|
|
1854
|
+
const expiresAt = createdAt + this.askTimeoutMs;
|
|
1855
|
+
this.askEdges.set(key, {
|
|
1856
|
+
messageId,
|
|
1857
|
+
from,
|
|
1858
|
+
to,
|
|
1859
|
+
createdAt,
|
|
1860
|
+
expiresAt,
|
|
1861
|
+
state: "blocking",
|
|
1862
|
+
timeout: this.scheduleAskExpiry(key, expiresAt),
|
|
1863
|
+
});
|
|
1864
|
+
this.persistAskEdges();
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
private removeAskEdge(key: string, reason?: AskCancellationReason, notifyRecipient = false): void {
|
|
1868
|
+
const edge = this.askEdges.get(key);
|
|
1869
|
+
if (!edge) {
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
clearTimeout(edge.timeout);
|
|
1873
|
+
this.askEdges.delete(key);
|
|
1874
|
+
this.persistAskEdges();
|
|
1875
|
+
if (reason && notifyRecipient) {
|
|
1876
|
+
this.notifyAskCancelled(edge.to, edge.messageId, edge.from, reason);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
private notifyAskDeferred(edge: AskEdge): void {
|
|
1881
|
+
const recipient = this.sessions.get(edge.to);
|
|
1882
|
+
if (recipient) {
|
|
1883
|
+
writeMessage(recipient.socket, {
|
|
1884
|
+
type: "ask_deferred",
|
|
1885
|
+
messageId: edge.messageId,
|
|
1886
|
+
fromSessionId: edge.from,
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
private notifyAskCancelled(
|
|
1892
|
+
sessionId: string,
|
|
1893
|
+
messageId: string,
|
|
1894
|
+
fromSessionId: string,
|
|
1895
|
+
reason: AskCancellationReason,
|
|
1896
|
+
): void {
|
|
1897
|
+
const session = this.sessions.get(sessionId);
|
|
1898
|
+
if (session) {
|
|
1899
|
+
writeMessage(session.socket, { type: "ask_cancelled", messageId, fromSessionId, reason });
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
private clearAskEdgesForSession(sessionId: string, reason: AskCancellationReason): void {
|
|
1904
|
+
let changed = false;
|
|
1905
|
+
for (const [key, edge] of this.askEdges) {
|
|
1906
|
+
if (edge.from === sessionId || edge.to === sessionId) {
|
|
1907
|
+
clearTimeout(edge.timeout);
|
|
1908
|
+
this.askEdges.delete(key);
|
|
1909
|
+
changed = true;
|
|
1910
|
+
if (edge.from === sessionId) {
|
|
1911
|
+
this.notifyAskCancelled(edge.to, edge.messageId, edge.from, reason);
|
|
1912
|
+
} else {
|
|
1913
|
+
this.notifyAskCancelled(edge.from, edge.messageId, edge.to, reason);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
if (changed) {
|
|
1918
|
+
this.persistAskEdges();
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
private deferAskEdgesForSession(sessionId: string): void {
|
|
1923
|
+
let changed = false;
|
|
1924
|
+
for (const edge of this.askEdges.values()) {
|
|
1925
|
+
if ((edge.from === sessionId || edge.to === sessionId) && edge.state === "blocking") {
|
|
1926
|
+
edge.state = "deferred";
|
|
1927
|
+
changed = true;
|
|
1928
|
+
if (edge.from === sessionId) {
|
|
1929
|
+
this.notifyAskDeferred(edge);
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
if (changed) {
|
|
1934
|
+
this.persistAskEdges();
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
private scheduleAskExpiry(key: string, expiresAt: number): NodeJS.Timeout {
|
|
1939
|
+
const delay = Math.max(1, Math.min(expiresAt - Date.now(), 2_147_483_647));
|
|
1940
|
+
const timeout = setTimeout(() => {
|
|
1941
|
+
if (expiresAt > Date.now()) {
|
|
1942
|
+
const edge = this.askEdges.get(key);
|
|
1943
|
+
if (edge) {
|
|
1944
|
+
clearTimeout(edge.timeout);
|
|
1945
|
+
edge.timeout = this.scheduleAskExpiry(key, expiresAt);
|
|
1946
|
+
}
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
this.removeAskEdge(key, "expired", true);
|
|
1950
|
+
}, delay);
|
|
1951
|
+
timeout.unref?.();
|
|
1952
|
+
return timeout;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
private loadAskEdges(): void {
|
|
1956
|
+
if (!existsSync(ASK_STATE_PATH)) {
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
try {
|
|
1961
|
+
const parsed: unknown = JSON.parse(readFileSync(ASK_STATE_PATH, "utf-8"));
|
|
1962
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1963
|
+
throw new Error("expected an object");
|
|
1964
|
+
}
|
|
1965
|
+
const state = parsed as Record<string, unknown>;
|
|
1966
|
+
if (state.version !== 1) {
|
|
1967
|
+
throw new Error("unsupported state version");
|
|
1968
|
+
}
|
|
1969
|
+
const edges = state.edges;
|
|
1970
|
+
if (!Array.isArray(edges)) {
|
|
1971
|
+
throw new Error("expected an edges array");
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
const now = Date.now();
|
|
1975
|
+
for (const candidate of edges) {
|
|
1976
|
+
if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) {
|
|
1977
|
+
continue;
|
|
1978
|
+
}
|
|
1979
|
+
const edge = candidate as Record<string, unknown>;
|
|
1980
|
+
if (
|
|
1981
|
+
typeof edge.messageId !== "string"
|
|
1982
|
+
|| edge.messageId.length === 0
|
|
1983
|
+
|| edge.messageId.length > MAX_MESSAGE_ID_LENGTH
|
|
1984
|
+
|| !isSessionId(edge.from)
|
|
1985
|
+
|| !isSessionId(edge.to)
|
|
1986
|
+
|| typeof edge.createdAt !== "number"
|
|
1987
|
+
|| !Number.isFinite(edge.createdAt)
|
|
1988
|
+
|| typeof edge.expiresAt !== "number"
|
|
1989
|
+
|| !Number.isFinite(edge.expiresAt)
|
|
1990
|
+
|| edge.expiresAt <= now
|
|
1991
|
+
|| (edge.state !== "blocking" && edge.state !== "deferred")
|
|
1992
|
+
) {
|
|
1993
|
+
continue;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
const key = this.askKey(edge.from, edge.messageId);
|
|
1997
|
+
this.askEdges.set(key, {
|
|
1998
|
+
messageId: edge.messageId,
|
|
1999
|
+
from: edge.from,
|
|
2000
|
+
to: edge.to,
|
|
2001
|
+
createdAt: edge.createdAt,
|
|
2002
|
+
expiresAt: edge.expiresAt,
|
|
2003
|
+
state: "deferred",
|
|
2004
|
+
timeout: this.scheduleAskExpiry(key, edge.expiresAt),
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
this.persistAskEdges();
|
|
2008
|
+
} catch (error) {
|
|
2009
|
+
console.error(`Failed to load persisted ask state at ${ASK_STATE_PATH}:`, error);
|
|
2010
|
+
for (const edge of this.askEdges.values()) {
|
|
2011
|
+
clearTimeout(edge.timeout);
|
|
2012
|
+
}
|
|
2013
|
+
this.askEdges.clear();
|
|
2014
|
+
try {
|
|
2015
|
+
const corruptPath = `${ASK_STATE_PATH}.corrupt-${Date.now()}`;
|
|
2016
|
+
renameSync(ASK_STATE_PATH, corruptPath);
|
|
2017
|
+
restrictIntercomRuntimeFile(corruptPath);
|
|
2018
|
+
} catch {
|
|
2019
|
+
// Keep running with empty state even if the corrupt file cannot be moved.
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
private persistAskEdges(): void {
|
|
2025
|
+
const edges: PersistedAskEdge[] = Array.from(this.askEdges.values(), (edge) => ({
|
|
2026
|
+
messageId: edge.messageId,
|
|
2027
|
+
from: edge.from,
|
|
2028
|
+
to: edge.to,
|
|
2029
|
+
createdAt: edge.createdAt,
|
|
2030
|
+
expiresAt: edge.expiresAt,
|
|
2031
|
+
state: edge.state,
|
|
2032
|
+
}));
|
|
2033
|
+
writeDurableJson(ASK_STATE_PATH, { version: 1, edges });
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
private countAskEdgesFrom(sessionId: string): number {
|
|
2037
|
+
let count = 0;
|
|
2038
|
+
for (const edge of this.askEdges.values()) {
|
|
2039
|
+
if (edge.from === sessionId) {
|
|
2040
|
+
count += 1;
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
return count;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
private countPendingDeliveriesFrom(sessionId: string): number {
|
|
2047
|
+
let count = 0;
|
|
2048
|
+
for (const delivery of this.pendingDeliveries.values()) {
|
|
2049
|
+
if (delivery.from === sessionId) {
|
|
2050
|
+
count += 1;
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
return count;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
private bossControlKey(fromSessionId: string, envelope: BossControlEnvelope): string {
|
|
2057
|
+
return canonicalHash("agent-intercom-codex/boss-control/idempotency-scope/v1", {
|
|
2058
|
+
fromSessionId,
|
|
2059
|
+
bossRunId: envelope.bossRunId,
|
|
2060
|
+
participantId: envelope.participantId,
|
|
2061
|
+
bindingEpoch: Number(envelope.bindingEpoch),
|
|
2062
|
+
idempotencyKey: envelope.idempotencyKey,
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
private bossControlFingerprint(toSessionId: string, envelope: BossControlEnvelope): string {
|
|
2067
|
+
const { messageId: _transportMessageId, ...stableEnvelope } = envelope;
|
|
2068
|
+
return canonicalHash("agent-intercom-codex/boss-control/request/v1", { toSessionId, envelope: stableEnvelope });
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
private acknowledgePendingBossControl(
|
|
2072
|
+
deliveryId: string,
|
|
2073
|
+
messageId: string,
|
|
2074
|
+
idempotencyKey: string,
|
|
2075
|
+
sessionId: string,
|
|
2076
|
+
socket: net.Socket,
|
|
2077
|
+
): void {
|
|
2078
|
+
const pending = this.pendingBossControls.get(deliveryId);
|
|
2079
|
+
if (
|
|
2080
|
+
!pending
|
|
2081
|
+
|| pending.to !== sessionId
|
|
2082
|
+
|| pending.recipientSocket !== socket
|
|
2083
|
+
|| pending.messageId !== messageId
|
|
2084
|
+
|| pending.envelope.idempotencyKey !== idempotencyKey
|
|
2085
|
+
) return;
|
|
2086
|
+
const sender = this.sessions.get(pending.from);
|
|
2087
|
+
const recipient = this.sessions.get(pending.to);
|
|
2088
|
+
const { controlKind } = bossControlKind(pending.envelope);
|
|
2089
|
+
if (
|
|
2090
|
+
!sender
|
|
2091
|
+
|| !recipient
|
|
2092
|
+
|| !authorizeSessionAction(
|
|
2093
|
+
Array.from(this.sessions.values(), (session) => session.info),
|
|
2094
|
+
pending.from,
|
|
2095
|
+
"control",
|
|
2096
|
+
pending.to,
|
|
2097
|
+
{ controlKind, correlated: hasAuthoritativeBossControlCorrelation() },
|
|
2098
|
+
).allowed
|
|
2099
|
+
) {
|
|
2100
|
+
this.failPendingBossControl(deliveryId, "POLICY_DENIED", "Boss control authorization changed before acknowledgement");
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
if (sender.socket === pending.senderSocket) {
|
|
2104
|
+
const result: BossControlResult = {
|
|
2105
|
+
type: "boss_control_result",
|
|
2106
|
+
requestId: pending.requestId,
|
|
2107
|
+
messageId: pending.messageId,
|
|
2108
|
+
idempotencyKey: pending.envelope.idempotencyKey,
|
|
2109
|
+
status: "delivered",
|
|
2110
|
+
deliveryId,
|
|
2111
|
+
delivered: true,
|
|
2112
|
+
};
|
|
2113
|
+
this.bossControlLedger.recordTerminal(pending.key, pending.fingerprint, result);
|
|
2114
|
+
clearTimeout(pending.timeout);
|
|
2115
|
+
this.pendingBossControls.delete(deliveryId);
|
|
2116
|
+
this.pendingBossControlKeys.delete(pending.key);
|
|
2117
|
+
writeMessage(sender.socket, result);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
private failPendingBossControl(
|
|
2122
|
+
deliveryId: string,
|
|
2123
|
+
code: BossControlFailureCode,
|
|
2124
|
+
reason: string,
|
|
2125
|
+
): void {
|
|
2126
|
+
const pending = this.pendingBossControls.get(deliveryId);
|
|
2127
|
+
if (!pending) return;
|
|
2128
|
+
const sender = this.sessions.get(pending.from);
|
|
2129
|
+
const result: BossControlResult = {
|
|
2130
|
+
type: "boss_control_result",
|
|
2131
|
+
requestId: pending.requestId,
|
|
2132
|
+
messageId: pending.messageId,
|
|
2133
|
+
idempotencyKey: pending.envelope.idempotencyKey,
|
|
2134
|
+
status: "rejected",
|
|
2135
|
+
delivered: false,
|
|
2136
|
+
code,
|
|
2137
|
+
reason,
|
|
2138
|
+
deliveryId,
|
|
2139
|
+
};
|
|
2140
|
+
this.bossControlLedger.recordTerminal(pending.key, pending.fingerprint, result);
|
|
2141
|
+
clearTimeout(pending.timeout);
|
|
2142
|
+
this.pendingBossControls.delete(deliveryId);
|
|
2143
|
+
this.pendingBossControlKeys.delete(pending.key);
|
|
2144
|
+
if (sender?.socket === pending.senderSocket) writeMessage(sender.socket, result);
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
private clearPendingBossControlsForSession(sessionId: string, socket: net.Socket): void {
|
|
2148
|
+
for (const pending of Array.from(this.pendingBossControls.values())) {
|
|
2149
|
+
if (pending.to === sessionId && pending.recipientSocket === socket) {
|
|
2150
|
+
this.failPendingBossControl(pending.deliveryId, "RECIPIENT_DISCONNECTED", "Boss control recipient disconnected before acknowledgement");
|
|
2151
|
+
} else if (pending.from === sessionId && pending.senderSocket === socket) {
|
|
2152
|
+
clearTimeout(pending.timeout);
|
|
2153
|
+
this.pendingBossControls.delete(pending.deliveryId);
|
|
2154
|
+
this.pendingBossControlKeys.delete(pending.key);
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
private acknowledgePendingDelivery(deliveryId: string, sessionId: string, socket: net.Socket): void {
|
|
2160
|
+
const pending = this.pendingDeliveries.get(deliveryId);
|
|
2161
|
+
if (!pending || pending.to !== sessionId || pending.recipientSocket !== socket) {
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
const sender = this.sessions.get(pending.from);
|
|
2165
|
+
const recipient = this.sessions.get(pending.to);
|
|
2166
|
+
if (
|
|
2167
|
+
!sender
|
|
2168
|
+
|| !recipient
|
|
2169
|
+
|| (sender.info.generation ?? 1) !== pending.fromGeneration
|
|
2170
|
+
|| (recipient.info.generation ?? 1) !== pending.toGeneration
|
|
2171
|
+
|| !this.isAuthorized(pending.from, pending.action, pending.to)
|
|
2172
|
+
) {
|
|
2173
|
+
this.failPendingDelivery(deliveryId, "SESSION_NOT_FOUND", "Delivery authorization changed before acknowledgement");
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
clearTimeout(pending.timeout);
|
|
2178
|
+
this.pendingDeliveries.delete(deliveryId);
|
|
2179
|
+
this.pendingDeliveryKeys.delete(pending.key);
|
|
2180
|
+
if (pending.message.replyTo) {
|
|
2181
|
+
this.removeAskEdge(this.askKey(pending.to, pending.message.replyTo));
|
|
2182
|
+
}
|
|
2183
|
+
const response = { type: "delivered" as const, messageId: pending.message.id, deliveryId };
|
|
2184
|
+
this.recentDeliveries.set(pending.key, {
|
|
2185
|
+
fingerprint: pending.fingerprint,
|
|
2186
|
+
from: pending.from,
|
|
2187
|
+
to: pending.to,
|
|
2188
|
+
action: pending.action,
|
|
2189
|
+
fromGeneration: pending.fromGeneration,
|
|
2190
|
+
toGeneration: pending.toGeneration,
|
|
2191
|
+
retryable: false,
|
|
2192
|
+
response,
|
|
2193
|
+
expiresAt: Date.now() + RECENT_DELIVERY_TTL_MS,
|
|
2194
|
+
});
|
|
2195
|
+
if (sender.socket === pending.senderSocket) {
|
|
2196
|
+
writeMessage(sender.socket, response);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
private failPendingDelivery(deliveryId: string, code: DeliveryFailureCode, reason: string): void {
|
|
2201
|
+
const pending = this.pendingDeliveries.get(deliveryId);
|
|
2202
|
+
if (!pending) {
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
clearTimeout(pending.timeout);
|
|
2207
|
+
this.pendingDeliveries.delete(deliveryId);
|
|
2208
|
+
this.pendingDeliveryKeys.delete(pending.key);
|
|
2209
|
+
if (pending.message.expectsReply) {
|
|
2210
|
+
this.removeAskEdge(this.askKey(pending.from, pending.message.id), "delivery_failed", true);
|
|
2211
|
+
}
|
|
2212
|
+
const response = {
|
|
2213
|
+
type: "delivery_failed" as const,
|
|
2214
|
+
messageId: pending.message.id,
|
|
2215
|
+
accepted: true,
|
|
2216
|
+
code,
|
|
2217
|
+
reason,
|
|
2218
|
+
};
|
|
2219
|
+
this.recentDeliveries.set(pending.key, {
|
|
2220
|
+
fingerprint: pending.fingerprint,
|
|
2221
|
+
from: pending.from,
|
|
2222
|
+
to: pending.to,
|
|
2223
|
+
action: pending.action,
|
|
2224
|
+
fromGeneration: pending.fromGeneration,
|
|
2225
|
+
toGeneration: pending.toGeneration,
|
|
2226
|
+
retryable: true,
|
|
2227
|
+
response,
|
|
2228
|
+
expiresAt: Date.now() + RECENT_DELIVERY_TTL_MS,
|
|
2229
|
+
});
|
|
2230
|
+
const sender = this.sessions.get(pending.from);
|
|
2231
|
+
if (sender?.socket === pending.senderSocket) {
|
|
2232
|
+
writeMessage(sender.socket, response);
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
private clearPendingDeliveriesForSession(sessionId: string, socket: net.Socket): void {
|
|
2237
|
+
this.clearPendingBossControlsForSession(sessionId, socket);
|
|
2238
|
+
for (const delivery of Array.from(this.pendingDeliveries.values())) {
|
|
2239
|
+
if (delivery.to === sessionId && delivery.recipientSocket === socket) {
|
|
2240
|
+
this.failPendingDelivery(delivery.id, "RECIPIENT_DISCONNECTED", "Recipient disconnected before acknowledging the message");
|
|
2241
|
+
} else if (delivery.from === sessionId && delivery.senderSocket === socket) {
|
|
2242
|
+
this.failPendingDelivery(delivery.id, "SENDER_DISCONNECTED", "Sender disconnected before delivery was acknowledged");
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
private pruneRecentDeliveries(now = Date.now()): void {
|
|
2248
|
+
for (const [key, delivery] of this.recentDeliveries) {
|
|
2249
|
+
if (delivery.expiresAt <= now) {
|
|
2250
|
+
this.recentDeliveries.delete(key);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
private findSessions(actorId: string, nameOrId: string): ConnectedSession[] {
|
|
2256
|
+
// v4 resolution order: global exact full session ID wins across scopes.
|
|
2257
|
+
const byId = this.sessions.get(nameOrId);
|
|
2258
|
+
if (byId) return [byId];
|
|
2259
|
+
|
|
2260
|
+
const actor = this.sessions.get(actorId);
|
|
2261
|
+
if (!actor) return [];
|
|
2262
|
+
// Same-scope exact name, then same-scope unique ID prefix; never cross scope.
|
|
2263
|
+
const sameScope = Array.from(this.sessions.values()).filter((session) =>
|
|
2264
|
+
sameIntercomScope(actor.scopeId, session.scopeId)
|
|
2265
|
+
);
|
|
2266
|
+
const lowerName = nameOrId.toLowerCase();
|
|
2267
|
+
const byName = sameScope.filter((session) => session.info.name?.toLowerCase() === lowerName);
|
|
2268
|
+
if (byName.length > 0) return byName;
|
|
2269
|
+
|
|
2270
|
+
return sameScope.filter((session) => session.info.id.startsWith(nameOrId));
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
private shutdown(): void {
|
|
2274
|
+
console.log("Broker shutting down");
|
|
2275
|
+
if (this.expiryTimer) {
|
|
2276
|
+
clearInterval(this.expiryTimer);
|
|
2277
|
+
this.expiryTimer = null;
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
for (const session of this.sessions.values()) {
|
|
2281
|
+
session.socket.end();
|
|
2282
|
+
}
|
|
2283
|
+
this.sessions.clear();
|
|
2284
|
+
for (const delivery of this.pendingDeliveries.values()) {
|
|
2285
|
+
clearTimeout(delivery.timeout);
|
|
2286
|
+
}
|
|
2287
|
+
this.pendingDeliveries.clear();
|
|
2288
|
+
this.pendingDeliveryKeys.clear();
|
|
2289
|
+
for (const pending of this.pendingBossControls.values()) {
|
|
2290
|
+
clearTimeout(pending.timeout);
|
|
2291
|
+
}
|
|
2292
|
+
this.pendingBossControls.clear();
|
|
2293
|
+
this.pendingBossControlKeys.clear();
|
|
2294
|
+
for (const edge of this.askEdges.values()) {
|
|
2295
|
+
clearTimeout(edge.timeout);
|
|
2296
|
+
}
|
|
2297
|
+
this.askEdges.clear();
|
|
2298
|
+
const ownsBroker = hasBrokerOwnership(OWNER_PATH);
|
|
2299
|
+
if (ownsBroker && typeof LISTEN_TARGET === "string" && process.platform !== "win32") {
|
|
2300
|
+
for (const socketPath of [LISTEN_TARGET, REMOTE_LISTEN_TARGET]) {
|
|
2301
|
+
try {
|
|
2302
|
+
unlinkSync(socketPath);
|
|
2303
|
+
} catch {
|
|
2304
|
+
// The socket may already be gone if shutdown started after a disconnect.
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
if (ownsBroker) {
|
|
2309
|
+
try {
|
|
2310
|
+
unlinkSync(PORT_PATH);
|
|
2311
|
+
} catch {
|
|
2312
|
+
// The TCP endpoint file only exists when opt-in TCP transport is active.
|
|
2313
|
+
}
|
|
2314
|
+
try {
|
|
2315
|
+
unlinkSync(PID_PATH);
|
|
2316
|
+
} catch {
|
|
2317
|
+
// The PID file may already be gone if startup never completed.
|
|
2318
|
+
}
|
|
2319
|
+
releaseBrokerOwnership(OWNER_PATH);
|
|
2320
|
+
}
|
|
2321
|
+
this.server.close();
|
|
2322
|
+
this.remoteServer?.close();
|
|
2323
|
+
process.exit(0);
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
new IntercomBroker().start();
|