Release evidence / API preview

Supply-chain integration guide

Connect signed release evidence to the files you actually use. Register a manifest, set a receiver policy, and check the artifact locally before use.

For developers integrating within one Apostille workspace. English reference; includes HTTP contracts and Go snippets.

Apostille release evidence and policy API — preview.

Status: API preview, per deployment. A separate hosted profile; Core 0.1 and Core 0.3 signed formats are unchanged. Check support with GET /api/apostille/v1/status: enabled must be true and features must contain supply_chain_releases. The feature can be present while enabled is false.

CI read-only credentials are an API preview with per-deployment availability. Before using that extension, also require supply_chain_ci_credentials in features and confirm the credential routes are available on your deployment. This guide describes the API contract; verify rollout status on the target service before use. The extension does not change Core signed formats.

The service registers a signed release manifest, evaluates artifact metadata against an administrator's policy, and records an irreversible withdrawal. Consumers verify the actual bytes locally before use.

1. Scope and flow

text
Publisher's actual files
  -> local SHA-256 and byte counts
  -> canonical release manifest
  -> registered agent's Core origin-statement + admin publication-grant
  -> existing POST /submissions -> existing certificate
  -> separate admin register_release authorization -> POST /releases

Administrator's independently selected issuer/producer pins
  -> signed, versioned policy -> POST /release-policies

Consumer's actual file
  -> local hash + independent bundle/manifest verification
  -> POST /release-policy/evaluations with metadata and current policy version
  -> use the same checked bytes only if local checks and policy permit

Administrator's signed withdraw_release authorization
  -> POST /releases/{id}/status-events -> future evaluations are blocked

Limits

  • allowed_by_policy is a scoped policy result. Signatures and policy checks do not prove a file is safe or malware-free.
  • The service does not fetch artifact URLs, accept uploads, scan, build or run anything. Artifacts, source, weights and private keys stay with you. Hashes, names, keys and timestamps can still link records.
  • Release records never enter IFF's x402 cards, observations, transparency log, monitor webhooks, anchors or reputation. There is no registry crawler, hardware authenticity check or remote attestation.
  • Hosted status is an operational snapshot, not an offline revocation proof.
  • The consumer must enforce the gate on the same bytes it checked. There is no remote enforcement, secure launcher, restore route, remote install or model activation.
  • Withdrawal is irreversible and cannot stop a running program.

2. Release and credential API routes

Base: /api/apostille/v1. See authentication and existing issuance. All GET routes also support HEAD. Owner routes accept the administrator's workspace session. Three read/evaluation routes also accept the scoped CI bearer described in section 7; all other owner routes reject it. Another workspace or an out-of-scope CI credential gets 404 for a private release or policy. The public route takes the release's separate public_id.

Operation Method / path Access Request
Register evidence POST /releases Owner certificate_id, manifest_base64, visibility, authorization
Retrieve evidence and status GET /releases/{id} Owner session or scoped CI No body
Retrieve public evidence and status GET /public/releases/{id} Public id is the release's public_id; no body
Set receiver policy POST /release-policies Owner policy, authorization
Read current receiver policy GET /release-policies/{id} Owner session or scoped CI id is a policy label; no body
Evaluate metadata POST /release-policy/evaluations Owner session or scoped CI release_id, policy_id, policy_version, artifact_name, artifact_sha256, artifact_size
Withdraw POST /releases/{id}/status-events Owner release_id, action, reason, authorization
Create CI credential POST /release-credentials Owner session + signed authorization name, product, policy_id, expires_at, authorization
List CI credential metadata GET /release-credentials Owner session No body; includes expired/revoked history
Revoke CI credential POST /release-credentials/{id}/revoke Owner session No body; idempotent

visibility is exactly private or public. action is exactly withdraw. Withdrawal reasons are security_incident, superseded, or maintainer_request. The signed body must include release_id, matching the path parameter.

Writes use strict UTF-8 JSON with Content-Type: application/json, limited to 256 KiB. Duplicate keys, unknown fields and JSON numbers are rejected. Artifact sizes and policy versions are canonical decimal strings; operational counters stay JSON numbers. Never put a secret in a manifest or authorization.

Response records

