# Scope recipes for agent jobs

> The minimum scope set for each common agent job, what each scope buys, and what to add when a job needs more.

Canonical page: https://anectico.com/docs/agents/scope-recipes/


Every MCP tool is gated **twice**, and both gates must pass:

1. the **umbrella** for that side of the endpoint — `mcp:read` for every read, `mcp:write` for every
   mutation;
2. the **domain scope** in that tool's own row of the
   [MCP tool reference](/docs/reference/mcp-tools).

A key holding an umbrella and nothing else authenticates and is served an empty tool list. That is
the single most common "the server does not support this" report, and it is a scope problem.

## Start from a bundle

`anectico apikey create --scope-profile <name>` and `anectico agent bootstrap --scope-profile <name>`
materialize a bundle into **explicit scopes**, stored on the key. The key records the scopes, never
the bundle name, so its authority is fixed at mint time and is auditable by reading it back with
`anectico apikey list -o json`.

| Bundle | Scopes | Tools the key is served | Mintable by |
| --- | ---: | ---: | --- |
| `investigate` | 20 | 36 — read-only, no mutation at all | any member |
| `incident` | 38 | 49 | owner or admin |
| `agent-observability` | 10 | 14 | owner or admin |
| `admin` | 2 (the two umbrellas) | 0 | owner or admin |

Three properties of that table are decisions rather than accidents:

- **`incident` contains `investigate` in full.** Incident response is investigation plus a lifecycle
  surface, not a different way of looking at the same data.
- **`agent-observability` does not.** It is deliberately narrow — agent runs, their transcripts,
  what they cost, and the person behind them. Add `--scope traces:read` when the raw spans behind a
  run are needed.
- **`admin` refuses to stand alone.** It buys the two umbrellas and a statement of intent; the
  authority is typed out one `--scope` at a time. `anectico apikey create --scope-profile admin` with
  no `--scope` is a usage error, and a key holding only the umbrellas is served nothing.

**No bundle ever materializes a `*:delete` scope.** Deletion is the one class of write that cannot be
undone, and the preview-then-confirm gate that makes it survivable lives in the MCP write framework —
a key used against the REST API gets no such gate. Every delete scope must be named explicitly.

Two things about who can mint. **A key can never carry a scope its creator lacks** — the mint is
refused by name, so `incident`, `agent-observability` and `admin` are owner-or-admin work because all
three carry `mcp:write`. And **a viewer cannot mint a key at all**, of any shape: key creation is
itself a privileged act, and the refusal comes before the scope list is read, so hunting for the
missing scope is a dead end.

## Recipes

Each row is the **minimum** that makes the job work end to end. Every one also needs the umbrella
already shown.

**These recipes assume a project-bound key** — what `anectico agent bootstrap` mints unless you pass
`--org-wide`. A project-bound key carries its project signed into the credential; it supplies that
project on every call, and an explicit different one is denied. It therefore needs **no**
`projects:read`: there is no project for it to discover.

**An organization-wide key adds exactly one scope: `projects:read`.** Without a binding, it must pass
a project id to every project-addressed read and write, and `list_projects` is the only way an agent
finds one. That is the whole difference — everything else in each row is the same. Counts below are
for the project-bound form, derived from the current capability catalog and registration rules on
2026-09-04. Server tests pin the published bundle and served-tool counts, but catalog or profile
changes can alter them; verify the surface served to your own key before automating against a count.

### Read-only jobs (`mcp:read`)

| Job | Add these scopes | Tools |
| --- | --- | ---: |
| Triage an error issue | `errors:read` `logs:read` `traces:read` `persons:read` `services:read` | 14 |
| Answer "what happened to this customer" | `persons:read` `persons:profile:read` `errors:read` `logs:read` `traces:read` `analytics:read` `replay:read` `replay:content:read` `groups:read` `agents:read` | 28 |
| Watch AI agents and their spend | `agents:read` `llm:read` | 4 |
| Everything an investigation needs | the `investigate` bundle | 36 |

