---
id: http/websocket/connection-overlay
name: Implement connection-local Layer 2 overlay for browser-registered ops (no PeerId, ADR-024/034/044)
status: completed
depends_on: [http/websocket/upgrade-handler]
scope: moderate
risk: medium
impact: component
level: implementation
---

## Description

Implement the connection-local Layer 2 overlay for browser-registered
ops in `src/websocket/overlay.rs`. This is the mechanism that gives a
browser bidirectional-call capability *without* peer-graph membership
(ADR-024, ADR-034 §4, ADR-044 §5). A browser over WebSocket has no
`PeerId`, does not enter `PeerCompositeEnv`, and any ops it registers
land in a per-`CallConnection` overlay that dies when the connection
drops.

### Browsers are not alknet peers (websocket.md §"Browsers are not alknet peers")

A browser over WebSocket authenticates by bearer token, gets no
`PeerId`, does not enter `PeerCompositeEnv`, and its registered ops (if
any) land in the connection-local Layer 2 overlay. The rationale, stated
in ADR-044 §5 and amending ADR-034 §4 by reference, is a load-bearing
distinction:

**"Peer" in alknet means an addressable node in the call-protocol peer
graph** — a stable `PeerId`, reachable via `PeerRef::Specific`, whose ops
land in `PeerCompositeEnv`, whose identity is stable across reconnects.
It does *not* mean "any endpoint that exchanges calls during a live
session." A browser is the second thing but not the first, on three
concrete grounds:

1. **No stable cryptographic identity of its own.** A `PeerEntry` is
   anchored to fingerprints (Ed25519, X.509) that *the peer* presents
   and the local node pins. A browser presents a bearer token the *hub*
   issued; the "identity" is the hub's bookkeeping for that token, not
   something the browser owns or that could be pinned by another node.
   There is nothing to put in `PeerEntry.fingerprints`.

2. **Ephemeral.** Close the tab → connection dies → the connection-local
   Layer 2 overlay dies with it. A `PeerEntry` keyed to a browser would
   be a permanently-dead entry within seconds. `PeerRef::Specific("browser-X")`
   from another node would route to nothing.

3. **Not addressable from other nodes.** `PeerRef::Specific` resolves
   through `PeerEntry` → `PeerId`. Another alknet node has no way to
   reach "the browser currently connected to hub-A"; the hub holds that
   connection as a live `CallConnection` handle, not as a peer-graph
   entry. The connection-local overlay is precisely the mechanism that
   gives the browser bidirectional-call capability *without* peer-graph
   membership.

### The overlay (websocket.md §"Connection-local overlay")

A browser over WebSocket has no `PeerId` on the hub's side. Any ops the
browser registers land in a **connection-local Layer 2 overlay**
(ADR-024) — a per-`CallConnection` overlay that dies when the connection
drops. This is the same mechanism ADR-034 §2 describes for the inbound
browser case: the browser is a bidirectional call target during a live
session, not a peer-graph member, and the connection-local overlay is
what gives it bidirectional-call capability *without* peer-graph
membership.

When the WS connection closes (browser closes the tab, network drops),
the overlay and all its registered ops are dropped — no explicit
deregistration needed. A `PeerRef::Specific("browser-X")` from another
node would route to nothing, because there is no `PeerEntry` for the
browser.

### Bidirectionality (websocket.md §"Bidirectionality")

The WS call-protocol session inherits the call protocol's native
bidirectionality: both sides can send `call.requested` frames. The
browser calls operations on the hub; the hub can call operations
registered on the browser's side, over the same session, using the same
`PendingRequestMap` and `EventEnvelope` framing as `alknet/call`.

The browser case where the client registers no operations of its own
is the common case — the server→client call direction is unused because
the browser has nothing to call. That is a use-case scoping, not an
architectural limitation. A browser that *does* expose ops (e.g., a UI
that registers a `ui/dragged` op the hub can call to push live updates)
registers them in the connection-local Layer 2 overlay, and the hub
reaches them through the live `CallConnection` handle — not through
`PeerRef::Specific` (the browser is not a peer).

### Implementation

