Renames the written-usage rule into practice across the contract surface: docs/ (architecture, research, plans, sdd), tasks/ (all 43 task files, taskgraph validate passes), AGENTS.md, README.md, CHANGELOG.md, and the .opencode/agents/ prompts. Every replacement is a sense-preserving rewrite (colons, semicolons, commas, parentheses, sentence splits, varied), never a mechanical swap; no spaced hyphens were substituted. Enum fields, frontmatter, tables, wire data, and cited identifiers survive verbatim; en dashes inside numeric ranges keep their meaning. Verification: taskgraph validate (43 tasks), repo grep clean (single exempt hit: the verification command's own search pattern in AGENTS.md), cargo test, clippy --all-targets -D warnings, fmt --check, doc all green.
5.8 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| w4-router-collision | Wave 4: ListenRouter token-collision pre-check (wave-3 gate booked item 4) | completed |
|
single | low | isolated | implementation |
Description
Close the wave-3 gate's booked finding item 4
(review-wave-3 §Booked findings): a defensive ListenRouter
token-collision pre-check. register silently replaces on token
collision today; safety rests on the fabric's ADR-008 mint discipline
(collision "can't happen" upstream). The gate's disposition: a
fail-closed decline-the-listen-on-collision is a cheap wave-4
addition, following the same posture the gate landed inline for
duplicate
tunnel/forwarded op registration (install_forwarded_surface now
refuses with AdapterError::ListenRegistration instead of trusting
the silent-replace).
- The pre-check: a
registeron an already-held token refuses (the listen declines fail-closed) instead of silently replacing the held route, so a collided mint can never steal a live adapter's arrival routes. - The error surface: reuse/extend the
AdapterError::ListenRegistrationclass (decide + record, with a distinct variant only if the gate's variant can't carry the distinction honestly). - Wire row(s): the collision constructible (two registers on one token, with the test forcing what the fabric's discipline prevents) → the second refuses, the first route still serves (the steal is impossible, row-pinned); the existing arrival/cancel/teardown rows stay green.
Acceptance Criteria
ListenRouter::registerrefuses on token collision (fail-closed, no silent replace); the error surface decided + recorded- Wire row(s): the second register refuses, the first route still serves; existing rows green
cargo test(+ per-feature +--all-features), clippy ×2 (-D warnings), fmt,cargo doc --no-depspass
References
- tasks/implementation/review-wave-3.md §Booked findings item 4 (and
§Minor issues, the
install_forwarded_surfacepre-check this mirrors) - src/tunnel_map.rs (the
ListenRouter), src/error.rs (theAdapterErrorclass), docs/architecture/decisions/ (the fabric's ADR-008 mint discipline, the upstream guarantee the pre-check defends in depth against)
Notes
Implementation (2026-10-29)
ListenRouter::register(src/tunnel_map.rs) now returnsResult<(), String>: a register on an already-held token returnsErrand never replaces the held route, so a collided mint can never steal a live adapter's arrival routes. The refusal detail carries the token + the steal posture.- Error surface (decided + recorded): the refusal surfaces as the
request-time decline disposition (
RequestDenied, reasontoken-collision), not a distinctAdapterErrorvariant. The deciding fact: a global-request hook'sErris the taxonomy's only route into the request surface, and russh 0.65 propagates it with?out of the packet loop, which kills the whole session, while the vanilla contract for a refused listen isRequestDenied.AdapterError::ListenRegistrationhonestly carries the construction-time class it always did (the doc in src/error.rs records the wire-time sibling); the reuse is the shared fail-closed registration posture, no new variant needed. listen_request_dispatch(src/handler.rs): the register moved before the&mut u32out-param write (a refused listen never writes the bound port); on refusal the path releases the listener cap, emitsdecline_global_request(row, "token-collision")(the uniform vocabulary, closed, with no dynamic fields), and tears the minted fabric listener down immediately in a spawned task (TunnelListener::close, the cancel path's mechanism; a refused registration never left a half state).- Wire row (
tests/tunnel_mapping.rsa_collided_mint_is_refused_and_never_steals_the_live_route): the fabric double gained a mint-strategy construct (wire_fabric_with_listen_mint+MintStrategy::WithCollisions, where the first N mints return a fixed token, then the ADR-008 counter continues; the test forces exactly what the fabric's discipline prevents). The row pins, against a 2-count listener ceiling (the ceiling that admits the collided mint past the cap, since the cap row's shape would decline at the cap before the register, and it makes the release observable): the first listen registers normally, the colliding mint's listen answersRequestDenied, its bound endpoint never serves again (teardown observable, per the cancel row's bounded-retry shape), the listener count was released (a freshport = 0mint is admitted and registers, with its success reply proves the shape), and the first route still serves end to end (an arrival opens the echoedforwarded-tcpipand the bytes ride); the steal is impossible, row-pinned.
Summary
Completed. The ListenRouter token-collision pre-check landed
fail-closed (register refuses, never replaces) with the error-surface
decision recorded: the wire-time collision is the decline disposition
(token-collision / RequestDenied), not the taxonomy, since an Err
would kill the whole russh session, while AdapterError:: ListenRegistration keeps its construction-time class. The refusal
path is the full fail-closed shape (cap release, uniform decline
event, and immediate fabric teardown, so no half state). Row-pinned through
a mint-strategy construct on the fabric double: second register
refuses, first route still serves end to end, refused endpoint never
serves, count released. Full verification set green: cargo test
(default + git/session/tunnel/sftp + --all-features), clippy ×2
-D warnings, fmt, cargo doc --no-deps; existing arrival/cancel/
teardown rows unchanged and green.