- 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 Follows6b20924(src doc comments) andcd0afe0(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/
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 |
|
moderate | 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, since 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, because 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 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 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
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 thelistentoken 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): parsesTunnelForwardedParams(deny_unknown_fields+ the landed positive-listenserde pin), accepts the channelBiStream(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'sJoinHandleinline (R-02, since the forwarded channel's lifetime tracks it; the wrapper's tracking reaps on handler exit).- No establisher,
register_openableexactly (the forwarded channel needs no establishment; the data plane starts immediately, ADR-008 §3). The wrapper'schannel_idallocation 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); thepeerparam 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 bytunnel:open; probed with atunnel: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 intunnels/bindfirst-producer's per-accept isolation). - Reply reader lands in
src/params.rsaslisten_reply_fields(the lenient, infallible session-surface projection) DELEGATING to the landedparse_listen_reply(the loud typed viewer): unknown extra reply fields tolerated (the additive posture; no deny-unknown gate on the reply), a malformed shape logstracing::error!loud and surfaces both-None(the session presentsOptionfields and never refuses construction over an informational field; useparse_listen_replydirectly when the loud typed distinction matters). BothOption, absent for dial/accept-first producers; the projection is unit-testable independent of the session type (this task's placement rationale), consumed byTunnelListener(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/ missingpeerfail 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(thebindfirst-producershape), so no harness changes are needed; the forwarded-op serving stub helpers (make_forwarded_stub_handleretc.) 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).