ADR-017: Host Explicit Binary CLI Plugins In Rust
The Rust pmds binary owns extraction, audits, reports, and catalog operations. Third-party workflows need resolved project configuration and semantic catalog discovery, but independently shipped extensions must not link into the CLI or execute implicitly during built-in commands.
- Status: Accepted
- Date: 2026-07-22
- Revised: 2026-08-03
- Issue: #365
Context
The first implementation hosted JavaScript plugins in the npm launcher. That made the launcher a second CLI runtime, required executable JavaScript config, and duplicated configuration and dispatch semantics outside Rust. Palamedes plugins are distributed as standalone executables now, so that boundary no longer needs a JavaScript host.
Decision
Use one native command boundary:
- The Rust
pmdsexecutable owns built-in and plugin command dispatch. - Built-in feature commands (
extract,lint,audit,report,catalog, andversion) execute before configuration or plugin resolution. The current reserved plugin namespace tokens are those commands plus Clap'shelpsubcommand; visible and hidden aliases of any root command are also reserved. - Any other namespace is resolved only from the explicit
pluginslist in a Palamedes data config (yaml,yml,json, ortoml). JavaScript and TypeScript files are not CLI configuration. - Every plugin is a standalone executable that speaks the versioned binary plugin protocol from ADR 018. ESM and CJS plugin modules are not supported.
- Package resolution is deterministic and relative to the config file. Installed packages are never discovered or executed merely because they are present.
- A plugin package declares
palamedes.pluginBinaryinpackage.json. A meta package may instead carry platform-specific optional dependencies whose installed package declarespluginBinaryand matchingos,cpu, andlibcconstraints. - Text output and one JSON envelope share explicit exit-code semantics.
Built-in commands remain available to a trusted plugin through the absolute
pmdspath inPALAMEDES_NATIVE.
The npm package keeps only the platform-selection launcher required to expose a
stable pmds bin from a platform-neutral package. It does not parse commands,
load configuration, or host plugins.
Trust Model
Configured plugins are trusted local executables with the same filesystem,
environment, and network permissions as pmds. The protocol is an API
boundary, not a sandbox. Projects must review and pin plugin packages as they
would build tools. Built-in commands bypass plugin loading, so a missing or
broken plugin cannot affect core workflows.
Alternatives Considered
- JavaScript module plugins: independently deployable, but require a second runtime and executable configuration for behavior already owned by Rust.
- Rust compile-time extensions: strongly typed but require rebuilding the CLI for independently released plugins.
- Dynamic libraries: Rust has no stable ABI and a plugin fault would take down the host process.
- WASI/WASM sandbox: a stronger isolation boundary than the trust model requires, with additional capability wiring for filesystem and network use.
- External wrapper CLIs: duplicate config discovery, catalogs, output envelopes, diagnostics, and version negotiation.
Consequences
pmdshas one parser and one dispatch table, both in Rust.- Plugins can be written in any language that produces an executable speaking the protocol; the supported SDK is Rust-first.
- CLI plugin configuration is portable data and cannot execute code while loading.
- The previous
@palamedes/cli/pluginJavaScript API and JS/TS CLI configs are removed rather than maintained as a parallel compatibility surface. - The npm launcher remains a packaging bridge until package managers expose a transitive optional dependency's bin as the installed package's public bin. ADR-024 records the launcher distribution decision and its rejected alternatives.