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,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WireMeshTransport's own core/data frame dispatch (data-have, data-request, data-entries), split out under the repo's max-lines cap the same way gossip-directory.ts and hub-forwarding.ts were each split from the same owning file.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
handleDataEntries,
|
|
7
|
+
handleDataHave,
|
|
8
|
+
handleDataRequest,
|
|
9
|
+
} from "wire-mesh-core/domain/data-sync";
|
|
10
|
+
import { deviceIdToHex } from "wire-mesh-core/domain/device-id";
|
|
11
|
+
import { isDataFrame } from "wire-mesh-core/domain/hub-mailbox";
|
|
12
|
+
import type { DataDomainFrame } from "wire-mesh-core/domain/mesh-session";
|
|
13
|
+
import type {
|
|
14
|
+
DataHaveFrame,
|
|
15
|
+
DataRequestFrame,
|
|
16
|
+
Frame,
|
|
17
|
+
} from "wire-mesh-core/generated/protocol";
|
|
18
|
+
import type { Connection } from "wire-mesh-core/ports/transport";
|
|
19
|
+
import type { KeyValueStorage } from "wire-mesh-core/ports/storage";
|
|
20
|
+
import type { AccountLedgerReplica } from "./account-ledger.js";
|
|
21
|
+
import { readMembershipProof } from "./gossip-directory.js";
|
|
22
|
+
import type { WireMeshTransportOptions } from "./wire-mesh-transport-options.js";
|
|
23
|
+
|
|
24
|
+
/** The most entries one data-entries reply carries, for every log this side answers for, the account ledger's included: a longer log catches up over several rounds, so the bound only caps one frame's size. */
|
|
25
|
+
export const DATA_ENTRIES_RESPONSE_LIMIT = 100;
|
|
26
|
+
|
|
27
|
+
/** What the router needs to replicate the account's grant ledger (agent-comms#344). */
|
|
28
|
+
export interface AccountReplication {
|
|
29
|
+
/** The ledger, read on every frame and every announcement round because it opens after the transport is built and is replaced when the machine joins another account. Undefined until it has opened. */
|
|
30
|
+
ledger: () => AccountLedgerReplica | undefined;
|
|
31
|
+
/** Whether the peer whose session authenticated it as `deviceHex` holds the same account. Only such a peer is offered the ledger or answered for it, so a device of another account never stores the ciphertext or follows its head. */
|
|
32
|
+
isAccountPeer: (deviceHex: string) => Promise<boolean>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** How core/data frames reach a device that this side meets only through the relay hub, over a relay pairing sealed end to end between the two devices: the hub carries ciphertext and never sees a frame. */
|
|
36
|
+
export interface HubDataChannel {
|
|
37
|
+
/** The devices (hex) the hub's gossiped directory has admitted, whether or not this side also has a direct session to them. */
|
|
38
|
+
peers: () => readonly string[];
|
|
39
|
+
/** Sends one frame to `deviceHex` through the hub. Rejects when no hub session is live. */
|
|
40
|
+
send: (deviceHex: string, frame: DataDomainFrame) => Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The hub data channel over whatever hub session `hub()` currently returns, read on each call because the router is built before the hub it talks through. */
|
|
44
|
+
export function hubDataChannel(
|
|
45
|
+
hub: () => {
|
|
46
|
+
peers: () => readonly string[];
|
|
47
|
+
sendDataFrame: (
|
|
48
|
+
deviceHex: string,
|
|
49
|
+
frame: Readonly<DataDomainFrame>,
|
|
50
|
+
) => Promise<void>;
|
|
51
|
+
},
|
|
52
|
+
): HubDataChannel {
|
|
53
|
+
return {
|
|
54
|
+
peers: () => hub().peers(),
|
|
55
|
+
send: async (deviceHex, frame) => hub().sendDataFrame(deviceHex, frame),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Builds the router's account replication from the transport option: a peer is an account peer when the membership proof in its gossiped advert (looked up through `advertOf`) verifies against this machine's own principal. Undefined when the transport has no account ledger. */
|
|
60
|
+
export function accountReplicationFrom(
|
|
61
|
+
options: Readonly<WireMeshTransportOptions>["accountReplication"],
|
|
62
|
+
advertOf: (
|
|
63
|
+
deviceHex: string,
|
|
64
|
+
) => Readonly<Record<string, unknown>> | undefined,
|
|
65
|
+
): AccountReplication | undefined {
|
|
66
|
+
if (options === undefined) return undefined;
|
|
67
|
+
return {
|
|
68
|
+
ledger: options.getLedger,
|
|
69
|
+
isAccountPeer: async (deviceHex) => {
|
|
70
|
+
const advert = advertOf(deviceHex);
|
|
71
|
+
const proof =
|
|
72
|
+
advert === undefined
|
|
73
|
+
? undefined
|
|
74
|
+
: readMembershipProof(advert, "membership");
|
|
75
|
+
return proof === undefined
|
|
76
|
+
? false
|
|
77
|
+
: options.isAccountMember({ proof, deviceHex });
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Routes one frame received on a raw data connection, tracking the connection against its peer's device-id along the way (connectionsByPeer, mutated in place) so a later data-have, data-request or data-entries send addressed to that same peer reuses it. Nothing further happens until the sender is a tracked peer session. A frame naming one of the account's writer logs goes to the account ledger when the sender holds the same account and is dropped otherwise, without reaching dataStorage either way; any other data frame goes to wire-mesh-core's own core/data handlers over dataStorage, and is dropped when this side has no dataStorage. Any handler error is reported via onError and swallowed, never thrown, since this runs inside the transport's own onFrame callback with no caller awaiting it directly. */
|
|
83
|
+
export async function routeDataFrame(
|
|
84
|
+
deps: Readonly<{
|
|
85
|
+
connectionsByPeer: Map<string, Readonly<Connection>>;
|
|
86
|
+
dataStorage: KeyValueStorage | undefined;
|
|
87
|
+
account: Readonly<AccountReplication> | undefined;
|
|
88
|
+
peerSessions: ReadonlyMap<string, unknown>;
|
|
89
|
+
onError: ((error: Error) => void) | undefined;
|
|
90
|
+
}>,
|
|
91
|
+
connection: Readonly<Connection>,
|
|
92
|
+
frame: Frame,
|
|
93
|
+
): Promise<void> {
|
|
94
|
+
const peerDeviceId = connection.peerDeviceId;
|
|
95
|
+
if (peerDeviceId === undefined) return;
|
|
96
|
+
const deviceIdHex = deviceIdToHex(peerDeviceId);
|
|
97
|
+
deps.connectionsByPeer.set(deviceIdHex, connection);
|
|
98
|
+
if (!deps.peerSessions.has(deviceIdHex)) return;
|
|
99
|
+
try {
|
|
100
|
+
const ledger = deps.account?.ledger();
|
|
101
|
+
if (
|
|
102
|
+
isDataFrame(frame) &&
|
|
103
|
+
ledger !== undefined &&
|
|
104
|
+
deps.account !== undefined &&
|
|
105
|
+
ledger.ownsLog(frame.peer)
|
|
106
|
+
) {
|
|
107
|
+
if (!(await deps.account.isAccountPeer(deviceIdHex))) return;
|
|
108
|
+
const reply = await ledger.handleDataFrame(frame);
|
|
109
|
+
if (reply !== null) await connection.send(reply);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (deps.dataStorage === undefined) return;
|
|
113
|
+
if (frame.type === "data-have") {
|
|
114
|
+
const request = await handleDataHave(deps.dataStorage, frame);
|
|
115
|
+
if (request !== null) await connection.send(request);
|
|
116
|
+
} else if (frame.type === "data-request") {
|
|
117
|
+
const entries = await handleDataRequest(
|
|
118
|
+
deps.dataStorage,
|
|
119
|
+
frame,
|
|
120
|
+
DATA_ENTRIES_RESPONSE_LIMIT,
|
|
121
|
+
);
|
|
122
|
+
if (entries !== null) await connection.send(entries);
|
|
123
|
+
} else if (frame.type === "data-entries") {
|
|
124
|
+
await handleDataEntries(deps.dataStorage, frame);
|
|
125
|
+
}
|
|
126
|
+
} catch (error: unknown) {
|
|
127
|
+
deps.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Handles one core/data frame that arrived through a relay pairing, from the device the pairing's secure channel authenticated as `fromDeviceHex`. Only a frame naming one of the account's writer logs, from a device that holds the same account, is acted on, and any answer goes back through the same pairing; every other relayed data frame is dropped, since a device met only through the hub has no standing to read or write this side's other logs. Any handler error is reported via onError and swallowed, as in routeDataFrame. */
|
|
132
|
+
export async function routeRelayedDataFrame(
|
|
133
|
+
deps: Readonly<{
|
|
134
|
+
account: Readonly<AccountReplication> | undefined;
|
|
135
|
+
hub: Readonly<HubDataChannel> | undefined;
|
|
136
|
+
onError: ((error: Error) => void) | undefined;
|
|
137
|
+
}>,
|
|
138
|
+
fromDeviceHex: string,
|
|
139
|
+
frame: Readonly<DataDomainFrame>,
|
|
140
|
+
): Promise<void> {
|
|
141
|
+
const { account, hub } = deps;
|
|
142
|
+
if (account === undefined || hub === undefined) return;
|
|
143
|
+
try {
|
|
144
|
+
const ledger = account.ledger();
|
|
145
|
+
if (ledger?.ownsLog(frame.peer) !== true) return;
|
|
146
|
+
if (!(await account.isAccountPeer(fromDeviceHex))) return;
|
|
147
|
+
const reply = await ledger.handleDataFrame(frame);
|
|
148
|
+
if (reply !== null) await hub.send(fromDeviceHex, reply);
|
|
149
|
+
} catch (error: unknown) {
|
|
150
|
+
deps.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Offers every account writer log this machine holds to every device that holds the same account and that this side can reach (agent-comms#344): over a live, trusted data connection, or, for a device it meets only through the relay hub, over a relay pairing. Such a device that is behind asks for what it is missing, which is the data-domain fan-out that carries each machine's grants and revocations to every other. A device reachable both ways is offered the logs once, directly. Called on the gossip cadence, so a machine that was away when an entry was written still catches up the next time it is reachable. A send that fails is reported and the rest carry on; the next round retries it. */
|
|
155
|
+
export async function announceAccountLedger(
|
|
156
|
+
deps: Readonly<{
|
|
157
|
+
connectionsByPeer: ReadonlyMap<string, Readonly<Connection>>;
|
|
158
|
+
account: Readonly<AccountReplication>;
|
|
159
|
+
hub: Readonly<HubDataChannel> | undefined;
|
|
160
|
+
peerSessions: ReadonlyMap<string, unknown>;
|
|
161
|
+
onError: ((error: Error) => void) | undefined;
|
|
162
|
+
}>,
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
const ledger = deps.account.ledger();
|
|
165
|
+
if (ledger === undefined) return;
|
|
166
|
+
const frames = await ledger.announcements();
|
|
167
|
+
if (frames.length === 0) return;
|
|
168
|
+
const reportError = (error: unknown): void => {
|
|
169
|
+
deps.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
170
|
+
};
|
|
171
|
+
const direct = new Set<string>();
|
|
172
|
+
const offers: Promise<void>[] = [];
|
|
173
|
+
for (const [deviceHex, connection] of deps.connectionsByPeer) {
|
|
174
|
+
if (!deps.peerSessions.has(deviceHex)) continue;
|
|
175
|
+
if (!(await deps.account.isAccountPeer(deviceHex))) continue;
|
|
176
|
+
direct.add(deviceHex);
|
|
177
|
+
for (const frame of frames) {
|
|
178
|
+
try {
|
|
179
|
+
await connection.send(frame);
|
|
180
|
+
} catch (error: unknown) {
|
|
181
|
+
reportError(error);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const { hub } = deps;
|
|
186
|
+
if (hub === undefined) return;
|
|
187
|
+
for (const deviceHex of hub.peers()) {
|
|
188
|
+
if (direct.has(deviceHex)) continue;
|
|
189
|
+
if (!(await deps.account.isAccountPeer(deviceHex))) continue;
|
|
190
|
+
// Each device is offered concurrently: a pairing to a device that is not there waits out its own handshake, which must not hold up the others.
|
|
191
|
+
offers.push(
|
|
192
|
+
(async () => {
|
|
193
|
+
for (const frame of frames) {
|
|
194
|
+
try {
|
|
195
|
+
await hub.send(deviceHex, frame);
|
|
196
|
+
} catch (error: unknown) {
|
|
197
|
+
reportError(error);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
})(),
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
await Promise.all(offers);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface DataFrameRouterDeps {
|
|
207
|
+
dataStorage: KeyValueStorage | undefined;
|
|
208
|
+
/** Absent for a transport with no account ledger. */
|
|
209
|
+
account: Readonly<AccountReplication> | undefined;
|
|
210
|
+
/** The transport's own trusted sessions, keyed by device-id hex: the gate every data frame and every announcement passes. */
|
|
211
|
+
peerSessions: ReadonlyMap<string, unknown>;
|
|
212
|
+
onError: ((error: Error) => void) | undefined;
|
|
213
|
+
/** Absent for a transport that never dials a relay hub. */
|
|
214
|
+
hub: Readonly<HubDataChannel> | undefined;
|
|
215
|
+
/** How often the account ledger is offered to peers: the transport's own re-advertise cadence. */
|
|
216
|
+
announceIntervalMs: number;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** WireMeshTransport's core/data state and dispatch, split out under the repo's max-lines cap: the connection each peer's frames arrive on, routing of every data frame received, the mechanical send primitive, and the periodic offer of the account ledger's writer logs, and the same exchange with a device met only through the relay hub. */
|
|
220
|
+
export class DataFrameRouter {
|
|
221
|
+
/** Backs this side's own responder for an incoming data-have, data-request or data-entries frame about any log other than the account ledger's (agent-comms#50's P5 integration). When undefined, such frames are dropped; the account ledger's are routed either way. For those logs, deciding when to proactively call sendDataFrame at all (the catch-up policy: which peers' logs to track, when to send an initial data-have) stays the caller's business and this field only backs the mechanical parts (answering a have or request, storing entries); the account ledger's own offers are the router's, sent every announceIntervalMs. */
|
|
222
|
+
private readonly dataStorage: KeyValueStorage | undefined;
|
|
223
|
+
|
|
224
|
+
/** Every peer this side has ever received a frame from, keyed by device-id hex, tracking the raw wire-mesh-core Connection each frame arrived on: what sendDataFrame needs, since neither AcceptedMeshSession nor MeshSession exposes a generic "send an arbitrary frame" method the way the raw Connection itself does. Registered eagerly on the very first frame from a connection (including one still in quarantine, e.g. before connect_request approval) so a later sendDataFrame call can reach it; routeDataFrame's own trust gate (peerSessions.has) is what actually decides whether to act on anything received this way, not this map. */
|
|
225
|
+
private readonly connectionsByPeer = new Map<string, Connection>();
|
|
226
|
+
|
|
227
|
+
private announceInterval: ReturnType<typeof setInterval> | undefined;
|
|
228
|
+
|
|
229
|
+
/** Whether an announcement round is still running, so a round slowed by an unreachable hub peer is not stacked on by the next tick. */
|
|
230
|
+
private announcing = false;
|
|
231
|
+
|
|
232
|
+
constructor(private readonly deps: Readonly<DataFrameRouterDeps>) {
|
|
233
|
+
this.dataStorage = deps.dataStorage;
|
|
234
|
+
const { account } = deps;
|
|
235
|
+
if (account === undefined) return;
|
|
236
|
+
this.announceInterval = setInterval(() => {
|
|
237
|
+
if (this.announcing) return;
|
|
238
|
+
this.announcing = true;
|
|
239
|
+
announceAccountLedger({
|
|
240
|
+
connectionsByPeer: this.connectionsByPeer,
|
|
241
|
+
account,
|
|
242
|
+
hub: deps.hub,
|
|
243
|
+
peerSessions: deps.peerSessions,
|
|
244
|
+
onError: deps.onError,
|
|
245
|
+
})
|
|
246
|
+
.catch((error: unknown) => {
|
|
247
|
+
deps.onError?.(
|
|
248
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
249
|
+
);
|
|
250
|
+
})
|
|
251
|
+
.finally(() => {
|
|
252
|
+
this.announcing = false;
|
|
253
|
+
});
|
|
254
|
+
}, deps.announceIntervalMs);
|
|
255
|
+
this.announceInterval.unref();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** See routeDataFrame. */
|
|
259
|
+
async route(connection: Readonly<Connection>, frame: Frame): Promise<void> {
|
|
260
|
+
await routeDataFrame(
|
|
261
|
+
{
|
|
262
|
+
connectionsByPeer: this.connectionsByPeer,
|
|
263
|
+
dataStorage: this.dataStorage,
|
|
264
|
+
account: this.deps.account,
|
|
265
|
+
peerSessions: this.deps.peerSessions,
|
|
266
|
+
onError: this.deps.onError,
|
|
267
|
+
},
|
|
268
|
+
connection,
|
|
269
|
+
frame,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** See routeRelayedDataFrame. An arrow property, so the transport can hand it to the hub session as a callback. */
|
|
274
|
+
readonly routeRelayed = async (
|
|
275
|
+
fromDeviceHex: string,
|
|
276
|
+
frame: Readonly<DataDomainFrame>,
|
|
277
|
+
): Promise<void> => {
|
|
278
|
+
await routeRelayedDataFrame(
|
|
279
|
+
{
|
|
280
|
+
account: this.deps.account,
|
|
281
|
+
hub: this.deps.hub,
|
|
282
|
+
onError: this.deps.onError,
|
|
283
|
+
},
|
|
284
|
+
fromDeviceHex,
|
|
285
|
+
frame,
|
|
286
|
+
);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
/** Sends one frame on the connection peerDeviceHex's frames last arrived on. Throws if none has arrived from it yet. */
|
|
290
|
+
async send(
|
|
291
|
+
peerDeviceHex: string,
|
|
292
|
+
frame: Readonly<DataHaveFrame> | Readonly<DataRequestFrame>,
|
|
293
|
+
): Promise<void> {
|
|
294
|
+
const connection = this.connectionsByPeer.get(peerDeviceHex);
|
|
295
|
+
if (connection === undefined) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`WireMeshTransport: no live connection for peer ${peerDeviceHex}`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
await connection.send(frame);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Stops offering the account ledger. A router that never started offering it has no timer to clear. */
|
|
304
|
+
stop(): void {
|
|
305
|
+
if (this.announceInterval === undefined) return;
|
|
306
|
+
clearInterval(this.announceInterval);
|
|
307
|
+
this.announceInterval = undefined;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves what a bare `agent-comms` (no subcommand) should do from how it was started (agent-comms#339).
|
|
3
|
+
*
|
|
4
|
+
* A terminal on stdin means a person ran it, so it stays the setup tool. A stdin that is not a terminal means a harness launched it and is about to speak MCP over it, so it serves the generic `mcp` bridge, which makes the package name alone a valid MCP server command. Any explicit subcommand is returned untouched, so `setup`, `status`, `remove` and `bridge <id>` keep their meaning.
|
|
5
|
+
*/
|
|
6
|
+
export function resolveDefaultCommand(
|
|
7
|
+
args: readonly string[],
|
|
8
|
+
stdinIsTerminal: boolean,
|
|
9
|
+
): readonly string[] {
|
|
10
|
+
if (args.length > 0 || stdinIsTerminal) return args;
|
|
11
|
+
return ["bridge", "mcp"];
|
|
12
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issuer-side delegation policy (agent-comms#163): deciding which capability verbs an issuer never lets a bearer redelegate, expressed entirely with wire-mesh-core's own shipped primitives. A capability on the policy's nonDelegable set is always minted with delegationsRemaining forced to 0 -- the grant is usable by its own bearer but cannot be passed further down the chain -- regardless of what the caller would otherwise request; mintCapabilityToken needs no change for this, since delegationsRemaining: 0 already means exactly "no further hops" (tokens-CpLDI5Fo.d.mts's own MintCapabilityTokenOptions.delegationsRemaining doc comment).
|
|
3
|
+
*
|
|
4
|
+
* Per-agent overrides are deliberately just a second, bearer-keyed nonDelegable set that entirely REPLACES the policy's own default for that one bearer, never merges with it. This needs no bookkeeping of its own: every bearer's grant is already its own independent mint (mintCapabilityToken places no bound tying separate bearers' root-level grants together), so narrowing one bearer's policy is simply supplying a different nonDelegable set to that bearer's own resolution -- there is no prior per-bearer state to merge into, and an override naming zero verbs makes every capability delegable for that one bearer without touching the default any other bearer still resolves against.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* One issuer's non-delegable policy: `nonDelegable` is the default set of capability verbs this issuer never lets any bearer redelegate; `perAgentOverrides` narrows that default per bearer (device-id hex), replacing it outright for that bearer's own resolution rather than merging with it.
|
|
9
|
+
*/
|
|
10
|
+
export interface DelegationPolicy {
|
|
11
|
+
readonly nonDelegable: ReadonlySet<string>;
|
|
12
|
+
readonly perAgentOverrides?: ReadonlyMap<string, ReadonlySet<string>>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolves the delegationsRemaining value to actually mint for bearer's grant of capability under policy, given whatever delegationsRemaining the caller would otherwise request. Returns 0 when capability is on the bearer's own effective nonDelegable set (its per-agent override when policy defines one for that bearer, otherwise policy's own default set) -- overriding requested outright, the same "issuer refuses to mint an invalid delegation rather than let the far end discover it later" posture mintCapabilityToken's own parent narrowing already applies. Returns requested unchanged for every other capability.
|
|
17
|
+
*
|
|
18
|
+
* Overloaded on requested's own definiteness rather than always returning `number | undefined`: a caller minting a root-level grant always passes a definite `number` (mintCapabilityToken's own `delegationsRemaining?: number` field is entirely absent, never present-as-undefined, under this codebase's exactOptionalPropertyTypes), so the result at those call sites is provably a `number` too -- proper return-type narrowing in place of a defensive `?? 0` the caller would otherwise need purely to satisfy the type checker.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveDelegationsRemaining(
|
|
21
|
+
policy: Readonly<DelegationPolicy>,
|
|
22
|
+
capability: string,
|
|
23
|
+
bearer: string,
|
|
24
|
+
requested: number,
|
|
25
|
+
): number;
|
|
26
|
+
export function resolveDelegationsRemaining(
|
|
27
|
+
policy: Readonly<DelegationPolicy>,
|
|
28
|
+
capability: string,
|
|
29
|
+
bearer: string,
|
|
30
|
+
requested: undefined,
|
|
31
|
+
): undefined;
|
|
32
|
+
export function resolveDelegationsRemaining(
|
|
33
|
+
policy: Readonly<DelegationPolicy>,
|
|
34
|
+
capability: string,
|
|
35
|
+
bearer: string,
|
|
36
|
+
requested: number | undefined,
|
|
37
|
+
): number | undefined {
|
|
38
|
+
const effectiveNonDelegable =
|
|
39
|
+
policy.perAgentOverrides?.get(bearer) ?? policy.nonDelegable;
|
|
40
|
+
return effectiveNonDelegable.has(capability) ? 0 : requested;
|
|
41
|
+
}
|