Closes the four gaps the security review surfaced:
- OS permission model on the prod plugin child (Node --permission with
fs-read scoped to the compiled server dir + the plugin's own code dir, no
fs-write/child_process/worker/native). A plugin can no longer read trek.db
or the .jwt_secret/.encryption_key files, nor shell out — the direct-fs and
RCE escapes that bypassed the RPC layer. Opt-out via TREK_PLUGIN_PERMISSIONS=off.
- Egress guard extended from fetch to the net.Socket connect choke point, so
node:http/https/net/tls obey the declared-egress allowlist too (no declared
egress = no outbound). Under the permission model there is no clean escape to
an unwrapped runtime. Kernel/network-namespace containment remains the
container step.
- Trip reads are membership-checked against the acting user the HOST binds from
the authenticated invocation, not an asUserId the plugin supplies; a job/onLoad
(no user) can't read user-scoped trips.
- Optional minisign (Ed25519) author signatures verified offline, TOFU-pinned
(migration 158). Unsigned plugins install on sha256 alone; a signed plugin
can't silently drop its signature or swap its author key.
Server suite green (permission-model activation verified against the Koffi
reference plugin on dev1).
Every plugin now runs in its own forked child process with a scrubbed env
(no JWT_SECRET, no db path, nothing inherited). It talks to TREK only over a
JSON-RPC channel, and the host's capability router registers ONLY the methods a
plugin's granted permissions unlock — so an ungranted call is unreachable, not
merely refused. The plugin's own data lives in a separate sqlite file it can
never open directly; core reads (trips/users) go through membership-checked,
column-projected host methods; ws broadcasts are force-namespaced.
- protocol/envelope: the wire types + method→permission map (pure, shared by
host and the isolated child)
- host/rpc-host: the capability router = the enforcement point (dispatch,
BAD_PARAMS / PERMISSION_DENIED / RESOURCE_FORBIDDEN / UNKNOWN_METHOD)
- host/plugin-data: the per-plugin sqlite file (db:own), guarded against
ATTACH/PRAGMA escape, idempotent migrations
- host/create-rpc-host: wires the router to the real db/websocket (host-only)
- runtime/plugin-sdk + plugin-host-entry: the child bootstrap + definePlugin
ctx; turns each ctx call into an RPC, never imports a privileged module
- supervisor: spawn on activate, heartbeat/reap, crash backoff + auto-disable,
graceful shutdown — a plugin crash/OOM/hang can never reach the Nest loop
- paths: code/data layout, dist-vs-tsx child entry resolution
Nothing is wired into activation yet (that's the next slice); exercised by unit
tests for the router/sdk/data and an integration test that forks a real child.