Skip to content

v2: internal tool plugins expose gaps in the public plugin API #34957

Description

@kitlangton

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

added
discussionUsed for feature requests, proposals, ideas, etc. Open discussion
coreAnything pertaining to core functionality of the application (opencode server stuff)
on Jul 2, 2026

Dante-dan commented on Sep 26, 2026

@Dante-dan

I rechecked this tracker against current v2. The startup race is covered by #35755; Tool.Options.permission now supports a shared permission action; MCP tools register through Tool.Service.transform. The concrete gap for an external glob/grep tool remains: public Plugin.Context has ctx.tool, but no local search domain, while the built-in glob tool receives Ripgrep, FileAccess, Location, and Permission as internal services.

I propose a bounded public ctx.search domain 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 raw Ripgrep or 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    2.0coreAnything pertaining to core functionality of the application (opencode server stuff)discussionUsed for feature requests, proposals, ideas, etc. Open discussion

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions