Treeship
Concepts

Approval Authority

How Treeship turns scoped approvals into consumable, auditable authority.

Treeship's approval model is designed around four claims, each made by a different artifact:

Nonces bind. Scopes authorize. Approval Uses prove consumption. Local journals enforce local replay. Hub checkpoints upgrade replay to org/distributed. Reports say exactly which level was checked.

Each layer answers exactly one question, and the report format makes it impossible to confuse one layer for another.

The model

ArtifactWhat it provesWhere it lives
Approval Grant (treeship/approval/v1)An approver authorized this scope: who, what, against which subject, how many timesSigned envelope in chain + package
Approval Use (treeship/approval-use/v1)This grant was consumed by this actionLocal journal + (optionally) package
Journal Checkpoint (treeship/journal-checkpoint/v1)A signed Merkle commitment to a contiguous range of journal recordsLocal journal + (optionally) package
Session ReceiptDeterministic proof of what happenedPackage
Session ReportHuman-readable explanation with evidence pointersGenerated from package

What each layer proves (and doesn't)

The grant alone proves binding. The action's approval_nonce matches a real signed approval. That's all. It does not mean the action was within the approver's scope, and it does not mean the approval wasn't reused.

The grant + scope proves authorization. The approval's ApprovalScope (introduced in v0.9.6) signs allowed_actors, allowed_actions, allowed_subjects, and max_actions into the grant. Verify checks each axis statelessly. An unscoped approval (no constraints) gets an explicit warning, not silent acceptance.

The Approval Use proves consumption. A signed-or-chained record in the local Approval Use Journal that says "this grant was consumed by this action at this time, as use N/M." Without a use record, you only know the action claimed the grant — not that the grant was reserved before signing.

The journal proves local replay. With the journal present, verify can say "use 1/1 — local Approval Use Journal passed" rather than the older "package-local only — no global ledger consulted."

A signed Hub checkpoint proves distributed replay. v0.9.9 ships the consumer-side verifier: when a .treeship package embeds a JournalCheckpoint with kind: HubOrg that's signed by an org and explicitly covers each use_id in the package, verify emits replay-hub-org PASS. The Hub server itself — the thing that signs checkpoints — is out of scope for v0.9.9 and lives in a separate release. Until a Hub signs a checkpoint and you embed it, any "global single-use" claim is overclaiming, and Treeship physically cannot say it.

The flow

1. Approver mints grant
   treeship attest approval --approver human://alice \
     --allowed-actor agent://deployer \
     --allowed-action deploy.production \
     --allowed-subject env://production \
     --max-uses 1
   → ApprovalStatement with random nonce + ApprovalScope

2. Agent attempts action with the nonce
   treeship attest action \
     --actor agent://deployer \
     --action deploy.production \
     --subject env://production \
     --approval-nonce <nonce>

3. consume_approval (PR 3) runs in this exact order:
   a. resolve grant by nonce            (cheap rejection: no grant)
   b. expiry check                      (cheap rejection: expired)
   c. scope check                       (cheap rejection: actor/action/subject)
   d. acquire journal lock
   e. idempotency-key short-circuit     (retry collapses to existing use)
   f. check_replay -> max_uses          (refuse if would exceed)
   g. RESERVE ApprovalUse with action_artifact_id = None
   h. SIGN action
   i. backfill action_artifact_id (sidecar; doesn't change the
      digest-stable record)
   j. stamp approval_use_id into action.meta

Crash semantics

If the process dies between g. (reserve) and h. (sign), the use is on disk. A retry without an idempotency key — or with a different one — sees the use as already-consumed and refuses if max_uses would be exceeded. A retry with the same --idempotency-key collapses to that record and signs a fresh action against it.

This is the reserved-counts-as-consumed rule: the trust property is "nobody can sign two actions against a single-use grant by racing," and we get that property by writing the use first. The retry primitive (idempotency key) gives crash-safety without weakening the trust property.

Replay levels

Reports surface up to four replay levels, each with its own row:

LevelWhat it checksStatus today
package-localDuplicate uses inside this packageAlways available
local-journalWorkspace <config>/journals/approval-use/ consultedAvailable since v0.9.9 PR 2
included-checkpointEmbedded JournalCheckpoint records verify offlineAvailable since v0.9.9 PR 4
hub-orgSigned Hub checkpoint validates global single-useAvailable since v0.9.9 (consumer side) — never claimed without a real checkpoint signed AND covering every use_id

A row reports the strongest level it actually achieved. It never silently downgrades, and it never claims a stronger level than the evidence supports. See Replay levels for the full ladder.

"Global single-use enforced" requires a verified Hub checkpoint that covers every use. v0.9.9 ships the consumer-side check: with a real org-signed JournalCheckpoint { kind: HubOrg } embedded in the package whose covered_use_ids includes every embedded use_id, verify emits replay-hub-org PASS. Without that evidence, the row is absent and the honest output caps at local-journal (or included-checkpoint for offline verifiers). The Hub signer is out of scope for v0.9.9 and lives in a separate release.

Privacy posture

The Approval Use Journal stores nonce_digest, never raw nonces. Records do not contain commands, prompts, file contents, bearer tokens, API keys, or secrets. The journal answers exactly one question:

"Has (grant_id, nonce_digest) been consumed before, and if so how many times?"

Everything else stays in the signed grant + receipt where it was already going to live.

See also