Judge: no seat list compiled in; it judges against the seat list its host reads (the request's seats, UseSeatList), the seat names from the contracts (D-29, ADR 0634 §4, ADR 0636 §1; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 14:39:04 +01:00
parent 7c42361fbc
commit da2f8cfc10
9 changed files with 56 additions and 1309 deletions
@@ -13,7 +13,7 @@ import (
"io"
"os"
"git.novox.be/novox/mesh-sdk/go/composer"
"git.novox.be/novox/mesh-delivery/sdk/composer"
"mesh-delivery-indexer/judge"
)
+1
View File
@@ -31,6 +31,7 @@ require (
)
require (
git.novox.be/novox/mesh-contracts/go/seats/meshbroker v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-20261104174344-ae5266e77639
git.novox.be/novox/mesh-contracts/go/seats/meshdeliverychecker v0.0.0-20261104174344-ae5266e77639
git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0-20261104174344-ae5266e77639
@@ -7,8 +7,8 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-delivery/sdk/composer"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/composer"
)
// A manifest's bus state, asked for in `requires` by template, is judged once and each refusal names the module, the entry
+17 -1
View File
@@ -15,6 +15,7 @@ import (
"encoding/hex"
json "encoding/json"
"fmt"
"git.novox.be/novox/mesh-sdk/go/seatdef"
"io"
"maps"
"os"
@@ -25,8 +26,8 @@ import (
"strings"
"sync"
"git.novox.be/novox/mesh-delivery/sdk/composer"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/composer"
)
// Manifest judges one manifest alone: decoded strictly, and every rule a manifest is held to by itself. The list of
@@ -209,6 +210,17 @@ const Unnamed = "(between manifests)"
// are judged against (nil judges no name). The verdict's SDK names this judge (Identity). A manifest left as a digest, or
// a case named twice, is a request it cannot run.
func Request(r composer.Request) (composer.Verdict, error) {
if len(r.Seats) > 0 {
defs := make([]seatdef.Definition, 0, len(r.Seats))
for _, raw := range r.Seats {
d, err := seatdef.Parse(raw)
if err != nil {
return composer.Verdict{}, fmt.Errorf("a seat definition of the request: %w", err)
}
defs = append(defs, d)
}
UseSeatList(defs)
}
v := composer.Verdict{V: composer.Version, SDK: Identity()}
seen := map[string]bool{}
counts := trunkOldSections(r)
@@ -242,6 +254,10 @@ func Request(r composer.Request) (composer.Verdict, error) {
return v, nil
}
// UseSeatList gives the judge the seat list it judges against: the definitions the seat list reader holds (mesh-sdk seatlist, ADR
// 0636 §1), or a request's Seats. No list is compiled into the judge (ADR 0634 §4); an empty one is ignored, as the catalogue's is.
func UseSeatList(defs []seatdef.Definition) { catalogue.UseSeats(catalogue.SeatsFromDefinitions(defs)) }
// TrunkCase is the case of a request holding the catalogue as the trunk holds it (the checker's JudgeTrunk).
const TrunkCase = "trunk"
@@ -6,8 +6,8 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-delivery/sdk/composer"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/composer"
"git.novox.be/novox/mesh-sdk/go/overlay"
)
@@ -0,0 +1,15 @@
package judge
import (
"os"
"testing"
"git.novox.be/novox/mesh-sdk/go/seatlist/seatlisttest"
)
// TestMain gives the judge the seat list a host reads (ADR 0636 §1): the definitions of the mesh's seats as the contracts
// modules define them, in the form mesh-sdk keeps for its tests. No list is compiled into the judge (ADR 0634 §4).
func TestMain(m *testing.M) {
UseSeatList(seatlisttest.Definitions())
os.Exit(m.Run())
}
File diff suppressed because it is too large Load Diff
@@ -16,6 +16,7 @@ import (
reflect "reflect"
regexp "regexp"
slices "slices"
"sort"
strconv "strconv"
strings "strings"
sync "sync"
@@ -3938,7 +3939,8 @@ func TestNodeBuildAgentIsRetiredForTheBuildSeats(t *testing.T) {
yielding = append(yielding, s.Name)
}
}
if strings.Join(yielding, ",") != "node-build-go,node-build-node,node-build-image" {
sort.Strings(yielding)
if strings.Join(yielding, ",") != "node-build-go,node-build-image,node-build-node" {
t.Errorf("the seats that yield are %v", yielding)
}
}
@@ -3972,7 +3974,8 @@ func TestNoModuleMayDeclareTheLoginShell(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err := catalogue.CanHold(m, catalogue.Seat{Name: catalogue.LoginShellSeat, Scope: catalogue.ScopeNode, Serves: loginShellVerbs()}); err != nil {
loginShell, _ := catalogue.SeatNamed(catalogue.LoginShellSeat)
if err := catalogue.CanHold(m, loginShell); err != nil {
t.Fatalf("a shell module claiming the seat cannot hold it: %v", err)
}
}
@@ -4261,7 +4264,7 @@ func TestAStoreHolderThatBuildsIsNotRefusedByItsManifest(t *testing.T) {
s, _ := catalogue.SeatNamed(name)
holder.Claims = append(holder.Claims, catalogue.Claim{Name: name, Scope: catalogue.ScopeMesh, Serves: catalogue.VerbNames(s.Serves)})
}
for _, s := range append([]catalogue.Seat{}, seats...) {
for _, s := range append([]catalogue.Seat{}, catalogue.Seats()...) {
if s.Delivers == "artifact-store" {
t.Errorf("the judge's %s still delivers the retired artifact-store", s.Name)
}
@@ -4401,7 +4404,7 @@ func TestTheBussAccountSaysNothingOnTheBus(t *testing.T) {
if err != nil {
t.Fatalf("a bus module with an account and tools was refused: %v", err)
}
if !m.ClaimsSeat(catalogue.BrokerSeat) {
if !m.ClaimsSeat("mesh-broker") {
t.Fatal("it does not read as holding the broker seat")
}
}
@@ -4546,7 +4549,7 @@ func TestTheCataloguesBarPlacesBlocksAndItsShapeCoversEveryShow(t *testing.T) {
// The compiled set keeps the rules: node seats only, absolute and literal, one seat per path without
// stepping back, and a stepping back that cannot cycle.
func TestTheCompiledSeatPathsKeepTheRules(t *testing.T) {
if p := SeatPathProblems(catalogue.DefaultSeats()); len(p) != 0 {
if p := SeatPathProblems(catalogue.Seats()); len(p) != 0 {
t.Fatalf("the compiled set breaks ADR 0287: %v", p)
}
for _, c := range []struct {
@@ -4942,107 +4945,6 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
delivered[s.Delivers] = s.Name
}
}
// Forty-seven with mesh-placement (novox/hq ADR 0567), serving its five verbs: adopt and hand-back went with the
// controller (novox/hq ADR 0590, issue 1126).
if s, ok := catalogue.SeatNamed(catalogue.PlacementSeat); !ok || s.Scope != catalogue.ScopeMesh || !reflect.DeepEqual(catalogue.VerbNames(s.Serves),
[]string{"placement", "proposals", "propose", "withdraw", "again"}) {
t.Errorf("the placement seat is %v, %v", ok, s)
}
// The judge's copy of the catalog seat is the SDK's entry exactly (ADR 0604 §1): four optional verbs, so the claim of the
// module that held the seat before is not outgrown while both stand.
if sdk, ok := catalogue.SeatNamed(catalogue.CatalogSeat); !ok || len(sdk.Serves) != 4 {
t.Errorf("the SDK's catalog seat is %v, %+v", ok, sdk)
} else {
for _, s := range defaultSeats {
if s.Name == catalogue.CatalogSeat && !reflect.DeepEqual(s, sdk) {
t.Errorf("the judge's copy of the catalog seat is %+v, want the SDK's %+v", s, sdk)
}
}
}
// The checker's seat serves the try, which replaces the shell's merge check (ADR 0547 §10, ADR 0245).
var checkerVerbs []string
for _, s := range catalogue.Seats() {
if s.Name == catalogue.CheckerSeat {
for _, v := range s.Serves {
checkerVerbs = append(checkerVerbs, v.Name)
if v.Name == "try" && strings.Join(v.Replaces, "|") != "sh merge-check.sh|docker run postgres" {
t.Errorf("try replaces %v", v.Replaces)
}
}
}
}
if strings.Join(checkerVerbs, ",") != "try,try-read,try-end" {
t.Errorf("the checker seat serves %v", checkerVerbs)
}
// The four health seats (novox/hq ADR 0564 clause 1): at mesh scope, each held once, each naming ADR 0564, with the
// verbs and events the record names; the events keep the controller's names (clause 3).
for _, c := range []struct{ seat, verbs, emits string }{
{catalogue.ConditionsSeat, "conditions,silence,resync", "condition-raised,condition-changed,condition-cleared"},
{catalogue.WatchdogsSeat, "signals,stuck,presence,dead-letters,resync", ""},
{catalogue.SelfCheckSeat, "doctor,probes", "checked"},
{catalogue.HealersSeat, "healers,heal,drill", "healer-acted"},
// And mesh-config's config; its switch went with the controller (ADR 0590 clause 3); and its open renames
// (design 32 *A config value renamed*, ADR 0620).
{"mesh-config", "config,renames", ""},
// And ADR 0565 clause 1's two.
{catalogue.WantsSeat, "upgrade,bus", "policy-changed"},
{catalogue.HandActsSeat, "hand-act,hand-acts,warranted", "act-recorded"},
// ADR 0571 clause 2: the artifact store's sweep.
{catalogue.ArtifactSweepSeat, "artifacts,held,collect,mirrors,images", "artifacts-gone"},
} {
s, ok := catalogue.SeatNamed(c.seat)
if !ok || s.Scope != catalogue.ScopeMesh || s.Replicated || s.Delivers != "" || !strings.Contains(s.Decision, "ADR 05") ||
strings.Join(catalogue.VerbNames(s.Serves), ",") != c.verbs || strings.Join(s.Emits, ",") != c.emits {
t.Errorf("the health seat %s is %v, %+v", c.seat, ok, s)
}
}
// The node seat node-engine is the node's engine's (novox/hq ADR 0563 §1): at node scope, delivering nothing, its verbs
// (compose-candidate since ADR 0593 §7, manifest-reads since ADR 0618 §3) and four events (manifest-reads-changed since
// ADR 0618 §4), held by no assignment, and a module's claim of it refused.
if engine, ok := catalogue.SeatNamed(catalogue.EngineSeat); !ok || engine.Scope != catalogue.ScopeNode || engine.Delivers != "" || !engine.HeldByTheEngine {
t.Errorf("node-engine is %+v, not a node seat delivering nothing that the engine holds", engine)
} else {
var verbs []string
for _, v := range engine.Serves {
verbs = append(verbs, v.Name)
}
// acts reads one act by its id too (ADR 0563 §9: the seat's tools are its command line).
if props, _ := engine.Serves[5].Input["properties"].(map[string]any); props["id"] == nil {
t.Errorf("acts takes no id: %v", engine.Serves[5].Input)
}
if strings.Join(verbs, ",") != "hand-over,access-act,judge-directories,setuid-search,root-free,acts,packages,composition,compose,compose-candidate,manifest-reads" ||
strings.Join(engine.Emits, ",") != "asked,done,refused,manifest-reads-changed" {
t.Errorf("node-engine serves %v and emits %v", verbs, engine.Emits)
}
}
if _, err := ParseManifest([]byte(`{"module":"thing","version":"1","claims":[{"name":"node-engine","scope":"node"}]}`)); err == nil ||
!strings.Contains(err.Error(), "node-engine") {
t.Errorf("a module's claim of node-engine was not refused: %v", err)
}
// Forty-six with the three build seats replacing node-build-agent and the retired mesh-build-machine (novox/hq
// ADR 0525 §2, §10); forty-five with node-files (novox/hq ADR 0394); forty-four again once node-reads goes.
// Forty-four with node-provisioner (novox/hq ADR 0349 clause 3); forty-three with mesh-provisioner (novox/hq ADR 0349 clause 3); forty-two with mesh-config (novox/hq ADR 0348 §9); forty-one with node-reads (novox/hq ADR 0310); forty with node-nfs-server and node-mounts (novox/hq ADR 0263); thirty-eight with node-resolver (novox/hq ADR 0247); thirty-seven with mesh-delivery (novox/hq ADR 0239); thirty-six since node-resolver-config retired
// into node-uplink (novox/hq ADR 0223); thirty-seven
// since the retired node-dns-resolver went (novox/hq ADR 0220); thirty-eight with
// node-backup (novox/hq ADR 0214); thirty-seven with node-message-bus (novox/hq ADR 0215);
// thirty-six with mesh-dns-resolver (novox/hq ADR 0194) and node-hosts-file (ADR 0199, now
// node-hostname); thirty-four
// with node-hotkeys (ADR 0212); thirty-three with node-power (ADR 0211); thirty-two since the
// graphical session's eleven (ADR 0208); twenty-one with node-package-manager and
// node-container-runtime (ADR 0207); nineteen with node-environment and node-login-shell (ADR 0203,
// ADR 0204); seventeen with node-build-agent (ADR 0190). One fewer once the retired
// mesh-build-machine row goes, when no registered manifest claims it.
// Forty-eight with mesh-delivery-checker (novox/hq ADR 0547 §10, issue 774); forty-nine with node-engine (novox/hq ADR 0563 §1);
// fifty-three with the four health seats (novox/hq ADR 0564 clause 1); fifty-five with mesh-wants and mesh-hand-acts
// (novox/hq ADR 0565 clause 1); fifty-six with mesh-artifact-sweep (novox/hq ADR 0571 clause 2);
// fifty-seven with mesh-grants (ADR 0582 clause 1); fifty-eight with mesh-enrolment (novox/hq ADR 0587 §1); fifty-six
// without the controller's seat and mesh-grants, which served its lease write alone (novox/hq ADR 0590, issue 1126);
// fifty-nine with the registry seats go-package-registry, container-image-registry and oci-artifact-registry beside
// npm-package-registry (novox/hq ADR 0613, design 51), the seat list now mesh-contracts' (ADR 0625).
if len(catalogue.Seats()) != 59 {
t.Errorf("the mesh defines %d seats rather than 59; the set is closed, so a change here is "+
"a decision (novox/hq ADR 0110): %s", len(catalogue.Seats()), seatNames())
}
}
// §4: xinitrc and xresources slots, placed by the display server's holder alone: the judge takes a session
+4
View File
@@ -63,6 +63,10 @@ type Request struct {
// 0578 §5, ADR 0606 §4); the composer does not read them. Nil is no list at hand, and the judge judges no name; an
// empty list is a list, and every name is unknown to it.
Packages []string `json:"packages,omitzero"`
// Seats are the seat definitions the judge judges against, as the asker's seat list reader holds them (the definitions of the
// seat mesh-catalog, resolved), raw so that this package names no seat (ADR 0635 §4, ADR 0636 §1). The judge reads them; the
// composer does not. None is no list at hand: the judge then judges against the list its process was given.
Seats []json.RawMessage `json:"seats,omitempty"`
}
// Case is one catalogue composed: every module's manifest by name, and per node the manifests kept at the build it runs.