Repository navigation
v2: internal tool plugins expose gaps in the public plugin API #34957
Description
Follow-up notes from migrating all built-in tools to internal plugins (#34946, #34956). The migration worked, but it surfaced a list of awkwardnesses worth deliberate decisions. Recording them here so they don't get lost.
1. Internal plugins run with privileged core services
PluginInternal injects ~28 Location services into internal plugin effects (Ripgrep, FileMutation, QuestionV2, SessionTodo, SessionInstructions, Image, ReadToolFileSystem, Shell, ...). An external plugin cannot build any of the built-in tools today because none of these capabilities exist on the public PluginContext.
That list is effectively a ranked backlog of candidate public domains. Strongest candidates based on what the tools actually needed:
- search (
Ripgrep: glob + grep) - file mutation with Location/permission integration (
FileMutation,LocationMutation) - interactive questions (
QuestionV2) - session todos (
SessionTodo)
2. PluginRuntime is a second, internal-only runtime surface
ShellTool and SubagentTool were plugins before the migration, but they lean on PluginRuntime.Service, which exposes session.{get,create,messages,prompt,resume,interrupt,synthetic}, job.{start,wait,block,background,cancel}, and location.agent.list. This overlaps awkwardly with the public ctx.session domain: the capabilities a tool actually needs to spawn subagents, wait on jobs, and inject synthetic messages live on the internal service, not the public context. Either the public session/job domains grow to cover this, or we accept a permanent two-tier API and say so explicitly.
3. Boot readiness is still advisory
PluginInternal.boot is forked, so every built-in tool now registers asynchronously. A ToolRegistry.materialize racing the first boot can see a partial tool set. Tests work around this with waitForTool polling. Now that all built-ins ride this path, the fix (await internal plugin readiness, e.g. via PluginV2.wait, before the first materialize in a Location) is worth prioritizing.
4. Tool.withPermission is an internal-only escape hatch
edit, write, and apply_patch share the edit permission action via Tool.withPermission, which is deliberately not part of public Tool.make. Fine for now, but it means external plugins cannot express shared-action definition filtering. Decide whether that stays internal or becomes a documented option.
5. WebSearchTool.ConfigService is a one-off config channel
websearch keeps a private ConfigService + configNode so embedders/tests can override search providers. No other tool has a config story, and plugins have no general one. Worth folding into a plugin-options or tool-config design instead of accreting more one-off services.
6. MCP tools are still registered directly
McpTool.node stays outside the plugin path because it reconciles dynamic tool sets on McpEvent.ToolsChanged. Making it a plugin needs the canonical scoped-registration design for dynamic sources (also noted in src/tool/AGENTS.md gaps).
7. No public harness for testing plugin tools
Core tests needed a registerToolPlugin helper that stubs PluginContext down to the tool domain. External plugin authors will want the same thing; a small published test utility (real registry, real settlement, stub context) would cover it.
Activity
Dante-dan commentedon Sep 26, 2026
I rechecked this tracker against current
v2. The startup race is covered by #35755;Tool.Options.permissionnow supports a shared permission action; MCP tools register throughTool.Service.transform. The concrete gap for an external glob/grep tool remains: publicPlugin.Contexthasctx.tool, but no local search domain, while the built-in glob tool receivesRipgrep,FileAccess,Location, andPermissionas internal services.I propose a bounded public
ctx.searchdomain for glob and grep. It would take the current tool-call context, resolve paths relative to the Location, apply the same external-path and permission checks as the built-ins, and return structured results. This would let external plugins implement search tools without exposing rawRipgrepor the broader privileged service set. I would verify a registered external tool can search an allowed path and that a denied or external path follows the existing policy. File mutation, questions/todos, and job orchestration can retain separate design decisions.Does this search boundary fit the intended public API? I can implement it once the core-feature design is approved.