mesh-grants reads placement alone and hears no controller event; this repository's manifests depend on the controller only by the lease

ADR 0549 §5, ADR 0564 §9, ADR 0571 §10: the part no longer reads mesh-controller.assignments (the union with
placement goes; placement not read is a failed read that places nothing) nor consumes the controller's
condition-raised and condition-cleared, the keeper's being the one source. ADR 0571 §13's test for this
repository replaces the guard of ADR 0549 §6 and its list ../assignments-readers, now empty and deleted: at
phase write no module.json here depends on mesh-controller beyond the lease and the three kept to the write
(catalogue.ManifestRefusals).

Tests: in TestGrants the fixture places through the seat's placement (assign, unassign), the subtest of the
stale assignments becomes 774/S29/AC1 (placement alone gives the fixture's three memberships with no read,
consume or call of the controller; the fixture is the suite's own, whose assignments named the same modules,
not a copy of live memberships), and the assignments-readers subtest becomes 774/S24/AC1. No new test function.

(issue 774, 774/S29, 774/S24)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-02 00:35:59 +01:00
parent b81d7475e8
commit a78fa3db9d
6 changed files with 104 additions and 142 deletions
-7
View File
@@ -1,7 +0,0 @@
# The modules of this repository that still read the controller's assignments (mesh-controller.assignments), each with
# why. mesh-grants' test (TestGrants, "assignments readers") refuses any other module.json that reads them, and an entry
# here whose module no longer does: the list only shrinks, and it is empty in the first change after the controller's
# lease is written to a holder that is no controller (novox/hq ADR 0549 §3, §6; issue 774). Each module named here
# uses the assignments only while the controller holds the lease (buscompose.LeaseHandedAway), placement alone after it.
mesh-delivery-assigner unioned with the placement until the lease write (assign.Placed with handedAway); placement alone after
mesh-grants unioned with the placement until the lease write (ReadInputs); placement alone after
+84 -72
View File
@@ -319,8 +319,38 @@ func (b *fakeBus) refuse(module, raw, problem string) {
Problems: []string{problem}})
}
// assign places module on node in the seat's placement, the one place the part reads it (ADR 0549 §5; issue 774).
func (b *fakeBus) assign(node, module string) {
b.put(grants.ReadAssignments, node+"."+module, map[string]any{"node": node, "module": module, "assigned": "2026-10-25T20:00:00Z"})
var pl delivery.Placement
if raw, ok := b.states[grants.ReadPlacement]["placement."+node]; ok {
pl, _ = delivery.DecodePlacement(raw)
}
pl.Node = node
for _, m := range pl.Modules {
if m.Module == module {
return
}
}
pl.Modules = append(pl.Modules, delivery.PlacedModule{Module: module})
raw, err := delivery.EncodePlacement(pl)
if err != nil {
panic(err)
}
if b.states[grants.ReadPlacement] == nil {
b.states[grants.ReadPlacement] = map[string][]byte{}
}
b.states[grants.ReadPlacement]["placement."+node] = raw
}
// unassign takes module off node in the seat's placement.
func (b *fakeBus) unassign(node, module string) {
pl, err := delivery.DecodePlacement(b.states[grants.ReadPlacement]["placement."+node])
if err != nil {
return
}
pl.Modules = slices.DeleteFunc(pl.Modules, func(m delivery.PlacedModule) bool { return m.Module == module })
raw, _ := delivery.EncodePlacement(pl)
b.states[grants.ReadPlacement]["placement."+node] = raw
}
const (
@@ -350,7 +380,6 @@ func fixture() *fakeBus {
b.assign("a", "nats")
b.assign("a", "mod-x")
b.assign("b", "mod-y")
b.put(grants.ReadAssignments, "b.gone", map[string]any{"node": "b", "module": "gone", "assigned": "2026-10-01T00:00:00Z", "ended": "2026-10-02T00:00:00Z"})
b.put(grants.ReadHave, "a.node", map[string]any{"v": 1})
b.put(grants.ReadHave, "b.node", map[string]any{"v": 1})
b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK,
@@ -512,8 +541,8 @@ func TestGrants(t *testing.T) {
key, _ := delivery.BuilderBuildKey("mod-x", fp)
b.put(grants.ReadBuilds, key, map[string]any{"v": delivery.Version, "state": "done", "manifest": digest})
}, []string{"a.mod-x", "a.nats", "b.mod-y", "controller", "node.a", "node.b"}, "enrolment tokens"},
{"placement refused is a gap; the machines come from the haves", func(b *fakeBus) { b.refused[grants.ReadAssignments] = true },
[]string{"controller", "node.a", "node.b"}, grants.ReadAssignments},
{"placement refused is a gap; the machines come from the haves", func(b *fakeBus) { b.refused[grants.ReadPlacement] = true },
[]string{"controller", "node.a", "node.b"}, grants.ReadPlacement},
} {
t.Run(row.name, func(t *testing.T) {
b := fixture()
@@ -1101,8 +1130,10 @@ func TestGrants(t *testing.T) {
t.Errorf("read of the keys per module: placed %v, gaps %v", got.Placed, got.Gaps)
}
b.refused[grants.ReadPlacement] = true
if read := grants.ReadInputs(b, &grants.Manifests{}); slices.ContainsFunc(read.Gaps, func(g grants.Gap) bool { return g.Input == grants.ReadPlacement && g.Failed }) {
t.Errorf("placement not on the bus is a failed read: %v", read.Gaps)
// The placement alone places (issue 774, 774/S29): not read, nothing is placed, and it is a failed read.
if read := grants.ReadInputs(b, &grants.Manifests{}); !slices.ContainsFunc(read.Gaps, func(g grants.Gap) bool { return g.Input == grants.ReadPlacement && g.Failed }) ||
len(read.Assigned) != 0 {
t.Errorf("placement not on the bus is no failed read, or something is placed: %v, %v", read.Gaps, read.Assigned)
}
// Issue 1945, the user list of 2026-10-27 09:21: a placement's holds are the holders on record, not every holder.
// A mesh seat no placement holds keeps the module claiming it as its holder, wherever placement puts it.
@@ -1374,7 +1405,7 @@ func TestGrants(t *testing.T) {
// settled is the placement of the lists agreeing, every hash held, and the loaded list as edit leaves it.
settled := func(edit func(modX *grants.User)) func() {
return func() {
b.states[grants.ReadAssignments] = nil
b.states[grants.ReadPlacement] = nil
b.assign("a", "nats")
b.assign("a", "mod-x")
b.assign("b", "mod-y")
@@ -1585,7 +1616,7 @@ func TestGrants(t *testing.T) {
b.loaded[i].Subscribe = append(slices.Clone(b.loaded[i].Subscribe), seatGrant)
}
}
b.put(grants.ReadAssignments, "a.mod-y", map[string]any{"node": "a", "module": "mod-y", "assigned": "2026-10-27T08:20:00Z"})
b.assign("a", "mod-y")
b.put(grants.ReadAccounts, "a.mod-y", map[string]any{"user": "a.mod-y", "hash": "$2a$10$x", "generation": 1})
said = nil
res := p.Compose()
@@ -1607,7 +1638,7 @@ func TestGrants(t *testing.T) {
b.refuseUsers = true
b.mu.Unlock()
b.mu.Lock()
delete(b.states[grants.ReadAssignments], "a.mod-y")
b.unassign("a", "mod-y")
b.mu.Unlock()
if res := p.Compose(); res.Sealed == nil || !strings.Contains(res.Sealed.Why, "the grants of the list in force cannot be read") {
t.Errorf("a pass whose loaded list cannot be read = %+v", res.Sealed)
@@ -2045,11 +2076,11 @@ func TestGrants(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() { f.Run(ctx); close(done) }()
waitOpened(t, w, grants.ReadAssignments)
waitOpened(t, w, grants.ReadPlacement)
if r := f.Refused(); r[grants.ReadModules] == "" {
waitFor(t, func() bool { return f.Refused()[grants.ReadModules] != "" })
}
w.change(grants.ReadAssignments, "a.mod-x")
w.change(grants.ReadPlacement, "placement.a")
waitOpened(t, w, grants.ReadModules)
waitFor(t, func() bool { return len(f.Refused()) == 0 })
select {
@@ -2242,83 +2273,64 @@ func TestGrants(t *testing.T) {
}
})
// The guard of ADR 0549 §6 for this repository, as mesh-catalog's checks/placement is for the catalogue: no module.json
// here reads the controller's assignments unless ../assignments-readers names it with why, and that list only shrinks.
t.Run("assignments readers: only the named modules read the controller's assignments, and the list only shrinks", func(t *testing.T) {
raw, err := os.ReadFile("../assignments-readers")
if err != nil {
t.Fatal(err)
}
allowed := map[string]bool{}
for _, line := range strings.Split(string(raw), "\n") {
if f := strings.Fields(line); len(f) > 1 && !strings.HasPrefix(f[0], "#") {
allowed[f[0]] = true
} else if len(f) == 1 {
t.Errorf("../assignments-readers names %s with no why", f[0])
}
}
// ADR 0571 §13's test for this repository (issue 774, 774/S24/AC1), replacing the guard of ADR 0549 §6 and its list
// ../assignments-readers: at phase write no module.json here depends on mesh-controller beyond the lease and the three
// kept to the write (mesh-sdk catalogue.ManifestRefusals, the judge grants_lease_check runs).
t.Run("no module of this repository depends on the controller beyond what the lease write keeps", func(t *testing.T) {
paths, _ := filepath.Glob("../*/module.json")
reads := map[string]bool{}
manifests := map[string]catalogue.Manifest{}
for _, path := range paths {
raw, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
var m struct {
Module string `json:"module"`
Reads []string `json:"reads"`
}
if err := json.Unmarshal(raw, &m); err != nil {
m, err := catalogue.Decode(raw)
if err != nil {
t.Fatalf("%s: %v", path, err)
}
if slices.Contains(m.Reads, grants.ReadAssignments) {
reads[m.Module] = true
if !allowed[m.Module] {
t.Errorf("%s reads %s: read where modules run from the seat mesh-placement's placement (ADR 0549 §3); the lease write "+
"is refused while a module reads the assignments (ADR 0549 §6)", m.Module, grants.ReadAssignments)
}
}
manifests[m.Module] = m
}
for m := range allowed {
if !reads[m] {
t.Errorf("../assignments-readers names %s, which no longer reads %s: take it off the list", m, grants.ReadAssignments)
}
if len(manifests) < 8 {
t.Fatalf("read %d manifests, want every module of the repository", len(manifests))
}
for _, why := range catalogue.ManifestRefusals(manifests, catalogue.PhaseWrite) {
t.Error(why)
}
})
// 774 (the L3 review, ADR 0549 §3, §6): once the controller's lease is handed away the assignments are frozen and place
// nothing; the seat's placement alone does. A module left only in the stale assignments loses its account and its
// grant names no account of it; before the lease write the two are still unioned.
t.Run("placement alone once the lease is handed away: a module only in the stale assignments has no account or grant of it", func(t *testing.T) {
// 774/S29/AC1 (ADR 0549 §5, ADR 0571 §10): the memberships composed from the placement alone, with no read of the
// controller, are the ones the union of placement and assignments gave on the same input: the fixture's (two nodes,
// the bus and mod-x on a, mod-y on b, as placed on the live mesh's own shape of nats beside its modules), whose
// assignments named the same modules. Neither the controller's assignments nor its condition events are read.
t.Run("placement alone places, with the memberships the union gave", func(t *testing.T) {
b := fixture()
b.loaded = shadowOf(t, b)
p := partOn(b)
p.Bus = grants.BusAddress{URL: "nats://a.mesh:4222", Fingerprint: "ab"}
pl, _ := delivery.EncodePlacement(delivery.Placement{Node: "a", Modules: []delivery.PlacedModule{{Module: "nats"}, {Module: "mod-x"}}})
b.states[grants.ReadPlacement] = map[string][]byte{"placement.a": pl}
for _, u := range []string{"a.mod-x", "a.nats", "b.mod-y"} {
b.put(grants.ReadAccounts, u, map[string]any{"user": u, "hash": "$2a$10$x", "generation": 1})
}
fp := "src2:" + strings.Repeat("ab", 32)
key, _ := delivery.PartGrantKey("mod-y", fp)
grantOf := func() []string {
g, _ := delivery.DecodeControllerGrant(b.own[grants.StateGrants][key])
return g.Accounts
}
b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"instance": "novox/1/2026-10-28", "epoch": 60})
p.Compose()
if b.own[grants.StateAccounts]["b.mod-y"] == nil || !slices.Equal(grantOf(), []string{"b.mod-y"}) {
t.Fatalf("under the controller's lease the assignments no longer count: account %v, grant %v",
b.own[grants.StateAccounts]["b.mod-y"] != nil, grantOf())
want := map[string]string{"a.nats": "", "a.mod-x": "", "b.mod-y": ""}
for key := range want {
raw, _, found, _ := b.ownGet(grants.StateMemberships, key)
if !found {
t.Fatalf("no membership of %s from the placement", key)
}
want[key] = string(raw)
}
b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"handed-to": "the operator's hand-over"})
in := grants.ReadInputs(b, &grants.Manifests{})
if len(in.Assigned["b"]) != 0 || !slices.Equal(in.Assigned["a"], []string{"mod-x", "nats"}) || in.BusNode != "a" {
t.Errorf("after the lease write the stale assignments still place: %v, bus %q", in.Assigned, in.BusNode)
if got := slices.Sorted(maps.Keys(b.own[grants.StateMemberships])); !slices.Equal(got, []string{"a.mod-x", "a.nats", "b.mod-y"}) {
t.Errorf("memberships %v", got)
}
res := p.Compose()
if res.Accounts == nil || !slices.Equal(res.Accounts.Deleted, []string{"b.mod-y"}) || slices.Contains(grantOf(), "b.mod-y") {
t.Fatalf("a module only in the stale assignments kept its account or grant: %+v, grant %v", res.Accounts, grantOf())
for _, r := range grants.Reads {
if strings.HasPrefix(r, "mesh-controller.") && r != grants.ReadLease {
t.Errorf("the part reads %s", r)
}
}
for _, c := range grants.Consumes {
if strings.HasPrefix(c, "mesh-controller.") {
t.Errorf("the part consumes %s", c)
}
}
for _, c := range b.calls {
if strings.Contains(c, "mesh-controller") {
t.Errorf("the part called %s", c)
}
}
})
@@ -2360,7 +2372,7 @@ func TestGrants(t *testing.T) {
t.Errorf("the wanted accounts are read by %v", readers)
}
// A read that fails retires nothing; once read, the user placement dropped is retired.
delete(b.states[grants.ReadAssignments], "b.mod-y")
b.unassign("b", "mod-y")
b.refused[grants.ReadResolved] = true
if res := p.Compose(); res.Accounts == nil || len(res.Accounts.Deleted) != 0 || b.own[grants.StateAccounts]["b.mod-y"] == nil {
t.Fatalf("a pass whose read failed deleted %+v", res.Accounts)
@@ -22,7 +22,7 @@ type Watcher interface {
// wants (the manifest each machine is wanted to run), and the vault's hashes of the bus accounts.
//
// And its own state lease, whose key removal-due's expiry marker starts the day-later removal (ADR 0582 clause 6).
var Followed = []string{ReadAssignments, ReadModules, ReadResolved, ReadHave, ReadWants, ReadAccounts, ReadPlacement, ReadLease, ReadTakeover,
var Followed = []string{ReadModules, ReadResolved, ReadHave, ReadWants, ReadAccounts, ReadPlacement, ReadLease, ReadTakeover,
OwnLease}
// OwnLease is the part's own state lease as the follower watches it, `<module>.<state>`.
+16 -57
View File
@@ -24,13 +24,12 @@ import (
// The states of other parts the grants part reads, as its manifest's reads name them (`<module>.<state>`).
const (
ReadAssignments = "mesh-controller.assignments"
ReadModules = "mesh-delivery-indexer.modules"
ReadManifests = "mesh-delivery-indexer.manifests"
ReadHave = "node-engine.have"
ReadResolved = "mesh-config.resolved"
ReadWants = "mesh-delivery-assigner.wants"
ReadBuilds = "mesh-delivery-builder.builds"
ReadModules = "mesh-delivery-indexer.modules"
ReadManifests = "mesh-delivery-indexer.manifests"
ReadHave = "node-engine.have"
ReadResolved = "mesh-config.resolved"
ReadWants = "mesh-delivery-assigner.wants"
ReadBuilds = "mesh-delivery-builder.builds"
// ReadAccounts is the vault's bcrypt hashes of the bus accounts it holds, by bus user (ADR 0547 clause 9).
ReadAccounts = "mesh-vault.bus-accounts"
// ReadPlacement is the seat mesh-placement's placement (ADR 0567 §1, §9): each node's modules and the seats
@@ -38,10 +37,9 @@ const (
ReadPlacement = delivery.PlacementRead
)
// Reads is every state the part reads, in the manifest's order. ReadAssignments stays (and in Followed) only while the
// controller holds the lease, when the assignments are unioned with the placement (ReadInputs); after the lease write
// they place nothing, and the first change after it takes them out (../assignments-readers names the part until then).
var Reads = []string{ReadAssignments, ReadModules, ReadManifests, ReadResolved, ReadHave, ReadWants, ReadBuilds,
// Reads is every state the part reads, in the manifest's order. Not the controller's assignments: the seat's placement
// alone places modules (novox/hq ADR 0549 §5, ADR 0571 §10; issue 774).
var Reads = []string{ReadModules, ReadManifests, ReadResolved, ReadHave, ReadWants, ReadBuilds,
ReadAccounts, ReadPlacement, ReadLease, ReadTakeover}
// The bus module's tools the part calls, as its manifest's invokes name them: read-only, each.
@@ -83,18 +81,6 @@ var standingGaps = []Gap{
{Input: "announced states", Why: "the controller does not announce its states-announced readable, so every read is composed from `reads`, as when announcements cannot be read"},
}
// assignment is a key of the controller's assignments as composing needs it (mesh-controller internal/chain Assignment):
// a value that names no time assigned (a module pulled in), has ended, or waits in a state, is no assignment.
type assignment struct {
Node string `json:"node"`
Module string `json:"module"`
Assigned time.Time `json:"assigned,omitzero"`
Ended time.Time `json:"ended,omitzero"`
State string `json:"state,omitempty"`
}
func (a assignment) stands() bool { return !a.Assigned.IsZero() && a.Ended.IsZero() && a.State == "" }
// resolved is a key of mesh-config's resolved values, `<node>.<module>.<key>`.
type resolved struct {
Node string `json:"node"`
@@ -105,10 +91,9 @@ type resolved struct {
// Inputs is what the shadow composes from, read from the bus.
type Inputs struct {
// Nodes is every machine seen: in a node agent's have or a standing assignment.
// Nodes is every machine seen: in a node agent's have or the placement.
Nodes []string
// Assigned is each machine's standing assignments and what the seat's placement puts there, sorted: while both
// stand (ADR 0549 §5) a module either names keeps its membership.
// Assigned is what the seat's placement puts on each machine, sorted (issue 774: never the controller's assignments).
Assigned map[string][]string
// Placed is each node's placement as the seat mesh-placement holds it (ADR 0567 §1); empty before any is read.
// Where it is, a seat held once for the mesh is held where placement says, not wherever a module claiming it runs.
@@ -248,26 +233,11 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
NoFingerprint: map[string]string{}, Running: map[string]catalogue.Manifest{},
Values: map[string]any{}, Gaps: append([]Gap(nil), standingGaps...)}
nodes := map[string]bool{}
// **After the lease write the assignments place nothing** (novox/hq ADR 0549 §3, §6): they are frozen then, so a
// module the catalogue takes off a node would keep its account, grant, membership and user-list entry. Once the lease
// is handed away and the placement is read, the placement alone places modules, as the assigner's Placed does with
// the same lease reading (buscompose.LeaseHandedAway); before, or with no placement on the bus, the two are unioned.
byAssignment, busByAssignment := map[string][]string{}, ""
for _, raw := range each(r, ReadAssignments, &in.Gaps) {
var a assignment
if json.Unmarshal(raw, &a) != nil || !a.stands() || a.Node == "" || a.Module == "" {
continue
}
byAssignment[a.Node] = append(byAssignment[a.Node], a.Module)
if a.Module == BusModule {
busByAssignment = a.Node
}
}
// The seat's placement (ADR 0567 §9): what it places is composed beside the assignments, and the seats held once
// for the mesh are held where it says. Not on the bus yet is a standing gap, never a failed read, so the list is still
// composed and sealed from the assignments alone.
// **The seat's placement alone places modules** (novox/hq ADR 0549 §5, ADR 0567 §9; issue 774): the controller's
// assignments are read no more, and the seats held once for the mesh are held where it says. Not read is a failed
// read: nothing is placed, so after take-over the list composed past it is not sealed.
if keys, err := r.Keys(ReadPlacement); err != nil {
in.Gaps = append(in.Gaps, Gap{Input: ReadPlacement, Why: "not read yet: the assignments alone place modules (" + err.Error() + ")"})
in.Gaps = append(in.Gaps, Gap{Input: ReadPlacement, Why: "refused or absent: nothing is placed (" + err.Error() + ")", Failed: true})
} else {
// Each node's keys in either form, assembled by the seat's one reader (delivery.PlacementsOf; research 072
// row 37): its node key and a key per module, or its legacy key `placement.<node>`.
@@ -294,17 +264,6 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
in.Placed[node] = p
}
}
away, _ := leaseHandedAway(r)
if away && len(in.Placed) > 0 {
in.Gaps = append(in.Gaps, Gap{Input: ReadAssignments, Why: "not used: the controller's lease is handed away, " +
"so the seat's placement alone places modules (ADR 0549 §3, §6)"})
} else {
for node, ms := range byAssignment {
nodes[node] = true
in.Assigned[node] = append(in.Assigned[node], ms...)
}
in.BusNode = busByAssignment
}
for node, p := range in.Placed {
nodes[node] = true
for _, m := range p.Assigned() {
@@ -328,7 +287,7 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
nodes[node] = true
}
}
in.Gaps = append(in.Gaps, Gap{Input: "machines", Why: "read from the node agents' haves and the assignments: a machine with neither is not seen"})
in.Gaps = append(in.Gaps, Gap{Input: "machines", Why: "read from the node agents' haves and the placement: a machine with neither is not seen"})
for n := range nodes {
in.Nodes = append(in.Nodes, n)
}
@@ -49,8 +49,9 @@ func HeardFrom(event string) string {
const EventAccountIssued = "mesh-vault." + buscompose.AccountIssuedEvent
// Consumes is every event the part hears, as its manifest's consumes name them.
var Consumes = []string{EventConditionRaised, EventConditionCleared, EventKeeperRaised, EventKeeperCleared, EventAccountIssued,
EventBucketExpirySet}
//
// Not the controller's condition events: the keeper's alone (novox/hq ADR 0564 §9, issue 774).
var Consumes = []string{EventKeeperRaised, EventKeeperCleared, EventAccountIssued, EventBucketExpirySet}
// IsAccountIssued says an event heard is the vault's account-issued, by its name or its bus subject.
func IsAccountIssued(event string) bool {
-3
View File
@@ -14,7 +14,6 @@
],
"reads": [
"mesh-config.resolved",
"mesh-controller.assignments",
"mesh-delivery-assigner.wants",
"mesh-delivery-builder.builds",
"mesh-delivery-indexer.manifests",
@@ -26,8 +25,6 @@
"mesh-conditions.takeover"
],
"consumes": [
"mesh-controller.condition-raised",
"mesh-controller.condition-cleared",
"mesh-conditions.condition-raised",
"mesh-conditions.condition-cleared",
"mesh-vault.account-issued",