# `@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.