docs(tasks): decompose wave 4 — the client direction

Eight implementation tasks + the gate under tasks/implementation/,
folding the wave-3 gate's four booked findings in as dedicated tasks
(the gate's own disposition — wave-4 material, not wave-3 backports).

Hardening set (server-side, independent of the client chain,
scheduled first as cheap remediation):

- w4-git-exit-close: booked item 1 — the git row's status-before-close
  by construction (the seam/shape decided + recorded in-task; the
  saturated-buffer scenario pinned on the wire)
- w4-channel-not-held-rows: booked item 2 — the guard's wire rows on
  git/tty pipe/shell, per build (test-only)
- w4-tty-coverage: booked item 3 — the channel_close-fired cancel
  path + the Closed/Break no-exit-chunk rows + the write_all
  no-deadline known-limit record
- w4-router-collision: booked item 4 — the ListenRouter
  token-collision fail-closed pre-check

Client chain (sequential logistically — one client Handler impl):

- w4-dial: the russh client stack + the connection handle + the
  client Handler skeleton at fail-closed defaults, over an in-process
  fake-sshd server double
- w4-op-rows: exec/pty, direct-tcpip, git exec over the shared
  grammar, the sftp client-side decline — the POC-3 §D.3 mechanics
- w4-reverse-listen: the -R leg — forward requests + the acceptor
  registry + late-install (the wave's highest-risk task)
- w4-lifecycle: the OQ-12 primitives (cancellable handles, clean
  disconnect, no reconnect policy)

review-wave-4 depends on all eight. The gate's item 5 (upstream asks)
stays booked with its owners — no task. Plan wave-4 section carries
the decomposition record + four flagged interpretations; wave table
status updated.

Verification: taskgraph validate ✓ (35 tasks)
This commit is contained in:
glm-5.3-flash committed 2026-10-29 05:59:03 +00:00
1 parent 930d9c1996
commit dba64d65d3
10 files changed
+820 -3

No files matched your search

+96
View File
@@ -0,0 +1,96 @@
---
id: review-wave-4
name: Wave 4 gate — the client-direction review
status: pending
depends_on: [w4-dial, w4-op-rows, w4-reverse-listen, w4-lifecycle, w4-git-exit-close, w4-channel-not-held-rows, w4-tty-coverage, w4-router-collision]
scope: narrow
risk: low
impact: phase
level: review
---
## Description
The wave-4 review gate. The client direction completes the
two-direction door — wave 5 builds the full matrix and the release on
both directions, and the published crate freezes this surface. The
gate reads the landed client direction against the architecture line
by line:
- **client.md line-by-line** — §The dial surface (key source
decrypt-at-dial fail-closed; the policy at `check_server_key`; the
bounded knobs with the keepalive 15 s / 3 posture), §Op rows (each
table row read against the landed code — the POC-3 §D.3 mechanics
pinned in code + rows), §The acceptor (served only under a
registered feature's acceptor; unregistered/unfeatured declined
cleanly — never accept-and-drop; late-install across a live
connection), §Lifecycle primitives (explicitly cancellable handles;
`Disconnect::ByApplication`; **no reconnect/restore policy in the
crate** — the boundary held).
- **ADR-003 symmetry** — the same features gate both directions; the
per-build matrices green client-side (the acceptor rows per build);
honest advertisement unchanged; the sftp placeholder declines in
every build both directions (OQ-13); x11/auth-agent never-features
untouched.
- **ADR-005 / OQ-05 / OQ-07** — decrypt-at-dial with every
`KeySourceError` posture a no-dial outcome; the policy seam
consumed at the hook (crate default connects to nothing; no
accept-all built-in); the `Zeroizing` wrap posture intact.
- **The wave-3 gate's booked items 1–4** — every one discharged or
explicitly re-booked: the git status-before-close by construction
(`w4-git-exit-close`), the channel-not-held wire rows
(`w4-channel-not-held-rows`), the tty coverage minors
(`w4-tty-coverage`), the router token-collision pre-check
(`w4-router-collision`). Item 5 (the upstream asks) verified still
booked with their owners — not absorbed silently.
- **Bounded resources** — no new unbounded shape on the client
surfaces: the acceptor registry bounded, the listener bookkeeping
bounded, the pumps bounded (the convention-9 read over every new
serving/pumping path).
- **Tracing** — the uniform event vocabulary across the new
client-side decline/serve paths; the safety posture held (no
secrets, no key material, no username).
- **The smokes** — the client-direction real-tool postures recorded
honestly (required/conditionally-skipped with reason); nothing
silently green; anything named wave-5 e2e territory named
explicitly.
- **Docs honesty** — module docs name what each new surface is, what
remains wave-5 material, and any new upstream notes (the russh
client-hook shapes at this pin; the acceptor-registry shape
decision; the DialConfig disposition).
Verdict: green (wave 5 decomposes, absorbing corrections) or findings
booked (remediation before wave 5 decomposes).
## Acceptance Criteria
- [ ] Every client.md/ADR surface above read and confirmed (or
corrected with the correction landed)
- [ ] The client-side per-build matrices verified through the server
double; the acceptor contract verified (registered/unregistered/
unfeatured/late-install); the wave-3 booked items 1–4 verified
discharged
- [ ] Full verification set green (`cargo test`, `--all-features`,
clippy ×2 `-D warnings`, fmt, `cargo doc --no-deps`)
- [ ] `taskgraph validate` clean
- [ ] Verdict recorded; wave 5 decomposition unblocked or findings
booked
## References
- docs/plans/implementation.md (wave 4 section + this gate's role)
- docs/architecture/client.md (the wave's contract), mapping.md (the
server-direction mirror), overview.md, open-questions.md (OQ-05/
07/11/12/13), ADRs 002/003/005/006
- tasks/implementation/review-wave-3.md (the booked items this gate
verifies discharged)
- docs/research/reverse-runner.md (the consumer pattern the client
direction serves), docs/research/poc-3-mapping-bakeoff.md §D.3
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion
@@ -0,0 +1,66 @@
---
id: w4-channel-not-held-rows
name: Wave 4 — channel-not-held guard wire rows (wave-3 gate booked item 2)
status: pending
depends_on: [w4-git-exit-close]
scope: narrow
risk: low
impact: component
level: implementation
---
## Description
Close the wave-3 gate's booked finding item 2
(`review-wave-3` §Booked findings): the **`channel-not-held` guard
lacks wire test rows**. The guard is a documented decline class on
all three rows (git / tty pipe / shell) — a second exec (or request)
on an already-served channel — constructible on the wire, but no row
asserts it in any build. `mapping.md`'s every-row-tested-in-every-build
convention names it contract territory; the fail-closed invariant is
contract on both sides of every feature (AGENTS.md convention 5).
**Test-only task** — no production code change expected (if landing
the rows exposes a guard gap, stop and book it for the gate rather
than silently widening scope):
- Wire rows through the harness, **per build** (the per-build
cfg-pinned module convention): a second exec on a served git
channel → the channel-not-held decline; the same on the tty pipe
row and the shell row — the decline class asserted in every build
that carries the row (featured builds carry the serving rows; the
decline-class rows ride the same per-build matrix the dispatch
tests use).
- The rows observe the decline's wire shape + the tracing event
vocabulary (the `w3-decline-tracing` class/reason fields — the
rows double as the vocabulary's coverage for this class).
- Depends on `w4-git-exit-close` only to keep `tests/git_mapping.rs`
single-writer (the hardening lands first; these rows land into the
settled file).
## Acceptance Criteria
- [ ] Wire rows for the channel-not-held decline class on all three
rows (git / tty pipe / shell), per build, through the harness
- [ ] The rows assert the wire shape + the tracing event class/reason
- [ ] No production code change (or a stop-and-booked finding
recorded for the gate — nothing silent)
- [ ] `cargo test` (+ per-feature + `--all-features`), clippy ×2
(`-D warnings`), fmt, `cargo doc --no-deps` pass
## References
- tasks/implementation/review-wave-3.md §Booked findings item 2
- docs/architecture/mapping.md (the request-level table — the
channel-not-held class; the every-row-tested convention)
- tests/git_mapping.rs, tests/tty_mapping.rs, tests/dispatch_core.rs
(the per-build matrix convention the rows join), src/observe.rs
(the event vocabulary the rows assert)
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion
+111
View File
@@ -0,0 +1,111 @@
---
id: w4-dial
name: Wave 4 — the client dial surface (russh client stack + the connection handle)
status: pending
depends_on: [review-wave-3]
scope: moderate
risk: medium
impact: phase
level: implementation
---
## Description
Land the client direction's substrate — the door outward
(`client.md` §The dial surface): the russh client stack every wave-4
op row composes into, mirroring `w2-server-mount`'s role for the
server direction. Everything later in the wave rides the authenticated
connection handle this task lands.
- **The dial surface** — `connect`-shaped entry taking the wave-1
[`DialConfig`](the address, the key source, the host-key policy, the
`Limits`): the russh client `Config` built from the bounded knobs
(keepalive 15 s / 3 default posture per the reverse-runner evidence —
`client.md` §Correctness facts; inactivity; window/packet knobs per
`overview.md`'s table), the dial target as `DialConfig.address`
carries it. If the dial surface needs a `DialConfig` amendment,
land + record it here (the `w3-seam-config` course-correction
pattern) — the wave-1 shape is expected to hold; decide and record.
- **Key source decrypt-at-dial** (ADR-005) — the [`KeySource`] load
happens at dial time; locked / absent / decryption-failure /
unknown-`key_version` are all **no-dial outcomes** with their
distinct `KeySourceError` variants surfaced (never env vars, never
plaintext at rest; the adapter-side `Zeroizing` wrap per OQ-07
already landed in wave 1 — consumed here).
- **Host-key policy at `check_server_key`** (OQ-05) — the wave-1
[`HostKeyPolicy`] seam consumed at russh's client hook (the verdict
type was pinned against this hook shape in `w1-host-key-policy`):
the crate default ([`RejectAll`]) connects to nothing; case-1 pins
hard-decline on mismatch; case-2 assembly policies decide. The
crate is policy-blind — no accept-all built-in.
- **The client Handler skeleton at fail-closed defaults** — the
russh `client::Handler` impl the dial constructs: server-initiated
channel opens (`server_channel_open_forwarded_tcpip` /
`_forwarded_streamlocal` / `_unknown` / x11 / agent) **declined
cleanly** — never russh's accept-and-drop default (ADR-003 §4, the
client-side symmetry rule; the acceptor registry that serves them
is `w4-reverse-listen`'s surface, this task pins the default).
- **The authenticated connection handle** — the type everything else
rides: the russh `client::Handle` wrapped with the crate's surface
(publickey-only authentication from the key source — the family
contract; no password/keyboard-interactive paths), the policy and
limits carried, the lifecycle primitives' raw material (full
cancellation/clean-close shape is `w4-lifecycle`'s).
- **Test vehicle** — the client direction dials *vanilla* servers, so
the loopback harness (alkssh server) is not the vehicle: land an
in-process russh **server double** (a fake `sshd` — hand-rolled
`russh::server` handler in `tests/common`) as the contract-test
vehicle, decide + record its placement and shape. Contract rows:
dial success (authenticated), each key-source failure posture
(locked/absent/decryption), policy-decline rows (default-reject,
pin-mismatch hard decline, case-2 accept), the bounded-config
wiring observable, server-initiated open declined cleanly.
- **The smoke posture** decided + recorded (an alkssh-client ↔
alkssh-server in-process loopback is the cheap real-stack smoke;
a real-`sshd` dial is wave-5 e2e territory — never silently
dropped).
## Acceptance Criteria
- [ ] The dial surface lands over `DialConfig` (+ any amendment
decided and recorded); the russh client `Config` carries the
bounded knobs (keepalive 15 s / 3 posture, inactivity, window)
- [ ] Key source decrypt-at-dial; every `KeySourceError` failure
posture is a no-dial outcome with its distinct variant surfaced
- [ ] `check_server_key` calls the injected policy; `RejectAll`
connects to nothing; pin-mismatch hard-declines; no accept-all
built-in
- [ ] The client Handler skeleton declines server-initiated opens
cleanly (fail-closed default pinned by wire rows)
- [ ] The authenticated connection handle lands; authentication is
publickey-only from the key source
- [ ] The in-process server double lands in `tests/common`; the
contract rows above green; the smoke posture decided + recorded
- [ ] `cargo test` (+ per-feature + `--all-features`), clippy ×2
(`-D warnings`), fmt, `cargo doc --no-deps` pass
## References
- docs/architecture/client.md §The dial surface, §Correctness facts
(the POC-recorded mechanics), §What the crate does *not* include
- docs/architecture/decisions/002-russh-pin.md (the client russh
surface), docs/architecture/decisions/003-feature-graph.md (§4 the
client-side fail-closed acceptor default this skeleton pins),
docs/architecture/decisions/005-key-sources.md (decrypt-at-dial)
- docs/research/russh.md §Client surface (`client::Handler` inventory
— `check_server_key` defaults to refuse; the
`server_channel_open_*` hook set; `client::Config` knobs),
docs/research/reverse-runner.md (lessons 3/5 — keepalives
load-bearing, the ephemeral-fleet policy posture)
- docs/research/poc-1-auth-bridge.md §6/§7 (the client-side plumbing
facts), docs/research/poc-2-vault-keys.md (the dial lifecycle)
- src/config.rs (`DialConfig`), src/host_key.rs, src/key_source.rs
(the wave-1 seams consumed)
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion
+78
View File
@@ -0,0 +1,78 @@
---
id: w4-git-exit-close
name: Wave 4 — git row hardening (status-before-close by construction — wave-3 gate booked item 1)
status: pending
depends_on: [w3-git-mapping]
scope: narrow
risk: medium
impact: component
level: implementation
---
## Description
Close the wave-3 gate's only structural booked finding
(`review-wave-3` §Booked findings item 1): the git row's
status-before-close is honored **by scheduling, not by construction**.
Today `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`. The ordering currently 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<Msg>` 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 silent exit-report loss on a published row.
The fix wants a seam/shape change — **decide + record which**:
- split the channel halves in `serve_git_row` and pass them into the
seam (the report then rides a half the seam never closes), or
- queue the exit report while the stream is alive (the report's send
ordered before any close-contracting drop by construction), or
- another shape that makes the ordering structural rather than
scheduler-luck — the gate's reasoning and the two named options are
the starting points, not the bounds.
Constraints: every existing git wire row stays green (no behavior
change on the happy path — this is ordering hardening, not semantics
change); the spawn-and-return constraint (ADR-006 decision 4) holds —
the reporting still rides the spawned task; the tty rows are NOT in
this class (their report precedes any close-contracting drop in one
task — the gate verified; don't touch them). New wire row(s) pinning
the construction: a saturated `Sender<Msg>` buffer at the serve tail
(the failure scenario made real) still delivers the exit report —
the by-construction property proven on the wire, not by reasoning.
## Acceptance Criteria
- [ ] The shape decided + recorded (module docs carry the reasoning);
the git row's status-before-close holds by construction, not
scheduling
- [ ] Every existing git wire row green (no happy-path behavior
change); the spawn-and-return constraint intact
- [ ] New wire row(s): the saturated-buffer scenario delivers the
exit report (the by-construction property pinned)
- [ ] `cargo test` (+ per-feature + `--all-features`), clippy ×2
(`-D warnings`), fmt, `cargo doc --no-deps` pass
## References
- tasks/implementation/review-wave-3.md §Booked findings item 1 (the
full finding + the two named fix shapes)
- docs/architecture/mapping.md (the git exec row — exit-status both
directions), docs/architecture/decisions/006-mapping-bridge-and-dispatch.md
(decision 4 — spawn-and-return)
- src/git_map.rs, src/serve.rs (`report_exit_status_then_close` — the
wave-2 reporting mechanics this hardening composes with), alkgit
0.1.0 `serve_duplex_resolved` (the seam whose half-ownership is the
question)
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion
+69
View File
@@ -0,0 +1,69 @@
---
id: w4-lifecycle
name: Wave 4 — lifecycle primitives (cancellable handles + clean disconnect)
status: pending
depends_on: [w4-op-rows, w4-reverse-listen]
scope: narrow
risk: low
impact: component
level: implementation
---
## Description
Land the OQ-12 resolution in code (`client.md` §Lifecycle
primitives): the primitives that make assembly-side reconnect/restore
policy *possible* — and nothing more:
- **Explicitly cancellable handles** — every listener/handle the
client direction returns (the `-R` listener handles from
`w4-reverse-listen`, the connection handle from `w4-dial`) carries
an explicit cancellation/clean-close path; no handle relies on drop
timing alone for wire-visible state.
- **Clean disconnect** — `Disconnect::ByApplication` (russh's client
`Handle::disconnect`): the explicit-close path leaves no half-open
channels, observed clean by the peer (row-pinned through the
server double).
- **No reconnect or restore policy owned by the crate** — the
module-doc posture: reconnect-and-restore (rebinding persisted
tunnel ports — the alkminer flow, reverse-runner fact 4) is
assembly policy; the crate provides bound ports, handles,
cancellation, clean close. The docs state the boundary explicitly
so no later task drifts into baking policy in.
- **Contract rows through the server double** — cancel a listener
mid-connection (the server's forward is torn down, the bound port
released); clean disconnect observed by the peer; the
connection-handle cancellation path; the no-policy posture is a
docs-honesty item for the gate, not a row.
## Acceptance Criteria
- [ ] Every client-direction listener/handle is explicitly
cancellable; no drop-timing-only wire state
- [ ] `Disconnect::ByApplication` clean-close lands; the peer
observes a clean disconnect, no half-open channels (row-pinned)
- [ ] The no-reconnect-policy boundary stated in module docs (the
primitives-not-policy posture)
- [ ] Contract rows green through the server double
- [ ] `cargo test` (+ per-feature + `--all-features`), clippy ×2
(`-D warnings`), fmt, `cargo doc --no-deps` pass
## References
- docs/architecture/client.md §Lifecycle primitives (the OQ-12
resolution), docs/architecture/open-questions.md (OQ-12 — resolved:
primitives only)
- docs/research/reverse-runner.md (fact 4 — reconnect-and-restore is
assembly policy, kept in view so the crate never bakes it in)
- docs/research/russh.md §Client surface (`Handle::disconnect`, the
forward cancel forms)
- src/tunnel_map.rs (the server-side teardown posture — the cancel
semantics mirror)
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion
+105
View File
@@ -0,0 +1,105 @@
---
id: w4-op-rows
name: Wave 4 — the client op rows (exec/pty, direct-tcpip, git exec, sftp decline)
status: pending
depends_on: [w4-dial]
scope: moderate
risk: medium
impact: phase
level: implementation
---
## Description
Land the op rows — family op → vanilla translation per
`client.md`'s table, pointed upstream at a vanilla server (the
reverse-runner shape: the hub drives every channel open). Type
symmetry at the seam is the contract: family-protocol types in,
vanilla wire ops out, no vanilla-concept leaks up. The POC-3 §D.3
mechanics are the body of knowledge this task lands in code:
- **Session exec/pty rows** (under `session`) — `channel_open_session`
+ the exec/pty-req/shell-req translations onto the alktty session
types (pipe vs PTY; exit semantics): `Channel::exec()` — **no
`request_exec` at this pin**; Success/Failure arrive on the channel
queue via `wait()`-drain, **not** as `Err` from the request call
(drain until the expected message, `continue` past the rest);
`window_change`/`signal` are fire-and-forget and must be sent
**before** `into_stream()` (the Channel is consumed by conversion);
**never consume the channel queue before `into_stream()`** (a
`wait()` on a fresh channel breaks it — POC-1 §6, both sides). The
data pump + the exit consumption mirror the server direction's
derivation contract (the pipe-mode pid-only signal caveat rides the
alktty exit-chunk semantics — `mapping.md`'s contract, consumed not
re-derived).
- **`direct-tcpip` typed dial** (under `tunnel`) — the row the `-L`
leg and the alksocks `DialFn` compose over: `channel_open_direct_tcpip`
resolves to `Err(ChannelOpenFailure)` on server reject (POC-1 §6) —
the typed open, no `wait()` before `into_stream`; **the connection
stays usable after a declined open** (pinned by row). The
`direct-streamlocal` twin with the runtime-Unix gate (OQ-11
symmetry: universal compile, clean off-Unix decline).
- **Git exec consumption** (under `git`) — the same exec grammar the
server side parses (`git-{upload,receive}-pack '/repo'`),
**reused, not forked** — pointed upstream at a vanilla `sshd`:
parse → exec → the pack duplex pumped → exit-status consumed. The
per-length limits and service-only decline classes ride the shared
grammar's verdicts.
- **The sftp subsystem placeholder decline** (every build, OQ-13) —
the client-side half of the tested decline placeholder: a
`subsystem "sftp"` open declines cleanly in every build (the
feature exists, the route doesn't until alkfs lands). The
server-side rows already landed in wave 2; this lands the client
mirror so both directions are contract-tested.
- **Contract rows through the server double, per build** — the
`w4-dial` fake-`sshd` vehicle grown with the op-row surfaces:
featured + working rows (exec exit-status, pty window-change
ordering, direct-tcpip pump, git exec round trip), decline rows
(server-rejected opens, the sftp decline, off-Unix streamlocal),
the connection-stays-usable row after each decline class. The
real-tool smoke posture decided + recorded (a real `sshd` exec is
cheap where tooling permits — check-gated like the wave-3 smokes;
else named wave-5 e2e territory explicitly).
## Acceptance Criteria
- [ ] Exec/pty rows land under `session` with the POC-3 §D.3
mechanics pinned in code + rows (`exec()` not `request_exec`;
`wait()`-drain Success/Failure; `window_change`/`signal` before
`into_stream`; queue never consumed before conversion)
- [ ] `direct-tcpip` (+ streamlocal twin, runtime-Unix gate) lands
under `tunnel`: typed open, reject → `Err(ChannelOpenFailure)`,
the connection stays usable after a decline (row-pinned)
- [ ] Git exec consumption lands under `git` over the shared grammar
(no forked parser); the pack duplex pumps; exit-status consumed
- [ ] The sftp subsystem placeholder declines in every build (the
client-side OQ-13 mirror)
- [ ] Per-build contract rows green through the server double; the
smoke posture decided + recorded
- [ ] `cargo test` (+ per-feature + `--all-features`), clippy ×2
(`-D warnings`), fmt, `cargo doc --no-deps` pass
## References
- docs/architecture/client.md §Op rows (the table), §Correctness
facts, §What the crate does *not* include (job supervision is
assembly work — the crate gives channels + exit semantics)
- docs/architecture/mapping.md §The exit derivation contract (the
mirror the client consumes), docs/architecture/decisions/003-feature-graph.md
(the feature rows), docs/architecture/decisions/006-mapping-bridge-and-dispatch.md
(exec-only git, both directions)
- docs/research/poc-3-mapping-bakeoff.md §D.3 (the client mechanics —
the body of knowledge), docs/research/poc-1-auth-bridge.md §6 (the
open-call `Err` semantics), docs/research/reverse-runner.md (the
per-connection surface table — the consumer evidence)
- src/git_map.rs (the grammar reused), src/tty_map.rs (the exit-chunk
semantics consumed), russh 0.65 `client::Handle` (`channel_open_session`
/ `channel_open_direct_tcpip` / `channel_open_direct_streamlocal`)
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion
+109
View File
@@ -0,0 +1,109 @@
---
id: w4-reverse-listen
name: Wave 4 — the client -R leg (forward requests + the acceptor registry)
status: pending
depends_on: [w4-op-rows]
scope: moderate
risk: high
impact: phase
level: implementation
---
## Description
Land the client direction's `-R` leg (`client.md` §Op rows row 4 +
§The acceptor): the hub asks the vanilla server to listen, then
**accepts the server-initiated `forwarded-tcpip` opens** and routes
each to the family handler. The acceptor row is unavoidable and its
disposition is ours (reverse-runner fact 1) — russh's client default
is accept-and-drop, a half-capability the family forbids; the
`w4-dial` skeleton pins the fail-closed default, this task grows the
registry that serves:
- **The forward request rows** — `tcpip_forward(address, port) ->
bound` (russh's client `Handle` returns the actually-bound `u32`;
`port = 0` → the server-picked number surfaced — the lifecycle
primitive `client.md` §Lifecycle pins), `cancel_tcpip_forward`,
`streamlocal_forward` + `cancel_streamlocal_forward` with the
**runtime-Unix gate** (OQ-11 symmetry: universal compile, clean
off-Unix decline — the dial-call-witness posture the server side
landed).
- **The acceptor registry** — the assembly-mutable, per-channel-type
registry the client Handler routes server-initiated opens through:
served **only** under a registered acceptor **for that type and
under its feature** (`forwarded-tcpip` / `forwarded-streamlocal`
under `tunnel` is the real case); unregistered/unfeatured accepted
opens **declined cleanly** — never silently dropped after accept
(ADR-003 §4). Decide + record the registry's exact shape (per-type
slots vs a typed map; the acceptor trait signature — what an
accepted open hands the assembly: the channel duplex + the open's
originator fields) against the harness's `ForwardedAcceptor`
vocabulary (`w3-tunnel-listen` landed the first shape client-side
in tests — grow it into the crate's real surface, don't fork it).
- **Late-install across a live connection** (the alkminer
`install_forward_sink` dynamic — reverse-runner fact 2): acceptors
install/uninstall while the connection is up; consumers turn
capabilities on/off per connection. An open arriving before its
acceptor installs is declined (fail-closed at arrival time —
decide + record; no buffering of unclaimed opens).
- **The data leg** — a registered acceptor's open: the channel duplex
handed to the assembly handler (the pump is the acceptor's, per the
family handler shape); bounded bookkeeping (no unbounded registry,
no unbounded per-connection listener map — the `Limits` cap
relation decided + recorded, the server-side listener-counter
posture as the template).
- **Contract rows through the server double, per build** — the fake
`sshd` grown with the server-initiated-open surfaces: featured +
registered → served; featured + unregistered → clean decline;
unfeatured → clean decline (per-build matrices, the symmetry rule);
late-install rows (decline-before-install, serve-after); the
bound-port out-param row (port 0 mint); the cancel rows; the
off-Unix streamlocal rows; the bounded-bookkeeping rows. The
real-tool smoke posture decided + recorded (a real `ssh -R`-shaped
round trip against the double is the in-process form; real-`sshd`
`-R` is wave-5 e2e — never silently dropped).
## Acceptance Criteria
- [ ] `tcpip_forward` / `streamlocal_forward` (+ cancel forms) land
with the actually-bound port surfaced (the port-0 mint row);
the runtime-Unix gate compiles everywhere, declines cleanly
off-Unix
- [ ] The acceptor registry lands (shape decided + recorded): served
only under a registered acceptor under `tunnel`;
unregistered/unfeatured server-initiated opens declined cleanly
(never accept-and-drop) — wire rows per build
- [ ] Late-install works across a live connection; a pre-install
arrival is declined (decided + recorded, no unclaimed buffering)
- [ ] The data leg hands accepted opens to the assembly handler;
bookkeeping bounded (the cap relation decided + recorded)
- [ ] Per-build contract rows green through the server double; the
smoke posture decided + recorded
- [ ] `cargo test` (+ per-feature + `--all-features`), clippy ×2
(`-D warnings`), fmt, `cargo doc --no-deps` pass
## References
- docs/architecture/client.md §Op rows (row 4), §The acceptor
(fail-closed client-side), §Lifecycle primitives
- docs/architecture/decisions/003-feature-graph.md (§4 the
client-side fail-closed acceptor — the rule this task implements),
docs/architecture/decisions/006-mapping-bridge-and-dispatch.md
(route 1, both legs)
- docs/research/reverse-runner.md (facts 1–2 — the acceptor row's
disposition is ours; the late-install dynamic; lesson 1), docs/research/russh.md
§Client surface (the `server_channel_open_*` hook set; the
`tcpip_forward -> u32` signature)
- docs/research/poc-3-mapping-bakeoff.md §C.4 (the fabric recipe —
the family-side half the accepted opens feed)
- tests/common/harness.rs (`ForwardedAcceptor` — the vocabulary to
grow, not fork), src/tunnel_map.rs (the server-side listener
bookkeeping posture as the template)
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion
@@ -0,0 +1,63 @@
---
id: w4-router-collision
name: Wave 4 — ListenRouter token-collision pre-check (wave-3 gate booked item 4)
status: pending
depends_on: [w3-tunnel-listen]
scope: single
risk: low
impact: isolated
level: 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 — 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 `register` on an already-held token **refuses**
(the listen declines fail-closed) instead of silently replacing the
held route — a collided mint can never steal a live adapter's
arrival routes.
- The error surface: reuse/extend the `AdapterError::ListenRegistration`
class (decide + record — 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 — the test forces 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::register` refuses 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-deps` pass
## References
- tasks/implementation/review-wave-3.md §Booked findings item 4 (and
§Minor issues — the `install_forwarded_surface` pre-check this
mirrors)
- src/tunnel_map.rs (the `ListenRouter`), src/error.rs (the
`AdapterError` class), docs/architecture/decisions/ (the fabric's
ADR-008 mint discipline — the upstream guarantee the pre-check
defends in depth against)
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion
+63
View File
@@ -0,0 +1,63 @@
---
id: w4-tty-coverage
name: Wave 4 — tty coverage minors (wave-3 gate booked item 3)
status: pending
depends_on: [w4-channel-not-held-rows]
scope: narrow
risk: low
impact: component
level: implementation
---
## Description
Close the wave-3 gate's booked finding item 3
(`review-wave-3` §Booked findings — "minors, one ticket's worth"):
- **The `channel_close`-fired per-channel cancel path is
unexercised on the wire** — only the teardown signal is. Land the
wire row: a channel closing (client-side close on a served tty
row) fires the per-channel cancel path (the cap release + the
per-channel state teardown observed — the disjoint release sites
the gate verified per row, now wire-witnessed for this path).
- **The Closed/Break no-exit-chunk close shape has no wire row** —
land the row(s): a session ending via the Closed/Break close shape
emits **no exit chunk** (the derivation contract's close-shape
class — the absent-report posture pinned, not just the happy
paths).
- **The tty stdout `write_all` carries no deadline** — record as a
**known limit only** (module docs): backpressure is bounded, liveness
is covered by the inactivity timeout; no code change (the gate's
recorded disposition — a deadline here is a behavior change, not a
coverage minor).
Depends on `w4-channel-not-held-rows` to keep `tests/tty_mapping.rs`
single-writer (that task's rows land first; these land into the
settled file).
## Acceptance Criteria
- [ ] Wire row: the `channel_close`-fired per-channel cancel path
(cap release + state teardown observed on the wire)
- [ ] Wire row(s): the Closed/Break no-exit-chunk close shape
- [ ] The stdout `write_all` no-deadline limit recorded in module
docs (no code change)
- [ ] `cargo test` (+ per-feature + `--all-features`), clippy ×2
(`-D warnings`), fmt, `cargo doc --no-deps` pass
## References
- tasks/implementation/review-wave-3.md §Booked findings item 3
- docs/architecture/mapping.md §The exit derivation contract (the
close-shape classes; the PTY-path rule-5 limit — the docs-posture
template), src/tty_map.rs, src/channel_cap.rs (the release sites
the rows witness)
- tests/tty_mapping.rs (the per-build matrix the rows join)
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion