From 930d9c1996b9655739405843644eabf84258c329 Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Thu, 29 Oct 2026 05:22:14 +0000 Subject: [PATCH] =?UTF-8?q?docs(review):=20close=20the=20wave-3=20review?= =?UTF-8?q?=20gate=20=E2=80=94=20verdict=20green=20(review-wave-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wave-3 gate read the landed server mappings against mapping.md/ADR-006/ADR-003/auth.md line by line: session/pty/shell/pipe (translation bridge + the derivation contract rules 1-6), git exec (full grammar, no-oracle validation, collapsed denial byte order), route-1 bridge direct rows, the full -R listen flow + forwarded-tcpip data leg, per-build decline matrices, identity observed at every seam, bounded bookkeeping, and the uniform safe tracing vocabulary. All four decomposition-time interpretations confirmed. Fixed inline (this gate): - install_forwarded_surface pre-checks the tunnel/forwarded op — alkcall's register silently replaces an existing entry, so a second adapter over the same shared seam registry silently stole the first adapter's arrival routes; now a failed construction (AdapterError::ListenRegistration), pinned by a new wire row - git take drops a stored pty entry (combined-build stale state) - doc hygiene: channel_cap release ordering, the session-row scoping of the handler Drop, the cancel-order recording, the post-cancel arrival caveat, the exit-chunk ADR lineage cross-ref, the stale lib.rs wave-state doc, the superseded dispatch test name, and mapping.md's listen-resource wording (address verbatim + tcp port) Booked for wave 4: the git row's status-before-close honored by scheduling not construction (needs a seam/shape change), the channel-not-held wire test rows, the tty coverage minors (cancel path, Closed/Break shape, stdout write deadline), and a defensive ListenRouter token-collision pre-check. Verified: cargo test (default) + --all-features (173 rows, 13 binaries), clippy --all-targets -D warnings ×2 (default + --all-features), fmt --check, cargo doc --no-deps (0 warnings), taskgraph validate (26 tasks). --- docs/architecture/mapping.md | 10 +- src/channel_cap.rs | 5 +- src/handler.rs | 30 ++-- src/lib.rs | 8 +- src/tty_map.rs | 3 +- src/tunnel_map.rs | 30 +++- tasks/implementation/review-wave-3.md | 217 +++++++++++++++++++++++++- tests/dispatch_core.rs | 9 +- tests/tunnel_mapping.rs | 40 +++++ 9 files changed, 316 insertions(+), 36 deletions(-) diff --git a/docs/architecture/mapping.md b/docs/architecture/mapping.md index 52dbcdc..7a82409 100644 --- a/docs/architecture/mapping.md +++ b/docs/architecture/mapping.md @@ -63,10 +63,14 @@ alktunnels 0.4.1's actual surfaces with POC-3 §C.4 as the template): - **The listen dial** — family-ward through the consumer surface: `TunnelListener::open(channel_client, TunnelParams {resource: , substrate: Tcp/Unix})`. The resource carries - the wire endpoint string verbatim — the OpenSSH client normalizes + wire bind endpoint>, substrate: Tcp/Unix})`. The resource is built + from the wire bind endpoint: the address string rides verbatim (the + OpenSSH client normalizes the no-address case to `"localhost"` before it sends - (`channel_rfwd_bind_host` parity), so the door passes through; whose + (`channel_rfwd_bind_host` parity), so the door passes through), with + the tcp port appended (`host:port`; `0` stays `0` — the OS-picking + is the fabric's bind step, whose bound report feeds the out-param); + the unix path rides alone; whose bind policy governs (wildcard vs loopback — the sshd `GatewayPorts` posture) is the fabric's establisher/bind closure, assembly policy at the family's provider. The dial is bounded (`LISTEN_DIAL_TIMEOUT` diff --git a/src/channel_cap.rs b/src/channel_cap.rs index caec428..b453901 100644 --- a/src/channel_cap.rs +++ b/src/channel_cap.rs @@ -40,8 +40,9 @@ //! succeeds), the channel never establishes, and *no //! `channel_close` can ever fire* — so the count must be released //! from where the failure is known: inside the spawned bridge -//! task, immediately after the reject is answered. A success never -//! releases there (its close rides path 1). +//! task, **before** the reject is answered (the very next open +//! then observes the recovered budget — pinned by wire test). A +//! success never releases there (its close rides path 1). //! 3. **The `-R` arrival pumps** (`w3-tunnel-listen`): a fabric //! arrival is admitted into the owning route's count and opens the //! SSH `forwarded-tcpip`/`forwarded-streamlocal` channel toward diff --git a/src/handler.rs b/src/handler.rs index f0258d8..f0013c2 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -372,10 +372,14 @@ pub struct SessionHandler { } impl Drop for SessionHandler { - /// Fires the session rows' teardown signal (`w3-tty-session`): - /// every spawned serving task of this connection watches it and + /// Fires the teardown signal (`w3-tty-session`): every spawned + /// **session-row** serving task of this connection watches it and /// cancels its alktty session (the kill-on-drop cleanup) instead - /// of outliving the connection. The `tunnel` build adds the `-R` + /// of outliving the connection. The git serving task does not + /// watch this signal — it ends through the channel duplex (stream + /// EOF / send failure) once the russh session dies, bounded by the + /// alkgit seam's own `session_wall_clock` (the alkgit limits the + /// assembly configured). The `tunnel` build adds the `-R` /// leg's teardown (`w3-tunnel-listen`): the session's pending /// listens' tokens leave the shared router (a fabric arrival after /// the session is gone is the correlation-miss class — @@ -1388,13 +1392,15 @@ async fn listen_request_dispatch( /// variants (the client sends the same normalized string it sent /// originally, and the *allocated* port for `port = 0` mints — the /// value the listen reply carried); the path alone for the unix -/// variant. Order: the route leaves the shared router **before** the -/// entry leaves the session map (so a racing arrival is already the -/// correlation-miss class — no post-cancel half-serve), then the -/// listener count releases, then the family teardown spawns -/// (`TunnelListener::close` inside the spawn — a bounded local -/// shutdown + entry reap, not a serving path the hook may await). -/// `Ok(true)` iff a pending listen matched. +/// variant. Order: the entry leaves the session map and the route +/// leaves the shared router before any family teardown spawns (a +/// racing arrival that has not yet cloned its route is already the +/// correlation-miss class — no post-cancel half-serve; one that +/// cloned just before completes independently — the recorded +/// caveat), then the listener count releases, then the family +/// teardown spawns (`TunnelListener::close` inside the spawn — a +/// bounded local shutdown + entry reap, not a serving path the hook +/// may await). `Ok(true)` iff a pending listen matched. #[cfg(feature = "tunnel")] fn cancel_listen_dispatch( handler: &mut SessionHandler, @@ -1540,6 +1546,10 @@ fn git_row_dispatch( let _ = session.channel_failure(channel); return; }; + #[cfg(feature = "session")] + { + let _ = handler.ptys.remove(&channel); + } let identity = handler.resolved_identity.clone(); let handle = session.handle(); serve::spawn_serving_path(git_map::serve_git_row( diff --git a/src/lib.rs b/src/lib.rs index 71747bb..2049023 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,10 +19,10 @@ //! rest, decrypted at use (ADR-005), never env vars. //! //! Architecture: `docs/architecture/` (phase 1 — ADRs 001–006 accepted; -//! the dependency surface, feature graph, and wave-1 foundations are -//! landed; the server stack, the mappings, the client direction, and -//! validation are later implementation waves — wave 2 decomposes after -//! this gate). See also `docs/research/` and family ADR-003. +//! the foundations, the server stack, and the server-direction mappings +//! are landed through wave 3; the client direction, the validation +//! suite, and the release are later implementation waves). See also +//! `docs/research/`, `docs/plans/implementation.md`, and family ADR-003. mod adapter; mod channel_cap; diff --git a/src/tty_map.rs b/src/tty_map.rs index 3fe9019..65fc97a 100644 --- a/src/tty_map.rs +++ b/src/tty_map.rs @@ -132,7 +132,8 @@ //! 1–6 — normative) //! //! The alktty exit chunk (`{"type":"exit","code":i32}`, -//! exit-chunk-is-last per alktty's ADR-005 lineage) is the sole +//! exit-chunk-is-last per alktty's ADR-004 on the alknet ADR-055 +//! lineage) is the sole //! fidelity input, **never widened**: [`derive_exit`] maps it to //! exactly one SSH exit report shape: //! diff --git a/src/tunnel_map.rs b/src/tunnel_map.rs index 9a921d2..4573227 100644 --- a/src/tunnel_map.rs +++ b/src/tunnel_map.rs @@ -151,7 +151,12 @@ //! misses → teardown-at-birth), and `cancel_*` of an unknown //! (address, bound-port) pair answers `false` — the vanilla //! cancel contract (sshd parity: match by the raw wire string + -//! the bound port). +//! the bound port). One recorded caveat: an arrival that *cloned* +//! its route just before the cancel removes it still completes its +//! open + pump (the forwarded channel is independent of the +//! listener's teardown — the arrival is never half-open, but a +//! cancelled listen can deliver one already-accepted connection; +//! sshd parity). //! //! ## The runtime-Unix gate (OQ-11) //! @@ -393,10 +398,13 @@ impl Default for ListenRouter { /// deployment policy; an assembly wanting a different per-identity /// cap shapes it through its own `ServingConfig`/`registry` /// composition upstream) — and spawns alktunnels' forward-op handler -/// over the crate's arrival pump. The registration is one-per-op-id- -/// per-registry (alkcall's rule): a conflicting registration is a -/// failed construction (fail-closed — never a route to a handler the -/// door does not own). +/// over the crate's arrival pump. The registration is +/// one-per-op-id-per-registry: alkcall's `register` silently +/// replaces an existing entry, so the door **pre-checks the op** — +/// a `tunnel/forwarded` registration that already exists (e.g. a +/// second adapter over the same shared registry) is a failed +/// construction (fail-closed — never a route to a handler the door +/// does not own). /// /// On success the router rides back into the serve-config (the /// session handlers read it) — `Ok` with the router installed, `Err` @@ -419,6 +427,18 @@ pub(crate) fn install_forwarded_surface(serve: &mut config::ServeConfig) -> Resu client.manager().clone(), alkcall::channels::policy::default_policy(), ); + if seam + .registry + .registration(alktunnels::params::OP_TUNNEL_FORWARDED) + .is_some() + { + return Err(format!( + "the tunnel/forwarded op ({}) is already registered in the \ + seam's registry — a conflicting registration would silently \ + replace the door's arrival route", + alktunnels::params::OP_TUNNEL_FORWARDED + )); + } register_tunnel_forwarded_openable( &core, &seam.registry, diff --git a/tasks/implementation/review-wave-3.md b/tasks/implementation/review-wave-3.md index 47d205f..3936245 100644 --- a/tasks/implementation/review-wave-3.md +++ b/tasks/implementation/review-wave-3.md @@ -1,7 +1,7 @@ --- id: review-wave-3 name: Wave 3 gate — the server-mappings review -status: pending +status: completed depends_on: [w3-seam-config, w3-decline-tracing, w3-git-mapping, w3-tty-session, w3-tunnel-direct, w3-tunnel-listen] scope: narrow risk: low @@ -68,15 +68,15 @@ booked (remediation before wave 4 decomposes). ## Acceptance Criteria -- [ ] Every ADR/mapping surface above read and confirmed (or +- [x] Every ADR/mapping surface above read and confirmed (or corrected with the correction landed) -- [ ] The decline matrix verified per build through the harness; +- [x] The decline matrix verified per build through the harness; the derivation contract verified against its rules; the spawn-and-return constraint re-pinned over real serving paths -- [ ] Full verification set green (`cargo test`, `--all-features`, +- [x] Full verification set green (`cargo test`, `--all-features`, clippy ×2 `-D warnings`, fmt, `cargo doc --no-deps`) -- [ ] `taskgraph validate` clean -- [ ] Verdict recorded; wave 4 decomposition unblocked or findings +- [x] `taskgraph validate` clean +- [x] Verdict recorded; wave 4 decomposition unblocked or findings booked ## References @@ -89,4 +89,207 @@ booked (remediation before wave 4 decomposes). ## Notes -> To be filled at gate time. \ No newline at end of file +### Gate reading (line by line, 2026-10-29; commits 270365e…3d0253d) + +- **mapping.md line-by-line** ✓ — each spine row read against the + landed code: the session/pty/shell/pipe rows (translation bridge, + OQ-08 argv conventions, pipe-less shell serves pipe-mode), the git + exec row (full grammar, no-oracle validation, alkgit dispatch), the + direct-tcpip / direct-streamlocal rows (route-1 bridge), the + forwarded-tcpip row (echo args + bound port, teardown-at-birth on + correlation miss), the global listen rows (verbatim resource + + bound-port out-param — one doc correction landed: §The listen dial + now states the port appending, the code's `host:port` form), the + request-level table, and the derivation contract rules 1–6 in + `derive_exit` (incl. `i32::MIN` via `checked_abs` → 255, + `core_dumped` never, rule 3's table entry unreachable by + construction, the PTY-path rule-5 limit documented). The decline + matrix is green per feature build through the harness (per-build + cfg-pinned modules; unset-field rows — feature on, handle unset — + present in every featured build). +- **ADR-006** ✓ — route-1 bridge (accept only after the fabric open; + `ConnectFailed` on failure; the wave-2 stub retired into the real + decline), exec-only git (no subsystem dispatch anywhere in the + grammar; git service names decline), parse-only dispatch (no + detect residue), spawn-and-return re-pinned over the real serving + paths (the wave-2 mechanics rows still green; the inline listen + dial is the one recorded exception, bounded by + `LISTEN_DIAL_TIMEOUT` — wire-necessity reasoning documented). +- **ADR-003** ✓ — per-build matrices; honest advertisement + unchanged; the sftp feature still gates nothing and its rows + decline in every build (OQ-13); x11/auth-agent never-features + untouched; feature-to-dep isolation (`cargo tree` proven in + `w3-seam-config`). +- **Auth/identity** ✓ — the resolved identity observed at every seam: + alktty (the ownership witness row), alkgit (the authorize/ deny + row), tunnels (the CF-005/CF-006 establisher + ACL rows); + `set_identity`/`take_call_connection` appears **nowhere in src/** + (the detach decision recorded, the upstream ask booked — see below + for final disposition); the wave-2 `auth_succeeded` parked item + discharged as ride-along store; no username anywhere (the + safety-scan row). +- **OQ resolutions in code** ✓ — OQ-08 (unset-declines, both field + classes, `wrapper ++ [cmd]` verbatim-append), OQ-09 (env declined + unconditionally), OQ-10 (derivation rules + PTY-path limit + documented), OQ-11 (runtime-Unix gates compile-everywhere with + dial-call-witness-at-zero off-Unix rows), OQ-13 (sftp rows + unchanged in every build). +- **Bounded resources** ✓ — the cap across the fabric window (admit + at the hook, release-before-reject on fabric failure, disjoint + saturating release sites verified per row), the listener + bookkeeping bounded by a dedicated `max_concurrent_channels`-ceiling + counter, the route map bounded by construction (minted only at + accepted listens, removed at cancels + session teardown — the + `Drop` really removes from the shared router), the control-row + surfaces bounded (`try_send`, never awaited on the packet loop), + the bridge adapters bounded (mpsc 64, 64 KiB duplexes, 16 KiB read + buffers, `MAX_CHUNK_LEN` frame gates). No unbounded shape found in + any serving path. +- **Tracing** ✓ — the uniform event vocabulary at every + decline/serve site (row + feature_build + channel_id + class + + reason + exec_route; declines `debug`, the one `warn` the + serving-failure arm), safety posture pinned by scan (no username, + no fingerprint, no secret ever in an event). +- **The smokes** ✓ — git clone 2.43 / `ssh -L` + curl / `ssh -tt` + pty / `ssh -R` + curl: all four landed and green in this + environment; every one carries a check-gated skip-with-reason + posture, nothing silently green. +- **Wave-2 booked items** ✓ — decline tracing landed + (`w3-decline-tracing`); the acceptor vocabulary landed + (`w3-tunnel-listen`: harness accept hook + per-arrival cap); the + deferred identity-landing assertion discharged (each seam rides the + resolved identity, wire-witnessed); the inherited + `auth-agent`/`forwarded-streamlocal`-open watch item unchanged (no + pin touch this wave). +- **Docs honesty** ✓ — each replaced stub's module docs name what it + now is and what remains wave-4/5; the `set_identity` reading, the + resource-naming convention, the inline-dial bound, and + portable-pty's PTY-path exit shape are all recorded in module docs + (one overbroad Drop doc corrected inline, below). + +### Interpretations (flagged at decomposition) — all confirmed + +1. **Whole-`Option` struct per seam** (vs flat `Option` fields) — + confirmed: each seam is consumed atomically by exactly one seam + call; the flat single-field tunnel handle stays flat. Recorded in + `w3-seam-config`. +2. **The server-side sftp bullet already discharged in wave 2** — + confirmed: the rows are in `tests/dispatch_core.rs` per build. +3. **The `-R` listen flow's biggest open closed without a fork** — + confirmed: the topology closed over the published alkcall 0.10.1 / + alktunnels 0.4.1 surfaces via the `TunnelListenSeam` + course-correction; a scratch probe (published crates only) proved + the flow before landing; nothing forked or absorbed. +4. **The wave-2 booked items' wave-3 homes** — confirmed as above. + +### Minor issues found and fixed inline (this session) + +Source/doc fixes (all commit-tested; suite re-run green after): + +- **Duplicate `tunnel/forwarded` registration now fails + construction** (the gate's one real fix): alkcall's + `OperationRegistry::register` silently *replaces* an existing op + entry, contradicting the `install_forwarded_surface` docs' recorded + fail-closed claim — a second adapter over the same shared seam + registry would silently steal the first adapter's arrival routes. + `install_forwarded_surface` now pre-checks the op + (`registry.registration(OP_TUNNEL_FORWARDED)`) and refuses with + `AdapterError::ListenRegistration`; new wire-level row + `a_second_adapter_over_a_shared_listen_registry_fails_construction` + pins both constructs/refuse. +- **channel_cap.rs** — the bridge-failure release doc said "after the + reject is answered"; the code (and the pinned test) release + *before* the reject. Doc corrected. +- **handler.rs** — the `Drop` doc claimed "every spawned serving task + watches" the teardown signal; scoping corrected (the session rows + watch; the git task ends through the duplex, bounded by alkgit's + `session_wall_clock`). The cancel-form doc's recorded ordering + (router-before-map) was inverted vs the code; corrected, with the + post-cancel-race caveat recorded. +- **tunnel_map.rs** — the "never half-serves" bookkeeping claim now + records the one-arrival caveat: an arrival that cloned its route + just before a cancel completes independently (never half-open; + sshd parity). +- **tty_map.rs** — stale ADR cross-ref on the exit chunk (alktty + ADR-005 → ADR-004 on the alknet ADR-055 lineage). +- **lib.rs** — the crate-level wave-state doc still said "wave 2 + decomposes after this gate"; updated to the landed state. +- **tests/dispatch_core.rs** — the stale + `unknown_git_services_route_to_the_pipe_row` name/doc superseded by + the w3 decision ("off the pipe row"); renamed, verdicts preserved + (the wire shape coincides — why it never failed). +- **handler.rs (git row)** — a pty-then-git-exec in a combined build + left the stored pty entry until close; the git take now drops it + (the pipe row's consumption shape). +- **mapping.md** — the §listen-dial resource wording corrected to the + code's assembled form (address verbatim + tcp port appended; + `0` stays `0`). + +### Booked findings (not remediation this gate — wave-4 material) + +1. **The git row's status-before-close is honored by scheduling, not + by construction** (the gate's only structural moderate): + `serve_duplex_resolved` splits the channel stream internally and + its session halves drop when the serve returns — russh's + `ChannelCloseOnDrop` rx-half emits a server→client + `CHANNEL_CLOSE` *inside* the seam call, before + `serve_git_row`'s explicit `exit_status_request`. Today the + ordering holds (the task's continuation queues the report in the + same poll, ahead of the drop's send) and every wire row passes, + but a full `Sender` buffer at the serve tail could order the + implicit close first and silently drop the report (russh no-ops + `exit_status_request` for an already-closed channel). A fix wants + a seam/shape change (split halves into the seam, or queue the + report while the stream is alive) — beyond this gate's inline + budget; book a wave-4 hardening item. (The tty rows are NOT in + this class: their report precedes any close-contracting drop in + one task.) +2. **The `channel-not-held` guard lacks wire test rows** — a + documented decline class on all three rows (git/tty pipe/shell), + constructible on the wire (a second exec on a served channel), + but no row asserts it in any build; mapping.md's every-row + tested-in-every-build convention names it contract territory. + Test-only task; book for wave 4's decomposition. +3. **Tty coverage gaps** (minors, one ticket's worth): the + `channel_close`-fired per-channel cancel path is unexercised on + the wire (only the teardown signal is); the Closed/Break + no-exit-chunk close shape has no wire row; the tty stdout + `write_all` carries no deadline (backpressure bounded, liveness + covered by inactivity timeout — record as a known limit only). +4. **Defensive `ListenRouter` token-collision pre-check** (minor): + `register` silently replaces on token collision; safety rests on + the fabric's ADR-008 mint discipline. A fail-closed + decline-the-listen-on-collision is a cheap wave-4 addition. +5. **Upstream asks to file** (booked with their owners, not this + crate): the alktunnels/alkcall "non-detaching post-construction + identity seam" note (`w3-tunnel-direct`'s recorded decision — the + door keeps the no-detach posture until upstream proposes a seam), + and mapping.md's recorded event-vocabulary nuance for the + unreachable mismatch arms (`correlation-miss` naming). + +No blocking issues; no code shape errors found. Wave 4 decomposition +is unblocked and should absorb items 1–4 above. + +### Verification (all green, 2026-10-29) + +`cargo test` (default build) · `cargo test --all-features` (173 rows +across 13 binaries) · `cargo clippy --all-targets -- -D warnings` ✓ · +`cargo clippy --all-features --all-targets -- -D warnings` ✓ · +`cargo fmt --check` ✓ · `cargo doc --no-deps` 0 warnings · +`taskgraph validate` ✓ (26 tasks). + +## Summary + +**Verdict: green.** The wave-3 server mappings read clean against +mapping.md/ADR-006/ADR-003/auth.md line by line: the translation +bridge + derivation contract, the exec-only git grammar with the +no-oracle dispatch and the byte-pinned collapsed denial, the route-1 +bridge and the full `-R` listen flow closed over the published +surfaces, per-build decline matrices, identity observed at every +seam, bounded bookkeeping throughout, and a uniform safe tracing +vocabulary. All four flagged interpretations confirmed; one real +fail-closed hole fixed inline (duplicate listen registration) plus +eight doc/test-hygiene minors; four findings booked for wave 4 (the +git exit-report close race, the `channel-not-held` wire rows, the +tty coverage minors, the router token-collision pre-check). Wave 4 +decomposition is unblocked. \ No newline at end of file diff --git a/tests/dispatch_core.rs b/tests/dispatch_core.rs index 49edfce..ac174f5 100644 --- a/tests/dispatch_core.rs +++ b/tests/dispatch_core.rs @@ -284,11 +284,12 @@ mod git_build { } /// - *Contract stamp:* POC-3 §2 (unknown git services take the - /// honest decline-with-reason surface — `w3-git-mapping`'s - /// decided class, off the pipe row too; the reply vocabulary is - /// shared with parse-then-fail: failure, channel alive). + /// agreed declined surface — `w3-git-mapping`'s decided class, + /// off the pipe row: `ExecRoute::Declined`, build-independent; + /// the reply vocabulary is shared with parse-then-fail: failure, + /// channel alive). #[tokio::test] - async fn unknown_git_services_route_to_the_pipe_row() { + async fn unknown_git_services_take_the_declined_surface() { let session = authenticated().await; for command in ["git-send-pack '/r'", "git-upload-archive '/r'"] { let mut channel = open_session_channel(&session.client) diff --git a/tests/tunnel_mapping.rs b/tests/tunnel_mapping.rs index 6ffb4af..57d5bd5 100644 --- a/tests/tunnel_mapping.rs +++ b/tests/tunnel_mapping.rs @@ -1696,6 +1696,46 @@ mod serving { session.unmount().await; } + + /// - *Contract stamp:* the door-side registration is + /// one-per-op-per-registry (mapping.md §Global-request rows / the + /// `install_forwarded_surface` claim): alkcall's `register` + /// silently replaces an existing op entry, so the door + /// pre-checks — a second `SshAdapter` composed over the same + /// shared seam registry is a **failed construction** + /// (`AdapterError::ListenRegistration`), never a silent steal of + /// the first adapter's arrival routes. + #[tokio::test] + async fn a_second_adapter_over_a_shared_listen_registry_fails_construction() { + let double = wire_fabric_with_listen(true, true).await; + let provider = Arc::new(MemIdentityProvider::new()); + provider.register( + fixture("alpha").fingerprint, + identity("tunnel-rider", &["ssh"]), + ); + + let mut first = harness_serve_config( + Arc::clone(&provider) as Arc, + &[fixture("bravo")], + Limits::default(), + ); + first.channel_client = Some(Arc::clone(&double.client)); + first.tunnel_listen_seam = Some(TunnelListenSeam::new(Arc::clone(&double.registry))); + let _first = SshAdapter::new(first) + .await + .expect("the first adapter over the shared registry constructs"); + + let mut second = harness_serve_config(provider, &[fixture("charlie")], Limits::default()); + second.channel_client = Some(Arc::clone(&double.client)); + second.tunnel_listen_seam = Some(TunnelListenSeam::new(Arc::clone(&double.registry))); + let refused = SshAdapter::new(second) + .await + .expect_err("a conflicting registration is a failed construction"); + assert!( + matches!(refused, alkssh::AdapterError::ListenRegistration(_)), + "the refusal is the ListenRegistration class, got {refused:?}" + ); + } } #[cfg(all(feature = "tunnel", unix))]