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

146 lines
6.3 KiB
Markdown

---
id: tunnels/forwarded-receipt
name: Forwarded-op serving side + TunnelSession adopt path (the opener half)
status: pending
depends_on: [tunnels/bindfirst-params]
scope: medium
risk: medium
impact: component
level: implementation
tags: [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)
```rust
/// 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)
```rust
/// 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.