> ## 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.

# Tool permissions

> Which tools each role in Scout is given, which it is not, and why. Read from the code, section by section.

This page lists, per role, the tools a session is **given**: registered with
the model and allowed by the session's ceiling. A tool that is not listed for a
role is not described to that model at all. Each section ends with the file and
symbol it was read from.

<Note>
  A test in the repository (`src/server/docs-site.test.ts`) imports the tool
  lists from the code and fails if any tool name is missing from this page.
  The prose around the names is checked by people, not by the test.
</Note>

## How a session's tools are decided

Three filters run in order, and a tool must survive all three.

1. **Registration.** A tool exists only if its server is built for the session.
   A connector's server is built only when that brand's credentials for it
   resolve. An operator switch that is off means the tool is never registered.
2. **The ceiling.** A coworker run carries a list of permitted tool names. Every
   server is intersected with it, and a server with nothing left is dropped.
   A call to a name outside the ceiling is denied and logged as a
   `permission_denied` event.
3. **The per-call gate.** Some registered tools ask a person before each call,
   or refuse above a spending limit. See
   [Approvals and budgets](/security/approvals-and-budgets).

On the default harness the session's tool list is passed to the Claude Agent
SDK as an exclusive allowlist (`tools`), with a permission callback that is
always present. The code deliberately does **not** pass `allowedTools`: an
entry there is approved automatically and would skip the callback.

Source: `src/engine/session.ts` (`createSession`), `src/engine/harness/claude-code.ts` (`claudeCodeOptions`), `src/engine/tool-surface.ts`

## Roles at a glance

| Role | Built-in tools | Platform tools | Connector tools | Asks a person? |
| - | - | - | - | - |
| Person chat | All configured | Memory, project, goals, transcripts, skills, team | Every connector the brand has connected | Yes, per gated call |
| Inbound email turn | None | None | AgentMail read and draft tools only | No one to ask |
| Coworker run | The profile's subset | Private memory notes, project folder, granted skills | None | No one to ask |
| Specialist coworker run | The profile's subset | As a coworker, plus skill tools | Read tools of granted connectors only | No one to ask |
| Goal agent step | Read-only built-ins | Project folder: list, read, write | Read tools of granted connectors only | No one to ask |
| Goal reviewer | None | None | None | Not applicable |
| Read-only route | `Read`, `Glob`, `Grep`, `WebSearch`, `WebFetch` | Transcripts, skills, and team on the default harness | None | Not applicable |

## Built-in Claude Code tools

The deployment's built-in list is `tools` in `agent.config.json`. As shipped it
is:

`Bash`, `Read`, `Write`, `Edit`, `Glob`, `Grep`, `WebSearch`, `WebFetch`

Because the list is an exclusive allowlist, every other built-in the CLI has
(for example `Agent`, `NotebookEdit`, `TodoWrite`) is not offered to any role.
Removing a name from `agent.config.json` removes it from every role.

| Built-in | Person chat (local) | Person chat (E2B sandbox) | Coworker run | Goal agent | Goal reviewer | Read-only route |
| - | - | - | - | - | - | - |
| `Bash` | Yes | No, replaced by the sandbox shell | If the profile asks | No | No | No |
| `Read` | Yes | No, replaced by sandbox file tools | If the profile asks | Yes | No | Yes |
| `Write` | Yes | No | If the profile asks | No | No | No |
| `Edit` | Yes | No | If the profile asks | No | No | No |
| `Glob` | Yes | No | If the profile asks | Yes | No | Yes |
| `Grep` | Yes | No | If the profile asks | Yes | No | Yes |
| `WebSearch` | Yes | Yes | If the profile asks | Yes | No | Yes |
| `WebFetch` | Yes | Yes | If the profile asks | Yes | No | Yes |

<Warning>
  In the local environment, `Bash` and the file tools run **on the server
  host**. They are confined to the session's workspace folder by a path check
  on the tool's arguments. The code calls this "policy, not a sandbox": it
  catches honest mistakes, not a determined adversary. Under E2B the host file
  tools are disallowed and the sandbox container is the boundary.
</Warning>

Under E2B (selected when the operator sets an E2B key), a coworker's ceiling
does not include the sandbox tools, so a coworker there keeps only `WebSearch`
and `WebFetch` of the built-ins. This is reasoned from the code, not observed on
a run.

Source: `agent.config.json`, `src/engine/config.ts` (`loadConfig`), `src/engine/environment.ts` (`HOST_FS_TOOLS`), `src/engine/guard.ts` (`evaluateAccess`), `src/server/registry.ts` (`coworkerCeiling`)

## Person chat

A person's own conversation in the browser. It gets the widest surface, and it
is the only role with someone present to approve gated calls.

### Platform tools

