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,89 @@
1
+ /**
2
+ * mesh-graph -- agent-comms#199's own mesh_graph/mesh_trace primitives, split from wire-mesh-transport.ts under the repo's max-lines cap, the same reason gossip-directory.ts/hub-forwarding.ts were each split from their own owning file. computeMeshGraph assembles this side's best-effort view of the mesh's connection graph out of every known device's own self-reported topology/peers gossip extension (wire-mesh#180); traceMeshPath sends the live path.trace verb (wire-mesh#181) to a specific device, direct or via the hub.
3
+ */
4
+
5
+ import { deviceIdToHex } from "wire-mesh-core/domain/device-id";
6
+ import { assembleTopologyGraph } from "wire-mesh-core/domain/topology";
7
+ import {
8
+ buildPathTraceResponse,
9
+ tracePath,
10
+ } from "wire-mesh-core/domain/path-trace";
11
+ import type {
12
+ AcceptedMeshSession,
13
+ DirectoryEntry,
14
+ IncomingManageRequest,
15
+ ManageOutcome,
16
+ } from "wire-mesh-core/domain/mesh-session";
17
+ import type { PeerAdvert } from "wire-mesh-core/generated/protocol";
18
+ import type { HubSession } from "./hub-session.js";
19
+ import type { RoomRequestOrigin } from "./room-router.js";
20
+ import type {
21
+ ConnectionHandle,
22
+ MeshGraph,
23
+ MeshTraceResult,
24
+ } from "./transport.js";
25
+
26
+ /**
27
+ * Assembles this side's own best-effort view of the mesh's connection graph out of knownDevices (WireMeshTransport's own mesh-wide gossip aggregation, the identical map listKnownDevices already reads), converted to DirectoryEntry[]. Device-ids are hex-encoded, never raw wire-mesh-core DeviceId bytes, matching listKnownDevices' own precedent.
28
+ *
29
+ * assembleTopologyGraph's own nodes list is only devices this side holds a directory entry (gossip advert) for -- its edges are deliberately "unreconciled by design" (see its own doc comment) and can name a `to`/`via` endpoint reported by another device's advert that this side has never itself gossiped with directly, so that endpoint never gets its own directory entry and is absent from nodes. A MeshGraph whose edges reference an undeclared node isn't well-formed for any consumer (a force-directed layout, a table, anything), so every edge endpoint is unioned into the returned nodes list here, not left for each consumer to work around individually.
30
+ */
31
+ export function computeMeshGraph(
32
+ knownDevices: ReadonlyMap<string, Readonly<PeerAdvert>>,
33
+ ): MeshGraph {
34
+ const directory: DirectoryEntry[] = [...knownDevices.values()].map(
35
+ (advert) => ({ device: advert.device, advert }),
36
+ );
37
+ const graph = assembleTopologyGraph(directory);
38
+ const edges = graph.edges.map((edge) => ({
39
+ kind: edge.kind,
40
+ from: deviceIdToHex(edge.from),
41
+ to: deviceIdToHex(edge.to),
42
+ ...(edge.kind === "relay" && edge.via !== undefined
43
+ ? { via: deviceIdToHex(edge.via) }
44
+ : {}),
45
+ }));
46
+ const nodes = new Set(graph.nodes.map(deviceIdToHex));
47
+ for (const edge of edges) {
48
+ nodes.add(edge.from);
49
+ nodes.add(edge.to);
50
+ if (edge.via !== undefined) nodes.add(edge.via);
51
+ }
52
+ return { nodes: [...nodes], edges };
53
+ }
54
+
55
+ /** Sends path.trace to targetDeviceHex, direct if a local peer session exists for it (peerSessions, WireMeshTransport's own addressing map), else via the hub's own relay pairing when connected to one, else resolving an ordinary not_connected outcome rather than throwing -- WireMeshTransport.sendRoomRequest's own identical direct-or-hub fallback, just for path.trace instead of a room-domain verb. Delegates the actual send/RTT-measurement/response-parsing to wire-mesh-core's own tracePath in the direct branch, and to HubSession.tracePath (which does the identical delegation on the hub's own session) in the relayed branch, so neither this function nor either of those duplicates that logic. */
56
+ export async function traceMeshPath(
57
+ peerSessions: ReadonlyMap<string, AcceptedMeshSession>,
58
+ hub: Readonly<Pick<HubSession, "isConnected" | "tracePath">>,
59
+ targetDeviceHex: string,
60
+ timeoutMs?: number,
61
+ ): Promise<MeshTraceResult> {
62
+ const directSession = peerSessions.get(targetDeviceHex);
63
+ if (directSession !== undefined) {
64
+ return tracePath({
65
+ session: directSession,
66
+ clock: { now: () => Date.now() },
67
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
68
+ });
69
+ }
70
+ if (hub.isConnected) {
71
+ return hub.tracePath(targetDeviceHex, timeoutMs);
72
+ }
73
+ return {
74
+ rttMs: 0,
75
+ local: { relayed: false },
76
+ outcome: { result: "error", code: "not_connected" },
77
+ };
78
+ }
79
+
80
+ /** Answers an incoming path.trace (agent-comms#199), registered as this side's own "path.trace" room-router handler (createRoomRouter's handlers are keyed by params.verb regardless of domain, despite the room-flavoured name -- see WireMeshTransport's own constructor for where this is wired in alongside the real room verbs). Reports origin?.relayHubAddress (agent-comms#216) as buildPathTraceResponse's own hubAddress parameter -- that function itself already gates inclusion on the request actually being relayed (request.fromDevice set), so this passes it through unconditionally rather than duplicating that check here; handle is unused, kept only to occupy RoomVerbHandler's own positional slot ahead of origin. */
81
+ export async function handlePathTraceRequest(
82
+ request: Readonly<IncomingManageRequest>,
83
+ _handle: Readonly<ConnectionHandle>,
84
+ origin?: Readonly<RoomRequestOrigin>,
85
+ ): Promise<ManageOutcome> {
86
+ return Promise.resolve(
87
+ buildPathTraceResponse(request, origin?.relayHubAddress),
88
+ );
89
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * CommsTool's mesh discovery/advertise/interfaces/listener/visibility action handlers -- split out of tool.ts purely to keep that file under the repo's max-lines cap, the same reason gateway-trust-actions.ts/mesh-graph-trace-tool.ts were split from it. Free functions over an explicit DiscoveryManager/CommsStore pair rather than class methods, since CommsTool owns no state of its own for this concern.
3
+ */
4
+
5
+ import type { CommsAction, NetworkInterface } from "./types.js";
6
+ import type { ListenerInfo } from "./transport.js";
7
+ import type { DiscoveryManager } from "./discovery.js";
8
+ import type { CommsResult, MeshOnlyFeatures } from "./tool.js";
9
+ import { notMeshBacked, tryMeshAction } from "./tool.js";
10
+
11
+ /** Table column widths for the plain-text listing helpers below, matching tool.ts's own aligned-column convention for every other tabular action result. */
12
+ const INTERFACE_NAME_COLUMN_WIDTH = 12;
13
+ const INTERFACE_FAMILY_COLUMN_WIDTH = 4;
14
+ const LISTENER_HOST_COLUMN_WIDTH = 15;
15
+ const LISTENER_PORT_COLUMN_WIDTH = 6;
16
+ const LISTENER_POLICY_COLUMN_WIDTH = 11;
17
+
18
+ /** Default port the mesh coordinator listens on when advertising with no explicit port given. */
19
+ const DEFAULT_MESH_COORDINATOR_PORT = 19876;
20
+
21
+ export async function meshDiscoverAction(
22
+ discovery: DiscoveryManager | undefined,
23
+ action: CommsAction & { action: "mesh_discover" },
24
+ ): Promise<CommsResult> {
25
+ if (!discovery) {
26
+ return {
27
+ content: "Discovery is not available (no backends registered).",
28
+ isError: true,
29
+ };
30
+ }
31
+ const meshes = await discovery.discover({ backendName: action.method });
32
+ if (meshes.length === 0) {
33
+ return { content: "No meshes discovered.", isError: false };
34
+ }
35
+ const lines = meshes.map(
36
+ (m) =>
37
+ ` ${m.host}:${String(m.port)} ${m.name}${m.agentCount !== undefined ? ` (${String(m.agentCount)} agents)` : ""}`,
38
+ );
39
+ return {
40
+ content: `Discovered meshes:\n${lines.join("\n")}`,
41
+ isError: false,
42
+ };
43
+ }
44
+
45
+ export async function meshAdvertiseAction(
46
+ discovery: DiscoveryManager | undefined,
47
+ action: CommsAction & { action: "mesh_advertise" },
48
+ ): Promise<CommsResult> {
49
+ if (!discovery) {
50
+ return {
51
+ content: "Discovery is not available (no backends registered).",
52
+ isError: true,
53
+ };
54
+ }
55
+ const port = action.port ?? DEFAULT_MESH_COORDINATOR_PORT;
56
+ const opts: { name: string; port: number; adapter?: string } = {
57
+ name: action.name,
58
+ port,
59
+ };
60
+ if (action.adapter !== undefined) opts.adapter = action.adapter;
61
+ const id = await discovery.advertise(action.method, opts);
62
+ return {
63
+ content: `Advertising mesh "${action.name}" on ${action.method} (port ${String(port)}). ID: ${id}`,
64
+ isError: false,
65
+ };
66
+ }
67
+
68
+ export function meshInterfacesAction(
69
+ store: Readonly<Pick<MeshOnlyFeatures, "getNetworkInterfaces">>,
70
+ ): CommsResult {
71
+ if (!store.getNetworkInterfaces) return notMeshBacked("mesh_interfaces");
72
+ const interfaces = store.getNetworkInterfaces();
73
+ if (interfaces.length === 0)
74
+ return { content: "No network interfaces found.", isError: false };
75
+
76
+ const lines = interfaces.map((iface: Readonly<NetworkInterface>) => {
77
+ const internal = iface.internal ? " (internal)" : "";
78
+ return `${iface.name.padEnd(INTERFACE_NAME_COLUMN_WIDTH)} ${iface.family.padEnd(INTERFACE_FAMILY_COLUMN_WIDTH)} ${iface.address}${internal}`;
79
+ });
80
+ return {
81
+ content: `Interfaces:\n${lines.join("\n")}`,
82
+ isError: false,
83
+ };
84
+ }
85
+
86
+ export async function meshUnadvertiseAction(
87
+ discovery: DiscoveryManager | undefined,
88
+ action: CommsAction & { action: "mesh_unadvertise" },
89
+ ): Promise<CommsResult> {
90
+ if (!discovery) {
91
+ return {
92
+ content: "Discovery is not available (no backends registered).",
93
+ isError: true,
94
+ };
95
+ }
96
+ await discovery.stopAdvertising(action.id);
97
+ return { content: `Stopped advertising ${action.id}.`, isError: false };
98
+ }
99
+
100
+ export async function meshListenAction(
101
+ store: Readonly<Pick<MeshOnlyFeatures, "addListener">>,
102
+ action: CommsAction & { action: "mesh_listen" },
103
+ ): Promise<CommsResult> {
104
+ const policy = action.policy ?? "full";
105
+ const validPolicies = ["full", "observe", "rooms-only", "gateway"];
106
+ if (!validPolicies.includes(policy)) {
107
+ return {
108
+ content: `Invalid policy "${policy}". Must be one of: ${validPolicies.join(", ")}`,
109
+ isError: true,
110
+ };
111
+ }
112
+ if (!store.addListener) return notMeshBacked("mesh_listen");
113
+ const addListener = store.addListener.bind(store);
114
+ return tryMeshAction("add listener", async () => {
115
+ const id = await addListener(action.host, action.port ?? 0, policy);
116
+ return `Listener added: ${id} on ${action.host}${String(action.port ?? "auto")} with policy ${policy}.`;
117
+ });
118
+ }
119
+
120
+ export async function meshUnlistenAction(
121
+ store: Readonly<Pick<MeshOnlyFeatures, "removeListener">>,
122
+ action: CommsAction & { action: "mesh_unlisten" },
123
+ ): Promise<CommsResult> {
124
+ if (!store.removeListener) return notMeshBacked("mesh_unlisten");
125
+ const removeListener = store.removeListener.bind(store);
126
+ return tryMeshAction("remove listener", async () => {
127
+ await removeListener(action.id);
128
+ return `Listener ${action.id} removed.`;
129
+ });
130
+ }
131
+
132
+ export function meshListenersAction(
133
+ store: Readonly<Pick<MeshOnlyFeatures, "listListeners">>,
134
+ ): CommsResult {
135
+ if (!store.listListeners) return notMeshBacked("mesh_listeners");
136
+ const listeners = store.listListeners();
137
+ if (listeners.length === 0)
138
+ return { content: "No listeners (not coordinator).", isError: false };
139
+
140
+ const lines = listeners.map((l: Readonly<ListenerInfo>) => {
141
+ const flag = l.isDefault ? " (default)" : "";
142
+ return `${l.id} ${l.host.padEnd(LISTENER_HOST_COLUMN_WIDTH)} ${String(l.port).padEnd(LISTENER_PORT_COLUMN_WIDTH)} ${l.policy.padEnd(LISTENER_POLICY_COLUMN_WIDTH)}${flag}`;
143
+ });
144
+ return {
145
+ content: `Listeners:\n ID Host Port Policy \n${lines.map((l) => ` ${l}`).join("\n")}`,
146
+ isError: false,
147
+ };
148
+ }
149
+
150
+ export async function meshSetVisibilityAction(
151
+ store: Readonly<Pick<MeshOnlyFeatures, "setVisibility">>,
152
+ action: CommsAction & { action: "mesh_set_visibility" },
153
+ ): Promise<CommsResult> {
154
+ if (!store.setVisibility) {
155
+ return {
156
+ content: "Visibility control is not available on this store.",
157
+ isError: true,
158
+ };
159
+ }
160
+ await store.setVisibility(action.visibility, action.adapter);
161
+ const adapter =
162
+ action.adapter !== undefined ? ` on adapter "${action.adapter}"` : "";
163
+ return {
164
+ content: `Mesh visibility set to "${action.visibility}"${adapter}.`,
165
+ isError: false,
166
+ };
167
+ }
168
+
169
+ export function meshGetVisibilityAction(
170
+ store: Readonly<Pick<MeshOnlyFeatures, "getVisibility">>,
171
+ ): CommsResult {
172
+ if (!store.getVisibility) {
173
+ return {
174
+ content: "Visibility control is not available on this store.",
175
+ isError: true,
176
+ };
177
+ }
178
+ const visibility = store.getVisibility();
179
+ return {
180
+ content: `Mesh visibility: ${visibility}`,
181
+ isError: false,
182
+ };
183
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * MeshStore's own constructor options, split out purely to keep mesh-store.ts under the repo's max-lines cap, the same reason WireMeshTransportOptions was split into its own wire-mesh-transport-options.ts.
3
+ */
4
+
5
+ import type { IdentitySlot } from "./identity-store.js";
6
+
7
+ /** Construction options for MeshStore. */
8
+ export interface MeshStoreOptions {
9
+ /** Localhost TCP port the coordinator role is contested on. Defaults to DEFAULT_COORDINATOR_PORT. */
10
+ readonly coordinatorPort?: number | undefined;
11
+ /** The relay hub this store holds its own session on from init until shutdown, so it is reachable from other machines whatever role it plays locally. Left out, the store never contacts a hub: only the bridge entry points name the production hub (bridge-mesh.ts), so a store built by anything else, a test included, stays local. */
12
+ readonly hubUrl?: string | undefined;
13
+ /** How long a room.join (including a first-contact DM request) may await a human decision, on this store as the receiver and on the transport it is wired to as the sender. Defaults to ROOM_JOIN_APPROVAL_TIMEOUT_MS; a test shortens it. */
14
+ readonly roomJoinApprovalTimeoutMs?: number | undefined;
15
+ /** How old a gossip-discovered device's own last-heard advert may be before listAgents stops trusting whatever presence status it last carried and reports the device offline instead (agent-comms#301). Defaults to DEFAULT_PRESENCE_STALE_AFTER_MS; a test shortens it to observe staleness without waiting out the real default. */
16
+ readonly presenceStaleAfterMs?: number | undefined;
17
+ /** The UDP port init() binds the first-contact presence on (agent-comms#341): beacons, probes, and discovered peers fed into the ordinary peer-list flood, so a mesh forms with no coordinator at all. Left out, the store runs no presence: only the bridge entry points set it (bridge-mesh.ts, FIRST_CONTACT_PORT by default), so a store built by anything else, a test included, stays inert and never cross-discovers another store. A bridge-level test overrides it with an OS-assigned free port. */
18
+ readonly firstContactPort?: number | undefined;
19
+ /** How long this store waits for an incumbent's coordinator claim to reach it before claiming the elected role itself (agent-comms#341): after the holder departs, after accepting a claim naming a holder it has no session to, and before a store that may meet others through first contact claims a vacant role. Defaults to ROOM_REQUEST_TIMEOUT_MS; a test shortens it. */
20
+ readonly coordinatorClaimWaitMs?: number | undefined;
21
+ /** Locates this store's persisted bootstrap state. The connectionCodes ledger is per slot. gatewayTrust is shared by every slot in the slot's identity directory, so all stores on a machine (each fronted session included) advertise under one operator decision. */
22
+ readonly slot?: Readonly<IdentitySlot>;
23
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Shared constants and helpers for MeshStore and its collaborator classes — module-level constants, types, and free functions with no `this` dependency, split out purely to keep mesh-store.ts under the repo's max-lines cap.
3
+ */
4
+
5
+ import type { Clock } from "wire-mesh-core/ports/clock";
6
+ import type { IdentityPort } from "wire-mesh-core/ports/identity";
7
+ import type { RevocationView } from "wire-mesh-core/domain/revocation-view";
8
+ import type { KeyValueStorage } from "wire-mesh-core/ports/storage";
9
+ import type { IdentitySlot } from "./identity-store.js";
10
+ import type { AccountLedger } from "./account-ledger.js";
11
+ import type { UserIdentityOptions } from "./user-identity.js";
12
+ import type { MachineIdentityOptions } from "./machine-identity.js";
13
+
14
+ /** The identity/clock/persistence collaborators MeshStore mints and persists room-membership grants against. Set via setIdentity(), mirroring the transport's own setTransport() contract. dataStorage backs this device's own room-notice oplog (P5, agent-comms#50): the same KeyValueStorage instance WireMeshTransport's own dataStorage constructor parameter is wired with, so a durable sendRoomMessage and the transport's own data-domain responder read and write the identical log. userIdentity/userIdentityOptions are the user-principal identity (user-identity.ts, agent-comms#160) this store's own bridge slot shares with every other bridge of this account, distinct from `identity`, which is this specific bridge's own per-slot device identity; userIdentity is what a dm:send grant (agent-comms#162) is minted and verified against. accountLedger is the account's replicated, encrypted grant ledger (account-ledger.ts, agent-comms#344), where every grant this principal mints is recorded so any machine holding the account key can revoke it. userIdentityOptions locates the file userIdentity was loaded from, which the account's own join flow (account-join.ts) reads to seal the key for a joining machine and writes to import one it joins. machineIdentity is this account's machine identity for the host (machine-identity.ts, agent-comms#343), shared by every bridge the account runs there: the second grouping issuer this store's device is vouched for by. machineIdentityOptions locates the file it was loaded from, where the machine's self display name (agent-comms#345) is kept. */
15
+ export interface MeshStoreIdentity {
16
+ identity: IdentityPort;
17
+ clock: Clock;
18
+ slot: IdentitySlot;
19
+ revocation: RevocationView;
20
+ dataStorage: KeyValueStorage;
21
+ userIdentity: IdentityPort;
22
+ accountLedger: AccountLedger;
23
+ /** Where userIdentity's key lives on this machine, for the account's own join flow (account-join.ts) to seal it for a joining machine and to import one it joins. */
24
+ userIdentityOptions: Readonly<UserIdentityOptions>;
25
+ machineIdentity: IdentityPort;
26
+ machineIdentityOptions: Readonly<MachineIdentityOptions>;
27
+ }
28
+
29
+ /**
30
+ * Lifetime of a freshly minted room:member grant (owner root grant or member join/invite grant alike). Deliberately generous rather than the "short expires, periodic re-issue" pattern the design calls for to bound kick-convergence to gossip-independent expiry -- that re-issue mechanism is P3.7's own deliverable (riding room.members refreshes), and shipping a short expiry before it exists would let ordinary grants go stale with nothing to renew them. 30 days comfortably outlives any realistic room lifetime for now; P3.7 tightens this once re-issue-on-refresh lands.
31
+ */
32
+ const ROOM_TOKEN_LIFETIME_DAYS = 30;
33
+ const HOURS_PER_DAY = 24;
34
+ const MINUTES_PER_HOUR = 60;
35
+ const SECONDS_PER_MINUTE = 60;
36
+ const MS_PER_SECOND = 1000;
37
+ export const ROOM_TOKEN_LIFETIME_MS =
38
+ ROOM_TOKEN_LIFETIME_DAYS *
39
+ HOURS_PER_DAY *
40
+ MINUTES_PER_HOUR *
41
+ SECONDS_PER_MINUTE *
42
+ MS_PER_SECOND;
43
+
44
+ /** Lifetime of a freshly minted dm:send grant (agent-comms#162) -- the same 30-day generosity ROOM_TOKEN_LIFETIME_MS applies, for the same reason: no periodic re-issue-on-refresh mechanism exists yet for this grant kind either, so a short expiry would just let ordinary admissions go stale with nothing to renew them. */
45
+ const DM_SEND_GRANT_LIFETIME_DAYS = 30;
46
+ export const DM_SEND_GRANT_LIFETIME_MS =
47
+ DM_SEND_GRANT_LIFETIME_DAYS *
48
+ HOURS_PER_DAY *
49
+ MINUTES_PER_HOUR *
50
+ SECONDS_PER_MINUTE *
51
+ MS_PER_SECOND;
52
+
53
+ /** The outcome of a pending room.join request: a human's accept or reject (reject carries an optional reason, mirroring rejectConnection's own equivalent room-independent decision), or expiry when nobody decided within the approval window. */
54
+ export type RoomJoinDecision =
55
+ | { kind: "accept" }
56
+ | { kind: "reject"; reason?: string }
57
+ | { kind: "expired" };
58
+
59
+ /**
60
+ * Bound on pending delivery events held per target agent. Events beyond the bound drop oldest-first: a long-offline agent's queue cannot grow without limit in memory or in synced snapshots (#28).
61
+ */
62
+ export const MAX_QUEUED_DELIVERIES_PER_AGENT = 100;
63
+
64
+ /** Bound on directed room-domain requests held for retry per unreachable member. Requests beyond the bound drop oldest-first, and each drop is reported to its own sender as a terminal delivery status, so a message given up on is never left looking merely pending. */
65
+ export const MAX_PENDING_ROOM_REQUESTS_PER_MEMBER = 100;
66
+
67
+ /** How long a directed room-domain request may sit in the retry queue before it is given up on and its sender told it expired. Generous enough to outlast a peer restarting, a laptop sleeping overnight, or a hub outage, all of which a retry genuinely does fix; short enough that a message to a device that is never coming back stops being reported as pending indefinitely. */
68
+ const PENDING_ROOM_REQUEST_TTL_HOURS = 24;
69
+ export const PENDING_ROOM_REQUEST_TTL_MS =
70
+ PENDING_ROOM_REQUEST_TTL_HOURS *
71
+ MINUTES_PER_HOUR *
72
+ SECONDS_PER_MINUTE *
73
+ MS_PER_SECOND;
74
+
75
+ /** The coordinator's own bind host -- always loopback, since the mesh coordinator role only ever needs to be reachable from other local peers on this machine. Shared between mesh-store.ts's own init() and PeerLifecycle's handleBecomeCoordinator. */
76
+ export const COORDINATOR_HOST = "127.0.0.1";
77
+
78
+ /** The production relay hub this machine's gateway dials once it becomes the local mesh coordinator (agent-comms#154). Configuration: MeshStore's own constructor accepts an override, threaded from createBridgeMesh/createBridgeMeshSync, for tests and any future non-default deployment; a user picks a different hub for every bridge on the machine with HUB_URL_ENV_VAR (see resolveHubUrl) -- this is only the default. */
79
+ export const DEFAULT_HUB_URL = "wss://mesh.exadev.io/";
80
+
81
+ /** The environment variable that points every bridge started from this environment at a self-hosted hub (for example `npx wire-mesh`) instead of DEFAULT_HUB_URL. */
82
+ export const HUB_URL_ENV_VAR = "AGENT_COMMS_HUB_URL";
83
+
84
+ /**
85
+ * The hub a bridge dials when its caller passes no explicit hubUrl: HUB_URL_ENV_VAR when it is set and not blank, DEFAULT_HUB_URL otherwise.
86
+ *
87
+ * A value that is not a ws:// or wss:// URL throws rather than falling back to DEFAULT_HUB_URL: someone who set the variable meant to keep their traffic off the public hub, so silently dialling it anyway would be the worst outcome.
88
+ */
89
+ export function resolveHubUrl(
90
+ env: Readonly<Record<string, string | undefined>> = process.env,
91
+ ): string {
92
+ const configured = env[HUB_URL_ENV_VAR]?.trim();
93
+ if (configured === undefined || configured === "") return DEFAULT_HUB_URL;
94
+ let protocol: string;
95
+ try {
96
+ protocol = new URL(configured).protocol;
97
+ } catch {
98
+ protocol = "";
99
+ }
100
+ if (protocol !== "ws:" && protocol !== "wss:") {
101
+ throw new Error(
102
+ `${HUB_URL_ENV_VAR} must be a ws:// or wss:// URL, got "${configured}"`,
103
+ );
104
+ }
105
+ return configured;
106
+ }
107
+
108
+ /** Shallow-clones an entry together with its own `readBy` array, so a merged history never shares mutable array references with either input it was built from. */
109
+ function cloneWithReadBy<T extends { readBy: string[] }>(entry: T): T {
110
+ return { ...entry, readBy: [...entry.readBy] };
111
+ }
112
+
113
+ /**
114
+ * Merge an incoming append-only message history with the local one, returning a new array: entries the local list does not have are appended, and read receipts are unioned on the ones it does. Local ordering is preserved; unseen entries are appended. Neither input array (nor its entries) is mutated -- the caller is responsible for storing the returned array back wherever the local history is kept.
115
+ */
116
+ export function mergeMessageHistories<
117
+ T extends { id: string; readBy: string[] },
118
+ >(local: readonly T[], incoming: readonly T[]): T[] {
119
+ const merged = local.map((m) => cloneWithReadBy(m));
120
+ const byId = new Map(merged.map((m) => [m.id, m]));
121
+ for (const msg of incoming) {
122
+ const existing = byId.get(msg.id);
123
+ if (existing === undefined) {
124
+ const copy = cloneWithReadBy(msg);
125
+ merged.push(copy);
126
+ byId.set(msg.id, copy);
127
+ continue;
128
+ }
129
+ for (const reader of msg.readBy) {
130
+ if (!existing.readBy.includes(reader)) existing.readBy.push(reader);
131
+ }
132
+ }
133
+ return merged;
134
+ }