OpenProgram Docs

Extension Gating#

Single-source design doc for how OpenProgram controls which extensions (tools, skills, MCP servers) the LLM can use, scoped by agent profile.

What this folder covers#

File Read it when
README.md first time — high-level model + agent.json schema + examples
reference-comparison.md considering a design change — see how claude-code / opencode / hermes do it
implementation.md touching the code — file paths, helper module, gate sites
future-work.md explicitly noted as "not built and not on the roadmap unless someone asks"

For the broader skills+plugins design (catalogues, discovery, hot reload, etc.) see ../skills-and-plugins.md. This folder is the gating subset only.


TL;DR#

Every extension type the LLM directly sees is gated through the same shape in the agent profile:

# agent.json
tools:
  disabled: ["bash", "*_dangerous"]   # fnmatch patterns
  allowed:  []                         # whitelist; empty = no constraint
skills:
  disabled: ["devops/*"]
  allowed:  []
  categories: ["research", "writing"]  # skill-only: gate by frontmatter category
mcp:
  disabled: ["slack/*"]                # filters MCP tools named "<server>__<tool>"
  allowed:  []
  required: ["drawio*"]                # agent unavailable when nothing matches

Plugins are not in this list — plugin is a host-level "contributor". Gating happens on what a plugin contributes (skills / tools / MCP), not on the plugin itself.


The unified model#

┌────────────────────────────────────────────────────────────────┐
│                       agent profile                            │
│                                                                │
│   ┌──────────┐    ┌──────────┐    ┌──────────┐                 │
│   │ tools    │    │ skills   │    │ mcp      │  ← same shape   │
│   └─────┬────┘    └─────┬────┘    └─────┬────┘                 │
│         │               │               │                      │
│  ┌──────┴──────┐  ┌─────┴──────┐  ┌─────┴──────┐               │
│  │ disabled    │  │ disabled   │  │ disabled   │   fnmatch     │
│  │ allowed     │  │ allowed    │  │ allowed    │   pattern     │
│  │             │  │ categories │  │ required   │   lists       │
│  └─────────────┘  └────────────┘  └────────────┘               │
└────────────────────────────────────────────────────────────────┘
              │
              │  shared helper: openprogram/agent/management/gating.py
              │
              ▼
   match_any(name, patterns)          —  fnmatch wildcard match
   gate(name, category, disabled,     —  resolve decision
        allowed, categories)              returns reason str | None
   check_required(installed, required) —  list missing patterns

Resolution order (per gate call)#

For a single extension (one tool, one skill, one MCP server):

  1. disabled — if matched, reject outright with reason
  2. allowed (non-empty) — if not matched, reject (allowlist mode)
  3. categories (non-empty, skill only) — if item's category not matched, reject
  4. required (MCP only) — separate hard check: if any required pattern matches nothing installed, the whole agent is unavailable for this turn

Exact names are the trivial case of fnmatch ("bash" matches only bash). Wildcards (*, ?, [abc]) work because matching goes through fnmatch.fnmatchcase.


Why this shape#

The shape combines claude-code's per-type field structure (tools: [], disallowedTools: [], mcpServers: []) with the wildcard expressiveness of opencode's permission: Ruleset:

  • Easy to read — each field name says what it gates
  • Easy to write — disabled: [anthropic-skills/*] is one line, not five
  • Backward compatible — old {disabled: [pdf]} profiles still work
  • Trivial to extend — new extension type = new top-level field with the same {disabled, allowed} shape

See reference-comparison.md for the full claude-code / opencode side-by-side.


Worked examples#

1. Customer-service agent — chatty, no edit tools#

id: customer-service
tools:
  disabled: ["write", "edit", "execute_*", "apply_patch"]
skills:
  categories: ["customer-service", "writing"]
mcp:
  disabled: ["*"]   # no MCP at all

Effect: LLM sees only read-only + research tools, only skills tagged for customer service or writing, no MCP servers.

2. DevOps agent — everything except destructive prod skills#

id: devops
skills:
  disabled: ["prod-deploy", "drop-database"]
mcp:
  required: ["github*", "k8s*"]   # agent unavailable if these aren't configured

Effect: agent loads only if GitHub + k8s MCP are installed; otherwise the dispatcher skips it. Two specific skills are blacklisted.

3. Research agent — read-heavy, narrow MCP#

id: research
tools:
  allowed: ["read", "grep", "glob", "web_search", "web_fetch"]
skills:
  categories: ["research"]
mcp:
  allowed: ["arxiv*", "scholar*"]

Effect: tools collapsed to read/search five, only research-tagged skills, only arxiv/scholar MCPs.


When NOT to use gating#

Most users should never touch this. The Anthropic SKILL.md design philosophy is LLM-mediated selection: every skill has a description field, the model reads all of them and picks. Gating is an escape hatch for when:

  • A skill is dangerous and you want a hard wall (prod-deploy never runs in customer-service agent)
  • An agent role is so narrow that listing the permitted set is shorter than the description-tuning argument (research agent only ever needs 5 tools)
  • A required MCP must exist for the agent to be coherent (research agent without arxiv* shouldn't even appear)

Default workflow: install skills globally, configure no gates, let the LLM pick. Add a gate when you observe a specific bad behavior, not preemptively.


See also#

Last updated · 2026-08-13