ToolPermissionContext is a lightweight, immutable filter that determines whether a given tool name should be blocked before execution. It supports exact-name matching and prefix-based matching, both case-insensitive.
from src.permissions import ToolPermissionContext
ToolPermissionContext
A frozen dataclass. Construct instances via the from_iterables factory rather than directly.
Fields
Exact tool names to block. All values are stored lower-cased at construction time.
Prefixes to block. Any tool whose lower-cased name starts with one of these prefixes is blocked. All values are stored lower-cased at construction time.
from_iterables
@classmethod
def from_iterables(
cls,
deny_names: list[str] | None = None,
deny_prefixes: list[str] | None = None,
) -> ToolPermissionContext
Factory method. Normalises all inputs to lower-case and returns a frozen ToolPermissionContext.
Exact tool names to block. Pass None or omit to allow all names.
Name prefixes to block. Pass None or omit to disable prefix filtering.
from src.permissions import ToolPermissionContext
ctx = ToolPermissionContext.from_iterables(
deny_names=["BashTool"],
deny_prefixes=["mcp_"],
)
blocks
def blocks(self, tool_name: str) -> bool
Returns True if tool_name should be blocked, False otherwise.
A tool is blocked when either of the following is true:
- Its lower-cased name is an exact member of
deny_names.
- Its lower-cased name starts with any value in
deny_prefixes.
The tool name to test. Comparison is always case-insensitive.
ctx = ToolPermissionContext.from_iterables(
deny_names=["BashTool"],
deny_prefixes=["mcp_"],
)
ctx.blocks("BashTool") # True — exact match
ctx.blocks("bashtool") # True — case-insensitive exact match
ctx.blocks("mcp_filesystem") # True — prefix match
ctx.blocks("FileReadTool") # False — no match
ctx.blocks("MCP_something") # True — prefix match is case-insensitive
PermissionDenial
Recorded when a tool is blocked. Used in TurnResult.permission_denials and passed as denied_tools to submit_message.
from src.models import PermissionDenial
Fields
The name of the tool that was blocked.
Human-readable explanation for why the tool was blocked.
denial = PermissionDenial(
tool_name="BashTool",
reason="destructive shell execution remains gated in the Python port",
)
ToolPermissionContext integrates with get_tools from src.tools to pre-filter the tool list before routing:
from src.permissions import ToolPermissionContext
from src.tools import get_tools
ctx = ToolPermissionContext.from_iterables(
deny_names=["BashTool", "ComputerTool"],
deny_prefixes=["mcp_"],
)
# Only tools not blocked by ctx are returned
allowed_tools = get_tools(permission_context=ctx)
When used with QueryEnginePort.submit_message, pass blocked tools as PermissionDenial instances so the engine records the denial in the turn result:
from src.models import PermissionDenial
from src.query_engine import QueryEnginePort
from src.permissions import ToolPermissionContext
ctx = ToolPermissionContext.from_iterables(deny_names=["BashTool"])
candidate_tools = ["BashTool", "FileReadTool"]
denied = tuple(
PermissionDenial(tool_name=t, reason="blocked by permission context")
for t in candidate_tools
if ctx.blocks(t)
)
allowed = tuple(t for t in candidate_tools if not ctx.blocks(t))
engine = QueryEnginePort.from_workspace()
result = engine.submit_message(
"run the test suite",
matched_tools=allowed,
denied_tools=denied,
)
print(result.permission_denials)
# (PermissionDenial(tool_name='BashTool', reason='blocked by permission context'),)
PortRuntime.bootstrap_session automatically creates PermissionDenial entries for any matched tool whose name contains "bash". For custom gating logic, bypass bootstrap_session and call QueryEnginePort.submit_message directly with your own denied_tools.