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:
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user