letta crash-loops on 'type "vector" does not exist': pgvector is not a trusted extension, so only the provider's superuser can create it, and the provisioner never did. A contribution may now name extensions; the provider creates each (IF NOT EXISTS, available ones only) in the consumer's database on every pass. Go per the standing rule for a TypeScript module that changes. letta asks for vector.
72 lines
4.2 KiB
Markdown
72 lines
4.2 KiB
Markdown
# 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 ever dropped.** A consumer the mesh no longer asks for is *withdrawn*: its login is
|
|
set `NOLOGIN` and its sessions are ended, and its database stays as it was under its own name
|
|
(issue 241). A consumer that comes back is given the same database.
|
|
- **`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` — `pg_read_all_data` and nothing else, 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.
|
|
|
|
## 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 |
|
|
|
|
## 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 withdraws each one no longer listed; a file it cannot read withdraws nobody.
|
|
It is the TypeScript SDK's `runProvisioner` loop, carried in the module until the Go SDK has one.
|
|
|
|
`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
|
|
withdrawn login locked out with its data kept.
|