From 61196c99e6a49bb42143fee56aafc2d697662274 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 25 Oct 2026 20:13:39 +0100 Subject: [PATCH] Hold a node's want until the grant names its account (issue 1945, rule 6 per node) Rule 6 read only whether a grant of the module's fingerprint existed. A grant written for the module's other nodes then let a node whose account was never issued or sealed be wanted: on 2026-10-25 at 20:04 ace's mesh-build-go was wanted with no ace.mesh-build-go on the bus, and crash-looped 20 times for its missing credential. A grant that names accounts now grants a node only when it names that node's own or a process's account; one that names none grants every node, as before. Research 072: the grants piece (memberships and credentials heard from placement). --- .../cmd/mesh-delivery-assigner/pass.go | 5 ++++ .../internal/assign/wants.go | 23 ++++++++++++++++ .../internal/assign/wants_test.go | 27 +++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go index 5a9402d..15282f6 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go @@ -129,6 +129,11 @@ func (p *passer) wants(ctx context.Context) { m.Released = p.answeredWith(hold, "release") m.Kept = p.answeredWith(hold, "keep") m.Built, m.Held, m.Granted = p.w.built(name, fp), p.w.has(delivery.AssignerWantsBucket, held) && !m.Released, p.w.has(delivery.ControllerGrantsBucket, grant) + if raw := p.w.value(delivery.ControllerGrantsBucket, grant); raw != nil { + if g, err := delivery.DecodeControllerGrant(raw); err == nil { + m.GrantedOn = func(node string) bool { return assign.GrantNamesNode(g, node, name) } + } + } if m.Released { p.clearHeld(name, fp) } diff --git a/modules/mesh-delivery-assigner/internal/assign/wants.go b/modules/mesh-delivery-assigner/internal/assign/wants.go index 3c25317..2f79b1b 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants.go @@ -23,6 +23,10 @@ type Module struct { // GrantedAt says whether the controller issued the module's credential for another fingerprint than FP: the first want of a node // that runs an older one names it (issue 1945). Nil: none. GrantedAt func(fp string) bool + // GrantedOn says whether the controller's grant of FP names the module's accounts on a node, when the grant names any: + // a grant written for the module's other nodes says nothing of a node whose account is not issued and sealed (issue + // 1945, live on 2026-10-25 at 20:04: ace's mesh-build-go was wanted with no account on the bus). Nil: the grant alone. + GrantedOn func(node string) bool Manifest catalogue.Manifest Nodes []string // Composed answers the controller's composition of the module for a node at a fingerprint (mesh-controller_composed): its @@ -108,6 +112,10 @@ func holds(m Module, node string, n Nodes, ordering bool) (bool, int, string) { if !m.Granted { return false, delivery.RuleGrant, fmt.Sprintf("the controller has not issued the credential of %s for %s", m.Name, short(m.FP)) } + if m.GrantedOn != nil && !m.GrantedOn(node) { + return false, delivery.RuleGrant, fmt.Sprintf("the controller has not issued the credential of %s on %s for %s", m.Name, node, + short(m.FP)) + } if _, ok := m.composition(node, m.FP); !ok { return false, delivery.RuleComposed, fmt.Sprintf("the controller has not composed %s for %s at %s", m.Name, node, short(m.FP)) } @@ -466,3 +474,18 @@ func (m Module) userListRevision(node string, before delivery.Want, had bool, n } return w, true } + +// GrantNamesNode says whether a grant of module names its accounts on node: its own `.` or a process's +// `..`. A grant that names no account (a module that does not speak on the bus) names every node. +func GrantNamesNode(g delivery.ControllerGrant, node, module string) bool { + if len(g.Accounts) == 0 { + return true + } + own := node + "." + module + for _, a := range g.Accounts { + if a == own || strings.HasPrefix(a, own+".") { + return true + } + } + return false +} diff --git a/modules/mesh-delivery-assigner/internal/assign/wants_test.go b/modules/mesh-delivery-assigner/internal/assign/wants_test.go index 5e9295c..775d53c 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants_test.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants_test.go @@ -86,6 +86,33 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { t.Errorf("rule %d broken: g14's want is %+v", rule, w) } } + + // Rule 6 per node: a grant written for the module's other nodes does not grant a node whose account it does not name + // (issue 1945, live on 2026-10-25 at 20:04: ace's mesh-build-go was wanted with no account on the bus). + m, n := ready() + grant := delivery.ControllerGrant{Accounts: []string{"ace.mail", "ace.mail.send"}} + m.GrantedOn = func(node string) bool { return GrantNamesNode(grant, node, "mail") } + out := Wants(m, n, wantedOld(), nil) + if w := out.Wants["ace"]; w.FP != fNew { + t.Errorf("ace, whose account the grant names, wants %+v", w) + } + if got := out.Rules["g14"]; got.Rule != delivery.RuleGrant || !strings.Contains(got.Words, "on g14") { + t.Errorf("g14, whose account the grant does not name, is held by %+v", got) + } + for _, c := range []struct { + accounts []string + node, module string + named bool + }{ + {nil, "g14", "mail", true}, + {[]string{"g14.mail.send"}, "g14", "mail", true}, + {[]string{"g14.mailer"}, "g14", "mail", false}, + {[]string{"ace.mail"}, "g14", "mail", false}, + } { + if got := GrantNamesNode(delivery.ControllerGrant{Accounts: c.accounts}, c.node, c.module); got != c.named { + t.Errorf("%v names %s of %s: %v, want %v", c.accounts, c.module, c.node, got, c.named) + } + } } // 1297/B4, ADR 0435 ยง3: a canary that is offline is passed over: the next node by name is the canary, and moves first.