| Server | Tools | When present |
| - | - | - |
| Memory | `mcp__memory__memory_read`, `mcp__memory__memory_search`, `mcp__memory__memory_list`, `mcp__memory__memory_write`, `mcp__memory__memory_append`, `mcp__memory__memory_review`, `mcp__memory__memory_merge` | Always |
| Standing instructions | `mcp__memory__soul_update`, `mcp__memory__goals_update` | Always; each call asks a person when approvals are on |
| Project folder | `mcp__project__project_list`, `mcp__project__project_read`, `mcp__project__project_write`, `mcp__project__project_append`, `mcp__project__project_delete` | Always |
| Goals | `mcp__goals__goals_status`, `mcp__goals__goals_ask`, `mcp__goals__goals_propose`, `mcp__goals__goals_note` | When the goals engine is on |
| Transcripts | `mcp__transcripts__transcript_search`, `mcp__transcripts__transcript_read` | Always for a person; never for a coworker |
| Skills | `mcp__skills__load_skill`, `mcp__skills__read_skill_file` | When the brand has at least one skill |
| Team | `mcp__team__team_roster`, `mcp__team__team_assign`, `mcp__team__team_status`, `mcp__team__team_result`, `mcp__team__team_cancel` | When the brand has at least one coworker |

### Environment tools (E2B only)

| Server | Tools | When present |
| - | - | - |
| Sandbox | `mcp__sandbox__bash`, `mcp__sandbox__read_file`, `mcp__sandbox__write_file` | Every E2B session |
| Browser | `mcp__browser__browse_text`, `mcp__browser__browse_screenshot` | `SANDBOX_ALLOW_BROWSER` is exactly `true` and a browser provider is configured |
| Browser logins | `mcp__browser__list_logins`, `mcp__browser__start_login`, `mcp__browser__finish_login` | Browserbase is the provider and `BROWSERBASE_PERSISTENT_LOGINS` is exactly `true` |
| Sandbox network | `mcp__sandbox_egress__sandbox_allow_host` | Person chat with approvals on and the sandbox in allowlist mode. Never in a coworker run, the goal agent or an inbound email turn |

The browser reads pages only. Clicking, typing, filling forms, running
JavaScript, downloads, PDFs, cookies and long-lived sessions are not built,
because each turns a read into a write on someone else's site.

