Hold a node's want until the grant names its account (issue 1945, rule 6 per node)
mesh/delivery-group group fix/1945-credential-when-row-without-seal held: a member is held: novox/mesh-controller@fbf14b68dbab (merged without a passing check:…
mesh/delivery held for a person: merged without a passing check: only a person decides what follows
mesh/check pass 61196c99e6a4 on 5ba6881f9fd3 | unit 163 run, 421 kept | integration 173 run, 487 kept | composition passed on ace
mesh/delivery-group group fix/1945-credential-when-row-without-seal held: a member is held: novox/mesh-controller@fbf14b68dbab (merged without a passing check:…
mesh/delivery held for a person: merged without a passing check: only a person decides what follows
mesh/check pass 61196c99e6a4 on 5ba6881f9fd3 | unit 163 run, 421 kept | integration 173 run, 487 kept | composition passed on ace
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).
This commit is contained in:
@@ -129,6 +129,11 @@ func (p *passer) wants(ctx context.Context) {
|
|||||||
m.Released = p.answeredWith(hold, "release")
|
m.Released = p.answeredWith(hold, "release")
|
||||||
m.Kept = p.answeredWith(hold, "keep")
|
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)
|
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 {
|
if m.Released {
|
||||||
p.clearHeld(name, fp)
|
p.clearHeld(name, fp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
// 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.
|
// that runs an older one names it (issue 1945). Nil: none.
|
||||||
GrantedAt func(fp string) bool
|
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
|
Manifest catalogue.Manifest
|
||||||
Nodes []string
|
Nodes []string
|
||||||
// Composed answers the controller's composition of the module for a node at a fingerprint (mesh-controller_composed): its
|
// 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 {
|
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))
|
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 {
|
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))
|
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
|
return w, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GrantNamesNode says whether a grant of module names its accounts on node: its own `<node>.<module>` or a process's
|
||||||
|
// `<node>.<module>.<process>`. 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -86,6 +86,33 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) {
|
|||||||
t.Errorf("rule %d broken: g14's want is %+v", rule, w)
|
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.
|
// 1297/B4, ADR 0435 §3: a canary that is offline is passed over: the next node by name is the canary, and moves first.
|
||||||
|
|||||||
Reference in New Issue
Block a user