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.
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, the
Auth::Rejectof 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, with 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, where a 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, as 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, so 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.