> ## Documentation Index
> Fetch the complete documentation index at: https://preloop.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Attestations

> Read signed job evidence, distinguish local reports from hosted observations, and show prior receipts, signers, and publication state.

An attestation is the signed record of one job attempt. It captures the exact
tested source, the workflow-byte digest and binding state, the runner environment,
and evidence that could or could not be collected. The engine attempts to sign
and archive each terminal job attempt after settlement (subject to archive
quotas), whether it ran on this engine or on a local engine reporting to a
trusted signer.

Collection is best-effort and never changes the job result. A collection
failure is logged, and the run's latest-projection manifest still reports what
was recorded. A process crash in the short window between settlement and
archive can leave no receipt; there is no durable pending-archive outbox yet,
so do not claim an attestation that the API cannot return.

The API is read-oriented. A receipt is immutable; a later facts correction or a
new attempt adds another receipt and leaves the earlier signed bytes untouched.

## What the UI can show

`GET /api/v1/attestations` is the **previous attestations** list. It returns
bounded summaries, newest first, and an opaque cursor. A row has:

* receipt id and creation time;
* repository, run, job, attempt, terminal conclusion;
* captured source: kind, snapshot commit, tree, dirty/unknown, execution
  binding (`bound`/`unbound`/unknown), and a compact commit-signature badge
  (signed commit, format, status, trust, coverage, key fingerprint, verifier)
  when captured;
* signer issuer/key id/algorithm;
* authenticated reporter subject/organization;
* execution origin, assurance (`observed` or `reported`), observer;
* the reporter's location assertion, if any (untrusted);
* manifest and unsigned-report digests;
* completeness and named evidence gaps.

`GET /api/v1/attestations/{id}` is the **detail** view. It adds the exact signed
manifest, DSSE envelope, pinned **public** JWKS snapshot, deterministic unsigned
report, and publication status. Use the manifest for "what was attested":

| UI section | Fields |
| - | - |
| Source | `spec.source` — captured tree/snapshot, dirty flag, workflow digest and binding, merge parents, `execution_binding` (`bound`/`unbound`/`unknown`), and `commit_signature` evidence for the exact head commit. Only `bound` ties the captured tree to what the job's checkouts executed; it requires every redirected checkout step to have completed successfully, and every other value is named in `collection.missing` as `spec.source.execution_binding`. |
| Commit signature | `spec.source.commit_signature` — format, cryptographic status, trust, coverage, key fingerprint, descriptive principal/trust source/reason; `attestation.commit_signature_assurance` says whether this receipt's signer observed the verification. `coverage = tested-tree` covers the captured tree; present it as covering executed bytes only together with `execution_binding = bound`. |
| Workflow and job | `spec.repository`, `workflow`, `event`, `git_ref`, `job` |
| Environment | `spec.environment` — per-attempt golden evidence, images, resources |
| Actions | `spec.actions` — declared and resolved refs, tarball digest when pinned |
| Execution | `job.completed_at` (server-stamped for observed receipts; reporter-provided for reported ones), `job.steps`, `job.conclusion`, `logs`, `tools`, caches, VM facts; each artifact entry has `scope: "run"` because the catalog is run-wide and may include another job's upload |
| Evidence gaps | `collection.missing` and `collection.complete` |
| Trust | `attestation` — signer, reporter, assurance, observer, signed time |
| Publication | `publication.status`, remote id/digest/issuer, bounded error |

An example summary:

```json theme={null}
{
  "id": "4b8f…",
  "created_at": "2026-10-27T18:42:10Z",
  "repository": "acme/widgets",
  "run_id": "0f1e…",
  "job_id": "build (node 20)",
  "attempt": 1,
  "conclusion": "success",
  "source": {
    "kind": "local-worktree",
    "snapshot_commit": "9a3f…",
    "tree": "c71d…",
    "dirty": true,
    "execution_binding": "bound",
    "commit_signature": {
      "commit": "4d2e…",
      "format": "ssh",
      "status": "valid",
      "trust": "trusted",
      "coverage": "commit-only",
      "key_fingerprint": "SHA256:…",
      "verifier": "engine"
    }
  },
  "signer": {"issuer": "https://cpane.example", "key_id": "kid-1", "algorithm": "RS256"},
  "reporter": {"subject": "developer:alice", "organization": "team-a"},
  "execution": {"origin": "unknown", "assurance": "reported", "observer": "reporter"},
  "asserted_origin": "local",
  "manifest_digest": "sha256:…",
  "report_digest": "sha256:…",
  "complete": false,
  "missing": ["environment.golden.execution_binding", "vm"]
}
```

## Assurance: signature is not observation

A valid signature says **who signed the record**. It does not prove the signer
watched the job run.

