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

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 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"