- SshAdapter:🆕 decrypt-at-construction host-key load (ADR-005
decision 6) — locked / absent / decryption / unknown-version /
decode all refuse with AdapterError::Construction(KeySourceError
preserved), no adapter; an empty key set refuses as Absent even
from a custom source.
- russh 0.65 server Config built from ServeConfig: methods exactly
{publickey} (MethodSet from [MethodKind::PublicKey] — honest
advertisement, ADR-004 decision 4); host keys ride Config::keys;
Limits knobs (window_size, maximum_packet_size,
channel_buffer_size, max_auth_attempts) one-for-one;
inactivity_timeout always Some (bounded-resources posture). All
other knobs stay russh's default posture: rekey Limits (RFC-4253
defaults, russh-enforced), auth_rejection_time, server_id
(russh default — recorded), server keepalive (client-direction
posture only).
- SshAdapter::handle: ADR-001 mount — generic duplex stream in,
run_stream per stream, run to completion (Result<(), AdapterError>;
no ALPN/relay code; the serving loop is spawned inside russh, so
ADR-006's spawn-and-return holds without double-spawning).
- SessionHandler skeleton (pub(crate)): per-connection
russh::server::Handler with type Error = AdapterError; every hook
at the inherited fail-closed default; carries Arc<ServeConfig> for
w2-auth-bridge / w2-dispatch-decline-core.
- AdapterError (error.rs): Construction + Wire (From<russh::Error>)
— the wave-1 error-model's reserved wave-2 shape.
- Tests: advertisement-exactly-publickey; knob mapping (tuned +
default postures); key-set parity; construction failure matrix;
EOF-on-mount; wire smoke (russh client over duplex: full KEX,
fail-closed auth decline, exactly the {publickey} method list on
the wire).
Verified: cargo test (61 across suites), cargo test --all-features,
clippy --all-targets -- -D warnings (default + all-features),
fmt --check, cargo doc --no-deps, taskgraph validate.
9.6 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 — 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): a generic stream in, the full russh server stack per stream viarussh::server::run_stream(edge termination only — no ALPN code, no relay leg; thealk/sshpin is a ledger entry, ADR-001 — 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>, 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 — 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 — 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 — 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)—Config::keysempty would 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 — 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.