Files
glm-5.3-flash f4b5f896b4 docs: sweep em dashes from all markdown prose
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.
2026-11-04 20:28:00 +00:00

98 lines
4.7 KiB
Markdown

---
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<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
> 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.