> ## Documentation Index
> Fetch the complete documentation index at: https://docs.corbits.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Durable approvals

> How the control plane suspends an agent when an action needs human approval, parks the call durably, and resumes the exact call once approved, surviving process crashes and reconnects.

When a grant resolves to `ask`, the agent does not fail and it does not guess. The control plane suspends the call, parks it durably, waits for a human decision, and then resumes the exact call that was paused. This is the durable approval substrate: the mechanism a human-in-the-loop control needs to stay trustworthy under real conditions, including process crashes and network reconnects.

<Note>
  This page describes the **control plane** and the agent runtime beneath it: the layers that evaluate the `ask` decision, park the call, and resume it. [Workbench](/platform/workbench) builds an approver experience on top, but the parking and resuming described here are not app features. The `ask` decision itself, and who may resolve it, are covered in [Guardrails](/platform/guardrails).
</Note>

## The `ask` decision suspends the call

The grant engine's effect decides what happens to a tool call:

| Effect   | Outcome                                            |
| -------- | -------------------------------------------------- |
| `allow`  | The call proceeds.                                 |
| `deny`   | The call is blocked.                               |
| `ask`    | The call is **suspended** and parked for approval. |
| no match | The call is blocked, fail-closed.                  |

`ask` is the effect that suspends. The call is parked and the agent's turn pauses cleanly, and the model is never handed a refusal it could try to work around.

## The lifecycle of an approval

<Steps>
  <Step title="Snapshot">
    At the moment of suspension the control plane captures an approval snapshot: the tool's name, its description, its input schema, and the **actual arguments** of the call being made. The approver reviews the real operation, not an opaque id. These fields are captured at suspension and stored with the approval, so the record is complete the instant it exists.
  </Step>

  <Step title="Durable park">
    The suspended call is persisted, not held in memory. Two records are written in one transaction: an `approval` row and a correlation that ties the eventual decision back to the exact parked call. An approval's status is `pending`, and resolves to one of `approved`, `rejected`, `timeout`, or `expired`. Because the park is durable, the pending approval outlives the process that created it.
  </Step>

  <Step title="Resolve">
    A decision is made through the API: approve or reject. Two properties keep this correct. It is **exactly once**: the resolver claims the correlation and flips the approval status in a single transaction, so a redelivered decision cannot resolve the same approval twice. And it **leaks no ids**: an approval id from another tenant is reported as not found before authorization runs, so a caller never learns a foreign id exists by getting a different error.
  </Step>

  <Step title="Resume">
    Approval does not ask the model to try again. The run **re-dispatches the exact call it parked**, with the snapshot arguments, so the approved operation is the one that runs. A one-shot, in-memory token lets that call clear its own gate once and only once. The agent keeps its place rather than re-inferring and hoping to reissue the same call.
  </Step>
</Steps>

Who may resolve an approval, and how visibility is scoped, is part of the grant model: resolving is itself a grant-gated action, it is pull-based, and nothing is pushed to a human. That is covered under the `ask` effect in [Guardrails](/platform/guardrails).

### Rejection and timeout do not strand the agent

A rejected or timed-out call does not leave a dangling request in the conversation. The control plane closes the open tool call with a synthetic error result and lets the model infer once more, so the agent continues from a coherent history instead of a stranded one.

## Durable under failure

The substrate is built to survive the failures a long-running approval will actually meet.

* **Child crash.** If the process running the agent dies while a call is parked, a fresh run re-drives its durable log and re-offers the same park. The pending approval is not lost with the process.
* **Hub reconnect.** When a supervisor reconnects, it re-queries its durably parked correlations and re-registers them. The hub deduplicates on a unique constraint, so a reconnect cannot create a second copy of the same pending approval.

## Resolving through the API

Approvals are resolved through the tenant-scoped API under `/api/tenants/:tenantId/approvals`:

| Method and path             | Purpose                                                |
| --------------------------- | ------------------------------------------------------ |
| `GET /`                     | List pending approvals (scoped by the caller's grant). |
| `GET /:approvalId`          | Read one approval and its snapshot.                    |
| `POST /:approvalId/approve` | Approve the parked call.                               |
| `POST /:approvalId/reject`  | Reject it.                                             |

The admin UI has a list view and a detail view, and **the detail view is read-only by design**: it shows the snapshot and status but carries no approve or reject control. Resolving is API-only.

## What this substrate does not do

Documenting the control means documenting its edges. Today, on the agent-step path:

* **There is no standing "approve always."** The control plane resolves an approval as approve-once or reject; the API rejects `scope: "always"`. Durable auto-approval exists one layer up, as a [Workbench](/platform/workbench) capability, and is described in [Guardrails](/platform/guardrails).
* **Approvals are pull, not push.** Nothing is routed, assigned, or notified to a human. Approvers find pending work by reading the list, and the appropriate approver is defined by who holds the resolve grant. Workbench adds an inbox and notifications on top; the control plane does not.
* **There is no approval deadline.** An agent-step suspend parks with no timeout and holds indefinitely. The reactor's own in-process gate has a one-hour default, but that governs an in-process wait, not the durable park, and nothing populates a per-approval deadline on this path.
* **The UI cannot resolve.** The detail page is read-only; resolution is API-only.
* **It carries approvals only.** The correlation machinery currently supports a single kind of signal, the approval. It is a durable approval substrate, not yet a general signal system.
* **A failed decision delivery is not retried.** The decision is persisted, but redelivering it to a parked run after a failed push is deferred.

<Note>
  **"What if nobody approves?"** The call holds. An agent-step approval parks indefinitely and waits: it is never silently allowed, and it is not auto-expired on this path. It stays pending until a human approves or rejects it.
</Note>
