docs(review 008 Unit 3 planning): ADR-051 — in-tree ChannelRelay + hub-leg assembly; plan split into 3a/3b/3c
Design session outcome for the relay unit. The hub/spoke family (hub or spoke may relay; the hub re-exposes spoke services by ACL without binding ports) needs the call-half support too, so the unit's scope grew beyond the as-pinned sketch and is now three sub-units. - ADR-051 (new): the relay is the wrapper composition — translate hop = the ADR-049 establisher shape, byte-forward hop = pump_bidi per ADR-050; the relay holds Arc<CallConnection> + ChannelManager per producer leg (not a ChannelClient — take_call_connection's detach is wrong for a hub with three CallConnection claimants); the reason mapping preserves the spoke's code+message (timeout is the one non-1:1 case, mapping to dial_failed); the registration seam is two-phase (discover/stash → per-connection fork-register — the ADR-047 §4 fork mechanism makes a one-call import impossible); the ADR-042 relay map dissolves (implicit per-channel mapping) and channel/close needs no translation surface (EOF cascade propagates with correct ledger accounting on both legs); Pub-typed marked specs are a loud assembly error; establishment bounds compound per hop (noted, no fix); the ACL layering note is pinned (the spoke's AccessControl sees only the hub identity; forwarded_for is never checked). - ADR-042 amended: the §Scope note is revised (implementation is an alkcall export; hub crates compose it) and the two mechanism supersessions are recorded — the contract and auth-model rationale unchanged. - ADR-047 amendment 3's forward reference and the README ADR index updated (001..051). - Review 008 remediation plan: Unit 3 split into 3a (ChannelRelay component + gates), 3b (hub-leg install template — the call-half support), 3c (gate-2 e2e incl. the mid-establishment disconnect window); sequencing note updated; adoption note records the session's decisions. Verified: cargo doc --no-deps (markdown-only change).
This commit is contained in:
1 parent
620180d615
commit
91765446b7
5 files changed
+505
-69
No files matched your search
@@ -213,6 +213,23 @@ from the as-filed text, adopted in planning:
|
||||
|
||||
Design decisions pinned above the as-filed option lists:
|
||||
|
||||
- **2026-09-17 (Unit 3 planning session): Unit 3 split into 3a/3b/3c
|
||||
and design pinned in ADR-051** (`docs/architecture/decisions/
|
||||
051-channel-relay-and-hub-leg.md`) — the session's discussion
|
||||
expanded the unit's scope beyond the as-pinned sketch: the hub/spoke
|
||||
family (hub or spoke may relay; the hub re-exposes spoke services by
|
||||
ACL without binding ports) needs the call-half support too (the
|
||||
hub-leg install template — now Unit 3b). Key decisions, all in
|
||||
ADR-051: the relay holds `Arc<CallConnection>` + `ChannelManager`
|
||||
per producer leg (not a `ChannelClient` — `take_call_connection`'s
|
||||
detach is wrong for a hub with three `CallConnection` claimants);
|
||||
the reason mapping preserves the spoke's code+message (`timeout` is
|
||||
the one non-1:1 case — it maps to `dial_failed`); the registration
|
||||
seam is two-phase (fork mechanism, §4); the relay map and
|
||||
`channel/close` translation dissolve (implicit mapping + EOF
|
||||
cascade, recorded as the ADR-042 amendment's mechanism supersessions);
|
||||
Pub-typed marked specs are a loud assembly error; establishment
|
||||
bounds compound per hop (noted, no fix).
|
||||
- **U-1 shape: combined (b) + (c).** The as-filed option (a) is
|
||||
unsound as stated — the derivation runs consumer-side against a wire
|
||||
JSON payload, so an allowlist "threaded from registration" has no
|
||||
@@ -233,7 +250,7 @@ Design decisions pinned above the as-filed option lists:
|
||||
|
||||
## Unit 1 — U-2: establisher reply projection (ADR-049 amendment 3)
|
||||
|
||||
Land first: the relay component (Unit 3) projects a spoke's extra reply
|
||||
Land first: the relay component (Unit 3a) projects a spoke's extra reply
|
||||
fields through `Establishment`, so the projection must exist before the
|
||||
e2e can assert `bound` flowing through a relay.
|
||||
|
||||
@@ -311,82 +328,153 @@ Verification gates:
|
||||
flavor-form spec serializes with the explicit string and rebuilds
|
||||
from it; `op/register` announced flavor-form spec round-trips with
|
||||
the marker.
|
||||
- Review gate 2 (hub-relay e2e) lands with Unit 3.
|
||||
- Review gate 2 (hub-relay e2e) lands with Unit 3c.
|
||||
|
||||
## Unit 3 — the in-tree relay component (ADR-042 amendment) + gate 2 e2e
|
||||
## Unit 3 — the in-tree relay component (ADR-042 amendment, ADR-051) + gate 2 e2e
|
||||
|
||||
The relay wrapper becomes a reusable alkcall export, composed from the
|
||||
existing machinery rather than a parallel path:
|
||||
Adopted 2026-09-17, design pinned in ADR-051 (`docs/architecture/
|
||||
decisions/051-channel-relay-and-hub-leg.md`): the relay is the wrapper
|
||||
composition (translate hop = the ADR-049 establisher shape; byte-
|
||||
forward hop = `pump_bidi` per ADR-050), the ADR-042 relay map
|
||||
dissolves (implicit per-channel mapping), `channel/close` needs no
|
||||
translation surface (the EOF cascade propagates with correct ledger
|
||||
accounting on both legs), and the registration seam is two-phase
|
||||
(discover/stash → per-connection fork-register — the ADR-047 §4
|
||||
amendment's fork mechanism makes a one-call import impossible without
|
||||
adapter changes). The call-half support the hub/spoke family needs is
|
||||
the hub-leg install template (Unit 3b) — composition of existing
|
||||
pieces, not a new protocol surface. Three sub-units, three sessions'
|
||||
scope:
|
||||
|
||||
1. New module `src/channels/relay.rs` —
|
||||
`ChannelRelay::register_relay_openable(consumer_registry, spec,
|
||||
producer_client)` (naming bikeshed at implementation): for a
|
||||
from_call-imported spec with `channel_open` set, registers on the
|
||||
hub's consumer-leg channel-0 registry via
|
||||
`register_openable_with_establisher`, where:
|
||||
- the **establisher** is the translate hop (ADR-042 layer 1):
|
||||
`call_open_op` on the producer leg's `ChannelClient`, populating
|
||||
`forwarded_for` from the consumer's per-call identity (ADR-026 §3
|
||||
— the same overlay the existing forwarding handlers do),
|
||||
`adopt_channel`ing the returned `spoke_id` into the producer-leg
|
||||
manager, and returning `Establishment::new(plan)` whose plan
|
||||
carries the spoke-leg streams; a spoke `channel:open_failed`
|
||||
maps into `EstablishmentError` by reason code (the vocabularies
|
||||
align 1:1); with U-2 landed, the establisher ALSO carries the
|
||||
spoke reply's extra fields via `with_reply_field` — the wrapper
|
||||
projects them onto the consumer reply with only `channel_id`
|
||||
rewritten (wrapper-owned key).
|
||||
- the **OpenHandler** is the byte-forward hop (ADR-042 layer 2):
|
||||
pumps the consumer-leg channel `BiStream` ↔ the producer-leg
|
||||
stream pair (the mux/demux absorbs the 8-byte header + ID rewrite
|
||||
— the relay never parses chunk framing), awaited inline per the
|
||||
R-02 lifetime contract; on completion it tears down BOTH legs
|
||||
(the wrapper's handler-exit teardown covers the consumer leg; the
|
||||
handler explicitly closes the producer leg).
|
||||
### Unit 3a — `ChannelRelay` (the relay component)
|
||||
|
||||
New module `src/channels/relay.rs`:
|
||||
|
||||
1. `ChannelRelay::register_relay_openable(consumer_registry, spec,
|
||||
producer_leg)` — for a from_call-imported spec with `channel_open`
|
||||
set, registers on the hub's consumer-leg channel-0 fork registry
|
||||
via `register_openable_with_establisher`, where:
|
||||
- **`producer_leg`** is `Arc<CallConnection>` +
|
||||
producer-leg `ChannelManager` (ADR-051 §2 — not a
|
||||
`ChannelClient`: the hub's `CallConnection` is shared by import,
|
||||
relay establisher, and hub ops, so `take_call_connection`'s
|
||||
detach is wrong; the establisher builds its forwarded payload
|
||||
directly — the hub as caller, the consumer as `forwarded_for`
|
||||
from the per-call identity, ADR-026 §3).
|
||||
- The **establisher** (the translate hop, ADR-042 layer 1): calls
|
||||
the producer leg's open op with the forwarded payload, adopts the
|
||||
returned spoke `channel_id` into the producer-leg manager, and
|
||||
returns `Establishment::new(plan)` whose plan carries the
|
||||
spoke-leg streams. The spoke reply's `channel_id` is stripped;
|
||||
the reply's other fields ride `with_reply_fields` (`bound`
|
||||
flows end-to-end, per-hop truthful). Failure mapping per
|
||||
ADR-051 §3 (the reason table; spoke `CallError` code + message
|
||||
preserved in the mapped variant).
|
||||
- The **OpenHandler** (the byte-forward hop, ADR-042 layer 2):
|
||||
`pump_bidi(consumer_leg_bistream, spoke_recv, spoke_send)` —
|
||||
the relay never parses chunk framing (per-leg mux/demux absorbs
|
||||
the 8-byte header; no ID rewrite exists — ids are
|
||||
per-connection). Awaited inline (R-02); on completion both legs
|
||||
tear down (the wrapper's handler-exit teardown covers the
|
||||
consumer leg; the pump's drops EOF the spoke leg).
|
||||
- The consumer leg keeps the full wrapper machinery: ACL,
|
||||
per-identity cap/ledger, establishment bound (bounds the spoke
|
||||
dial round-trip), teardown-on-failure — no parallel
|
||||
authorization path.
|
||||
2. `build_bundles` (`from_call.rs:128`) or a documented follow-on
|
||||
helper branches on the rebuilt spec's marker: marked op → relay
|
||||
openable (given a `ChannelClient` for the producer leg via
|
||||
`FromCallConfig`); unmarked → the plain forwarding handlers as
|
||||
today. This is the "consumer branches on the marker" seam Gap C
|
||||
named — now with an in-tree implementation downstream crates
|
||||
compose directly.
|
||||
3. ADR-042 amendment: the §"Scope note" (implementation lives in
|
||||
`alknet-hub`/downstream) is revised — the relay implementation is
|
||||
an alkcall export (`ChannelRelay`), hub crates compose it; the
|
||||
translate-not-forward contract and the auth-model rationale are
|
||||
unchanged.
|
||||
4. Changelog + minor version bump (new pub API: `ChannelRelay`,
|
||||
`open_channel_with_reply`, `Establishment::with_reply_field*`;
|
||||
`Establishment` field addition is additive behind
|
||||
`#[non_exhaustive]`).
|
||||
2. Rejection postures (ADR-051 §6): a `Pub`-typed marked spec is an
|
||||
assembly error at registration (`channel:pub_open_not_implemented`
|
||||
class, C-08 blocker) — never a silent stub; an unmarked spec
|
||||
passed to the relay registration is likewise an error (branch on
|
||||
the marker before calling).
|
||||
|
||||
Verification gate 2 (the review's full harness): consumer → hub
|
||||
(`ChannelsAdapter` on both legs + forked channel-0 registry +
|
||||
`register_relay_openable`) → producer, opening `channels/tunnel/direct`
|
||||
end to end: the open resolves on the consumer with the hub-allocated
|
||||
`channel_id`, the establisher-contributed `bound` field (Unit 1)
|
||||
survives the relay to the consumer reply, data flows both directions on
|
||||
the data channel (the hub never parses it), and hub-side disconnect /
|
||||
spoke-side close tears down both legs' channels. A companion
|
||||
`channels/tty/sub` relay run pins no regression on standard shapes.
|
||||
Verification gates:
|
||||
|
||||
- The establisher populates `forwarded_for` from the consumer's
|
||||
per-call identity; the spoke reply's `channel_id` is stripped (the
|
||||
consumer reply carries the hub-allocated id).
|
||||
- A spoke `channel:open_failed` maps to the consumer-leg
|
||||
`channel:open_failed` with the mapped reason and the spoke's
|
||||
message preserved; consumer leg tears down (ledger decremented);
|
||||
no spoke channel leaks (the adopt-then-fail window is covered).
|
||||
- `bound` (Unit 1's projection) survives the relay to the consumer
|
||||
reply.
|
||||
- Pub-typed and unmarked specs are rejected at registration.
|
||||
|
||||
### Unit 3b — the hub-leg install template (the call-half support)
|
||||
|
||||
The hub-side composition every test hand-rolls
|
||||
(`make_install_channel_zero`-shaped), exported in-tree (ADR-051 §5 —
|
||||
two-way-door API shape):
|
||||
|
||||
1. Fork the hub's base registry (ADR-047 §4 amendment mechanism).
|
||||
2. Register the generic channel ops (`ChannelOperations::register_on`)
|
||||
and the bootstrap discovery ops (closed over the fork, so
|
||||
`services/list` sees the fork's ops — review 004 F-06).
|
||||
3. Register the stashed plain bundles as-is (the existing forwarding
|
||||
handlers) and the marked specs via Unit 3a's relay registration.
|
||||
The stash is the `Clone`able `Vec<HandlerRegistration>` `from_call`
|
||||
returned (registrations and `HandlerKind` are `Clone`; one
|
||||
discovered set serves any number of consumer legs).
|
||||
4. Per-call identity resolution for the serving dispatch
|
||||
(`ServingConfig`-shaped precedence: payload token → explicit
|
||||
override → transport identity), then the single-stream dispatch
|
||||
loop.
|
||||
5. Per-consumer op-subset filtering composes here (the fork is per
|
||||
consumer leg — ADR-051 §4's composition note). ACL layering is
|
||||
pinned in ADR-051 §5: the spoke's `AccessControl` sees only the
|
||||
hub identity; the end consumer's identity is `forwarded_for`
|
||||
metadata, never checked by any `AccessControl::check`.
|
||||
|
||||
Verification gates:
|
||||
|
||||
- The template composes fork + generic ops + bootstrap discovery +
|
||||
plain bundles + relay openables + serving identity and dispatches
|
||||
channel 0; `services/list` on the consumer leg shows the
|
||||
re-exposed ops; a plain imported op round-trips through the hub
|
||||
(the call-half forwarding path exercised for real).
|
||||
|
||||
### Unit 3c — gate 2, the review's full e2e harness
|
||||
|
||||
The review's verification gate 2, now with the in-tree components:
|
||||
|
||||
1. Consumer → hub (Unit 3b's template on the consumer leg;
|
||||
`ChannelsAdapter` both legs) → producer, opening
|
||||
`channels/tunnel/direct` end to end: the open resolves on the
|
||||
consumer with the hub-allocated `channel_id`, the
|
||||
establisher-contributed `bound` field (Unit 1) survives the relay
|
||||
to the consumer reply, data flows both directions on the data
|
||||
channel (the hub never parses it), and hub-side disconnect /
|
||||
spoke-side close tears down both legs' channels.
|
||||
2. The mid-establishment disconnect window (ADR-051 §6): consumer
|
||||
leg dies between spoke open and pump spawn — the spoke channel is
|
||||
reclaimed when the pumps run (EOF cascade), both ledgers
|
||||
decremented, no leak.
|
||||
3. A companion `channels/tty/sub` relay run pins no regression on
|
||||
standard shapes.
|
||||
|
||||
Remaining bookkeeping for the final unit: ADR-042 amendment (landed
|
||||
with ADR-051 — the §Scope-note revision and the two mechanism
|
||||
supersessions), changelog + minor version bump (new pub API:
|
||||
`ChannelRelay`, `register_relay_openable`, the hub-leg template;
|
||||
`open_channel_with_reply` and `Establishment::with_reply_field*`
|
||||
landed with Units 1–2).
|
||||
|
||||
## Sequencing and non-goals
|
||||
|
||||
Order: Unit 1 → Unit 2 → Unit 3 (Unit 3's e2e asserts `bound` through
|
||||
the relay, so Units 1–2 are prerequisites). One release (minor bump),
|
||||
matching the review-007 precedent. Not in scope (extends the review's
|
||||
non-asks): the data-plane wire format (untouched — the relay rides
|
||||
`MpscSendStream`/`MpscRecvStream` and the existing pumps), any
|
||||
`channel/control` translation surface (the relay wrapper translates the
|
||||
open op; close/control ride the existing generic ops per leg — a hub
|
||||
wanting cross-leg control translation composes it downstream), and the
|
||||
alktunnels-side work (the bind-first establisher, the listen-op spec's
|
||||
`bound` documentation, ADR-007/008 implementation) which sequences
|
||||
AFTER these land.
|
||||
Order: Unit 1 → Unit 2 → Unit 3a → Unit 3b → Unit 3c (Unit 3a is the
|
||||
component; 3b the assembly; 3c the e2e gate asserting `bound` through
|
||||
the relay — Units 1–2 are prerequisites of 3a; 3b and 3c of each
|
||||
other). One release (minor bump), matching the review-007 precedent.
|
||||
Not in scope (extends the review's non-asks): the data-plane wire
|
||||
format (untouched — the relay rides `MpscSendStream`/`MpscRecvStream`
|
||||
and `pump_bidi`; no header rewrite exists — ids are per-connection,
|
||||
ADR-051), any `channel/control` translation surface (close propagates
|
||||
through the EOF cascade; control is per-leg, OQ-39 — a hub wanting
|
||||
cross-leg control translation composes it downstream), the Pub-typed
|
||||
open path (rejected loudly; C-08 blocker), and the alktunnels-side
|
||||
work (the bind-first establisher, the listen-op spec's `bound`
|
||||
documentation, ADR-007/008 implementation) which sequences AFTER these
|
||||
land.
|
||||
|
||||
Review-008 status updates when units land: mark U-1/U-2 resolved with
|
||||
commit refs (the review-007 Status pattern), and correct the U-2 line
|
||||
|
||||
Reference in new issue
Block a user