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.
L1 — Discover
Section titled “L1 — Discover”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 guidedworkflows, not tools. Call `skills__list` to see which skills are availableto 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:
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.
L2 — Load
Section titled “L2 — Load”skills__load returns the skill’s full instructions and its manifest — the exact L3 surface the model may use:
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:
- An injected protocol preamble followed by the unmodified
SKILL.mdbody. The preamble adapts the (portable)SKILL.mdto the governed protocol — “whenever it tells you to run a script, callskills__run_script; do not use a local shell or fetch files directly.” - 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.

L3 — Act
Section titled “L3 — Act”After loading, the model follows SKILL.md using the three act verbs. Filenames must match the manifest exactly — it is an allowlist.
skills__run_script
Section titled “skills__run_script”Runs one named script in Brutor’s sandbox and returns only its output:
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.
skills__read_resource
Section titled “skills__read_resource”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"}}skills__render_template
Section titled “skills__render_template”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"}}}Governance on every layer
Section titled “Governance on every layer”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_inputsurface) and argument policies run onskills__run_scriptargs 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_scriptreturns a pending envelope instead of running (see below). - Output guardrails (
skill_outputsurface) 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', subtypeskill_load/skill_read/skill_run/skill_render) sharing the workflow’s correlation id, plus skill usage attribution (resource_type='skill').
Approval-gated skills
Section titled “Approval-gated skills”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.
Errors
Section titled “Errors”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.
The sandbox: how skill code runs
Section titled “The sandbox: how skill code runs”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.
Access control: resource groups only
Section titled “Access control: resource groups only”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, Ckey allowed_skill_ids: [B]effective: B only
Deprecated: the REST execute lifecycle
Section titled “Deprecated: the REST execute lifecycle”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.
Related
Section titled “Related”- Build an agent — wiring an LLM agent loop to gateway MCP servers (the skills server is one of them)
- Use MCP tools through the gateway — connecting Claude Desktop, Goose, and MCP SDKs
- Resource groups — the access model
- Approvals — approval-gating skills and tools