A registration returns a Release with id, public_id, certificate_id, product, version, manifest_sha256, manifest_base64, is_public and created_at. Owner retrieval returns {release, bundle, current_status}. Public retrieval returns only public_id, product, version, manifest_base64, manifest_sha256, created_at, bundle and current_status. The public id does not work in owner routes.

current_status is {state, withdrawn_at, reason, agent_state, as_of, basis, offline_proof, issuer_key_revocation}. Release state is active or withdrawn; agent_state is active, revoked or expired. Before withdrawal, withdrawn_at is null and reason is empty. basis is hosted_operational_snapshot, offline_proof is false and issuer_key_revocation is unknown. Active does not mean authorized.

Policy responses contain id, decimal-string version, policy, policy_sha256, the signed authorization and created_at. Withdrawal returns {event, state:"withdrawn"}; event has id, release_id, reason and created_at. New mutations return HTTP 201; an identical retry returns 200.

3. Release manifest

Profile: https://ifandonlyif.io/apostille/supply-chain/release/0.1. Core artifact media type: application/vnd.iff.release+json.

Shape example, not canonical bytes. Calculate real digests and sizes locally.

json
{
  "profile": "https://ifandonlyif.io/apostille/supply-chain/release/0.1",
  "product": "example/agent-runtime",
  "version": "1.0.0",
  "artifacts": [
    {
      "name": "agent-runtime-linux-amd64.tar.gz",
      "sha256": "<64 lowercase hex characters from the actual file>",
      "size": "<canonical decimal byte count>",
      "media_type": "application/gzip"
    }
  ]
}

Requirements:

  • Canonicalize the manifest with the Core JCS helper, then encode those exact bytes as unpadded, canonical base64url in manifest_base64. The decoded manifest is at most 64 KiB with 1–32 artifacts.
  • product is at most 120 ASCII bytes, consisting of slash-separated labels. Each label and version is 1–80 bytes: an ASCII letter or digit first, then letters, digits, ., _ or -. Empty path segments are invalid. Matching is exact: no case folding, no SemVer ordering.
  • Artifact names are unique, use the same label grammar (128-byte limit), and contain no slash.
  • SHA-256 is 64 lowercase hex characters. Size is 0 or a decimal string without leading zeros, within signed 64-bit range. Media types are explicit type/subtype (no parameters or wildcards), at most 128 bytes.
  • The certificate must belong to the workspace, be fresh at registration, use the service's current issuer key for that Core version, and cover exactly these manifest bytes, size and media type. After issuer-key rotation, get a new certificate for new releases; earlier releases can still be evaluated with pinned historical issuer keys. The server loads its stored certificate.
  • Workspace, agent, certificate and administrator operation use the same Core version. Hosted Apostille issues Core 0.3 (ML-DSA-65, post-quantum) only, so POST /releases for a Core 0.1 workspace or certificate returns 410 core_0_1_retired. A policy may still list Core 0.1 in accepted_protocols to evaluate historical releases; evaluation and withdrawal are unchanged. There is no downgrade, mixed-version registration or Core 0.2 issuance.

(workspace, product, version) identifies one immutable release. A new artifact digest needs a new version. A retry cannot replace the manifest, certificate, visibility or authorization.

SBOMs, provenance documents and signature bundles can be named artifacts. This API does not validate SPDX, CycloneDX, SLSA provenance or Cosign signatures, assert a SLSA level, or prove a trusted build produced the bytes.

4. Separate administrator operation authorization

Release registration, policy updates, withdrawal and CI credential creation need this operation object in addition to the administrator session. The certificate submission separately needs its publication grant:

json
{
  "payload_base64": "<canonical authorization payload, unpadded base64url>",
  "statement": "<administrator-signed Core Envelope object, not a string>"
}

The decoded authorization payload has exactly these fields:

json
{
  "profile": "https://ifandonlyif.io/apostille/supply-chain/authorization/0.1",
  "purpose": "register_release",
  "audience": "https://ifandonlyif.io/apostille",
  "workspace_id": "<authenticated workspace UUID v4>",
  "body_sha256": "<SHA-256 of the canonical operation body>",
  "issued_at": "<UTC timestamp at whole seconds>",
  "expires_at": "<later UTC timestamp, at most five minutes after issued_at>",
  "nonce": "<new UUID v4>"
}

