From 9edfcc40e9dec1c467762516898b3417b502f099 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 27 Oct 2026 22:28:22 +0100 Subject: [PATCH] mesh-grants: allow take-over when the lists differ only by added rights MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The operator's yes to take-over was refused twice (2026-10-26 13:48 and 2026-10-27 21:19 UTC) because 41 users differ, every difference a right the grants part's list adds. The controller's list is not changed (ADR 0547 §1), so ADR 0547 §8's equality could never hold again. ADR 0562 changes it: take-over is allowed when no user is added or dropped, no loaded user loses a publish, subscribe or responses right and every hash is held; the ask names the added rights by user and kind, and a yes after they changed is refused. Tests: rows from the captured live comparison of 21:19 (testdata, no hashes) in the compare table, and rows in the take-over table for a right removed, added rights changed since the ask, and only added rights recorded. issue 1945 --- modules/mesh-grants/grants_test.go | 166 ++- modules/mesh-grants/internal/grants/part.go | 2 +- .../mesh-grants/internal/grants/takeover.go | 65 +- .../mesh-grants/internal/grants/userlist.go | 212 +++ .../testdata/takeover-compare-2026-10-27.json | 1166 +++++++++++++++++ 5 files changed, 1571 insertions(+), 40 deletions(-) create mode 100644 modules/mesh-grants/testdata/takeover-compare-2026-10-27.json diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 3d44661..44a5a05 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -21,6 +21,7 @@ import ( "git.novox.be/novox/mesh-sdk/go/buscompose" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/outward" "git.novox.be/novox/mesh-sdk/go/seal" "mesh-grants/internal/grants" @@ -745,28 +746,108 @@ func TestGrants(t *testing.T) { changed := slices.Clone(shadow) x := slices.IndexFunc(changed, func(u grants.User) bool { return u.User == "a.mod-x" }) changed[x].Publish = append(slices.Clone(changed[x].Publish[1:]), "extra.subject") + // Only added rights (ADR 0562): the loaded list lacks a publish of a.mod-x the shadow grants. + fewer := slices.Clone(shadow) + fewer[x].Publish = slices.Clone(fewer[x].Publish[1:]) + // The live comparison the operator's yes was refused on, 2026-10-27 21:19 UTC (issue 1945): 41 users differ, every + // difference an added right. Read whole; a row's mutate makes one of the cases that must still refuse. + var live struct { + Compare grants.Comparison `json:"compare"` + Credentials grants.Credentials `json:"credentials"` + } + if raw, err := os.ReadFile(filepath.Join("testdata", "takeover-compare-2026-10-27.json")); err != nil || json.Unmarshal(raw, &live) != nil { + t.Fatalf("the captured comparison cannot be read: %v", err) + } + if len(live.Compare.Differ) != 41 || len(live.Credentials.LosesLogin)+len(live.Credentials.LeftOut) != 0 { + t.Fatalf("the captured comparison is not the one of 21:19: %d differ", len(live.Compare.Differ)) + } + captured := func(mutate func(*grants.Comparison)) *grants.Comparison { + raw, _ := json.Marshal(live.Compare) + var c grants.Comparison + _ = json.Unmarshal(raw, &c) + if mutate != nil { + mutate(&c) + } + return &c + } + nodeAce := func(c *grants.Comparison) *grants.UserDiff { + return &c.Differ[slices.IndexFunc(c.Differ, func(d grants.UserDiff) bool { return d.User == "node.ace" })] + } for _, row := range []struct { name string loaded []grants.User added, dropped, gone []string differ []string equal bool + // live is a captured comparison used in place of Compare; removing the users take-over is refused for + // (ADR 0562 §2), and adds the users whose added rights the ask names. + live *grants.Comparison + removing []string + adds int }{ - {"equal", shadow, nil, nil, nil, nil, true}, - {"one user the bus lacks", shadow[1:], []string{shadow[0].User}, nil, nil, nil, false}, - {"one loaded user still placed", append(slices.Clone(shadow), grants.User{User: "a.mod-x.worker"}), nil, []string{"a.mod-x.worker"}, nil, nil, false}, - {"one loaded user placement no longer names", append(slices.Clone(shadow), grants.User{User: "b.gone"}), nil, []string{"b.gone"}, []string{"b.gone"}, nil, true}, - {"grants differ", changed, nil, nil, nil, []string{"a.mod-x"}, false}, + {"equal", shadow, nil, nil, nil, nil, true, nil, nil, 0}, + {"one user the bus lacks", shadow[1:], []string{shadow[0].User}, nil, nil, nil, false, nil, []string{shadow[0].User}, 0}, + {"one loaded user still placed", append(slices.Clone(shadow), grants.User{User: "a.mod-x.worker"}), nil, []string{"a.mod-x.worker"}, nil, nil, false, nil, []string{"a.mod-x.worker"}, 0}, + {"one loaded user placement no longer names", append(slices.Clone(shadow), grants.User{User: "b.gone"}), nil, []string{"b.gone"}, []string{"b.gone"}, nil, true, nil, nil, 0}, + {"grants differ", changed, nil, nil, nil, []string{"a.mod-x"}, false, nil, []string{"a.mod-x"}, 1}, + {"only a right added", fewer, nil, nil, nil, []string{"a.mod-x"}, false, nil, nil, 1}, + {"captured 21:19: only added rights", nil, nil, nil, nil, nil, false, captured(nil), nil, 41}, + {"captured, a right removed", nil, nil, nil, nil, nil, false, captured(func(c *grants.Comparison) { + nodeAce(c).SubscribeDropped = []string{"mesh.mod.mesh-vault.event.secret-changed"} + }), []string{"node.ace"}, 41}, + {"captured, responses lost", nil, nil, nil, nil, nil, false, captured(func(c *grants.Comparison) { + nodeAce(c).Responses = "shadow false, loaded true" + }), []string{"node.ace"}, 41}, + {"captured, a user dropped", nil, nil, nil, nil, nil, false, captured(func(c *grants.Comparison) { + c.Dropped = []string{"novox.mesh-vault.worker"} + }), []string{"novox.mesh-vault.worker"}, 41}, + {"captured, a user added", nil, nil, nil, nil, nil, false, captured(func(c *grants.Comparison) { + c.Added = []string{"shanks.new-module"} + }), []string{"shanks.new-module"}, 41}, } { t.Run(row.name, func(t *testing.T) { - c := grants.Compare(shadow, row.loaded, grants.Placed(in)) - var differ []string - for _, d := range c.Differ { - differ = append(differ, d.User) + var c grants.Comparison + if row.live != nil { + c = *row.live + } else { + c = grants.Compare(shadow, row.loaded, grants.Placed(in)) + var differ []string + for _, d := range c.Differ { + differ = append(differ, d.User) + } + if !slices.Equal(c.Added, nonNil(row.added)) || !slices.Equal(c.Dropped, nonNil(row.dropped)) || + !slices.Equal(c.Gone, row.gone) || !slices.Equal(differ, row.differ) || c.Equal() != row.equal { + t.Errorf("compare = %+v", c) + } } - if !slices.Equal(c.Added, nonNil(row.added)) || !slices.Equal(c.Dropped, nonNil(row.dropped)) || - !slices.Equal(c.Gone, row.gone) || !slices.Equal(differ, row.differ) || c.Equal() != row.equal { - t.Errorf("compare = %+v", c) + var removing []string + for _, r := range c.Removing() { + removing = append(removing, strings.Fields(r)[0]) + } + if !slices.Equal(removing, row.removing) || len(c.AddedRights()) != row.adds { + t.Errorf("take-over refused for %v, want %v; added rights of %d users, want %d", c.Removing(), row.removing, len(c.AddedRights()), row.adds) + } + // The ask's words for the added rights pass the operator channel's content rule, read at a glance and fit one + // message, and name every user whose rights are added. + summary, points, list := c.AddedWords() + if row.adds > 0 { + expl := summary + "\n" + strings.Join(points, "\n") + for _, text := range []string{expl, list} { + if r, ok := outward.Check(text); !ok { + t.Errorf("the ask's words break the content rule: %s", r) + } + } + if err := asks.Glance(expl); err != nil || len([]rune(list)) > grants.AddedWordsBudget { + t.Errorf("the ask's words do not fit: %v, a list of %d characters", err, len([]rune(list))) + } + for _, d := range c.AddedRights() { + if !strings.Contains(list, d.User) && !strings.Contains(list, "more users") { + t.Errorf("the ask does not name %s", d.User) + } + } + } + if row.live != nil && row.removing == nil && (!strings.Contains(summary, "adds 391 rights to 41 users") || !strings.Contains(list, "node.ace, node.novox: 36 reads, 1 call, 1 event each")) { + t.Errorf("the captured ask says %q and lists %q", summary, list) } if row.name == "grants differ" && (len(c.Differ[0].PublishAdded) != 1 || c.Differ[0].PublishDropped[0] != "extra.subject") { t.Errorf("the grant difference is not named: %+v", c.Differ[0]) @@ -1059,6 +1140,23 @@ func TestGrants(t *testing.T) { return res.Asked } yes, no := asks.Option{ID: "yes"}, asks.Option{ID: "no"} + // settled is the placement of the lists agreeing, every hash held, and the loaded list as edit leaves it. + settled := func(edit func(modX *grants.User)) func() { + return func() { + b.states[grants.ReadAssignments] = nil + b.assign("a", "nats") + b.assign("a", "mod-x") + b.assign("b", "mod-y") + b.loaded = append(shadowOf(t, b), grants.User{User: "b.gone"}) + x := slices.IndexFunc(b.loaded, func(u grants.User) bool { return u.User == "a.mod-x" }) + b.loaded[x].Publish = slices.Clone(b.loaded[x].Publish) + edit(&b.loaded[x]) + for _, u := range shadowOf(t, b) { + b.put(grants.ReadAccounts, u.User, map[string]any{"user": u.User, "hash": "$2a$10$x", "generation": 1}) + } + } + } + lacks := func(modX *grants.User) { modX.Publish = modX.Publish[1:] } // the shadow adds a right (ADR 0562) for _, row := range []struct { name, act string option asks.Option @@ -1066,20 +1164,22 @@ func TestGrants(t *testing.T) { said, never []string // the refusal's words and the users it must not name state string outcome string + // between changes the lists after the ask and before the yes; asked are words the ask must carry. + between func() + asked []string }{ - {"a yes while a user differs is refused and said, naming it", "take-over", yes, func() {}, []string{"users differ", "b.mod-x"}, []string{"b.gone"}, grants.Shadow, "refused"}, - {"a no leaves the switch", "take-over", no, func() {}, nil, nil, grants.Shadow, "declined"}, - {"a yes with the lists agreeing and every hash held records the switch", "take-over", yes, func() { - b.states[grants.ReadAssignments] = nil - b.assign("a", "nats") - b.assign("a", "mod-x") - b.assign("b", "mod-y") - b.loaded = append(shadowOf(t, b), grants.User{User: "b.gone"}) - for _, u := range shadowOf(t, b) { - b.put(grants.ReadAccounts, u.User, map[string]any{"user": u.User, "hash": "$2a$10$x", "generation": 1}) - } - }, nil, nil, grants.TookOver, "recorded"}, - {"a yes to hand back records the switch back", "hand-back", yes, func() {}, nil, nil, grants.Shadow, "recorded"}, + {"a yes while a user differs is refused and said, naming it", "take-over", yes, func() {}, []string{"users differ", "b.mod-x"}, []string{"b.gone"}, grants.Shadow, "refused", nil, nil}, + {"a no leaves the switch", "take-over", no, func() {}, nil, nil, grants.Shadow, "declined", nil, nil}, + {"a yes while a loaded user would lose a right is refused, naming it", "take-over", yes, settled(func(modX *grants.User) { + modX.Publish = append(modX.Publish, "extra.subject") + }), []string{"other than by an added right", "a.mod-x (would lose 1 publish)"}, []string{"b.gone"}, grants.Shadow, "refused", nil, nil}, + {"a yes after the added rights changed since the ask is refused", "take-over", yes, settled(lacks), []string{"changed since you were asked"}, nil, grants.Shadow, "refused", + func() { + lacks(&b.loaded[slices.IndexFunc(b.loaded, func(u grants.User) bool { return u.User == "a.mod-x" })]) + }, []string{"a.mod-x: 1 read"}}, + {"a yes with only added rights and every hash held records the switch, the ask naming them", "take-over", yes, settled(lacks), nil, nil, grants.TookOver, "recorded", + nil, []string{"adds 1 right to 1 user and takes none away", "a.mod-x: 1 read"}}, + {"a yes to hand back records the switch back", "hand-back", yes, func() {}, nil, nil, grants.Shadow, "recorded", nil, nil}, } { t.Run(row.name, func(t *testing.T) { row.setup() @@ -1100,6 +1200,22 @@ func TestGrants(t *testing.T) { if yesOpt, ok := ask.Option("yes"); !ok || yesOpt.Level != asks.Approve || !strings.HasPrefix(yesOpt.Binds, "sha256:") { t.Errorf("the yes does not approve and bind its act: %+v", yesOpt) } + for _, text := range []string{ask.Explanation, ask.Details} { + if r, ok := outward.Check(text); !ok { + t.Errorf("the ask breaks the content rule: %s", r) + } + } + if err := asks.Glance(ask.Explanation); err != nil { + t.Error(err) + } + for _, w := range row.asked { + if !strings.Contains(ask.Explanation+"\n"+ask.Details, w) { + t.Errorf("the ask does not say %q: %s\n%s", w, ask.Explanation, ask.Details) + } + } + if row.between != nil { + row.between() + } said = nil if err := p.Warranted(row.option, warrant(id)); err != nil { t.Fatal(err) diff --git a/modules/mesh-grants/internal/grants/part.go b/modules/mesh-grants/internal/grants/part.go index 7756720..cef6d3b 100644 --- a/modules/mesh-grants/internal/grants/part.go +++ b/modules/mesh-grants/internal/grants/part.go @@ -299,7 +299,7 @@ func (p *Part) Compose() Result { } } if !p.deciding.Load() { - if err := p.RaiseRequested(); err != nil { + if err := p.RaiseRequested(res.UserList.Compare); err != nil { res.Errors = append(res.Errors, err.Error()) } if err := p.Reconcile(); err != nil { diff --git a/modules/mesh-grants/internal/grants/takeover.go b/modules/mesh-grants/internal/grants/takeover.go index 6ee843d..eeaa087 100644 --- a/modules/mesh-grants/internal/grants/takeover.go +++ b/modules/mesh-grants/internal/grants/takeover.go @@ -67,6 +67,9 @@ type Request struct { // RaisedAt is when it was asked, and Failed why asking it failed (the ask is then closed as refused). RaisedAt time.Time `json:"raised_at,omitempty"` Failed string `json:"failed,omitempty"` + // Adds is the digest of the rights the grants part's list added when the take-over was asked (Comparison.AddedDigest): + // the rights the ask named, which its yes answers (ADR 0562 §3). + Adds string `json:"adds,omitempty"` } // Asker is the client of the router the part asks through (mesh-sdk asker.Client). @@ -174,9 +177,9 @@ func (p *Part) request(act, caller, why string) (any, error) { return map[string]any{"asked": a.Ask, "says": "the part's process asks the operator on their channel; only the operator's yes records the switch, and it takes effect when the bus node loads the grants part's list. grants_take_over_state shows where it stands"}, nil } -// RaiseRequested is the process's: it asks the operator the request a tool left, once. Where the part has no asker (the -// tools) it does nothing. -func (p *Part) RaiseRequested() error { +// RaiseRequested is the process's: it asks the operator the request a tool left, once, naming the rights the pass's +// comparison c adds. Where the part has no asker (the tools) it does nothing. +func (p *Part) RaiseRequested(c Comparison) error { if p.Asker == nil { return nil } @@ -194,10 +197,13 @@ func (p *Part) RaiseRequested() error { if s, err := p.readSwitch(); err != nil || s.Open != r.ID { return nil // an ask no longer open is not asked } - ask, err := p.AskOf(r) + ask, err := p.AskOf(r, c) if err != nil { return err } + if r.Act == ActTakeOver { + r.Adds = c.AddedDigest() + } if err := p.Asker.Ask(ask); err != nil { // Closed as refused, and the half-made record the asker kept before publishing is taken away, so nothing stays open. r.Failed = err.Error() @@ -295,16 +301,24 @@ func (p *Part) Following() string { return "not yet: the bus node loaded another list" } -// AskOf is the ask of a request, in the words the operator reads: what changes, why, what it risks. Its yes binds the act. -func (p *Part) AskOf(r Request) (asks.Ask, error) { +// AskOf is the ask of a request, in the words the operator reads: what changes, why, what it risks; for a take-over every +// right the grants part's list adds by the comparison c, by user (ADR 0562 §3). Its yes binds the act. +func (p *Part) AskOf(r Request, c Comparison) (asks.Ask, error) { verb, headline, does, stays, expl := "grants_take_over", "Switch the bus's user list to the grants part?", "The switch is recorded; it takes effect when the bus node loads the grants part's list.", "The controller's list stays in force.", "The grants part has composed the user list the bus holds today. Your yes records the switch; it takes effect when the bus node loads the part's list.\n"+ - "If the lists differ in any user, or a user has no hash in the vault, the switch is refused and you are told which." + "If the lists differ other than by an added right, or a user has no hash in the vault, the switch is refused and you are told which." if r.Act == ActHandBack { verb, headline, does, stays, expl = "grants_hand_back", "Give the bus's user list back to the controller?", "The switch back is recorded; it takes effect when the bus node loads the controller's list.", "The grants part's list stays in force.", "The bus follows the grants part's list now.\nThe controller still writes its list until it loses its lease, so the bus can follow it again." } - bind, err := asks.ActDigest(asks.Act{"verb": verb, "module": "mesh-grants", "ask": r.ID}) + details := "Asked by " + r.By + ": " + r.Why + act := asks.Act{"verb": verb, "module": "mesh-grants", "ask": r.ID} + if summary, points, list := c.AddedWords(); r.Act == ActTakeOver && summary != "" { + expl = summary + " Your yes records the switch; it takes effect when the bus node loads the part's list.\n" + strings.Join(points, "\n") + details = "Added, by user:\n" + list + "\nA user added or dropped, a right taken away or a missing hash refuses the switch, naming them.\n" + details + act["adds"] = c.AddedDigest() + } + bind, err := asks.ActDigest(act) if err != nil { return asks.Ask{}, err } @@ -312,7 +326,7 @@ func (p *Part) AskOf(r Request) (asks.Ask, error) { Options: []asks.Option{{ID: "yes", Label: "Yes, switch", Does: does, Level: asks.Approve, Binds: bind}, {ID: "no", Label: "No, leave it", Does: stays, Level: asks.Acknowledge}}, Expires: p.now().Add(asks.ApproveLasts - time.Minute), OnExpiry: stays + " Nothing changes.", About: "mesh-grants-" + r.Act, - Details: "Asked by " + r.By + ": " + r.Why}, nil + Details: details}, nil } // Warranted is the process's act on the operator's answer (asker.Client.Listen hands it once, after checking the ask, the @@ -336,7 +350,7 @@ func (p *Part) Warranted(o asks.Option, w asks.Warrant) error { } var reasons []string if act == ActTakeOver { - reasons = p.Decide() + reasons = p.decide(w.Ask) if s, err = p.readSwitch(); err != nil { // Decide clears a refusal and writes the switch: read it again return err } @@ -390,10 +404,17 @@ func (p *Part) finish(s Switch, a Act) error { } // Decide is what stands between a take-over and the grants part's list, read from a composition made now: the loaded list -// not read, any user the two lists keep apart (one the loaded list has that the shadow cannot compose among them, such as -// an enrolment user or a person whose records are in the controller's store alone), and any user with no hash in the vault. +// not read, any user the two lists keep apart other than by a right the shadow list adds (a user added, a loaded user +// dropped that placement still names, a right a loaded user would lose: ADR 0562, which changes ADR 0547 clause 8's +// equality), and any user with no hash in the vault. // Nothing here but what is true now: a take-over is refused for these and for no other reason. Empty means none. func (p *Part) Decide() []string { + return p.decide("") +} + +// decide is Decide for the yes to the ask (none: empty). A right the shadow list adds is no reason (ADR 0562 §1): the ask +// names it, and the yes covers the rights the ask named and no other (ADR 0562 §3). +func (p *Part) decide(ask string) []string { p.deciding.Store(true) res := p.Compose() p.deciding.Store(false) @@ -402,10 +423,13 @@ func (p *Part) Decide() []string { if c.Unread != "" { reasons = append(reasons, "the loaded list could not be read: "+c.Unread) } - if d := c.Differing(); len(d) > 0 { - reasons = append(reasons, fmt.Sprintf("%d users differ between the shadow list and the loaded list: %s", len(d), strings.Join(d, ", "))) + if d := c.Removing(); len(d) > 0 { + reasons = append(reasons, fmt.Sprintf("%d users differ between the shadow list and the loaded list other than by an added right: %s", len(d), strings.Join(d, ", "))) } } + if ask != "" && p.askedAdds(ask) != res.UserList.Compare.AddedDigest() { + reasons = append(reasons, "the rights the grants part's list adds changed since you were asked, so the yes does not cover them: ask the take-over again") + } reasons = append(reasons, p.followReasons(res, ReadInputs(p.Read, &p.cache))...) cr, gone := res.UserList.Credentials, res.UserList.Compare.Gone var unhashed []string @@ -428,6 +452,19 @@ func (p *Part) Decide() []string { return reasons } +// askedAdds is the digest of the added rights the ask named: empty when its request is not the one kept. +func (p *Part) askedAdds(ask string) string { + raw, _, found, err := p.Store.Get(StateTakeover, KeyRequest) + if err != nil || !found { + return "" + } + var r Request + if json.Unmarshal(raw, &r) != nil || r.ID != ask { + return "" + } + return r.Adds +} + func contains(s []string, v string) bool { for _, x := range s { if x == v { diff --git a/modules/mesh-grants/internal/grants/userlist.go b/modules/mesh-grants/internal/grants/userlist.go index bffade3..86eec88 100644 --- a/modules/mesh-grants/internal/grants/userlist.go +++ b/modules/mesh-grants/internal/grants/userlist.go @@ -12,6 +12,7 @@ import ( "git.novox.be/novox/mesh-sdk/go/buscompose" "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/outward" ) // User is one bus user and its grants, as the bus module's nats_users answers them: never a password hash. @@ -264,6 +265,217 @@ func (c Comparison) Differing() []string { return out } +// responsesLost says the shadow list takes away a user's allowance of responses the loaded list gives. +func (d UserDiff) responsesLost() bool { return strings.HasPrefix(d.Responses, "shadow false") } + +// Removing names every user that keeps the lists apart other than by a right the shadow list adds, each with how: a user +// the shadow list adds, a loaded user it drops that placement still names, a publish or subscribe right a loaded user +// would lose, or an allowance of responses lost (ADR 0562 §2). Empty when the lists agree or differ only by added rights. +func (c Comparison) Removing() []string { + var out []string + for _, u := range c.Added { + out = append(out, u+" (a user the loaded list does not hold)") + } + for _, u := range c.Dropped { + if !slices.Contains(c.Gone, u) { + out = append(out, u+" (dropped, and placement still names it)") + } + } + for _, d := range c.Differ { + var lost []string + if n := len(d.PublishDropped); n > 0 { + lost = append(lost, fmt.Sprintf("%d publish", n)) + } + if n := len(d.SubscribeDropped); n > 0 { + lost = append(lost, fmt.Sprintf("%d subscribe", n)) + } + if d.responsesLost() { + lost = append(lost, "its allowance of responses") + } + if len(lost) > 0 { + out = append(out, d.User+" (would lose "+strings.Join(lost, " and ")+")") + } + } + sort.Strings(out) + return out +} + +// AddedRights are the users whose grants the shadow list widens and nothing else, with what it adds: the differences of a +// comparison whose Removing is empty (ADR 0562 §1). +func (c Comparison) AddedRights() []UserDiff { + var out []UserDiff + for _, d := range c.Differ { + a := UserDiff{User: d.User, PublishAdded: d.PublishAdded, SubscribeAdded: d.SubscribeAdded} + if !d.responsesLost() { + a.Responses = d.Responses + } + if len(a.PublishAdded)+len(a.SubscribeAdded) > 0 || a.Responses != "" { + out = append(out, a) + } + } + return out +} + +// AddedDigest is the digest of every right the shadow list adds: what an ask names and its yes answers (ADR 0562 §3). +func (c Comparison) AddedDigest() string { + var lines []string + for _, d := range c.AddedRights() { + for _, s := range d.PublishAdded { + lines = append(lines, d.User+"\tpublish\t"+s) + } + for _, s := range d.SubscribeAdded { + lines = append(lines, d.User+"\tsubscribe\t"+s) + } + if d.Responses != "" { + lines = append(lines, d.User+"\tresponses") + } + } + sort.Strings(lines) + sum := sha256.Sum256([]byte(strings.Join(lines, "\n"))) + return "sha256:" + hex.EncodeToString(sum[:]) +} + +// The kinds an added right is told by in the operator's ask. +const ( + kindAdvisory = "advisories" + kindRead = "reads" + kindCall = "calls" + kindEvent = "events" + kindOther = "other" +) + +var rightKinds = []string{kindRead, kindAdvisory, kindCall, kindEvent, kindOther} + +func kindOfRight(subject string) string { + switch { + case strings.HasPrefix(subject, "$JS.EVENT.ADVISORY."): + return kindAdvisory + case strings.HasPrefix(subject, "$JS.") || strings.HasPrefix(subject, "$KV.") || strings.HasPrefix(subject, "$O."): + return kindRead + case strings.HasPrefix(subject, "mesh.seat."): + return kindCall + case strings.HasPrefix(subject, "mesh.mod.") && strings.Contains(subject, ".event."): + return kindEvent + } + return kindOther +} + +// kindWords says what each kind is, in the operator's words. +var kindWords = map[string]string{kindRead: "reads of buckets and streams", kindAdvisory: "stream-created advisories", + kindCall: "seat calls", kindEvent: "events", kindOther: "other subjects"} + +// tally is one user's added rights counted by kind, as "36 reads, 1 call"; responses allowed are said too. +func tally(d UserDiff) string { + n := map[string]int{} + for _, s := range append(append([]string(nil), d.PublishAdded...), d.SubscribeAdded...) { + n[kindOfRight(s)]++ + } + var parts []string + for _, k := range rightKinds { + if n[k] > 0 { + word := k + if n[k] == 1 { + word = map[string]string{kindAdvisory: "advisory", kindRead: "read", kindCall: "call", kindEvent: "event", kindOther: "other"}[k] + } + parts = append(parts, fmt.Sprintf("%d %s", n[k], word)) + } + } + if d.Responses != "" { + parts = append(parts, "responses allowed") + } + return strings.Join(parts, ", ") +} + +// AddedWordsBudget is how many characters the ask's list of added rights may take, so the ask fits one message on a +// channel such as Telegram (4096 characters) with its headline, explanation and options. +const AddedWordsBudget = 2200 + +// AddedWords are an ask's words for the rights the shadow list adds (ADR 0562 §3): a summary, a point per kind with its +// total, and the list by user, users with the same count grouped on one line; within the content rule (outward.Check, +// a name that would not pass written with spaces) and AddedWordsBudget, past which the last lines are counted, not named. +func (c Comparison) AddedWords() (summary string, points []string, list string) { + adds := c.AddedRights() + if len(adds) == 0 { + return "", nil, "" + } + total, byKind, usersOf := 0, map[string]int{}, map[string]int{} + group := map[string][]string{} + for _, d := range adds { + seen := map[string]bool{} + for _, s := range append(append([]string(nil), d.PublishAdded...), d.SubscribeAdded...) { + k := kindOfRight(s) + total++ + byKind[k]++ + if !seen[k] { + seen[k] = true + usersOf[k]++ + } + } + t := tally(d) + group[t] = append(group[t], plainName(d.User)) + } + summary = fmt.Sprintf("The grants part's list adds %s to %s and takes none away; no user, hash or login changes.", count(total, "right"), count(len(adds), "user")) + var kinds []string + for _, k := range rightKinds { + if byKind[k] > 0 { + kinds = append(kinds, fmt.Sprintf("%d %s (%s)", byKind[k], kindWords[k], count(usersOf[k], "user"))) + } + } + // One point of the kinds, wrapped at the explanation's point length. + line := "Added: " + for i, k := range kinds { + if i > 0 && len([]rune(line))+len([]rune(k))+2 > 160 { + points = append(points, strings.TrimSuffix(line, ", ")) + line = "Added: " + } + line += k + ", " + } + points = append(points, strings.TrimSuffix(line, ", ")+".") + if n := len(c.Excused); n > 0 { + points = append(points, fmt.Sprintf("Not counted: %d rights excused as before (a retired verb, a seat held on several machines).", n)) + } + tallies := sortedKeys(group) + sort.SliceStable(tallies, func(i, j int) bool { return len(group[tallies[i]]) > len(group[tallies[j]]) }) + var lines []string + used := 0 + for i, t := range tallies { + users := group[t] + sort.Strings(users) + l := strings.Join(users, ", ") + ": " + t + if len(users) > 1 { + l += " each" + } + if used+len([]rune(l))+1 > AddedWordsBudget-80 { + rest := 0 + for _, more := range tallies[i:] { + rest += len(group[more]) + } + lines = append(lines, fmt.Sprintf("and %d more users: grants_user_list names every right added.", rest)) + break + } + lines = append(lines, l) + used += len([]rune(l)) + 1 + } + return summary, points, strings.Join(lines, "\n") +} + +// count is n things, with the plural where n is not one. +func count(n int, thing string) string { + if n == 1 { + return "1 " + thing + } + return fmt.Sprintf("%d %ss", n, thing) +} + +// plainName is a user's name as the operator channel's content rule lets it pass: as it is, or with spaces for its dots +// where it reads as a host name. +func plainName(user string) string { + if _, ok := outward.Check(user); ok { + return user + } + return strings.ReplaceAll(user, ".", " ") +} + // Compare compares the shadow list with the loaded one. placed says whether placement still names a loaded user's // machine and module; nil names every one. Pure. func Compare(shadow, loaded []User, placed func(user string) bool) Comparison { diff --git a/modules/mesh-grants/testdata/takeover-compare-2026-10-27.json b/modules/mesh-grants/testdata/takeover-compare-2026-10-27.json new file mode 100644 index 0000000..7bb7387 --- /dev/null +++ b/modules/mesh-grants/testdata/takeover-compare-2026-10-27.json @@ -0,0 +1,1166 @@ +{ + "captured": "novox/mesh-grants.grants_user_list at 2026-10-27T21:19:53Z, revision 94593 (issue 1945): its compare and credentials, no hash", + "compare": { + "added": [], + "differ": [ + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-go_keys", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-go_pauses", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-go_steps", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-go_toolchains", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-checker_kept", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_manifests" + ], + "user": "ace.mesh-build-go" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-image_keys", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-image_pauses", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-image_steps" + ], + "user": "ace.mesh-build-image" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-node_keys", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-node_pauses", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-node_steps", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-node_toolchains" + ], + "user": "ace.mesh-build-node" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-go_steps", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-controller_assignments", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-controller_builds", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-controller_composed", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-controller_grants", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_asks", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_compositions", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_differences", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_puts", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_wants", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_builds", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_candidates", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_manifests", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_modules", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_placement", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_placement-candidates", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-teller_answers", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-watchman_presence", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_node-engine_have" + ], + "user": "ace.mesh-delivery-assigner" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_pacman_asks", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_pacman_installed", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_pacman_records", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_pacman_rounds", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_pacman_runs", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_pacman_updates" + ], + "user": "ace.pacman.keeper" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-controller_assignments", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_route-proxy_bound" + ], + "user": "ace.route-proxy" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-go_keys", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-go_pauses", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-go_steps", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-go_toolchains", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-checker_kept", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_manifests" + ], + "user": "g14.mesh-build-go" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-node_keys", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-node_pauses", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-node_steps", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-node_toolchains" + ], + "user": "g14.mesh-build-node" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-mail-notifier_shown" + ], + "user": "g14.mesh-mail-notifier" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_pacman_asks", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_pacman_installed", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_pacman_records", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_pacman_rounds", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_pacman_runs", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_pacman_updates" + ], + "user": "g14.pacman.keeper" + }, + { + "publish_added": [ + "$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.ace.>", + "$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.novox.keycloak.issuer", + "$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.novox.mailu.domain", + "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.>", + "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.>", + "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.>", + "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_placement-candidates.>", + "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_placement.>", + "$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node", + "$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.>", + "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.>", + "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.>", + "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.>", + "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_placement-candidates.>", + "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_placement.>", + "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.ace.>", + "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.novox.keycloak.issuer", + "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.novox.mailu.domain", + "$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.>", + "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.>", + "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.>", + "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_placement-candidates.>", + "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_placement.>", + "$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node", + "$JS.API.STREAM.INFO.KV_mesh-config_resolved", + "$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds", + "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_manifests", + "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules", + "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_placement", + "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_placement-candidates", + "$JS.FC.KV_mesh-config_resolved.>", + "$JS.FC.KV_mesh-delivery-builder_builds.>", + "$JS.FC.KV_mesh-delivery-indexer_manifests.>", + "$JS.FC.KV_mesh-delivery-indexer_modules.>", + "$JS.FC.KV_mesh-delivery-indexer_placement-candidates.>", + "$JS.FC.KV_mesh-delivery-indexer_placement.>", + "mesh.seat.mesh-vault.call.own.node~ace" + ], + "subscribe_added": [ + "mesh.mod.mesh-vault.event.secret-changed" + ], + "user": "node.ace" + }, + { + "publish_added": [ + "$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.ace.influxdb.bucket", + "$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.ace.influxdb.org", + "$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.g14.>", + "$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.novox.keycloak.issuer", + "$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.novox.mailu.domain", + "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.>", + "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.>", + "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.>", + "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_placement-candidates.>", + "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_placement.>", + "$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node", + "$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.>", + "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.>", + "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.>", + "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.>", + "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_placement-candidates.>", + "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_placement.>", + "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.ace.influxdb.bucket", + "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.ace.influxdb.org", + "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.g14.>", + "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.novox.keycloak.issuer", + "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.novox.mailu.domain", + "$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.>", + "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.>", + "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.>", + "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_placement-candidates.>", + "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_placement.>", + "$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node", + "$JS.API.STREAM.INFO.KV_mesh-config_resolved", + "$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds", + "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_manifests", + "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules", + "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_placement", + "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_placement-candidates", + "$JS.FC.KV_mesh-config_resolved.>", + "$JS.FC.KV_mesh-delivery-builder_builds.>", + "$JS.FC.KV_mesh-delivery-indexer_manifests.>", + "$JS.FC.KV_mesh-delivery-indexer_modules.>", + "$JS.FC.KV_mesh-delivery-indexer_placement-candidates.>", + "$JS.FC.KV_mesh-delivery-indexer_placement.>", + "mesh.seat.mesh-vault.call.own.node~g14" + ], + "subscribe_added": [ + "mesh.mod.mesh-vault.event.secret-changed" + ], + "user": "node.g14" + }, + { + "publish_added": [ + "$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.ace.influxdb.bucket", + "$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.ace.influxdb.org", + "$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.novox.>", + "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.>", + "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.>", + "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.>", + "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_placement-candidates.>", + "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_placement.>", + "$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node", + "$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.>", + "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.>", + "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.>", + "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.>", + "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_placement-candidates.>", + "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_placement.>", + "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.ace.influxdb.bucket", + "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.ace.influxdb.org", + "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.novox.>", + "$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.>", + "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.>", + "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.>", + "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_placement-candidates.>", + "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_placement.>", + "$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node", + "$JS.API.STREAM.INFO.KV_mesh-config_resolved", + "$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds", + "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_manifests", + "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules", + "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_placement", + "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_placement-candidates", + "$JS.FC.KV_mesh-config_resolved.>", + "$JS.FC.KV_mesh-delivery-builder_builds.>", + "$JS.FC.KV_mesh-delivery-indexer_manifests.>", + "$JS.FC.KV_mesh-delivery-indexer_modules.>", + "$JS.FC.KV_mesh-delivery-indexer_placement-candidates.>", + "$JS.FC.KV_mesh-delivery-indexer_placement.>", + "mesh.seat.mesh-vault.call.own.node~novox" + ], + "subscribe_added": [ + "mesh.mod.mesh-vault.event.secret-changed" + ], + "user": "node.novox" + }, + { + "publish_added": [ + "$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.ace.influxdb.bucket", + "$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.ace.influxdb.org", + "$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.novox.keycloak.issuer", + "$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.novox.mailu.domain", + "$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.shanks.>", + "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.>", + "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.>", + "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.>", + "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_placement-candidates.>", + "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_placement.>", + "$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node", + "$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.>", + "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.>", + "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.>", + "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.>", + "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_placement-candidates.>", + "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_placement.>", + "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.ace.influxdb.bucket", + "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.ace.influxdb.org", + "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.novox.keycloak.issuer", + "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.novox.mailu.domain", + "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.shanks.>", + "$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.>", + "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.>", + "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.>", + "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_placement-candidates.>", + "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_placement.>", + "$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node", + "$JS.API.STREAM.INFO.KV_mesh-config_resolved", + "$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds", + "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_manifests", + "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules", + "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_placement", + "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_placement-candidates", + "$JS.FC.KV_mesh-config_resolved.>", + "$JS.FC.KV_mesh-delivery-builder_builds.>", + "$JS.FC.KV_mesh-delivery-indexer_manifests.>", + "$JS.FC.KV_mesh-delivery-indexer_modules.>", + "$JS.FC.KV_mesh-delivery-indexer_placement-candidates.>", + "$JS.FC.KV_mesh-delivery-indexer_placement.>", + "mesh.seat.mesh-vault.call.own.node~shanks" + ], + "subscribe_added": [ + "mesh.mod.mesh-vault.event.secret-changed" + ], + "user": "node.shanks" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_desk-channel_shown" + ], + "user": "novox.desk-channel" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-go_keys", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-go_pauses", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-go_steps", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-go_toolchains", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-checker_kept", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_manifests" + ], + "user": "novox.mesh-build-go" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-node_keys", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-node_pauses", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-node_steps", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-node_toolchains" + ], + "user": "novox.mesh-build-node" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-config_kept", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-config_predefined", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-config_proposals", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-config_pulls", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-config_resolved", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-config_values", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-controller_assignments", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-controller_declared-config", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-controller_predefined", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_placement" + ], + "user": "novox.mesh-config" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-go_steps", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-go_toolchains", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-node_steps", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-node_toolchains", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-controller_assignments", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_compositions", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_builds", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-checker_hearing", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-checker_kept", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-checker_owed", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-checker_previews", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-checker_tries", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-checker_verdicts", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-gate_source", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-gate_turns", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_candidates", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_manifests", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_modules", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-watchman_presence", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_node-engine_have" + ], + "user": "novox.mesh-delivery-checker" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-forge-bridge_heads", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-forge-bridge_performed", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-gate_merges", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-gate_owed" + ], + "user": "novox.mesh-delivery-forge-bridge" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-go_toolchains", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-node_toolchains", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-controller_assignments", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-controller_builds", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-checker_previews", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-gate_source", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_candidates", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_manifests", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_modules", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_placement", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_placement-candidates", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_takeins", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_taken" + ], + "user": "novox.mesh-delivery-indexer" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_asks", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-gate_asks", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-teller_answers", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-teller_asked", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-view_answers" + ], + "user": "novox.mesh-delivery-teller" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-go_steps", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-image_steps", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-node_steps", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_wants", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-watchman_presence", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-watchman_pulse", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-watchman_stuck", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_node-engine_have" + ], + "user": "novox.mesh-delivery-watchman" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-config_resolved", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-controller_assignments", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-controller_grants", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_wants", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_builds", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_manifests", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_modules", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_placement", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-grants_assertions", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-grants_memberships", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-grants_takeover", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-grants_user-list", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-vault_bus-accounts", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_node-engine_have" + ], + "user": "novox.mesh-grants" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-mail_accounts" + ], + "user": "novox.mesh-mail-web" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-mail_sends" + ], + "user": "novox.mesh-mail.send" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-provisioner_applied", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-provisioner_collect", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-provisioner_needs", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-provisioner_registry" + ], + "user": "novox.mesh-provisioner" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-controller_assignments", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-vault_bus-accounts", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_node-engine_have" + ], + "user": "novox.mesh-vault" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_messenger_asks", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_messenger_identities", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_messenger_open", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_messenger_sent", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_messenger_walks" + ], + "user": "novox.messenger" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_only-office_convert-answers" + ], + "user": "novox.only-office.converter" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_pacman_asks", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_pacman_installed", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_pacman_records", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_pacman_rounds", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_pacman_runs", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_pacman_updates" + ], + "user": "novox.pacman.keeper" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-controller_assignments", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_route-proxy_bound" + ], + "user": "novox.route-proxy" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_telegram_messages", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_telegram_offset" + ], + "user": "novox.telegram" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_brother_device-answers" + ], + "user": "shanks.brother.device" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-go_keys", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-go_pauses", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-go_steps", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-go_toolchains", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-checker_kept", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_manifests" + ], + "user": "shanks.mesh-build-go" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-image_keys", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-image_pauses", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-image_steps" + ], + "user": "shanks.mesh-build-image" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-node_keys", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-node_pauses", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-node_steps", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-node_toolchains" + ], + "user": "shanks.mesh-build-node" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-go_steps", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-go_toolchains", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-image_steps", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-node_steps", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-node_toolchains", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-controller_assignments", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_builds", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_hearing", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_instances", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_queues", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_reasks", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-gate_source", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_candidates", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_manifests", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_modules" + ], + "user": "shanks.mesh-delivery-builder" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-go_pauses", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-go_steps", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-image_pauses", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-image_steps", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-node_pauses", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-build-node_steps", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-config_resolved", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-controller_assignments", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-controller_builds", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-controller_composed", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_asks", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_differences", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_wants", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_builds", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-checker_tries", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-gate_asks", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-gate_source", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_candidates", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_modules", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_placement", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_placement-candidates", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-indexer_taken", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-teller_answers", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-view_answers", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-view_reach", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-watchman_presence", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-watchman_stuck", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_node-engine_have" + ], + "user": "shanks.mesh-delivery-view" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-mail-notifier_shown" + ], + "user": "shanks.mesh-mail-notifier" + }, + { + "subscribe_added": [ + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_pacman_asks", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_pacman_installed", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_pacman_records", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_pacman_rounds", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_pacman_runs", + "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_pacman_updates" + ], + "user": "shanks.pacman.keeper" + } + ], + "dropped": [], + "excused": [ + { + "subject": "$SRV.INFO.mesh-store", + "user": "ace.node-tools", + "why": "seat mesh-store is claimed on several machines: composed as held on each until holdings follow the catalogue" + }, + { + "subject": "$SRV.INFO.mesh-store.>", + "user": "ace.node-tools", + "why": "seat mesh-store is claimed on several machines: composed as held on each until holdings follow the catalogue" + }, + { + "subject": "$SRV.PING.mesh-store", + "user": "ace.node-tools", + "why": "seat mesh-store is claimed on several machines: composed as held on each until holdings follow the catalogue" + }, + { + "subject": "$SRV.PING.mesh-store.>", + "user": "ace.node-tools", + "why": "seat mesh-store is claimed on several machines: composed as held on each until holdings follow the catalogue" + }, + { + "subject": "$SRV.STATS.mesh-store", + "user": "ace.node-tools", + "why": "seat mesh-store is claimed on several machines: composed as held on each until holdings follow the catalogue" + }, + { + "subject": "$SRV.STATS.mesh-store.>", + "user": "ace.node-tools", + "why": "seat mesh-store is claimed on several machines: composed as held on each until holdings follow the catalogue" + }, + { + "subject": "mesh.seat.mesh-store.call.databases.*", + "user": "ace.node-tools", + "why": "seat mesh-store is claimed on several machines: composed as held on each until holdings follow the catalogue" + }, + { + "subject": "mesh.seat.mesh-store.call.query.*", + "user": "ace.node-tools", + "why": "seat mesh-store is claimed on several machines: composed as held on each until holdings follow the catalogue" + }, + { + "subject": "mesh.seat.mesh-store.tool.databases", + "user": "ace.node-tools", + "why": "seat mesh-store is claimed on several machines: composed as held on each until holdings follow the catalogue" + }, + { + "subject": "mesh.seat.mesh-store.tool.query", + "user": "ace.node-tools", + "why": "seat mesh-store is claimed on several machines: composed as held on each until holdings follow the catalogue" + }, + { + "subject": "mesh.seat.mesh-delivery.call.mark-ready.*", + "user": "novox.node-tools", + "why": "a verb the seat catalogue retired" + }, + { + "subject": "mesh.seat.mesh-delivery.tool.mark-ready", + "user": "novox.node-tools", + "why": "a verb the seat catalogue retired" + } + ], + "loaded": 60, + "same": 19, + "shadow": 60 + }, + "credentials": { + "adds": [], + "hashes_held": 60, + "left_out": [], + "loses_login": [], + "users": [ + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "ace.dnsmasq", + "would": "same" + }, + { + "generation": 6, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "ace.mesh-build-go", + "would": "same" + }, + { + "generation": 2, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "ace.mesh-build-image", + "would": "same" + }, + { + "generation": 4, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "ace.mesh-build-node", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "ace.mesh-delivery-assigner", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "ace.mesh-reads", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "ace.node-tools", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "ace.pacman.keeper", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "ace.route-proxy", + "would": "same" + }, + { + "adopted": true, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "controller", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "g14.mesh-build-go", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "g14.mesh-build-node", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "g14.mesh-mail-notifier", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "g14.mesh-reads", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "g14.node-tools", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "g14.pacman.keeper", + "would": "same" + }, + { + "adopted": true, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "node.ace", + "would": "same" + }, + { + "adopted": true, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "node.g14", + "would": "same" + }, + { + "adopted": true, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "node.novox", + "would": "same" + }, + { + "adopted": true, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "node.shanks", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.desk-channel", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.distribution", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.dnsmasq", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.mesh-build-go", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.mesh-build-node", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.mesh-config", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.mesh-controller", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.mesh-delivery-checker", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.mesh-delivery-forge-bridge", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.mesh-delivery-indexer", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.mesh-delivery-teller", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.mesh-delivery-watchman", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.mesh-grants", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.mesh-mail-reader", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.mesh-mail-web", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.mesh-mail.send", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.mesh-provisioner", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.mesh-reads", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.mesh-vault", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.messenger", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.nats", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.node-tools", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.only-office.converter", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.pacman.keeper", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.route-proxy", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "novox.telegram", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "provisioner.ace", + "would": "same" + }, + { + "generation": 3, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "provisioner.g14", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "provisioner.novox", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "provisioner.shanks", + "would": "same" + }, + { + "adopted": true, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "shanks.brother.device", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "shanks.mesh-build-go", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "shanks.mesh-build-image", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "shanks.mesh-build-node", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "shanks.mesh-delivery-builder", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "shanks.mesh-delivery-view", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "shanks.mesh-mail-notifier", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "shanks.mesh-reads", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "shanks.node-tools", + "would": "same" + }, + { + "generation": 1, + "hash_held": true, + "in_shadow": true, + "loaded": true, + "user": "shanks.pacman.keeper", + "would": "same" + } + ] + } +}