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.
65 lines
2.6 KiB
Go
65 lines
2.6 KiB
Go
package main
|
|
|
|
// postgres's provisioner — the adapter that makes postgres a provider of the mesh
|
|
// `postgres-database` interface (novox/hq ADR 0039/0040/0048). A consumer connects to a database it
|
|
// alone owns, as `as` with the password the mesh minted.
|
|
//
|
|
// **The role name and password are the mesh's, not the provisioner's (ADR 0048).** postgres creates
|
|
// a role and a same-named database under exactly that login — a name the consumer cannot learn is a
|
|
// database it cannot reach.
|
|
//
|
|
// **Extensions are the provider's to install.** A contribution may name extensions
|
|
// (`"extensions": ["vector"]`); most are not trusted, so only the superuser this module holds can
|
|
// create them, in the consumer's database, on every pass, and never drops one.
|
|
|
|
import (
|
|
"context"
|
|
)
|
|
|
|
// provisioner is the adapter over the client; announce emits a lifecycle event.
|
|
type provisioner struct {
|
|
pg *Client
|
|
announce func(event string, body map[string]string)
|
|
}
|
|
|
|
func (a provisioner) Create(ctx context.Context, p Provision) error {
|
|
// Read before anything runs: a malformed list is refused without touching the server.
|
|
extensions, err := Extensions(p.Values)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Database and owning role share the consumer's login, so the consumer owns exactly its own.
|
|
database := p.As
|
|
if err := a.pg.CreateDatabaseAndRole(ctx, database, p.As, p.Password); err != nil {
|
|
return err
|
|
}
|
|
if err := a.pg.EnsureExtensions(ctx, database, extensions); err != nil {
|
|
return err
|
|
}
|
|
a.announce("database.provisioned", map[string]string{"consumer": p.Consumer, "database": database, "user": p.As})
|
|
return nil
|
|
}
|
|
|
|
// Remove withdraws, never drops (novox/hq issue 241). The login is locked and the database kept under
|
|
// its own name: on 2026-10-04 a misread contributions file withdrew every consumer at once, and
|
|
// dropping made that a loss of seven databases. Taking a database out of service is a person's act —
|
|
// postgres_retire_database — and even that renames rather than drops.
|
|
func (a provisioner) Remove(ctx context.Context, as string, _ map[string]any) error {
|
|
if err := a.pg.LockRole(ctx, as); err != nil {
|
|
return err
|
|
}
|
|
a.announce("database.deprovisioned", map[string]string{"database": as, "kept": "true"})
|
|
return nil
|
|
}
|
|
|
|
// Holds is asked every minute: whether the consumer can still log in as the mesh gave it, and finds
|
|
// the extensions it asked for, so a login or extension lost behind the provisioner's back is made
|
|
// again (novox/hq issue 120).
|
|
func (a provisioner) Holds(ctx context.Context, p Provision) (bool, error) {
|
|
extensions, err := Extensions(p.Values)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return a.pg.Holds(ctx, p.As, p.As, p.Password, extensions)
|
|
}
|