rpc_plugin_system is a local Go project for supervising executable plugins over Unix domain sockets using Go net/rpc.
The current codebase is a kernel-first substrate that now proves:
- multiple supervised plugin processes under one host
- per-plugin runtime isolation and explicit plugin-id routing
- Unix socket RPC transport
- one-time bootstrap token trust on startup, enforced before non-auth RPC methods are served
- Linux peer credential verification on startup
- heartbeat and health reporting
- timeout handling and poisoned-client teardown
- restart supervision with generation tracking
- verbose first-class append-only kernel event logging
- a small local admin/control CLI
Current stable release: v1.0.0
What v1.0.0 means here:
- the standalone local plugin substrate has reached its frozen v1 scope
- multi-plugin supervision, explicit routing, restart handling, and local operator control are part of the stable contract
- the release is source-only, with compatibility/install/release discipline documented in
docs/compatibility.md,docs/install.md, anddocs/release-promotion-checklist.md
Project context:
v0.1.0was the initial kernel proofv1.0.0is the first stable standalone plugin substrate release- memory-system work should begin only after the plugin substrate reaches v1, which this release now does
- the frozen v1 scope lives in
docs/v1-freeze.md
Main entrypoints:
- daemon:
cmd/rpcplugind/main.go - control CLI:
cmd/rpcpluginctl/main.go - example plugin:
cmd/rpcplugin-echo/main.go - failure-behavior plugin:
cmd/rpcplugin-failure/main.go
Important internals:
internal/kernel- supervisor and monitor loopinternal/adminrpc- local admin socket APIinternal/auth- one-time token bootstrap helpersinternal/runtime- runtime-dir, Unix socket helpers, and Linux-first peercred adapter pathsdk/go/plugin- public Go plugin authoring SDKinternal/testpluginapi- test-only env helpers and compatibility shims
See also:
docs/install.mddocs/compatibility.mddocs/release-promotion-checklist.md
From the project root:
go build ./...Build the main binaries explicitly:
go build -buildvcs=false -o .tmp-bin/rpcplugind ./cmd/rpcplugind
go build -buildvcs=false -o .tmp-bin/rpcpluginctl ./cmd/rpcpluginctl
go build -buildvcs=false -o .tmp-bin/rpcplugin-echo ./cmd/rpcplugin-echo
go build -buildvcs=false -o .tmp-bin/rpcplugin-failure ./cmd/rpcplugin-failureOr use the Makefile:
make buildFor v1, packaging means a clean stable source release and explicit operator guidance, not distro packaging or prebuilts.
Run the full test suite:
go test ./...Run the kernel-focused suite:
go test ./internal/kernel/...Or use:
make testPlugin authors should start from:
import plugin "rpc_plugin_system/sdk/go/plugin"That SDK is the intended public Go authoring surface. It provides:
- bootstrap config loading from env
- one-time token auth handling
- RPC service/method constants
- request/response types
- adapter-based optional capability registration
TemplatePluginas the stable minimal skeletonServe/ServeWithConfighelpers
The supported public authoring recipe is:
LoadConfigFromEnv()NewTemplate(...)- optionally implement capability interfaces like
Echo,Sleep, orCrash ServeWithConfig(...)
For the supported authoring path, start with:
sdk/go/plugin/template.gosdk/go/plugin/example_minimal.gocmd/rpcplugin-echo/main.gofor the minimal template extended with optional capabilities
The intended rule is simple:
- plugin authors should not need to read internal packages to get a basic plugin running
A minimal Go plugin needs to implement only the required core methods:
Version() stringHeartbeat(plugin.Empty, *plugin.HeartbeatResponse) errorShutdown(plugin.Empty, *plugin.Empty) error
Optional capabilities like Echo, Sleep, and Crash are added by implementing the matching optional interfaces.
The stable minimal authoring path is:
cfg, err := plugin.LoadConfigFromEnv()
if err != nil {
panic(err)
}
p := plugin.NewTemplate(cfg, "1.0.0")
if err := plugin.ServeWithConfig(cfg, p); err != nil {
panic(err)
}If you need custom heartbeat data or optional capabilities, copy the shape from sdk/go/plugin/template.go into your own main package and extend it there. The public echo plugin now does exactly that by embedding TemplatePlugin and adding Echo, Sleep, and Crash.
A minimal standalone plugin main.go can look like this:
package main
import plugin "rpc_plugin_system/sdk/go/plugin"
func main() {
cfg, err := plugin.LoadConfigFromEnv()
if err != nil {
panic(err)
}
p := plugin.NewTemplate(cfg, "1.0.0")
if err := plugin.ServeWithConfig(cfg, p); err != nil {
panic(err)
}
}That is the shortest supported public authoring path.
This walkthrough shows the normal happy-path flow using the example echo plugin.
cd /tank/development/rpc_plugin_system
make buildThat produces:
.tmp-bin/rpcplugind.tmp-bin/rpcpluginctl.tmp-bin/rpcplugin-echo.tmp-bin/rpcplugin-failure
A plugin launched by the kernel receives only these required environment variables:
RPC_PLUGIN_SYSTEM_PLUGIN_SOCKETRPC_PLUGIN_SYSTEM_PLUGIN_IDRPC_PLUGIN_SYSTEM_PLUGIN_GENERATIONRPC_PLUGIN_SYSTEM_AUTH_TOKEN_FILE
The daemon environment is not inherited by the plugin. Plugin authors must not depend on arbitrary parent environment variables.
If one is missing or malformed, plugin.LoadConfigFromEnv() now fails with an explicit author-facing startup error naming the missing variable.
Examples:
- missing socket env ->
RPC_PLUGIN_SYSTEM_PLUGIN_SOCKET is required for plugin startup - bad generation env -> parse error naming
RPC_PLUGIN_SYSTEM_PLUGIN_GENERATION - unreadable auth token file -> read error naming
RPC_PLUGIN_SYSTEM_AUTH_TOKEN_FILE
In one terminal:
cd /tank/development/rpc_plugin_system
.tmp-bin/rpcplugind \
-runtime-dir /tmp/rpc_plugin_system-demo \
-plugin $(pwd)/.tmp-bin/rpcplugin-echo \
-plugin-id echoWhat this does:
- creates a runtime directory under
/tmp/rpc_plugin_system-demo - creates a one-time bootstrap token for this generation
- launches the plugin executable
- verifies Linux peer credentials for the connected Unix socket when supported
- authenticates the plugin through the one-time token bootstrap before non-auth plugin RPC methods are trusted
- opens the admin socket for local control
- starts the monitor loop
Trust-boundary note:
- plugin RPC trust is established by the kernel's bootstrap-authenticated connection plus Linux peer credential verification on supported Linux hosts
- the plugin socket is a local Unix socket artifact, not a general remote API surface
- the admin socket is trusted through local runtime-dir filesystem access and socket permissions, not through a separate admin auth layer
- if runtime directory ownership or socket permissions are wrong, local control assumptions are wrong too
If startup succeeds, the daemon stays running in the foreground.
Open another terminal and run:
cd /tank/development/rpc_plugin_system
.tmp-bin/rpcpluginctl -runtime-dir /tmp/rpc_plugin_system-demo statusYou should get JSON showing fields like:
- plugin id
- generation id
- health
- socket path
- pid
Healthy output means:
- the plugin started
- auth succeeded
- capabilities were read
- the kernel currently trusts that generation
From the second terminal:
cd /tank/development/rpc_plugin_system
.tmp-bin/rpcpluginctl -runtime-dir /tmp/rpc_plugin_system-demo restartThat should:
- stop the current plugin process
- start a new process
- create a new generation
- perform fresh bootstrap auth
- return updated state
The important thing to verify is:
- the
generation_idincreases - the plugin becomes healthy again
The frozen v1 control surface includes direct plugin-id targeting for inspection and a deliberately small routed-call set. Even in single-plugin mode, the control CLI exposes that narrow explicit surface.
Examples:
cd /tank/development/rpc_plugin_system
.tmp-bin/rpcpluginctl -runtime-dir /tmp/rpc_plugin_system-demo plugin -plugin-id echo
.tmp-bin/rpcpluginctl -runtime-dir /tmp/rpc_plugin_system-demo capabilities
.tmp-bin/rpcpluginctl -runtime-dir /tmp/rpc_plugin_system-demo routes
.tmp-bin/rpcpluginctl -runtime-dir /tmp/rpc_plugin_system-demo heartbeat -plugin-id echo
.tmp-bin/rpcpluginctl -runtime-dir /tmp/rpc_plugin_system-demo echo -plugin-id echo -message helloCurrent routing note:
- direct routing by plugin id exists now through a small routed-call layer for targeted operations such as
Heartbeat,Echo, and restart - the host resolves direct targets explicitly before routed calls execute
routesshows the current explicit direct-routing table- current routes are explicitly marked as
direct-plugin-id - the current intent is to freeze this small explicit routed-call set for v1 unless a real missing operation appears
- broader capability-based routing is still later work
While the daemon is running, inspect the runtime directory:
ls -la /tmp/rpc_plugin_system-demo
ls -la /tmp/rpc_plugin_system-demo/echoYou should typically see artifacts like:
/tmp/rpc_plugin_system-demo/admin.sock/tmp/rpc_plugin_system-demo/echo/echo.sock/tmp/rpc_plugin_system-demo/echo/events.jsonl/tmp/rpc_plugin_system-demo/echo/plugin-events.jsonl
The kernel event log is the canonical operator log for v0.1.0. It is append-only JSONL with verbose lifecycle, auth, RPC, restart, and cleanup events.
SDK-backed plugins now write their own append-only JSONL log as plugin-events.jsonl so plugin-side events do not contend with kernel-managed events.jsonl rotation and writes.
In single-plugin host mode, rpcpluginctl logs will automatically fall back to the sole kernel plugin log when there is exactly one plugin log under the runtime directory. In multi-plugin host mode, pass -plugin-id explicitly.
For v1, plugin-side plugin-events.jsonl remains an intentional shell-first inspection surface rather than a first-class CLI surface.
The one-time auth token file is bootstrap-only and should be removed after successful auth.
Go back to the terminal running rpcplugind and press Ctrl+C.
After shutdown, you can check whether runtime artifacts were cleaned up:
ls -la /tmp/rpc_plugin_system-demoThe failure plugin exists so the kernel can be tested against controlled bad behavior.
cd /tank/development/rpc_plugin_system
RPC_PLUGIN_SYSTEM_PLUGIN_BEHAVIOR_FAIL_AUTH=true \
.tmp-bin/rpcplugind \
-runtime-dir /tmp/rpc_plugin_system-failure \
-plugin ./.tmp-bin/rpcplugin-failure \
-plugin-id failureExpected result:
- daemon startup should fail
- the kernel should reject the plugin during bootstrap
This path is mainly exercised in tests, but the behavior is controlled through env vars such as:
RPC_PLUGIN_SYSTEM_PLUGIN_BEHAVIOR_CLOSE_ON_ECHORPC_PLUGIN_SYSTEM_PLUGIN_BEHAVIOR_CRASH_ON_ECHORPC_PLUGIN_SYSTEM_PLUGIN_BEHAVIOR_HEARTBEAT_STATUSRPC_PLUGIN_SYSTEM_PLUGIN_BEHAVIOR_HEARTBEAT_ERRORSRPC_PLUGIN_SYSTEM_PLUGIN_BEHAVIOR_SLEEP_SCALERPC_PLUGIN_SYSTEM_PLUGIN_BEHAVIOR_CLOSE_ON_ACCEPT
See:
cmd/rpcplugin-failure/main.gointernal/testpluginapi/env.gointernal/kernel/failure_suite_test.go
The supervisor passes runtime information to plugins through environment variables, including:
RPC_PLUGIN_SYSTEM_PLUGIN_SOCKETRPC_PLUGIN_SYSTEM_PLUGIN_IDRPC_PLUGIN_SYSTEM_PLUGIN_GENERATIONRPC_PLUGIN_SYSTEM_AUTH_TOKEN_FILE
The auth token file is one-time-use per generation and is removed after successful bootstrap.
The repo includes a configurable failure plugin used by the kernel tests:
cmd/rpcplugin-failure
Behavior is driven by environment variables and exercised through:
internal/kernel/failure_suite_test.go
That suite currently covers startup/auth/identity/generation/capabilities/heartbeat/timeout/crash/transport/restart-churn behavior.
v0.1.0 logging is intentionally first-class.
Current logging design:
- canonical log format is append-only JSONL
- kernel and SDK-backed plugins share the same event schema and severity model
- kernel-managed logs are written as per-plugin
events.jsonlfiles under plugin runtime subdirectories - SDK-backed plugin logs are written as sibling
plugin-events.jsonlfiles under the same runtime subdirectories rpcpluginctl logscan target a specific kernel log with-plugin-id, and in single-plugin host mode it falls back to the only kernel plugin log automatically- plugin-side
plugin-events.jsonlremains an intentional shell-inspection surface for v1 rather than a first-class CLI surface - every write is flushed with
fsyncso logs are durable and human-inspectable during failures, but that durability is an intentional write-cost tradeoff - entries are verbose and include level, component, event, plugin id, generation id, pid, socket path, method, message, error/reason, and optional details
The log is designed to be both:
- machine-parseable for later tooling
- human-readable enough to inspect directly with normal shell tools
The kernel logs at least these categories:
- manager initialization
- plugin start request/success/failure
- socket dial request/success/failure
- auth start/success/failure
- capability load start/success/failure
- heartbeat unhealthy/failure transitions
- RPC start/success/failure/timeout
- RPC poison events
- restart request/success/failure
- shutdown request/success/failure
- forced kill
- runtime cleanup success/failure
SDK-backed plugins log at least these categories:
- boot/config load
- listener start/stop
- auth attempt/accept/reject
- capability reporting
- RPC handler success/failure
- shutdown handling
The control CLI now supports log inspection:
.tmp-bin/rpcpluginctl -runtime-dir /tmp/rpc_plugin_system-demo logs
.tmp-bin/rpcpluginctl -runtime-dir /tmp/rpc_plugin_system-demo -component rpc -level warn logs
.tmp-bin/rpcpluginctl -runtime-dir /tmp/rpc_plugin_system-demo -event plugin_auth_failed -format json logs
.tmp-bin/rpcpluginctl -runtime-dir /tmp/rpc_plugin_system-demo -since 15m -summary logsLogging final-form notes for v0.1.0:
- event logs rotate automatically when they grow too large
- rotated backups are retained and included in CLI log reads
- CLI supports text/json output, filtering, recency windows, reverse order, and summary mode
v1.0.0 is a stable local substrate release, not an everything-platform release.
Current constraints include:
- local Unix-socket operation only
- Go
net/rpctransport only - plugin auth/bootstrap UX is still developer-oriented
- admin/control trust is still local-filesystem based rather than backed by a separate admin auth layer
- plugin ids are intentionally restricted to path-safe names using only letters, digits, dot, underscore, and dash
- plugin-side logs are intentionally shell-first in
v1.0.0rather than fully integrated into the CLI - naming rough edges remain, but they were judged non-blocking for the stable local v1 scope
With v1.0.0 shipped, the next work can move beyond proving the standalone substrate.
Near-term follow-up areas include:
- post-v1 hardening and stress expansion
- broader transport/runtime adapter growth where justified
- continued SDK/plugin authoring polish
- operator-surface refinement where it improves the local substrate without breaking the stable contract
- memory-system implementation on top of the now-stable plugin substrate
docs/v0.1.0.mddocs/plugin-api.mddocs/plugin-abi.mddocs/compatibility.mddocs/plugin-standard-v0.md
The kernel slice is real, tested, and being hardened through failure-point and stress-oriented validation before the standard is considered fully earned.
Current honest status:
- the core substrate is credible
- the local hardening story is real for the narrow Linux-first scope
- the public docs and operator story are much closer to the code than they were before
- the remaining work is mostly release-discipline polish, trust-boundary clarity, and final coherence review rather than core-mechanism rescue
- plugin executable paths must be absolute
- plugin executable paths must not be symlinks
- plugin executable paths must point to regular executable files
- group/world-writable plugin executables are rejected
- runtime directories are private trust boundaries and must not be symlinks