6.8 KiB
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
WorkerReadyenvelope 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_FAILEDAND 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
installAND the plugin'sinstallhook returns - THEN a
LifecycleAck{ok: true}envelope is sent back
Scenario: stop hook throw is reconstructed
- WHEN the plugin's
stophook throwsSuaveplanError("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_CRASHEDenvelope 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_LIMITenvelope is observed AND the worker exits
Scenario: wall-clock-budget exceeded aborts the hook
- WHEN a
starthook configured withwallClockBudgetMs: 100runs for 500ms - THEN the hook's promise rejects with
AbortErrorANDWORKER_WALL_CLOCK_BUDGETis 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
RpcAbortmid-dispatch - THEN the handler's
ctx.abortSignal.abortedbecomestrue
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"