The triage row is the smallest genuinely useful read key: it gets `search`, `fetch`,
`list_issues`, `get_issue`, `get_affected_users`, `search_logs`, `get_log_patterns`,
`search_traces`, `get_trace`, `get_person_timeline`, `resolve_person`, `search_persons`,
`search_issues` and `get_service_health`. Drop `persons:read` and it falls to 11: no person
timeline, no identity resolution, and `search` stops having a people source to search.

**The agent-watch row looks too small, and is not.** Four tools is right: run metadata is
`list_agent_runs`, fleet posture is `list_agent_fleet`, and everything else this key can do is a read
*action* reached through `list_read_actions` and `execute_read_action`, which is why none of it adds
an entry to the tool list. Behind those two doors the same key gets all three LLM-cost capabilities
— the summary, the series and the top spenders — plus `list_agent_sessions` (which conversations
happened), `list_agent_turns` (the exchanges within one of them), `get_agent_run_graph` (one run's
step structure), `list_context_events` (the context assembly evidence),
`get_telemetry_completeness` (how current the agent telemetry itself is), and
`verify_receipt_chain` (whether the agent's action receipts still verify). Two scopes are commonly
added to the row, and each should be a decision rather than a reflex:

| Add | What it buys | What it also exposes |
| --- | --- | --- |
| `agents:content:read` | `get_agent_run` — one run's detail | **The verbatim transcript**: the prompts, completions and tool messages the model exchanged. If the agent under observation handled customer data, this is that data |
| `persons:read` | `get_person_timeline`, `resolve_person`, `search_persons` — tying a run or a bill to the person behind it | The people surface generally, and a `search` that now has a person source |

Neither is required to watch runs or to read spend. Add them when correlation to a person, or
reading what a run actually said, is the job.

### Jobs that change something (`mcp:read` + `mcp:write`)

| Job | Add these scopes | What it unlocks |
| --- | --- | --- |
| Triage **and** resolve issues | `errors:read` `errors:write` `logs:read` `traces:read` `persons:read` | `set_issue_status`, `snooze_issue`, `merge_issues`, `unmerge_issue` |
| Take a page and run the response | `alerts:read` `alerts:acknowledge` `alerts:resolve` `incidents:read` `incidents:respond` `channels:read` `oncall:read` `members:read` | `acknowledge_alert`, `resolve_alert`, `comment_incident`, `set_incident_status`, plus delivery receipts and who is on call |
| Silence a storm | `silences:read` `silences:write` `silences:delete` | `create_silence`, `delete_silence` |
| File a ticket from an issue | `connections:read` `errors:read` `tickets:read` `tickets:write` | `create_ticket`, `list_tickets_for_issue`, `get_ticket`. Add `tickets:delete` for `unlink_ticket` |
| Build and maintain dashboards | `dashboard:read` `dashboard:write` `dashboard:delete` | the seven dashboard and widget actions. `dashboard:delete` covers `delete_dashboard` **and** `delete_widget` |
| Export data for offline analysis | `export:read` `export:write` `export:download` **plus the read scope of every signal exported** (`logs:read`, `traces:read`, …) | `create_export_job`, `export_logs`, `cancel_export_job`, `retry_export_job`, and the three export reads |
| Delete exact telemetry on request | `logs:delete` and/or `traces:delete` — project-bound only | `delete_log`, `delete_trace`. An org-wide key is not served these at all |

**Export is the row where a key most often "sees a tool it cannot call", and it is deliberate.**
`export:write` alone commissions nothing: creating a job also requires the exported signal's own read
scope, so a key cannot export data it may not read. `retry_export_job` re-checks that against the
stored job, so a key that has since lost the read scope cannot regenerate the file. And
`get_export_download` re-checks it **and** requires being the job's creator or an organization owner
or admin — so a key with only `export:write` can create a job and can neither poll it nor download
it, and a key that did not create a job is answered as though the job does not exist.

## What to add, and why

**`projects:read`, only for an organization-wide key.** It buys exactly one capability,
`list_projects`, which is how an unbound agent discovers the project ids the rest of the surface
takes as an argument — without it such a key fails on the first call an agent makes. A project-bound
key gains nothing from it: adding `projects:read` to the customer recipe above changes the tools
served from 28 to 28. It is in the `investigate` bundle because that bundle has to work either way.

**The read that matches the write.** A write scope without its matching read scope is what turns an
ordinary edit into an unrecoverable one: the credential cannot capture what it is about to
overwrite, so the preview can only tell you it is unable to show you — and the action is graded
destructive for precisely that reason. `escalation:write` without `escalation:read` cannot list the
policy it is about to replace. `evals:write` without `evals:read` can replace a sampling rule
or a budget it never saw, and setting a rate to zero turns automatic evaluation off. If an agent should be able to
undo its own edits, give it the read.

**A content scope, only when verbatim customer content is genuinely needed.** `persons:profile:read`
(profile properties), `replay:content:read` (session playback and its console and network payloads)
and `agents:content:read` (prompts, completions, tool arguments, reasoning transcripts) each name
recorded content rather than a summary of it. The `investigate` bundle grants all three, because
that detail is what makes an investigation possible — if an agent should not see it, name scopes
individually instead of taking the bundle. `traces:read`, `logs:read` and `metrics:read` used to be
second routes to model prompts and completions recorded as *attributes* by third-party
instrumentation, and are not any more: those attributes are withheld from a credential without
`agents:content:read` — on spans, on log records (including the live tail) and on metric exemplars —
and the response comes back carrying an `anectico.content.withheld` attribute naming what was
removed. An agent with the signal read scopes and no content scope reads latency, status, token
counts and log messages, never model bodies.

Your project's **content policy** can withhold the same bodies from an agent that *does* hold
`agents:content:read`. Those responses carry `anectico.content.policy_withheld` instead, and the
difference matters to whoever is debugging: the first is fixed by minting a key with the content
scope, the second only by changing the project's policy. One more thing to know before you blame a
key: a content policy belongs to a project, so a read that does not name one — the MCP trace and log
searches on an organization-scoped key, or an API call without `project_id` — comes back without
content for that reason alone. Name the project and the bodies return.

**A delete scope, one at a time and never by reflex.** `--scope logs:delete`, `--scope
traces:delete`, `--scope dashboard:delete`, and so on. Typing it out is the point.

**Nothing for the organization itself unless the job is administration.** `members:write`,
`projects:write`, `settings:write`, `connections:write` and `api_key:write` all belong to the
`admin` privilege class. A key that investigates does not need them, and the wildcard platform-admin
permission is not grantable to any key.

## Narrow a session without minting a key

Adding `?profile=<name>` to the endpoint URL narrows one connection. It takes tools away for that
whole connection — an excluded tool is not listed **and** cannot be called — but it can only ever
subtract from what the key's scopes already permit, and the key's authority is unchanged.

The useful case is turning a write-capable key into a read-only session:

```text
https://app.anectico.com/mcp?profile=investigate
```

`investigate` declares no mutation at all, so a write-capable key connecting that way is served no
write gateway whatsoever — an `incident` key drops from 49 tools to 35, and cannot invoke a mutation
for the life of that connection. Reconnecting without the parameter restores everything, which is
why this is a focus control and not an authorization boundary: the caller chooses it.

Watch for the side effect. `investigate` also excludes reads that are native to other bundles, so an
`incident` key connected this way loses `get_incident`, `list_incidents` and the on-call and
delivery reads too, despite holding their scopes. For a genuinely narrower **authority**, mint a
narrower key; see [How agent access is enforced](/docs/agents/security-model) for exactly where that
line falls.

- [Permission scope reference](/docs/reference/permissions) — the complete scope catalog.
- [MCP tool reference](/docs/reference/mcp-tools) — every tool with the scopes it requires.
- [How agent access is enforced](/docs/agents/security-model)