The decoded payload is canonical JSON, at most 4 KiB. The server accepts an issued_at up to two minutes ahead of its clock and requires now < expires_at; keep client clocks synchronized. The signed body excludes authorization:

Purpose Canonical body whose SHA-256 is signed
register_release {certificate_id, manifest_base64, visibility}
withdraw_release {release_id, action, reason}
set_release_policy {policy}
create_release_credential {name, product, policy_id, expires_at}

The administrator signs the payload as a producer-only Core origin-statement with media type application/vnd.iff.release-authorization+json. agent_id equals workspace_id; delegation_sha256 is empty. The issuer identity, key ID and public key must identify the workspace administrator. The statement's digest, size, media type, issued_at and nonce must match the payload.

Use the released Core signing helpers. Do not use SignChallenge, SignChallenge03, a login prefix or a login signature. An authorization cannot be reused across purposes, audiences, workspaces or bodies; the server checks the nonce atomically.

CI credential creation requires a Core 0.3 administrator. Credential revocation only removes access and needs the owner session, with no additional signed operation. A CI credential cannot create signing requests or authorize mutations.

5. Administrator-selected receiver policy

Exact fields; the example pins are placeholders. Obtain real pins independently of the bundle and key directory. Never copy embedded keys into a policy.

json
{
  "profile": "https://ifandonlyif.io/apostille/supply-chain/policy/0.1",
  "id": "production-agent-runtime",
  "version": "1",
  "product": "example/agent-runtime",
  "expected_issuer": "https://ifandonlyif.io/apostille",
  "trusted_issuer_key_ids": ["sha256:<independently obtained issuer fingerprint>"],
  "trusted_producer_key_ids": ["sha256:<independently obtained producer fingerprint>"],
  "accepted_protocols": ["https://ifandonlyif.io/apostille/spec/0.3"],
  "require_fresh_certificate": true
}
  • IDs use the 1–80-byte label grammar. Product matching is exact.
  • Version is a canonical decimal string from "1" to "1000000". The first version is "1"; each new version increments by one. Saved versions are immutable. GET /release-policies/{id} returns the latest version.
  • Each key list holds 1–16 distinct sha256: fingerprints. Issuer and producer pins are independent. accepted_protocols lists Core 0.1, Core 0.3 or both. Nothing is trusted implicitly.
  • require_fresh_certificate must be present. true requires the certificate's current validity interval. false permits historical certificates but still checks signatures, pins, agent authorization and withdrawal.
  • Evaluation uses the latest policy version; an older version selects nothing and allows no rollback. Only a separately signed administrator update changes the policy.

Origin certificates last at most 24 hours, bounded by delegation expiry. Expiry does not alter historical signatures; policy decides whether one suffices now.

6. Evaluation and the local file boundary

json
{
  "release_id": "<owner release UUID>",
  "policy_id": "production-agent-runtime",
  "policy_version": "1",
  "artifact_name": "agent-runtime-linux-amd64.tar.gz",
  "artifact_sha256": "<hash freshly calculated from the local file>",
  "artifact_size": "<byte count freshly calculated from the local file>"
}

The server reads the release, agent, withdrawal and current policy in one snapshot. An HTTP success can still carry a non-permitting decision:

Decision Meaning
allowed_by_policy The submitted metadata and evidence satisfy this policy at evaluation time.
blocked A checked condition forbids use, for example withdrawal, a digest mismatch or a failed policy constraint.
unknown Required evidence or current information does not establish permission.

The response includes decision, reasons, release_id, policy_id, policy_version, evaluated_at, valid_until, current_status, the submitted artifact_name/artifact_sha256/artifact_size, and artifact_bytes_checked: false (the service never sees file bytes). When a policy exists, policy_version and policy_sha256 describe its current version, even on a version mismatch; with none, the version is the requested value and the hash is absent. Bundle verification adds protocol, certificate_freshness and issuer_trust, absent after an early failure. organization_binding stays unproven and content_truth stays not_established.

