Grants compose the holders on record through mesh-sdk's SeatsToPassOn, so a seat whose holder's manifest left it is granted to its only claimant (issue 1966, ADR 0660)

Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5
This commit is contained in:
2026-11-08 15:35:26 +01:00
parent 9c6bf236b6
commit bddb6b1eb3
2 changed files with 149 additions and 20 deletions
+85
View File
@@ -1281,6 +1281,91 @@ func TestGrants(t *testing.T) {
}
})
// Issue 1966, ADR 0660: the live case. The assigner's record still names the catalogue module as the holder of
// mesh-catalog, whose manifest claims it no more, and the indexer claims it: the rule of mesh-sdk (the node-engine's
// too) passes the seat to the indexer, the user list grants the seat's permissions to it, the pass is said, and the list
// is not held back as the loss of the seat's last holder.
t.Run("a holder on record whose manifest left the seat passes it to its only claimant", func(t *testing.T) {
seat, ok := catalogue.SeatNamed("mesh-catalog")
if !ok {
t.Fatal("mesh-catalog is not a seat")
}
var verbs []string
for _, v := range seat.Serves {
verbs = append(verbs, v.Name)
}
claimer := func(module string) catalogue.Manifest {
return catalogue.Manifest{Module: module, Version: "1", RunsAs: module, OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/var/lib/" + module + "/broker"}}, Claims: []catalogue.Claim{{Name: "mesh-catalog", Scope: catalogue.ScopeMesh, Serves: verbs}},
Provides: []catalogue.Offer{{Name: seat.Delivers, Scope: catalogue.ScopeMesh}}}
}
left := catalogue.Manifest{Module: "mesh-catalog", Version: "1", RunsAs: "mesh-catalog", OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/var/lib/mesh-catalog/broker"}}}
rows := []struct {
name string
running map[string]catalogue.Manifest // by `<node>.<module>`
extra []string // more claimants, assigned on node b
want []string // the modules holding the seat afterwards
said string
}{
{"the trunk of the holder dropped the seat", nil, nil, []string{"mesh-delivery-indexer"}, "composed as held by mesh-delivery-indexer"},
{"the holder's running build still claims it", map[string]catalogue.Manifest{"a.mesh-catalog": claimer("mesh-catalog")}, nil, []string{"mesh-catalog"}, ""},
{"a second claimant", nil, []string{"other"}, nil, "seat mesh-catalog --to <node>/<module>"},
}
for _, row := range rows {
t.Run(row.name, func(t *testing.T) {
in := grants.Inputs{Nodes: []string{"a", "b"},
Assigned: map[string][]string{"a": {"mesh-catalog"}, "b": {"mesh-delivery-indexer"}},
Manifests: map[string]catalogue.Manifest{"mesh-catalog": left, "mesh-delivery-indexer": claimer("mesh-delivery-indexer")},
Running: map[string]catalogue.Manifest{},
Placed: map[string]meshmoduleassigner.Assignments{
"a": {Node: "a", Modules: []meshmoduleassigner.PlacedModule{{Module: "mesh-catalog"}},
Holds: []meshmoduleassigner.Holding{{Claim: "mesh-catalog", Scope: "mesh", Module: "mesh-catalog"}}},
"b": {Node: "b", Modules: []meshmoduleassigner.PlacedModule{{Module: "mesh-delivery-indexer"}}}}}
maps.Copy(in.Running, row.running)
for _, c := range row.extra {
in.Manifests[c] = claimer(c)
in.Assigned["b"] = append(in.Assigned["b"], c)
}
before := in.Placed
recs, gaps := grants.ListRecords(in)
users, refusedU, err := grants.ComposeEach(recs)
if len(refusedU) != 0 {
t.Fatalf("refused %v", refusedU)
}
if err != nil {
t.Fatal(err)
}
h := grants.HoldingsOf(in)
var got []string
for _, u := range users {
if h.ByUsers([]grants.User{u})["mesh-catalog"] {
_, module, _ := strings.Cut(u.User, ".")
got = append(got, module)
}
}
slices.Sort(got)
if !slices.Equal(got, row.want) {
t.Errorf("granted the seat's permissions to %v, want %v", got, row.want)
}
var said []string
for _, g := range gaps {
said = append(said, g.Why)
}
if row.said == "" && len(gaps) != 0 || !strings.Contains(strings.Join(said, "\n"), row.said) {
t.Errorf("the pass is said as %q, want %q", said, row.said)
}
// The in-force list carries the seat (granted to the holder on record); the composed one must too, or the
// list is held back as the last holder's loss. Two claimants grant none, and that is held back and said.
lost := h.LastHolders(map[string]bool{"mesh-catalog": true}, h.ByUsers(users))
if (len(lost) != 0) != (len(row.want) == 0) {
t.Errorf("the seat's last holder would be lost: %v", lost)
}
if !reflect.DeepEqual(before, in.Placed) {
t.Errorf("the assigner's record was written: %+v", in.Placed)
}
})
}
})
// 1126 (ADR 0549 §1, §3): once the catalogue places the nodes, a seat held once for the mesh is held where assignments
// says, not on every machine running a module that claims it; what assignments adds is composed beside the assignments.
t.Run("a mesh seat is held where assignments says", func(t *testing.T) {
+64 -20
View File
@@ -3,11 +3,11 @@ package grants
import (
"fmt"
"git.novox.be/novox/mesh-contracts/go/seats/meshbroker"
"maps"
"reflect"
"slices"
"strings"
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"mesh-grants/internal/buscompose"
)
@@ -50,6 +50,10 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) {
out.BusState[module] = m.BusState
}
}
// The holders on record, after the seats whose holder has left are passed (ADR 0660): the one rule of mesh-sdk, the
// node-engine's too, read with the manifest each node runs, else the trunk's. Composed, never recorded.
holders, passed := holdersAfterPass(in)
gaps = append(gaps, passed...)
values := func(provider, setting string) []any {
var vs []any
for _, node := range in.Nodes {
@@ -83,7 +87,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) {
declare := func(m catalogue.Manifest) buscompose.Declared {
d := declaredFor(m, seats, declarers)
if len(in.Placed) > 0 {
d.Holds = heldWherePlaced(d.Holds, in.Placed, node, m.Module)
d.Holds = heldWherePlaced(d.Holds, holders, node, m.Module)
}
for _, sr := range catalogue.ReadsGranted(m, here, node) {
d.KeyedReads = append(d.KeyedReads, buscompose.KeyedRead{Read: sr.Read, Key: sr.Key})
@@ -421,31 +425,71 @@ func hearsOf(m catalogue.Manifest, declared map[string]catalogue.Manifest, node
return out, touches, onEvents
}
// heldWherePlaced is a module's holds on a node as the catalogue's assignments says them (ADR 0549 §1, §3). The assignments'
// holds are the holders on record, as catalogue.World's Holdings are: a seat held once for the mesh that some node's
// assignments holds is held only where assignments names it (this node's naming this module), so a module claiming it on
// two machines holds it on the one assignments names, not on both. A seat no assignments holds is held by derivation, by the
// module claiming it wherever it is placed, as it was before assignments: the seeded assignments hold only the seats the
// controller had on record (twelve on the control-node), and treating their holds as every holder left nine seats with
// no holder in the user list of 2026-10-27 09:21 (issue 1945). A seat of every node is held wherever the module runs.
func heldWherePlaced(holds []buscompose.Seat, placed map[string]meshmoduleassigner.Assignments, node, module string) []buscompose.Seat {
same := func(x meshmoduleassigner.Holding, h buscompose.Seat) bool {
return (x.Scope == "" || x.Scope == catalogue.ScopeMesh) && sameSeat(x.Claim, h.Name) && x.Kind == h.Kind
// holdersAfterPass is the holders on record (each node's assignments, as the seat mesh-module-assigner holds them) as they
// stand after catalogue.SeatsToPassOn (novox/hq ADR 0660): a holder whose every assignment's manifest no longer claims
// its seat has left it, and the seat is the only claimant's. Each assignment is read with the manifest its node runs
// (Inputs.Running), else the trunk's, else the running build of a held module; one with none is unread and keeps what it
// holds. A pass is said as a gap and recorded nowhere (ADR 0660 §4). Pure.
func holdersAfterPass(in Inputs) ([]catalogue.Held, []Gap) {
var held []catalogue.Held
for _, node := range slices.Sorted(maps.Keys(in.Placed)) {
for _, x := range in.Placed[node].Holds {
if x.Scope != "" && x.Scope != catalogue.ScopeMesh {
continue
}
held = append(held, catalogue.Held{Claim: x.Claim, Scope: catalogue.ScopeMesh, Node: node, Module: x.Module, Site: x.Site, Kind: x.Kind})
}
}
var assignments []catalogue.Assignment
for _, node := range slices.Sorted(maps.Keys(in.Assigned)) {
for _, module := range in.Assigned[node] {
m, ok := in.Running[node+"."+module]
if !ok {
m, ok = in.Manifests[module]
}
if !ok {
m, ok = in.Held[module]
}
if !ok {
m = catalogue.Manifest{Module: module}
}
assignments = append(assignments, catalogue.Assignment{Node: node, Manifest: m, Unread: !ok})
}
}
passes, said := catalogue.SeatsToPassOn(held, assignments)
var gaps []Gap
for _, p := range passes {
held = slices.DeleteFunc(held, func(h catalogue.Held) bool { return h.Kind == p.From.Kind && sameSeat(h.Claim, p.Seat.Name) })
p.To.Site = p.From.Site
held = append(held, p.To)
gaps = append(gaps, Gap{Input: "seat " + p.Seat.Name, Why: fmt.Sprintf(
"%s on %s is on record as its holder and its manifest no longer claims it: composed as held by %s on %s, not recorded (ADR 0660)",
p.From.Module, p.From.Node, p.To.Module, p.To.Node)})
}
for _, w := range said {
gaps = append(gaps, Gap{Input: "seat passes", Why: w})
}
return held, gaps
}
// heldWherePlaced is a module's holds on a node as the catalogue's assignments says them (ADR 0549 §1, §3). The assignments'
// holds are the holders on record, as catalogue.World's Holdings are, after the passes of holdersAfterPass: a seat held
// once for the mesh that some node's assignments holds is held only where assignments names it (this node's naming this
// module), so a module claiming it on two machines holds it on the one assignments names, not on both. A seat no
// assignments holds is held by derivation, by the module claiming it wherever it is placed, as it was before assignments:
// the seeded assignments hold only the seats the controller had on record (twelve on the control-node), and treating their
// holds as every holder left nine seats with no holder in the user list of 2026-10-27 09:21 (issue 1945). A seat of every
// node is held wherever the module runs.
func heldWherePlaced(holds []buscompose.Seat, holders []catalogue.Held, node, module string) []buscompose.Seat {
same := func(x catalogue.Held, h buscompose.Seat) bool { return sameSeat(x.Claim, h.Name) && x.Kind == h.Kind }
var out []buscompose.Seat
for _, h := range holds {
if h.Scope != "" && h.Scope != catalogue.ScopeMesh {
out = append(out, h)
continue
}
onRecord := false
for _, p := range placed {
if slices.ContainsFunc(p.Holds, func(x meshmoduleassigner.Holding) bool { return same(x, h) }) {
onRecord = true
break
}
}
if !onRecord || slices.ContainsFunc(placed[node].Holds, func(x meshmoduleassigner.Holding) bool { return same(x, h) && x.Module == module }) {
onRecord := slices.ContainsFunc(holders, func(x catalogue.Held) bool { return same(x, h) })
if !onRecord || slices.ContainsFunc(holders, func(x catalogue.Held) bool { return same(x, h) && x.Node == node && x.Module == module }) {
out = append(out, h)
}
}