ADR-018: Binary Plugin Protocol For Rust-Hosted Extensions
Palamedes extensions are independently shipped executables. Rust-first extensions such as commercial workflow commands should link the palamedes crates directly without maintaining N-API bindings, while users keep the single pmds entry point.
- Status: Accepted
- Date: 2026-07-25
- Revised: 2026-08-03
Context
ADR 017 places explicit plugin dispatch in the native CLI. A language-neutral subprocess protocol keeps independently versioned plugins outside the CLI's Rust ABI and prevents one plugin crash from corrupting the host process.
Decision
- A plugin is a standalone executable spawned once per request. The host and plugin exchange newline-delimited JSON on stdio.
- Configuration entries remain a package/path specifier or
[specifier, options]. Packages declarepalamedes.pluginBinary; native meta packages may resolve the matching installed optional platform package. Direct executable paths are accepted for local development. JavaScript and TypeScript files are rejected. - Protocol version
1uses two request kinds:describereturns one manifest with the plugin namespace, protocol version, and commands;runreceives the command, arguments, plugin options, resolved data config, semantic catalogs, output mode, and interactive capability.
- A run emits zero or more
diagnosticandoutputevents followed by at most oneresultevent (text,data,exitCode). stdout is reserved for protocol events; stderr passes through for progress logging. - The manifest uses lowercase kebab-case names, may not collide with built-in namespaces, and must match the host protocol major exactly.
- The
resultexit code is authoritative. Without a result, the child process exit code is the fallback and the host emits a protocol diagnostic. - The host sets
PALAMEDES_NATIVEto the absolute path of its own executable. Trusted plugins may invoke documented built-ins directly as subprocesses. - On Unix the host starts each plugin in an isolated process group and forwards
SIGINTandSIGTERMto that group. Direct and terminal signals therefore reach the plugin tree exactly once; shell-compatible cancellation codes remain 130 and 143. - A configured plugin that fails to resolve or describe blocks only its own namespace. Commands of other configured plugins run and surface the skipped plugin as a warning diagnostic; the failure is fatal only when it may own the requested namespace.
Packaging And Resolution
Resolution starts at the data config and walks parent node_modules
directories. Scoped and unscoped package names are supported without evaluating
Node exports. pnpm's package symlinks are canonicalized before resolving a meta
package's optional dependencies.
For a direct binary package:
{
"name": "@acme/palamedes-plus-darwin-arm64",
"os": ["darwin"],
"cpu": ["arm64"],
"palamedes": { "pluginBinary": "./bin/palamedes-plus" }
}A platform-neutral package can list such packages as optionalDependencies.
The host selects the one installed for its compiled os, cpu, and libc.
Trust Model
Configured plugins are trusted local code with the same permissions as pmds.
The protocol is a compatibility and failure-isolation boundary, not a sandbox.
Only external namespaces load configuration or resolve plugins, so built-ins
remain independent of plugin health.
Consequences
- The request, event, and manifest schemas are a versioned public contract.
- The native CLI owns package resolution, process lifecycle, diagnostics, rendering, and exit codes.
- The
palamedes-plugincrate wraps the plugin side of the protocol. - Plugin packages may ship independently on the normal npm integrity and optional-platform-package path without any JavaScript runtime integration.