--- status: closed last_updated: 2026-10-30 (the plan closed at the publish of alkssh 0.1.0, tag `alkssh-v0.1.0`; waves 1–5 decomposed + implemented + gated 2026-10-28/30) --- # alkssh: Implementation plan Wave-based decomposition of the architecture (`docs/architecture/`) into units of implementable work. The sibling pattern proven on alkgit/alkstore/alkblobs, adopted deliberately as a deviation from the SDD process's decompose-everything-upfront step: early waves change the shape of later ones (wave 1's config/error/key-source types are what every later task implements against; wave 2's server stack is what every mapping composes into), and decomposing only the next wave at a time keeps each session's task set reviewable and lets later decompositions absorb earlier waves' course corrections: reviews sometimes change things, and tasks downstream then have to be edited, so decomposing too far ahead is paid rework. Rhythm: decompose a wave → implement it → review gate → decompose the next wave. Task files live in `tasks/implementation/` (frontmatter carries the categorical estimates; the DAG ordering keeps parallel branches from stepping on each other). Wave boundaries are also review boundaries. Each wave's section here carries a **decomposition record** added when that wave actually decomposes (not written upfront). Scope authority: **v0.1.0 publishes the crate**: the complete two-direction door (server stack + all mappings, client stack + op rows, key sources, the decline matrix contract-tested per build). The sftp decline placeholder *is* shipped behavior (OQ-13 resolved). **The assembly stays out of these waves**: alkforge's mounting of `SshAdapter::handle` at an alkcall `channel/open` boundary, its feature/ACL selection, its `IdentityProvider` and serve-config values are downstream composition work; the crate ships the door, not the deployment. No binary, no workspace split (single crate, the alktty root pattern; no tracks: one crate, one publish). ## The waves Dependency logic in one line: foundations → server stack → (server mappings ∥ client direction) → validation & release. | Wave | Contents | Depends on | Status | |---|---|---|---| | [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 | none | 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 | decomposed | | [5](#wave-5-validation--release) | Decline-matrix contract suite; real-tool e2e; fuzzing; docs; **the publish** | waves 3 + 4 | published | ## Wave 1: Foundations The contract surface in code, engine-blind: everything later waves implement *against*, and nothing that needs a live SSH session to test. The alkssh analog of alkgit's seam wave. - **Manifest landing** (ADR-002): the dependency set lands with implementation start: `russh = "0.65"` with `aws-lc-rs` (no `flate2`, no `rsa`; the tree RC-free except russh's own `ssh-key` pin), `alkcall 0.10` (identity/ACL types at the wire layer), `alkvault 0.1` (the custody model), plus the standing toolkit (`tokio`, `thiserror`, `tracing`, `parking_lot`, `zeroize`). `edition = "2024"`, `rust-version = "1.89"`. The ADR-003 feature graph is declared from the first commit: `session`, `git`, `tunnel`, `sftp`, `default = []` `default = []`: features gate nothing yet (the decline core references them from wave 2); the per-feature sibling deps (`alktty` under `session`, `alktunnels` under `tunnel`, `alkgit` under `git`) land with their first consuming wave, not as dead manifest entries. All family deps pinned by version, never sibling-checkout paths; pins verified against crates.io before committing. - **Error model**: the crate's `thiserror` taxonomy, fail-closed with no catch-alls: the key-source failure matrix as distinct variants (ADR-005 decision 4: locked / absent / decryption failure / unknown `key_version`, plus decode failure; the vault's taxonomy reshaped, never collapsed). Auth declines are *dispositions* (`Auth::Reject`), not errors; wire decline reasons are wave-2 shaped. What the config surface needs beyond this lands with it. - **Config surface**: the pure configuration types both directions implement against: the **serve-config** (feature wiring; OQ-08's two assembly-supplied fields: shell argv and exec wrapper argv, each `Option`, unset → its row declines, never a fallback; the `IdentityProvider` handle; the host-key source handle; the tunnel `ChannelClient` handle; the `Limits`), the **dial-config** (address, key source, `HostKeyPolicy` handle, keepalive/inactivity/window knobs), and **`Limits`**, the bounded-resources budget model (`overview.md`'s table: in-crate max-concurrent-channels cap, exec-parse length caps, and the russh knob mapping with the documented default postures: `max_auth_attempts` 10 sized for the counting `none`-probe, keepalives 15 s / 3 client-side per the reverse-runner evidence). - **Vault-backed key sources** (ADR-005): the custody model in code: decrypt-at-use (`vault.decrypt` → `Zeroizing` wrap at the adapter seam → one-time `decode_secret_key(plain, None)`), the client dial-key source and the server host-key load path (decrypt-at-construction; locked vault → no server constructed), `EncryptedData` opaque to the crate, plaintext never persists, every failure mode mapped to its distinct error variant. - **Host-key policy seam** (OQ-05): the `HostKeyPolicy` trait the client dial calls with (host, port, presented fingerprint): case-1 pins (hard-decline on mismatch) and case-2 assembly policies are the implementor's; the crate ships the fail-closed default (reject unknown) and is otherwise policy-blind; no accept-all built-in. - **Fingerprint** (ADR-004): the `SHA256:` + unpadded base64 computation via the `ssh-key` re-export (`HashAlg::Sha256`); the string format is a contract invariant, proven against independent issuers (`ssh-keygen -lf` vectors, the POC-1 §1 checkpoint shape). - **Test doubles**: the in-memory seams every later wave's contract tests run against: an in-memory `IdentityProvider` (registered fingerprints → identities), an in-memory vault (encrypt/decrypt round-trip + the failure postures), an in-memory `HostKeyPolicy`, and ed25519 test-key fixtures. The convention (adapted from the sibling crates) is established here even though the harness that exercises them end to end lands with wave 2's server stack. The wave-1 review gate is the cheap-place gate: the config types, the error taxonomy, and the key-source seam are one-way doors in practice (every later task implements against them; the published crate freezes them). A shape error found at this gate costs a task revision; found after the stack and mappings implement against them it costs churn across both directions. The gate reads the landed surface against ADR-002/003/004/005 and the OQ-08/05 resolutions line by line. **Decomposition record** (2026-10-28): wave 1 decomposed into eight task files under `tasks/implementation/` (flat, one track, no per-track subdir). Shape: `w1-manifest` first (the ADR-002 dependency landing + the ADR-003 feature graph declaration everything lands inside); then four independent foundations in parallel: `w1-error-model` (the `KeySourceError` five-variant taxonomy), `w1-fingerprint` (the contract-invariant computation + parity vectors), `w1-key-sources` (the ADR-005 custody chain, over manifest + error model), and `w1-host-key-policy` (the OQ-05 seam + fail-closed default); then `w1-config` (serve-config + dial-config + `Limits`) over the key-source and policy seams it carries handles for; `w1-test-doubles` (the in-memory seams + ed25519 fixtures) over error model + key sources + policy; and `review-wave-1` (the cheap-place gate) depending on all seven. Interpretations recorded (flagged for the wave-1 gate): - **Per-feature sibling deps land with their first consuming wave**, not in the wave-1 manifest: `alktty`/`alktunnels`/`alkgit` would be dead manifest entries in a build where the features gate nothing yet; the wave-1 manifest lands russh + alkcall + alkvault + the toolkit only. The feature *names* are declared from the first commit; the wave-2 decline core is their first `cfg` consumer. - **No config-error type ships in wave 1** unless the config task finds one needed: the OQ-08 fields are `Option` and decline at request time (never construction time), so the error model carries only the key-source taxonomy; wire decline reasons are wave-2 shaped. - **The policy verdict type** (bool vs a richer reason-carrying verdict) is pinned in `w1-host-key-policy` against the russh `check_server_key` hook shape at this pin; flagged for the gate. - **The vault-double shape** (wrap alkvault's in-memory construction if it ships one, else a local double behind the source trait) is pinned in `w1-test-doubles`; flagged for the gate. ## Wave 2: The server stack The russh server substrate every mapping composes into (the door frame, before any door is hung): - **`SshAdapter::handle` + `Config` construction** (ADR-001/002): the mount point ADR-001 pins as the `alk/ssh` receiving endpoint: a generic stream in, the full russh server stack per stream (edge termination only: no ALPN code, no relay leg; the pin is a ledger entry, already decided). `Config` built from the serve-config: honest advertisement (`methods = {publickey}` exactly, ADR-004 decision 4), host keys from the vault-backed source (locked → no server constructed), the bounded knobs from `Limits`. - **The auth bridge** (ADR-004): the dispositional handler surface: `auth_publickey` fingerprint → `resolve_from_fingerprint` → accept with the resolved identity / reject fail-closed; ed25519-only (non-ed25519 keys declined explicitly); `auth_publickey_offered` accept-and-delegate (counting stays honest); `auth_none` / `auth_password` / `auth_keyboard_interactive` / `auth_openssh_certificate` explicit declines (family contract, not inherited defaults); the username carries no semantics (OQ-03). - **Parse-only dispatch + the fail-closed decline core** (ADR-006 as amended, ADR-003): session-channel dispatch skeleton (parse path: git-parseable exec → `git`, else → `session`; request-less session channels decline), unfeatured channel types decline at open, x11 / `auth-agent` always decline (never feature-gated), the sftp subsystem decline-with-reason placeholder (OQ-13, folded here server side), **decline first, alloc never** (the POC-3 stage-A matrix: unknown repo → collapsed generic denial before any protocol byte). - **Spawn-and-return plumbing** (ADR-006 decision 4): the structural constraint in code: every serving path `tokio::spawn`ed, handlers return fast, exit/status reporting from spawned tasks rides the cloned `Session::handle()`. The reporting *mechanics* land here; the derivation contract's rules are wave-3 session-mapping material. - **Bounded resources**: the in-crate concurrent-channel cap enforced at the handler surface, `inactivity_timeout`/`max_auth_attempts` wired from `Limits`, per-length exec-parse limits reserved for the grammar's consumption (wave 3). - **Loopback harness**: the in-process client/server test pair the decline matrix and every mapping's contract tests run through (in-memory doubles from wave 1; real-tool smokes ride the mapping waves). Wave 2 decomposes after the wave-1 gate, absorbing whatever it corrects. **Decomposition record** (2026-10-28, after the wave-1 gate closed green): wave 2 decomposed into six task files under `tasks/implementation/` plus the gate. Shape: `w2-server-mount` first (the `SshAdapter` mount + russh `Config` construction from the serve-config, the substrate, with the Handler skeletoned at its fail-closed defaults); then `w2-auth-bridge` (the ADR-004 dispositions fill the auth hooks, over the mount + the wave-1 fingerprint path and doubles); then `w2-loopback-harness` (the in-process client/server pair, the contract-test vehicle, proving the doubles live through the stack); then `w2-dispatch-decline-core` (parse-only dispatch + the fail-closed decline core, contract-tested per feature build through the harness); then `w2-spawn-return` (the structural constraint pinned by test + the exit-reporting mechanics via the cloned `Session::handle()`); then `w2-bounded-resources` (the in-crate channel cap at the handler surface); and `review-wave-2` (the gate) depending on all six. The chain is deliberately sequential: the auth hooks, the dispatch, and the cap all edit the same per-connection Handler impl, so no two tasks hold it in parallel. Interpretations recorded (flagged for the wave-2 gate): - **The git-parseability probe in wave 2 is skeleton-level** (service prefix + path shape), which is enough to contract-test the routing decision and the session-build's git-gated decline; the full grammar (quoted/unquoted paths, service-only → decline-with-reason, per-length limits) is wave-3's git task, which replaces the probe. - **Wave-2 routing stubs decline cleanly at the family-seam boundary**: the seams land in wave 3; the stub behavior is documented as skeleton so the routing decisions are contract-tested now. The spawn-and-return mechanics are proven on a stub serving path. - **The adapter error surface is shaped in `w2-server-mount`**: the construction error wrapping `KeySourceError` (locked → no server) plus the Handler `type Error` (`From`); this is where the wave-1 note's "wire decline reasons are wave-2 shaped" lands. - **The harness transport and placement are pinned in `w2-loopback-harness`** (duplex pair vs TCP loopback; module placement beside the wave-1 doubles); flagged for the gate. ## Wave 3: Server mappings Three parallel branches over the stack (plus the folded sftp row), each carrying a minimal real-tool smoke (one clone, one `-L`, one pty session: the POC-3 gates re-landed cheaply); the full matrix is wave 5's: - **session → alktty**: the translation bridge, not a byte pump (POC-3 §D.1): `pty_request` args → `TerminalParamsWire` (SSH modes → the reserved modes JSON); shell argv / exec wrapper from the serve-config (`wrapper ++ [cmd]`, never naively split; unset → clean decline, per OQ-08); stdin/stdout/stderr chunk translation (`extended_data(1)` for stderr); `window_change_request`/`signal` rows; `env_request` declined unconditionally (OQ-09); the **exit derivation contract** (`mapping.md` rules 1–6: `code ≥ 0` → `exit-status(N)`, negative `−N` → `exit-signal(name)` via the server-side Unix signal-number→name table, `−1` → `exit-status(255)` never a signal claim, `core_dumped` never asserted, PTY-path rule-5 fidelity limit); exit-status sent before close, from the spawned task. - **git → alkgit**: exec-only (ADR-006 decision 2), with the parse grammar (`git-{upload,receive}-pack '/repo'`, quoted/unquoted paths, service-only → decline-with-reason, non-git → parse-then-fail, per-length limits), unknown-repo → collapsed generic denial before any protocol byte (no existence oracle), dispatch to `serve_duplex_resolved(repo_id, identity, service, duplex, limits, …)` over the channel duplex, process-mode dialect, exit-status both directions. - **tunnel → alktunnels route 1** (ADR-006 decision 1): the adapter-side bridge taking the serve-config's `ChannelClient` into the family fabric (identity propagated once via `set_identity` so `AccessControl::check` gates the mapped opens, the visible-surface invariant), `TunnelSession::open_direct` + `take_halves()` pumped against the russh channel, accept only after the fabric open succeeds (failure → explicit `ConnectFailed` reject), `direct-streamlocal` runtime-Unix-gated (OQ-11: universal compile, clean open-decline off-Unix), the `forwarded-tcpip` handler shape, the `-R` listen flow (`tcpip_forward`/`streamlocal_forward` + the same runtime-Unix gate). - **sftp placeholder**: the decline-with-reason rows (folded into the dispatch task server-side; the client-side placeholder folds into wave 4's acceptor/op-row work; the wave-5 matrix covers both explicitly). Wave 3 decomposes after the wave-2 gate, absorbing whatever it corrects. **Decomposition record** (2026-10-28, after the wave-2 gate closed green): wave 3 decomposed into six task files under `tasks/implementation/` plus the gate. Shape: `w3-seam-config` first (the serve-config's per-feature seam handles + the optional `alktty`/`alkgit` deps; see the interpretations; the mappings dispatch against them, so it lands before the mappings); then `w3-decline-tracing` (the wave-2 gate's booked finding: the decline/ serve-path event vocabulary, landed **before** the mappings so they inherit a uniform shape); then the three mapping branches (deliberately sequential): `w3-git-mapping` (the full grammar replaces the wave-2 probe + the `serve_duplex_resolved` dispatch + the store-and-take handoff it lands first for the session task to reuse), `w3-tty-session` (the translation bridge + the exit-derivation contract, the biggest single-row surface), `w3-tunnel-direct` (the POC-proven route-1 bridge leg + identity `set_identity` propagation), and `w3-tunnel-listen` (the `-R` listen flow + `forwarded-tcpip` data leg: the POC-untested leg, sized with the stop-and-book provision); and `review-wave-3` (the gate) depending on all six. As with wave 2, the branch DAG is sequential: the three mappings replace stubs in the same `Handler` impl and share the decline-matrix test binaries, so no two tasks hold them in parallel: the branches are parallel *conceptually* (independent seams), sequential *logistically*. Interpretations recorded (flagged for the wave-3 gate): - **The serve-config extension is wave 3's first course correction to the wave-1 config surface**: the assembly-supplied alktty/alkgit seam handles the mappings' seam signatures take (the tty backends map + selector + ownership provider; the alkgit backend quartet + pipeline semaphore + push-options flag) were never carried by the wave-1 config bullet, and `w3-seam-config` lands them feature-gated with the optional sibling deps (`alktty = "0.8.1"`, `alkgit = "0.1.0"`, crates.io-verified at decomposition) before the mappings; `alktunnels = "0.4.1"` still lands with its first consuming code (`w3-tunnel-direct`) to avoid a dead entry. The unset-declines posture extends to the new handles (unset → the owned rows decline even with the feature on). - **The server-side sftp bullet is already discharged**: the OQ-13 decline rows landed with `w2-dispatch-decline-core` (every subsystem name declines in every build) and are already contract-tested per build in `tests/dispatch_core.rs`; no wave-3 task exists for it: the client-side placeholder folds into wave 4's acceptor/op-row work and the wave-5 matrix covers both rows explicitly. - **The `-R` listen flow carries the wave-3 gate's biggest open**: the fabric-side inbound-forwarded mechanics (who dials the forwarded op at the door, whether the assembly-supplied `Arc` composition can carry the door-side accept or the serve-config's tunnel handle must grow) is decided in `w3-tunnel-listen` against alkcall 0.10.1 / alktunnels 0.4.1's published surfaces; if the topology does not close, the task stops and books it for the gate; nothing is silently forked or absorbed. - **The wave-2 gate's three booked items all have wave-3 homes**: the decline-path tracing task (`w3-decline-tracing`), the forwarded- tcpip acceptor vocabulary (`w3-tunnel-listen`, with the client-side accept hook landing in the harness), and the deferred identity- landing assertion (each mapping asserts the resolved identity reaches its seam; the tunnel branch additionally propagates it via `set_identity`; see auth.md §Server direction step 4). ## Wave 4: Client direction The door outward (independent of wave 3; shares only wave 1's foundations), each branch carrying its real-tool smoke where cheap: - **Dial surface** (`client.md`): connect with address, key source (decrypt-at-dial, fail-closed on locked/absent; never env vars), host-key policy at `check_server_key` (the wave-1 seam consumed: case-1 pins hard-decline on mismatch, case-2 policies decide), the bounded config (keepalives 15 s / 3, inactivity, window knobs). The authenticated connection handle everything else rides. - **Op rows**: family op → vanilla translation per `client.md`'s table: session exec/pty opens (the POC-3 §D.3 mechanics: `Channel::exec()` not `request_exec`, Success/Failure via `wait()`-drain not `Err`, `window_change`/`signal` before `into_stream()`, never consume the queue before conversion); `direct-tcpip` typed dial (reject → `Err(ChannelOpenFailure)`, the connection stays usable); git exec consumption (the same grammar the server parses, pointed upstream); the sftp subsystem placeholder decline. - **The `-R` leg**: `tcpip_forward(address, port) -> bound` (the actually-bound port returned), server-initiated `forwarded-tcpip` opens routed to the **acceptor registry**: served only under a registered feature's acceptor, unregistered/unfeatured accepted opens declined cleanly (never russh's accept-and-drop default), late-install across a live connection (the alkminer dynamic), `streamlocal_forward` with the runtime-Unix gate. - **Lifecycle primitives** (OQ-12): explicitly cancellable listener/session handles, clean `Disconnect::ByApplication`; **no reconnect or restore policy owned by the crate** (assembly policy; the primitives make it possible). Wave 4 decomposes after the wave-3 gate (it may start any time after 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 with the POC-3 §D.3 mechanics pinned), then `w4-reverse-listen` (the `-R` leg: forward requests + the acceptor registry + late-install, making it 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 build (none / `git` / `session` / `tunnel` / `--all-features`) × every row it can appear in × both directions, including the sftp placeholder rows and the never-features (x11, `auth-agent`). The fail-closed invariant is contract territory on both sides of every feature (AGENTS.md conventions 5–6). - **Real-tool e2e**: the POC-3 gates re-landed against the shipped crate: OpenSSH (pty/shell/exec, `-L`, `-R`, `-D`'s `direct-tcpip` leg, decline observability), git 2.43+ (clone/push through the git mapping), curl through a `-D` proxy; client direction against a real `sshd` (the reverse-runner shape: exec, direct-tcpip dial, `-R` listen + acceptor). - **Fuzzing**: the family standing rule (anything worth fuzzing gets fuzzed) on the crate's ingress surfaces: the exec parse grammar, the fingerprint format path, the key-decode path, the TerminalParamsWire conversion. The `fuzz/` sidecar convention (detached runner, corpus gate joining the verification list). - **Release readiness**: crate docs (public-API rustdoc warning-free; docs honesty), the feature matrix (`default`, each feature over it, `--all-features`), CHANGELOG/README, publish dry-run, then **the publish of `alkssh 0.1`**, the gate review that closes the plan. **Decomposition record** (2026-10-30, after the wave-4 gate closed green, with the gate's verdict: "wave 5 decomposition is unblocked and should absorb nothing beyond its own plan"; the wave-3 gate's item-5 upstream asks verified still booked with their owners, no task): wave 5 decomposed into seven task files under `tasks/implementation/`. Shape: four independent implementation tasks over the landed waves: `w5-decline-matrix` (the coverage audit of `mapping.md`'s matrix table cell by cell + the gap rows in the existing per-build binaries + the matrix runner joining the verification list), `w5-e2e-server` (OpenSSH pty/shell/exec/`-L`/`-R`/`-D`+curl, decline observability, git clone **and** push in a dedicated new e2e binary, the wave-3 smokes re-landed/extended), `w5-e2e-client` (the reverse-runner shape against a spawned real `sshd`: the sshd fixture, exec/direct-tcpip/ `-R`+acceptor, the decline rows), and `w5-fuzz-harness` (the `fuzz/` sidecar per the alkgit convention: four targets (exec_parse, fingerprint, key_decode, terminal_params), the shared-library corpus gate joining the verification list, the detached runner, the root workspace `exclude`); then `w5-release-readiness` (rustdoc warning-free × builds, README lands, the CHANGELOG 0.1.0 entry, the feature matrix, the publish dry-run + package-policy file-list check, the freeze inventory recorded for the gate) over all four; `review-wave-5` (the release gate, a **pure review**) over all five; and `w5-publish` (the publish checklist: manifest sanity, the full verification set, the dry-run, the crate-qualified `alkssh-v0.1.0` tag, `cargo publish`, the plan's closure recorded) over the gate. The four implementation tasks are mutually independent by construction (disjoint files: the matrix task owns the existing per-build binaries, the e2e tasks own new binaries + their own fixture modules, the fuzz task owns `fuzz/` + at most `#[doc(hidden)]` exposures), parallel *conceptually*; any src/ fix either finds is fixed in-task, with the gate as the merge point. Interpretations recorded (flagged for the wave-5 gate): - **The gate-then-publish split**: the plan's release-readiness bullet folds the publish into release readiness, but the decomposition follows the alkgit pattern: `review-wave-5` stays a pure review and `w5-publish` is its own task (the publish is a one-way door; it executes only over a green gate). - **The e2e vehicle/placement**: dedicated new test binaries (`tests/e2e_server.rs` / `tests/e2e_client.rs`, reusing `tests/common`) rather than extensions of the wave-3/4 mapping binaries, keeping those single-writer for the matrix task; the check-gated skipped-with-reason posture (the wave-3 smoke convention) holds for every row. Flagged for the gate. - **The matrix runner vehicle** (script vs documented command list) is decided + recorded in `w5-decline-matrix`; the corpus gate joins the verification list alongside it (the alkgit posture). - **The fuzz exposure shape** (public API vs `#[doc(hidden)]` fuzz-visible exposures per target) is decided + recorded in `w5-fuzz-harness` (the alkgit pattern); the sidecar is excluded from the root single-member workspace and never a root dep. - **The tag convention**: crate-qualified `alkssh-v0.1.0` (the alkgit convention; a bare `v0.1.0` would be ambiguous across the family's multi-crate repos). Pinned in `w5-publish`; flagged for the gate to confirm. - **No version bump**: the manifest already carries 0.1.0; the publish task verifies rather than bumps. ## Plan closure (2026-10-30) The plan closed at the publish of **`alkssh 0.1.0`**: the publish commit, tagged **`alkssh-v0.1.0`** (the crate-qualified alkgit tag convention) and pushed to `origin/main`. Scope authority discharged: the complete two-direction door shipped (server stack + all mappings, client stack + op rows, key sources, host-key policy seam, the decline matrix contract-tested per build per `scripts/matrix.sh` (see AGENTS.md's verification list), the assembly stays out (seams only; identity/ACL/fabric policy is downstream composition), no binary, single crate. All five review gates closed green (waves 1–4 2026-10-28/29; the wave-5 release gate 2026-10-30). **The freeze inventory is one-way from the publish** (recorded in `w5-release-readiness.md` §The freeze inventory, verified accurate against the landed code at the wave-5 gate): the mapping contracts, the honest-advertisement posture, the decline-matrix behavior, the fingerprint string format, the russh 0.65 pin's wire posture. Further wire-relevant change is a new ADR + a semver event. Still booked with their owners (nothing absorbed through the release): the wave-3 gate's item-5 upstream asks (the alktunnels/alkcall non-detaching post-construction identity seam; mapping.md's correlation-miss event-vocabulary nuance) and the `ssh-key` PPK parse-echo ask recorded in `w5-fuzz-harness.md` (the tracker filing is owner-territory). ## References - `docs/architecture/`: overview, mapping, client, auth, open-questions (all reviewed 2026-10-28), ADR-001…006 - `docs/research/`: vision, russh inventory, auth bridge, mapping table, POC-1/2/3 findings, reverse-runner extraction - Family ADR-003 (the structural decision); the alkgit implementation plan (the pattern this follows)