* `execution.assurance = observed` means the signing engine assembled the
  record from its own control-plane state and captured snapshot.
* `execution.assurance = reported` means an authenticated caller submitted it.
  The signature proves the trusted signer accepted that report.
* `execution.origin` is a location the receiving service established from
  operator configuration. `unknown` is the honest value when it did not.
* `asserted_origin` is the reporter's claim. Show it as an assertion, never as
  verified execution location.

For UI policy, a hosted-observed badge requires `assurance == observed` **and**
the origin policy the organization chose. A cryptographically valid receipt of
a local report must not display the same badge.

The same distinction applies to `spec.source.commit_signature`.
`attestation.commit_signature_assurance = observed` means this engine verified
the commit signature at source intake; `reported` means the evidence came from
the authenticated reporter and was not independently verified by the signing
service. Render `status = valid`, `trust = trusted`, and
`coverage = tested-tree` only when all three are present: a valid signature
over a dirty or merged source is `coverage = commit-only` and covers the named
commit, not the executed bytes. `coverage = tested-tree` still describes the
captured snapshot tree, so do not present it as covering the executed bytes
unless `spec.source.execution_binding = bound` as well. When the binding is
`unbound` or `unknown`, show the captured-source evidence and the
`spec.source.execution_binding` gap instead. Never treat the commit's
author/committer text or a `Signed-off-by` trailer as a cryptographic signer
identity.

## History and pagination

The list is a live keyset feed, newest first. It is not a frozen export: a
receipt committed after a cursor was issued can appear above that cursor.
`limit` defaults to 25 and caps at 100. Pages are also byte-bounded. The cursor
is bound to the authenticated organization and the selected filters; do not
decode or construct one.

Detail and export are the exact evidence. The list never loads signed bodies,
so it is safe to render a dense history table and fetch detail on selection.

## Publishing local receipts

A local engine can publish to one configured trusted signer. Its locally
observed receipt remains, and the hosted receipt is stored as a separate
receipt linked by the unsigned report digest. If the dedicated remote
credential uses the same subject and organization as the local intermediate
reporter, the archive's replay key dedupes the hosted receipt into that local
row; the publication relation still carries the authoritative remote id, and
the hosted envelope remains at the trusted signer.

The publication relation reports:

| Status | Meaning |
| - | - |
| absent | No publication was attempted from this engine (no trusted signer configured, or this receipt was not published here). The `not_configured` enum value is reserved and not currently emitted. |
| `pending` | One bounded attempt is in progress. |
| `acknowledged` | The hosted signer returned a verified receipt. |
| `failed` | The attempt was definitely rejected. |
| `unknown` | The request may have committed remotely (for example a timeout). Retry with the same report identity. |

`POST /api/v1/attestations/{id}/publish` retries the configured target through
the local engine. It cannot be pointed at another URL or credential. Because
the report identity is stable, a retry after a lost acknowledgement returns
the existing hosted receipt rather than creating a second claim.

## Verification

`preloop attestation show <id> --verify` checks the DSSE payload type,
canonical bytes, signature, both digests, the normalized report digest, and
that the signed signer key id matches the verifying key. Use
`--trusted-jwks <file>` to pin an approved key set, including a retired key.
The pinned JWKS returned in a receipt is evidence, not its own trust anchor.
It verifies the receipt, not the Git commit signature: the commit evidence was
verified by the engine at snapshot intake and is reported here as signed
evidence. Re-verifying it would require the original commit object and the same
operator trust root; `preloop manifest` prints the recorded status, trust,
coverage, fingerprint, and reason.

## Privacy and limits

A manifest never contains a secret value, and it does not contain a salted
secret-value commitment: a public salt would let anyone test low-entropy
guesses offline. Secret names and the `env.secret_values` evidence gap remain.
Ambiguous environment values and an unbound VM image are also gaps, not
inferred claims.

The API accepts at most a 4 MiB canonical manifest, enforces bounded field
counts and entry lengths, and bounds the archive per reporter and per
organization. The organization bounds are 100,000 receipts and 4 GiB of
retained bodies; an external reporter is additionally bounded to 10,000
receipts and 1 GiB of retained bodies, so one reporter cannot spend the whole
organization budget. Every bound is charged from an aggregate usage ledger
inside the receipt-insert transaction. The engine's automatic observer is
exempt from both per-reporter bounds (it remains inside the organization
bounds) so automatic attestation does not stop after a fixed number of job
attempts or bytes. These caps are not operator-configurable, and receipts are
immutable: when the organization bound is reached, new receipts (including
automatic ones) are refused until the archive is exported and rotated.
Exhaustion returns a retryable error; accepted receipts are never silently
dropped.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.