Renames the written-usage rule into practice across the contract surface: docs/ (architecture, research, plans, sdd), tasks/ (all 43 task files, taskgraph validate passes), AGENTS.md, README.md, CHANGELOG.md, and the .opencode/agents/ prompts. Every replacement is a sense-preserving rewrite (colons, semicolons, commas, parentheses, sentence splits, varied), never a mechanical swap; no spaced hyphens were substituted. Enum fields, frontmatter, tables, wire data, and cited identifiers survive verbatim; en dashes inside numeric ranges keep their meaning. Verification: taskgraph validate (43 tasks), repo grep clean (single exempt hit: the verification command's own search pattern in AGENTS.md), cargo test, clippy --all-targets -D warnings, fmt --check, doc all green.
32 KiB
status, last_updated
| status | last_updated |
|---|---|
| closed | 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 | 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 | 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 | session → alktty (translation bridge + exit derivation); git → alkgit (exec grammar + dispatch); tunnel → alktunnels route 1; sftp placeholder declines | wave 2 | decomposed |
| 4 | Dial surface; op rows (exec/pty, direct-tcpip, git exec); the -R leg (listen + acceptor registry); lifecycle primitives |
wave 1 | decomposed |
| 5 | 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"withaws-lc-rs(noflate2, norsa; the tree RC-free except russh's ownssh-keypin),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 (alkttyundersession,alktunnelsundertunnel,alkgitundergit) 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
thiserrortaxonomy, fail-closed with no catch-alls: the key-source failure matrix as distinct variants (ADR-005 decision 4: locked / absent / decryption failure / unknownkey_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; theIdentityProviderhandle; the host-key source handle; the tunnelChannelClienthandle; theLimits), the dial-config (address, key source,HostKeyPolicyhandle, keepalive/inactivity/window knobs), andLimits, 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_attempts10 sized for the countingnone-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<String>wrap at the adapter seam → one-timedecode_secret_key(plain, None)), the client dial-key source and the server host-key load path (decrypt-at-construction; locked vault → no server constructed),EncryptedDataopaque to the crate, plaintext never persists, every failure mode mapped to its distinct error variant. - Host-key policy seam (OQ-05): the
HostKeyPolicytrait 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 thessh-keyre-export (HashAlg::Sha256); the string format is a contract invariant, proven against independent issuers (ssh-keygen -lfvectors, 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-memoryHostKeyPolicy, 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/alkgitwould 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 firstcfgconsumer. - No config-error type ships in wave 1 unless the config task
finds one needed: the OQ-08 fields are
Optionand 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-policyagainst the russhcheck_server_keyhook 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+Configconstruction (ADR-001/002): the mount point ADR-001 pins as thealk/sshreceiving 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).Configbuilt 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 fromLimits.- The auth bridge (ADR-004): the dispositional handler surface:
auth_publickeyfingerprint →resolve_from_fingerprint→ accept with the resolved identity / reject fail-closed; ed25519-only (non-ed25519 keys declined explicitly);auth_publickey_offeredaccept-and-delegate (counting stays honest);auth_none/auth_password/auth_keyboard_interactive/auth_openssh_certificateexplicit 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-agentalways 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::spawned, handlers return fast, exit/status reporting from spawned tasks rides the clonedSession::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_attemptswired fromLimits, 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 wrappingKeySourceError(locked → no server) plus the Handlertype Error(From<russh::Error>); 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_requestargs →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/signalrows;env_requestdeclined unconditionally (OQ-09); the exit derivation contract (mapping.mdrules 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_dumpednever 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 toserve_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
ChannelClientinto the family fabric (identity propagated once viaset_identitysoAccessControl::checkgates 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 → explicitConnectFailedreject),direct-streamlocalruntime-Unix-gated (OQ-11: universal compile, clean open-decline off-Unix), theforwarded-tcpiphandler shape, the-Rlisten 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-configlands 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 intests/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
-Rlisten 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-suppliedArc<ChannelClient>composition can carry the door-side accept or the serve-config's tunnel handle must grow) is decided inw3-tunnel-listenagainst 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 viaset_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 atcheck_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()notrequest_exec, Success/Failure viawait()-drain notErr,window_change/signalbeforeinto_stream(), never consume the queue before conversion);direct-tcpiptyped 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
-Rleg:tcpip_forward(address, port) -> bound(the actually-bound port returned), server-initiatedforwarded-tcpipopens 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_forwardwith 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-
sshdserver double this task lands), thenw4-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), thenw4-reverse-listen(the-Rleg: forward requests + the acceptor registry + late-install, making it the wave's highest-risk task, growing the harness'sForwardedAcceptorvocabulary into the crate's real surface), thenw4-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 inw4-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 intests/common): the loopback harness drives the server direction and is not the vehicle here; real-sshdclient-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
DialConfigcourse-correction is anticipated: the wave-1 shape already carries everythingclient.md's dial surface names;w4-dialdecides + records if that proves wrong (thew3-seam-configpattern), 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-rowsfollowsw4-git-exit-close(both touchtests/git_mapping.rs) andw4-tty-coveragefollows the rows task (both touchtests/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'sdirect-tcpipleg, decline observability), git 2.43+ (clone/push through the git mapping), curl through a-Dproxy; client direction against a realsshd(the reverse-runner shape: exec, direct-tcpip dial,-Rlisten + 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 ofalkssh 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 realsshd: the sshd fixture, exec/direct-tcpip/-R+acceptor, the decline rows), andw5-fuzz-harness(thefuzz/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 workspaceexclude); thenw5-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; andw5-publish(the publish checklist: manifest sanity, the full verification set, the dry-run, the crate-qualifiedalkssh-v0.1.0tag,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 ownsfuzz/+ 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-5stays a pure review andw5-publishis 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, reusingtests/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 inw5-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 barev0.1.0would be ambiguous across the family's multi-crate repos). Pinned inw5-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…006docs/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)