diff --git a/docs/plans/implementation.md b/docs/plans/implementation.md index cd5424d..c10efc8 100644 --- a/docs/plans/implementation.md +++ b/docs/plans/implementation.md @@ -1,7 +1,8 @@ --- status: draft -last_updated: 2026-10-28 (initial plan — five waves committed; wave 1 - decomposed same day, see its decomposition record) +last_updated: 2026-10-29 (wave 4 decomposed after the wave-3 gate + closed green — absorbing the gate's four booked findings; waves 1–3 + decomposed + implemented + gated 2026-10-28/29) --- # alkssh — Implementation plan @@ -46,7 +47,7 @@ mappings ∥ client direction) → validation & release. | [1](#wave-1--foundations) | Manifest landing (russh pin, feature graph); error model; config surface (serve-config + dial-config + `Limits`); vault-backed key sources; host-key policy seam; fingerprint; test doubles | — | decomposed | | [2](#wave-2--the-server-stack) | `SshAdapter::handle` mount + `Config` construction; the auth bridge; parse-only dispatch + fail-closed decline core; spawn-and-return plumbing; bounded resources; loopback harness | wave 1 | decomposed | | [3](#wave-3--server-mappings) | session → alktty (translation bridge + exit derivation); git → alkgit (exec grammar + dispatch); tunnel → alktunnels route 1; sftp placeholder declines | wave 2 | decomposed | -| [4](#wave-4--client-direction) | Dial surface; op rows (exec/pty, direct-tcpip, git exec); the `-R` leg (listen + acceptor registry); lifecycle primitives | wave 1 | pending | +| [4](#wave-4--client-direction) | Dial surface; op rows (exec/pty, direct-tcpip, git exec); the `-R` leg (listen + acceptor registry); lifecycle primitives | wave 1 | decomposed | | [5](#wave-5--validation--release) | Decline-matrix contract suite; real-tool e2e; fuzzing; docs; **the publish** | waves 3 + 4 | pending | ## Wave 1 — Foundations @@ -385,6 +386,62 @@ wave 1 in principle, but decomposing it after wave 3's gate keeps the session task sets in rhythm and lets it absorb any seam corrections the mappings surfaced). +**Decomposition record** (2026-10-29, after the wave-3 gate closed +green): wave 4 decomposed into eight task files under +`tasks/implementation/` plus the gate. The wave-3 gate's four booked +findings are folded in as dedicated tasks (the gate's own +disposition — "wave-4 material"), not wave-3 backports: the +hardening set is server-side and independent of the client chain — +`w4-git-exit-close` (booked item 1: the git row's status-before-close +by construction — the seam/shape decided + recorded in-task), +`w4-channel-not-held-rows` (item 2: the guard's wire rows on all +three rows, per build — test-only), `w4-tty-coverage` (item 3: the +`channel_close`-fired cancel path + the Closed/Break no-exit-chunk +rows + the write_all no-deadline known-limit record), and +`w4-router-collision` (item 4: the `ListenRouter` token-collision +fail-closed pre-check); the gate's item 5 (the upstream asks) stays +booked with its owners — no task, nothing absorbed. The client chain +is deliberately sequential — `w4-dial` first (the russh client stack ++ the connection handle + the client Handler skeleton at fail-closed +defaults, over an in-process fake-`sshd` server double this task +lands), then `w4-op-rows` (exec/pty, direct-tcpip, git exec over the +shared grammar, the sftp client-side decline — the POC-3 §D.3 +mechanics pinned), then `w4-reverse-listen` (the `-R` leg: forward +requests + the acceptor registry + late-install — the wave's +highest-risk task, growing the harness's `ForwardedAcceptor` +vocabulary into the crate's real surface), then `w4-lifecycle` (the +OQ-12 primitives); `review-wave-4` (the gate) depends on all eight. +As with waves 2–3, the chain is sequential *logistically* (the dial, +op rows, and listen leg all edit the same client Handler impl) even +where the branches are independent *conceptually*; the hardening +tasks carry no dependency on the client chain (disjoint files) and +are scheduled first as cheap remediation. + +Interpretations recorded (flagged for the wave-4 gate): + +- **The client Handler skeleton lands fail-closed in `w4-dial`, the + acceptor registry in `w4-reverse-listen`** — the dial task pins the + decline-all default for server-initiated opens (the ADR-003 §4 + rule) and the listen task grows the registry that serves them; the + split mirrors wave 2's mount-before-mappings shape and keeps the + dial task's surface reviewable. +- **The client-direction test vehicle is an in-process fake `sshd`** + (a hand-rolled russh server double in `tests/common`) — the + loopback harness drives the *server* direction and is not the + vehicle here; real-`sshd` client-direction e2e stays wave-5 + territory (the plan's wave-5 bullet), with per-task check-gated + real-tool smokes where cheap (the wave-3 smoke posture). +- **No `DialConfig` course-correction is anticipated** — the wave-1 + shape already carries everything `client.md`'s dial surface names; + `w4-dial` decides + records if that proves wrong (the + `w3-seam-config` pattern), and the acceptor registry is + client-connection state, not serve-config. +- **The hardening tasks are ordered by single-writer needs, not + risk** — `w4-channel-not-held-rows` follows `w4-git-exit-close` + (both touch `tests/git_mapping.rs`) and `w4-tty-coverage` follows + the rows task (both touch `tests/tty_mapping.rs`), the wave-3 + no-two-tasks-hold-one-file lesson applied to the test binaries. + ## Wave 5 — Validation & release - **The decline-matrix contract suite** (`mapping.md`'s table) — every diff --git a/tasks/implementation/review-wave-4.md b/tasks/implementation/review-wave-4.md new file mode 100644 index 0000000..ce13558 --- /dev/null +++ b/tasks/implementation/review-wave-4.md @@ -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 \ No newline at end of file diff --git a/tasks/implementation/w4-channel-not-held-rows.md b/tasks/implementation/w4-channel-not-held-rows.md new file mode 100644 index 0000000..7b54ab7 --- /dev/null +++ b/tasks/implementation/w4-channel-not-held-rows.md @@ -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 \ No newline at end of file diff --git a/tasks/implementation/w4-dial.md b/tasks/implementation/w4-dial.md new file mode 100644 index 0000000..cc2856f --- /dev/null +++ b/tasks/implementation/w4-dial.md @@ -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 \ No newline at end of file diff --git a/tasks/implementation/w4-git-exit-close.md b/tasks/implementation/w4-git-exit-close.md new file mode 100644 index 0000000..cfbe651 --- /dev/null +++ b/tasks/implementation/w4-git-exit-close.md @@ -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` 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` 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 \ No newline at end of file diff --git a/tasks/implementation/w4-lifecycle.md b/tasks/implementation/w4-lifecycle.md new file mode 100644 index 0000000..001e13e --- /dev/null +++ b/tasks/implementation/w4-lifecycle.md @@ -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 \ No newline at end of file diff --git a/tasks/implementation/w4-op-rows.md b/tasks/implementation/w4-op-rows.md new file mode 100644 index 0000000..e45dbce --- /dev/null +++ b/tasks/implementation/w4-op-rows.md @@ -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 \ No newline at end of file diff --git a/tasks/implementation/w4-reverse-listen.md b/tasks/implementation/w4-reverse-listen.md new file mode 100644 index 0000000..2986465 --- /dev/null +++ b/tasks/implementation/w4-reverse-listen.md @@ -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 \ No newline at end of file diff --git a/tasks/implementation/w4-router-collision.md b/tasks/implementation/w4-router-collision.md new file mode 100644 index 0000000..4cd2878 --- /dev/null +++ b/tasks/implementation/w4-router-collision.md @@ -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 \ No newline at end of file diff --git a/tasks/implementation/w4-tty-coverage.md b/tasks/implementation/w4-tty-coverage.md new file mode 100644 index 0000000..7745ac7 --- /dev/null +++ b/tasks/implementation/w4-tty-coverage.md @@ -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 \ No newline at end of file