@ctliz/agent-intercom-claude 0.12.0-connect.3
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/.claude-plugin/plugin.json +26 -0
- package/.mcp.json +10 -0
- package/CHANGELOG.md +62 -0
- package/COPYRIGHT +6 -0
- package/LICENSE +661 -0
- package/LICENSE_TRANSITION.md +25 -0
- package/README.md +506 -0
- package/THIRD_PARTY_NOTICES.md +25 -0
- package/broker/access-credential.ts +76 -0
- package/broker/access-registry.ts +460 -0
- package/broker/audit.ts +60 -0
- package/broker/authorization.ts +125 -0
- package/broker/boss-contracts.ts +267 -0
- package/broker/broker.ts +2028 -0
- package/broker/client.ts +1048 -0
- package/broker/framing.ts +87 -0
- package/broker/ownership.ts +61 -0
- package/broker/paths.ts +161 -0
- package/broker/spawn.ts +463 -0
- package/broker/validation.ts +132 -0
- package/claude/cci.ts +512 -0
- package/claude/ccim.ts +31 -0
- package/claude/cli-runner.ts +220 -0
- package/claude/contact.ts +33 -0
- package/claude/inbox-monitor.ts +49 -0
- package/claude/inbox.ts +77 -0
- package/claude/mcp-protocol.ts +253 -0
- package/claude/native-bridge.ts +219 -0
- package/claude/native-protocol.ts +346 -0
- package/claude/permission-policy.ts +287 -0
- package/claude/runtime.ts +510 -0
- package/claude/server.ts +69 -0
- package/claude/team.ts +348 -0
- package/claude/transport.ts +97 -0
- package/claude/worker-config.ts +198 -0
- package/claude/worker-daemon.ts +418 -0
- package/commands/intercom-id.md +9 -0
- package/commands/intercom.md +8 -0
- package/config.ts +195 -0
- package/dist/broker.mjs +2530 -0
- package/dist/cci.mjs +3607 -0
- package/dist/ccim.mjs +3621 -0
- package/dist/claude-server.mjs +2730 -0
- package/dist/inbox-monitor.mjs +68 -0
- package/dist/worker-daemon.mjs +2797 -0
- package/durable-json.ts +25 -0
- package/licenses/MIT-pi-claude-link.txt +21 -0
- package/licenses/MIT-pi-intercom.txt +21 -0
- package/monitors/monitors.json +8 -0
- package/outbound-outbox.ts +116 -0
- package/package.json +93 -0
- package/protocol-v4/contract.ts +45 -0
- package/provider/protected-service.ts +162 -0
- package/provider/provider.mjs +34 -0
- package/scripts/build.mjs +71 -0
- package/scripts/verify-core-provenance.mjs +64 -0
- package/scripts/verify-packed-runtime.mjs +140 -0
- package/skills/claude-intercom/SKILL.md +90 -0
- package/types.ts +212 -0
|
@@ -0,0 +1,2797 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// claude/worker-daemon.ts
|
|
4
|
+
import { once } from "node:events";
|
|
5
|
+
import { realpathSync as realpathSync2 } from "node:fs";
|
|
6
|
+
import { basename } from "node:path";
|
|
7
|
+
|
|
8
|
+
// claude/cli-runner.ts
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
|
|
11
|
+
// claude/permission-policy.ts
|
|
12
|
+
import {
|
|
13
|
+
closeSync,
|
|
14
|
+
constants,
|
|
15
|
+
fstatSync,
|
|
16
|
+
lstatSync,
|
|
17
|
+
openSync,
|
|
18
|
+
realpathSync
|
|
19
|
+
} from "node:fs";
|
|
20
|
+
import { dirname, isAbsolute, join, parse, relative, sep } from "node:path";
|
|
21
|
+
var CLAUDE_PERMISSION_MODES = [
|
|
22
|
+
"acceptEdits",
|
|
23
|
+
"auto",
|
|
24
|
+
"bypassPermissions",
|
|
25
|
+
"manual",
|
|
26
|
+
"dontAsk",
|
|
27
|
+
"plan"
|
|
28
|
+
];
|
|
29
|
+
function validateClaudePermissionMode(value, field = "permissionMode") {
|
|
30
|
+
if (typeof value !== "string" || !CLAUDE_PERMISSION_MODES.includes(value)) {
|
|
31
|
+
throw new Error(`${field} must be one of: ${CLAUDE_PERMISSION_MODES.join(", ")}`);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
function resolveClaudePermissionPolicy(input) {
|
|
36
|
+
const ceiling = input.ceiling ?? "standard";
|
|
37
|
+
if (ceiling !== "standard" && ceiling !== "read-only") {
|
|
38
|
+
throw new Error("permission ceiling must be standard or read-only");
|
|
39
|
+
}
|
|
40
|
+
if (input.dangerouslySkipPermissions !== void 0 && typeof input.dangerouslySkipPermissions !== "boolean") {
|
|
41
|
+
throw new Error("dangerouslySkipPermissions must be a boolean");
|
|
42
|
+
}
|
|
43
|
+
const dangerouslySkipPermissions = input.dangerouslySkipPermissions === true;
|
|
44
|
+
const requestedMode = input.permissionMode === void 0 ? void 0 : validateClaudePermissionMode(input.permissionMode);
|
|
45
|
+
if (ceiling === "read-only") {
|
|
46
|
+
if (dangerouslySkipPermissions) {
|
|
47
|
+
throw new Error("read-only Claude roles cannot skip permission checks");
|
|
48
|
+
}
|
|
49
|
+
if (requestedMode !== void 0 && requestedMode !== "plan") {
|
|
50
|
+
throw new Error("read-only Claude roles require permission mode plan");
|
|
51
|
+
}
|
|
52
|
+
return { permissionMode: "plan", dangerouslySkipPermissions: false, ceiling };
|
|
53
|
+
}
|
|
54
|
+
if (dangerouslySkipPermissions) {
|
|
55
|
+
return { dangerouslySkipPermissions: true, ceiling };
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
permissionMode: requestedMode ?? "manual",
|
|
59
|
+
dangerouslySkipPermissions: false,
|
|
60
|
+
ceiling
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
var PERMISSION_OVERRIDE_OPTIONS = /* @__PURE__ */ new Set([
|
|
64
|
+
"--allow-dangerously-skip-permissions",
|
|
65
|
+
"--dangerously-skip-permissions",
|
|
66
|
+
"--permission-mode"
|
|
67
|
+
]);
|
|
68
|
+
var CLAUDE_2_1_220_HARDENED_CAPABILITY_OPTIONS = [
|
|
69
|
+
"--add-dir",
|
|
70
|
+
"--agent",
|
|
71
|
+
"--agents",
|
|
72
|
+
"--allowed-tools",
|
|
73
|
+
"--allowedTools",
|
|
74
|
+
"--mcp-config",
|
|
75
|
+
"--plugin-dir",
|
|
76
|
+
"--plugin-dir-no-mcp",
|
|
77
|
+
"--plugin-url",
|
|
78
|
+
"--setting-sources",
|
|
79
|
+
"--settings",
|
|
80
|
+
"--tools",
|
|
81
|
+
"--worktree",
|
|
82
|
+
"-w"
|
|
83
|
+
];
|
|
84
|
+
var HARDENED_PERMISSION_CAPABILITY_OPTIONS = new Set(
|
|
85
|
+
CLAUDE_2_1_220_HARDENED_CAPABILITY_OPTIONS
|
|
86
|
+
);
|
|
87
|
+
function permissionCapabilityOption(arg) {
|
|
88
|
+
const equalsIndex = arg.indexOf("=");
|
|
89
|
+
const option = equalsIndex === -1 ? arg : arg.slice(0, equalsIndex);
|
|
90
|
+
if (HARDENED_PERMISSION_CAPABILITY_OPTIONS.has(option)) return option;
|
|
91
|
+
if (arg.length > 1 && arg.startsWith("-") && !arg.startsWith("--")) return arg;
|
|
92
|
+
return void 0;
|
|
93
|
+
}
|
|
94
|
+
function isContainedPath(root, candidate) {
|
|
95
|
+
const pathFromRoot = relative(root, candidate);
|
|
96
|
+
return pathFromRoot === "" || !isAbsolute(pathFromRoot) && pathFromRoot !== ".." && !pathFromRoot.startsWith(`..${sep}`);
|
|
97
|
+
}
|
|
98
|
+
function sameFile(left, right) {
|
|
99
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
100
|
+
}
|
|
101
|
+
function pathError(field, detail, cause) {
|
|
102
|
+
return cause === void 0 ? new Error(`${field} ${detail}`) : new Error(`${field} ${detail}`, { cause });
|
|
103
|
+
}
|
|
104
|
+
function inspectExistingRealDirectory(input, base, field) {
|
|
105
|
+
if (input.length === 0) {
|
|
106
|
+
throw pathError(field, "must name an existing real directory");
|
|
107
|
+
}
|
|
108
|
+
const rawAbsolute = isAbsolute(input) ? input : `${base.endsWith(sep) ? base : `${base}${sep}`}${input}`;
|
|
109
|
+
const root = parse(rawAbsolute).root;
|
|
110
|
+
let cursor = root;
|
|
111
|
+
const inspect = (path) => {
|
|
112
|
+
let descriptor2;
|
|
113
|
+
try {
|
|
114
|
+
descriptor2 = lstatSync(path, { bigint: true });
|
|
115
|
+
} catch (cause) {
|
|
116
|
+
throw pathError(field, "must name an existing real directory", cause);
|
|
117
|
+
}
|
|
118
|
+
if (descriptor2.isSymbolicLink()) {
|
|
119
|
+
throw pathError(field, "must not contain a symbolic-link component");
|
|
120
|
+
}
|
|
121
|
+
if (!descriptor2.isDirectory()) {
|
|
122
|
+
throw pathError(field, "must contain only existing directories");
|
|
123
|
+
}
|
|
124
|
+
return descriptor2;
|
|
125
|
+
};
|
|
126
|
+
let descriptor = inspect(cursor);
|
|
127
|
+
for (const component of rawAbsolute.slice(root.length).split(sep)) {
|
|
128
|
+
if (component === "" || component === ".") continue;
|
|
129
|
+
cursor = component === ".." ? dirname(cursor) : join(cursor, component);
|
|
130
|
+
descriptor = inspect(cursor);
|
|
131
|
+
}
|
|
132
|
+
let canonicalPath;
|
|
133
|
+
try {
|
|
134
|
+
canonicalPath = realpathSync(cursor);
|
|
135
|
+
} catch (cause) {
|
|
136
|
+
throw pathError(field, "cannot be bound to a canonical real directory", cause);
|
|
137
|
+
}
|
|
138
|
+
return { canonicalPath, descriptor };
|
|
139
|
+
}
|
|
140
|
+
function openBoundDirectory(input, base, field) {
|
|
141
|
+
if (typeof constants.O_DIRECTORY !== "number" || typeof constants.O_NOFOLLOW !== "number") {
|
|
142
|
+
throw pathError(field, "cannot be securely bound on this platform");
|
|
143
|
+
}
|
|
144
|
+
const first = inspectExistingRealDirectory(input, base, field);
|
|
145
|
+
let fd;
|
|
146
|
+
try {
|
|
147
|
+
fd = openSync(first.canonicalPath, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
148
|
+
} catch (cause) {
|
|
149
|
+
throw pathError(field, "cannot be bound to an open real directory", cause);
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const opened = fstatSync(fd, { bigint: true });
|
|
153
|
+
if (!opened.isDirectory() || !sameFile(opened, first.descriptor)) {
|
|
154
|
+
throw pathError(field, "changed while its real directory was being bound");
|
|
155
|
+
}
|
|
156
|
+
const second = inspectExistingRealDirectory(input, base, field);
|
|
157
|
+
if (second.canonicalPath !== first.canonicalPath || !sameFile(opened, second.descriptor)) {
|
|
158
|
+
throw pathError(field, "changed while its real directory was being bound");
|
|
159
|
+
}
|
|
160
|
+
return { path: first.canonicalPath, fd };
|
|
161
|
+
} catch (error) {
|
|
162
|
+
closeSync(fd);
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function bindHardenedClaudePaths(cwd, addDirs, field = "hardened Claude paths") {
|
|
167
|
+
const descriptors = [];
|
|
168
|
+
try {
|
|
169
|
+
const cwdBinding = openBoundDirectory(cwd, process.cwd(), `${field}.cwd`);
|
|
170
|
+
descriptors.push(cwdBinding.fd);
|
|
171
|
+
const canonicalAddDirs = addDirs.map((addDir, index) => {
|
|
172
|
+
const binding = openBoundDirectory(addDir, cwdBinding.path, `${field}.addDirs[${index}]`);
|
|
173
|
+
descriptors.push(binding.fd);
|
|
174
|
+
if (!isContainedPath(cwdBinding.path, binding.path)) {
|
|
175
|
+
throw pathError(`${field}.addDirs[${index}]`, "must stay within the assigned workspace root");
|
|
176
|
+
}
|
|
177
|
+
return binding.path;
|
|
178
|
+
});
|
|
179
|
+
let released = false;
|
|
180
|
+
return {
|
|
181
|
+
cwd: cwdBinding.path,
|
|
182
|
+
addDirs: canonicalAddDirs,
|
|
183
|
+
release() {
|
|
184
|
+
if (released) return;
|
|
185
|
+
released = true;
|
|
186
|
+
for (const fd of descriptors.splice(0)) closeSync(fd);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
} catch (error) {
|
|
190
|
+
for (const fd of descriptors) closeSync(fd);
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function assertNoPermissionOverrides(args, field = "claudeArgs", ceiling = "standard") {
|
|
195
|
+
if (ceiling !== "read-only") return;
|
|
196
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
197
|
+
const arg = args[index];
|
|
198
|
+
const option = arg.includes("=") ? arg.slice(0, arg.indexOf("=")) : arg;
|
|
199
|
+
if (PERMISSION_OVERRIDE_OPTIONS.has(option)) {
|
|
200
|
+
throw new Error(`${field}[${index}] cannot override the validated Claude permission policy`);
|
|
201
|
+
}
|
|
202
|
+
if (ceiling === "read-only" && permissionCapabilityOption(arg) !== void 0) {
|
|
203
|
+
throw new Error(`${field}[${index}] cannot add a permission capability to a hardened Claude role`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// claude/cli-runner.ts
|
|
209
|
+
function prepareClaudeLaunch(options) {
|
|
210
|
+
const args = ["-p", "--output-format", "json"];
|
|
211
|
+
const permission = resolveClaudePermissionPolicy({
|
|
212
|
+
permissionMode: options.permissionMode,
|
|
213
|
+
dangerouslySkipPermissions: options.dangerouslySkipPermissions,
|
|
214
|
+
ceiling: options.permissionCeiling
|
|
215
|
+
});
|
|
216
|
+
assertNoPermissionOverrides(options.extraArgs ?? [], "extraArgs", permission.ceiling);
|
|
217
|
+
let cwd = options.cwd;
|
|
218
|
+
let addDirs = options.addDirs ?? [];
|
|
219
|
+
let release = () => {
|
|
220
|
+
};
|
|
221
|
+
if (permission.ceiling === "read-only") {
|
|
222
|
+
if (options.mcpConfig !== void 0) {
|
|
223
|
+
throw new Error("mcpConfig cannot add an arbitrary MCP capability to a hardened Claude role");
|
|
224
|
+
}
|
|
225
|
+
const binding = bindHardenedClaudePaths(options.cwd, addDirs);
|
|
226
|
+
cwd = binding.cwd;
|
|
227
|
+
addDirs = binding.addDirs;
|
|
228
|
+
release = binding.release;
|
|
229
|
+
args.push("--bare");
|
|
230
|
+
}
|
|
231
|
+
if (options.sessionId) {
|
|
232
|
+
args.push("--resume", options.sessionId);
|
|
233
|
+
}
|
|
234
|
+
if (options.model) {
|
|
235
|
+
args.push("--model", options.model);
|
|
236
|
+
}
|
|
237
|
+
if (options.appendSystemPrompt) {
|
|
238
|
+
args.push("--append-system-prompt", options.appendSystemPrompt);
|
|
239
|
+
}
|
|
240
|
+
if (permission.dangerouslySkipPermissions) {
|
|
241
|
+
args.push("--dangerously-skip-permissions");
|
|
242
|
+
} else if (permission.permissionMode && (permission.ceiling === "read-only" || options.permissionMode !== void 0)) {
|
|
243
|
+
args.push("--permission-mode", permission.permissionMode);
|
|
244
|
+
}
|
|
245
|
+
for (const dir of addDirs) {
|
|
246
|
+
args.push("--add-dir", dir);
|
|
247
|
+
}
|
|
248
|
+
if (options.mcpConfig) {
|
|
249
|
+
args.push("--mcp-config", options.mcpConfig);
|
|
250
|
+
}
|
|
251
|
+
if (options.extraArgs?.length) {
|
|
252
|
+
args.push(...options.extraArgs);
|
|
253
|
+
}
|
|
254
|
+
return { args, cwd, release };
|
|
255
|
+
}
|
|
256
|
+
function isRecord(value) {
|
|
257
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
258
|
+
}
|
|
259
|
+
function runClaudeTurn(options) {
|
|
260
|
+
return new Promise((resolve4, reject) => {
|
|
261
|
+
const command = options.claudeCommand ?? "claude";
|
|
262
|
+
const launch = prepareClaudeLaunch(options);
|
|
263
|
+
let child;
|
|
264
|
+
try {
|
|
265
|
+
child = spawn(command, launch.args, {
|
|
266
|
+
cwd: launch.cwd,
|
|
267
|
+
env: { ...process.env, ...options.env },
|
|
268
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
269
|
+
});
|
|
270
|
+
} finally {
|
|
271
|
+
launch.release();
|
|
272
|
+
}
|
|
273
|
+
let stdout = "";
|
|
274
|
+
let stderr = "";
|
|
275
|
+
let settled = false;
|
|
276
|
+
let timeoutHandle;
|
|
277
|
+
const cleanup = () => {
|
|
278
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
279
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
280
|
+
};
|
|
281
|
+
const finishReject = (error) => {
|
|
282
|
+
if (settled) return;
|
|
283
|
+
settled = true;
|
|
284
|
+
cleanup();
|
|
285
|
+
reject(error);
|
|
286
|
+
};
|
|
287
|
+
const finishResolve = (result) => {
|
|
288
|
+
if (settled) return;
|
|
289
|
+
settled = true;
|
|
290
|
+
cleanup();
|
|
291
|
+
resolve4(result);
|
|
292
|
+
};
|
|
293
|
+
const onAbort = () => {
|
|
294
|
+
child.kill("SIGTERM");
|
|
295
|
+
finishReject(new Error("Claude turn aborted"));
|
|
296
|
+
};
|
|
297
|
+
if (options.signal) {
|
|
298
|
+
if (options.signal.aborted) {
|
|
299
|
+
child.kill("SIGTERM");
|
|
300
|
+
finishReject(new Error("Claude turn aborted"));
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
304
|
+
}
|
|
305
|
+
if (options.timeoutMs) {
|
|
306
|
+
timeoutHandle = setTimeout(() => {
|
|
307
|
+
child.kill("SIGTERM");
|
|
308
|
+
finishReject(new Error(`Claude turn timed out after ${options.timeoutMs}ms`));
|
|
309
|
+
}, options.timeoutMs);
|
|
310
|
+
}
|
|
311
|
+
child.on("error", (error) => {
|
|
312
|
+
finishReject(new Error(`Failed to spawn "${command}": ${error.message}`, { cause: error }));
|
|
313
|
+
});
|
|
314
|
+
child.stdout?.on("data", (chunk) => {
|
|
315
|
+
stdout += chunk.toString("utf8");
|
|
316
|
+
});
|
|
317
|
+
child.stderr?.on("data", (chunk) => {
|
|
318
|
+
stderr += chunk.toString("utf8");
|
|
319
|
+
});
|
|
320
|
+
child.on("close", (exitCode) => {
|
|
321
|
+
if (settled) return;
|
|
322
|
+
let parsed;
|
|
323
|
+
try {
|
|
324
|
+
parsed = JSON.parse(stdout.trim());
|
|
325
|
+
} catch {
|
|
326
|
+
const fallback = stdout.trim() || stderr.trim();
|
|
327
|
+
finishResolve({
|
|
328
|
+
sessionId: options.sessionId ?? null,
|
|
329
|
+
result: fallback,
|
|
330
|
+
isError: exitCode !== 0,
|
|
331
|
+
raw: stdout
|
|
332
|
+
});
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (isRecord(parsed) && typeof parsed.result === "string") {
|
|
336
|
+
finishResolve({
|
|
337
|
+
sessionId: typeof parsed.session_id === "string" ? parsed.session_id : null,
|
|
338
|
+
result: parsed.result,
|
|
339
|
+
isError: Boolean(parsed.is_error),
|
|
340
|
+
raw: parsed
|
|
341
|
+
});
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
finishResolve({
|
|
345
|
+
sessionId: options.sessionId ?? null,
|
|
346
|
+
result: stdout.trim(),
|
|
347
|
+
isError: exitCode !== 0,
|
|
348
|
+
raw: parsed
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
child.stdin?.write(options.prompt);
|
|
352
|
+
child.stdin?.end();
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// claude/worker-config.ts
|
|
357
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
358
|
+
import { dirname as dirname2, join as join3, resolve as resolve2 } from "node:path";
|
|
359
|
+
import { cwd as processCwd } from "node:process";
|
|
360
|
+
|
|
361
|
+
// broker/paths.ts
|
|
362
|
+
import { chmodSync, mkdirSync, readFileSync } from "fs";
|
|
363
|
+
import { isAbsolute as isAbsolute2, join as join2, resolve } from "path";
|
|
364
|
+
import { homedir } from "os";
|
|
365
|
+
var INTERCOM_DIR_MODE = 448;
|
|
366
|
+
var INTERCOM_RUNTIME_FILE_MODE = 384;
|
|
367
|
+
var INTERCOM_TCP_HOST = "127.0.0.1";
|
|
368
|
+
var INTERCOM_PROTOCOL_NAME = "pi-intercom";
|
|
369
|
+
var INTERCOM_PROTOCOL_VERSION = 4;
|
|
370
|
+
function sanitizePipeSegment(value) {
|
|
371
|
+
return value.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "default";
|
|
372
|
+
}
|
|
373
|
+
function getAgentDirPath(env = process.env, homeDir = homedir(), cwd = process.cwd()) {
|
|
374
|
+
const configured = env.PI_CODING_AGENT_DIR?.trim();
|
|
375
|
+
if (!configured) {
|
|
376
|
+
return join2(homeDir, ".pi/agent");
|
|
377
|
+
}
|
|
378
|
+
return isAbsolute2(configured) ? configured : resolve(cwd, configured);
|
|
379
|
+
}
|
|
380
|
+
function getIntercomDirPath(agentDir = getAgentDirPath()) {
|
|
381
|
+
return join2(agentDir, "intercom");
|
|
382
|
+
}
|
|
383
|
+
function shouldUseWindowsTcpTransport(platform = process.platform, env = process.env) {
|
|
384
|
+
if (platform !== "win32") {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
const transport = env.PI_INTERCOM_TRANSPORT?.trim().toLowerCase();
|
|
388
|
+
if (transport === "tcp") {
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
const legacyOptIn = env.PI_INTERCOM_TCP?.trim().toLowerCase();
|
|
392
|
+
return legacyOptIn === "1" || legacyOptIn === "true";
|
|
393
|
+
}
|
|
394
|
+
function getBrokerPortFilePath(intercomDir = getIntercomDirPath()) {
|
|
395
|
+
return join2(intercomDir, "broker.port.json");
|
|
396
|
+
}
|
|
397
|
+
function getBrokerSocketPath(platform = process.platform, agentDir = getAgentDirPath()) {
|
|
398
|
+
if (platform === "win32") {
|
|
399
|
+
return `\\\\.\\pipe\\pi-intercom-${sanitizePipeSegment(agentDir)}`;
|
|
400
|
+
}
|
|
401
|
+
return join2(getIntercomDirPath(agentDir), "broker.sock");
|
|
402
|
+
}
|
|
403
|
+
function getBrokerConnectTarget(platform = process.platform, env = process.env, intercomDir = getIntercomDirPath(getAgentDirPath(env))) {
|
|
404
|
+
if (shouldUseWindowsTcpTransport(platform, env)) {
|
|
405
|
+
const endpointFile = getBrokerPortFilePath(intercomDir);
|
|
406
|
+
const raw = readFileSync(endpointFile, "utf-8");
|
|
407
|
+
const parsed = JSON.parse(raw);
|
|
408
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
409
|
+
throw new Error(`Invalid intercom TCP endpoint at ${endpointFile}: expected a JSON object`);
|
|
410
|
+
}
|
|
411
|
+
const endpoint = parsed;
|
|
412
|
+
if (endpoint.transport !== "tcp" || endpoint.host !== INTERCOM_TCP_HOST || typeof endpoint.port !== "number" || !Number.isSafeInteger(endpoint.port) || endpoint.port <= 0 || endpoint.port > 65535 || typeof endpoint.stateId !== "string" || endpoint.stateId.length === 0) {
|
|
413
|
+
throw new Error(`Invalid intercom TCP endpoint at ${endpointFile}`);
|
|
414
|
+
}
|
|
415
|
+
return { transport: "tcp", host: endpoint.host, port: endpoint.port, stateId: endpoint.stateId };
|
|
416
|
+
}
|
|
417
|
+
return getBrokerSocketPath(platform, getAgentDirPath(env));
|
|
418
|
+
}
|
|
419
|
+
function ensureIntercomRuntimeDir(intercomDir = getIntercomDirPath(), platform = process.platform) {
|
|
420
|
+
mkdirSync(intercomDir, { recursive: true, mode: INTERCOM_DIR_MODE });
|
|
421
|
+
if (platform !== "win32") {
|
|
422
|
+
chmodSync(intercomDir, INTERCOM_DIR_MODE);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function restrictIntercomRuntimeFile(filePath, platform = process.platform) {
|
|
426
|
+
if (platform !== "win32") {
|
|
427
|
+
chmodSync(filePath, INTERCOM_RUNTIME_FILE_MODE);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// broker/validation.ts
|
|
432
|
+
import { types as nodeUtilTypes } from "node:util";
|
|
433
|
+
function isPlainDataRecord(value) {
|
|
434
|
+
if (typeof value !== "object" || value === null || nodeUtilTypes.isProxy(value) || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype) {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
return Reflect.ownKeys(value).every((key) => {
|
|
439
|
+
if (typeof key !== "string") return false;
|
|
440
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
441
|
+
return descriptor !== void 0 && descriptor.enumerable && Object.hasOwn(descriptor, "value");
|
|
442
|
+
});
|
|
443
|
+
} catch {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function hasExactDataKeys(value, required, optional = []) {
|
|
448
|
+
if (!isPlainDataRecord(value)) return false;
|
|
449
|
+
const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
|
|
450
|
+
const keys = Reflect.ownKeys(value);
|
|
451
|
+
return keys.length >= required.length && keys.every((key) => typeof key === "string" && allowed.has(key)) && required.every((key) => Object.hasOwn(value, key));
|
|
452
|
+
}
|
|
453
|
+
function assertPlainDataGraph(value, path = "$", seen = /* @__PURE__ */ new WeakSet()) {
|
|
454
|
+
if (typeof value !== "object" && typeof value !== "function" || value === null) return;
|
|
455
|
+
if (nodeUtilTypes.isProxy(value)) {
|
|
456
|
+
throw new Error(`${path} must not contain a Proxy`);
|
|
457
|
+
}
|
|
458
|
+
if (seen.has(value)) {
|
|
459
|
+
throw new Error(`${path} must not contain a cycle or repeated object reference`);
|
|
460
|
+
}
|
|
461
|
+
seen.add(value);
|
|
462
|
+
if (Array.isArray(value)) {
|
|
463
|
+
if (Object.getPrototypeOf(value) !== Array.prototype) {
|
|
464
|
+
throw new Error(`${path} must use the exact Array prototype`);
|
|
465
|
+
}
|
|
466
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
|
|
467
|
+
if (lengthDescriptor === void 0 || !Object.hasOwn(lengthDescriptor, "value")) {
|
|
468
|
+
throw new Error(`${path}.length must be an own data property`);
|
|
469
|
+
}
|
|
470
|
+
const length = lengthDescriptor.value;
|
|
471
|
+
const descriptors = /* @__PURE__ */ new Map();
|
|
472
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
473
|
+
if (key === "length") continue;
|
|
474
|
+
if (typeof key !== "string") throw new Error(`${path} must not contain symbol properties`);
|
|
475
|
+
const index = Number(key);
|
|
476
|
+
if (!Number.isInteger(index) || index < 0 || index >= length || String(index) !== key) {
|
|
477
|
+
throw new Error(`${path} must not contain non-index properties`);
|
|
478
|
+
}
|
|
479
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
480
|
+
if (descriptor === void 0 || !descriptor.enumerable || !Object.hasOwn(descriptor, "value")) {
|
|
481
|
+
throw new Error(`${path}[${index}] must be an enumerable own data property`);
|
|
482
|
+
}
|
|
483
|
+
descriptors.set(index, descriptor);
|
|
484
|
+
}
|
|
485
|
+
if (descriptors.size !== length) throw new Error(`${path} must be dense`);
|
|
486
|
+
for (let index = 0; index < length; index += 1) {
|
|
487
|
+
const descriptor = descriptors.get(index);
|
|
488
|
+
if (descriptor === void 0) throw new Error(`${path} must be dense`);
|
|
489
|
+
assertPlainDataGraph(descriptor.value, `${path}[${index}]`, seen);
|
|
490
|
+
}
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (Object.getPrototypeOf(value) !== Object.prototype) {
|
|
494
|
+
throw new Error(`${path} must be a plain data object`);
|
|
495
|
+
}
|
|
496
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
497
|
+
if (typeof key !== "string") throw new Error(`${path} must not contain symbol properties`);
|
|
498
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
499
|
+
if (descriptor === void 0 || !descriptor.enumerable || !Object.hasOwn(descriptor, "value")) {
|
|
500
|
+
throw new Error(`${path}.${key} must be an enumerable own data property`);
|
|
501
|
+
}
|
|
502
|
+
assertPlainDataGraph(descriptor.value, `${path}.${key}`, seen);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function isDenseArrayOf(value, predicate) {
|
|
506
|
+
if (typeof value !== "object" || value === null || nodeUtilTypes.isProxy(value) || !Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) return false;
|
|
507
|
+
try {
|
|
508
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
|
|
509
|
+
if (lengthDescriptor === void 0 || !Object.hasOwn(lengthDescriptor, "value")) return false;
|
|
510
|
+
const length = lengthDescriptor.value;
|
|
511
|
+
const entries = /* @__PURE__ */ new Map();
|
|
512
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
513
|
+
if (key === "length") continue;
|
|
514
|
+
if (typeof key !== "string") return false;
|
|
515
|
+
const index = Number(key);
|
|
516
|
+
if (!Number.isInteger(index) || index < 0 || index >= length || String(index) !== key) return false;
|
|
517
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
518
|
+
if (descriptor === void 0 || !descriptor.enumerable || !Object.hasOwn(descriptor, "value")) return false;
|
|
519
|
+
entries.set(index, descriptor.value);
|
|
520
|
+
}
|
|
521
|
+
if (entries.size !== length) return false;
|
|
522
|
+
for (let index = 0; index < length; index += 1) {
|
|
523
|
+
if (!predicate(entries.get(index), index)) return false;
|
|
524
|
+
}
|
|
525
|
+
return true;
|
|
526
|
+
} catch {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// claude/transport.ts
|
|
532
|
+
import { spawnSync } from "node:child_process";
|
|
533
|
+
var CLAUDE_INTERCOM_TRANSPORT_ENV = "CLAUDE_INTERCOM_TRANSPORT";
|
|
534
|
+
var MIN_NATIVE_CLAUDE_VERSION = "2.1.220";
|
|
535
|
+
var MAX_NATIVE_CLAUDE_VERSION = "2.1.226";
|
|
536
|
+
function parseClaudeIntercomTransport(value, source = "transport") {
|
|
537
|
+
if (value === void 0 || value === null || value === "") return "auto";
|
|
538
|
+
if (value === "auto" || value === "native" || value === "mcp") return value;
|
|
539
|
+
throw new Error(`${source} must be one of auto, native, or mcp`);
|
|
540
|
+
}
|
|
541
|
+
function parseClaudeVersion(output) {
|
|
542
|
+
const match = output.match(/(?:^|\s)(\d+\.\d+\.\d+)(?:\s|$)/);
|
|
543
|
+
return match?.[1] ?? null;
|
|
544
|
+
}
|
|
545
|
+
function compareVersions(left, right) {
|
|
546
|
+
const a = left.split(".").map(Number);
|
|
547
|
+
const b = right.split(".").map(Number);
|
|
548
|
+
for (let index = 0; index < 3; index += 1) {
|
|
549
|
+
const difference = (a[index] ?? 0) - (b[index] ?? 0);
|
|
550
|
+
if (difference !== 0) return difference;
|
|
551
|
+
}
|
|
552
|
+
return 0;
|
|
553
|
+
}
|
|
554
|
+
function isNativeClaudeVersionCompatible(version) {
|
|
555
|
+
return compareVersions(version, MIN_NATIVE_CLAUDE_VERSION) >= 0 && compareVersions(version, MAX_NATIVE_CLAUDE_VERSION) <= 0;
|
|
556
|
+
}
|
|
557
|
+
function probeClaudeVersion(command = "claude") {
|
|
558
|
+
const result = spawnSync(command, ["--version"], {
|
|
559
|
+
encoding: "utf8",
|
|
560
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
561
|
+
});
|
|
562
|
+
if (result.error || result.status !== 0) {
|
|
563
|
+
return {
|
|
564
|
+
command,
|
|
565
|
+
version: null,
|
|
566
|
+
compatible: false,
|
|
567
|
+
reason: `could not run ${command} --version`
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
const version = parseClaudeVersion(`${result.stdout}
|
|
571
|
+
${result.stderr}`);
|
|
572
|
+
if (!version) {
|
|
573
|
+
return { command, version: null, compatible: false, reason: "Claude version output was not recognized" };
|
|
574
|
+
}
|
|
575
|
+
const compatible = isNativeClaudeVersionCompatible(version);
|
|
576
|
+
return {
|
|
577
|
+
command,
|
|
578
|
+
version,
|
|
579
|
+
compatible,
|
|
580
|
+
reason: compatible ? `Claude ${version} is within the native transport compatibility window ${MIN_NATIVE_CLAUDE_VERSION}-${MAX_NATIVE_CLAUDE_VERSION}` : `Claude ${version} is outside the verified native transport compatibility window ${MIN_NATIVE_CLAUDE_VERSION}-${MAX_NATIVE_CLAUDE_VERSION}`
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function resolveClaudeIntercomTransport(options = {}) {
|
|
584
|
+
const requested = parseClaudeIntercomTransport(
|
|
585
|
+
options.requested ?? options.env?.[CLAUDE_INTERCOM_TRANSPORT_ENV] ?? "auto",
|
|
586
|
+
options.requested === void 0 ? CLAUDE_INTERCOM_TRANSPORT_ENV : "transport"
|
|
587
|
+
);
|
|
588
|
+
const command = options.claudeCommand || "claude";
|
|
589
|
+
const versionProbe = (options.probe ?? probeClaudeVersion)(command);
|
|
590
|
+
if (requested === "mcp") return { requested, selected: "mcp", ...versionProbe };
|
|
591
|
+
if (requested === "native" && !versionProbe.compatible) {
|
|
592
|
+
throw new Error(`Native Claude intercom transport was requested, but ${versionProbe.reason}. Use --transport mcp or install a verified-compatible Claude version.`);
|
|
593
|
+
}
|
|
594
|
+
return {
|
|
595
|
+
requested,
|
|
596
|
+
selected: requested === "native" || versionProbe.compatible ? "native" : "mcp",
|
|
597
|
+
...versionProbe
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// claude/worker-config.ts
|
|
602
|
+
var DEFAULT_WORKER_CONFIG_PATH = join3(getIntercomDirPath(), "claude-worker.json");
|
|
603
|
+
var DEFAULT_WORKER_STATE_PATH = join3(getIntercomDirPath(), "claude-worker-state.json");
|
|
604
|
+
function isRecord2(value) {
|
|
605
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
606
|
+
}
|
|
607
|
+
function optionalString(value, field) {
|
|
608
|
+
if (value === void 0 || value === null) return void 0;
|
|
609
|
+
if (typeof value !== "string") throw new Error(`${field} must be a string`);
|
|
610
|
+
const trimmed = value.trim();
|
|
611
|
+
return trimmed || void 0;
|
|
612
|
+
}
|
|
613
|
+
function requireString(value, field) {
|
|
614
|
+
const result = optionalString(value, field);
|
|
615
|
+
if (!result) throw new Error(`${field} must be a non-empty string`);
|
|
616
|
+
return result;
|
|
617
|
+
}
|
|
618
|
+
function optionalBoolean(value, field) {
|
|
619
|
+
if (value === void 0 || value === null) return void 0;
|
|
620
|
+
if (typeof value !== "boolean") throw new Error(`${field} must be a boolean`);
|
|
621
|
+
return value;
|
|
622
|
+
}
|
|
623
|
+
function optionalStringArray(value, field) {
|
|
624
|
+
if (value === void 0 || value === null) return void 0;
|
|
625
|
+
if (!isDenseArrayOf(value, (_entry) => true)) {
|
|
626
|
+
throw new Error(`${field} must be an exact dense array`);
|
|
627
|
+
}
|
|
628
|
+
return value.map((item, index) => requireString(item, `${field}[${index}]`));
|
|
629
|
+
}
|
|
630
|
+
function normalizeAgent(raw, index) {
|
|
631
|
+
if (!isRecord2(raw)) throw new Error(`agents[${index}] must be an object`);
|
|
632
|
+
const id = requireString(raw.id, `agents[${index}].id`);
|
|
633
|
+
const name = optionalString(raw.name, `agents[${index}].name`) ?? id;
|
|
634
|
+
const cwdValue = optionalString(raw.cwd, `agents[${index}].cwd`) ?? processCwd();
|
|
635
|
+
let cwd = resolve2(cwdValue);
|
|
636
|
+
const permissionModeValue = optionalString(raw.permissionMode, `agents[${index}].permissionMode`);
|
|
637
|
+
const dangerouslySkipPermissionsValue = optionalBoolean(
|
|
638
|
+
raw.dangerouslySkipPermissions,
|
|
639
|
+
`agents[${index}].dangerouslySkipPermissions`
|
|
640
|
+
);
|
|
641
|
+
const permissionCeilingValue = optionalString(raw.permissionCeiling, `agents[${index}].permissionCeiling`);
|
|
642
|
+
if (permissionCeilingValue !== void 0 && permissionCeilingValue !== "standard" && permissionCeilingValue !== "read-only") {
|
|
643
|
+
throw new Error(`agents[${index}].permissionCeiling must be standard or read-only`);
|
|
644
|
+
}
|
|
645
|
+
const bossRoleValue = optionalString(raw.bossRole, `agents[${index}].bossRole`);
|
|
646
|
+
if (bossRoleValue !== void 0 && bossRoleValue !== "adversary" && bossRoleValue !== "council") {
|
|
647
|
+
throw new Error(`agents[${index}].bossRole must be adversary or council`);
|
|
648
|
+
}
|
|
649
|
+
if (bossRoleValue !== void 0 && permissionCeilingValue === "standard") {
|
|
650
|
+
throw new Error(`agents[${index}].permissionCeiling cannot widen an Adversary/Council role`);
|
|
651
|
+
}
|
|
652
|
+
const permission = resolveClaudePermissionPolicy({
|
|
653
|
+
permissionMode: permissionModeValue,
|
|
654
|
+
dangerouslySkipPermissions: dangerouslySkipPermissionsValue,
|
|
655
|
+
ceiling: bossRoleValue === void 0 ? permissionCeilingValue : "read-only"
|
|
656
|
+
});
|
|
657
|
+
const claudeArgs = optionalStringArray(raw.claudeArgs, `agents[${index}].claudeArgs`);
|
|
658
|
+
let addDirs = optionalStringArray(raw.addDirs, `agents[${index}].addDirs`);
|
|
659
|
+
const mcpConfig = optionalString(raw.mcpConfig, `agents[${index}].mcpConfig`);
|
|
660
|
+
assertNoPermissionOverrides(claudeArgs ?? [], `agents[${index}].claudeArgs`, permission.ceiling);
|
|
661
|
+
if (permission.ceiling === "read-only") {
|
|
662
|
+
if (mcpConfig !== void 0) {
|
|
663
|
+
throw new Error(`agents[${index}].mcpConfig cannot add an arbitrary MCP capability to a hardened Claude role`);
|
|
664
|
+
}
|
|
665
|
+
const binding = bindHardenedClaudePaths(cwdValue, addDirs ?? [], `agents[${index}]`);
|
|
666
|
+
try {
|
|
667
|
+
cwd = binding.cwd;
|
|
668
|
+
addDirs = binding.addDirs.length ? binding.addDirs : void 0;
|
|
669
|
+
} finally {
|
|
670
|
+
binding.release();
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
id,
|
|
675
|
+
name,
|
|
676
|
+
cwd,
|
|
677
|
+
model: optionalString(raw.model, `agents[${index}].model`),
|
|
678
|
+
sessionId: optionalString(raw.sessionId, `agents[${index}].sessionId`),
|
|
679
|
+
instructions: optionalString(raw.instructions, `agents[${index}].instructions`),
|
|
680
|
+
...permission.ceiling === "standard" && permissionModeValue === void 0 ? {} : { permissionMode: permission.permissionMode },
|
|
681
|
+
...dangerouslySkipPermissionsValue === void 0 && permission.ceiling !== "read-only" ? {} : { dangerouslySkipPermissions: permission.dangerouslySkipPermissions },
|
|
682
|
+
...permissionCeilingValue === void 0 && bossRoleValue === void 0 ? {} : { permissionCeiling: permission.ceiling },
|
|
683
|
+
...bossRoleValue === void 0 ? {} : { bossRole: bossRoleValue },
|
|
684
|
+
addDirs,
|
|
685
|
+
mcpConfig,
|
|
686
|
+
claudeArgs,
|
|
687
|
+
transport: raw.transport === void 0 ? void 0 : parseClaudeIntercomTransport(raw.transport, `agents[${index}].transport`)
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
function defaultWorkerConfig(env = process.env) {
|
|
691
|
+
const id = env.CLAUDE_INTERCOM_WORKER_ID?.trim() || "claude-worker";
|
|
692
|
+
return {
|
|
693
|
+
statePath: env.CLAUDE_INTERCOM_WORKER_STATE?.trim() || DEFAULT_WORKER_STATE_PATH,
|
|
694
|
+
agents: [{
|
|
695
|
+
id,
|
|
696
|
+
name: env.CLAUDE_INTERCOM_WORKER_NAME?.trim() || id,
|
|
697
|
+
cwd: resolve2(env.CLAUDE_INTERCOM_WORKER_CWD?.trim() || processCwd()),
|
|
698
|
+
model: env.CLAUDE_INTERCOM_WORKER_MODEL?.trim() || void 0,
|
|
699
|
+
instructions: env.CLAUDE_INTERCOM_WORKER_INSTRUCTIONS?.trim() || void 0,
|
|
700
|
+
transport: parseClaudeIntercomTransport(env.CLAUDE_INTERCOM_TRANSPORT, "CLAUDE_INTERCOM_TRANSPORT")
|
|
701
|
+
}]
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
function loadWorkerConfig(path = process.env.CLAUDE_INTERCOM_WORKER_CONFIG || DEFAULT_WORKER_CONFIG_PATH) {
|
|
705
|
+
if (!existsSync(path)) return defaultWorkerConfig();
|
|
706
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
707
|
+
if (!isRecord2(parsed)) throw new Error("Worker config must be a JSON object");
|
|
708
|
+
if (!isDenseArrayOf(parsed.agents, (_entry) => true)) {
|
|
709
|
+
throw new Error("Worker config requires an exact dense agents array");
|
|
710
|
+
}
|
|
711
|
+
return {
|
|
712
|
+
statePath: resolve2(optionalString(parsed.statePath, "statePath") ?? DEFAULT_WORKER_STATE_PATH),
|
|
713
|
+
claudeCommand: optionalString(parsed.claudeCommand, "claudeCommand"),
|
|
714
|
+
agents: parsed.agents.map(normalizeAgent)
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
function loadWorkerState(path) {
|
|
718
|
+
if (!existsSync(path)) return { agents: {} };
|
|
719
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
720
|
+
if (!isRecord2(parsed) || !isRecord2(parsed.agents)) return { agents: {} };
|
|
721
|
+
const agents = {};
|
|
722
|
+
for (const [id, value] of Object.entries(parsed.agents)) {
|
|
723
|
+
if (!isRecord2(value) || typeof value.sessionId !== "string") continue;
|
|
724
|
+
agents[id] = {
|
|
725
|
+
sessionId: value.sessionId,
|
|
726
|
+
updatedAt: typeof value.updatedAt === "number" ? value.updatedAt : 0
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
return { agents };
|
|
730
|
+
}
|
|
731
|
+
function saveWorkerState(path, state) {
|
|
732
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
733
|
+
writeFileSync(path, `${JSON.stringify(state, null, 2)}
|
|
734
|
+
`, { mode: 384 });
|
|
735
|
+
restrictIntercomRuntimeFile(path);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// broker/client.ts
|
|
739
|
+
import { EventEmitter } from "events";
|
|
740
|
+
import net from "net";
|
|
741
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
742
|
+
import { POLICY_SEMANTICS_HASH, POLICY_SEMANTICS_VERSION } from "@ctliz/agent-intercom-core";
|
|
743
|
+
|
|
744
|
+
// broker/framing.ts
|
|
745
|
+
var MAX_FRAME_BYTES = 1024 * 1024;
|
|
746
|
+
function writeMessage(socket, msg) {
|
|
747
|
+
const json = JSON.stringify(msg);
|
|
748
|
+
const payload = Buffer.from(json, "utf-8");
|
|
749
|
+
const header = Buffer.alloc(4);
|
|
750
|
+
header.writeUInt32BE(payload.length, 0);
|
|
751
|
+
socket.write(Buffer.concat([header, payload]));
|
|
752
|
+
}
|
|
753
|
+
function createMessageReader(onMessage, onError, maxFrameBytes = MAX_FRAME_BYTES) {
|
|
754
|
+
let buffer = Buffer.alloc(0);
|
|
755
|
+
function reportMessage(payload) {
|
|
756
|
+
let msg;
|
|
757
|
+
try {
|
|
758
|
+
msg = JSON.parse(payload.toString("utf-8"));
|
|
759
|
+
} catch (error) {
|
|
760
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
761
|
+
onError(new Error(`Failed to parse intercom message: ${message}`, { cause: error }));
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
try {
|
|
765
|
+
onMessage(msg);
|
|
766
|
+
return true;
|
|
767
|
+
} catch (error) {
|
|
768
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
769
|
+
onError(new Error(`Failed to handle intercom message: ${message}`, { cause: error }));
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return (data) => {
|
|
774
|
+
let remaining = data;
|
|
775
|
+
while (remaining.length > 0) {
|
|
776
|
+
if (buffer.length < 4) {
|
|
777
|
+
const headerBytes = Math.min(4 - buffer.length, remaining.length);
|
|
778
|
+
buffer = Buffer.concat([buffer, remaining.subarray(0, headerBytes)]);
|
|
779
|
+
remaining = remaining.subarray(headerBytes);
|
|
780
|
+
if (buffer.length < 4) {
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
const length = buffer.readUInt32BE(0);
|
|
785
|
+
if (length > maxFrameBytes) {
|
|
786
|
+
buffer = Buffer.alloc(0);
|
|
787
|
+
onError(new Error(`Intercom frame length ${length} exceeds maximum ${maxFrameBytes} bytes`));
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
const missingPayloadBytes = length - Math.max(0, buffer.length - 4);
|
|
791
|
+
const payloadBytes = Math.min(missingPayloadBytes, remaining.length);
|
|
792
|
+
if (payloadBytes > 0) {
|
|
793
|
+
buffer = Buffer.concat([buffer, remaining.subarray(0, payloadBytes)]);
|
|
794
|
+
remaining = remaining.subarray(payloadBytes);
|
|
795
|
+
}
|
|
796
|
+
if (buffer.length < 4 + length) {
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
const payload = buffer.subarray(4, 4 + length);
|
|
800
|
+
buffer = Buffer.alloc(0);
|
|
801
|
+
if (!reportMessage(payload)) {
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// outbound-outbox.ts
|
|
809
|
+
import { createHash } from "crypto";
|
|
810
|
+
import { chmodSync as chmodSync2, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2 } from "fs";
|
|
811
|
+
import { join as join4 } from "path";
|
|
812
|
+
|
|
813
|
+
// durable-json.ts
|
|
814
|
+
import { randomUUID } from "crypto";
|
|
815
|
+
import { closeSync as closeSync2, fsyncSync, openSync as openSync2, renameSync, writeFileSync as writeFileSync2 } from "fs";
|
|
816
|
+
import { dirname as dirname3 } from "path";
|
|
817
|
+
function writeDurableJson(filePath, value) {
|
|
818
|
+
const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
819
|
+
writeFileSync2(temporaryPath, JSON.stringify(value), { encoding: "utf-8", mode: INTERCOM_RUNTIME_FILE_MODE });
|
|
820
|
+
const fileDescriptor = openSync2(temporaryPath, "r");
|
|
821
|
+
try {
|
|
822
|
+
fsyncSync(fileDescriptor);
|
|
823
|
+
} finally {
|
|
824
|
+
closeSync2(fileDescriptor);
|
|
825
|
+
}
|
|
826
|
+
renameSync(temporaryPath, filePath);
|
|
827
|
+
restrictIntercomRuntimeFile(filePath);
|
|
828
|
+
if (process.platform !== "win32") {
|
|
829
|
+
const directoryDescriptor = openSync2(dirname3(filePath), "r");
|
|
830
|
+
try {
|
|
831
|
+
fsyncSync(directoryDescriptor);
|
|
832
|
+
} finally {
|
|
833
|
+
closeSync2(directoryDescriptor);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// outbound-outbox.ts
|
|
839
|
+
var OUTBOX_STATE_VERSION = 1;
|
|
840
|
+
var MAX_OUTBOX_MESSAGES = 256;
|
|
841
|
+
function fingerprint(entry) {
|
|
842
|
+
return JSON.stringify({
|
|
843
|
+
to: entry.to,
|
|
844
|
+
replyTo: entry.message.replyTo,
|
|
845
|
+
expectsReply: entry.message.expectsReply,
|
|
846
|
+
content: entry.message.content
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
function isStoredOutboundMessage(value) {
|
|
850
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
851
|
+
const entry = value;
|
|
852
|
+
if (typeof entry.to !== "string" || typeof entry.queuedAt !== "number") return false;
|
|
853
|
+
if (typeof entry.message !== "object" || entry.message === null || Array.isArray(entry.message)) return false;
|
|
854
|
+
const message = entry.message;
|
|
855
|
+
return typeof message.id === "string" && typeof message.timestamp === "number" && typeof message.content === "object" && message.content !== null && typeof message.content.text === "string";
|
|
856
|
+
}
|
|
857
|
+
function fileName(sessionId) {
|
|
858
|
+
return `${createHash("sha256").update(sessionId).digest("hex")}.json`;
|
|
859
|
+
}
|
|
860
|
+
var PersistentOutboundOutbox = class {
|
|
861
|
+
directory;
|
|
862
|
+
filePath;
|
|
863
|
+
state;
|
|
864
|
+
constructor(sessionId, intercomDir = getIntercomDirPath()) {
|
|
865
|
+
ensureIntercomRuntimeDir(intercomDir);
|
|
866
|
+
this.directory = join4(intercomDir, "outbox");
|
|
867
|
+
mkdirSync3(this.directory, { recursive: true, mode: INTERCOM_DIR_MODE });
|
|
868
|
+
if (process.platform !== "win32") chmodSync2(this.directory, INTERCOM_DIR_MODE);
|
|
869
|
+
this.filePath = join4(this.directory, fileName(sessionId));
|
|
870
|
+
this.state = this.load();
|
|
871
|
+
}
|
|
872
|
+
list() {
|
|
873
|
+
return this.state.entries.map((entry) => ({ ...entry, message: { ...entry.message, content: { ...entry.message.content } } }));
|
|
874
|
+
}
|
|
875
|
+
enqueue(to, message) {
|
|
876
|
+
const existing = this.state.entries.find((entry) => entry.message.id === message.id);
|
|
877
|
+
if (existing) {
|
|
878
|
+
if (fingerprint(existing) !== fingerprint({ to, message })) {
|
|
879
|
+
throw new Error(`Message ID ${message.id} is already queued with a different payload`);
|
|
880
|
+
}
|
|
881
|
+
return "existing";
|
|
882
|
+
}
|
|
883
|
+
if (this.state.entries.length >= MAX_OUTBOX_MESSAGES) {
|
|
884
|
+
throw new Error(`Durable outbox is full (${MAX_OUTBOX_MESSAGES} messages)`);
|
|
885
|
+
}
|
|
886
|
+
this.state.entries.push({ to, message, queuedAt: Date.now() });
|
|
887
|
+
this.persist();
|
|
888
|
+
return "added";
|
|
889
|
+
}
|
|
890
|
+
remove(messageId) {
|
|
891
|
+
const remaining = this.state.entries.filter((entry) => entry.message.id !== messageId);
|
|
892
|
+
if (remaining.length === this.state.entries.length) return;
|
|
893
|
+
this.state.entries = remaining;
|
|
894
|
+
this.persist();
|
|
895
|
+
}
|
|
896
|
+
clear() {
|
|
897
|
+
if (this.state.entries.length === 0) return;
|
|
898
|
+
this.state.entries = [];
|
|
899
|
+
this.persist();
|
|
900
|
+
}
|
|
901
|
+
load() {
|
|
902
|
+
if (!existsSync2(this.filePath)) return { version: OUTBOX_STATE_VERSION, entries: [] };
|
|
903
|
+
try {
|
|
904
|
+
const parsed = JSON.parse(readFileSync3(this.filePath, "utf-8"));
|
|
905
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("expected object");
|
|
906
|
+
const state = parsed;
|
|
907
|
+
if (state.version !== OUTBOX_STATE_VERSION || !Array.isArray(state.entries) || !state.entries.every(isStoredOutboundMessage)) {
|
|
908
|
+
throw new Error("invalid outbox state");
|
|
909
|
+
}
|
|
910
|
+
return { version: OUTBOX_STATE_VERSION, entries: state.entries };
|
|
911
|
+
} catch {
|
|
912
|
+
const corruptPath = `${this.filePath}.corrupt-${Date.now()}`;
|
|
913
|
+
renameSync2(this.filePath, corruptPath);
|
|
914
|
+
restrictIntercomRuntimeFile(corruptPath);
|
|
915
|
+
return { version: OUTBOX_STATE_VERSION, entries: [] };
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
persist() {
|
|
919
|
+
writeDurableJson(this.filePath, this.state);
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
// broker/access-credential.ts
|
|
924
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
925
|
+
var ACCESS_CREDENTIAL_ENV = "AGENT_INTERCOM_ACCESS_CREDENTIAL_PATH";
|
|
926
|
+
var ACCESS_CREDENTIAL_VERSION = 1;
|
|
927
|
+
function nonEmptyString(value) {
|
|
928
|
+
return typeof value === "string" && value.length > 0 && !value.includes("\0");
|
|
929
|
+
}
|
|
930
|
+
function loadRemoteAccessCredential(env = process.env) {
|
|
931
|
+
const path = env[ACCESS_CREDENTIAL_ENV]?.trim();
|
|
932
|
+
if (!path) return void 0;
|
|
933
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
934
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
935
|
+
throw new Error(`Invalid Agent Intercom access credential at ${path}`);
|
|
936
|
+
}
|
|
937
|
+
const credential = parsed;
|
|
938
|
+
if (nonEmptyString(credential.enrollmentToken)) {
|
|
939
|
+
return { path, access: { enrollmentToken: credential.enrollmentToken }, enrollment: true };
|
|
940
|
+
}
|
|
941
|
+
if (credential.version === ACCESS_CREDENTIAL_VERSION && nonEmptyString(credential.sessionCredential) && nonEmptyString(credential.sessionId) && typeof credential.generation === "number" && Number.isSafeInteger(credential.generation) && credential.generation > 0) {
|
|
942
|
+
return {
|
|
943
|
+
path,
|
|
944
|
+
access: {
|
|
945
|
+
sessionCredential: credential.sessionCredential,
|
|
946
|
+
sessionId: credential.sessionId,
|
|
947
|
+
generation: credential.generation
|
|
948
|
+
},
|
|
949
|
+
enrollment: false
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
throw new Error(`Invalid Agent Intercom access credential at ${path}`);
|
|
953
|
+
}
|
|
954
|
+
function writeRemoteSessionCredential(path, sessionId, metadata) {
|
|
955
|
+
if (!metadata.sessionCredential) {
|
|
956
|
+
throw new Error("Remote enrollment response omitted the session credential");
|
|
957
|
+
}
|
|
958
|
+
writeDurableJson(path, {
|
|
959
|
+
version: ACCESS_CREDENTIAL_VERSION,
|
|
960
|
+
sessionCredential: metadata.sessionCredential,
|
|
961
|
+
sessionId,
|
|
962
|
+
generation: metadata.generation
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// protocol-v4/contract.ts
|
|
967
|
+
import {
|
|
968
|
+
INTERCOM_PROTOCOL_NAME as INTERCOM_PROTOCOL_NAME2,
|
|
969
|
+
INTERCOM_PROTOCOL_V4_SEMANTICS_HASH,
|
|
970
|
+
INTERCOM_PROTOCOL_V4_VECTOR_SCHEMA_VERSION,
|
|
971
|
+
INTERCOM_PROTOCOL_V4_VECTORS,
|
|
972
|
+
INTERCOM_PROTOCOL_VERSION as INTERCOM_PROTOCOL_VERSION2,
|
|
973
|
+
INTERCOM_SCOPE_ENV,
|
|
974
|
+
INTERCOM_SCOPE_ID_PATTERN,
|
|
975
|
+
INTERCOM_SCOPE_ID_PATTERN_SOURCE,
|
|
976
|
+
intercomScopeIdFromEnv,
|
|
977
|
+
parseIntercomScopeId,
|
|
978
|
+
sameIntercomScope
|
|
979
|
+
} from "@ctliz/agent-intercom-core/protocol-v4";
|
|
980
|
+
var INTERCOM_REGISTRATION_METADATA_ERROR = "Registration metadata is invalid";
|
|
981
|
+
function parseIntercomScopeIdForRegistration(value) {
|
|
982
|
+
try {
|
|
983
|
+
return parseIntercomScopeId(value, "registrationMetadata");
|
|
984
|
+
} catch {
|
|
985
|
+
throw new Error(INTERCOM_REGISTRATION_METADATA_ERROR);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
function intercomScopeIdFromEnvForRegistration(env = process.env) {
|
|
989
|
+
try {
|
|
990
|
+
return intercomScopeIdFromEnv(env);
|
|
991
|
+
} catch {
|
|
992
|
+
throw new Error(INTERCOM_REGISTRATION_METADATA_ERROR);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// types.ts
|
|
997
|
+
var DELIVERY_FAILURE_CODES = [
|
|
998
|
+
"INVALID_MESSAGE",
|
|
999
|
+
"SESSION_NOT_FOUND",
|
|
1000
|
+
"AMBIGUOUS_TARGET",
|
|
1001
|
+
"SENDER_NOT_FOUND",
|
|
1002
|
+
"INVALID_REPLY_TARGET",
|
|
1003
|
+
"MUTUAL_ASK",
|
|
1004
|
+
"ASK_ALREADY_PENDING",
|
|
1005
|
+
"DUPLICATE_MESSAGE_ID",
|
|
1006
|
+
"CONFLICTING_MESSAGE_ID",
|
|
1007
|
+
"TOO_MANY_PENDING_DELIVERIES",
|
|
1008
|
+
"TOO_MANY_PENDING_ASKS",
|
|
1009
|
+
"RECIPIENT_DISCONNECTED",
|
|
1010
|
+
"SENDER_DISCONNECTED",
|
|
1011
|
+
"DELIVERY_TIMEOUT",
|
|
1012
|
+
"CONTROL_DISPATCH_UNAVAILABLE"
|
|
1013
|
+
];
|
|
1014
|
+
|
|
1015
|
+
// broker/boss-contracts.ts
|
|
1016
|
+
import {
|
|
1017
|
+
BOSS_CAPABILITY_FEATURE_DIGEST,
|
|
1018
|
+
BOSS_POLICY_SEMANTICS_HASH,
|
|
1019
|
+
BOSS_RUN_FEATURE,
|
|
1020
|
+
BOSS_RUN_FEATURE_CONTRACT,
|
|
1021
|
+
INTERCOM_BASE_PROTOCOL_VERSION,
|
|
1022
|
+
evaluateBrokerCompatibility,
|
|
1023
|
+
parseBossControlEnvelope,
|
|
1024
|
+
parseBossParticipantBinding,
|
|
1025
|
+
parseBossParticipantCredentialEnvelope,
|
|
1026
|
+
parseBossPolicyPrincipal,
|
|
1027
|
+
parseBossRunFeatureContract,
|
|
1028
|
+
parseBrokerCapabilityAdvertisement,
|
|
1029
|
+
isTerminalParticipantState,
|
|
1030
|
+
parseParticipantState,
|
|
1031
|
+
parseWorkerIdentityV2
|
|
1032
|
+
} from "@ctliz/agent-intercom-core/boss";
|
|
1033
|
+
import {
|
|
1034
|
+
ContractValidationError,
|
|
1035
|
+
assertExactKeys,
|
|
1036
|
+
assertRecord,
|
|
1037
|
+
readHexDigest
|
|
1038
|
+
} from "@ctliz/agent-intercom-core/canonical";
|
|
1039
|
+
function assertCompatibleOrdinaryAdvertisement(value) {
|
|
1040
|
+
assertPlainDataGraph(value, "$.capabilities");
|
|
1041
|
+
const advertisement = parseBrokerCapabilityAdvertisement(value);
|
|
1042
|
+
const decision = evaluateBrokerCompatibility({
|
|
1043
|
+
clientKind: "ordinary",
|
|
1044
|
+
supportedBaseProtocolVersions: [INTERCOM_BASE_PROTOCOL_VERSION]
|
|
1045
|
+
}, advertisement);
|
|
1046
|
+
if (!decision.compatible || decision.mode !== "ordinary") {
|
|
1047
|
+
throw new ContractValidationError("$.capabilities", "broker does not support exact base protocol v3");
|
|
1048
|
+
}
|
|
1049
|
+
return advertisement;
|
|
1050
|
+
}
|
|
1051
|
+
function assertCompatibleBossAdvertisement(value) {
|
|
1052
|
+
assertPlainDataGraph(value, "$.capabilities");
|
|
1053
|
+
const advertisement = parseBrokerCapabilityAdvertisement(value);
|
|
1054
|
+
if (advertisement.baseProtocolVersion !== INTERCOM_BASE_PROTOCOL_VERSION || !advertisement.features.some((feature) => feature.feature === BOSS_RUN_FEATURE)) {
|
|
1055
|
+
throw new ContractValidationError("$.capabilities", "broker did not negotiate the exact required Boss feature");
|
|
1056
|
+
}
|
|
1057
|
+
return advertisement;
|
|
1058
|
+
}
|
|
1059
|
+
function parseBossRegistrationRequest(value) {
|
|
1060
|
+
assertPlainDataGraph(value, "$.boss");
|
|
1061
|
+
assertRecord(value, "$.boss");
|
|
1062
|
+
assertExactKeys(value, ["featureContract", "capabilityDigest", "credential"], [], "$.boss");
|
|
1063
|
+
const featureContract = parseBossRunFeatureContract(value.featureContract);
|
|
1064
|
+
const capabilityDigest = readHexDigest(value.capabilityDigest, "$.boss.capabilityDigest");
|
|
1065
|
+
if (featureContract.feature !== BOSS_RUN_FEATURE_CONTRACT.feature || featureContract.version !== BOSS_RUN_FEATURE_CONTRACT.version || featureContract.baseProtocolVersion !== INTERCOM_BASE_PROTOCOL_VERSION || featureContract.semanticsHash !== BOSS_RUN_FEATURE_CONTRACT.semanticsHash || featureContract.controlEnvelopeVersion !== BOSS_RUN_FEATURE_CONTRACT.controlEnvelopeVersion || capabilityDigest !== BOSS_CAPABILITY_FEATURE_DIGEST) {
|
|
1066
|
+
throw new ContractValidationError("$.boss", "Boss registration diverges from the canonical feature contract");
|
|
1067
|
+
}
|
|
1068
|
+
return {
|
|
1069
|
+
featureContract,
|
|
1070
|
+
capabilityDigest,
|
|
1071
|
+
credential: parseBossParticipantCredentialEnvelope(value.credential)
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
function parseBossSessionMetadata(value, sessionId) {
|
|
1075
|
+
assertPlainDataGraph(value, "$.boss");
|
|
1076
|
+
assertRecord(value, "$.boss");
|
|
1077
|
+
assertExactKeys(value, [
|
|
1078
|
+
"binding",
|
|
1079
|
+
"principal",
|
|
1080
|
+
"liveWorker",
|
|
1081
|
+
"featureContract",
|
|
1082
|
+
"policySemanticsHash",
|
|
1083
|
+
"capabilityDigest",
|
|
1084
|
+
"brokerIdentityVerified"
|
|
1085
|
+
], [], "$.boss");
|
|
1086
|
+
const binding = parseBossParticipantBinding(value.binding);
|
|
1087
|
+
const parsedPrincipal = parseBossPolicyPrincipal(value.principal);
|
|
1088
|
+
if (parsedPrincipal.principalClass !== "boss-private") {
|
|
1089
|
+
throw new ContractValidationError("$.boss.principal", "must be a Boss-private principal");
|
|
1090
|
+
}
|
|
1091
|
+
const principal = parsedPrincipal;
|
|
1092
|
+
assertRecord(value.liveWorker, "$.boss.liveWorker");
|
|
1093
|
+
assertExactKeys(value.liveWorker, ["identity", "state"], [], "$.boss.liveWorker");
|
|
1094
|
+
const liveWorkerIdentity = parseWorkerIdentityV2(value.liveWorker.identity);
|
|
1095
|
+
const liveWorkerState = parseParticipantState(value.liveWorker.state, "$.boss.liveWorker.state");
|
|
1096
|
+
const featureContract = parseBossRunFeatureContract(value.featureContract);
|
|
1097
|
+
const policySemanticsHash = readHexDigest(value.policySemanticsHash, "$.boss.policySemanticsHash");
|
|
1098
|
+
const capabilityDigest = readHexDigest(value.capabilityDigest, "$.boss.capabilityDigest");
|
|
1099
|
+
if (value.brokerIdentityVerified !== true) {
|
|
1100
|
+
throw new ContractValidationError("$.boss.brokerIdentityVerified", "must be true for a live Boss session");
|
|
1101
|
+
}
|
|
1102
|
+
if (sessionId !== void 0 && binding.sessionId !== sessionId) {
|
|
1103
|
+
throw new ContractValidationError("$.boss.binding.sessionId", "must match the registered session ID");
|
|
1104
|
+
}
|
|
1105
|
+
if (sessionId !== void 0 && principal.principalId !== sessionId) {
|
|
1106
|
+
throw new ContractValidationError("$.boss.principal.principalId", "must match the registered session ID");
|
|
1107
|
+
}
|
|
1108
|
+
if (!("bossRunId" in liveWorkerIdentity) || sessionId !== void 0 && liveWorkerIdentity.workerId !== sessionId || liveWorkerIdentity.bossRunId !== binding.bossRunId || liveWorkerIdentity.participantId !== binding.participantId || liveWorkerIdentity.bindingEpoch !== binding.bindingEpoch || isTerminalParticipantState(liveWorkerState)) {
|
|
1109
|
+
throw new ContractValidationError(
|
|
1110
|
+
"$.boss.liveWorker",
|
|
1111
|
+
"must attest the same live Boss worker identity and binding as the registered session"
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
if (binding.state !== "active" || principal.state !== "active" || binding.bossRunId !== principal.bossRunId || binding.participantId !== principal.participantId || binding.role !== principal.role || binding.bindingEpoch !== principal.bindingEpoch || binding.assignedManagerParticipantId !== principal.assignedManagerParticipantId) {
|
|
1115
|
+
throw new ContractValidationError("$.boss", "binding and policy principal are not the same active participant");
|
|
1116
|
+
}
|
|
1117
|
+
if (featureContract.semanticsHash !== BOSS_RUN_FEATURE_CONTRACT.semanticsHash || policySemanticsHash !== BOSS_POLICY_SEMANTICS_HASH || capabilityDigest !== BOSS_CAPABILITY_FEATURE_DIGEST) {
|
|
1118
|
+
throw new ContractValidationError("$.boss", "Boss session attestation diverges from Core");
|
|
1119
|
+
}
|
|
1120
|
+
return {
|
|
1121
|
+
binding,
|
|
1122
|
+
principal,
|
|
1123
|
+
liveWorker: { identity: liveWorkerIdentity, state: liveWorkerState },
|
|
1124
|
+
featureContract,
|
|
1125
|
+
policySemanticsHash,
|
|
1126
|
+
capabilityDigest,
|
|
1127
|
+
brokerIdentityVerified: true
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
function assertBossRegistrationEcho(requestValue, metadataValue, sessionId) {
|
|
1131
|
+
const request = parseBossRegistrationRequest(requestValue);
|
|
1132
|
+
const metadata = parseBossSessionMetadata(metadataValue, sessionId);
|
|
1133
|
+
const credential = request.credential;
|
|
1134
|
+
const binding = metadata.binding;
|
|
1135
|
+
if (credential.bossRunId !== binding.bossRunId || credential.participantId !== binding.participantId || credential.role !== binding.role || credential.communicationProfile !== binding.communicationProfile || credential.bindingEpoch !== binding.bindingEpoch || request.featureContract.feature !== metadata.featureContract.feature || request.featureContract.version !== metadata.featureContract.version || request.featureContract.baseProtocolVersion !== metadata.featureContract.baseProtocolVersion || request.featureContract.semanticsHash !== metadata.featureContract.semanticsHash || request.featureContract.controlEnvelopeVersion !== metadata.featureContract.controlEnvelopeVersion || request.capabilityDigest !== metadata.capabilityDigest) {
|
|
1136
|
+
throw new ContractValidationError("$.boss", "broker-owned Boss registration echo does not match the requested participant binding");
|
|
1137
|
+
}
|
|
1138
|
+
return metadata;
|
|
1139
|
+
}
|
|
1140
|
+
function parseCorrelatedBossControl(value, messageId) {
|
|
1141
|
+
assertPlainDataGraph(value, "$.control");
|
|
1142
|
+
const envelope = parseBossControlEnvelope(value);
|
|
1143
|
+
if (messageId !== void 0 && envelope.messageId !== messageId) {
|
|
1144
|
+
throw new ContractValidationError("$.control.messageId", "must match the transport message ID");
|
|
1145
|
+
}
|
|
1146
|
+
return envelope;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// broker/client.ts
|
|
1150
|
+
function toError(error) {
|
|
1151
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
1152
|
+
}
|
|
1153
|
+
function connectToBrokerTarget(target) {
|
|
1154
|
+
return typeof target === "string" ? net.connect(target) : net.connect({ host: target.host, port: target.port });
|
|
1155
|
+
}
|
|
1156
|
+
function isAttachment(value) {
|
|
1157
|
+
if (!hasExactDataKeys(value, ["type", "name", "content"], ["language"])) {
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1160
|
+
const attachment = value;
|
|
1161
|
+
if (attachment.type !== "file" && attachment.type !== "snippet" && attachment.type !== "context") {
|
|
1162
|
+
return false;
|
|
1163
|
+
}
|
|
1164
|
+
if (typeof attachment.name !== "string" || typeof attachment.content !== "string") {
|
|
1165
|
+
return false;
|
|
1166
|
+
}
|
|
1167
|
+
return attachment.language === void 0 || typeof attachment.language === "string";
|
|
1168
|
+
}
|
|
1169
|
+
function isMessage(value) {
|
|
1170
|
+
if (!hasExactDataKeys(value, ["id", "timestamp", "content"], ["replyTo", "expectsReply", "control"])) {
|
|
1171
|
+
return false;
|
|
1172
|
+
}
|
|
1173
|
+
const message = value;
|
|
1174
|
+
if (typeof message.id !== "string" || typeof message.timestamp !== "number") {
|
|
1175
|
+
return false;
|
|
1176
|
+
}
|
|
1177
|
+
if (message.replyTo !== void 0 && typeof message.replyTo !== "string") {
|
|
1178
|
+
return false;
|
|
1179
|
+
}
|
|
1180
|
+
if (message.expectsReply !== void 0 && typeof message.expectsReply !== "boolean") {
|
|
1181
|
+
return false;
|
|
1182
|
+
}
|
|
1183
|
+
if (message.control !== void 0) {
|
|
1184
|
+
try {
|
|
1185
|
+
parseCorrelatedBossControl(message.control, message.id);
|
|
1186
|
+
} catch {
|
|
1187
|
+
return false;
|
|
1188
|
+
}
|
|
1189
|
+
if (message.replyTo !== void 0 || message.expectsReply !== void 0) return false;
|
|
1190
|
+
}
|
|
1191
|
+
if (!hasExactDataKeys(message.content, ["text"], ["attachments"])) {
|
|
1192
|
+
return false;
|
|
1193
|
+
}
|
|
1194
|
+
const content = message.content;
|
|
1195
|
+
if (typeof content.text !== "string") {
|
|
1196
|
+
return false;
|
|
1197
|
+
}
|
|
1198
|
+
if (message.control !== void 0 && (content.text !== "" || content.attachments !== void 0)) return false;
|
|
1199
|
+
return content.attachments === void 0 || isDenseArrayOf(content.attachments, isAttachment);
|
|
1200
|
+
}
|
|
1201
|
+
function isSessionInfo(value) {
|
|
1202
|
+
if (!hasExactDataKeys(
|
|
1203
|
+
value,
|
|
1204
|
+
["id", "cwd", "model", "pid", "startedAt", "lastActivity"],
|
|
1205
|
+
[
|
|
1206
|
+
"name",
|
|
1207
|
+
"status",
|
|
1208
|
+
"peerUid",
|
|
1209
|
+
"trustedLocal",
|
|
1210
|
+
"origin",
|
|
1211
|
+
"remoteHostId",
|
|
1212
|
+
"parentSessionId",
|
|
1213
|
+
"rootSessionId",
|
|
1214
|
+
"generation",
|
|
1215
|
+
"canDelegate",
|
|
1216
|
+
"depth",
|
|
1217
|
+
"maxDepth",
|
|
1218
|
+
"maxChildren",
|
|
1219
|
+
"boss"
|
|
1220
|
+
]
|
|
1221
|
+
)) {
|
|
1222
|
+
return false;
|
|
1223
|
+
}
|
|
1224
|
+
const session = value;
|
|
1225
|
+
if (typeof session.id !== "string" || typeof session.cwd !== "string" || typeof session.model !== "string" || typeof session.pid !== "number" || typeof session.startedAt !== "number" || typeof session.lastActivity !== "number") {
|
|
1226
|
+
return false;
|
|
1227
|
+
}
|
|
1228
|
+
if (session.name !== void 0 && typeof session.name !== "string") {
|
|
1229
|
+
return false;
|
|
1230
|
+
}
|
|
1231
|
+
if (session.status !== void 0 && typeof session.status !== "string") {
|
|
1232
|
+
return false;
|
|
1233
|
+
}
|
|
1234
|
+
if (session.peerUid !== void 0 && typeof session.peerUid !== "number") {
|
|
1235
|
+
return false;
|
|
1236
|
+
}
|
|
1237
|
+
if (session.trustedLocal !== void 0 && typeof session.trustedLocal !== "boolean") return false;
|
|
1238
|
+
if (session.origin !== void 0 && session.origin !== "local" && session.origin !== "remote") return false;
|
|
1239
|
+
if (session.remoteHostId !== void 0 && typeof session.remoteHostId !== "string") return false;
|
|
1240
|
+
if (session.parentSessionId !== void 0 && typeof session.parentSessionId !== "string") return false;
|
|
1241
|
+
if (session.rootSessionId !== void 0 && typeof session.rootSessionId !== "string") return false;
|
|
1242
|
+
if (session.generation !== void 0 && (typeof session.generation !== "number" || !Number.isSafeInteger(session.generation))) return false;
|
|
1243
|
+
if (session.canDelegate !== void 0 && typeof session.canDelegate !== "boolean") return false;
|
|
1244
|
+
for (const field of ["depth", "maxDepth", "maxChildren"]) {
|
|
1245
|
+
if (session[field] !== void 0 && (typeof session[field] !== "number" || !Number.isSafeInteger(session[field]))) return false;
|
|
1246
|
+
}
|
|
1247
|
+
if (session.boss !== void 0) {
|
|
1248
|
+
try {
|
|
1249
|
+
parseBossSessionMetadata(session.boss, session.id);
|
|
1250
|
+
} catch {
|
|
1251
|
+
return false;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
return true;
|
|
1255
|
+
}
|
|
1256
|
+
function isRemoteAccessMetadata(value) {
|
|
1257
|
+
if (!hasExactDataKeys(
|
|
1258
|
+
value,
|
|
1259
|
+
["origin", "remoteHostId", "parentSessionId", "rootSessionId", "generation", "canDelegate", "depth", "maxDepth", "maxChildren"],
|
|
1260
|
+
["sessionCredential"]
|
|
1261
|
+
)) return false;
|
|
1262
|
+
const access = value;
|
|
1263
|
+
return access.origin === "remote" && typeof access.remoteHostId === "string" && typeof access.parentSessionId === "string" && typeof access.rootSessionId === "string" && typeof access.generation === "number" && Number.isSafeInteger(access.generation) && access.generation > 0 && typeof access.canDelegate === "boolean" && typeof access.depth === "number" && Number.isSafeInteger(access.depth) && typeof access.maxDepth === "number" && Number.isSafeInteger(access.maxDepth) && typeof access.maxChildren === "number" && Number.isSafeInteger(access.maxChildren) && (access.sessionCredential === void 0 || typeof access.sessionCredential === "string");
|
|
1264
|
+
}
|
|
1265
|
+
function isDeliveryFailureCode(value) {
|
|
1266
|
+
return typeof value === "string" && DELIVERY_FAILURE_CODES.includes(value);
|
|
1267
|
+
}
|
|
1268
|
+
var IntercomClient = class extends EventEmitter {
|
|
1269
|
+
constructor(options = {}) {
|
|
1270
|
+
super();
|
|
1271
|
+
this.options = options;
|
|
1272
|
+
this.scopeId = options.scopeId === void 0 ? intercomScopeIdFromEnvForRegistration(options.env ?? process.env) : parseIntercomScopeIdForRegistration(options.scopeId);
|
|
1273
|
+
}
|
|
1274
|
+
options;
|
|
1275
|
+
socket = null;
|
|
1276
|
+
scopeId;
|
|
1277
|
+
_sessionId = null;
|
|
1278
|
+
pendingSends = /* @__PURE__ */ new Map();
|
|
1279
|
+
lateDeliveryAcceptances = /* @__PURE__ */ new Map();
|
|
1280
|
+
pendingLists = /* @__PURE__ */ new Map();
|
|
1281
|
+
pendingAskControls = /* @__PURE__ */ new Map();
|
|
1282
|
+
outbox = null;
|
|
1283
|
+
remoteAccessCredential;
|
|
1284
|
+
requestedBossRegistration;
|
|
1285
|
+
bossSessionMetadata;
|
|
1286
|
+
disconnecting = false;
|
|
1287
|
+
disconnectError = null;
|
|
1288
|
+
failPending(error) {
|
|
1289
|
+
for (const pending of this.pendingSends.values()) {
|
|
1290
|
+
pending.reject(error);
|
|
1291
|
+
}
|
|
1292
|
+
this.pendingSends.clear();
|
|
1293
|
+
for (const pending of this.pendingLists.values()) {
|
|
1294
|
+
pending.reject(error);
|
|
1295
|
+
}
|
|
1296
|
+
this.pendingLists.clear();
|
|
1297
|
+
for (const pending of this.pendingAskControls.values()) {
|
|
1298
|
+
clearTimeout(pending.timeout);
|
|
1299
|
+
pending.resolve(false);
|
|
1300
|
+
}
|
|
1301
|
+
this.pendingAskControls.clear();
|
|
1302
|
+
this.lateDeliveryAcceptances.clear();
|
|
1303
|
+
}
|
|
1304
|
+
hasQueuedOutboundMessage(messageId) {
|
|
1305
|
+
return this.outbox?.list().some((entry) => entry.message.id === messageId) ?? false;
|
|
1306
|
+
}
|
|
1307
|
+
get sessionId() {
|
|
1308
|
+
return this._sessionId;
|
|
1309
|
+
}
|
|
1310
|
+
get outboxSize() {
|
|
1311
|
+
return this.outbox?.list().length ?? 0;
|
|
1312
|
+
}
|
|
1313
|
+
isConnected() {
|
|
1314
|
+
const socket = this.socket;
|
|
1315
|
+
return Boolean(socket && this._sessionId && !this.disconnecting && !socket.destroyed && !socket.writableEnded && socket.writable);
|
|
1316
|
+
}
|
|
1317
|
+
requireActiveSocket() {
|
|
1318
|
+
if (this.disconnecting) {
|
|
1319
|
+
throw new Error("Client disconnecting");
|
|
1320
|
+
}
|
|
1321
|
+
const socket = this.socket;
|
|
1322
|
+
if (!socket || !this._sessionId) {
|
|
1323
|
+
throw new Error("Not connected");
|
|
1324
|
+
}
|
|
1325
|
+
if (socket.destroyed || socket.writableEnded || !socket.writable) {
|
|
1326
|
+
throw new Error("Client disconnected");
|
|
1327
|
+
}
|
|
1328
|
+
return socket;
|
|
1329
|
+
}
|
|
1330
|
+
connect(session, sessionId) {
|
|
1331
|
+
if (this.socket) {
|
|
1332
|
+
return Promise.reject(new Error("Already connected"));
|
|
1333
|
+
}
|
|
1334
|
+
return new Promise((resolve4, reject) => {
|
|
1335
|
+
let socket;
|
|
1336
|
+
let target;
|
|
1337
|
+
try {
|
|
1338
|
+
this.requestedBossRegistration = session.boss === void 0 ? void 0 : parseBossRegistrationRequest(session.boss);
|
|
1339
|
+
target = getBrokerConnectTarget();
|
|
1340
|
+
this.remoteAccessCredential = loadRemoteAccessCredential();
|
|
1341
|
+
socket = connectToBrokerTarget(target);
|
|
1342
|
+
} catch (error) {
|
|
1343
|
+
this.requestedBossRegistration = void 0;
|
|
1344
|
+
reject(toError(error));
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
this.socket = socket;
|
|
1348
|
+
this.disconnectError = null;
|
|
1349
|
+
let settled = false;
|
|
1350
|
+
const timeout = setTimeout(() => {
|
|
1351
|
+
if (!this._sessionId) {
|
|
1352
|
+
cleanupConnectionAttempt();
|
|
1353
|
+
cleanupSocketListeners();
|
|
1354
|
+
if (this.socket === socket) {
|
|
1355
|
+
this.socket = null;
|
|
1356
|
+
}
|
|
1357
|
+
this.requestedBossRegistration = void 0;
|
|
1358
|
+
socket.destroy();
|
|
1359
|
+
reject(new Error("Connection timeout"));
|
|
1360
|
+
}
|
|
1361
|
+
}, 1e4);
|
|
1362
|
+
let connectionEstablished = false;
|
|
1363
|
+
const onRegistered = () => {
|
|
1364
|
+
settled = true;
|
|
1365
|
+
connectionEstablished = true;
|
|
1366
|
+
cleanupConnectionAttempt();
|
|
1367
|
+
resolve4();
|
|
1368
|
+
};
|
|
1369
|
+
const onError = (err) => {
|
|
1370
|
+
settled = true;
|
|
1371
|
+
cleanupConnectionAttempt();
|
|
1372
|
+
cleanupSocketListeners();
|
|
1373
|
+
if (this.socket === socket) {
|
|
1374
|
+
this.socket = null;
|
|
1375
|
+
}
|
|
1376
|
+
this.requestedBossRegistration = void 0;
|
|
1377
|
+
socket.destroy();
|
|
1378
|
+
reject(err);
|
|
1379
|
+
};
|
|
1380
|
+
const onClose = () => {
|
|
1381
|
+
const wasConnecting = !settled && !this._sessionId;
|
|
1382
|
+
const wasDisconnecting = this.disconnecting;
|
|
1383
|
+
const disconnectError = this.disconnectError ?? new Error("Client disconnected");
|
|
1384
|
+
this.disconnecting = false;
|
|
1385
|
+
cleanupConnectionAttempt();
|
|
1386
|
+
cleanupSocketListeners();
|
|
1387
|
+
this.failPending(disconnectError);
|
|
1388
|
+
if (this.socket === socket) {
|
|
1389
|
+
this.socket = null;
|
|
1390
|
+
}
|
|
1391
|
+
this._sessionId = null;
|
|
1392
|
+
this.requestedBossRegistration = void 0;
|
|
1393
|
+
this.bossSessionMetadata = void 0;
|
|
1394
|
+
this.disconnectError = null;
|
|
1395
|
+
if (connectionEstablished && !wasDisconnecting) {
|
|
1396
|
+
this.emit("disconnected", disconnectError);
|
|
1397
|
+
}
|
|
1398
|
+
if (wasConnecting) {
|
|
1399
|
+
reject(new Error("Connection closed before registration"));
|
|
1400
|
+
}
|
|
1401
|
+
};
|
|
1402
|
+
const onSocketError = (err) => {
|
|
1403
|
+
if (connectionEstablished) {
|
|
1404
|
+
this.disconnectError = err;
|
|
1405
|
+
this.emit("error", err);
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
1408
|
+
const onReaderError = (error) => {
|
|
1409
|
+
const protocolError = new Error(`Intercom protocol error: ${error.message}`, { cause: error });
|
|
1410
|
+
if (!connectionEstablished) {
|
|
1411
|
+
onError(protocolError);
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
this.disconnectError = protocolError;
|
|
1415
|
+
this.emit("error", protocolError);
|
|
1416
|
+
socket.destroy();
|
|
1417
|
+
};
|
|
1418
|
+
const reader = createMessageReader((msg) => {
|
|
1419
|
+
this.handleBrokerMessage(msg);
|
|
1420
|
+
}, onReaderError);
|
|
1421
|
+
const cleanupConnectionAttempt = () => {
|
|
1422
|
+
this.off("_registered", onRegistered);
|
|
1423
|
+
socket.off("error", onError);
|
|
1424
|
+
clearTimeout(timeout);
|
|
1425
|
+
};
|
|
1426
|
+
const cleanupSocketListeners = () => {
|
|
1427
|
+
socket.off("data", reader);
|
|
1428
|
+
socket.off("error", onSocketError);
|
|
1429
|
+
socket.off("close", onClose);
|
|
1430
|
+
};
|
|
1431
|
+
socket.on("data", reader);
|
|
1432
|
+
socket.on("error", onError);
|
|
1433
|
+
socket.on("close", onClose);
|
|
1434
|
+
socket.on("error", onSocketError);
|
|
1435
|
+
this.once("_registered", onRegistered);
|
|
1436
|
+
try {
|
|
1437
|
+
writeMessage(socket, {
|
|
1438
|
+
type: "register",
|
|
1439
|
+
protocol: INTERCOM_PROTOCOL_NAME,
|
|
1440
|
+
version: INTERCOM_PROTOCOL_VERSION,
|
|
1441
|
+
session,
|
|
1442
|
+
...!this.remoteAccessCredential && sessionId ? { sessionId } : {},
|
|
1443
|
+
...this.remoteAccessCredential ? { access: this.remoteAccessCredential.access } : {},
|
|
1444
|
+
...this.scopeId ? { scopeId: this.scopeId } : {},
|
|
1445
|
+
...typeof target === "string" ? {} : { stateId: target.stateId }
|
|
1446
|
+
});
|
|
1447
|
+
} catch (error) {
|
|
1448
|
+
cleanupConnectionAttempt();
|
|
1449
|
+
cleanupSocketListeners();
|
|
1450
|
+
if (this.socket === socket) {
|
|
1451
|
+
this.socket = null;
|
|
1452
|
+
}
|
|
1453
|
+
this.requestedBossRegistration = void 0;
|
|
1454
|
+
socket.destroy();
|
|
1455
|
+
reject(toError(error));
|
|
1456
|
+
}
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
handleBrokerMessage(msg) {
|
|
1460
|
+
if (!isPlainDataRecord(msg) || typeof msg.type !== "string") {
|
|
1461
|
+
throw new Error("Invalid broker message");
|
|
1462
|
+
}
|
|
1463
|
+
const brokerMessage = msg;
|
|
1464
|
+
if (this._sessionId === null && brokerMessage.type !== "registered" && brokerMessage.type !== "error") {
|
|
1465
|
+
throw new Error(`Received ${brokerMessage.type} before registered`);
|
|
1466
|
+
}
|
|
1467
|
+
switch (brokerMessage.type) {
|
|
1468
|
+
case "registered": {
|
|
1469
|
+
const baseKeys = ["type", "sessionId", "protocol", "version"];
|
|
1470
|
+
if (this.requestedBossRegistration !== void 0 && (brokerMessage.capabilities === void 0 || brokerMessage.boss === void 0)) {
|
|
1471
|
+
throw new Error("Boss registration was downgraded to an ordinary session");
|
|
1472
|
+
}
|
|
1473
|
+
const exactKeys = this.requestedBossRegistration !== void 0 ? [...baseKeys, "capabilities", "boss"] : this.remoteAccessCredential ? [...baseKeys, "remoteAccess", "access"] : baseKeys;
|
|
1474
|
+
const optionalKeys = this.requestedBossRegistration === void 0 ? ["capabilities"] : [];
|
|
1475
|
+
if (!hasExactDataKeys(brokerMessage, exactKeys, optionalKeys) || typeof brokerMessage.sessionId !== "string" || brokerMessage.protocol !== INTERCOM_PROTOCOL_NAME || brokerMessage.version !== INTERCOM_PROTOCOL_VERSION) {
|
|
1476
|
+
throw new Error("Invalid registered message");
|
|
1477
|
+
}
|
|
1478
|
+
if (this._sessionId !== null) {
|
|
1479
|
+
throw new Error("Received duplicate registered message");
|
|
1480
|
+
}
|
|
1481
|
+
if (this.requestedBossRegistration !== void 0) {
|
|
1482
|
+
assertCompatibleBossAdvertisement(brokerMessage.capabilities);
|
|
1483
|
+
this.bossSessionMetadata = assertBossRegistrationEcho(
|
|
1484
|
+
this.requestedBossRegistration,
|
|
1485
|
+
brokerMessage.boss,
|
|
1486
|
+
brokerMessage.sessionId
|
|
1487
|
+
);
|
|
1488
|
+
this.requestedBossRegistration = void 0;
|
|
1489
|
+
} else {
|
|
1490
|
+
if (brokerMessage.boss !== void 0) {
|
|
1491
|
+
throw new Error("Ordinary registration received unsolicited Boss authority metadata");
|
|
1492
|
+
}
|
|
1493
|
+
if (brokerMessage.capabilities !== void 0) {
|
|
1494
|
+
assertCompatibleOrdinaryAdvertisement(brokerMessage.capabilities);
|
|
1495
|
+
}
|
|
1496
|
+
this.bossSessionMetadata = void 0;
|
|
1497
|
+
}
|
|
1498
|
+
if (this.remoteAccessCredential) {
|
|
1499
|
+
const contract = brokerMessage.remoteAccess;
|
|
1500
|
+
const contractFields = hasExactDataKeys(
|
|
1501
|
+
contract,
|
|
1502
|
+
["feature", "policySemanticsVersion", "policySemanticsHash"]
|
|
1503
|
+
) ? contract : void 0;
|
|
1504
|
+
if (!contractFields || contractFields.feature !== "remote-access-v1" || contractFields.policySemanticsVersion !== POLICY_SEMANTICS_VERSION || contractFields.policySemanticsHash !== POLICY_SEMANTICS_HASH) {
|
|
1505
|
+
throw new Error("Remote Intercom policy contract is absent or incompatible");
|
|
1506
|
+
}
|
|
1507
|
+
if (!isRemoteAccessMetadata(brokerMessage.access)) {
|
|
1508
|
+
throw new Error("Remote Intercom registration omitted broker-owned provenance");
|
|
1509
|
+
}
|
|
1510
|
+
if (this.remoteAccessCredential.enrollment) {
|
|
1511
|
+
writeRemoteSessionCredential(this.remoteAccessCredential.path, brokerMessage.sessionId, brokerMessage.access);
|
|
1512
|
+
} else {
|
|
1513
|
+
const reconnect = this.remoteAccessCredential.access;
|
|
1514
|
+
if (!isPlainDataRecord(reconnect) || reconnect.sessionId !== brokerMessage.sessionId || reconnect.generation !== brokerMessage.access.generation) {
|
|
1515
|
+
throw new Error("Remote Intercom reconnect identity or generation changed unexpectedly");
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
this._sessionId = brokerMessage.sessionId;
|
|
1520
|
+
this.outbox = new PersistentOutboundOutbox(brokerMessage.sessionId);
|
|
1521
|
+
this.lateDeliveryAcceptances.clear();
|
|
1522
|
+
this.replayOutbox();
|
|
1523
|
+
this.emit("_registered", { type: "registered", sessionId: brokerMessage.sessionId });
|
|
1524
|
+
break;
|
|
1525
|
+
}
|
|
1526
|
+
case "sessions": {
|
|
1527
|
+
const { requestId, sessions } = brokerMessage;
|
|
1528
|
+
if (!hasExactDataKeys(brokerMessage, ["type", "requestId", "sessions"]) || typeof requestId !== "string" || !isDenseArrayOf(sessions, isSessionInfo)) {
|
|
1529
|
+
throw new Error("Invalid sessions message");
|
|
1530
|
+
}
|
|
1531
|
+
const pending = this.pendingLists.get(requestId);
|
|
1532
|
+
if (!pending) {
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
this.pendingLists.delete(requestId);
|
|
1536
|
+
pending.resolve(sessions);
|
|
1537
|
+
break;
|
|
1538
|
+
}
|
|
1539
|
+
case "message": {
|
|
1540
|
+
const { deliveryId, from, message } = brokerMessage;
|
|
1541
|
+
if (!hasExactDataKeys(brokerMessage, ["type", "deliveryId", "from", "message"]) || typeof deliveryId !== "string" || !isSessionInfo(from) || !isMessage(message)) {
|
|
1542
|
+
throw new Error("Invalid message event");
|
|
1543
|
+
}
|
|
1544
|
+
if (message.control !== void 0) {
|
|
1545
|
+
if (this.bossSessionMetadata === void 0) {
|
|
1546
|
+
throw new Error("Ordinary session received unavailable Boss control traffic");
|
|
1547
|
+
}
|
|
1548
|
+
this.emit("control", from, message.control, deliveryId);
|
|
1549
|
+
} else {
|
|
1550
|
+
this.emit("message", from, message, deliveryId);
|
|
1551
|
+
}
|
|
1552
|
+
break;
|
|
1553
|
+
}
|
|
1554
|
+
case "delivery_accepted": {
|
|
1555
|
+
const { deliveryId, messageId } = brokerMessage;
|
|
1556
|
+
if (!hasExactDataKeys(brokerMessage, ["type", "messageId", "deliveryId"]) || typeof deliveryId !== "string" || typeof messageId !== "string") {
|
|
1557
|
+
throw new Error("Invalid delivery_accepted message");
|
|
1558
|
+
}
|
|
1559
|
+
const pending = this.pendingSends.get(messageId);
|
|
1560
|
+
if (pending) {
|
|
1561
|
+
if (pending.accepted) {
|
|
1562
|
+
throw new Error("Duplicate delivery_accepted message");
|
|
1563
|
+
}
|
|
1564
|
+
pending.accepted = true;
|
|
1565
|
+
pending.deliveryId = deliveryId;
|
|
1566
|
+
this.emit("delivery_accepted", messageId, deliveryId);
|
|
1567
|
+
break;
|
|
1568
|
+
}
|
|
1569
|
+
if (!this.hasQueuedOutboundMessage(messageId)) {
|
|
1570
|
+
throw new Error("Unexpected delivery_accepted message without a pending send or queued outbox message");
|
|
1571
|
+
}
|
|
1572
|
+
if (this.lateDeliveryAcceptances.has(messageId)) {
|
|
1573
|
+
throw new Error("Duplicate delivery_accepted message");
|
|
1574
|
+
}
|
|
1575
|
+
this.lateDeliveryAcceptances.set(messageId, deliveryId);
|
|
1576
|
+
this.emit("delivery_accepted", messageId, deliveryId);
|
|
1577
|
+
break;
|
|
1578
|
+
}
|
|
1579
|
+
case "delivered": {
|
|
1580
|
+
const { deliveryId, messageId } = brokerMessage;
|
|
1581
|
+
if (!hasExactDataKeys(brokerMessage, ["type", "messageId", "deliveryId"]) || typeof deliveryId !== "string" || typeof messageId !== "string") {
|
|
1582
|
+
throw new Error("Invalid delivered message");
|
|
1583
|
+
}
|
|
1584
|
+
const pending = this.pendingSends.get(messageId);
|
|
1585
|
+
if (pending) {
|
|
1586
|
+
if (!pending.accepted) {
|
|
1587
|
+
throw new Error("Delivered message arrived before delivery acceptance");
|
|
1588
|
+
}
|
|
1589
|
+
if (pending.deliveryId !== deliveryId) {
|
|
1590
|
+
throw new Error("Delivered message used a mismatched delivery ID");
|
|
1591
|
+
}
|
|
1592
|
+
this.outbox?.remove(messageId);
|
|
1593
|
+
this.pendingSends.delete(messageId);
|
|
1594
|
+
this.lateDeliveryAcceptances.delete(messageId);
|
|
1595
|
+
pending.resolve({ id: messageId, accepted: true, delivered: true, deliveryId });
|
|
1596
|
+
break;
|
|
1597
|
+
}
|
|
1598
|
+
if (!this.hasQueuedOutboundMessage(messageId)) {
|
|
1599
|
+
throw new Error("Unexpected delivered message without a pending send or queued outbox message");
|
|
1600
|
+
}
|
|
1601
|
+
const acceptedDeliveryId = this.lateDeliveryAcceptances.get(messageId);
|
|
1602
|
+
if (acceptedDeliveryId === void 0) {
|
|
1603
|
+
throw new Error("Delivered message arrived before delivery acceptance");
|
|
1604
|
+
}
|
|
1605
|
+
if (acceptedDeliveryId !== deliveryId) {
|
|
1606
|
+
throw new Error("Delivered message used a mismatched delivery ID");
|
|
1607
|
+
}
|
|
1608
|
+
this.outbox?.remove(messageId);
|
|
1609
|
+
this.lateDeliveryAcceptances.delete(messageId);
|
|
1610
|
+
this.emit("outbox_delivered", messageId, deliveryId);
|
|
1611
|
+
break;
|
|
1612
|
+
}
|
|
1613
|
+
case "delivery_failed": {
|
|
1614
|
+
const { accepted, code, messageId, reason } = brokerMessage;
|
|
1615
|
+
if (!hasExactDataKeys(brokerMessage, ["type", "messageId", "accepted", "code", "reason"]) || typeof accepted !== "boolean" || !isDeliveryFailureCode(code) || typeof messageId !== "string" || typeof reason !== "string") {
|
|
1616
|
+
throw new Error("Invalid delivery_failed message");
|
|
1617
|
+
}
|
|
1618
|
+
const pending = this.pendingSends.get(messageId);
|
|
1619
|
+
if (pending) {
|
|
1620
|
+
if (accepted !== pending.accepted) {
|
|
1621
|
+
throw new Error("Delivery failure contradicted the accepted delivery state");
|
|
1622
|
+
}
|
|
1623
|
+
this.outbox?.remove(messageId);
|
|
1624
|
+
this.pendingSends.delete(messageId);
|
|
1625
|
+
this.lateDeliveryAcceptances.delete(messageId);
|
|
1626
|
+
pending.resolve({
|
|
1627
|
+
id: messageId,
|
|
1628
|
+
accepted,
|
|
1629
|
+
delivered: false,
|
|
1630
|
+
code,
|
|
1631
|
+
reason,
|
|
1632
|
+
...pending.deliveryId !== void 0 ? { deliveryId: pending.deliveryId } : {}
|
|
1633
|
+
});
|
|
1634
|
+
break;
|
|
1635
|
+
}
|
|
1636
|
+
if (!this.hasQueuedOutboundMessage(messageId)) {
|
|
1637
|
+
throw new Error("Unexpected delivery_failed message without a pending send or queued outbox message");
|
|
1638
|
+
}
|
|
1639
|
+
if (accepted !== this.lateDeliveryAcceptances.has(messageId)) {
|
|
1640
|
+
throw new Error("Delivery failure contradicted the accepted delivery state");
|
|
1641
|
+
}
|
|
1642
|
+
this.outbox?.remove(messageId);
|
|
1643
|
+
this.lateDeliveryAcceptances.delete(messageId);
|
|
1644
|
+
this.emit("outbox_failed", messageId, code, reason);
|
|
1645
|
+
break;
|
|
1646
|
+
}
|
|
1647
|
+
case "ask_deferred": {
|
|
1648
|
+
const { fromSessionId, messageId } = brokerMessage;
|
|
1649
|
+
if (typeof fromSessionId !== "string" || typeof messageId !== "string") {
|
|
1650
|
+
throw new Error("Invalid ask_deferred message");
|
|
1651
|
+
}
|
|
1652
|
+
this.emit("ask_deferred", messageId, fromSessionId);
|
|
1653
|
+
break;
|
|
1654
|
+
}
|
|
1655
|
+
case "ask_cancelled": {
|
|
1656
|
+
const { fromSessionId, messageId, reason } = brokerMessage;
|
|
1657
|
+
if (typeof fromSessionId !== "string" || typeof messageId !== "string" || typeof reason !== "string") {
|
|
1658
|
+
throw new Error("Invalid ask_cancelled message");
|
|
1659
|
+
}
|
|
1660
|
+
this.emit("ask_cancelled", messageId, fromSessionId, reason);
|
|
1661
|
+
break;
|
|
1662
|
+
}
|
|
1663
|
+
case "ask_control_result": {
|
|
1664
|
+
const { action, applied, messageId, requestId } = brokerMessage;
|
|
1665
|
+
if (action !== "defer" && action !== "cancel" || typeof applied !== "boolean" || typeof messageId !== "string" || typeof requestId !== "string") {
|
|
1666
|
+
throw new Error("Invalid ask_control_result message");
|
|
1667
|
+
}
|
|
1668
|
+
const pending = this.pendingAskControls.get(requestId);
|
|
1669
|
+
if (!pending) return;
|
|
1670
|
+
clearTimeout(pending.timeout);
|
|
1671
|
+
this.pendingAskControls.delete(requestId);
|
|
1672
|
+
pending.resolve(applied);
|
|
1673
|
+
break;
|
|
1674
|
+
}
|
|
1675
|
+
case "session_joined": {
|
|
1676
|
+
if (!isSessionInfo(brokerMessage.session)) {
|
|
1677
|
+
throw new Error("Invalid session_joined message");
|
|
1678
|
+
}
|
|
1679
|
+
this.emit("session_joined", brokerMessage.session);
|
|
1680
|
+
break;
|
|
1681
|
+
}
|
|
1682
|
+
case "session_left": {
|
|
1683
|
+
if (typeof brokerMessage.sessionId !== "string") {
|
|
1684
|
+
throw new Error("Invalid session_left message");
|
|
1685
|
+
}
|
|
1686
|
+
this.emit("session_left", brokerMessage.sessionId);
|
|
1687
|
+
break;
|
|
1688
|
+
}
|
|
1689
|
+
case "presence_update": {
|
|
1690
|
+
if (!isSessionInfo(brokerMessage.session)) {
|
|
1691
|
+
throw new Error("Invalid presence_update message");
|
|
1692
|
+
}
|
|
1693
|
+
this.emit("presence_update", brokerMessage.session);
|
|
1694
|
+
break;
|
|
1695
|
+
}
|
|
1696
|
+
case "error": {
|
|
1697
|
+
if (typeof brokerMessage.code !== "string" || typeof brokerMessage.error !== "string") {
|
|
1698
|
+
throw new Error("Invalid error message");
|
|
1699
|
+
}
|
|
1700
|
+
if (this._sessionId === null) {
|
|
1701
|
+
const error2 = new Error(brokerMessage.error);
|
|
1702
|
+
error2.code = brokerMessage.code;
|
|
1703
|
+
throw error2;
|
|
1704
|
+
}
|
|
1705
|
+
const error = new Error(brokerMessage.error);
|
|
1706
|
+
error.code = brokerMessage.code;
|
|
1707
|
+
this.emit("error", error);
|
|
1708
|
+
break;
|
|
1709
|
+
}
|
|
1710
|
+
default:
|
|
1711
|
+
throw new Error(`Unknown broker message type: ${brokerMessage.type}`);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
async disconnect(preserveAsks = false) {
|
|
1715
|
+
const socket = this.socket;
|
|
1716
|
+
if (!socket) {
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
this.disconnecting = true;
|
|
1720
|
+
this.disconnectError = null;
|
|
1721
|
+
this.failPending(new Error("Client disconnected"));
|
|
1722
|
+
if (!preserveAsks) this.outbox?.clear();
|
|
1723
|
+
await new Promise((resolve4) => {
|
|
1724
|
+
let settled = false;
|
|
1725
|
+
const finish = () => {
|
|
1726
|
+
if (settled) {
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
settled = true;
|
|
1730
|
+
clearTimeout(timeout);
|
|
1731
|
+
socket.off("close", onClose);
|
|
1732
|
+
socket.off("error", onError);
|
|
1733
|
+
resolve4();
|
|
1734
|
+
};
|
|
1735
|
+
const onClose = () => finish();
|
|
1736
|
+
const onError = () => {
|
|
1737
|
+
socket.destroy();
|
|
1738
|
+
};
|
|
1739
|
+
const timeout = setTimeout(() => {
|
|
1740
|
+
socket.destroy();
|
|
1741
|
+
}, 2e3);
|
|
1742
|
+
socket.once("close", onClose);
|
|
1743
|
+
socket.once("error", onError);
|
|
1744
|
+
try {
|
|
1745
|
+
writeMessage(socket, { type: "unregister", ...preserveAsks ? { preserveAsks: true } : {} });
|
|
1746
|
+
socket.end();
|
|
1747
|
+
} catch {
|
|
1748
|
+
socket.destroy();
|
|
1749
|
+
}
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
listSessions() {
|
|
1753
|
+
let socket;
|
|
1754
|
+
try {
|
|
1755
|
+
socket = this.requireActiveSocket();
|
|
1756
|
+
} catch (error) {
|
|
1757
|
+
return Promise.reject(toError(error));
|
|
1758
|
+
}
|
|
1759
|
+
return new Promise((resolve4, reject) => {
|
|
1760
|
+
const requestId = randomUUID2();
|
|
1761
|
+
const wrappedResolve = (sessions) => {
|
|
1762
|
+
clearTimeout(timeout);
|
|
1763
|
+
resolve4(sessions);
|
|
1764
|
+
};
|
|
1765
|
+
const wrappedReject = (error) => {
|
|
1766
|
+
clearTimeout(timeout);
|
|
1767
|
+
reject(error);
|
|
1768
|
+
};
|
|
1769
|
+
const timeout = setTimeout(() => {
|
|
1770
|
+
if (this.pendingLists.has(requestId)) {
|
|
1771
|
+
this.pendingLists.delete(requestId);
|
|
1772
|
+
wrappedReject(new Error("List sessions timeout"));
|
|
1773
|
+
}
|
|
1774
|
+
}, 5e3);
|
|
1775
|
+
this.pendingLists.set(requestId, { resolve: wrappedResolve, reject: wrappedReject });
|
|
1776
|
+
try {
|
|
1777
|
+
writeMessage(socket, { type: "list", requestId });
|
|
1778
|
+
} catch (error) {
|
|
1779
|
+
clearTimeout(timeout);
|
|
1780
|
+
this.pendingLists.delete(requestId);
|
|
1781
|
+
reject(toError(error));
|
|
1782
|
+
}
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
send(to, options) {
|
|
1786
|
+
return this.sendInternal(to, options);
|
|
1787
|
+
}
|
|
1788
|
+
sendControl(to, controlValue) {
|
|
1789
|
+
const control = parseCorrelatedBossControl(controlValue);
|
|
1790
|
+
return this.sendInternal(to, {
|
|
1791
|
+
text: "",
|
|
1792
|
+
messageId: control.messageId,
|
|
1793
|
+
control
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
sendInternal(to, options) {
|
|
1797
|
+
let socket;
|
|
1798
|
+
try {
|
|
1799
|
+
socket = this.requireActiveSocket();
|
|
1800
|
+
} catch (error) {
|
|
1801
|
+
return Promise.reject(toError(error));
|
|
1802
|
+
}
|
|
1803
|
+
const messageId = options.messageId ?? randomUUID2();
|
|
1804
|
+
if (this.pendingSends.has(messageId)) {
|
|
1805
|
+
return Promise.resolve({
|
|
1806
|
+
id: messageId,
|
|
1807
|
+
accepted: false,
|
|
1808
|
+
delivered: false,
|
|
1809
|
+
code: "DUPLICATE_MESSAGE_ID",
|
|
1810
|
+
reason: `Message ID ${messageId} is already pending`
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
const message = {
|
|
1814
|
+
id: messageId,
|
|
1815
|
+
timestamp: Date.now(),
|
|
1816
|
+
replyTo: options.replyTo,
|
|
1817
|
+
expectsReply: options.expectsReply,
|
|
1818
|
+
...options.control === void 0 ? {} : { control: options.control },
|
|
1819
|
+
content: {
|
|
1820
|
+
text: options.text,
|
|
1821
|
+
...options.attachments === void 0 ? {} : { attachments: options.attachments }
|
|
1822
|
+
}
|
|
1823
|
+
};
|
|
1824
|
+
try {
|
|
1825
|
+
this.outbox?.enqueue(to, message);
|
|
1826
|
+
} catch (error) {
|
|
1827
|
+
return Promise.reject(toError(error));
|
|
1828
|
+
}
|
|
1829
|
+
return new Promise((resolve4, reject) => {
|
|
1830
|
+
const wrappedResolve = (result) => {
|
|
1831
|
+
clearTimeout(timeout);
|
|
1832
|
+
resolve4(result);
|
|
1833
|
+
};
|
|
1834
|
+
const wrappedReject = (error) => {
|
|
1835
|
+
clearTimeout(timeout);
|
|
1836
|
+
reject(error);
|
|
1837
|
+
};
|
|
1838
|
+
const timeout = setTimeout(() => {
|
|
1839
|
+
const pending = this.pendingSends.get(messageId);
|
|
1840
|
+
if (pending) {
|
|
1841
|
+
if (pending.accepted && pending.deliveryId !== void 0 && this.hasQueuedOutboundMessage(messageId)) {
|
|
1842
|
+
this.lateDeliveryAcceptances.set(messageId, pending.deliveryId);
|
|
1843
|
+
}
|
|
1844
|
+
this.pendingSends.delete(messageId);
|
|
1845
|
+
wrappedReject(new Error("Send timeout"));
|
|
1846
|
+
}
|
|
1847
|
+
}, 1e4);
|
|
1848
|
+
this.pendingSends.set(messageId, {
|
|
1849
|
+
accepted: false,
|
|
1850
|
+
resolve: wrappedResolve,
|
|
1851
|
+
reject: wrappedReject
|
|
1852
|
+
});
|
|
1853
|
+
try {
|
|
1854
|
+
writeMessage(socket, { type: "send", to, message });
|
|
1855
|
+
} catch (error) {
|
|
1856
|
+
clearTimeout(timeout);
|
|
1857
|
+
this.pendingSends.delete(messageId);
|
|
1858
|
+
reject(toError(error));
|
|
1859
|
+
}
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
acknowledgeMessage(deliveryId) {
|
|
1863
|
+
return this.writeControlMessage({ type: "message_received", deliveryId });
|
|
1864
|
+
}
|
|
1865
|
+
rejectMessage(deliveryId, reason) {
|
|
1866
|
+
return this.writeControlMessage({ type: "message_rejected", deliveryId, code: "CONFLICTING_MESSAGE_ID", reason });
|
|
1867
|
+
}
|
|
1868
|
+
deferAsk(messageId) {
|
|
1869
|
+
return this.sendAskControl("defer", messageId);
|
|
1870
|
+
}
|
|
1871
|
+
cancelAsk(messageId) {
|
|
1872
|
+
return this.sendAskControl("cancel", messageId);
|
|
1873
|
+
}
|
|
1874
|
+
sendAskControl(action, messageId) {
|
|
1875
|
+
const requestId = randomUUID2();
|
|
1876
|
+
return new Promise((resolve4) => {
|
|
1877
|
+
const timeout = setTimeout(() => {
|
|
1878
|
+
this.pendingAskControls.delete(requestId);
|
|
1879
|
+
resolve4(false);
|
|
1880
|
+
}, 2e3);
|
|
1881
|
+
timeout.unref?.();
|
|
1882
|
+
this.pendingAskControls.set(requestId, { resolve: resolve4, timeout });
|
|
1883
|
+
if (!this.writeControlMessage({ type: action === "defer" ? "defer_ask" : "cancel_ask", requestId, messageId })) {
|
|
1884
|
+
clearTimeout(timeout);
|
|
1885
|
+
this.pendingAskControls.delete(requestId);
|
|
1886
|
+
resolve4(false);
|
|
1887
|
+
}
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
writeControlMessage(message) {
|
|
1891
|
+
if (this.disconnecting) {
|
|
1892
|
+
return false;
|
|
1893
|
+
}
|
|
1894
|
+
const socket = this.socket;
|
|
1895
|
+
if (!socket || !this._sessionId || socket.destroyed || socket.writableEnded || !socket.writable) {
|
|
1896
|
+
return false;
|
|
1897
|
+
}
|
|
1898
|
+
try {
|
|
1899
|
+
writeMessage(socket, message);
|
|
1900
|
+
return true;
|
|
1901
|
+
} catch {
|
|
1902
|
+
return false;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
replayOutbox() {
|
|
1906
|
+
const socket = this.socket;
|
|
1907
|
+
if (!socket || !this._sessionId || socket.destroyed || socket.writableEnded || !socket.writable) return;
|
|
1908
|
+
for (const entry of this.outbox?.list() ?? []) {
|
|
1909
|
+
if (this.pendingSends.has(entry.message.id)) continue;
|
|
1910
|
+
this.lateDeliveryAcceptances.delete(entry.message.id);
|
|
1911
|
+
this.pendingSends.set(entry.message.id, {
|
|
1912
|
+
accepted: false,
|
|
1913
|
+
resolve: (result) => {
|
|
1914
|
+
if (result.delivered && result.deliveryId) {
|
|
1915
|
+
this.emit("outbox_delivered", result.id, result.deliveryId);
|
|
1916
|
+
} else {
|
|
1917
|
+
this.emit("outbox_failed", result.id, result.code, result.reason);
|
|
1918
|
+
}
|
|
1919
|
+
},
|
|
1920
|
+
reject: () => {
|
|
1921
|
+
}
|
|
1922
|
+
});
|
|
1923
|
+
try {
|
|
1924
|
+
writeMessage(socket, { type: "send", to: entry.to, message: entry.message });
|
|
1925
|
+
} catch {
|
|
1926
|
+
this.pendingSends.delete(entry.message.id);
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
updatePresence(updates) {
|
|
1932
|
+
if (this.disconnecting) {
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
const socket = this.socket;
|
|
1936
|
+
if (!socket || !this._sessionId || socket.destroyed || socket.writableEnded || !socket.writable) {
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
writeMessage(socket, { type: "presence", ...updates });
|
|
1940
|
+
}
|
|
1941
|
+
};
|
|
1942
|
+
|
|
1943
|
+
// broker/spawn.ts
|
|
1944
|
+
import { spawn as spawn2 } from "child_process";
|
|
1945
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
1946
|
+
import { join as join5, dirname as dirname4 } from "path";
|
|
1947
|
+
import { fileURLToPath } from "url";
|
|
1948
|
+
import { createRequire } from "module";
|
|
1949
|
+
import net2 from "net";
|
|
1950
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1951
|
+
import { POLICY_SEMANTICS_HASH as POLICY_SEMANTICS_HASH2, POLICY_SEMANTICS_VERSION as POLICY_SEMANTICS_VERSION2 } from "@ctliz/agent-intercom-core";
|
|
1952
|
+
var INTERCOM_DIR = getIntercomDirPath();
|
|
1953
|
+
var EXTENSION_DIR = join5(dirname4(fileURLToPath(import.meta.url)), "..");
|
|
1954
|
+
var BROKER_PID = join5(INTERCOM_DIR, "broker.pid");
|
|
1955
|
+
var BROKER_SPAWN_LOCK = join5(INTERCOM_DIR, "broker.spawn.lock");
|
|
1956
|
+
function sleep(ms) {
|
|
1957
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
1958
|
+
}
|
|
1959
|
+
function getBrokerEntryPath(moduleUrl = import.meta.url) {
|
|
1960
|
+
const moduleDir = dirname4(fileURLToPath(moduleUrl));
|
|
1961
|
+
const bundledBroker = join5(moduleDir, "broker.mjs");
|
|
1962
|
+
return existsSync3(bundledBroker) ? bundledBroker : join5(moduleDir, "broker.ts");
|
|
1963
|
+
}
|
|
1964
|
+
function getTsxCliPath(extensionDir = EXTENSION_DIR) {
|
|
1965
|
+
try {
|
|
1966
|
+
const requireFromExtension = createRequire(import.meta.url);
|
|
1967
|
+
const tsxMain = requireFromExtension.resolve("tsx");
|
|
1968
|
+
return join5(dirname4(tsxMain), "cli.mjs");
|
|
1969
|
+
} catch {
|
|
1970
|
+
return join5(extensionDir, "node_modules", "tsx", "dist", "cli.mjs");
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
function quoteWindowsArg(value) {
|
|
1974
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
1975
|
+
}
|
|
1976
|
+
function getWindowsHiddenLauncherPath(intercomDir = INTERCOM_DIR) {
|
|
1977
|
+
return join5(intercomDir, "broker-launch.vbs");
|
|
1978
|
+
}
|
|
1979
|
+
function usesDefaultBrokerCommand(brokerCommand, brokerArgs) {
|
|
1980
|
+
return brokerCommand === "npx" && brokerArgs.length === 2 && brokerArgs[0] === "--no-install" && brokerArgs[1] === "tsx";
|
|
1981
|
+
}
|
|
1982
|
+
function getWindowsBrokerCommandLine(brokerPath, extensionDir = EXTENSION_DIR, nodePath = process.execPath, brokerCommand = "npx", brokerArgs = ["--no-install", "tsx"]) {
|
|
1983
|
+
if (usesDefaultBrokerCommand(brokerCommand, brokerArgs)) {
|
|
1984
|
+
return [quoteWindowsArg(nodePath), quoteWindowsArg(getTsxCliPath(extensionDir)), quoteWindowsArg(brokerPath)].join(" ");
|
|
1985
|
+
}
|
|
1986
|
+
return [quoteWindowsArg(brokerCommand), ...brokerArgs.map(quoteWindowsArg), quoteWindowsArg(brokerPath)].join(" ");
|
|
1987
|
+
}
|
|
1988
|
+
function getWindowsHiddenLauncherScript(commandLine) {
|
|
1989
|
+
return [
|
|
1990
|
+
'Set WshShell = CreateObject("WScript.Shell")',
|
|
1991
|
+
`WshShell.Run "${commandLine.replace(/"/g, '""')}", 0, False`,
|
|
1992
|
+
"Set WshShell = Nothing",
|
|
1993
|
+
""
|
|
1994
|
+
].join("\r\n");
|
|
1995
|
+
}
|
|
1996
|
+
function isBrokerHealthOkMessage(message, requestId) {
|
|
1997
|
+
if (!hasExactDataKeys(
|
|
1998
|
+
message,
|
|
1999
|
+
["type", "requestId", "protocol", "version", "endpoint", "remoteAccess"],
|
|
2000
|
+
["capabilities"]
|
|
2001
|
+
)) return false;
|
|
2002
|
+
const response = message;
|
|
2003
|
+
if (response.type !== "health_ok" || response.requestId !== requestId || response.protocol !== INTERCOM_PROTOCOL_NAME || response.version !== INTERCOM_PROTOCOL_VERSION || response.endpoint !== "local") return false;
|
|
2004
|
+
const remoteAccess = response.remoteAccess;
|
|
2005
|
+
if (!hasExactDataKeys(remoteAccess, ["feature", "policySemanticsVersion", "policySemanticsHash"])) return false;
|
|
2006
|
+
const contract = remoteAccess;
|
|
2007
|
+
const legacyCompatible = contract.feature === "remote-access-v1" && contract.policySemanticsVersion === POLICY_SEMANTICS_VERSION2 && contract.policySemanticsHash === POLICY_SEMANTICS_HASH2;
|
|
2008
|
+
if (!legacyCompatible) return false;
|
|
2009
|
+
if (response.capabilities !== void 0) {
|
|
2010
|
+
try {
|
|
2011
|
+
assertCompatibleOrdinaryAdvertisement(response.capabilities);
|
|
2012
|
+
} catch {
|
|
2013
|
+
return false;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
return true;
|
|
2017
|
+
}
|
|
2018
|
+
function writeWindowsHiddenLauncher(commandLine, launcherPath = getWindowsHiddenLauncherPath()) {
|
|
2019
|
+
ensureIntercomRuntimeDir(dirname4(launcherPath));
|
|
2020
|
+
writeFileSync3(launcherPath, getWindowsHiddenLauncherScript(commandLine), {
|
|
2021
|
+
encoding: "utf-8",
|
|
2022
|
+
mode: INTERCOM_RUNTIME_FILE_MODE
|
|
2023
|
+
});
|
|
2024
|
+
restrictIntercomRuntimeFile(launcherPath);
|
|
2025
|
+
return launcherPath;
|
|
2026
|
+
}
|
|
2027
|
+
function getBrokerLaunchSpec(brokerPath, brokerCommand, brokerArgs, extensionDir = EXTENSION_DIR, platform = process.platform, intercomDir = INTERCOM_DIR, nodePath = process.execPath) {
|
|
2028
|
+
if (platform === "win32") {
|
|
2029
|
+
const launcherPath = getWindowsHiddenLauncherPath(intercomDir);
|
|
2030
|
+
return {
|
|
2031
|
+
kind: "windows-launcher",
|
|
2032
|
+
command: "wscript.exe",
|
|
2033
|
+
args: [launcherPath],
|
|
2034
|
+
launcherPath,
|
|
2035
|
+
launcherCommandLine: getWindowsBrokerCommandLine(brokerPath, extensionDir, nodePath, brokerCommand, brokerArgs)
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
if (usesDefaultBrokerCommand(brokerCommand, brokerArgs)) {
|
|
2039
|
+
return {
|
|
2040
|
+
kind: "direct",
|
|
2041
|
+
command: nodePath,
|
|
2042
|
+
args: [getTsxCliPath(extensionDir), brokerPath]
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
return {
|
|
2046
|
+
kind: "direct",
|
|
2047
|
+
command: brokerCommand,
|
|
2048
|
+
args: [...brokerArgs, brokerPath]
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
function getBrokerSpawnOptions(extensionDir = EXTENSION_DIR, env = process.env) {
|
|
2052
|
+
return {
|
|
2053
|
+
detached: true,
|
|
2054
|
+
stdio: "ignore",
|
|
2055
|
+
cwd: extensionDir,
|
|
2056
|
+
env: { ...env, PI_CODING_AGENT_DIR: getAgentDirPath(env), NODE_NO_WARNINGS: "1" },
|
|
2057
|
+
windowsHide: true
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
function toError2(error) {
|
|
2061
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
2062
|
+
}
|
|
2063
|
+
async function spawnBrokerIfNeeded(brokerCommand, brokerArgs) {
|
|
2064
|
+
ensureIntercomRuntimeDir(INTERCOM_DIR);
|
|
2065
|
+
if (await isBrokerRunning()) {
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
const ownsLock = acquireSpawnLock();
|
|
2069
|
+
if (!ownsLock) {
|
|
2070
|
+
await waitForBroker();
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
try {
|
|
2074
|
+
if (await isBrokerRunning()) {
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
if (await checkBrokerHealth() === "incompatible") {
|
|
2078
|
+
throw new Error(`Incompatible live intercom broker; expected ${INTERCOM_PROTOCOL_NAME} v${INTERCOM_PROTOCOL_VERSION}. Stop it explicitly during a coordinated migration.`);
|
|
2079
|
+
}
|
|
2080
|
+
const brokerPath = getBrokerEntryPath();
|
|
2081
|
+
const launch = getBrokerLaunchSpec(brokerPath, brokerCommand, brokerArgs);
|
|
2082
|
+
if (launch.kind === "windows-launcher") {
|
|
2083
|
+
writeWindowsHiddenLauncher(launch.launcherCommandLine, launch.launcherPath);
|
|
2084
|
+
}
|
|
2085
|
+
const child = spawn2(launch.command, launch.args, getBrokerSpawnOptions());
|
|
2086
|
+
child.unref();
|
|
2087
|
+
await new Promise((resolve4, reject) => {
|
|
2088
|
+
const cleanup = () => {
|
|
2089
|
+
child.off("error", onError);
|
|
2090
|
+
child.off("exit", onExit);
|
|
2091
|
+
};
|
|
2092
|
+
const onError = (error) => {
|
|
2093
|
+
cleanup();
|
|
2094
|
+
reject(new Error(`Failed to spawn intercom broker: ${error.message}`, { cause: error }));
|
|
2095
|
+
};
|
|
2096
|
+
const onExit = (code, signal) => {
|
|
2097
|
+
if (launch.kind === "windows-launcher" && code === 0 && signal === null) {
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
cleanup();
|
|
2101
|
+
if (signal) {
|
|
2102
|
+
reject(new Error(`Intercom broker exited before startup with signal ${signal}`));
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
reject(new Error(`Intercom broker exited before startup with code ${code ?? "unknown"}`));
|
|
2106
|
+
};
|
|
2107
|
+
child.once("error", onError);
|
|
2108
|
+
child.once("exit", onExit);
|
|
2109
|
+
waitForBroker().then(() => {
|
|
2110
|
+
cleanup();
|
|
2111
|
+
resolve4();
|
|
2112
|
+
}, (error) => {
|
|
2113
|
+
cleanup();
|
|
2114
|
+
reject(toError2(error));
|
|
2115
|
+
});
|
|
2116
|
+
});
|
|
2117
|
+
} finally {
|
|
2118
|
+
releaseSpawnLock();
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
async function isBrokerRunning() {
|
|
2122
|
+
if (await checkSocketConnectable()) {
|
|
2123
|
+
return true;
|
|
2124
|
+
}
|
|
2125
|
+
if (!existsSync3(BROKER_PID)) return false;
|
|
2126
|
+
try {
|
|
2127
|
+
const pid = parseInt(readFileSync5(BROKER_PID, "utf-8").trim(), 10);
|
|
2128
|
+
if (!Number.isFinite(pid)) return false;
|
|
2129
|
+
process.kill(pid, 0);
|
|
2130
|
+
return checkSocketConnectable();
|
|
2131
|
+
} catch {
|
|
2132
|
+
return false;
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
function connectToBrokerTarget2(target) {
|
|
2136
|
+
return typeof target === "string" ? net2.connect(target) : net2.connect({ host: target.host, port: target.port });
|
|
2137
|
+
}
|
|
2138
|
+
async function checkSocketConnectable() {
|
|
2139
|
+
return await checkBrokerHealth() === "compatible";
|
|
2140
|
+
}
|
|
2141
|
+
function checkBrokerHealth() {
|
|
2142
|
+
return new Promise((resolve4) => {
|
|
2143
|
+
let target;
|
|
2144
|
+
try {
|
|
2145
|
+
target = getBrokerConnectTarget();
|
|
2146
|
+
} catch {
|
|
2147
|
+
resolve4("unreachable");
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
const socket = connectToBrokerTarget2(target);
|
|
2151
|
+
const requestId = randomUUID3();
|
|
2152
|
+
const expectedStateId = typeof target === "string" ? void 0 : target.stateId;
|
|
2153
|
+
let settled = false;
|
|
2154
|
+
const finish = (health) => {
|
|
2155
|
+
if (settled) {
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
settled = true;
|
|
2159
|
+
clearTimeout(timeout);
|
|
2160
|
+
socket.off("connect", onConnect);
|
|
2161
|
+
socket.off("error", onError);
|
|
2162
|
+
socket.off("data", reader);
|
|
2163
|
+
socket.destroy();
|
|
2164
|
+
resolve4(health);
|
|
2165
|
+
};
|
|
2166
|
+
const onConnect = () => {
|
|
2167
|
+
try {
|
|
2168
|
+
writeMessage(socket, {
|
|
2169
|
+
type: "health",
|
|
2170
|
+
requestId,
|
|
2171
|
+
...expectedStateId ? { stateId: expectedStateId } : {}
|
|
2172
|
+
});
|
|
2173
|
+
} catch {
|
|
2174
|
+
finish("unreachable");
|
|
2175
|
+
}
|
|
2176
|
+
};
|
|
2177
|
+
const onError = () => finish("unreachable");
|
|
2178
|
+
const reader = createMessageReader((message) => {
|
|
2179
|
+
if (isBrokerHealthOkMessage(message, requestId)) {
|
|
2180
|
+
finish("compatible");
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
if (isPlainDataRecord(message) && message.type === "health_ok" && message.requestId === requestId) {
|
|
2184
|
+
finish("incompatible");
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
finish("unreachable");
|
|
2188
|
+
}, () => finish("unreachable"));
|
|
2189
|
+
socket.on("connect", onConnect);
|
|
2190
|
+
socket.on("error", onError);
|
|
2191
|
+
socket.on("data", reader);
|
|
2192
|
+
const timeout = setTimeout(() => finish("unreachable"), 1e3);
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
function acquireSpawnLock() {
|
|
2196
|
+
const maxRetries = 5;
|
|
2197
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
2198
|
+
try {
|
|
2199
|
+
writeFileSync3(BROKER_SPAWN_LOCK, `${process.pid}
|
|
2200
|
+
${Date.now()}
|
|
2201
|
+
`, {
|
|
2202
|
+
flag: "wx",
|
|
2203
|
+
mode: INTERCOM_RUNTIME_FILE_MODE
|
|
2204
|
+
});
|
|
2205
|
+
restrictIntercomRuntimeFile(BROKER_SPAWN_LOCK);
|
|
2206
|
+
return true;
|
|
2207
|
+
} catch (error) {
|
|
2208
|
+
if (!(error instanceof Error) || error.code !== "EEXIST") {
|
|
2209
|
+
throw error;
|
|
2210
|
+
}
|
|
2211
|
+
if (isSpawnLockStale()) {
|
|
2212
|
+
try {
|
|
2213
|
+
unlinkSync(BROKER_SPAWN_LOCK);
|
|
2214
|
+
} catch {
|
|
2215
|
+
}
|
|
2216
|
+
continue;
|
|
2217
|
+
}
|
|
2218
|
+
return false;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
return false;
|
|
2222
|
+
}
|
|
2223
|
+
function isSpawnLockStale() {
|
|
2224
|
+
if (!existsSync3(BROKER_SPAWN_LOCK)) {
|
|
2225
|
+
return false;
|
|
2226
|
+
}
|
|
2227
|
+
try {
|
|
2228
|
+
const [pidLine = "", createdAtLine = "0"] = readFileSync5(BROKER_SPAWN_LOCK, "utf-8").trim().split("\n");
|
|
2229
|
+
const pid = Number.parseInt(pidLine, 10);
|
|
2230
|
+
const createdAt = Number.parseInt(createdAtLine, 10);
|
|
2231
|
+
const ageMs = Date.now() - createdAt;
|
|
2232
|
+
if (Number.isFinite(pid)) {
|
|
2233
|
+
try {
|
|
2234
|
+
process.kill(pid, 0);
|
|
2235
|
+
} catch {
|
|
2236
|
+
return true;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
return !Number.isFinite(createdAt) || ageMs > 1e4;
|
|
2240
|
+
} catch {
|
|
2241
|
+
return true;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
function releaseSpawnLock() {
|
|
2245
|
+
try {
|
|
2246
|
+
unlinkSync(BROKER_SPAWN_LOCK);
|
|
2247
|
+
} catch {
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
async function waitForBroker(timeoutMs = 5e3) {
|
|
2251
|
+
const start = Date.now();
|
|
2252
|
+
while (Date.now() - start < timeoutMs) {
|
|
2253
|
+
if (await checkSocketConnectable()) {
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
await sleep(100);
|
|
2257
|
+
}
|
|
2258
|
+
throw new Error("Broker failed to start within timeout");
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
// config.ts
|
|
2262
|
+
import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
|
|
2263
|
+
import { join as join6, resolve as resolve3 } from "path";
|
|
2264
|
+
import { homedir as homedir2 } from "os";
|
|
2265
|
+
var DEFAULT_ASK_TIMEOUT_MS = 45 * 1e3;
|
|
2266
|
+
var MAX_ASK_TIMEOUT_MS = 120 * 1e3;
|
|
2267
|
+
function getConfigPath() {
|
|
2268
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR ? resolve3(process.env.PI_CODING_AGENT_DIR) : join6(homedir2(), ".pi", "agent");
|
|
2269
|
+
return join6(agentDir, "intercom", "config.json");
|
|
2270
|
+
}
|
|
2271
|
+
var defaults = {
|
|
2272
|
+
brokerCommand: "npx",
|
|
2273
|
+
brokerArgs: ["--no-install", "tsx"],
|
|
2274
|
+
confirmSend: false,
|
|
2275
|
+
enabled: true,
|
|
2276
|
+
replyHint: true,
|
|
2277
|
+
inboundForkHandlers: {
|
|
2278
|
+
enabled: true,
|
|
2279
|
+
when: "auto",
|
|
2280
|
+
notify: "summary",
|
|
2281
|
+
triggerParentOnSummary: "auto"
|
|
2282
|
+
}
|
|
2283
|
+
};
|
|
2284
|
+
function loadConfig() {
|
|
2285
|
+
const configPath = getConfigPath();
|
|
2286
|
+
if (!existsSync4(configPath)) {
|
|
2287
|
+
return { ...defaults };
|
|
2288
|
+
}
|
|
2289
|
+
try {
|
|
2290
|
+
const raw = readFileSync6(configPath, "utf-8");
|
|
2291
|
+
const parsed = JSON.parse(raw);
|
|
2292
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2293
|
+
throw new Error("Config must be a JSON object");
|
|
2294
|
+
}
|
|
2295
|
+
const parsedConfig = parsed;
|
|
2296
|
+
const config = { ...defaults };
|
|
2297
|
+
if (Object.hasOwn(parsedConfig, "brokerCommand")) {
|
|
2298
|
+
if (typeof parsedConfig.brokerCommand !== "string") {
|
|
2299
|
+
throw new Error(`"brokerCommand" must be a string`);
|
|
2300
|
+
}
|
|
2301
|
+
const brokerCommand = parsedConfig.brokerCommand.trim();
|
|
2302
|
+
if (!brokerCommand) {
|
|
2303
|
+
throw new Error(`"brokerCommand" must not be empty`);
|
|
2304
|
+
}
|
|
2305
|
+
config.brokerCommand = brokerCommand;
|
|
2306
|
+
}
|
|
2307
|
+
if (Object.hasOwn(parsedConfig, "brokerArgs")) {
|
|
2308
|
+
if (!Array.isArray(parsedConfig.brokerArgs)) {
|
|
2309
|
+
throw new Error(`"brokerArgs" must be an array`);
|
|
2310
|
+
}
|
|
2311
|
+
const brokerArgs = [];
|
|
2312
|
+
for (const arg of parsedConfig.brokerArgs) {
|
|
2313
|
+
if (typeof arg !== "string") {
|
|
2314
|
+
throw new Error(`"brokerArgs" items must be strings`);
|
|
2315
|
+
}
|
|
2316
|
+
brokerArgs.push(arg);
|
|
2317
|
+
}
|
|
2318
|
+
config.brokerArgs = brokerArgs;
|
|
2319
|
+
}
|
|
2320
|
+
if (Object.hasOwn(parsedConfig, "confirmSend")) {
|
|
2321
|
+
if (typeof parsedConfig.confirmSend !== "boolean") {
|
|
2322
|
+
throw new Error(`"confirmSend" must be a boolean`);
|
|
2323
|
+
}
|
|
2324
|
+
config.confirmSend = parsedConfig.confirmSend;
|
|
2325
|
+
}
|
|
2326
|
+
if (Object.hasOwn(parsedConfig, "enabled")) {
|
|
2327
|
+
if (typeof parsedConfig.enabled !== "boolean") {
|
|
2328
|
+
throw new Error(`"enabled" must be a boolean`);
|
|
2329
|
+
}
|
|
2330
|
+
config.enabled = parsedConfig.enabled;
|
|
2331
|
+
}
|
|
2332
|
+
if (Object.hasOwn(parsedConfig, "replyHint")) {
|
|
2333
|
+
if (typeof parsedConfig.replyHint !== "boolean") {
|
|
2334
|
+
throw new Error(`"replyHint" must be a boolean`);
|
|
2335
|
+
}
|
|
2336
|
+
config.replyHint = parsedConfig.replyHint;
|
|
2337
|
+
}
|
|
2338
|
+
if (Object.hasOwn(parsedConfig, "status")) {
|
|
2339
|
+
if (typeof parsedConfig.status !== "string") {
|
|
2340
|
+
throw new Error(`"status" must be a string`);
|
|
2341
|
+
}
|
|
2342
|
+
config.status = parsedConfig.status;
|
|
2343
|
+
}
|
|
2344
|
+
if (Object.hasOwn(parsedConfig, "inboundForkHandlers")) {
|
|
2345
|
+
if (typeof parsedConfig.inboundForkHandlers !== "object" || parsedConfig.inboundForkHandlers === null || Array.isArray(parsedConfig.inboundForkHandlers)) {
|
|
2346
|
+
throw new Error(`"inboundForkHandlers" must be an object`);
|
|
2347
|
+
}
|
|
2348
|
+
const forkConfig = parsedConfig.inboundForkHandlers;
|
|
2349
|
+
config.inboundForkHandlers = { ...defaults.inboundForkHandlers };
|
|
2350
|
+
if (Object.hasOwn(forkConfig, "enabled")) {
|
|
2351
|
+
if (typeof forkConfig.enabled !== "boolean") throw new Error(`"inboundForkHandlers.enabled" must be a boolean`);
|
|
2352
|
+
config.inboundForkHandlers.enabled = forkConfig.enabled;
|
|
2353
|
+
}
|
|
2354
|
+
if (Object.hasOwn(forkConfig, "when")) {
|
|
2355
|
+
if (forkConfig.when !== "auto" && forkConfig.when !== "busy" && forkConfig.when !== "always") throw new Error(`"inboundForkHandlers.when" must be "auto", "busy", or "always"`);
|
|
2356
|
+
config.inboundForkHandlers.when = forkConfig.when;
|
|
2357
|
+
}
|
|
2358
|
+
if (Object.hasOwn(forkConfig, "notify")) {
|
|
2359
|
+
if (forkConfig.notify !== "ack-and-summary" && forkConfig.notify !== "summary" && forkConfig.notify !== "none") throw new Error(`"inboundForkHandlers.notify" must be "ack-and-summary", "summary", or "none"`);
|
|
2360
|
+
config.inboundForkHandlers.notify = forkConfig.notify;
|
|
2361
|
+
}
|
|
2362
|
+
if (Object.hasOwn(forkConfig, "piCommand")) {
|
|
2363
|
+
if (typeof forkConfig.piCommand !== "string") throw new Error(`"inboundForkHandlers.piCommand" must be a string`);
|
|
2364
|
+
const piCommand = forkConfig.piCommand.trim();
|
|
2365
|
+
if (piCommand) config.inboundForkHandlers.piCommand = piCommand;
|
|
2366
|
+
}
|
|
2367
|
+
if (Object.hasOwn(forkConfig, "triggerParentOnSummary")) {
|
|
2368
|
+
const triggerParentOnSummary = forkConfig.triggerParentOnSummary;
|
|
2369
|
+
if (typeof triggerParentOnSummary !== "boolean" && triggerParentOnSummary !== "auto") {
|
|
2370
|
+
throw new Error(`"inboundForkHandlers.triggerParentOnSummary" must be a boolean or "auto"`);
|
|
2371
|
+
}
|
|
2372
|
+
config.inboundForkHandlers.triggerParentOnSummary = triggerParentOnSummary;
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
return config;
|
|
2376
|
+
} catch (error) {
|
|
2377
|
+
console.error(`Failed to load intercom config at ${configPath}:`, error);
|
|
2378
|
+
return { ...defaults };
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
// claude/team.ts
|
|
2383
|
+
import { PARTICIPANT_STATES, TERMINAL_PARTICIPANT_STATES } from "@ctliz/agent-intercom-core/boss";
|
|
2384
|
+
var BOSS_NONTERMINAL_STATES = new Set(
|
|
2385
|
+
PARTICIPANT_STATES.filter((state) => !TERMINAL_PARTICIPANT_STATES.includes(state))
|
|
2386
|
+
);
|
|
2387
|
+
|
|
2388
|
+
// claude/runtime.ts
|
|
2389
|
+
function formatAttachments(attachments) {
|
|
2390
|
+
if (!attachments?.length) return "";
|
|
2391
|
+
return attachments.map((attachment) => {
|
|
2392
|
+
if (attachment.language) {
|
|
2393
|
+
return `
|
|
2394
|
+
|
|
2395
|
+
---
|
|
2396
|
+
Attachment: ${attachment.name}
|
|
2397
|
+
~~~${attachment.language}
|
|
2398
|
+
${attachment.content}
|
|
2399
|
+
~~~`;
|
|
2400
|
+
}
|
|
2401
|
+
return `
|
|
2402
|
+
|
|
2403
|
+
---
|
|
2404
|
+
Attachment: ${attachment.name}
|
|
2405
|
+
${attachment.content}`;
|
|
2406
|
+
}).join("");
|
|
2407
|
+
}
|
|
2408
|
+
function formatSessionDisplay(session) {
|
|
2409
|
+
const name = session.name || session.id;
|
|
2410
|
+
return session.origin === "remote" ? `${name} [remote:${session.remoteHostId || "unknown-host"}]` : name;
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
// claude/contact.ts
|
|
2414
|
+
import { spawn as spawn3, spawnSync as spawnSync2 } from "node:child_process";
|
|
2415
|
+
function formatContactInstruction(current, sessions) {
|
|
2416
|
+
const name = current.name?.trim();
|
|
2417
|
+
const duplicate = Boolean(name && sessions.some((session) => session.id !== current.id && session.name?.trim().toLowerCase() === name.toLowerCase()));
|
|
2418
|
+
return name && !duplicate ? `Intercom target: ${name}
|
|
2419
|
+
Stable session ID: ${current.id}` : `Intercom target: ${current.id}`;
|
|
2420
|
+
}
|
|
2421
|
+
function copyTextToClipboard(text) {
|
|
2422
|
+
const candidates = process.platform === "darwin" ? [["pbcopy", []]] : process.platform === "win32" ? [["clip.exe", []]] : [
|
|
2423
|
+
...process.env.WAYLAND_DISPLAY ? [["wl-copy", []]] : [],
|
|
2424
|
+
...process.env.DISPLAY ? [["xclip", ["-selection", "clipboard"]], ["xsel", ["--clipboard", "--input"]]] : [],
|
|
2425
|
+
["clip.exe", []]
|
|
2426
|
+
];
|
|
2427
|
+
for (const [command, args] of candidates) {
|
|
2428
|
+
if (spawnSync2("which", [command], { stdio: "ignore" }).status !== 0) continue;
|
|
2429
|
+
if (command === "wl-copy") {
|
|
2430
|
+
const child = spawn3(command, args, { stdio: ["pipe", "ignore", "ignore"] });
|
|
2431
|
+
child.stdin.end(text);
|
|
2432
|
+
child.unref();
|
|
2433
|
+
return true;
|
|
2434
|
+
}
|
|
2435
|
+
if (spawnSync2(command, args, { input: text, stdio: ["pipe", "ignore", "ignore"] }).status === 0) return true;
|
|
2436
|
+
}
|
|
2437
|
+
return false;
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
// claude/worker-daemon.ts
|
|
2441
|
+
var MAX_WAKES_PER_MINUTE = 20;
|
|
2442
|
+
function formatWorkerActivity(activity) {
|
|
2443
|
+
const sender = formatSessionDisplay(activity.from);
|
|
2444
|
+
if (activity.type === "started") {
|
|
2445
|
+
return `
|
|
2446
|
+
[intercom] Wake from ${sender}: ${activity.message.content.text}
|
|
2447
|
+
[intercom] Claude is working\u2026
|
|
2448
|
+
`;
|
|
2449
|
+
}
|
|
2450
|
+
if (activity.type === "completed") {
|
|
2451
|
+
const result = activity.result || "(Claude finished without a final message)";
|
|
2452
|
+
return `[intercom] Completed wake from ${sender} (session ${activity.sessionId ?? "none"})
|
|
2453
|
+
${result}
|
|
2454
|
+
`;
|
|
2455
|
+
}
|
|
2456
|
+
return `[intercom] Wake from ${sender} failed: ${activity.error}
|
|
2457
|
+
`;
|
|
2458
|
+
}
|
|
2459
|
+
function reportToTerminal(activity) {
|
|
2460
|
+
process.stderr.write(formatWorkerActivity(activity));
|
|
2461
|
+
}
|
|
2462
|
+
function formatMessage(from, message, agent) {
|
|
2463
|
+
const replyInstruction = message.expectsReply ? [
|
|
2464
|
+
"",
|
|
2465
|
+
"",
|
|
2466
|
+
"The sender is blocking on your reply.",
|
|
2467
|
+
"Your FINAL assistant message will be sent back to them automatically as the reply.",
|
|
2468
|
+
"Do not try to use intercom tools to answer; just put your answer in your final message."
|
|
2469
|
+
].join("\n") : "";
|
|
2470
|
+
const attachments = formatAttachments(message.content.attachments);
|
|
2471
|
+
const custom = agent.instructions ? `
|
|
2472
|
+
|
|
2473
|
+
Agent instructions:
|
|
2474
|
+
${agent.instructions}` : "";
|
|
2475
|
+
return [
|
|
2476
|
+
`Intercom message for ${agent.name}.`,
|
|
2477
|
+
`From: ${formatSessionDisplay(from)} (${from.id})`,
|
|
2478
|
+
`Message id: ${message.id}`,
|
|
2479
|
+
"",
|
|
2480
|
+
message.content.text,
|
|
2481
|
+
attachments,
|
|
2482
|
+
custom,
|
|
2483
|
+
replyInstruction
|
|
2484
|
+
].join("\n");
|
|
2485
|
+
}
|
|
2486
|
+
var VirtualClaudeAgent = class {
|
|
2487
|
+
constructor(agent, state, statePath, claudeCommand, reportActivity = reportToTerminal, runTurn = runClaudeTurn, options = {}) {
|
|
2488
|
+
this.agent = agent;
|
|
2489
|
+
this.state = state;
|
|
2490
|
+
this.statePath = statePath;
|
|
2491
|
+
this.claudeCommand = claudeCommand;
|
|
2492
|
+
this.reportActivity = reportActivity;
|
|
2493
|
+
this.runTurn = runTurn;
|
|
2494
|
+
const scopeId = options.scopeId === void 0 ? intercomScopeIdFromEnvForRegistration(options.env ?? process.env) : parseIntercomScopeIdForRegistration(options.scopeId);
|
|
2495
|
+
const scopeEnv = scopeId === void 0 ? {} : { [INTERCOM_SCOPE_ENV]: scopeId };
|
|
2496
|
+
this.client = options.client ?? new IntercomClient({ env: scopeEnv });
|
|
2497
|
+
this.sessionId = agent.sessionId ?? state.agents[agent.id]?.sessionId ?? null;
|
|
2498
|
+
this.prepareConnection = options.prepareConnection ?? (async () => {
|
|
2499
|
+
const config = loadConfig();
|
|
2500
|
+
await spawnBrokerIfNeeded(config.brokerCommand, config.brokerArgs);
|
|
2501
|
+
});
|
|
2502
|
+
this.reconnectDelays = options.reconnectDelays?.length ? options.reconnectDelays : [250, 500, 1e3, 2e3, 5e3];
|
|
2503
|
+
}
|
|
2504
|
+
agent;
|
|
2505
|
+
state;
|
|
2506
|
+
statePath;
|
|
2507
|
+
claudeCommand;
|
|
2508
|
+
reportActivity;
|
|
2509
|
+
runTurn;
|
|
2510
|
+
client;
|
|
2511
|
+
sessionId;
|
|
2512
|
+
messageQueue = Promise.resolve();
|
|
2513
|
+
wakeTimestamps = [];
|
|
2514
|
+
connectPromise = null;
|
|
2515
|
+
reconnectTimer = null;
|
|
2516
|
+
reconnectAttempt = 0;
|
|
2517
|
+
reconnectEnabled = true;
|
|
2518
|
+
intercomStartedAt = Date.now();
|
|
2519
|
+
prepareConnection;
|
|
2520
|
+
reconnectDelays;
|
|
2521
|
+
get id() {
|
|
2522
|
+
return this.agent.id;
|
|
2523
|
+
}
|
|
2524
|
+
async copyContact() {
|
|
2525
|
+
const sessions = await this.client.listSessions();
|
|
2526
|
+
const current = sessions.find((session) => session.id === this.client.sessionId);
|
|
2527
|
+
const text = current ? formatContactInstruction(current, sessions) : `Intercom target: ${this.client.sessionId ?? this.agent.id}`;
|
|
2528
|
+
return copyTextToClipboard(text) ? `Copied ${text.replace(/\n/g, "; ")}` : `Clipboard unavailable. ${text.replace(/\n/g, "; ")}`;
|
|
2529
|
+
}
|
|
2530
|
+
async listPeers() {
|
|
2531
|
+
const sessions = await this.client.listSessions();
|
|
2532
|
+
return sessions.filter((session) => session.id !== this.client.sessionId);
|
|
2533
|
+
}
|
|
2534
|
+
async sendMessage(to, text) {
|
|
2535
|
+
const sessions = await this.client.listSessions();
|
|
2536
|
+
const normalized = to.toLowerCase();
|
|
2537
|
+
const matches = sessions.filter(
|
|
2538
|
+
(session) => session.id === to || session.id.startsWith(to) || session.name?.toLowerCase() === normalized
|
|
2539
|
+
);
|
|
2540
|
+
if (matches.length > 1) {
|
|
2541
|
+
throw new Error(`Multiple intercom sessions match "${to}"; choose a full ID.`);
|
|
2542
|
+
}
|
|
2543
|
+
const target = matches[0]?.id ?? to;
|
|
2544
|
+
const result = await this.client.send(target, { text });
|
|
2545
|
+
if (!result.delivered) {
|
|
2546
|
+
throw new Error(result.reason ?? `Intercom session "${to}" is unavailable.`);
|
|
2547
|
+
}
|
|
2548
|
+
return `Message sent to ${matches[0]?.name || to}.`;
|
|
2549
|
+
}
|
|
2550
|
+
async start() {
|
|
2551
|
+
this.reconnectEnabled = true;
|
|
2552
|
+
this.client.on("message", (from, message, deliveryId) => {
|
|
2553
|
+
this.routeMessage(from, message);
|
|
2554
|
+
this.client.acknowledgeMessage(deliveryId);
|
|
2555
|
+
});
|
|
2556
|
+
this.client.on("error", (error) => {
|
|
2557
|
+
process.stderr.write(`worker ${this.agent.id}: ${error.message}
|
|
2558
|
+
`);
|
|
2559
|
+
});
|
|
2560
|
+
this.client.on("disconnected", () => {
|
|
2561
|
+
this.scheduleReconnect();
|
|
2562
|
+
});
|
|
2563
|
+
await this.connectIntercom();
|
|
2564
|
+
}
|
|
2565
|
+
async connectIntercom() {
|
|
2566
|
+
this.clearReconnectTimer();
|
|
2567
|
+
if (this.client.isConnected()) return;
|
|
2568
|
+
if (this.connectPromise) return this.connectPromise;
|
|
2569
|
+
this.connectPromise = (async () => {
|
|
2570
|
+
await this.prepareConnection();
|
|
2571
|
+
await this.client.connect({
|
|
2572
|
+
name: this.agent.name,
|
|
2573
|
+
cwd: this.agent.cwd,
|
|
2574
|
+
model: this.agent.model ?? "claude",
|
|
2575
|
+
pid: process.pid,
|
|
2576
|
+
startedAt: this.intercomStartedAt,
|
|
2577
|
+
lastActivity: Date.now(),
|
|
2578
|
+
status: "idle"
|
|
2579
|
+
}, this.agent.id);
|
|
2580
|
+
this.reconnectAttempt = 0;
|
|
2581
|
+
})();
|
|
2582
|
+
try {
|
|
2583
|
+
await this.connectPromise;
|
|
2584
|
+
} finally {
|
|
2585
|
+
this.connectPromise = null;
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
scheduleReconnect() {
|
|
2589
|
+
if (!this.reconnectEnabled || this.reconnectTimer) return;
|
|
2590
|
+
const delay = this.reconnectDelays[Math.min(this.reconnectAttempt, this.reconnectDelays.length - 1)];
|
|
2591
|
+
this.reconnectTimer = setTimeout(() => {
|
|
2592
|
+
this.reconnectTimer = null;
|
|
2593
|
+
void this.connectIntercom().then(() => {
|
|
2594
|
+
if (!this.client.isConnected()) {
|
|
2595
|
+
this.reconnectAttempt += 1;
|
|
2596
|
+
this.scheduleReconnect();
|
|
2597
|
+
}
|
|
2598
|
+
}).catch((error) => {
|
|
2599
|
+
this.reconnectAttempt += 1;
|
|
2600
|
+
process.stderr.write(`worker ${this.agent.id}: reconnect failed: ${error instanceof Error ? error.message : String(error)}
|
|
2601
|
+
`);
|
|
2602
|
+
this.scheduleReconnect();
|
|
2603
|
+
});
|
|
2604
|
+
}, delay);
|
|
2605
|
+
this.reconnectTimer.unref?.();
|
|
2606
|
+
}
|
|
2607
|
+
clearReconnectTimer() {
|
|
2608
|
+
if (!this.reconnectTimer) return;
|
|
2609
|
+
clearTimeout(this.reconnectTimer);
|
|
2610
|
+
this.reconnectTimer = null;
|
|
2611
|
+
}
|
|
2612
|
+
async stop() {
|
|
2613
|
+
this.reconnectEnabled = false;
|
|
2614
|
+
this.clearReconnectTimer();
|
|
2615
|
+
if (this.connectPromise) {
|
|
2616
|
+
try {
|
|
2617
|
+
await this.connectPromise;
|
|
2618
|
+
} catch {
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
await this.client.disconnect();
|
|
2622
|
+
}
|
|
2623
|
+
routeMessage(from, message) {
|
|
2624
|
+
if (this.isRateLimited()) {
|
|
2625
|
+
process.stderr.write(`worker ${this.agent.id}: rate limited, dropping message from ${from.id}
|
|
2626
|
+
`);
|
|
2627
|
+
if (message.expectsReply) {
|
|
2628
|
+
void this.client.send(from.id, {
|
|
2629
|
+
text: "This worker is rate limited (too many wakes this minute). Please try again shortly.",
|
|
2630
|
+
replyTo: message.id
|
|
2631
|
+
}).catch((error) => {
|
|
2632
|
+
process.stderr.write(`worker ${this.agent.id}: rate-limit reply failed: ${error instanceof Error ? error.message : String(error)}
|
|
2633
|
+
`);
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
2636
|
+
return;
|
|
2637
|
+
}
|
|
2638
|
+
this.messageQueue = this.messageQueue.catch(() => void 0).then(() => this.handleMessage(from, message));
|
|
2639
|
+
}
|
|
2640
|
+
isRateLimited() {
|
|
2641
|
+
const now = Date.now();
|
|
2642
|
+
this.wakeTimestamps = this.wakeTimestamps.filter((timestamp) => now - timestamp < 6e4);
|
|
2643
|
+
if (this.wakeTimestamps.length >= MAX_WAKES_PER_MINUTE) return true;
|
|
2644
|
+
this.wakeTimestamps.push(now);
|
|
2645
|
+
return false;
|
|
2646
|
+
}
|
|
2647
|
+
async handleMessage(from, message) {
|
|
2648
|
+
try {
|
|
2649
|
+
const prompt = formatMessage(from, message, this.agent);
|
|
2650
|
+
this.client.updatePresence({ status: "active" });
|
|
2651
|
+
this.reportActivity({ type: "started", agent: this.agent, from, message });
|
|
2652
|
+
const result = await this.runTurn({
|
|
2653
|
+
prompt,
|
|
2654
|
+
cwd: this.agent.cwd,
|
|
2655
|
+
sessionId: this.sessionId ?? void 0,
|
|
2656
|
+
model: this.agent.model,
|
|
2657
|
+
appendSystemPrompt: this.agent.instructions,
|
|
2658
|
+
// Historical worker configs omitted both permission fields. Preserve
|
|
2659
|
+
// their noninteractive behavior; hardened roles are normalized to an
|
|
2660
|
+
// explicit plan mode before a VirtualClaudeAgent can be constructed.
|
|
2661
|
+
permissionMode: this.agent.permissionMode ?? "bypassPermissions",
|
|
2662
|
+
dangerouslySkipPermissions: this.agent.dangerouslySkipPermissions,
|
|
2663
|
+
permissionCeiling: this.agent.permissionCeiling,
|
|
2664
|
+
addDirs: this.agent.addDirs,
|
|
2665
|
+
mcpConfig: this.agent.mcpConfig,
|
|
2666
|
+
claudeCommand: this.claudeCommand,
|
|
2667
|
+
extraArgs: this.agent.claudeArgs
|
|
2668
|
+
});
|
|
2669
|
+
if (result.sessionId && result.sessionId !== this.sessionId) {
|
|
2670
|
+
this.sessionId = result.sessionId;
|
|
2671
|
+
this.state.agents[this.agent.id] = { sessionId: result.sessionId, updatedAt: Date.now() };
|
|
2672
|
+
saveWorkerState(this.statePath, this.state);
|
|
2673
|
+
}
|
|
2674
|
+
if (result.isError) {
|
|
2675
|
+
const text = result.result || "Claude exited with an error and no message";
|
|
2676
|
+
this.client.updatePresence({ status: `error: ${text}` });
|
|
2677
|
+
this.reportActivity({ type: "error", agent: this.agent, from, message, error: text });
|
|
2678
|
+
} else {
|
|
2679
|
+
this.client.updatePresence({ status: "idle" });
|
|
2680
|
+
this.reportActivity({
|
|
2681
|
+
type: "completed",
|
|
2682
|
+
agent: this.agent,
|
|
2683
|
+
from,
|
|
2684
|
+
message,
|
|
2685
|
+
result: result.result,
|
|
2686
|
+
sessionId: this.sessionId
|
|
2687
|
+
});
|
|
2688
|
+
}
|
|
2689
|
+
if (message.expectsReply) {
|
|
2690
|
+
await this.client.send(from.id, {
|
|
2691
|
+
text: result.result || "(the worker finished without a final message)",
|
|
2692
|
+
replyTo: message.id
|
|
2693
|
+
});
|
|
2694
|
+
process.stderr.write(`worker ${this.agent.id}: replied to ${from.name || from.id} (session ${this.sessionId ?? "none"})
|
|
2695
|
+
`);
|
|
2696
|
+
}
|
|
2697
|
+
} catch (error) {
|
|
2698
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
2699
|
+
this.client.updatePresence({ status: `error: ${text}` });
|
|
2700
|
+
this.reportActivity({ type: "error", agent: this.agent, from, message, error: text });
|
|
2701
|
+
if (message.expectsReply) {
|
|
2702
|
+
await this.client.send(from.id, {
|
|
2703
|
+
text: `Worker error: ${text}`,
|
|
2704
|
+
replyTo: message.id
|
|
2705
|
+
}).catch((sendError) => {
|
|
2706
|
+
process.stderr.write(`worker ${this.agent.id}: error reply failed: ${sendError instanceof Error ? sendError.message : String(sendError)}
|
|
2707
|
+
`);
|
|
2708
|
+
});
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
};
|
|
2713
|
+
var ClaudeWorkerDaemon = class {
|
|
2714
|
+
constructor(config, reportActivity = reportToTerminal, options = {}) {
|
|
2715
|
+
this.config = config;
|
|
2716
|
+
this.reportActivity = reportActivity;
|
|
2717
|
+
this.scopeId = options.scopeId === void 0 ? intercomScopeIdFromEnvForRegistration(options.env ?? process.env) : parseIntercomScopeIdForRegistration(options.scopeId);
|
|
2718
|
+
}
|
|
2719
|
+
config;
|
|
2720
|
+
reportActivity;
|
|
2721
|
+
agents = [];
|
|
2722
|
+
scopeId;
|
|
2723
|
+
async start() {
|
|
2724
|
+
const intercomConfig = loadConfig();
|
|
2725
|
+
await spawnBrokerIfNeeded(intercomConfig.brokerCommand, intercomConfig.brokerArgs);
|
|
2726
|
+
const state = loadWorkerState(this.config.statePath);
|
|
2727
|
+
const claudeCommand = this.config.claudeCommand ?? "claude";
|
|
2728
|
+
this.agents = this.config.agents.map((agent) => {
|
|
2729
|
+
const transport = resolveClaudeIntercomTransport({
|
|
2730
|
+
requested: agent.transport,
|
|
2731
|
+
claudeCommand
|
|
2732
|
+
});
|
|
2733
|
+
process.stderr.write(`worker ${agent.id}: ${transport.selected} transport (${transport.reason})
|
|
2734
|
+
`);
|
|
2735
|
+
return new VirtualClaudeAgent(
|
|
2736
|
+
{ ...agent, transport: transport.selected },
|
|
2737
|
+
state,
|
|
2738
|
+
this.config.statePath,
|
|
2739
|
+
claudeCommand,
|
|
2740
|
+
this.reportActivity,
|
|
2741
|
+
runClaudeTurn,
|
|
2742
|
+
{ scopeId: this.scopeId }
|
|
2743
|
+
);
|
|
2744
|
+
});
|
|
2745
|
+
for (const agent of this.agents) await agent.start();
|
|
2746
|
+
process.stderr.write(`claude-intercom worker running ${this.agents.length} agent(s)
|
|
2747
|
+
`);
|
|
2748
|
+
}
|
|
2749
|
+
async stop() {
|
|
2750
|
+
for (const agent of this.agents) await agent.stop().catch(() => void 0);
|
|
2751
|
+
}
|
|
2752
|
+
async copyPrimaryContact() {
|
|
2753
|
+
if (!this.agents[0]) throw new Error("No Claude intercom agent is running");
|
|
2754
|
+
return this.agents[0].copyContact();
|
|
2755
|
+
}
|
|
2756
|
+
async listPrimaryPeers() {
|
|
2757
|
+
if (!this.agents[0]) throw new Error("No Claude intercom agent is running");
|
|
2758
|
+
return this.agents[0].listPeers();
|
|
2759
|
+
}
|
|
2760
|
+
async sendFromPrimary(to, text) {
|
|
2761
|
+
if (!this.agents[0]) throw new Error("No Claude intercom agent is running");
|
|
2762
|
+
return this.agents[0].sendMessage(to, text);
|
|
2763
|
+
}
|
|
2764
|
+
};
|
|
2765
|
+
async function main() {
|
|
2766
|
+
intercomScopeIdFromEnvForRegistration(process.env);
|
|
2767
|
+
const configPath = process.argv.includes("--config") ? process.argv[process.argv.indexOf("--config") + 1] : void 0;
|
|
2768
|
+
const config = loadWorkerConfig(configPath);
|
|
2769
|
+
if (!config.agents.length) throw new Error("Worker config must include at least one agent");
|
|
2770
|
+
const daemon = new ClaudeWorkerDaemon(config);
|
|
2771
|
+
const stop = () => {
|
|
2772
|
+
void daemon.stop().finally(() => process.exit(0));
|
|
2773
|
+
};
|
|
2774
|
+
process.on("SIGINT", stop);
|
|
2775
|
+
process.on("SIGTERM", stop);
|
|
2776
|
+
await daemon.start();
|
|
2777
|
+
await once(process, "SIGTERM");
|
|
2778
|
+
}
|
|
2779
|
+
function invokedFileBasename() {
|
|
2780
|
+
try {
|
|
2781
|
+
return process.argv[1] ? basename(realpathSync2(process.argv[1])) : "";
|
|
2782
|
+
} catch {
|
|
2783
|
+
return process.argv[1] ? basename(process.argv[1]) : "";
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
if (invokedFileBasename() === "worker-daemon.ts" || invokedFileBasename() === "worker-daemon.mjs") {
|
|
2787
|
+
void main().catch((error) => {
|
|
2788
|
+
process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}
|
|
2789
|
+
`);
|
|
2790
|
+
process.exit(1);
|
|
2791
|
+
});
|
|
2792
|
+
}
|
|
2793
|
+
export {
|
|
2794
|
+
ClaudeWorkerDaemon,
|
|
2795
|
+
VirtualClaudeAgent,
|
|
2796
|
+
formatWorkerActivity
|
|
2797
|
+
};
|