The `CallConnection` constructed by the upgrade handler (the
`upgrade-handler` task, via the `dispatcher-transport-abstraction`
task's non-QUIC constructor) already holds a Layer 2 overlay
(`imported_operations: Arc<RwLock<HashMap<String, HandlerRegistration>>>`)
and exposes `register_imported()` / `register_imported_all()` /
`overlay_env()`. The browser registers ops via these methods; the
overlay is per-connection and dies when the `CallConnection` is dropped
(WS close).

This task ensures:

1. The overlay is correctly scoped to the WS connection (not the
   `PeerCompositeEnv` — no `PeerId`, no `PeerEntry`).
2. The hub's outgoing `call.requested` to browser-registered ops routes
   through the `CallConnection`'s overlay (via `overlay_env()`), not
   through `PeerRef::Specific`.
3. The overlay is dropped on WS close (no explicit deregistration; the
   `Arc<RwLock<HashMap>>` is dropped when the `CallConnection` is
   dropped).
4. `AccessControl::check(identity)` gates the hub's calls to
   browser-registered ops (the browser's bearer-token identity is the
   caller identity for the hub's outgoing calls — wait, no: the *hub*
   is the caller when it calls a browser op; the browser's identity is
   the *handler* identity. Clarify: the hub's `call.requested` to a
   browser op runs with the hub's identity as caller, the browser's
   registration bundle's `composition_authority` as handler identity.
   The browser's `AccessControl` on its registered ops gates whether
   the hub is allowed to call them.)
5. Abort cascade on WS disconnect (ADR-016): when the WS connection
   closes, all in-flight subscriptions and calls to browser ops are
   aborted, cascading to descendants.

### What this task does NOT do

- **No `PeerEntry` for the browser.** The browser is not in the peer
  graph. This task ensures the overlay is connection-local, not
  peer-graph.
- **No `from_wss` adapter.** Out of scope (websocket.md §"Future" —
  scope decision). This task is about the browser *registering* ops on
  its connection, not about importing a remote node's ops over WS.

## Acceptance Criteria

- [ ] Browser-registered ops land in the `CallConnection`'s Layer 2 overlay (not `PeerCompositeEnv`)
- [ ] No `PeerId` created for the browser (no `PeerEntry`, no peer-graph membership)
- [ ] `register_imported()` / `register_imported_all()` work for browser ops
- [ ] Hub's outgoing `call.requested` to browser ops routes through `overlay_env()`
- [ ] Hub's outgoing calls do NOT route through `PeerRef::Specific` (browser is not a peer)
- [ ] `AccessControl` on browser-registered ops gates the hub's calls
- [ ] Overlay dropped on WS close (no explicit deregistration; `Arc<RwLock<HashMap>>` dropped)
- [ ] `PeerRef::Specific("browser-X")` from another node → routes to nothing (no `PeerEntry`)
- [ ] WS close → all in-flight subscriptions/calls to browser ops aborted (ADR-016 cascade)
- [ ] WS close → overlay and all registered ops dropped
- [ ] Bidirectionality: hub can `call.requested` to browser-registered ops
- [ ] Browser with no registered ops → server→client direction unused (use-case scoping, not a limitation)
- [ ] Integration test: browser registers op → hub calls it via overlay
- [ ] Integration test: WS close → overlay dropped (op no longer reachable)
- [ ] Integration test: `PeerRef::Specific("browser-X")` → NOT_FOUND (no PeerEntry)
- [ ] Integration test: WS close mid-call to browser op → `call.aborted` cascade
- [ ] Integration test: `AccessControl` on browser op gates hub's call
- [ ] `cargo test -p alknet-http` succeeds
- [ ] `cargo clippy -p alknet-http --all-targets` succeeds with no warnings

## References

- docs/architecture/crates/http/websocket.md — Connection-local overlay (§"Connection-local overlay"), Bidirectionality (§"Bidirectionality"), Browsers are not peers (§"Browsers are not alknet peers")
- docs/architecture/decisions/024-operation-registry-layering.md — ADR-024 (Layer 2 connection-local overlay)
- docs/architecture/decisions/034-outgoing-only-x509-and-three-peer-roles.md — ADR-034 §4 (browsers are not peers)
- docs/architecture/decisions/044-defer-webtransport-browsers-use-websocket.md — ADR-044 §5 (addressability vs bidirectionality rationale)
- docs/architecture/decisions/016-abort-cascade-for-nested-calls.md — ADR-016 (abort cascade on disconnect)
- docs/architecture/decisions/029-peer-graph-routing-model.md — ADR-029 (PeerRef::Specific routes through PeerEntry → PeerId)

## Notes

> The connection-local overlay is the mechanism that gives a browser
> bidirectional-call capability without peer-graph membership. The
> browser has no PeerId, no PeerEntry, no PeerCompositeEnv entry — it is
> a bidirectional call target during a live session, not a peer-graph
> member. The overlay dies with the WS connection (no explicit
> deregistration). The hub reaches browser ops through the live
> CallConnection handle's overlay_env(), not through PeerRef::Specific.
> The "browsers are not peers" rationale (ADR-044 §5) is load-bearing:
> "peer" means addressable peer-graph node, not "any endpoint that
> exchanges calls during a live session." A browser has no stable
> cryptographic identity, is ephemeral, and is not addressable from
> other nodes — three concrete grounds for not being a peer.

## Summary

> Added AccessControl::check to OverlayOperationEnv::invoke_with_policy in alknet-call
> so hub's calls to browser-registered ops are gated by the browser's AccessControl
> (caller identity = parent handler_identity.as_identity(), matching OperationRegistry
> semantics). Created src/websocket/overlay.rs with 19 integration tests: overlay
> scoping (not PeerCompositeEnv), no PeerId for browser, register_imported/all,
> overlay_env() routing, PeerRef::Specific('browser-X')→NOT_FOUND, AccessControl gating
> (allowed/forbidden/default), overlay drop on WS close + isolation, ADR-016 abort
> cascade on disconnect, bidirectionality, no-ops use-case scoping. Zero regressions:
> alknet-call 277+2 tests pass, alknet-http 207 tests pass, clippy clean on both.