feat: Ekklesia Module Package v1, the canonical packager and verifier (ADR 0049)
CI / verify (pull_request) Successful in 57s
CI / infrastructure-smoke (pull_request) Successful in 35s
CI / migration-smoke (pull_request) Successful in 1m42s
CI / image-build (pull_request) Successful in 29s
CI / deploy-smoke (same-origin) (pull_request) Successful in 51s
CI / deploy-smoke (split) (pull_request) Failing after 54s

A separately versioned module is now distributed as one deterministic
.tar.gz and its SHA-256, produced by an Ekklesia-owned packager and checked by
Core before anything is installed. This slice implements the portable `any`
target, with the module template as the end-to-end proof. ADR 0049 is
accepted.

@ekklesia/contracts: the Module Package v1 contract. The strict
ekklesia.package.json schema (packageFormat 1, identity, target, runtime,
provenance, incorporated Ekklesia packages, strict transitional facts, an
exact byte-wise-sorted file list with size and SHA-256), target names, and the
generated runtime package.json.

@ekklesia/module-pack (new, tooling): `ekklesia-pack-module --module <dir>
--target any --out <dir> --source-revision <commit>`.
- Bundles server, schema, and jobs with esbuild, SDK, contracts, and Drizzle
  included; only Node built-ins remain imports. Any warning, unresolved
  import, external dependency, or native or platform-specific package fails.
- Copies browser artifacts as built, after proving them current, and the
  migrations; generates package.json and ekklesia.package.json; never ships
  src, tests, declarations, source maps, or workspace ranges.
- THIRD_PARTY_NOTICES: every third-party package traced from the bundles'
  own inputs, credited once with its declared licence and every licence,
  copying, copyright, and notice file it ships. A missing licence fails; a
  missing text fails unless its standard text is complete without copyright
  lines (Apache-2.0, vendored, needed by drizzle-orm).
- Deterministic ustar and gzip: sorted paths, uid/gid 0, empty names, mtime
  0, 0755/0644, no links. Byte-identical across runs and machines.
- Writes only the archive and its .sha256; never deploys, links, or installs,
  so the workspace's install state is untouched.

Core: verifyModulePackage and `verify-module-package.js <archive>
[--sha256 | --sha256-file] [--extract-to <new dir>]`. Reads the archive in
memory and refuses links, special entries, setuid/setgid/sticky modes, unsafe
or escaping paths, and a second top-level directory; checks metadata, every
file, identity, target, Node.js range, SDK, and the transitional Drizzle
agreement; runs the platform's module and migration discovery; loads the
backend once in a forked process with an empty environment; only then
extracts, with normalized modes, into a new directory. Errors are sanitized.

@ekklesia/module-ui-build: analyzeModuleUi reports each artifact's inputs
and whether the built file is current. The nested stylesheet build now works
from the module directory, so artifacts no longer depend on the caller's
working directory.

The split deploy smoke builds the package with the canonical packager
(scripts/lib/build-module-package.Dockerfile replaces the test-only pnpm
deploy packaging), verifies it with the same generic image, installs the
verified tree, removes it, and reinstalls the exact same archive.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ekklesia
2026-10-07 12:44:54 -05:00
co-authored by Claude Opus 5.5
parent 45d6506c10
commit 35ef31df9f
45 changed files with 4876 additions and 151 deletions
+6 -1
View File
@@ -479,7 +479,12 @@ Installed modules come from installation-owned module storage, which the referen
is an integrity property, not a sandbox: an installed module is still trusted code with the API's
database role, exactly as above. The repository's boundary checks prove only source modules in a
workspace, and an installed package usually ships none, so installing one means trusting that
module's own CI and whoever placed it in module storage.
module's own CI and whoever placed it in module storage. A Module Package v1 (ADR 0049) is verified
before it is installed: its digest, its archive safety (no links, special files, setuid bits, or
escaping paths), every file's digest, and its identity and compatibility. The digest proves
integrity, not who published the package; signing is not implemented. The verifier loads the backend
in a short-lived process with an empty environment, so module code never sees a database URL or a
secret while it is being checked.
Two production defaults are relevant but are **not** by themselves a deployment guarantee:
+26 -3
View File
@@ -564,6 +564,29 @@ the following pull-request status context to `main` branch protection:
CI / migration-smoke (pull_request)
```
## Module packages
Ekklesia Module Package v1 (ADR 0049) is proven at three levels, all in `pnpm verify`:
- `packages/contracts/tests/module-package.test.ts`: the `ekklesia.package.json` schema, target
names, and the generated runtime `package.json`.
- `core/tests/module-package-verification.test.ts`: the verifier against hand-built archives,
including what the packager never produces. A valid package verifies and extracts with normalized
modes. A wrong digest, a corrupted or missing or unlisted file, disagreeing identities, an unknown
format, unknown transitional metadata, an incompatible target, Node.js range, SDK, or database
runtime, a missing entrypoint, a backend that imports something the package lacks, absolute and
`..` paths, symbolic and hard links, device nodes and FIFOs, setuid and setgid entries, a second
top-level directory, and a duplicate entry each fail with their own sanitized code. The backend
load check runs in a real child process.
- `packages/module-pack/tests/`: the packager against generated fixture modules (payload contents,
generated `package.json`, metadata, bundled backend with only built-in imports, refusal of native
and platform-specific dependencies and of unknown or textless licences, notices, byte-identical
archives, normalized tar and gzip headers), and end to end against the built module template:
packed twice to identical bytes without changing the workspace's install state, verified by Core's
verifier, extracted elsewhere, and loaded with Node's own resolution hooks proving nothing outside
the package is resolved. Turbo builds the template and Core first, and runs the template's own
tests (which rebuild its UI in place) before these.
## The generic platform image and the reference deployment
These need Docker and build images locally; nothing is pushed or published.
@@ -581,13 +604,13 @@ These need Docker and build images locally; nothing is pushed or published.
- `pnpm deploy:smoke` (`scripts/smoke-deploy.sh`) runs the reference deployment end to end on the
generic image with empty module storage. With `EKKLESIA_SMOKE_TOPOLOGY=split`, the same image also
runs the installed-module lifecycle:
- the module template, packaged as a self-contained test module
(`scripts/lib/test-module-package.Dockerfile`, test-only), is installed into module storage;
- the module template is packaged by the canonical packager (Module Package v1, ADR 0049),
verified by the image's own verifier, and installed into module storage;
- its migration is planned and explicitly applied, and the API restarted;
- a real browser mounts it;
- it is removed and the API restarted, and Core still runs, migrations still plan and apply, and
its data survives;
- it is restored, with its data.
- the exact same package is verified and installed again, and the module returns with its data.
See [deploy/README.md](../../deploy/README.md#testing-this-deployment).