36 lines
1.8 KiB
Markdown
36 lines
1.8 KiB
Markdown
# ADR-0042: Immutable verified backup sets
|
|
|
|
## Status
|
|
|
|
Accepted in M15 on 2026-08-27.
|
|
|
|
## Decision
|
|
|
|
Make `BackupSet` immutable recovery identity, sealed by `immutable_at` once written, carrying the
|
|
ModelForge version and commit, the canonical repository and reference, the Alembic revision, the
|
|
PostgreSQL server identity and tool version, the destination, the hashed manifest and payload, the
|
|
included and excluded asset classes and the encryption identity.
|
|
|
|
Treat verification, not creation, as the gate. Only `VERIFIED` is restore eligible, and verification
|
|
checks the manifest hash, manifest-to-journal agreement, the presence of a database payload, every
|
|
object's size and hash at an allowlisted path, decryption against the recorded plaintext hash, and
|
|
archive readability. Anything else fails closed with a typed code.
|
|
|
|
Protect the last known-good backups from retention unconditionally, and reconcile any backup
|
|
interrupted mid-write to `FAILED`.
|
|
|
|
## Consequences
|
|
|
|
A backup cannot look usable because a file exists. A single flipped byte in a 5.4 MB encrypted
|
|
payload, a one-integer edit to a manifest entry, a wrong encryption key and a mid-write interruption
|
|
were each caught in live rehearsal with `HASH_MISMATCH`, `MANIFEST_HASH_MISMATCH`,
|
|
`DECRYPTION_FAILED` and `MANIFEST_INCOMPLETE`, and none of them could become a restore plan.
|
|
|
|
Immutability across several flushes required loosening the immutability guard to permit exactly the
|
|
sealing flush — a backup is planned, written and only then stamped — while still rejecting every
|
|
later change to identity fields.
|
|
|
|
The platform can never retain zero recovery points because a timer expired, and a backup can never
|
|
contain a record of itself as complete, so a restored plane consistently reports the backup it came
|
|
from as not restore eligible.
|