@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
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
|
+
}
|