config

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 28, 2026 License: MIT Imports: 18 Imported by: 0

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

View Source
const (
	KindMachine = "machine"
	KindFleet   = "fleet"
	KindFriend  = "friend"
	KindSprint  = "sprint"
)

The four kinds of this cut (docs/SPEC-CONFIG.md lists the planned ones).

View Source
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).

View Source
const (
	OpAdd    = "add"
	OpSet    = "set"
	OpRemove = "remove"
)

The three operations a history row records.

View Source
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

View Source
var (
	ErrCeiling = errors.New("ceiling")
	ErrActor   = errors.New("actor")
	ErrInUse   = errors.New("in use")
)

The refusals the runtime's functions answer with.

View Source
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.

View Source
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.

View Source
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").

View Source
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.

View Source
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

func BeatKey(m string) string

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 Conflict

func Conflict(kind string, redis, wanted int64) error

Conflict is the refusal Stamp returns when the stamp moved.

func FleetKey

func FleetKey(field string) string

FleetKey is the plain key one fleet field is written to: fleet:store, fleet:coordinator.

func FriendBeatKey

func FriendBeatKey(f string) string

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

func HistoryLine(c Change) string

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

func Idem(kind string, rev int64) string

Idem is the idempotency marker every apply write carries into cap:log: the kind and the revision it applied.

func IsConflict

func IsConflict(err error) bool

IsConflict reports whether err is the compare-and-set refusal.

func KindLine

func KindLine(k *Kind) string

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 KindNames

func KindNames() []string

KindNames lists the kinds in apply order.

func LiveLine

func LiveLine(b *Beat) string

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

func MachineCeilingKey(m string) string

MachineCeilingKey is what ns_capacity_machine writes.

func MachineKey

func MachineKey(m string) string

MachineKey is the machine's registry hash.

func OpLine

func OpLine(word string, kind string, op Op) string

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 Redact

func Redact(dsn string) string

Redact returns the DSN with any password replaced, for a line.

func Refused

func Refused(err error) bool

Refused reports whether err is a store refusal (exit 1), as opposed to a store that could not answer (exit 2).

func RowLine

func RowLine(k *Kind, row Row) string

RowLine is a row: `FRIEND name=<n> <field>=<v> ...`, every field of the kind in declaration order.

func ShowLine

func ShowLine(k *Kind, row Row) string

ShowLine is RowLine with the row's stamps.

func SprintKey

func SprintKey(field string) string

SprintKey is the plain key one sprint field is written to: sprint:coordinator.

func ValidateName

func ValidateName(name string) error

ValidateName checks a row key.

func Value

func Value(s string) string

Value renders one field value.

func Words

func Words(list string) []string

Words splits a canonical list ("" is none).

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

type BeatReader interface {
	Beats(ctx context.Context, names []string) (map[string]*Beat, error)
}

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.

func (Field) Canonical

func (f Field) Canonical(raw string) (string, error)

Canonical validates one field's raw value and returns its canonical text: an int as digits, an enum as its word, a list deduplicated and sorted, a wake path in its one spelling. The error names the field and what it wants.

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 Lookup

func Lookup(name string) (*Kind, bool)

Lookup finds a kind by name.

func (*Kind) Changes

func (k *Kind) Changes(raw map[string]string) (map[string]string, error)

Changes validates the named fields of a set and returns their canonical values. Every problem is reported at once.

func (*Kind) Field

func (k *Kind) Field(name string) (Field, bool)

Field finds a field by name.

func (*Kind) FieldNames

func (k *Kind) FieldNames() []string

FieldNames lists the fields in declaration order.

func (*Kind) NewRow

func (k *Kind) NewRow(name string, raw map[string]string) (Row, error)

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).

func (*Kind) Sorted

func (k *Kind) Sorted(rows []Row) []Row

Sorted returns the rows in apply order: Kind.ApplyOrder first, then name.

type Mem

type Mem struct {
	Now func() time.Time
	// contains filtered or unexported fields
}

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.

func (*Mem) Counts

func (m *Mem) Counts(_ context.Context) (map[string]int, error)

func (*Mem) Delete

func (m *Mem) Delete(ctx context.Context, kind, name string, actor string) (int64, error)

func (*Mem) Get

func (m *Mem) Get(_ context.Context, kind, name string) (Row, bool, error)

func (*Mem) History

func (m *Mem) History(_ context.Context, kind, name string) ([]Change, error)

func (*Mem) Insert

func (m *Mem) Insert(ctx context.Context, kind string, row Row, actor string) (int64, error)

func (*Mem) List

func (m *Mem) List(_ context.Context, kind string) ([]Row, error)

func (*Mem) Rev

func (m *Mem) Rev(_ context.Context, kind string) (int64, error)

func (*Mem) Update

func (m *Mem) Update(ctx context.Context, kind, name string, changes map[string]string, actor string) (Row, int64, error)

type Migration

type Migration struct {
	Version int
	Name    string
	SQL     string
}

Migration is one embedded migration.

func Migrations

func Migrations() ([]Migration, error)

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

func Plan(k *Kind, rows []Row, views map[string]View) []Op

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

func OpenPG(ctx context.Context, dsn string) (*PG, error)

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) Close

func (p *PG) Close() error

Close closes the pool.

func (*PG) Counts

func (p *PG) Counts(ctx context.Context) (map[string]int, error)

func (*PG) Delete

func (p *PG) Delete(ctx context.Context, kind, name string, actor string) (int64, error)

func (*PG) Get

func (p *PG) Get(ctx context.Context, kind, name string) (Row, bool, error)

func (*PG) History

func (p *PG) History(ctx context.Context, kind, name string) ([]Change, error)

func (*PG) Insert

func (p *PG) Insert(ctx context.Context, kind string, row Row, actor string) (int64, error)

func (*PG) List

func (p *PG) List(ctx context.Context, kind string) ([]Row, error)

func (*PG) Migrate

func (p *PG) Migrate(ctx context.Context) (from, to int, applied []int, err error)

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.

func (*PG) Rev

func (p *PG) Rev(ctx context.Context, kind string) (int64, error)

func (*PG) Update

func (p *PG) Update(ctx context.Context, kind, name string, changes map[string]string, actor string) (Row, int64, error)

func (*PG) Version

func (p *PG) Version(ctx context.Context) (int, error)

Version is the greatest applied migration, 0 before the first migrate (the ledger table itself does not exist yet).

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) Beats

func (a *RedisApplier) Beats(ctx context.Context, names []string) (map[string]*Beat, error)

Beats reads every named machine's beat in one pipeline.

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) Read

func (a *RedisApplier) Read(ctx context.Context, kind string) (map[string]View, int64, error)

func (*RedisApplier) Remove

func (a *RedisApplier) Remove(ctx context.Context, kind, name, actor, idem string) error

func (*RedisApplier) Stamp

func (a *RedisApplier) Stamp(ctx context.Context, kind string, prev, rev int64) error

Stamp is the compare-and-set of friend_declare.lua in a WATCH/MULTI: the stamp is written only while it still reads prev.

func (*RedisApplier) Write

func (a *RedisApplier) Write(ctx context.Context, kind string, row Row, prev View, actor, idem string) error

type RefusedError

type RefusedError struct {
	Err    error
	Detail string
}

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.

func (Row) Clone

func (r Row) Clone() Row

Clone copies a row.

func (Row) Int

func (r Row) Int(field string) int

Int reads an int field of a canonical row (0 when absent).

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

type View map[string]string

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").

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL