Skip to content
Rymflux Documentation
Esc
navigateopen⌘Jpreview
On this page

ADR-001: Plugin IPC — JSON-RPC over stdin/stdout

ADR-001: Plugin IPC — JSON-RPC over stdin/stdout

Status

Accepted

Context

Plugins are external executables (constraint C1). The platform must communicate with them over a language-agnostic IPC protocol (constraint C2). Three common IPC mechanisms exist:

  • stdin/stdout: The core launches the plugin and sends/receives JSON lines over standard streams. Used by LSP, Git, and countless CLI tools. Simple, no port management, no socket files.
  • Unix/TCP socket: Plugin opens a local socket; core connects. Used by Herdr. Requires socket path coordination, port allocation, and cleanup on crash.
  • HTTP localhost: Plugin runs a tiny HTTP server on a random port. Used by many plugin systems. Requires HTTP library in every plugin, port conflict resolution, and TTL management.

The communication is request-response (JSON-RPC): core sends a request with an id, plugin replies with the matching response. No bidirectional streaming in v1 — streaming is URL-based (see ADR-006).

Decision

Use JSON-RPC 2.0 over the plugin’s stdin/stdout. The core writes a JSON-RPC request to the plugin’s stdin as a single line terminated by \n. The plugin writes the JSON-RPC response to its stdout as a single line terminated by \n.

  • Stderr is reserved for plugin logging/diagnostics. The core captures stderr and surfaces it through the diagnostics system (M16).
  • Each message is exactly one JSON object on one line. No length-prefixed framing — the \n delimiter is sufficient because JSON-RPC messages do not contain raw newlines inside a compact-serialized JSON object.
  • The plugin must flush stdout after each response.
  • The request includes an id field (integer). The response echoes it. The core uses id to match responses to in-flight requests.

Request format:

{"jsonrpc":"2.0","id":1,"method":"search","params":{"query":"Imagine Dragons"}}

Response format:

{"jsonrpc":"2.0","id":1,"result":{"tracks":[...]}}

Error format:

{"jsonrpc":"2.0","id":1,"error":{"code":-1,"message":"PLUGIN_CRASHED"}}

Alternatives considered

  • Unix domain socket: More complex lifecycle — socket path must be chosen, created before plugin launch, cleaned up after crash. Adds platform differences (Windows named pipes vs Unix sockets). Benefit (independent processes can reconnect) is irrelevant for v1’s launch-per-request-until-idle model.
  • HTTP localhost: Every plugin needs an HTTP server library. Port conflicts must be handled (try-bind-loop). The core must poll or block on HTTP connections. stdin/stdout requires zero networking code on either side.
  • Socketpair: Tauri could create a socketpair and hand one end to the child process. More secure (no filesystem path) but adds platform-specific syscall complexity with no benefit over stdin/stdout for line-delimited JSON.

Consequences

Positive:

  • Zero networking code in plugins. Every language can read stdin and write stdout.
  • Stdout buffering issues are well-understood (plugin must flush).
  • Stderr is naturally separated from the protocol — plugin authors can eprintln! / print(..., file=sys.stderr) without protocol corruption.
  • The LSP ecosystem proves this model scales to hundreds of language servers.

Negative:

  • stdin/stdout is exclusive to one plugin process. You cannot have two cores talking to one plugin instance. Not a v1 concern.
  • Binary data (pipe mode) is excluded from the control channel (see ADR-006 — URL-only for v1).
  • Plugin authors must ensure their runtime does not buffer stdout (Python’s print(flush=True), Rust’s std::io::Write::flush).

References

  • Constraint C1, C2 (constraints-assumptions.md)
  • FR-PLUGIN-3, FR-PLUGIN-4 (functional.md)
  • Herdr JSON-RPC discussion (vision/sources-plugin-system/herdr-plugin-discuss.md)

Was this page helpful?