Skip to content

Agent Skills

An Agent Skill is a packaged, versioned unit of agent capability — instructions (SKILL.md), executable scripts, reference resources, and templates — published in the Admin UI and governed like any other resource: assigned to resource groups, guarded by skill_input/skill_output guardrails, optionally approval-gated, always audited.

Skills are not tools. A tool is something a model calls once and gets a result. A skill is a guided workflow: the model loads the skill’s instructions, then follows them — reading bundled files, running scripts, rendering templates — with every step routed through the gateway. Brutor implements this as progressive disclosure over MCP in three layers:

Layer What happens Cost in context
L1 — Discover The caller learns which skills exist (name + one-line description) ~100 tokens per skill
L2 — Load skills__load returns the full SKILL.md plus a manifest of scripts/resources/templates Only for the skill actually being used
L3 — Act skills__read_resource / skills__run_script / skills__render_template, each independently governed Only the outputs

The skill bundle itself never leaves Brutor. The agent receives instructions, resource content, script output, and rendered templates — never the code — and nothing executes outside Brutor’s sandbox.

Endpoint POST http://localhost:8100/v1/proxy/mcp/system-agent-skill-server-{tenant} (e.g. ...-default)
Protocol MCP JSON-RPC 2.0 (initialize, tools/list, tools/call)
Auth API key or gateway JWT (see Overview)

Every skill-capable tenant has this system MCP server — it is the single surface for skills. Point any MCP client at it and the five skills__* verbs below appear in its tool palette.

Discovery happens in two complementary places:

initialize returns an instructions string containing the protocol preamble and the caller’s access-scoped catalog — MCP clients fold this into the model’s context:

These tools give you access to governed Agent Skills. Skills are guided
workflows, not tools. Call `skills__list` to see which skills are available
to you (...). To use one, call `skills__load` with its name...
Available skills — call `skills__load` with the name to use one:
- quarterly-pipeline-report: Compile the quarterly revenue pipeline report from CRM data.
- triage-ticket: Classify and route an inbound support ticket.

tools/list advertises exactly five generic verbs — skills themselves are deliberately not in the tool list:

{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}
Tool Layer Required arguments
skills__list L1
skills__load L2 skill_name
skills__read_resource L3 skill_name, path
skills__run_script L3 skill_name, script (optional args, approval_token)
skills__render_template L3 skill_name, template (optional vars)

On the stateless MCP dialect (2026-07-28, when MCP_STATELESS_ENABLED is on), the same preamble + catalog is returned in the instructions field of the server/discover result — its spec-sanctioned home; no handshake needed. And because some MCP clients drop instructions entirely, the catalog is also reachable as a tool — the client-independent path on every dialect:

Terminal window
curl -s -X POST http://localhost:8100/v1/proxy/mcp/system-agent-skill-server-default \
-H "Authorization: Bearer sk_brutor_api_..." \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"skills__list","arguments":{}}}'
{
"content": [{
"type": "text",
"text": "{\"skills\": [{\"name\": \"quarterly-pipeline-report\", \"description\": \"Compile the quarterly revenue pipeline report from CRM data.\"}]}"
}],
"isError": false
}

The catalog is access-scoped: it contains only published, enabled skills assigned to a resource group the caller belongs to.

skills__load returns the skill’s full instructions and its manifest — the exact L3 surface the model may use:

Terminal window
curl -s -X POST http://localhost:8100/v1/proxy/mcp/system-agent-skill-server-default \
-H "Authorization: Bearer sk_brutor_api_..." \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":"skills__load",
"arguments":{"skill_name":"quarterly-pipeline-report"}}}'

The result carries two text content blocks:

  1. An injected protocol preamble followed by the unmodified SKILL.md body. The preamble adapts the (portable) SKILL.md to the governed protocol — “whenever it tells you to run a script, call skills__run_script; do not use a local shell or fetch files directly.”
  2. The manifest as JSON:
{
"skill": {
"name": "quarterly-pipeline-report",
"version": "1.2.0",
"description": "Compile the quarterly revenue pipeline report from CRM data."
},
"manifest": {
"resources": [
{"path": "reference.md", "kind": "reference", "mime": "text/markdown", "bytes": 4812}
],
"scripts": [
{"name": "run.py", "language": "python", "execution_mode": "sandbox",
"args_schema": {"type": "object", "properties": {"quarter": {"type": "string"}}},
"description": "Aggregate pipeline data for the given quarter."}
],
"templates": [
{"name": "report.html", "kind": "template", "mime": "text/html",
"vars_schema": {"type": "object", "properties": {"title": {"type": "string"}}},
"description": "Final report layout."}
]
}
}

The load itself is a governed, audited event (skill_load in the audit trail) — it executes nothing.

A skill’s SKILL.md authored in the Admin UI

After loading, the model follows SKILL.md using the three act verbs. Filenames must match the manifest exactly — it is an allowlist.

Runs one named script in Brutor’s sandbox and returns only its output:

