A tool is a named, permission-gated capability. All tools live in one registry read by two consumers — the copilot’s run loop and the MCP endpoint — so neither can grow a private set of rules. Each tool ships in the plugin that owns its data, as a thin wrapper over the same service the HTTP routes call.
The copilot’s catalogue
Twenty-six tools. A tool whose plugin is not registered is absent; a tool the caller lacks the permission for is not offered.
Content
| Tool | Does |
|---|---|
admin_content_types | The content types available, with their filterable fields |
admin_content_search | Search, filter, sort, page, locale, sparse fields, source |
admin_content_get | One entry |
admin_content_revisions | An entry’s version timeline |
admin_content_diff | The changed fields between two versions |
content_propose_create | Draft a new entry |
content_propose_update | Draft an edit to one entry |
content_propose_bulk_save | Draft edits across several entries as one change |
The admin_* reads go through the admin’s services, so they see drafts — a
Viewer’s copilot sees what that Viewer sees.
Localization and media
| Tool | Does |
|---|---|
i18n_locales_list, i18n_translations_get | Read locale state |
i18n_propose_translation, i18n_propose_bulk_translation | Draft a translation |
media_assets_search, media_asset_read, media_folders_list | Read the library |
media_propose_alt_text, media_propose_file | Draft an asset change |
Access, reviews and the rest
| Tool | Does |
|---|---|
segments_list, content_access_get | Segments this workspace can restrict to; who may read one entry |
content_propose_access | Draft a change to who may read an entry — needs segments:manage |
protection_review_status, protection_review_diff | Where a review stands; what changed since an approval |
protection_request_review | Ask named members to review an entry (writes directly, still prompted) |
admin_alarms_findings | Content problems flagged by alarms |
activity_recent | The audit trail — needs activity:read (Admin) |
workspace_members_list | People in the current workspace |
Absent on purpose: a publish tool, an approve tool, a bulk delete. See the authority model.
What a call looks like
A run streams each call and its result (see starting a run):
{ "type": "tool-call", "id": "call_1", "name": "admin_content_search",
"input": { "typeName": "article", "search": "pricing", "fields": ["title"], "pageSize": 5 } }
{ "type": "tool-result", "id": "call_1", "name": "admin_content_search",
"ok": true, "durationMs": 38, "summary": "…",
"output": { "typeName": "article", "total": 12, "page": 1, "pageSize": 5, "items": [ … ] } }A write parks first:
{ "type": "tool-permission-request", "id": "call_2", "runId": "…",
"name": "content_propose_update", "input": { … }, "expiresAt": "2026-09-30T10:17:44.000Z" }When the tool is previewable (the content plugin’s three propose tools
are), the engine runs it once before asking — same checks, nothing recorded or
applied — and the frame carries a preview: { kind, summary, lands, records, changes, omitted }, at most 100 changes. The admin draws it as the change card,
marked Not saved. A preview that fails still asks, without one; a tool not
marked previewable never runs before the answer.
The admin answers on POST /api/copilot/runs/:runId/permission with
{ "callId": "call_2", "decision": "once" | "chat" | "deny" }.
Three enforcement points
- Offer time. The capability profile is resolved per run from live permissions. A tool the caller cannot use is never offered.
- Permission prompt. Every
proposetool andprotection_request_reviewparks the run and asks — unless this chat already allowed it. - Execution. The registry re-checks
requiresagainst live grants. This is the boundary — the analogue of@RequirePermissions.
- A name the model was not offered gets
Unknown tool "<name>"., not an execution. - Arguments are checked against the tool’s schema before dispatch.
- The run engine refuses a call identical to one already made in the run, and tells the model — so a small model cannot burn its budget repeating one query.
content_propose_bulk_save is one change
“Add a summary to these eight posts” is one tool call, one card, one receipt — not eight.
- Reads every edited entry at propose time, so the card shows a real diff.
- Drops fields that would not change, and items left with nothing. (Re-saving current values would append a version and take a live entry back to draft.)
- Applies one at a time and stops at the first failure, reporting how many landed.
- Translations are
i18n_propose_bulk_translation’s job; a content save cannot add a language.
An edit that reaches other locales says so
On a localized type, a field not marked localized is shared — writing it
changes every sibling. The write tools ask i18n what the change reaches and put
it in the summary: the card title, the audit row and the model’s receipt.
Shared workspaces
Since 0.7.0 the content tools see published records of shared workspaces, read-only.
// admin_content_search input
{ "typeName": "author", "search": "ada", "source": "all" }source:"own"(default),"shared"or"all". Every item reportssource—nullfor own, else{ "workspaceId", "workspaceName" }.admin_content_geton a shared record returnssourceandreadOnly: true.- Propose tools refuse a shared record, telling the model to link it by id instead of copying it. Linking a shared record from a relation field is allowed.
The MCP content tools behave the same way — see MCP.
Surfaces
Each tool declares copilot, mcp, or both. Omitting the field means
both — so a new tool must decide who it is for.
| Surface | Tools |
|---|---|
| Copilot only | The admin_* tools, every propose tool, i18n_translations_get, activity_recent, workspace_members_list |
| Both | i18n_locales_list, the three media_* reads, segments_list, content_access_get, the three protection_* tools |
| MCP only | The sixteen generic content_* tools, and content_access_set |
Copilot-only means it writes through a proposal, shows drafts on
content:read, needs a permission no token carries, or only makes sense for a
signed-in person. Adding your own: agent tools.
Untrusted content is fenced
Entry bodies and tool output reach the model inside a fence marking them as data. That raises the cost of an injection; it does not remove it — see prompt injection.