Files
Ekklesia/docs/architecture/decisions/0006-versioned-capabilities-and-compiled-module-runtime.md
T
ekklesiaandClaude Sonnet 5 1285c90618 docs: record module HTTP architecture
Phase 5B, pass 3: adds ADR 0015 recording the durable module HTTP
routing and production runtime lifecycle architecture pass 1 and pass
2 implemented, extending ADR 0006 and ADR 0014 without superseding
either. Adds narrow cross-reference notes to both, and to the ADR
index, pointing at ADR 0015 for the work they deferred, while
preserving their historical text and accepted status.

Corrects stale forward-looking "pass 2"/future-tense language left
over from pass 1 in core/src/modules/runtime.ts and
packages/contracts/src/module-http.ts (comment-only; production
behavior unchanged), and a shutdown-lifecycle description in
docs/development/testing.md that predated the module-runtime-stop
step pass 2 added between draining the HTTP server and closing the
pool.

Full audit against origin/main...HEAD found the architecture,
contract, startup ordering, routing, authorization/origin
enforcement, and PostgreSQL integration coverage from passes 1-2
sound; the one concrete defect found (Vary-header merge producing an
invalid "*, Origin, Cookie" combination and case-sensitive duplicate
tokens) was already fixed with focused regression tests in a separate
commit (bf76f4e), not amending either checkpoint commit.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-07-25 10:22:35 -05:00

100 lines
6.2 KiB
Markdown

# 0006: Versioned capabilities and compiled module runtime
- Status: Accepted
- Date: 2026-07-12
- Supersedes: [0005](0005-static-module-manifests.md)
## Context
Static module manifests let Core validate the complete installed module graph without executing
module code. The first manifest shape treated capabilities as unversioned identifiers and the
Module SDK helper validated a second programmatic copy of that manifest. Neither model could
express compatibility between independently evolving capability contracts without duplicating
canonical metadata.
Core also needs an explicit production loading boundary. Importing source TypeScript would couple
production startup to a development loader. Resolving arbitrary package exports from Core would
require each discovered module to become a declared Core dependency or require a central import
registry, which conflicts with install-by-cloning discovery.
## Decision
> **Partial correction — see [ADR 0014](0014-module-permission-declarations-and-platform-registry.md).**
> `manifestVersion: 2` and the permission-string-array shape described immediately below (the
> `permissions` field of the static manifest) were superseded by ADR 0014's `manifestVersion: 3`,
> which represents each permission as a complete `{id, baseline, description}` declaration instead
> of a bare identifier string. Every other decision in this ADR — versioned capabilities,
> `defineModuleRuntime`, the compiled-runtime production loading boundary, and registration/lifecycle
> ordering — remains accepted and unchanged. This section is left as originally accepted, as the
> historical record of the manifest's prior shape.
> **Cross-reference — see [ADR 0015](0015-static-module-http-routing-and-runtime-lifecycle.md).**
> "HTTP routing... remain[s] outside this slice" (Consequences, below) and "The injected contexts
> contain only the module ID and a capability registrar or resolver" (this section, below) described
> this ADR's own scope accurately at the time. ADR 0015 implements module HTTP routing: it extends
> `ModuleRegistrationContext` (the `register`-only context) with an `http` route registrar, while
> `ModuleRuntimeContext` (the `start`/`stop` context this sentence also describes) remains exactly as
> limited to module ID and capability access as originally decided here. Every other statement in
> this ADR remains accepted and unchanged.
`ekklesia.module.json` remains the one canonical static manifest and moves to
`manifestVersion: 2`. Provided capabilities are declarations containing an identifier and an exact
semantic version. Required and optional capability dependencies map identifiers to semantic-version
ranges. Capability versions describe their public contracts and evolve independently from the
module package version. A missing optional capability is allowed; an installed but incompatible
optional capability fails discovery.
`defineModuleRuntime` is the Module SDK helper for a module's executable server definition. The
definition repeats only module ID and module version as build-artifact identity markers, plus the
`register`, `start`, and `stop` hooks. It does not duplicate capabilities, dependencies,
permissions, storage requests, or entrypoints from the static manifest. Core validates every
runtime definition against its manifest before running any hook.
Production server entrypoints are direct package-relative paths to compiled ESM JavaScript ending
in `.js` or `.mjs`, normally `./dist/server/index.js`. Core imports their canonical file URLs only
after complete atomic discovery succeeds. A module can therefore be installed without a registry
edit by cloning it into `modules/`, installing workspace dependencies, building the workspace, and
running module validation. `pnpm modules:validate` builds the workspace before discovery so missing
or stale build artifacts are caught at that boundary.
Registration and lifecycle execution use discovery's deterministic dependency-first order. Each
module registers its declared capabilities into a private stage; successful stages become visible
to later dependent registrations but are not published to callers until all registration and
startup hooks succeed. All registration completes before startup. Normal shutdown runs in reverse
order. If startup fails, Core stops modules whose startup completed, in reverse order, and continues
rollback even if cleanup fails.
The injected contexts contain only the module ID and a capability registrar or resolver. Runtime
errors use stable structured issues and do not expose caught exception messages, stacks, import
URLs, absolute paths, or causes.
## Alternatives
- Source TypeScript entrypoints were rejected because production should not depend on an implicit
TypeScript loader or execute unbuilt source.
- Package export subpaths were deferred because pnpm's strict dependency model cannot resolve an
arbitrary discovered package from Core without a declared dependency or generated registry.
- Duplicating the complete static manifest in TypeScript was rejected because the two copies could
drift and make authority ambiguous.
- Publishing capability registrations as each hook runs was rejected because a later registration
failure would expose a partially initialized platform.
## Consequences
Module builds must preserve the runtime definition's ID and version and produce the declared server
artifact. Entrypoint modules are trusted backend plugins, not a sandbox boundary. Their top-level
evaluation must be side-effect-free because ESM evaluation cannot be rolled back or unloaded.
Registration hooks must remain declarative; modules acquire resources in `start` and release them
in `stop`. A failing `start` hook is responsible for cleaning up work it began before rejecting;
Core rollback covers modules whose startup already completed.
Runtime restart, hot replacement, typed capability tokens, schema aggregation, authentication,
HTTP routing, jobs, and physical storage provisioning remain outside this slice.
## Migration implications
Manifest version 1 is rejected rather than interpreted as version 2. Existing development modules
must convert capability arrays into versioned declarations, capability dependency arrays into
range maps, build their server entrypoint to ESM JavaScript, and export a `defineModuleRuntime`
definition from that entrypoint.