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

# Bot permissions and budget

> What a bot may do and spend. Permission classes and presets, hand-off, admin locks, approvals and their scope, and the per-run and monthly spend caps, applied the same on every door.


A bot's rules live in two places that are merged for every run: the `permissions` map in its YAML, and a policy stored on the bot's row. The engine loads the stored policy once per run, so a run by id, a schedule, a trigger, a chat message, MCP, the CLI and Studio all stop at the same holds and caps. A shared or templated bot cannot unlock itself, because locks come from the row, never from the YAML.

## Classes and presets

Every tool call has a class: `send`, `write`, `pay`, `delete` or `read`. A preset says what happens to a class (or to one tool name):

| Preset | Effect |
|---|---|
| `allow` | The call runs. |
| `ask` | The run pauses and waits for the owner. |
| `deny` | The call does not run. |
| `hand_off` | A deny that does not retry. The call never runs, and the result tells the owner it is theirs to do. Only One Agent understands it; any other runtime gets `deny`. |

The stored policy covers `send`, `write`, `pay` and `delete`. In YAML, `permissions` can also name `read` and individual tools:

```yaml
- type: harness
  id: bot
  agent: one-agent
  permissions:
    pay: hand_off
    send: ask
  budget_usd_run: 0.5
```

How the order of checks works is on [Policy and review](/one-agent/policy-and-review). Two rules to remember: an operation named `buy`, `purchase`, `fetch_paid`, `send`, `transfer`, `pay`, `send_payment`, `withdraw` or `shop` is on the money floor and always `ask`, even with `send: allow`, and `posture: read_only` denies every mutating class whatever the presets say.

## Set the stored policy

`PUT /v1/workflows/{id}/permissions` replaces the fields you send and leaves the others. It is owner-only.

```bash
curl -s -X PUT https://api.plungeai.com/v1/workflows/<bot id>/permissions \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
  -d '{"classes":{"send":"ask","pay":"deny"},"budget_usd_run":0.25,"budget_usd_month":10}'
```

| Field | Meaning |
|---|---|
| `classes` | A preset per class: `send`, `write`, `pay`, `delete`. A class you leave out falls back to the bot's YAML, then to the default. |
| `locks` | Classes an admin locked. A locked class lifts the bot's own `allow` to `ask`; it never lifts a `deny`. |
| `budget_usd_run` | Cap on one run, in USD. A positive number sets it and `null` removes it; `0` is stored but means no cap. |
| `budget_usd_month` | Cap on a month, in USD. A positive number sets it and `null` removes it; `0` is stored but means no cap. |

`GET /v1/workflows/{id}/permissions` returns the stored policy. The same settings are in Studio under the bot's Permissions and Budget fields, on MCP, and on the CLI:

```bash
ocean-cli workflows permissions set <bot id> --class pay=ask --lock pay --budget-run 0.5 --budget-month 20
```

- **Locks need a team admin** over the workflow: the project owner or an admin collaborator, or a company owner or admin. Anyone else gets `403 lock_admin_required`, and nothing in that body is written.
- An invalid field is `422 invalid_params` and nothing is written.

## How the YAML and the row combine

| Part | Rule |
|---|---|
| Class presets | The two are merged; the YAML's own entry wins per class. |
| Locks | The row's locks replace whatever the YAML carried. |
| `budget_usd_run` | The lower of the YAML's value and the row's wins. |

## Approvals

A run that hits an `ask` pauses with status `needs_approval` and a `continuation` that carries the pending action, its price and its class. Answer it with `POST /v1/executions/{id}/continue`:

```bash
curl -s -X POST https://api.plungeai.com/v1/executions/<id>/continue \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
  -d '{"approve":true,"allow_scope":"run"}'
```

| `allow_scope` | The approval reaches |
|---|---|
| `once` (default) | This call only. |
| `run` | The same tool for the rest of the run. |
| `always` | Not written by this call: it behaves as `once` for the run, and the next gated call asks again. The rule is the class preset stored with `PUT .../permissions`. Studio's "Allow always for this bot" stores the preset first and then approves; over REST, do the `PUT` yourself. |

`{"answer": "..."}` replies to a question, or denies or redirects an approval. An unattended run that hits an `ask` waits for the owner, up to seven days.

## Spend caps

- **Per run.** When the running token cost passes `budget_usd_run`, the run stops at the next turn with `stop_reason: budget`, and the result says `[run stopped at $... per-run budget cap after N turns ...]`. The engine applies it on every door.
- **Per month.** `budget_usd_month` is enforced by the scheduler and gates scheduled runs only. A chat message or a web widget visitor is bounded by the per-run cap, which is why a bot behind a public surface needs one.
