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
mainis 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.