Files
alktunnels/tasks/tunnels/forwarded-receipt.md
T
glm-5.3-flash f315b0e0ca docs+tasks: ADR-008 Amendment 1 (correlation token); decompose the graduation into tasks/tunnels
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).
2026-09-18 14:26:21 +00:00

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
tunnels/bindfirst-params
medium medium component implementation
opener
forwarded-op
serving
adopt
correlation

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's channel_id allocation 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 the BiStream, invoke the injected assembly handler with the parsed params + the stream. The assembly correlates (via listen), adopts nothing (the WRAPPER owns this channel — the serving side allocated it; the assembly pumps the BiStream against its local halves via pump_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 ForwardedHandler receives the channel BiStream and 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_token when it is itself forwarding); the peer param is producer-asserted metadata, not transport truth (producer.md's posture — ACL trusts the grant, app treats peer as metadata). The registration's auth + 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 by tunnel: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 in tunnels/bindfirst-producer (cross-referenced, not duplicated here).

Acceptance Criteria

  • register_tunnel_forwarded_openable: no-establisher registration, scope gate tunnel: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_fields extraction (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 test green

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.