Approval Use Journal
Local append-only memory of consumed approvals. Records are truth; indexes are cache.
The Approval Use Journal is a per-workspace append-only JSON store that records every consumed Approval Grant. It's the load-bearing piece behind v0.9.9's local-journal replay level: with the journal present, treeship verify can say "use 1/1 — local Approval Use Journal passed" instead of the older "package-local only — no global ledger consulted."
Layout
<config_dir>/journals/approval-use/
journal.json # metadata: kind, version, format
records/ # truth (append-only)
0000000001.approval-use.<short-digest>.json
0000000002.approval-use.<short-digest>.json
0000000003.approval-revocation.<short-digest>.json
...
heads/
current.json # {index, digest, updated_at}
indexes/ # rebuildable cache
by-grant/<safe-name>.txt
by-nonce/<safe-name>.txt
backfill/<use_id>.txt # action_artifact_id sidecar
locks/
journal.lock # fs2 exclusive lock for appendsThe directory follows the same precedence rule as Agent Cards and Harness state from v0.9.8: project-local <workspace>/.treeship/journals/approval-use/ first, falling back to the global config's location.
Records are truth, indexes are cache
Two principles, both load-bearing:
-
Records are truth. Every appended file is a complete, digest-stable JSON document. The integrity check (
treeship approval journal verify) walks records in index order, recomputes eachrecord_digest, and asserts the chain matches. -
Indexes are cache.
indexes/by-grant/andindexes/by-nonce/exist to makecheck_replayfast. They can be deleted at any time and rebuilt from records viajournal::rebuild_indexes. If you don't trust them, run the rebuilder.
The split keeps the trust surface small: the only thing the journal needs to keep honest is the records directory and its hash chain. The indexes can be stale, missing, or even corrupted without affecting the trust answer.
Hash chain
Every record carries:
previous_record_digest—record_digestof the prior record (empty for the genesis record)record_digest— sha256 of the canonical-JSON form of the record withrecord_digestitself cleared
The first invariant is checked by verify_integrity: tampering with any field changes record_digest, which means the next record's previous_record_digest no longer matches.
record N record N+1
+----------+ +-----------------+
| ... | | ... |
| digest:X |<-| prev_digest: X |
+----------+ | digest: Y|
+-----------------+A broken chain is detected at any record by walking from the genesis. Tampering with one record breaks every subsequent verification.
Three record types
The journal carries three sibling record types in the same chain:
| Type | What it records | Wired in |
|---|---|---|
treeship/approval-use/v1 | A grant was consumed by an action | v0.9.9 PR 2 |
treeship/approval-revocation/v1 | An approver revoked a grant | v0.9.9 PR 1 (schema), wiring deferred |
treeship/journal-checkpoint/v1 | Signed Merkle commitment over a record range | v0.9.9 PR 1 (schema), Hub-signed consumer-side verifier in v0.9.9 PR 6 |
All three follow the same digest discipline. A future record type (e.g. delegation) plugs in as a fourth variant without breaking the chain check.
What's in an ApprovalUse — and what isn't
{
"type": "treeship/approval-use/v1",
"use_id": "use_b733c1a3c90d84df",
"grant_id": "art_2a325283550936d0c32a15ba",
"grant_digest": "art_2a325283550936d0c32a15ba",
"nonce_digest": "sha256:67e3db06f16af062...",
"actor": "agent://deployer",
"action": "deploy.production",
"subject": "env://production",
"use_number": 1,
"max_uses": 1,
"idempotency_key": "abc123",
"created_at": "2026-04-30T07:13:00Z",
"previous_record_digest": "",
"record_digest": "sha256:..."
}What you'll never find on disk: raw nonce, command, prompt, file_content, bearer_token, or api_key. The privacy invariant is pinned by the test record_files_contain_no_raw_nonce_or_signature_secrets — by construction, since the schema doesn't have those fields.
CLI
treeship approval uses <grant-id> # list every recorded use
treeship approval status <grant-id> # use_count vs max_uses, would-exceed
treeship approval journal verify # walk chain, report integrityAll three are read-only. Writes happen via treeship attest action --approval-nonce (PR 3's consume-before-action flow); the journal layer doesn't expose a "manually append a use" command, deliberately.
Idempotency keys
Crash-recovery primitive. When treeship attest action --idempotency-key <key> --approval-nonce <n> runs:
- If a use record for this grant carries the same key, the consume flow collapses to that record and signs a fresh action against it (no second use slot consumed).
- If no record carries the key, a fresh use is reserved.
- Without
--idempotency-key, retries always allocate a new use andmax_usesenforcement applies normally.
This means a flaky network or crashed CLI can retry safely without burning a use slot per retry.
What this is not
- Not a public ledger. No external publishing. The journal is private workspace memory.
- Not SQLite. Source of truth is the JSON file tree, not a database.
- Not a Hub server. v0.9.9 PR 6 ships the consumer-side verifier for
JournalCheckpoint { kind: HubOrg }records: when a package embeds an org-signed checkpoint that covers every use,treeship verifyemitsreplay-hub-org PASS. The Hub server itself, the thing that signs those checkpoints, is out of scope for v0.9.9 and lives in a separate release. - Not a global single-use enforcer on its own. The local journal cannot speak for what happens on another machine. Without an embedded Hub-signed checkpoint, verify caps at
local-journaland never claimshub-orgorglobal single-use.
Records are truth, indexes are cache is also the recovery story. If your machine drops the indexes (filesystem corruption, manual rm, etc.), journal::rebuild_indexes reconstructs them from records. The trust answer is unaffected.
See also
- Approval Authority — the wider model
- Replay levels — what each level proves
- Approvals — minting, consuming, verifying flows