# Supply-chain integration guide 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](https://ifandonlyif.io/apostille/api). 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": "", "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": "", "statement": "" } ``` 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": "", "body_sha256": "", "issued_at": "", "expires_at": "", "nonce": "" } ``` 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:"], "trusted_producer_key_ids": ["sha256:"], "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": "", "policy_id": "production-agent-runtime", "policy_version": "1", "artifact_name": "agent-runtime-linux-amd64.tar.gz", "artifact_sha256": "", "artifact_size": "" } ``` 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 '' \ --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:' \ --producer-key-id 'sha256:' ``` 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": "", "authorization": {"payload_base64": "", "statement": ""} } ``` 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 '' ``` 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 '' \ --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:' \ --producer-key-id 'sha256:' ``` 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, ®istration, "", 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).