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

# Approvals and budgets

> How a person approves a single tool call, and every cap that limits what a session can spend.

[Tool permissions](/security/tool-permissions) says which tools a role holds.
This page covers the two controls that act on each call of a tool already
held: asking a person, and refusing past a limit.

## How an approval works

A gated tool call parks and shows the person a card with three things: the
tool's name, one plain sentence saying what will change (`intent`), and the
literal operation (`detail`) so the sentence can be checked against what will
run. For a spend, the engine writes the sentence from its own cost estimate;
the model does not get to describe the price.

| Outcome | Result |
| - | - |
| A person approves | The call runs |
| A person denies | The call is refused; nothing changed |
| Nobody answers before the timeout | Denied |
| The session ends | Denied |
| 20 requests are already waiting | Denied at once |

Only a person can approve. Every other way a request settles is a denial, so
"nobody answered" is safe by construction. The timeout is five minutes by
default (`APPROVAL_TIMEOUT_MS`; `0` denies at once). A pending request is
written to the transcript, so a dropped connection loses the view, not the
decision.

**Turning approvals off.** `REQUIRE_WRITE_APPROVAL` set to `false`, `0` or `no`
removes the approval channel from person chat. The effect differs by tool and
is listed below; most gated writes then refuse rather than run.

**Unattended runs** (coworkers, the goal agent, the reviewer, and since
2026-10-11 an inbound email turn) have no approval channel at all, whatever the
setting.

Source: `src/engine/approval.ts` (`ApprovalBroker`, `approvalRequired`, `approvalTimeoutFromEnv`)

## Which calls ask a person

| Tool | Asks when | With approvals off | Unattended run |
| - | - | - | - |
| `mcp__memory__soul_update`, `mcp__memory__goals_update` | Every call | Runs without asking | Not registered |
| `mcp__shopify__shopify_admin_graphql` mutation | Every mutation, with `SHOPIFY_ALLOW_WRITES` on | Refused (reads still run) | Not grantable |
| DataForSEO tools | Estimate at or above \$0.25 | Runs if under the per-call and monthly limits | Runs if under the limits and the run's allowance |
| Firecrawl tools | Estimate at or above 25 credits | Runs if under the per-call and monthly limits | Runs if under the limits and the run's allowance |
| `mcp__sheets__sheets_export`, `mcp__sheets__sheets_append` | Every call | Refused | Not grantable |
| GitHub write tools | Every write, with `GITHUB_ALLOW_WRITES` on | Refused | Not grantable |
| AgentMail send tools | Every send, with `AGENTMAIL_ALLOW_SENDS` on | Refused | Not grantable |
| `mcp__cloudflare__execute` | Every call | Not registered | Not grantable |
| `mcp__railway__railway_reveal_variables`, `mcp__railway__railway_redeploy` | Every call, with the matching switch on | Not registered | Not registered |
| `mcp__sandbox_egress__sandbox_allow_host` | Every call: the card offers **Allow once**, **Allow for this store** and **Don't allow** | Not registered; an unlisted site stays blocked | Not registered |

Not gated by an approval: memory notes, project files, AgentMail drafts,
content writer drafts, browser logins, and Contentsquare tools.

A card with more than one yes puts the narrowest one where Approve usually
is, so a quick approval grants the least. "Allow once" lasts until the
conversation's sandbox ends; "Allow for this store" is saved for the brand
and can be removed under Settings → Account and security.

Source: `src/engine/session.ts` (where each server is given or denied the approval channel), `src/engine/sandbox-egress-tools.ts`, `src/engine/memory.ts`, `src/engine/shopify.ts`, `src/engine/dataforseo.ts`, `src/engine/firecrawl.ts`, `src/engine/sheets.ts`, `src/engine/github.ts`, `src/engine/agentmail.ts`, `src/engine/cloudflare.ts`, `src/engine/railway.ts`

## Model spend

| Cap | Default | Where set | Applies to |
| - | - | - | - |
| Per session, in dollars | \$10 | `sessionBudgetUsd` in `agent.config.json` | Each person chat |
| Turns per session | 50 | `maxTurns` in `agent.config.json` | Each person chat |
| Per month, in dollars | \$100 | `monthlyBudgetUsd` in `agent.config.json` | Each brand, across its chats and its coworkers |
| Per coworker run | Profile's `runBudgetUsd` and `maxTurns` | The coworker's settings | Each run |
| Loop and Hermes harnesses | \$0.25 and 8 | Fixed in code | Each session, whichever is smaller of this and the config |

A limit in `agent.config.json` that is zero, negative or not finite stops the
server from loading the config. A monthly ledger that cannot be read stops the
session before it starts, rather than reading as nothing spent. The session
checks the monthly figure before starting and after every turn.

