Promotion Evidence Contract

Serving API behavior verified against 60913a04352cb4899ab6443e596811b11cc3f01a on 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 with rg -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.

CapabilityCurrent ownerBoundary
Evidence custody, attestation, transparency material, export and verificationevidence-web / evidence-worker, source src/Evidence/, database stellaops_evidenceConsolidated Evidence owns the serving API and migrations. Attestor/EvidenceLocker names remain on carried libraries and valid test projects.
Key custody and signingSeparate Signer familyEvidence uses configured cryptography and the closed Signer contract; it does not absorb Signer’s database or keys.
Policy resultPolicy EngineEvaluates its inputs and policy version; evidence storage does not authorize a release.
Committed release decision, approval and executionRelease Orchestrator (RO)Owns release history and target observations, and the durable seal intent written with each committed decision revision.
Independent replay and offline exchangeReplay and OfflineKit, respectivelyReplay 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.

EndpointActual 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/snapshotCreates 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/verifyCompares 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/verifyCalls IAttestorVerificationService and returns structured verification checks under the current attestor:verify policy.
GET /api/evidence/v1/evidence/{bundleId}/portableExports the selected bundle for supported offline verification. Export availability does not imply that verification passed.
GET /api/evidence/v1/evidence/by-release/{releaseId}/portableResolves 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

Source and validation entry points

Related: Policy ownership, portable format, offline verification, and accepted recovery decision.