@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.
Files changed (59) hide show
  1. package/.claude-plugin/plugin.json +26 -0
  2. package/.mcp.json +10 -0
  3. package/CHANGELOG.md +62 -0
  4. package/COPYRIGHT +6 -0
  5. package/LICENSE +661 -0
  6. package/LICENSE_TRANSITION.md +25 -0
  7. package/README.md +506 -0
  8. package/THIRD_PARTY_NOTICES.md +25 -0
  9. package/broker/access-credential.ts +76 -0
  10. package/broker/access-registry.ts +460 -0
  11. package/broker/audit.ts +60 -0
  12. package/broker/authorization.ts +125 -0
  13. package/broker/boss-contracts.ts +267 -0
  14. package/broker/broker.ts +2028 -0
  15. package/broker/client.ts +1048 -0
  16. package/broker/framing.ts +87 -0
  17. package/broker/ownership.ts +61 -0
  18. package/broker/paths.ts +161 -0
  19. package/broker/spawn.ts +463 -0
  20. package/broker/validation.ts +132 -0
  21. package/claude/cci.ts +512 -0
  22. package/claude/ccim.ts +31 -0
  23. package/claude/cli-runner.ts +220 -0
  24. package/claude/contact.ts +33 -0
  25. package/claude/inbox-monitor.ts +49 -0
  26. package/claude/inbox.ts +77 -0
  27. package/claude/mcp-protocol.ts +253 -0
  28. package/claude/native-bridge.ts +219 -0
  29. package/claude/native-protocol.ts +346 -0
  30. package/claude/permission-policy.ts +287 -0
  31. package/claude/runtime.ts +510 -0
  32. package/claude/server.ts +69 -0
  33. package/claude/team.ts +348 -0
  34. package/claude/transport.ts +97 -0
  35. package/claude/worker-config.ts +198 -0
  36. package/claude/worker-daemon.ts +418 -0
  37. package/commands/intercom-id.md +9 -0
  38. package/commands/intercom.md +8 -0
  39. package/config.ts +195 -0
  40. package/dist/broker.mjs +2530 -0
  41. package/dist/cci.mjs +3607 -0
  42. package/dist/ccim.mjs +3621 -0
  43. package/dist/claude-server.mjs +2730 -0
  44. package/dist/inbox-monitor.mjs +68 -0
  45. package/dist/worker-daemon.mjs +2797 -0
  46. package/durable-json.ts +25 -0
  47. package/licenses/MIT-pi-claude-link.txt +21 -0
  48. package/licenses/MIT-pi-intercom.txt +21 -0
  49. package/monitors/monitors.json +8 -0
  50. package/outbound-outbox.ts +116 -0
  51. package/package.json +93 -0
  52. package/protocol-v4/contract.ts +45 -0
  53. package/provider/protected-service.ts +162 -0
  54. package/provider/provider.mjs +34 -0
  55. package/scripts/build.mjs +71 -0
  56. package/scripts/verify-core-provenance.mjs +64 -0
  57. package/scripts/verify-packed-runtime.mjs +140 -0
  58. package/skills/claude-intercom/SKILL.md +90 -0
  59. package/types.ts +212 -0
