---
url: 'https://adk.nht.io/batteries/orchestration/approval.md'
description: >-
  What an operator approves is the authority set bound to a digest — and the
  gate refuses a record that does not match the plan exactly.
---

# Approval

## LLM summary — orchestration approval

* `approvePlan(store, planId, record)` recomputes the reachable authority set from the folded log and asserts SET-EQUALITY with `record.authoritySet` BEFORE touching the store. A mismatch returns a `TransitionResult`-shaped failure (`digest_mismatch`), never a throw.
* `computeAuthoritySet(view, alreadyLive?)` — sorted, de-duplicated, REACHABLE-only claims. `alreadyLive` omits a claim an authority layer reports already granted.
* `AuthorityClaim` = `{capability, scope, verb}`; `AuthorityVerb` is the CLOSED set `list`|`read`|`create`|`update`|`delete`.
* `ApprovalRecord` = `{planId, digest, authoritySet, decidedBy, decidedAt, disposition}`; persisted in the SAME COMMIT as the transition.
* Activation is ALL-OR-NOTHING: a refused approval leaves state, digest and revision untouched.
* Asserting the authority gate against `transition` is WRONG — by contract the store checks only lifecycle state and digest.

An operator does not approve a vague feeling that "this plan looks fine." Treating human-in-the-loop as a rubber stamp on a chat summary is security theater: the model promises one thing, executes another, and the audit log records a human signing off on an uninspected blast radius.

Here, an operator approves an enumerable, closed claim: **this exact set of authorities, over this byte-for-byte content.**

```typescript
const record: ApprovalRecord = {
  planId: 'archive-q3',
  digest: state.digest,           // the content they were shown
  authoritySet: [                 // what it may do
    { capability: 'file', scope: '/reports/q3', verb: 'read' },
    { capability: 'file', scope: '/backup',     verb: 'create' },
  ],
  decidedBy: 'operator@example.com',
  decidedAt: new Date().toISOString(),
  disposition: 'approved',
}

const result = await orchestration.approvePlan('archive-q3', record)
```

## The authority set

`computeAuthoritySet` traverses the folded plan and computes the canonical union of what every **reachable** `call` claims. It does not care what an agent says it intends to do; it extracts what the IR will actually execute:

* **De-duplicated and sorted**, deterministically. Two plans whose authors listed identical claims in different orders yield the exact same set. If they did not, the operator UI would present cosmetic differences as separate sets, and set-equality checks would fail on array ordering.
* **Reachable only.** An unreachable node cannot execute. Deriving authority from dead code tricks an operator into approving ambient privileges the plan can never exercise. We prune unreachable claims before the review surface is built.
* **Optionally filtered.** Pass `alreadyLive`, and any claim your backing authority layer reports as already granted is omitted from the prompt. The operator reviews only the net-new delta requiring a decision. That filtering governs the *display*; it never weakens the validation gate, which checks the full reachable set against the plan.

The verb set is **closed** — `list`, `read`, `create`, `update`, `delete`. Open-ended verb vocabularies make authority sets impossible to compare mechanically, which reduces policy enforcement back to string matching and hope.

## Set equality, before the store is touched

`approvePlan` recomputes the authority set directly from the folded log and asserts strict set-equality against `record.authoritySet`. If the record misses a single claim or invents an extra one, the operator signed something other than what the plan executes. The gate rejects the operation **before** writing a single byte to the store:

```typescript
// The plan grants file:read AND file:create; the record names only one.
const result = await orchestration.approvePlan('archive-q3', narrowRecord)
// result.ok === false — and the plan is still `reviewable`, untouched.
```

Failure returns a **value**, not a thrown exception — a `TransitionResult`-shaped refusal that callers handle through normal control flow. Because `TransitionResult` carries no arbitrary message field, an authority mismatch reports as `digest_mismatch`. That is not a workaround: an altered authority set is, by definition, different content.

Activation is strictly **all-or-nothing**. A rejected approval leaves plan state, digest, and revision completely untouched.

::: warning We are about to state an opinion
Test the authority gate against `approvePlan`, never against `transition`. By contract, the store checks only lifecycle state and digest. It does not evaluate policy, and it cannot: deciding whether a tool is on a tier-C allowlist or whether a reference taints a call argument requires battery knowledge that a BYO storage adapter does not have. **The battery validates; the store commits.** A test asserting the authority rule against `transition` tests the wrong layer and passes for the wrong reason.
:::

## What a forged record can and cannot do

::: warning We are about to state an opinion
`approvePlan` is the only supported route to `executable`, and the reason is worth being blunt
about rather than leaving implied.

A caller who skips it and invokes the store's `transition` directly — with a hand-built
`ApprovalRecord` and a valid `expectedDigest` — **will have that record persisted**. `transition`
proves the digest and the lifecycle state. It does not check `authoritySet`, and it cannot:
recomputing that set means walking the graph and knowing what a `call` node is, which is battery
knowledge a BYO store does not have. That is the same split as everywhere else here — the battery
validates, the store commits.

What that forged record does **not** do is grant anything. The executor never reads
`authoritySet`; what a run may invoke is bounded by the tier-C allowlist enforced at freeze. A
plan naming a tool outside that allowlist is refused before it can be approved at all, and a plan
cannot be edited after approval without an unfreeze that sends it back through the gate.

The damage is to the **audit trail**. `readApproval` hands back what was written, so a forged
record tells anyone reading it that an operator approved something they never saw. If you display
or audit an approval, recompute the set with `computeAuthoritySet` and compare — do not trust the
stored copy to describe the plan.
:::

## The digest binds it

The record carries the digest of the content the operator inspected, and the transition passes it as `expectedDigest`. This closes the stale-approval race by construction:

```mermaid
sequenceDiagram
  autonumber
  actor A as Operator A
  actor B as Actor B
  participant P as Plan

  A->>P: read — digest D1
  Note over A: reviewing the plan<br/>they were shown
  B->>P: unfreeze, edit, refreeze
  Note over P: content is now D2
  A->>P: approve, carrying D1
  P--xA: REFUSED — plan is no longer at D1
```

1. Actor A reads the plan at digest **D1** and begins review.
2. Actor B unfreezes, mutates the plan, and refreezes — now at digest **D2**.
3. Actor A submits an approval carrying **D1**.
4. The store rejects the write: the plan is no longer at **D1**.

Actor A never approves content nobody showed them. That is the entire reason approval binds a content digest rather than a mutable plan ID.
