Features Apograph CMS on GitHub

Tools and permissions

One registry, two surfaces, and the three points at which a tool call is checked.

Documents 0.5.2 Updated Edit this page Report a problem

On this page

A tool is a named, permission-gated capability an agent can invoke. They live in one shared registry with two consumers: the copilot’s run loop, and the MCP endpoint.

That is deliberate. A tool added for either is available to both, and neither can acquire a private set of rules.

The catalogue

Content, read

ToolDoes
admin_content_typesThe content types available, with their filterable fields
admin_content_searchSearch, filter, sort, page, locale, sparse fields
admin_content_getOne entry
admin_content_revisionsAn entry’s version timeline
admin_content_diffThe changed fields between two versions

Content, write

ToolDoes
content_propose_createDraft a new entry
content_propose_updateDraft an edit to one entry
content_propose_bulk_saveDraft edits across several entries as one change

Localization and media

ToolDoes
i18n_locales_list, i18n_translations_getRead locale state
i18n_propose_translation, i18n_propose_bulk_translationDraft a translation
media_assets_search, media_asset_read, media_folders_listRead the library
media_propose_alt_text, media_propose_fileDraft an asset change

Notice what is absent: there is no publish tool, and no bulk delete. See the authority model.

Three enforcement points

A call passes three gates, and they are not redundant.

  1. Offer time. The capability profile is resolved per run from the caller’s live permissions. A tool the caller could not use is not offered to the model at all.
  2. Permission prompt. A tool that changes something parks the run and asks, unless this conversation already allowed it.
  3. Execution. The registry authorises the call against live grants. This is the analogue of @RequirePermissions on an HTTP route, and it is the one that actually decides.

Gate 1 shapes what the model tries. Gate 3 is the boundary. A model that invents a tool name it was not offered gets an error, not an execution.

content_propose_bulk_save is one change, not a loop

“Translate these eight posts” used to be eight tool calls — eight steps against a bounded run, and eight cards in the transcript for one instruction, none of which said what the other seven were.

The bulk tool takes a list and produces one change with one receipt. It reads every edited entry at propose time so the card carries a real diff, drops fields that would not actually change, and drops an item left with nothing.

That last part matters more than it sounds: writing an entry its own current values still appends a version and takes a live entry back to draft. A model re-sending twenty unchanged records would unpublish twenty live pages.

The applier writes items one at a time and stops at the first failure, reporting how many landed — a proposal is one row with one status, so carrying on would grow the number of entries written under a receipt that then reports failure.

An edit that reaches other locales says so

On a localized type, a field the type does not mark localized is shared, so writing it propagates to every sibling in the translation group.

The write tools ask the localization plugin what a change would reach — against the patched values, so the disclosure names what will actually be written — and fold the answer into the change summary. That summary is the card’s title, the audit row, and the receipt the model sees, so one string reaches the person, the log, and the model.

The model correcting itself — “that touched every language; a translation wants i18n_propose_translation” — is worth as much as the card.

Surfaces

A tool declares which surfaces it is offered to: copilot, mcp, or both.

Omitting the field means both. So adding a tool anywhere means deciding who it is for — an internal admin tool that leaks onto the MCP endpoint is a tool an external agent can call with a bearer token.

The admin_* tools are copilot-only. The MCP surface gets its own generic set; see the MCP endpoint.

Untrusted content is fenced

Content the model reads — entry bodies, tool output — is passed inside a fence marking it as data rather than instructions.

This raises the cost of an injection. It does not eliminate it, and it is not claimed to. See the warning on the authority model.