Beta . Ortha CMS is in public beta. Every plan is free until it ends.
Features Ortha CMS on GitHub

Tools and permissions

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

Documents 0.13.1 Updated Edit this page Report a problem

On this page

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

ToolDoes
admin_content_typesThe content types available, with their filterable fields
admin_content_searchSearch, filter, sort, page, locale, sparse fields, source
admin_content_getOne entry
admin_content_revisionsAn entry’s version timeline
admin_content_diffThe changed fields between two versions
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

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

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

Access, reviews and the rest

ToolDoes
segments_list, content_access_getSegments this workspace can restrict to; who may read one entry
content_propose_accessDraft a change to who may read an entry — needs segments:manage
protection_review_status, protection_review_diffWhere a review stands; what changed since an approval
protection_request_reviewAsk named members to review an entry (writes directly, still prompted)
admin_alarms_findingsContent problems flagged by alarms
activity_recentThe audit trail — needs activity:read (Admin)
workspace_members_listPeople 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

  1. Offer time. The capability profile is resolved per run from live permissions. A tool the caller cannot use is never offered.
  2. Permission prompt. Every propose tool and protection_request_review parks the run and asks — unless this chat already allowed it.
  3. Execution. The registry re-checks requires against 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 reports source — null for own, else { "workspaceId", "workspaceName" }.
  • admin_content_get on a shared record returns source and readOnly: 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.

SurfaceTools
Copilot onlyThe admin_* tools, every propose tool, i18n_translations_get, activity_recent, workspace_members_list
Bothi18n_locales_list, the three media_* reads, segments_list, content_access_get, the three protection_* tools
MCP onlyThe 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.