Documentation
¶
Overview ¶
Package config is nova-config's library: the permanent, non-ephemeral configuration of the fleet, kept in Postgres (schema `config`) and applied into Redis so Redis is always a rebuildable copy (Glenn 2026-09-26: "redis is good as a hot store of data that can be rebuilt"; docs/SPEC-CONFIG.md).
Every kind of configuration is one Kind descriptor: its name, its table, its fields with their types and validators, and the two Redis functions that write and remove one row. The CLI's add, remove, set, list, show and history verbs, the SQL, the history rows and the apply diff are all generated from the descriptor, so a new kind is one descriptor, one migration and one Redis writer (docs/SPEC-CONFIG.md, "Adding a kind").
Index ¶
- Constants
- Variables
- func BeatKey(m string) string
- func Conflict(kind string, redis, wanted int64) error
- func FleetKey(field string) string
- func FriendBeatKey(f string) string
- func HistoryLine(c Change) string
- func Idem(kind string, rev int64) string
- func IsConflict(err error) bool
- func KindLine(k *Kind) string
- func KindNames() []string
- func LiveLine(b *Beat) string
- func MachineCeilingKey(m string) string
- func MachineKey(m string) string
- func OpLine(word string, kind string, op Op) string
- func Redact(dsn string) string
- func Refused(err error) bool
- func RowLine(k *Kind, row Row) string
- func ShowLine(k *Kind, row Row) string
- func SprintKey(field string) string
- func ValidateName(name string) error
- func Value(s string) string
- func Words(list string) []string
- type Applier
- type Beat
- type BeatReader
- type Change
- type Field
- type Kind
- type Mem
- func (m *Mem) Counts(_ context.Context) (map[string]int, error)
- func (m *Mem) Delete(ctx context.Context, kind, name string, actor string) (int64, error)
- func (m *Mem) Get(_ context.Context, kind, name string) (Row, bool, error)
- func (m *Mem) History(_ context.Context, kind, name string) ([]Change, error)
- func (m *Mem) Insert(ctx context.Context, kind string, row Row, actor string) (int64, error)
- func (m *Mem) List(_ context.Context, kind string) ([]Row, error)
- func (m *Mem) Rev(_ context.Context, kind string) (int64, error)
- func (m *Mem) Update(ctx context.Context, kind, name string, changes map[string]string, ...) (Row, int64, error)
- type Migration
- type Op
- type PG
- func (p *PG) Close() error
- func (p *PG) Counts(ctx context.Context) (map[string]int, error)
- func (p *PG) Delete(ctx context.Context, kind, name string, actor string) (int64, error)
- func (p *PG) Get(ctx context.Context, kind, name string) (Row, bool, error)
- func (p *PG) History(ctx context.Context, kind, name string) ([]Change, error)
- func (p *PG) Insert(ctx context.Context, kind string, row Row, actor string) (int64, error)
- func (p *PG) List(ctx context.Context, kind string) ([]Row, error)
- func (p *PG) Migrate(ctx context.Context) (from, to int, applied []int, err error)
- func (p *PG) Rev(ctx context.Context, kind string) (int64, error)
- func (p *PG) Update(ctx context.Context, kind, name string, changes map[string]string, ...) (Row, int64, error)
- func (p *PG) Version(ctx context.Context) (int, error)
- type RedisApplier
- func (a *RedisApplier) Beats(ctx context.Context, names []string) (map[string]*Beat, error)
- func (a *RedisApplier) Prepare(ctx context.Context) error
- func (a *RedisApplier) Read(ctx context.Context, kind string) (map[string]View, int64, error)
- func (a *RedisApplier) Remove(ctx context.Context, kind, name, actor, idem string) error
- func (a *RedisApplier) Stamp(ctx context.Context, kind string, prev, rev int64) error
- func (a *RedisApplier) Write(ctx context.Context, kind string, row Row, prev View, actor, idem string) error
- type RefusedError
- type Result
- type Row
- type Store
- type Type
- type View
Constants ¶
const ( KindMachine = "machine" KindFleet = "fleet" KindFriend = "friend" KindSprint = "sprint" )
The four kinds of this cut (docs/SPEC-CONFIG.md lists the planned ones).
const ( DeclKey = "config:decl" FriendsKey = "friends" MachinesKey = "machines" CapLogKey = "cap:log" )
The Redis keys apply writes, per kind. A friend's keys are the ones the nova-sprint verbs wrote by hand until now, through the same Redis Functions (internal/nsprint/fn/lua: capacity.lua's ns_capacity_desired for slots and tiers, friend_roles.lua's ns_friend_roles for roles). Her logins and wake path are what she would just know: her own presence writes them, apply never touches friends:login or friend:<f>:wakepath. A machine's ceiling goes through ns_capacity_machine; its registry row has no writer in the library, so it is the hash machine:<m> and the set `machines`, both nova-config's own. The fleet row is two plain keys of nova-config's own, fleet:store and fleet:coordinator, each a machine's name; the sprint row is sprint:coordinator, a friend's name (each absent when the row names none).
config:decl is the stamp: rev:<kind> is the Postgres revision last applied and at:<kind> the server time it was written (the shape of friends:decl in friend_declare.lua).
const ( OpAdd = "add" OpSet = "set" OpRemove = "remove" )
The three operations a history row records.
const CoordinatorRole = "coordinator"
CoordinatorRole is the Redis role ns_friend_roles and the deal read (friend:<f>:roles), derived at apply from the sprint row.
Variables ¶
var ( ErrCeiling = errors.New("ceiling") ErrActor = errors.New("actor") ErrInUse = errors.New("in use") )
The refusals the runtime's functions answer with.
var ( ErrExists = errors.New("exists") ErrNotFound = errors.New("not found") ErrNoRef = errors.New("names no row") ErrReferenced = errors.New("is named by") )
The refusals a store returns; the CLI turns each into one line with its remedy.
var FriendRoles = []string{"builder", "may-hold", "reader"}
FriendRoles are the roles someone decides for a friend. The coordinator role is not one: who coordinates is the sprint row's one field, and apply derives the Redis role from it (Kind.Derive below), so ns_friend_roles still sees exactly one coordinator.
var Kinds = []*Kind{ { Name: KindMachine, Table: "machines", Doc: "a machine of the fleet, named by its tailnet host: the login, the seat, and how many cards and runners it takes", Fields: []Field{ {Name: "user", Type: TypeText, Required: true, Help: "the login the plays and seals use on it (ssh <user>@<name>)"}, {Name: "seat", Type: TypeText, Required: true, Help: "its nova-secrets seat (studio, swarm-hulk, ...)"}, {Name: "slots", Type: TypeInt, Required: true, Help: "how many cards it may run at once, the machine ceiling (machine:<m>:ceiling); 0 runs none"}, {Name: "runners", Type: TypeInt, Help: "how many CI runners it hosts; 0 (the default) hosts none"}, }, }, { Name: KindFleet, Table: "fleet", Singleton: true, Doc: "the one row of fleet-wide facts: which machine is the store and which the coordinator", Fields: []Field{ {Name: "store", Type: TypeRef, Ref: KindMachine, Help: "the machine that runs Redis and Postgres (a machine row), or empty"}, {Name: "coordinator", Type: TypeRef, Ref: KindMachine, Help: "the machine the coordinator's loops run on (a machine row), or empty"}, }, }, { Name: KindFriend, Table: "friends", Doc: "an AI friend: how wide she runs, which tiers she can do, and her roles", Fields: []Field{ {Name: "slots", Type: TypeInt, Required: true, Help: "her desired slots, under the ceiling of the machine her beat reports"}, {Name: "tiers", Type: TypeList, Enum: Tiers, Required: true, Help: "which tiers she can do: comma list of " + strings.Join(Tiers, ", ")}, {Name: "roles", Type: TypeList, Enum: FriendRoles, Help: "comma list of " + strings.Join(FriendRoles, ", ") + " (who coordinates is the sprint row's)"}, }, ApplyOrder: func(r Row) int { if hasWord(r.Fields["roles"], CoordinatorRole) { return 0 } return 1 }, Derive: deriveCoordinator, }, { Name: KindSprint, Table: "sprint", Singleton: true, Doc: "the one row of sprint-global facts: which friend coordinates", Fields: []Field{ {Name: "coordinator", Type: TypeRef, Ref: KindFriend, Help: "the friend who holds the coordinator role (a friend row), or empty; set it to hand over"}, }, }, }
Kinds is the registry, in apply order: machines first, the fleet row next (it names machines, and a friend's desired slots are charged to the fleet's coordinator machine when her beat names none), friends, and the sprint row last (it names a friend).
A machine's record is exactly the declared facts something reads, one reader each, and nothing invented (Glenn 2026-09-27: "I only want the fleet to have actual defined useful things associated with each machine, not invented rando stuff"). Its name is the tailnet host: `ssh <name>` reaches it, so there is no address field ("All fleet machines must be on the tailnet. This is a hard requirement."). Measured facts (os, arch, cores, memory) are never typed: they come live from the machine's own beat (Beat, docs/SPEC-CONFIG.md, "Declared and measured").
var NamePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
NamePattern is the shape of a row key: lower-case, digits and dashes, the registry key friends and machines already use in Redis.
var Tiers = []string{"flash", "frontier", "pro"}
Tiers are the model tiers a friend can do, capacity.lua's filter_ok spelling (frontier, pro, flash).
Functions ¶
func BeatKey ¶
BeatKey is the machine's own heartbeat, written by the bench that runs on it (nova-sprint bench beat): host, at (unix ms), load1, ncpu, cpu today; os, arch and memory_gb when the beat carries them. nova-config reads it, never writes it.
func FleetKey ¶
FleetKey is the plain key one fleet field is written to: fleet:store, fleet:coordinator.
func FriendBeatKey ¶
FriendBeatKey is a friend's own heartbeat (nova-friend): its host field is the machine she runs on now, what her desired slots are charged to.
func HistoryLine ¶
HistoryLine is one change: `HISTORY id=<n> kind=<k> name=<n> op=<add|set|remove> actor=<a> at=<rfc3339>` then, for a set, each changed field as `<field>=<before>><after>`; for an add every field's value; for a remove every field's last value.
func Idem ¶
Idem is the idempotency marker every apply write carries into cap:log: the kind and the revision it applied.
func IsConflict ¶
IsConflict reports whether err is the compare-and-set refusal.
func KindLine ¶
KindLine is one line of `nova-config kinds`: `CONFIG KIND name=<k> table=config.<t> fields=<f,g,...> required=<f,...> rows=many|one` (one: a singleton kind, whose row the migration creates).
func LiveLine ¶
LiveLine is the measured facts a machine's list and show lines carry after the declared fields when Redis is at hand (docs/SPEC-CONFIG.md, "Declared and measured"): ` os=<v> arch=<v> cores=<n> memory_gb=<n> beat=<rfc3339>`, each `-` when the beat does not carry it, and `beat=none` alone when the machine has no beat. Nothing here is stored or typed: it is what the machine reported last.
func MachineCeilingKey ¶
MachineCeilingKey is what ns_capacity_machine writes.
func OpLine ¶
OpLine is one line of an apply or a check: `APPLY ADD kind=<k> name=<n>`, `APPLY SET kind=<k> name=<n> changed=<f,g>`, `APPLY REMOVE kind=<k> name=<n>`; the first word is CHECK when nothing is written.
func Refused ¶
Refused reports whether err is a store refusal (exit 1), as opposed to a store that could not answer (exit 2).
func RowLine ¶
RowLine is a row: `FRIEND name=<n> <field>=<v> ...`, every field of the kind in declaration order.
Types ¶
type Applier ¶
type Applier interface {
// Read returns Redis's rows of the kind by name, and the revision of the
// kind stamped in config:decl (0 when none). It writes nothing, so
// --check can call it against a store whose function library is
// missing.
Read(ctx context.Context, kind string) (map[string]View, int64, error)
// Prepare is called once before the first write: it installs the Redis
// Function library when the store has none (fn.LoadMissing).
Prepare(ctx context.Context) error
// Write adds (prev nil) or updates one row through the runtime's own
// functions. A refusal (CEILING, an actor without the role) is a
// *RefusedError.
Write(ctx context.Context, kind string, row Row, prev View, actor, idem string) error
// Remove removes one row's keys. A row the runtime still holds (a
// machine with consumers) is a *RefusedError naming them, and
// nothing is written.
Remove(ctx context.Context, kind, name, actor, idem string) error
// Stamp records rev as the kind's applied revision, compare-and-set
// against prev: a store whose stamp moved past prev is ErrConflict.
Stamp(ctx context.Context, kind string, prev, rev int64) error
}
Applier is the Redis side of apply. RedisApplier is the one over go-redis; the unit tests hand in a fake.
type Beat ¶
type Beat struct {
OS, Arch, Cores, MemoryGB, At string
}
Beat is what a machine reported last, read from BeatKey: never stored in Postgres, never typed (Glenn 2026-09-27: measured facts coming live is "more robust"). Cores is the beat's ncpu; OS, Arch and MemoryGB are "" until the beat carries them; At is the beat's time as RFC 3339 UTC.
type BeatReader ¶
BeatReader reads the beats of named machines; nil for one with no beat.
type Change ¶
type Change struct {
ID int64
Kind string
Name string
Op string // OpAdd, OpSet or OpRemove
Before map[string]string
After map[string]string
Actor string
At string // RFC 3339 UTC
}
Change is one history row.
type Field ¶
type Field struct {
Name string
Type Type
// Required is true when add refuses a row without it. set never requires
// a field: it updates the ones named.
Required bool
// Enum is the word list of a TypeEnum or TypeList field.
Enum []string
// Ref is the kind a TypeRef field names.
Ref string
// Help is the flag's help line, one sentence.
Help string
}
Field is one column of a kind: the flag `--<Name>` on add and set, the column of the same name (quoted) in Postgres, and the key on every typed line.
type Kind ¶
type Kind struct {
// Name is the kind's word in the grammar (`nova-config <kind> ...`), the
// singular; Table is its table under schema config.
Name string
Table string
// Fields are the columns after the row key `name`, in the order every
// line prints them and every migration declares them.
Fields []Field
// Doc is the one sentence `nova-config kinds` prints about the kind.
Doc string
// Singleton is a kind of exactly one row, named as the kind is (the
// fleet: Glenn 2026-09-27, "in the fleet there is only one coordinator
// at a time"). Its migration creates the row, so the grammar has no add,
// remove or list and its set, show and history take no name
// (docs/SPEC-CONFIG.md, "Singleton kinds").
Singleton bool
// Derive, when set, is run by Apply on the kind's rows before they are
// planned: a value another kind's row decides (the sprint's coordinator
// as a friend's Redis role) is added here, so Redis holds it and the
// stored row does not. nil derives nothing.
Derive func(ctx context.Context, st Store, rows []Row) ([]Row, error)
// ApplyOrder sorts the rows of this kind for apply: a row with a lower
// number is written first. nil keeps name order. Friends put the
// coordinator first so ns_friend_roles' bootstrap has one.
ApplyOrder func(r Row) int
}
Kind is one kind of configuration. See the package comment.
func (*Kind) Changes ¶
Changes validates the named fields of a set and returns their canonical values. Every problem is reported at once.
func (*Kind) FieldNames ¶
FieldNames lists the fields in declaration order.
func (*Kind) NewRow ¶
NewRow builds a canonical row of the kind from raw flag values: every field named in raw is validated, every required field must be present, and every problem is reported in one error so a first run is refused once (docs/ONBOARDING.md point 2). Fields not named are "" (an int, 0).
type Mem ¶
Mem is the in-memory Store the unit tests use. It is strict like PG: the same refusals, the same history, the same revision.
func NewMem ¶
func NewMem() *Mem
NewMem returns an empty store: no rows of any kind but the singleton kinds' one row each, as a migrated Postgres has.
type Migration ¶
Migration is one embedded migration.
func Migrations ¶
Migrations lists the embedded migrations in version order. A file whose name does not begin with its number, or a number declared twice, is an error: the ledger keys on the number.
type Op ¶
type Op struct {
Op string // OpAdd, OpSet or OpRemove
Name string
Changed []string // the fields that differ, for OpSet
Row Row // the row to write, for OpAdd and OpSet
Prev View // Redis's row, for OpSet and OpRemove
}
Op is one line of a plan.
func Plan ¶
Plan diffs the kind's rows against Redis's views: an add for a row Redis lacks, a set for one that differs in any field, a remove for a name in Redis that Postgres has not. Adds and sets come in the kind's apply order, then removes by name, so a machine's ceiling is written before a friend on it is, and a friend is unregistered after everything else.
type PG ¶
type PG struct {
// contains filtered or unexported fields
}
PG is the Postgres Store. Open it with OpenPG; the DSN carries no password on a command line (cmd/nova-config resolves it from the environment).
func OpenPG ¶
OpenPG opens the store and pings it once, so a wrong address or login is refused here rather than on the first verb.
func (*PG) Migrate ¶
Migrate applies every embedded migration the ledger lacks, each in its own transaction with its ledger row, and returns the version before, the version after and the versions applied. Running it twice applies nothing the second time.
type RedisApplier ¶
type RedisApplier struct {
Client *redis.Client
// Now is the stamp's clock (time.Now when nil).
Now func() time.Time
}
RedisApplier is the Applier over a live store.
func (*RedisApplier) Prepare ¶
func (a *RedisApplier) Prepare(ctx context.Context) error
Prepare installs the nova_sprint function library when the store has none (fn.LoadMissing: never replaces a deployed one).
func (*RedisApplier) Remove ¶
func (a *RedisApplier) Remove(ctx context.Context, kind, name, actor, idem string) error
type RefusedError ¶
RefusedError is a store refusal with the words for the line: Err is one of the sentinels above, Detail says which row or field.
func RedisRefusal ¶
func RedisRefusal(format string, args ...any) *RefusedError
RedisRefusal is a *RefusedError with a formatted detail.
func (*RefusedError) Error ¶
func (e *RefusedError) Error() string
func (*RefusedError) Unwrap ¶
func (e *RefusedError) Unwrap() error
type Result ¶
type Result struct {
Kind string
Rev int64 // Postgres's revision of the kind
Add, Set, Remove int
Ops []Op
Check bool // --check: nothing was written
RedisRev int64
PreparedForWritesOK bool
}
Result is one kind's apply.
func Apply ¶
func Apply(ctx context.Context, st Store, ap Applier, kind, actor string, check bool, report func(Op)) (Result, error)
Apply applies one kind from the store into Redis (docs/SPEC-CONFIG.md, "Apply"): read Postgres and its revision, read Redis and its stamp, refuse CONFLICT when Redis is ahead, plan, and unless check is set write every op in order and stamp the revision. Every op is reported to report before it is written, so a refusal part way names what was written before it.
type Row ¶
type Row struct {
Name string
Fields map[string]string
// CreatedAt and UpdatedAt are RFC 3339 UTC, "" for a row a store has not
// stamped (a row built from flags).
CreatedAt, UpdatedAt string
}
Row is one row of a kind: its name and every field as canonical text (ints as digits, lists sorted and comma joined, "" for an empty value). Values are text end to end so the descriptor, not the row, knows the type.
type Store ¶
type Store interface {
// Get reads one row; found is false when there is none.
Get(ctx context.Context, kind, name string) (row Row, found bool, err error)
// List reads every row of a kind, by name.
List(ctx context.Context, kind string) ([]Row, error)
// Insert adds a row. A row of that name is ErrExists; a ref field naming
// no row of its kind is ErrNoRef. It returns the history id.
Insert(ctx context.Context, kind string, row Row, actor string) (int64, error)
// Update changes the named fields of a row (ErrNotFound when there is
// none) and returns the row after and the history id.
Update(ctx context.Context, kind, name string, changes map[string]string, actor string) (Row, int64, error)
// Delete removes a row (ErrNotFound when there is none; ErrReferenced
// when a row of another kind names it) and returns the history id.
Delete(ctx context.Context, kind, name string, actor string) (int64, error)
// History reads the change rows of one row, oldest first.
History(ctx context.Context, kind, name string) ([]Change, error)
// Rev is the kind's revision: the greatest history id of the kind, 0
// when it has none. apply stamps it into Redis.
Rev(ctx context.Context, kind string) (int64, error)
// Counts is the row count per kind that has many (a singleton kind is
// always one row and is left out).
Counts(ctx context.Context) (map[string]int, error)
}
Store is the permanent store: Postgres in production (PG), Mem in the unit tests. Every write is one transaction that changes the row AND appends its history row (kind, name, op, before, after, actor, at); there is no write without a record (docs/SPEC-CONFIG.md, "History").
type Type ¶
type Type string
Type is a field's type. It decides the SQL column, the flag's parsing and the validator.
const ( // TypeText is free text, stored as text, escaped on the typed line. TypeText Type = "text" // TypeInt is a non-negative integer, stored as integer. TypeInt Type = "int" // TypeEnum is one word from Field.Enum, stored as text. TypeEnum Type = "enum" // TypeList is a comma list of words from Field.Enum, deduplicated and // sorted, stored as text ("" is the empty list). TypeList Type = "list" // TypeNames is a comma list of names (Field.Pattern each), deduplicated // and sorted, stored as text. TypeNames Type = "names" // TypeRef is the name of a row of another kind (Field.Ref), stored as // text with a foreign key. TypeRef Type = "ref" )
type View ¶
View is one row as Redis holds it: the kind's fields in the same canonical text as a Row, read back from the keys the runtime tools read. The diff is Row.Fields against View, field by field, so Redis is always a copy of Postgres and never the other way round (docs/SPEC-CONFIG.md, "Apply").