Initial Commit
This commit is contained in:
59
openspec/archive/2026-05-25-api-worker-host/proposal.md
Normal file
59
openspec/archive/2026-05-25-api-worker-host/proposal.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# `@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.
|
||||
Reference in New Issue
Block a user