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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/dist/bridges/user/web/server.d.ts.map +1 -1
- package/dist/bridges/user/web/server.js +44 -24
- package/dist/bridges/user/web/server.js.map +1 -1
- package/dist/bridges/user/web/test/web-assets.unit.test.d.ts +5 -0
- package/dist/bridges/user/web/test/web-assets.unit.test.d.ts.map +1 -0
- package/dist/bridges/user/web/test/web-assets.unit.test.js +90 -0
- package/dist/bridges/user/web/test/web-assets.unit.test.js.map +1 -0
- package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.d.ts +2 -0
- package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.d.ts.map +1 -0
- package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.js +75 -0
- package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.js.map +1 -0
- package/dist/bridges/user/web/web-assets.d.ts +28 -0
- package/dist/bridges/user/web/web-assets.d.ts.map +1 -0
- package/dist/bridges/user/web/web-assets.js +67 -0
- package/dist/bridges/user/web/web-assets.js.map +1 -0
- package/package.json +8 -2
- package/src/account-cli.ts +195 -0
- package/src/account-join-cli.ts +191 -0
- package/src/bridges/cc-peer/approval-commands.ts +82 -0
- package/src/bridges/cc-peer/bridge.ts +138 -0
- package/src/bridges/cc-peer/default-front.ts +58 -0
- package/src/bridges/cc-peer/front-controller.ts +160 -0
- package/src/bridges/cc-peer/front-relay.ts +191 -0
- package/src/bridges/cc-peer/front-runtime.ts +167 -0
- package/src/bridges/cc-peer/front.ts +59 -0
- package/src/bridges/cc-peer/reply-aliases.ts +90 -0
- package/src/bridges/cc-peer/run.ts +100 -0
- package/src/bridges/cc-peer/shared-peer.ts +28 -0
- package/src/bridges/claude-code/channel.ts +318 -0
- package/src/bridges/codex/tool.ts +132 -0
- package/src/bridges/mcp/server.ts +137 -0
- package/src/bridges/opencode/plugin.ts +130 -0
- package/src/bridges/pi/extension.ts +330 -0
- package/src/bridges/registry.ts +17 -0
- package/src/bridges/user/cli.ts +87 -0
- package/src/bridges/user/controller.ts +345 -0
- package/src/bridges/user/tui.ts +300 -0
- package/src/bridges/user/web/actions.ts +173 -0
- package/src/bridges/user/web/contract.ts +222 -0
- package/src/bridges/user/web/dist/bundle.js +49 -0
- package/src/bridges/user/web/dist/icons/icon-192x192.svg +9 -0
- package/src/bridges/user/web/dist/icons/icon-512x512.svg +9 -0
- package/src/bridges/user/web/dist/icons/icon-96x96.svg +9 -0
- package/src/bridges/user/web/dist/index.html +21 -0
- package/src/bridges/user/web/dist/manifest.json +26 -0
- package/src/bridges/user/web/dist/mesh-worker.js +64 -0
- package/src/bridges/user/web/dist/sw.js +1 -0
- package/src/bridges/user/web/event-publisher.ts +114 -0
- package/src/bridges/user/web/port-discovery.ts +49 -0
- package/src/bridges/user/web/reads.ts +35 -0
- package/src/bridges/user/web/router.ts +158 -0
- package/src/bridges/user/web/server.ts +782 -0
- package/src/bridges/user/web/vite.config.ts +71 -0
- package/src/bridges/user/web/web-access.ts +404 -0
- package/src/bridges/user/web/web-assets.ts +90 -0
- package/src/bridges/user/web/web-beacon.ts +69 -0
- package/src/bridges/user/web/web-console-static.ts +90 -0
- package/src/cli-main.ts +635 -0
- package/src/cli.ts +13 -0
- package/src/core/account-bundle.ts +264 -0
- package/src/core/account-join.ts +289 -0
- package/src/core/account-ledger-crypto.ts +154 -0
- package/src/core/account-ledger-store.ts +66 -0
- package/src/core/account-ledger.ts +385 -0
- package/src/core/agent-details.ts +216 -0
- package/src/core/agent-list-format.ts +97 -0
- package/src/core/agent-registry.ts +282 -0
- package/src/core/agent-table.ts +58 -0
- package/src/core/atomic-file.ts +46 -0
- package/src/core/bind-retry.ts +38 -0
- package/src/core/bridge-mesh.ts +176 -0
- package/src/core/bridge.ts +718 -0
- package/src/core/capability-ask.ts +162 -0
- package/src/core/capability-bubble-up.ts +237 -0
- package/src/core/comms-store.ts +130 -0
- package/src/core/connection-approval.ts +134 -0
- package/src/core/connection-code-tool.ts +92 -0
- package/src/core/connection-code.ts +296 -0
- package/src/core/coordinator-role.ts +224 -0
- package/src/core/data-directory.ts +32 -0
- package/src/core/data-frame-routing.ts +309 -0
- package/src/core/default-command.ts +12 -0
- package/src/core/delegation-policy.ts +41 -0
- package/src/core/delivery-engine.ts +649 -0
- package/src/core/device-membership-verification.ts +125 -0
- package/src/core/device-membership.ts +130 -0
- package/src/core/directory-admission.ts +155 -0
- package/src/core/discovery-mdns.ts +247 -0
- package/src/core/discovery-tailscale.ts +224 -0
- package/src/core/discovery.ts +303 -0
- package/src/core/display-name.ts +73 -0
- package/src/core/dm-grant-actions.ts +72 -0
- package/src/core/dm-send-delegation.ts +73 -0
- package/src/core/dm-token-verification.ts +76 -0
- package/src/core/election-sessions.ts +135 -0
- package/src/core/error-reporting.ts +85 -0
- package/src/core/first-contact.ts +242 -0
- package/src/core/gateway-trust-actions.ts +184 -0
- package/src/core/gateway-trust.ts +305 -0
- package/src/core/gossip-directory.ts +276 -0
- package/src/core/gossip-extensions.ts +123 -0
- package/src/core/group-proofs.ts +205 -0
- package/src/core/handshake.ts +354 -0
- package/src/core/hub-forwarding.ts +24 -0
- package/src/core/hub-link.ts +176 -0
- package/src/core/hub-session.ts +374 -0
- package/src/core/identity-store.ts +715 -0
- package/src/core/identity.ts +489 -0
- package/src/core/index.ts +65 -0
- package/src/core/issuer-grouping.ts +165 -0
- package/src/core/issuer-identity-file.ts +187 -0
- package/src/core/lan-web-actions.ts +34 -0
- package/src/core/lan-web-uis.ts +55 -0
- package/src/core/ledger-lock.ts +81 -0
- package/src/core/listener-registry.ts +133 -0
- package/src/core/loopback-address.ts +22 -0
- package/src/core/machine-identity.ts +77 -0
- package/src/core/machine-list-groups.ts +53 -0
- package/src/core/mcp-registry-retry.ts +69 -0
- package/src/core/mcp-registry-version.ts +18 -0
- package/src/core/membership-proof.ts +139 -0
- package/src/core/membership-proofs.ts +149 -0
- package/src/core/mesh-graph-trace-tool.ts +81 -0
- package/src/core/mesh-graph.ts +89 -0
- package/src/core/mesh-network-actions.ts +183 -0
- package/src/core/mesh-store-options.ts +23 -0
- package/src/core/mesh-store-shared.ts +134 -0
- package/src/core/mesh-store.ts +1114 -0
- package/src/core/name-claim.ts +145 -0
- package/src/core/naming-actions.ts +90 -0
- package/src/core/naming.ts +138 -0
- package/src/core/nanoid.ts +20 -0
- package/src/core/network-interfaces.ts +24 -0
- package/src/core/package-version.ts +34 -0
- package/src/core/peer-lifecycle.ts +377 -0
- package/src/core/peer-link-error.ts +41 -0
- package/src/core/peer-versions.ts +98 -0
- package/src/core/pending-connection.ts +53 -0
- package/src/core/petnames.ts +121 -0
- package/src/core/pgp-keyserver.ts +32 -0
- package/src/core/pid-lock-file.ts +122 -0
- package/src/core/push-manager.ts +76 -0
- package/src/core/random-id.ts +9 -0
- package/src/core/receiver-set.ts +42 -0
- package/src/core/relay-role.ts +182 -0
- package/src/core/relay-selection.ts +120 -0
- package/src/core/relay-server.ts +198 -0
- package/src/core/relay-uplink.ts +78 -0
- package/src/core/request-timeouts.ts +37 -0
- package/src/core/room-discovery.ts +48 -0
- package/src/core/room-lifecycle.ts +838 -0
- package/src/core/room-lookup.ts +41 -0
- package/src/core/room-messaging.ts +247 -0
- package/src/core/room-notice-log.ts +61 -0
- package/src/core/room-path.ts +84 -0
- package/src/core/room-protocol.ts +893 -0
- package/src/core/room-router.ts +180 -0
- package/src/core/room-token-verification.ts +85 -0
- package/src/core/room-wire-extensions.ts +60 -0
- package/src/core/self-name-claim.ts +67 -0
- package/src/core/send-outcome.ts +69 -0
- package/src/core/shutdown-signals.ts +57 -0
- package/src/core/stale-agent-checker.ts +108 -0
- package/src/core/store.ts +704 -0
- package/src/core/token-text.ts +40 -0
- package/src/core/tool.ts +853 -0
- package/src/core/transport-events.ts +73 -0
- package/src/core/transport-queries.ts +54 -0
- package/src/core/transport.ts +367 -0
- package/src/core/types.ts +550 -0
- package/src/core/user-identity.ts +290 -0
- package/src/core/vapid.ts +81 -0
- package/src/core/version-check.ts +147 -0
- package/src/core/version-report-actions.ts +79 -0
- package/src/core/web-beacon.ts +31 -0
- package/src/core/web-push.ts +322 -0
- package/src/core/wire-mesh-core-version.ts +25 -0
- package/src/core/wire-mesh-identity.ts +36 -0
- package/src/core/wire-mesh-transport-options.ts +82 -0
- package/src/core/wire-mesh-transport.ts +1007 -0
- package/src/core/wire-protocol.ts +153 -0
- package/src/core/ws-dial.ts +191 -0
|
@@ -0,0 +1,1114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MeshStore — transport-agnostic peer mesh for agent communication.
|
|
3
|
+
*
|
|
4
|
+
* Each bridge instance is a peer in the mesh. Peers find each other through first contact (a UDP presence beaconing over loopback multicast to stores on this machine and over limited broadcast to other machines on the LAN) or through the well-known port, whose holder introduces joiners; neither is required for peers to keep talking once connected. The coordinator duties are held by an elected CoordinatorRole, not by the port holder. All state is held in memory and synchronised between peers. Delivery events are pushed directly over the transport, with no polling and no filesystem.
|
|
5
|
+
*
|
|
6
|
+
* Transport is set via setTransport() (e.g. WireMeshTransport for encrypted connections) before init() or any other transport-using method is called -- there is no default, since every real bridge builds its own transport from this store's own events getter, which needs the store to already exist.
|
|
7
|
+
*
|
|
8
|
+
* MeshStore itself is an orchestrator: it owns the shared state (the core agents/rooms/messages/dms/deliveryQueues Maps and a handful of smaller fields) and constructs the collaborators that implement almost every behaviour against direct references into that state -- DeliveryEngine, RoomProtocol, RoomMessaging, RoomLifecycle, AgentRegistry, ConnectionApproval, StaleAgentChecker, and PeerLifecycle. Every public method below that isn't inherently a MeshStore-level concern (transport/identity wiring, init/shutdown lifecycle, the events getter, mesh visibility, listener management) is a thin delegating wrapper to whichever collaborator now owns the real implementation, kept here only because CommsStore/MeshOnlyFeatures and a handful of concrete-only call sites (tests, bridge-mesh.ts, the web server, etc.) reach these names directly on a MeshStore-typed value.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { bytesToHex, deviceIdToHex } from "wire-mesh-core/domain/device-id";
|
|
12
|
+
import { GroupProofs } from "./group-proofs.js";
|
|
13
|
+
import { Naming } from "./naming.js";
|
|
14
|
+
import { Petnames } from "./petnames.js";
|
|
15
|
+
import {
|
|
16
|
+
ROOM_JOIN_APPROVAL_TIMEOUT_MS,
|
|
17
|
+
ROOM_REQUEST_TIMEOUT_MS,
|
|
18
|
+
} from "./request-timeouts.js";
|
|
19
|
+
import { DiscoveryManager } from "./discovery.js";
|
|
20
|
+
import { MdnsDiscoveryBackend } from "./discovery-mdns.js";
|
|
21
|
+
import { TailscaleDiscoveryBackend } from "./discovery-tailscale.js";
|
|
22
|
+
import type { MeshStoreIdentity } from "./mesh-store-shared.js";
|
|
23
|
+
import { GatewayTrust, type VerifiedMember } from "./gateway-trust.js";
|
|
24
|
+
import { ReceiverSet } from "./receiver-set.js";
|
|
25
|
+
import {
|
|
26
|
+
ConnectionCodeLedger,
|
|
27
|
+
type GenerateConnectionCodeOptions,
|
|
28
|
+
type RedeemConnectionCodeOptions,
|
|
29
|
+
type RedeemConnectionCodeResult,
|
|
30
|
+
} from "./connection-code.js";
|
|
31
|
+
import type { MeshStoreOptions } from "./mesh-store-options.js";
|
|
32
|
+
import { DeliveryEngine } from "./delivery-engine.js";
|
|
33
|
+
import { RoomProtocol } from "./room-protocol.js";
|
|
34
|
+
import { RoomMessaging } from "./room-messaging.js";
|
|
35
|
+
import { RoomLifecycle } from "./room-lifecycle.js";
|
|
36
|
+
import { AgentRegistry } from "./agent-registry.js";
|
|
37
|
+
import { ConnectionApproval } from "./connection-approval.js";
|
|
38
|
+
import { CapabilityAskAdmission } from "./capability-ask.js";
|
|
39
|
+
import type { IncomingManageRequest } from "wire-mesh-core/domain/mesh-session";
|
|
40
|
+
import type { DeviceId } from "wire-mesh-core/generated/protocol";
|
|
41
|
+
import {
|
|
42
|
+
followLedgerRevocations,
|
|
43
|
+
type AccountLedgerReplica,
|
|
44
|
+
} from "./account-ledger.js";
|
|
45
|
+
import { StaleAgentChecker } from "./stale-agent-checker.js";
|
|
46
|
+
import { PeerLifecycle } from "./peer-lifecycle.js";
|
|
47
|
+
import {
|
|
48
|
+
CoordinatorRole,
|
|
49
|
+
type CoordinatorClaimView,
|
|
50
|
+
} from "./coordinator-role.js";
|
|
51
|
+
import type { RoomVerbHandler } from "./room-router.js";
|
|
52
|
+
import { DEFAULT_PRESENCE_STALE_AFTER_MS } from "./gossip-extensions.js";
|
|
53
|
+
import {
|
|
54
|
+
hostedRoomAdverts,
|
|
55
|
+
type AgentSelfAdvert,
|
|
56
|
+
type HostedRoomAdvert,
|
|
57
|
+
} from "./gossip-extensions.js";
|
|
58
|
+
import { peerVersionReaders } from "./peer-versions.js";
|
|
59
|
+
import { listNetworkInterfaces } from "./network-interfaces.js";
|
|
60
|
+
import {
|
|
61
|
+
parseListenerPolicy,
|
|
62
|
+
requireMeshGraph,
|
|
63
|
+
requireMeshTrace,
|
|
64
|
+
} from "./transport-queries.js";
|
|
65
|
+
import { ACCOUNT_JOIN_VERB, AccountJoin } from "./account-join.js";
|
|
66
|
+
import { LanWebUis } from "./lan-web-uis.js";
|
|
67
|
+
import { AGENT_DETAILS_VERB, AgentDetailsExchange } from "./agent-details.js";
|
|
68
|
+
import type {
|
|
69
|
+
MeshStatePatch,
|
|
70
|
+
PeerInfo,
|
|
71
|
+
SerialisedState,
|
|
72
|
+
} from "./wire-protocol.js";
|
|
73
|
+
import type {
|
|
74
|
+
ListenerInfo,
|
|
75
|
+
MeshGraph,
|
|
76
|
+
MeshTraceResult,
|
|
77
|
+
MeshTransport,
|
|
78
|
+
TransportEvents,
|
|
79
|
+
} from "./transport.js";
|
|
80
|
+
import { createTransportEvents } from "./transport-events.js";
|
|
81
|
+
import type {
|
|
82
|
+
CommsStore,
|
|
83
|
+
SendRoomMessageOptions,
|
|
84
|
+
SentDm,
|
|
85
|
+
SentRoomMessage,
|
|
86
|
+
} from "./comms-store.js";
|
|
87
|
+
import type { CapabilityToken } from "wire-mesh-core/generated/protocol";
|
|
88
|
+
import type {
|
|
89
|
+
AgentIdentity,
|
|
90
|
+
AgentStatus,
|
|
91
|
+
ConnectionCode,
|
|
92
|
+
DeliveryEvent,
|
|
93
|
+
DmMessage,
|
|
94
|
+
MeshVisibility,
|
|
95
|
+
NetworkInterface,
|
|
96
|
+
Room,
|
|
97
|
+
RoomMessage,
|
|
98
|
+
RoomType,
|
|
99
|
+
StreamingBehavior,
|
|
100
|
+
Visibility,
|
|
101
|
+
} from "./types.js";
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Constants
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
const DEFAULT_COORDINATOR_PORT = 19876;
|
|
108
|
+
/** A device-id is a SHA-256 digest, so the stand-in peer ID a store starts with (every bridge replaces it with its identity's own device-id) has the same shape and can take part in the coordinator election. */
|
|
109
|
+
const DEVICE_ID_BYTE_LENGTH = 32;
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// MeshStore
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
export class MeshStore implements CommsStore {
|
|
116
|
+
peerId: string;
|
|
117
|
+
readonly startedAt: string;
|
|
118
|
+
readonly coordinatorPort: number;
|
|
119
|
+
private readonly hubUrl: string | undefined;
|
|
120
|
+
/** Read by whoever wires this store to a transport, so both halves of a room.join agree on the approval window. */
|
|
121
|
+
readonly roomJoinApprovalTimeoutMs: number;
|
|
122
|
+
/** Forwards a collaborator's failure to this store's own error channel, normalising a non-Error rejection on the way so a bridge always sees one shape. An arrow field rather than a method, so every collaborator can take it as a bare callback reference. */
|
|
123
|
+
private readonly reportError = (error: unknown): void => {
|
|
124
|
+
this.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** This device's own membership proofs under its user principal and its machine, and the checks for other devices' proofs (agent-comms#266, agent-comms#343). */
|
|
128
|
+
readonly membership = new GroupProofs({
|
|
129
|
+
getIdentity: () => this.requireIdentity(),
|
|
130
|
+
peekIdentity: () => this.storeIdentity,
|
|
131
|
+
getPeerId: () => this.peerId,
|
|
132
|
+
requireTransport: () => this.requireTransport(),
|
|
133
|
+
onError: this.reportError,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
private readonly agents = new Map<string, AgentIdentity>();
|
|
137
|
+
private readonly rooms = new Map<string, Room>();
|
|
138
|
+
private readonly messages = new Map<string, RoomMessage[]>();
|
|
139
|
+
private readonly dms = new Map<string, DmMessage[]>();
|
|
140
|
+
private readonly deliveryQueues = new Map<string, DeliveryEvent[]>();
|
|
141
|
+
private readonly identityCache = new Map<string, { id: string }>();
|
|
142
|
+
private readonly peerInfo = new Map<string, PeerInfo>();
|
|
143
|
+
private readonly localDeliveryKeys = new Set<string>();
|
|
144
|
+
private readonly pendingMarkReadTimers: ReturnType<typeof setTimeout>[] = [];
|
|
145
|
+
/** DM paths this store has itself sent an outbound room.join for -- section 6's own basis for auto-approving the counterpart's reciprocal room.join without a second human decision. Shared between RoomProtocol (reads, in handleRoomJoin) and RoomLifecycle (writes, in requestDmAccess). Never cleared: a DM conversation, once opened, stays open, and holding a stale entry here costs nothing beyond a few bytes per DM this node has ever initiated. */
|
|
146
|
+
private readonly dmRequestsInitiatedByMe = new Set<string>();
|
|
147
|
+
|
|
148
|
+
private transport: MeshTransport | undefined;
|
|
149
|
+
private storeIdentity: MeshStoreIdentity | undefined;
|
|
150
|
+
private isShutDown = false;
|
|
151
|
+
private initialised = false;
|
|
152
|
+
|
|
153
|
+
discovery: DiscoveryManager;
|
|
154
|
+
|
|
155
|
+
/** The cross-machine trust boundary (agent-comms#156), constructed once in the constructor below (mirroring discovery above) and shared with WireMeshTransport by every construction site (bridge-mesh.ts, test-transport.ts) that passes it into WireMeshTransport's own constructor, so store.addTrustedGateway() and the transport's own hub-forwarding/hub-session gates read the exact same set. Persists across restarts (agent-comms#186) when a slot is passed to this store's own constructor; stays in-memory only, exactly as before, for every construction site that omits one. Public so those construction sites can reach it; addTrustedGateway/removeTrustedGateway/listTrustedGateways below are the methods CommsTool actually calls through MeshOnlyFeatures. */
|
|
156
|
+
readonly gatewayTrust: GatewayTrust;
|
|
157
|
+
|
|
158
|
+
/** Petnames, this machine's own name, and the namer every listing formats ids with (agent-comms#345). Handed to CommsTool alongside discovery. Petnames live beside gatewayTrust's file when a slot is given, in memory otherwise. */
|
|
159
|
+
readonly naming: Naming;
|
|
160
|
+
|
|
161
|
+
/** The connection-code generate/redeem pair (agent-comms#188) bootstrapping gatewayTrust above between two devices with no existing mesh connection. Persists across restarts per slot, from the slot passed to this store's own constructor, whereas gatewayTrust is shared by every slot in the slot's identity directory. generateConnectionCode/redeemConnectionCode below are the methods CommsTool actually calls through MeshOnlyFeatures; redeemConnectionCode is also where a successful redemption's deviceId gets fed into gatewayTrust.add, the actual point of this whole bootstrap. */
|
|
162
|
+
private readonly connectionCodes: ConnectionCodeLedger;
|
|
163
|
+
|
|
164
|
+
/** The LAN web UIs this machine's first-contact presence has heard web beacons for (agent-comms#353), listed by the lan_web_uis action. */
|
|
165
|
+
readonly lanWebUis = new LanWebUis();
|
|
166
|
+
|
|
167
|
+
/** Issues and answers account invites, and joins this machine to an account (agent-comms#344). Public so `agent-comms account invite` and `agent-comms account join` (account-cli.ts) reach it; deliberately not on CommsTool's surface (see account-join.ts). */
|
|
168
|
+
readonly account: AccountJoin;
|
|
169
|
+
|
|
170
|
+
private readonly deliveryEngine: DeliveryEngine;
|
|
171
|
+
private readonly roomProtocol: RoomProtocol;
|
|
172
|
+
private readonly roomMessaging: RoomMessaging;
|
|
173
|
+
private readonly roomLifecycle: RoomLifecycle;
|
|
174
|
+
private readonly agentRegistry: AgentRegistry;
|
|
175
|
+
private readonly connectionApproval: ConnectionApproval;
|
|
176
|
+
private readonly capabilityAskAdmission: CapabilityAskAdmission;
|
|
177
|
+
private readonly staleAgentChecker: StaleAgentChecker;
|
|
178
|
+
private readonly coordinatorRole: CoordinatorRole;
|
|
179
|
+
private readonly peerLifecycle: PeerLifecycle;
|
|
180
|
+
|
|
181
|
+
/** Whether this store holds the elected coordinator role (agent-comms#341), and so runs the stale-agent probe, announces departures, and fronts local Claude Code sessions. Independent of whether its transport holds the well-known port. */
|
|
182
|
+
get holdsCoordinatorRole(): boolean {
|
|
183
|
+
return this.coordinatorRole.isHolder();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** The coordinator claim this store currently accepts (the elected holder's device-id and the term that put it there), or undefined before it has made or heard one. */
|
|
187
|
+
get coordinatorClaim(): CoordinatorClaimView | undefined {
|
|
188
|
+
return this.coordinatorRole.current();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** This store's own current AgentStatus, synchronously -- the value WireMeshTransport's presence re-advertisement timer reads on every tick. undefined before registerAgent has ever run (no self agent record exists yet), in which case there is nothing yet to advertise. */
|
|
192
|
+
get selfStatus(): AgentStatus | undefined {
|
|
193
|
+
return this.agents.get(this.peerId)?.status;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** This store's own currently-hosted public/private rooms, synchronously, in the lightweight gossip-safe shape WireMeshTransport's own hosted-rooms re-advertisement timer reads on every tick -- the write half of P3.8's room-discovery replacement for createRoom's own broadcastPatch (agent-comms#48). Secret rooms are never included (never worth advertising at all), and a room this store has merely replicated via the legacy room_upsert broadcast, rather than owns, is excluded too -- "hosted" means "this device is the one a joiner should actually reach", which only its own owner is. */
|
|
197
|
+
get hostedRooms(): readonly HostedRoomAdvert[] {
|
|
198
|
+
return hostedRoomAdverts(this.rooms, this.peerId);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** This store's own gossip-safe agent-identity advert, synchronously, in the shape WireMeshTransport's own gossip re-advertisement timer reads on every tick -- the write half of P3.8's eventual agent register/update/offline retirement (agent-comms#48). undefined before registerAgent has ever run (nothing to advertise yet), or when this agent's own visibility isn't "visible" -- gossip already reaches every connected peer regardless of mesh-approval status (see wire-mesh-transport.ts's own allSessions/quarantine comments), so advertising a hidden or ghost agent's identity this way would leak exactly what those visibility levels exist to withhold. */
|
|
202
|
+
get selfAgentAdvert(): AgentSelfAdvert | undefined {
|
|
203
|
+
const agent = this.agents.get(this.peerId);
|
|
204
|
+
if (agent?.visibility !== "visible") return undefined;
|
|
205
|
+
return {
|
|
206
|
+
name: agent.name,
|
|
207
|
+
harness: agent.harness,
|
|
208
|
+
cwd: agent.cwd,
|
|
209
|
+
pid: agent.pid,
|
|
210
|
+
startedAt: agent.startedAt,
|
|
211
|
+
tags: agent.tags,
|
|
212
|
+
subscribedRooms: agent.subscribedRooms,
|
|
213
|
+
...this.membership.advertFields(),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Whether this store should hold a hub session right now. It has something to say to another machine only when it has an agent that is not a ghost and its machine trusts at least one remote device, principal or machine. Until then, holding a session would put its device id on a public hub for nobody's benefit, and a ghost agent is by definition not reachable from anywhere. A hidden agent does hold one, and advertises only the device advert every session opens with, which is what lets it be reached by device id.
|
|
219
|
+
*/
|
|
220
|
+
private wantsHubSession(): boolean {
|
|
221
|
+
const self = this.agents.get(this.peerId);
|
|
222
|
+
return (
|
|
223
|
+
self !== undefined &&
|
|
224
|
+
self.visibility !== "ghost" &&
|
|
225
|
+
this.gatewayTrust.hasAny()
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Has the hub session re-evaluate wantsHubSession now, after a change to the agent or to the trust that it reads. Changes made by another process reach it within the link's poll interval instead. */
|
|
230
|
+
private reconsiderHub(): void {
|
|
231
|
+
this.requireTransport().reconsiderHub?.();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Whether the mesh has a live coordinator connection. */
|
|
235
|
+
get connected(): boolean {
|
|
236
|
+
return (
|
|
237
|
+
this.requireTransport().isCoordinator ||
|
|
238
|
+
this.requireTransport().hasCoordinatorConnection
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
onDelivery:
|
|
243
|
+
| ((agentId: string, event: DeliveryEvent) => void | Promise<void>)
|
|
244
|
+
| undefined;
|
|
245
|
+
|
|
246
|
+
private readonly deliveryReceivers = new ReceiverSet<
|
|
247
|
+
[agentId: string, event: DeliveryEvent]
|
|
248
|
+
>();
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Observes every delivery event this store's own agent receives, alongside onDelivery, and returns a function that stops observing. Several parties can share one store this way, a bridge's own delivery handler and the web UI it serves, where a single assignable onDelivery would let whichever assigned last silence the other. A listener that throws or rejects is reported through onError and does not stop the others.
|
|
252
|
+
*/
|
|
253
|
+
addDeliveryListener(
|
|
254
|
+
listener: (agentId: string, event: DeliveryEvent) => void | Promise<void>,
|
|
255
|
+
): () => void {
|
|
256
|
+
return this.deliveryReceivers.add(listener);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Fires for every state patch — both locally generated and remote. */
|
|
260
|
+
onPatch: ((patch: MeshStatePatch) => void | Promise<void>) | undefined;
|
|
261
|
+
|
|
262
|
+
private readonly patchReceivers = new ReceiverSet<[patch: MeshStatePatch]>();
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Observes every state patch, alongside onPatch, and returns a function that stops observing. The counterpart of addDeliveryListener: the web UI a bridge serves publishes patches from the bridge's own store, and must not take over the one assignable onPatch. A listener that throws or rejects is reported through onError and does not stop the others.
|
|
266
|
+
*/
|
|
267
|
+
addPatchListener(
|
|
268
|
+
listener: (patch: MeshStatePatch) => void | Promise<void>,
|
|
269
|
+
): () => void {
|
|
270
|
+
return this.patchReceivers.add(listener);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Fires for a transport-level error a caller may want to observe (a connection rejected for presenting a certificate that doesn't match its claimed peer ID, this store's own inability to join or create a mesh, etc). Left undefined by default (matching onDelivery/onPatch): a caller that wants visibility sets it, exactly like those two.
|
|
275
|
+
*/
|
|
276
|
+
onError: ((error: Error) => void) | undefined;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Fires whenever this store gains or loses the elected coordinator role (agent-comms#341): true once a claim naming this store is accepted, false when a superseding claim is accepted or right before shutdown() gives the role up. Holding the well-known port has no bearing on it. A call runs only once the previous one has finished, so a caller stopping something asynchronously is never asked to start it again mid-stop. Left undefined by default (matching onDelivery/onPatch/onError): a caller that wants to react to owning the coordinator role (e.g. bridge-mesh.ts starting/stopping the cc-peer front (agent-comms#157), a Node/filesystem-specific capability that has no place in this transport-agnostic core) sets it, exactly like those three.
|
|
280
|
+
*/
|
|
281
|
+
onCoordinatorRoleChanged:
|
|
282
|
+
((isCoordinator: boolean) => void | Promise<void>) | undefined;
|
|
283
|
+
|
|
284
|
+
/** 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 (agent-comms#198) -- undefined by default (matching onDelivery/onPatch/onError/onCoordinatorRoleChanged), since every other bridge never loads the cc-peer package at all. Set once, right after construction, by front-runtime.ts/bridges/cc-peer/run.ts (a plain closure over cc-peer's own exported CC_PEER_VERSION constant) -- read by both this store's own WireMeshTransport construction (bridge-mesh.ts, for the gossiped versions advert) and CommsTool (for whoami/update/list_agents' own self-report), so the one field backs both surfaces. */
|
|
285
|
+
getCcPeerVersion: (() => string | undefined) | undefined;
|
|
286
|
+
|
|
287
|
+
/** Serialise the full mesh state for state_sync messages. */
|
|
288
|
+
serialise(): SerialisedState {
|
|
289
|
+
return {
|
|
290
|
+
agents: Object.fromEntries(this.agents),
|
|
291
|
+
rooms: Object.fromEntries(this.rooms),
|
|
292
|
+
messages: Object.fromEntries(this.messages),
|
|
293
|
+
dms: Object.fromEntries(this.dms),
|
|
294
|
+
deliveryQueues: Object.fromEntries(this.deliveryQueues),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
constructor(options?: Readonly<MeshStoreOptions>) {
|
|
299
|
+
const {
|
|
300
|
+
coordinatorPort = DEFAULT_COORDINATOR_PORT,
|
|
301
|
+
hubUrl,
|
|
302
|
+
roomJoinApprovalTimeoutMs = ROOM_JOIN_APPROVAL_TIMEOUT_MS,
|
|
303
|
+
presenceStaleAfterMs = DEFAULT_PRESENCE_STALE_AFTER_MS,
|
|
304
|
+
coordinatorClaimWaitMs = ROOM_REQUEST_TIMEOUT_MS,
|
|
305
|
+
slot,
|
|
306
|
+
} = options ?? {};
|
|
307
|
+
this.peerId = bytesToHex(
|
|
308
|
+
crypto.getRandomValues(new Uint8Array(DEVICE_ID_BYTE_LENGTH)),
|
|
309
|
+
);
|
|
310
|
+
this.startedAt = new Date().toISOString();
|
|
311
|
+
this.coordinatorPort = coordinatorPort;
|
|
312
|
+
this.hubUrl = hubUrl;
|
|
313
|
+
this.roomJoinApprovalTimeoutMs = roomJoinApprovalTimeoutMs;
|
|
314
|
+
this.gatewayTrust = new GatewayTrust({ location: slot });
|
|
315
|
+
this.naming = new Naming({
|
|
316
|
+
petnames: new Petnames(slot),
|
|
317
|
+
issuerNames: async () => this.membership.issuerNames(),
|
|
318
|
+
listAgents: async (requesterId) => this.listAgents(requesterId),
|
|
319
|
+
saveMachineName: async (name) => this.membership.saveMachineName(name),
|
|
320
|
+
saveUserName: async (name) => this.membership.saveUserName(name),
|
|
321
|
+
});
|
|
322
|
+
this.connectionCodes = new ConnectionCodeLedger(slot);
|
|
323
|
+
this.account = new AccountJoin({
|
|
324
|
+
requireIdentity: () => this.requireIdentity(),
|
|
325
|
+
requireTransport: () => this.requireTransport(),
|
|
326
|
+
getPeerId: () => this.peerId,
|
|
327
|
+
connectionCodes: this.connectionCodes,
|
|
328
|
+
gatewayTrust: this.gatewayTrust,
|
|
329
|
+
reconsiderHub: this.reconsiderHub.bind(this),
|
|
330
|
+
setPetname: (deviceHex, name) => this.naming.setPetname(deviceHex, name),
|
|
331
|
+
redeemConnectionCode: this.redeemConnectionCode.bind(this),
|
|
332
|
+
setIdentity: this.setIdentity.bind(this),
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Discovery manager — registers available backends
|
|
336
|
+
this.discovery = new DiscoveryManager();
|
|
337
|
+
this.discovery.registerBackend(new MdnsDiscoveryBackend());
|
|
338
|
+
this.discovery.registerBackend(new TailscaleDiscoveryBackend());
|
|
339
|
+
|
|
340
|
+
this.deliveryEngine = new DeliveryEngine({
|
|
341
|
+
agents: this.agents,
|
|
342
|
+
rooms: this.rooms,
|
|
343
|
+
messages: this.messages,
|
|
344
|
+
dms: this.dms,
|
|
345
|
+
deliveryQueues: this.deliveryQueues,
|
|
346
|
+
localDeliveryKeys: this.localDeliveryKeys,
|
|
347
|
+
pendingMarkReadTimers: this.pendingMarkReadTimers,
|
|
348
|
+
getPeerId: () => this.peerId,
|
|
349
|
+
requireIdentity: () => this.requireIdentity(),
|
|
350
|
+
requireTransport: () => this.requireTransport(),
|
|
351
|
+
getOnDelivery: () =>
|
|
352
|
+
this.deliveryReceivers.handler(this.onDelivery, this.reportError),
|
|
353
|
+
getOnPatch: () =>
|
|
354
|
+
this.patchReceivers.handler(this.onPatch, this.reportError),
|
|
355
|
+
isShutDown: () => this.isShutDown,
|
|
356
|
+
// RoomProtocol doesn't exist yet at this point in the constructor -- this closure resolves `this.roomProtocol` lazily, only once markRead actually calls it at runtime, well after the constructor has finished.
|
|
357
|
+
sendRoomRequestToMember: async (...args) =>
|
|
358
|
+
this.roomProtocol.sendRoomRequestToMember(...args),
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
this.roomProtocol = new RoomProtocol({
|
|
362
|
+
rooms: this.rooms,
|
|
363
|
+
messages: this.messages,
|
|
364
|
+
dms: this.dms,
|
|
365
|
+
agents: this.agents,
|
|
366
|
+
dmRequestsInitiatedByMe: this.dmRequestsInitiatedByMe,
|
|
367
|
+
getPeerId: () => this.peerId,
|
|
368
|
+
requireIdentity: () => this.requireIdentity(),
|
|
369
|
+
requireTransport: () => this.requireTransport(),
|
|
370
|
+
deliveryEngine: this.deliveryEngine,
|
|
371
|
+
joinDecisionTimeoutMs: this.roomJoinApprovalTimeoutMs,
|
|
372
|
+
// RoomLifecycle doesn't exist yet at this point -- deferred the same way DeliveryEngine's sendRoomRequestToMember closure above is.
|
|
373
|
+
revokeMemberGrant: async (roomId, memberId) =>
|
|
374
|
+
this.roomLifecycle.revokeMemberGrant(roomId, memberId),
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
this.roomMessaging = new RoomMessaging({
|
|
378
|
+
rooms: this.rooms,
|
|
379
|
+
messages: this.messages,
|
|
380
|
+
dms: this.dms,
|
|
381
|
+
requireIdentity: () => this.requireIdentity(),
|
|
382
|
+
roomProtocol: this.roomProtocol,
|
|
383
|
+
// RoomLifecycle doesn't exist yet at this point -- deferred the same lazy-`this`-capture way as the closures above.
|
|
384
|
+
requestDmAccess: async (counterpart) =>
|
|
385
|
+
this.roomLifecycle.requestDmAccess(counterpart),
|
|
386
|
+
resolveRoomId: (roomIdOrName) =>
|
|
387
|
+
this.roomLifecycle.resolveRoomId(roomIdOrName),
|
|
388
|
+
// AgentRegistry doesn't exist yet at this point in the constructor -- deferred the same lazy-`this`-capture way DeliveryEngine's own sendRoomRequestToMember closure above is.
|
|
389
|
+
resolveAgent: async (id) => this.agentRegistry.getAgent(id),
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
this.roomLifecycle = new RoomLifecycle({
|
|
393
|
+
rooms: this.rooms,
|
|
394
|
+
messages: this.messages,
|
|
395
|
+
agents: this.agents,
|
|
396
|
+
dmRequestsInitiatedByMe: this.dmRequestsInitiatedByMe,
|
|
397
|
+
getPeerId: () => this.peerId,
|
|
398
|
+
requireIdentity: () => this.requireIdentity(),
|
|
399
|
+
requireTransport: () => this.requireTransport(),
|
|
400
|
+
deliveryEngine: this.deliveryEngine,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
this.agentRegistry = new AgentRegistry({
|
|
404
|
+
agents: this.agents,
|
|
405
|
+
identityCache: this.identityCache,
|
|
406
|
+
startedAt: this.startedAt,
|
|
407
|
+
getPeerId: () => this.peerId,
|
|
408
|
+
requireTransport: () => this.requireTransport(),
|
|
409
|
+
presenceStaleAfterMs,
|
|
410
|
+
deliveryEngine: this.deliveryEngine,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
this.connectionApproval = new ConnectionApproval({
|
|
414
|
+
agents: this.agents,
|
|
415
|
+
peerInfo: this.peerInfo,
|
|
416
|
+
startedAt: this.startedAt,
|
|
417
|
+
getPeerId: () => this.peerId,
|
|
418
|
+
requireTransport: () => this.requireTransport(),
|
|
419
|
+
getOnDelivery: () =>
|
|
420
|
+
this.deliveryReceivers.handler(this.onDelivery, this.reportError),
|
|
421
|
+
queueDelivery: (agentId, event) => {
|
|
422
|
+
this.deliveryEngine.queueDelivery(agentId, event);
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
this.capabilityAskAdmission = new CapabilityAskAdmission({
|
|
427
|
+
getPeerId: () => this.peerId,
|
|
428
|
+
getOnDelivery: () =>
|
|
429
|
+
this.deliveryReceivers.handler(this.onDelivery, this.reportError),
|
|
430
|
+
queueDelivery: (agentId, event) => {
|
|
431
|
+
this.deliveryEngine.queueDelivery(agentId, event);
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
this.staleAgentChecker = new StaleAgentChecker({
|
|
436
|
+
agents: this.agents,
|
|
437
|
+
deliveryQueues: this.deliveryQueues,
|
|
438
|
+
identityCache: this.identityCache,
|
|
439
|
+
peerInfo: this.peerInfo,
|
|
440
|
+
notifyRoomsOfStatus: async (agentId, status) =>
|
|
441
|
+
this.deliveryEngine.notifyRoomsOfStatus(agentId, status),
|
|
442
|
+
broadcastPatch: async (patch) =>
|
|
443
|
+
this.deliveryEngine.broadcastPatch(patch),
|
|
444
|
+
bump: (agent) => {
|
|
445
|
+
this.deliveryEngine.bump(agent);
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
this.coordinatorRole = new CoordinatorRole({
|
|
450
|
+
getPeerId: () => this.peerId,
|
|
451
|
+
requireTransport: () => this.requireTransport(),
|
|
452
|
+
claimWaitMs: coordinatorClaimWaitMs,
|
|
453
|
+
onGained: async () => {
|
|
454
|
+
this.staleAgentChecker.start();
|
|
455
|
+
this.requireTransport().setRelayEligible?.(true);
|
|
456
|
+
await this.onCoordinatorRoleChanged?.(true);
|
|
457
|
+
},
|
|
458
|
+
onLost: async () => {
|
|
459
|
+
this.staleAgentChecker.stop();
|
|
460
|
+
this.requireTransport().setRelayEligible?.(false);
|
|
461
|
+
await this.onCoordinatorRoleChanged?.(false);
|
|
462
|
+
},
|
|
463
|
+
onError: this.reportError,
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
this.peerLifecycle = new PeerLifecycle({
|
|
467
|
+
peerInfo: this.peerInfo,
|
|
468
|
+
agents: this.agents,
|
|
469
|
+
coordinatorPort: this.coordinatorPort,
|
|
470
|
+
firstContactPort: options?.firstContactPort,
|
|
471
|
+
onWebBeacon: (b) => {
|
|
472
|
+
this.lanWebUis.recordBeacon(b);
|
|
473
|
+
},
|
|
474
|
+
getPeerId: () => this.peerId,
|
|
475
|
+
getCoordinatorPeerId: () => this.requireTransport().coordinatorPeerId,
|
|
476
|
+
requireTransport: () => this.requireTransport(),
|
|
477
|
+
serialise: () => this.serialise(),
|
|
478
|
+
roomProtocol: this.roomProtocol,
|
|
479
|
+
deliveryEngine: this.deliveryEngine,
|
|
480
|
+
coordinatorRole: this.coordinatorRole,
|
|
481
|
+
onError: this.reportError,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** Sets the transport (e.g. WireMeshTransport for encrypted connections). Must be called before init() or any other transport-using method. */
|
|
486
|
+
setTransport(transport: Readonly<MeshTransport>): void {
|
|
487
|
+
this.transport = transport;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** The set transport, or throws if setTransport() hasn't been called yet -- the single place every transport-using method reads through, so the "must call setTransport() first" contract is enforced at one boundary rather than checked ad hoc at each call site. */
|
|
491
|
+
private requireTransport(): MeshTransport {
|
|
492
|
+
if (this.transport === undefined) {
|
|
493
|
+
throw new Error(
|
|
494
|
+
"MeshStore: no transport set; call setTransport() before using the store",
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
return this.transport;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Stops feeding the current account ledger's replicated revocations into the RevocationView; undefined until an identity is attached. */
|
|
501
|
+
private stopFollowingLedger: (() => void) | undefined;
|
|
502
|
+
|
|
503
|
+
/** Sets the identity/clock/slot this store mints and persists room-membership grants against. Must be called before createRoom() or any other identity-using method, mirroring setTransport()'s own contract. */
|
|
504
|
+
setIdentity(identity: MeshStoreIdentity): void {
|
|
505
|
+
this.storeIdentity = identity;
|
|
506
|
+
this.membership.start();
|
|
507
|
+
this.stopFollowingLedger?.();
|
|
508
|
+
this.stopFollowingLedger = followLedgerRevocations(
|
|
509
|
+
identity,
|
|
510
|
+
this.reportError,
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** The account's replicated grant ledger (agent-comms#344) once an identity is attached, for the transport to replicate. */
|
|
515
|
+
get accountLedger(): AccountLedgerReplica | undefined {
|
|
516
|
+
return this.storeIdentity?.accountLedger;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** Every device this side trusts only because a trusted principal or machine vouches for it, with that issuer. */
|
|
520
|
+
readonly listVerifiedMembers = (): VerifiedMember[] =>
|
|
521
|
+
this.gatewayTrust.listVerifiedMembers();
|
|
522
|
+
|
|
523
|
+
/** The machine (hex) of each device this side can place on one, keyed by device id (agent-comms#343): this device's own, and every gossiped device carrying a valid machine proof. Empty before this store's identity is attached. */
|
|
524
|
+
readonly listAgentMachines = async (): Promise<Map<string, string>> =>
|
|
525
|
+
await this.membership.machinesByDevice();
|
|
526
|
+
|
|
527
|
+
/** The set identity, or throws if setIdentity() hasn't been called yet -- the single place every identity-using method reads through, mirroring requireTransport() above. */
|
|
528
|
+
private requireIdentity(): MeshStoreIdentity {
|
|
529
|
+
if (this.storeIdentity === undefined) {
|
|
530
|
+
throw new Error(
|
|
531
|
+
"MeshStore: no identity set; call setIdentity() before using the store",
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
return this.storeIdentity;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// -----------------------------------------------------------------------
|
|
538
|
+
// Mesh lifecycle
|
|
539
|
+
// -----------------------------------------------------------------------
|
|
540
|
+
|
|
541
|
+
async init(): Promise<void> {
|
|
542
|
+
if (this.initialised) return;
|
|
543
|
+
this.initialised = true;
|
|
544
|
+
await this.requireTransport().startDataServer();
|
|
545
|
+
if (this.hubUrl !== undefined) {
|
|
546
|
+
this.requireTransport().joinHub?.(this.hubUrl, () =>
|
|
547
|
+
this.wantsHubSession(),
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Register our own peer info
|
|
552
|
+
this.peerInfo.set(this.peerId, {
|
|
553
|
+
id: this.peerId,
|
|
554
|
+
port: this.requireTransport().dataPort,
|
|
555
|
+
startedAt: this.startedAt,
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
await this.peerLifecycle.joinWellKnownPort();
|
|
559
|
+
|
|
560
|
+
this.requireTransport().unref();
|
|
561
|
+
|
|
562
|
+
// First contact (agent-comms#341): a coordinator-free presence on this machine or LAN, owned by PeerLifecycle; a no-op unless a firstContactPort was configured, which only bridge-mesh (every real bridge's shared bootstrap) does.
|
|
563
|
+
this.peerLifecycle.startFirstContact();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// -----------------------------------------------------------------------
|
|
567
|
+
// Transport events accessor (for bridges to wire up)
|
|
568
|
+
// -----------------------------------------------------------------------
|
|
569
|
+
|
|
570
|
+
/** Returns the TransportEvents object that bridges should pass to the transport constructor. */
|
|
571
|
+
get events(): TransportEvents {
|
|
572
|
+
return createTransportEvents({
|
|
573
|
+
peerLifecycle: this.peerLifecycle,
|
|
574
|
+
connectionApproval: this.connectionApproval,
|
|
575
|
+
deliveryEngine: this.deliveryEngine,
|
|
576
|
+
roomProtocol: this.roomProtocol,
|
|
577
|
+
coordinatorRole: this.coordinatorRole,
|
|
578
|
+
reportError: this.reportError,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** Both halves of the agent-comms/agent-details exchange (agent-comms#323): the verb a trusted peer's request is answered by, and the background fetch filling this side's cache from peers this machine trusts. */
|
|
583
|
+
readonly agentDetailsExchange = new AgentDetailsExchange({
|
|
584
|
+
selfAgent: () => this.agents.get(this.peerId),
|
|
585
|
+
hostedRooms: () => this.hostedRooms,
|
|
586
|
+
isTrusted: (hex) => this.gatewayTrust.isTrusted(hex),
|
|
587
|
+
requireTransport: () => this.requireTransport(),
|
|
588
|
+
});
|
|
589
|
+
/** Room verb handlers this store registers with its own WireMeshTransport, keyed by params.verb per room-router.ts's own dispatch discipline. */
|
|
590
|
+
get roomVerbHandlers(): Partial<Record<string, RoomVerbHandler>> {
|
|
591
|
+
return {
|
|
592
|
+
...this.roomProtocol.roomVerbHandlers,
|
|
593
|
+
[ACCOUNT_JOIN_VERB]: this.account.handleJoinRequest,
|
|
594
|
+
[AGENT_DETAILS_VERB]: this.agentDetailsExchange.handler,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// -----------------------------------------------------------------------
|
|
599
|
+
// CommsStore — Identity
|
|
600
|
+
// -----------------------------------------------------------------------
|
|
601
|
+
|
|
602
|
+
async readIdentity(
|
|
603
|
+
harness: string,
|
|
604
|
+
cwd: string,
|
|
605
|
+
): Promise<{ id: string } | undefined> {
|
|
606
|
+
return this.agentRegistry.readIdentity(harness, cwd);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async writeIdentity(harness: string, cwd: string, id: string): Promise<void> {
|
|
610
|
+
return this.agentRegistry.writeIdentity(harness, cwd, id);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// -----------------------------------------------------------------------
|
|
614
|
+
// CommsStore — Agent registry
|
|
615
|
+
// -----------------------------------------------------------------------
|
|
616
|
+
|
|
617
|
+
async registerAgent(opts: {
|
|
618
|
+
name: string;
|
|
619
|
+
harness: string;
|
|
620
|
+
cwd: string;
|
|
621
|
+
pid: number;
|
|
622
|
+
visibility: Visibility;
|
|
623
|
+
tags: string[];
|
|
624
|
+
}): Promise<AgentIdentity> {
|
|
625
|
+
const agent = await this.agentRegistry.registerAgent(opts);
|
|
626
|
+
this.reconsiderHub();
|
|
627
|
+
return agent;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
async getAgent(id: string): Promise<AgentIdentity | undefined> {
|
|
631
|
+
return this.agentRegistry.getAgent(id);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async updateAgent(
|
|
635
|
+
id: string,
|
|
636
|
+
patch: Partial<
|
|
637
|
+
Pick<AgentIdentity, "name" | "visibility" | "status" | "tags" | "pid">
|
|
638
|
+
>,
|
|
639
|
+
): Promise<AgentIdentity> {
|
|
640
|
+
const agent = await this.agentRegistry.updateAgent(id, patch);
|
|
641
|
+
this.reconsiderHub();
|
|
642
|
+
return agent;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
async listAgents(requesterId: string): Promise<AgentIdentity[]> {
|
|
646
|
+
return this.agentRegistry.listAgents(requesterId);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async setAgentOffline(id: string): Promise<void> {
|
|
650
|
+
return this.agentRegistry.setAgentOffline(id);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// -----------------------------------------------------------------------
|
|
654
|
+
// CommsStore — Rooms
|
|
655
|
+
// -----------------------------------------------------------------------
|
|
656
|
+
|
|
657
|
+
async createRoom(
|
|
658
|
+
opts: Readonly<{
|
|
659
|
+
name: string;
|
|
660
|
+
type: RoomType;
|
|
661
|
+
owner: string;
|
|
662
|
+
description: string;
|
|
663
|
+
}>,
|
|
664
|
+
): Promise<Room> {
|
|
665
|
+
return this.roomLifecycle.createRoom(opts);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async getRoom(id: string): Promise<Room | undefined> {
|
|
669
|
+
return this.roomLifecycle.getRoom(id);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
async listRooms(requesterId: string): Promise<Room[]> {
|
|
673
|
+
return this.roomLifecycle.listRooms(requesterId);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** Refreshes this store's own local copy of a named room's membership and room-state via a real room.members request (P3.6). Concrete-only -- reached directly by tests. */
|
|
677
|
+
async refreshRoomMembers(roomPath: string): Promise<Room> {
|
|
678
|
+
return this.roomLifecycle.refreshRoomMembers(roomPath);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** The requester's own half of section 6's two-round DM consent flow, optionally presenting a dm:send grant (agent-comms#162) the counterpart's own user principal already minted for this device via admitAgentForDm -- when given and valid, the counterpart auto-admits immediately rather than holding the request open for a human decision. Deliberately outside the CommsStore interface, like connection approval, since it is a wire-mesh-specific concern FileStore has no equivalent for. Concrete-only -- reached directly by tests. */
|
|
682
|
+
async requestDmAccess(
|
|
683
|
+
counterpart: string,
|
|
684
|
+
dmSendGrant?: CapabilityToken,
|
|
685
|
+
): Promise<void> {
|
|
686
|
+
return this.roomLifecycle.requestDmAccess(counterpart, dmSendGrant);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/** Presents a dm:send grant issued by another user -- see RoomLifecycle.presentDmGrant. */
|
|
690
|
+
async presentDmGrant(
|
|
691
|
+
counterpart: string,
|
|
692
|
+
grant: CapabilityToken,
|
|
693
|
+
): Promise<void> {
|
|
694
|
+
return this.roomLifecycle.presentDmGrant(counterpart, grant);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** This user's own principal id (hex): the identity every device on this account shares, and the thing another user names to admit or trust this whole user rather than one device. Undefined until this store's identity is attached. */
|
|
698
|
+
getUserPrincipalId(): string | undefined {
|
|
699
|
+
const user = this.storeIdentity?.userIdentity;
|
|
700
|
+
return user === undefined ? undefined : deviceIdToHex(user.deviceId);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/** This store's machine id (hex, agent-comms#343): the identity every bridge this OS account runs on the host shares, and the thing another machine names to trust or revoke all of them at once. Undefined until this store's identity is attached. */
|
|
704
|
+
getMachineId(): string | undefined {
|
|
705
|
+
const machine = this.storeIdentity?.machineIdentity;
|
|
706
|
+
return machine === undefined ? undefined : deviceIdToHex(machine.deviceId);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** Admits bearerId into this user's own DM-communication scope (agent-comms#162): mints and persists a dm:send grant, self-signed by this store's own user principal. Returns the minted token for the caller to deliver to bearerId out of band. Deliberately outside the CommsStore interface, like requestDmAccess above. Concrete-only -- reached directly by tests. delegationsRemaining defaults to 0 (non-delegable, the original behaviour); a positive value admits bearerId as a user principal capable of sub-delegating to its own devices (agent-comms#187) -- see RoomLifecycle.admitAgentForDm's own doc comment. */
|
|
710
|
+
async admitAgentForDm(
|
|
711
|
+
bearerId: string,
|
|
712
|
+
delegationsRemaining = 0,
|
|
713
|
+
): Promise<CapabilityToken> {
|
|
714
|
+
return this.roomLifecycle.admitAgentForDm(bearerId, delegationsRemaining);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** Revokes bearerId's own dm:send grant for real (agent-comms#162), the DM-scope counterpart to kickFromRoom. A no-op if bearerId was never admitted. Deliberately outside the CommsStore interface, like requestDmAccess above. Concrete-only -- reached directly by tests. */
|
|
718
|
+
async revokeAgentDmAccess(bearerId: string): Promise<void> {
|
|
719
|
+
return this.roomLifecycle.revokeAgentDmAccess(bearerId);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
async joinRoom(roomId: string, agentId: string): Promise<Room> {
|
|
723
|
+
return this.roomLifecycle.joinRoom(roomId, agentId);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
async leaveRoom(roomId: string, agentId: string): Promise<void> {
|
|
727
|
+
return this.roomLifecycle.leaveRoom(roomId, agentId);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
async inviteToRoom(
|
|
731
|
+
roomId: string,
|
|
732
|
+
targetId: string,
|
|
733
|
+
inviterId: string,
|
|
734
|
+
): Promise<void> {
|
|
735
|
+
return this.roomLifecycle.inviteToRoom(roomId, targetId, inviterId);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
async declineInvite(
|
|
739
|
+
roomId: string,
|
|
740
|
+
agentId: string,
|
|
741
|
+
reason: string,
|
|
742
|
+
): Promise<void> {
|
|
743
|
+
return this.roomLifecycle.declineInvite(roomId, agentId, reason);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
async kickFromRoom(
|
|
747
|
+
roomId: string,
|
|
748
|
+
targetId: string,
|
|
749
|
+
kickerId: string,
|
|
750
|
+
): Promise<void> {
|
|
751
|
+
return this.roomLifecycle.kickFromRoom(roomId, targetId, kickerId);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
async destroyRoom(roomId: string, agentId: string): Promise<void> {
|
|
755
|
+
return this.roomLifecycle.destroyRoom(roomId, agentId);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// -----------------------------------------------------------------------
|
|
759
|
+
// CommsStore — Messages
|
|
760
|
+
// -----------------------------------------------------------------------
|
|
761
|
+
|
|
762
|
+
async sendRoomMessage(
|
|
763
|
+
roomId: string,
|
|
764
|
+
from: string,
|
|
765
|
+
content: string,
|
|
766
|
+
options?: SendRoomMessageOptions,
|
|
767
|
+
): Promise<SentRoomMessage> {
|
|
768
|
+
return this.roomMessaging.sendRoomMessage(roomId, from, content, options);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
async readRoomMessages(
|
|
772
|
+
roomId: string,
|
|
773
|
+
since?: string,
|
|
774
|
+
): Promise<RoomMessage[]> {
|
|
775
|
+
return this.roomMessaging.readRoomMessages(roomId, since);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// -----------------------------------------------------------------------
|
|
779
|
+
// CommsStore — DMs
|
|
780
|
+
// -----------------------------------------------------------------------
|
|
781
|
+
|
|
782
|
+
async sendDm(
|
|
783
|
+
from: string,
|
|
784
|
+
to: string,
|
|
785
|
+
content: string,
|
|
786
|
+
streamingBehavior?: StreamingBehavior,
|
|
787
|
+
): Promise<SentDm> {
|
|
788
|
+
return this.roomMessaging.sendDm(from, to, content, streamingBehavior);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// -----------------------------------------------------------------------
|
|
792
|
+
// CommsStore — Delivery
|
|
793
|
+
// -----------------------------------------------------------------------
|
|
794
|
+
|
|
795
|
+
async deliver(agentId: string, event: DeliveryEvent): Promise<void> {
|
|
796
|
+
return this.deliveryEngine.deliver(agentId, event);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
async drainDelivery(agentId: string): Promise<DeliveryEvent[]> {
|
|
800
|
+
return this.deliveryEngine.drainDelivery(agentId);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// -----------------------------------------------------------------------
|
|
804
|
+
// Concrete-only surface reached directly by tests
|
|
805
|
+
// -----------------------------------------------------------------------
|
|
806
|
+
|
|
807
|
+
/** Merge a peer's state snapshot into the local state (#27/#28). Concrete-only -- reached directly by tests. */
|
|
808
|
+
applyStateSync(state: SerialisedState): void {
|
|
809
|
+
this.deliveryEngine.applyStateSync(state);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/** Sends one directed room.send to a single member's own session (P3.5). Concrete-only -- reached directly by tests. */
|
|
813
|
+
async sendRoomMessageDirected(
|
|
814
|
+
roomPath: string,
|
|
815
|
+
memberId: string,
|
|
816
|
+
text: string,
|
|
817
|
+
): Promise<void> {
|
|
818
|
+
return this.roomProtocol.sendRoomMessageDirected(roomPath, memberId, text);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/** Starts only the data server without connecting to a coordinator. Used for testing scenarios where the peer connects via connectToRemote. */
|
|
822
|
+
async startDataServerOnly(): Promise<void> {
|
|
823
|
+
return this.connectionApproval.startDataServerOnly();
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// -----------------------------------------------------------------------
|
|
827
|
+
// Connection approval (mesh-only)
|
|
828
|
+
// -----------------------------------------------------------------------
|
|
829
|
+
|
|
830
|
+
/** Accept a pending inbound connection. */
|
|
831
|
+
async acceptConnection(connectionId: string): Promise<void> {
|
|
832
|
+
return this.connectionApproval.acceptConnection(connectionId);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/** Reject a pending inbound connection. */
|
|
836
|
+
async rejectConnection(connectionId: string, reason: string): Promise<void> {
|
|
837
|
+
return this.connectionApproval.rejectConnection(connectionId, reason);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/** List all pending inbound connections awaiting approval. */
|
|
841
|
+
listPendingConnections(): {
|
|
842
|
+
connectionId: string;
|
|
843
|
+
peerId: string;
|
|
844
|
+
dataPort: number;
|
|
845
|
+
name: string;
|
|
846
|
+
fingerprint: string;
|
|
847
|
+
}[] {
|
|
848
|
+
return this.connectionApproval.listPendingConnections();
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/** Initiate an outbound connection to a remote coordinator requiring approval. */
|
|
852
|
+
async connectToRemote(host: string, port: number): Promise<void> {
|
|
853
|
+
return this.connectionApproval.connectToRemote(host, port);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// -----------------------------------------------------------------------
|
|
857
|
+
// Room-join approval (mesh-only)
|
|
858
|
+
// -----------------------------------------------------------------------
|
|
859
|
+
|
|
860
|
+
/** Every room.join request currently held open awaiting this store's own accept/reject decision. */
|
|
861
|
+
listPendingRoomJoins(): { roomPath: string; requesterId: string }[] {
|
|
862
|
+
return this.roomProtocol.listPendingRoomJoins();
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/** Approves a pending room.join request. */
|
|
866
|
+
acceptRoomJoin(roomPath: string, requesterId: string): void {
|
|
867
|
+
this.roomProtocol.acceptRoomJoin(roomPath, requesterId);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/** Denies a pending room.join request, optionally with a reason. */
|
|
871
|
+
rejectRoomJoin(roomPath: string, requesterId: string, reason?: string): void {
|
|
872
|
+
this.roomProtocol.rejectRoomJoin(roomPath, requesterId, reason);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// -----------------------------------------------------------------------
|
|
876
|
+
// Capability-request ask tier (mesh-only, agent-comms#165)
|
|
877
|
+
// -----------------------------------------------------------------------
|
|
878
|
+
|
|
879
|
+
/** Builds a manage-request handler surfacing one capability's incoming capability-requests as held-open asks (agent-comms#165's own tool-layer surface for the ask tier) -- the registration point a capability-gated verb (e.g. agent-comms#162's dm:send) wires into its own session dispatch, backed by this store's identity/clock and this admission's live pending state. */
|
|
880
|
+
createCapabilityAskHandler(
|
|
881
|
+
options: Readonly<{
|
|
882
|
+
capability: string;
|
|
883
|
+
bearerDevice: DeviceId;
|
|
884
|
+
timeoutMs: number;
|
|
885
|
+
}>,
|
|
886
|
+
): (incoming: Readonly<IncomingManageRequest>) => Promise<void> {
|
|
887
|
+
const { identity, clock } = this.requireIdentity();
|
|
888
|
+
return this.capabilityAskAdmission.createAskHandler({
|
|
889
|
+
capability: options.capability,
|
|
890
|
+
identity,
|
|
891
|
+
clock,
|
|
892
|
+
bearerDevice: options.bearerDevice,
|
|
893
|
+
timeoutMs: options.timeoutMs,
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/** Every capability-request currently held open awaiting this store's own accept/reject decision. */
|
|
898
|
+
listPendingCapabilityRequests(): {
|
|
899
|
+
requestId: string;
|
|
900
|
+
capability: string;
|
|
901
|
+
scopeKind: string;
|
|
902
|
+
scopePath?: string;
|
|
903
|
+
requesterDevice: string;
|
|
904
|
+
}[] {
|
|
905
|
+
return this.capabilityAskAdmission.listPendingCapabilityRequests();
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/** Approves a pending capability request, minting and returning the granted token. */
|
|
909
|
+
async acceptCapabilityRequest(
|
|
910
|
+
requestId: string,
|
|
911
|
+
options: Readonly<{
|
|
912
|
+
expires: number;
|
|
913
|
+
delegationsRemaining?: number;
|
|
914
|
+
capability?: string;
|
|
915
|
+
}>,
|
|
916
|
+
): Promise<void> {
|
|
917
|
+
await this.capabilityAskAdmission.acceptCapabilityRequest(
|
|
918
|
+
requestId,
|
|
919
|
+
options,
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/** Denies a pending capability request, optionally with a reason. */
|
|
924
|
+
async rejectCapabilityRequest(
|
|
925
|
+
requestId: string,
|
|
926
|
+
reason?: string,
|
|
927
|
+
): Promise<void> {
|
|
928
|
+
await this.capabilityAskAdmission.rejectCapabilityRequest(
|
|
929
|
+
requestId,
|
|
930
|
+
reason,
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// -----------------------------------------------------------------------
|
|
935
|
+
// Mesh visibility
|
|
936
|
+
// -----------------------------------------------------------------------
|
|
937
|
+
|
|
938
|
+
/** Set mesh discovery visibility. Delegates to discovery manager. */
|
|
939
|
+
async setVisibility(level: MeshVisibility, adapter?: string): Promise<void> {
|
|
940
|
+
await this.discovery.setVisibility(level, adapter);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/** Get current mesh discovery visibility. */
|
|
944
|
+
getVisibility(adapter?: string): MeshVisibility {
|
|
945
|
+
return this.discovery.getVisibility(adapter);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// -----------------------------------------------------------------------
|
|
949
|
+
// Gateway trust (agent-comms#156) -- the cross-machine trust boundary
|
|
950
|
+
// -----------------------------------------------------------------------
|
|
951
|
+
|
|
952
|
+
/** Trusts a remote device-id (hex): this store will advertise itself on the hub, merge this device's gossiped directory entries, dispatch its relayed requests, and route outbound hub requests to it. The trust is shared by every store on the machine and survives a restart. See GatewayTrust's own class doc for why it is keyed per device-id rather than per remote machine. Trusting a device is itself a route to it appearing, so anything queued for it is retried immediately rather than waiting out the hub's next gossip tick. */
|
|
953
|
+
addTrustedGateway(deviceHex: string): void {
|
|
954
|
+
this.gatewayTrust.add(deviceHex);
|
|
955
|
+
this.reconsiderHub();
|
|
956
|
+
void this.roomProtocol.flushPendingRoomRequests(deviceHex);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/** Withdraws trust from a remote device-id (hex). A no-op if it was never trusted. */
|
|
960
|
+
removeTrustedGateway(deviceHex: string): void {
|
|
961
|
+
this.gatewayTrust.remove(deviceHex);
|
|
962
|
+
this.reconsiderHub();
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/** Every currently trusted remote device-id (hex). */
|
|
966
|
+
readonly listTrustedGateways = (): string[] => this.gatewayTrust.list();
|
|
967
|
+
|
|
968
|
+
/** Trusts a remote user-principal device-id (hex, agent-comms#187): a peer presenting a token whose delegation chain roots at this principal is trusted via GatewayTrust.isTrustedFor, without its own bare device-id ever needing individual trust. Entirely independent of the bare-device allowlist addTrustedGateway manages. */
|
|
969
|
+
addTrustedGatewayPrincipal(deviceHex: string): void {
|
|
970
|
+
this.gatewayTrust.addPrincipal(deviceHex);
|
|
971
|
+
this.reconsiderHub();
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/** Withdraws trust from a remote user-principal device-id (hex). A no-op if it was never trusted. */
|
|
975
|
+
removeTrustedGatewayPrincipal(deviceHex: string): void {
|
|
976
|
+
this.gatewayTrust.removePrincipal(deviceHex);
|
|
977
|
+
this.reconsiderHub();
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/** Every currently trusted remote user-principal device-id (hex). */
|
|
981
|
+
readonly listTrustedGatewayPrincipals = (): string[] =>
|
|
982
|
+
this.gatewayTrust.listPrincipals();
|
|
983
|
+
|
|
984
|
+
/** Trusts a remote machine device-id (hex, agent-comms#343): every device whose gossiped proof that machine vouches for becomes reachable without being trusted individually. */
|
|
985
|
+
addTrustedGatewayMachine(deviceHex: string): void {
|
|
986
|
+
this.gatewayTrust.addMachine(deviceHex);
|
|
987
|
+
this.reconsiderHub();
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/** Withdraws trust from a remote machine device-id (hex), and with it every device reachable only through that machine: revoking every bridge that machine key vouches for in one act. A no-op if it was never trusted. */
|
|
991
|
+
removeTrustedGatewayMachine(deviceHex: string): void {
|
|
992
|
+
this.gatewayTrust.removeMachine(deviceHex);
|
|
993
|
+
this.reconsiderHub();
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/** Every currently trusted remote machine device-id (hex). */
|
|
997
|
+
readonly listTrustedGatewayMachines = (): string[] =>
|
|
998
|
+
this.gatewayTrust.listMachines();
|
|
999
|
+
|
|
1000
|
+
// -----------------------------------------------------------------------
|
|
1001
|
+
// Connection codes (agent-comms#188) -- bootstrapping gateway trust with no existing mesh connection between the two devices
|
|
1002
|
+
// -----------------------------------------------------------------------
|
|
1003
|
+
|
|
1004
|
+
/** Generates a fresh single-use ConnectionCode vouching for this store's own device-id (peerId), for the operator to relay to a counterpart out of band. See ConnectionCodeLedger.generate's own doc comment for what options does. */
|
|
1005
|
+
async generateConnectionCode(
|
|
1006
|
+
options: Readonly<GenerateConnectionCodeOptions> = {},
|
|
1007
|
+
): Promise<ConnectionCode> {
|
|
1008
|
+
return this.connectionCodes.generate(this.peerId, options);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/** Validates a candidate ConnectionCode (see ConnectionCodeLedger.redeem for the checks performed) and, only once it passes every check, trusts the device-id it vouches for via gatewayTrust.add -- the actual point of this whole bootstrap. A rejected candidate never reaches gatewayTrust at all. */
|
|
1012
|
+
async redeemConnectionCode(
|
|
1013
|
+
candidate: Readonly<ConnectionCode>,
|
|
1014
|
+
options: Readonly<RedeemConnectionCodeOptions> = {},
|
|
1015
|
+
): Promise<RedeemConnectionCodeResult> {
|
|
1016
|
+
const result = await this.connectionCodes.redeem(candidate, options);
|
|
1017
|
+
this.gatewayTrust.add(result.deviceId);
|
|
1018
|
+
this.reconsiderHub();
|
|
1019
|
+
return result;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// -----------------------------------------------------------------------
|
|
1023
|
+
// Listener management (coordinator only)
|
|
1024
|
+
// -----------------------------------------------------------------------
|
|
1025
|
+
|
|
1026
|
+
async addListener(
|
|
1027
|
+
host: string,
|
|
1028
|
+
port: number,
|
|
1029
|
+
policy: string,
|
|
1030
|
+
): Promise<string> {
|
|
1031
|
+
return this.requireTransport().addListener(
|
|
1032
|
+
host,
|
|
1033
|
+
port,
|
|
1034
|
+
parseListenerPolicy(policy),
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
async removeListener(id: string): Promise<void> {
|
|
1039
|
+
await this.requireTransport().removeListener(id);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
readonly listListeners = (): ListenerInfo[] =>
|
|
1043
|
+
this.requireTransport().listListeners();
|
|
1044
|
+
|
|
1045
|
+
/** Delegates to the transport's own meshGraph, throwing if the current transport doesn't support it (agent-comms#199) -- CommsTool's own notMeshBacked distinguishes "no MeshStore at all" from this narrower "MeshStore, but a transport without this capability" case by catching the throw, the same way requireTransport's own "no transport set" throw is already handled. */
|
|
1046
|
+
meshGraph(): MeshGraph {
|
|
1047
|
+
return requireMeshGraph(this.requireTransport());
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/** Delegates to the transport's own meshTrace, same "throw if unsupported" contract as meshGraph above. */
|
|
1051
|
+
async meshTrace(
|
|
1052
|
+
target: string,
|
|
1053
|
+
timeoutMs?: number,
|
|
1054
|
+
): Promise<MeshTraceResult> {
|
|
1055
|
+
return requireMeshTrace(this.requireTransport(), target, timeoutMs);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
readonly getNetworkInterfaces = (): NetworkInterface[] =>
|
|
1059
|
+
listNetworkInterfaces();
|
|
1060
|
+
|
|
1061
|
+
// -----------------------------------------------------------------------
|
|
1062
|
+
// Peer versions (agent-comms#198)
|
|
1063
|
+
// -----------------------------------------------------------------------
|
|
1064
|
+
|
|
1065
|
+
private readonly peerVersions = peerVersionReaders(() =>
|
|
1066
|
+
this.requireTransport(),
|
|
1067
|
+
);
|
|
1068
|
+
|
|
1069
|
+
/** deviceId's own gossiped agent-comms package version, if this side has heard it advertised. */
|
|
1070
|
+
readonly getPeerAgentCommsVersion = this.peerVersions.agentComms;
|
|
1071
|
+
|
|
1072
|
+
/** deviceId's own gossiped cc-peer package version, present only while that device is fronting a cc-peer session or running `bridge cc-peer`. */
|
|
1073
|
+
readonly getPeerCcPeerVersion = this.peerVersions.ccPeer;
|
|
1074
|
+
|
|
1075
|
+
/** deviceId's own gossiped wire-mesh-core version (wire-mesh#179). */
|
|
1076
|
+
readonly getPeerWireMeshCoreVersion = this.peerVersions.wireMeshCore;
|
|
1077
|
+
|
|
1078
|
+
/** Asks deviceId for its own, currently-running wire-mesh-core version live rather than trusting whatever it last gossiped (the query_version action's backing call). */
|
|
1079
|
+
readonly queryVersion = this.peerVersions.query;
|
|
1080
|
+
|
|
1081
|
+
// -----------------------------------------------------------------------
|
|
1082
|
+
// Shutdown
|
|
1083
|
+
// -----------------------------------------------------------------------
|
|
1084
|
+
|
|
1085
|
+
async shutdown(): Promise<void> {
|
|
1086
|
+
// Idempotent: a bridge's signal handler and whatever already owns its exit path can both reach here, and a second pass would try to hand the coordinator role on over sessions the first pass has already closed.
|
|
1087
|
+
if (this.isShutDown) return;
|
|
1088
|
+
this.isShutDown = true;
|
|
1089
|
+
this.membership.stop();
|
|
1090
|
+
this.peerLifecycle.stopFirstContact();
|
|
1091
|
+
// Clear any pending markRead timers so they don't fire after the transport is shut down (which would attempt sends on closed sockets) or keep the event loop alive after process.exit().
|
|
1092
|
+
for (const timer of this.pendingMarkReadTimers) {
|
|
1093
|
+
clearTimeout(timer);
|
|
1094
|
+
}
|
|
1095
|
+
this.pendingMarkReadTimers.length = 0;
|
|
1096
|
+
|
|
1097
|
+
const agent = this.agents.get(this.peerId);
|
|
1098
|
+
// Sent first, so the notice goes out ahead of every session closing, but not waited on until the transport has shut down. A peer that cannot be told is a peer that has gone, so a failure to send it is dropped. Each peer's answer comes under the network deadline, so waiting here would spend that whole deadline on a peer that has gone without this side noticing yet; closing the sessions is what ends every wait that is still open.
|
|
1099
|
+
let farewell: Promise<void> | undefined;
|
|
1100
|
+
if (agent) {
|
|
1101
|
+
agent.status = "offline";
|
|
1102
|
+
farewell = this.deliveryEngine
|
|
1103
|
+
.broadcastPatch({ type: "agent_offline", agentId: this.peerId })
|
|
1104
|
+
.catch(() => undefined);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// Gives the elected coordinator role up (stopping the stale-agent probe and firing onCoordinatorRoleChanged(false)) when this store held it; survivors recover it from this store's departure.
|
|
1108
|
+
await this.coordinatorRole.stop();
|
|
1109
|
+
// Graceful handover of the well-known port listener (agent-comms#170): a no-op unless this side currently holds it, so this runs unconditionally rather than being gated behind an isCoordinator check duplicated here. Must run before the transport shuts down: the handoff message rides the very peer sessions shutdown() is about to close.
|
|
1110
|
+
await this.peerLifecycle.sendCoordinatorHandover();
|
|
1111
|
+
await this.requireTransport().shutdown();
|
|
1112
|
+
await farewell;
|
|
1113
|
+
}
|
|
1114
|
+
}
|