Files
alktunnels/tasks/tunnels/forwarded-receipt.md
T
glm-5.3-flash 0ceecf582b docs: remove em dashes from repo prose and forbid them going forward
- Rewrote the remaining em dashes outside docs/reviews/ (preserved as
  historical record, matching the alktty precedent): tests/ (313),
  fuzz/ (34 plus one shell comment), tasks/ (671 plus 12 kept in
  fences), docs/research + sdd_process + plans (525 plus 25 in
  fences), docs/architecture/overview.md fence labels (13), README,
  CHANGELOG, AGENTS.md, fuzz/README.md (176), and the
  .opencode/agents prompts (110)
- Rewrote fenced content too: ASCII tree and module-map label
  separators, Rust snippet comments, and template placeholders; wire
  data, JSON/BAST examples, and transcripts survive verbatim
- Rewrote 6 assert/expect message literals in tests/ and 2 format
  strings in fuzz/shared/src; no test data changed
- Added Project Convention 18: no em dashes in prose (doc comments,
  markdown including docs/ and tasks/, agent prompts, commit
  messages); fence content rule, en-dash clause rule, and the
  no-spaced-hyphen prohibition follow alktty's convention 15

Follows 6b20924 (src doc comments) and cd0afe0 (docs/architecture).

Verification: cargo test (141 pass); cargo clippy --all-targets -D
warnings; cargo fmt --check; cargo doc --no-deps; wasm check and
clippy for wasm32-unknown-unknown with -D warnings; em dash grep over
the repo returns matches only in docs/reviews/
2026-11-03 14:12:46 +00:00

12 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) completed
tunnels/bindfirst-params
moderate 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, since 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, because 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 drops the stream and returns (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

  • ForwardedHandler (the injected assembly handler): Arc<dyn Fn(TunnelForwardedParams, BiStream) -> tokio::task::JoinHandle<()> + Send + Sync>, the correlation material the opener-side handler yields per forwarded open. The assembly owns the pump (the honest split: the local halves, the SOCKS5 BIND client's socket, are assembly state), correlating via the listen token against its own waiting-listener map; the protocol provides the op + params, not the wait state machine (ADR-008 §3's posture, realized).
  • make_forwarded_op_handler (the built-in wrapper): parses TunnelForwardedParams (deny_unknown_fields + the landed positive-listen serde pin), accepts the channel BiStream (yield-once; accepting BEFORE the assembly handler runs keeps the R-02 accepted-flag telemetry truthful: a parse-gated early return never accepted, which the wrapper's birth-teardown hint reports), then awaits the injected handler's JoinHandle inline (R-02, since the forwarded channel's lifetime tracks it; the wrapper's tracking reaps on handler exit).
  • No establisher, register_openable exactly (the forwarded channel needs no establishment; the data plane starts immediately, ADR-008 §3). The wrapper's channel_id allocation IS the reply (the producer adopts it): nothing to implement, upstream machinery.
  • Identity posture (upstream machinery, test-pinned): the opener-side gate sees the PRODUCER's per-call identity (CF-005 precedence: payload token > ServingConfig.identity > transport); the peer param is producer-asserted metadata, not transport truth. Nothing implemented here; the FORBIDDEN probe pins the gate runs (identity without the grant → FORBIDDEN with the missing scope named, never implied by tunnel:open; probed with a tunnel:open-only identity).
  • Correlation-miss semantics (documented + test-pinned): the listen-token miss is the ASSEMBLY's rejection (it holds the map): the injected handler's error path drops the stream and returns; teardown-at-birth reaps the forwarded channel. The producer's accept loop drops the accepted connection on the open's failure (the shapes mirror; pinned producer-side in tunnels/bindfirst-producer's per-accept isolation).
  • Reply reader lands in src/params.rs as listen_reply_fields (the lenient, infallible session-surface projection) DELEGATING to the landed parse_listen_reply (the loud typed viewer): unknown extra reply fields tolerated (the additive posture; no deny-unknown gate on the reply), a malformed shape logs tracing::error! loud and surfaces both-None (the session presents Option fields and never refuses construction over an informational field; use parse_listen_reply directly when the loud typed distinction matters). Both Option, absent for dial/accept-first producers; the projection is unit-testable independent of the session type (this task's placement rationale), consumed by TunnelListener (tunnels/tunnel-listener, pending).
  • Test-data-plane shape (the harness): the e2e drives the forwarded channel through BOTH pumps: the opener-side assembly pump pops correlated duplex halves for the token (pushed BEFORE the forwarded open, the waiting-session posture; a late push is the miss shape, probed separately), and the producer-side test adopts the opener-allocated ID and pumps it against a driving echo duplex, then round-trips bytes from the client end through the whole path (assembly pump → forwarded channel → adopted pump → driving echo → back). The malformed-params probes pin the no-phantom property at both gates: listen: 0 / missing peer fail the registry's schema (INVALID_INPUT, no channel ever); an unknown top-level field passes the schema (additive posture) but fails the serde parse, so the channel (whose ID the reply already carried) reaps promptly.
  • Harness note: the suite reuses the existing harness's wire_with + Topology.consumer_registry (the bindfirst-producer shape), so no harness changes are needed; the forwarded-op serving stub helpers (make_forwarded_stub_handler etc.) stay as-is (they remain the producer-leg stub stand-in; the real opener-side registration is this task's, exercised directly).

Summary

Implemented the forwarded op's SERVING side (the listen-opener half, ADR-008 §3 as amended) per the task: src/producer.rs gained ForwardedHandler (the injected assembly handler: parsed params + channel BiStream; the assembly owns the pump, correlating by the listen token against its waiting-listener bookkeeping); make_forwarded_op_handler (the built-in no-establisher wrapper: deny-unknown params parse, so a malformed open fails loud → early return → teardown-at-birth reaps the channel, no phantom; accept_bi FIRST per R-02 telemetry; the injected handler's JoinHandle awaited inline); register_tunnel_forwarded_openable (register_openable, no establisher, the data plane starts immediately; scope gate tunnel:forwarded via tunnel_forwarded_spec). src/params.rs gained listen_reply_fields (the lenient, infallible reply projection: unknown extra fields tolerated, a malformed shape logs loud and surfaces both-None; shared with TunnelListener), delegating to the landed parse_listen_reply. Re-exported through lib.rs.

Tests: 4 new unit tests in params.rs (full/bare/extra-field/malformed reply projection) + 4 integration tests in tests/producer_forwarded.rs (the serving-half e2e round-trip gate: reply projection → token correlation → forwarded open → byte round-trip through the assembly pump + the producer-side adopted pump; the FORBIDDEN scope gate: tunnel:open-only identity, no channel allocated, handler never ran; malformed params: both gates pinned loud, the serde-gated shape tears down at birth; the correlation-miss teardown: the assembly's rejection, drop the stream, the channel reaps, the invocation witnessed). Verified: cargo test green (126 default; 146 --all-features), clippy --all-targets -D warnings clean (native + wasm32), fmt clean, wasm32-unknown-unknown check passes (pure protocol code; the default crate stays wasm-clean), cargo doc --no-deps clean. CHANGELOG updated (Unreleased → Added).