Decision Current reason codes
allowed_by_policy policy_checks_passed
blocked release_withdrawn, agent_inactive, product_not_allowed, bundle_or_protocol_rejected, issuer_not_trusted, producer_not_trusted, manifest_binding_invalid, certificate_not_fresh, artifact_mismatch, artifact_not_listed
unknown policy_not_found, policy_version_mismatch, stored_policy_invalid, stored_release_invalid

Treat any non-permitting decision, including an unknown future reason, as a failed gate. The response reports the first failed condition only.

valid_until is an advisory window of at most 30 seconds, further bounded by the CI credential's expiry when one is used. Withdrawal, revocation or a policy update can follow the snapshot immediately; re-evaluate close to use.

A consumer must check the manifest/bundle and pins independently, hash the actual file, require allowed_by_policy, and use the same bytes it checked. Do not verify a pathname and later execute a replaceable file there; use an immutable staged artifact.

The reference command cmd/iff-release-check is source-only in the private hosted service repository, with no public binary or Go module. API consumers can implement the checks with the released Core SDK and this HTTP contract. To build it (macOS or Linux, Go version in go.mod, from that repository root), supply a local artifact, a token file and independent issuer/producer pins:

sh
go build -o ./bin/iff-release-check ./cmd/iff-release-check
./bin/iff-release-check \
  --base-url https://ifandonlyif.io \
  --access-token-file /secure/path/apostille-ci-token \
  --release-id '<private-release-uuid>' \
  --policy-id production-agent-runtime --policy-version 1 \
  --artifact-name agent-runtime-linux-amd64.tar.gz \
  --artifact /immutable/staging/agent-runtime-linux-amd64.tar.gz \
  --issuer https://ifandonlyif.io/apostille \
  --issuer-key-id 'sha256:<independently-obtained-issuer-fingerprint>' \
  --producer-key-id 'sha256:<independently-obtained-producer-fingerprint>'

For CI, use a scoped CI credential, never an administrator session or signing key. The token file holds only the bearer token (no Bearer prefix, no JSON): a regular file owned by the operator, mode 0600 or 0400, at most 4 KiB; symlinks are rejected. Other operating systems fail closed. The base URL must be a public HTTPS origin on port 443 with no path prefix, credentials, query or fragment. Redirects and environment proxies are disabled. The command neither uploads nor executes the artifact.

Stdout is one JSON object. Exit 0 means allowed_by_policy; exit 2 means an input/transport failure; exit 3 means the artifact or decision was rejected. Check the exit status and deadline, and keep the clock synchronized. Its artifact_bytes_checked: true describes its own local hash; the API response always says false. Treat a non-permitting, expired or unavailable result as a failed gate.

7. CI read-only credentials

Scope and lifetime

An administrator grants a credential access to one workspace, one exact product and one policy label. Scopes are immutable, with no wildcard or cross-workspace access. The policy must already exist for that product.

Allowed operation Required scope
GET/HEAD /releases/{id} Same workspace and exact product
GET/HEAD /release-policies/{id} Same workspace, exact policy label and product
POST /release-policy/evaluations Same workspace, exact product and policy label; request must still select the current policy version

All other owner routes reject CI credentials with HTTP 401 (invalid_session), including /me, certificate downloads, signing/submission, release registration, policy changes, withdrawal and credential management. Read access includes private manifests and certificate bundles for releases of the allowed product; protect the credential accordingly.

The bearer is opaque: iff_sc_ci_ followed by unpadded base64url encoding of 32 random bytes. The database retains only its SHA-256 digest. It is not a JWT or an offline proof. Expiry and revocation are checked on every request; an in-flight request may finish after revocation. A credential's expiry does not make a previous policy decision a continuing deployment authorization.

Creation requires an explicit future expires_at, at most 90 days from creation; 30 days is recommended. Use a canonical whole-second UTC timestamp. name is 1–120 UTF-8 bytes, with no control characters or leading/trailing whitespace. There are at most 100 active credentials and 1,000 total retained credentials per workspace. Expired and revoked metadata remains in history.

Administrator setup and one-time delivery

POST /release-credentials takes the four signed body fields below and the section 4 authorization, with purpose create_release_credential. The authorization still binds audience, workspace, nonce and a five-minute maximum lifetime; that lifetime is separate from the credential's expiry.

