> For the complete documentation index, see [llms.txt](/llms.txt)

# The verification report

Generating an SDK once is easy; *proving it's still correct on every regeneration, forever* is
the hard part — and that proof is the **verification report**. Glotto regenerates your
SDKs, docs site and MCP server in memory, compares
against the committed output, and emits a deterministic report that records, for the exact
inputs used:

- **Pinned inputs** — the spec hash (canonical and raw), the `glotto.yml` config hash, and the
  tool version. Two runs over identical inputs produce byte-identical reports.
- **Per-target results** — for each generated target: the **drift** status (does the committed
  output still match what the generator produces?), the **managed-file integrity** status (has a
  checksum-stamped generated file been hand-edited? — your [custom code](/docs/custom-code) in
  `lib/` is yours and is never flagged), and the **compile** and **contract** statuses
  (`not-run` / `ok` / `failed` — see [check results](#real-compile--contract-statuses-the-checks-loop)
  below for how these become real).
- **Two renderings** — a JSON document (`reportVersion: 1`, with a machine-readable
  `summary`) and a Markdown rendering for humans. Both land in your CI artifacts, and the
  Markdown is what appears on the pull request.

Drift and integrity are deliberately distinct signals: drift is checksum-aware, so a hand-edited
managed file with an intact embedded checksum does **not** drift — only the integrity check
catches hand-edits. The report carries both, and verification exits non-zero when either
fails. See [verification report](/docs/verification-report) in the CLI reference for the full flag
set, including checkout-free verification of a remote branch (`--provider`).

## What a report looks like

Nothing below is typed by hand. Every pane is what `renderVerificationReportMarkdown` emitted
over the committed fixture this repo dogfoods, produced the same way your CI produces yours.

**a clean regeneration of the committed SDK**

A clean regeneration. Six targets, no drift, every checksum-stamped file intact.

**regenerated + byte-diffed in CI** `renderVerificationReportMarkdown (@glotto/cli)` `packages/cli/tests/fixtures/drift-gate` `43c4d424bd49`

targets 6

drifted 0

hand-edited managed files 0

```markdown
# Glotto verification report

**Result: ✓ verified**

## Inputs

| Input | Value |
|---|---|
| Spec | `./petstore.openapi.yaml` (openapi) |
| Spec sha256 (canonical) | `a7da38d032e4642349d2e4531a12400d2634905b9d78dadc681b571c1d064760` |
| Spec sha256 (raw) | `88fd4f083e7abc781f553ba21280d697d86116c40b9ced90f18a4f06f7e472f8` |
| Config | `glotto.yml` |
| Config sha256 | `a243677cd26512cf42a63076089373818acb76aeba92de35441555cd5945cc77` |
| Tool | glotto 0.0.0 |

## Targets

| Target | Kind | Files | Drift | Custom code | Compile | Contract |
|---|---|--:|---|---|---|---|
| cli | sdk | 9 | ✓ in-sync | none | not-run | not-run |
| graph | sdk | 4 | ✓ in-sync | none | not-run | not-run |
| mcp | mcp | 38 | ✓ in-sync | ✓ intact (31 managed) | not-run | not-run |
| root | root | 1 | ✓ in-sync | none | not-run | not-run |
| spec_repo | sdk | 10 | ✓ in-sync | none | not-run | not-run |
| typescript | sdk | 18 | ✓ in-sync | ✓ intact (11 managed) | not-run | not-run |
```

`compile` and `contract` read `not-run` above because building a report never runs a toolchain —
the report says what it verified and what it did not, rather than implying more. Real statuses
enter as a declared input, and then the same report carries them:

**the same regeneration, with the TypeScript target's compile status folded in from a real `` `tsc --noEmit` `` run**

The same regeneration, with the TypeScript target's compile status folded in from a real tsc run. One status attested; the rest still say not-run, because they were not.

**regenerated + byte-diffed in CI** `renderVerificationReportMarkdown (@glotto/cli)` `packages/cli/tests/fixtures/drift-gate` `ea9ae2632587`

targets 6

compile statuses attested 1

failed checks 0

```markdown
# Glotto verification report

**Result: ✓ verified**

## Inputs

| Input | Value |
|---|---|
| Spec | `./petstore.openapi.yaml` (openapi) |
| Spec sha256 (canonical) | `a7da38d032e4642349d2e4531a12400d2634905b9d78dadc681b571c1d064760` |
| Spec sha256 (raw) | `88fd4f083e7abc781f553ba21280d697d86116c40b9ced90f18a4f06f7e472f8` |
| Config | `glotto.yml` |
| Config sha256 | `a243677cd26512cf42a63076089373818acb76aeba92de35441555cd5945cc77` |
| Tool | glotto 0.0.0 |

## Targets

| Target | Kind | Files | Drift | Custom code | Compile | Contract |
|---|---|--:|---|---|---|---|
| cli | sdk | 9 | ✓ in-sync | none | not-run | not-run |
| graph | sdk | 4 | ✓ in-sync | none | not-run | not-run |
| mcp | mcp | 38 | ✓ in-sync | ✓ intact (31 managed) | not-run | not-run |
| root | root | 1 | ✓ in-sync | none | not-run | not-run |
| spec_repo | sdk | 10 | ✓ in-sync | none | not-run | not-run |
| typescript | sdk | 18 | ✓ in-sync | ✓ intact (11 managed) | ok | not-run |
```

And when a managed file has been hand-edited in place, the report is where you find out — drift
alone will not tell you, because drift is checksum-aware:

**a managed file hand-edited in place — `` `typescript/src/client.ts` `` no longer hashes to its embedded checksum**

One managed file edited without restamping. Nothing drifted; the integrity check caught it and the report fails.

**regenerated + byte-diffed in CI** `renderVerificationReportMarkdown (@glotto/cli)` `packages/cli/tests/fixtures/drift-gate` `3a6c95e8e1a5`

targets 6

drifted 0

hand-edited managed files 1

```markdown
# Glotto verification report

**Result: ✗ verification failed**

## Inputs

| Input | Value |
|---|---|
| Spec | `./petstore.openapi.yaml` (openapi) |
| Spec sha256 (canonical) | `a7da38d032e4642349d2e4531a12400d2634905b9d78dadc681b571c1d064760` |
| Spec sha256 (raw) | `88fd4f083e7abc781f553ba21280d697d86116c40b9ced90f18a4f06f7e472f8` |
| Config | `glotto.yml` |
| Config sha256 | `a243677cd26512cf42a63076089373818acb76aeba92de35441555cd5945cc77` |
| Tool | glotto 0.0.0 |

## Targets

| Target | Kind | Files | Drift | Custom code | Compile | Contract |
|---|---|--:|---|---|---|---|
| cli | sdk | 9 | ✓ in-sync | none | not-run | not-run |
| graph | sdk | 4 | ✓ in-sync | none | not-run | not-run |
| mcp | mcp | 38 | ✓ in-sync | ✓ intact (31 managed) | not-run | not-run |
| root | root | 1 | ✓ in-sync | none | not-run | not-run |
| spec_repo | sdk | 10 | ✓ in-sync | none | not-run | not-run |
| typescript | sdk | 18 | ✓ in-sync | ✗ modified (1 of 11 managed) | not-run | not-run |

### Modified managed files: `typescript`

- `typescript/src/client.ts` — hand-edited (embedded @glotto:generated-checksum no longer matches)
```

## Where the report lands

The same report is delivered on four surfaces, so the proof is visible wherever you review a
regeneration.

### In CI, on every PR

The CI workflows Glotto emits (the CI workflow Glotto emits) gate every PR/MR with
verification and publish the report to the job summary and the build artifacts — on GitHub,
GitLab, Bitbucket, and Azure alike. The check fails on drift **or** a hand-edited managed file,
and the report is attached even when the job fails. See [Drift
detection](/docs/drift-detection) for the gate itself.

### On the release PR, as a comment

Glotto's hosted verification can post the report directly on your open release PR — the place
you actually review a regeneration. The semantics are deliberately conservative:

- **Opt-in, default off.** Commenting writes to *your* repository, so it only happens when you
  enable the verification source's `pr_comment` flag — it is never on retroactively.
- **One comment, refreshed in place.** The comment is marker-tagged per project: every
  succeeded verification run — and every [checks ingest](#real-compile--contract-statuses-the-checks-loop)
  into the latest run — updates the *same* comment rather than stacking a new one per run.
- **Best-effort.** A comment failure (no open PR on the verified branch, a provider error)
  never changes the verification run, the ingest result, or the API response — the report is
  the record; the comment is a convenience.

### In the console

The project's **surface health** panel shows the latest verification runs — verified or not,
which targets drifted, per-target compile/contract statuses — rendered honestly from the stored
report (a status that never ran shows as `not-run`, never as a green check).

### As a signed attestation

Any stored successful run can be downloaded as a **signed attestation**: a single
self-contained JSON file — a DSSE v1 envelope, Ed25519-signed — that binds the exact report
document to the run that produced it. Verify it offline against the public key served by the
unauthenticated `GET /v1/verification/attestation-key` endpoint; the envelope is standard DSSE,
so a stock crypto library is all an auditor needs. This is the artifact an enterprise buyer can
file: cryptographic evidence that a given SDK surface was verified against a given spec.

Verify it with one command — `glotto verify-attestation attestation.json` — which resolves the
signing key by the fingerprint the envelope itself records (so an artifact stays verifiable across a
key rotation), works [completely offline](/docs/cli#glotto-verify-attestation) from an archived key
file, and reports a revoked key as a distinct fact from a bad signature.

### Counter-signed by you, not just by us

A single-party attestation is only as trustworthy as the party issuing it. You can **co-sign** an
attestation with your own Ed25519 key, so the artifact carries a signature that does not depend on
trusting Glotto:

```sh
glotto verify-attestation attestation.json --countersign my-key.pem --upload
```

Three things this deliberately does:

- **Verification runs first.** An envelope that failed verification is never counter-signed — the
  command has no mode that vouches for something it did not check.
- **The signature is verified before it is stored.** Your co-signature is checked against the run's
  own payload server-side; an unverifiable one is rejected rather than filed, because an envelope
  that *looks* multi-party while proving nothing is worse than one that carries no counter-signature.
- **A co-signature binds one exact payload.** If a later CI check-results upload changes the report,
  every counter-signature over the old bytes is dropped from the envelope and listed as `stale`
  rather than quietly served — re-run the command to sign the new payload.

A counter-signature proves *who else vouched for these exact bytes*. It carries **no timestamp** and
is not a trusted clock: Glotto deliberately ships no RFC 3161 or transparency-log integration, so
nothing in the artifact claims to establish *when* it was signed.

## Real compile & contract statuses (the checks loop)

verification never runs a compiler — your SDKs actually compile, and the contract suites
actually run, in **your CI**, where the toolchains and the generated output live. So the
compile/contract statuses enter the report as a declared input: your CI produces a
checksVersion-1 **check-results document** recording per-target `ok`/`failed` outcomes, then
either

- folds it into the report before rendering, where a `failed` status makes verification
  fail; or
- posts it to the hosted run —
  `POST /v1/projects/:projectId/verification-runs/:runId/checks` folds it into the stored
  report, and the updated statuses ripple to the console panel, the attestation, and the
  release-PR comment.

The document can pin the spec hash it was produced against, so results from different spec
bytes are rejected rather than silently folded in.

## Verified provenance in CI

When your CI posts check-results to the hosted run, the control plane can verify **which CI run
produced them** — not merely that a valid API token uploaded them. Your runner presents a
provider-signed CI-run OIDC token alongside the upload; the server checks it against the
provider's public keys and marks the run **verified**, and that verified origin is bound into
the [signed attestation](#as-a-signed-attestation) an auditor downloads.

- **Turn-key on GitHub and GitLab.** The workflows Glotto emits wire this for you: the GitHub
  template grants the job `id-token: write` so the runner can mint an OIDC token, and the GitLab
  template pre-mints one via an `id_tokens:` block. With hosted mode enabled (a project id + API
  token), verified provenance is on by default — there is nothing else to configure.
- **Manual on other CI.** On Bitbucket, Azure Pipelines, a self-hosted runner, or any custom CI,
  inject a pre-minted CI-run OIDC token into the environment variable **`GLOTTO_CI_OIDC_TOKEN`**;
  hosted mode presents it automatically. (GitHub's request-and-exchange path reads
  `ACTIONS_ID_TOKEN_REQUEST_*` instead, so it needs no explicit token variable.)
- **The audience contract.** The token's `aud` claim must equal your Glotto API URL, which must
  in turn match the server's `CI_OIDC_AUDIENCE`. For the default hosted service that is
  `https://api.glotto.dev` — already the value the emitted templates use, so there is nothing to
  set. If you **self-host** the control plane, set the token audience (the GitLab `id_tokens:`
  `aud`, or `GLOTTO_API_URL` for GitHub's exchange) to your own API URL and configure the
  server's `CI_OIDC_AUDIENCE` to match.

Provenance verification is optional and non-breaking: with no token the upload still succeeds —
the run is simply recorded **unverified** rather than verified.

## Why this works

The report is only as trustworthy as the regeneration behind it, and that's the point of the
[deterministic pipeline](/docs/pipeline): a byte-stable canonical spec means regeneration is
reproducible, so a drift result is a *real* change and a byte-identical report is a real
guarantee — not noise.
