Files
alktunnels/tasks/tunnels/bindfirst-params.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

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
wire
params
bind-first
forwarded-op
correlation-token

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 and returned in the reply as the listen field, stamped into each forwarded open's listen param. 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). SubstrateAddr is defined by tunnels/direct-op, the same value shape ({host, port} / {path}); if that task has not landed, define it here and let tunnels/direct-op consume 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 (listen integer > 0; peer an object with the per-substrate shape), output schema {channel_id: integer > 0}, description carried. No establisher on the forwarded op, because the data plane starts immediately (ADR-008 §3).
  • TunnelListenReply is the consumer-side reader (the wire itself is plain JSON reply fields: additive, with 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 with deny_unknown_fields deserialization of the whole reply. The struct above is the projected pair; the parse helper handles channel_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 TargetHandle halves, since a listener is accepted FROM, not pumped. The plan carries whatever the assembly's BindFn produced (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 the Any to the assembly's listener type via a type-erased accept closure (the same injection pattern as DialFn/AcceptFn). The ADR's "listener handle" in the plan is assembly-typed; the protocol sees BindFirstPlan { inner, token }.
  • The token mint: std::sync::atomic::AtomicU32 counter seeded non-zero (e.g. 1) inside the establisher closure state; wraps are acceptable (u32; per-producer uniqueness is all that's required, per 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_fields on TunnelForwardedParams (loud rejection).

Tests (unit, params.rs)

  • TunnelForwardedParams round-trip per substrate; unknown-field and missing-field rejection; listen: 0 rejected (schema minimum 1 + serde-level pin).
  • TunnelListenReply projected parse: full reply {channel_id: 7, bound: {...}, listen: 918} → both fields; bare {channel_id: 7} → both None (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_SCOPE constants; tunnel_forwarded_spec() matches wire.md §The Forwarded Op
  • TunnelForwardedParams (deny-unknown, listen: u32 > 0) + the reply-projection viewer round-trip the pinned shapes
  • BindFirstPlan (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 listen reply 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, the shared type)

Notes

  • SubstrateAddr had already landed (via tunnels/direct-op), so it is 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 (the open_channel_with_reply shape), tolerates unknown extra reply fields (the additive posture: no deny-unknown gate on the reply), requires a positive channel_id (its absence/zero is not an open reply and fails loud), and fails loud on malformed bound (not substrate-shaped) or listen (not a positive u32) via the new TunnelError::MalformedReply variant (error.rs). null fields parse as None (defensive; a well-formed reply omits them).
  • TunnelForwardedParams.listen carries a serde-level positive_u32 pin behind the schema's minimum: 1 (the task's "listen: 0 rejected (schema minimum 1 + serde-level pin)": both gates, test-pinned).
  • The peer sub-schema follows the direct op's if/then precedent (tcp/udp → {host, port} with additionalProperties: 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.
  • BindFirstPlan lives in producer.rs (the plan-payload family, next to TargetHandle): { listener: Arc<dyn Any + Send + Sync>, token: u32 } per the honest shape, with the listener NOT TargetHandle halves (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 with tunnels/bindfirst-producer.
  • Custom Debug impls (matching TargetHandle'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: 0 serde rejection, full/bare/extra-field reply projection, malformed-reply rejection, spec conformance, peer-schema structure, the parse-backstop asymmetry) + the BindFirstPlan downcast-through-ChannelPlan test 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 gained the 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 defines 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, so the default crate stays wasm-clean), cargo doc --no-deps clean. CHANGELOG updated (Unreleased → Added).