Skip to content

feat(core): gateway allowlist, the cross-machine trust boundary - #183

Merged
Mearman merged 12 commits into
mainfrom
feat/gateway-allowlist
Sep 17, 2026
Merged

Mearman merged 12 commits into
mainfrom
feat/gateway-allowlist

Conversation

@Mearman

@Mearman Mearman commented Sep 17, 2026

Copy link
Copy Markdown
Member

Implements agent-comms#156, the third leg of the cross-machine mesh epic (#153).

Adds GatewayTrust: an in-memory, deny-by-default allowlist of trusted remote device-ids (not persisted to disk, mirroring v1's own FederationManager.trustedFingerprints). Wired through the entire hub-crossing surface:

  • Outbound gossip advertisement (forwardAdvertsToHub/pushHubCatchUp) is withheld until at least one remote device is trusted -- necessarily coarse, since wire-mesh-core's relay-hub broadcasts to every connected peer with no per-recipient targeting.
  • Inbound directory-merge and relayed-request dispatch (HubSession.consume/connect) are gated per device-id, the precise boundary hub-session.ts's own pre-existing isStateMutatingMessage comment already flagged as "agent-comms#156's own future deliverable".
  • Outbound targeted hub requests (WireMeshTransport.sendRoomRequest) refuse an untrusted target immediately with an unauthorized outcome, rather than waiting out the hub's own silent-drop timeout.
  • readvertiseGossip's own periodic hub-directed self-advert (a separate mechanism from forwardAdvertsToHub, discovered while writing the deny-by-default integration tests -- it was leaking this side's presence/hosted-rooms/self-agent advert onto the hub regardless of trust) is now gated the same way.

