- HostKeyPolicy trait: async, dyn-compatible (Arc<dyn> handle, the alkcall injected-handle pattern) — decide(&self, &PresentedHostKey) -> bool with host/port/fingerprint as the decision input - Verdict type pinned to the russh 0.65 check_server_key shape: bool is the trust decision; decline reasons stay with the policy (declines are dispositions, not errors). Flagged for the wave-1 gate - Built-in default RejectAll: fail-closed reject-all (russh's default, kept); no accept-all option exists in the crate - async-trait 0.1 added for the dyn-compatible async handle (alkcall-precedented; tree-deduped). Flagged for the wave-1 gate - Tests: default rejects; custom verdict honored; both through the dyn handle; the whole presented context reaches the policy Verified: cargo test (14), cargo test --all-features, cargo clippy --all-targets -- -D warnings, cargo fmt --check, taskgraph validate
5.6 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| w1-host-key-policy | Wave 1 — the HostKeyPolicy seam (OQ-05, policy-blind) | completed |
|
narrow | low | project | implementation |
Description
Land the client-side host-key policy seam (auth.md §Host-key policy,
OQ-05 resolution) — the trust-decision contract the dial surface calls:
- The
HostKeyPolicytrait — the crate surfaces the presented host key (host, port, fingerprint) to an assembly-provided policy that decides accept/reject. The two OQ-05 cases are the implementor's: case-1 pins (known beforehand — hard-decline on mismatch) and case-2 policies (unknown beforehand — prompt-and-record, learn-and-record, provider-metadata match, accept-and-continue for ephemeral fleets). The crate is policy-blind: it never decides trust, never hard-codes a trust model. - The crate default is fail-closed: reject unknown (russh's own reject-all default, kept as the built-in default policy). No accept-all built-in crate option — a permissive case-2 policy is the assembly's to write.
- The trait receives the fingerprint string (the wave-1 fingerprint
computation is the caller's input path, wave-4 dial); the exact
decision type (bool vs a richer verdict carrying a reason) is the
task's to pin against the russh
check_server_keyhook shape at this pin — flagged for the wave-1 gate. - The russh client-handler consumption (
check_server_keywiring) is wave-4 dial-surface material — this task lands the seam and its default only.
Acceptance Criteria
HostKeyPolicytrait in place; the presented-key context (host, port, fingerprint) is the decision input- Built-in default policy rejects unknown keys (fail-closed); no accept-all option exists in the crate
- Doc comments carry the two-case model and the policy-blind rule
- Unit tests: default policy rejects; a custom policy's verdict is honored
cargo test,cargo clippy --all-targets -- -D warnings,cargo fmt --checkpass
References
- docs/architecture/auth.md §Host-key policy (OQ-05 resolution)
- docs/architecture/client.md §The dial surface, §What the crate does not include (trust decisions)
- docs/architecture/decisions/005-at-rest-key-custody.md (decision 6 — the client host-pin is public config, never vault material)
- docs/research/auth-bridge.md §Host keys and
check_server_key
Notes
Landed
src/host_key.rs(HostKeyPolicy,PresentedHostKey,RejectAll) + lib.rs re-exports. Design decisions pinned against the russh 0.65 hook shape (Handler::check_server_key -> Result<bool, Self::Error>, async viaimpl Future, defaultOk(false)— verified in the russh 0.65.0 source):
- The verdict type is
bool(the flagged pin): the hook's bool is the trust decision and itsErrchannel is the handler's wire/IO-failure territory — a trust decline has no typed reason channel on this surface. A decline is a disposition, not an error (src/error.rscontract); decline reasons are the policy's own record (case-2 policies log their decisions), not a crate surface. Wave-4 wiring mapsfalse→ russh's hard, cleanError::UnknownKeydecline (mismatch = hard decline, per auth.md).- The trait is async and dyn-compatible:
#[async_trait],decide(&self, &PresentedHostKey) -> bool,Send + Sync + 'static— the family injected-handle pattern (Arc<dyn HostKeyPolicy>, the alkcallIdentityProvider/OperationEnvprecedent), which w1-config's dial-config handle and w1-test-doubles' inspectable double both require. State-holding (learn-and-record) policies record by interior mutability behind the shared&selfhandle.- New dep
async-trait = "0.1"(MIT OR Apache-2.0; the only way to get a dyn-compatible async trait at this toolchain — native AFIT is not object-safe, and russh 0.65 removed its ownasync-traitfeature, per ADR-002's context). Precedented and deduped: alkcall already carriesasync-trait 0.1, so the tree adds no new transitive package. Flagged for the wave-1 gate (manifest addition outside w1-manifest, same class as the verdict-type pin).RejectAllis the built-in default (russh's reject-all default, kept); doc comments carry the two-case model, the policy-blind rule, the no-accept-all rule, the fail-closed disposition (an undecided policy declines), and the host-pin-is- public-config note (ADR-005 decision 6).- The
PresentedHostKey.fingerprintdoc pins exact string equality against assembly pins; the string is the single fingerprint path's format, computed at wave-4 wiring.
Summary
Seam landed:
src/host_key.rswith theHostKeyPolicytrait (async fn decide(&self, &PresentedHostKey) -> bool, dyn-compatible via#[async_trait]), thePresentedHostKey(host/port/fingerprint) decision input, and the fail-closedRejectAllbuilt-in default; no accept-all option exists in the crate. Verdict type pinned to russh 0.65'scheck_server_keyshape (bool = the trust decision; decline reasons live with the policy, not the crate).async-trait = "0.1"added for the dyn-compatible async handle (alkcall-precedented, tree-deduped). Tests: default rejects; custom verdict honored; both honored through anArc<dyn Handle>; the whole presented context reaches the policy. Verified:cargo test(14),cargo test --all-features,cargo clippy --all-targets -- -D warnings,cargo fmt --checkall pass.taskgraph validateclean.