agent-comms 8.15.9 → 8.15.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +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/dist/test/pi-manifest-packed.integration.test.d.ts +2 -0
- package/dist/test/pi-manifest-packed.integration.test.d.ts.map +1 -0
- package/dist/test/pi-manifest-packed.integration.test.js +68 -0
- package/dist/test/pi-manifest-packed.integration.test.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,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HubSession -- the relay-hub connection mode (agent-comms#151), split from wire-mesh-transport.ts under the max-lines cap the same way connection-approval.ts and its siblings were. A hub connection (wss://mesh.exadev.io/) is a RELAY, not a coordinator: no connect_request/introduce approval applies. The session's own self-advert is forwarded by the hub to every other connected agent and the hub answers with a catch-up of everyone already there, so peers discover each other purely through gossip. Messages ride relay-connect pairings -- sendManageRequest's own targetDevice routing, which the session layer wraps as relay-data to exactly that device.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
acceptMeshSession,
|
|
7
|
+
type AcceptedMeshSession,
|
|
8
|
+
type DataDomainFrame,
|
|
9
|
+
type DirectoryEntry,
|
|
10
|
+
type IncomingManageRequest,
|
|
11
|
+
type ManageOutcome,
|
|
12
|
+
} from "wire-mesh-core/domain/mesh-session";
|
|
13
|
+
import { deviceIdToHex } from "wire-mesh-core/domain/device-id";
|
|
14
|
+
import { tracePath as coreTracePath } from "wire-mesh-core/domain/path-trace";
|
|
15
|
+
import type {
|
|
16
|
+
CapabilityScope,
|
|
17
|
+
CapabilityToken,
|
|
18
|
+
Frame,
|
|
19
|
+
ManageCommand,
|
|
20
|
+
} from "wire-mesh-core/generated/protocol";
|
|
21
|
+
import type { IdentityPort } from "wire-mesh-core/ports/identity";
|
|
22
|
+
import type { Connection } from "wire-mesh-core/ports/transport";
|
|
23
|
+
import type {
|
|
24
|
+
ConnectionHandle,
|
|
25
|
+
MeshTraceResult,
|
|
26
|
+
TransportEvents,
|
|
27
|
+
} from "./transport.js";
|
|
28
|
+
import {
|
|
29
|
+
ROOM_REQUEST_TIMEOUT_MS,
|
|
30
|
+
manageRequestTimeoutMs,
|
|
31
|
+
} from "./request-timeouts.js";
|
|
32
|
+
import type { RoomRequestOrigin } from "./room-router.js";
|
|
33
|
+
import { connectWsUrl } from "./ws-dial.js";
|
|
34
|
+
import type { UplinkFeed } from "./relay-uplink.js";
|
|
35
|
+
import { DOMAIN, FRAME_VERB } from "./wire-mesh-transport.js";
|
|
36
|
+
|
|
37
|
+
/** How long a room-domain request routed through the hub's relay-connect/relay-data pairing waits for a response before giving up. Unlike an ordinary local peer session, a relay-connect naming an unknown target-device is silently dropped by the hub (spec/relay-hub's own documented behaviour -- no error frame exists for "no such device"), so a request to a device that turns out not to be reachable via any gateway would otherwise hang forever rather than surfacing as a normal "not reachable" outcome. */
|
|
38
|
+
|
|
39
|
+
export interface HubSessionDeps {
|
|
40
|
+
/** How long a room.join sent through this session may await a human decision at the receiving end -- see manageRequestTimeoutMs. */
|
|
41
|
+
roomJoinApprovalTimeoutMs: number;
|
|
42
|
+
/** Resolves this node's own identity port once ready. */
|
|
43
|
+
identityReady: Promise<Readonly<IdentityPort>>;
|
|
44
|
+
events: Readonly<TransportEvents>;
|
|
45
|
+
/** The transport's own shutdown gate -- checked around every await, the same discipline the transport itself applies. */
|
|
46
|
+
isShuttingDown: () => boolean;
|
|
47
|
+
/** The addresses this node advertises in its own gossip self-advert. */
|
|
48
|
+
advertisedAddresses: readonly string[];
|
|
49
|
+
/** Registers a frame observer + dispatch for raw frames arriving on the hub connection (the transport's own handleDataFrame path). */
|
|
50
|
+
onFrame: (
|
|
51
|
+
connection: Readonly<Connection>,
|
|
52
|
+
frame: Readonly<Frame>,
|
|
53
|
+
) => void | Promise<void>;
|
|
54
|
+
/** Receives each core/data frame that arrived through a relay pairing, with the device (hex) the pairing's secure channel authenticated as its sender, never the one the hub stamps. Frames that arrived directly on the hub connection are the hub mailbox's, not a device's, and never reach it. */
|
|
55
|
+
onRelayedDataFrame: (
|
|
56
|
+
fromDeviceHex: string,
|
|
57
|
+
frame: Readonly<DataDomainFrame>,
|
|
58
|
+
) => Promise<void>;
|
|
59
|
+
/** Tracks the session for shutdown -- every session the transport ever creates, always. */
|
|
60
|
+
trackForShutdown: (session: AcceptedMeshSession) => void;
|
|
61
|
+
/** Stops tracking a session that has ended, so a store that redials the hub for as long as it runs does not accumulate dead sessions the transport would keep gossiping to. */
|
|
62
|
+
untrack: (session: AcceptedMeshSession) => void;
|
|
63
|
+
/** Fires with every remote (non-self) directory entry the hub's own gossip/catch-up surfaces, every time the session's directory changes (agent-comms#155's own remote-directory-merge leg) -- the transport merges these into its own mesh-wide knownDevices the same way it already merges a local peer session's directory, so a hub-learned agent surfaces in listAgents/getAgent with no separate lookup path. */
|
|
64
|
+
onDirectory: (entries: readonly DirectoryEntry[]) => void;
|
|
65
|
+
/** Dispatches an already-received, non-legacy-frame request (a real core/room verb: room.send, room.notify, room.join, ...) to WireMeshTransport's own roomRouter through its relayed-request entry point, which shares the room-verb dispatch a local peer session's own drainSession uses but never routes a legacy frame -- the outbound half of agent-comms#155's "remote to local" routing leg. Deferred the same lazy-`this`-capture way DeliveryEngine's own sendRoomRequestToMember closure is, since roomRouter is constructed after this class's own instance in WireMeshTransport's constructor. Declared with the identical (request, handle, origin?) shape RoomRouter.handleRequest itself has (agent-comms#216) so WireMeshTransport can keep wiring this dep as a direct method reference rather than a wrapper -- dispatchHubRequest below is what actually supplies a non-empty origin, carrying this side's own dialled hub address. */
|
|
66
|
+
handleRoomRequest: (
|
|
67
|
+
request: IncomingManageRequest,
|
|
68
|
+
handle: Readonly<ConnectionHandle>,
|
|
69
|
+
origin?: Readonly<RoomRequestOrigin>,
|
|
70
|
+
) => Promise<void>;
|
|
71
|
+
/** The gateway trust boundary (agent-comms#156): whether the given device-id (hex) is currently trusted as a bare device. It only decides which room-domain senders dispatchHubRequest records as known hub peers; a real room-domain verb's own dispatch is never gated on it at all, see dispatchHubRequest's own doc for why that is sound. connect()'s own directory-merge filter uses admitEntries below instead, not this. */
|
|
72
|
+
isTrusted: (deviceHex: string) => boolean;
|
|
73
|
+
/** Whether a gossiped directory entry may be merged (agent-comms#156, widened by #187 and #266): its device is trusted by id, or is itself a trusted principal's own device (agent-comms#192), or carries a membership proof a trusted principal vouches for. A gossiped advert is self-asserted, so the proof is what makes trusting a principal cover its devices; verifying one is cryptography, hence asynchronous. */
|
|
74
|
+
admitEntries: (
|
|
75
|
+
entries: readonly DirectoryEntry[],
|
|
76
|
+
) => Promise<DirectoryEntry[]>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class HubSession {
|
|
80
|
+
private session: AcceptedMeshSession | undefined;
|
|
81
|
+
/** The connection the held session runs over, which a relay served by this store takes as its uplink. */
|
|
82
|
+
private connection: Readonly<Connection> | undefined;
|
|
83
|
+
/** Where the frames read from the hub are also fed while a relay served by this store has taken the session's connection as its uplink. */
|
|
84
|
+
private uplink: UplinkFeed | undefined;
|
|
85
|
+
/** The URL this side itself dialled to reach the hub -- tracePath's own local.hubAddress (agent-comms#199), the equivalent of wire-mesh-core's own TracePathOptions.localHubAddress. */
|
|
86
|
+
private url: string | undefined;
|
|
87
|
+
private readonly hubPeersKnown = new Set<string>();
|
|
88
|
+
/** Resolvers of every whenDisconnected() call still waiting for the live session to end. */
|
|
89
|
+
private readonly disconnectWaiters: (() => void)[] = [];
|
|
90
|
+
|
|
91
|
+
constructor(private readonly deps: Readonly<HubSessionDeps>) {}
|
|
92
|
+
|
|
93
|
+
/** This node's own device-id, hex-encoded -- the identifier hub peers address it by. */
|
|
94
|
+
async ownDeviceHex(): Promise<string> {
|
|
95
|
+
const identity = await this.deps.identityReady;
|
|
96
|
+
return deviceIdToHex(identity.deviceId);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Whether a hub session is currently live (connect() has resolved and disconnect() hasn't run since, and the far end hasn't closed it -- watchDisconnect clears this.session when the hub's own event stream reports closed). */
|
|
100
|
+
get isConnected(): boolean {
|
|
101
|
+
return this.session !== undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Whether the given session object is this side's own currently-held hub session -- lets WireMeshTransport distinguish the hub's session from an ordinary local-peer session within its own allSessions bookkeeping (agent-comms#156's own readvertiseGossip gate needs this) without this class ever exposing the raw session object itself. */
|
|
105
|
+
ownsSession(session: Readonly<object>): boolean {
|
|
106
|
+
return this.session === session;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Resolves once no hub session is live: at once when none is, otherwise when the current one ends, whether the hub closed it or disconnect() dropped it. HubLink waits on this to know when to dial again. */
|
|
110
|
+
async whenDisconnected(): Promise<void> {
|
|
111
|
+
if (this.session === undefined) return;
|
|
112
|
+
await new Promise<void>((resolve) => {
|
|
113
|
+
this.disconnectWaiters.push(resolve);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Forgets the held session and releases everyone waiting in whenDisconnected(). */
|
|
118
|
+
private clearSession(): void {
|
|
119
|
+
if (this.session !== undefined) this.deps.untrack(this.session);
|
|
120
|
+
this.detachUplink();
|
|
121
|
+
this.session = undefined;
|
|
122
|
+
this.connection = undefined;
|
|
123
|
+
this.url = undefined;
|
|
124
|
+
for (const resolve of this.disconnectWaiters.splice(0)) resolve();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Lets a relay this store serves take the held session's connection as its uplink: attach receives the connection and returns the feed this session then hands every frame it reads from the hub, so the relay carries its clients over the same connection the session uses. Returns whether a session was held to attach to. The uplink ends with the session, or when detachUplink is called. */
|
|
128
|
+
attachUplink(
|
|
129
|
+
attach: (connection: Readonly<Connection>) => UplinkFeed,
|
|
130
|
+
): boolean {
|
|
131
|
+
if (this.connection === undefined) return false;
|
|
132
|
+
this.detachUplink();
|
|
133
|
+
this.uplink = attach(this.connection);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Whether a relay currently has this session's connection as its uplink. */
|
|
138
|
+
get hasUplink(): boolean {
|
|
139
|
+
return this.uplink !== undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Ends the uplink taken from this session, if any. The session itself is untouched. */
|
|
143
|
+
detachUplink(): void {
|
|
144
|
+
this.uplink?.end();
|
|
145
|
+
this.uplink = undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Drops the held hub connection. A no-op if none is live (connect() was never called, disconnect() already ran, or the hub itself already closed the session). */
|
|
149
|
+
async disconnect(): Promise<void> {
|
|
150
|
+
const session = this.session;
|
|
151
|
+
if (session === undefined) return;
|
|
152
|
+
this.clearSession();
|
|
153
|
+
await session.close();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The device ids (hex) of peers discovered through the hub's gossiped directory. */
|
|
157
|
+
peers(): readonly string[] {
|
|
158
|
+
return [...this.hubPeersKnown];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Dials the hub and participates as a peer (see the class doc for the discovery and routing model). */
|
|
162
|
+
async connect(url: string): Promise<void> {
|
|
163
|
+
const connection = await connectWsUrl(url);
|
|
164
|
+
if (this.deps.isShuttingDown()) {
|
|
165
|
+
await connection.close();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const identity = await this.deps.identityReady;
|
|
169
|
+
const session = await acceptMeshSession(connection, identity, [DOMAIN], {
|
|
170
|
+
onFrame: async (conn, frame) => {
|
|
171
|
+
this.uplink?.feed(frame);
|
|
172
|
+
await this.deps.onFrame(conn, frame);
|
|
173
|
+
},
|
|
174
|
+
addresses: [...this.deps.advertisedAddresses],
|
|
175
|
+
});
|
|
176
|
+
if (this.deps.isShuttingDown()) {
|
|
177
|
+
await session.close();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
this.session = session;
|
|
181
|
+
this.connection = connection;
|
|
182
|
+
this.url = url;
|
|
183
|
+
this.deps.trackForShutdown(session);
|
|
184
|
+
void this.consumeEvents(session, deviceIdToHex(identity.deviceId)).catch(
|
|
185
|
+
(error: unknown) => {
|
|
186
|
+
// The merge loop must not die silently: a failure here would stop this side learning of every remote device until the hub is redialled.
|
|
187
|
+
this.deps.events.onError?.(
|
|
188
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
189
|
+
);
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
this.consume(session);
|
|
193
|
+
this.consumeDataFrames(session);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The single consumer of one hub session's own event stream: merges the hub's directory (its catch-up arrives as the first event, and it is refreshed on every subsequent one) and, once the session ends, drops the held connection so isConnected answers honestly.
|
|
198
|
+
*
|
|
199
|
+
* Single is the operative word. wire-mesh-core delivers each event to exactly one waiting reader, so two concurrent loops over the same stream split it between them, and the closed event reaches whichever happened to be waiting first. If that is the directory loop, the connection stays recorded as live for the rest of the process.
|
|
200
|
+
*
|
|
201
|
+
* Every trusted remote entry is surfaced via onDirectory, so the transport can merge it into its own mesh-wide knownDevices (agent-comms#155's remote-directory-merge leg). Filtered to admitEntries (agent-comms#156, widened by agent-comms#187's principal allowlist per agent-comms#192 and by #266's membership proofs) before either hubPeersKnown tracking or onDirectory sees it: an untrusted device's gossiped presence is not merely withheld from listAgents, it is never even recorded as "known" here, so nothing downstream can act on it via any path this class exposes.
|
|
202
|
+
*/
|
|
203
|
+
private async consumeEvents(
|
|
204
|
+
session: AcceptedMeshSession,
|
|
205
|
+
ownHex: string,
|
|
206
|
+
): Promise<void> {
|
|
207
|
+
try {
|
|
208
|
+
for await (const event of session.events) {
|
|
209
|
+
if (this.deps.isShuttingDown()) break;
|
|
210
|
+
const remoteEntries = await this.deps.admitEntries(
|
|
211
|
+
event.directory.filter(
|
|
212
|
+
(entry) => deviceIdToHex(entry.device) !== ownHex,
|
|
213
|
+
),
|
|
214
|
+
);
|
|
215
|
+
for (const entry of remoteEntries) {
|
|
216
|
+
this.hubPeersKnown.add(deviceIdToHex(entry.device));
|
|
217
|
+
}
|
|
218
|
+
if (remoteEntries.length > 0) this.deps.onDirectory(remoteEntries);
|
|
219
|
+
if (event.state.status === "closed") break;
|
|
220
|
+
}
|
|
221
|
+
} finally {
|
|
222
|
+
if (this.session === session) this.clearSession();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Consumes one hub session's inbound relayed manage-requests until it ends, dispatching each in arrival order to dispatchHubRequest -- see that function's own doc for the actual per-request trust decision. Split out purely so the decision itself is directly unit-testable against fake requests/deps (hub-session-dispatch.test.ts), the same reason hub-forwarding.ts's own routeRoomRequestViaHub is a standalone function rather than a private method. Passes this.url through as dispatchHubRequest's own hubAddress -- this side's own dialled address for the hub every request on this specific session arrived over (agent-comms#216), the answering-side equivalent of tracePath's own local.hubAddress above. */
|
|
227
|
+
private consume(session: AcceptedMeshSession): void {
|
|
228
|
+
void (async () => {
|
|
229
|
+
for await (const request of session.incomingManageRequests) {
|
|
230
|
+
if (this.deps.isShuttingDown()) break;
|
|
231
|
+
await dispatchHubRequest({
|
|
232
|
+
request,
|
|
233
|
+
deps: this.deps,
|
|
234
|
+
onKnownPeer: (hex) => {
|
|
235
|
+
this.hubPeersKnown.add(hex);
|
|
236
|
+
},
|
|
237
|
+
hubAddress: this.url,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
})();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Consumes one hub session's inbound core/data frames until it ends, handing each one that came through a relay pairing to onRelayedDataFrame in arrival order. A failure in the handler is reported and the loop carries on, since one bad frame must not stop every later one. */
|
|
244
|
+
private consumeDataFrames(session: AcceptedMeshSession): void {
|
|
245
|
+
void (async () => {
|
|
246
|
+
for await (const incoming of session.incomingDataFrames) {
|
|
247
|
+
if (this.deps.isShuttingDown()) break;
|
|
248
|
+
if (incoming.fromDevice === undefined) continue;
|
|
249
|
+
await this.deps
|
|
250
|
+
.onRelayedDataFrame(
|
|
251
|
+
deviceIdToHex(incoming.fromDevice),
|
|
252
|
+
incoming.frame,
|
|
253
|
+
)
|
|
254
|
+
.catch((error: unknown) => {
|
|
255
|
+
this.deps.events.onError?.(
|
|
256
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
})();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Sends one core/data frame to a specific hub-reachable device through a relay pairing, sealed end to end with that device. Rejects when no hub session is live. */
|
|
264
|
+
async sendDataFrame(
|
|
265
|
+
peerDeviceHex: string,
|
|
266
|
+
frame: Readonly<DataDomainFrame>,
|
|
267
|
+
): Promise<void> {
|
|
268
|
+
const session = this.session;
|
|
269
|
+
if (session === undefined) {
|
|
270
|
+
throw new Error(
|
|
271
|
+
"HubSession: no live hub session to send a data frame on",
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
await session.sendDataFrame(frame, hexToBytes(peerDeviceHex));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Sends a real core/room manage-request to a specific hub-reachable peer, through the hub's relay-connect/relay-data pairing -- the local-to-remote leg of agent-comms#155's routing, mirroring MeshTransport.sendRoomRequest's own not_connected/outcome contract so WireMeshTransport can fall back to this uniformly when memberId isn't a local peer session. Bounded by HUB_ROOM_REQUEST_TIMEOUT_MS (see its own doc) since an unreachable target-device is silently dropped by the hub with no error frame, unlike a local session's own connection-level failure. */
|
|
278
|
+
async sendRoomRequest(
|
|
279
|
+
peerDeviceHex: string,
|
|
280
|
+
command: ManageCommand,
|
|
281
|
+
scope: Readonly<CapabilityScope>,
|
|
282
|
+
token?: CapabilityToken,
|
|
283
|
+
): Promise<ManageOutcome> {
|
|
284
|
+
const session = this.session;
|
|
285
|
+
if (session === undefined) {
|
|
286
|
+
return { result: "error", code: "not_connected" };
|
|
287
|
+
}
|
|
288
|
+
return session.sendManageRequest(
|
|
289
|
+
command,
|
|
290
|
+
scope,
|
|
291
|
+
hexToBytes(peerDeviceHex),
|
|
292
|
+
token,
|
|
293
|
+
manageRequestTimeoutMs(command, this.deps.roomJoinApprovalTimeoutMs),
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Sends path.trace to a specific hub-reachable peer, through the hub's relay-connect/relay-data pairing -- WireMeshTransport.meshTrace's own hub-relayed fallback (agent-comms#199), mirroring sendRoomRequest's own not_connected contract and HUB_ROOM_REQUEST_TIMEOUT_MS default for the identical reason (an unreachable target-device is silently dropped by the hub, not answered with an error). Delegates the actual send/RTT-measurement/response-parsing to wire-mesh-core's own tracePath, reporting this.url (the address this side itself dialled to reach the hub) as local.hubAddress. */
|
|
298
|
+
async tracePath(
|
|
299
|
+
peerDeviceHex: string,
|
|
300
|
+
timeoutMs?: number,
|
|
301
|
+
): Promise<MeshTraceResult> {
|
|
302
|
+
const session = this.session;
|
|
303
|
+
if (session === undefined) {
|
|
304
|
+
return {
|
|
305
|
+
rttMs: 0,
|
|
306
|
+
local: { relayed: true },
|
|
307
|
+
outcome: { result: "error", code: "not_connected" },
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
return coreTracePath({
|
|
311
|
+
session,
|
|
312
|
+
clock: { now: () => Date.now() },
|
|
313
|
+
targetDevice: hexToBytes(peerDeviceHex),
|
|
314
|
+
timeoutMs: timeoutMs ?? ROOM_REQUEST_TIMEOUT_MS,
|
|
315
|
+
...(this.url !== undefined ? { localHubAddress: this.url } : {}),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function hexToBytes(hex: string): Uint8Array<ArrayBuffer> {
|
|
321
|
+
const bytes = new Uint8Array(new ArrayBuffer(hex.length / 2));
|
|
322
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
323
|
+
bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
324
|
+
}
|
|
325
|
+
return bytes;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** The slice of HubSessionDeps dispatchHubRequest actually needs. Named so its own tests, and consume()'s call site, don't repeat the same Pick inline. */
|
|
329
|
+
export type HubRequestDispatchDeps = Pick<
|
|
330
|
+
HubSessionDeps,
|
|
331
|
+
"isTrusted" | "handleRoomRequest"
|
|
332
|
+
>;
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Decides what to do with one inbound relayed manage-request, keeping every downstream consumer's handle keyed by the SENDING device (request.fromDevice names it on relay-routed requests) rather than by the hub.
|
|
336
|
+
*
|
|
337
|
+
* A request with no fromDevice at all is refused before anything below sees it. A room-domain verb's own downstream handler (a capability-token check such as verifyRoomToken) calls deviceIdFromHex on whatever handle.id it's given, which throws on anything that isn't valid hex rather than answering with an ordinary unauthorized outcome. Answering unauthorized here, before that, is what keeps this a clean, fast failure for the request's own caller instead of an unhandled rejection that would kill this session's whole drain loop.
|
|
338
|
+
*
|
|
339
|
+
* Only room-domain verbs are accepted over a relay at all. The legacy FRAME_VERB carriage (an opaque MeshMessage, which local peer sessions still use for mesh-state gossip) is refused here with unsupported_verb whoever sent it and whether or not that sender is trusted: a relayed sender has passed neither connect_request approval nor the coordinator's own membership, so it has no business feeding state_sync or state_update into this side's mesh state, and nothing on the relay path has any other use for a bare frame. Refusing the verb wholesale is the allow-list; there is no per-message-type list to keep in step with new MeshMessage variants (agent-comms#169, agent-comms#268). The router's own handleRelayedRequest refuses a frame too, so the local branch below cannot deliver one by construction; the check here is what stops a frame being forwarded on to a local peer session, whose own drain loop would otherwise treat it as an ordinary local frame.
|
|
340
|
+
*
|
|
341
|
+
* A real room-domain verb (room.send, room.join, room.notify, ...) is never gated on isTrusted at all (agent-comms#192): it is independently gated by its own room:member capability token, verified regardless of which transport path it arrived over, and already principal-aware since agent-comms#187 (device-membership-verification.ts's own chain-walk). Stacking the coarse bare-device allowlist in front of that check would be redundant, not protective, and for room.join specifically (the one deliberately ungated verb, whose own security model is a human decision) it would actively defeat the protocol's own intended design by never letting an untrusted device reach that approval step at all. onKnownPeer is still only called here for a sender isTrusted already recognises: reaching this branch has not yet had its own token verified (that happens inside handleRoomRequest), so hubPeersKnown stays "gateway-trusted hub peers", never "every device that has sent a syntactically valid room-domain request".
|
|
342
|
+
*/
|
|
343
|
+
export async function dispatchHubRequest(options: {
|
|
344
|
+
request: IncomingManageRequest;
|
|
345
|
+
deps: Readonly<HubRequestDispatchDeps>;
|
|
346
|
+
onKnownPeer: (deviceHex: string) => void;
|
|
347
|
+
hubAddress?: string | undefined;
|
|
348
|
+
}): Promise<void> {
|
|
349
|
+
const { request, deps, onKnownPeer, hubAddress } = options;
|
|
350
|
+
if (request.command.verb === FRAME_VERB) {
|
|
351
|
+
await request
|
|
352
|
+
.respond({ result: "error", code: "unsupported_verb" })
|
|
353
|
+
.catch(() => undefined);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (request.fromDevice === undefined) {
|
|
358
|
+
await request
|
|
359
|
+
.respond({ result: "error", code: "unauthorized" })
|
|
360
|
+
.catch(() => undefined);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const senderHex = deviceIdToHex(request.fromDevice);
|
|
365
|
+
const handle: Readonly<ConnectionHandle> = { id: senderHex };
|
|
366
|
+
|
|
367
|
+
if (deps.isTrusted(senderHex)) onKnownPeer(senderHex);
|
|
368
|
+
|
|
369
|
+
await deps.handleRoomRequest(
|
|
370
|
+
request,
|
|
371
|
+
handle,
|
|
372
|
+
hubAddress !== undefined ? { relayHubAddress: hubAddress } : {},
|
|
373
|
+
);
|
|
374
|
+
}
|