agent-comms 8.15.8 → 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,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CapabilityAskAdmission — the ask tier surfaced at the tool layer (agent-comms#165): the generic, capability-agnostic counterpart to RoomProtocol's own pendingRoomJoins and ConnectionApproval's own pendingInboundConnections, built directly on wire-mesh-core's createCapabilityRequestHandler (the ask primitive agent-comms#164's own bubble-up module already builds on top of, wire-mesh#78). Three tiers exist at the tool layer for whether an action gets a capability token: allow (a held token already answers canGrant), deny (canGrant says no and resolveBubbleUpRoute finds no route to bubble the ask up to either), and ask -- a capability-request held open pending a human decision, which is what this module gives a name and a surface to. A capability-gated verb that lands in that third tier registers a handler built by createAskHandler below instead of failing outright: the request is held open exactly as core/room's own room.join admission holds one open, surfaced to the owning agent as a capability_request delivery event a human can act on (the approval prompt), and the outcome (acceptCapabilityRequest/rejectCapabilityRequest) resolves the held request in place, minting the granted token on acceptance -- the same accept/reject/list shape room_accept/room_reject/room_pending and mesh_accept/mesh_reject/mesh_pending already establish in tool.ts, generalised the way the issue's own framing asks for ("mirror its shape for capability requests generally"). With only one user-principal identity per machine today (agent-comms#160/#161), "the right device or principal" the issue's own framing anticipates routing an ask to is currently always this device -- the same simplification core/room's own single-decision-maker admission already makes; createAskHandler is the integration point a future capability-gated verb (e.g. agent-comms#162's dm:send) registers against its own session to get a ready `(incoming) => Promise<void>` handler backed by this admission's live pending state.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { deviceIdToHex } from "wire-mesh-core/domain/device-id";
|
|
6
|
+
import { createCapabilityRequestHandler } from "wire-mesh-core/domain/capability-request";
|
|
7
|
+
import type {
|
|
8
|
+
CapabilityGrantRequestEvent,
|
|
9
|
+
CapabilityGrantDecision,
|
|
10
|
+
} from "wire-mesh-core/domain/capability-request";
|
|
11
|
+
import type { IncomingManageRequest } from "wire-mesh-core/domain/mesh-session";
|
|
12
|
+
import type {
|
|
13
|
+
CapabilityScope,
|
|
14
|
+
DeviceId,
|
|
15
|
+
} from "wire-mesh-core/generated/protocol";
|
|
16
|
+
import type { Clock } from "wire-mesh-core/ports/clock";
|
|
17
|
+
import type { IdentityPort } from "wire-mesh-core/ports/identity";
|
|
18
|
+
import { nanoid } from "./nanoid.js";
|
|
19
|
+
import { CommsError } from "./store.js";
|
|
20
|
+
import type { DeliveryEngine } from "./delivery-engine.js";
|
|
21
|
+
import type { DeliveryEvent } from "./types.js";
|
|
22
|
+
|
|
23
|
+
/** The state and collaborators CapabilityAskAdmission needs from MeshStore -- getPeerId/getOnDelivery/queueDelivery mirror ConnectionApprovalDeps exactly, since surfacing a capability_request event to the owning agent is the identical "queue it, then push it live if a callback is registered" mechanism connection_request already uses. */
|
|
24
|
+
export interface CapabilityAskAdmissionDeps {
|
|
25
|
+
getPeerId: () => string;
|
|
26
|
+
getOnDelivery: () =>
|
|
27
|
+
| ((agentId: string, event: DeliveryEvent) => void | Promise<void>)
|
|
28
|
+
| undefined;
|
|
29
|
+
queueDelivery: DeliveryEngine["queueDelivery"];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CreateCapabilityAskHandlerOptions {
|
|
33
|
+
/** The capability this handler surfaces asks for -- one handler per capability, the same convention createCapabilityRequestHandler and createBubbleUpCapabilityRequestHandler already establish. */
|
|
34
|
+
capability: string;
|
|
35
|
+
identity: IdentityPort;
|
|
36
|
+
clock: Clock;
|
|
37
|
+
/** The peer device-id authenticated on this session's own connection -- the requester, and so the future bearer of any token accepting the ask mints. */
|
|
38
|
+
bearerDevice: DeviceId;
|
|
39
|
+
/** Receiver-side auto-reject window (wire-mesh#81): how long the ask may sit awaiting a human decision before wire-mesh-core's own createCapabilityRequestHandler responds with a manage-error timeout on this admission's behalf. */
|
|
40
|
+
timeoutMs: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface PendingCapabilityAsk {
|
|
44
|
+
capability: string;
|
|
45
|
+
scope: Readonly<CapabilityScope>;
|
|
46
|
+
requesterDevice: DeviceId;
|
|
47
|
+
decide: (decision: Readonly<CapabilityGrantDecision>) => Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class CapabilityAskAdmission {
|
|
51
|
+
private readonly pending = new Map<string, PendingCapabilityAsk>();
|
|
52
|
+
|
|
53
|
+
constructor(private readonly deps: Readonly<CapabilityAskAdmissionDeps>) {}
|
|
54
|
+
|
|
55
|
+
/** Builds a manage-request handler for one capability's incoming capability-requests that never decides on its own: every not-malformed, not-yet-expired ask is held open and surfaced as a capability_request delivery event, exactly like handleConnectionRequest already does for an inbound mesh connection. */
|
|
56
|
+
createAskHandler(
|
|
57
|
+
options: Readonly<CreateCapabilityAskHandlerOptions>,
|
|
58
|
+
): (incoming: Readonly<IncomingManageRequest>) => Promise<void> {
|
|
59
|
+
return createCapabilityRequestHandler({
|
|
60
|
+
capability: options.capability,
|
|
61
|
+
identity: options.identity,
|
|
62
|
+
clock: options.clock,
|
|
63
|
+
bearerDevice: options.bearerDevice,
|
|
64
|
+
timeoutMs: options.timeoutMs,
|
|
65
|
+
onRequest: (event: Readonly<CapabilityGrantRequestEvent>) => {
|
|
66
|
+
this.handleCapabilityRequest(options.capability, event);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private handleCapabilityRequest(
|
|
72
|
+
capability: string,
|
|
73
|
+
event: Readonly<CapabilityGrantRequestEvent>,
|
|
74
|
+
): void {
|
|
75
|
+
const requestId = nanoid();
|
|
76
|
+
this.pending.set(requestId, {
|
|
77
|
+
capability,
|
|
78
|
+
scope: event.scope,
|
|
79
|
+
requesterDevice: event.requesterDevice,
|
|
80
|
+
decide: event.decide,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const deliveryEvent: DeliveryEvent = {
|
|
84
|
+
type: "capability_request",
|
|
85
|
+
requestId,
|
|
86
|
+
capability,
|
|
87
|
+
scopeKind: event.scope.kind,
|
|
88
|
+
...(event.scope.path !== undefined
|
|
89
|
+
? { scopePath: event.scope.path }
|
|
90
|
+
: {}),
|
|
91
|
+
requesterDevice: deviceIdToHex(event.requesterDevice),
|
|
92
|
+
};
|
|
93
|
+
const peerId = this.deps.getPeerId();
|
|
94
|
+
this.deps.queueDelivery(peerId, deliveryEvent);
|
|
95
|
+
const onDelivery = this.deps.getOnDelivery();
|
|
96
|
+
if (onDelivery) {
|
|
97
|
+
void onDelivery(peerId, deliveryEvent);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Every capability-request currently held open awaiting a human decision. */
|
|
102
|
+
listPendingCapabilityRequests(): {
|
|
103
|
+
requestId: string;
|
|
104
|
+
capability: string;
|
|
105
|
+
scopeKind: string;
|
|
106
|
+
scopePath?: string;
|
|
107
|
+
requesterDevice: string;
|
|
108
|
+
}[] {
|
|
109
|
+
return [...this.pending.entries()].map(([requestId, ask]) => ({
|
|
110
|
+
requestId,
|
|
111
|
+
capability: ask.capability,
|
|
112
|
+
scopeKind: ask.scope.kind,
|
|
113
|
+
...(ask.scope.path !== undefined ? { scopePath: ask.scope.path } : {}),
|
|
114
|
+
requesterDevice: deviceIdToHex(ask.requesterDevice),
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Approves a pending capability-request, resolving its held decide() with an accept -- wire-mesh-core's own createCapabilityRequestHandler mints the granted token and responds on this admission's behalf. `capability`, when given, grants something narrower than what was originally asked for (the primitive's own CapabilityGrantDecision allows this); absent, the request's own originally-asked-for capability is granted unchanged. */
|
|
119
|
+
async acceptCapabilityRequest(
|
|
120
|
+
requestId: string,
|
|
121
|
+
options: Readonly<{
|
|
122
|
+
expires: number;
|
|
123
|
+
delegationsRemaining?: number;
|
|
124
|
+
capability?: string;
|
|
125
|
+
}>,
|
|
126
|
+
): Promise<void> {
|
|
127
|
+
const pending = this.requirePending(requestId);
|
|
128
|
+
this.pending.delete(requestId);
|
|
129
|
+
await pending.decide({
|
|
130
|
+
kind: "accept",
|
|
131
|
+
capability: options.capability ?? pending.capability,
|
|
132
|
+
expires: options.expires,
|
|
133
|
+
...(options.delegationsRemaining !== undefined
|
|
134
|
+
? { delegationsRemaining: options.delegationsRemaining }
|
|
135
|
+
: {}),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Denies a pending capability-request, optionally with a reason surfaced to the requester in the resulting manage-error's own message field. */
|
|
140
|
+
async rejectCapabilityRequest(
|
|
141
|
+
requestId: string,
|
|
142
|
+
reason?: string,
|
|
143
|
+
): Promise<void> {
|
|
144
|
+
const pending = this.requirePending(requestId);
|
|
145
|
+
this.pending.delete(requestId);
|
|
146
|
+
await pending.decide({
|
|
147
|
+
kind: "reject",
|
|
148
|
+
...(reason !== undefined ? { reason } : {}),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private requirePending(requestId: string): PendingCapabilityAsk {
|
|
153
|
+
const pending = this.pending.get(requestId);
|
|
154
|
+
if (pending === undefined) {
|
|
155
|
+
throw new CommsError(
|
|
156
|
+
`No pending capability request ${requestId}`,
|
|
157
|
+
"NOT_PENDING",
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return pending;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The routing policy for the ask tier's escalation path (agent-comms#164): walking a held capability token's own issuer chain when the immediate holder cannot grant a request itself. wire-mesh's capability-request primitive (domain/capability-request.ts) already carries the ask/held-open/decide/mint mechanics of a single hop; gossip-advertised grant candidates (domain/grant-candidates.ts) already answer "who nearer than the root can grant" for a requester picking its first hop. Neither decides what an intermediate device, having received a request it cannot fully satisfy from its own authority, should do next: that per-hop decision, and the handler that acts on it by minting a delegated child or forwarding the same ask further up the chain, is what this module adds.
|
|
3
|
+
*
|
|
4
|
+
* Two responsibilities, split the same way capability-request.ts already splits its own: resolveBubbleUpRoute is the pure decision (grant from held depth, forward to the held token's own issuer, or no-route), and createBubbleUpCapabilityRequestHandler is the wire-facing orchestration that acts on it. Deliberately built directly on wire-mesh's exported mintCapabilityToken rather than reusing createCapabilityRequestHandler's own decide(): that primitive's accept path always mints a root token (no `parent`), which is correct for the primitive's own reference consumer (a room owner minting membership tokens directly) but cannot express "mint a narrower child of the token I already hold", the one thing granting from delegated depth actually requires.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { buildCapabilityRequestCommand } from "wire-mesh-core/domain/capability-request";
|
|
8
|
+
import { deviceIdToHex } from "wire-mesh-core/domain/device-id";
|
|
9
|
+
import type {
|
|
10
|
+
MeshSession,
|
|
11
|
+
IncomingManageRequest,
|
|
12
|
+
} from "wire-mesh-core/domain/mesh-session";
|
|
13
|
+
import {
|
|
14
|
+
canGrant,
|
|
15
|
+
mintCapabilityToken,
|
|
16
|
+
verifyCapabilityToken,
|
|
17
|
+
type RevocationCheck,
|
|
18
|
+
} from "wire-mesh-core/domain/tokens";
|
|
19
|
+
import {
|
|
20
|
+
capabilityGrantOkSchema,
|
|
21
|
+
capabilityRequestSchema,
|
|
22
|
+
type CapabilityScope,
|
|
23
|
+
type CapabilityToken,
|
|
24
|
+
type DeviceId,
|
|
25
|
+
} from "wire-mesh-core/generated/protocol";
|
|
26
|
+
import type { Clock } from "wire-mesh-core/ports/clock";
|
|
27
|
+
import type { IdentityPort } from "wire-mesh-core/ports/identity";
|
|
28
|
+
import { randomId } from "./random-id.js";
|
|
29
|
+
|
|
30
|
+
export interface ResolveBubbleUpRouteOptions {
|
|
31
|
+
/** This device's own held credential establishing its authority over `capability`/`scope`, if it holds one at all. A device with none is either the capability's own structural root (which mints fresh from its own identity, outside this module's concern -- see wire-mesh's own createCapabilityRequestHandler for that path) or genuinely has nothing to grant or forward from. */
|
|
32
|
+
heldToken: CapabilityToken | undefined;
|
|
33
|
+
/** This device's own identity: both the crypto primitives heldToken is verified with, and (via `.deviceId`) the bearer heldToken must actually name. */
|
|
34
|
+
identity: IdentityPort;
|
|
35
|
+
clock: Clock;
|
|
36
|
+
revocation: RevocationCheck;
|
|
37
|
+
/** The capability and scope the incoming ask is for -- forwarded unchanged from the request this route decides for. */
|
|
38
|
+
capability: string;
|
|
39
|
+
scope: Readonly<CapabilityScope>;
|
|
40
|
+
/** The expiry and delegation depth a grant would carry if minted at this hop, checked against heldToken's own narrowing bounds via canGrant. The caller (see createBubbleUpCapabilityRequestHandler) owns this policy; this module only evaluates it. */
|
|
41
|
+
expires: number;
|
|
42
|
+
delegationsRemaining?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type BubbleUpRoute =
|
|
46
|
+
| { kind: "grant"; token: CapabilityToken }
|
|
47
|
+
| { kind: "forward"; target: DeviceId }
|
|
48
|
+
| {
|
|
49
|
+
kind: "no-route";
|
|
50
|
+
reason: "no-held-token" | "held-token-invalid" | "self-issued";
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Decides, for one device holding (or not holding) a capability token, whether it can grant a request for `capability`/`scope`/`expires`/`delegationsRemaining` from its own held depth, must forward the same ask to whoever issued its own token, or has no route to a grantor at all.
|
|
55
|
+
*
|
|
56
|
+
* heldToken is independently verified here (signature, expiry, revocation, and that this device is genuinely its bearer) via verifyCapabilityToken rather than trusted as already-checked: a caller's own held-token cache can go stale (expiry, a since-recorded revocation) between when it was stored and when this decision runs, and granting or forwarding on an invalid credential would be a real authority leak, not merely a stale-read symptom.
|
|
57
|
+
*
|
|
58
|
+
* Ordering matches the issue's own framing (agent-comms#164): a device with real remaining depth grants outright (checked via canGrant, the same narrowing arithmetic mintCapabilityToken itself enforces, so this decision and the mint it authorises can never disagree); otherwise, absent a nearer route this module has no way to discover on its own (see grant-candidates.ts for the requester's own gossip-based shortcut, deliberately a separate concern), the only further hop toward the root a token's own claims expose is its issuer. Forwarding to that issuer when it is this very device (a self-issued token, which canGrant already found could not satisfy the request) would loop forever, so that case reports no-route instead.
|
|
59
|
+
*/
|
|
60
|
+
export async function resolveBubbleUpRoute(
|
|
61
|
+
options: Readonly<ResolveBubbleUpRouteOptions>,
|
|
62
|
+
): Promise<BubbleUpRoute> {
|
|
63
|
+
if (options.heldToken === undefined) {
|
|
64
|
+
return { kind: "no-route", reason: "no-held-token" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const verdict = await verifyCapabilityToken(options.heldToken, {
|
|
68
|
+
identity: options.identity,
|
|
69
|
+
clock: options.clock,
|
|
70
|
+
revocation: options.revocation,
|
|
71
|
+
expectedBearer: options.identity.deviceId,
|
|
72
|
+
});
|
|
73
|
+
if (!verdict.ok) {
|
|
74
|
+
return { kind: "no-route", reason: "held-token-invalid" };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const canGrantDirectly = await canGrant(
|
|
78
|
+
options.heldToken,
|
|
79
|
+
options.identity.deviceId,
|
|
80
|
+
{
|
|
81
|
+
capability: options.capability,
|
|
82
|
+
scope: options.scope,
|
|
83
|
+
expires: options.expires,
|
|
84
|
+
...(options.delegationsRemaining !== undefined
|
|
85
|
+
? { delegationsRemaining: options.delegationsRemaining }
|
|
86
|
+
: {}),
|
|
87
|
+
},
|
|
88
|
+
options.clock.now(),
|
|
89
|
+
);
|
|
90
|
+
if (canGrantDirectly) {
|
|
91
|
+
return { kind: "grant", token: options.heldToken };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (
|
|
95
|
+
deviceIdToHex(verdict.claims.issuer) ===
|
|
96
|
+
deviceIdToHex(options.identity.deviceId)
|
|
97
|
+
) {
|
|
98
|
+
return { kind: "no-route", reason: "self-issued" };
|
|
99
|
+
}
|
|
100
|
+
return { kind: "forward", target: verdict.claims.issuer };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface CreateBubbleUpCapabilityRequestHandlerOptions {
|
|
104
|
+
/** The capability this handler grants or forwards asks for, checked against the incoming request's own `params.capability`. One handler is constructed per capability, the same convention capability-request.ts's own createCapabilityRequestHandler already establishes. */
|
|
105
|
+
capability: string;
|
|
106
|
+
identity: IdentityPort;
|
|
107
|
+
clock: Clock;
|
|
108
|
+
revocation: RevocationCheck;
|
|
109
|
+
/** The peer device-id authenticated on this session's own connection -- the requester, and so the bearer of whichever token this handler ultimately mints (whether granted from own depth or re-delegated from an upstream grant). */
|
|
110
|
+
bearerDevice: DeviceId;
|
|
111
|
+
/** The expiry and delegation depth this handler mints with, at either hop (its own grant, or its re-delegation of an upstream one). A fixed per-handler policy, not per-request: the wire-level capability-request carries no requested expiry or depth of its own (management.cddl leaves both to the granter), matching the same policy CapabilityGrantDecision.expires/delegationsRemaining already leaves to the domain in wire-mesh's base primitive. */
|
|
112
|
+
expires: number;
|
|
113
|
+
delegationsRemaining?: number;
|
|
114
|
+
/** Looks up this device's own currently held credential for `capability`, if any, at the moment each request is handled -- a function rather than a static value since a held token can be minted, renewed, or revoked over the handler's own lifetime. */
|
|
115
|
+
ownToken: () => CapabilityToken | undefined;
|
|
116
|
+
/** Used to forward an ask this device cannot grant itself further up the issuer chain. Only sendManageRequest is used, so a caller can supply a minimal stand-in in tests rather than a full MeshSession. */
|
|
117
|
+
session: Pick<MeshSession, "sendManageRequest">;
|
|
118
|
+
/** Bounds how long a forwarded ask waits for the next hop's own answer, forwarded directly to sendManageRequest's own identically-named parameter. Absent, a forward waits with no time limit of its own, exactly as sendManageRequest already behaves. */
|
|
119
|
+
forwardTimeoutMs?: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Builds a reusable handler for one capability's incoming capability-requests that answers each one by walking the issuer chain: grant a delegated child from this device's own held depth when resolveBubbleUpRoute says it can, forward the identical ask to the held token's own issuer and re-delegate whatever grant comes back when it cannot, or refuse outright when there is no route to a grantor at all.
|
|
124
|
+
*
|
|
125
|
+
* Parses and validates the incoming request the same way capability-request.ts's own createCapabilityRequestHandler does (wrong capability or malformed payload refuses `{result:"error",code:"malformed"}`; an already-expired `valid-until` refuses `{result:"error",code:"expired"}`), so a requester using the ordinary requestCapability wrapper sees identical wire behaviour from either handler.
|
|
126
|
+
*
|
|
127
|
+
* A forward's own upstream outcome is relayed verbatim on refusal (the exact code and message an intermediate or root grantor sent), rather than collapsed to a generic denial, so the original requester learns exactly what stopped the chain. A forward's own granted token is independently re-verified (expectedBearer this device's own identity) before being trusted as a mint parent: mintCapabilityToken's own `parent` handling only checks structural narrowing against the decoded claims, not the token's signature, expiry, or revocation status, so an unverified inbound grant would otherwise let a malicious or buggy upstream peer hand this device a bogus credential to unknowingly re-delegate.
|
|
128
|
+
*/
|
|
129
|
+
export function createBubbleUpCapabilityRequestHandler(
|
|
130
|
+
options: Readonly<CreateBubbleUpCapabilityRequestHandlerOptions>,
|
|
131
|
+
): (incoming: Readonly<IncomingManageRequest>) => Promise<void> {
|
|
132
|
+
return async function handleBubbleUpCapabilityRequest(
|
|
133
|
+
incoming: Readonly<IncomingManageRequest>,
|
|
134
|
+
): Promise<void> {
|
|
135
|
+
const parsed = capabilityRequestSchema.safeParse(incoming.command.params);
|
|
136
|
+
if (!parsed.success || parsed.data.capability !== options.capability) {
|
|
137
|
+
await incoming.respond({ result: "error", code: "malformed" });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const validUntil = parsed.data["valid-until"];
|
|
141
|
+
if (validUntil !== undefined && validUntil <= options.clock.now()) {
|
|
142
|
+
await incoming.respond({ result: "error", code: "expired" });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const route = await resolveBubbleUpRoute({
|
|
147
|
+
heldToken: options.ownToken(),
|
|
148
|
+
identity: options.identity,
|
|
149
|
+
clock: options.clock,
|
|
150
|
+
revocation: options.revocation,
|
|
151
|
+
capability: options.capability,
|
|
152
|
+
scope: incoming.scope,
|
|
153
|
+
expires: options.expires,
|
|
154
|
+
...(options.delegationsRemaining !== undefined
|
|
155
|
+
? { delegationsRemaining: options.delegationsRemaining }
|
|
156
|
+
: {}),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (route.kind === "grant") {
|
|
160
|
+
await mintAndRespond(options, incoming, route.token);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (route.kind === "forward") {
|
|
165
|
+
const forwardOutcome = await options.session.sendManageRequest(
|
|
166
|
+
buildCapabilityRequestCommand(options.capability, validUntil),
|
|
167
|
+
incoming.scope,
|
|
168
|
+
route.target,
|
|
169
|
+
undefined,
|
|
170
|
+
options.forwardTimeoutMs,
|
|
171
|
+
);
|
|
172
|
+
if (forwardOutcome.result !== "ok") {
|
|
173
|
+
await incoming.respond(forwardOutcome);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const parsedGrant = capabilityGrantOkSchema.safeParse(forwardOutcome);
|
|
177
|
+
if (!parsedGrant.success) {
|
|
178
|
+
await incoming.respond({ result: "error", code: "malformed" });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const upstreamVerdict = await verifyCapabilityToken(
|
|
182
|
+
parsedGrant.data["granted-token"],
|
|
183
|
+
{
|
|
184
|
+
identity: options.identity,
|
|
185
|
+
clock: options.clock,
|
|
186
|
+
revocation: options.revocation,
|
|
187
|
+
expectedBearer: options.identity.deviceId,
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
if (!upstreamVerdict.ok) {
|
|
191
|
+
await incoming.respond({
|
|
192
|
+
result: "error",
|
|
193
|
+
code: upstreamVerdict.reason,
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
await mintAndRespond(
|
|
198
|
+
options,
|
|
199
|
+
incoming,
|
|
200
|
+
parsedGrant.data["granted-token"],
|
|
201
|
+
);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
await incoming.respond({
|
|
206
|
+
result: "error",
|
|
207
|
+
code: "denied",
|
|
208
|
+
message: `no route to a grantor for "${options.capability}"`,
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Mints a delegated child of `parent` (this device's own held token, or a freshly re-verified upstream grant) for the requester, and responds with it -- shared by both createBubbleUpCapabilityRequestHandler branches that end in a mint, so the two can never drift into two different ideas of what the resulting token should carry. */
|
|
214
|
+
async function mintAndRespond(
|
|
215
|
+
options: Readonly<CreateBubbleUpCapabilityRequestHandlerOptions>,
|
|
216
|
+
incoming: Readonly<IncomingManageRequest>,
|
|
217
|
+
parent: CapabilityToken,
|
|
218
|
+
): Promise<void> {
|
|
219
|
+
const verdict = await mintCapabilityToken({
|
|
220
|
+
identity: options.identity,
|
|
221
|
+
clock: options.clock,
|
|
222
|
+
tokenId: randomId(),
|
|
223
|
+
bearer: options.bearerDevice,
|
|
224
|
+
capability: options.capability,
|
|
225
|
+
scope: incoming.scope,
|
|
226
|
+
expires: options.expires,
|
|
227
|
+
...(options.delegationsRemaining !== undefined
|
|
228
|
+
? { delegationsRemaining: options.delegationsRemaining }
|
|
229
|
+
: {}),
|
|
230
|
+
parent,
|
|
231
|
+
});
|
|
232
|
+
if (!verdict.ok) {
|
|
233
|
+
await incoming.respond({ result: "error", code: "mint_failed" });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
await incoming.respond({ result: "ok", "granted-token": verdict.token });
|
|
237
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CommsStore — abstract interface for the agent communication store.
|
|
3
|
+
*
|
|
4
|
+
* Two implementations: FileStore — filesystem-backed, no server process (fallback) MeshStore — TCP peer mesh, in-memory, real-time push (preferred)
|
|
5
|
+
*
|
|
6
|
+
* Bridges depend on this interface, not on a specific implementation.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately excludes listener management and connection approval: those are transport concerns MeshStore alone can support -- FileStore has no network transport to manage listeners on or approve inbound connections for. Widening this interface to cover them (as it once did, via always-throwing FileStore stubs) is what forced server.ts and the bridge controller to reach past CommsStore into the concrete MeshStore anyway; CommsTool, the one consumer that genuinely needs to expose these when a MeshStore backs it, takes them as an optional extension (see MeshOnlyFeatures in tool.ts) rather than the shared interface pretending every implementation supports them.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
AgentIdentity,
|
|
13
|
+
DeliveryEvent,
|
|
14
|
+
DmMessage,
|
|
15
|
+
MessageDelivery,
|
|
16
|
+
Room,
|
|
17
|
+
RoomMessage,
|
|
18
|
+
RoomType,
|
|
19
|
+
StreamingBehavior,
|
|
20
|
+
Visibility,
|
|
21
|
+
} from "./types.js";
|
|
22
|
+
|
|
23
|
+
export interface SendRoomMessageOptions {
|
|
24
|
+
replyTo?: string | undefined;
|
|
25
|
+
streamingBehavior?: StreamingBehavior | undefined;
|
|
26
|
+
/** When true, `MeshStore` also records the message as a signed room-notice in the sender's own log for offline catch-up. `FileStore` ignores it, since it already persists every message. */
|
|
27
|
+
durable?: boolean | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** What became of a room message for one specific member of that room. Delivery is per member because a room send fans out one directed request each, and a member that is unreachable right now is independent of every other. */
|
|
31
|
+
export interface MemberDelivery {
|
|
32
|
+
member: string;
|
|
33
|
+
delivery: MessageDelivery;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A room message as sent: the message itself, plus what became of it for each member other than its sender. A caller that only wants the message reads `message`; one that needs to know whether anybody actually received it reads `deliveries`. */
|
|
37
|
+
export interface SentRoomMessage {
|
|
38
|
+
message: RoomMessage;
|
|
39
|
+
deliveries: MemberDelivery[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A DM as sent: the message itself, plus what became of it. Exactly one delivery state, since a DM has exactly one recipient. */
|
|
43
|
+
export interface SentDm {
|
|
44
|
+
message: DmMessage;
|
|
45
|
+
delivery: MessageDelivery;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface CommsStore {
|
|
49
|
+
// -- Identity --
|
|
50
|
+
readIdentity: (
|
|
51
|
+
harness: string,
|
|
52
|
+
cwd: string,
|
|
53
|
+
) => Promise<{ id: string } | undefined>;
|
|
54
|
+
writeIdentity: (harness: string, cwd: string, id: string) => Promise<void>;
|
|
55
|
+
|
|
56
|
+
// -- Agent registry --
|
|
57
|
+
registerAgent: (
|
|
58
|
+
opts: Readonly<{
|
|
59
|
+
name: string;
|
|
60
|
+
harness: string;
|
|
61
|
+
cwd: string;
|
|
62
|
+
pid: number;
|
|
63
|
+
visibility: Visibility;
|
|
64
|
+
tags: string[];
|
|
65
|
+
}>,
|
|
66
|
+
) => Promise<AgentIdentity>;
|
|
67
|
+
getAgent: (id: string) => Promise<AgentIdentity | undefined>;
|
|
68
|
+
updateAgent: (
|
|
69
|
+
id: string,
|
|
70
|
+
patch: Partial<
|
|
71
|
+
Pick<AgentIdentity, "name" | "visibility" | "status" | "tags" | "pid">
|
|
72
|
+
>,
|
|
73
|
+
) => Promise<AgentIdentity>;
|
|
74
|
+
listAgents: (requesterId: string) => Promise<AgentIdentity[]>;
|
|
75
|
+
setAgentOffline: (id: string) => Promise<void>;
|
|
76
|
+
|
|
77
|
+
// -- Rooms --
|
|
78
|
+
createRoom: (
|
|
79
|
+
opts: Readonly<{
|
|
80
|
+
name: string;
|
|
81
|
+
type: RoomType;
|
|
82
|
+
owner: string;
|
|
83
|
+
description: string;
|
|
84
|
+
}>,
|
|
85
|
+
) => Promise<Room>;
|
|
86
|
+
getRoom: (id: string) => Promise<Room | undefined>;
|
|
87
|
+
listRooms: (requesterId: string) => Promise<Room[]>;
|
|
88
|
+
joinRoom: (roomId: string, agentId: string) => Promise<Room>;
|
|
89
|
+
leaveRoom: (roomId: string, agentId: string) => Promise<void>;
|
|
90
|
+
inviteToRoom: (
|
|
91
|
+
roomId: string,
|
|
92
|
+
targetId: string,
|
|
93
|
+
inviterId: string,
|
|
94
|
+
) => Promise<void>;
|
|
95
|
+
declineInvite: (
|
|
96
|
+
roomId: string,
|
|
97
|
+
agentId: string,
|
|
98
|
+
reason: string,
|
|
99
|
+
) => Promise<void>;
|
|
100
|
+
kickFromRoom: (
|
|
101
|
+
roomId: string,
|
|
102
|
+
targetId: string,
|
|
103
|
+
kickerId: string,
|
|
104
|
+
) => Promise<void>;
|
|
105
|
+
destroyRoom: (roomId: string, agentId: string) => Promise<void>;
|
|
106
|
+
|
|
107
|
+
// -- Messages --
|
|
108
|
+
sendRoomMessage: (
|
|
109
|
+
roomId: string,
|
|
110
|
+
from: string,
|
|
111
|
+
content: string,
|
|
112
|
+
options?: SendRoomMessageOptions,
|
|
113
|
+
) => Promise<SentRoomMessage>;
|
|
114
|
+
readRoomMessages: (roomId: string, since?: string) => Promise<RoomMessage[]>;
|
|
115
|
+
|
|
116
|
+
// -- DMs --
|
|
117
|
+
sendDm: (
|
|
118
|
+
from: string,
|
|
119
|
+
to: string,
|
|
120
|
+
content: string,
|
|
121
|
+
streamingBehavior?: StreamingBehavior,
|
|
122
|
+
) => Promise<SentDm>;
|
|
123
|
+
|
|
124
|
+
// -- Delivery --
|
|
125
|
+
deliver: (agentId: string, event: DeliveryEvent) => Promise<void>;
|
|
126
|
+
drainDelivery: (agentId: string) => Promise<DeliveryEvent[]>;
|
|
127
|
+
|
|
128
|
+
// -- Lifecycle --
|
|
129
|
+
init: () => Promise<void>;
|
|
130
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConnectionApproval — inbound mesh-connection admission: queues a connection_request for the owning agent to accept/reject, and the accept/reject/list/connectToRemote/startDataServerOnly operations around it. Split out of mesh-store.ts to reduce it under the repo's max-lines cap. Owns pendingInboundConnections exclusively -- nothing outside this class ever reads or writes it.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { DeliveryEngine } from "./delivery-engine.js";
|
|
6
|
+
import type { ConnectionHandle, MeshTransport } from "./transport.js";
|
|
7
|
+
import type { AgentIdentity, DeliveryEvent } from "./types.js";
|
|
8
|
+
import type { PeerInfo } from "./wire-protocol.js";
|
|
9
|
+
|
|
10
|
+
/** The state and collaborators ConnectionApproval needs from MeshStore. agents/peerInfo are direct references into MeshStore's own fields; startedAt is a readonly value copied once (MeshStore never reassigns it); the closures read MeshStore's own current peerId/transport/onDelivery, and queueDelivery is DeliveryEngine's own already-constructed method. */
|
|
11
|
+
export interface ConnectionApprovalDeps {
|
|
12
|
+
agents: Map<string, AgentIdentity>;
|
|
13
|
+
peerInfo: Map<string, PeerInfo>;
|
|
14
|
+
startedAt: string;
|
|
15
|
+
getPeerId: () => string;
|
|
16
|
+
requireTransport: () => MeshTransport;
|
|
17
|
+
getOnDelivery: () =>
|
|
18
|
+
| ((agentId: string, event: DeliveryEvent) => void | Promise<void>)
|
|
19
|
+
| undefined;
|
|
20
|
+
queueDelivery: DeliveryEngine["queueDelivery"];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class ConnectionApproval {
|
|
24
|
+
private readonly pendingInboundConnections = new Map<
|
|
25
|
+
string,
|
|
26
|
+
{ peerId: string; dataPort: number; name: string; fingerprint: string }
|
|
27
|
+
>();
|
|
28
|
+
|
|
29
|
+
constructor(private readonly deps: ConnectionApprovalDeps) {}
|
|
30
|
+
|
|
31
|
+
handleConnectionRequest(
|
|
32
|
+
handle: Readonly<ConnectionHandle>,
|
|
33
|
+
request: Readonly<{
|
|
34
|
+
peerId: string;
|
|
35
|
+
dataPort: number;
|
|
36
|
+
name: string;
|
|
37
|
+
fingerprint: string;
|
|
38
|
+
}>,
|
|
39
|
+
): void {
|
|
40
|
+
this.pendingInboundConnections.set(handle.id, {
|
|
41
|
+
peerId: request.peerId,
|
|
42
|
+
dataPort: request.dataPort,
|
|
43
|
+
name: request.name,
|
|
44
|
+
fingerprint: request.fingerprint,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Deliver connection_request event to the owning agent
|
|
48
|
+
const connectionId = handle.id;
|
|
49
|
+
const event: DeliveryEvent = {
|
|
50
|
+
type: "connection_request",
|
|
51
|
+
connectionId,
|
|
52
|
+
peerId: request.peerId,
|
|
53
|
+
dataPort: request.dataPort,
|
|
54
|
+
name: request.name,
|
|
55
|
+
fingerprint: request.fingerprint,
|
|
56
|
+
};
|
|
57
|
+
const peerId = this.deps.getPeerId();
|
|
58
|
+
this.deps.queueDelivery(peerId, event);
|
|
59
|
+
const onDelivery = this.deps.getOnDelivery();
|
|
60
|
+
if (onDelivery) {
|
|
61
|
+
void onDelivery(peerId, event);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Accept a pending inbound connection. */
|
|
66
|
+
async acceptConnection(connectionId: string): Promise<void> {
|
|
67
|
+
const pending = this.pendingInboundConnections.get(connectionId);
|
|
68
|
+
if (!pending) {
|
|
69
|
+
throw new Error(`No pending connection ${connectionId}`);
|
|
70
|
+
}
|
|
71
|
+
this.pendingInboundConnections.delete(connectionId);
|
|
72
|
+
const handle: ConnectionHandle = { id: connectionId };
|
|
73
|
+
await this.deps.requireTransport().acceptConnection(handle);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Reject a pending inbound connection. */
|
|
77
|
+
async rejectConnection(connectionId: string, reason: string): Promise<void> {
|
|
78
|
+
const pending = this.pendingInboundConnections.get(connectionId);
|
|
79
|
+
if (!pending) {
|
|
80
|
+
throw new Error(`No pending connection ${connectionId}`);
|
|
81
|
+
}
|
|
82
|
+
this.pendingInboundConnections.delete(connectionId);
|
|
83
|
+
const handle: ConnectionHandle = { id: connectionId };
|
|
84
|
+
await this.deps.requireTransport().rejectConnection(handle, reason);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** List all pending inbound connections awaiting approval. */
|
|
88
|
+
listPendingConnections(): {
|
|
89
|
+
connectionId: string;
|
|
90
|
+
peerId: string;
|
|
91
|
+
dataPort: number;
|
|
92
|
+
name: string;
|
|
93
|
+
fingerprint: string;
|
|
94
|
+
}[] {
|
|
95
|
+
return [...this.pendingInboundConnections.entries()].map(([id, info]) => ({
|
|
96
|
+
connectionId: id,
|
|
97
|
+
...info,
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Initiate an outbound connection to a remote coordinator requiring approval. Fires the connect_request and returns immediately. The connection completes asynchronously when the coordinator accepts or rejects. `host` is either a hostname (TLS-dialled with `port`) or a ws:// / wss:// URL (WebSocket-dialled to a hub such as mesh.exadev.io; the port is ignored). */
|
|
102
|
+
async connectToRemote(host: string, port: number): Promise<void> {
|
|
103
|
+
const peerId = this.deps.getPeerId();
|
|
104
|
+
const agent = this.deps.agents.get(peerId);
|
|
105
|
+
// Fire-and-forget: don't await the full approval handshake. The coordinator will either accept (triggering normal introduction flow) or reject (closing the socket). Handle rejection to avoid unhandled rejection.
|
|
106
|
+
this.deps
|
|
107
|
+
.requireTransport()
|
|
108
|
+
.connectToRemote({
|
|
109
|
+
host,
|
|
110
|
+
port,
|
|
111
|
+
peerId,
|
|
112
|
+
dataPort: this.deps.requireTransport().dataPort,
|
|
113
|
+
name: agent?.name ?? "",
|
|
114
|
+
fingerprint: "",
|
|
115
|
+
})
|
|
116
|
+
.catch(() => {
|
|
117
|
+
// Rejection is expected when the coordinator denies the connection. Log silently — the calling tool already returned success.
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return Promise.resolve();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Start only the data server without connecting to a coordinator. Used for testing scenarios where the peer connects via connectToRemote. */
|
|
124
|
+
async startDataServerOnly(): Promise<void> {
|
|
125
|
+
await this.deps.requireTransport().startDataServer();
|
|
126
|
+
const peerId = this.deps.getPeerId();
|
|
127
|
+
this.deps.peerInfo.set(peerId, {
|
|
128
|
+
id: peerId,
|
|
129
|
+
port: this.deps.requireTransport().dataPort,
|
|
130
|
+
startedAt: this.deps.startedAt,
|
|
131
|
+
});
|
|
132
|
+
this.deps.requireTransport().unref();
|
|
133
|
+
}
|
|
134
|
+
}
|