- params.rs: OP_TUNNEL_FORWARDED / TUNNEL_FORWARDED_SCOPE; TunnelForwardedParams
{listen, substrate, peer} (deny-unknown; listen positive via schema minimum 1 +
a serde pin); tunnel_forwarded_spec() (Sub, External, alk/tunnel ALPN, scope
tunnel:forwarded, per-substrate if/then peer sub-schema, no establisher);
TunnelListenReply + parse_listen_reply (the additive bound/listen reply
projection — unknown reply fields tolerated, malformed ones fail loud)
- error.rs: TunnelError::MalformedReply (the viewer's fail-loud path)
- producer.rs: BindFirstPlan {listener: Arc<dyn Any + Send + Sync>, token: u32}
— the opaque assembly-typed listener + the correlation token; the mint
discipline pinned in docs (non-zero-seeded AtomicU32, per-open fresh,
per-producer unique, wraps acceptable)
- Wire-stable types pin first (the tunnels/params ordering); the establisher +
accept dispatcher + registration land with tunnels/bindfirst-producer
Verified: cargo test (105 default; 125 --all-features), clippy --all-targets
-D warnings (native + wasm32), fmt --check, wasm32-unknown-unknown check,
cargo doc --no-deps — all clean.
10 KiB
id, name, status, depends_on, scope, risk, impact, level, tags
| id | name | status | depends_on | scope | risk | impact | level | tags | |||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| tunnels/bindfirst-params | Bind-first params — ListenParams, the forwarded-op spec, the bind-first plan type (ADR-008 as amended) | completed | narrow | medium | component | implementation |
|
Description
Implement the bind-first wire surface's params half (ADR-008, as
amended 2026-09-18 — Amendment 1): the listen-open reply fields
(bound + listen), the forwarded-op params + spec, and the
bind-first plan payload type. Split from the establisher/pump task
(tunnels/bindfirst-producer) so the wire-stable types land and get
pinned first — the same ordering tunnels/params used for the base
op.
params.rs — constants + types
pub const OP_TUNNEL_FORWARDED: &str = "channels/tunnel/forwarded";
pub const TUNNEL_FORWARDED_SCOPE: &str = "tunnel:forwarded";
/// The bind-first listen-open reply (ADR-008 §2 as amended): the two
/// additive fields the establisher contributes. Extracted from the
/// full success reply (`{channel_id, bound?, listen?}`).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TunnelListenReply {
#[serde(skip_serializing_if = "Option::is_none")]
pub bound: Option<SubstrateAddr>,
#[serde(skip_serializing_if = "Option::is_none")]
pub listen: Option<u32>,
}
/// The forwarded-op input params (ADR-008 §3 as amended).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TunnelForwardedParams {
pub listen: u32,
pub substrate: Substrate,
pub peer: SubstrateAddr,
}
pub fn tunnel_forwarded_spec() -> OperationSpec
- The correlation token (Amendment 1):
listen(u32) — minted by the bind-first establisher, returned in the reply as thelistenfield, stamped into each forwarded open'slistenparam. NOT the listen channel's ID (the pre-amendment key is superseded — the serving-side handler never sees its own channel ID, and generic relays don't rewrite params; ADR-008 Amendment 1 has the full rationale).SubstrateAddris defined bytunnels/direct-op— same value shape ({host, port}/{path}); if that task has not landed, define it here and lettunnels/direct-opconsume it (either order works; the type lands once). tunnel_forwarded_spec()per wire.md §The Forwarded Op:Sub-typed,Visibility::External,ChannelOpenSpec::new(TUNNEL_ALPN), scope["tunnel:forwarded"](ADR-006 Amendment 1 — ingress, a separate grant), input schema pinning all three fields required (listeninteger > 0;peeran object with the per-substrate shape), output schema{channel_id: integer > 0}, description carried. No establisher on the forwarded op — the data plane starts immediately (ADR-008 §3).TunnelListenReplyis the consumer-side reader (the wire itself is plain JSON reply fields — additive, unknown fields ignored by old consumers; the struct is the typed view, not a deny-unknown wire gate on the reply — NOTE: the reply arrives as{channel_id, bound?, listen?}; parse it with a dedicated reply-viewing function that extracts the optional fields from the full output Value, not withdeny_unknown_fieldsdeserialization of the whole reply. The struct above is the projected pair; the parse helper handleschannel_id's presence).
The bind-first plan type (producer.rs or a small shared module)
/// The bind-first establisher's plan payload: the listener handle
/// (typed-opaque to the pump via downcast) + the correlation token
/// the accept dispatcher stamps into forwarded opens (ADR-008
/// Amendment 1 — the handler cannot learn its own channel ID, so the
/// establisher-minted token is the correlation key).
pub struct BindFirstPlan {
pub listener: TargetHandle, // the listener's halves? — NO: see the shape note
pub token: u32,
}
- Shape note (decide during implementation, document the choice):
the listener is NOT
TargetHandlehalves — a listener is accepted FROM, not pumped. The plan carries whatever the assembly'sBindFnproduced (opaque to the protocol), plus the token. Honest shape:pub struct BindFirstPlan { pub listener: Arc<dyn Any + Send + Sync>, pub token: u32 }— the accept dispatcher downcasts theAnyto the assembly's listener type via a type-erased accept closure (the same injection pattern asDialFn/AcceptFn). The ADR's "listener handle" in the plan is assembly-typed; the protocol seesBindFirstPlan { inner, token }. - The token mint:
std::sync::atomic::AtomicU32counter seeded non-zero (e.g. 1) inside the establisher closure state; wraps are acceptable (u32, per-producer uniqueness is all that's required — ADR-008 Amendment 1's token-space note; a wrap collision requires 2^32 concurrent opens on one producer). - Wire-stability note for the implementer:
listen's u32 type and field name, the forwarded params field names, and the op id are one-way doors (ADR-008 Consequences).deny_unknown_fieldsonTunnelForwardedParams(loud rejection).
Tests (unit, params.rs)
TunnelForwardedParamsround-trip per substrate; unknown-field and missing-field rejection;listen: 0rejected (schema minimum 1 + serde-level pin).TunnelListenReplyprojected parse: full reply{channel_id: 7, bound: {...}, listen: 918}→ both fields; bare{channel_id: 7}→ bothNone(accept-first/dial shape); unknown extra reply fields tolerated (the additive posture — the reply viewer must not deny unknown fields).- Spec-shape conformance: op id, Sub type, scope, schema structure (all three required; peer object), ALPN marker, description.
- No forwarded establisher: the registration test lands in
tunnels/bindfirst-producer(this task pins the spec shape only).
Acceptance Criteria
OP_TUNNEL_FORWARDED/TUNNEL_FORWARDED_SCOPEconstants;tunnel_forwarded_spec()matches wire.md §The Forwarded OpTunnelForwardedParams(deny-unknown,listen: u32 > 0) + the reply-projection viewer round-trip the pinned shapesBindFirstPlan(opaque listener + token) defined; the token mint documented (per-producer unique, per-open fresh)- Unit tests green; wasm32 check passes (pure protocol types)
- Clippy/fmt clean
References
- docs/architecture/decisions/008-listen-metadata-forwarded-op.md
(§2 as amended — the
listenreply field; §3 as amended — the forwarded params; Amendment 1 — the token rationale) - docs/architecture/wire.md §The Listen Open Op's Reply + §The Forwarded Op
- tasks/tunnels/params.md (the ordering precedent: wire types first)
- tasks/tunnels/direct-op.md (
SubstrateAddr— shared type)
Notes
SubstrateAddrhad already landed (viatunnels/direct-op) — consumed, not redefined (the "either order works" branch resolved to the consuming side).- The reply viewer is
parse_listen_reply(&Value) -> Result<TunnelListenReply, TunnelError>: parses the FULL success output (theopen_channel_with_replyshape), tolerates unknown extra reply fields (the additive posture — no deny-unknown gate on the reply), requires a positivechannel_id(its absence/zero = not an open reply — loud), and fails loud on malformedbound(not substrate-shaped) orlisten(not a positive u32) via the newTunnelError::MalformedReplyvariant (error.rs).nullfields parse asNone(defensive; a well-formed reply omits them). TunnelForwardedParams.listencarries a serde-levelpositive_u32pin behind the schema'sminimum: 1(the task's "listen: 0 rejected (schema minimum 1 + serde-level pin)" — both gates, test-pinned).- The
peersub-schema follows the direct op's if/then precedent (tcp/udp →{host, port}withadditionalProperties: false+maximum: 65535; unix →{path}), structural assertions per the no-jsonschema-dep precedent. The untagged-peer lenient-extra-fields asymmetry carries over from the direct op (schema = the shape gate; serde = the backstop) — documented in the test. BindFirstPlanlives inproducer.rs(the plan-payload family, next toTargetHandle):{ listener: Arc<dyn Any + Send + Sync>, token: u32 }per the honest shape — the listener is NOTTargetHandlehalves (a listener is accepted FROM, not pumped); the accept dispatcher downcasts via a type-erased accept closure. The token-mint discipline (AtomicU32 seeded non-zero, per-open fresh, per-producer unique, wraps acceptable) is pinned in the type's doc — the establisher that implements it lands withtunnels/bindfirst-producer.- Custom
Debugimpls (matchingTargetHandle's precedent): the listener renders<opaque>, the token renders plainly. - Tests: 10 new unit tests in params.rs (forwarded round-trips per
substrate, unknown/missing-field rejection,
listen: 0serde rejection, full/bare/extra-field reply projection, malformed-reply rejection, spec conformance, peer-schema structure, the parse-backstop asymmetry) + theBindFirstPlandowncast-through-ChannelPlantest in producer.rs (incl. the wrong-type downcast failure).
Summary
Implemented the bind-first wire surface's params half per ADR-008 as
amended: params.rs — OP_TUNNEL_FORWARDED / TUNNEL_FORWARDED_ SCOPE constants; TunnelForwardedParams {listen, substrate, peer}
(deny-unknown, listen positive via schema minimum: 1 + the serde
pin); tunnel_forwarded_spec() (Sub-typed, External,
ChannelOpenSpec::new(TUNNEL_ALPN), scope ["tunnel:forwarded"],
per-substrate if/then peer sub-schema, output
{channel_id: integer > 0}, description, no establisher);
TunnelListenReply + parse_listen_reply (the projected reply
viewer — unknown extra fields tolerated, channel_id presence
required, malformed fields fail loud via TunnelError:: MalformedReply). producer.rs — BindFirstPlan {listener: Arc<dyn Any + Send + Sync>, token: u32} with the mint discipline
pinned in docs (non-zero-seeded AtomicU32, per-open fresh,
per-producer unique, wraps acceptable). Re-exported through
lib.rs; the establisher/dispatcher/registration that consume these
types land with tunnels/bindfirst-producer.
Verified: cargo test green (105 default; 125 with --all- features), clippy --all-targets -D warnings clean (native +
wasm32), fmt clean, wasm32-unknown-unknown check passes (pure
protocol types — the default crate stays wasm-clean), cargo doc --no-deps clean. CHANGELOG updated (Unreleased → Added).