Skip to content

Add subprocess-isolated TS plugin host (.maestro/plugins/) #297

Description

@haasonsaas

Context

Today our extensibility surface is MCP servers + shell hooks (see docs/design/HOOKS_SYSTEM.md). MCP is correct for cross-org integrations and shell hooks are correct for stdin/stdout glue, but neither covers the lightest-weight case: "I want to drop a TS file in my repo that adds a tool, listens for events, and prompts me through ctx.ui.*."

Sourcegraph Amp's "Neo" rebuild ships exactly this surface, and the implementation pattern is interesting:

  • Plugins live at .amp/plugins/*.ts (workspace) or ~/.config/.amp/plugins/*.ts (user)
  • Each plugin runs in its own Node subprocess. Capability surface is mediated by JSON-RPC over IPC; no in-process VM access.
  • Method namespace is small: command.list | tool.list | events.list | command.execute | tool.execute
  • API surface inside the plugin: amp.on(...), amp.registerTool(...), amp.registerCommand(...), ctx.ui.{notify,confirm,input,select}, amp.ai.ask(...)
  • Per-plugin stderr is captured and surfaced to the host; plugin crashes don't take the CLI down
  • Built-in plugins (e.g. their permissions system) reuse the same machinery — same loader, same IPC, different entry point. No fork between "user plugin" and "built-in plugin" code paths.

Scope

  1. Define a MaestroPlugin API in a new packages/plugin-sdk/ (TS). Surface mirrors the Amp shape but uses our naming:
    • maestro.on(event, handler) — typed events from proto/maestro/v1/headless.proto
    • maestro.registerTool({ name, description, inputSchema, execute })
    • maestro.registerCommand({ name, handler })
    • ctx.ui.{notify, confirm, input, select} — routed through the ServerRequest envelope (SERVER_REQUEST_TYPE_USER_INPUT) so it works for TUI/web/IDE/Slack uniformly
    • maestro.ai.ask(...) — classification helper using the active agent's model
  2. Plugin loader: scans .maestro/plugins/ (workspace) and ~/.config/.maestro/plugins/ (user). For each plugin file, fork a Node subprocess; communicate over Node IPC channel (JSON-RPC).
  3. Per-plugin state machine: loading | running | failed, with registered events/commands/tools tracked separately. Surface via a maestro plugins list CLI command.
  4. Per-plugin stderr captured and rendered in the TUI as a collapsible diagnostic stream.
  5. Convert at least one existing internal extension into a built-in plugin (candidate: the hooks system itself — would unify two extension surfaces, keeping the env-var/JSON config compat layer).

Non-goals

  • No in-process plugin loader. Subprocess isolation is the whole point.
  • No Wasm. Subprocess + Node is enough.
  • Not replacing MCP — they coexist. MCP for cross-org tools, plugins for personal/repo-local extensions.

Acceptance criteria

  • A user drops a plugin file under .maestro/plugins/, restarts maestro, and the agent can invoke its tool.
  • Killing the plugin subprocess does not kill the agent. The host marks the plugin failed and renders the captured stderr.
  • A built-in (e.g. converted hooks) loaded via entryPoint argument runs through the same code path as a user plugin.
  • Plugins can elicit UI through ctx.ui.confirm and the elicitation surfaces correctly in TUI and web (because it is a typed ServerRequest of type USER_INPUT).

Why now

We have a versioned protobuf headless protocol with first-class ServerRequest enums. This is the right moment to add an extension surface that uses that protocol cleanly — drop-in TS plugins that talk to the host through the same typed envelope every other client uses. Doing it later means retrofitting against three different transports (TUI, web, IDE).

Reference

Sourcegraph Amp shipped this in their "Neo" rebuild on 2026-05-06. Pattern observed by inspecting the published @sourcegraph/amp npm bundle (dist/main.js). Per-plugin process creation visible at createProcess({onRequest, onEvent, onStateChange}); method namespace and URI scheme (internal://plugin/<name>.ts for built-ins, amp-global-plugin:<name> for user/global) extracted from string literals in the bundle.

Metadata

Metadata

Assignees

No one assigned

    Labels

    architecture-reviewCross-service architecture review requestedenhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions