mesh-grants: a partial pass keeps every deny and every published membership
mesh/delivery merged: source.main moved to novox/mesh-delivery@cd7f85c74a75

Review round 1 of issue 1966's keep-what-is-loaded fix.

- KeepLoaded intersected the denies, and the composer gives every composed user an empty
  deny, so a user in force with a wide publish and a deny was sealed with no deny. The
  union of every deny any list says is kept now, and Losses counts a deny dropped.
- A pass composing part of the mesh (the seat list unread, or placed by the haves) no
  longer narrows a published membership: one whose published value serves, hears or gives
  more than the pass composes is kept as published and said so (not_published), so a tool
  runner or a seat holder goes on serving what it serves while keeping its login.

Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5
This commit is contained in:
2026-11-07 23:33:07 +01:00
parent dead64ee6e
commit ab2bbe9ff6
3 changed files with 119 additions and 10 deletions
+47
View File
@@ -2448,6 +2448,31 @@ func TestGrants(t *testing.T) {
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)
@@ -2497,6 +2522,22 @@ func TestGrants(t *testing.T) {
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 {
@@ -2521,6 +2562,12 @@ func TestGrants(t *testing.T) {
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)
@@ -1,6 +1,7 @@
package grants
import (
"encoding/json"
"fmt"
"slices"
"sort"
@@ -11,7 +12,7 @@ import (
// 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
// 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).
@@ -52,18 +53,16 @@ func KeepLoaded(composed, loaded []User, inForce map[string]User) map[string]Use
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}
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 o.PublishDeny != nil || u.PublishDeny != nil {
u.PublishDeny = set(append(slices.Clone(u.PublishDeny), o.PublishDeny...))
}
}
if u.Publish == nil {
@@ -77,8 +76,9 @@ func KeepLoaded(composed, loaded []User, inForce map[string]User) map[string]Use
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.
// 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) {
@@ -94,6 +94,9 @@ func Losses(now, had User) []string {
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
}
@@ -120,3 +123,51 @@ func LoadedLosses(listed []string, composed, loaded []User) []string {
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
}
@@ -434,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 {