feat(plugins): tool override flag for replacing built-in tools (closes #11049) (#26759)

Plugins can now replace a built-in tool by passing override=True to
ctx.register_tool(). Without it, the registry rejects any registration
that would shadow an existing tool from a different toolset (unchanged
default behavior).

Unlocks the use case from #11049: drop-in replacement of browser/web
backends without forking core. Composes with the existing pre_tool_call
hook for runtime interception of any implementation.

The override is audit-logged at INFO so it surfaces in agent.log.
This commit is contained in:
Teknium 2026-05-15 22:12:57 -07:00 committed by GitHub
parent 9c304a7f56
commit 016c772e7f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 180 additions and 5 deletions

View file

@ -244,8 +244,16 @@ class ToolRegistry:
emoji: str = "",
max_result_size_chars: int | float | None = None,
dynamic_schema_overrides: Callable = None,
override: bool = False,
):
"""Register a tool. Called at module-import time by each tool file."""
"""Register a tool. Called at module-import time by each tool file.
``override=True`` is an explicit opt-in for plugins that intend to
replace an existing built-in tool implementation (e.g. swap the
default browser tool for a headed-Chrome CDP backend). Without it,
registrations that would shadow an existing tool from a different
toolset are rejected to prevent accidental overwrites.
"""
with self._lock:
existing = self._tools.get(name)
if existing and existing.toolset != toolset:
@ -260,13 +268,22 @@ class ToolRegistry:
"Tool '%s': MCP toolset '%s' overwriting MCP toolset '%s'",
name, toolset, existing.toolset,
)
elif override:
# Explicit plugin opt-in: replace the existing tool.
# Logged at INFO so the override is auditable in agent.log.
logger.info(
"Tool '%s': toolset '%s' overriding existing toolset '%s' "
"(override=True opt-in)",
name, toolset, existing.toolset,
)
else:
# Reject shadowing — prevent plugins/MCP from overwriting
# built-in tools or vice versa.
logger.error(
"Tool registration REJECTED: '%s' (toolset '%s') would "
"shadow existing tool from toolset '%s'. Deregister the "
"existing tool first if this is intentional.",
"shadow existing tool from toolset '%s'. Pass "
"override=True to register() if the replacement is "
"intentional, or deregister the existing tool first.",
name, toolset, existing.toolset,
)
return