agent-comms 8.15.9 → 8.15.10

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 (184) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/dist/bridges/user/web/server.d.ts.map +1 -1
  4. package/dist/bridges/user/web/server.js +44 -24
  5. package/dist/bridges/user/web/server.js.map +1 -1
  6. package/dist/bridges/user/web/test/web-assets.unit.test.d.ts +5 -0
  7. package/dist/bridges/user/web/test/web-assets.unit.test.d.ts.map +1 -0
  8. package/dist/bridges/user/web/test/web-assets.unit.test.js +90 -0
  9. package/dist/bridges/user/web/test/web-assets.unit.test.js.map +1 -0
  10. package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.d.ts +2 -0
  11. package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.d.ts.map +1 -0
  12. package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.js +75 -0
  13. package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.js.map +1 -0
  14. package/dist/bridges/user/web/web-assets.d.ts +28 -0
  15. package/dist/bridges/user/web/web-assets.d.ts.map +1 -0
  16. package/dist/bridges/user/web/web-assets.js +67 -0
  17. package/dist/bridges/user/web/web-assets.js.map +1 -0
  18. package/package.json +8 -2
  19. package/src/account-cli.ts +195 -0
  20. package/src/account-join-cli.ts +191 -0
  21. package/src/bridges/cc-peer/approval-commands.ts +82 -0
  22. package/src/bridges/cc-peer/bridge.ts +138 -0
  23. package/src/bridges/cc-peer/default-front.ts +58 -0
  24. package/src/bridges/cc-peer/front-controller.ts +160 -0
  25. package/src/bridges/cc-peer/front-relay.ts +191 -0
  26. package/src/bridges/cc-peer/front-runtime.ts +167 -0
  27. package/src/bridges/cc-peer/front.ts +59 -0
  28. package/src/bridges/cc-peer/reply-aliases.ts +90 -0
  29. package/src/bridges/cc-peer/run.ts +100 -0
  30. package/src/bridges/cc-peer/shared-peer.ts +28 -0
  31. package/src/bridges/claude-code/channel.ts +318 -0
  32. package/src/bridges/codex/tool.ts +132 -0
  33. package/src/bridges/mcp/server.ts +137 -0
  34. package/src/bridges/opencode/plugin.ts +130 -0
  35. package/src/bridges/pi/extension.ts +330 -0
  36. package/src/bridges/registry.ts +17 -0
  37. package/src/bridges/user/cli.ts +87 -0
  38. package/src/bridges/user/controller.ts +345 -0
  39. package/src/bridges/user/tui.ts +300 -0
  40. package/src/bridges/user/web/actions.ts +173 -0
  41. package/src/bridges/user/web/contract.ts +222 -0
  42. package/src/bridges/user/web/dist/bundle.js +49 -0
  43. package/src/bridges/user/web/dist/icons/icon-192x192.svg +9 -0
  44. package/src/bridges/user/web/dist/icons/icon-512x512.svg +9 -0
  45. package/src/bridges/user/web/dist/icons/icon-96x96.svg +9 -0
  46. package/src/bridges/user/web/dist/index.html +21 -0
  47. package/src/bridges/user/web/dist/manifest.json +26 -0
  48. package/src/bridges/user/web/dist/mesh-worker.js +64 -0
  49. package/src/bridges/user/web/dist/sw.js +1 -0
  50. package/src/bridges/user/web/event-publisher.ts +114 -0
  51. package/src/bridges/user/web/port-discovery.ts +49 -0
  52. package/src/bridges/user/web/reads.ts +35 -0
  53. package/src/bridges/user/web/router.ts +158 -0
  54. package/src/bridges/user/web/server.ts +782 -0
  55. package/src/bridges/user/web/vite.config.ts +71 -0
  56. package/src/bridges/user/web/web-access.ts +404 -0
  57. package/src/bridges/user/web/web-assets.ts +90 -0
  58. package/src/bridges/user/web/web-beacon.ts +69 -0
  59. package/src/bridges/user/web/web-console-static.ts +90 -0
  60. package/src/cli-main.ts +635 -0
  61. package/src/cli.ts +13 -0
  62. package/src/core/account-bundle.ts +264 -0
  63. package/src/core/account-join.ts +289 -0
  64. package/src/core/account-ledger-crypto.ts +154 -0
  65. package/src/core/account-ledger-store.ts +66 -0
  66. package/src/core/account-ledger.ts +385 -0
  67. package/src/core/agent-details.ts +216 -0
  68. package/src/core/agent-list-format.ts +97 -0
  69. package/src/core/agent-registry.ts +282 -0
  70. package/src/core/agent-table.ts +58 -0
  71. package/src/core/atomic-file.ts +46 -0
  72. package/src/core/bind-retry.ts +38 -0
  73. package/src/core/bridge-mesh.ts +176 -0
  74. package/src/core/bridge.ts +718 -0
  75. package/src/core/capability-ask.ts +162 -0
  76. package/src/core/capability-bubble-up.ts +237 -0
  77. package/src/core/comms-store.ts +130 -0
  78. package/src/core/connection-approval.ts +134 -0
  79. package/src/core/connection-code-tool.ts +92 -0
  80. package/src/core/connection-code.ts +296 -0
  81. package/src/core/coordinator-role.ts +224 -0
  82. package/src/core/data-directory.ts +32 -0
  83. package/src/core/data-frame-routing.ts +309 -0
  84. package/src/core/default-command.ts +12 -0
  85. package/src/core/delegation-policy.ts +41 -0
  86. package/src/core/delivery-engine.ts +649 -0
  87. package/src/core/device-membership-verification.ts +125 -0
  88. package/src/core/device-membership.ts +130 -0
  89. package/src/core/directory-admission.ts +155 -0
  90. package/src/core/discovery-mdns.ts +247 -0
  91. package/src/core/discovery-tailscale.ts +224 -0
  92. package/src/core/discovery.ts +303 -0
  93. package/src/core/display-name.ts +73 -0
  94. package/src/core/dm-grant-actions.ts +72 -0
  95. package/src/core/dm-send-delegation.ts +73 -0
  96. package/src/core/dm-token-verification.ts +76 -0
  97. package/src/core/election-sessions.ts +135 -0
  98. package/src/core/error-reporting.ts +85 -0
  99. package/src/core/first-contact.ts +242 -0
  100. package/src/core/gateway-trust-actions.ts +184 -0
  101. package/src/core/gateway-trust.ts +305 -0
  102. package/src/core/gossip-directory.ts +276 -0
  103. package/src/core/gossip-extensions.ts +123 -0
  104. package/src/core/group-proofs.ts +205 -0
  105. package/src/core/handshake.ts +354 -0
  106. package/src/core/hub-forwarding.ts +24 -0
  107. package/src/core/hub-link.ts +176 -0
  108. package/src/core/hub-session.ts +374 -0
  109. package/src/core/identity-store.ts +715 -0
  110. package/src/core/identity.ts +489 -0
  111. package/src/core/index.ts +65 -0
  112. package/src/core/issuer-grouping.ts +165 -0
  113. package/src/core/issuer-identity-file.ts +187 -0
  114. package/src/core/lan-web-actions.ts +34 -0
  115. package/src/core/lan-web-uis.ts +55 -0
  116. package/src/core/ledger-lock.ts +81 -0
  117. package/src/core/listener-registry.ts +133 -0
  118. package/src/core/loopback-address.ts +22 -0
  119. package/src/core/machine-identity.ts +77 -0
  120. package/src/core/machine-list-groups.ts +53 -0
  121. package/src/core/mcp-registry-retry.ts +69 -0
  122. package/src/core/mcp-registry-version.ts +18 -0
  123. package/src/core/membership-proof.ts +139 -0
  124. package/src/core/membership-proofs.ts +149 -0
  125. package/src/core/mesh-graph-trace-tool.ts +81 -0
  126. package/src/core/mesh-graph.ts +89 -0
  127. package/src/core/mesh-network-actions.ts +183 -0
  128. package/src/core/mesh-store-options.ts +23 -0
  129. package/src/core/mesh-store-shared.ts +134 -0
  130. package/src/core/mesh-store.ts +1114 -0
  131. package/src/core/name-claim.ts +145 -0
  132. package/src/core/naming-actions.ts +90 -0
  133. package/src/core/naming.ts +138 -0
  134. package/src/core/nanoid.ts +20 -0
  135. package/src/core/network-interfaces.ts +24 -0
  136. package/src/core/package-version.ts +34 -0
  137. package/src/core/peer-lifecycle.ts +377 -0
  138. package/src/core/peer-link-error.ts +41 -0
  139. package/src/core/peer-versions.ts +98 -0
  140. package/src/core/pending-connection.ts +53 -0
  141. package/src/core/petnames.ts +121 -0
  142. package/src/core/pgp-keyserver.ts +32 -0
  143. package/src/core/pid-lock-file.ts +122 -0
  144. package/src/core/push-manager.ts +76 -0
  145. package/src/core/random-id.ts +9 -0
  146. package/src/core/receiver-set.ts +42 -0
  147. package/src/core/relay-role.ts +182 -0
  148. package/src/core/relay-selection.ts +120 -0
  149. package/src/core/relay-server.ts +198 -0
  150. package/src/core/relay-uplink.ts +78 -0
  151. package/src/core/request-timeouts.ts +37 -0
  152. package/src/core/room-discovery.ts +48 -0
  153. package/src/core/room-lifecycle.ts +838 -0
  154. package/src/core/room-lookup.ts +41 -0
  155. package/src/core/room-messaging.ts +247 -0
  156. package/src/core/room-notice-log.ts +61 -0
  157. package/src/core/room-path.ts +84 -0
  158. package/src/core/room-protocol.ts +893 -0
  159. package/src/core/room-router.ts +180 -0
  160. package/src/core/room-token-verification.ts +85 -0
  161. package/src/core/room-wire-extensions.ts +60 -0
  162. package/src/core/self-name-claim.ts +67 -0
  163. package/src/core/send-outcome.ts +69 -0
  164. package/src/core/shutdown-signals.ts +57 -0
  165. package/src/core/stale-agent-checker.ts +108 -0
  166. package/src/core/store.ts +704 -0
  167. package/src/core/token-text.ts +40 -0
  168. package/src/core/tool.ts +853 -0
  169. package/src/core/transport-events.ts +73 -0
  170. package/src/core/transport-queries.ts +54 -0
  171. package/src/core/transport.ts +367 -0
  172. package/src/core/types.ts +550 -0
  173. package/src/core/user-identity.ts +290 -0
  174. package/src/core/vapid.ts +81 -0
  175. package/src/core/version-check.ts +147 -0
  176. package/src/core/version-report-actions.ts +79 -0
  177. package/src/core/web-beacon.ts +31 -0
  178. package/src/core/web-push.ts +322 -0
  179. package/src/core/wire-mesh-core-version.ts +25 -0
  180. package/src/core/wire-mesh-identity.ts +36 -0
  181. package/src/core/wire-mesh-transport-options.ts +82 -0
  182. package/src/core/wire-mesh-transport.ts +1007 -0
  183. package/src/core/wire-protocol.ts +153 -0
  184. package/src/core/ws-dial.ts +191 -0
