Files
suaveplan/openspec/archive/2026-05-25-api-worker-host/proposal.md
2026-05-27 23:29:53 +01:00

5.2 KiB
Raw Blame History

@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; reads workerData (manifest, plugin module specifier, capability port table); imports the plugin module via await import(specifier); wires every capability slot from its MessagePort via api-worker-rpc's createRpcCaller; constructs PluginContext + 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 final WORKER_CRASHED envelope sent over the control port; calls process.exit(1) so the main host sees the worker die.
  • ./budget-guard — periodic check of process.memoryUsage().heapUsed against host.config.worker.heapLimitBytes; CPU sampling via performance.now() deltas; wall-clock-budget enforcement on start/stop/install/uninstall hooks. Exceeding any budget triggers a controlled WORKER_BUDGET_EXCEEDED exit.
  • ./request-dispatcher — receives marshalled requests from api-router-worker-bridge; constructs a per-request RequestContext; runs the handler under the plugin's AbortSignal; 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 (uses MessageChannel instead of a real Worker) so conformance suites run without spawning real OS threads.

Out of scope

  • Sandbox isolation primitives — vm.Context guards and WASI permission gates live in api-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 in api-host's LifecycleDriver. This package only provides the script that runs INSIDE the worker.
  • HTTP request marshalling — api-router-worker-bridge owns 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 as SuaveplanError.
  • src/crash-detector/crash-detector.test.ts — uncaughtException becomes WORKER_CRASHED envelope; non-zero exit.
  • src/budget-guard/budget-guard.test.ts — heap-limit triggers WORKER_HEAP_LIMIT; wall-clock exceeded on start triggers WORKER_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 real Worker (gated INTEGRATION=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.