Files
2026-05-27 23:29:53 +01:00

60 lines
5.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# `@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.