Terminal window
curl -s -X POST http://localhost:8100/v1/proxy/mcp/system-agent-skill-server-default \
-H "Authorization: Bearer sk_brutor_api_..." \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":4,"method":"tools/call",
"params":{"name":"skills__run_script",
"arguments":{"skill_name":"quarterly-pipeline-report",
"script":"run.py",
"args":{"quarter":"Q3-2026"}}}}'
{
"content": [{"type": "text", "text": "{\"summary\": \"Pipeline by region: EMEA 4.2M, AMER 6.1M, ...\"}"}],
"isError": false
}

Scripts in the same conversation share a session workspace keyed by the MCP correlation id — a script can write a file that a later script in the same workflow reads.

Returns a bundled reference file’s content (with its mimeType), for when the instructions say “consult reference.md”:

{"name": "skills__read_resource",
"arguments": {"skill_name": "quarterly-pipeline-report", "path": "reference.md"}}

Renders a bundled template/asset with variables substituted:

{"name": "skills__render_template",
"arguments": {"skill_name": "quarterly-pipeline-report",
"template": "report.html",
"vars": {"title": "Q3 2026 Pipeline"}}}

Each L2/L3 call runs the full governance pipeline independently — there is no “approved once, trusted after” window:

  • Access — the caller’s resource groups must include the skill, at every layer (L1 catalog, L2 load, L3 act).
  • Input guardrails (skill_input surface) and argument policies run on skills__run_script args before anything reaches the sandbox.
  • Per-skill quotas and rate limits from the group’s Agent Skills settings.
  • Human approval — if the skill is approval-gated, skills__run_script returns a pending envelope instead of running (see below).
  • Output guardrails (skill_output surface) run on script output, resource content, and rendered templates before they reach the model — PII redaction applies.
  • Audit — every step is one proxy-log row (proxy_type='skill', subtype skill_load / skill_read / skill_run / skill_render) sharing the workflow’s correlation id, plus skill usage attribution (resource_type='skill').

When a skill requires approval, skills__run_script parks the run and returns:

{
"content": [{
"type": "text",
"text": "{\"execution_id\": \"exe-01JZ...\", \"status\": \"pending_approval\", \"message\": \"Skill 'quarterly-pipeline-report' execution pending approval...\", \"skill_name\": \"quarterly-pipeline-report\"}"
}],
"isError": false
}

The flow mirrors MCP tool approvals: a group member approves in the portal inbox (/v1/portal/approvals), then the caller retries the same skills__run_script call with the one-time token in the approval_token argument.

MCP transport always answers HTTP 200; failures come back as a tool result with isError: true and a model-readable message — e.g. an access denial:

{
"content": [{"type": "text", "text": "Access denied: skill 'quarterly-pipeline-report' is not available to your group."}],
"isError": true
}

The governance verdict (403 access denial, guardrail block, quota trip) is recorded on the audit row for that call — check Logs & Audit Trail, not the HTTP status.

Executable skill code never runs inside the gateway — and never on the agent’s machine. skills__run_script dispatches to the skill-runner, a separate container with a deliberately hostile-to-escape posture:

Property Value
Network exposure Internal-only, port 8210 — never published to the host
Auth Per-request JWT signed with SKILL_RUNNER_JWT_SECRET (must match the core proxy), e.g. TTL 300s
Process isolation Subprocess execution, read-only root filesystem, all Linux capabilities dropped (cap_drop: ALL)
Timeouts SANDBOX_TIMEOUT_SECS=30 (untrusted), TRUSTED_TIMEOUT_SECS=120 (trusted skills)
Request cap MAX_REQUEST_BYTES=16777216 (16 MiB)

A runaway or malicious script times out and dies in the runner; the gateway records the failed run. See ports & services for the deployment picture.

Skill access is governed exclusively by resource groups — a skill is available to a caller if (and only if) the skill is assigned to a group the caller belongs to (with normal RG inheritance rules). There is deliberately no per-skill ACL — don’t look for one, and don’t try to emulate one; group membership is the single source of truth for “who can run what”.

Two sharpening tools within that model:

  • Per-group governance on skills (quotas, rate limits, approval mode) via the group’s Agent Skills settings.

  • API keys with allowed_skill_ids — a key can be narrowed to a subset of the skills its group allows. Useful for a CI pipeline key that may run exactly one skill:

    key group allows: skills A, B, C
    key allowed_skill_ids: [B]
    effective: B only

Earlier releases exposed a one-shot REST lifecycle — POST /v1/proxy/skills/execute, GET /v1/proxy/skills/executions/{id}, .../cancel, and GET /v1/proxy/skills/catalog. It treated a skill like a tool: one call, one result, with the caller’s own agent loop consuming skill_prompt + allowed_tools outside governance.

That model is deprecated. The execute endpoint still answers during the migration window (and logs a deprecation warning), but it will be removed — migrate to the MCP progressive-disclosure surface above, where every step of the workflow stays governed.