feat: add the module configuration and secrets contract
Modules had no way to be told anything. Announcements — the first real product Ekklesia will absorb — has zero environment variables and compiles a LAN IP address into its source, so this is the contract standing between it and the module system. - A module declares what it needs in its manifest, using six bounded types (string, integer, boolean, enum, url, secret) with no nesting and no regular expressions. Static declaration is what lets the platform validate configuration *before* deciding whether a module may start: an enabled module missing a required value is reported `blocked · configuration_missing · <key>` rather than taking startup down from inside its own `start` hook, and Doctor reports the same while still importing no module code. That forces `manifestVersion: 5`, a hard cutover. - Ordinary values persist in one Core table (migration 0005), one row per (module_id, key), stored as text until validated against the declaring module's current manifest. Nothing is coerced; an invalid stored value blocks the module rather than falling back to the default. A key the manifest no longer declares is retained and ignored. - Secrets are never persisted. There is no encryption at rest in this repository, so a `secret` key is deployment-supplied and mapped in by the host under a name the module never sees. A secret declaration has no `default` field, the write API refuses secret material, and the HTTP view has no `value` field for one — absent by construction, not filtered. - Modules receive a frozen `context.config` on the registration, runtime, and HTTP handler contexts: the same object identity, per module, beside `services` rather than inside it (services is deliberately one shared instance). No environment access, no database reference, no way to reach another module's values. - Configuration decides eligibility through ADR 0028's existing resolver: three new blocked reasons, each with a distinct operator fix, propagating to required dependents through the same mechanism as a disabled module. - Changes take effect at the next restart; both resources report restartRequired. New restricted permission `core.modules.configuration.manage`, separate from lifecycle, at the sibling prefix `/api/v1/module-configuration`. - Migration participation stays independent of configuration validity. Endpoints and credentials for shared infrastructure stay platform concerns; a module declares which bucket it needs, not an S3 endpoint. See ADR 0029. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
fdaad58848
commit
baa689b2a7
@@ -12,8 +12,8 @@ transport), and Phase 5C (module database access) have landed. **Phase 5D (secur
|
||||
against an independent security audit) is complete: all five passes have landed and the final
|
||||
independent whole-repository re-audit has closed, under the current trusted-contributor,
|
||||
not-yet-deployed threat model — and Phase 6 is unblocked and underway.** Phase 6 has since
|
||||
completed through 6D, and **Phase 7A pass 1 (the minimal operational module lifecycle) has
|
||||
landed.** The operational
|
||||
completed through 6D, and **Phase 7A pass 1 (the minimal operational module lifecycle) and
|
||||
pass 2 (the module configuration and secrets contract) have landed.** The operational
|
||||
requirements that remain are prerequisites for two specific *future* activities — executing
|
||||
untrusted CI contributions, and a public production launch — and neither gates Phase 6; see the
|
||||
pass 5 closure below and
|
||||
@@ -449,9 +449,67 @@ logs. Reading and changing both require the new restricted platform permission
|
||||
code is still on disk and still trusted. There is still **no installer**. See
|
||||
[ADR 0028](docs/architecture/decisions/0028-module-operational-lifecycle.md).
|
||||
|
||||
Phase 7A pass 2 adds the **module configuration and secrets contract** — the thing standing between
|
||||
Announcements and the module system, since that product currently has zero environment variables and
|
||||
compiles a LAN IP address into its source.
|
||||
|
||||
A module declares what it needs configured in its manifest, and receives it as a frozen, validated
|
||||
snapshot:
|
||||
|
||||
```json
|
||||
"configuration": [
|
||||
{ "key": "slideSeconds", "type": "integer", "default": 7, "minimum": 1, "maximum": 600 },
|
||||
{ "key": "bucket", "type": "string", "required": true },
|
||||
{ "key": "storageCredential", "type": "secret", "required": true }
|
||||
]
|
||||
```
|
||||
|
||||
```ts
|
||||
start({ config }) {
|
||||
const slideSeconds = config.getInteger("slideSeconds");
|
||||
const credential = config.getSecret("storageCredential");
|
||||
}
|
||||
```
|
||||
|
||||
**Declaration is static, and that is the whole point.** The platform validates configuration
|
||||
*before* deciding whether a module may start, so an enabled module missing a required value is
|
||||
reported `blocked · configuration_missing · bucket` — never imported, never able to take startup down
|
||||
from inside its own `start` hook — and its required dependents block behind it through ADR 0028's
|
||||
existing mechanism. Doctor and `pnpm modules:validate` report the same thing while still importing no
|
||||
module code. That forces `manifestVersion: 5`, a hard cutover in this repository's usual style.
|
||||
|
||||
**Ekklesia does not store secrets, and does not pretend to.** There is no encryption at rest here and
|
||||
no key management, so a `secret` key's value is supplied by the deployment
|
||||
(`EKKLESIA_MODULE_SECRET__<MODULE_ID>__<KEY>`) and never touches PostgreSQL. The module never sees
|
||||
that variable name — it calls `config.getSecret("storageCredential")` — which is exactly what lets a
|
||||
future standalone host supply the same declared secret from somewhere else. A `secret` declaration has
|
||||
no `default` field, the write API refuses secret material outright, and the HTTP view has no `value`
|
||||
field for one, so a secret is absent from responses *by construction* rather than filtered by a branch
|
||||
somebody could delete.
|
||||
|
||||
Six types (`string`, `integer`, `boolean`, `enum`, `url`, `secret`), bounded constraints, no nesting,
|
||||
no regular expressions — a validation vocabulary, not a form-builder DSL. Nothing is coerced:
|
||||
`{"slideSeconds": "7"}` is a `400`, and a stored value that fails its own declared constraints blocks
|
||||
the module rather than being quietly replaced by the default. Ordinary values live in one Core table
|
||||
(migration `0005`), one row per `(module_id, key)`; a key the current manifest no longer declares is
|
||||
retained and ignored so a downgrade never destroys an operator's value.
|
||||
|
||||
Changes take effect at the **next restart**, and `GET`/`PUT /api/v1/module-configuration/<id>` report
|
||||
`restartRequired` rather than pretending otherwise — a sibling prefix, because `config` is a legal
|
||||
module route segment. Both verbs need the new restricted `core.modules.configuration.manage`,
|
||||
deliberately separate from the lifecycle permission: running a module and configuring it are different
|
||||
authorities. A setting that must change *without* a restart is the module's own domain state — like
|
||||
Announcements' `mode` toggle — not platform configuration.
|
||||
|
||||
**There is no settings UI**, and module UI receives no configuration at all in this phase. Endpoints
|
||||
and credentials for shared infrastructure stay platform concerns: a module declares which *bucket* it
|
||||
needs, not an S3 endpoint, leaving room for the object storage service Core still lacks. See
|
||||
[ADR 0029](docs/architecture/decisions/0029-module-configuration-and-secrets.md).
|
||||
|
||||
**Still not the finished Dashboard.** There is no navigation editor or drag and drop, no nested
|
||||
folders, no icons or colors, no Dashboard Home or widgets, no design system, no client-side routing,
|
||||
and no module-management UI — module enablement is an API and a CLI, not a screen. Sign-in UI, a production CSP and deployment for the built shell, asset publication beyond
|
||||
and no module-management or settings UI — module enablement and configuration are an API and a CLI,
|
||||
not a screen. Sign-in UI, a production CSP and deployment for the built shell, asset publication beyond
|
||||
the UI entrypoint, React runtime sharing, the standalone host, and every real feature module also do
|
||||
not exist.
|
||||
|
||||
@@ -509,20 +567,23 @@ Modules
|
||||
✓ Dependency graph valid
|
||||
Dependency order is empty.
|
||||
✓ Permission registry composed
|
||||
4 permissions registered
|
||||
5 permissions registered
|
||||
○ Module lifecycle skipped
|
||||
DATABASE_URL is not configured.
|
||||
○ Module configuration skipped
|
||||
DATABASE_URL is not configured.
|
||||
|
||||
Data definitions
|
||||
✓ Schemas validated
|
||||
0 modules · 0 tables
|
||||
✓ Migrations validated
|
||||
5 migrations · checksums valid
|
||||
6 migrations · checksums valid
|
||||
core:0000_identity_core_tables · f8f2086ef06f
|
||||
core:0001_authorization_core_tables · 830e1fbc26d0
|
||||
core:0002_authorization_integrity · 634294731180
|
||||
core:0003_dashboard_navigation_tables · 2cde27807aa4
|
||||
core:0004_module_lifecycle_state · c8b5d8b3d48b
|
||||
core:0005_module_configuration · c8d57f24f84b
|
||||
|
||||
PostgreSQL
|
||||
✓ Database TLS verified
|
||||
|
||||
Reference in New Issue
Block a user