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.
182 lines
9.5 KiB
Markdown
182 lines
9.5 KiB
Markdown
---
|
|
id: w2-server-mount
|
|
name: "Wave 2: SshAdapter::handle mount + russh Config construction"
|
|
status: completed
|
|
depends_on: [review-wave-1, w1-config, w1-key-sources, w1-error-model]
|
|
scope: moderate
|
|
risk: medium
|
|
impact: project
|
|
level: 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**, and 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)` with a generic stream
|
|
in,
|
|
the full russh server stack per stream via `russh::server::run_stream`
|
|
(edge termination only, with no ALPN code and no relay leg; the
|
|
`alk/ssh` pin is a ledger entry, ADR-001, and 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>` 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
|
|
|
|
- [x] `SshAdapter::new(ServeConfig)` loads host keys
|
|
decrypt-at-construction; locked / absent / decryption-failure /
|
|
unknown-version → `Err` with the `KeySourceError` variant
|
|
preserved, no adapter constructed
|
|
- [x] 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`
|
|
- [x] `SshAdapter::handle` mounts the full russh server stack per
|
|
generic stream (`run_stream`); no ALPN/relay code exists
|
|
- [x] The Handler skeleton compiles with an adapter error type
|
|
(`From<russh::Error>`); every hook at its fail-closed default
|
|
- [x] Tests: honest advertisement (methods exactly `{publickey}`); the
|
|
knob mapping; construction failure postures (locked vault → no
|
|
server, absent host keys → no server)
|
|
- [x] `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, matching 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, as 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)`, since an empty `Config::keys` 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 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. |