Promotion Evidence Contract
Serving API behavior verified against
60913a04352cb4899ab6443e596811b11cc3f01aon 2026-09-30; the snapshot idempotency contract and the seal recovery boundary below were updated from source on 2026-10-10 (CCP-050). This is source evidence, not a live verification receipt. Recheck withrg -n 'MapGet|MapPost|RequireAuthorization|Idempotency' src/Evidence/__Libraries/StellaOps.EvidenceLocker.Application/Api/EvidenceCoreEndpoints.cs.
Purpose and ownership
Promotion evidence must identify the exact artifact, evaluated inputs, decision revision and observed execution it supports. A later reassessment or verification adds its own attributable result; it must not rewrite the earlier basis. The required end-to-end outcome is defined by CA-08–CA-12, which remain acceptance work rather than a claim that every record already has complete proof.
| Capability | Current owner | Boundary |
|---|---|---|
| Evidence custody, attestation, transparency material, export and verification | evidence-web / evidence-worker, source src/Evidence/, database stellaops_evidence | Consolidated Evidence owns the serving API and migrations. Attestor/EvidenceLocker names remain on carried libraries and valid test projects. |
| Key custody and signing | Separate Signer family | Evidence uses configured cryptography and the closed Signer contract; it does not absorb Signer’s database or keys. |
| Policy result | Policy Engine | Evaluates its inputs and policy version; evidence storage does not authorize a release. |
| Committed release decision, approval and execution | Release Orchestrator (RO) | Owns release history and target observations, and the durable seal intent written with each committed decision revision. |
| Independent replay and offline exchange | Replay and OfflineKit, respectively | Replay uses recorded inputs; OfflineKit transports supported material. Neither receipt nor export alone establishes verification. |
Each family owns its physical database and role. A shared PostgreSQL server does not authorize sibling database access. See Evidence architecture and database ownership.
Identity and material contracts
The RO seal writer assembles release-decision-bundle@v1 from the captured release, the FINAL gate decision revision (operator-decision signature applied), decision basis, signed envelope where present, approval and evaluated component digests. ReleaseDecisionEvidenceSealWriter.BuildSnapshotRequest serializes these sections and sends their hashes, sizes, media types and attributes as snapshot materials. It does not upload the section bytes in that request. ReleaseDecisionSealPayload.Capture serializes that request canonically (metadata and attributes in ordinal key order); its SHA-256 is the decision revision. The stored request adds decisionRevision and sealIntentKey to the metadata, so the manifest Evidence signs names the exact revision it seals.
The serving snapshot request is EvidenceSnapshotRequestDto with kind, description, metadata and materials. Each material carries section, path, sha256, sizeBytes, mediaType and attributes. The optional Idempotency-Key header binds the request to a tenant-scoped key. The response is bundleId, rootHash, optional signature, idempotencyKey (the header value, or null) and replayed. These DTOs define the serving wire contract.
Preserve the owner’s canonicalization and digest formats. Join records by tenant and immutable subject/decision identity; a display name, mutable tag or latest-by-release lookup cannot identify the intended historical revision. Missing provenance, proof or replay inputs remain explicit.
Current serving API mapping
The Evidence host applies the /api/evidence/v1 prefix in src/Evidence/StellaOps.Evidence.WebService/Program.cs.
| Endpoint | Actual result and limit |
|---|---|
GET /api/evidence/v1/evidence/score?artifact_id=... | Stored artifact score/status through EvidenceGateArtifactService; no implicit promotion authorization. |
POST /api/evidence/v1/evidence/snapshot | Creates a bundle from material descriptors and returns bundleId, rootHash, optional signature (201). With an Idempotency-Key header: the same tenant, key and request return the first bundle and its stored signature (200, replayed: true); a different request under the key is refused (409 idempotency_key_conflict). Requires the existing evidence-hold authority. |
GET /api/evidence/v1/evidence/{bundleId} | Bundle identity, status, root/storage metadata, timestamps and signature. It does not synthesize SBOM, VEX or in-toto content that was never recorded. |
POST /api/evidence/v1/evidence/verify | Compares the caller’s rootHash with the stored bundle root and returns trusted. This implementation does not verify signatures, transparency or replay. |
GET /api/evidence/v1/attestations?subject=... | Lists recorded subject-bound attestation entries. Presence establishes receipt, not verification. |
POST /api/evidence/v1/rekor/verify | Calls IAttestorVerificationService and returns structured verification checks under the current attestor:verify policy. |
GET /api/evidence/v1/evidence/{bundleId}/portable | Exports the selected bundle for supported offline verification. Export availability does not imply that verification passed. |
GET /api/evidence/v1/evidence/by-release/{releaseId}/portable | Resolves the current chain head before export. Use an exact bundle ID when reproducing a historical decision. |
EvidenceSnapshotService.VerifyAsync currently performs the root comparison described above. The separate proof VerifyController returns 501 for its unimplemented receipt operations; its DTO declarations do not establish a working verification-history API. CCP-011 owns missing durable verification result/retrieval behavior. UI labels must name the checks actually performed.
Seal failure and recovery boundary
Capture (RO). PromotionRequestProcessor applies the verified operator-decision signature of a signed promotion before its single decision write, so the only revision of a decision id that is persisted or sealed is the signed one. With ReleaseOrchestrator:EvidenceSeal:Enabled true, the final decision and its seal intent commit in ONE transaction (release_orchestrator.decision_seal_intents, migration 012): the exact request body, the revision, and the key ro-decision-seal:v1:<sha256(tenant, decision id, revision)>. A failed write fails the promotion request and leaves neither row. A later legitimate revision of the same decision gets its own intent; the earlier row, payload and receipt stay unchanged (the revision and a recorded receipt are immutable at the database). With sealing disabled (the option’s default; the shipped Compose files enable it through RO_EVIDENCE_SEAL_ENABLED, which defaults to true there), no intent is captured, no reconciler runs and no request is sent.
Delivery (RO). ReleaseDecisionSealReconciler claims due intents from the table under a lease (competing replicas skip each other’s rows; a crashed claim is due again when the lease ends), posts the stored body byte for byte with the Idempotency-Key header and records Evidence’s receipt (bundle id, root hash) on the row only when the tenant, intent, revision and key match and Evidence’s answer confirms the key. A failure is fail-visible (critical log, metric releaseorchestrator.evidence_seal.failures, failed row with the error code and the next attempt) and retried with doubling, bounded backoff (RetryBaseSeconds, RetryMaxSeconds). Nothing is held in process memory, so queue saturation, an Evidence outage, a restart and a lost response all leave the intent in the table until it is sealed. Reconciliation sends only the stored request; it never re-runs a deployment, rebuilds the payload from today’s decision row, or changes another intent, decision or evidence record.
Idempotent result (Evidence). The snapshot route binds (tenant, key) to the canonical request digest and the bundle in the bundle’s own transaction (evidence_locker.evidence_snapshot_requests, migration 014: FORCE row-level security, append-only, FK RESTRICT to the bundle). The same tenant, key and request return the first bundle; a changed request conflicts; concurrent first requests converge on one bundle; another tenant’s key is never visible.
Proof state (release read). Release Orchestrator’s first-delivery read (first-delivery API) reports the proof state of three decisions: the one bound to the first observed deployment’s approval (proofCompletion), the one bound to the latest observed deployment’s approval (latestProofCompletion) and the newest decision captured for sealing in the tenant, delivered or not (newestDecisionProof, tenant-scope callers only). Each state is completed (with the bundle, root hash and the revision it seals), pending, failed (with the error code and the next attempt), disabled (sealing off and no intent) or unavailable (no bound decision, no intent, or the intents could not be read). The newest captured revision decides; the receipt of an older revision is never presented as the proof of a newer one.
Limits. Evidence must run the idempotent snapshot route before RO delivers: an Evidence service that does not echo the key leaves intents failed with evidence_idempotency_unconfirmed and is retried at the backoff bound. Activation on an estate, Authority’s evidence:hold grant to RO’s service client, and live outage or restart drills need the applicable operator authorization. Missing proof does not itself authorize automatic rollback, and existing predeployment evidence gates are unchanged.
Independent verification and retained history
- Identify the exported bytes, verifier version, trust roots/policy, evidence/subject digests and properties checked. Signature validity, trusted signer, transparency/witness availability, target observation and deterministic replay are separate claims.
- Verify using the selected supported offline package without the producer’s browser session or live external fetches. Missing historical material remains unavailable; it cannot become success.
- Reverification under changed trust records a new result with its own context. Current advisory, VEX or runtime changes produce a new assessment while the earlier decision remains retrievable.
- Preserve append-only bundles, supersession links, retention and active holds. No historical proof deletion or new retention period follows from this contract.
Source and validation entry points
src/Evidence/__Libraries/StellaOps.EvidenceLocker.Application/Api/EvidenceCoreEndpoints.cssrc/Evidence/__Libraries/StellaOps.EvidenceLocker.Application/Contracts/EvidenceContracts.cssrc/Evidence/__Libraries/StellaOps.EvidenceLocker.Infrastructure/Services/EvidenceSnapshotService.cssrc/Evidence/__Libraries/StellaOps.Attestor.Application/AttestorWebServiceEndpoints.cssrc/Evidence/__Libraries/StellaOps.EvidenceLocker.Infrastructure/Repositories/EvidenceBundleRepository.cssrc/Evidence/__Libraries/StellaOps.Evidence.Persistence/Migrations/014_evidence_snapshot_idempotency.sqlsrc/ReleaseOrchestrator/__Apps/StellaOps.ReleaseOrchestrator.WebApi/Services/ReleaseDecisionEvidenceSealWriter.cssrc/ReleaseOrchestrator/__Apps/StellaOps.ReleaseOrchestrator.WebApi/Services/ReleaseDecisionSealIntentStore.cssrc/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Persistence/Migrations/012_release_orchestrator_decision_seal_intents.sql- RO Integration.Tests
ReleaseDecisionEvidenceSealTests,ReleaseDecisionSealRecoveryTestsandReleaseTruthRepositoryPersistenceTests. - Evidence.WebService.Tests
EvidenceSnapshotIdempotencyTestsand Evidence.Persistence.TestsEvidenceSnapshotIdempotencySchemaTests. - Retained EvidenceLocker.Tests
EvidenceSnapshotServiceTests, which references consolidated Evidence libraries; select host tests by their actual project references.
Related: Policy ownership, portable format, offline verification, and accepted recovery decision.
