@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
@@ -0,0 +1,460 @@
1
+ import { createHash, randomBytes, randomUUID, timingSafeEqual } from "crypto";
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { writeDurableJson } from "../durable-json.ts";
4
+ import { restrictIntercomRuntimeFile } from "./paths.ts";
5
+
6
+ export const REMOTE_ACCESS_STATE_VERSION = 2;
7
+ export const REMOTE_ACCESS_CREDENTIAL_VERSION = 1;
8
+ export const DEFAULT_ENROLLMENT_TTL_MS = 10 * 60 * 1000;
9
+ export const DEFAULT_PRINCIPAL_TTL_MS = 30 * 24 * 60 * 60 * 1000;
10
+
11
+ export type RemotePrincipalState = "active" | "revoked";
12
+ export type RemotePrincipalPolicy = "remote-tree";
13
+
14
+ export interface RemotePrincipalRecord {
15
+ id: string;
16
+ name: string;
17
+ credentialHash: string;
18
+ parentSessionId: string;
19
+ rootSessionId: string;
20
+ remoteHostId: string;
21
+ generation: number;
22
+ policy: RemotePrincipalPolicy;
23
+ canDelegate: boolean;
24
+ depth: number;
25
+ maxDepth: number;
26
+ maxChildren: number;
27
+ state: RemotePrincipalState;
28
+ expiresAt: number;
29
+ createdAt: number;
30
+ updatedAt: number;
31
+ }
32
+
33
+ export interface EnrollmentTemplate {
34
+ name: string;
35
+ parentSessionId: string;
36
+ rootSessionId: string;
37
+ remoteHostId: string;
38
+ expiresAt?: number;
39
+ canDelegate?: boolean;
40
+ depth?: number;
41
+ maxDepth?: number;
42
+ maxChildren?: number;
43
+ }
44
+
45
+ export interface ChildEnrollmentRequest {
46
+ name: string;
47
+ expiresAt?: number;
48
+ canDelegate?: boolean;
49
+ maxDepth?: number;
50
+ maxChildren?: number;
51
+ }
52
+
53
+ export interface EnrollmentRecord {
54
+ tokenHash: string;
55
+ template: EnrollmentTemplate;
56
+ expiresAt: number;
57
+ createdAt: number;
58
+ }
59
+
60
+ export interface RemoteAccessState {
61
+ version: typeof REMOTE_ACCESS_STATE_VERSION;
62
+ adminCredentialHash?: string;
63
+ principals: Record<string, RemotePrincipalRecord>;
64
+ enrollments: Record<string, EnrollmentRecord>;
65
+ }
66
+
67
+ export interface IssuedEnrollment {
68
+ enrollmentToken: string;
69
+ expiresAt: number;
70
+ }
71
+
72
+ export interface ConsumedEnrollment {
73
+ principal: RemotePrincipalRecord;
74
+ sessionCredential: string;
75
+ }
76
+
77
+ export type RemotePrincipalMetadata = Omit<RemotePrincipalRecord, "credentialHash">;
78
+
79
+ export interface PersistedAdminCredential {
80
+ version: typeof REMOTE_ACCESS_CREDENTIAL_VERSION;
81
+ adminToken: string;
82
+ }
83
+
84
+ export class RemoteAccessError extends Error {
85
+ constructor(
86
+ readonly code: "INVALID_CREDENTIAL" | "EXPIRED_CREDENTIAL" | "REVOKED_CREDENTIAL" | "STALE_GENERATION" | "INVALID_ENROLLMENT",
87
+ message: string,
88
+ ) {
89
+ super(message);
90
+ this.name = "RemoteAccessError";
91
+ }
92
+ }
93
+
94
+ function emptyState(): RemoteAccessState {
95
+ return { version: REMOTE_ACCESS_STATE_VERSION, principals: {}, enrollments: {} };
96
+ }
97
+
98
+ function hashSecret(secret: string): string {
99
+ return createHash("sha256").update(secret, "utf8").digest("hex");
100
+ }
101
+
102
+ function secretsMatch(secret: string, expectedHash: string): boolean {
103
+ const actual = Buffer.from(hashSecret(secret), "hex");
104
+ const expected = Buffer.from(expectedHash, "hex");
105
+ return actual.length === expected.length && timingSafeEqual(actual, expected);
106
+ }
107
+
108
+ function newSecret(): string {
109
+ return randomBytes(32).toString("base64url");
110
+ }
111
+
112
+ function requireText(value: string, field: string, maxLength = 512): string {
113
+ const normalized = value.trim();
114
+ if (!normalized || normalized.length > maxLength || /[\u0000-\u001f\u007f]/.test(normalized)) {
115
+ throw new Error(`Invalid ${field}`);
116
+ }
117
+ return normalized;
118
+ }
119
+
120
+ function boundedInteger(value: number | undefined, fallback: number, field: string, minimum: number, maximum: number): number {
121
+ const resolved = value ?? fallback;
122
+ if (!Number.isSafeInteger(resolved) || resolved < minimum || resolved > maximum) throw new Error(`Invalid ${field}`);
123
+ return resolved;
124
+ }
125
+
126
+ function parseState(raw: unknown): RemoteAccessState {
127
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new Error("expected object");
128
+ const state = raw as Record<string, unknown>;
129
+ if (state.version !== 1 && state.version !== REMOTE_ACCESS_STATE_VERSION) throw new Error("unsupported version");
130
+ if (typeof state.principals !== "object" || state.principals === null || Array.isArray(state.principals)) throw new Error("invalid principals");
131
+ if (typeof state.enrollments !== "object" || state.enrollments === null || Array.isArray(state.enrollments)) throw new Error("invalid enrollments");
132
+ if (state.adminCredentialHash !== undefined && typeof state.adminCredentialHash !== "string") throw new Error("invalid admin credential hash");
133
+ const principals: Record<string, RemotePrincipalRecord> = {};
134
+ for (const [id, value] of Object.entries(state.principals as Record<string, unknown>)) {
135
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`invalid principal ${id}`);
136
+ const principal = value as Record<string, unknown>;
137
+ if (
138
+ principal.id !== id
139
+ || typeof principal.name !== "string"
140
+ || typeof principal.credentialHash !== "string"
141
+ || typeof principal.parentSessionId !== "string"
142
+ || typeof principal.rootSessionId !== "string"
143
+ || typeof principal.remoteHostId !== "string"
144
+ || typeof principal.generation !== "number"
145
+ || (principal.state !== "active" && principal.state !== "revoked")
146
+ || typeof principal.expiresAt !== "number"
147
+ || typeof principal.createdAt !== "number"
148
+ || typeof principal.updatedAt !== "number"
149
+ ) throw new Error(`invalid principal ${id}`);
150
+ const depth = boundedInteger(principal.depth as number | undefined, 1, "principal depth", 1, 32);
151
+ const maxDepth = boundedInteger(principal.maxDepth as number | undefined, depth, "maximum delegation depth", depth, 32);
152
+ const maxChildren = boundedInteger(principal.maxChildren as number | undefined, 0, "maximum child count", 0, 128);
153
+ principals[id] = {
154
+ ...(principal as unknown as RemotePrincipalRecord),
155
+ policy: "remote-tree",
156
+ canDelegate: principal.canDelegate === true,
157
+ depth,
158
+ maxDepth,
159
+ maxChildren,
160
+ };
161
+ }
162
+ const enrollments: Record<string, EnrollmentRecord> = {};
163
+ for (const [hash, value] of Object.entries(state.enrollments as Record<string, unknown>)) {
164
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`invalid enrollment ${hash}`);
165
+ const enrollment = value as EnrollmentRecord;
166
+ const template = enrollment.template;
167
+ if (!template || typeof template !== "object") throw new Error(`invalid enrollment ${hash}`);
168
+ const depth = boundedInteger(template.depth, 1, "enrollment depth", 1, 32);
169
+ enrollments[hash] = {
170
+ ...enrollment,
171
+ template: {
172
+ ...template,
173
+ canDelegate: template.canDelegate === true,
174
+ depth,
175
+ maxDepth: boundedInteger(template.maxDepth, depth, "enrollment maximum depth", depth, 32),
176
+ maxChildren: boundedInteger(template.maxChildren, 0, "enrollment maximum children", 0, 128),
177
+ },
178
+ };
179
+ }
180
+ return {
181
+ version: REMOTE_ACCESS_STATE_VERSION,
182
+ ...(typeof state.adminCredentialHash === "string" ? { adminCredentialHash: state.adminCredentialHash } : {}),
183
+ principals,
184
+ enrollments,
185
+ };
186
+ }
187
+
188
+ export class RemoteAccessRegistry {
189
+ private state: RemoteAccessState;
190
+
191
+ constructor(private readonly statePath: string, private readonly now: () => number = Date.now) {
192
+ this.state = this.load();
193
+ }
194
+
195
+ snapshot(): RemoteAccessState {
196
+ return structuredClone(this.state);
197
+ }
198
+
199
+ ensureAdminCredential(credentialPath: string): string {
200
+ if (existsSync(credentialPath)) {
201
+ const parsed = JSON.parse(readFileSync(credentialPath, "utf8")) as Partial<PersistedAdminCredential>;
202
+ if (parsed.version === REMOTE_ACCESS_CREDENTIAL_VERSION && typeof parsed.adminToken === "string" && parsed.adminToken.length >= 32) {
203
+ const hash = hashSecret(parsed.adminToken);
204
+ if (this.state.adminCredentialHash !== hash) {
205
+ this.state.adminCredentialHash = hash;
206
+ this.persist();
207
+ }
208
+ restrictIntercomRuntimeFile(credentialPath);
209
+ return parsed.adminToken;
210
+ }
211
+ }
212
+ const adminToken = newSecret();
213
+ writeDurableJson(credentialPath, { version: REMOTE_ACCESS_CREDENTIAL_VERSION, adminToken });
214
+ this.state.adminCredentialHash = hashSecret(adminToken);
215
+ this.persist();
216
+ return adminToken;
217
+ }
218
+
219
+ authenticateAdmin(adminToken: string): boolean {
220
+ return typeof this.state.adminCredentialHash === "string" && secretsMatch(adminToken, this.state.adminCredentialHash);
221
+ }
222
+
223
+ issueEnrollment(template: EnrollmentTemplate, ttlMs = DEFAULT_ENROLLMENT_TTL_MS): IssuedEnrollment {
224
+ if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0 || ttlMs > 24 * 60 * 60 * 1000) throw new Error("Invalid enrollment TTL");
225
+ const now = this.now();
226
+ const enrollmentToken = newSecret();
227
+ const tokenHash = hashSecret(enrollmentToken);
228
+ const expiresAt = now + ttlMs;
229
+ const principalExpiresAt = template.expiresAt ?? now + DEFAULT_PRINCIPAL_TTL_MS;
230
+ if (!Number.isSafeInteger(principalExpiresAt) || principalExpiresAt <= now) throw new Error("Invalid principal expiry");
231
+ const depth = boundedInteger(template.depth, 1, "principal depth", 1, 32);
232
+ const maxDepth = boundedInteger(template.maxDepth, depth, "maximum delegation depth", depth, 32);
233
+ const maxChildren = boundedInteger(template.maxChildren, 0, "maximum child count", 0, 128);
234
+ const canDelegate = template.canDelegate === true;
235
+ if (canDelegate && (maxDepth <= depth || maxChildren === 0)) throw new Error("Delegating principals require remaining depth and child capacity");
236
+ const normalized: EnrollmentTemplate = {
237
+ name: requireText(template.name, "principal name", 256),
238
+ parentSessionId: requireText(template.parentSessionId, "parent session ID"),
239
+ rootSessionId: requireText(template.rootSessionId, "root session ID"),
240
+ remoteHostId: requireText(template.remoteHostId, "remote host ID", 256),
241
+ expiresAt: principalExpiresAt,
242
+ canDelegate,
243
+ depth,
244
+ maxDepth,
245
+ maxChildren,
246
+ };
247
+ this.pruneExpiredEnrollments(now);
248
+ this.state.enrollments[tokenHash] = { tokenHash, template: normalized, expiresAt, createdAt: now };
249
+ this.persist();
250
+ return { enrollmentToken, expiresAt };
251
+ }
252
+
253
+ issueChildEnrollment(
254
+ parentSessionId: string,
255
+ parentGeneration: number,
256
+ request: ChildEnrollmentRequest,
257
+ ttlMs = DEFAULT_ENROLLMENT_TTL_MS,
258
+ ): IssuedEnrollment {
259
+ const parent = this.validatePrincipal(parentSessionId, parentGeneration);
260
+ if (!parent.canDelegate) throw new RemoteAccessError("INVALID_ENROLLMENT", "Parent principal cannot delegate children");
261
+ const now = this.now();
262
+ const activeChildren = Object.values(this.state.principals).filter((principal) =>
263
+ principal.parentSessionId === parent.id && principal.state === "active" && principal.expiresAt > now
264
+ ).length;
265
+ const pendingChildren = Object.values(this.state.enrollments).filter((enrollment) =>
266
+ enrollment.template.parentSessionId === parent.id && enrollment.expiresAt > now
267
+ ).length;
268
+ if (activeChildren + pendingChildren >= parent.maxChildren) throw new RemoteAccessError("INVALID_ENROLLMENT", "Parent child limit is exhausted");
269
+ const depth = parent.depth + 1;
270
+ if (depth > parent.maxDepth) throw new RemoteAccessError("INVALID_ENROLLMENT", "Parent delegation depth is exhausted");
271
+ const maxDepth = boundedInteger(request.maxDepth, depth, "child maximum depth", depth, parent.maxDepth);
272
+ const maxChildren = boundedInteger(request.maxChildren, 0, "child maximum count", 0, parent.maxChildren);
273
+ const canDelegate = request.canDelegate === true;
274
+ if (canDelegate && (maxDepth <= depth || maxChildren === 0)) throw new Error("Delegating child requires remaining depth and child capacity");
275
+ const expiresAt = request.expiresAt ?? parent.expiresAt;
276
+ if (!Number.isSafeInteger(expiresAt) || expiresAt <= this.now() || expiresAt > parent.expiresAt) {
277
+ throw new Error("Child expiry must not exceed the parent expiry");
278
+ }
279
+ return this.issueEnrollment({
280
+ name: request.name,
281
+ parentSessionId: parent.id,
282
+ rootSessionId: parent.rootSessionId,
283
+ remoteHostId: parent.remoteHostId,
284
+ expiresAt,
285
+ canDelegate,
286
+ depth,
287
+ maxDepth,
288
+ maxChildren,
289
+ }, ttlMs);
290
+ }
291
+
292
+ consumeEnrollment(enrollmentToken: string): ConsumedEnrollment {
293
+ const now = this.now();
294
+ const tokenHash = hashSecret(enrollmentToken);
295
+ const enrollment = this.state.enrollments[tokenHash];
296
+ if (!enrollment) throw new RemoteAccessError("INVALID_ENROLLMENT", "Enrollment credential is invalid or already consumed");
297
+ delete this.state.enrollments[tokenHash];
298
+ if (enrollment.expiresAt <= now) {
299
+ this.persist();
300
+ throw new RemoteAccessError("INVALID_ENROLLMENT", "Enrollment credential has expired");
301
+ }
302
+ const sessionCredential = newSecret();
303
+ const id = randomUUID();
304
+ const principal: RemotePrincipalRecord = {
305
+ id,
306
+ name: enrollment.template.name,
307
+ credentialHash: hashSecret(sessionCredential),
308
+ parentSessionId: enrollment.template.parentSessionId,
309
+ rootSessionId: enrollment.template.rootSessionId,
310
+ remoteHostId: enrollment.template.remoteHostId,
311
+ generation: 1,
312
+ policy: "remote-tree",
313
+ canDelegate: enrollment.template.canDelegate === true,
314
+ depth: enrollment.template.depth!,
315
+ maxDepth: enrollment.template.maxDepth!,
316
+ maxChildren: enrollment.template.maxChildren!,
317
+ state: "active",
318
+ expiresAt: enrollment.template.expiresAt!,
319
+ createdAt: now,
320
+ updatedAt: now,
321
+ };
322
+ this.state.principals[id] = principal;
323
+ this.persist();
324
+ return { principal: structuredClone(principal), sessionCredential };
325
+ }
326
+
327
+ authenticateSession(sessionId: string, generation: number, sessionCredential: string): RemotePrincipalRecord {
328
+ const principal = this.state.principals[sessionId];
329
+ if (!principal || !secretsMatch(sessionCredential, principal.credentialHash)) {
330
+ throw new RemoteAccessError("INVALID_CREDENTIAL", "Session credential is invalid");
331
+ }
332
+ return this.validatePrincipal(sessionId, generation);
333
+ }
334
+
335
+ validatePrincipal(sessionId: string, generation: number): RemotePrincipalRecord {
336
+ const principal = this.state.principals[sessionId];
337
+ if (!principal) throw new RemoteAccessError("INVALID_CREDENTIAL", "Remote principal does not exist");
338
+ if (principal.state !== "active") throw new RemoteAccessError("REVOKED_CREDENTIAL", "Session credential is revoked");
339
+ if (principal.expiresAt <= this.now()) throw new RemoteAccessError("EXPIRED_CREDENTIAL", "Session credential has expired");
340
+ if (principal.generation !== generation) throw new RemoteAccessError("STALE_GENERATION", "Session credential generation is stale");
341
+ return structuredClone(principal);
342
+ }
343
+
344
+ inspectSubtree(principalId: string): RemotePrincipalMetadata[] {
345
+ const result: RemotePrincipalMetadata[] = [];
346
+ const queue = [principalId];
347
+ const seen = new Set<string>();
348
+ while (queue.length) {
349
+ const id = queue.shift()!;
350
+ if (seen.has(id)) continue;
351
+ seen.add(id);
352
+ const principal = this.state.principals[id];
353
+ if (!principal) continue;
354
+ const { credentialHash: _credentialHash, ...metadata } = principal;
355
+ result.push(structuredClone(metadata));
356
+ for (const candidate of Object.values(this.state.principals)) {
357
+ if (candidate.parentSessionId === id) queue.push(candidate.id);
358
+ }
359
+ }
360
+ return result;
361
+ }
362
+
363
+ adoptSubtree(principalId: string, newParentSessionId: string, newRootSessionId: string): RemotePrincipalRecord[] {
364
+ const principal = this.state.principals[principalId];
365
+ if (!principal) throw new Error("Unknown adopted principal");
366
+ if (principalId === newParentSessionId) throw new Error("Adoption would create an ownership cycle");
367
+ let ancestor = this.state.principals[newParentSessionId];
368
+ const seen = new Set<string>();
369
+ while (ancestor && !seen.has(ancestor.id)) {
370
+ if (ancestor.id === principalId) throw new Error("Adoption would create an ownership cycle");
371
+ seen.add(ancestor.id);
372
+ ancestor = this.state.principals[ancestor.parentSessionId];
373
+ }
374
+ const ids = this.subtreePrincipalIds(principalId);
375
+ const now = this.now();
376
+ principal.parentSessionId = requireText(newParentSessionId, "new parent session ID");
377
+ for (const id of ids) {
378
+ const changed = this.state.principals[id];
379
+ changed.rootSessionId = requireText(newRootSessionId, "new root session ID");
380
+ changed.generation += 1;
381
+ changed.updatedAt = now;
382
+ }
383
+ const changedIds = new Set(ids);
384
+ for (const [hash, enrollment] of Object.entries(this.state.enrollments)) {
385
+ if (changedIds.has(enrollment.template.parentSessionId)) delete this.state.enrollments[hash];
386
+ }
387
+ this.persist();
388
+ return ids.map((id) => structuredClone(this.state.principals[id]));
389
+ }
390
+
391
+ expirePrincipals(now = this.now()): RemotePrincipalRecord[] {
392
+ const changed = new Map<string, RemotePrincipalRecord>();
393
+ const expiredRoots = Object.values(this.state.principals)
394
+ .filter((principal) => principal.state === "active" && principal.expiresAt <= now)
395
+ .sort((left, right) => left.depth - right.depth);
396
+ for (const principal of expiredRoots) {
397
+ if (changed.has(principal.id)) continue;
398
+ for (const revoked of this.revoke(principal.id)) changed.set(revoked.id, revoked);
399
+ }
400
+ return [...changed.values()];
401
+ }
402
+
403
+ revoke(principalId: string): RemotePrincipalRecord[] {
404
+ const now = this.now();
405
+ const queue = [principalId];
406
+ const seen = new Set<string>();
407
+ const changed: RemotePrincipalRecord[] = [];
408
+ while (queue.length) {
409
+ const id = queue.shift()!;
410
+ if (seen.has(id)) continue;
411
+ seen.add(id);
412
+ const principal = this.state.principals[id];
413
+ if (!principal) continue;
414
+ principal.state = "revoked";
415
+ principal.generation += 1;
416
+ principal.updatedAt = now;
417
+ changed.push(structuredClone(principal));
418
+ for (const candidate of Object.values(this.state.principals)) {
419
+ if (candidate.parentSessionId === id) queue.push(candidate.id);
420
+ }
421
+ }
422
+ if (changed.length) this.persist();
423
+ return changed;
424
+ }
425
+
426
+ private subtreePrincipalIds(principalId: string): string[] {
427
+ const result: string[] = [];
428
+ const queue = [principalId];
429
+ const seen = new Set<string>();
430
+ while (queue.length) {
431
+ const id = queue.shift()!;
432
+ if (seen.has(id) || !this.state.principals[id]) continue;
433
+ seen.add(id);
434
+ result.push(id);
435
+ for (const candidate of Object.values(this.state.principals)) {
436
+ if (candidate.parentSessionId === id) queue.push(candidate.id);
437
+ }
438
+ }
439
+ return result;
440
+ }
441
+
442
+ private pruneExpiredEnrollments(now = this.now()): void {
443
+ for (const [hash, enrollment] of Object.entries(this.state.enrollments)) {
444
+ if (enrollment.expiresAt <= now) delete this.state.enrollments[hash];
445
+ }
446
+ }
447
+
448
+ private load(): RemoteAccessState {
449
+ if (!existsSync(this.statePath)) return emptyState();
450
+ try {
451
+ return parseState(JSON.parse(readFileSync(this.statePath, "utf8")));
452
+ } catch (error) {
453
+ throw new Error(`Invalid remote access registry at ${this.statePath}: ${error instanceof Error ? error.message : String(error)}`);
454
+ }
455
+ }
456
+
457
+ private persist(): void {
458
+ writeDurableJson(this.statePath, this.state);
459
+ }
460
+ }
@@ -0,0 +1,60 @@
1
+ import { closeSync, fsyncSync, openSync, writeSync } from "fs";
2
+ import { INTERCOM_RUNTIME_FILE_MODE, restrictIntercomRuntimeFile } from "./paths.ts";
3
+
4
+ export const BROKER_AUDIT_VERSION = 1;
5
+
6
+ export type BrokerAuditEvent =
7
+ | "enrollment_issued"
8
+ | "enrollment_consumed"
9
+ | "remote_connect"
10
+ | "remote_reconnect"
11
+ | "remote_disconnect"
12
+ | "remote_registration_denied"
13
+ | "remote_delivery_denied"
14
+ | "credential_reuse_denied"
15
+ | "remote_visibility_filtered"
16
+ | "tree_inspected"
17
+ | "principal_revoked"
18
+ | "principal_expired"
19
+ | "principal_adopted"
20
+ | "generation_fenced";
21
+
22
+ export interface BrokerAuditEntry {
23
+ event: BrokerAuditEvent;
24
+ outcome: "allowed" | "denied" | "observed";
25
+ actorId?: string;
26
+ targetId?: string;
27
+ remoteHostId?: string;
28
+ generation?: number;
29
+ reason?: string;
30
+ visibleCount?: number;
31
+ hiddenCount?: number;
32
+ }
33
+
34
+ export class BrokerAuditLog {
35
+ constructor(private readonly path: string, private readonly now: () => number = Date.now) {}
36
+
37
+ record(entry: BrokerAuditEntry): void {
38
+ const line = `${JSON.stringify({
39
+ version: BROKER_AUDIT_VERSION,
40
+ timestamp: this.now(),
41
+ ...entry,
42
+ })}\n`;
43
+ const descriptor = openSync(this.path, "a", INTERCOM_RUNTIME_FILE_MODE);
44
+ try {
45
+ writeSync(descriptor, line, undefined, "utf8");
46
+ fsyncSync(descriptor);
47
+ } finally {
48
+ closeSync(descriptor);
49
+ }
50
+ restrictIntercomRuntimeFile(this.path);
51
+ }
52
+
53
+ tryRecord(entry: BrokerAuditEntry): void {
54
+ try {
55
+ this.record(entry);
56
+ } catch (error) {
57
+ console.error("Failed to append Agent Intercom broker audit event:", error);
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,125 @@
1
+ import { type AuthorizationDecision, type PolicyAction, type PolicyPrincipal, type PolicyState } from "@ctliz/agent-intercom-core";
2
+ import {
3
+ authorizeFeatureAware,
4
+ type BossControlKind,
5
+ type BossPolicyAction,
6
+ type FeatureAwareAuthorizationDecision,
7
+ type FeatureAwarePolicyState,
8
+ } from "@ctliz/agent-intercom-core/boss";
9
+ import type { SessionInfo } from "../types.ts";
10
+ import { parseBossSessionMetadata } from "./boss-contracts.ts";
11
+
12
+ export type SessionPolicyAction = PolicyAction | BossPolicyAction;
13
+ export type SessionAuthorizationDecision = AuthorizationDecision | FeatureAwareAuthorizationDecision;
14
+
15
+ export interface SessionAuthorizationContext {
16
+ controlKind?: BossControlKind;
17
+ correlated?: boolean;
18
+ }
19
+
20
+ export function policyPrincipalForSession(session: SessionInfo): PolicyPrincipal {
21
+ if (session.boss !== undefined) {
22
+ throw new Error(`Boss session ${session.id} cannot cross the frozen legacy policy boundary`);
23
+ }
24
+ if (session.origin === "remote") {
25
+ if (!session.parentSessionId || !session.rootSessionId || !session.generation) {
26
+ throw new Error(`Remote session ${session.id} is missing broker-owned policy metadata`);
27
+ }
28
+ return {
29
+ id: session.id,
30
+ kind: "remote",
31
+ state: "active",
32
+ generation: session.generation,
33
+ policy: "remote-tree",
34
+ parentSessionId: session.parentSessionId,
35
+ rootSessionId: session.rootSessionId,
36
+ };
37
+ }
38
+ return {
39
+ id: session.id,
40
+ kind: "local",
41
+ state: "active",
42
+ generation: 1,
43
+ policy: "local-public",
44
+ rootSessionId: session.id,
45
+ };
46
+ }
47
+
48
+ export function policyStateForSessions(sessions: Iterable<SessionInfo>): PolicyState {
49
+ const principals: Record<string, PolicyPrincipal> = {};
50
+ for (const session of sessions) principals[session.id] = policyPrincipalForSession(session);
51
+ return { principals };
52
+ }
53
+
54
+ export function featureAwarePolicyStateForSessions(sessions: Iterable<SessionInfo>): FeatureAwarePolicyState {
55
+ const legacy: PolicyState = { principals: {} };
56
+ const boss: FeatureAwarePolicyState["boss"] = { principals: {} };
57
+ const registrations: FeatureAwarePolicyState["registrations"] = {};
58
+
59
+ for (const session of sessions) {
60
+ if (session.boss === undefined) {
61
+ const principal = policyPrincipalForSession(session);
62
+ legacy.principals[session.id] = principal;
63
+ registrations[session.id] = {
64
+ principalId: session.id,
65
+ principalClass: "ordinary",
66
+ state: "active",
67
+ };
68
+ continue;
69
+ }
70
+
71
+ const metadata = parseBossSessionMetadata(session.boss, session.id);
72
+ boss.principals[session.id] = metadata.principal;
73
+ registrations[session.id] = {
74
+ principalId: session.id,
75
+ principalClass: "boss-bound",
76
+ state: "active",
77
+ bossRunId: metadata.binding.bossRunId,
78
+ participantId: metadata.binding.participantId,
79
+ bindingEpoch: metadata.binding.bindingEpoch,
80
+ featureContract: metadata.featureContract,
81
+ policySemanticsHash: metadata.policySemanticsHash,
82
+ capabilityDigest: metadata.capabilityDigest,
83
+ brokerIdentityVerified: metadata.brokerIdentityVerified,
84
+ };
85
+ }
86
+
87
+ return { legacy, boss, registrations };
88
+ }
89
+
90
+ export function authorizeSessionAction(
91
+ sessions: Iterable<SessionInfo>,
92
+ actorId: string,
93
+ action: SessionPolicyAction,
94
+ targetId: string,
95
+ context: SessionAuthorizationContext = {},
96
+ ): SessionAuthorizationDecision {
97
+ const state = featureAwarePolicyStateForSessions(sessions);
98
+ const actor = state.registrations[actorId];
99
+ const target = state.registrations[targetId];
100
+ return authorizeFeatureAware(state, {
101
+ actorId,
102
+ targetId,
103
+ action,
104
+ ...(actor?.principalClass === "ordinary" && target?.principalClass === "ordinary"
105
+ ? {
106
+ legacyContext: {
107
+ actorGeneration: state.legacy.principals[actorId]?.generation,
108
+ targetGeneration: state.legacy.principals[targetId]?.generation,
109
+ },
110
+ }
111
+ : action === "control"
112
+ ? {
113
+ bossContext: {
114
+ controlKind: context.controlKind,
115
+ correlated: context.correlated,
116
+ },
117
+ }
118
+ : {}),
119
+ });
120
+ }
121
+
122
+ export function visibleSessions(sessions: Iterable<SessionInfo>, actorId: string): SessionInfo[] {
123
+ const values = Array.from(sessions);
124
+ return values.filter((target) => authorizeSessionAction(values, actorId, "discover", target.id).allowed);
125
+ }