253 lines
120 KiB
Markdown
253 lines
120 KiB
Markdown
# KBN-101 — Database Runtime/Migration Role Split
|
||
|
||
**Status:** frozen implementation contract; rc.11 closes the target-bound importer-attestation and complete operator-document-inventory HIGH findings, awaiting independent exact-head re-review for [#771](https://git.mosaicstack.dev/mosaicstack/stack/issues/771)
|
||
|
||
**Version:** 1.0.0-rc.11
|
||
|
||
**Dependency:** KBN-010 → **KBN-101 foundation** → KBN-100 → **KBN-101 deployed-role certification** → KBN-105
|
||
**Scope:** PostgreSQL standalone/federated runtime identity, the sole application DDL runner, TLS bootstrap, readiness, deployment handoff, and evidence. This documentation card changes no database, secret, deployment, CI, runtime, migration, or compose artifact.
|
||
|
||
## 1. Decision, modes, and non-negotiable boundaries
|
||
|
||
Current `main` has one `DATABASE_URL` path in `packages/db/src/client.ts`, `migrate.ts`, `drizzle.config.ts`, storage adapters/CLI, Gateway startup, fleet-backlog, CI, installer output, compose, and Portainer. It also has PostgreSQL DDL outside a controlled migration phase: direct Drizzle scripts, `CREATE EXTENSION` probes, a direct-DDL federated integration test, and platform init SQL. None of those current paths is certified by this contract; every implementation card below must close its listed path.
|
||
|
||
| Item | Exact name / shape | Rule |
|
||
| ------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| Runtime URL | `DATABASE_URL` | PostgreSQL **non-owner runtime** connection. Required only by runtime services in `standalone`/`federated`; those services never receive `DATABASE_MIGRATION_URL`. |
|
||
| Migration URL | `DATABASE_MIGRATION_URL` | Required only by `mosaic-db-migrator`, the dedicated one-shot runner/Job. It is forbidden in Gateway, storage runtime, fleet, and ordinary CLI environments. |
|
||
| Runtime DTO | `DatabaseRuntimeConnectionConfigDto` | `tier`, `databaseUrl`, `runtimeMode`; maps only `DATABASE_URL`. It is not constructible from migration configuration. |
|
||
| Migration DTO | `DatabaseMigrationConnectionConfigDto` | `tier`, `migrationDatabaseUrl`, `migrationMode`; maps only `DATABASE_MIGRATION_URL`, is accepted only by `mosaic-db-migrator`, and has no runtime-bootstrap import path. |
|
||
| TLS DTO | `DatabaseTlsConfigDto` | `caCertificatePath`, `rejectUnauthorized: true`, `serverName`; validates the mounted CA without serializing its bytes. `serverName` is derived only from the validated connection host. |
|
||
| Readiness DTO | `DatabaseSchemaReadinessDto` | `state`, `expectedSchemaVersion`, `observedSchemaVersion`, `migrationRequired`, `roleCheck`, `checkedAt`; no URL, user, host, database name, or secret. |
|
||
| Modes | `local`, `standalone`, `federated` | `local` is the explicit PGlite-only exception. `standalone` and `federated` are production-like PostgreSQL modes. |
|
||
|
||
**No fallback or plaintext:** in production-like modes, missing `DATABASE_URL`, `DATABASE_MIGRATION_URL`, or `DATABASE_TLS_CA_CERT_PATH` is a phase-specific error. No command may substitute a runtime URL, config-file URL, default URL, inferred URL, or hard-coded URL. PostgreSQL URLs must use `sslmode=verify-full` with `rejectUnauthorized: true`; `disable`, `allow`, `prefer`, `require`, `no-verify`, an absent CA, a wrong CA, an unverified certificate, or host/SAN mismatch fails before readiness. PGlite uses only its configured local data directory and explicit PGlite routine; it neither reads nor interprets PostgreSQL URL/TLS variables.
|
||
|
||
**ASSUMPTION K101-A1:** `standalone` and `federated` are the complete current PostgreSQL production-like modes. A future PostgreSQL tier inherits this contract until a versioned amendment names its DNS, secrets, bootstrap, and evidence.
|
||
|
||
## 2. The sole PostgreSQL DDL control plane
|
||
|
||
`mosaic-db-migrator` is the **only application/CI/test command that may connect with DDL authority to PostgreSQL**. It owns, in one `max: 1` PostgreSQL session: migration-DTO parse, TLS/identity/search-path preflight, advisory-lock acquisition, manifest/ledger reconciliation, migration execution, postflight/readiness verification, lock release, and session close. It rejects `DATABASE_URL`-only invocation **before opening a connection or emitting DDL**. The only exception is the external privileged platform/IaC bootstrap actor (§4/§7), which is not an application command, never receives either application URL, and executes only the fixed bootstrap artifact.
|
||
|
||
**Executable contract (KBN-101-03 exclusive):** `packages/db/package.json` publishes the exact mapping `"mosaic-db-migrator": "./dist/cli.js"`; `packages/db/src/cli.ts` compiles to that target. `docker/db-migrator.Dockerfile` builds that package and has the exact image entrypoint `ENTRYPOINT ["mosaic-db-migrator"]`. KBN-101-03 alone owns package/bin/build/pack/discovery tests that execute this compiled bin and image as `mosaic-db-migrator --help|--run|--verify`. The CLI imports only private implementation modules in `packages/db/src/migrator/`; package root exports do **not** expose `runMigrations`, and no other programmatic migration API is public. The sole typed programmatic runner is private to the package/CLI boundary and accepts the typed migration DTO, never a URL/string/SQL/schema/role argument.
|
||
|
||
The exact user-facing interface is `mosaic-db-migrator --run` and `mosaic-db-migrator --verify`. `--help` is required and is the only discovery mode. Both commands use the fixed packaged migration folder and accept only `DATABASE_MIGRATION_URL`, `DATABASE_TLS_CA_CERT_PATH`, and non-secret `MOSAIC_DATABASE_TIER`/correlation input from the environment. They reject `DATABASE_URL` as fallback and reject URL, SQL, schema, role, database, migration-folder, and identifier arguments in argv. Stable sanitized process exits are: `0` success; `64` configuration; `65` TLS; `66` unsafe identity; `67` ledger/schema; `68` lock contention; `69` migration failure. CI, Compose, Swarm, and each two-gateway migration Job invoke the immutable db-migrator image with exactly `mosaic-db-migrator --run`; their readiness verification invokes exactly `mosaic-db-migrator --verify`. Required tests cover `--help`, argv rejection, runtime-only refusal before connect, migration-only success, each sanitized error/lock exit, and ordered `postgres-a → mosaic-db-migrator-a → gateway-a` plus `postgres-b → mosaic-db-migrator-b → gateway-b` execution.
|
||
|
||
The implementation must inventory and close every present and future entrypoint as follows:
|
||
|
||
| Current entrypoint | Required rc.11 disposition |
|
||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `packages/db/src/migrate.ts:runMigrations()` | Remove its optional URL, `DATABASE_URL`, and default fallback API from public/runtime exports. Its PostgreSQL behavior moves behind `mosaic-db-migrator`; callers cannot invoke it with an arbitrary URL. |
|
||
| `packages/db/src/index.ts` | `KBN-101-03` removes the public `runMigrations` re-export; only the explicit PGlite-local API remains as separately typed local behavior. A compile/import-negative proves `@mosaicstack/db` cannot directly import `runMigrations`; a `DATABASE_URL`-only direct-library attempt fails before connect. |
|
||
| `packages/db/drizzle.config.ts` and direct `drizzle-kit migrate` | A database-connecting Drizzle configuration reads only `DATABASE_MIGRATION_URL` through the migration DTO and rejects its absence before connection. Replace direct `drizzle-kit migrate` exposure with `mosaic-db-migrator`. `db:generate` is an offline schema artifact command and must neither resolve nor connect to a URL. |
|
||
| `pnpm --filter @mosaicstack/db db:migrate` and CI invocation | Make it a thin `mosaic-db-migrator` wrapper; no direct Drizzle migrator invocation remains. CI supplies an isolated disposable migration URL only to that job. |
|
||
| `db:push` / direct `drizzle-kit push` | Forbidden for standalone, federated, CI production-like, Portainer, and any URL outside a disposable developer database. If retained for local experimentation, a wrapper requires `MOSAIC_DISPOSABLE_DEVELOPER_DB=1`, a locally allowlisted disposable target, `DATABASE_MIGRATION_URL`, verified TLS when PostgreSQL is used, and rejects `DATABASE_URL`, any production-like tier, and every non-allowlisted host/database before connection. It is never a release, repair, or migration procedure. |
|
||
| `mosaic storage migrate --run` | Delegates only to `mosaic-db-migrator`; it rejects `DATABASE_URL`-only execution before spawning or connecting. Its PGlite form remains explicitly local-only. |
|
||
| `PostgresAdapter.migrate()`, Gateway `DatabaseModule`/startup, and PostgreSQL adapter factories | Runtime PostgreSQL migration is removed: no `runMigrations`, DDL, `CREATE EXTENSION`, or migration-compatible handle is reachable from startup. Gateway performs read-only identity, TLS, `search_path`, and manifest-ledger readiness checks only. |
|
||
| `packages/mosaic/src/commands/fleet-backlog.ts` | PostgreSQL fleet backlog never migrates or creates tables. It consumes a ready runtime connection; PGlite may use only its explicit local migration routine. |
|
||
| `packages/storage/src/{adapters/postgres,tier-detection}.ts` extension work and `infra/pg-init/01-extensions.sql` | Runtime probes become read-only catalog/extension-presence checks. Extension provisioning is a fixed external bootstrap prerequisite or a reviewed runner migration where the migrator has the required scoped authority; it is never a probe side effect or a reason to grant runtime database CREATE. |
|
||
| `packages/db/src/federation.integration.test.ts` direct type/table/index DDL | `KBN-101-02` replaces it with a pre-migrated disposable database created by `mosaic-db-migrator`, or makes the test invoke that runner. The test itself has runtime credentials and no direct DDL. |
|
||
| `apps/gateway/src/__tests__/integration/federated-pgvector.integration.test.ts` `CREATE TEMP TABLE` | `KBN-101-02` replaces the temporary runtime DDL with a runner-prepared disposable **persistent** `mosaic.federated_pgvector_fixture` database/table. The test receives only its runtime URL/CA and performs read/query-only qualified-vector assertions (including the selected vector operator); it has no setup hook, temporary privilege, or DDL. Runtime `TEMPORARY` remains denied. |
|
||
| `docker/init-db.sql` and `infra/pg-init/01-extensions.sql` | `KBN-101-02` retires these duplicate tracked init artifacts; neither may remain as a hidden extension authority. The sole extension action is the fixed external bootstrap artifact described in §4/§5, or the runner only when its reviewed implementation card explicitly grants that authority. |
|
||
| `packages/storage/src/{cli,migrate-tier}.ts` operator/runtime command closure | `KBN-101-02` alone replaces raw SQL/DDL behavior with delegation to `mosaic-db-migrator --run` (or the explicit PGlite-local path). Data copy requires the exact paired non-secret argv references `--target-url-file /run/secrets/mosaic_migrate_target_url` and `--target-attestation-file /run/mosaic-attestations/migrate-target.v1.json`. The trusted `mosaic-db-migrator --verify` producer reads the exact importer target-secret file only to bind and attest it; it never uses importer credentials for DDL/DML. Before target connection the importer verifies strict files, signed target attestation, credential-file digest/version, canonical TLS/database binding, role, manifest, expiry, and replay; after verified TLS but before transaction/DML it verifies server/database identity and schema. It opens only a dedicated non-DDL `mosaic_data_importer` target connection. Its DML registry is limited to declared mutable application tables and excludes schemas, roles, memberships, extensions, extension/catalog objects, Drizzle ledger, and immutable KBN relations. Raw `--target-url`, `DATABASE_URL` fallback, runtime-owner/importer role confusion, missing/unsafe/substituted files, stale/replayed/tampered/wrong-key attestation, wrong binding, and any DDL attempt fail before target connection/DDL or copy. KBN-101-07 documents that produced interface but owns neither source file. |
|
||
| `tools/federation-harness/docker-compose.two-gateways.yml` | Active topology; `KBN-101-05` migrates it rather than retires it. It must contain `postgres-a`, `postgres-b`, `mosaic-db-migrator-a`, `mosaic-db-migrator-b`, `gateway-a`, and `gateway-b`; each database has separate runtime/migrator URL and CA consumers, TLS server material, verified-TLS readiness, and runner-before-Gateway ordering. Its existing plaintext URLs/init mount are forbidden. |
|
||
| `docs/fleet/backlog-conventions.md` | `KBN-101-07` removes the current automatic first-use PostgreSQL `runMigrations()` claim. It points PostgreSQL operators to the sole runner/readiness sequence and labels only the PGlite routine as local-only. A documentation-route `DATABASE_URL`-only test asserts no first-use/migration instruction is executable before connect. |
|
||
| `docs/PERFORMANCE.md` | `KBN-101-07` removes direct `drizzle-kit migrate` and Gateway-startup `runMigrations()` instructions. It points to the sole runner/readiness sequence. A documentation-route `DATABASE_URL`-only test asserts neither direct command nor Gateway fallback remains. |
|
||
| `README.md`, `CLAUDE.md`, `docs/guides/{admin-guide,dev-guide,deployment,migrate-tier,user-guide}.md`, `docs/federation/{SETUP,TASKS}.md`, `docs/fleet/backlog-conventions.md`, `docs/PERFORMANCE.md`, `docs/design/storage-abstraction-middleware.md`, and `docs/plans/2026-03-15-agent-platform-architecture.md` documentation hits | `KBN-101-07` owns the non-normative disposition inventory in §2.2. It replaces, retires, or labels status-only every direct `db:migrate`, `db:push`, `CREATE EXTENSION`, first-use migration, Gateway-startup migration, and generic `mosaic storage migrate` instruction with the runner/bootstrap or secure `migrate-tier` route. The architecture plan explicitly marks direct `db:migrate` superseded. Historical shipped-status language is adjacent to an exact non-authorizing KBN-101 note. Documentation is a DDL entrypoint and cannot advertise a bypass. |
|
||
| New package scripts, test helpers, setup hooks, CLI commands, installers, CI steps, adapters, or operator docs | A repository check rejects any new PostgreSQL DDL-capable entrypoint or instruction unless it is the dedicated runner or the named external bootstrap artifact. No future script may accept a URL parameter or `DATABASE_URL` as a DDL escape hatch. |
|
||
|
||
The runner must run the original migration bytes, including shipped `0009`; no migration command may repair a ledger by manual insertion, adoption, `db:push`, or schema diff. Tests requiring PostgreSQL schema consume a pre-migrated disposable database or invoke this exact runner.
|
||
|
||
`KBN-101-06` alone owns `.woodpecker/ci.yml`, `tools/ci/kbn101-ddl-inventory.ts`, `tools/ci/kbn101-ddl-inventory.spec.ts`, `tools/ci/fixtures/kbn101-ddl-inventory.json`, `tools/ci/kbn101-entrypoint-matrix.ts`, and `tools/ci/kbn101-entrypoint-matrix.spec.ts`. The scanner inventory is canonical JSON whose every record has exactly `path`, `class`, `ownerCard`, `disposition`, `allowedTokens`, `rationale`, `expiry`, and `reviewRevision`. `class` is one of `executable-source`, `script-or-package-bin`, `operator-document`, `deploy-manifest`, or `normative-contract`; `ownerCard` is one KBN-101 card; `disposition` is one of `runner`, `delegate`, `deny`, `retire`, `superseded-document`, `active-secure-data-migration`, `status-only`, or `allowlisted`; and `allowedTokens` is a nonempty subset of the fixed rules below only for `allowlisted` or `superseded-document`. `normative-contract` is excluded from **operator execution**, never from scanning: it is valid only for the exact KBN PRD/contract/shared/task canonical paths, must carry declarative requirements rather than an executable shell instruction, and a fenced executable instruction or an imperative bypass in that class fails rather than being masked. An `active-secure-data-migration` record has empty `allowedTokens` and additionally pins `route`, `targetCredentialOption`, `targetCredentialFile`, `targetAttestationOption`, `targetAttestationFile`, `attestationProducer`, `attestationKeyIds`, `attestationBindings`, `targetSchemaPrerequisite`, `targetConnectionRole`, `forbiddenInputs`, and `routeTest`; its parser rejects a missing field, a route that accepts a credential value on argv, an attestation missing any canonical binding, or a target that is not runner-prepared/verified. The latter `superseded-document` disposition is allowed only for the exact architecture-plan path when adjacent text says the direct command is superseded/MUST NOT run and names `mosaic-db-migrator --run`. The scanner rejects duplicate path owners, ownerless non-allowlisted rows, missing paths, malformed fields, an inventory path outside its declared class, and every unrecognized active command. The classifier's exact case-insensitive token/rule set is: `runMigrations\\s*\\(`; `drizzle-kit\\s+(?:migrate|push)`; `\\bdb:(?:migrate|push)\\b`; `mosaic\\s+storage\\s+migrate`; `CREATE\\s+(?:TEMP(?:ORARY)?\\s+)?(?:EXTENSION|TABLE|TYPE|INDEX|SCHEMA)`; `ALTER\\s+(?:EXTENSION|TABLE|TYPE|SCHEMA)`; `DROP\\s+(?:EXTENSION|TABLE|TYPE|INDEX|SCHEMA)`; and `DATABASE_URL` when it occurs in the same file as any preceding rule. It scans exact-path current executable source/scripts/package bins, operator documents, deploy manifests, and named normative contracts; any hit not represented by a permitted inventory record fails. The matrix harness invokes the compiled runner/package/image and produced deployment artifacts; it edits no producer file.
|
||
|
||
The canonical inventory includes this active—not historical—operator route record: `path=docs/guides/migrate-tier.md`, `class=operator-document`, `ownerCard=KBN-101-07`, `disposition=active-secure-data-migration`, `allowedTokens=[]`, `route=mosaic storage migrate-tier`, `targetCredentialOption=--target-url-file`, `targetCredentialFile=/run/secrets/mosaic_migrate_target_url`, `targetAttestationOption=--target-attestation-file`, `targetAttestationFile=/run/mosaic-attestations/migrate-target.v1.json`, `attestationProducer=mosaic-db-migrator --verify`, `attestationKeyIds=[migrate-target-v1-active,migrate-target-v1-overlap]`, `attestationBindings=[credentialSecretVersion,credentialFileSha256,tlsHostPortDatabase,caSpkiFingerprint,postgresSystemIdentifier,databaseOid,expectedImporterRole,manifestFingerprint,schemaFingerprint]`, `targetSchemaPrerequisite=mosaic-db-migrator --verify`, `targetConnectionRole=mosaic_data_importer`, `forbiddenInputs=[--target-url,DATABASE_URL,runtime-owner,signing-key]`, and `routeTest=packages/storage/src/migrate-tier.spec.ts::secureTargetRouteAndAttestation`. The -06 scanner/matrix must reject a changed/missing field, any `--target-url` or credential-bearing argv, a `DATABASE_URL` target fallback, a missing/unsafe/substituted credential file, missing/stale/replayed/tampered/wrong-key attestation, wrong binding, wrong mode, runtime-owner/importer confusion, target connection before attestation verification, or any DDL attempt before target connection/DDL.
|
||
|
||
### 2.1 Target-bound importer attestation v1
|
||
|
||
`mosaic-db-migrator --verify` is the trusted producer only after its verified-TLS identity, manifest/ledger, and schema checks pass. Its root-owned launch wrapper is the sole reader of the fixed `MOSAIC_DB_ATTESTATION_SIGNING_KEY_FILE=/run/secrets/mosaic-db-migrate-target-ed25519` reference: that regular, non-symlink `root:root` `0400` Ed25519 private-key file is mounted only to the one-shot runner. It opens the key once, passes signing capability only in memory to `mosaic-db-migrator`, and never mounts, forwards, logs, prints, or otherwise exposes the private key to the importer, runtime, or an application container. KBN-101-05 renders both that runner-only secret and the importer-only pinned public-key/key-ring mount; an alternative env path, unsafe key file, or key material in importer/runtime is a hard failure.
|
||
|
||
After verification the runner atomically emits the credential-free, non-secret `0444` controlled artifact `/run/mosaic-attestations/migrate-target.v1.json`: write a same-directory `0600` temporary file, write canonical bytes, `fsync` file and directory, set final owner/mode, and rename atomically. The single JSON envelope carries an RFC 8785 JCS canonical UTF-8 `attestation` payload plus a detached Ed25519 `signature` envelope (`algorithm`, `keyId`, base64 signature); the signature covers only the canonical payload bytes, never a reformatted document. The v1 payload fields are exactly `format`, `version`, `keyId`, `issuedAt`, `expiresAt`, `nonce`, `targetCredentialSecretVersion`, `targetCredentialFileSha256`, `tlsHost`, `tlsPort`, `database`, `caSpkiFingerprint`, `postgresSystemIdentifier`, `databaseOid`, `expectedImporterRole`, `manifestFingerprint`, `schemaFingerprint`, `producerInvocation`, `producerBuildDigest`, `producerImageDigest`, and `correlationId`. It contains no URL/DSN, username, password, private key, or credential bytes. The target credential must be a high-entropy DSN secret because its SHA-256 is non-secret binding evidence, never a substitute secret.
|
||
|
||
For binding, the runner receives and reads the exact importer target-secret file only after `lstat` regular-file/no-symlink/owner/mode checks. It derives the secret version and SHA-256 from those exact bytes, parses only canonical TLS host/port/database without retaining username/password, verifies that tuple plus CA/SPKI, PostgreSQL system identifier, database OID, manifest/schema fingerprints, and expected `mosaic_data_importer` role against its verified target, then signs. The runner uses `DATABASE_MIGRATION_URL` for its verification connection; it does not use importer credentials for DDL or DML.
|
||
|
||
`mosaic storage migrate-tier` accepts exactly `--target-url-file /run/secrets/mosaic_migrate_target_url` and `--target-attestation-file /run/mosaic-attestations/migrate-target.v1.json`. Before **any** target connection it `lstat`s both files; validates the pinned public key/key ID, signature, JCS canonicality, expiry, nonce replay cache, secret version, target-file digest, canonical host/port/database, CA binding, expected importer role, and manifest/schema fields; then opens the validated URL fd once, digests and parses those in-memory bytes, and connects from those same bytes with no reread. Missing, substituted, changed-after-check, stale, replayed, tampered, wrong-key, or wrong-binding inputs return stable sanitized attestation errors with **zero target connection and zero DDL**. The nonce replay cache is durable through the artifact expiry window and atomically claims `(keyId, nonce, credentialFileSha256)` before connecting; a replay is terminal. New active and overlap public keys are pinned by key ID; rotation accepts only the bounded overlap, revocation removes a key immediately, and any credential secret-version rotation invalidates prior artifacts and requires a fresh runner verification/signature.
|
||
|
||
Only after verified TLS connection and before transaction/DML, the importer compares server system identifier, database OID, `current_user`, CA/SPKI, and manifest/schema fingerprint with the attestation. A mismatch closes the connection with **zero DML/DDL**. Its grants and statement classifier make DDL impossible. KBN-101-02 tests missing/wrong/stale/replayed/tampered attestation, wrong/revoked key, substituted URL file, file changed after check, wrong host/database/CA/role/manifest/system ID, digest/version mismatch, and both `zero target connection` versus `connection then zero DML` outcomes. KBN-101-03 tests JCS/signing/atomic producer semantics; KBN-101-05 tests private/public key and artifact mounts/rendering; KBN-101-06 invokes the complete matrix; KBN-101-07 owns the operator guide only.
|
||
|
||
### 2.2 Complete current documentation disposition inventory
|
||
|
||
The KBN-101-06 fixture records these exact non-normative paths: `README.md`, `CLAUDE.md`, `docs/federation/SETUP.md`, `docs/guides/admin-guide.md`, `docs/guides/dev-guide.md`, `docs/guides/deployment.md`, `docs/guides/migrate-tier.md`, `docs/guides/user-guide.md`, `docs/fleet/backlog-conventions.md`, `docs/PERFORMANCE.md`, `docs/plans/2026-03-15-agent-platform-architecture.md`, and `docs/design/storage-abstraction-middleware.md` as `operator-document`; each is runner/delegate/deny/superseded/active-secure as adjacent text specifies. `docs/federation/TASKS.md` is an exact `operator-document` `status-only` historical record: its shipped M1 language is superseded and cannot authorize current DDL, extension creation, or importer execution. `docs/PRD.md`, this contract, `SHARED-CONTRACT.md`, and `native-kanban-sot/TASKS.md` are exact `normative-contract` records; their declarative KBN requirements are scan-visible but excluded from operator execution and cannot mask an instruction. The full-doc scanner must fail an unknown active command, a missing record, or a status-only/normative path containing executable bypass guidance.
|
||
|
||
The only allowlist categories are `historical-sql` (`packages/db/drizzle/**`, byte-immutable runner input only), `pglite-local` (an explicitly PGlite-only source/test path), `negative-test-literal` (a focused negative test), `vendored-generated` (a generated or vendored artifact), and `historical-review-report` (`docs/reports/**` only). Every allowed record names its exact path, token(s), rationale, expiry, and review revision. No allowlist category is valid for an executable current source/script/package bin, operator document, deploy manifest, `README.md`, `CLAUDE.md`, `docs/plans/**`, or `docs/guides/**`; `historical-review-report` cannot contain an operative command. Scanner self-tests place each token in an unowned current path, prove duplicate-owner/ownerless/path-existence failure, and prove an operative `db:migrate` instruction in a path labeled historical fails rather than being masked. The scanner is a classifier plus path inventory and review—not a naive token scan alone—and never by itself proves DDL authority. For every non-allowlisted inventory row and `gateway-a`/`gateway-b`, the matrix proves `DATABASE_URL`-only, missing migration URL, direct library import, direct Drizzle, `db:push`, init artifact, operator route, runtime-only fixture/test, and each harness migration Job/Gateway fail before connection/DDL as applicable. The `db:push` negatives also cover production-like tier and production-like URL rejection.
|
||
|
||
## 3. Exact migration manifest, ledger, and lock
|
||
|
||
### 3.1 Manifest v1
|
||
|
||
The runner generates and verifies a source-controlled **migration manifest v1** from the shipped Drizzle journal and SQL files. A record has exactly:
|
||
|
||
```text
|
||
logicalIndex: non-negative integer from journal array position
|
||
journalTag: exact journal `tag` string
|
||
migrationSha256: lowercase SHA-256 of the exact migration `.sql` file bytes
|
||
```
|
||
|
||
The canonical SQL bytes are the raw Git blob bytes at the signed source-release commit, not workstation checkout bytes. KBN-101-03 adds/validates an LF-pinning `.gitattributes` rule for `packages/db/drizzle/**/*.sql`, generates the source-controlled manifest from those canonical blobs in CI, and makes the runner verify deployed file bytes against the manifest before DDL. No newline, Unicode, whitespace, SQL, or line-ending normalization is applied. Manifest canonical serialization is UTF-8 bytes of:
|
||
|
||
```text
|
||
mosaic-drizzle-manifest-v1\n
|
||
<logicalIndex>\t<JSON.stringify(journalTag)>\t<migrationSha256>\n
|
||
... in ascending logicalIndex with no omitted index
|
||
```
|
||
|
||
`manifestSha256` is SHA-256 of those canonical bytes. The manifest has no timestamp-derived ordering; `folderMillis`, legacy ledger `id`, and `created_at` are diagnostic only. KBN-101-03 corrects journal **logical** order metadata (including the current `0008`/`0009` anomaly) without changing shipped `0009` bytes.
|
||
|
||
The runner stores certification in `drizzle.__mosaic_migration_manifest` with exactly one active v1 row (`manifest_version=1`, `manifest_sha256`, `certified_at`). It is owned by `mosaic_schema_owner`; only the migrator after `SET ROLE mosaic_schema_owner` may insert/update it; runtime gets `SELECT` only, and `PUBLIC` gets no schema/table privilege. Existing databases receive this table through the runner after backup/preflight; it is not created by Gateway, a test, or manual SQL.
|
||
|
||
### 3.2 Reconciliation and 0009 transition
|
||
|
||
The expected ledger is the ordered list of v1 manifest tuples. The runner reads every observed `drizzle.__drizzle_migrations.hash`, maps **each observed hash to exactly one** manifest tuple, and rejects a missing, unknown, duplicate, ambiguous, corrupt, or stale-replica mapping. Equality is tuple-complete: every expected tuple occurs once, no additional tuple occurs, and the manifest digest matches. A count comparison or hash-set comparison is forbidden. Physical legacy insertion `id`, insertion timestamp, and order are expressly non-normative.
|
||
|
||
For an existing database:
|
||
|
||
1. take the KBN-101-07 approved backup and capture a read-only inventory before changing anything;
|
||
2. if `0009` is missing **and** its effects are absent, run the original shipped `0009` through `mosaic-db-migrator` and record it normally;
|
||
3. if the `0009` hash is present but physically late, accept it when its one-to-one tuple mapping is exact;
|
||
4. if an expected hash is missing while its effects are partial or complete, or catalog/ledger evidence conflicts, fail closed as `DATABASE_MIGRATION_RECONCILIATION_AMBIGUOUS`. Recovery is backup restoration or an explicit separately reviewed repair artifact with its own owner, tests, backup, rollback, and approval—not manual ledger insertion/adoption; and
|
||
5. write/update the v1 certification row only after exact reconciliation and final catalog/readiness verification.
|
||
|
||
Required runner tests cover clean, pre-0009, skipped-0009/effects-absent, applied-late, duplicate, unknown, missing, corrupt tuple-pair, partial/full-effect ambiguity, stale replica, backup/restore, and rerun idempotence. The tests prove the `0009` SQL bytes are unchanged and logical journal ordering—not physical ledger order—controls reconciliation.
|
||
|
||
### 3.3 Fixed advisory-lock namespace
|
||
|
||
Before any preflight that can decide migration state, `mosaic-db-migrator` acquires `pg_try_advisory_lock(1297044289, 1262636593)`. The constants are signed-int4-safe fixed namespace values: class `1297044289` (`MOSA`) and object `1262636593` (`KBN1`). The same `max: 1` session retains it for preflight, reconcile, migrate, verify, release, and close. Failure returns `DATABASE_MIGRATION_LOCKED` immediately; a process crash releases it when the PostgreSQL connection closes. Runtime readiness remains unready while a holder is active and never waits by running migrations. Tests prove concurrent contention, crash/connection-loss release, readiness while held, and non-interference from an unrelated advisory key.
|
||
|
||
## 4. PostgreSQL roles, identifiers, and trusted sessions
|
||
|
||
Role creation, passwords, membership, database ownership, certificates, and Vault values are platform/IaC/operator work—not Drizzle/application migrations. Application SQL must not issue credential/role management statements or embed credentials.
|
||
|
||
| Role | Attributes and ownership | Membership / session use |
|
||
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||
| `mosaic_platform_database_owner` | `NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS`; platform-only database owner after bootstrap. | Never granted to application roles. |
|
||
| external platform bootstrap actor | Provider/operator/IaC-controlled, externally audited **superuser** identity outside the Mosaic role graph and Vault/application configuration. | Creates/transitions the database and roles, then retires from application use. It alone `SET ROLE`s the extension owner for `CREATE EXTENSION`, `ALTER EXTENSION ... UPDATE`, or `ALTER EXTENSION ... SET SCHEMA`, records the action, and `RESET ROLE`s. |
|
||
| `mosaic_schema_owner` | `NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS`; owns only `mosaic`/`drizzle` schemas and application/ledger objects. It has only `USAGE` on `mosaic_extensions` for fixed legacy type resolution. | Never an application login; no ownership, `CREATE`, `ALTER`, `DROP`, extension/member-change, or default-privilege authority in `mosaic_extensions`; its migrator subphase never receives temporary `CREATE` there. |
|
||
| `mosaic_extension_owner` | Dedicated `NOLOGIN SUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS` extension owner, distinct from platform/schema/migrator/runtime. It is used solely to own/maintain untrusted `vector`, `mosaic_extensions`, and every owner-bearing extension member there. | `rolcanlogin=false`, `rolsuper=true`, and **zero members** are catalog-proven. No application role has membership, `SET ROLE`, credential, or inheritable grant. An externally controlled, audited platform-bootstrap **superuser** session alone executes `SET ROLE mosaic_extension_owner` for fresh creation, approved-owner update/relocation, or shadow bootstrap, then `RESET ROLE`; no persistent membership is ever granted. |
|
||
| `mosaic_migrator` | `LOGIN NOINHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS`. | Only `mosaic_schema_owner`; runner verifies `session_user=mosaic_migrator`, `SET ROLE mosaic_schema_owner`, then `current_user=mosaic_schema_owner`. |
|
||
| `mosaic_data_importer` | `LOGIN NOINHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS`; dedicated data-copy identity, not a DDL or schema owner. | It is used only after runner preparation/verification through the KBN-101-02 file-reference interface. It has no owner/migrator/extension membership and cannot `SET ROLE mosaic_extension_owner`, or `ALTER`/catalog-or-extension-member `UPDATE`/`DROP`/change extension membership. Its bounded data-copy DML is not extension authority. |
|
||
| `mosaic_runtime_capability` | `NOLOGIN` and no ownership/administrative attributes. | Holds only named runtime grants. |
|
||
| `mosaic_runtime` | `LOGIN INHERIT` with no ownership/administrative attributes. | Only `mosaic_runtime_capability WITH INHERIT TRUE, SET FALSE, ADMIN FALSE`; never owner/migrator member. |
|
||
|
||
The fixed application/runtime schema is **`mosaic`**. Every application/runtime pooled connection executes and verifies exactly `SET search_path TO pg_catalog, mosaic` before its first application query; connection checkout repeats this after reset/reconnect. Transactional application operations use `SET LOCAL search_path TO pg_catalog, mosaic` and verify it before query execution. `public` and `$user` are forbidden in all runtime paths.
|
||
|
||
The sole runner has one narrowly bounded **legacy-history bootstrap subphase** for the immutable current journal: before it runs, external bootstrap revokes `CREATE` on `public` from `PUBLIC`, temporarily assigns its ownership to `mosaic_schema_owner`, and denies all runtime connections. The externally controlled audited bootstrap-superuser session first executes `SET ROLE mosaic_extension_owner` to create/own `mosaic_extensions` and fresh `vector`, or to perform approved-owner relocation/update, records that control-plane action, and executes `RESET ROLE`; it receives no membership because a superuser can assume the role without one. In its locked `max:1` migration session only, after `SET ROLE mosaic_schema_owner`, the runner has only `USAGE` (not ownership or `CREATE`) on `mosaic_extensions` and uses the fixed legacy-only `SET LOCAL search_path TO pg_catalog, public, mosaic_extensions` solely so immutable `0001` resolves its unqualified existing `vector` type. It cannot create, alter, drop, reassign, or change a member there. The preflight catalogs `pg_namespace.nspowner`, schema ACL/default ACLs, `pg_extension.extowner`, and member owners, then directly proves `CREATE`/`ALTER`/`DROP`/member-change denial for runtime, migrator, and schema owner. After relocation/catalog verification and before manifest certification or any runtime readiness, the runner transfers `public` ownership to `mosaic_platform_database_owner`, revokes application `USAGE`/`CREATE`, and restores `pg_catalog,mosaic`. This compatibility subphase is not a runtime/operator option, accepts no configuration identifier, has no fallback, and is removed/disabled before KBN-101-08.
|
||
|
||
`KBN-101-03` exclusively owns `packages/db/src/schema.ts`, `packages/db/drizzle/meta/*`, `packages/db/drizzle/meta/_journal.json`, the generated relocation migration, and exact database/Drizzle tests. It freezes one exported `export const mosaic = pgSchema('mosaic')`; every application `pgTable` and every application `pgEnum` must be declared through that export. The generated snapshot/journal and future `db:generate` output must target `mosaic` only; a static declaration/SQL test rejects a default-schema application `pgTable`/`pgEnum`, `public` application declaration, or future generated application DDL outside `mosaic`. Historical `0000` through current SQL and the shipped journal provenance are byte-immutable and execute only in the trusted legacy-`public` subphase; they are never rewritten to claim they originally targeted `mosaic`.
|
||
|
||
### 4.1 pgvector ownership and supported transition
|
||
|
||
The selected extension policy is fixed for PostgreSQL 17 + pgvector 0.8.2. Its actual target-image `vector.control` evidence must show `relocatable = true` and no `trusted = true` or `superuser = false` setting: `vector` is therefore untrusted and creation requires superuser authority. Fresh databases: the external audited bootstrap-superuser session executes `SET ROLE mosaic_extension_owner`, creates and owns `mosaic_extensions`, then creates `vector` as `CREATE EXTENSION vector WITH SCHEMA mosaic_extensions`; it records the control-plane action and executes `RESET ROLE`. Under that role it revokes default `TABLES`, `SEQUENCES`, `FUNCTIONS`, `TYPES`, and `SCHEMAS` privileges from `PUBLIC`, `mosaic_schema_owner`, `mosaic_migrator`, `mosaic_data_importer`, and `mosaic_runtime`, then grants only the explicitly required read/type/function privileges. It validates `rolcanlogin=false`, `rolsuper=true`, zero role members, `pg_namespace.nspowner`, `pg_extension.extowner`, and owner-bearing extension-member ownership/schema/version (ownerless PostgreSQL catalog member classes are verified as ownerless, never falsely assigned), schema/default privileges, `RESET ROLE`, and the external audit record. Approved-owner update/relocation is likewise performed only by that external session while set to `mosaic_extension_owner`; the existing path is eligible only when `extowner` is already exact. `mosaic_extensions` remains non-writable by runtime, migrator, schema owner, importer, and every service role. They have no membership in `mosaic_extension_owner` and must fail catalog assertions plus `SET ROLE`, `CREATE`/`ALTER`/`DROP EXTENSION`, extension-member `UPDATE`/DDL, and role-membership change denials. Shadow migration and rollback repeat these owner/default-privilege/preflight assertions before copying, before atomic switch, after resume, and before read-only rollback.
|
||
|
||
PostgreSQL has **no supported** `ALTER EXTENSION ... OWNER TO`. No card may invent it, mutate system catalogs, use `DROP ... CASCADE`, or adopt legacy extension ownership. An existing `vector` is eligible for the clean in-place path only when `pg_extension.extowner` already resolves to the approved `mosaic_extension_owner`; after exact supported-version, `extrelocatable=true`, dependency, and complete expected member-set checks, the bootstrap actor may perform the tested `ALTER EXTENSION vector SET SCHEMA mosaic_extensions`. The same postflight verifies `extowner`, each member owner/schema, version, dependency inventory, and denial for runtime/migrator/schema owner.
|
||
|
||
A `vector` extension owned by a legacy runtime/single login is **not activated in place**. It fails closed before activation and requires this controlled shadow-database migration: (1) approved backup and read-only inventory; (2) create new database/roles; (3) the audited external bootstrap-superuser session `SET ROLE mosaic_extension_owner`, creates `mosaic_extensions`/`vector`, records the audit event, and `RESET ROLE`; (4) sole runner applies migrations and relocation; (5) copy data with vector dimensions, row counts, checksums, FK, and sequence evidence; (6) quiesce/drain writers; (7) apply and verify final delta; (8) prove role/TLS/readiness; (9) atomically switch connections; and (10) retain the old database read-only for the approved rollback window. Partial/cancel/resume and rollback preserve the old read-only source until the switch; no target unable to shadow-migrate is eligible until separately reviewed. Required tests cover fresh, approved-owner clean existing relocation, legacy-owner shadow path, partial/resume/rollback, N-1, exact `extowner`/member/schema/version assertions, and `SET ROLE`/ALTER/DROP/member-update denials for runtime, migrator, schema owner, importer, and all service roles.
|
||
|
||
**Superuser exception and threat boundary:** PostgreSQL superuser authority cannot be privilege-limited by `GRANT`/`REVOKE`; this is deliberately **not** a least-privilege claim for `mosaic_extension_owner`. The containment is its dedicated identity, `NOLOGIN`, zero membership, absence from runtime credentials/Vault/application containers, external control-plane-only audited use, and independent review. Any extension create/update/schema operation is a control-plane change requiring independent review, approved backup/rollback, maintenance window, and audit evidence. A managed target that cannot establish this exact `NOLOGIN SUPERUSER`, zero-member, externally controlled role is ineligible until an independently approved, versioned provider-owned extension-owner profile exists; it must not silently retain app/migrator ownership.
|
||
|
||
`schema.ts` must emit `mosaic_extensions.vector(...)`, and all vector casts/operators/functions in runner, application queries, fixtures, and generated SQL must explicitly qualify `mosaic_extensions` (for example `OPERATOR(mosaic_extensions.<->)`); `mosaic_extensions` is deliberately **not** added to runtime `search_path`.
|
||
|
||
Before relocation, the runner records a parameterized catalog inventory and dependency graph. It must classify, in dependency order, application enum/domain/base types; owned and identity sequences; application tables; table columns/defaults; functions/procedures; views/materialized views; extension-owned objects; then triggers, constraints/FKs, indexes, and dependent rules/policies. It moves base types, sequences, tables, and functions as required; OID-bound constraints/indexes follow their owning objects and are catalog-verified rather than recreated blindly. Every object must be expected exactly once and be in either the immutable legacy/bootstrap allowlist, the `mosaic` application allowlist, or the selected `mosaic_extensions` extension membership; unknown, extra, duplicate, cross-schema, dependency-cycle, or partial-resume state fails closed. No raw client-side identifier interpolation is allowed.
|
||
|
||
`KBN-101-00` owns bootstrap-role/schema/extension/default-privilege and direct-denial tests. `KBN-101-03` owns the runner integration tests that consume those bootstrap fixtures: approved-owner existing relocation, legacy-owner shadow migration, interrupted/partial relocation resume, partial/cancel shadow resume, and pre-activation reverse rollback-before-activation. Reverse rollback is allowed only before KBN-101-08 and restores the approved backup or the reviewed inverse relocation, never a runtime `search_path` bypass. Its N-1 order is: current legacy release → inactive bootstrap/runner and `mosaic` declarations → catalog relocation/generated-artifact verification or approved shadow migration → verified non-owner runtime activation. The combined evidence includes byte-immutable historical execution, no public application objects/declarations/future SQL, vector type/cast/operator query success under fixed `pg_catalog,mosaic`, exact extension-owner/member/schema/version assertions, catalog and direct ALTER/DROP/member-change denials for runtime/migrator/schema owner, and all extension eligibility negatives.
|
||
|
||
No SQL identifier may come from URL/config/environment/operator input. Catalog comparisons use parameter values. The fixed identifiers above are constants; the external bootstrap artifact alone may use server-side `format('%I', fixed_allowlisted_identifier)` after allowlist validation. Raw client-side interpolation for identifiers, `SET search_path`, database, schema, role, table, or extension names is forbidden. Tests include injection-shaped values, a poisoned pooled-session reset, and transaction `SET LOCAL` restoration negatives.
|
||
|
||
External bootstrap executes `REVOKE CONNECT, TEMPORARY ON DATABASE <fixed_database> FROM PUBLIC`, then grants `CONNECT` only to `mosaic_runtime`, `mosaic_migrator`, and the time-bounded external bootstrap actor while it is required. Certification fails if an unrelated login retains `CONNECT` or either application login retains `TEMPORARY`. Runtime receives `USAGE` on `mosaic`, named table/sequence grants through `mosaic_runtime_capability`, and `USAGE` on `drizzle` plus `SELECT` only on `drizzle.__drizzle_migrations` and `drizzle.__mosaic_migration_manifest`. `INSERT`, `UPDATE`, `DELETE`, `TRUNCATE`, and DDL rights on ledger/manifest are revoked. Revoke public CREATE and function EXECUTE; `SECURITY DEFINER` is forbidden unless a separately reviewed exception pins trusted path and grants only the capability role. Database TEMPORARY, role management, extension, schema, and object ownership are denied.
|
||
|
||
Immutable KBN relations, after KBN-100 creates them, grant runtime only `SELECT, INSERT` and explicitly deny `UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER`: `task_events`, `artifacts`, `task_checkpoints`, `task_checkpoint_artifacts`, and `approval_decision_artifacts`. KBN-100 retains RESTRICT/no-cascade semantics. Foundation certification verifies the role/schema boundary only; post-KBN-100 certification verifies this real deployed-role matrix.
|
||
|
||
## 5. Deployable verified-TLS bootstrap
|
||
|
||
`mosaicstack/stack` is the named repository/control plane. Ownership is intentionally non-overlapping: **KBN-101-00 exclusively owns** the versioned external bootstrap interface `infra/pg-bootstrap/roles.sql`, `infra/pg-bootstrap/extensions.sql`, and `infra/pg-bootstrap/README.md`, plus its bootstrap tests—roles, extension-owner transition, and no renderer/deployment manifests. **KBN-101-05 exclusively owns** `tools/db/render-postgres-secrets.ts`, its tests, the current `docker-compose.yml`, `docker-compose.federated.yml`, `deploy/portainer/federated-test.stack.yml`, `tools/federation-harness/docker-compose.two-gateways.yml`, and `apps/gateway/Dockerfile`; it consumes the versioned KBN-101-00 bootstrap interface and owns no bootstrap SQL. The named **Mosaic deployment control plane / Jason** is activation authority; the environment-specific IaC/Vault owner supplies only approved input secret versions and may not substitute an unreviewed current-repository artifact. The KBN-101-05 renderer is the only deployment handoff: it reads secret-provider references, validates owners/modes/digests/SANs, writes each output atomically (`mkstemp` on the target tmpfs, `fsync`, `chmod`/`chown`, atomic rename), and records only secret-version identifiers and hashes.
|
||
|
||
`KBN-101-05` changes the Gateway image to fixed non-root `USER 10001:10001`. Gateway CA and Gateway leaf-certificate mounts, and its own Gateway private key only when it terminates its HTTPS listener, must be readable by `10001:10001`; PostgreSQL private keys and migration-only material are never mounted there, and no secret is world-readable. PostgreSQL is not assigned a guessed UID/GID: its image must first be pinned by digest, and an image-inspection plus rendered Compose/Swarm test freezes the image's effective PostgreSQL UID:GID before the renderer selects mount owner/group. A digest, service UID/GID, rendered secret `uid`/`gid`/`mode`, or container `USER` mismatch is a KBN-101-05 failure. Mosaic applications never generate, self-sign, copy, or persist production certificates; the external bootstrap actor receives them only through the deployment secret mechanism and no plaintext development exception exists for production-like modes.
|
||
|
||
| Material | Vault target / deployment secret | Mount, injection, and authorized consumer |
|
||
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| Runtime URL | `secret-{env}/mosaic-stack/database/runtime` (`url`) → `mosaic-db-runtime-url-v1` | Gateway only: `/run/secrets/mosaic-db-runtime-url`, `0600`, `10001:10001`; entrypoint maps it to `DATABASE_URL` only at process exec. It is denied to every migrator, storage runtime, fleet, ordinary CLI, and test except an explicit runtime-negative fixture. |
|
||
| Migration URL | `secret-{env}/mosaic-stack/database/migrator` (`url`) → `mosaic-db-migrator-url-v1` | Each one-shot migrator only: `/run/secrets/mosaic-db-migrator-url`, `0600`, fixed migrator UID:GID asserted by the image/render test; entrypoint maps it only to `DATABASE_MIGRATION_URL`. It is denied to Gateway, storage runtime, fleet, and ordinary CLI. |
|
||
| Attestation signing key | `secret-{env}/mosaic-stack/database/migrate-target-attestation` (`private_key`) → `mosaic-db-migrate-target-ed25519-v1` | Runner root-wrapper only: fixed `/run/secrets/mosaic-db-migrate-target-ed25519`, `root:root` `0400`, referenced only by `MOSAIC_DB_ATTESTATION_SIGNING_KEY_FILE`; no importer/runtime/Gateway mount or log/export is permitted. |
|
||
| Attestation public key ring | versioned deployment public-key bundle → `mosaic-db-migrate-target-ed25519-public-v1` | Importer only: pinned `/run/mosaic-attestations/migrate-target.ed25519.pub`, `0444`; active/overlap key IDs are explicit and revoked IDs fail closed. It contains no private key. |
|
||
| Target attestation artifact | runner-produced non-secret file | Controlled shared mount `/run/mosaic-attestations/migrate-target.v1.json`, `0444`, atomic write/rename only; importer may read it but cannot write it. |
|
||
| CA bundle | `secret-{env}/mosaic-stack/database/tls-ca` (`certificate`) → `mosaic-db-ca-v1` | Gateway/migrator: `/run/secrets/mosaic-db-ca.crt`, `0444`, owned by the consuming UID:GID; CA is public trust material. PostgreSQL receives a distinct read-only CA copy only when client-cert validation is enabled. |
|
||
| Gateway leaf certificate | `secret-{env}/mosaic-stack/federation/gateway-server-tls` (`certificate`) → `mosaic-gateway-server-cert-v1` | Gateway only: `/run/secrets/mosaic-gateway-server.crt`, `0444`, `10001:10001`; renderer emits this exact Compose and Swarm target and validates it before start. |
|
||
| Gateway private key | same Vault record (`private_key`) → `mosaic-gateway-server-key-v1` | Gateway only: `/run/secrets/mosaic-gateway-server.key`, `0400`, `10001:10001`; not mounted to migrator or PostgreSQL and never world-readable. |
|
||
| PostgreSQL leaf certificate | `secret-{env}/mosaic-stack/database/postgres-server-tls` (`certificate`) → `mosaic-postgres-server-cert-v1` | PostgreSQL only: `/run/secrets/mosaic-postgres-server.crt`, `0444`, frozen verified postgres UID:GID. |
|
||
| PostgreSQL private key | same Vault record (`private_key`) → `mosaic-postgres-server-key-v1` | PostgreSQL only: `/run/secrets/mosaic-postgres-server.key`, `0400`, frozen verified postgres UID:GID; never mounted to Gateway or migrator. |
|
||
|
||
Compose uses identically named local **secret references** rendered by the named renderer into non-repository tmpfs paths; Swarm declares the same secrets and target paths with the tested `uid`, `gid`, and mode. KBN-101-05 rejects bind-mounted committed cert/key files, environment-encoded PEM, missing secrets, non-atomic renderer output, private-key access outside its named consumer (Gateway for Gateway key; PostgreSQL for PostgreSQL key), and any world-readable URL/key. The existing target Vault names are planned canonical paths and must be verified/provisioned by the deployment-owner input; the planning card does not claim they exist.
|
||
|
||
The server leaf SANs are frozen to actual connection DNS names, not a configurable alias:
|
||
|
||
| topology | PostgreSQL service DNS names that must be SANs | Gateway leaf SANs / consumers |
|
||
| ------------------------------------ | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| standalone compose | `DNS:postgres`, `DNS:localhost` (only for the documented host-port disposable test path) | `DNS:gateway`; its Gateway process consumes runtime URL + CA + Gateway leaf/key only |
|
||
| federated compose | `DNS:postgres-federated`, `DNS:localhost` (only for the documented host-port disposable test path) | `DNS:gateway-federated`; runtime/migrator consume only their database-specific material |
|
||
| Portainer/Swarm federated test stack | `DNS:postgres` (the in-stack service endpoint used by Gateway and migrator) | `DNS:gateway`; same consumer isolation |
|
||
| two-gateway harness | `DNS:postgres-a`, `DNS:postgres-b` | `DNS:gateway-a`, `DNS:gateway-b`; each Gateway gets only its own runtime URL, CA, and Gateway leaf/key; each `mosaic-db-migrator-{a,b}` gets only the matching migration URL and CA |
|
||
|
||
A new topology requires a versioned amendment before issuance. The PostgreSQL container activation artifact sets `ssl=on`, `ssl_cert_file`, and `ssl_key_file` to those paths, uses a locked-down `postgresql.conf` include, and verifies file ownership/mode before start. The server health/readiness gate makes a `verify-full` CA/SAN-validated connection as the approved runtime/migrator identity; `pg_isready` alone is insufficient. Migration Job starts only after server TLS readiness. Gateway replicas start only after a successful runner result and independently pass verified-TLS, identity, search-path, and ledger readiness. In the two-gateway harness this ordering occurs independently as `postgres-a → mosaic-db-migrator-a → gateway-a` and `postgres-b → mosaic-db-migrator-b → gateway-b`; no Gateway starts against its database before its own runner certificate succeeds.
|
||
|
||
**Fresh DB:** provision CA/leaf/secrets and server TLS configuration before initial database bootstrap; bootstrap/extension prerequisites run, then the runner migrates over verified TLS, then runtime deploys. **Existing DB:** take the approved backup, provision/mount TLS material, and **drain/scale to zero every N-1 runtime, worker, CLI maintenance process, and replica before TLS enforcement**. Enable server TLS, terminate any residual non-TLS PostgreSQL backend sessions, set `pg_hba.conf` to `hostssl` for all application/migrator CIDRs with no matching `host` rule, reload/restart as required, and prove the non-TLS session count is zero. Only then prove `verify-full` through the existing endpoint, run reconciliation/migration, and roll the non-owner TLS runtime. There is no plaintext transition interval.
|
||
|
||
**Rotation/rollback:** stage a CA bundle containing old+new trust roots to runtime/migrator, validate a new server leaf with exact SANs, restart PostgreSQL and validate it, roll consumers, then remove the old root only after evidence. Credential rotation remains independent and never mounts migration material into Gateway. Before expiry, rollback restores the prior known-valid leaf/key and overlapping CA bundle, restarts PostgreSQL, enforces `hostssl`, terminates residual non-TLS sessions, and verifies `verify-full`; it never downgrades `sslmode`, restores a plaintext-only N-1 runtime after enforcement, or accepts plaintext. A pre-enforcement abort may restore the backed-up N-1 state only before `hostssl` is enabled and is recorded as an aborted—not activated—release. The runbook records expiry windows, secret versions, backup ID, drained-service/session evidence, activation actor, and validation result—not secret values.
|
||
|
||
Required disposable tests cover standalone compose, federated/Swarm, and the two-gateway harness positives using verified TLS, plus for **both** gateway/database pairs: missing CA, wrong CA, wrong PostgreSQL SAN, wrong Gateway SAN, `sslmode` downgrade, missing/mispermissioned server or Gateway key, runtime-with-migrator-secret, cross-pair secret leakage, rendered secret-consumer/UID/GID/mode isolation, legacy plaintext drain/termination/`hostssl` enforcement, and readiness-before-migration negatives. PGlite local tests are explicitly classified as non-PostgreSQL and do not satisfy a PostgreSQL TLS test.
|
||
|
||
## 6. Runtime verification and sanitized failures
|
||
|
||
Before accepting traffic, runtime queries only parameterized/sanitized identity and privilege metadata on its already-open verified-TLS connection. It fails closed for owner/migrator identity or assumability; superuser/CREATEROLE/CREATEDB/BYPASSRLS; object/schema/database ownership; TEMPORARY; unexpected function execute; missing inherited capability/sequence/ledger grants; any non-exact search path; wrong schema/database target; bad TLS; or manifest/ledger mismatch. After KBN-100 it also checks every immutable grant/denial and relation presence.
|
||
|
||
The runner performs reciprocal preflight under the same session/lock: dedicated migration DTO only, verified TLS, exact allowlisted target, migrator `session_user`, schema-owner `current_user`, exact trusted search path, and no unsafe attributes. It fails before DDL otherwise.
|
||
|
||
Stable sanitized codes are `DATABASE_RUNTIME_URL_REQUIRED`, `DATABASE_MIGRATION_URL_REQUIRED`, `DATABASE_TLS_REQUIRED`, `DATABASE_TLS_VERIFICATION_FAILED`, `DATABASE_ROLE_UNSAFE`, `DATABASE_ROLE_GRANT_MISMATCH`, `DATABASE_SEARCH_PATH_UNSAFE`, `DATABASE_SCHEMA_MISMATCH`, `DATABASE_MIGRATION_RECONCILIATION_AMBIGUOUS`, `DATABASE_MIGRATION_LOCKED`, `DATABASE_MIGRATION_IDENTITY_UNSAFE`, `MIGRATE_TARGET_ATTESTATION_REQUIRED`, `MIGRATE_TARGET_ATTESTATION_INVALID`, `MIGRATE_TARGET_ATTESTATION_EXPIRED`, `MIGRATE_TARGET_ATTESTATION_REPLAYED`, and `MIGRATE_TARGET_BINDING_MISMATCH`. Logs/metrics may contain code, tier, manifest fingerprint, role class, and correlation ID only; never DSN, username, host, database name, SQL parameter, secret, or raw catalog result. External health exposes only unavailable/not-ready.
|
||
|
||
## 7. Safe DAG, activation, and rollback authority
|
||
|
||
Every KBN-101 card remains one PR with exclusive ownership. Cards `00`–`07` may merge only as **prepared, inactive capability**: no current owner-runtime deployment consumes their image/config, and no compatibility switch is exposed to a runtime operator. They must not retain `ALLOW_LEGACY_*`, runtime DDL, `DATABASE_URL` migration fallback, plaintext TLS, direct Drizzle, or test-only bypass flags. Current owner-runtime deployments remain on their known N-1 release until final activation.
|
||
|
||
| Card | Depends on | Complete, disjoint file/glob manifest and required test/evidence paths |
|
||
| ----------------------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `KBN-101-00` platform bootstrap / IaC | contract | **Only:** `infra/pg-bootstrap/roles.sql`, `infra/pg-bootstrap/extensions.sql`, `infra/pg-bootstrap/README.md`, `infra/pg-bootstrap/tests/**`. It creates the extension-owner role/schema/extension interface and proves fresh, approved-owner, legacy-shadow, catalog/default-privilege, and direct-denial bootstrap cases. No renderer, runner, Compose, CI, or deployment path. |
|
||
| `KBN-101-01` typed runtime config and verifier | 00 | **Only:** `packages/config/src/index.ts`, `packages/config/src/mosaic-config.ts`, `packages/config/src/mosaic-config.spec.ts`; `packages/db/src/client.ts`, `packages/db/src/defaults.ts`, `packages/db/src/connection-identity.ts`, `packages/db/src/client.spec.ts`, `packages/db/src/defaults.spec.ts`, `packages/db/src/connection-identity.spec.ts`; `apps/gateway/src/database/database.module.ts`, `apps/gateway/src/database/database.module.spec.ts`. It supplies runtime/migration/TLS DTO parsing plus runtime identity/search-path/readiness verification. No migrator, storage, installer, deploy, or CI path. |
|
||
| `KBN-101-03` sole runner, manifest, and schema foundation | 00,01 | **Only:** `.gitattributes`; `packages/db/package.json`; `packages/db/drizzle.config.ts`; `packages/db/src/cli.ts`, `packages/db/src/cli.spec.ts`, `packages/db/src/index.ts`, `packages/db/src/index.import-negative.spec.ts`, `packages/db/src/migrate.ts`, `packages/db/src/migrate.test.ts`, `packages/db/src/schema.ts`, `packages/db/src/schema.spec.ts`; `packages/db/src/migrator/**`; `packages/db/drizzle/*.sql`, `packages/db/drizzle/meta/*.json`; `docker/db-migrator.Dockerfile`, `docker/db-migrator.Dockerfile.spec.ts`; `packages/db/package-bin.spec.ts`. It alone publishes `"mosaic-db-migrator": "./dist/cli.js"`, verifies source/build/pack/discovery, and sets `ENTRYPOINT ["mosaic-db-migrator"]`; it exclusively owns `packages/db/src/migrator/target-attestation.dto.ts`, `target-attestation-signer.ts`, and their specs: fixed-key reference validation, JCS canonical payload, Ed25519 signing, producer TLS/identity/manifest binding, atomic artifact emission, and producer tests. It owns journal/manifest/ledger/lock/relocation/shadow tests. Shipped `0009` bytes stay unchanged. |
|
||
| `KBN-101-02` runtime DDL closure | 01,03 | **Only:** `docker/init-db.sql`, `infra/pg-init/01-extensions.sql`; `packages/storage/src/adapters/postgres.ts`, `packages/storage/src/adapters/postgres.spec.ts`, `packages/storage/src/factory.ts`, `packages/storage/src/factory.spec.ts`, `packages/storage/src/types.ts`, `packages/storage/src/tier-detection.ts`, `packages/storage/src/tier-detection.spec.ts`, `packages/storage/src/cli.ts`, `packages/storage/src/cli.spec.ts`, `packages/storage/src/migrate-tier.ts`, `packages/storage/src/migrate-tier.spec.ts`, `packages/storage/src/migrate-tier.integration.test.ts`; `apps/gateway/src/main.ts`, `apps/gateway/src/__tests__/integration/federated-boot.pg-unreachable.integration.test.ts`, `apps/gateway/src/__tests__/integration/federated-boot.success.integration.test.ts`, `apps/gateway/src/__tests__/integration/federated-pgvector.integration.test.ts`; `packages/db/src/federation.integration.test.ts`; `packages/mosaic/src/commands/fleet-backlog.ts`, `packages/mosaic/src/commands/fleet-backlog.spec.ts`. It consumes the -03 runner and exclusively owns importer verification/interface in `packages/storage/src/{cli,migrate-tier}.ts` and the named unit/integration specs: paired URL/attestation options, file/fd/TOCTOU checks, signature/key/expiry/replay/secret/digest/binding validation, post-TLS zero-DML comparison, and DDL classifier. It closes runtime/retired-init DDL only and excludes every -03 signer/runner, index, migrate, and Drizzle-config asset, and every deployment/CI/doc path. |
|
||
| `KBN-101-04` installer/wizard | 01 | **Only:** `packages/mosaic/src/stages/gateway-config.ts`, `packages/mosaic/src/stages/gateway-config.spec.ts`, `packages/mosaic/src/stages/gateway-config-cors.spec.ts`, `packages/mosaic/src/stages/wizard-menu.spec.ts`, `packages/mosaic/src/wizard.ts`. It persists only non-secret references/injected-variable contracts; source inspection excludes `tools/install.sh`, which does not read/write the database DSN. |
|
||
| `KBN-101-05` renderer and deployment | 00,03 | **Only:** `tools/db/render-postgres-secrets.ts`, `tools/db/render-postgres-secrets.spec.ts`; `apps/gateway/Dockerfile`, `apps/gateway/Dockerfile.spec.ts`; `docker-compose.yml`, `docker-compose.spec.ts`; `docker-compose.federated.yml`, `docker-compose.federated.spec.ts`; `deploy/portainer/federated-test.stack.yml`, `deploy/portainer/federated-test.stack.spec.ts`; `tools/federation-harness/docker-compose.two-gateways.yml`, `tools/federation-harness/docker-compose.two-gateways.spec.ts`. It consumes the -00 bootstrap interface and -03 immutable runner image, and exclusively renders/tests runner-only root-owned signing-key reference, importer-only public-key/key-ring, and controlled `0444` attestation artifact mount; it owns no bootstrap, runner, config, storage, or CI file. |
|
||
| `KBN-101-07` operator/runbook/docs | 02,03,04,05 | **Only:** `README.md`, `CLAUDE.md`, `docs/guides/admin-guide.md`, `docs/guides/dev-guide.md`, `docs/guides/deployment.md`, **`docs/guides/migrate-tier.md`**, `docs/guides/user-guide.md`, `docs/federation/SETUP.md`, `docs/federation/TASKS.md`, `docs/fleet/backlog-conventions.md`, `docs/PERFORMANCE.md`, `docs/design/storage-abstraction-middleware.md`, `docs/plans/2026-03-15-agent-platform-architecture.md`, `docs/runbooks/kbn-101-database-role-split.md`, `docs/reports/native-kanban-sot/kbn-101-operator-readiness-report.md`, `docs/native-kanban-sot/tests/kbn-101-operator-docs.spec.ts`. It exclusively owns the active migrate-tier operator route and documents the interfaces produced by -02/-03/-04/-05, including both file references, signing-key isolation, attestation bindings, rotation/replay, and no-connection/no-DML errors; it owns no source, storage, CLI, runner, or CI file. |
|
||
| `KBN-101-06` CI classifier and command matrix | 02,03,05,07 | **Only:** `.woodpecker/ci.yml`, `tools/ci/kbn101-ddl-inventory.ts`, `tools/ci/kbn101-ddl-inventory.spec.ts`, `tools/ci/fixtures/kbn101-ddl-inventory.json`, `tools/ci/kbn101-entrypoint-matrix.ts`, `tools/ci/kbn101-entrypoint-matrix.spec.ts`. It invokes the already-produced bin/image/deployment/doc artifacts and edits no producer file. Its inventory test enforces manifest overlap, ownerless, duplicate-owner, path-existence, allowlist, active-route field completeness, finite operator-document inventory, normative-contract non-masking, unknown-command, and historical/status-only masking failures; its matrix invokes the -02 secure target route and verifies every declared attestation refusal before target connection/DDL and post-connect mismatch with zero DML. |
|
||
| `KBN-101-08` foundation certification and **atomic activation release** | 00…07 | **Only evidence:** `docs/reports/native-kanban-sot/kbn-101-foundation-activation-certificate.md`, `docs/reports/native-kanban-sot/kbn-101-foundation-activation-evidence.json`. It changes no implementation path. Independent review and terminal-green CI must verify prepared artifacts before Mosaic control plane/Jason authorizes backup → drain/scale-zero N-1 → TLS → roles → runner → verified readiness → rolling runtime; any red result aborts. |
|
||
| `KBN-101-09` post-KBN-100 certification | KBN-100,08 | **Only evidence:** `docs/reports/native-kanban-sot/kbn-101-immutable-role-certificate.md`, `docs/reports/native-kanban-sot/kbn-101-immutable-role-evidence.json`. It changes no implementation path and records real deployed runtime INSERT/SELECT plus UPDATE/DELETE-denial evidence and independent security/Ultron approval. |
|
||
|
||
The manifests above are the complete ownership universe for KBN-101 implementation paths; the KBN-101-06 inventory test fails on overlap, an ownerless in-scope path, or a nonexistent declared path. Cards `00`–`07` are prepared artifacts, not independently deployed releases: the immutable N-1 owner-runtime image stays live until KBN-101-08 control-plane atomic activation. No activation card edits a source-changing path, and no runtime bypass or broken deployed intermediate exists.
|
||
|
||
**Authority:** Mosaic control plane/Jason is the sole activation and rollback authority. CI, Gateway, migrator, Coordinator, and Certifier cannot activate, waive a red result, or force release. Before an incompatible KBN-100 switch, the authority stops/scales runtime, uses the approved backup/restore or separately reviewed runner artifact, restores only a known TLS-compatible runtime with its runtime secret after `hostssl` enforcement, and verifies no plaintext sessions plus TLS/readiness. Migration URL is never injected into Gateway to enable rollback. KBN-100 starts only after KBN-101-08; KBN-105 starts only after KBN-101-09.
|
||
|
||
**ASSUMPTION K101-A2:** every eligible production-like deployment can schedule a dedicated migration Job/one-shot command and an operator/IaC-controlled TLS bootstrap. A target that cannot do both is ineligible for KBN certification.
|
||
|
||
## 8. Acceptance traceability
|
||
|
||
| Requirement / acceptance criterion | Required implementation evidence |
|
||
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| K101-REQ-01 / AC-K101-01 | DTO/command matrix covers local/PGlite, standalone, federated, and both harness pairs; every finite classified §2 path rejects `DATABASE_URL`-only before connection/DDL; no runtime fallback/default; `--help`, argv, import-compile, and live-operator-route negatives pass. The active migrate-tier route proves runner-produced JCS/Ed25519 attestation, root-owned signing/private-key and importer-public-key isolation, atomic artifact, exact `--target-url-file /run/secrets/mosaic_migrate_target_url` plus `--target-attestation-file /run/mosaic-attestations/migrate-target.v1.json`, secret-version/digest/TLS/CA/server/database/role/manifest/schema bindings, expiry/replay/rotation/TOCTOU checks, bounded non-DDL importer DML, and missing/wrong/stale/replayed/tampered/substituted/mismatched no-connection versus zero-DML refusal. |
|
||
| K101-REQ-02 / AC-K101-02 | KBN-101-03 one-session fixed two-int lock, `--run`/`--verify` exit-code, contention/crash/readiness/unrelated-key tests; manifest-v1 canonical bytes/digest and all reconciliation states; Gateway/replica DDL impossibility. |
|
||
| K101-REQ-03 / AC-K101-03 | KBN-101-00 bootstrap schema/extension-owner/default-privilege and direct-denial proof against actual PostgreSQL 17 + pgvector 0.8.2 control metadata (`trusted` absent/untrusted, `relocatable=true`); external-superuser `SET ROLE` create/update/`RESET ROLE` audit proof; `rolcanlogin=false`, `rolsuper=true`, zero members/no runtime credential proof; KBN-101-03 catalog relocation and future-Drizzle-only-`mosaic` proof plus approved-owner/shadow runner integration; `pg_namespace.nspowner`, `pg_extension.extowner`, owner-bearing member/schema/version, and runtime/migrator/schema-owner/importer/all-service-role catalog plus `SET ROLE`/ALTER/DROP/member-update denials; KBN-101-01/03 role, path, TEMP, ledger/default-grant, and pool-reset tests. |
|
||
| K101-REQ-04 / AC-K101-04 | KBN-101-00 bootstrap-interface and KBN-101-05 renderer/deployment tests; immutable-image exact Job commands and runner-before-readiness order; fresh/existing verified-TLS Compose/Swarm/two-gateway positives; both-pair CA/SAN/downgrade/key-permission negatives; exact UID/GID/mode and runtime/migrator secret-consumer rendering/CI negatives. |
|
||
| K101-REQ-05 / AC-K101-05 | KBN-101-09 after KBN-100: real deployed runtime INSERT/SELECT success and UPDATE/DELETE denial for each frozen relation, with RESTRICT retention evidence. |
|
||
| K101-REQ-06 / AC-K101-06 | KBN-101-00…08 prepared-card/no-intermediate-deploy evidence; one final activation authority record; N-1 drain/zero-plaintext-session/`hostssl`, backup/restore, CA overlap rotation, TLS-only rollback, Vault/redaction, and no-force-on-red evidence. |
|
||
| K101-REQ-07 / KBN sequence | KBN-101-08 foundation certificate before KBN-100, KBN-101-09 real immutable-role certificate plus Ultron approval before KBN-105; KBN-100 rebases/restores Drizzle consistency and never bypasses the serial gates. |
|
||
| Delivery integrity | One-card/one-PR DAG, exact file ownership, docs/link/contract checks, independent author≠reviewer re-review on the pushed exact head, and terminal-green CI for implementation cards. |
|
||
|
||
## 9. Non-goals and residual authority
|
||
|
||
KBN-101 planning does not create roles, certificates, Vault paths, migrations, deployment artifacts, or a deployed certificate. It does not replace KBN-100’s data migration, immutable retention, tenant constraints, or KBN-105 endpoint freeze. A PostgreSQL superuser/break-glass operator—including the deliberately isolated extension owner—remains outside application containment: `GRANT`/`REVOKE` cannot constrain that authority. Separate identity/non-login/zero-membership/external-control/audit controls, independent review, maintenance windows, backup/rollback evidence, and drills are mandatory.
|