Receipts in CI: a pipeline that proves what the agent did.

A pipeline page tells you a job passed. It does not give you a record of the run you can check without trusting the runner. So the AER-1 kit pipeline now closes its own run as a verifiable workflow: one receipt for the step call, one closing receipt, and a single root digest over the set. On October 13, 2026 the template ran end to end in a live pipeline, and the root was recomputed independently and matched. Everything below is public: the pipeline, the job, the workflow, the template. Zambo is the verifiable work layer for AI: the same endpoint that runs the work issues the receipt for it.

See the building block first: the live demo runs one real call and hands you its verifiable receipt in about 10 seconds, no signup.

Published October 13, 2026. Updated October 15, 2026. Written against draft-zambo-aer1-23.

The proof run

Pipeline 2944444147, every job green

Pipeline: https://gitlab.com/rambozambodotdev/zambo/-/pipelines/2944444147 (branch ci-receipt-proof of the AER-1 kit repository). The receipt job zambo_receipt_close ran in the .post stage after the kit's own verify jobs: selftest, conformance, chain vectors, conformance page, middleware tests, and the cross-verify matrix. All green.

Step receipt: 698630bf-ec71-43c1-b0cc-0d3ad10893ef, a live_price call (BTC $83,397.00) made on the pipeline session at 2026-10-13 22:06:53 UTC. It verifies clean at the public verify endpoint.

Workflow: zambo.dev/workflow/d4582939-a1dd-457a-b618-d2d3c3dda0b3, closed by the same job seconds later (closing receipt 8a53634e-ee64-4806-9610-4fb4f16361b3).

Root: a29b4589bc31d93e4299c6182b1323f6c496601edbb489116b2f4bd56b0e546b

Recompute: the Section 8.1 construction for a one-step workflow says the root is the SHA-256 of the step receipt id itself. Computed from the id alone, off the record, it equals the served root exactly, under both workflow id forms. A first passing run (pipeline 2944428438, workflow f20901ea-cf61-4897-a828-934293cbdf3d) recomputed the same way, same result.

The pipeline also leaves its receipt note, zambo-receipt.md, as a job artifact on the pipeline page: pipeline, commit, ref, the step response, the closing response, and the workflow link. A reviewer who already lives on the pipeline page meets the record at the moment they are deciding whether to trust the run.

The first run failed, and the record says why

The template's first live run (pipeline 2944414222) did not produce a workflow. Its closing call was refused with HTTP 409, in the artifact, in plain text: closing an empty workflow is refused by design, and the template as first written called the close without making any step call first, even though its own header documented the step-then-close flow. The fix was small and is in the template's commit history: make the one summary call on the pipeline session first, then close, and print the workflow link in the note. The next run passed end to end. That is the point of running a template before calling it proven: the failure was public, specific, and fixable, and now the header says PROVEN with the pipeline named.

A second, unrelated failure surfaced in the same first pipeline: the kit's conformance-page check failed because its page generator let invalid test fixtures (one carries the literal timestamp "yesterday") poison the generated header date, and hardcoded a stale kit version. The generator now reads the kit version from CITATION.cff and only lets real timestamps compete for the header date. Fixed in the same evening's commits, and the check has been green since.

How it works

  1. Include the template. The job lives in aer-1/ci/gitlab-ci-receipt.yml in the kit repository. Copy the job into your .gitlab-ci.yml or include the file.
  2. Flip one variable. ZAMBO_RECEIPT_MINT defaults to off (artifact summary only). Set it to "true" and the job also mints through the public endpoint at https://zambo.dev/mcp. No token, no secret, no stored credential.
  3. The job runs last, and cannot break your build. It is a .post job with when: always and allow_failure: true. A receipt problem is recorded in the note; it never fails the pipeline.
  4. Steps, then close. Calls made with the pipeline session id (gitlab-ci- plus the pipeline id) each return an AER-1 receipt. A final workflow_close binds them into one workflow with one root digest. Extend the step list to record one call per job if you want per-job receipts instead of one summary call.
  5. The record lands where reviewers already look. The note is a job artifact on the pipeline page, with the workflow link inside. An optional MR note is off by default.
The format behind the receipt is AER-1, an IETF Internet-Draft (draft-zambo-aer1-23), an open draft and not a finished standard.

