# Approval

> @faustine-ai/browser-sandbox documentation · https://browser-sandbox.faustine.ai/docs/approval/ · source: README.md on GitHub (https://github.com/faustine-ai/browser-sandbox)

A function with `approval: true` never runs by itself. What happens when
the code calls one depends on the run's `approval` option.

## `deny` (default)

The call throws `ApprovalRejected` into the code without running. Nothing
marked ever runs by omission.

## `pause`: one decision per call, the run waits

```ts
await sandbox.run({
  code, functions,
  approval: {
    mode: 'pause',
    onApproval: async ({ fn, input, seq }) => (await confirmDialog(`${fn}`, input)) ? 'approve' : 'reject',
  },
});
```

The worker waits on the call; the run clock pauses while the human decides;
`approve` runs it and returns the value, `reject` throws `ApprovalRejected`
at the call site. Uncaught, the run ends with that error.

## `collect`: one decision per batch, after the run was cut

```ts
let result = await sandbox.run({ code, functions, approval: { mode: 'collect' } });

while (result.status === 'pending') {
  // one card, N rows: result.pending = [{ callId, seq, fn, input }, …]
  const decisions = await showBatchCard(result.pending);   // { all: 'approve' } | { approve: [ids], reject: [ids] }
  result = await result.resume(decisions);
}
```

A marked call does not run; the code receives a **pending value** instead
and goes on. Reads keep executing, writes keep accumulating. The run is cut
at the first **observation** of a pending value (a property read, a
coercion, a spread, `JSON.stringify`, a call) or at the **end of the pass**
with calls still pending. `resume` re-runs the same code with a replay:
calls already made return their recorded result without executing again,
approved ones execute, rejected ones throw `ApprovalRejected`. A call that
differs from what was shown ends the run with `ReplayMismatch`, and nothing
runs. In `collect` mode calls are serialised in creation order so the
replay is deterministic (a `Promise.all` still works, queued).

A loop of sixteen deletes is therefore one card with sixteen rows. A second
card happens only when the code needed the result of a marked call to
continue and then reached another marked call.

With a batch hook the library owns the loop and `run` never resolves
`pending`:

```ts
await sandbox.run({
  code, functions,
  approval: { mode: 'collect', onApproval: (pending) => showBatchCard(pending) },
});
```

The two hooks differ on purpose: `pause` decides one call with a string
while the worker waits; `collect` decides a batch with `Decisions` after the
run was cut, and you can approve eight of sixteen.
