Skip to content

feat(protocol): an application sees only the neighbours that prove membership - #688

Closed
bahdotsh wants to merge 1 commit into
feat/app-membership-udlfrom
feat/app-visibility-gate
Closed

bahdotsh wants to merge 1 commit into
feat/app-membership-udlfrom
feat/app-visibility-gate

Conversation

@bahdotsh

@bahdotsh bahdotsh commented Nov 4, 2026

Copy link
Copy Markdown
Member

Step 7 (and 7a) of the shared network plan (ADR 0037). Stacked on #687.

The gate

When a publisher key is configured, the application learns about a neighbour, and the automatic effects of discovering it run, only after the neighbour proves membership in the exchange.

  • The decision is made in the core.
    • neighbor_reachable(peer, transport) -> bool runs discovery, then says whether the caller reports NeighborDiscovered now.
    • If the peer is not visible yet, its report is held. The core emits the event itself, naming the carrier, once the peer becomes visible.
    • neighbor_unreachable(peer) -> bool returns true only for a peer whose discovery was reported.
  • Every emission site goes through those two calls. On the UniFFI side that is notify_neighbor_reachable and the four NeighborLost sites. In the browser crate it is ble_host, nostr_host, and node (link up and link down).
  • PeerRssi and a received __PRESENCE__ are reported only when app_sees_peer is true.
  • The automatic key-package push waits for visibility. It is the one discovery effect that answers a stranger with this node's key material, and it runs when the held report is released.

Not gated, on purpose (7a)

  • Carriage: relay targets, the outbox flush to that peer, and custody redelivery.
  • Things the app asked for toward a peer it named: subscription renewal, data sync for shared spaces, Welcome re-arm, and relay or gateway presence for its watchlist.
    • Gating these would break the leaf pairing traced in the plan.
    • The relay presence path is the app's own watchlist, so it is named by construction.

The spec section control-messages.md#application-membership now has a "What visibility gates" list.

Behaviour without a publisher key

Unchanged for the bindings: app_sees_peer is always true and nothing is held. The browser's peer-stream discovery has one change: it now suppresses a blocked peer, which every other site already did.

Tests

Six new tests in protocol::tests::membership:

  • a member is reported once it is proven, and a stranger never is;
  • an unconfigured node reports every peer at once;
  • a loss is reported only after a discovery was;
  • a peer lost before it proves membership is never reported;
  • the key-package push waits until the peer is visible, and never reaches a stranger;
  • presence from an unseen peer is not reported.

Suite results:

  • offline-protocol lib: 2205 pass.
  • offline-protocol-uniffi: 318 pass.
  • offline-protocol-web: 88 of 89 pass. The one failure is no_shipped_file_names_a_relay, which fails locally on old agent worktrees under .claude/worktrees and is not related to this change.

Clippy over the workspace is clean.

Next in the stack

7b: the inbound reply gate (named set and end-to-end proof). Until it lands, a configured member still answers a stranger's key package. This PR gates only what discovery does on its own.

…mbership

Step 7 of the shared network plan (ADR 0037): the visibility gate. With
a publisher key configured, a neighbour is reported to the application,
and the automatic effects of discovering it run, only once the
membership exchange makes it visible.

The decision moves into the core. `neighbor_reachable(peer, transport)`
runs discovery and says whether the caller reports `NeighborDiscovered`
now; a report for a peer not yet visible is held, and the core emits
the event itself, naming the carrier, when the exchange proves
membership. `neighbor_unreachable(peer)` says whether `NeighborLost`
is reported, which is only for a peer whose discovery was. Every UniFFI
and browser emission site goes through the two, so there is one
decision for every carrier. `PeerRssi` and a received `__PRESENCE__`
ask `app_sees_peer`.

The automatic key package push waits for visibility too. It is the one
discovery effect that answers a stranger with this node's key material,
and it runs when the held report is released.

Not gated, on purpose: carriage (relay targets, the outbox flush,
custody redelivery), and what the application asked for itself toward
a peer it named (subscriptions, shared spaces, Welcomes, the relay
presence answers for its watchlist). With no publisher key every peer
is visible, which is the behaviour before this change.

The browser's peer-stream discovery now also suppresses a blocked peer,
as every other site always did.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant