feat(permissions): allowed_tools / disallowed_tools schema-level visibility#246
Merged
emal-avala merged 1 commit intomainfrom Apr 24, 2026
Merged
Conversation
…bility
Adds two new `PermissionsConfig` fields that hide tools from the
model's schema before the LLM ever sees them:
[permissions]
allowed_tools = ["FileRead", "Grep", "mcp__*"]
disallowed_tools = ["Bash", "WebFetch"]
Distinct from the existing `rules` surface. Rules gate authorization
at call time — the tool is still in the schema, the model still
considers calling it, and a denial fires permission_denied + the
model sees an error. Visibility filtering removes the tool from the
schema entirely, so:
- The model never considers it (can't hallucinate a workflow around
a tool that isn't there).
- Prompt token budget is reclaimed (for large MCP surfaces this can
be hundreds of tokens per turn).
Semantics:
- Empty allowlist = all tools pass; non-empty = strict allowlist.
- Denylist always wins over allowlist — operators can safely add
denylist entries without auditing the allowlist for overlaps.
- Trailing `*` wildcard matches name prefixes (e.g. `mcp__*` hides
every MCP-namespaced tool). `*` in other positions is treated
literally — the matcher is deliberately minimal.
- Applied in `schemas()`, `core_schemas()`, and `deferred_names()`,
so ToolSearch can't resurrect a hidden tool either.
Wired from `config.permissions.allowed_tools` /
`config.permissions.disallowed_tools` into the `ToolRegistry` at
CLI startup, after default tools are registered but before MCP
proxy tools are appended. The registry re-filters on each
schemas() call so there's no ordering dependency with MCP
discovery.
Tests (9 new in registry, plus schema coverage):
- Empty filter allows everything
- Allowlist restricts to listed names
- Denylist wins over allowlist
- Wildcard matches prefix
- Denylist wildcard hides whole namespace
- Non-trailing `*` is treated literally
- schemas / core_schemas / deferred_names all honor the filter
Full test suite passes. Clippy clean.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
permissions.allowed_toolsandpermissions.disallowed_tools— schema-level tool visibility filtering. Tools on the denylist (or off a non-empty allowlist) are hidden from the model's schema before the LLM sees them, not just gated at call time.Problem this closes
The existing
permissions.rulessurface gates authorization at call time — the tool is still in the schema, the model still considers calling it, and a denial firespermission_denied+ the model sees an error. Useful, but:Schema-level filtering removes the tool entirely.
Config
Semantics
*wildcard matches prefixes (e.g.mcp__*hides every MCP-namespaced tool).*in other positions is treated literally — matcher is deliberately minimal for readability.schemas(),core_schemas(), anddeferred_names()— so ToolSearch can't resurrect a hidden tool.Wiring
main.rsinstalls the filter on theToolRegistryimmediately afterdefault_tools()and before MCP discovery. The registry re-filters on eachschemas()call, so there's no ordering dependency with later tool registrations.Tests (9 new registry + schema updates)
*wildcard matches prefix*is treated literally (explicit restriction test)schemas()/core_schemas()/deferred_names()all honor the filterFull
tools::registry+permissions_integrationsuites pass. Clippy clean under-D warnings.Test plan
cargo test -p agent-code-lib --lib tools::registrycargo test -p agent-code-lib --test permissions_integrationcargo clippy --workspace --tests --no-deps -- -D warningscargo fmt --all --check