Native programs

Every plugin runs as JavaScript in the sandboxed plugin host (ui-plugins.md), the four agent providers (codex, claude, opencode, acp) included. No plugin is a native program:

  • a manifest whose main is an array (a plugin API 1 process plugin) fails with "native plugins are not supported; use api.process.spawn";
  • a manifest with legacyNative (the Rust providers before their port to JavaScript) fails with "legacyNative is not supported: native providers are gone; make main a JavaScript module that runs the program with api.process.spawn". This holds for official plugins too.

Settings shows such a plugin as failed, with that message.

Running a program from a plugin

A plugin that needs a program (an agent's CLI, a language server, a build tool) starts it from JavaScript:

const child = await api.process.spawn("codex", ["app-server"], { cwd });

api.process.spawn needs the process grant for that program by name (programs), or process.any for a path or an interpreter (../AGENTS.md, Permissions). The host starts it with the login shell's PATH, as the leader of its own process group, and stops the group when the plugin reloads or is turned off. api.process.which(program) says where it is ({ path, realPath }) without starting it. A marketplace plugin that ships its own binaries lists them in binaries and needs native.binaries (and process.any).

The provider SDK (../sdk/) speaks the protocols agent programs use over those streams: JSON-RPC (spawnJsonRpc), line output (spawnLines), SSE (sse). See agent-protocol.md, "Providers in JavaScript".

What the native providers kept

The Rust providers kept files in <data>/agents/<name>/ (the accounts in instances.json, Claude's session aliases in sessions/). The first time the official JavaScript plugin of that name loads, the host moves the folder into the plugin's data folder as legacy/ (api.paths.data, read under the data scope of fs.read); the plugin reads it once and keeps what it needs in its storage.

The wire protocol

A plugin's runtime speaks JSON-RPC 2.0 with the host for it, in frames on the helper's pipes; a plugin never sees the messages. For reference, the host calls:

Method Answered from
initialize the runtime, once activate(api) has run
agents/list the agents the plugin registered (api.agents.register): { agents: [{ id, name, icon?, description? }] }
agent/<method> the registered agent's handler (agent-protocol.md)
tools/list, tool/call, tool/cancel (notification) api.tools.register (tools.md)
command/run, slash/run api.command, api.slash
hook/before_prompt; hook/after_turn and hook/tool_call (notifications) api.hooks.on
ui/event, ui/tool_render the plugin's views and tool renderers

The runtime sends agent/event for emit(event), and a host call (api.host.call(method, params), host-api.md) is a request to the host. A call without the grant is answered with the error code -32001, the message (PermissionNotGranted: <plugin> has no grant for <permission> <scope>) and data: { code, message, permission?, scope? }, and nothing happens; the runtime throws it as a PermissionNotGranted error.

Source: plugins/docs/process-plugins.md in the Divergence repository, built with this site.