json
{
  "name": "production-artifact-check",
  "product": "example/agent-runtime",
  "policy_id": "production-agent-runtime",
  "expires_at": "<future whole-second UTC timestamp, within 90 days>",
  "authorization": {"payload_base64": "<canonical payload>", "statement": "<Core 0.3 Envelope object>"}
}

The first response is HTTP 201 {credential, token}. Metadata contains id, name, product, policy_id, created_at, expires_at and optional revoked_at. The raw token is returned only once. Store it directly in a private file or secret manager; never print or log the response.

An identical, still-authorized nonce/body retry returns HTTP 200 {credential}, without the token. The server cannot recover a lost token. After a lost response, list metadata to reconcile, revoke the old credential and create a replacement with a fresh nonce. A changed body with the same nonce conflicts. The owner-only list returns {credentials: []}; revoke returns {credential} and is idempotent.

Maintainers with access to the private service checkout can build the source-only cmd/iff-release-credential administrator helper. It signs and sends the creation authorization locally; it is not a published binary or a public go install target. Run it on the administrator's machine, outside CI:

sh
go build -o ./bin/iff-release-credential ./cmd/iff-release-credential
./bin/iff-release-credential \
  --base-url https://ifandonlyif.io \
  --admin-key-file /secure/admin/apostille-key.json \
  --name production-artifact-check \
  --product example/agent-runtime \
  --policy-id production-agent-runtime \
  --out-token /secure/ci/apostille-ci-token

The helper defaults to 30 days; --expires-at selects an explicit expiry up to 90 days. The Core 0.3 key must be an operator-owned regular file, mode 0600 or 0400, at most 4 KiB, with no symlink. Prepare an operator-owned 0700 output directory; the helper creates the token file as 0600 and refuses to overwrite an existing file. Stdout contains metadata/status only, never the token, key or administrator session. --issuer defaults to the base origin plus /apostille. Exit 0 is success; 2 is an input/session/API failure; 3 means a credential is known but its token was unavailable or could not be saved. Follow next_action: list_before_retry after an uncertain response, or revoke_credential when its ID is known. Do not retry creation blindly.

sh
./bin/iff-release-credential \
  --base-url https://ifandonlyif.io \
  --admin-key-file /secure/admin/apostille-key.json --list
./bin/iff-release-credential \
  --base-url https://ifandonlyif.io \
  --admin-key-file /secure/admin/apostille-key.json \
  --revoke '<credential-uuid>'

Keep the credential ID in operator records for revocation. CI receives only the scoped token. Rotation creates a replacement with the same intended scope, updates the CI secret, checks it, and revokes the old credential.

CI gate example

Provision a trusted iff-release-check build and the immutable staged artifact before this step. This GitHub Actions excerpt is for a protected workflow, with the scoped bearer in the IFF_SC_CI_TOKEN secret. Supply the current policy version and independent pins through reviewed configuration. An older version fails closed after a policy update.

yaml
steps:
  - name: Verify the staged release
    shell: bash
    env:
      IFF_SC_CI_TOKEN: ${{ secrets.IFF_SC_CI_TOKEN }}
    run: |
      set -euo pipefail
      set +x
      umask 077
      token_dir="$(mktemp -d)"
      export IFF_CI_TOKEN_FILE="$token_dir/token"
      trap 'rm -f "$IFF_CI_TOKEN_FILE"; rmdir "$token_dir"' EXIT
      python3 - <<'PY'
      import os
      token = os.environ.pop("IFF_SC_CI_TOKEN")
      fd = os.open(os.environ["IFF_CI_TOKEN_FILE"], os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
      with os.fdopen(fd, "w") as output:
          output.write(token)
      PY
      unset IFF_SC_CI_TOKEN
      ./bin/iff-release-check \
        --base-url https://ifandonlyif.io \
        --access-token-file "$IFF_CI_TOKEN_FILE" \
        --release-id '<private-release-uuid>' \
        --policy-id production-agent-runtime --policy-version 1 \
        --artifact-name agent-runtime-linux-amd64.tar.gz \
        --artifact /immutable/staging/agent-runtime-linux-amd64.tar.gz \
        --issuer https://ifandonlyif.io/apostille \
        --issuer-key-id 'sha256:<independently-obtained-issuer-fingerprint>' \
        --producer-key-id 'sha256:<independently-obtained-producer-fingerprint>'

Replace the non-secret placeholders before use. Never expose this secret to untrusted pull-request code or an unreviewed workflow, and never enable shell tracing around secrets. Keep it out of command-line arguments, job output, uploaded artifacts and caches. A CI bearer permits evidence reads and evaluation; the caller still verifies and uses the same local bytes. It proves neither file safety nor payment safety.

8. Go lifecycle example

Use the released github.com/ifandonlyif-io/iff-apostille/apostille core (Go root v0.4.0-alpha.1, Go 1.27) and its apostille/client package for login, registration and issuance. Supply your own administrator/agent key files and independent issuer pins. The Core client has no release wrapper.

go
// client is an authenticated apostille/client.Client. protocol, admin, agent,
// registration and manifest are supplied by your application; check all errors.
manifestRaw, err := core.Canonical(manifest)
if err != nil { return err }
statement, err := core.CreateStatementFor(protocol, bytes.NewReader(manifestRaw),
    "application/vnd.iff.release+json", agent, &registration, "", now)
if err != nil { return err }
grant, err := core.CreateGrantFor(protocol, statement, registration, admin,
    expectedIssuer, "private", now)
if err != nil { return err }
certificate, err := client.Submit(ctx, statement, grant)
if err != nil { return err }

registerBody := map[string]any{
    "certificate_id": certificate.ID,
    "manifest_base64": base64.RawURLEncoding.EncodeToString(manifestRaw),
    "visibility": "private",
}
// Sign registerBody with the authorization construction below, then POST
// {certificate_id, manifest_base64, visibility, authorization} to /releases.
// Sign {policy} with purpose set_release_policy and POST /release-policies.
// Before use, hash the actual local artifact and call /release-policy/evaluations.
// To withdraw, sign {release_id, action:"withdraw", reason:"security_incident"}
// with purpose withdraw_release and POST /releases/{id}/status-events.

This authorization construction runs locally with public Core helpers; the private key never enters a request. body is one of the bodies in section 4.

go
now := time.Now().UTC().Truncate(time.Second)
nonce, err := core.NewID()
if err != nil { return err }
bodyRaw, err := core.Canonical(body)
if err != nil { return err }
payload := map[string]any{
    "profile": "https://ifandonlyif.io/apostille/supply-chain/authorization/0.1",
    "purpose": purpose,
    "audience": expectedIssuer,
    "workspace_id": workspaceID,
    "body_sha256": core.Hash(bodyRaw),
    "issued_at": now.Format(core.TimestampLayout),
    "expires_at": now.Add(5*time.Minute).Format(core.TimestampLayout),
    "nonce": nonce,
}
payloadRaw, err := core.Canonical(payload)
if err != nil { return err }
header, err := core.NewHeaderFor(protocol, core.KindStatement,
    core.KeyIdentity(admin.KeyID()), admin, now)
if err != nil { return err }
authorizationStatement, err := admin.SignFor(protocol, core.KindStatement, core.Statement{
    Header: header, AgentID: workspaceID, DelegationSHA256: "",
    ArtifactSHA256: core.Hash(payloadRaw), ArtifactSize: strconv.Itoa(len(payloadRaw)),
    ArtifactMediaType: "application/vnd.iff.release-authorization+json", Nonce: nonce,
})
if err != nil { return err }
authorization := map[string]any{
    "payload_base64": base64.RawURLEncoding.EncodeToString(payloadRaw),
    "statement": authorizationStatement,
}
// Add authorization to the request without including it in body_sha256.

Use HTTPS to a configured origin with bounded requests and timeouts. Keep tokens out of URLs, logs and command-line arguments. The public read route needs no bearer token. Choose public only after reviewing the exact manifest and bundle.

9. Visibility, withdrawal and retries

Public release evidence needs both an explicit public registration authorization and a currently public certificate. Hiding the certificate hides its release; a public workspace does not publish a release. Public responses exclude private workspace/certificate mappings and administrator authorizations.

Withdrawal leaves manifest and certificate bytes unchanged and is kept privately as an immutable record. Downloaded copies stay with their holders.

The administrator nonce and canonical body hash identify retries. The same nonce with different content conflicts; an identical retry returns the original record. The authorization must still be valid on retry, and registration rechecks the certificate. Use fresh authorization for a new mutation. After a lost response, retrieve the existing record; never infer rollback from a timeout. Product/version collisions and skipped or stale policy versions are conflicts, not updates.

HTTP errors

Errors use the {error:true,code,message} envelope; base API authentication and rate limits apply. An HTTP 200 blocked or unknown evaluation is not an error.

HTTP Release error codes Action
400 invalid_release, invalid_release_manifest, release_certificate_mismatch, invalid_release_status_event, invalid_release_policy, invalid_release_evaluation, invalid_release_credential Correct the body, canonical encoding, certificate binding or credential scope/expiry before retrying.
401 invalid_ci_credential The CI bearer is invalid, expired or revoked; do not fall back to an administrator token in CI.
403 invalid_release_authorization, release_publication_denied, agent_inactive Obtain valid scoped administrator authorization or resolve the state; a bearer token does not bypass it.
404 certificate_not_found, release_not_found, release_policy_not_found, release_credential_not_found Check the identifier, workspace and CI scope. Private or out-of-scope records are not disclosed.
409 release_nonce_conflict, release_conflict, policy_version_conflict, release_already_withdrawn, release_credential_nonce_conflict, release_credential_conflict Read the existing state. A conflicting retry cannot overwrite it, change credential scope or restore a withdrawn release.
429 release_write_limit, release_rate_limit, release_credential_limit Respect any supplied Retry-After; reconcile active/history credential limits rather than retrying in a tight loop.
503 release_service_unavailable, apostille_service_unavailable, release_rate_limit_unavailable, release_credential_unavailable Treat the gate as unavailable; retry with bounded backoff and check state after an uncertain write.

Initial store limits: 10,000 releases per workspace and 100 new release writes per workspace per UTC day, counting registrations, policy versions and withdrawals. Withdrawals bypass quota admission. Registration, policy creation and evaluation share a per-IP release limit (APOSTILLE_RELEASE_RATE_LIMIT_PER_MINUTE, default 60), separate from the sign-in challenge limit.

10. Threat model and operational acceptance

Threat Control and remaining limit
File substituted after approval Manifest digest plus local byte verification detects the mismatch; the consumer must enforce the gate.
Wrong producer, issuer or product Exact independent pins, product matching and current administrator policy. Embedded keys alone do not establish trust.
Stolen bearer session A release mutation also needs a scoped administrator origin-statement over its exact body.
Stolen CI credential Immutable workspace/product/policy scope and expiry limit access; revoke it to stop new requests. Scoped private evidence can still be read, and an in-flight request may finish. No signing or mutation authority is granted.
Replayed operation or policy rollback Tenant-scoped nonces, immutable versions and latest-policy evaluation; offline copies need the receiver's own historical policy.
Publisher or trusted build compromised Valid signatures can cover malicious bytes. This profile does not replace code review, isolated builds or signing-key controls.
Hardware substituted A signed document cannot establish which physical device is in a box or provide hardware attestation.
Delayed withdrawal information Hosted checks report one current snapshot; offline checks do not fetch revocation and cannot infer current authorization.

Before deployment, run an up/down/up test of the release migration on an isolated database, run API/store tests with both Core versions, and verify private/public isolation, certificate hiding, concurrent version conflicts, policy rollback rejection and irreversible withdrawal. The public HTTP MCP profile stays read-only.

Run migration 12 before serving release requests; a generic health check does not cover the new tables, so verify a private release lifecycle afterwards. To roll back to an image with migrations only through 11, keep schema 12 and either provide the complete migration files or disable migration execution. Never run the destructive down migration on production release records.

The CI credential extension additionally requires migration 13 before enabling its routes. Verify clean up/down/up on an isolated database and test scope isolation, expiry, revocation, one-time token delivery and nonce replay. Preserve credential history on rollback; do not run the down migration on production credentials. Capability flags alone do not establish a completed rollout.

Maintainers with private-repository access: internal/supplychain/types.go (profile types), profile.go (manifest and policy validation), authorization.go (operation authorization) and internal/apostille/supplychain_types.go (storage types).