--- id: w1-error-model name: "Wave 1: the error model (fail-closed taxonomy)" status: completed depends_on: [w1-manifest] scope: narrow risk: low impact: project level: implementation --- ## Description Land the crate's error taxonomy in `src/error.rs` (one module per file, re-exported from the crate root, the alktty pattern), `thiserror` throughout, no catch-alls anywhere (AGENTS.md convention 2): - **`KeySourceError`**: the ADR-005 decision-4 failure matrix as distinct variants, the vault's error taxonomy reshaped by the adapter, **never collapsed**: `Locked` (vault locked → no dial, and no server: a host key that can't load prevents server construction), `Absent` (no blob at the requested path), `Decryption` (wrong mnemonic / tampered ciphertext: the refuse-at-crypto class), `UnknownKeyVersion` (the vault's refuse-before-crypto `InvalidPath` class), `Decode` (plaintext arrived but is not decodable key material: the one-time `decode_secret_key` failure). Doc comments carry the ADR semantics per variant, including the fail-closed consequence of each. - Deliberately **not** error types: auth declines (dispositions, the `Auth::Reject` of ADR-004; never errors), wire decline reasons (SSH-shaped responses, wave-2 material), config validation (the OQ-08 fields are `Option` and decline at request time, not construction time; no config-error type ships unless the config task finds one needed, flagged for the gate). The exact variant payload shapes are the task's to pin (minimal per the sibling-crate posture; stringly-typed payloads only where a message is the honest content), flagged for the wave-1 gate. ## Acceptance Criteria - [x] `KeySourceError` has exactly the five pinned variants; no catch-all, no `Other`/`Unknown` - [x] Doc comments state each variant's ADR-005 semantics and its fail-closed consequence - [x] Unit tests construct every variant and assert `Display`/`Debug` paths compile clean - [x] `cargo test`, `cargo clippy --all-targets -- -D warnings`, `cargo fmt --check` pass ## References - docs/architecture/decisions/005-at-rest-key-custody.md (decision 4) - docs/architecture/auth.md §Key custody (the failure-mode list) - docs/research/poc-2-vault-keys.md §3 (the failure matrix, 39/39) ## Notes > Payload shapes pinned (flagged for the wave-1 gate): `Locked` and > `Absent` are payload-free, with no honest content beyond the > variant itself (the caller knows which source it requested); > `Decryption(String)` and `UnknownKeyVersion(String)` carry the > vault's opaque messages (`VaultServiceError::Encryption` / > `InvalidPath` arrive as strings per POC-2 §3, where a string is the > honest content). `Decode` carries `String` rather than the typed > `ssh_key::Error` (`russh::keys::decode_secret_key`'s error type): > the ssh-key RC type is deliberately kept out of the published > public API; when the RC settles the payload can be re-typed, but > freezing an RC error type now would be a worse one-way door. The > module doc comment records the deliberately-not-error classes > (auth declines = dispositions per ADR-004; wire decline reasons = > wave-2; config validation = none unless w1-config finds a need, > as the OQ-08 fields decline at request time). No `From` > blanket impl on purpose: alkvault's taxonomy has variants > (`Mnemonic`, `Derivation`, `AlreadyUnlocked`, `UnsupportedKeyType`) > that do not map onto the five without collapsing, and the ADR's > reshape-never-collapse rule kills the conversion; w1-key-sources > maps explicitly at its seam. The module stays engine-blind: no > alkvault or russh types referenced at the error layer. Tests: an > exhaustive `match` (no wildcard, so the closed-variant invariant is > compile-time enforced) plus `Display` string assertions over all > five variants, `Debug` output, and `Clone` payload carrying. ## Summary > `src/error.rs` landed: `KeySourceError` with exactly five variants > (`Locked`, `Absent`, `Decryption(String)`, `UnknownKeyVersion(String)`, > `Decode(String)`), each doc-commented with its ADR-005 semantics and > fail-closed consequence (locked → no dial + no server; absent/decryption/ > unknown-version/decode → no dial; host-key path → no server > construction throughout). Re-exported from the crate root > (`mod error; pub use error::KeySourceError;`, the alktty pattern). > No catch-all, no `Other/Unknown`, no blanket `From` from the vault's > taxonomy. Four unit tests (exhaustive match over the enum, > `Display`, `Debug`, `Clone`). Verified: `cargo test` (4/4 + doc > tests), `cargo test --all-features`, `cargo clippy --all-targets > -- -D warnings` (default + all-features), `cargo fmt --check`, > `cargo doc --no-deps` (no warnings), `taskgraph validate` clean.