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.
9.5 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | ||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| w2-server-mount | Wave 2: SshAdapter::handle mount + russh Config construction | completed |
|
moderate | medium | project | implementation |
Description
Land the door frame: the SshAdapter type, the ADR-001 mount point,
and the russh server Config construction from the wave-1
ServeConfig. This is the substrate every mapping composes into; the
auth bridge and dispatch fill the Handler this task skeletons.
SshAdapter: holds theServeConfig; construction (SshAdapter::new) loads the host keys decrypt-at-construction via the injectedHostKeySource(ADR-005 decision 6): a locked / absent / decryption-failed / unknown-version load means no server is constructed, and the error surfaces as a distinct adapter construction error wrapping theKeySourceErrorvariant (never a half-configured adapter, never a keyless server that fails later).- russh
Configconstruction (ADR-002 surface notes), built from the serve-config:methods={publickey}exactly (MethodSetfrom[MethodKind::Publickey]: honest advertisement, ADR-004 decision 4);keysfrom the loaded host-key set;window_size/maximum_packet_size/channel_buffer_size/max_auth_attemptsmapped verbatim fromLimits;inactivity_timeoutmapped always-Some(the bounded-resources posture; the crate never constructs a session with no wall clock); russh's rekeyLimitsstay russh-enforced at their RFC-4253 defaults (recorded in the wave-1 config notes; keep that recording).server_idstays russh's default posture unless the task finds a family reason otherwise (decide and record). - The mount:
SshAdapter::handle(stream)with a generic stream in, the full russh server stack per stream viarussh::server::run_stream(edge termination only, with no ALPN code and no relay leg; thealk/sshpin is a ledger entry, ADR-001, and nothing here speaks it). The exact return shape (awaitedRunningSession, an internal spawn + join handle, …) is decided here against the spawn-and-return posture (ADR-006 decision 4) and the assembly's mounting needs: decide and record; it is semver-visible once consumers exist. - The Handler skeleton: the per-connection
russh::server::Handlerimpl with the adapter'stype Error(From<russh::Error>via thiserror, the wave-2-shaped error surface the wave-1 error-model note reserved); every hook left at its fail-closed russh default (decline/reject). Later wave-2 tasks fill the auth hooks (w2-auth-bridge) and the channel/request dispatch (w2-dispatch-decline-core).
No mapping seams, no feature-gated serving (wave 3); no client dial
(wave 4). The feature gates stay untouched, since nothing consumes
them yet (the dispatch task is their first cfg consumer).
Acceptance Criteria
SshAdapter::new(ServeConfig)loads host keys decrypt-at-construction; locked / absent / decryption-failure / unknown-version →Errwith theKeySourceErrorvariant preserved, no adapter constructed- russh
Configbuilt from the serve-config:methodsis exactly{publickey}; host keys rideConfig::keys; theLimitsknobs map one-for-one withinactivity_timeoutalwaysSome SshAdapter::handlemounts the full russh server stack per generic stream (run_stream); no ALPN/relay code exists- The Handler skeleton compiles with an adapter error type
(
From<russh::Error>); every hook at its fail-closed default - Tests: honest advertisement (methods exactly
{publickey}); the knob mapping; construction failure postures (locked vault → no server, absent host keys → no server) cargo test,cargo clippy --all-targets -- -D warnings,cargo fmt --checkpass
References
- docs/architecture/decisions/001-alkssh-alpn-pin.md (the mount-point
contract:
SshAdapter::handle, raw vanilla-SSH stream in) - docs/architecture/decisions/002-russh-pin.md (the Config surface notes; honest advertisement)
- docs/architecture/decisions/004-auth-dispositions.md (decision 4:
methods = {publickey}) - docs/architecture/decisions/005-at-rest-key-custody.md (decrypt-at-construction; locked → no server)
- docs/architecture/overview.md §Bounded-resources policy
- docs/plans/implementation.md §Wave 2 (first bullet)
- russh 0.65.0 in-source:
server::Config,server::run_stream,auth::MethodSet
Notes
Implementation record (decisions made at this task's discretion, the "decide and record" items):
- The mount's return shape (
handleruns to completion) (Result<(), AdapterError>):handleis an async per-stream call that awaitsrun_stream'sRunningSessionthrough the session's end, matching the alkhttp door precedent (HttpAdapter/ProtocolHandler:: handleis spawned per connection by the assembly's accept loop; alkhttp/src/server/adapter.rs spawns it in tests the same way). ADR-006 decision 4 is satisfied inside russh itself:run_streamspawns the session's packet loop on its own task before returning, so the serving path is already spawned-and-returned at the handler-hook level;handledoes not double-spawn (that would add a task layer and a join-handle surface without adding posture). The alternative candidates (returningRunningSession, or an internal spawn +JoinHandle) were rejected: the first leaks a generic russh type into the public signature (plus theHandlertype it parameterizes); the second hides the failure boundary behind a detached task while still not matching the accept-loop shape any better. The chosen shape's signature isResult<(), AdapterError>`, the smallest honest surface; it is semver-visible and recorded here. server_idstays russh's default (SSH-2.0-russh_0.65.0, theConfig::default()posture). No family reason found to change it: the vanilla-ssh wire is vanilla; the version string is interoperability information, not a family advertisement, and POC-3's real-tool evidence rode that posture. Changing it would be policy the crate doesn't own; recorded for any future pin bump.- The ed25519-only posture holds by construction + the auth bridge, not by a construction check. The loaded key set is the advertisement; ADR-002 decision 4's "an assembly registers ed25519 fingerprints only" governs what the assembly sources. Flagged for the wave-2 gate: if host-key algorithm filtering ever becomes wire-contract territory (currently it is not, as the key source contract is silent), it would be an ADR amendment, not this task.
- Empty key set =
KeySourceError::Absenteven from a custom source:SshAdapter::newadditionally refuses anOk(vec![])load asConstruction(Absent), since an emptyConfig::keyswould serve no host key honestly, and thekey_source.rswave-1 contract already documents the empty set as the absent shape. - Keepalive stays russh-default on the server direction
(
None/ 3): theLimitskeepalive knobs are documented client-direction posture (reverse-runner evidence); no server-side keepalive posture exists and none is invented here. SessionHandlerispub(crate): the skeleton's type never appears in a public signature (thanks to the run-to-completion return shape), so the public API this task exposes is exactlySshAdapter+AdapterError;w2-auth-bridgecan widen visibility when its handler-level tests need it.- Tests ride the wave-1 fixture keys via
include_str!(the wave-1 src-unit convention) and the realVaultHostKeySourcethrough an in-memory vault for the decrypt-at-construction rows; one wire smoke (russh's own client over a tokio duplex throughhandle) proves the full stack mounts and the skeleton declines auth fail-closed with exactly the{publickey}advertisement visible on the wire.
Summary
SshAdapter is landed as the ADR-001 mount point: SshAdapter::new
decrypt-loads the host key set at construction (any load failure or
empty set → AdapterError::Construction(KeySourceError variant), no
adapter) and builds the russh 0.65 server Config with honest
advertisement (methods exactly {publickey}), the loaded keys, the
Limits knobs mapped one-for-one, inactivity_timeout always
Some, and everything else (rekey Limits, auth_rejection_time,
server_id, preferred sets) left verbatim at russh's default posture.
SshAdapter::handle is the edge-termination mount: a generic duplex
stream in, run_stream per stream, run to completion to
Result<(), AdapterError> (no ALPN/relay code; the alk/ssh pin
unspoken). The per-connection SessionHandler skeleton (pub(crate))
carries the Arc<ServeConfig> for the auth-bridge/dispatch tasks and
stands entirely at russh's fail-closed defaults, with
AdapterError (Construction(#[from] KeySourceError) +
Wire(#[from] russh::Error)) as its type Error.
Verification: 12 new tests covering advertisement-exactly-publickey,
knob
mapping (tuned + documented-default postures), decrypt-at-construction
key-set parity, construction failure matrix (locked / absent / empty /
tampered / wrong-vault / unknown-version / decode, variant preserved),
EOF-on-mount, and a wire smoke (russh client over duplex completing
KEX against the fixture host key and observing the fail-closed
decline with the exact {publickey} method list). cargo test,
cargo test --all-features, cargo clippy --all-targets -- -D warnings, cargo clippy --all-features --all-targets -- -D warnings,
cargo fmt --check, and cargo doc --no-deps all green.