@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,140 @@
1
+ import { gunzipSync } from "node:zlib";
2
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+
7
+ const repositoryRoot = resolve(import.meta.dirname, "..");
8
+ const corePackage = "@ctliz/agent-intercom-core";
9
+ const coreDevSpec = "git+https://github.com/ctliz/agent-intercom-core.git#37e074970e2a9de32a16fc325607c3b476b0bd45";
10
+ const coreConsumers = new Set([
11
+ "broker.mjs",
12
+ "cci.mjs",
13
+ "ccim.mjs",
14
+ "claude-server.mjs",
15
+ "worker-daemon.mjs",
16
+ ]);
17
+ const bundles = [...coreConsumers, "inbox-monitor.mjs"];
18
+
19
+ function tarString(buffer, offset, length) {
20
+ const end = buffer.indexOf(0, offset);
21
+ return buffer.subarray(offset, end === -1 || end > offset + length ? offset + length : end).toString("utf8");
22
+ }
23
+
24
+ function readTarEntries(archivePath) {
25
+ const archive = gunzipSync(readFileSync(archivePath));
26
+ const entries = new Map();
27
+ for (let offset = 0; offset + 512 <= archive.length;) {
28
+ const header = archive.subarray(offset, offset + 512);
29
+ if (header.every((byte) => byte === 0)) break;
30
+ const name = tarString(header, 0, 100);
31
+ const prefix = tarString(header, 345, 155);
32
+ const path = prefix ? `${prefix}/${name}` : name;
33
+ const sizeText = tarString(header, 124, 12).trim();
34
+ const size = Number.parseInt(sizeText || "0", 8);
35
+ if (!Number.isSafeInteger(size)) throw new Error(`Invalid tar size for ${path}`);
36
+ const contentOffset = offset + 512;
37
+ entries.set(path, archive.subarray(contentOffset, contentOffset + size));
38
+ offset = contentOffset + Math.ceil(size / 512) * 512;
39
+ }
40
+ return entries;
41
+ }
42
+
43
+ function createPackageArchive(temp) {
44
+ const result = spawnSync("npm", [
45
+ "pack",
46
+ "--ignore-scripts",
47
+ "--json",
48
+ "--pack-destination",
49
+ temp,
50
+ ], {
51
+ cwd: repositoryRoot,
52
+ env: { ...process.env, npm_config_cache: join(temp, "npm-cache") },
53
+ encoding: "utf8",
54
+ });
55
+ if (result.error) throw result.error;
56
+ if (result.status !== 0) throw new Error(`npm pack failed: ${result.stderr || result.stdout}`);
57
+ const packed = JSON.parse(result.stdout);
58
+ if (!Array.isArray(packed) || packed.length !== 1 || typeof packed[0]?.filename !== "string") {
59
+ throw new Error("npm pack did not report exactly one package archive");
60
+ }
61
+ return join(temp, packed[0].filename);
62
+ }
63
+
64
+ const archiveArgument = process.argv.indexOf("--archive");
65
+ const suppliedArchive = archiveArgument === -1 ? undefined : process.argv[archiveArgument + 1];
66
+ if (archiveArgument !== -1 && !suppliedArchive) throw new Error("--archive requires a package tarball path");
67
+ const temp = suppliedArchive ? undefined : mkdtempSync(join(tmpdir(), "claude-intercom-pack-check-"));
68
+
69
+ try {
70
+ const archivePath = suppliedArchive ? resolve(suppliedArchive) : createPackageArchive(temp);
71
+ const entries = readTarEntries(archivePath);
72
+ const manifestEntry = entries.get("package/package.json");
73
+ if (!manifestEntry) throw new Error("Packed adapter is missing package.json");
74
+ const manifest = JSON.parse(manifestEntry.toString("utf8"));
75
+ if (manifest.peerDependencies?.[corePackage] !== "0.1.0") throw new Error("Packed adapter must require Core peer 0.1.0");
76
+ if (manifest.peerDependenciesMeta?.[corePackage]?.optional === true) throw new Error("Packed Core peer must not be optional");
77
+ if (manifest.dependencies?.[corePackage] !== undefined) throw new Error("Packed adapter must not install a private Core dependency");
78
+ if (manifest.devDependencies?.[corePackage] !== coreDevSpec) throw new Error("Packed adapter lost the approved Core build commit");
79
+ if ([...entries.keys()].some((path) => path.includes("node_modules/@ctliz/agent-intercom-core/"))) {
80
+ throw new Error("Packed adapter contains a private Core module tree");
81
+ }
82
+
83
+ // 1. Assert Claude Plugin manifest exists and is valid
84
+ const pluginEntry = entries.get("package/.claude-plugin/plugin.json");
85
+ if (!pluginEntry) throw new Error("Packed adapter is missing .claude-plugin/plugin.json");
86
+ const plugin = JSON.parse(pluginEntry.toString("utf8"));
87
+ if (plugin.name !== "claude-intercom") throw new Error(`Unexpected plugin name: ${plugin.name}`);
88
+
89
+ // 2. Assert Plugin -> MCP configuration exists and points to real dist bundle
90
+ if (typeof plugin.mcpServers !== "string") throw new Error("plugin.json missing mcpServers declaration");
91
+ const mcpRelPath = plugin.mcpServers.replace(/^\.\//, "");
92
+ const mcpEntry = entries.get(`package/${mcpRelPath}`);
93
+ if (!mcpEntry) throw new Error(`Packed adapter missing plugin mcpServers file: ${mcpRelPath}`);
94
+ const mcpConfig = JSON.parse(mcpEntry.toString("utf8"));
95
+ const claudeServerArg = mcpConfig.mcpServers?.["claude-intercom"]?.args?.[0];
96
+ if (!claudeServerArg || !claudeServerArg.includes("/dist/claude-server.mjs")) {
97
+ throw new Error(`MCP config does not reference dist/claude-server.mjs: ${claudeServerArg}`);
98
+ }
99
+
100
+ // 3. Assert Plugin -> Monitors configuration exists and points to real dist bundle
101
+ if (typeof plugin.monitors !== "string") throw new Error("plugin.json missing monitors declaration");
102
+ const monitorsRelPath = plugin.monitors.replace(/^\.\//, "");
103
+ const monitorsEntry = entries.get(`package/${monitorsRelPath}`);
104
+ if (!monitorsEntry) throw new Error(`Packed adapter missing plugin monitors file: ${monitorsRelPath}`);
105
+ const monitorsConfig = JSON.parse(monitorsEntry.toString("utf8"));
106
+ if (!Array.isArray(monitorsConfig) || monitorsConfig.length === 0) {
107
+ throw new Error("monitors.json is empty or not an array");
108
+ }
109
+ const inboxMonitor = monitorsConfig.find((m) => m.name === "intercom-inbox");
110
+ if (!inboxMonitor || !inboxMonitor.command?.includes("/dist/inbox-monitor.mjs")) {
111
+ throw new Error(`monitors.json missing intercom-inbox command referencing dist/inbox-monitor.mjs: ${JSON.stringify(inboxMonitor)}`);
112
+ }
113
+
114
+ // 4. Assert Plugin -> Skills & Commands exist in packed archive
115
+ if (typeof plugin.skills !== "string") throw new Error("plugin.json missing skills declaration");
116
+ const skillEntry = entries.get("package/skills/claude-intercom/SKILL.md");
117
+ if (!skillEntry) throw new Error("Packed adapter missing skills/claude-intercom/SKILL.md");
118
+
119
+ if (typeof plugin.commands !== "string") throw new Error("plugin.json missing commands declaration");
120
+ const commandEntry1 = entries.get("package/commands/intercom.md");
121
+ const commandEntry2 = entries.get("package/commands/intercom-id.md");
122
+ if (!commandEntry1 || !commandEntry2) throw new Error("Packed adapter missing commands markdown files");
123
+
124
+ // 5. Assert all dist bundles exist and adhere to Core externalization
125
+ for (const bundle of bundles) {
126
+ const entry = entries.get(`package/dist/${bundle}`);
127
+ if (!entry) throw new Error(`Packed adapter is missing dist/${bundle}`);
128
+ const source = entry.toString("utf8");
129
+ if (source.includes("node_modules/@ctliz/agent-intercom-core/") || /var POLICY_SEMANTICS_VERSION\s*=/.test(source)) {
130
+ throw new Error(`Packed dist/${bundle} embeds a private Core copy`);
131
+ }
132
+ if (coreConsumers.has(bundle) && !/from ["']@ctliz\/agent-intercom-core(?:\/[A-Za-z/-]+)?["']/.test(source)) {
133
+ throw new Error(`Packed dist/${bundle} does not retain its Core peer import`);
134
+ }
135
+ }
136
+
137
+ console.log(`Verified complete plugin+MCP+monitors+dist chain and ${bundles.length} packed bundles using Core peer 0.1.0`);
138
+ } finally {
139
+ if (temp) rmSync(temp, { recursive: true, force: true });
140
+ }
@@ -0,0 +1,90 @@
1
+ ---
2
+ name: claude-intercom
3
+ description: |
4
+ Coordinate with local Pi, Codex, or Claude coding-agent sessions through
5
+ claude-intercom MCP tools. Use for same-machine planner-worker workflows,
6
+ direct peer questions, cross-session context sharing, and checking or
7
+ replying to pending asks.
8
+ ---
9
+
10
+ # Claude Intercom
11
+
12
+ Use this skill when you need to coordinate with another local coding-agent
13
+ session through the `claude-intercom` MCP server. It speaks the same broker
14
+ protocol as `pi-intercom` and `codex-intercom`, so Pi, Codex, and Claude
15
+ sessions can all message each other.
16
+
17
+ A plain Claude Code MCP session does not receive unsolicited turns. Inbound
18
+ messages are queued by the MCP server while it is running. Check
19
+ `intercom_pending` at natural boundaries: before starting delegated work, and
20
+ when you expect a response.
21
+
22
+ For true wake-on-message behavior, use or recommend a **wakeable worker**
23
+ (`cci` or `claude-intercom-worker`). Messages to a worker start a fresh
24
+ headless `claude -p` turn that resumes the worker's own conversation, so the
25
+ worker can read files, run commands, and reply on its own. Normal `cci` workers
26
+ automatically load the packaged Intercom MCP server, including under isolated
27
+ proxy-backed Claude profiles; `ccim` safe mode intentionally does not.
28
+
29
+ ## Tools
30
+
31
+ - `intercom_whoami`: show this session's intercom ID, name, cwd, and model.
32
+ - `intercom_team`: show the current orchestrator manager and live same-manager coworkers.
33
+ - `intercom_status`: connection, active session count, unread messages, pending asks.
34
+ - `intercom_list`: list connected Pi, Codex, and Claude sessions.
35
+ - `intercom_set_summary`: publish a short discoverable status.
36
+ - `intercom_send`: fire-and-forget direct message.
37
+ - `intercom_ask`: send a question and wait for the target's reply (blocking, bounded).
38
+ - `intercom_pending`: read queued inbound messages and pending asks.
39
+ - `intercom_reply`: reply to a pending inbound ask.
40
+
41
+ ## Workflow
42
+
43
+ 1. Call `intercom_status` or `intercom_whoami` to confirm this session is connected.
44
+ 2. Call `intercom_set_summary` with a concise status so peers can discover your role.
45
+ 3. If this is an orchestrator-owned coworker, call `intercom_team` to get the manager and sibling targets without searching globally. Otherwise use `intercom_list`.
46
+ 4. Use `intercom_send` for non-blocking updates and handoffs.
47
+ 5. Use `intercom_ask` only when you need the answer before continuing. Assignments, progress/status checkpoints, notifications, and completion reports use `intercom_send`.
48
+ 6. Call `intercom_pending` before ending a coordination turn, then answer blocking asks with `intercom_reply`.
49
+
50
+ ## Patterns
51
+
52
+ Planner delegates to a wakeable worker without blocking:
53
+
54
+ ```typescript
55
+ intercom_send({
56
+ to: "worker",
57
+ message: "Task-3: add retry logic in src/api/client.ts. Ask if the retry scope is unclear."
58
+ })
59
+ ```
60
+
61
+ Ask a worker and wait for its answer (the worker's final message comes back as the reply):
62
+
63
+ ```typescript
64
+ intercom_ask({
65
+ to: "worker",
66
+ message: "Inspect the failing test in test/auth.test.ts and reply with the most likely cause."
67
+ })
68
+ ```
69
+
70
+ Reply to an inbound ask:
71
+
72
+ ```typescript
73
+ intercom_pending({ mark_read: false })
74
+ intercom_reply({ message: "Use GET/PUT/DELETE only, max 3 retries." })
75
+ ```
76
+
77
+ When multiple asks are pending, select the sender with `to`. If that sender has multiple unresolved asks, select `oldest` or `latest` without using protocol IDs:
78
+
79
+ ```typescript
80
+ intercom_reply({ to: "planner", which: "oldest", message: "Proceed." })
81
+ ```
82
+
83
+ Keep at most one unresolved `intercom_ask` to the same recipient. Use `intercom_send` for non-blocking follow-ups and every progress/status request.
84
+
85
+ ## Boundaries
86
+
87
+ - Do not assume push delivery into a plain MCP session. Check `intercom_pending`.
88
+ - A plain MCP session cannot wake itself from idle. Use a wakeable worker (`cci`) for wake-on-message.
89
+ - Do not use `intercom_ask` to poll files, ports, or process completion — use normal shell checks.
90
+ - Keep messages concise; include file paths, command-output summaries, and decision options when useful.
package/types.ts ADDED
@@ -0,0 +1,212 @@
1
+ import type {
2
+ BossControlEnvelope,
3
+ BossParticipantBinding,
4
+ BossParticipantCredentialEnvelope,
5
+ BossPrivatePrincipal,
6
+ BossRunFeatureContract,
7
+ BossWorkerIdentityV2,
8
+ BrokerCapabilityAdvertisement,
9
+ ParticipantState,
10
+ } from "@ctliz/agent-intercom-core/boss";
11
+
12
+ export interface BossLiveWorkerAuthority {
13
+ identity: BossWorkerIdentityV2;
14
+ state: ParticipantState;
15
+ }
16
+
17
+ export interface BossSessionMetadata {
18
+ binding: BossParticipantBinding;
19
+ principal: BossPrivatePrincipal;
20
+ /** Broker-authenticated live worker identity, independent of the worker registry projection. */
21
+ liveWorker: BossLiveWorkerAuthority;
22
+ featureContract: BossRunFeatureContract;
23
+ policySemanticsHash: string;
24
+ capabilityDigest: string;
25
+ brokerIdentityVerified: true;
26
+ }
27
+
28
+ export interface BossRegistrationRequest {
29
+ featureContract: BossRunFeatureContract;
30
+ capabilityDigest: string;
31
+ credential: BossParticipantCredentialEnvelope;
32
+ }
33
+
34
+ export interface SessionInfo {
35
+ id: string;
36
+ name?: string;
37
+ cwd: string;
38
+ model: string;
39
+ pid: number;
40
+ startedAt: number;
41
+ lastActivity: number;
42
+ status?: string;
43
+ peerUid?: number;
44
+ trustedLocal?: boolean;
45
+ origin?: "local" | "remote";
46
+ remoteHostId?: string;
47
+ parentSessionId?: string;
48
+ rootSessionId?: string;
49
+ generation?: number;
50
+ canDelegate?: boolean;
51
+ depth?: number;
52
+ maxDepth?: number;
53
+ maxChildren?: number;
54
+ /** Broker-authoritative metadata; absent for every ordinary/legacy session. */
55
+ boss?: BossSessionMetadata;
56
+ }
57
+
58
+ export interface Message {
59
+ id: string;
60
+ timestamp: number;
61
+ replyTo?: string;
62
+ expectsReply?: boolean;
63
+ /** Internal typed control. Model-facing text tools cannot construct this. */
64
+ control?: BossControlEnvelope;
65
+ content: {
66
+ text: string;
67
+ attachments?: Attachment[];
68
+ };
69
+ }
70
+
71
+ export interface Attachment {
72
+ type: "file" | "snippet" | "context";
73
+ name: string;
74
+ content: string;
75
+ language?: string;
76
+ }
77
+
78
+ export type SessionRegistration = Omit<
79
+ SessionInfo,
80
+ "id" | "peerUid" | "trustedLocal" | "origin" | "remoteHostId" | "parentSessionId" | "rootSessionId" | "generation" | "canDelegate" | "depth" | "maxDepth" | "maxChildren" | "boss"
81
+ > & {
82
+ /** Ephemeral identity shared only by reconnects from one live runtime. */
83
+ runtimeInstanceId?: string;
84
+ /** Requested Boss enrollment. The dormant Stage-B broker rejects it closed. */
85
+ boss?: BossRegistrationRequest;
86
+ };
87
+
88
+ export interface RemoteEnrollmentAccess {
89
+ enrollmentToken: string;
90
+ }
91
+
92
+ export interface RemoteSessionAccess {
93
+ sessionCredential: string;
94
+ sessionId: string;
95
+ generation: number;
96
+ }
97
+
98
+ export type RemoteRegistrationAccess = RemoteEnrollmentAccess | RemoteSessionAccess;
99
+
100
+ export interface RemoteAccessMetadata {
101
+ origin: "remote";
102
+ remoteHostId: string;
103
+ parentSessionId: string;
104
+ rootSessionId: string;
105
+ generation: number;
106
+ canDelegate: boolean;
107
+ depth: number;
108
+ maxDepth: number;
109
+ maxChildren: number;
110
+ sessionCredential?: string;
111
+ }
112
+
113
+ export interface RemotePrincipalSummary {
114
+ id: string;
115
+ name: string;
116
+ parentSessionId: string;
117
+ rootSessionId: string;
118
+ remoteHostId: string;
119
+ generation: number;
120
+ policy: "remote-tree";
121
+ canDelegate: boolean;
122
+ depth: number;
123
+ maxDepth: number;
124
+ maxChildren: number;
125
+ state: "active" | "revoked";
126
+ expiresAt: number;
127
+ createdAt: number;
128
+ updatedAt: number;
129
+ connected: boolean;
130
+ }
131
+
132
+ export interface RemoteAccessContract {
133
+ feature: "remote-access-v1";
134
+ policySemanticsVersion: number;
135
+ policySemanticsHash: string;
136
+ }
137
+
138
+ export const DELIVERY_FAILURE_CODES = [
139
+ "INVALID_MESSAGE",
140
+ "SESSION_NOT_FOUND",
141
+ "AMBIGUOUS_TARGET",
142
+ "SENDER_NOT_FOUND",
143
+ "INVALID_REPLY_TARGET",
144
+ "MUTUAL_ASK",
145
+ "ASK_ALREADY_PENDING",
146
+ "DUPLICATE_MESSAGE_ID",
147
+ "CONFLICTING_MESSAGE_ID",
148
+ "TOO_MANY_PENDING_DELIVERIES",
149
+ "TOO_MANY_PENDING_ASKS",
150
+ "RECIPIENT_DISCONNECTED",
151
+ "SENDER_DISCONNECTED",
152
+ "DELIVERY_TIMEOUT",
153
+ "CONTROL_DISPATCH_UNAVAILABLE",
154
+ ] as const;
155
+
156
+ export type DeliveryFailureCode = (typeof DELIVERY_FAILURE_CODES)[number];
157
+
158
+ export type BrokerErrorCode =
159
+ | "PROTOCOL_MISMATCH"
160
+ | "INVALID_REQUEST"
161
+ | "SESSION_ID_IN_USE"
162
+ | "ACCESS_DENIED"
163
+ | "BOSS_FEATURE_UNAVAILABLE"
164
+ | "REMOTE_ACCESS_INCOMPATIBLE"
165
+ | "RATE_LIMITED"
166
+ | "TOO_MANY_SESSIONS";
167
+
168
+ export type AskCancellationReason =
169
+ | "cancelled"
170
+ | "expired"
171
+ | "delivery_failed"
172
+ | "session_disconnected"
173
+ | "authorization_revoked";
174
+
175
+ export type ClientMessage =
176
+ | { type: "health"; requestId: string; stateId?: string }
177
+ | { type: "register"; protocol: string; version: number; session: SessionRegistration; sessionId?: string; stateId?: string; access?: RemoteRegistrationAccess; scopeId?: string }
178
+ | { type: "access_control"; requestId: string; adminToken: string; action: "issue_enrollment"; enrollment: { name: string; parentSessionId: string; rootSessionId: string; remoteHostId: string; ttlMs?: number; expiresAt?: number; canDelegate?: boolean; maxDepth?: number; maxChildren?: number } }
179
+ | { type: "access_control"; requestId: string; adminToken: string; action: "revoke_subtree"; principalId: string }
180
+ | { type: "access_control"; requestId: string; adminToken: string; action: "inspect_tree"; principalId: string }
181
+ | { type: "access_control"; requestId: string; adminToken: string; action: "adopt_subtree"; principalId: string; newParentSessionId: string }
182
+ | { type: "access_control"; requestId: string; access: RemoteSessionAccess; action: "issue_child_enrollment"; enrollment: { name: string; ttlMs?: number; expiresAt?: number; canDelegate?: boolean; maxDepth?: number; maxChildren?: number } }
183
+ | { type: "access_control"; requestId: string; access: RemoteSessionAccess; action: "inspect_tree"; principalId?: string }
184
+ | { type: "unregister"; preserveAsks?: boolean }
185
+ | { type: "list"; requestId: string }
186
+ | { type: "send"; to: string; message: Message }
187
+ | { type: "message_received"; deliveryId: string }
188
+ | { type: "message_rejected"; deliveryId: string; code: "CONFLICTING_MESSAGE_ID"; reason: string }
189
+ | { type: "defer_ask"; requestId: string; messageId: string }
190
+ | { type: "cancel_ask"; requestId: string; messageId: string }
191
+ | { type: "presence"; name?: string; status?: string; model?: string };
192
+
193
+ export type BrokerMessage =
194
+ | { type: "health_ok"; requestId: string; protocol: string; version: number; endpoint: "local" | "remote"; remoteAccess?: RemoteAccessContract; capabilities?: BrokerCapabilityAdvertisement }
195
+ | { type: "registered"; sessionId: string; protocol: string; version: number; remoteAccess?: RemoteAccessContract; access?: RemoteAccessMetadata; capabilities?: BrokerCapabilityAdvertisement; boss?: BossSessionMetadata }
196
+ | { type: "access_control_result"; requestId: string; action: "issue_enrollment"; enrollmentToken: string; expiresAt: number }
197
+ | { type: "access_control_result"; requestId: string; action: "revoke_subtree"; changedPrincipalIds: string[] }
198
+ | { type: "access_control_result"; requestId: string; action: "inspect_tree"; principals: RemotePrincipalSummary[] }
199
+ | { type: "access_control_result"; requestId: string; action: "adopt_subtree"; principals: RemotePrincipalSummary[] }
200
+ | { type: "access_control_result"; requestId: string; action: "issue_child_enrollment"; enrollmentToken: string; expiresAt: number; parentSessionId: string }
201
+ | { type: "sessions"; requestId: string; sessions: SessionInfo[] }
202
+ | { type: "message"; deliveryId: string; from: SessionInfo; message: Message }
203
+ | { type: "presence_update"; session: SessionInfo }
204
+ | { type: "session_joined"; session: SessionInfo }
205
+ | { type: "session_left"; sessionId: string }
206
+ | { type: "error"; code: BrokerErrorCode; error: string }
207
+ | { type: "delivery_accepted"; messageId: string; deliveryId: string }
208
+ | { type: "delivered"; messageId: string; deliveryId: string }
209
+ | { type: "delivery_failed"; messageId: string; accepted: boolean; code: DeliveryFailureCode; reason: string }
210
+ | { type: "ask_deferred"; messageId: string; fromSessionId: string }
211
+ | { type: "ask_cancelled"; messageId: string; fromSessionId: string; reason: AskCancellationReason }
212
+ | { type: "ask_control_result"; requestId: string; action: "defer" | "cancel"; messageId: string; applied: boolean };