# Transparency log

hushrun's server keeps each repository's events in an append-only log, and signs what it says about that log with a key `hushrun.yaml` pins, so every command can check the server instead of trusting it.

## The log key in hushrun.yaml

Every write to a repository on hushrun is a leaf of an append-only Merkle log. The server signs the log's checkpoints, each listing it answers, and its answer about who counts, with the repository's log key. `hushrun.yaml` names that key at its top level, so the server can't vouch for itself:

```yaml title="hushrun.yaml"
transparency_log:
  origin: api.hushrun.com/l/acme/api
  key: "ed25519:…"
```

- `npx hushrun init` writes these lines when the server tells it the key. Otherwise, `hushrun doctor` prints them.
- `origin` is the repository's URL on hushrun without its scheme, and `key` the log's Ed25519 public key.
- hushrun reads them with the rest of `hushrun.yaml`, as committed, so changing them is a reviewed change like any other.

## What every command checks

With the key pinned, every command that reads or writes the repository's events checks what the server answers:

- the receipt of each write;
- the signed statement of each listing;
- the signers answer, which says who counts;
- that every checkpoint it meets, the server's own and those recorded inside events, belongs to one history.

A check that fails ends the command with exit code 6. `hushrun plan --start` records the listings its decision read in its `started` event, for monitors to check, and `hushrun mark deployed` refuses an attempt without that event, so every deploy has one.

Until `hushrun.yaml` names the key, hushrun checks none of this, and warns on every command.

## Monitor the log

A listing that leaves an event out could hide a failure from a plan. `hushrun repository monitor` finds it afterwards:

```bash
hushrun repository monitor --state monitor.json
```

- It replays the repository's log, and checks it against the server's latest signed checkpoint, which must extend the one the last run kept in `--state`.
- It checks every checkpoint and listing plans recorded, every deletion against an org admin's signed statement, and every map the server committed to.
- It exits 5 when the server contradicts its own log, and prints the server's own signed statements as proof, and 0 when everything agrees.
- It needs read access only, and `hushrun.yaml`'s log key.

The monitor that counts is one the server's operator doesn't run. Run it from your own CI, every hour:

```yaml title=".github/workflows/hushrun-monitor.yml"
name: Monitor the hushrun repository
on:
  schedule:
    - cron: "17 * * * *"
  workflow_dispatch:

concurrency:
  group: hushrun-monitor
  cancel-in-progress: false

jobs:
  monitor:
    runs-on: ubuntu-latest
    # id-token: write lets hushrun ask for an ID token whose audience is the repository's URL.
    permissions: { contents: read, id-token: write }
    steps:
      - uses: actions/checkout@v4                       # hushrun.yaml names the log key
      - uses: hushrun/hushrun/action/setup@main        # pin to a release commit
        with: { version: 0.1.0 }
      # The newest checkpoint the last run verified: a server that rolls back between runs
      # contradicts it.
      - uses: actions/cache/restore@v4
        with:
          path: hushrun-monitor.json
          key: hushrun-monitor-${{ github.run_id }}
          restore-keys: hushrun-monitor-
      - run: hushrun repository monitor --state hushrun-monitor.json --format json > monitor.json
      - if: always()
        run: jq '{ok, checkpoint: .checkpoint.size, checked, findings}' monitor.json
      - uses: actions/cache/save@v4
        if: success()
        with:
          path: hushrun-monitor.json
          key: hushrun-monitor-${{ github.run_id }}
      - uses: actions/upload-artifact@v4               # the findings and their proofs, if any
        if: failure()
        with: { name: hushrun-monitor, path: monitor.json }
```

## Who can read and write

A repository's events must be append-only for CI: a pipeline that could delete or overwrite an event could remove a `failed` result and unblock a digest that never passed. Signatures can't prove an event is missing, so hushrun's server enforces it:

| Who | Events | Delete |
|---|---|---|
| CI, with its ID token or a writer token | Append and read | Never |
| A member or an agent, from a registered machine | Append what that machine signed, and read | Never |
| Org admins and owners | Read | Only with `hushrun repository gc` |

- A token has a role: `reader` reads, `writer` also appends events, and `admin` also deletes. hushrun creates each event only if it doesn't exist yet, and the roles keep a client that misbehaves from overwriting or deleting one.
- A GitHub Actions job with `id-token: write` gets the writer role for the repository its ID token names. Elsewhere, CI uses an agent token ([GitLab CI](https://hushrun.com/docs/promotion/gitlab.md#variables)).
- An org admin makes repository tokens, `hrl_…`, for monitors and scripts, on the repository's page in the dashboard. They go in `HUSHRUN_REPOSITORY_TOKEN`, or come from the command in `HUSHRUN_REPOSITORY_TOKEN_COMMAND`.
- hushrun reads tokens only from the environment, never from the repository, and never logs them. `HUSHRUN_REPOSITORY_TOKEN` goes only to a URL that `HUSHRUN_REPOSITORY_URL` or `--repository-url` names. A URL only `hushrun.yaml` names gets a CI job's ID token; outside CI, it gets the login you made for its origin, or an agent token when it is on hushrun's own host.
- A repository's URL is `https://`; `http://` works only on a loopback host.

Old events go with `hushrun repository gc`, run by an org admin or owner from their signed-in machine, first with `--dry-run`:

```bash
hushrun repository gc --older-than 180d --dry-run
```

It keeps the newest deployment of each scope and stage, and the events of its digest. On a repository that keeps a log, it signs each deletion with the admin's machine, so a deletion is never the server's own doing, and monitors report any deletion without such a signature.
