Release notes — EvidenceLocker append-only bundle storage
Sprint: SPRINT_20260501_050_EvidenceLocker_append_only_storage Audit finding: Pass-2 audit Group L finding L1 (post-seal mutation of evidence bundles). Status: Pre-prod breaking change. Existing dev databases must be reset.
Summary
EvidenceLocker enforces strict append-only storage for sealed evidence bundles. The four mutation methods that previously walked a single bundle row through Pending → Assembling → Sealed (SetBundleAssemblyAsync, MarkBundleSealedAsync, UpdateStorageKeyAsync, UpdatePortableStorageKeyAsync) are removed. A bundle row is now written exactly once at seal time via a single SealBundleAsync call. Re-seals (e.g. portable bundle materialisation) write a new sealed row whose supersedes column points at the predecessor; the predecessor’s superseded_by field is the only field permitted to mutate post-insert and is set via a SECURITY DEFINER SQL function.
What changed
Database schema
- Migration
008_append_only_bundles.sqldrops and recreatesevidence_locker.evidence_bundleswith:supersedes UUID NULLandsuperseded_by UUID NULLchain pointers.sealed_atis nowNOT NULL.statusis constrained to3(Sealed) or5(Archived);Pending,Assembling, andFailedare in-memory states only.- A
BEFORE UPDATE OR DELETEtrigger that raisesEVIDENCE_BUNDLE_IMMUTABLE(SQLSTATEEL010) for any mutation other than settingsuperseded_byNULL → not-NULL. - A
SECURITY DEFINERfunctionevidence_locker.evidence_bundle_set_superseded_by(predecessor, successor, tenant)is the only sanctioned path to mutatesuperseded_by. It validates tenant scope, idempotent re-link, and chain integrity.
Repository contract
IEvidenceBundleRepositoryno longer exposesCreateBundleAsync,SetBundleAssemblyAsync,MarkBundleSealedAsync,UpdateStorageKeyAsync, orUpdatePortableStorageKeyAsync. Replacements:SealBundleAsync(EvidenceBundle, EvidenceBundleSignature?, ct)— transactional single-insert of the sealed bundle plus signature row.SupersedeBundleAsync(predecessor, successor, tenant, ct)— calls the SQL function above.GetSupersedesChainAsync(head, tenant, ct)— recursive walk back to the first-seal row, ordered oldest-first.GetBundlesForReindexAsyncgained a default-falseincludeSupersededflag; default queries return only chain heads (superseded_by IS NULL).
Service-layer changes
EvidenceSnapshotService.CreateSnapshotAsyncbuilds the manifest in memory, signs it, and emits exactly one repository write per seal. Pending / Assembling intermediate state never touches the database.EvidenceBundleBuilderis repository-free; it computes the Merkle root from the canonical manifest and returns the result. Persistence moves entirely to the snapshot service.EvidenceBundlePackagingServiceno longer rewritesstorage_key. A mismatch between the object-store key and the sealed row’s key is now an error rather than a silent rewrite.EvidencePortableBundleService.EnsurePortablePackageAsyncwrites a NEW sealed bundle row withportable_storage_keypopulated and links the predecessor viaSupersedeBundleAsync.EvidenceReindexService.ReindexAsyncreports root-hash mismatches as errors instead of rewriting the sealedroot_hash.EvidenceReindexService.RollbackToCheckpointAsyncthrowsNotSupportedException. Producing a successor sealed bundle is the replacement workflow.IEvidenceBundleRepository.ExtendBundleRetentionAsyncis a no-op against the bundle row; retention extension is carried by theevidence_holdstable. Effective retention =MAX(bundle.expires_at, MAX(active hold.expires_at)).
Domain model
EvidenceBundlegainsSupersedes : EvidenceBundleId?andSupersededBy : EvidenceBundleId?fields.EvidenceBundleStatus.Pending,Assembling, andFailedretain their enum values for in-memory state-machine clarity but are documented as never-persisted.
Breaking changes
- Schema is destructive. Migration 008 issues
DROP TABLE IF EXISTS evidence_locker.evidence_bundles CASCADE(and the signatures FK chain). Pre-existing data is wiped. Production has not shipped this code path; dev volumes must be reset (docker volume rm compose_postgres-datafor the local compose stack). - Removed repository methods. Any caller still referencing the four mutation methods will fail to compile. The only legitimate callers are inside
src/EvidenceLocker/(the snapshot, packaging, portable, and reindex services), all of which have been migrated. - Removed product capability: evidence rollback to a prior reindex checkpoint. The replacement is to seal a new corrected bundle and link it via supersedes.
Operational notes
- Out-of-band edits. The
BEFORE UPDATEtrigger blocks all UPDATE/DELETE except forsuperseded_by. DBAs needing an emergency edit mustALTER TABLE evidence_locker.evidence_bundles DISABLE TRIGGER evidence_bundles_block_mutationfor the duration of the edit and re-enable the trigger immediately. Document the exemption in the audit trail. - Effective retention is now a read-time computation across
evidence_bundles.expires_atand active rows inevidence_holds. Reports that currently readevidence_bundles.expires_atdirectly should be updated to consult holds as well. - Auditor walks of a re-sealed chain go via
IEvidenceBundleRepository.GetSupersedesChainAsync(oldest-first). The read-only HTTP endpoint surfacing this walk is tracked as follow-upL1-050-5.
Tests
- All 186 EvidenceLocker tests pass (
dotnet test src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Tests/StellaOps.EvidenceLocker.Tests.csproj), including new integration assertions inEvidenceBundleImmutabilityTeststhat exercise:- the
EVIDENCE_BUNDLE_IMMUTABLEUPDATE/DELETE rejection, - the SECURITY DEFINER function’s idempotent and double-link rejection semantics,
- the chain walk via
GetSupersedesChainAsync, - the
includeSupersededflag on reindex queries.
- the
References
- Sprint plan:
docs/implplan/SPRINT_20260501_050_EvidenceLocker_append_only_storage.md - Migration:
src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Infrastructure/Db/Migrations/008_append_only_bundles.sql - Architecture dossier:
docs/modules/evidence-locker/architecture.md§6.1 - Bundle v1 schema:
docs/modules/evidence-locker/evidence-bundle-v1.md - Pass-2 audit (Group L):
docs-archive/qa/audits/microservice-audit-pass2-2026-04-29.md