On a gateway model the per-turn cost is the CLI's estimate, not the provider's
bill. The cap still fires on the estimate.

Source: `src/engine/config.ts` (`loadConfig`), `src/engine/session.ts` (monthly cap), `src/engine/harness/registry.ts` (`harnessSessionLimits`), `src/server/registry.ts` (worker overrides)

## Goals

| Cap | Default | Variable or setting |
| - | - | - |
| Per review | \$0.10 | Fixed |
| Reviews per day | The smaller of the deployment's and the brand's figure | `GOALS_REVIEW_DAILY_USD` (deployment), Goals settings (brand); default \$0.25 |
| Goal agent step | \$0.50, ten turns, two attempts | Fixed |

A daily figure of zero or below is refused. If today's review spend cannot be
read, no review runs.

Source: `src/engine/goal-reviewer.ts`, `src/engine/goal-store.ts`, `src/engine/goal-agent.ts`

## Connector spend

| Connector | Ask at | Refuse above, per call | Monthly | Unit |
| - | - | - | - | - |
| DataForSEO | \$0.25 (`DATAFORSEO_APPROVAL_THRESHOLD_USD`) | \$2 (`DATAFORSEO_MAX_CALL_USD`) | \$25 (`DATAFORSEO_MONTHLY_BUDGET_USD`) | Dollars |
| Firecrawl | 25 (`FIRECRAWL_APPROVAL_THRESHOLD_CREDITS`) | 100 (`FIRECRAWL_MAX_CALL_CREDITS`) | 2000 (`FIRECRAWL_MONTHLY_BUDGET_CREDITS`) | Credits |

The connector's own ledger is separate from the model ledger, so buying data
never consumes the budget that keeps the session alive. The counter follows
the credential: brands sharing one account's key share one monthly counter.

<Note>
  For both connectors, `0` is the strictest setting, never "no limit". A
  threshold of `0` asks on every call, a per-call ceiling of `0` refuses every
  billed call, and a monthly budget of `0` refuses every billed call for the
  month. Free calls, such as the account check, still run. Before PR #139
  a monthly budget of `0` switched the monthly check off; that was a bug and
  is fixed.
</Note>

### Coworker allowances

A specialist that holds DataForSEO or Firecrawl must carry an allowance:

| Connector | Per run | Per day |
| - | - | - |
| DataForSEO | `perRunUsd` | `perDayUsd` |
| Firecrawl | `perRunCredits` | `perDayCredits` |

The per-run figure is checked inside the connector's gate on every call,
counting what is spent plus what is held for calls still in flight. The
per-day figure is checked before a run starts. Both sit under the deployment's
per-call ceiling and monthly budget, never above them.

Measured once on 2026-09-23 for Firecrawl: a scrape was charged 1 credit, then
a crawl that would have taken the run past a 3-credit allowance was refused
with nothing spent (`ALLOWANCE_CAPTURED`). The DataForSEO allowance and the
per-day refusal have not been exercised (`ALLOWANCE_UNVERIFIED`).

Source: `src/engine/dataforseo.ts`, `src/engine/firecrawl.ts`, `src/engine/allowance.ts`, `src/engine/coworkers.ts` (`specialistProblem`)

## Other caps

| What | Cap | Variable |
| - | - | - |
| Google Ads API operations | 2500 per day for the deployment, 300 per brand | `GOOGLE_ADS_DAILY_OPS`, `GOOGLE_ADS_DAILY_OPS_PER_TENANT` |
| Microsoft Ads API calls | 2000 per day for the deployment, 200 per brand | `MICROSOFT_ADS_DAILY_CALLS`, `MICROSOFT_ADS_DAILY_CALLS_PER_TENANT` |
| AgentMail sends | 200 recipients per month | `AGENTMAIL_MONTHLY_SEND_LIMIT` |
| Inbound email turns | 10 per minute per brand | `AGENTMAIL_INBOUND_RATE_PER_MIN` |
| Browserbase minutes | Required; no default | `BROWSERBASE_MONTHLY_MINUTES_LIMIT`, optional per-brand `BROWSERBASE_TENANT_MONTHLY_MINUTES_LIMIT` |
| Coworker concurrency | Profile's `maxConcurrentRuns` (at least 1) | The coworker's settings |
| Delegation depth | Profile's `maxDelegationDepth` | The coworker's settings |

Source: `src/engine/googleads.ts`, `src/engine/microsoftads.ts`, `src/engine/agentmail.ts`, `src/engine/agentmail-inbound.ts`, `src/engine/browser-config.ts`, `src/engine/coworkers.ts`


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.