Files
alkssh/tasks/implementation/w2-server-mount.md
T
glm-5.3-flash b90e89b600 feat(server): land the SshAdapter mount + russh Config construction (w2-server-mount)
- 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.
2026-10-28 07:51:50 +00:00

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
review-wave-1
w1-config
w1-key-sources
w1-error-model
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 the ServeConfig; construction (SshAdapter::new) loads the host keys decrypt-at-construction via the injected HostKeySource (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 the KeySourceError variant (never a half-configured adapter, never a keyless server that fails later).
  • russh Config construction (ADR-002 surface notes) — built from the serve-config: methods = {publickey} exactly (MethodSet from [MethodKind::Publickey] — honest advertisement, ADR-004 decision 4); keys from the loaded host-key set; window_size / maximum_packet_size / channel_buffer_size / max_auth_attempts mapped verbatim from Limits; inactivity_timeout mapped always-Some (the bounded-resources posture — the crate never constructs a session with no wall clock); russh's rekey Limits stay russh-enforced at their RFC-4253 defaults (recorded in the wave-1 config notes — keep that recording). server_id stays 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 via russh::server::run_stream (edge termination only — no ALPN code, no relay leg; the alk/ssh pin is a ledger entry, ADR-001 — nothing here speaks it). The exact return shape (awaited RunningSession, 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::Handler impl with the adapter's type 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 → Err with the KeySourceError variant preserved, no adapter constructed
  • russh Config built from the serve-config: methods is exactly {publickey}; host keys ride Config::keys; the Limits knobs map one-for-one with inactivity_timeout always Some
  • SshAdapter::handle mounts 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 --check pass

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 — handle runs to completion (Result<(), AdapterError>): handle is an async per-stream call that awaits run_stream's RunningSession through the session's end — the alkhttp door precedent (HttpAdapter/ProtocolHandler:: handle is 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_stream spawns 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; handle does not double-spawn (that would add a task layer and a join-handle surface without adding posture). The alternative candidates (returning RunningSession, or an internal spawn + JoinHandle) were rejected: the first leaks a generic russh type into the public signature (plus the Handler type 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 is Result<(), AdapterError> — the smallest honest surface; it is semver-visible and recorded here.
  • server_id stays russh's default (SSH-2.0-russh_0.65.0, the Config::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::Absent even from a custom source: SshAdapter::new additionally refuses an Ok(vec![]) load as Construction(Absent) — Config::keys empty would serve no host key honestly, and the key_source.rs wave-1 contract already documents the empty set as the absent shape.
  • Keepalive stays russh-default on the server direction (None / 3): the Limits keepalive knobs are documented client-direction posture (reverse-runner evidence); no server-side keepalive posture exists and none is invented here.
  • SessionHandler is pub(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 exactly SshAdapter + AdapterError; w2-auth-bridge can 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 real VaultHostKeySource through an in-memory vault for the decrypt-at-construction rows; one wire smoke (russh's own client over a tokio duplex through handle) 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.