docs: review 008 — graduation upstream asks (flavor-form discovery derivation; establisher reply projection)

Filed from the alktunnels graduation spec work (ADRs 007/008):
U-1 extends ADR-047's op-name → ALPN derivation to flavor-form open
op ids (channels/tunnel/direct, channels/tunnel/forwarded) so the
marker survives discovery and hub relay wraps them as channels, not
plain forwarding stubs; U-2 (ADR-049 amendment 3 sketch) gives the
establisher a path to contribute additive reply fields (the bind-first
listen 'bound' address — BND.ADDR fidelity). Both additive; data
plane untouched. alktunnels implementation is sequenced after these
land.
This commit is contained in:
2026-09-16 08:32:19 +00:00
parent 098090d564
commit 3b36b4074f
@@ -0,0 +1,193 @@
# Review 008 — Graduation Upstream Asks (from the alktunnels graduation)
## Status
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.** 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.** 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)