Every seat constant of mesh-sdk is mesh-contracts' generated name; the four whole seats' (BrokerSeat, CheckerSeat, asks.Seat, buscompose.VaultSeat) are marked deprecated for their importers. The package walk of mesh-sdk's own check refuses a string literal naming a defined seat or its subjects, and an exported name given to one, beyond testdata/seat-names.txt, which lists what stands before the move (module names that equal a seat's among them) and only shrinks. No new test function. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
335 lines
16 KiB
Go
335 lines
16 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
contractseats "git.novox.be/novox/mesh-contracts/go/seats"
|
|
)
|
|
|
|
// Seat is one role the mesh defines.
|
|
type Seat struct {
|
|
// Name is what a manifest claims.
|
|
Name string
|
|
// Scope is where there may be only one holder — unless the seat is Replicated.
|
|
Scope string
|
|
// Replicated says a mesh seat may be held on several machines at once, each holder answering the
|
|
// same thing (novox/hq ADR 0223): the mesh's resolver, held on the anchor and the home server so a
|
|
// machine's resolver file lists two that give one answer. Each holder is on record, added by an
|
|
// act (`seat <name> --add <node>/<module>`), never by being assigned: two claimants with nothing on
|
|
// record are refused exactly as for any mesh seat. One per machine still — two modules on one
|
|
// node claiming it are refused. Compiled, never stored, like Receives: it is the mesh's definition of
|
|
// the role, and the store's rows carry no column for it.
|
|
Replicated bool
|
|
// Delivers is the provision the seat's holder answers for, or empty. A seat that delivers a
|
|
// provision may only be held by a module providing it at the seat's scope, and its holder is
|
|
// what a requirement for that provision resolves to when several modules provide it.
|
|
Delivers string
|
|
// Accepts, Emits and Serves are the protocol of the role, as local verbs — the same three a
|
|
// module declares for a seat of its own (novox/hq ADR 0118), and empty for most of these: a seat
|
|
// is usually about who does a job and not about what may be said to them.
|
|
//
|
|
// **Named here so the mesh has no role it cannot describe** (ADR 0121). Without them a build
|
|
// machine had three audiences for one outcome and nothing derived a grant for any of them, and an
|
|
// event about a role had nowhere to live but the namespace of whichever module held that role
|
|
// today — which the bus refuses, because a namespace belongs to who it is named for.
|
|
Accepts []string
|
|
Emits []string
|
|
// PulledByKind says each accepted kind is a worker of its own on the seat's stream, which its holders pull in the
|
|
// order Accepts lists them (novox/hq ADR 0525 §2, ADR 0536 §4): the build seats'. Compiled, never stored, like
|
|
// Receives.
|
|
PulledByKind bool
|
|
// Yields says the seat's holder gives way to everything else on its node (novox/hq ADR 0553): its runtime runs at
|
|
// idle priority in the node's one yielding slice, under the node's one processor share (buildcpu.ShareSetting) that
|
|
// every yielding holder of the node shares. The build seats': a build must never slow the operator's desktop.
|
|
// Compiled, never stored, like Receives.
|
|
Yields bool
|
|
// Serves carries each verb in full — name, description, schema — because a role's tools are the
|
|
// mesh's to define and an agent's to call (novox/hq ADR 0132, design 33 §2).
|
|
Serves []Verb
|
|
// Receives is what other modules may contribute to the seat's holder, by kind (novox/hq ADR
|
|
// 0212): each kind is text in the tool's own grammar, placed by the holder with
|
|
// ${contribution:<seat>:<kind>}. Compiled, never stored: like the protocol, it is the mesh's
|
|
// definition of the role, and the store's rows carry no column for it.
|
|
Receives []Receivable
|
|
// Paths are the paths on its machine the seat's holder owns (novox/hq ADR 0287): a holder declares
|
|
// every one, and a module claiming no seat that names a path may not declare it. Only a node seat
|
|
// names paths. Compiled, never stored, like Receives: adding or taking away a path is a decision,
|
|
// recorded, and the store's rows carry no column for it.
|
|
Paths []SeatPath
|
|
// HeldByTheEngine says the seat is held on every node by that node's engine, which serves its verbs on its own bus
|
|
// account (novox/hq ADR 0563 §1): no module claims it and no assignment holds it. Compiled, never stored, like
|
|
// Receives.
|
|
HeldByTheEngine bool
|
|
// Decision is the record that made it a seat.
|
|
Decision string
|
|
}
|
|
|
|
// Receivable is one kind of contribution a seat receives (novox/hq ADR 0212 §2): its name, and the
|
|
// comment prefix of the tool's grammar, with which the controller names each contributing module.
|
|
type Receivable struct {
|
|
Kind string
|
|
Comment string
|
|
// Dirs says a contribution of this kind may name its contributor's own directories as
|
|
// `${dir:<id>}`, filled with where they are on the machine before the holder places it (novox/hq
|
|
// to-be 43): a module saying which of its data to back up names a directory the mesh placed, and
|
|
// only the mesh knows where. Off for every kind written in a tool's grammar that has its own
|
|
// `${…}` — a shell's — where the mesh filling one would change what the tool reads.
|
|
Dirs bool
|
|
// Shape, when set, makes the kind data rather than text (novox/hq ADR 0255): a contributor gives
|
|
// `data` in this shape, and the holder renders each piece with the template its claim gives.
|
|
Shape *Shape
|
|
// Offered says a contribution of this kind does not depend on the seat (novox/hq ADR 0255): where
|
|
// nobody holds it nothing is placed, and the contributor runs there all the same. A battery's bar
|
|
// block is offered — the power module is on every machine, and a server has no bar.
|
|
Offered bool
|
|
}
|
|
|
|
// defaultSeats is the set the mesh ships with — the seed for the control plane's seat table and the
|
|
// fallback when it has none (novox/hq ADR 0122). **It is generated, never written here** (novox/hq ADR 0608 §5,
|
|
// ADR 0613 §2, design 26 *The seat set has one source*): every seat's definition lives in mesh-contracts, and its
|
|
// generated seat list is what this runtime reads, in the order a person reads it: the mesh's own, then a node's. What
|
|
// a definition cannot say, being code, is attached here: a contribution kind's checks (withShapeCode).
|
|
var defaultSeats = fromContracts(contractseats.List())
|
|
|
|
// fromContracts is the generated seat list in this runtime's types, each data-shaped kind given its checks.
|
|
func fromContracts(list []contractseats.Seat) []Seat {
|
|
out := make([]Seat, 0, len(list))
|
|
for _, c := range list {
|
|
s := Seat{Name: c.Name, Scope: c.Scope, Replicated: c.Replicated, Delivers: c.Delivers, Accepts: c.Accepts,
|
|
Emits: c.Emits, PulledByKind: c.PulledByKind, Yields: c.Yields, HeldByTheEngine: c.HeldByTheEngine,
|
|
Decision: c.Decision}
|
|
for _, v := range c.Serves {
|
|
s.Serves = append(s.Serves, Verb{Name: v.Name, Description: v.Description, Input: v.Input, Output: v.Output,
|
|
Optional: v.Optional, Replaces: v.Replaces})
|
|
}
|
|
for _, r := range c.Receives {
|
|
rv := Receivable{Kind: r.Kind, Comment: r.Comment, Dirs: r.Dirs, Offered: r.Offered}
|
|
if r.Shape != nil {
|
|
sh := &Shape{Order: r.Shape.Order, Examples: r.Shape.Examples, Text: r.Shape.Text}
|
|
for _, f := range r.Shape.Places {
|
|
sh.Places = append(sh.Places, PlaceField{Field: f.Field, Values: f.Values})
|
|
}
|
|
withShapeCode(c.Name, r.Kind, sh)
|
|
rv.Shape = sh
|
|
}
|
|
s.Receives = append(s.Receives, rv)
|
|
}
|
|
for _, p := range c.Paths {
|
|
s.Paths = append(s.Paths, SeatPath{Path: p.Path, StepsBackFor: p.StepsBackFor})
|
|
}
|
|
out = append(out, s)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// seats is the working set the lookups read. It starts as the compiled defaults and is replaced by
|
|
// what the control plane loaded from its store (novox/hq ADR 0122), so a change to the set is a
|
|
// change to data, not to this code.
|
|
var seats = defaultSeats
|
|
|
|
// DefaultSeats is the set the mesh ships with, for seeding the store's seat table.
|
|
func DefaultSeats() []Seat { return append([]Seat(nil), defaultSeats...) }
|
|
|
|
// UseSeats replaces the working set with the one the control plane read from its store.
|
|
//
|
|
// **Empty is ignored on purpose.** A store that has not been seeded yet — or one that could not be
|
|
// read — must leave the compiled defaults in force rather than emptying the set: an empty set would
|
|
// refuse every claim and could stop the control plane composing at all, which is a far worse failure
|
|
// than running on the set the binary shipped with. So the store can only ever *replace* the set with
|
|
// a non-empty one, never erase it.
|
|
func UseSeats(s []Seat) {
|
|
if len(s) == 0 {
|
|
return
|
|
}
|
|
// **The store's rows carry no protocol yet, and the protocol is what the bus is derived
|
|
// from.** ADR 0129 gives a seat what it accepts, emits and serves; ADR 0122 moved the set into
|
|
// a table that has name, scope, delivers and decision and nothing else, and the columns for
|
|
// the rest are not there yet. So a row replacing a compiled entry would silently drop the
|
|
// protocol, and the roles' work queues would never be raised — found live as "no response
|
|
// from stream" the first time a build was submitted over the new bus (2026-09-28). Until the
|
|
// table gains the columns, a row without a protocol keeps the compiled one of the same name.
|
|
byName := map[string]Seat{}
|
|
for _, d := range defaultSeats {
|
|
byName[d.Name] = d
|
|
}
|
|
merged := make([]Seat, 0, len(s))
|
|
for _, row := range s {
|
|
if len(row.Accepts)+len(row.Emits)+len(row.Serves) == 0 {
|
|
if d, known := byName[row.Name]; known {
|
|
row.Accepts, row.Emits, row.Serves = d.Accepts, d.Emits, d.Serves
|
|
}
|
|
}
|
|
// What a seat receives, whether it is replicated and the paths it names are never stored
|
|
// (novox/hq ADR 0212, ADR 0223, ADR 0287), so they are always the compiled ones.
|
|
if d, known := byName[row.Name]; known {
|
|
row.Receives = d.Receives
|
|
row.Paths = d.Paths
|
|
row.Replicated = d.Replicated
|
|
row.PulledByKind = d.PulledByKind
|
|
row.Yields = d.Yields // never stored (ADR 0553)
|
|
row.HeldByTheEngine = d.HeldByTheEngine // never stored (ADR 0563)
|
|
row.Serves = optionalAsCompiled(row.Serves, d.Serves)
|
|
}
|
|
merged = append(merged, row)
|
|
}
|
|
seats = merged
|
|
}
|
|
|
|
// optionalAsCompiled is a row's verbs with each one the compiled seat marks optional marked so. The mark
|
|
// is never stored (Verb.Optional), and a row is what the working set holds once the store is read: a verb
|
|
// the seeding added to the row — `failed`, `checks` — would otherwise come back required, and every holder
|
|
// not serving it yet would be refused at registration and at handover, which the mark exists to prevent.
|
|
//
|
|
// **And what a verb replaces is the compiled one** (novox/hq ADR 0245): a row seeded before the field
|
|
// existed carries none, and re-seeding widens a verb's arguments only, so read from the row alone the
|
|
// `tools` answer — what the mesh MCP server's search and the agent's instructions are built from — would
|
|
// say no verb replaces anything on a mesh whose rows exist. The mesh's statement of what a role replaces
|
|
// is this binary's, like the optional mark.
|
|
func optionalAsCompiled(row, compiled []Verb) []Verb {
|
|
optional := map[string]bool{}
|
|
replaces := map[string][]string{}
|
|
for _, v := range compiled {
|
|
if v.Optional {
|
|
optional[v.Name] = true
|
|
}
|
|
if len(v.Replaces) > 0 {
|
|
replaces[v.Name] = v.Replaces
|
|
}
|
|
}
|
|
if len(optional) == 0 && len(replaces) == 0 {
|
|
return row
|
|
}
|
|
out := make([]Verb, len(row))
|
|
for i, v := range row {
|
|
v.Optional = optional[v.Name]
|
|
if r, ok := replaces[v.Name]; ok {
|
|
v.Replaces = r
|
|
}
|
|
out[i] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
// aliases maps a seat's former names to its current canonical name (novox/hq ADR 0122). Loaded from
|
|
// the store alongside the set, so a reference to a name a seat used to have — a manifest's claim, a
|
|
// held record — still resolves to it after a rename, and nothing downstream has to change.
|
|
var aliases = map[string]string{}
|
|
|
|
// Seats is every seat the mesh defines, in reading order.
|
|
func Seats() []Seat {
|
|
return append([]Seat(nil), seats...)
|
|
}
|
|
|
|
// SeatNamed is the seat a name refers to, whether that is its current name or one it used to have
|
|
// (novox/hq ADR 0122). A former name resolves to the seat's canonical row, so a rename breaks no
|
|
// reference to the old name.
|
|
func SeatNamed(name string) (Seat, bool) {
|
|
for _, s := range seats {
|
|
if s.Name == name {
|
|
return s, true
|
|
}
|
|
}
|
|
if canonical, aliased := aliases[name]; aliased {
|
|
for _, s := range seats {
|
|
if s.Name == canonical {
|
|
return s, true
|
|
}
|
|
}
|
|
}
|
|
return Seat{}, false
|
|
}
|
|
|
|
// SeatDelivering is the seat whose holder answers for a provision, if there is one.
|
|
func SeatDelivering(provision string) (Seat, bool) {
|
|
if provision == "" {
|
|
return Seat{}, false
|
|
}
|
|
for _, s := range seats {
|
|
if s.Delivers == provision {
|
|
return s, true
|
|
}
|
|
}
|
|
return Seat{}, false
|
|
}
|
|
|
|
// CanHold is why a module could not hold a seat, or nothing: its definition must claim the seat at
|
|
// the seat's scope, and provide what the seat delivers, if it delivers anything. The seat is the
|
|
// store's row, so this is judged only where the store's set is loaded — at registration and in the
|
|
// handover command (novox/hq ADR 0131), never in the parser.
|
|
func CanHold(m Manifest, seat Seat) error {
|
|
var claimed *Claim
|
|
for i := range m.Claims {
|
|
if hs, ok := SeatNamed(m.Claims[i].Name); ok && hs.Name == seat.Name {
|
|
claimed = &m.Claims[i]
|
|
}
|
|
}
|
|
if claimed == nil {
|
|
return fmt.Errorf("%s does not claim %s", m.Module, seat.Name)
|
|
}
|
|
if claimed.At() != seat.Scope {
|
|
return fmt.Errorf("%s claims %s at scope %q, and %s is a %s seat",
|
|
m.Module, seat.Name, claimed.At(), seat.Name, seat.Scope)
|
|
}
|
|
if seat.Delivers != "" && !providesAt(m, seat.Delivers, seat.Scope) {
|
|
return fmt.Errorf("%s claims %s, whose holder answers for %q, and %s does not provide %q at %s scope",
|
|
m.Module, seat.Name, seat.Delivers, m.Module, seat.Delivers, seat.Scope)
|
|
}
|
|
// **A holder owns what the role owns** (novox/hq ADR 0287): every path the seat names, declared as
|
|
// a resource or as a file the mesh renders for it, so which module owns /etc/hosts on a machine is
|
|
// answered by the seat's definition and not by reading every manifest.
|
|
if missing := missingPaths(m, seat.Paths); len(missing) > 0 {
|
|
return fmt.Errorf("%s claims %s but does not declare %s, which that seat's holder owns (novox/hq "+
|
|
"ADR 0287) — a holder declares every path its seat names, as a resource or as a file the mesh "+
|
|
"renders for it", m.Module, seat.Name, strings.Join(missing, ", "))
|
|
}
|
|
// **Serving the seat's tools is a condition of holding it** (novox/hq ADR 0132). A holder that
|
|
// does not answer what the role promises is every caller's timeout, found at registration and
|
|
// at handover instead, naming the verbs rather than the fact that something is missing.
|
|
if missing := unservedVerbs(claimed.ServesFor(m), seat.Serves); len(missing) > 0 {
|
|
return fmt.Errorf("%s claims %s but does not serve %s, which that seat's protocol promises "+
|
|
"(novox/hq ADR 0132) — a holder names every verb its seat declares, under the claim's "+
|
|
"serves or among its own tools",
|
|
m.Module, seat.Name, strings.Join(missing, ", "))
|
|
}
|
|
// And nothing the seat does not promise: a verb named here that the protocol lacks is served
|
|
// to nobody, which is a typo the holder would otherwise discover as a caller's timeout.
|
|
if extra := unpromised(claimed.Serves, seat.Serves); len(extra) > 0 {
|
|
return fmt.Errorf("%s claims %s and says it serves %s, which that seat's protocol does not "+
|
|
"promise — a claim's serves names the seat's verbs and nothing else",
|
|
m.Module, seat.Name, strings.Join(extra, ", "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func providesAt(m Manifest, provision, scope string) bool {
|
|
for _, o := range m.Provides {
|
|
if o.Name == provision && o.At() == scope {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// SeatsWithAProtocol are the mesh's own seats that say something about what may be said to them or by
|
|
// them, which is the set the bus derives streams, consumers and permissions from.
|
|
//
|
|
// Most of the set is not here, and that is the ordinary case: a seat saying only who does a job grants
|
|
// nothing on the bus and needs no queue.
|
|
func SeatsWithAProtocol() []Seat {
|
|
var out []Seat
|
|
for _, s := range seats {
|
|
if len(s.Accepts) > 0 || len(s.Emits) > 0 || len(s.Serves) > 0 {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// BrokerSeat is the mesh's bus. Its holder is the bus, and its account — when it declares one — may
|
|
// snapshot the bus's streams and do nothing else (novox/hq ADR 0235).
|
|
//
|
|
// Deprecated: a whole seat's name is mesh-contracts' (seats.MeshBroker, meshbroker.Seat); kept for its importers
|
|
// until their next change takes it from there (testdata/seat-names.txt).
|
|
const BrokerSeat = contractseats.MeshBroker
|