Files
suaveplan/openspec/archive/2026-05-25-api-host/proposal.md
2026-05-27 23:29:53 +01:00

83 lines
8.1 KiB
Markdown
Raw 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-host` — host orchestration core for the API plugin foundation
**Package**: `@suaveplan/api-host`
**Status**: PROPOSED
**Type**: new-package
**Stage**: 2 (Host orchestration core) — see umbrella `openspec/changes/api-plugin-foundation/`
## Why
Stage 1 ships the contracts (`api-types`, `api-manifest`, abstract router/auth/db/etc.). Nothing in the foundation is exercised until a host process actually loads a plugin, drives it through `install → start → stop`, injects the declared capabilities into its `PluginContext`, mounts its routes under the configured router adapter, runs its requests through the per-request pipeline (§6), and emits the OTel surface (§12). `api-host` is the package that does all of that.
This is the keystone of the foundation. Every Stage 3+ change either consumes `api-host` directly (worker proxies talk to it) or extends it (adapters register with its `CapabilityRegistry`). Specs in this change set bind the contract that every later stage rests on — getting the surface right here is what lets us add isolation tiers, conformance suites, sagas, and engines without rewriting the core.
Per design doc §4, §5, §6, §8, §12: lifecycle FSM, capability injection, per-request pipeline, hot reload, and observability all live in this package.
## What changes
Net-new package. Public surface is large but cleanly partitioned. Subpath exports per concern so consumers tree-shake to what they need; `./testing` subpath is **reserved** here and populated in Stage 5.
**ADDED exports:**
- `./define-plugin` — `definePlugin<Caps>(spec)` author-facing API; generic over manifest's `requiredCapabilities`; returns a `PluginModule<Caps>` discriminated by `isolation` tier.
- `./capability-registry` — `CapabilityRegistry`: register, lookup, version-compat check against `apiHost` + `requiredCapabilities`; emits `CAPABILITY_VERSION_MISMATCH` / `CAPABILITY_NOT_REGISTERED` / `CAPABILITY_DUPLICATE_REGISTRATION`.
- `./lifecycle` — `LifecycleDriver`: per-plugin FSM mutex; transitions named in §4 ledger; `start_failed` auto-rollback hand-off to `RollbackJournal`; `crashed`/auto-restart for worker-tier plugins; emits `plugin.lifecycle_transitions_total`.
- `./hot-reload` — `ReloadDriver`: blue-green primitive (§8.1), `RollbackJournal` impl (§8.1a), drain timer, force-cancel, file-watch + admin-API triggers, native-binding guard.
- `./plugin-ledger` — host-owned metadata DB. Migrations create tables `suaveplan_plugin_ledger`, `suaveplan_rollback_journal`, `suaveplan_saga_instances`, `suaveplan_saga_steps`, `suaveplan_2pc_transactions`. (Saga/2PC tables are reserved — populated by `api-saga`/`api-2pc` in Stage 6; their schema ships here because the host owns its metadata schema lifecycle.) Migration runner is forward-only and idempotent.
- `./admin-api` — Hono sub-router mounted under `adminPrefix` (default `/admin`, `null` to disable). Endpoints from §8.4. RBAC scope `host:admin`; per-principal rate-limit (60/min, 1/min for destructive); audit envelope per call.
- `./error-boundary` — wraps every pipeline step (§6 steps 1–11); maps `SuaveplanError → manifest statusMap`, `AbortError → 499`, anything else → 500 + cause-chain log; serializes the `ErrorEnvelope` from `api-types/errors`.
- `./request-pipeline` — orders steps 1–11 from §6 exactly; cookie-auth → CSRF; streaming-route bypass; response-validation strictness `enforce` (prod) / `warn` (dev); emits the OTel span tree from §12.3.
- `./otel` — `HostInstruments` registry: every counter/histogram in §12.3; OTel resource builder pre-tagged with `host.version`/`host.id`; per-plugin meter created at install, disposed at uninstall.
- `./host` — top-level `createHost(config)` entry point composing every internal driver; `host.start()` / `host.stop()` / `host.fetch(...)` (test handle).
- `./error-codes` — every `HOST_*` / `PLUGIN_*` / `RELOAD_*` / `CAPABILITY_*` / `ADMIN_*` code defined via `@suaveplan/error`'s `defineCodes`. Status mapping per code.
- `./testing` — **reserved subpath**. Populated in Stage 5 with `createTestHost` + in-memory adapter wiring. Stage 2 ships an empty placeholder typed export so consumers can declare a peer-dep on the path.
- `.` (root barrel) — namespace re-exports per CLAUDE.md rule 10.
## Out of scope
- **Concrete capability adapters** — `api-router-hono`, `api-auth-jwt`, `api-db-adapter`, etc. all live in Stage 4. The host accepts adapter instances via `CapabilityRegistry.register(...)` — it does not bundle any.
- **Worker isolation runtime** — `api-worker-host`, `api-worker-rpc`, and the worker-proxy packages all live in Stage 3. The host's lifecycle driver KNOWS about the `isolated`/`sandboxed` tiers (it computes the right capability surface per plugin) but the proxy plumbing is a separate package.
- **Saga / 2PC orchestrators** — `api-saga` and `api-2pc` are Stage 6. Their ledger tables ship here only because the host owns the metadata schema.
- **In-memory test adapters** — every `*-in-memory` package lives in Stage 5. The `./testing` subpath is reserved but empty in Stage 2.
- **OTel SDK initialisation** — the host emits via `@suaveplan/opentelemetry`'s contract types; SDK wiring (exporter URL, sampler) is operator config, not this package's responsibility.
## Source mapping
Genesis-original. Single source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`.
| Requirement | Design doc section |
|---|---|
| Lifecycle FSM states + transitions + dedup mutex | §4 |
| Capability registry + version-compat check | §5.2 |
| Two-context separation (`PluginContext` admin DB vs `RequestContext` tenant DB) | §5.1 |
| Per-request pipeline ordering (steps 1–11) | §6 |
| Error envelope + boundary placement | §6 end, §12.1, §12.7 |
| Hot-reload blue-green primitive | §8.1 |
| `RollbackJournal` + auto-rollback to prior known-good | §8.1a, §8.7 |
| Reinstall criteria (schemaVersion, scopes, secrets, dependsOn) | §8.6 |
| Admin API surface + RBAC + rate-limit + audit | §8.4 |
| Plugin ledger host-owned metadata DB | §4 (closing paragraph), §8.1a |
| OTel span tree + metric surface | §12.3 |
| Audit envelope | §12.5 |
| Health/readiness endpoints | §12.6 |
| `definePlugin` capability-map typing (`as const` literal arrays) | §5.3 |
## Verification approach
Per requirement cluster:
- `src/lifecycle/lifecycle.test.ts` + `*.spec-conformance.test.ts` — every transition exercised; FSM mutex dedup tested; `crashed` reachable only from worker tiers; `rolled_back` distinct admin display.
- `src/hot-reload/blue-green.test.ts` + `rollback-journal.test.ts` — shadow slot lifecycle, version pointer atomic flip, drain timer, force-cancel, native-binding refusal, ring-buffer depth 3, in-memory `WeakRef` + persisted-cursor recovery after restart.
- `src/capability-registry/capability-registry.test.ts` — register / lookup / version compat / duplicate / missing-required.
- `src/plugin-ledger/migrations.test.ts` — forward-only migrations run idempotently against testcontainer SQLite + Postgres + (optional) SurrealDB; table schemas match §8.1a / §9.
- `src/admin-api/*.test.ts` — every endpoint from §8.4; RBAC scope enforcement; per-principal rate-limit; audit emission; `adminPrefix: null` disables HTTP entirely.
- `src/error-boundary/*.test.ts` — `SuaveplanError → statusMap`, `AbortError → 499`, unknown → 500; envelope shape matches `api-types/errors`.
- `src/request-pipeline/*.test.ts` — steps 1–11 ordered, CSRF gating on cookie auth, streaming bypass, response-validation strictness.
- `src/otel/*.test.ts` — every metric in §12.3 emitted from at least one production path, asserted with `@opentelemetry/sdk-metrics`'s `InMemoryMetricExporter`.
- `src/define-plugin/define-plugin.spec-conformance.test.ts` — `expectTypeOf` proves `ctx.db` available iff `db` declared; undeclared capability is a type error.
- `tests/integration/host-boot.test.ts` — `bun examples/api-host-empty.ts`-style boot: empty host returns 200 on `/internal/livez`; this is the umbrella's Stage-2-complete gate.
## Migration impact
None — additive only. Nothing in the monorepo depends on `@suaveplan/api-host` yet. Per umbrella §Stage-2, this package is the keystone; every later stage depends on it.