Files
2026-05-27 23:29:53 +01:00

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-slot Keychain, validity windows, parent-signed bundles, hierarchical identity, revocation, audit, packer, replay protection, trust-anchor) as named requirements in openspec/specs/crypto/spec.md.
  • Every new requirement carries Verified by: pointers to existing test files at concrete path::"test name" granularity.
  • Eliminate the contradiction between openspec/specs/crypto/spec.md (says crypto-keys is gone) and openspec/specs/crypto-keys/spec.md (still describes it) by removing the orphan stub.
  • After archive, openspec validate --strict is green for the live crypto capability and openspec/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:

  1. 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.
  2. openspec validate does not traverse archive paths.
  3. The umbrella contained more than this change is folding (e.g. Domain-separation tag on every signed payload cross-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:

  1. Bigger requirements with multiple #### Scenario: blocks are the existing house style in openspec/specs/crypto/spec.md.
  2. Splitting would reformat the umbrella's authorial intent without adding signal.
  3. 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:

  1. Eight-slot Keychain with full-width derivation coordinates
  2. DerivationLayout registry — pluggable u64 layout
  3. Validity windows enforced at every verify call
  4. Domain-separation tag on every signed payload
  5. Public bundles are parent-signed and carry an issuer reference
  6. Chain bundle traversal — concatenated parent-signed links
  7. Wire-format schemas exposed and committed
  8. Authority — signing abstraction over hierarchical identity
  9. Hierarchical identity — Root and Server
  10. RevocationList — root-signed CRL
  11. AuditLog — append-only Merkle log
  12. Packer — encrypt-and-sign envelope
  13. 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 in tasks.md greps each cited test file for the exact it( / 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.md includes a grep step that confirms each cited symbol exists in packages/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.md substantially, raising the chance of merge conflicts with parallel changes touching crypto. → Mitigation: this change is small in absolute terms (~600 lines of spec text) and ships before any other open crypto change can land. We do not block on adjacent capabilities.

  • [Risk] Removing openspec/specs/crypto-keys/spec.md breaks any external doc/link that references it. → Mitigation: a grep over the genesis tree will be run as part of tasks.md. If any consumer references the path, the change blocks until those references are updated. Empirically (per the search done while drafting this proposal), only archive/2026-05-03-crypto-completion/ references crypto-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=verbose and confirm the cited scenarios pass. The contract is testable, not just readable.

Migration Plan

  1. Add deltas: write specs/crypto/spec.md (## ADDED Requirements section, 14 entries) and specs/crypto-keys/spec.md (## REMOVED Requirements section listing every requirement currently in openspec/specs/crypto-keys/spec.md). The delta files live under openspec/changes/crypto-spec-restore-versioning/specs/.
  2. Validate: bun openspec validate crypto-spec-restore-versioning --strict passes.
  3. Verified-by audit: for each new requirement, grep its Verified by: pointer against the cited test file and assert match. Block on any miss.
  4. Symbol audit: grep each cited symbol (DERIVATION_LAYOUT_V1, DOMAIN_VAULT_V1, PUBLIC_BUNDLE_MAGIC, etc.) against packages/crypto/crypto/src/ and assert presence.
  5. External-reference audit: grep crypto-keys against the live tree (excluding openspec/archive/) and assert no consumer breaks.
  6. Apply: fold the deltas into openspec/specs/crypto/spec.md; delete openspec/specs/crypto-keys/; archive openspec/changes/crypto-spec-restore-versioning/ to openspec/archive/<date>-crypto-spec-restore-versioning/.
  7. Post-archive validation: bun openspec validate --strict passes 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 payload be 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 committed reference the KAT-fixture directory packages/crypto/crypto/test-fixtures/wire-format/ if it exists, or stay symbol-only? Default: stay symbol-only; if a test-fixtures/ dir is found during the symbol-audit step, add a scenario that asserts byte-stability against it.