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).
146 lines
6.3 KiB
Markdown
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. |