# Gatesi receipt and trace format, version 1

This document describes the two files a Gatesi produces so that anyone can check a run of an AI agent
without trusting the server that recorded it: the **trace** (`agentgate-trace/2`) and the **receipt**
(`agentgate-receipt/1`). Everything here is plain JSON, SHA-256 and Ed25519. You can write a checker in any
language from this page alone. Two reference checkers are published next to this file
(`/verifier/agentgate_verify.py`, `/verifier/agentgate_verify.mjs`).

## 1. What a valid receipt does and does not prove

A valid receipt proves: the holder of the issuer's private key signed exactly this run id, agent id, chain head,
event count, decision counts and digest of proxied calls, at the stated time.

With the matching trace it also proves: the events in that trace are exactly the events the receipt's chain head
commits to, in order, and none has been edited, removed from the middle, reordered or added.

It does NOT prove: that the agent did nothing outside Gatesi, that the issuer is honest, or that the other side
of a proxied call agrees with the recorded fingerprints. The receipt is the issuer's own attestation. Who the issuer
is, is decided by the public key you trust (compare `issuer.key_id` with a source you trust).

## 2. Canonical JSON

Whenever a value is hashed or signed it is first written in canonical form:

- `undefined` counts as `null`
- numbers, strings, booleans and `null` are written as JavaScript's `JSON.stringify` writes them
- arrays: `[` items joined by `,` `]`
- objects: `{` then `"key":value` pairs joined by `,`, keys sorted by plain (UTF-16 code unit) order, `}`
- no spaces or line breaks anywhere

Note for other languages: write non-ASCII text exactly as `JSON.stringify` does (characters are written as
themselves, not as `\uXXXX`, except control characters and lone surrogates). Whole numbers print without `.0`.

## 3. Trace (`agentgate-trace/2`)

    { "format": "agentgate-trace/2", "run": { ...run fields... }, "events": [ { ...event fields... }, ... ] }

Each event has at least: `run_id, seq, agent_id, ts, type, tool, action, args, decision, reason, cost, result,
approval_id, prev_hash, hash`. Events recorded before hash chaining existed have `seq` = null and are not covered.

### 3.1 Chain

    GENESIS = 64 zeros
    for each chained event in order of seq (1, 2, 3, ...):
        prev_hash must equal the hash of the previous event (GENESIS for seq 1)
        hash = SHA-256 hex of the UTF-8 text:   prev_hash + "|" + JSON.stringify([
                 run_id, seq, agent_id, ts, type, tool, action, args,
                 decision', reason, cost, result, approval'
               ])

with these rules (each missing value is `null`):

- `decision'`: if `type` is `check` and the stored `decision` is one of `approved, denied, expired, cancelled`
  use the text `pending` (a later human answer is chained as its own `approval` event); otherwise the stored decision
- `cost`: `0` if missing or zero
- `approval'`: `null` if `type` is `check`, otherwise the stored `approval_id`
- `args` and `result` are the stored text exactly as stored (they are already JSON text, do not parse them)
- this array is written with plain `JSON.stringify` (NOT the canonical form of section 2)

Seqs must be 1..n with no gaps. The last event's `hash` is the **chain head**.

A checker should also confirm for every `approval` event with `approval_id` X and a settled decision, that every
`check` event with approval_id X and a settled decision carries the same decision.

## 4. Receipt (`agentgate-receipt/1`)

    {
      "format": "agentgate-receipt/1",
      "issuer": { "brand": "...", "algorithm": "Ed25519", "key_id": "<16 hex>", "public_key": "<64 hex>" },
      "run":    { "id": "...", "agent_id": "...", "started_at": ms, "last_at": ms },
      "chain":  { "head_seq": n, "head_hash": "<64 hex>", "events": n },
      "counts": { "<decision>": n, ... },
      "calls":  { "count": n, "truncated": bool, "digest": "<sha256 hex>" },
      "issued_at": ms,
      "signature": "<128 hex>",
      "calls_detail": [ ... ]            // optional, not signed directly
    }

- Signed bytes: the canonical JSON (section 2) of the receipt without `signature` and without `calls_detail`,
  as UTF-8. Algorithm Ed25519 (RFC 8032), public key `issuer.public_key` (32 bytes, hex), signature 64 bytes, hex.
- `issuer.key_id` is the first 16 hex characters of SHA-256 of the `public_key` hex text.
- `calls.digest` is SHA-256 hex of the canonical JSON of the list of calls (at most 500, oldest first). If
  `calls_detail` is present, its canonical JSON must hash to `calls.digest`.
- `chain.head_seq`/`head_hash`/`events` must match the trace: the number of chained events, the last `seq`, the last `hash`.

## 5. Steps to check a receipt with its trace

1. Check the receipt's format string and fields.
2. Compute `key_id` from the public key; compare it with the key id you trust.
3. Verify the Ed25519 signature over the canonical payload.
4. If `calls_detail` is present, check its digest.
5. With a trace: check `trace.run.id` equals `receipt.run.id`, replay the chain (section 3.1) and compare head and count.

Any failure means do not trust the receipt.

## 6. Versioning

The format strings (`agentgate-trace/2`, `agentgate-receipt/1`) change if anything above changes. A checker must
refuse formats it does not know.
