Files
mesh-catalog/modules/postgres
jochen 4f44bf1eb5
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds postgres, sent nowhere; no bus step; 2 wait(s) for a person; every machine composes with the change as it did without (4 of 4 …
mesh/repo-check pass: its merge-check.sh passed
Give the store's reader only the columns a module declares readable, never a secret, so a query cannot read the controller's signing key (hq issue 337)
mesh_store_reader held pg_read_all_data, so every caller of mesh-store.query
and postgres_query could read every column the store holds: the signing key
nodes trust, sealed secrets, password hashes. The reader now holds no role;
each minute, on every database, its memberships are revoked and its grants
brought to what the consumer's contribution lists under readable, minus any
column or table whose name says it holds a secret. Before a statement runs,
the reader's own session is asked which secret columns it can read and the
statement is refused if any; a statement naming a known secret table is
refused before it is sent.
2026-10-08 21:43:15 +02:00
..

postgres

The mesh store (novox/hq ADR 0079): one PostgreSQL server that provides the postgres-database provision to every module on any machine that requires one, and holds the mesh-store seat.

Each consumer is given a database it alone owns, under the login the mesh derived for it and the password the mesh minted (ADR 0048). The provider never invents either: it makes exactly what the mesh handed both ends, so they agree by construction.

Requiring a database

A consumer requires postgres-database and contributes what it wants:

"requires":    ["postgres-database"],
"contributes": {"postgres-database": {"name": "letta", "extensions": ["vector"]}},
"binds":       {"postgres-database": "${dir:state}/database.json"},
"secrets":     {"postgres-database": "${dir:state}/database.secret"}
field meaning
name what the consumer calls its database. The database and its owning login are both named by the login the mesh derives (${bound:postgres-database:as}), so that is the name to connect to.
extensions optional; a list of extension names to install in the consumer's database, e.g. ["vector"] for pgvector.

Extensions are the provider's to install. Most extensions are not trusted (pgvector is superuser=t, trusted=f), so the login that owns the database cannot create them. The provider does, as the superuser, connected to that database, with CREATE EXTENSION IF NOT EXISTS — on every provisioning pass, so a consumer that adds a name to its contribution, or a database that predates this, gets it on the next pass, and a second pass changes nothing. Only a name the server lists in pg_available_extensions is installed; any other is refused, by name, in the provider's log: the consumer's database and login are still made, but none of its extensions is installed until the contribution is corrected. An extension is never dropped — not when it leaves the list, not when the consumer goes.

The minute-by-minute check (issue 120) connects as the consumer and looks for each extension it asked for, so one removed by hand is installed again.

What is never done

  • No database is dropped by the mesh on its own. A consumer the mesh no longer asks for is retired (novox/hq ADR 0230), once the same result holds for five passes and ten minutes and, if it is more than three consumers or more than half of those held, once a person approved: its login is set NOLOGIN, its sessions are ended, its role's comment marks it retired with when and why, and its database stays exactly as it was under its own name — still backed up. A consumer asked for again is enabled at once with the same database. Only cleanup delete, a person's act through the controller, drops it.

  • postgres_retire_database renames, it does not drop: the database becomes <name>_deleted_<yyyymmdd> and its owner is locked. Removing the data is a person's act, by hand.

  • A caller's statement never runs as the superuser. postgres_query and the seat's query verb run as mesh_store_reader — every session read-only by the server's own setting, a 60 s statement timeout — with the statement sent as given (issue 193). Without the reader's password (own-secrets.reader) the call is refused.

  • The reader reads only what a module declares readable, and never a secret (issue 337). It holds no role — until issue 337 it held pg_read_all_data, which let any caller of the seat's query read the controller's signing key — and is granted per column, in each consumer's database, what that consumer's contribution lists:

    "contributes": {"postgres-database": {"name": "letta", "readable": ["agents", "steps(id, created_at)"]}}

    An entry is [schema.]table (every column it has, at each pass) or [schema.]table(column, …). A database no contribution names — the controller's own among them — gives the reader nothing. A column or table whose name says it holds a secret (private, secret, sealed, token, password, credential, salt, a key) is never granted, declared or not, and the refusal is reported. Every minute, on every database, the reader's role memberships are revoked and its grants brought to exactly that, so an existing install is narrowed without anybody acting and a narrowed declaration takes effect at the next pass. Before a caller's statement runs, the reader's own session is asked which secret-named columns it can read (a grant to PUBLIC or by hand would show there); if any, the statement is refused and the columns named. A statement that names a known secret table of the controller's, or a secret-sounding name, is refused before it is sent — a second layer, not the guard.

Tools

tool answers
postgres_list_databases every non-template database with its size
postgres_query {database, sql} one read-only statement, as the reader; rows keyed by column, NULL as null
postgres_retire_database {database, confirm} renames a database aside and locks its owner; confirm repeats the name
mesh-store.databases, mesh-store.query the store seat's verbs: the same listing and read-only query
postgres_reader_grants what the reader may read in each database as the last pass left it: readable columns, refused secrets, declared but missing, revoked, and any secret it can still read; names only
provisioner_retirement what is held, what waits for a person, what was rejected, every retired consumer and set-aside database with when, why and size
provisioner_retire_approve, provisioner_retire_reject {consumers, why, by} a person's answer to a set waiting; through the controller's `retire approve
provisioner_delete {consumer, confirm, why, by} drops one retired consumer's database and role, or one set-aside database; through the controller's cleanup delete

Where the code lives

One Go bundle, cmd/postgres-provider, launched by the node's runtime and speaking MCP over stdio through the Go SDK (ADR 0193). Beside the tools it runs the provisioner: every five seconds it reads the contributions file the mesh writes (MESH_RECEIVES), applies each consumer whose login, password or contribution changed, and retires what is no longer listed (retirement.go); a file it cannot read retires nobody. It is the TypeScript SDK's runProvisioner loop, carried in the module until the Go SDK has one, byte for byte the same as keycloak's.

go test ./... runs against a fake server. MESH_POSTGRES_LIVE=postgres://postgres:…@host:port/postgres also runs live_test.go against a real, throwaway one (see the file for a pgvector/pgvector container): the extension installed and a second pass a no-op, the reader unable to write, a retired login locked out with its data kept and listed retired, enabled again as it was, and a deletion dropping only its own database and role. live_reader_test.go and readable_live_test.go need no extension: an install's reader as issue 337 found it reading no secret after its first query, and the reconcile granting exactly what is declared, refusing secrets, taking back what is no longer declared, and finding a grant to PUBLIC.