Merge pull request 'mesh-grants keeps what the bus loaded while it composes only part of the mesh (issue 1966, bootstrap recovery)' (#171) from fix/1966-grants-keeps-what-is-loaded into main

This commit was merged in pull request #171.
This commit is contained in:
2026-11-07 22:35:23 +00:00
6 changed files with 420 additions and 15 deletions
+189 -1
View File
@@ -2414,9 +2414,15 @@ func TestGrants(t *testing.T) {
b.loaded = append(b.loaded, grants.User{User: "c.old"})
b.put(grants.ReadHave, "b.mod-y", runs)
hash("b.mod-y")
if res := p.Compose(); res.Sealed == nil || !strings.Contains(res.Sealed.Why, "would drop c.old of the list in force") {
if res := p.Compose(); res.Sealed == nil || !strings.Contains(res.Sealed.Why, "would take from loaded users: c.old (left out)") ||
!slices.Contains(res.KeptLoaded, "c.old") {
t.Errorf("a list dropping a loaded user was not held: %+v", res.Sealed)
}
// With its hash, the loaded user the haves do not compose is kept as the loaded list gives it, and the list sealed.
hash("c.old")
if res := p.Compose(); res.Sealed == nil || res.Sealed.Why != "" || !strings.Contains(string(b.own[grants.StateUserList][grants.KeyWrittenUsers]), `"c.old"`) {
t.Errorf("the loaded user was not kept in the list sealed: %+v", res.Sealed)
}
// The view answers: the assignments alone places again.
delete(b.refused, grants.ReadAssignments)
if in := grants.ReadInputs(b, &grants.Manifests{}); in.ByHaves != "" || slices.Contains(in.Assigned["a"], "mesh-delivery-assigner") {
@@ -2424,6 +2430,184 @@ func TestGrants(t *testing.T) {
}
})
// Issue 1966, the list sealed 2026-11-07 23:09: composed from the bootstrapped seats alone, it took the node seats from the
// tool runners, the vault's reads from the node-engines, the seat queues from the build modules, and left out the ledger
// account of a held module. A pass that composes part of the mesh (the seat list unread, or placed by the haves) seals
// each user of the loaded list and of the list in force with the union of what it composes and what those give it, so it
// only adds (the indexer's serving of mesh-catalog included); one that would still take a permission is held, naming it.
t.Run("a pass composing part of the mesh keeps what the bus loaded, adding only (issue 1966)", func(t *testing.T) {
indexer, err := os.ReadFile("../mesh-delivery-indexer/module.json")
if err != nil {
t.Fatal(err)
}
const (
nodeSeat = "mesh.seat.node-files.call.*.a.*"
vaultRead = "$JS.API.DIRECT.GET.KV_mesh-vault_signers.>"
buildPull = "$JS.API.CONSUMER.MSG.NEXT.SEAT_NODE_BUILD_GO.workers"
)
ledgerX := strings.Replace(xManifest, `"state"`, `"requires":[{"name":"records","storage":"ledger","template":"ledger",`+
`"types":[{"name":"decision","schema":{},"operations":[{"name":"propose","effect":"set-fields"}]}]}],"state"`, 1)
catalogSeat := func(s string) bool { return strings.HasPrefix(s, "mesh.seat.mesh-catalog.") }
// Review round 1: the list in force gives a wide publish and a deny, the pass composes the user narrower with the
// composer's empty deny: the user kept keeps the deny, and dropping it counts as a loss.
for _, d := range []struct {
name string
composed, kept grants.User
loss string
}{
{"a deny of the list in force is kept",
grants.User{User: "a.mod-v", Publish: []string{"mesh.a"}, Subscribe: []string{}, PublishDeny: []string{}},
grants.User{User: "a.mod-v", Publish: []string{"mesh.>", "mesh.a"}, Subscribe: []string{}, PublishDeny: []string{"mesh.seat.mesh-vault.desk.>"}},
"deny dropped mesh.seat.mesh-vault.desk.>"},
{"a deny the pass composes is kept beside it",
grants.User{User: "a.mod-v", Publish: []string{"mesh.a"}, Subscribe: []string{}, PublishDeny: []string{"mesh.b"}},
grants.User{User: "a.mod-v", Publish: []string{"mesh.>", "mesh.a"}, Subscribe: []string{}, PublishDeny: []string{"mesh.b", "mesh.seat.mesh-vault.desk.>"}},
"deny mesh.b"},
} {
inForce := grants.User{User: "a.mod-v", Publish: []string{"mesh.>"}, Subscribe: []string{}, PublishDeny: []string{"mesh.seat.mesh-vault.desk.>"}}
got := grants.KeepLoaded([]grants.User{d.composed}, nil, map[string]grants.User{"a.mod-v": inForce})["a.mod-v"]
if !slices.Equal(got.Publish, d.kept.Publish) || !slices.Equal(got.PublishDeny, d.kept.PublishDeny) {
t.Errorf("%s: kept %+v, want %+v", d.name, got, d.kept)
}
if !slices.Contains(grants.Losses(d.composed, inForce), d.loss) {
t.Errorf("%s: losses %v name no %q", d.name, grants.Losses(d.composed, inForce), d.loss)
}
}
for _, row := range []struct {
name string
bend func(*fakeBus, *grants.Part)
// held is what the pass's sealed says it holds, "" when it seals.
held string
}{
{"the seat list unread", func(b *fakeBus, p *grants.Part) {
p.SeatList = &seatGate{bootstrapped: []string{meshbroker.Seat, meshcatalog.Seat}}
}, ""},
{"placed by the haves", func(b *fakeBus, p *grants.Part) {
b.refused[grants.ReadAssignments] = true
runs := nodeengine.HaveByNodeModuleValue{V: keys.Version, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}
b.put(grants.ReadHave, "a.mod-x", runs)
b.put(grants.ReadHave, "b.mod-y", runs)
b.put(grants.ReadHave, "a.mesh-delivery-indexer", runs)
}, ""},
// The ledger of a held module stays with its module's users, whatever the seat list (held.go).
{"the ledger of a module whose manifest is refused", func(b *fakeBus, p *grants.Part) {
b.refuse("mod-x", ledgerX, "it is held")
}, ""},
// The union cannot give a login a loaded user has no hash for: held, naming it.
{"a loaded user with no hash, the seat list unread", func(b *fakeBus, p *grants.Part) {
p.SeatList = &seatGate{bootstrapped: []string{meshbroker.Seat, meshcatalog.Seat}}
delete(b.states[grants.ReadAccounts], "b.mod-y")
}, "would take from loaded users: b.mod-y (left out)"},
} {
t.Run(row.name, func(t *testing.T) {
b := fixture()
b.manifestOf("mod-x", ledgerX)
b.manifestOf("mesh-delivery-indexer", string(indexer))
b.assign("a", "mesh-delivery-indexer")
busKey, err := seal.GenerateKeyPair()
if err != nil {
t.Fatal(err)
}
b.put(grants.ReadHave, "a.nats", nodeengine.HaveByNodeModuleValue{V: keys.Version, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK,
UserList: &nodeengine.UserListWritten{Source: grantsstate.GrantsUserListSource, Revision: 8, Wanted: "sha256:aa", Digest: "sha256:aa", Users: 5,
Key: busKey.PublicKey, Follows: grantsstate.FollowGrants}})
whole := shadowOf(t, b)
for _, u := range whole {
b.put(grants.ReadAccounts, u.User, map[string]any{"user": u.User, "hash": "$2a$10$x", "generation": 1, "fingerprint": "sha256:1"})
}
p := partOn(b)
p.Objects = &fakeObjects{}
// The list in force, composed whole.
p.Compose()
if g, _, _ := p.Sealed(); g.Digest == "" {
t.Fatal("no list in force sealed")
}
// The indexer's membership as published serves a tool more than any pass composes now (a tool runner's
// seat, say): a pass composing part of the mesh keeps it as published, a whole one publishes what it composes.
const extra = `{"subject":"mesh.tools.a.mesh-delivery-indexer.extra"}`
{
var m map[string]any
if err := json.Unmarshal(b.own[grants.StateMemberships]["a.mesh-delivery-indexer"], &m); err != nil {
t.Fatal(err)
}
var e any
_ = json.Unmarshal([]byte(extra), &e)
m["serves"] = append(m["serves"].([]any), e)
b.own[grants.StateMemberships]["a.mesh-delivery-indexer"], _ = json.Marshal(m)
}
servesExtra := func() bool {
return strings.Contains(string(b.own[grants.StateMemberships]["a.mesh-delivery-indexer"]), "mesh-delivery-indexer.extra")
}
// The bus loaded it with what a whole seat list gives besides, and without the indexer's serving yet.
var loaded []grants.User
for _, u := range whole {
switch u.User {
case "a.mod-x":
u.Subscribe = append(slices.Clone(u.Subscribe), nodeSeat)
case "node.a":
u.Subscribe = append(slices.Clone(u.Subscribe), vaultRead)
case "b.mod-y":
u.Publish = append(slices.Clone(u.Publish), buildPull)
case "a.mesh-delivery-indexer":
u.Subscribe = slices.DeleteFunc(slices.Clone(u.Subscribe), catalogSeat)
}
loaded = append(loaded, u)
}
if !slices.ContainsFunc(loaded, func(u grants.User) bool { return u.User == "ledger.a.mod-x" }) {
t.Fatalf("the fixture composes no ledger user: %v", names(loaded))
}
b.loaded = loaded
row.bend(b, p)
res := p.Compose()
if g, _, _ := p.Sealed(); true {
t.Logf("sealed %d users (%s), kept with the loaded list's grants %v, said %q", g.Users, short(g.Digest), res.KeptLoaded, res.Sealed.Why)
}
partial := row.name != "the ledger of a module whose manifest is refused"
if servesExtra() != partial || partial != slices.ContainsFunc(res.NotPublished, func(n string) bool {
return strings.HasPrefix(n, "a.mesh-delivery-indexer: kept as published: ") && strings.Contains(n, "mesh-delivery-indexer.extra")
}) {
t.Errorf("the published membership serving more: kept %v in a pass composing part of the mesh %v; not published %v", servesExtra(), partial, res.NotPublished)
}
if row.held != "" {
if res.Sealed == nil || !strings.Contains(res.Sealed.Why, row.held) {
t.Fatalf("not held for %q: %+v", row.held, res.Sealed)
}
return
}
g, _, _ := p.Sealed()
if res.Sealed == nil || res.Sealed.Why != "" || g.Digest == "" {
t.Fatalf("not sealed: %+v (kept %v, refused %v)", res.Sealed, res.KeptLoaded, res.Refused)
}
sealedAs := func(user string) (grants.User, bool) {
raw, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(g.Digest, user))
var u grants.User
return u, ok && json.Unmarshal(raw, &u) == nil
}
if row.name == "the ledger of a module whose manifest is refused" {
// The seat list read, the pass composes the whole mesh and keeps no loaded permission beyond it; the held
// module's users, its ledger's among them, stay as the list in force holds them.
if u, ok := sealedAs("ledger.a.mod-x"); !ok || len(grants.Losses(u, whole[slices.IndexFunc(whole, func(u grants.User) bool { return u.User == "ledger.a.mod-x" })])) > 0 {
t.Errorf("the held module's ledger account is not kept: %+v (%v)", u, ok)
}
return
}
for _, l := range loaded {
u, ok := sealedAs(l.User)
if !ok {
t.Errorf("%s of the loaded list is not in the list sealed", l.User)
continue
}
if lost := grants.Losses(u, l); len(lost) > 0 {
t.Errorf("%s loses %v in the list sealed", l.User, lost)
}
}
if ix, _ := sealedAs("a.mesh-delivery-indexer"); !slices.ContainsFunc(ix.Subscribe, catalogSeat) {
t.Errorf("the indexer does not gain its serving of mesh-catalog: %v", ix.Subscribe)
}
})
}
})
// ADR 0590 clause 5's test for this repository (issue 1126): no module.json here names mesh-controller, the lease
// read and the builder's consume of artifacts-gone included. Read as text, as mesh-catalog's controllerdeps check reads
// its own: mesh-sdk names no module, so it holds no such check (issue 774, the operator 2026-11-03).
@@ -3657,3 +3841,7 @@ func (g *seatGate) MayAct(seats ...string) bool {
}
return true
}
func short(d string) string {
return strings.TrimPrefix(d, "sha256:")[:min(12, len(strings.TrimPrefix(d, "sha256:")))]
}
+4 -2
View File
@@ -26,7 +26,8 @@ const HeldModuleCondition = "mesh-grants.module-held"
// heldModuleUsers are, by user, why it is kept: the users of candidates (the ones the list in force and the bus carry) that
// belong to a module placed on a node whose manifest the indexer holds (in.Refused) and whose want names no build. A user of
// a module on a node is `<node>.<module>` or `<node>.<module>.<process>` (buscompose.Principal.Username). Pure.
// a module on a node is `<node>.<module>`, `<node>.<module>.<process>` or its ledger's `ledger.<node>.<module>`
// (buscompose.Principal.Username). Pure.
func heldModuleUsers(in Inputs, candidates []string) map[string]string {
held := map[string]string{}
for _, g := range in.Refused {
@@ -55,7 +56,8 @@ func heldModuleUsers(in Inputs, candidates []string) map[string]string {
state = "unreadable"
}
for _, user := range candidates {
if user == prefix || strings.HasPrefix(user, prefix+".") {
// And its ledger account, `ledger.<node>.<module>` (issue 1966: dropped from the list sealed 2026-11-07 23:09).
if user == prefix || strings.HasPrefix(user, prefix+".") || user == "ledger."+prefix {
out[user] = fmt.Sprintf("the manifest of %s is %s: %s", module, state, why)
}
}
@@ -167,6 +167,9 @@ type Inputs struct {
// Refused is every manifest the indexer holds that the part cannot read or mesh-sdk's reader refuses, assigned or
// not, each with why: its module counts as placed nowhere, so it is said loudly, never passed over (issue 1945).
Refused []Gap
// SeatListUnread says the seat list has not been read (the gate says so, seatlist.Reader.MayAct with no seat): only the
// bootstrapped seats compose, so the pass keeps what the bus loaded (keeploaded.go).
SeatListUnread bool
// ByHaves says why this pass placed the modules where the node-engines' haves say they run, the assignments view not
// answering (bootstrap only, issue 1966); empty when the assignments placed them. Such a pass retires nothing
// (ReadFailures) and seals no list dropping a user of the list in force (holdsBack).
@@ -321,6 +324,7 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
in := Inputs{Assigned: map[string][]string{}, Manifests: map[string]catalogue.Manifest{}, Digests: map[string]string{}, Fingerprints: map[string]string{},
NoFingerprint: map[string]string{}, Running: map[string]catalogue.Manifest{},
Values: map[string]any{}, Gaps: append([]Gap(nil), standingGaps...)}
in.SeatListUnread = gate != nil && !gate.MayAct()
nodes := map[string]bool{}
// **The seat's assignments 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
@@ -0,0 +1,173 @@
package grants
import (
"encoding/json"
"fmt"
"slices"
"sort"
)
// **A pass that cannot compose the whole mesh keeps what the bus loaded** (issue 1966, the registry delivery's sealed list of
// 2026-11-07 23:09). While the seat list is unread (only the bootstrapped seats compose, ADR 0636 §1) or a pass is placed by
// the node-engines' haves (Inputs.ByHaves), what the composer derives is a part of the mesh: that list sealed took the node
// seats from every tool runner, the vault's reads from the node-engines and the seat queues from the build modules. Such a
// pass composes each user of the loaded list and of the list in force as the union of what it composes and what those lists
// give it (publish, subscribe, the allowance of responses, and every deny any of them says), so it
// only adds, the indexer's serving of the seat that reads the list included; and the list is held where it would still take
// a permission from a loaded user (holdsBack, LoadedLosses).
// keepsLoaded says why this pass keeps what the bus loaded, or "" when it composes the whole mesh.
func (in Inputs) keepsLoaded() string {
switch {
case in.ByHaves != "":
return in.ByHaves
case in.SeatListUnread:
return "the seat list is unread, so only the bootstrapped seats compose"
}
return ""
}
// KeepLoaded is, by user, the union of each user of loaded and of inForce with what composed gives it, for every such user
// composed gives less than either list (or not at all). Pure.
func KeepLoaded(composed, loaded []User, inForce map[string]User) map[string]User {
byUser := map[string]User{}
for _, u := range composed {
byUser[u.User] = u
}
others := map[string][]User{}
for _, u := range loaded {
others[u.User] = append(others[u.User], u)
}
for name, u := range inForce {
others[name] = append(others[name], u)
}
out := map[string]User{}
for name, from := range others {
c, composedHere := byUser[name]
short := !composedHere
for _, o := range from {
if len(Losses(c, o)) > 0 {
short = true
}
}
if !short {
continue
}
// Every deny any list says is kept: a deny the composer drops while it composes part of the mesh would widen the
// user (the composer gives every composed user a deny, empty or not, so an intersection would drop the list in
// force's).
u := User{User: name, Publish: c.Publish, Subscribe: c.Subscribe, AllowResponses: c.AllowResponses, PublishDeny: c.PublishDeny}
for _, o := range from {
u.Publish = set(append(slices.Clone(u.Publish), o.Publish...))
u.Subscribe = set(append(slices.Clone(u.Subscribe), o.Subscribe...))
u.AllowResponses = u.AllowResponses || o.AllowResponses
if o.PublishDeny != nil || u.PublishDeny != nil {
u.PublishDeny = set(append(slices.Clone(u.PublishDeny), o.PublishDeny...))
}
}
if u.Publish == nil {
u.Publish = []string{}
}
if u.Subscribe == nil {
u.Subscribe = []string{}
}
out[name] = u
}
return out
}
// Losses are how now differs from had where had's word is lost: each publish and subscribe had gives that now does not,
// the allowance of responses, and, where had's denies are known, each deny now adds and each deny of had now drops (a deny
// dropped widens the user past what had held it to). Pure.
func Losses(now, had User) []string {
var out []string
for _, s := range minus(had.Publish, now.Publish) {
out = append(out, "publish "+s)
}
for _, s := range minus(had.Subscribe, now.Subscribe) {
out = append(out, "subscribe "+s)
}
if had.AllowResponses && !now.AllowResponses {
out = append(out, "responses")
}
if had.PublishDeny != nil {
for _, s := range minus(now.PublishDeny, had.PublishDeny) {
out = append(out, "deny "+s)
}
for _, s := range minus(had.PublishDeny, now.PublishDeny) {
out = append(out, "deny dropped "+s)
}
}
return out
}
// LoadedLosses names, sorted, each loaded user the list composed now (listed: its users; composed: their grants) would
// take a permission from or leave out, with how many it loses. Pure.
func LoadedLosses(listed []string, composed, loaded []User) []string {
byUser := map[string]User{}
for _, u := range composed {
byUser[u.User] = u
}
var out []string
for _, l := range loaded {
c, ok := byUser[l.User]
switch {
case !slices.Contains(listed, l.User) || !ok:
out = append(out, l.User+" (left out)")
default:
if n := len(Losses(c, l)); n > 0 {
out = append(out, fmt.Sprintf("%s (%d permissions)", l.User, n))
}
}
}
sort.Strings(out)
return slices.Compact(out)
}
// MembershipNarrows names what the membership last published (was) gives that the one composed now does not, field by field
// (`<field> <value>`), the private network, the contributions received and the bindings aside, which the pass carries over
// or the bus module writes; none when now covers was. Both are memberships as JSON. Pure.
func MembershipNarrows(was, now []byte) []string {
var a, b map[string]any
if json.Unmarshal(was, &a) != nil || json.Unmarshal(now, &b) != nil {
return nil
}
var out []string
for _, field := range sortedKeys(a) {
switch field {
case "mesh", "receives", "bindings":
continue
}
out = append(out, uncovered(field, a[field], b[field])...)
}
return out
}
// uncovered names each part of a that b does not hold: a key of an object by key, an element of a list by its value.
func uncovered(path string, a, b any) []string {
switch av := a.(type) {
case map[string]any:
bv, _ := b.(map[string]any)
var out []string
for _, k := range sortedKeys(av) {
out = append(out, uncovered(path+"."+k, av[k], bv[k])...)
}
return out
case []any:
bv, _ := b.([]any)
var out []string
for _, e := range av {
ej, _ := json.Marshal(e)
if !slices.ContainsFunc(bv, func(x any) bool { xj, _ := json.Marshal(x); return string(xj) == string(ej) }) {
out = append(out, path+" "+string(ej))
}
}
return out
}
aj, _ := json.Marshal(a)
bj, _ := json.Marshal(b)
if string(aj) != string(bj) {
return []string{path + " " + string(aj)}
}
return nil
}
+38 -3
View File
@@ -8,6 +8,7 @@ import (
"encoding/json"
"fmt"
"git.novox.be/novox/mesh-contracts/go/seats/meshbroker"
"slices"
"sort"
"strings"
"sync"
@@ -105,7 +106,10 @@ type Result struct {
Memberships int `json:"memberships"`
MembershipsWritten int `json:"memberships_written"`
Published []string `json:"published,omitempty"`
NotPublished []string `json:"not_published,omitempty"`
// KeptLoaded are the users a pass composing part of the mesh gave the union of what it composes and what the loaded list
// and the list in force give them (keeploaded.go, issue 1966), read with grants_user_list's last pass.
KeptLoaded []string `json:"kept_loaded,omitempty"`
NotPublished []string `json:"not_published,omitempty"`
// Retired is, by `<node>.<module>`, the seat verbs its last published membership served that mesh-sdk's seat
// catalogue no longer has (Membership.Retired): the pass keeps it, since the state memberships holds the bare
// membership alone (issue 774).
@@ -331,7 +335,27 @@ func (p *Part) Compose() Result {
}
p.sayPrincipalsRefused(refused, kept)
p.sayHeldModules(heldUsers, kept)
users = WithHashes(withKept(users, kept), in.Hashes)
users = withKept(users, kept)
// A pass that composes part of the mesh keeps what the bus loaded and the list in force give each user (keeploaded.go).
if why := in.keepsLoaded(); why != "" {
if union := KeepLoaded(users, loaded, inForce); len(union) > 0 {
merged := make(map[string]string, len(allRefused)+len(union))
keptAll := make(map[string]User, len(kept)+len(union))
for u, w := range allRefused {
merged[u] = w
}
for u, k := range kept {
keptAll[u] = k
}
for u, k := range union {
merged[u], keptAll[u] = "keeps what the loaded list gives it: "+why, k
}
users = withKept(slices.DeleteFunc(users, func(u User) bool { _, ok := union[u.User]; return ok }), union)
allRefused, kept = merged, keptAll
res.KeptLoaded = sortedKeys(union)
}
}
users = WithHashes(users, in.Hashes)
st.Digest, st.Users = Digest(users), len(users)
st.Compare = Compare(users, loaded, Placed(in)).Excusing(ExcusedBy(MultiHeld(listRecs)))
if loadErr != nil {
@@ -381,7 +405,7 @@ func (p *Part) Compose() Result {
bus = names(loaded)
lost = holdings.LastHolders(holdings.ByUsers(loaded), holdings.ByUsers(users))
}
p.writeSealed(&res, list, n, leftOut, dropped, in, bus, lost, users)
p.writeSealed(&res, list, n, leftOut, dropped, in, bus, lost, users, loaded)
}
ms := Memberships(recs)
res.Memberships = len(ms)
@@ -410,6 +434,17 @@ func (p *Part) Compose() Result {
continue
}
m, now := publish(was, found, ms[key], holdings, seated)
// A pass that composes part of the mesh never narrows a published membership: it keeps the one published
// (keeploaded.go, issue 1966), so a tool runner or a seat holder goes on serving what it serves.
if why := in.keepsLoaded(); now && found && why != "" {
if body, err := json.Marshal(m.Membership); err == nil {
if bare, err := membership.BareGrantsMembership(was); err == nil {
if narrower := MembershipNarrows(bare, body); len(narrower) > 0 {
m.Published, now = "kept as published: "+why+", and this pass composes it without "+strings.Join(narrower, ", "), false
}
}
}
}
// Published is written to the state, where every runtime reads it (process.FollowMembership); only one
// published, so a membership held back never reaches a reader.
if now {
+12 -9
View File
@@ -83,7 +83,7 @@ func (p *Part) Sealed() (grantsstate.GrantsUserList, uint64, bool) {
// writeSealed puts the list sealed to the bus node and writes the key, each only when what it says changed. It says in
// res what it wrote, or why it wrote nothing.
func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut, dropped []string, in Inputs, bus []string, lost []string, composed []User) {
func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut, dropped []string, in Inputs, bus []string, lost []string, composed, loaded []User) {
cur, _, had := p.Sealed()
next := cur
// The bus node follows this list alone (ADR 0571 clause 8): always since the controller ended by its unassignment
@@ -104,10 +104,10 @@ func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut, dropped
why = "no node runs the bus's module, so the list is sealed to nobody"
case key == "":
why = "the bus node's have of the bus module names no key yet (its node-engine says it from this release)"
case cur.Digest != digest && p.holdsBack(in, leftOut, dropped, bus, lost, listUsers(list), digest) != "":
case cur.Digest != digest && p.holdsBack(in, leftOut, dropped, bus, lost, listUsers(list), composed, loaded, digest) != "":
// The list in force is never replaced by one composed past a failed read, or one that drops a user of it for want
// of a hash: that user's module would be locked out of the bus (review of the switch).
why = p.holdsBack(in, leftOut, dropped, bus, lost, listUsers(list), digest)
why = p.holdsBack(in, leftOut, dropped, bus, lost, listUsers(list), composed, loaded, digest)
held = why
p.keepHashHold(res, digest)
case cur.Digest != digest || cur.SealedTo != grantsstate.SealingKeyID(key) || cur.Node != in.BusNode:
@@ -299,10 +299,10 @@ func listUsers(list []byte) []string {
// it kept of it; where it kept none (a list written before this release), the users the bus loaded (bus, nil when
// they could not be read). Empty when it may.
//
// In a pass placed by the node-engines' haves (in.ByHaves, issue 1966) the list is held, too, while it would leave out any user
// of the list in force or of the list the bus loaded (listed are the users of the list composed now): a bootstrap placement
// adds logins and never takes one away.
func (p *Part) holdsBack(in Inputs, leftOut, dropped, bus, lost, listed []string, digest string) string {
// In a pass that composes part of the mesh (placed by the node-engines' haves, or the seat list unread; keeploaded.go, issue
// 1966) the list is held, too, while it would leave out any user of the list in force or take any permission from a user of
// the list the bus loaded (listed are the users of the list composed now, composed their grants): such a pass only adds.
func (p *Part) holdsBack(in Inputs, leftOut, dropped, bus, lost, listed []string, composed, loaded []User, digest string) string {
p.hashHold = nil
if f := in.ReadFailed(); len(f) > 0 {
return "held: the list in force is kept while a read of this pass failed (" + strings.Join(f, "; ") + ")"
@@ -324,7 +324,10 @@ func (p *Part) holdsBack(in Inputs, leftOut, dropped, bus, lost, listed []string
default:
had = bus
}
if in.ByHaves != "" {
if why := in.keepsLoaded(); why != "" {
if losses := LoadedLosses(listed, composed, loaded); len(losses) > 0 {
return "held: " + why + ", and the list composed so would take from loaded users: " + strings.Join(losses, ", ")
}
var gone []string
for _, u := range append(slices.Clone(had), bus...) {
if !slices.Contains(listed, u) && !slices.Contains(gone, u) {
@@ -333,7 +336,7 @@ func (p *Part) holdsBack(in Inputs, leftOut, dropped, bus, lost, listed []string
}
if len(gone) > 0 {
slices.Sort(gone)
return "held: " + in.ByHaves + ", and the list composed so would drop " + strings.Join(gone, ", ") + " of the list in force"
return "held: " + why + ", and the list composed so would drop " + strings.Join(gone, ", ") + " of the list in force"
}
}
var unhashed []string