Verification pass against the landed alkcall 0.8.0 surface surfaced one
design gap: ADR-008 §3 pinned the forwarded op's correlation key as the
listen channel's ID, but (1) the serving-side OpenHandler/establisher
surface never exposes the allocated channel ID, and (2) the in-tree
generic ChannelRelay forwards open-op params untouched (it rewrites
only the reply's channel_id), so a spoke's forwarded open would carry
an id the end opener never allocated. Amended (wire still unconsumed —
alksocks is docs-only — so pre-consumer and legal):
- ADR-008 Amendment 1: the bind-first establisher mints a fresh u32
correlation token per open — carried in the plan (for the accept
dispatcher) and contributed as the additive `listen` reply field
(the same projection carrier `bound` rides); the forwarded op's
params rename `channel` -> `listen` with token semantics. Tokens
ride reply fields + params through hubs untouched (relay-transparent
by construction, alkcall ADR-051). Consumer-minted tokens and a new
upstream handler surface were considered and rejected (rationale in
the amendment).
- Spec docs synced: wire.md (the `bound`+`listen` reply section, the
forwarded params), producer.md (bind-first shape + forwarded op),
consumer.md (TunnelListener gains `listen()`; correlation registry
posture), overview.md, OQ ledger (OQ-TN-16 updated),
tunnels-graduation.md.
Task decomposition (tasks/tunnels/, Phase 2 of the graduation — the
alkcall prerequisites are gone):
- direct-op — params/spec/establisher/open_direct (ADR-007)
- bindfirst-params — forwarded-op spec, TunnelListenReply, the
BindFirstPlan + token mint (wire types first, the params.md
ordering precedent)
- bindfirst-producer — bind-first establisher, accept dispatcher,
register_tunnel_bindfirst_openable
- forwarded-receipt — the opener-side serving op + reply-field reader
- tunnel-listener — the listen-flow consumer session (ADR-008 §4)
- local-bind-halves — the `local` TCP bind + accept-pairs loop,
unix parity decision, direct-op DialFn reuse pin
Dependency order: direct-op and bindfirst-params are independent
roots; bindfirst-producer -> {bindfirst-params}; forwarded-receipt ->
bindfirst-params; tunnel-listener -> {bindfirst-params,
forwarded-receipt}; local-bind-halves -> bindfirst-producer.
Verification: cargo test 71 passed; clippy --all-targets -D warnings;
fmt --check; wasm32-unknown-unknown check + clippy clean (docs + task
files only — no code change).
6.3 KiB
6.3 KiB
id, name, status, depends_on, scope, risk, impact, level, tags
| id | name | status | depends_on | scope | risk | impact | level | tags | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| tunnels/forwarded-receipt | Forwarded-op serving side + TunnelSession adopt path (the opener half) | pending |
|
medium | medium | component | implementation |
|
Description
Implement the forwarded op's SERVING side (the listen-opener half,
ADR-008 §3): register_tunnel_forwarded_openable — the
register_openable shape (no establisher; the data plane starts
immediately), scope-gated tunnel:forwarded, whose handler parses
TunnelForwardedParams, hands the correlation material to the
assembly, accepts the BiStream, and presents it for pumping against
the local session's halves. Plus the typed consumer surface for
reading the listen reply (bound/listen extraction, shared with
tunnels/tunnel-listener).
The serving registration (opener side)
/// The correlation material the opener-side handler yields per
/// forwarded open: the parsed params + the channel's BiStream
/// accessor. The assembly correlates (token → its waiting listener
/// session bookkeeping) and drives the pump against the local
/// halves. The protocol provides the op + params, not the wait
/// state machine (producer.md §The Forwarded Op).
pub type ForwardedHandler = Arc<
dyn Fn(TunnelForwardedParams, BiStream) -> tokio::task::JoinHandle<()>
+ Send + Sync,
>;
pub fn make_forwarded_op_handler(handler: ForwardedHandler) -> OpenHandler
pub fn register_tunnel_forwarded_openable(
core: &ChannelCore,
on_registry: &Arc<OperationRegistry>,
auth: AuthContext,
handler: ForwardedHandler,
) -> Result<(), String>
- No establisher (
register_openable— the forwarded channel needs no establishment; its data plane starts immediately — ADR-008 §3). The wrapper'schannel_idallocation IS the reply (the producer adopts it). - The built-in handler shape: parse
TunnelForwardedParams(deny_unknown_fields; a malformed open → the handler logs + returns early — teardown-at-birth is correct for a malformed ingress), accept theBiStream, invoke the injected assembly handler with the parsed params + the stream. The assembly correlates (vialisten), adopts nothing (the WRAPPER owns this channel — the serving side allocated it; the assembly pumps the BiStream against its local halves viapump_bidi, and the wrapper's R-02 tracking reaps on handler exit). - Pump responsibility split (the honest-shape decision to make and
document): the injected
ForwardedHandlerreceives the channelBiStreamand the params — the ASSEMBLY owns the pump (pump_bidi(forwarded_bidi, local_read, local_write)), because the local halves are assembly state (the SOCKS5 BIND client's socket). The protocol-side built-in handler only bridges wrapper→assembly. Await the injected handler's JoinHandle inline (R-02 — the forwarded channel's lifetime tracks it). - Identity: the opener-side gate sees the PRODUCER's per-call
identity (CF-005 precedence — the producer may carry
auth_tokenwhen it is itself forwarding); thepeerparam is producer-asserted metadata, not transport truth (producer.md's posture — ACL trusts the grant, app treatspeeras metadata). The registration'sauth+ the per-call overlay handle this automatically (upstream machinery); nothing to implement, but the test suite pins it. - The
listen-token correlation failure (no matching session) is the ASSEMBLY's rejection (it holds the map): the injected handler's error path — drop the stream, return (teardown-at-birth reaps the channel). Document that a correlation miss tears the forwarded channel down; the producer's accept loop drops the accepted connection on the open's failure (the shapes mirror).
The typed reply reader (shared with TunnelListener)
/// Extract the bind-first reply fields from an open-op success reply
/// (the full output Value — `channel_id` + optional `bound`/`listen`).
pub fn listen_reply_fields(reply: &Value) -> TunnelListenReply
- Tolerates unknown fields (the additive posture); both fields
Option(absent for dial/accept-first producers). - Lands here (not in
tunnels/tunnel-listener) so the projection logic is unit-testable independent of the session type.
Tests
- Serving-side e2e with the harness's producer-leg stub: an open with
valid params + scope → handler invoked with the parsed params +
the stream; the assembly's test handler pumps against local
duplex halves → byte round-trip. (The full bind-first flow e2e
lives in
tunnels/bindfirst-producer; this task's e2e is the serving half standalone.) - Scope gate: identity without
tunnel:forwarded→ FORBIDDEN (never implied bytunnel:open/tunnel:direct). - Malformed params (missing
peer,listen: 0, unknown field) → handler logs + early return → the channel tears down (no phantom). - The reply reader: full/empty/extra-field replies parse per the additive posture (unit).
- Old-opener posture is upstream's registry lookup (
NOT_FOUND) — pin it in the producer-side test intunnels/bindfirst-producer(cross-referenced, not duplicated here).
Acceptance Criteria
register_tunnel_forwarded_openable: no-establisher registration, scope gatetunnel:forwarded, injected assembly handler receives parsed params + BiStream- The injected handler's JoinHandle is awaited inline (R-02); correlation-miss teardown semantics documented + tested
listen_reply_fieldsextraction (additive-tolerant)- Integration tests: serving e2e round-trip, FORBIDDEN gate, malformed-params teardown; unit tests for the reply reader
- Clippy/fmt clean; wasm32 check passes
cargo testgreen
References
- docs/architecture/decisions/008-listen-metadata-forwarded-op.md (§3 the forwarded op — the opener-side serving shape; Amendment 1)
- docs/architecture/producer.md §The Forwarded Op; consumer.md §The Listen Flow; wire.md §The Forwarded Op
- docs/architecture/decisions/006-access-control-posture.md Amendment 1
- tasks/tunnels/bindfirst-params.md (the types); tasks/tunnels/bindfirst-producer.md (the caller half — the tests compose)
Notes
Agent fills during implementation.
Summary
Agent fills this on completion.