Trust is keyed per individual device-id rather than per remote machine: the wire protocol carries no field attributing a gossiped entry or relayed request to its originating gateway connection, only to the individual device (which may be an ordinary local peer forwarded on a remote machine's behalf). This is documented in GatewayTrust's own class doc as the intended, wire-protocol-honest granularity, not a shortfall.

Exposed via three CommsTool actions (gateway_trust, gateway_untrust, gateway_list_trusted), following mesh_set_visibility/mesh_get_visibility's own established shape.

Closes #156

An in-memory, deny-by-default set of trusted remote device-ids, keyed
per individual device rather than per remote machine since wire-mesh-core's
relay-hub protocol carries no field attributing a gossiped entry or
relayed request to its originating gateway connection. Not yet wired
into the hub forwarding/session paths.
forwardAdvertsToHub/pushHubCatchUp now refuse to advertise anything
onto the hub until at least one remote device is trusted. Coarse by
necessity: wire-mesh-core's relay-hub broadcasts a gossip frame to
every connected peer with no per-recipient targeting, so this
approximates "advertise only to allowlisted gateways" as "advertise
nothing until the operator has opted in by trusting someone".
…trust

HubSessionDeps gains isTrusted; consume() now refuses to dispatch a
relayed request from an untrusted device-id (a room-domain request
gets an explicit unauthorized error, a legacy frame is silently
acked and dropped), and connect()'s own directory-merge loop drops
any entry whose device-id isn't trusted before it ever reaches
onDirectory. The state_sync/state_update filter stays unconditional
even for a now-trusted sender: gateway trust means "worth acting
on", not "may directly overwrite this side's mesh state".
Pick<...> written literally in a doc comment is parsed as malformed
HTML by the tsdoc linter; wrap it in backticks instead, and reference
the retiring federation.ts commit by hash rather than a bare hyphen
number that reads like a GitHub issue reference.
MeshStore owns one GatewayTrust instance (public, mirroring discovery)
and exposes addTrustedGateway/removeTrustedGateway/listTrustedGateways.
WireMeshTransport takes it as a trailing constructor argument
(defaulting to a fresh, empty instance for every existing call site),
passes isTrusted into HubSession, hasAny into both hub-forwarding call
sites via connectHubGateway (hub-forwarding.ts's own connect-then-catch-up
helper), and gates sendRoomRequest's own hub fallback on the target
device being trusted so an untrusted target fails fast with an
unauthorized outcome rather than waiting out the hub's own silent-drop
timeout. bridge-mesh.ts and test-transport.ts both pass store.gatewayTrust
through so the store's own add/remove calls and the transport's own
gates read the same set.

Extracts listener-registry.ts (addListener/removeListener/listListeners/
advertisedAddresses, previously inline in WireMeshTransport) to stay
under this repo's max-lines cap, the same reason hub-session.ts,
connection-approval.ts, room-router.ts, peer-lifecycle.ts, and
gossip-directory.ts were each split from the same file -- a pure
extraction, unchanged behaviour.
Gateway trust (agent-comms#156) is deny-all by default, so every
scenario here now trusts the specific remote device-ids it needs
before expecting forwarding, directory merge, or DM routing to
happen -- trust is keyed per device, not per remote machine, so a
non-gateway local peer (a2) needs its own device-id trusted even
though its own machine's gateway (a1) is trusted separately.
… tests

Deny-all by default (agent-comms#156) means the two transports here
no longer discover or message each other, or exercise the
state_sync/state_update filter, without each explicitly trusting the
other's device-id first. Strengthens the #169 security-finding test
along the way: it now proves the filter still applies even to a hub
peer this side has explicitly trusted, not merely an anonymous one.
An untrusted target now resolves unauthorized before ever touching
the hub; a trusted one falls through to the pre-existing not_connected
outcome. Split into wire-mesh-transport-gateway-trust.test.ts rather
than growing wire-mesh-transport.test.ts past this repo's max-lines
cap, the same reason wire-mesh-transport-hub.test.ts and
wire-mesh-transport-shutdown-unref.test.ts were split before it.
Removes a now-redundant double type cast on a test fixture and
reflows an import that now fits on one line.
…eway_list_trusted tool actions

Follows mesh_set_visibility/mesh_get_visibility's own established
shape: three CommsAction variants, a device field on the shared MCP
tool parameter schema, buildAction parsing with an exhaustiveness
check against every action literal, and three MeshOnlyFeatures-gated
CommsTool handlers delegating to MeshStore's own
addTrustedGateway/removeTrustedGateway/listTrustedGateways.
…ateway trust

readvertiseGossip's periodic tick called sendGossipUpdate on every
live session unconditionally, including the hub's own session -- a
completely separate mechanism from forwardAdvertsToHub's own
already-gated advertiseDevices path, and one that bypassed the
gateway trust boundary entirely. A visible local agent's presence,
hosted rooms, and self-advert leaked onto the hub the moment the
periodic gossip timer fired, regardless of whether any remote
gateway was trusted. Caught by gateway-trust.integration.test.ts's
own deny-by-default coverage. Fixed via HubSession.ownsSession, so
the loop can identify and gate only the hub's own session without
ever exposing the raw session object.
Three real-hub scenarios: a remote agent's own directory entry never
merges without explicit trust even once a trusted control agent
proves gossip propagation had time; an untrusted side's own local
agents never reach the hub at all, regardless of whether the far
side would have accepted them; and a room request to an untrusted
device is refused immediately rather than hanging on the hub's own
silent-drop timeout. This is the test that caught readvertiseGossip's
own trust-gate bypass in the preceding commit.
@Mearman
Mearman force-pushed the feat/gateway-allowlist branch from cea5a99 to 4ad262b Compare September 17, 2026 18:15
@Mearman
Mearman marked this pull request as ready for review September 17, 2026 18:17
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 17, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review Completed 2026-09-17T18:24:15.938888Z 4ad262b Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Mearman
Mearman merged commit 7559c21 into main Sep 17, 2026
6 checks passed
@Mearman
Mearman deleted the feat/gateway-allowlist branch September 17, 2026 18:18
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 3.13.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@Mearman Mearman changed the title feat(core): gateway allowlist -- the cross-machine trust boundary feat(core): gateway allowlist, the cross-machine trust boundary Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Gateway allowlist of trusted remote gateways

1 participant