Files
mesh-controller/internal/facts/facts.go
T
jochen a011743c69
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Raise the mesh as it is in the gate, call a baseline that does not compose an error, and let a check run by hand as the seat runs it
The gate composed 0 of 4 machines with the change and without, and passed every change: the store it
raised held each module's bus credential but no account for it (issue 203's refusal), no outward links
(so no filter could be composed), and refused settings the mesh holds. Now the account is minted with
its credential, the facts carry each machine's outward links (a stand-in for an older snapshot), the
mesh's layers are kept as held, and a withheld path keeps a path's shape. A machine the mesh composes
that the gate cannot raise makes the verdict an error, never a pass; the verdict alone is on stdout.

A merge-check.sh that passed on an agent's machine failed on the build seat: a newer gofmt, siblings at
a feature branch, another user. `mesh-controller check-here` runs builder.Check with the ask the
controller would make, from facts that now name the toolchains and the refs cloned beside; a failed
script is said by what failed. (novox/hq issues 282, 283)
2026-10-07 01:33:18 +02:00

348 lines
15 KiB
Go

// Package facts is the mesh's facts snapshot: what a check needs to judge a change against the mesh
// that runs, and nothing it could leak (novox/hq to-be 45 §9, ADR 0227 rule 9).
//
// **Why it exists.** Every check the mesh had was right about the world it was given, and none was
// given the mesh's world: a module passed every test and refused the anchor's whole declaration because
// a real machine's name made its identity 23 characters (issue 263); a manifest passed the catalogue
// check and was refused by the node-engine (issue 236); a resolver answer that glibc forgave was final
// to musl (issue 262). The controller holds those facts. It writes them here, the build seat reads them,
// and a merge check composes every machine of the snapshot with the change applied.
//
// **What it holds, and what it never holds.** Every machine — under a stable pseudonym of the same length
// as its name, because the length is what a name limit meets — with its roles, system, C library,
// architecture, builds, what it reported it can do, what is assigned there, its pins and settings;
// every seat and its holders; every module the mesh holds, as its manifest; the sources the mesh built
// them from; the versions of the bus, the store and the node-engine it runs; and how each machine's
// declaration composes today. **No secret and no address**: a setting whose key or value reads as a
// secret is withheld, an address is replaced by one from a documentation range, and a machine's name, a
// site, an account and a public domain are replaced wherever they appear. So a snapshot can be copied
// into a test, a replay or a pull request without carrying anything of the installation it came from.
package facts
import (
"crypto/sha256"
"encoding/json"
"fmt"
"sort"
"time"
)
// Format is the snapshot's version. A reader refuses one newer than it knows: a field it cannot read
// is a fact it would judge without.
const Format = 1
// Repository and Tag are where the controller keeps the newest snapshot in the artifact store, and
// MediaType what it is kept as.
const (
Repository = "facts"
Tag = "latest"
MediaType = "application/vnd.novox.mesh.facts.v1+json"
)
// Facts is one snapshot.
type Facts struct {
Format int `json:"facts"`
// Taken is when the controller composed it.
Taken time.Time `json:"taken"`
// Controller is the controller build that composed it — the one the mesh runs, which is the one a
// change to the catalogue must be readable by (version skew).
Controller Build `json:"controller"`
// Versions are what the mesh runs of the things its tests stand in for.
Versions Versions `json:"versions"`
// Sources are the repositories the mesh builds modules from, each with the newest commit it built.
Sources []Source `json:"sources"`
// Machines, in name order of their pseudonyms.
Machines []Machine `json:"machines"`
// Seats are every seat held, with its holders.
Seats []Seat `json:"seats"`
// Modules are every module the mesh holds, as it holds them.
Modules []Module `json:"modules"`
// Settings are the mesh-wide layer of every module's settings, scrubbed.
Settings []Settings `json:"settings,omitempty"`
// Edges are the build dependencies between modules, as the mesh recorded them — what a merge's
// rebuild width is computed from.
Edges []Edge `json:"edges,omitempty"`
// Beside is the ref each repository is cloned at beside a merge check, by the directory it is found
// under — the commits the mesh runs, the catalogue's main — so a check run by hand reads the siblings
// the build seat reads (novox/hq issue 283).
Beside map[string]string `json:"beside,omitempty"`
}
// Build names one build of a core component.
type Build struct {
Version string `json:"version,omitempty"`
Commit string `json:"commit,omitempty"`
}
// Versions are what the mesh runs.
type Versions struct {
// Bus is the bus server's release, as the server tells a client that connects.
Bus string `json:"bus,omitempty"`
// Store is the store's server version, as the store answers it.
Store string `json:"store,omitempty"`
// NodeEngines are the node-engine builds the machines report, each once.
NodeEngines []string `json:"node-engines,omitempty"`
// Toolchains are the toolchain images a merge check runs in, by language, as the mesh holds them —
// with no address: the artifact store the snapshot is read from is where they are pulled from. What
// a repository's merge-check.sh runs in on the build seat, and so what it must run in anywhere else
// (novox/hq issue 283): two releases of one compiler disagree, down to how gofmt lays out a file.
Toolchains map[string]string `json:"toolchains,omitempty"`
}
// Source is a repository the mesh builds from, and the newest commit it built a module of.
type Source struct {
Repository string `json:"repository"`
Commit string `json:"commit,omitempty"`
Modules int `json:"modules"`
}
// Machine is one machine, under its pseudonym.
type Machine struct {
Name string `json:"name"`
// Length is the length of its real name, which the pseudonym keeps.
Length int `json:"length"`
// Roles are what it is to the mesh, in words: the control node, the hub, the bus's machine, a
// holder of a mesh seat. What a check names when it fails one.
Roles []string `json:"roles,omitempty"`
// System is the node-engine's system (arch, alpine, …), Libc its C library, as far as the mesh knows.
System string `json:"system,omitempty"`
Libc string `json:"libc,omitempty"`
Architecture string `json:"architecture,omitempty"`
Kernel string `json:"kernel,omitempty"`
// NodeEngine and NodeTools are the builds it runs.
NodeEngine string `json:"node-engine,omitempty"`
NodeTools string `json:"node-tools,omitempty"`
// Capabilities are what it reported it can do; the detail kept only where it is a version.
Capabilities []Capability `json:"capabilities,omitempty"`
// Site, Hub, Public: where it is on the private network — its site (a pseudonym), whether it is the
// hub, whether others can dial it. No address.
Site string `json:"site,omitempty"`
Hub bool `json:"hub,omitempty"`
Public bool `json:"public,omitempty"`
// OnNetwork is whether it has a place on the private network at all.
OnNetwork bool `json:"on-network,omitempty"`
// OutwardLinks are the links it reported as facing outside it, scrubbed: a machine that reported none
// is sent no filter, so a check that raised it without them would compose a machine the mesh does
// not (novox/hq ADR 0140).
OutwardLinks []string `json:"outward-links,omitempty"`
// Adopted is whether the mesh adopted it rather than converged it.
Adopted bool `json:"adopted,omitempty"`
// Account is the operator's login there (a pseudonym of the same length), and AccountHome where its
// home is when that is not the derived one.
Account string `json:"account,omitempty"`
AccountHome string `json:"account-home,omitempty"`
// PublicDomain is the domain it answers for, its labels replaced.
PublicDomain string `json:"public-domain,omitempty"`
// Assigned is every module assigned there.
Assigned []string `json:"assigned,omitempty"`
// Pins are where it was told a provision comes from.
Pins []Pin `json:"pins,omitempty"`
// Settings are its own layer of each module's settings, scrubbed.
Settings []Settings `json:"settings,omitempty"`
// Accepted are the secrets a person gave the mesh for modules here, by name only: the mesh cannot
// make them, so a check composing this machine gives each a stand-in instead of refusing it.
Accepted []Accepted `json:"accepted,omitempty"`
// Declaration is how its declaration composes today, as the controller that took this saw it.
Declaration Declaration `json:"declaration"`
}
// Capability is one thing a machine reported it can or cannot do.
type Capability struct {
Name string `json:"name"`
Present bool `json:"present"`
Detail string `json:"detail,omitempty"`
}
// Pin is one provision a machine was told where to take from.
type Pin struct {
Provision string `json:"provision"`
Machine string `json:"machine"`
Module string `json:"module,omitempty"`
}
// Accepted is one secret a person gave: the module's own when Provider is empty, else the credential it
// takes from that machine's provider under Local.
type Accepted struct {
Module string `json:"module"`
Name string `json:"name"`
Provider string `json:"provider,omitempty"`
Local string `json:"local,omitempty"`
}
// Settings is one layer of one module's settings.
type Settings struct {
Module string `json:"module"`
Values map[string]any `json:"values"`
}
// Declaration is how one machine's declaration composed when the snapshot was taken.
type Declaration struct {
// Composes is whether it composed and the node-engine's validator took it.
Composes bool `json:"composes"`
// Digest is its body's digest — composed as the next push would, so it moves with what the mesh
// would send, and is not the digest of what was last sent.
Digest string `json:"digest,omitempty"`
// Resources are what it declares, as `type:id`, sorted.
Resources []string `json:"resources,omitempty"`
// Problems are why it does not compose or validate, scrubbed.
Problems []string `json:"problems,omitempty"`
// LeftOut are the modules assigned there and left out of it, each with why.
LeftOut map[string]string `json:"left-out,omitempty"`
// Withheld are the consumers its grants leave out because an identity overflows the provision's
// bound (ADR 0225), and Unbound the credentials on record for consumers bound elsewhere (issue 274),
// each said in words.
Withheld []string `json:"withheld,omitempty"`
Unbound []string `json:"unbound,omitempty"`
}
// Seat is one seat and the machines holding it.
type Seat struct {
Name string `json:"name"`
Scope string `json:"scope"`
Holders []Holder `json:"holders"`
}
// Holder is one module on one machine holding a seat.
type Holder struct {
Machine string `json:"machine"`
Module string `json:"module"`
}
// Module is one module the mesh holds.
type Module struct {
Name string `json:"name"`
// Repository and Path are where it is built from; empty for one that came with the controller.
Repository string `json:"repository,omitempty"`
Path string `json:"path,omitempty"`
// Commit is the commit the manifest the mesh holds was read at.
Commit string `json:"commit,omitempty"`
// Provided is a module that came with the controller rather than from a repository.
Provided bool `json:"provided,omitempty"`
// RollOut is its upgrade policy: rolled out when built, or recorded.
RollOut bool `json:"roll-out,omitempty"`
// Reads are the other repositories its build read source from.
Reads []string `json:"reads,omitempty"`
// Manifest is the module as the mesh holds it: artifacts resolved to the builds it runs.
Manifest json.RawMessage `json:"manifest"`
}
// Edge is one build dependency: From is built standing on To.
type Edge struct {
From string `json:"from"`
To string `json:"to"`
Kind string `json:"kind,omitempty"`
}
// Sorted puts every list in a fixed order, so two snapshots of one mesh are the same bytes apart from
// when they were taken.
func (f *Facts) Sorted() {
sort.Slice(f.Machines, func(i, j int) bool { return f.Machines[i].Name < f.Machines[j].Name })
for i := range f.Machines {
m := &f.Machines[i]
sort.Strings(m.Roles)
sort.Strings(m.Assigned)
sort.Slice(m.Capabilities, func(a, b int) bool { return m.Capabilities[a].Name < m.Capabilities[b].Name })
sort.Slice(m.Pins, func(a, b int) bool { return m.Pins[a].Provision < m.Pins[b].Provision })
sort.Slice(m.Settings, func(a, b int) bool { return m.Settings[a].Module < m.Settings[b].Module })
sort.Slice(m.Accepted, func(a, b int) bool {
x, y := m.Accepted[a], m.Accepted[b]
return x.Module+"\x00"+x.Name+"\x00"+x.Provider+"\x00"+x.Local < y.Module+"\x00"+y.Name+"\x00"+y.Provider+"\x00"+y.Local
})
sort.Strings(m.Declaration.Resources)
}
sort.Slice(f.Seats, func(i, j int) bool {
if f.Seats[i].Name != f.Seats[j].Name {
return f.Seats[i].Name < f.Seats[j].Name
}
return f.Seats[i].Scope < f.Seats[j].Scope
})
for i := range f.Seats {
h := f.Seats[i].Holders
sort.Slice(h, func(a, b int) bool {
if h[a].Machine != h[b].Machine {
return h[a].Machine < h[b].Machine
}
return h[a].Module < h[b].Module
})
}
sort.Slice(f.Modules, func(i, j int) bool { return f.Modules[i].Name < f.Modules[j].Name })
sort.Slice(f.Sources, func(i, j int) bool { return f.Sources[i].Repository < f.Sources[j].Repository })
sort.Slice(f.Settings, func(i, j int) bool { return f.Settings[i].Module < f.Settings[j].Module })
sort.Slice(f.Edges, func(i, j int) bool {
if f.Edges[i].From != f.Edges[j].From {
return f.Edges[i].From < f.Edges[j].From
}
return f.Edges[i].To < f.Edges[j].To
})
sort.Strings(f.Versions.NodeEngines)
}
// Encode is the snapshot as it is kept: sorted, indented, so a person can read the one the build seat
// read and a diff of two says what moved.
func (f Facts) Encode() ([]byte, error) {
f.Sorted()
return json.MarshalIndent(f, "", " ")
}
// Content is the digest of everything but when it was taken: two snapshots of an unchanged mesh have
// the same content, which is how the controller tells a change from another day.
func (f Facts) Content() (string, error) {
f.Taken = time.Time{}
body, err := f.Encode()
if err != nil {
return "", err
}
sum := sha256.Sum256(body)
return fmt.Sprintf("sha256:%x", sum), nil
}
// Decode reads a snapshot, refusing one newer than this reader knows and one that is not a snapshot.
func Decode(body []byte) (Facts, error) {
var f Facts
if err := json.Unmarshal(body, &f); err != nil {
return Facts{}, fmt.Errorf("not a facts snapshot: %w", err)
}
switch {
case f.Format == 0:
return Facts{}, fmt.Errorf("not a facts snapshot: it says no format")
case f.Format > Format:
return Facts{}, fmt.Errorf("a facts snapshot of format %d, and this reads format %d: a newer controller "+
"took it, and what it says beyond %d would be judged without", f.Format, Format, Format)
}
return f, nil
}
// Longest is the length of the longest machine name in the snapshot — what a consumer's identity is
// judged on (novox/hq ADR 0225).
func (f Facts) Longest() int {
longest := 0
for _, m := range f.Machines {
if m.Length > longest {
longest = m.Length
}
}
return longest
}
// Machine is the machine of that pseudonym.
func (f Facts) Machine(name string) (Machine, bool) {
for _, m := range f.Machines {
if m.Name == name {
return m, true
}
}
return Machine{}, false
}
// Described is how a check names a machine: its roles, then its pseudonym.
func (m Machine) Described() string {
if len(m.Roles) == 0 {
return "a machine (" + m.Name + ")"
}
out := m.Roles[0]
for _, r := range m.Roles[1:] {
out += ", " + r
}
return out + " (" + m.Name + ")"
}