agent-comms 8.15.9 → 8.15.11

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 (189) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +1 -1
  4. package/dist/bridges/user/web/server.d.ts.map +1 -1
  5. package/dist/bridges/user/web/server.js +44 -24
  6. package/dist/bridges/user/web/server.js.map +1 -1
  7. package/dist/bridges/user/web/test/web-assets.unit.test.d.ts +5 -0
  8. package/dist/bridges/user/web/test/web-assets.unit.test.d.ts.map +1 -0
  9. package/dist/bridges/user/web/test/web-assets.unit.test.js +90 -0
  10. package/dist/bridges/user/web/test/web-assets.unit.test.js.map +1 -0
  11. package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.d.ts +2 -0
  12. package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.d.ts.map +1 -0
  13. package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.js +75 -0
  14. package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.js.map +1 -0
  15. package/dist/bridges/user/web/web-assets.d.ts +28 -0
  16. package/dist/bridges/user/web/web-assets.d.ts.map +1 -0
  17. package/dist/bridges/user/web/web-assets.js +67 -0
  18. package/dist/bridges/user/web/web-assets.js.map +1 -0
  19. package/dist/test/pi-manifest-packed.integration.test.d.ts +2 -0
  20. package/dist/test/pi-manifest-packed.integration.test.d.ts.map +1 -0
  21. package/dist/test/pi-manifest-packed.integration.test.js +68 -0
  22. package/dist/test/pi-manifest-packed.integration.test.js.map +1 -0
  23. package/package.json +8 -2
  24. package/src/account-cli.ts +195 -0
  25. package/src/account-join-cli.ts +191 -0
  26. package/src/bridges/cc-peer/approval-commands.ts +82 -0
  27. package/src/bridges/cc-peer/bridge.ts +138 -0
  28. package/src/bridges/cc-peer/default-front.ts +58 -0
  29. package/src/bridges/cc-peer/front-controller.ts +160 -0
  30. package/src/bridges/cc-peer/front-relay.ts +191 -0
  31. package/src/bridges/cc-peer/front-runtime.ts +167 -0
  32. package/src/bridges/cc-peer/front.ts +59 -0
  33. package/src/bridges/cc-peer/reply-aliases.ts +90 -0
  34. package/src/bridges/cc-peer/run.ts +100 -0
  35. package/src/bridges/cc-peer/shared-peer.ts +28 -0
  36. package/src/bridges/claude-code/channel.ts +318 -0
  37. package/src/bridges/codex/tool.ts +132 -0
  38. package/src/bridges/mcp/server.ts +137 -0
  39. package/src/bridges/opencode/plugin.ts +130 -0
  40. package/src/bridges/pi/extension.ts +330 -0
  41. package/src/bridges/registry.ts +17 -0
  42. package/src/bridges/user/cli.ts +87 -0
  43. package/src/bridges/user/controller.ts +345 -0
  44. package/src/bridges/user/tui.ts +300 -0
  45. package/src/bridges/user/web/actions.ts +173 -0
  46. package/src/bridges/user/web/contract.ts +222 -0
  47. package/src/bridges/user/web/dist/bundle.js +49 -0
  48. package/src/bridges/user/web/dist/icons/icon-192x192.svg +9 -0
  49. package/src/bridges/user/web/dist/icons/icon-512x512.svg +9 -0
  50. package/src/bridges/user/web/dist/icons/icon-96x96.svg +9 -0
  51. package/src/bridges/user/web/dist/index.html +21 -0
  52. package/src/bridges/user/web/dist/manifest.json +26 -0
  53. package/src/bridges/user/web/dist/mesh-worker.js +64 -0
  54. package/src/bridges/user/web/dist/sw.js +1 -0
  55. package/src/bridges/user/web/event-publisher.ts +114 -0
  56. package/src/bridges/user/web/port-discovery.ts +49 -0
  57. package/src/bridges/user/web/reads.ts +35 -0
  58. package/src/bridges/user/web/router.ts +158 -0
  59. package/src/bridges/user/web/server.ts +782 -0
  60. package/src/bridges/user/web/vite.config.ts +71 -0
  61. package/src/bridges/user/web/web-access.ts +404 -0
  62. package/src/bridges/user/web/web-assets.ts +90 -0
  63. package/src/bridges/user/web/web-beacon.ts +69 -0
  64. package/src/bridges/user/web/web-console-static.ts +90 -0
  65. package/src/cli-main.ts +635 -0
  66. package/src/cli.ts +13 -0
  67. package/src/core/account-bundle.ts +264 -0
  68. package/src/core/account-join.ts +289 -0
  69. package/src/core/account-ledger-crypto.ts +154 -0
  70. package/src/core/account-ledger-store.ts +66 -0
  71. package/src/core/account-ledger.ts +385 -0
  72. package/src/core/agent-details.ts +216 -0
  73. package/src/core/agent-list-format.ts +97 -0
  74. package/src/core/agent-registry.ts +282 -0
  75. package/src/core/agent-table.ts +58 -0
  76. package/src/core/atomic-file.ts +46 -0
  77. package/src/core/bind-retry.ts +38 -0
  78. package/src/core/bridge-mesh.ts +176 -0
  79. package/src/core/bridge.ts +718 -0
  80. package/src/core/capability-ask.ts +162 -0
  81. package/src/core/capability-bubble-up.ts +237 -0
  82. package/src/core/comms-store.ts +130 -0
  83. package/src/core/connection-approval.ts +134 -0
  84. package/src/core/connection-code-tool.ts +92 -0
  85. package/src/core/connection-code.ts +296 -0
  86. package/src/core/coordinator-role.ts +224 -0
  87. package/src/core/data-directory.ts +32 -0
  88. package/src/core/data-frame-routing.ts +309 -0
  89. package/src/core/default-command.ts +12 -0
  90. package/src/core/delegation-policy.ts +41 -0
  91. package/src/core/delivery-engine.ts +649 -0
  92. package/src/core/device-membership-verification.ts +125 -0
  93. package/src/core/device-membership.ts +130 -0
  94. package/src/core/directory-admission.ts +155 -0
  95. package/src/core/discovery-mdns.ts +247 -0
  96. package/src/core/discovery-tailscale.ts +224 -0
  97. package/src/core/discovery.ts +303 -0
  98. package/src/core/display-name.ts +73 -0
  99. package/src/core/dm-grant-actions.ts +72 -0
  100. package/src/core/dm-send-delegation.ts +73 -0
  101. package/src/core/dm-token-verification.ts +76 -0
  102. package/src/core/election-sessions.ts +135 -0
  103. package/src/core/error-reporting.ts +85 -0
  104. package/src/core/first-contact.ts +242 -0
  105. package/src/core/gateway-trust-actions.ts +184 -0
  106. package/src/core/gateway-trust.ts +305 -0
  107. package/src/core/gossip-directory.ts +276 -0
  108. package/src/core/gossip-extensions.ts +123 -0
  109. package/src/core/group-proofs.ts +205 -0
  110. package/src/core/handshake.ts +354 -0
  111. package/src/core/hub-forwarding.ts +24 -0
  112. package/src/core/hub-link.ts +176 -0
  113. package/src/core/hub-session.ts +374 -0
  114. package/src/core/identity-store.ts +715 -0
  115. package/src/core/identity.ts +489 -0
  116. package/src/core/index.ts +65 -0
  117. package/src/core/issuer-grouping.ts +165 -0
  118. package/src/core/issuer-identity-file.ts +187 -0
  119. package/src/core/lan-web-actions.ts +34 -0
  120. package/src/core/lan-web-uis.ts +55 -0
  121. package/src/core/ledger-lock.ts +81 -0
  122. package/src/core/listener-registry.ts +133 -0
  123. package/src/core/loopback-address.ts +22 -0
  124. package/src/core/machine-identity.ts +77 -0
  125. package/src/core/machine-list-groups.ts +53 -0
  126. package/src/core/mcp-registry-retry.ts +69 -0
  127. package/src/core/mcp-registry-version.ts +18 -0
  128. package/src/core/membership-proof.ts +139 -0
  129. package/src/core/membership-proofs.ts +149 -0
  130. package/src/core/mesh-graph-trace-tool.ts +81 -0
  131. package/src/core/mesh-graph.ts +89 -0
  132. package/src/core/mesh-network-actions.ts +183 -0
  133. package/src/core/mesh-store-options.ts +23 -0
  134. package/src/core/mesh-store-shared.ts +134 -0
  135. package/src/core/mesh-store.ts +1114 -0
  136. package/src/core/name-claim.ts +145 -0
  137. package/src/core/naming-actions.ts +90 -0
  138. package/src/core/naming.ts +138 -0
  139. package/src/core/nanoid.ts +20 -0
  140. package/src/core/network-interfaces.ts +24 -0
  141. package/src/core/package-version.ts +34 -0
  142. package/src/core/peer-lifecycle.ts +377 -0
  143. package/src/core/peer-link-error.ts +41 -0
  144. package/src/core/peer-versions.ts +98 -0
  145. package/src/core/pending-connection.ts +53 -0
  146. package/src/core/petnames.ts +121 -0
  147. package/src/core/pgp-keyserver.ts +32 -0
  148. package/src/core/pid-lock-file.ts +122 -0
  149. package/src/core/push-manager.ts +76 -0
  150. package/src/core/random-id.ts +9 -0
  151. package/src/core/receiver-set.ts +42 -0
  152. package/src/core/relay-role.ts +182 -0
  153. package/src/core/relay-selection.ts +120 -0
  154. package/src/core/relay-server.ts +198 -0
  155. package/src/core/relay-uplink.ts +78 -0
  156. package/src/core/request-timeouts.ts +37 -0
  157. package/src/core/room-discovery.ts +48 -0
  158. package/src/core/room-lifecycle.ts +838 -0
  159. package/src/core/room-lookup.ts +41 -0
  160. package/src/core/room-messaging.ts +247 -0
  161. package/src/core/room-notice-log.ts +61 -0
  162. package/src/core/room-path.ts +84 -0
  163. package/src/core/room-protocol.ts +893 -0
  164. package/src/core/room-router.ts +180 -0
  165. package/src/core/room-token-verification.ts +85 -0
  166. package/src/core/room-wire-extensions.ts +60 -0
  167. package/src/core/self-name-claim.ts +67 -0
  168. package/src/core/send-outcome.ts +69 -0
  169. package/src/core/shutdown-signals.ts +57 -0
  170. package/src/core/stale-agent-checker.ts +108 -0
  171. package/src/core/store.ts +704 -0
  172. package/src/core/token-text.ts +40 -0
  173. package/src/core/tool.ts +853 -0
  174. package/src/core/transport-events.ts +73 -0
  175. package/src/core/transport-queries.ts +54 -0
  176. package/src/core/transport.ts +367 -0
  177. package/src/core/types.ts +550 -0
  178. package/src/core/user-identity.ts +290 -0
  179. package/src/core/vapid.ts +81 -0
  180. package/src/core/version-check.ts +147 -0
  181. package/src/core/version-report-actions.ts +79 -0
  182. package/src/core/web-beacon.ts +31 -0
  183. package/src/core/web-push.ts +322 -0
  184. package/src/core/wire-mesh-core-version.ts +25 -0
  185. package/src/core/wire-mesh-identity.ts +36 -0
  186. package/src/core/wire-mesh-transport-options.ts +82 -0
  187. package/src/core/wire-mesh-transport.ts +1007 -0
  188. package/src/core/wire-protocol.ts +153 -0
  189. package/src/core/ws-dial.ts +191 -0
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Real cc-peer/MeshStore construction for the default coordinator-run front (agent-comms#157) -- the counterpart to bridges/cc-peer/run.ts's own real CcPeer.create()/createBridgeMesh construction for the one-shot `bridge cc-peer` command. Wires front-controller.ts's CcPeerFront (attach/detach diffing across ticks) to front-relay.ts's buildFrontedSessionRecord/detachFrontedSession (one session's own relay) against one shared, front-wide CcPeer instance and a fresh per-session MeshStore built from loadIdentityForFront's lock-free identity.
3
+ *
4
+ * Untested directly, exactly like run.ts's own real construction (see cc-peer-bridge.test.ts's header comment): front.ts, front-controller.ts, and front-relay.ts already carry the front's entire decision/diffing/relay logic under direct DI-based unit tests, so this file's only remaining job -- calling the real cc-peer/core APIs in the right order -- is exercised by actually running a bridge as this machine's coordinator.
5
+ */
6
+
7
+ import { parseDisplayName } from "../../core/display-name.js";
8
+ import { CcPeer, CC_PEER_VERSION } from "cc-peer";
9
+ import { AliasPool } from "cc-peer/alias-pool";
10
+ import type { AliasMessage } from "cc-peer/alias-pool";
11
+ import {
12
+ createBridgeMeshFromIdentity,
13
+ ensureRegistered,
14
+ ensureProjectRoom,
15
+ } from "../../core/index.js";
16
+ import {
17
+ loadIdentityForFront,
18
+ probeSlotOwner,
19
+ } from "../../core/identity-store.js";
20
+ import { CcPeerFront } from "./front-controller.js";
21
+ import { computeFrontSlot } from "./front.js";
22
+ import type { CcPeerRosterEntryLike } from "./front.js";
23
+ import type { CcPeerInboundMessage } from "./bridge.js";
24
+ import {
25
+ buildFrontedSessionRecord,
26
+ detachFrontedSession,
27
+ type FrontedRelayRecord,
28
+ type FrontRelayPeer,
29
+ } from "./front-relay.js";
30
+ import { ReplyAliasDirectory } from "./reply-aliases.js";
31
+ import { SharedPeer } from "./shared-peer.js";
32
+
33
+ /** cc-peer's own registered display name for the front's shared peer -- distinct from any individual fronted session's own agent-comms display name (front-relay.ts's ensureRegistered call uses the session's own cc-peer name/pid for that). */
34
+ const FRONT_PEER_NAME = "agent-comms-front";
35
+
36
+ /** The slice of a cc-peer peer the front uses: reading the local roster, receiving messages, and sending to a fronted session. The real CcPeer satisfies it. */
37
+ export interface FrontCcPeer extends FrontRelayPeer {
38
+ roster: () => Promise<readonly CcPeerRosterEntryLike[]>;
39
+ on: (
40
+ event: "message",
41
+ listener: (message: Readonly<CcPeerInboundMessage>) => void,
42
+ ) => unknown;
43
+ stop: () => Promise<void>;
44
+ }
45
+
46
+ export interface CreateDefaultCcPeerFrontOptions {
47
+ coordinatorPort?: number | undefined;
48
+ hubUrl?: string | undefined;
49
+ pollIntervalMs?: number | undefined;
50
+ onError?: ((error: Error) => void) | undefined;
51
+ /** The cc-peer peer this process already owns, when it has one. cc-peer allows one peer per process, so a process that runs its own (the one-shot `bridge cc-peer` command) must lend it here rather than let the front try to create a second. The front only borrows it: it never stops it, and registers its inbound listener on it once. Without this the front creates and owns its own peer. */
52
+ peer?: FrontCcPeer | undefined;
53
+ /** The name the shared peer is registered under, which fronted sessions are told to message to answer a join request. Defaults to the front's own peer name; a host process that lends its own peer names it here. */
54
+ peerName?: string | undefined;
55
+ /** Sessions this process already relays by other means, which the front must therefore leave alone. */
56
+ excludeSession?:
57
+ ((entry: Readonly<CcPeerRosterEntryLike>) => boolean) | undefined;
58
+ }
59
+
60
+ /** The session's own cc-peer display name when it is usable as an agent name (parseDisplayName, which registration enforces), otherwise "claude-code-<pid>": ensureRegistered requires a defaultName, and a session that never picked a name, or picked one registration would refuse, is still worth fronting under something stable and identifiable. */
61
+ function defaultSessionName(entry: Readonly<CcPeerRosterEntryLike>): string {
62
+ const name =
63
+ entry.name === undefined ? undefined : parseDisplayName(entry.name);
64
+ return name ?? `claude-code-${String(entry.pid)}`;
65
+ }
66
+
67
+ /**
68
+ * Builds the default cc-peer front. start()/stop() are safe to call from bridge-mesh.ts's onCoordinatorRoleChanged hook regardless of whether cc-peer itself is usable on this machine: the shared CcPeer instance is created lazily, on the first poll tick that actually needs it (listRoster), not at start() itself, and any construction or attach failure is reported via onError rather than thrown -- "absent sockets" (no local Claude Code sessions at all) is then a clean no-op front rather than a crash of the coordinator that owns it.
69
+ */
70
+ export function createDefaultCcPeerFront(
71
+ options: Readonly<CreateDefaultCcPeerFrontOptions> = {},
72
+ ): Pick<CcPeerFront<FrontedRelayRecord>, "start" | "stop"> {
73
+ const borrowedPeer = options.peer;
74
+ const peerName = options.peerName ?? FRONT_PEER_NAME;
75
+ let aliasPool: AliasPool | undefined;
76
+ const aliasDirectory = new ReplyAliasDirectory();
77
+
78
+ const front = new CcPeerFront<FrontedRelayRecord>({
79
+ listRoster: async () => {
80
+ const peer = await ensureSharedPeer();
81
+ return peer.roster();
82
+ },
83
+ probeSlotOwner,
84
+ attach: async (entry) => attachSession(entry, await ensureSharedPeer()),
85
+ detach: detachFrontedSession,
86
+ aliasDirectory,
87
+ excludeSession: options.excludeSession,
88
+ pollIntervalMs: options.pollIntervalMs,
89
+ onError: options.onError,
90
+ });
91
+
92
+ const sharedPeer = new SharedPeer<FrontCcPeer>(async () => {
93
+ if (borrowedPeer !== undefined) return borrowedPeer;
94
+ const peer = await CcPeer.create({ name: FRONT_PEER_NAME });
95
+ peer.on("message", (message: Readonly<CcPeerInboundMessage>) => {
96
+ front.handleInboundMessage(message);
97
+ });
98
+ return peer;
99
+ });
100
+ // A borrowed peer outlives the front's own start/stop cycles, so its listener is attached once here rather than on every (re)creation, which would relay each inbound message once per restart.
101
+ borrowedPeer?.on("message", (message: Readonly<CcPeerInboundMessage>) => {
102
+ front.handleInboundMessage(message);
103
+ });
104
+
105
+ return {
106
+ start: () => {
107
+ front.start();
108
+ },
109
+ stop: async () => {
110
+ await front.stop();
111
+ const peer = await sharedPeer.release();
112
+ if (borrowedPeer === undefined) await peer?.stop();
113
+ await aliasPool?.stopAll();
114
+ aliasPool = undefined;
115
+ },
116
+ };
117
+
118
+ /** Materialises the front's own shared AliasPool on first use (mirroring ensureSharedPeer's own lazy-construction convention) and wires its "message" event, every reply arriving on any fronted session's own correspondent aliases, straight into the controller's handleAliasMessage, which resolves the sending session and the alias's own correspondent before routing it on. The pool is also what delivers a correspondent's own messages into a fronted session, from that correspondent's alias, so the session's reply comes back on this same "message" event. */
119
+ function ensureAliasPool(): AliasPool {
120
+ if (aliasPool) return aliasPool;
121
+ const pool = AliasPool.create();
122
+ pool.on("message", (message: Readonly<AliasMessage>) => {
123
+ front.handleAliasMessage(message);
124
+ });
125
+ aliasPool = pool;
126
+ return pool;
127
+ }
128
+
129
+ async function ensureSharedPeer(): Promise<FrontCcPeer> {
130
+ return sharedPeer.get();
131
+ }
132
+
133
+ async function attachSession(
134
+ entry: Readonly<CcPeerRosterEntryLike>,
135
+ peer: Readonly<FrontCcPeer>,
136
+ ): Promise<FrontedRelayRecord> {
137
+ const slot = computeFrontSlot(entry.cwd);
138
+ const identity = loadIdentityForFront(slot);
139
+ const { store, tool } = await createBridgeMeshFromIdentity(identity, slot, {
140
+ coordinatorPort: options.coordinatorPort,
141
+ hubUrl: options.hubUrl,
142
+ });
143
+ store.getCcPeerVersion = () => CC_PEER_VERSION;
144
+
145
+ const reg = await ensureRegistered({
146
+ store,
147
+ harness: "claude-code",
148
+ cwd: entry.cwd,
149
+ defaultName: defaultSessionName(entry),
150
+ visibility: "visible",
151
+ });
152
+ const roomId = await ensureProjectRoom(store, reg.agentId, entry.cwd);
153
+
154
+ return buildFrontedSessionRecord({
155
+ entry,
156
+ peerName,
157
+ agentId: reg.agentId,
158
+ roomId,
159
+ store,
160
+ tool,
161
+ peer,
162
+ aliasPool: ensureAliasPool(),
163
+ aliasDirectory,
164
+ onError: options.onError,
165
+ });
166
+ }
167
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Pure decision logic for the default cc-peer front (agent-comms#157): a machine's coordinator bridge relays every local Claude Code session that doesn't already front itself, using the same (harness, cwd) identity slot that session's own agent-comms bridge would use, so addressing carries over unchanged the moment that session's own bridge appears.
3
+ *
4
+ * Kept free of any real cc-peer/MeshStore construction so it's testable without a real local Claude Code session, a real filesystem, or real sockets -- see front-runtime.ts for the stateful controller and the real wiring built on top of these functions.
5
+ */
6
+
7
+ import type { IdentitySlot } from "../../core/identity-store.js";
8
+
9
+ /** The slice of cc-peer's own RegistryEntry this module needs. Narrowed rather than importing cc-peer's type directly so this file has no compile-time dependency on the cc-peer package (only front-runtime.ts, which does the real construction, needs that). */
10
+ export interface CcPeerRosterEntryLike {
11
+ pid: number;
12
+ cwd: string;
13
+ name?: string | undefined;
14
+ /** cc-peer's own registry entries for library-backed peers (this front itself, the one-shot `bridge cc-peer` command, an alias-pool worker) always carry the literal string "cc-peer" here -- see buildRegistryEntry in cc-peer's own source. A real interactive Claude Code session, which registers itself natively rather than through the cc-peer library, reports its own Claude Code version instead. */
15
+ version: string;
16
+ messagingSocketPath: string;
17
+ }
18
+
19
+ /** True for a roster entry that is itself a cc-peer-library-backed peer (this front's own shared identity, the one-shot bridge command, an alias-pool worker) rather than a real interactive Claude Code session -- see CcPeerRosterEntryLike.version's own doc comment for the mechanism. The front must never try to front one of these: it would compute a spurious claude-code identity slot for a process that isn't a Claude Code session at all, and relay cc-peer traffic into it that nothing there ever reads. */
20
+ export function isCcPeerLibraryPeer(
21
+ entry: Readonly<CcPeerRosterEntryLike>,
22
+ ): boolean {
23
+ return entry.version === "cc-peer";
24
+ }
25
+
26
+ /** The identity slot a session's own agent-comms bridge would hold if it started right now -- the same (harness, cwd) pair every real claude-code bridge entry point (bridges/claude-code/channel.ts) already constructs. */
27
+ export function computeFrontSlot(cwd: string): IdentitySlot {
28
+ return { harness: "claude-code", cwd };
29
+ }
30
+
31
+ /**
32
+ * Filters a cc-peer roster down to the real Claude Code sessions this front should attach to right now: excludes other cc-peer-library peers, and excludes any session whose own slot is already held by a live PID (that session already fronts itself, via its own agent-comms bridge). probeSlotOwner is injected rather than imported directly so this stays a pure function over its inputs -- the real probe (identity-store.ts's probeSlotOwner) reads the filesystem, which has no place in a decision function tested purely over data.
33
+ */
34
+ export function selectSessionsToFront(
35
+ roster: readonly CcPeerRosterEntryLike[],
36
+ probeSlotOwner: (slot: Readonly<IdentitySlot>) => number | undefined,
37
+ ): CcPeerRosterEntryLike[] {
38
+ return roster.filter((entry) => {
39
+ if (isCcPeerLibraryPeer(entry)) return false;
40
+ const slot = computeFrontSlot(entry.cwd);
41
+ return probeSlotOwner(slot) === undefined;
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Finds which currently-fronted session an inbound cc-peer message came from, by matching the envelope's own `from` field (always the literal string "uds:" followed by the sender's own listening socket path, per cc-peer's own send() -- see CcPeer.send in cc-peer's source) against each fronted session's messagingSocketPath. Generic over the fronted-session record type so front-runtime.ts's real records (which carry a live MeshStore/CommsTool alongside the roster entry) can be matched directly without this module needing to know their shape.
47
+ */
48
+ export function matchInboundMessageSession<
49
+ T extends { readonly messagingSocketPath: string },
50
+ >(
51
+ fronted: Readonly<Iterable<T>>,
52
+ message: Readonly<{ from?: string }>,
53
+ ): T | undefined {
54
+ if (message.from === undefined) return undefined;
55
+ for (const record of fronted) {
56
+ if (message.from === `uds:${record.messagingSocketPath}`) return record;
57
+ }
58
+ return undefined;
59
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Lazy per-correspondent reply aliases for the default cc-peer front (agent-comms#158) — pure alias-name derivation and the in-memory correspondent↔alias directory the front consults on both the mesh-to-session direction (materialise an alias so the session can address a reply to it) and the session-to-mesh direction (translate a reply arriving on an alias back into the correspondent it stands for and where that reply should go).
3
+ *
4
+ * Deliberately not persisted: aliases are ephemeral by design, lost on restart and re-materialised by the next inbound message from that correspondent (see the issue body's "Rejected alternatives" for why a directory of every mesh agent is not minted upfront). Kept free of any real cc-peer/AliasPool construction, exactly like front.ts's pure decision logic — front-relay.ts and front-controller.ts consult this directory, and front-runtime.ts supplies the real AliasPool the alias names get materialised against.
5
+ */
6
+
7
+ import type { DeliveryEvent } from "../../core/types.js";
8
+
9
+ /** How many leading hex characters of a correspondent's device-id go into its alias name -- 48 bits of a SHA-256 hash, chosen purely for a short, readable cc-peer peer name; cc-peer's own registry name field has no length or format restriction beyond non-empty (see AliasStartCommandSchema in cc-peer's alias-ipc schema), so this is a readability choice, not a protocol requirement. */
10
+ const ALIAS_ID_LENGTH = 12;
11
+
12
+ /** cc-peer peer names materialised for reply aliases are prefixed so they read unambiguously as a mesh correspondent, not a real local Claude Code session, in `roster()`/UI listings that show every locally known peer name side by side. */
13
+ const ALIAS_NAME_PREFIX = "mesh-";
14
+
15
+ /** Derives the cc-peer peer name a correspondent's reply alias registers under. Deterministic and side-effect-free: the same correspondent id always derives the same name, so ReplyAliasDirectory only needs to remember the mapping, not invent a fresh name per call. */
16
+ export function deriveAliasName(correspondentId: string): string {
17
+ return `${ALIAS_NAME_PREFIX}${correspondentId.slice(0, ALIAS_ID_LENGTH)}`;
18
+ }
19
+
20
+ /** Where a reply arriving on a correspondent's alias should go back to: a DM straight to that correspondent, or a post back into the room the message that produced the alias was itself sent in. A room's own reply-aliased message always names the room it came from, so a reply lands in the same room rather than always falling back to a DM (agent-comms#289). */
21
+ export type ReplyContext =
22
+ { readonly kind: "dm" } | { readonly kind: "room"; readonly room: string };
23
+
24
+ /** The mesh correspondent and reply context a DeliveryEvent carries, for the event types with a single, repliable-to sender — undefined for every other event type (room membership/status/capability events describe something happening, not a message from one correspondent worth aliasing). */
25
+ export interface ReplyTarget {
26
+ readonly correspondentId: string;
27
+ readonly context: ReplyContext;
28
+ }
29
+
30
+ /** Resolves a DeliveryEvent's own reply target, or undefined when the event has no single originating correspondent. A dm event's reply target is always another dm; a room_message event's reply target is the room the message was posted in, since that is what a reply to it should return to, not the sender's own dm channel (agent-comms#289). */
31
+ export function replyTargetForEvent(
32
+ event: Readonly<DeliveryEvent>,
33
+ ): ReplyTarget | undefined {
34
+ switch (event.type) {
35
+ case "dm":
36
+ return { correspondentId: event.message.from, context: { kind: "dm" } };
37
+ case "room_message":
38
+ return {
39
+ correspondentId: event.message.from,
40
+ context: { kind: "room", room: event.message.room },
41
+ };
42
+ case "room_invite":
43
+ case "member_joined":
44
+ case "member_left":
45
+ case "room_members":
46
+ case "member_status":
47
+ case "delivery_status":
48
+ case "invite_declined":
49
+ case "name_changed":
50
+ case "connection_request":
51
+ case "capability_request":
52
+ case "room_join_request":
53
+ return undefined;
54
+ default:
55
+ return event satisfies never;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Bounded, in-memory, bijective map between the cc-peer peer names materialised for a front's reply aliases and the mesh correspondent id each one stands for, plus the reply context (dm vs. room) each alias currently carries. Bounded by real correspondents, per the issue's own "Rejected alternatives": a name is only ever minted by ensure(), which the front calls solely on genuine inbound contact from that correspondent — nothing pre-populates this directory with the wider mesh roster.
61
+ *
62
+ * A single correspondent id can reach a fronted session two different ways — a direct dm, and a message posted in a room they share with the session — and both share the same alias, since the alias scheme is keyed on the correspondent alone (agent-comms#158). ensure() therefore always records the context of the *most recent* event sent from that alias: a session's reply always continues whatever conversation it was last shown on that alias, so the context that matters is the one behind the last message it actually saw, not whichever context first minted the name.
63
+ */
64
+ export class ReplyAliasDirectory {
65
+ private readonly nameToCorrespondent = new Map<string, string>();
66
+ private readonly correspondentToName = new Map<string, string>();
67
+ private readonly nameToContext = new Map<string, ReplyContext>();
68
+
69
+ /** Returns the alias name for a correspondent, deriving and recording it the first time this correspondent is seen, and records `context` as that alias's current reply target regardless of whether the alias was just minted or already existed. Idempotent on the name itself: a correspondent already known always gets back its existing name, so a session's reply target stays stable across repeated inbound contact within the same front lifetime; the context, however, is refreshed on every call. */
70
+ ensure(correspondentId: string, context: ReplyContext): string {
71
+ const existing = this.correspondentToName.get(correspondentId);
72
+ const name = existing ?? deriveAliasName(correspondentId);
73
+ if (existing === undefined) {
74
+ this.correspondentToName.set(correspondentId, name);
75
+ this.nameToCorrespondent.set(name, correspondentId);
76
+ }
77
+ this.nameToContext.set(name, context);
78
+ return name;
79
+ }
80
+
81
+ /** The correspondent a previously-minted alias name stands for, or undefined for a name this directory never minted (or no longer remembers, e.g. after a front restart cleared it) -- the front's own signal to treat an inbound reply as addressed to a stale alias rather than a known correspondent. */
82
+ correspondentFor(aliasName: string): string | undefined {
83
+ return this.nameToCorrespondent.get(aliasName);
84
+ }
85
+
86
+ /** The reply context a previously-minted alias name currently carries (dm or a named room), or undefined for a name this directory never minted. Always defined whenever correspondentFor(aliasName) is, since both are set together by ensure(). */
87
+ contextFor(aliasName: string): ReplyContext | undefined {
88
+ return this.nameToContext.get(aliasName);
89
+ }
90
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * cc-peer bridge — CLI entry point.
3
+ *
4
+ * Run via: npx agent-comms bridge cc-peer <target-name> npx agent-comms bridge cc-peer --pid=<pid>
5
+ *
6
+ * Constructs the real CcPeer and MeshStore, then hands both to wireCcPeerBridge for the actual relay wiring (see bridge.ts).
7
+ */
8
+
9
+ import { CcPeer, CC_PEER_VERSION } from "cc-peer";
10
+ import {
11
+ createBridgeMesh,
12
+ createMeshErrorReporter,
13
+ ensureRegistered,
14
+ ensureProjectRoom,
15
+ installShutdownSignalHandlers,
16
+ } from "../../core/index.js";
17
+ import type { IdentitySlot } from "../../core/identity-store.js";
18
+ import { releaseIdentityLock } from "../../core/identity-store.js";
19
+ import {
20
+ createTargetSenderMatcher,
21
+ targetMatchesEntry,
22
+ wireCcPeerBridge,
23
+ type CcPeerRef,
24
+ } from "./bridge.js";
25
+ import { wireDefaultCcPeerFront } from "./default-front.js";
26
+
27
+ const PID_FLAG_PREFIX = "--pid=";
28
+ /** argv layout for `node cli.js bridge cc-peer <target-arg>`: index 0/1 are the node binary and script path, 2 is "bridge", 3 is the bridge id ("cc-peer") itself -- this bridge's own args start one past that. */
29
+ const BRIDGE_ARGS_START_INDEX = 4;
30
+ /** The name this command registers its one cc-peer peer under, which the target session is told to message to answer a join request. */
31
+ const BRIDGE_PEER_NAME = "agent-comms-bridge";
32
+
33
+ function parseTarget(argv: readonly string[]): CcPeerRef {
34
+ const arg = argv[0];
35
+ if (arg === undefined || arg === "") {
36
+ console.error(
37
+ "Usage: agent-comms bridge cc-peer <target-name> | --pid=<pid>",
38
+ );
39
+ process.exit(1);
40
+ }
41
+ if (arg.startsWith(PID_FLAG_PREFIX)) {
42
+ const pid = Number(arg.slice(PID_FLAG_PREFIX.length));
43
+ if (!Number.isInteger(pid) || pid <= 0) {
44
+ console.error(`Invalid --pid value: ${arg}`);
45
+ process.exit(1);
46
+ }
47
+ return { pid };
48
+ }
49
+ return { name: arg };
50
+ }
51
+
52
+ export async function run(): Promise<void> {
53
+ const target = parseTarget(process.argv.slice(BRIDGE_ARGS_START_INDEX));
54
+
55
+ const identitySlot: IdentitySlot = { harness: "cc-peer", cwd: process.cwd() };
56
+ // cc-peer allows one peer per process, so this is the only one: the default front borrows it below rather than creating its own, and the front leaves this bridge's own target alone since the relay below already covers it.
57
+ const peer = await CcPeer.create({ name: BRIDGE_PEER_NAME });
58
+ const { store, tool } = await createBridgeMesh(identitySlot);
59
+ store.onError = createMeshErrorReporter();
60
+ store.getCcPeerVersion = () => CC_PEER_VERSION;
61
+ store.onCoordinatorRoleChanged = wireDefaultCcPeerFront(store, {
62
+ peer,
63
+ peerName: BRIDGE_PEER_NAME,
64
+ excludeSession: (entry) => targetMatchesEntry(target, entry),
65
+ });
66
+
67
+ const reg = await ensureRegistered({
68
+ store,
69
+ cwd: process.cwd(),
70
+ harness: "cc-peer",
71
+ defaultName: "cc-peer-bridge",
72
+ });
73
+ const roomId = await ensureProjectRoom(store, reg.agentId, process.cwd());
74
+
75
+ wireCcPeerBridge({
76
+ store,
77
+ tool,
78
+ peer,
79
+ agentId: reg.agentId,
80
+ roomId,
81
+ target,
82
+ cwd: process.cwd(),
83
+ peerName: BRIDGE_PEER_NAME,
84
+ isFromTarget: createTargetSenderMatcher(target, async () => peer.roster()),
85
+ });
86
+
87
+ await store.init();
88
+
89
+ installShutdownSignalHandlers({
90
+ shutdown: async () => {
91
+ await peer.stop();
92
+ await store.setAgentOffline(reg.agentId);
93
+ // After store.shutdown(), not before: the mesh handover this bridge owes its peers rides the sessions shutdown() closes, and the lock is what stops a replacement claiming this slot's identity while that is still in flight.
94
+ await store.shutdown();
95
+ releaseIdentityLock(identitySlot);
96
+ },
97
+ disposition: "exit",
98
+ onError: createMeshErrorReporter(),
99
+ });
100
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * SharedPeer -- a lazily-created, process-wide holder of one cc-peer peer.
3
+ *
4
+ * cc-peer binds one socket per process (`<socket dir>/<pid>.sock`) and registers one session entry per pid, so a second CcPeer.create in the same process fails with EADDRINUSE. Every role in a process that talks through cc-peer (the default front, the one-shot `bridge cc-peer` command) therefore has to go through a single instance.
5
+ */
6
+
7
+ /** Holds at most one peer of type P, created by the injected factory on first use. */
8
+ export class SharedPeer<P> {
9
+ private promise: Promise<P> | undefined;
10
+
11
+ constructor(private readonly create: () => Promise<P>) {}
12
+
13
+ /** The shared peer, creating it on the first call. Concurrent callers share one creation. A failed creation is forgotten, so the next call retries rather than replaying the same rejection forever. */
14
+ async get(): Promise<P> {
15
+ this.promise ??= this.create().catch((error: unknown) => {
16
+ this.promise = undefined;
17
+ throw error;
18
+ });
19
+ return this.promise;
20
+ }
21
+
22
+ /** Forgets the shared peer and returns it so the caller can stop it, or undefined if none was ever successfully created. The next get() creates a fresh one. */
23
+ async release(): Promise<P | undefined> {
24
+ const pending = this.promise;
25
+ this.promise = undefined;
26
+ return pending?.catch(() => undefined);
27
+ }
28
+ }