Skip to content

`pmds` Binary Plugin Protocol

Binary plugins are standalone executables that extend pmds with namespaced commands. The native Rust CLI spawns them and exchanges newline-delimited JSON over stdio. The protocol is the only CLI plugin boundary; JavaScript modules and executable JavaScript/TypeScript configs are not supported.

pmds Binary Plugin Protocol

Configure A Plugin

Configuration uses a package or config-relative executable, optionally paired with JSON-compatible options:

plugins:
  - ["@acme/palamedes-plus", { license: "…" }]

A direct plugin package declares its executable relative to package.json:

{
  "name": "@acme/palamedes-plus-darwin-arm64",
  "os": ["darwin"],
  "cpu": ["arm64"],
  "palamedes": { "pluginBinary": "./bin/palamedes-plus" }
}

A platform-neutral meta package may list native packages in optionalDependencies. The Rust host follows the installed dependency whose os, cpu, and libc fields match the running native CLI and whose manifest declares palamedes.pluginBinary.

Resolution starts next to the Palamedes data config and walks parent node_modules directories. pnpm symlinks are supported. A local package directory or a direct native executable path can be used for development. Nothing is discovered via PATH, and merely installing a package never causes it to run.

A configured plugin that fails to resolve or describe blocks only its own namespace. Commands of other configured plugins still run and report the skipped plugin as a PLUGIN_UNAVAILABLE warning; the failure is fatal only when the requested namespace cannot be served.

After a successful describe, the host caches the validated manifest in .palamedes/plugin-manifests.json. Later plugin invocations reuse manifests whose canonical executable path and fingerprint still match, while continuing to validate every configured namespace and collision. On Unix, an unchanged device, inode, byte length, modification time, and nanosecond change time let the host reuse the cached SHA-256 digest without reading the binary again. Any metadata change triggers a fresh digest; platforms without that complete Unix identity hash the binary on every lookup. A missing inode or a zero nanosecond change-time component is treated conservatively as incomplete metadata and also keeps full hashing enabled. Changing a binary, the host version, or the protocol version forces another describe, including replacements that preserve file size and modification time. The cache is only a startup optimization: missing, old, corrupt, or unwritable cache data falls back to the normal handshake without blocking a plugin command. Pass --refresh-plugin-manifests to a plugin invocation to ignore the cached manifests once, describe every configured plugin again, and replace the cache with the validated results. Removing .palamedes/plugin-manifests.json remains an equivalent manual recovery path.

The host consumes its own invocation flags before it forwards args to the plugin: --json, --config PATH, -c PATH, --config=PATH, --plugin-timeout-ms N, and --refresh-plugin-manifests never appear there. A bare -- is likewise a host passthrough marker rather than a plugin argument; every token after it is forwarded verbatim, including flag-shaped ones.

Protocol

Protocol version 1 must match exactly. It is versioned independently of the CLI and plugin package versions.

The host spawns the executable once per request and writes one JSON object to stdin before closing it. describe asks for the plugin manifest:

{
  "palamedesBinaryPluginProtocol": 1,
  "hostVersion": "<host pmds version>",
  "kind": "describe"
}

The plugin answers with exactly one manifest event:

{
  "event": "manifest",
  "name": "acme",
  "protocolVersion": 1,
  "commands": {
    "inspect": { "description": "Inspect configured catalogs." }
  }
}

Namespaces and commands must be lowercase kebab-case. The six built-in feature commands are extract, lint, audit, report, catalog, and version. Plugin namespaces cannot use those tokens, Clap's help subcommand, or any visible or hidden alias of a root command; they also cannot collide with another configured plugin.

A run request carries the invocation and resolved project context:

{
  "palamedesBinaryPluginProtocol": 1,
  "hostVersion": "<host pmds version>",
  "kind": "run",
  "command": "inspect",
  "args": ["one", "two"],
  "options": { "license": "…" },
  "json": false,
  "interactive": false,
  "config": { "rootDir": "/project" },
  "catalogs": [
    {
      "path": "locales/{locale}/messages",
      "format": "po",
      "include": ["src"],
      "exclude": [],
      "locales": [{ "locale": "en", "path": "/project/locales/en/messages.po" }]
    }
  ]
}

catalogs[].path is the configured {locale} pattern; each catalogs[].locales[].path is the absolute file the extraction pass writes, storage extension included.

The plugin emits zero or more diagnostic and output events followed by at most one result event:

{ "event": "diagnostic", "severity": "info", "code": "ACME_INSPECTED", "message": "Inspected 2 catalogs." }
{ "event": "output", "text": "inspecting" }
{ "event": "result", "text": "done", "data": { "count": 2 }, "exitCode": 0 }
  • stdout is reserved for protocol events; free-form progress belongs on stderr, which passes through to the terminal.
  • In text mode the host renders output events as they arrive; result text, data, and diagnostics follow after the plugin exits.
  • Diagnostics use info, warning, or error severity and may include code and structured details.
  • --json folds the result and diagnostics into one host envelope.
  • A result exit code is authoritative. Without a result, the process exit code is the fallback and the host emits PLUGIN_BINARY_PROTOCOL.
  • Exit codes range from 0 through 255. Terminal cancellation keeps the usual 130 (SIGINT) and 143 (SIGTERM) meanings. On Unix the host isolates the plugin process group and forwards SIGINT and SIGTERM to that group, so direct and terminal signals reach the plugin tree exactly once.

Time And Output Limits

Every describe request has a 5,000 ms deadline. A normal run request has no deadline so that intentionally long-running commands continue to work; callers can opt into one with --plugin-timeout-ms N, where N is a positive integer. The deadline covers the complete request, through process exit and stdout EOF.

The host applies these fixed stdout protocol budgets to both request kinds:

  • 1,048,576 bytes per line, including its terminating newline;
  • 16,777,216 bytes in total;
  • 10,000 non-empty protocol events.

In text mode, an output event is rendered and discarded immediately instead of also being retained until exit. JSON mode retains output events within the same budgets so it can produce the final envelope. A deadline violation emits PLUGIN_BINARY_TIMEOUT; an output-budget violation emits PLUGIN_BINARY_PROTOCOL_LIMIT. In either case the host terminates the isolated process group on Unix or the child process tree on Windows before returning.

Built-In Commands And Trust

The host sets PALAMEDES_NATIVE to the absolute path of the running pmds executable. Trusted plugins may invoke documented built-in commands through that path. In JSON mode they should capture built-in output so it does not corrupt the plugin protocol on stdout.

Configured plugins are trusted local code with the same filesystem, environment, and network permissions as pmds. The protocol is a compatibility boundary, not a sandbox. Built-ins bypass config and plugin resolution, so a missing or broken plugin cannot affect core commands.

Rust SDK

The palamedes-plugin crate wraps the plugin side: command registration, typed request context, catalogs, diagnostics, results, and protocol I/O. Plugin::run() handles stdin/stdout; Plugin::dispatch() exposes the same path for tests. CommandContext::built_in_command() prepares a pmds subprocess from PALAMEDES_NATIVE. See crates/palamedes-plugin/examples/inspect.rs for a complete plugin.