- src/channels/gate2_tests.rs (ADR-051 gates 6/7, review 008 gate 2): the full consumer -> hub (HubLegTemplate) -> spoke relay e2e — the open resolves with the hub-allocated channel_id, `bound` survives the relay, data flows both directions with a fake 8-byte chunk header riding verbatim (the hub never parses the data plane, ADR-034/035), spoke-side close cascades to clean reclaim on both legs; hub-side disconnect (the consumer's duplex end dropped via a killable transport proxy) tears down both legs with the ledger decremented; the mid-establishment window (ADR-051 §6) pinned with its two reclaim signals — the consumer leg reclaims at its own transport EOF, the spoke channel (allocated before the establisher replied) is the honest residual, reclaimed when the spoke-leg transport ends; the channels/tty/sub standard-shape companion pins no derivation regression. - src/channels/relay.rs: the relay's adopted producer-leg channel entry now reclaims when the relayed pump completes (teardown after pump_bidi) — the consumer-leg wrapper's teardown cannot see the producer leg's manager; the RelayPlan carries the spoke id for the reclaim. - Release bookkeeping: 0.7.1 -> 0.8.0, the CHANGELOG entry covering Units 1-3 (Establishment reply projection, open_channel_with_reply, flavor-form discovery derivation, ChannelRelay, HubLegTemplate, gate-2 harness); review 008 Status -> Resolved with the U-1/U-2 commit refs and the 955->945 errata note; ADR-051 Status -> all units landed + the §6 mid-establishment residual expanded to the two-reclaim-signal shape the gate pins; the stale "0.7.2" version mentions in ADR-047/049 corrected to 0.8.0 (the units land unreleased). Verification: cargo test 669 passed / 0 failed; clippy --all-targets -- -D warnings clean; fmt --check clean; cargo doc --no-deps clean; cargo check + clippy on wasm32-unknown-unknown clean; cargo publish --dry-run --allow-dirty passed.
519 lines
27 KiB
Markdown
519 lines
27 KiB
Markdown
# Review 008 — Graduation Upstream Asks (from the alktunnels graduation)
|
||
|
||
## Status
|
||
|
||
Resolved — all three remediation units landed in alkcall 0.8.0
|
||
(2026-09-18): Unit 1 `82ddddf` (U-2, ADR-049 amendment 3), Unit 2
|
||
`620180d` (U-1, ADR-047 amendment 3), Unit 3a `90c1820` (the in-tree
|
||
`ChannelRelay`, ADR-051), Unit 3b `4e52bd4` (the hub-leg install
|
||
template, ADR-051 §5), Unit 3c (the gate-2 e2e harness in
|
||
`src/channels/gate2_tests.rs` + the relay's producer-leg teardown +
|
||
this release's bookkeeping). Two notes on the as-filed text: the U-2
|
||
line citation is 945, not 955 (see Errata), and U-1's gate 2 landed as
|
||
an in-tree relay component plus a permanent e2e gate rather than a
|
||
test-only harness (the escalation the remediation plan pinned above).
|
||
Original findings below, retained as filed (verified against tree
|
||
`3b36b40`, 0.7.1).
|
||
|
||
> **Errata (post-landing):** U-2 cites the reply construction at
|
||
> `src/channels/operations.rs:955`; the cf-006/007 commit had already
|
||
> shifted it to 945 by filing time. The citation in the finding below
|
||
> is left as filed; the verified line is 945.
|
||
|
||
Open — filed 2026-09-16 from the alktunnels graduation spec work
|
||
(ADRs 007/008 at
|
||
`/workspace/@alkdev/alktunnels/docs/architecture/decisions/`;
|
||
research record at
|
||
`/workspace/@alkdev/alktunnels/docs/research/tunnels-graduation.md`).
|
||
Both asks are prerequisites for the alktunnels graduation
|
||
implementation (the direct op + the listen-metadata shape), which is
|
||
deliberately sequenced AFTER these land — alksocks (the first
|
||
consumer) composes against the graduated surface, so the discovery
|
||
and reply shapes must be final before it pins them. Verified against
|
||
alkcall tree @ HEAD, 0.7.1 (all code references below cite current
|
||
files/lines).
|
||
|
||
Findings continue the review numbering with prefix `U`.
|
||
|
||
## Scope
|
||
|
||
The two wire-adjacent surfaces the graduation's ADRs depend on:
|
||
(1) the open-op ↔ discovery relationship (alkcall ADR-047's op-name →
|
||
ALPN derivation and its relay reconstruction), which flavor-form open
|
||
op ids must survive; (2) the establishment → reply boundary
|
||
(alkcall ADR-049's wrapper), which a bind-first listen establisher
|
||
must be able to project metadata into. Both are additive; neither
|
||
changes the channels data-plane wire format (ADR-034/071 untouched).
|
||
|
||
## U-1: Flavor-form open-op ids in discovery derivation
|
||
|
||
**Finding.** *(Resolved — ADR-047 amendment 3, Unit 2 `620180d`; the
|
||
explicit `channel_open_alpn` field plus the last-segment derivation,
|
||
exactly the pinned (b)+(c) shape. Gate 2 landed as the in-tree relay +
|
||
`src/channels/gate2_tests.rs`, Unit 3.)* ADR-047 §1 pins the open-op
|
||
naming to exactly two shapes
|
||
per ALPN — `channels/<alpn>/sub` and `channels/<alpn>/pub` — and Gap
|
||
F's marker derivation (`rebuild_spec_for`,
|
||
`src/client/from_call.rs:209` + `derive_alpn_from_op_name`,
|
||
`src/client/from_call.rs:305`) parses exactly those suffixes to
|
||
reconstruct the `channel_open` marker after discovery
|
||
(`spec_to_json` serializes the marker as a boolean,
|
||
`src/registry/discovery.rs:244` — the ALPN string itself does not
|
||
survive the round trip). alktunnels' graduation pins two NEW
|
||
`Sub`-typed open ops on the existing `alk/tunnel` ALPN with
|
||
flavor-form op ids:
|
||
|
||
- `channels/tunnel/direct` (ADR-007 — dynamic-target egress; the
|
||
ssh `direct-tcpip` capability, scope `tunnel:direct`)
|
||
- `channels/tunnel/forwarded` (ADR-008 — accept-as-open toward the
|
||
listen-opener side; the ssh `forwarded-tcpip` capability, scope
|
||
`tunnel:forwarded`)
|
||
|
||
Runtime is NOT blocked today: registration sets the ALPN marker
|
||
explicitly (`with_channel_open`), dispatch never parses the op name
|
||
(`register_openable_with_establisher` accepts any spec name with a
|
||
marker — verified, `src/channels/operations.rs:596`), and the client
|
||
takes the ALPN as an argument (`open_channel(op_id, params, alpn)`).
|
||
The gap is **discovery + hub relay**: a flavor-form op id fails
|
||
`derive_alpn_from_op_name`'s suffix match, so `rebuild_spec_for`
|
||
rebuilds the spec WITHOUT the `channel_open` marker, so a hub
|
||
consuming through discovery (the `from_call` relay path, ADR-047 Gap
|
||
C — the alknodes re-produce shape) treats the open op as a plain
|
||
forwarding stub instead of wrapping it with relay machinery.
|
||
|
||
**Prior art in-tree.** `derive_alpn_from_op_name` already tolerates
|
||
multi-segment ALPNs (`segment.contains('/')` → used verbatim, so
|
||
`channels/x-y/sub` → `alk/x-y`); the shape is one suffix away from
|
||
flavor support.
|
||
|
||
**Requested change.** Extend the op-name convention (ADR-047
|
||
amendment): `channels/<alpn>/<flavor>` is a valid open-op name where
|
||
`<flavor>` is a bare path segment (no `/`); the derivation generalizes
|
||
from "strip `/sub` or `/pub`" to "strip the LAST segment when it is a
|
||
known op-type marker (`sub`, `pub`) or a flavor registered with an
|
||
explicit ALPN marker." Two viable implementation shapes, either
|
||
acceptable:
|
||
|
||
- **(a) Suffix-set extension** — `strip_suffix` against
|
||
`["/sub", "/pub"]` extended with a flavor allowlist threaded from
|
||
registration. Minimal, but couples discovery to a registry.
|
||
- **(b) Wire the flavor op's ALPN explicitly through discovery** —
|
||
add an optional `channel_open_alpn` string field to the
|
||
`services/schema` payload (`spec_to_json` emits the ALPN string
|
||
instead of `true` when the op name is not a standard `…/sub`/`…/pub`
|
||
shape; `rebuild_spec_for` prefers the explicit string). This also
|
||
future-proofs the boolean-only marker (Gap F) whose round-trip is
|
||
lossy by construction for any non-derivable name.
|
||
|
||
alktunnels ADR-002 Amendment 1 pins the convention sentence ("new
|
||
flavors are new op ids — additive; the `…/sub` op is never reused for
|
||
a different meaning"); alkcall's amendment should mirror it. The
|
||
boolean marker's wire meaning for standard-shape ops is unchanged.
|
||
|
||
**Why one-way-door timing:** the op ids are wire-stable from the
|
||
first consumer (alksocks). If discovery's derivation ships after
|
||
consumers exist, hubs deployed in between cannot relay the new ops —
|
||
the failure is silent (plain forwarding stub instead of channel
|
||
relay), which is the worst failure mode for a relay.
|
||
|
||
**Verification gates:**
|
||
|
||
1. `rebuild_spec_for("channels/tunnel/direct" summary)` reconstructs
|
||
the spec WITH `channel_open = alk/tunnel`.
|
||
2. A hub-relay round trip over a real channels connection: consumer →
|
||
hub (`from_call` + relay wrapper) → producer, opening
|
||
`channels/tunnel/direct` end to end, the hub forwarding the
|
||
channel (the ADR-042 relay path riding the reconstructed marker).
|
||
3. Old boolean-marker ops (`channels/tty/sub`) round-trip unchanged
|
||
(no regression on the existing derivation).
|
||
|
||
## U-2: Establisher → reply projection (`Establishment` contributing reply fields)
|
||
|
||
**Finding.** *(Resolved — ADR-049 amendment 3, Unit 1 `82ddddf`: the
|
||
`reply_fields` carrier, the `channel_id` reservation failing loudly,
|
||
`open_channel_with_reply` on the client.)* The open-op success reply
|
||
is constructed by the wrapper
|
||
and hardcoded to the channel id:
|
||
`ResponseEnvelope::ok(request_id, json!({ "channel_id": channel_id }))`
|
||
(`src/channels/operations.rs:955` in `run_open_wrapper`). ADR-049 §1
|
||
explicitly reserved the establishment phase as the wire's pre-data
|
||
moment and ADR-049 amendment 2 landed the typed-opaque plan
|
||
(`Establishment { plan }`) for the establisher → handler direction —
|
||
but the establisher has no path to contribute to the REPLY (the
|
||
establisher → opener direction). alktunnels ADR-008's bind-first
|
||
listen establisher needs exactly that: establishment ends at bind
|
||
time, and the observed OS-chosen bound address must ride the open-op
|
||
reply as an additive `"bound"` field (the SOCKS5 BIND reply#1
|
||
`BND.ADDR` fidelity ask; the alternative — an out-of-band query op —
|
||
is a new wire surface, strictly worse than an optional reply field).
|
||
|
||
**Shape note.** `#[non_exhaustive]` on `Establishment`
|
||
(ADR-049 amendment 2) anticipated exactly this carrier change. The
|
||
sketch:
|
||
|
||
- `Establishment` gains optional reply fields — e.g.
|
||
`Establishment::new(plan).with_reply_field("bound", json!({...}))`
|
||
or `reply_fields: Option<Map<String, Value>>` (bikeshed: builder
|
||
vs struct field; the map shape generalizes beyond `bound` without
|
||
a second amendment).
|
||
- The wrapper merges them into the success output AFTER reserving
|
||
`channel_id` (wrapper-owned key: an establisher-provided
|
||
`channel_id` key is an establisher bug — reject or
|
||
`handler_error`-class it loudly rather than shadowing).
|
||
- Absent fields → the reply is byte-identical to today's; no schema
|
||
change on the wrapper side (the output schema is the op's own
|
||
concern — alktunnels' listen-op spec documents `bound` as an
|
||
optional output field).
|
||
|
||
**Consumer-compat check:** `open_channel` extracts `channel_id` from
|
||
the response and ignores unknown fields (the client-side parse,
|
||
`src/channels/client.rs:932`'s contract: "respond `{ channel_id }` —
|
||
asserts the response carries a channel_id"), so old consumers are
|
||
unaffected by a NEW field; new consumers reading `bound` against an
|
||
OLD alkcall simply see the field absent — the additive posture
|
||
alktunnels ADR-008 §2 pins ("until upstream lands, the field is
|
||
simply absent").
|
||
|
||
**Requested change (ADR-049 amendment 3):** the reply projection as
|
||
sketched above, with the `channel_id` reservation documented.
|
||
|
||
**Verification gates:**
|
||
|
||
1. An establisher returning a reply field produces
|
||
`{ channel_id, bound }` on the wire; one returning none produces
|
||
`{ channel_id }` (byte-identical to today's).
|
||
2. An establisher attempting to set `channel_id` fails loudly (the
|
||
reservation holds).
|
||
3. The existing establishment tests (establisher-success pump
|
||
round-trip, no-establisher compat) pass unchanged.
|
||
|
||
## Non-asks (recorded to bound the review)
|
||
|
||
- **The channels data plane is untouched** — the forwarded op's data
|
||
plane is the existing `BiStream` + `pump_bidi`; the `bound` field
|
||
is call-plane JSON. ADR-034/071's one-way doors stay closed.
|
||
- **No new alkcall op types** — both new tunnel ops are `Sub`-typed
|
||
open ops (the existing `HandlerKind::Stream` wrapper path).
|
||
- **No identity/ACL changes** — the new scopes (`tunnel:direct`,
|
||
`tunnel:forwarded`) ride the existing op-spec `required_scopes`;
|
||
the per-call opener identity (CF-005/CF-006) is the only identity
|
||
surface the establisher/handler need (the forwarded op's `peer`
|
||
param is producer-asserted data, not transport truth — alkcall need
|
||
not validate it).
|
||
|
||
## References
|
||
|
||
- alktunnels ADR-007 (`docs/architecture/decisions/007-direct-open-op.md`),
|
||
ADR-008 (`008-listen-metadata-forwarded-op.md`) — the consumers of
|
||
both asks, with the full context and alternatives
|
||
- alktunnels ADR-002 Amendment 1 — the flavor-form op-id convention
|
||
sentence
|
||
- alkcall ADR-047 (open ops, Gap F, the derivation),
|
||
ADR-049 (establishment; amendment 2 — the plan payload whose
|
||
`#[non_exhaustive]` anticipates this carrier), ADR-042 (hub relay —
|
||
the relay path U-1's gate 2 exercises)
|
||
- alksocks `poc-bind-findings.md` (the two fidelity asks with
|
||
evidence; the upstream-ask ledger this review retires)
|
||
- Review 007 (the precedent for this filing: POC-driven upstream asks
|
||
before the dependent implementation)
|
||
|
||
---
|
||
|
||
# Remediation plan (adopted 2026-09-16 — findings verified, plan pinned)
|
||
|
||
Both findings verified against HEAD `3b36b40` (0.7.1) — all code
|
||
citations check out, with one line drift: U-2's reply construction is
|
||
`src/channels/operations.rs:945` (review cites 955; the cf-006/007
|
||
commit shifted the wrapper down ten lines). One review-scope deviation
|
||
from the as-filed text, adopted in planning:
|
||
|
||
- **U-1's gate 2 escalates from "test harness" to an in-tree relay
|
||
component.** The as-filed review treated ADR-042's relay wrapper as
|
||
consumer-side code (the ADR's §Scope note pins the implementation
|
||
downstream). The consumer decision is that the re-produce/relay shape
|
||
is general (alkhttp's fallback hub, alknodes, the vpn-like endgame all
|
||
compose it), and a real consumer now requires it — so ADR-042 is
|
||
amended: the relay implementation moves into alkcall as a reusable
|
||
component, and gate 2 becomes a genuine in-tree e2e.
|
||
|
||
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
|
||
path to `rebuild_spec_for` (both its consumers — `from_call`,
|
||
`op/register` — are wire-fed). The pinned shape: `spec_to_json_pub`
|
||
emits an explicit `channel_open_alpn` string alongside the boolean
|
||
when the op name is not a standard `…/sub`/`…/pub` shape (standard
|
||
shapes stay byte-identical); `rebuild_spec_for` prefers the explicit
|
||
string, else generalizes the derivation to strip the LAST segment
|
||
when the boolean marker is present (the marker is the gate, so any
|
||
bare-segment flavor works; `op/register` is fixed by the same
|
||
parser).
|
||
- **U-2 client read path: add the ergonomic accessor now** —
|
||
`open_channel_with_reply` returning the extra reply fields alongside
|
||
the streams; `open_channel` unchanged. Keeps alkcall's primary
|
||
surface intact while giving alktunnels' `TunnelListener::open` a
|
||
first-class way to read `bound`.
|
||
|
||
## Unit 1 — U-2: establisher reply projection (ADR-049 amendment 3)
|
||
|
||
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.
|
||
|
||
1. `Establishment` gains `reply_fields: Option<Map<String, Value>>` +
|
||
builder (`Establishment::new(plan).with_reply_field("bound",
|
||
json!({...}))`; also `with_reply_fields(map)` / `reply_fields()`
|
||
accessor). `#[non_exhaustive]` holds; additive, no break.
|
||
2. `run_open_wrapper`: on establisher success, merge
|
||
`reply_fields` into the success output AFTER reserving
|
||
`channel_id`. An establisher-supplied `channel_id` key is an
|
||
establisher bug — fail loudly as
|
||
`EstablishmentError::HandlerError` (reason `handler_error`, message
|
||
naming the reserved key), tearing the channel down (the wrapper's
|
||
existing failure path). No silent shadowing.
|
||
3. Client: `ChannelClient::open_channel_with_reply(operation_id, input,
|
||
alpn) -> Result<(u32, Value, MpscSendStream, MpscRecvStream),
|
||
ChannelOpenError>` — the full success output (the consumer reads
|
||
`bound` from it); `open_channel` delegates with the fields
|
||
discarded. Additive; no signature change.
|
||
4. Doc: ADR-049 amendment 3 (the projection, the `channel_id`
|
||
reservation, the accessor). The output schema remains the op's own
|
||
concern (alktunnels' listen-op spec documents `bound` as optional).
|
||
|
||
Verification gates (from the review, tightened):
|
||
|
||
- Establisher returning a reply field produces `{ channel_id, bound }`
|
||
on the wire; one returning none produces `{ channel_id }`
|
||
(byte-identical to today's — assert exact JSON).
|
||
- Establisher attempting `channel_id` → `channel:open_failed` with
|
||
reason `handler_error`; channel torn down; ledger decremented.
|
||
- The existing establishment tests (establisher-success pump
|
||
round-trip, plan-flow, no-establisher compat) pass unchanged.
|
||
- `open_channel_with_reply` returns the extra fields end-to-end over a
|
||
real channels connection; `open_channel` behavior unchanged.
|
||
|
||
## Unit 2 — U-1: flavor-form derivation + explicit ALPN field (ADR-047 amendment)
|
||
|
||
1. `derive_alpn_from_op_name` (`src/client/from_call.rs:305`)
|
||
generalizes from strip-`/sub`|`/pub` to strip the LAST path segment
|
||
(`rsplit_once('/')`), which is a strict superset of today's behavior
|
||
(standard two-suffix shapes derive identically; multi-segment ALPNs
|
||
survive the same way). The marker boolean remains the gate in
|
||
`rebuild_spec_for` — the derivation is only consulted for marked
|
||
ops, so plain ops named `channels/x/y` are unaffected (the
|
||
`channels/tty/query`-returns-None unit test moves from the function
|
||
to the marker-gated rebuild level).
|
||
2. `spec_to_json_pub` (`src/registry/discovery.rs:222`): when
|
||
`channel_open` is set and the op name is NOT a standard
|
||
`channels/<seg>/(sub|pub)` shape, emit `"channel_open": true` PLUS
|
||
`"channel_open_alpn": "<alpn>"`. Standard shapes keep the boolean
|
||
only — byte-stable for every existing consumer. The advertised
|
||
`operation_spec_schema` (`discovery.rs:159`) gains the optional
|
||
string property (schema-type widening, additive).
|
||
3. `rebuild_spec_for` (`from_call.rs:209`): prefer the explicit
|
||
`channel_open_alpn` string; else boolean → generalized derivation.
|
||
Covers both wire consumers (`from_call`, `op/register`) by
|
||
construction.
|
||
4. Doc: ADR-047 amendment — the convention sentence mirroring
|
||
alktunnels ADR-002 Amendment 1 (`channels/<alpn>/<flavor>` is a
|
||
valid open-op name, `<flavor>` a bare segment; flavors are additive,
|
||
the `…/sub` op is never reused), the explicit-field wire shape, and
|
||
the one-way-door note (the field joins the stable
|
||
`services/schema` payload; the boolean's meaning for standard-shape
|
||
ops is unchanged). Record the residual ambiguity: an ALPN segment
|
||
that itself collides with a flavor name (`channels/x/direct` where
|
||
the ALPN is `alk/x/direct`-shaped) is undecidable from the name
|
||
alone — the explicit field is the disambiguator.
|
||
|
||
Verification gates:
|
||
|
||
- Review gates 1 and 3 as unit/round-trip tests:
|
||
`rebuild_spec_for("channels/tunnel/direct" summary)` reconstructs
|
||
WITH `channel_open = alk/tunnel`; `channels/tty/sub` (boolean) and
|
||
`channels/custom/proto/sub` (multi-segment) round-trip unchanged;
|
||
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 3c.
|
||
|
||
## Unit 3 — the in-tree relay component (ADR-042 amendment, ADR-051) + gate 2 e2e
|
||
|
||
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:
|
||
|
||
### 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. 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 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)
|
||
|
||
Landed (2026-09-17): `src/channels/hub_leg.rs` — `HubLegImports`
|
||
(the `Clone` stash split by the marker) and `HubLegTemplate`
|
||
(the §5 composition exported as the `InstallChannelZero` hook;
|
||
`serving identity` via the CF-005 seam). One assembly rule the
|
||
planning sketch missed, surfaced by the template's gate e2e:
|
||
`from_call` discovers the spoke's bootstrap discovery ops like any
|
||
plain op, so the template skips
|
||
`registry::discovery::BOOTSTRAP_DISCOVERY_OPS` when re-registering
|
||
plain bundles — the template's own install (closed over the fork,
|
||
review 004 F-06) supersedes the imported copies. Unit 3c (the
|
||
gate-2 e2e) remains.
|
||
|
||
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 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
|
||
citation (955 → 945) in an errata note rather than silently editing the
|
||
as-filed text. |