Adds ADR 0014 recording the durable architecture Phase 5A established across both passes: the shared contracts-layer PermissionDeclaration, manifest version 3 replacing version 2's bare permission strings, the reserved core.* namespace and the structural guarantee that a module-vs-Core collision always surfaces as ModulePermissionCompositionError rather than the generic PermissionRegistryError duplicate path, deterministic registry composition, startup ordering, repository-root/package-version resolution, registry injection into apps/api's enforcement and explicit-grant HTTP API, the static-snapshot/historical-record preservation guarantees, and Doctor/modules:validate integration. Indexes it in the ADR README, cross-references it from ADR 0006 (only its manifestVersion 2 / permission-string decision is superseded; the rest of ADR 0006 remains accepted, and its historical text is left untouched) and ADR 0013 (module permission registration, deferred there, is what ADR 0014 implements), and links it from the relevant development documentation. Corrects two comments discovered during a direct source audit that overstated where a module-vs-Core permission collision is actually caught (core/src/modules/permissions.ts, apps/api/src/platform-permissions.ts) — comment-only, no behavior change, already covered by existing tests. Also corrects a documentation claim in docs/development/authorization.md that incorrectly implied removing a module blocks explicit revocation of its historical grants; grants-routes.ts's revoke handler never consults the permission registry, so revocation remains unaffected.
91 lines
5.5 KiB
Markdown
91 lines
5.5 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.
|
|
|
|
`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.
|