**What the sandbox can reach (since 2026-10-11).** The E2B sandbox, shell and
browser alike, connects only to an allowlist: a short default list of package
registries, code hosts, CDNs and reference sites, plus the sites a brand adds
under Settings → Account and security → "Sites the sandbox can reach". Private, link-local and
metadata addresses (`169.254.0.0/16` among them) can never be added to it.
That does not make them unreachable: E2B's filter was measured on 2026-10-11
not to block `169.254.169.254` under this policy either (it answered HTTP 401).
Since 2026-10-12 each sandbox gets a root firewall rule at start that refuses
`169.254.0.0/16`. A sandbox where the rule does not take is killed, not used.
The rule was measured to work. The first run also measured the agent's own
user removing it with passwordless `sudo`, and this page then said the rule
did not stop a command that tries to remove it. Since a second run that day,
the sandbox's user loses its sudo at start, before the agent gets the sandbox.
After that, `sudo`, `su` and E2B's own in-sandbox service all refused, and the
rule stayed. A sandbox where the removal fails is killed, not used. Three gaps
remain: the browser sandbox runs the agent as root, so there the rule is still
removable; the operator can turn the removal off (`E2B_SANDBOX_SUDO=keep`);
and the base sandbox left E2B's own root-run program writable by the agent.
Since later that day, the files root runs under `/etc` and `/usr`
(`/usr/local` excepted) are made read-only to the agent at start, and a
sandbox where that does not hold is killed, not used. On one test sandbox
that took the two writable files to none, and npm and pip installs still
worked. It does not cover `/usr/local`, `/opt`, setuid programs or the
browser sandbox.
When a
command fails against a site that is not listed, the agent can ask with
`mcp__sandbox_egress__sandbox_allow_host`, which parks a card offering
**Allow once** (this conversation's sandbox only), **Allow for this store**
(saved to the brand's list) and **Don't allow**. Either yes widens the running
sandbox in place. In a run with nobody to ask the tool is absent, and an
unlisted site stays blocked. `SANDBOX_EGRESS=open` restores the earlier
posture: the open internet minus the private ranges, with no card.

### Connector tools

A person's session gets every tool of every connector the brand has
connected, subject to the operator switches below. The full list, and what
each connector's writes require, is on [Connections](/connections). The names
in brief:

| Connector | Read tools | Write or spend tools |
| - | - | - |
| Shopify | `mcp__shopify__shopify_shop_info`, `mcp__shopify__shopify_search_products`, `mcp__shopify__shopify_recent_orders` | `mcp__shopify__shopify_admin_graphql` (reads freely; mutations need `SHOPIFY_ALLOW_WRITES` and an approval) |
| DataForSEO | `mcp__dataforseo__dataforseo_account`, `mcp__dataforseo__dataforseo_keyword_ideas`, `mcp__dataforseo__dataforseo_search_volume`, `mcp__dataforseo__dataforseo_keyword_overview`, `mcp__dataforseo__dataforseo_ranked_keywords`, `mcp__dataforseo__dataforseo_serp_organic`, `mcp__dataforseo__dataforseo_competitors`, `mcp__dataforseo__dataforseo_page_audit`, `mcp__dataforseo__dataforseo_keyword_difficulty`, `mcp__dataforseo__dataforseo_search_intent`, `mcp__dataforseo__dataforseo_rank_history`, `mcp__dataforseo__dataforseo_trends`, `mcp__dataforseo__seo_page_opportunities` | Every call bills; calls above a threshold ask a person |
| Firecrawl | `mcp__firecrawl__firecrawl_account`, `mcp__firecrawl__firecrawl_scrape`, `mcp__firecrawl__firecrawl_search`, `mcp__firecrawl__firecrawl_map`, `mcp__firecrawl__firecrawl_crawl` | Every call bills credits; calls above a threshold ask a person |
| Google Ads | `mcp__googleads__googleads_accounts`, `mcp__googleads__googleads_report`, `mcp__googleads__googleads_campaigns`, `mcp__googleads__googleads_search_terms`, `mcp__googleads__googleads_account_hierarchy`, `mcp__googleads__googleads_compare_periods`, `mcp__googleads__googleads_summarize`, `mcp__googleads__googleads_search_term_audit` | None |
| Search Console | `mcp__searchconsole__searchconsole_properties`, `mcp__searchconsole__searchconsole_totals`, `mcp__searchconsole__searchconsole_queries`, `mcp__searchconsole__searchconsole_pages`, `mcp__searchconsole__searchconsole_breakdown`, `mcp__searchconsole__searchconsole_inspect_url` | None |
| Google Analytics | `mcp__analytics__analytics_properties`, `mcp__analytics__analytics_use_property`, `mcp__analytics__analytics_overview`, `mcp__analytics__analytics_trend`, `mcp__analytics__analytics_traffic_sources`, `mcp__analytics__analytics_landing_pages`, `mcp__analytics__analytics_report` | None |
| Google Sheets | `mcp__sheets__sheets_list` | `mcp__sheets__sheets_export`, `mcp__sheets__sheets_append` (each asks a person) |
| Microsoft Ads | `mcp__microsoftads__microsoftads_accounts`, `mcp__microsoftads__microsoftads_campaigns`, `mcp__microsoftads__microsoftads_performance_report`, `mcp__microsoftads__microsoftads_report_fetch` | None |
| Figma | `mcp__figma__figma_whoami`, `mcp__figma__figma_file_outline`, `mcp__figma__figma_read_nodes`, `mcp__figma__figma_export_images`, `mcp__figma__figma_comments`, `mcp__figma__figma_components_styles`, `mcp__figma__figma_team_projects`, `mcp__figma__figma_project_files` | None; no write scope is requested |
| GitHub | `mcp__github__github_read_file`, `mcp__github__github_list_path`, `mcp__github__github_list_branches`, `mcp__github__github_list_issues`, `mcp__github__github_read_issue`, `mcp__github__github_list_pull_requests`, `mcp__github__github_read_pull_request` | `mcp__github__github_create_branch`, `mcp__github__github_commit_files`, `mcp__github__github_open_pull_request`, `mcp__github__github_comment` (need `GITHUB_ALLOW_WRITES` and an approval each) |
| AgentMail | `mcp__agentmail__agentmail_list_threads`, `mcp__agentmail__agentmail_search_threads`, `mcp__agentmail__agentmail_get_thread`, `mcp__agentmail__agentmail_list_messages`, `mcp__agentmail__agentmail_search_messages`, `mcp__agentmail__agentmail_get_message`, `mcp__agentmail__agentmail_get_attachment`, `mcp__agentmail__agentmail_whoami`, `mcp__agentmail__agentmail_list_drafts`, `mcp__agentmail__agentmail_get_draft` | `mcp__agentmail__agentmail_create_draft` (no approval; a draft is not sent). `mcp__agentmail__agentmail_send_message`, `mcp__agentmail__agentmail_reply_to_message`, `mcp__agentmail__agentmail_forward_message`, `mcp__agentmail__agentmail_send_draft` (need `AGENTMAIL_ALLOW_SENDS` and an approval each) |
| Cloudflare | `mcp__cloudflare__docs`, `mcp__cloudflare__search` | `mcp__cloudflare__execute` (asks a person on every call; absent when approvals are off) |
| Contentsquare | `mcp__contentsquare__*` (every tool the brand's Contentsquare server offers) | No gate in this code; see [Connections](/connections) |
| Railway | `mcp__railway__railway_workspaces`, `mcp__railway__railway_project`, `mcp__railway__railway_deployments`, `mcp__railway__railway_variable_names`, `mcp__railway__railway_logs` | `mcp__railway__railway_reveal_variables`, `mcp__railway__railway_redeploy` (each needs its own switch and an approval) |
| Tailscale | `mcp__tailscale__tailscale_devices`, `mcp__tailscale__tailscale_device`, `mcp__tailscale__tailscale_users`, `mcp__tailscale__tailscale_policy`, `mcp__tailscale__tailscale_dns` | None; every token is requested with read scopes only |

The Shopify-side content tools for the Content writer preset are listed under
[Specialist coworker run](#specialist-coworker-run): a person's session does not
register them.

Whose credentials a session uses: a brand's own stored connections first; then
a connection the account owner has shared with that brand; then, for the
deployment owner's own brand only, the server's host environment. No other
brand ever inherits the host environment.

Source: `src/engine/session.ts` (`createSession`), `src/engine/connector-tools.ts` (`TOOLS_BY_CONNECTOR`), `src/engine/credentials.ts` (`resolveConnectors`), `src/server/registry.ts` (`create`)

## Inbound email turn

When `AGENTMAIL_WEBHOOK_URL_SECRET` is set, an email that arrives in the
brand's AgentMail inbox starts one turn on the default model. A stranger chose
its input and nobody is watching it, so its session is built narrow:

**Tools:** `mcp__agentmail__agentmail_list_threads`,
`mcp__agentmail__agentmail_search_threads`, `mcp__agentmail__agentmail_get_thread`,
`mcp__agentmail__agentmail_list_messages`, `mcp__agentmail__agentmail_search_messages`,
`mcp__agentmail__agentmail_get_message`, `mcp__agentmail__agentmail_get_attachment`,
`mcp__agentmail__agentmail_whoami`, `mcp__agentmail__agentmail_create_draft`,
`mcp__agentmail__agentmail_list_drafts`, `mcp__agentmail__agentmail_get_draft`.

**Not given:** any built-in tool, the sandbox or browser, memory, the project
folder, transcripts, skills, goals, team, every other connector, and the four
AgentMail send tools whatever `AGENTMAIL_ALLOW_SENDS` says. The session is
handed AgentMail's credentials and no others, so no other connector's server is
even built.

**No one to ask.** Like a coworker run, an inbound turn has no approval channel,
so nothing it does can park an approval card.

**Limits:** \$0.50 and eight turns per message, or the deployment's own
per-session limits if they are lower.

<Note>
  Until 2026-10-11 an inbound turn had the same tool surface as person chat, on
  the deployment's per-session limits, and "read-only" was only an instruction
  in its prompt. That was the gap this section used to warn about. The new
  surface is proved against a stand-in for the CLI, not on a live delivery.
</Note>

Each brand's inbound turns are rate-limited, and they count against the live
session cap.

Source: `src/server/http.ts` (inbound webhook route), `src/server/registry.ts` (`createInbound`), `src/engine/agentmail-inbound.ts` (`inboundSessionShape`, `INBOUND_TOOLS`, `composeInboundTurn`)

## Coworker run

A coworker is a saved profile that runs a task unattended. Its permitted tools
are the intersection of what its profile asks for and the coworker ceiling.

**The coworker ceiling** on a standard route is the deployment's built-ins plus
memory and the project folder:

`Bash`, `Read`, `Write`, `Edit`, `Glob`, `Grep`, `WebSearch`, `WebFetch`,
the nine memory tools, and four of the five project tools. Since 2026-10-11
`mcp__project__project_delete` is not in it: a delete is recursive, unapproved
and cannot be undone, so only a person's own chat holds it. A saved profile
that lists it keeps the entry and loses the tool.

On a read-only route it is only the read-only built-ins: `Read`, `Glob`,
`Grep`, `WebSearch`, `WebFetch`.

What a coworker run actually holds, beyond the ceiling:

| Area | What the run gets |
| - | - |
| Memory | Its **own private** notebook, not the brand's. Only the seven note tools register; `mcp__memory__soul_update` and `mcp__memory__goals_update` are not registered for an unattended run, so a coworker cannot rewrite the standing instructions. |
| Project folder | The brand's shared project folder, if the profile asks for project tools. Never `mcp__project__project_delete`. |
| Skills | Only the bundled skills its grant names, and only for a specialist. |
| Transcripts | Never. A coworker cannot read the brand's chat history. |
| Goals | Never. |
| Team | Never today. The ceiling holds no team tool, so the team server is not registered even for a profile allowed to delegate. |
| Connectors | **None**, unless the coworker is a specialist (next section). A plain coworker's session is handed an empty credential set. |
| Approvals | None. A run has no one to ask, so it has no approval channel at all. |

**Limits.** Each profile carries its own `runBudgetUsd` (must be above zero),
`maxTurns` (at least 1), `maxConcurrentRuns` (at least 1),
`maxDelegationDepth` (0 or more), and optionally `maxAttempts` (default 1). The
run's dollar and turn caps are the profile's, not the deployment's. The
monthly model cap is the brand's and is shared with person chat.

A retry is narrowed to the tools the previous attempt actually ran with. A
profile whose requested tools all fall outside the ceiling is refused rather
than started with none.

Source: `src/server/registry.ts` (`coworkerCeiling`, `create`), `src/engine/coworkers.ts` (`CoworkerLimits`, `permittedTools`), `src/engine/tasks.ts` (`ceilingFor`), `src/engine/memory.ts` (`memoryToolNames`)

## Specialist coworker run

A specialist is a coworker with a **grant**: a list of connectors, a spending
allowance for the connectors that bill, and optionally bundled skills. Only a
person can write a grant, through the coworker settings
(`PUT /coworkers/:id`). An agent cannot widen its own or another coworker's
grant.

The grant adds to the coworker ceiling, in this order:

1. The tools of each granted connector that are in the **read table**
   (`CONNECTOR_READS`) and whose server will actually register with the
   credentials the run is handed.
2. A joint tool, such as `mcp__dataforseo__seo_page_opportunities`, only when
   every connector it reads (DataForSEO, Search Console and Shopify) is granted
   and live.
3. Opt-in tools of a granted connector: today the content writer's tools beside
   Shopify, given only to a profile that asks for them by name.
4. The skill tools, when the grant names at least one bundled skill that
   resolves.
5. Tools that ride on one skill: today the SEO brief tools on `seo-method`.

### The read table

These are the only connector tools a specialist or the goal agent can be
granted.

| Connector | Grantable | Never grantable |
| - | - | - |
| Shopify | `mcp__shopify__shopify_shop_info`, `mcp__shopify__shopify_search_products`, `mcp__shopify__shopify_recent_orders` | `mcp__shopify__shopify_admin_graphql` |
| DataForSEO | All twelve tools and `mcp__dataforseo__seo_page_opportunities` | None |
| Firecrawl | All five tools | None |
| Google Ads | All eight tools | None |
| Search Console | All six tools | None |
| Google Analytics | Six tools | `mcp__analytics__analytics_use_property` |
| Google Sheets | `mcp__sheets__sheets_list` | `mcp__sheets__sheets_export`, `mcp__sheets__sheets_append` |
| Microsoft Ads | All four tools, when the deployment offers Microsoft Ads | None |
| Figma | All eight tools, when the deployment offers Figma (Nango configured) | None |
| GitHub | The seven read tools | The four write tools |
| AgentMail | The ten read tools | `mcp__agentmail__agentmail_create_draft` and the four send tools |
| Railway | The five read tools | `mcp__railway__railway_reveal_variables`, `mcp__railway__railway_redeploy` |
| Tailscale | All five tools | None |
| Content writer (with Shopify) | `mcp__writer__writer_read_voice_guide`, `mcp__writer__writer_save_voice_guide`, `mcp__writer__writer_page_content`, `mcp__writer__writer_read_brief`, `mcp__writer__writer_save_draft` | None |
| Cloudflare, Contentsquare | Not grantable | Everything |

The content writer tools write only into the brand's project folder: a voice
guide saved as a draft, and page copy saved as a draft. Nothing is written to
the store. A voice guide is used only once its status line says `APPROVED`,
which the tool itself can never set.

### SEO brief tools

| Tool | Given when |
| - | - |
| `mcp__seo__seo_save_brief` | The grant names the `seo-method` skill |
| `mcp__seo__seo_hand_off_brief` | As above, and the profile's `maxDelegationDepth` is above 0 |

### Rules a grant must pass

A profile is refused when it is saved, not at its first call, if:

* it asks for a connector tool its grant does not cover;
* it asks for any tool outside the read table ("a specialist reads and proposes; it never commits");
* it grants a connector that bills (DataForSEO or Firecrawl) without an allowance, or with a per-run figure above the per-day figure;
* it asks for a skill tool without granting a skill, or names a skill that is not bundled;
* it asks for `mcp__seo__seo_hand_off_brief` at delegation depth 0.

### Allowance

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

The per-run figure is enforced inside the connector's spending gate on every
call. The per-day figure is checked before a run starts: runs already ended
today count at what they recorded (or their whole per-run figure if they
recorded nothing), and live runs count at their whole per-run figure.

Google products in a grant also narrow the Google credential the run is handed
to the scopes of the granted products.

### Presets

The coworker setup screen offers these starting points. Each is a profile a
person can still edit before saving.

| Preset | Built-ins and project tools | Connectors granted | Skills |
| - | - | - | - |
| Researcher | Read-only built-ins; project list, read, write | Firecrawl (25 credits per run, 100 per day) | None |
| Data analyst | Read-only built-ins; project list, read, write | None | None |
| Writer | Read-only built-ins; project list, read, write | None | None |
| Store assistant | Read-only built-ins; project list, read, write | Shopify | None |
| Marketing assistant | Read-only built-ins; project list, read, write | DataForSEO, Google Ads, Analytics, Search Console | None |
| Paid traffic manager | Read-only built-ins; project list, read, write | Google Ads, Analytics, Search Console, Shopify | `ppc-method`, `ppc-honesty` |
| SEO analyst | Read-only built-ins; project list, read | DataForSEO ($1 per run, $3 per day), Search Console, Analytics, Shopify | `seo-method` and its data skills; SEO brief tools |
| Content writer | Read-only built-ins; project list, read | Shopify, plus the content writer tools | `voice-guide`, `product-content`, `product-page-seo`, `humanizer` |

Presets default to \$0.50 per run, one run at a time, no delegation and ten
turns. The SEO analyst may delegate one level so it can hand a brief to the
Content writer. The Content writer is not given `mcp__project__project_write`,
so it cannot mark its own voice guide approved by editing the file directly.

Source: `src/server/specialist.ts` (`specialistCeilingFor`, `specialistToolsFor`, `workerConnectorEnv`), `src/engine/connector-tools.ts` (`CONNECTOR_READS`, `OPT_IN_TOOLS_BY_CONNECTOR`), `src/engine/coworkers.ts` (`specialistProblem`), `src/engine/allowance.ts` (`specialistDayProblem`), `src/engine/seo/handoff.ts` (`TOOLS_BY_SKILL`), `public/coworker-presets.js`

## Goal agent step

The built-in goal agent (`goal-agent`) runs one step of a goal plan when a
person starts it from the Goals page. It runs as an unattended coworker.

**Tools:** `Read`, `Glob`, `Grep`, `WebSearch`, `WebFetch`,
`mcp__project__project_list`, `mcp__project__project_read`,
`mcp__project__project_write`.

**Plus,** if a person has given it a connector grant under Goals, Advanced
(`PUT /goals/agent`), the granted connectors' tools from the read table, with
an allowance for any that bill.

**Not given:** `Bash`, `Write`, `Edit`, memory, transcripts, skills, team,
goals, and any connector tool outside the read table.

**Limits:** \$0.50 per step, ten turns, one at a time, no delegation, at most
two attempts. There is no separate daily cap for the goal agent; a granted
billing connector's per-day allowance applies.

<Note>
  An agent step has never been run against the real service, with or without a
  grant (`GOALS_UNVERIFIED.agent_step_run`, `GOALS_UNVERIFIED.agent_step_with_grant`).
</Note>

Source: `src/engine/goal-agent.ts` (`GOAL_AGENT_TOOLS`, `goalAgentToolsFor`, `goalAgentInvariantProblem`)

## Goal reviewer

The reviewer reads goal progress on each goals tick and writes a short review.

**Tools: none.** Its profile asks for an empty tool list, and the code refuses
to start a reviewer that has any tool, any delegation, or any grant.

**Limits:** $0.10 per review, two turns, one at a time, one attempt. A daily
cap applies: the smaller of the deployment's figure and the brand's setting
(default $0.25 per day, `GOALS_REVIEW_DAILY_USD`). If today's spend cannot be
read, no review runs.

**Model:** a reviewer created after 2026-09-23 defaults to
`google/gemini-3.8-flash` through OpenRouter.

What an empty tool list does on the wire was measured locally on 2026-09-05
against CLI 2.1.261: zero tool schemas sent, against 24 with the option
omitted (`GOALS_CAPTURED.empty_tools_on_the_wire`). A review on the deployment
has never been run (`GOALS_UNVERIFIED.zero_tool_worker`).

Source: `src/engine/goal-reviewer.ts` (`reviewerProfile`, `reviewerInvariantProblem`, `REVIEWER_MODEL`), `src/engine/goal-store.ts`

## Read-only route

A route is read-only when its model is not certified for the standard tool
set, or when it runs on an uncertified harness. The session is assembled
narrow, not fenced wide.

| Kept | Dropped |
| - | - |
| `Read`, `Glob`, `Grep`, `WebSearch`, `WebFetch` | `Bash`, `Write`, `Edit` |
| Transcripts and skills | Memory, project folder and goals |
| Team tools, on the default harness only | Sandbox and browser |
| | Every connector: the session is handed an empty credential set, so none registers |

Which routes are read-only today:

* **Models.** Every gateway model in the registry is certified. The one held
  back is `claude-haiku-5-5`, which is read-only for every brand except the
  owner's: the owner's own sessions run uncertified models on the standard set.
* **The loop harness.** On only when `HARNESS_LOOP_ENABLED` is `1` or `true`.
  Owner only, OpenRouter only, \$0.25 and eight model calls per session. It is
  given MCP tools only, never the CLI built-ins.
* **The Hermes harness.** On only when `HARNESS_HERMES_ENABLED` is `1` or
  `true` **and** the MCP bridge is on (`HARNESS_MCP_BRIDGE_ENABLED`). Owner
  only, OpenRouter only, \$0.25 and eight turns. Of Hermes's own tools, only
  `todo_list` (also called `todo`), `skills_list`, `skill_view`, `skill_manage`
  and `session_search` are enabled, all scoped to a throwaway folder deleted
  when the session ends. Every permission request Hermes raises is rejected.
  The bridge listens on loopback only and serves only the owner on a
  read-only route.

<Note>
  The loop harness has one observed owner turn (2026-10-10,
  `LOOP_CAPTURED`). Hermes has never run against this adapter:
  `HERMES_CAPTURED` and `BRIDGE_CAPTURED` are empty.
</Note>

Source: `src/engine/session.ts` (read-only ceiling), `src/engine/backends.ts` (`READ_ONLY_BUILTINS`, `CLAUDE_HELD_BACK`), `src/engine/harness/registry.ts` (`switchOn`, `harnessSessionLimits`), `src/engine/harness/hermes.ts` (`HERMES_OWN_TOOLS`), `src/engine/harness/mcp-bridge.ts`

## What is never given, to any role

Each connector keeps a written list of what it deliberately does not build,
and why. A summary:

| Connector | Not built |
| - | - |
| Shopify | Nothing beyond the single Admin GraphQL tool; mutations through it are gated |
| Google Ads | Any mutation: bids, budgets, campaign status, recommendations, assets, account and user changes, billing |
| Microsoft Ads | Budgets, bids, status changes, create and delete, bulk upload, extensions and assets, users and billing; the search query report |
| Figma | Posting or deleting comments and reactions; variables; dev resources and webhooks; library analytics |
| Search Console | Sitemap submit and delete; paging walks |
| Google Analytics | Admin writes; user-level exports; realtime, pivot and funnel reports |
| Google Sheets | Reading files this app did not create; delete; sharing; overwriting cells; formatting |
| GitHub | Merging; force-push and history rewrites; deletes; repository settings; collaborators; branch protection; secrets; triggering workflows; releases; search; GraphQL. Commits to the default branch and to `.github/workflows/` are refused in code |
| AgentMail | Deleting inboxes, threads, drafts or messages; creating or changing inboxes; switching organisation |
| Firecrawl | Browser interaction, monitors, agent jobs, parse and extract, research papers |
| Railway | Variable writes, service and project changes, domains, volumes, deploy controls other than redeploy, service settings, members and tokens, networking |
| Tailscale | Authorizing, tagging or removing devices; auth keys; policy, DNS and settings edits; a worker's sandbox joining the tailnet |
| Browser | Click, type, fill, evaluate, download, PDF, cookies, persistent sessions |

Source: `GOOGLEADS_WITHHELD`, `MICROSOFT_ADS_WITHHELD`, `FIGMA_WITHHELD`, `SEARCH_CONSOLE_WITHHELD`, `ANALYTICS_WITHHELD`, `SHEETS_WITHHELD`, `GITHUB_WITHHELD`, `RAILWAY_WITHHELD`, `TAILSCALE_WITHHELD`, `BROWSER_WITHHELD`, and `WITHHELD_TOOLS` in `src/engine/agentmail.ts` and `src/engine/firecrawl.ts`

## Where "absent beats refusing" has exceptions

Three groups of tools stay registered and refuse, on purpose:

* **AgentMail sends** register whenever `AGENTMAIL_ALLOW_SENDS` is `true`, and
  refuse when the session has no approval channel. The refusal is the control:
  a session that cannot ask anybody cannot send mail.
* **GitHub writes** (`mcp__github__github_create_branch`,
  `mcp__github__github_commit_files`, `mcp__github__github_open_pull_request`,
  `mcp__github__github_comment`) register with the connector and refuse,
  before any request to GitHub, when `GITHUB_ALLOW_WRITES` is off or there is
  no approval channel.
* **Google Sheets** `mcp__sheets__sheets_export` and `mcp__sheets__sheets_append`
  refuse without an approval channel.
* **Shopify mutations** through `mcp__shopify__shopify_admin_graphql` refuse,
  before any request to Shopify, when there is no approval channel. The tool
  stays registered because it is also the read path, and reads still run.

Coworkers hold none of these: they are outside the read table.

Until 2026-10-11 the Shopify gate loosened rather than tightened without an
approval channel: a mutation ran on `SHOPIFY_ALLOW_WRITES` alone when approvals
were switched off. That was a hole, and it is closed.

Source: `src/engine/session.ts` (comments at the AgentMail and Shopify servers), `src/engine/agentmail.ts`, `src/engine/github.ts` (`preflight`), `src/engine/sheets.ts`, `src/engine/shopify.ts`

## Operator switches

Set on the server's host environment. None can be changed from inside a
session, and none can be set per brand.

| Variable | Effect | Counts as on |
| - | - | - |
| `REQUIRE_WRITE_APPROVAL` | Per-call approvals for gated tools | On unless `false`, `0` or `no` |
| `APPROVAL_TIMEOUT_MS` | How long a pending approval waits before it denies | Default 300000 (five minutes); `0` denies at once |
| `SHOPIFY_ALLOW_WRITES` | Allows Shopify mutations | `true`, `1` or `yes` |
| `GITHUB_ALLOW_WRITES` | Lets the four GitHub write tools act (they refuse without it) | `true`, `1` or `yes` |
| `AGENTMAIL_ALLOW_SENDS` | Registers the four send tools | Exactly `true` |
| `RAILWAY_ALLOW_VARIABLE_VALUES` | Registers `mcp__railway__railway_reveal_variables` (still needs approval) | `true`, `1` or `yes` |
| `RAILWAY_ALLOW_REDEPLOY` | Registers `mcp__railway__railway_redeploy` (still needs approval) | `true`, `1` or `yes` |
| `SANDBOX_ALLOW_BROWSER` | Registers the browser tools under E2B | Exactly `true` |
| `BROWSERBASE_PERSISTENT_LOGINS` | Registers the browser login tools | Exactly `true` |
| `SANDBOX_EGRESS` | `open` lets the sandbox reach the whole internet except private ranges, and removes `mcp__sandbox_egress__sandbox_allow_host` | `open`, any case; anything else is the allowlist, and a typo is reported in the startup banner |
| `SANDBOX_EGRESS_ALLOWLIST` | Replaces the default sandbox site list (comma-separated hosts, `*.domain`, or public IPs and CIDRs narrower than /8) | Any non-empty value |
| `HARNESS_LOOP_ENABLED` | Makes the loop harness exist | `1` or `true` |
| `HARNESS_HERMES_ENABLED` | Makes the Hermes harness exist | `1` or `true` |
| `HARNESS_MCP_BRIDGE_ENABLED` | Opens the bridge Hermes needs | `1` or `true` |
| `AGENTMAIL_WEBHOOK_URL_SECRET` | Makes the inbound email route exist | Any value |

Spending limits for each connector are on
[Approvals and budgets](/security/approvals-and-budgets).

Source: `src/engine/approval.ts` (`approvalRequired`, `approvalTimeoutFromEnv`), `src/engine/shopify.ts`, `src/engine/github.ts`, `src/engine/agentmail.ts`, `src/engine/railway.ts`, `src/engine/browser.ts`, `src/engine/browser-config.ts`, `src/engine/sandbox-egress.ts`, `src/engine/harness/registry.ts`

## What has been measured and what is reasoned

The code keeps two kinds of record side by side: `*_CAPTURED` maps for
behaviour observed against the real service, with a date, and `*_UNVERIFIED`
maps for behaviour reasoned from documentation. Only the first kind is a
measurement. A passing test suite is evidence about the test fakes, not about
any vendor.

### Measured

| What | Record |
| - | - |
| An empty tool list sends zero tool schemas (local CLI 2.1.261, 2026-09-05) | `GOALS_CAPTURED.empty_tools_on_the_wire`, `CLAUDE_CODE_CAPTURED` |
| A specialist's Firecrawl allowance charged a scrape and refused an over-allowance crawl with nothing spent (2026-09-23) | `ALLOWANCE_CAPTURED`, `SPECIALIST_CAPTURED` |
| One owner turn on the loop harness, with four MCP tools on the wire (2026-10-10) | `LOOP_CAPTURED` |
| The inbound email instruction to draft, not send, was followed on two messages (2026-08-25) | `AGENTMAIL_CAPTURED` |
| The E2B sandbox runs as an ordinary user (2026-08-22), and browsing worked through the deployed app on the promoted browser template (2026-08-26) | `BROWSER_CAPTURED` |
| Cloudflare's OAuth discovery, registration and authorisation steps (2026-09-22) | `MCP_OAUTH_CAPTURED` |
| The sandbox allowlist: a listed host answered, unlisted hosts and a public IP were cut at TLS, `updateNetwork` widened a running sandbox in 55ms, and `169.254.169.254` still answered HTTP 401 (2026-10-11) | `SANDBOX_EGRESS_CAPTURED` |
| The root link-local rule at sandbox start refused `169.254.169.254` (`401` to `curl: (7)`) with the shell, files and `updateNetwork` still working, and the agent's user removed it with passwordless `sudo` (2026-10-12) | `SANDBOX_EGRESS_CAPTURED` |
| With the sandbox user's sudo taken away at start, `sudo -n`, `sudo -S`, `su` and tokenless calls to envd all failed and the link-local rule held, while `npm install`, `pip install --user` and `updateNetwork` still worked (2026-10-12, base template only) | `SANDBOX_EGRESS_CAPTURED.sudo_removed_live_2026_10_12` |
| With the start-step lock, the root-run files the sandbox user could write went from 2 (`/usr/bin/envd`, `/etc/inittab`, both mode 777) to 0, appends to both were refused, and `npm install -g` and `pip install --user` still worked (2026-10-12, base template only) | `SANDBOX_EGRESS_CAPTURED.root_run_lock_live_2026_10_12` |

### Reasoned, never exercised

| What | Record |
| - | - |
| A goal reviewer run on the deployment | `GOALS_UNVERIFIED.zero_tool_worker` |
| A goal agent step, with or without a grant | `GOALS_UNVERIFIED.agent_step_run`, `GOALS_UNVERIFIED.agent_step_with_grant` |
| A DataForSEO allowance, and the per-day cap refusing a start | `ALLOWANCE_UNVERIFIED.dataforseo_absent_cost`, `ALLOWANCE_UNVERIFIED.day_cap` |
| Google scope narrowing on a real grant | `SPECIALIST_UNVERIFIED.google_narrowing_live` |
| Whether a denied call is fully prevented on the loop harness | `LOOP_UNVERIFIED.denial_prevents_execution` |
| Anything on the Hermes harness or the MCP bridge | `HERMES_UNVERIFIED`, `BRIDGE_UNVERIFIED` |
| Railway calls with a real credential | `RAILWAY_UNVERIFIED.live_calls` |
| Tailscale calls with a real OAuth client | `TAILSCALE_UNVERIFIED.live_calls` |
| The narrowed inbound email session on a live delivery (built 2026-10-11) | `AGENTMAIL_UNVERIFIED.inbound_narrow_session` |
| What the CLI does with `settingSources: []` (no settings files, no CLAUDE.md) | `CLAUDE_CODE_UNVERIFIED.setting_sources_empty` |
| The link-local rule and the sudo removal on the browser template, where the agent is root | `SANDBOX_EGRESS_UNVERIFIED.link_local_block_browser_template` |
| What that start step leaves open: `/usr/local`, `/opt`, setuid programs, the browser template | `SANDBOX_EGRESS_UNVERIFIED.root_run_lock_gaps` |
| A Cloudflare MCP handshake | `CLOUDFLARE_UNVERIFIED.live_handshake` |
| The DNS channel, writes to listed hosts with a caller's own token, and SNI/Host mismatch on shared CDNs | `SANDBOX_EGRESS_UNVERIFIED` |
| A chat turn using the Allow once card end to end on the deployment | Not recorded; the card and route are tested against fakes only |
| A live run of the SEO analyst, paid traffic and Content writer methods | `SEO_UNVERIFIED.no_live_run`, `PPC_UNVERIFIED.no_live_run`; `WRITER_CAPTURED` is empty |

Source: the `*_CAPTURED` and `*_UNVERIFIED` exports across `src/engine/`


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