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:
@@ -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)
|
||||
Reference in New Issue
Block a user