13 KiB
Context
The restore-crypto-keychain-packer-umbrella change was sliced into four sequential children on 2026-05-03 (restore-crypto-cleanup-duplicates, restore-crypto-fold-keys-vault, restore-crypto-opinionated-layer, restore-crypto-cli-and-allowlist). Each child shipped its source-code delta. The umbrella itself was archived to openspec/archive/2026-05-03-restore-crypto-keychain-packer-umbrella/ with its 20-requirement spec under specs/crypto/spec.md. The slice plan implicitly assumed each child would carry its share of the umbrella's spec deltas into openspec/specs/crypto/spec.md. That did not happen. The current live spec at openspec/specs/crypto/spec.md carries primitive-substrate requirements, the keychain CLI surface, the streaming-AEAD façade, and password-blob helpers — but the central "every key carries (masterVersion, slotVersion)" invariant is absent at the requirement layer. The supporting source code shipped (verified by direct inspection of packages/crypto/crypto/src/derivation/{coordinate,layouts}/{coordinate,layouts}.ts and the test files alongside them).
In parallel, openspec/specs/crypto-keys/spec.md exists as a stub: Purpose: TBD - created by archiving change crypto-completion. That stub describes a BaseKeySchema device-key surface for a @suaveplan/crypto-keys package that no longer exists on genesis (packages/crypto/crypto-keys/ is absent). Live openspec/specs/crypto/spec.md:204-207 asserts the package must be deleted; the stub contradicts it.
This is a documentation-debt change. The code is correct and tested. The spec corpus is incomplete and self-contradictory.
Goals / Non-Goals
Goals:
- Restore the versioned-key vocabulary (
DerivationCoordinate,DerivationLayout,masterVersion,slotVersion, eight-slotKeychain, validity windows, parent-signed bundles, hierarchical identity, revocation, audit, packer, replay protection, trust-anchor) as named requirements inopenspec/specs/crypto/spec.md. - Every new requirement carries
Verified by:pointers to existing test files at concretepath::"test name"granularity. - Eliminate the contradiction between
openspec/specs/crypto/spec.md(says crypto-keys is gone) andopenspec/specs/crypto-keys/spec.md(still describes it) by removing the orphan stub. - After archive,
openspec validate --strictis green for the livecryptocapability andopenspec/specs/crypto-keys/no longer exists.
Non-Goals:
- No source-code changes. No new tests. No deleted tests. No moved files in
packages/. - No new requirements that the existing test suite does not already cover. If the umbrella had a requirement (e.g. a specific CRL fetch endpoint) that has no current test, it is not folded — it gets a follow-up implementation change.
- No reopening of any of the four archived child changes (
restore-crypto-cleanup-duplicates,restore-crypto-fold-keys-vault,restore-crypto-opinionated-layer,restore-crypto-cli-and-allowlist). Their archives remain authoritative for their respective slices. - No reorganisation of unrelated requirements (primitive substrate, AEAD façade, password blob, allowlist gate). Their text is unchanged.
- No spec changes for
bitpacker-crypto,crypto-cli,crypto-core,slsa. The umbrella touched their specs too; those folds are out of scope here and tracked as separate follow-ups if found necessary.
Decisions
Decision 1: spec-only change, zero code touch
Rationale: the source already implements every requirement to be folded. Any diff outside openspec/specs/crypto/ and openspec/specs/crypto-keys/ would expand scope into a re-audit of restore-crypto-opinionated-layer's archive. By constraining the change to spec text plus deletion of the stub, the review surface is bounded and the risk of accidentally re-implementing already-shipped logic is zero.
Alternative considered: a combined "spec restore + missing-test backfill" change. Rejected — coverage is already at 100% on this package per the gate, so any "missing test" would be a requirement that was never shipped, which makes it a new feature change, not a fold.
Decision 2: fold by direct copy, not by reference
Each folded requirement's text lives in openspec/specs/crypto/spec.md as-written. We do not add See archived umbrella cross-references. Reasons:
- Live specs are the contract. A live spec that delegates to an archived spec for its central invariants is dead-on-arrival when the archive is read in a different context.
openspec validatedoes not traverse archive paths.- The umbrella contained more than this change is folding (e.g.
Domain-separation tag on every signed payloadcross-cuts requirements that are out of this change's scope). Direct-copy lets us select cleanly.
Decision 3: every folded requirement carries Verified by:
Verified by: <test-file-path>::"<test name>" is mandatory on every new requirement. The format is the same one already used in newer parts of openspec/specs/crypto/spec.md (e.g. lines 311, 367). The validation step in tasks.md greps each cited test file for the exact it( / test( string before the change is allowed to archive.
Rationale: a folded requirement without a Verified by: pointer is indistinguishable from a forward-looking spec, which is exactly the failure mode that produced the current contradictory corpus.
Decision 4: requirements are folded at umbrella granularity, not split
The umbrella named 14 requirements that are missing from the live spec. Each becomes a single requirement here, with the same name. We do not split a single umbrella requirement into multiple smaller ones (e.g. one per scenario). Reasons:
- Bigger requirements with multiple
#### Scenario:blocks are the existing house style inopenspec/specs/crypto/spec.md. - Splitting would reformat the umbrella's authorial intent without adding signal.
- A single requirement per umbrella concept makes the diff between umbrella archive and live spec trivially auditable.
Decision 5: delete openspec/specs/crypto-keys/spec.md, do not rewrite it
The stub describes BaseKeySchema device keys ({s, v, m, u, d, o}) for a deleted package. Two alternatives were considered:
a. Rewrite the stub as a crypto-keys capability that documents the post-fold "the package no longer exists" reality. Rejected — non-existent capabilities are noise, and openspec/specs/crypto/spec.md already asserts non-existence.
b. Move the stub's content (the device-key requirements) into openspec/specs/crypto/spec.md under a new requirement. Rejected — the underlying device-key derivation API is not implemented on genesis (per archive/2026-05-03-crypto-completion/tasks.md:60, the device-key suite is deferred to a crypto-keys-port follow-up). Folding it now would re-introduce the same problem this change is fixing: a requirement with no Verified by: pointer.
Chosen: delete the stub. If crypto-keys-port ever ships, it gets to add its own requirements with its own implementing tests.
Decision 6: name the requirements verbatim from the umbrella, drop the unbacked ones
The 13 requirement headings will read exactly as they did in the umbrella's specs/crypto/spec.md:
- Eight-slot Keychain with full-width derivation coordinates
- DerivationLayout registry — pluggable u64 layout
- Validity windows enforced at every verify call
- Domain-separation tag on every signed payload
- Public bundles are parent-signed and carry an issuer reference
- Chain bundle traversal — concatenated parent-signed links
- Wire-format schemas exposed and committed
- Authority — signing abstraction over hierarchical identity
- Hierarchical identity — Root and Server
- RevocationList — root-signed CRL
- AuditLog — append-only Merkle log
- Packer — encrypt-and-sign envelope
- ChunkedPacker — per-chunk-signed P2P fragmentation
The umbrella's Trust-anchor bootstrap requirement is dropped from this fold. Direct inspection of packages/crypto/crypto/src/ confirms there is no trust-anchors/ directory, no loadTrustAnchor() / loadTrustAnchorFromEnv() symbol, and no SUAVEPLAN_TRUST_ANCHOR_PATH consumer. Folding the requirement now would create a spec entry whose Verified by: pointer cites a test that does not exist — exactly the failure mode this change exists to fix. Trust-anchor bootstrap needs an implementation change first; this change leaves it explicitly out of scope.
The umbrella's Vault and Replay protection via issuedAt + nonce requirements are not added either — their content overlaps with the existing live requirement Opinionated layer modules — Identity, Revocation, Audit, Packer, Vault (line 209) plus the replay scenarios that ride on the new Packer requirement. Folding them as separate requirements would create cross-cutting duplication. The live Opinionated layer modules requirement is left intact as a higher-level overview; the 13 new requirements add the missing per-module detail.
The umbrella's Wire-format KAT vectors, Property-based round-trip coverage, OpenTelemetry instrumentation with key-byte redaction, and Structured error codes with stable identifiers requirements are not folded either: KAT and property-coverage are implementation gates already enforced by the existing 100%-coverage requirement; OTel redaction is covered by the live "Structured errors never leak secrets" requirement at line 139; structured error codes are covered by the same live requirement. Adding any of them would duplicate.
Risks / Trade-offs
-
[Risk] A
Verified by:pointer cites a test that does not actually assert what the requirement says. → Mitigation: the validation step intasks.mdgreps each cited test file for the exactit(/test(string and reports any miss as a hard failure before archive. A second mitigation: every requirement's scenario block is hand-copied from the umbrella, where each scenario was originally written to map to a planned test name. -
[Risk] The umbrella's wording references symbols (
KEYCHAIN.UNKNOWN_LAYOUT,TRUST_ANCHOR.MISMATCH,DOMAIN_PUBLIC_KEYCHAIN_V1) that may have been renamed in source after archive. → Mitigation:tasks.mdincludes a grep step that confirms each cited symbol exists inpackages/crypto/crypto/src/. A miss either reveals a stale-spec bug (which fails the change) or a stale-source bug (which gets a separate fix). -
[Risk] Folding 14 requirements expands
openspec/specs/crypto/spec.mdsubstantially, raising the chance of merge conflicts with parallel changes touchingcrypto. → Mitigation: this change is small in absolute terms (~600 lines of spec text) and ships before any other opencryptochange can land. We do not block on adjacent capabilities. -
[Risk] Removing
openspec/specs/crypto-keys/spec.mdbreaks any external doc/link that references it. → Mitigation: a grep over the genesis tree will be run as part oftasks.md. If any consumer references the path, the change blocks until those references are updated. Empirically (per the search done while drafting this proposal), onlyarchive/2026-05-03-crypto-completion/referencescrypto-keys, and archives are immutable. -
[Trade-off] Spec-only changes are cheap to write and risky to review — they look low-stakes but encode contracts. → Counterweight: every requirement has a verifiable test pointer, so reviewers can run
bunx vitest run <test-file> --reporter=verboseand confirm the cited scenarios pass. The contract is testable, not just readable.
Migration Plan
- Add deltas: write
specs/crypto/spec.md(## ADDED Requirementssection, 14 entries) andspecs/crypto-keys/spec.md(## REMOVED Requirementssection listing every requirement currently inopenspec/specs/crypto-keys/spec.md). The delta files live underopenspec/changes/crypto-spec-restore-versioning/specs/. - Validate:
bun openspec validate crypto-spec-restore-versioning --strictpasses. - Verified-by audit: for each new requirement, grep its
Verified by:pointer against the cited test file and assert match. Block on any miss. - Symbol audit: grep each cited symbol (
DERIVATION_LAYOUT_V1,DOMAIN_VAULT_V1,PUBLIC_BUNDLE_MAGIC, etc.) againstpackages/crypto/crypto/src/and assert presence. - External-reference audit: grep
crypto-keysagainst the live tree (excludingopenspec/archive/) and assert no consumer breaks. - Apply: fold the deltas into
openspec/specs/crypto/spec.md; deleteopenspec/specs/crypto-keys/; archiveopenspec/changes/crypto-spec-restore-versioning/toopenspec/archive/<date>-crypto-spec-restore-versioning/. - Post-archive validation:
bun openspec validate --strictpasses for the entire spec corpus.
Rollback strategy: revert the single archive commit. The change is one commit by design.
Open Questions
- Should
Domain-separation tag on every signed payloadbe folded here, or already covered by the existing live "Structured errors never leak secrets" + "Opinionated layer modules" pair? Default per Decision 6: fold it. Reviewer can pull it if the duplication is judged worse than the explicit mention. - Should
Wire-format schemas exposed and committedreference the KAT-fixture directorypackages/crypto/crypto/test-fixtures/wire-format/if it exists, or stay symbol-only? Default: stay symbol-only; if atest-fixtures/dir is found during the symbol-audit step, add a scenario that asserts byte-stability against it.