mesh-grants: a pass composing part of the mesh keeps what the bus loaded

Issue 1966: the list sealed 2026-11-07 23:09, composed while the seat list was unread
(only the bootstrapped seats compose), took the node seats from the tool runners, the
vault's reads from the node-engines and the seat queues from the build modules, and left
out ledger.novox.mesh-knowledge.

- While the seat list is unread (the gate's MayAct with no seat) or a pass is placed by
  the haves, each user of the loaded list and of the list in force is sealed with the
  union of what the pass composes and what those lists give it (publish, subscribe,
  responses; a deny only where every list saying one denies it), kept the way a refused
  principal is; the users so kept are the pass's kept_loaded.
- In such a pass a list that still takes a permission from, or leaves out, a loaded user
  is held (user-list-held), naming each user and how many permissions it loses.
- A held module keeps its ledger account, ledger.<node>.<module>, with its other users:
  heldModuleUsers matched <node>.<module>[.<process>] only, which dropped the ledger user.

Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5
This commit is contained in:
2026-11-07 23:28:17 +01:00
parent dcc48b82d0
commit dead64ee6e
6 changed files with 311 additions and 15 deletions
+142 -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,137 @@ 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.") }
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 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)
}
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 +3794,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,122 @@
package grants
import (
"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; a deny only where every list that says one denies it), 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
}
u := User{User: name, Publish: c.Publish, Subscribe: c.Subscribe, AllowResponses: c.AllowResponses, PublishDeny: c.PublishDeny}
denyKnown := composedHere && c.PublishDeny != nil
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
switch {
case o.PublishDeny == nil:
case !denyKnown:
u.PublishDeny, denyKnown = set(o.PublishDeny), true
default:
u.PublishDeny = slices.DeleteFunc(slices.Clone(u.PublishDeny), func(d string) bool { return !slices.Contains(o.PublishDeny, d) })
}
}
if u.Publish == nil {
u.Publish = []string{}
}
if u.Subscribe == nil {
u.Subscribe = []string{}
}
out[name] = u
}
return out
}
// Losses are the permissions had gives that now does not: each publish and subscribe, the allowance of responses, and
// each deny now adds where had's denies are known. 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)
}
}
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)
}
+27 -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)
+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