# Capability Spec Delta: api-worker-host (api-worker-host) **Package**: `@suaveplan/api-worker-host` **Status**: PROPOSED > Every requirement below carries a `**Verified by:**` line at authoring time. ## ADDED Requirements ### Requirement: Worker hosts exactly one plugin The package SHALL provide a worker-boot script that loads exactly ONE plugin module per Worker thread. The script SHALL read `workerData` for `{ manifest, moduleSpecifier, controlPort, capabilityPorts }`, import the module via `await import(moduleSpecifier)`, validate that the module's default export is a `PluginModule` produced by `definePlugin`, and emit a `WorkerReady` envelope over `controlPort`. Multiple plugins per worker MUST be rejected with `WORKER_BOOT_FAILED`. #### Scenario: trivial plugin module boots and signals ready - **WHEN** the simulator is started with a one-route plugin module - **THEN** the `WorkerReady` envelope is observed on the control port within 100ms #### Scenario: module without `definePlugin` default export rejects - **WHEN** the simulator boots with a module whose default export is a plain object - **THEN** the boot phase throws `WORKER_BOOT_FAILED` AND the worker exits non-zero **Verified by:** `src/worker-boot/worker-boot.test.ts::"trivial plugin module boots and signals ready"` AND `src/worker-boot/worker-boot.test.ts::"non-definePlugin export rejects with WORKER_BOOT_FAILED"` --- ### Requirement: Lifecycle commands dispatch to plugin hooks over the control port The package SHALL receive lifecycle command envelopes (`install`, `start`, `stop`, `uninstall`, `reload`) over the dedicated control port. Each command SHALL dispatch to the corresponding hook on the loaded plugin with a freshly-constructed `PluginContext` whose capability slots are RPC proxies wired in `worker-boot`. Hook outcomes SHALL be reported back: success → `LifecycleAck{ok}`, `SuaveplanError` → `LifecycleAck{error}` with the error reconstructed via `api-worker-rpc`'s error-reconstruction, anything else → wrapped in `WORKER_REMOTE_THROW`. #### Scenario: install hook runs and acks success - **WHEN** the receiver dispatches `install` AND the plugin's `install` hook returns - **THEN** a `LifecycleAck{ok: true}` envelope is sent back #### Scenario: stop hook throw is reconstructed - **WHEN** the plugin's `stop` hook throws `SuaveplanError("PLUGIN.X.Y")` - **THEN** the ack envelope carries an error with `code === "PLUGIN.X.Y"` **Verified by:** `src/lifecycle-receiver/lifecycle-receiver.test.ts::"install hook runs and acks"` AND `src/lifecycle-receiver/lifecycle-receiver.test.ts::"stop hook throw is reconstructed"` --- ### Requirement: Uncaught exceptions surface as WORKER_CRASHED and exit non-zero The package SHALL install `process.on("uncaughtException")` and `process.on("unhandledRejection")` handlers. Each handler SHALL: (a) emit a `WORKER_CRASHED` envelope over the control port carrying the original `name`, `message`, and stack as `details`, (b) flush the port, (c) call `process.exit(1)`. Silent crashes MUST NOT be possible — main-host's `LifecycleDriver` relies on these envelopes to transition plugins to the `crashed` state. #### Scenario: uncaught exception emits envelope before exit - **WHEN** the simulator throws an uncaught exception inside a handler - **THEN** a `WORKER_CRASHED` envelope is observed on the control port AND the worker process exits with code 1 **Verified by:** `src/crash-detector/crash-detector.test.ts::"uncaught exception emits WORKER_CRASHED envelope"` --- ### Requirement: Heap / CPU / wall-clock budgets trigger controlled exit The package SHALL sample `process.memoryUsage().heapUsed` at a configurable interval (default 1s) and reject when it exceeds `host.config.worker.heapLimitBytes` with a `WORKER_HEAP_LIMIT` envelope followed by `process.exit(1)`. The package SHALL track CPU consumption via `performance.now()` deltas across handler invocations and trigger `WORKER_CPU_BUDGET` when the rolling window exceeds the configured budget. The package SHALL enforce per-hook wall-clock budgets — a `start` / `stop` / `install` / `uninstall` hook that exceeds its budget MUST surface `WORKER_WALL_CLOCK_BUDGET` AND the hook's promise MUST reject with `AbortError`. #### Scenario: heap-limit exceeded triggers WORKER_HEAP_LIMIT - **WHEN** the simulator allocates past `heapLimitBytes` - **THEN** a `WORKER_HEAP_LIMIT` envelope is observed AND the worker exits #### Scenario: wall-clock-budget exceeded aborts the hook - **WHEN** a `start` hook configured with `wallClockBudgetMs: 100` runs for 500ms - **THEN** the hook's promise rejects with `AbortError` AND `WORKER_WALL_CLOCK_BUDGET` is sent **Verified by:** `src/budget-guard/budget-guard.test.ts::"heap limit triggers WORKER_HEAP_LIMIT"` AND `src/budget-guard/budget-guard.test.ts::"wall-clock budget aborts hook with AbortError"` --- ### Requirement: Request dispatcher runs handler under plugin AbortSignal The package SHALL accept marshalled request envelopes (the validated `body` / `params` / `query` per design doc §6, NOT the raw `Request`). It SHALL construct a `RequestContext` with the plugin's capability proxies + a fresh per-request `AbortSignal`, run the route handler, and serialise the response back. Main-side cancellation of the request MUST propagate through `api-worker-rpc`'s abort wiring and fire the request's `AbortSignal`. #### Scenario: handler runs with validated body in context - **WHEN** a dispatcher receives an envelope with `body: { name: "x" }` - **THEN** the handler observes `ctx.request.body.name === "x"` AND a response envelope is sent back #### Scenario: main-side cancel fires handler abort signal - **WHEN** main sends an `RpcAbort` mid-dispatch - **THEN** the handler's `ctx.abortSignal.aborted` becomes `true` **Verified by:** `src/request-dispatcher/request-dispatcher.test.ts::"handler runs with validated body in context"` AND `src/request-dispatcher/request-dispatcher.test.ts::"main-side cancel fires AbortSignal"` --- ### Requirement: Plugin code cannot distinguish trusted from isolated tier The package SHALL construct a `PluginContext` whose surface is structurally identical to the one a `trusted`-tier plugin receives directly from `api-host`. The capability slots SHALL be RPC proxies but their method names, return-type wrapping (always `Promise`), and field set MUST match exactly. A plugin author SHALL be able to write `definePlugin({ ... })` once and have it run under any tier without code change. #### Scenario: structurally-identical PluginContext - **WHEN** the same plugin module is run under the simulator AND under a direct in-process construction - **THEN** `Object.keys(ctx)` is identical AND every capability slot has the same method names **Verified by:** `tests/integration/real-worker.test.ts::"plugin context surface matches direct construction"`