5.2 KiB
@suaveplan/api-worker-host — worker-side runtime hosting one plugin
Package: @suaveplan/api-worker-host
Status: PROPOSED
Type: new-package
Stage: 3 (Worker isolation infrastructure)
Why
isolated and sandboxed plugins run in their own Worker thread (design doc §2.2). Something inside that worker has to: load the plugin module, build a PluginContext whose capability slots are RPC proxies back to the main host, drive the plugin's own lifecycle hooks, and surface crashes / OOM / wall-clock-budget exhaustion back to the main host's LifecycleDriver. api-worker-host is that something. It's the worker-side counterpart to api-host: where api-host runs in the main process and orchestrates plugins, api-worker-host runs inside each worker and hosts exactly one plugin.
Splitting this out from api-host keeps the main-process bundle free of worker-only code and lets us evolve worker-side concerns (heap limits, AbortController plumbing, crash telemetry) independently.
What changes
Net-new package. Server-tier (worker_threads API). Single binary entry-point: a Worker boot script that the main host's loader spawns with new Worker(...).
ADDED exports:
./worker-boot— script entry; readsworkerData(manifest, plugin module specifier, capability port table); imports the plugin module viaawait import(specifier); wires every capability slot from itsMessagePortviaapi-worker-rpc'screateRpcCaller; constructsPluginContext+RequestContext; signals readiness back to main../lifecycle-receiver— receives lifecycle commands (install,start,stop,uninstall,reload) over a dedicated control port; dispatches to the plugin's hooks; propagates outcomes (success /SuaveplanError/ unhandled throw) back to main../crash-detector—process.on("uncaughtException")/process.on("unhandledRejection")handlers; converts to a finalWORKER_CRASHEDenvelope sent over the control port; callsprocess.exit(1)so the main host sees the worker die../budget-guard— periodic check ofprocess.memoryUsage().heapUsedagainsthost.config.worker.heapLimitBytes; CPU sampling viaperformance.now()deltas; wall-clock-budget enforcement onstart/stop/install/uninstallhooks. Exceeding any budget triggers a controlledWORKER_BUDGET_EXCEEDEDexit../request-dispatcher— receives marshalled requests fromapi-router-worker-bridge; constructs a per-requestRequestContext; runs the handler under the plugin'sAbortSignal; serialises the response back../error-codes—WORKER_*codes (WORKER_CRASHED,WORKER_BUDGET_EXCEEDED,WORKER_HEAP_LIMIT,WORKER_CPU_BUDGET,WORKER_WALL_CLOCK_BUDGET,WORKER_BOOT_FAILED,WORKER_MODULE_IMPORT_FAILED)../testing— in-process worker simulator (usesMessageChannelinstead of a realWorker) so conformance suites run without spawning real OS threads.
Out of scope
- Sandbox isolation primitives —
vm.Contextguards and WASI permission gates live inapi-sandbox-vm/api-sandbox-wasm(Stage 8). This package hosts the plugin module; the sandbox wraps the host. - Capability marshalling protocol — that's
api-worker-rpc. This package CONSUMES it. - Main-host spawning —
new Worker(...)and worker pool management live inapi-host'sLifecycleDriver. This package only provides the script that runs INSIDE the worker. - HTTP request marshalling —
api-router-worker-bridgeowns the request/response boundary; this package only receives the already-marshalled handler invocation.
Source mapping
Genesis-original. Design doc sections: §2.2 (isolation tiers), §5.2a (worker-RPC variants), §4 (crashed state propagation), §8.5 (forceTimeoutMs on stragglers).
| Requirement | Design doc section |
|---|---|
| Worker hosts exactly one plugin | §2.2 |
Plugin author writes the same definePlugin regardless of tier |
§2.2 |
Crash detection propagates to main → crashed state |
§2.2, §4 |
Auto-restart crashRestartMax × triggered by main |
§2.2 |
| Wall-clock / heap / CPU budgets | §2.2 ("CPU/mem budgets") |
Capability slots are RPC proxies via MessagePort |
§5.2a |
Verification approach
src/worker-boot/worker-boot.test.ts— boots the worker simulator with a trivial plugin module; readiness signal observed; capability slots present.src/lifecycle-receiver/lifecycle-receiver.test.ts— every command dispatches to the right hook; throws propagate asSuaveplanError.src/crash-detector/crash-detector.test.ts—uncaughtExceptionbecomesWORKER_CRASHEDenvelope; non-zero exit.src/budget-guard/budget-guard.test.ts— heap-limit triggersWORKER_HEAP_LIMIT; wall-clock exceeded onstarttriggersWORKER_WALL_CLOCK_BUDGET.src/request-dispatcher/request-dispatcher.test.ts— request envelope decoded → handler runs → response encoded; abort signal fires on main-side cancel.tests/integration/real-worker.test.ts— spawns a realWorker(gatedINTEGRATION=1); end-to-end boot + lifecycle + crash propagation (timeout 30000).
Migration impact
None — additive only. api-host will depend on this for spawning workers in Stage 4 wiring.