Can CI check whether an agent actually ran the tests it claims?

Yes, when the test run itself goes through a receipt-issuing call. The agent runs the tests as a tool call, and the call returns an AER-1 receipt that commits to the inputs and the saved output at the moment the call ran. CI then checks the claim against the record: fetch the receipt, recompute its hash, and compare. If the saved output was edited afterwards, or the claimed result was never recorded, the check fails.

That is exactly the shape of the proof run above. The kit's own selftest, conformance, chain-vector, and cross-verify jobs ran inside pipeline 2944444147, and the same pipeline closed as workflow d4582939-a1dd-457a-b618-d2d3c3dda0b3 under root a29b4589bc31d93e4299c6182b1323f6c496601edbb489116b2f4bd56b0e546b. The test record and the run record are one checkable set, and the root was recomputed from the step receipt id alone and matched. A claim of "tests passed" that has no receipt behind it, or whose saved output no longer hashes to the receipt, does not survive that check.

The limit stays narrow: the receipt proves the saved result was not changed since it was saved. It does not prove the tests were the right tests, or that passing them makes the code correct.

What this run proves, and what it does not

The claim stays narrow on purpose. The closing root commits to the exact set of step receipts, and each receipt commits to the output saved when its call ran. Recompute and any later change to a saved output, or to the set, breaks the match. That is what the recompute above demonstrates.

Two honest limits. First, it does not grade the work: a receipt proves the record was not changed since it was saved, never that the answer was right. Second, this proof run exercised the free close: the workflow page is in its free state, unsealed. Sealing, the step that keeps the page, badge, and PDF pack permanent, is the paid Day Pass step ($0.99 at zambo.dev/day-pass/) and was exercised separately, not in this pipeline. Nothing in the run above needed a payment, a token, or a secret.

Run it on your pipeline

Receipts in CI: FAQ

Can a CI pipeline produce receipts for agent work?

Yes. A pipeline job can call a receipt-issuing endpoint as work runs, one call per step, and close the run at the end. Each step call returns an AER-1 execution receipt naming the tool that ran, its inputs, and its saved output, committed under a hash at execution time. The closing step binds the step receipts into one workflow under a single root digest, so the run can be checked as a set, not just step by step. The AER-1 kit pipeline does exactly this on its proof branch: a .post job closes the run as a workflow and leaves the receipt note on the pipeline page.

Can CI check whether an AI agent actually ran the tests it claims?

Yes, when the test run itself goes through a receipt-issuing call. The agent runs the tests as a tool call, and the call returns an AER-1 receipt that commits to the inputs and the saved output at the moment the call ran. CI then checks the claim against the record: fetch the receipt, recompute its hash, and compare. If the saved output was edited afterwards, or the claimed result was never recorded, the check fails. In the proof pipeline on this page, the kit's own conformance and selftest jobs ran in the same pipeline that closed as a workflow, so the test record and the run record form one checkable set. The limit stays narrow: the receipt proves the saved result was not changed since it was saved. It does not prove the tests were the right tests, or that passing them makes the code correct.

How do I add receipt checks to GitLab CI?

Copy the receipt job from the AER-1 kit template (aer-1/ci/gitlab-ci-receipt.yml) into your .gitlab-ci.yml, or include the file, and set ZAMBO_RECEIPT_MINT to true. The job runs in the .post stage with when: always and allow_failure: true, so it records the run without ever breaking your build. It makes one summary call on a session named for the pipeline, closes the workflow, and writes zambo-receipt.md as a job artifact with the workflow link inside. Extend the step list if you want one receipt per job instead of one summary receipt per pipeline. No secrets or tokens are needed: the endpoint is public.

What does a sealed pipeline run prove?

Sealing fixes a closed run in place: the workflow page, its verify badge, and its PDF pack stay put after the free viewing window ends. What the run proves, sealed or not, stays narrow. The closing root commits to the exact set of step receipts, and each receipt commits to the output that was saved when its call ran. Recompute the hashes and any change to a saved output, or to the set itself, breaks the match. It proves the record was not changed since it was saved. It does not prove the work was correct, only that the record of it is the record that was made at the time. The proof run on this page used the free close; sealing is the paid Day Pass step and was exercised separately.