Files
alkssh/tasks/implementation/w1-error-model.md
T
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

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
w1-manifest
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-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

  • KeySourceError has exactly the five pinned variants; no catch-all, no Other/Unknown
  • Doc comments state each variant's ADR-005 semantics and its fail-closed consequence
  • Unit tests construct every variant and assert Display/Debug paths compile clean
  • 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.