The part adopts meshcatalogclient's branch follower on a views.ProcessReader holding its held finding writer (so its view-waits is the reader's); the manifest of the build a want or a have names is the catalog graph's entry of that fingerprint, not a builder's build (the `builds` read is gone); until every first view of the catalog half is in, the pass keeps the list in force and only adds, says so in its log and in grants_first_views. Builds: design 47 G §684, §444, §614, §191, A-5, A-52 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
249 lines
7.8 KiB
Go
249 lines
7.8 KiB
Go
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.WantsUnread != "":
|
|
return in.WantsUnread
|
|
case in.CatalogUnread != "":
|
|
return in.CatalogUnread
|
|
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
|
|
}
|
|
|
|
// UnionMembership is the membership last published (was) grown by the one composed now: every list of both, deduplicated, was's
|
|
// elements first and then what now adds; an object by key; a single value as now says it. The private network, the contributions
|
|
// received and the bindings are now's, which carry them over or the bus module writes. A pass composing part of the mesh publishes
|
|
// it, so it only grows (issue 1966): a membership kept whole could never receive what the pass that ends the hold needs. It also
|
|
// names, as MembershipNarrows does, what was has that now lacks. Pure.
|
|
func UnionMembership(was, now []byte) ([]byte, []string, error) {
|
|
var a, b map[string]any
|
|
if err := json.Unmarshal(was, &a); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if err := json.Unmarshal(now, &b); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
out := map[string]any{}
|
|
for k, v := range a {
|
|
out[k] = v
|
|
}
|
|
for k, v := range b {
|
|
switch k {
|
|
case "mesh", "receives", "bindings":
|
|
out[k] = v
|
|
default:
|
|
out[k] = unionOf(a[k], v)
|
|
}
|
|
}
|
|
for _, k := range []string{"mesh", "receives", "bindings"} {
|
|
if _, ok := b[k]; !ok {
|
|
delete(out, k)
|
|
}
|
|
}
|
|
body, err := json.Marshal(out)
|
|
return body, MembershipNarrows(was, now), err
|
|
}
|
|
|
|
func unionOf(a, b any) any {
|
|
switch bv := b.(type) {
|
|
case map[string]any:
|
|
av, ok := a.(map[string]any)
|
|
if !ok {
|
|
return b
|
|
}
|
|
out := map[string]any{}
|
|
for k, v := range av {
|
|
out[k] = v
|
|
}
|
|
for k, v := range bv {
|
|
out[k] = unionOf(av[k], v)
|
|
}
|
|
return out
|
|
case []any:
|
|
av, ok := a.([]any)
|
|
if !ok {
|
|
return b
|
|
}
|
|
out := slices.Clone(av)
|
|
seen := map[string]bool{}
|
|
for _, e := range out {
|
|
j, _ := json.Marshal(e)
|
|
seen[string(j)] = true
|
|
}
|
|
for _, e := range bv {
|
|
if j, _ := json.Marshal(e); !seen[string(j)] {
|
|
seen[string(j)] = true
|
|
out = append(out, e)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
return b
|
|
}
|