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]>
100 lines
6.2 KiB
Markdown
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.
|