- KeySourceError with exactly five variants (Locked, Absent, Decryption, UnknownKeyVersion, Decode) — ADR-005 decision 4's failure matrix as distinct shapes, the vault taxonomy reshaped never collapsed; no catch-all, no Other/Unknown - payload shapes pinned: Locked/Absent payload-free; Decryption/UnknownKeyVersion carry the vault's opaque messages; Decode carries a message so the RC-pinned ssh-key error type never enters the public API (flagged for the wave-1 gate) - each variant doc-commented with ADR-005 semantics + fail-closed consequence; module stays engine-blind (no vault/russh types) - no From<VaultServiceError>: non-mappable vault variants would force a collapse; w1-key-sources maps explicitly at its seam - exhaustive-match + Display/Debug/Clone unit tests Verified: cargo test (default + all-features), cargo clippy --all-targets --all-features -- -D warnings, cargo fmt --check, cargo doc --no-deps, taskgraph validate
4.7 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| w1-error-model | Wave 1 — the error model (fail-closed taxonomy) | completed |
|
narrow | low | project | 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-cryptoInvalidPathclass),Decode(plaintext arrived but is not decodable key material — the one-timedecode_secret_keyfailure). Doc comments carry the ADR semantics per variant, including the fail-closed consequence of each.- Deliberately not error types: auth declines (dispositions —
Auth::Reject, ADR-004; never errors), wire decline reasons (SSH-shaped responses — wave-2 material), config validation (the OQ-08 fields areOptionand 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
KeySourceErrorhas exactly the five pinned variants; no catch-all, noOther/Unknown- Doc comments state each variant's ADR-005 semantics and its fail-closed consequence
- Unit tests construct every variant and assert
Display/Debugpaths compile clean cargo test,cargo clippy --all-targets -- -D warnings,cargo fmt --checkpass
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):
LockedandAbsentare payload-free — no honest content beyond the variant itself (the caller knows which source it requested);Decryption(String)andUnknownKeyVersion(String)carry the vault's opaque messages (VaultServiceError::Encryption/InvalidPatharrive as strings per POC-2 §3 — string is the honest content).DecodecarriesStringrather than the typedssh_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 — the OQ-08 fields decline at request time). NoFrom<VaultServiceError>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 exhaustivematch(no wildcard — the closed-variant invariant is compile-time enforced) plusDisplaystring assertions over all five variants,Debugoutput, andClonepayload carrying.
Summary
src/error.rslanded:KeySourceErrorwith 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, noOther/Unknown, no blanketFromfrom 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 validateclean.