package/claude/team.ts ADDED
@@ -0,0 +1,348 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { PARTICIPANT_STATES, TERMINAL_PARTICIPANT_STATES } from "@ctliz/agent-intercom-core/boss";
4
+ import type { BossSessionMetadata } from "../types.ts";
5
+ import { getAgentDirPath } from "../broker/paths.ts";
6
+
7
+ export interface TeamSession {
8
+ id: string;
9
+ name?: string;
10
+ /** Already parsed and broker-authenticated by the client session decoder. */
11
+ boss?: Pick<BossSessionMetadata, "binding" | "principal" | "liveWorker">;
12
+ }
13
+
14
+ interface StoredWorker {
15
+ id?: unknown;
16
+ runId?: unknown;
17
+ workerIncarnationId?: unknown;
18
+ workerGeneration?: unknown;
19
+ bossRunId?: unknown;
20
+ participantId?: unknown;
21
+ bindingEpoch?: unknown;
22
+ sessionId?: unknown;
23
+ harness?: unknown;
24
+ role?: unknown;
25
+ state?: unknown;
26
+ owned?: unknown;
27
+ managerSessionId?: unknown;
28
+ intercomTarget?: unknown;
29
+ }
30
+
31
+ export interface TeamMember {
32
+ id: string;
33
+ target: string;
34
+ harness?: string;
35
+ role?: string;
36
+ state?: string;
37
+ connected: boolean;
38
+ }
39
+
40
+ export interface IntercomTeam {
41
+ teamId?: string;
42
+ self: { id: string; workerId?: string; isManager: boolean };
43
+ manager?: { target: string; connected: boolean };
44
+ coworkers: TeamMember[];
45
+ }
46
+
47
+ const LEGACY_LIVE_STATES = new Set(["provisioning", "running", "idle", "needs_attention", "stopping"]);
48
+ const BOSS_NONTERMINAL_STATES: ReadonlySet<string> = new Set(
49
+ PARTICIPANT_STATES.filter((state) => !(TERMINAL_PARTICIPANT_STATES as readonly string[]).includes(state)),
50
+ );
51
+
52
+ function stringValue(value: unknown): string | undefined {
53
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
54
+ }
55
+
56
+ function exactStringValue(value: unknown): string | undefined {
57
+ return typeof value === "string" && value.length > 0 ? value : undefined;
58
+ }
59
+
60
+ function positiveInteger(value: unknown): number | undefined {
61
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
62
+ }
63
+
64
+ function positiveIntegerString(value: unknown): number | undefined {
65
+ if (typeof value !== "string" || !/^[1-9]\d*$/.test(value)) return undefined;
66
+ const parsed = Number(value);
67
+ return Number.isSafeInteger(parsed) ? parsed : undefined;
68
+ }
69
+
70
+ function connectedTo(sessions: TeamSession[], target: string): boolean {
71
+ const normalized = target.toLowerCase();
72
+ return sessions.some((session) => session.id === target || session.name?.toLowerCase() === normalized);
73
+ }
74
+
75
+ async function readWorkers(agentDir: string): Promise<StoredWorker[]> {
76
+ try {
77
+ const parsed = JSON.parse(await readFile(join(agentDir, "intercom", "orchestrator", "workers.json"), "utf8")) as { workers?: unknown };
78
+ return Array.isArray(parsed.workers) ? parsed.workers as StoredWorker[] : [];
79
+ } catch {
80
+ return [];
81
+ }
82
+ }
83
+
84
+ function bossSessionHasExactAuthority(session: TeamSession): boolean {
85
+ const binding = session.boss?.binding;
86
+ const principal = session.boss?.principal;
87
+ const liveWorker = session.boss?.liveWorker;
88
+ const liveIdentity = liveWorker?.identity;
89
+ return binding !== undefined
90
+ && principal !== undefined
91
+ && liveWorker !== undefined
92
+ && liveIdentity !== undefined
93
+ && binding.sessionId === session.id
94
+ && binding.state === "active"
95
+ && principal.principalClass === "boss-private"
96
+ && principal.principalId === session.id
97
+ && principal.state === "active"
98
+ && principal.bossRunId === binding.bossRunId
99
+ && principal.participantId === binding.participantId
100
+ && principal.role === binding.role
101
+ && principal.bindingEpoch === binding.bindingEpoch
102
+ && principal.assignedManagerParticipantId === binding.assignedManagerParticipantId
103
+ && ((binding.role === "worker" || binding.role === "scout") === (binding.assignedManagerParticipantId !== undefined))
104
+ && ((principal.role === "manager") === Array.isArray(principal.assignedParticipantIds))
105
+ && (principal.role === "manager" || principal.assignedParticipantIds === undefined)
106
+ && liveIdentity.workerId === session.id
107
+ && "bossRunId" in liveIdentity
108
+ && liveIdentity.bossRunId === binding.bossRunId
109
+ && liveIdentity.participantId === binding.participantId
110
+ && liveIdentity.bindingEpoch === binding.bindingEpoch
111
+ && BOSS_NONTERMINAL_STATES.has(liveWorker.state);
112
+ }
113
+
114
+ function bossWorkerMatchesSession(worker: StoredWorker, session: TeamSession): boolean {
115
+ const binding = session.boss?.binding;
116
+ const liveWorker = session.boss?.liveWorker;
117
+ const liveIdentity = liveWorker?.identity;
118
+ const id = exactStringValue(worker.id);
119
+ const target = exactStringValue(worker.intercomTarget);
120
+ const storedSessionId = exactStringValue(worker.sessionId) ?? target;
121
+ return bossSessionHasExactAuthority(session)
122
+ && binding !== undefined
123
+ && liveWorker !== undefined
124
+ && liveIdentity !== undefined
125
+ && worker.owned === true
126
+ && id !== undefined
127
+ && target === id
128
+ && storedSessionId === id
129
+ && session.id === id
130
+ && binding.sessionId === session.id
131
+ && binding.state === "active"
132
+ && BOSS_NONTERMINAL_STATES.has(liveWorker.state)
133
+ && exactStringValue(worker.state) === liveWorker.state
134
+ && exactStringValue(worker.bossRunId) === binding.bossRunId
135
+ && exactStringValue(worker.participantId) === binding.participantId
136
+ && positiveInteger(worker.bindingEpoch) === binding.bindingEpoch
137
+ && exactStringValue(worker.role) === binding.role
138
+ && liveIdentity.workerId === id
139
+ && liveIdentity.workerId === session.id
140
+ && liveIdentity.workerIncarnationId === exactStringValue(worker.workerIncarnationId)
141
+ && liveIdentity.workerGeneration === positiveInteger(worker.workerGeneration)
142
+ && liveIdentity.bossRunId === binding.bossRunId
143
+ && liveIdentity.participantId === binding.participantId
144
+ && liveIdentity.bindingEpoch === binding.bindingEpoch;
145
+ }
146
+
147
+ function isReciprocallyAssignedToManager(workerSession: TeamSession, managerSession: TeamSession): boolean {
148
+ const workerBinding = workerSession.boss?.binding;
149
+ const workerPrincipal = workerSession.boss?.principal;
150
+ const managerBinding = managerSession.boss?.binding;
151
+ const managerPrincipal = managerSession.boss?.principal;
152
+ return bossSessionHasExactAuthority(workerSession)
153
+ && bossSessionHasExactAuthority(managerSession)
154
+ && (workerBinding?.role === "worker" || workerBinding?.role === "scout")
155
+ && workerPrincipal?.role === workerBinding.role
156
+ && managerBinding?.role === "manager"
157
+ && managerPrincipal?.role === "manager"
158
+ && workerBinding.bossRunId === managerBinding.bossRunId
159
+ && workerBinding.assignedManagerParticipantId === managerBinding.participantId
160
+ && workerPrincipal.assignedManagerParticipantId === managerPrincipal.participantId
161
+ && managerPrincipal.assignedParticipantIds?.includes(workerBinding.participantId) === true;
162
+ }
163
+
164
+ function currentBossWorker(
165
+ workers: StoredWorker[],
166
+ self: TeamSession,
167
+ env: NodeJS.ProcessEnv,
168
+ ): StoredWorker | undefined {
169
+ const binding = self.boss?.binding;
170
+ const liveIdentity = self.boss?.liveWorker.identity;
171
+ if (!binding) return undefined;
172
+ const workerId = exactStringValue(env.AGENT_INTERCOM_WORKER_ID);
173
+ const incarnationId = exactStringValue(env.AGENT_INTERCOM_WORKER_INCARNATION_ID);
174
+ const deprecatedIncarnationId = exactStringValue(env.AGENT_INTERCOM_RUN_ID);
175
+ const workerGeneration = positiveIntegerString(env.AGENT_INTERCOM_WORKER_GENERATION);
176
+ const bindingEpoch = positiveIntegerString(env.AGENT_INTERCOM_BINDING_EPOCH);
177
+ if (
178
+ workerId === undefined
179
+ || incarnationId === undefined
180
+ || (deprecatedIncarnationId !== undefined && deprecatedIncarnationId !== incarnationId)
181
+ || workerGeneration === undefined
182
+ || bindingEpoch === undefined
183
+ || liveIdentity === undefined
184
+ || liveIdentity.workerId !== workerId
185
+ || liveIdentity.workerIncarnationId !== incarnationId
186
+ || liveIdentity.workerGeneration !== workerGeneration
187
+ || liveIdentity.bossRunId !== binding.bossRunId
188
+ || liveIdentity.participantId !== binding.participantId
189
+ || liveIdentity.bindingEpoch !== binding.bindingEpoch
190
+ || exactStringValue(env.AGENT_INTERCOM_BOSS_RUN_ID) !== binding.bossRunId
191
+ || exactStringValue(env.AGENT_INTERCOM_PARTICIPANT_ID) !== binding.participantId
192
+ || bindingEpoch !== binding.bindingEpoch
193
+ ) return undefined;
194
+
195
+ return workers.find((worker) =>
196
+ exactStringValue(worker.id) === workerId
197
+ && exactStringValue(worker.workerIncarnationId) === incarnationId
198
+ && positiveInteger(worker.workerGeneration) === workerGeneration
199
+ && bossWorkerMatchesSession(worker, self)
200
+ );
201
+ }
202
+
203
+ function teamMember(worker: StoredWorker, session: TeamSession): TeamMember {
204
+ const id = stringValue(worker.id)!;
205
+ return {
206
+ id,
207
+ target: id,
208
+ ...(stringValue(worker.harness) ? { harness: stringValue(worker.harness) } : {}),
209
+ ...(stringValue(worker.role) ? { role: stringValue(worker.role) } : {}),
210
+ ...(stringValue(worker.state) ? { state: stringValue(worker.state) } : {}),
211
+ connected: session.id === id,
212
+ };
213
+ }
214
+
215
+ function resolveLegacyTeam(
216
+ input: { selfId: string; sessions: TeamSession[] },
217
+ workers: StoredWorker[],
218
+ env: NodeJS.ProcessEnv,
219
+ ): IntercomTeam {
220
+ const workerId = stringValue(env.AGENT_INTERCOM_WORKER_ID);
221
+ const runId = stringValue(env.AGENT_INTERCOM_RUN_ID);
222
+ const current = workerId
223
+ ? workers.find((worker) =>
224
+ stringValue(worker.id) === workerId
225
+ && (!runId || stringValue(worker.runId) === runId)
226
+ )
227
+ : undefined;
228
+ const managerTarget = stringValue(current?.managerSessionId)
229
+ ?? stringValue(env.AGENT_INTERCOM_MANAGER_TARGET)
230
+ ?? stringValue(env.AGENT_INTERCOM_MANAGER_SESSION_ID);
231
+ const teamId = managerTarget ?? input.selfId;
232
+ const coworkers = workers
233
+ .filter((worker) => worker.owned === true)
234
+ .filter((worker) => stringValue(worker.managerSessionId) === teamId)
235
+ .filter((worker) => LEGACY_LIVE_STATES.has(stringValue(worker.state) ?? ""))
236
+ .filter((worker) => stringValue(worker.id) !== workerId)
237
+ .map((worker): TeamMember | undefined => {
238
+ const id = stringValue(worker.id);
239
+ if (!id) return undefined;
240
+ const target = stringValue(worker.intercomTarget) ?? id;
241
+ return {
242
+ id,
243
+ target,
244
+ ...(stringValue(worker.harness) ? { harness: stringValue(worker.harness) } : {}),
245
+ ...(stringValue(worker.role) ? { role: stringValue(worker.role) } : {}),
246
+ ...(stringValue(worker.state) ? { state: stringValue(worker.state) } : {}),
247
+ connected: connectedTo(input.sessions, target),
248
+ };
249
+ })
250
+ .filter((member): member is TeamMember => Boolean(member));
251
+ return {
252
+ teamId,
253
+ self: { id: input.selfId, ...(workerId ? { workerId } : {}), isManager: !managerTarget },
254
+ manager: managerTarget
255
+ ? { target: managerTarget, connected: connectedTo(input.sessions, managerTarget) }
256
+ : { target: input.selfId, connected: true },
257
+ coworkers,
258
+ };
259
+ }
260
+
261
+ export async function resolveIntercomTeam(input: {
262
+ selfId: string;
263
+ sessions: TeamSession[];
264
+ env?: NodeJS.ProcessEnv;
265
+ agentDir?: string;
266
+ }): Promise<IntercomTeam> {
267
+ const env = input.env ?? process.env;
268
+ const workers = await readWorkers(input.agentDir ?? getAgentDirPath());
269
+ const selfSession = input.sessions.find((session) => session.id === input.selfId);
270
+ if (!selfSession?.boss) return resolveLegacyTeam(input, workers, env);
271
+
272
+ const current = currentBossWorker(workers, selfSession, env);
273
+ const workerId = stringValue(env.AGENT_INTERCOM_WORKER_ID);
274
+ if (!current) {
275
+ return {
276
+ teamId: selfSession.boss.binding.bossRunId,
277
+ self: { id: input.selfId, ...(workerId ? { workerId } : {}), isManager: false },
278
+ coworkers: [],
279
+ };
280
+ }
281
+
282
+ const currentRole = selfSession.boss.binding.role;
283
+ const selfIsManager = currentRole === "manager";
284
+ const configuredManagerTarget = selfIsManager ? input.selfId : exactStringValue(current.managerSessionId);
285
+ const managerSession = configuredManagerTarget === undefined
286
+ ? undefined
287
+ : input.sessions.find((session) => session.id === configuredManagerTarget);
288
+ const managerWorker = managerSession === undefined
289
+ ? undefined
290
+ : workers.find((worker) => exactStringValue(worker.id) === configuredManagerTarget && bossWorkerMatchesSession(worker, managerSession));
291
+ const managerTarget = selfIsManager
292
+ ? input.selfId
293
+ : managerWorker !== undefined
294
+ && managerSession?.boss?.binding.bossRunId === selfSession.boss.binding.bossRunId
295
+ && isReciprocallyAssignedToManager(selfSession, managerSession)
296
+ ? configuredManagerTarget
297
+ : undefined;
298
+
299
+ if (managerTarget === undefined) {
300
+ return {
301
+ teamId: selfSession.boss.binding.bossRunId,
302
+ self: { id: input.selfId, ...(workerId ? { workerId } : {}), isManager: false },
303
+ coworkers: [],
304
+ };
305
+ }
306
+
307
+ const coworkers = workers
308
+ .filter((worker) => exactStringValue(worker.id) !== workerId)
309
+ .filter((worker) => exactStringValue(worker.managerSessionId) === managerTarget)
310
+ .map((worker): TeamMember | undefined => {
311
+ const id = exactStringValue(worker.id);
312
+ if (!id) return undefined;
313
+ const session = input.sessions.find((candidate) => candidate.id === id);
314
+ if (
315
+ session === undefined
316
+ || !bossWorkerMatchesSession(worker, session)
317
+ || session.boss?.binding.bossRunId !== selfSession.boss!.binding.bossRunId
318
+ || managerSession === undefined
319
+ || !isReciprocallyAssignedToManager(session, managerSession)
320
+ ) return undefined;
321
+ return teamMember(worker, session);
322
+ })
323
+ .filter((member): member is TeamMember => Boolean(member));
324
+
325
+ return {
326
+ teamId: selfSession.boss.binding.bossRunId,
327
+ self: { id: input.selfId, ...(workerId ? { workerId } : {}), isManager: selfIsManager },
328
+ manager: { target: managerTarget, connected: true },
329
+ coworkers,
330
+ };
331
+ }
332
+
333
+ export function formatIntercomTeam(team: IntercomTeam): string {
334
+ const lines = [
335
+ `Manager: ${team.manager ? `${team.manager.target} [${team.manager.connected ? "connected" : "not connected"}]` : "unknown"}`,
336
+ `You: ${team.self.id}${team.self.isManager ? " [manager]" : ""}`,
337
+ ];
338
+ if (!team.coworkers.length) {
339
+ lines.push("Coworkers: none");
340
+ } else {
341
+ lines.push("Coworkers:");
342
+ for (const coworker of team.coworkers) {
343
+ const metadata = [coworker.harness, coworker.role, coworker.state].filter(Boolean).join(", ");
344
+ lines.push(`- ${coworker.id} target=${coworker.target}${metadata ? ` (${metadata})` : ""} [${coworker.connected ? "connected" : "not connected"}]`);
345
+ }
346
+ }
347
+ return lines.join("\n");
348
+ }
@@ -0,0 +1,97 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ export const CLAUDE_INTERCOM_TRANSPORT_ENV = "CLAUDE_INTERCOM_TRANSPORT";
4
+ export const MIN_NATIVE_CLAUDE_VERSION = "2.1.220";
5
+ export const MAX_NATIVE_CLAUDE_VERSION = "2.1.226";
6
+
7
+ export type ClaudeIntercomTransport = "auto" | "native" | "mcp";
8
+ export type ResolvedClaudeIntercomTransport = Exclude<ClaudeIntercomTransport, "auto">;
9
+
10
+ export interface ClaudeVersionProbe {
11
+ command: string;
12
+ version: string | null;
13
+ compatible: boolean;
14
+ reason: string;
15
+ }
16
+
17
+ export interface ClaudeTransportResolution extends ClaudeVersionProbe {
18
+ requested: ClaudeIntercomTransport;
19
+ selected: ResolvedClaudeIntercomTransport;
20
+ }
21
+
22
+ export function parseClaudeIntercomTransport(value: unknown, source = "transport"): ClaudeIntercomTransport {
23
+ if (value === undefined || value === null || value === "") return "auto";
24
+ if (value === "auto" || value === "native" || value === "mcp") return value;
25
+ throw new Error(`${source} must be one of auto, native, or mcp`);
26
+ }
27
+
28
+ export function parseClaudeVersion(output: string): string | null {
29
+ const match = output.match(/(?:^|\s)(\d+\.\d+\.\d+)(?:\s|$)/);
30
+ return match?.[1] ?? null;
31
+ }
32
+
33
+ function compareVersions(left: string, right: string): number {
34
+ const a = left.split(".").map(Number);
35
+ const b = right.split(".").map(Number);
36
+ for (let index = 0; index < 3; index += 1) {
37
+ const difference = (a[index] ?? 0) - (b[index] ?? 0);
38
+ if (difference !== 0) return difference;
39
+ }
40
+ return 0;
41
+ }
42
+
43
+ export function isNativeClaudeVersionCompatible(version: string): boolean {
44
+ return compareVersions(version, MIN_NATIVE_CLAUDE_VERSION) >= 0
45
+ && compareVersions(version, MAX_NATIVE_CLAUDE_VERSION) <= 0;
46
+ }
47
+
48
+ export function probeClaudeVersion(command = "claude"): ClaudeVersionProbe {
49
+ const result = spawnSync(command, ["--version"], {
50
+ encoding: "utf8",
51
+ stdio: ["ignore", "pipe", "pipe"],
52
+ });
53
+ if (result.error || result.status !== 0) {
54
+ return {
55
+ command,
56
+ version: null,
57
+ compatible: false,
58
+ reason: `could not run ${command} --version`,
59
+ };
60
+ }
61
+ const version = parseClaudeVersion(`${result.stdout}\n${result.stderr}`);
62
+ if (!version) {
63
+ return { command, version: null, compatible: false, reason: "Claude version output was not recognized" };
64
+ }
65
+ const compatible = isNativeClaudeVersionCompatible(version);
66
+ return {
67
+ command,
68
+ version,
69
+ compatible,
70
+ reason: compatible
71
+ ? `Claude ${version} is within the native transport compatibility window ${MIN_NATIVE_CLAUDE_VERSION}-${MAX_NATIVE_CLAUDE_VERSION}`
72
+ : `Claude ${version} is outside the verified native transport compatibility window ${MIN_NATIVE_CLAUDE_VERSION}-${MAX_NATIVE_CLAUDE_VERSION}`,
73
+ };
74
+ }
75
+
76
+ export function resolveClaudeIntercomTransport(options: {
77
+ requested?: ClaudeIntercomTransport | string;
78
+ env?: NodeJS.ProcessEnv;
79
+ claudeCommand?: string;
80
+ probe?: (command: string) => ClaudeVersionProbe;
81
+ } = {}): ClaudeTransportResolution {
82
+ const requested = parseClaudeIntercomTransport(
83
+ options.requested ?? options.env?.[CLAUDE_INTERCOM_TRANSPORT_ENV] ?? "auto",
84
+ options.requested === undefined ? CLAUDE_INTERCOM_TRANSPORT_ENV : "transport",
85
+ );
86
+ const command = options.claudeCommand || "claude";
87
+ const versionProbe = (options.probe ?? probeClaudeVersion)(command);
88
+ if (requested === "mcp") return { requested, selected: "mcp", ...versionProbe };
89
+ if (requested === "native" && !versionProbe.compatible) {
90
+ throw new Error(`Native Claude intercom transport was requested, but ${versionProbe.reason}. Use --transport mcp or install a verified-compatible Claude version.`);
91
+ }
92
+ return {
93
+ requested,
94
+ selected: requested === "native" || versionProbe.compatible ? "native" : "mcp",
95
+ ...versionProbe,
96
+ };
97
+ }
@@ -0,0 +1,198 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { cwd as processCwd } from "node:process";
4
+ import { getIntercomDirPath, restrictIntercomRuntimeFile } from "../broker/paths.ts";
5
+ import { isDenseArrayOf } from "../broker/validation.ts";
6
+ import {
7
+ assertNoPermissionOverrides,
8
+ bindHardenedClaudePaths,
9
+ resolveClaudePermissionPolicy,
10
+ type ClaudePermissionCeiling,
11
+ type ClaudePermissionMode,
12
+ } from "./permission-policy.ts";
13
+ import { parseClaudeIntercomTransport, type ClaudeIntercomTransport } from "./transport.ts";
14
+
15
+ export interface WorkerAgentConfig {
16
+ id: string;
17
+ name: string;
18
+ cwd: string;
19
+ model?: string;
20
+ sessionId?: string;
21
+ instructions?: string;
22
+ permissionMode?: ClaudePermissionMode;
23
+ dangerouslySkipPermissions?: boolean;
24
+ permissionCeiling?: ClaudePermissionCeiling;
25
+ /** Tightening-only role hint; authority still comes exclusively from the broker binding. */
26
+ bossRole?: "adversary" | "council";
27
+ addDirs?: string[];
28
+ mcpConfig?: string;
29
+ claudeArgs?: string[];
30
+ transport?: ClaudeIntercomTransport;
31
+ }
32
+
33
+ export interface WorkerConfig {
34
+ agents: WorkerAgentConfig[];
35
+ statePath: string;
36
+ claudeCommand?: string;
37
+ }
38
+
39
+ export interface WorkerState {
40
+ agents: Record<string, { sessionId: string; updatedAt: number }>;
41
+ }
42
+
43
+ export const DEFAULT_WORKER_CONFIG_PATH = join(getIntercomDirPath(), "claude-worker.json");
44
+ export const DEFAULT_WORKER_STATE_PATH = join(getIntercomDirPath(), "claude-worker-state.json");
45
+
46
+ function isRecord(value: unknown): value is Record<string, unknown> {
47
+ return typeof value === "object" && value !== null && !Array.isArray(value);
48
+ }
49
+
50
+ function optionalString(value: unknown, field: string): string | undefined {
51
+ if (value === undefined || value === null) return undefined;
52
+ if (typeof value !== "string") throw new Error(`${field} must be a string`);
53
+ const trimmed = value.trim();
54
+ return trimmed || undefined;
55
+ }
56
+
57
+ function requireString(value: unknown, field: string): string {
58
+ const result = optionalString(value, field);
59
+ if (!result) throw new Error(`${field} must be a non-empty string`);
60
+ return result;
61
+ }
62
+
63
+ function optionalBoolean(value: unknown, field: string): boolean | undefined {
64
+ if (value === undefined || value === null) return undefined;
65
+ if (typeof value !== "boolean") throw new Error(`${field} must be a boolean`);
66
+ return value;
67
+ }
68
+
69
+ function optionalStringArray(value: unknown, field: string): string[] | undefined {
70
+ if (value === undefined || value === null) return undefined;
71
+ if (!isDenseArrayOf<unknown>(value, (_entry): _entry is unknown => true)) {
72
+ throw new Error(`${field} must be an exact dense array`);
73
+ }
74
+ return value.map((item, index) => requireString(item, `${field}[${index}]`));
75
+ }
76
+
77
+ function normalizeAgent(raw: unknown, index: number): WorkerAgentConfig {
78
+ if (!isRecord(raw)) throw new Error(`agents[${index}] must be an object`);
79
+ const id = requireString(raw.id, `agents[${index}].id`);
80
+ const name = optionalString(raw.name, `agents[${index}].name`) ?? id;
81
+ const cwdValue = optionalString(raw.cwd, `agents[${index}].cwd`) ?? processCwd();
82
+ let cwd = resolve(cwdValue);
83
+ const permissionModeValue = optionalString(raw.permissionMode, `agents[${index}].permissionMode`);
84
+ const dangerouslySkipPermissionsValue = optionalBoolean(
85
+ raw.dangerouslySkipPermissions,
86
+ `agents[${index}].dangerouslySkipPermissions`,
87
+ );
88
+ const permissionCeilingValue = optionalString(raw.permissionCeiling, `agents[${index}].permissionCeiling`);
89
+ if (permissionCeilingValue !== undefined && permissionCeilingValue !== "standard" && permissionCeilingValue !== "read-only") {
90
+ throw new Error(`agents[${index}].permissionCeiling must be standard or read-only`);
91
+ }
92
+ const bossRoleValue = optionalString(raw.bossRole, `agents[${index}].bossRole`);
93
+ if (bossRoleValue !== undefined && bossRoleValue !== "adversary" && bossRoleValue !== "council") {
94
+ throw new Error(`agents[${index}].bossRole must be adversary or council`);
95
+ }
96
+ if (bossRoleValue !== undefined && permissionCeilingValue === "standard") {
97
+ throw new Error(`agents[${index}].permissionCeiling cannot widen an Adversary/Council role`);
98
+ }
99
+ const permission = resolveClaudePermissionPolicy({
100
+ permissionMode: permissionModeValue,
101
+ dangerouslySkipPermissions: dangerouslySkipPermissionsValue,
102
+ ceiling: bossRoleValue === undefined
103
+ ? permissionCeilingValue as ClaudePermissionCeiling | undefined
104
+ : "read-only",
105
+ });
106
+ const claudeArgs = optionalStringArray(raw.claudeArgs, `agents[${index}].claudeArgs`);
107
+ let addDirs = optionalStringArray(raw.addDirs, `agents[${index}].addDirs`);
108
+ const mcpConfig = optionalString(raw.mcpConfig, `agents[${index}].mcpConfig`);
109
+ assertNoPermissionOverrides(claudeArgs ?? [], `agents[${index}].claudeArgs`, permission.ceiling);
110
+ if (permission.ceiling === "read-only") {
111
+ if (mcpConfig !== undefined) {
112
+ throw new Error(`agents[${index}].mcpConfig cannot add an arbitrary MCP capability to a hardened Claude role`);
113
+ }
114
+ const binding = bindHardenedClaudePaths(cwdValue, addDirs ?? [], `agents[${index}]`);
115
+ try {
116
+ cwd = binding.cwd;
117
+ addDirs = binding.addDirs.length ? binding.addDirs : undefined;
118
+ } finally {
119
+ binding.release();
120
+ }
121
+ }
122
+ return {
123
+ id,
124
+ name,
125
+ cwd,
126
+ model: optionalString(raw.model, `agents[${index}].model`),
127
+ sessionId: optionalString(raw.sessionId, `agents[${index}].sessionId`),
128
+ instructions: optionalString(raw.instructions, `agents[${index}].instructions`),
129
+ ...(permission.ceiling === "standard" && permissionModeValue === undefined
130
+ ? {}
131
+ : { permissionMode: permission.permissionMode }),
132
+ ...(dangerouslySkipPermissionsValue === undefined && permission.ceiling !== "read-only"
133
+ ? {}
134
+ : { dangerouslySkipPermissions: permission.dangerouslySkipPermissions }),
135
+ ...(permissionCeilingValue === undefined && bossRoleValue === undefined
136
+ ? {}
137
+ : { permissionCeiling: permission.ceiling }),
138
+ ...(bossRoleValue === undefined ? {} : { bossRole: bossRoleValue }),
139
+ addDirs,
140
+ mcpConfig,
141
+ claudeArgs,
142
+ transport: raw.transport === undefined
143
+ ? undefined
144
+ : parseClaudeIntercomTransport(raw.transport, `agents[${index}].transport`),
145
+ };
146
+ }
147
+
148
+ export function defaultWorkerConfig(env: NodeJS.ProcessEnv = process.env): WorkerConfig {
149
+ const id = env.CLAUDE_INTERCOM_WORKER_ID?.trim() || "claude-worker";
150
+ return {
151
+ statePath: env.CLAUDE_INTERCOM_WORKER_STATE?.trim() || DEFAULT_WORKER_STATE_PATH,
152
+ agents: [{
153
+ id,
154
+ name: env.CLAUDE_INTERCOM_WORKER_NAME?.trim() || id,
155
+ cwd: resolve(env.CLAUDE_INTERCOM_WORKER_CWD?.trim() || processCwd()),
156
+ model: env.CLAUDE_INTERCOM_WORKER_MODEL?.trim() || undefined,
157
+ instructions: env.CLAUDE_INTERCOM_WORKER_INSTRUCTIONS?.trim() || undefined,
158
+ transport: parseClaudeIntercomTransport(env.CLAUDE_INTERCOM_TRANSPORT, "CLAUDE_INTERCOM_TRANSPORT"),
159
+ }],
160
+ };
161
+ }
162
+
163
+ export function loadWorkerConfig(path = process.env.CLAUDE_INTERCOM_WORKER_CONFIG || DEFAULT_WORKER_CONFIG_PATH): WorkerConfig {
164
+ if (!existsSync(path)) return defaultWorkerConfig();
165
+
166
+ const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
167
+ if (!isRecord(parsed)) throw new Error("Worker config must be a JSON object");
168
+ if (!isDenseArrayOf<unknown>(parsed.agents, (_entry): _entry is unknown => true)) {
169
+ throw new Error("Worker config requires an exact dense agents array");
170
+ }
171
+
172
+ return {
173
+ statePath: resolve(optionalString(parsed.statePath, "statePath") ?? DEFAULT_WORKER_STATE_PATH),
174
+ claudeCommand: optionalString(parsed.claudeCommand, "claudeCommand"),
175
+ agents: parsed.agents.map(normalizeAgent),
176
+ };
177
+ }
178
+
179
+ export function loadWorkerState(path: string): WorkerState {
180
+ if (!existsSync(path)) return { agents: {} };
181
+ const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
182
+ if (!isRecord(parsed) || !isRecord(parsed.agents)) return { agents: {} };
183
+ const agents: WorkerState["agents"] = {};
184
+ for (const [id, value] of Object.entries(parsed.agents)) {
185
+ if (!isRecord(value) || typeof value.sessionId !== "string") continue;
186
+ agents[id] = {
187
+ sessionId: value.sessionId,
188
+ updatedAt: typeof value.updatedAt === "number" ? value.updatedAt : 0,
189
+ };
190
+ }
191
+ return { agents };
192
+ }
193
+
194
+ export function saveWorkerState(path: string, state: WorkerState): void {
195
+ mkdirSync(dirname(path), { recursive: true });
196
+ writeFileSync(path, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
197
+ restrictIntercomRuntimeFile(path);
198
+ }