@@ -0,0 +1,853 @@
1
+ /**
2
+ * Tool handler — processes CommsAction objects and returns human-readable results.
3
+ *
4
+ * This is the shared logic that every bridge calls into. Bridges just:
5
+ * 1. Parse the LLM's tool call into a CommsAction
6
+ * 2. Call handleAction(action)
7
+ * 3. Return the result string to the LLM
8
+ */
9
+
10
+ import { renderAgentList } from "./agent-list-format.js";
11
+ import type { AgentDetailsExchange } from "./agent-details.js";
12
+ import { plainNamer, type Namer, type Naming } from "./naming.js";
13
+ import { lanWebUisAction } from "./lan-web-actions.js";
14
+ import type { LanWebUiEntry } from "./lan-web-uis.js";
15
+ import {
16
+ machineName,
17
+ principalName,
18
+ petnameClear,
19
+ petnameList,
20
+ petnameSet,
21
+ } from "./naming-actions.js";
22
+ import { shortId } from "./display-name.js";
23
+ import type { VerifiedMember } from "./gateway-trust.js";
24
+ import type {
25
+ AgentId,
26
+ AgentIdentity,
27
+ CommsAction,
28
+ ConnectionCode,
29
+ MeshVisibility,
30
+ NetworkInterface,
31
+ Room,
32
+ RoomMessage,
33
+ } from "./types.js";
34
+ import type { ListenerInfo, MeshGraph, MeshTraceResult } from "./transport.js";
35
+ import type { CommsStore } from "./comms-store.js";
36
+ import type { CapabilityToken } from "wire-mesh-core/generated/protocol";
37
+ import type { DiscoveryManager } from "./discovery.js";
38
+ import { CommsError } from "./store.js";
39
+ import { getOwnPackageVersion } from "./package-version.js";
40
+ import { describeDeliveryFor } from "./send-outcome.js";
41
+ import {
42
+ formatSelfVersionLines,
43
+ formatSelfVersionSuffix,
44
+ handleQueryVersion,
45
+ } from "./version-report-actions.js";
46
+ import type {
47
+ GenerateConnectionCodeOptions,
48
+ RedeemConnectionCodeOptions,
49
+ RedeemConnectionCodeResult,
50
+ } from "./connection-code.js";
51
+ import { fetchPgpPublicKeyByFingerprint } from "./pgp-keyserver.js";
52
+ import {
53
+ handleGatewayGenerateConnectionCode,
54
+ handleGatewayRedeemConnectionCode,
55
+ } from "./connection-code-tool.js";
56
+ import {
57
+ gatewayTrust,
58
+ gatewayUntrust,
59
+ gatewayListTrusted,
60
+ } from "./gateway-trust-actions.js";
61
+ import { dmAdmit, dmRevoke, dmUseGrant } from "./dm-grant-actions.js";
62
+ import { meshGraphAction, meshTraceAction } from "./mesh-graph-trace-tool.js";
63
+ import {
64
+ meshDiscoverAction,
65
+ meshAdvertiseAction,
66
+ meshInterfacesAction,
67
+ meshUnadvertiseAction,
68
+ meshListenAction,
69
+ meshUnlistenAction,
70
+ meshListenersAction,
71
+ meshSetVisibilityAction,
72
+ meshGetVisibilityAction,
73
+ } from "./mesh-network-actions.js";
74
+
75
+ /** Table column widths for the plain-text listing helpers below, chosen to line up with the existing aligned output. */
76
+ const ROOM_TYPE_COLUMN_WIDTH = 7;
77
+
78
+ /** Start/end indices into an ISO-8601 timestamp string ("YYYY-MM-DDTHH:MM:SS.sssZ") that slice out the "HH:MM:SS" portion. */
79
+ const ISO_TIME_START_INDEX = 11;
80
+ const ISO_TIME_END_INDEX = 19;
81
+
82
+ /** Maximum length of the JSON preview shown for an unrecognised action. */
83
+ const UNKNOWN_ACTION_PREVIEW_LENGTH = 100;
84
+
85
+ export interface CommsContext {
86
+ agentId: AgentId;
87
+ harness: AgentIdentity["harness"];
88
+ cwd: string;
89
+ pid: number;
90
+ }
91
+
92
+ /** The calling bridge's own web UI address, as reported by the web_url action -- mirrors comms-url's own three-way distinction between a server that was never started, one whose OS-assigned port hasn't come back yet, and one that's actually reachable. */
93
+ export type WebUrlStatus =
94
+ | { kind: "not_running" }
95
+ | { kind: "pending" }
96
+ | { kind: "ready"; url: string };
97
+
98
+ export interface CommsResult {
99
+ content: string;
100
+ /** If true, the result is an error. */
101
+ isError: boolean;
102
+ }
103
+
104
+ /**
105
+ * Listener management and connection approval: transport concerns CommsStore deliberately excludes (see comms-store.ts's own header) since only MeshStore, never FileStore, can support them. Every method here is optional for exactly that reason -- a CommsTool backed by a FileStore simply doesn't have them, and each call site below reports that as an ordinary CommsResult error rather than assuming they exist.
106
+ */
107
+ export interface MeshOnlyFeatures {
108
+ addListener?: (host: string, port: number, policy: string) => Promise<string>;
109
+ removeListener?: (id: string) => Promise<void>;
110
+ listListeners?: () => ListenerInfo[];
111
+ getNetworkInterfaces?: () => NetworkInterface[];
112
+ acceptConnection?: (connectionId: string) => Promise<void>;
113
+ rejectConnection?: (connectionId: string, reason: string) => Promise<void>;
114
+ listPendingConnections?: () => {
115
+ connectionId: string;
116
+ peerId: string;
117
+ dataPort: number;
118
+ name: string;
119
+ fingerprint: string;
120
+ }[];
121
+ acceptRoomJoin?: (roomPath: string, requesterId: string) => void;
122
+ rejectRoomJoin?: (
123
+ roomPath: string,
124
+ requesterId: string,
125
+ reason?: string,
126
+ ) => void;
127
+ listPendingRoomJoins?: () => { roomPath: string; requesterId: string }[];
128
+ acceptCapabilityRequest?: (
129
+ requestId: string,
130
+ options: Readonly<{
131
+ expires: number;
132
+ delegationsRemaining?: number;
133
+ capability?: string;
134
+ }>,
135
+ ) => Promise<void>;
136
+ rejectCapabilityRequest?: (
137
+ requestId: string,
138
+ reason?: string,
139
+ ) => Promise<void>;
140
+ listPendingCapabilityRequests?: () => {
141
+ requestId: string;
142
+ capability: string;
143
+ scopeKind: string;
144
+ scopePath?: string;
145
+ requesterDevice: string;
146
+ }[];
147
+ connectToRemote?: (host: string, port: number) => Promise<void>;
148
+ setVisibility?: (level: MeshVisibility, adapter?: string) => Promise<void>;
149
+ getVisibility?: (adapter?: string) => MeshVisibility;
150
+ addTrustedGateway?: (deviceHex: string) => void;
151
+ removeTrustedGateway?: (deviceHex: string) => void;
152
+ listTrustedGateways?: () => string[];
153
+ generateConnectionCode?: (
154
+ options: Readonly<GenerateConnectionCodeOptions>,
155
+ ) => Promise<ConnectionCode>;
156
+ redeemConnectionCode?: (
157
+ candidate: Readonly<ConnectionCode>,
158
+ options: Readonly<RedeemConnectionCodeOptions>,
159
+ ) => Promise<RedeemConnectionCodeResult>;
160
+ addTrustedGatewayPrincipal?: (deviceHex: string) => void;
161
+ /** Mints a dm:send grant admitting bearerId to DM this user without a decision (agent-comms#162). */
162
+ admitAgentForDm?: (
163
+ bearerId: string,
164
+ delegationsRemaining?: number,
165
+ ) => Promise<CapabilityToken>;
166
+ /** Presents a dm:send grant another user issued, so the first DM to counterpart needs no decision there. */
167
+ presentDmGrant?: (
168
+ counterpart: string,
169
+ grant: CapabilityToken,
170
+ ) => Promise<void>;
171
+ /** This user's own principal id (hex), or undefined before this store's identity is attached. */
172
+ getUserPrincipalId?: () => string | undefined;
173
+ /** This host's own machine id (hex, agent-comms#343), or undefined before this store's identity is attached. */
174
+ getMachineId?: () => string | undefined;
175
+ /** The machine (hex) of each device this side can place on one, keyed by device id. */
176
+ listAgentMachines?: () => Promise<Map<string, string>>;
177
+ addTrustedGatewayMachine?: (deviceHex: string) => void;
178
+ removeTrustedGatewayMachine?: (deviceHex: string) => void;
179
+ listTrustedGatewayMachines?: () => string[];
180
+ /** Withdraws the dm:send grant issued for bearerId. */
181
+ revokeAgentDmAccess?: (bearerId: string) => Promise<void>;
182
+ removeTrustedGatewayPrincipal?: (deviceHex: string) => void;
183
+ listTrustedGatewayPrincipals?: () => string[];
184
+ /** Devices trusted only because a trusted principal or machine vouches for them (a verified membership proof), with that issuer. */
185
+ listVerifiedMembers?: () => VerifiedMember[];
186
+ /** deviceId's own gossiped agent-comms package version, cached from whatever it last advertised -- undefined for a device this side has never heard gossip from, or one running a version that predates agent-comms#198. */
187
+ getPeerAgentCommsVersion?: (deviceId: string) => string | undefined;
188
+ /** deviceId's own gossiped cc-peer package version, present only while that device is actually fronting a cc-peer session or running the one-shot `bridge cc-peer` command. */
189
+ getPeerCcPeerVersion?: (deviceId: string) => string | undefined;
190
+ /** deviceId's own gossiped wire-mesh-core version, self-advertised automatically by wire-mesh-core itself since wire-mesh#179. */
191
+ getPeerWireMeshCoreVersion?: (deviceId: string) => string | undefined;
192
+ /** This process's own currently-running cc-peer package version, when it is actually fronting a cc-peer session or running the one-shot `bridge cc-peer` command -- undefined for every other bridge, which never loads the cc-peer package at all. Assignable post-construction on the concrete MeshStore (mirroring its own onDelivery/onCoordinatorRoleChanged hooks); front-runtime.ts/bridges/cc-peer/run.ts are the only two call sites that ever set it. Typed as `(() => string | undefined) | undefined` rather than plain optional (`?:`) because MeshStore's own field is a required property whose value happens to be undefined by default, not an absent key -- exactOptionalPropertyTypes distinguishes the two. */
193
+ getCcPeerVersion?: (() => string | undefined) | undefined;
194
+ /** Asks deviceId for its own, currently-running wire-mesh-core version live, right now, rather than trusting whatever it last gossiped (agent-comms#198's own query_version action). */
195
+ queryVersion?: (
196
+ deviceId: string,
197
+ ) => Promise<{ version: string } | { error: string }>;
198
+ /** The LAN web UI table this machine's first-contact presence feeds (agent-comms#353). */
199
+ lanWebUis?: Readonly<{ list: () => readonly Readonly<LanWebUiEntry>[] }>;
200
+ /** The agent-details exchange with trusted remote peers (agent-comms#323). */
201
+ agentDetailsExchange?: Readonly<
202
+ Pick<AgentDetailsExchange, "refresh" | "cached">
203
+ >;
204
+ meshGraph?: () => MeshGraph;
205
+ meshTrace?: (target: string, timeoutMs?: number) => Promise<MeshTraceResult>;
206
+ }
207
+
208
+ /** Uniform "this bridge isn't backed by a mesh transport" result for a MeshOnlyFeatures method that isn't present on the current store. */
209
+ export function notMeshBacked(action: string): CommsResult {
210
+ return {
211
+ isError: true,
212
+ content: `${action} requires a mesh-backed store (this session is running on FileStore)`,
213
+ };
214
+ }
215
+
216
+ /** Runs a mesh store call that may throw, converting a thrown error into a CommsResult instead of repeating the same try/catch at every call site. `action` performs the call and returns the success message directly. */
217
+ export async function tryMeshAction(
218
+ verb: string,
219
+ action: () => Promise<string>,
220
+ ): Promise<CommsResult> {
221
+ try {
222
+ return { content: await action(), isError: false };
223
+ } catch (err) {
224
+ return {
225
+ content: `Failed to ${verb}: ${err instanceof Error ? err.message : String(err)}`,
226
+ isError: true,
227
+ };
228
+ }
229
+ }
230
+
231
+ /** Synchronous counterpart to tryMeshAction, for the room-join accept/reject calls, which aren't promise-returning. */
232
+ function trySyncAction(verb: string, action: () => string): CommsResult {
233
+ try {
234
+ return { content: action(), isError: false };
235
+ } catch (err) {
236
+ return {
237
+ content: `Failed to ${verb}: ${err instanceof Error ? err.message : String(err)}`,
238
+ isError: true,
239
+ };
240
+ }
241
+ }
242
+
243
+ /** Construction options for CommsTool. */
244
+ export interface CommsToolOptions {
245
+ /** Backs the mesh_discover / mesh_advertise / mesh_unadvertise actions. Undefined means this bridge wires no discovery. */
246
+ readonly discovery?: DiscoveryManager;
247
+ /** Backs the petname and machine_name actions and the names every listing shows (agent-comms#345). Undefined means this bridge wires no naming, and listings show only what each subject asserts and the short id. */
248
+ readonly naming?: Naming;
249
+ /** Returns the newest npm release known to be available, or undefined when none is known (no checker wired up, no successful check yet, or this bridge is already current). Wired at bridge construction time by a VersionDriftChecker (see version-check.ts) -- optional so every call site, and every test that has no interest in drift reporting, is unaffected. */
250
+ readonly getNewerVersionIfAny?: () => string | undefined;
251
+ /** Fetches a signer's armored PGP public key by fingerprint, used by gateway_redeem_connection_code when the caller supplies a fingerprint but not the key text itself. Defaults to the real keys.openpgp.org lookup (pgp-keyserver.ts); a test that would otherwise trigger real network I/O injects a fake resolver instead, the same reasoning bridge-mesh.ts's own fetchLatestVersion override already establishes for getNewerVersionIfAny above. */
252
+ readonly fetchPgpPublicKeyByFingerprintImpl?: (
253
+ fingerprint: string,
254
+ ) => Promise<string>;
255
+ }
256
+
257
+ export class CommsTool {
258
+ /** Reports this bridge's own web UI address for the web_url action. Assignable post-construction, mirroring MeshStore's own onDelivery/onCoordinatorRoleChanged hooks, because the underlying web server handle isn't known until after this bridge's own tryStartBridgeWebServer() call -- which every real bridge makes after building its CommsTool, not before. Undefined (the default) means this bridge never wires web UI reporting. */
259
+ getWebUrlStatus?: () => WebUrlStatus;
260
+
261
+ private readonly discovery: DiscoveryManager | undefined;
262
+ private readonly naming: Naming | undefined;
263
+ private readonly getNewerVersionIfAny: (() => string | undefined) | undefined;
264
+ private readonly fetchPgpPublicKeyByFingerprintImpl: (
265
+ fingerprint: string,
266
+ ) => Promise<string>;
267
+
268
+ constructor(
269
+ private readonly store: CommsStore & MeshOnlyFeatures,
270
+ options?: Readonly<CommsToolOptions>,
271
+ ) {
272
+ const {
273
+ discovery,
274
+ naming,
275
+ getNewerVersionIfAny,
276
+ fetchPgpPublicKeyByFingerprintImpl = fetchPgpPublicKeyByFingerprint,
277
+ } = options ?? {};
278
+ this.discovery = discovery;
279
+ this.naming = naming;
280
+ this.getNewerVersionIfAny = getNewerVersionIfAny;
281
+ this.fetchPgpPublicKeyByFingerprintImpl =
282
+ fetchPgpPublicKeyByFingerprintImpl;
283
+ }
284
+
285
+ /** The "Update available: ..." line appended to whoami/update output when a newer release is known, or undefined when there's nothing to report. */
286
+ private formatUpdateAvailableLine(): string | undefined {
287
+ const newerVersion = this.getNewerVersionIfAny?.();
288
+ if (newerVersion === undefined) return undefined;
289
+ return `Update available: ${newerVersion} (running ${getOwnPackageVersion()})`;
290
+ }
291
+
292
+ async handle(
293
+ ctx: Readonly<CommsContext>,
294
+ action: CommsAction,
295
+ ): Promise<CommsResult> {
296
+ try {
297
+ switch (action.action) {
298
+ case "register":
299
+ return await this.register(ctx, action);
300
+ case "update":
301
+ return await this.update(ctx, action);
302
+ case "whoami":
303
+ return await this.whoami(ctx);
304
+ case "web_url":
305
+ return this.webUrl();
306
+ case "create_room":
307
+ return await this.createRoom(ctx, action);
308
+ case "list_rooms":
309
+ return await this.listRooms(ctx);
310
+ case "join_room":
311
+ return await this.joinRoom(ctx, action);
312
+ case "leave_room":
313
+ return await this.leaveRoom(ctx, action);
314
+ case "send":
315
+ return await this.send(ctx, action);
316
+ case "dm":
317
+ return await this.dm(ctx, action);
318
+ case "list_agents":
319
+ return await this.listAgents(ctx);
320
+ case "read_room":
321
+ return await this.readRoom(ctx, action);
322
+ case "invite":
323
+ return await this.invite(ctx, action);
324
+ case "decline_invite":
325
+ return await this.declineInvite(ctx, action);
326
+ case "kick":
327
+ return await this.kick(ctx, action);
328
+ case "destroy_room":
329
+ return await this.destroyRoom(ctx, action);
330
+ case "mesh_connect":
331
+ return await this.meshConnect(ctx, action);
332
+ case "mesh_accept":
333
+ return await this.meshAccept(ctx, action);
334
+ case "mesh_reject":
335
+ return await this.meshReject(ctx, action);
336
+ case "mesh_pending":
337
+ return this.meshPending(ctx);
338
+ case "room_accept":
339
+ return this.roomAccept(ctx, action);
340
+ case "room_reject":
341
+ return this.roomReject(ctx, action);
342
+ case "room_pending":
343
+ return this.roomPending(ctx);
344
+ case "capability_accept":
345
+ return await this.capabilityAccept(ctx, action);
346
+ case "capability_reject":
347
+ return await this.capabilityReject(ctx, action);
348
+ case "capability_pending":
349
+ return this.capabilityPending(ctx);
350
+ case "mesh_discover":
351
+ return await meshDiscoverAction(this.discovery, action);
352
+ case "mesh_advertise":
353
+ return await meshAdvertiseAction(this.discovery, action);
354
+ case "mesh_unadvertise":
355
+ return await meshUnadvertiseAction(this.discovery, action);
356
+ case "mesh_interfaces":
357
+ return meshInterfacesAction(this.store);
358
+ case "lan_web_uis":
359
+ return lanWebUisAction(this.store, await this.namerFor(ctx));
360
+ case "mesh_listen":
361
+ return await meshListenAction(this.store, action);
362
+ case "mesh_unlisten":
363
+ return await meshUnlistenAction(this.store, action);
364
+ case "mesh_listeners":
365
+ return meshListenersAction(this.store);
366
+ case "mesh_set_visibility":
367
+ return await meshSetVisibilityAction(this.store, action);
368
+ case "mesh_get_visibility":
369
+ return meshGetVisibilityAction(this.store);
370
+ case "mesh_graph":
371
+ return meshGraphAction(this.store, await this.namerFor(ctx));
372
+ case "mesh_trace":
373
+ return await meshTraceAction(this.store, action);
374
+ case "gateway_trust":
375
+ return gatewayTrust(this.store, action);
376
+ case "gateway_untrust":
377
+ return gatewayUntrust(this.store, action);
378
+ case "gateway_list_trusted":
379
+ return gatewayListTrusted(this.store, await this.namerFor(ctx));
380
+ case "petname_set":
381
+ return petnameSet(this.naming, action);
382
+ case "petname_clear":
383
+ return petnameClear(this.naming, action);
384
+ case "petname_list":
385
+ return petnameList(this.naming, await this.namerFor(ctx));
386
+ case "machine_name":
387
+ return await machineName(this.naming, action);
388
+ case "principal_name":
389
+ return await principalName(this.naming, action);
390
+ case "dm_admit":
391
+ return await dmAdmit(this.store, action, ctx.agentId);
392
+ case "dm_use_grant":
393
+ return await dmUseGrant(this.store, action);
394
+ case "dm_revoke":
395
+ return await dmRevoke(this.store, action);
396
+ case "gateway_generate_connection_code":
397
+ return await handleGatewayGenerateConnectionCode(this.store, action);
398
+ case "gateway_redeem_connection_code":
399
+ return await handleGatewayRedeemConnectionCode(
400
+ this.store,
401
+ action,
402
+ this.fetchPgpPublicKeyByFingerprintImpl,
403
+ );
404
+ case "query_version":
405
+ return await handleQueryVersion(this.store, action);
406
+ default:
407
+ return {
408
+ content: `Unknown action: ${JSON.stringify(action).slice(0, UNKNOWN_ACTION_PREVIEW_LENGTH)}`,
409
+ isError: true,
410
+ };
411
+ }
412
+ } catch (err) {
413
+ if (err instanceof CommsError) {
414
+ return {
415
+ content: `Error: ${err.message} (${err.code})`,
416
+ isError: true,
417
+ };
418
+ }
419
+ return {
420
+ content: `Internal error: ${err instanceof Error ? err.message : String(err)}`,
421
+ isError: true,
422
+ };
423
+ }
424
+ }
425
+
426
+ private async register(
427
+ ctx: Readonly<CommsContext>,
428
+ action: CommsAction & { action: "register" },
429
+ ): Promise<CommsResult> {
430
+ const agent = await this.store.registerAgent({
431
+ name: action.name,
432
+ harness: ctx.harness,
433
+ cwd: ctx.cwd,
434
+ pid: ctx.pid,
435
+ visibility: action.visibility,
436
+ tags: action.tags,
437
+ });
438
+ return {
439
+ content: `Registered as ${agent.name} (${agent.id}) with visibility "${agent.visibility}".`,
440
+ isError: false,
441
+ };
442
+ }
443
+
444
+ private async update(
445
+ ctx: Readonly<CommsContext>,
446
+ action: CommsAction & { action: "update" },
447
+ ): Promise<CommsResult> {
448
+ const patch: Partial<
449
+ Pick<AgentIdentity, "name" | "visibility" | "status" | "tags" | "pid">
450
+ > = {};
451
+ if (action.visibility !== undefined) patch.visibility = action.visibility;
452
+ if (action.status !== undefined) patch.status = action.status;
453
+ if (action.name !== undefined) patch.name = action.name;
454
+ if (action.tags !== undefined) patch.tags = action.tags;
455
+ const agent = await this.store.updateAgent(ctx.agentId, patch);
456
+ const lines = [
457
+ `Updated: name=${agent.name}, visibility=${agent.visibility}, status=${agent.status}, version=${getOwnPackageVersion()}${formatSelfVersionSuffix(this.store.getCcPeerVersion)}`,
458
+ ];
459
+ const updateAvailable = this.formatUpdateAvailableLine();
460
+ if (updateAvailable !== undefined) lines.push(updateAvailable);
461
+ return {
462
+ content: lines.join("\n"),
463
+ isError: false,
464
+ };
465
+ }
466
+
467
+ /** The namer for ctx's view: petnames and known self names when naming is wired, otherwise only what each caller passes. */
468
+ private async namerFor(ctx: Readonly<CommsContext>): Promise<Namer> {
469
+ return this.naming === undefined
470
+ ? plainNamer
471
+ : this.naming.namer(ctx.agentId);
472
+ }
473
+
474
+ private async whoami(ctx: Readonly<CommsContext>): Promise<CommsResult> {
475
+ const agent = await this.store.getAgent(ctx.agentId);
476
+ if (!agent) return { content: "Not registered.", isError: true };
477
+ const principal = this.store.getUserPrincipalId?.();
478
+ const machine = this.store.getMachineId?.();
479
+ const namer = await this.namerFor(ctx);
480
+ const lines = [
481
+ `ID: ${agent.id}`,
482
+ ...(principal !== undefined ? [`Principal: ${principal}`] : []),
483
+ ...(machine !== undefined ? [`Machine: ${machine}`] : []),
484
+ ...nameLine("Principal name", principal, namer),
485
+ ...nameLine("Machine name", machine, namer),
486
+ `Name: ${agent.name}`,
487
+ `Harness: ${agent.harness}`,
488
+ `Visibility: ${agent.visibility}`,
489
+ `Status: ${agent.status}`,
490
+ `Version: ${getOwnPackageVersion()}`,
491
+ ...formatSelfVersionLines(this.store.getCcPeerVersion),
492
+ `Tags: ${agent.tags.join(", ") || "(none)"}`,
493
+ `Rooms: ${agent.subscribedRooms.join(", ") || "(none)"}`,
494
+ ];
495
+ const updateAvailable = this.formatUpdateAvailableLine();
496
+ if (updateAvailable !== undefined) lines.push(updateAvailable);
497
+ return {
498
+ content: lines.join("\n"),
499
+ isError: false,
500
+ };
501
+ }
502
+
503
+ private webUrl(): CommsResult {
504
+ const status = this.getWebUrlStatus?.() ?? { kind: "not_running" };
505
+ switch (status.kind) {
506
+ case "not_running":
507
+ return { content: "Web UI is not running.", isError: true };
508
+ case "pending":
509
+ return { content: "Web UI port not yet assigned.", isError: true };
510
+ case "ready":
511
+ return { content: status.url, isError: false };
512
+ default:
513
+ return status satisfies never;
514
+ }
515
+ }
516
+
517
+ private async createRoom(
518
+ ctx: Readonly<CommsContext>,
519
+ action: CommsAction & { action: "create_room" },
520
+ ): Promise<CommsResult> {
521
+ const room = await this.store.createRoom({
522
+ name: action.name,
523
+ type: action.type,
524
+ owner: ctx.agentId,
525
+ description: action.description,
526
+ });
527
+ // Auto-join the creator
528
+ await this.store.joinRoom(room.id, ctx.agentId);
529
+ return {
530
+ content: `Created ${room.type} room "${room.name}" (${room.id}).`,
531
+ isError: false,
532
+ };
533
+ }
534
+
535
+ private async listRooms(ctx: Readonly<CommsContext>): Promise<CommsResult> {
536
+ const rooms = await this.store.listRooms(ctx.agentId);
537
+ if (rooms.length === 0)
538
+ return { content: "No rooms found.", isError: false };
539
+
540
+ const lines = rooms.map((r: Room) => {
541
+ const memberFlag = r.members.includes(ctx.agentId) ? "✓" : " ";
542
+ return `[${memberFlag}] ${r.type.padEnd(ROOM_TYPE_COLUMN_WIDTH)} ${r.name} (${String(r.members.length)} members) — ${r.description}`;
543
+ });
544
+ return {
545
+ content: `Rooms ([✓] = joined):\n${lines.join("\n")}`,
546
+ isError: false,
547
+ };
548
+ }
549
+
550
+ private async joinRoom(
551
+ ctx: Readonly<CommsContext>,
552
+ action: CommsAction & { action: "join_room" },
553
+ ): Promise<CommsResult> {
554
+ const roomId = action.room;
555
+ const room = await this.store.joinRoom(roomId, ctx.agentId);
556
+ return {
557
+ content: `Joined room "${room.name}" (${String(room.members.length)} members).`,
558
+ isError: false,
559
+ };
560
+ }
561
+
562
+ private async leaveRoom(
563
+ ctx: Readonly<CommsContext>,
564
+ action: CommsAction & { action: "leave_room" },
565
+ ): Promise<CommsResult> {
566
+ await this.store.leaveRoom(action.room, ctx.agentId);
567
+ return { content: `Left room "${action.room}".`, isError: false };
568
+ }
569
+
570
+ private async send(
571
+ ctx: Readonly<CommsContext>,
572
+ action: CommsAction & { action: "send" },
573
+ ): Promise<CommsResult> {
574
+ const roomId = action.target;
575
+ const { message, deliveries } = await this.store.sendRoomMessage(
576
+ roomId,
577
+ ctx.agentId,
578
+ action.content,
579
+ {
580
+ replyTo: action.replyTo,
581
+ streamingBehavior: action.streamingBehavior,
582
+ },
583
+ );
584
+ const unsettled = deliveries.filter(
585
+ (entry) => entry.delivery.status !== "delivered",
586
+ );
587
+ if (unsettled.length === 0) {
588
+ return {
589
+ content: `Sent to ${action.target}: ${message.id}`,
590
+ isError: false,
591
+ };
592
+ }
593
+ const detail = unsettled
594
+ .map((entry) => describeDeliveryFor(entry.delivery, entry.member))
595
+ .join("; ");
596
+ // A refusal is the recipient's settled answer and no retry will change it, so the caller is told this send failed for that member. A member it is merely queued for may still receive it, so that is reported as a fact about the send rather than as its failure.
597
+ const refused = unsettled.some(
598
+ (entry) => entry.delivery.status === "refused",
599
+ );
600
+ return {
601
+ content: `Sent to ${action.target}: ${message.id}. Not delivered to every member: ${detail}.`,
602
+ isError: refused,
603
+ };
604
+ }
605
+
606
+ private async dm(
607
+ ctx: Readonly<CommsContext>,
608
+ action: CommsAction & { action: "dm" },
609
+ ): Promise<CommsResult> {
610
+ const targetId = action.target;
611
+ const { message, delivery } = await this.store.sendDm(
612
+ ctx.agentId,
613
+ targetId,
614
+ action.content,
615
+ action.streamingBehavior,
616
+ );
617
+ // A DM the recipient refused never reaches here: sendDm raises SEND_REFUSED, which handle() turns into an error result naming the reason. What is left is a real delivery or a message merely held for retry, and the wording distinguishes the two rather than calling both "sent".
618
+ return {
619
+ content: `DM ${describeDeliveryFor(delivery, action.target)}: ${message.id}`,
620
+ isError: false,
621
+ };
622
+ }
623
+
624
+ private async listAgents(ctx: Readonly<CommsContext>): Promise<CommsResult> {
625
+ const agents = await this.store.listAgents(ctx.agentId);
626
+ if (agents.length === 0)
627
+ return { content: "No other agents online.", isError: false };
628
+
629
+ this.store.agentDetailsExchange?.refresh(agents, ctx.agentId);
630
+ const namer = await this.namerFor(ctx);
631
+ return renderAgentList(agents, {
632
+ selfId: ctx.agentId,
633
+ namer,
634
+ store: this.store,
635
+ });
636
+ }
637
+
638
+ private async readRoom(
639
+ ctx: Readonly<CommsContext>,
640
+ action: CommsAction & { action: "read_room" },
641
+ ): Promise<CommsResult> {
642
+ const roomId = action.room;
643
+ const messages = await this.store.readRoomMessages(roomId, action.since);
644
+ if (messages.length === 0)
645
+ return { content: "No messages.", isError: false };
646
+
647
+ const lines = messages.map((m: RoomMessage) => {
648
+ const time = m.timestamp.slice(ISO_TIME_START_INDEX, ISO_TIME_END_INDEX);
649
+ return `[${time}] ${m.from}: ${m.content}`;
650
+ });
651
+ return { content: lines.join("\n"), isError: false };
652
+ }
653
+
654
+ private async invite(
655
+ ctx: Readonly<CommsContext>,
656
+ action: CommsAction & { action: "invite" },
657
+ ): Promise<CommsResult> {
658
+ await this.store.inviteToRoom(action.room, action.agent, ctx.agentId);
659
+ return {
660
+ content: `Invited ${action.agent} to ${action.room}.`,
661
+ isError: false,
662
+ };
663
+ }
664
+
665
+ private async declineInvite(
666
+ ctx: Readonly<CommsContext>,
667
+ action: CommsAction & { action: "decline_invite" },
668
+ ): Promise<CommsResult> {
669
+ await this.store.declineInvite(action.room, ctx.agentId, action.reason);
670
+ return {
671
+ content: `Declined invite to ${action.room}.`,
672
+ isError: false,
673
+ };
674
+ }
675
+
676
+ private async kick(
677
+ ctx: Readonly<CommsContext>,
678
+ action: CommsAction & { action: "kick" },
679
+ ): Promise<CommsResult> {
680
+ await this.store.kickFromRoom(action.room, action.agent, ctx.agentId);
681
+ return {
682
+ content: `Kicked ${action.agent} from ${action.room}.`,
683
+ isError: false,
684
+ };
685
+ }
686
+
687
+ private async destroyRoom(
688
+ ctx: Readonly<CommsContext>,
689
+ action: CommsAction & { action: "destroy_room" },
690
+ ): Promise<CommsResult> {
691
+ await this.store.destroyRoom(action.room, ctx.agentId);
692
+ return { content: `Destroyed room "${action.room}".`, isError: false };
693
+ }
694
+
695
+ private async meshConnect(
696
+ _ctx: Readonly<CommsContext>,
697
+ action: CommsAction & { action: "mesh_connect" },
698
+ ): Promise<CommsResult> {
699
+ if (!this.store.connectToRemote) return notMeshBacked("mesh_connect");
700
+ const connectToRemote = this.store.connectToRemote.bind(this.store);
701
+ return tryMeshAction("connect", async () => {
702
+ await connectToRemote(action.host, action.port);
703
+ const target = /^wss?:\/\//.test(action.host)
704
+ ? action.host
705
+ : `${action.host}:${String(action.port)}`;
706
+ return `Connection request sent to ${target}.`;
707
+ });
708
+ }
709
+
710
+ private async meshAccept(
711
+ _ctx: Readonly<CommsContext>,
712
+ action: CommsAction & { action: "mesh_accept" },
713
+ ): Promise<CommsResult> {
714
+ if (!this.store.acceptConnection) return notMeshBacked("mesh_accept");
715
+ const acceptConnection = this.store.acceptConnection.bind(this.store);
716
+ return tryMeshAction("accept", async () => {
717
+ await acceptConnection(action.connectionId);
718
+ return `Accepted connection ${action.connectionId}.`;
719
+ });
720
+ }
721
+
722
+ private async meshReject(
723
+ _ctx: Readonly<CommsContext>,
724
+ action: CommsAction & { action: "mesh_reject" },
725
+ ): Promise<CommsResult> {
726
+ if (!this.store.rejectConnection) return notMeshBacked("mesh_reject");
727
+ const rejectConnection = this.store.rejectConnection.bind(this.store);
728
+ return tryMeshAction("reject", async () => {
729
+ await rejectConnection(action.connectionId, action.reason);
730
+ return `Rejected connection ${action.connectionId}: ${action.reason}`;
731
+ });
732
+ }
733
+
734
+ private meshPending(_ctx: Readonly<CommsContext>): CommsResult {
735
+ if (!this.store.listPendingConnections)
736
+ return notMeshBacked("mesh_pending");
737
+ const pending = this.store.listPendingConnections();
738
+ if (pending.length === 0)
739
+ return { content: "No pending connections.", isError: false };
740
+
741
+ const lines = pending.map(
742
+ (p) =>
743
+ `${p.connectionId} ${p.peerId} ${p.name} port:${String(p.dataPort)} fp:${p.fingerprint}`,
744
+ );
745
+ return {
746
+ content: `Pending connections:\n${lines.join("\n")}`,
747
+ isError: false,
748
+ };
749
+ }
750
+
751
+ private roomAccept(
752
+ _ctx: Readonly<CommsContext>,
753
+ action: CommsAction & { action: "room_accept" },
754
+ ): CommsResult {
755
+ if (!this.store.acceptRoomJoin) return notMeshBacked("room_accept");
756
+ const acceptRoomJoin = this.store.acceptRoomJoin.bind(this.store);
757
+ return trySyncAction("accept", () => {
758
+ acceptRoomJoin(action.room, action.requesterId);
759
+ return `Accepted ${action.requesterId}'s request to join ${action.room}.`;
760
+ });
761
+ }
762
+
763
+ private roomReject(
764
+ _ctx: Readonly<CommsContext>,
765
+ action: CommsAction & { action: "room_reject" },
766
+ ): CommsResult {
767
+ if (!this.store.rejectRoomJoin) return notMeshBacked("room_reject");
768
+ const rejectRoomJoin = this.store.rejectRoomJoin.bind(this.store);
769
+ return trySyncAction("reject", () => {
770
+ rejectRoomJoin(action.room, action.requesterId, action.reason);
771
+ return `Rejected ${action.requesterId}'s request to join ${action.room}.`;
772
+ });
773
+ }
774
+
775
+ private roomPending(_ctx: Readonly<CommsContext>): CommsResult {
776
+ if (!this.store.listPendingRoomJoins) return notMeshBacked("room_pending");
777
+ const pending = this.store.listPendingRoomJoins();
778
+ if (pending.length === 0)
779
+ return { content: "No pending room join requests.", isError: false };
780
+
781
+ const lines = pending.map((p) => `${p.roomPath} ${p.requesterId}`);
782
+ return {
783
+ content: `Pending room join requests:\n${lines.join("\n")}`,
784
+ isError: false,
785
+ };
786
+ }
787
+
788
+ private async capabilityAccept(
789
+ _ctx: Readonly<CommsContext>,
790
+ action: CommsAction & { action: "capability_accept" },
791
+ ): Promise<CommsResult> {
792
+ if (!this.store.acceptCapabilityRequest)
793
+ return notMeshBacked("capability_accept");
794
+ const acceptCapabilityRequest = this.store.acceptCapabilityRequest.bind(
795
+ this.store,
796
+ );
797
+ return tryMeshAction("accept", async () => {
798
+ await acceptCapabilityRequest(action.requestId, {
799
+ expires: action.expires,
800
+ ...(action.delegationsRemaining !== undefined
801
+ ? { delegationsRemaining: action.delegationsRemaining }
802
+ : {}),
803
+ ...(action.capability !== undefined
804
+ ? { capability: action.capability }
805
+ : {}),
806
+ });
807
+ return `Accepted capability request ${action.requestId}.`;
808
+ });
809
+ }
810
+
811
+ private async capabilityReject(
812
+ _ctx: Readonly<CommsContext>,
813
+ action: CommsAction & { action: "capability_reject" },
814
+ ): Promise<CommsResult> {
815
+ if (!this.store.rejectCapabilityRequest)
816
+ return notMeshBacked("capability_reject");
817
+ const rejectCapabilityRequest = this.store.rejectCapabilityRequest.bind(
818
+ this.store,
819
+ );
820
+ return tryMeshAction("reject", async () => {
821
+ await rejectCapabilityRequest(action.requestId, action.reason);
822
+ return `Rejected capability request ${action.requestId}.`;
823
+ });
824
+ }
825
+
826
+ private capabilityPending(_ctx: Readonly<CommsContext>): CommsResult {
827
+ if (!this.store.listPendingCapabilityRequests)
828
+ return notMeshBacked("capability_pending");
829
+ const pending = this.store.listPendingCapabilityRequests();
830
+ if (pending.length === 0)
831
+ return { content: "No pending capability requests.", isError: false };
832
+
833
+ const lines = pending.map(
834
+ (p) =>
835
+ `${p.requestId} ${p.capability} ${p.scopeKind}${p.scopePath !== undefined ? `:${p.scopePath}` : ""} from ${p.requesterDevice}`,
836
+ );
837
+ return {
838
+ content: `Pending capability requests:\n${lines.join("\n")}`,
839
+ isError: false,
840
+ };
841
+ }
842
+ }
843
+
844
+ /** whoami's "<label>:" line for a grouping issuer: it by the display convention, only when it has a name, since the line above already shows its id. */
845
+ function nameLine(
846
+ label: string,
847
+ issuer: string | undefined,
848
+ namer: Namer,
849
+ ): string[] {
850
+ if (issuer === undefined) return [];
851
+ const named = namer(issuer);
852
+ return named === shortId(issuer) ? [] : [`${label}: ${named}`];
853
+ }