diff --git a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/consumes.go b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/consumes.go new file mode 100644 index 00000000..12cfabb3 --- /dev/null +++ b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/consumes.go @@ -0,0 +1,118 @@ +package main + +import ( + "context" + "encoding/json" + "strings" + "sync" + + "github.com/nats-io/nats.go/jetstream" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/membership" + "git.novox.be/novox/mesh-sdk/go/process" + + "mesh-delivery-watchman/internal/watch" +) + +// **What the watchman consumes** (its manifest's `consumes`; novox/hq ADR 0595 §4, design 25 § 12.2; DLV-W1): the indexer's +// manifest-accepted and the seat mesh-broker's created and refused, all on its durable consumer on EVENTS (the seat's +// events on the seat's subjects like every seat's: the lead's ruling, registry milestone 13:0x); beside it any module +// stream its membership names (Hears). Each consumer is the one the grants part makes; one +// not there yet is opened when the watchman's membership next changes, and one opened is never opened twice. An event is +// acknowledged once handed to the watchman, so one given while it was down is heard when it is back. + +// Consumed event names, as the consumes name them (the emitter is read from the subject's emitter token). +const ( + eventAccepted = "manifest-accepted" + eventCreated = "created" + eventRefused = "refused" +) + +// consumed hands one consumed event to the watchman, by its subject: manifest-accepted, and the seat mesh-broker's created +// and refused. Anything else is left. +func consumed(w *watch.Watchman, subject string, body []byte, logf func(string, ...any)) { + switch { + case strings.HasSuffix(subject, ".event."+eventAccepted) && strings.HasPrefix(subject, "mesh.mod."): + var a delivery.ManifestAccepted + if err := json.Unmarshal(body, &a); err != nil || a.Module == "" { + logf("a manifest-accepted that does not read is left: %v", err) + return + } + w.Accepted(a) + case brokerSubject(subject) && strings.HasSuffix(subject, ".event."+eventCreated): + if s, err := watch.ReadBrokerSaid(body); err != nil { + logf("%v; left", err) + } else { + w.BrokerCreated(s) + } + case brokerSubject(subject) && strings.HasSuffix(subject, ".event."+eventRefused): + if s, err := watch.ReadBrokerSaid(body); err != nil { + logf("%v; left", err) + } else { + w.BrokerRefused(s) + } + } +} + +// hearConsumed opens the watchman's durable consumers: on EVENTS now, and on each module stream its membership names as +// that membership arrives. A consumer the bus does not hold yet is said and tried again at the membership's next change. +func hearConsumed(ctx context.Context, js jetstream.JetStream, cred process.Account, w *watch.Watchman, logf func(string, ...any)) { + var mu sync.Mutex + opened := map[string]bool{} + open := func(stream, name string) { + mu.Lock() + defer mu.Unlock() + if opened[stream] { + return + } + cons, err := js.Consumer(ctx, stream, name) + if err != nil { + logf("the consumer %s on %s cannot be bound yet (%v): what it carries is heard once the grants part has made it", name, stream, err) + return + } + if _, err := process.HearEvents(cons, func(m jetstream.Msg) { + consumed(w, m.Subject(), m.Data(), logf) + _ = m.Ack() + }, logf); err != nil { + logf("the consumer %s on %s cannot be heard (%v)", name, stream, err) + return + } + opened[stream] = true + } + open("EVENTS", cred.EventConsumerName()) + kv, err := js.KeyValue(ctx, membership.GrantsBucket) + if err != nil { + logf("its own membership cannot be read (%v): the bus module's created and refused are not heard until the watchman starts again", err) + return + } + watcher, err := kv.Watch(ctx, membership.GrantsKey(cred.Node, cred.Module)) + if err != nil { + logf("its own membership cannot be watched (%v): the bus module's created and refused are not heard until the watchman starts again", err) + return + } + go func() { + defer watcher.Stop() + for e := range watcher.Updates() { + if e == nil || e.Operation() != jetstream.KeyValuePut { + continue + } + m, err := membership.DecodeGrantsMembership(e.Value()) + if err != nil { + logf("its own membership does not read (%v): left", err) + continue + } + open("EVENTS", cred.EventConsumerName()) + for _, h := range m.Hears { + open(h.Stream, h.Consumer) + } + } + }() +} + +// brokerSubject is a subject the bus module's created or refused arrives on: the seat mesh-broker's, where only its holder +// publishes. The bus module's own stream is its history for its tool, never heard here. +func brokerSubject(subject string) bool { + return strings.HasPrefix(subject, "mesh.seat."+catalogue.BrokerSeat+".event.") +} diff --git a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/integration_test.go b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/integration_test.go index 0077f109..6ea1b1fd 100644 --- a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/integration_test.go +++ b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/integration_test.go @@ -318,11 +318,14 @@ func TestWatchmanWhole(t *testing.T) { // 1135/AC8 !strings.Contains(err.Error(), "issue 1953") { t.Fatalf("a drop was not refused naming the hand-act log's home: %v", err) } + // Each on a bus of its own, as subtests of the one whole-module test (DLV-T1). + t.Run("an ask running before the start gets its pulse", anAskRunningBeforeTheStartGetsItsPulse) + t.Run("a want is mirrored as a pulse and the have naming it ends it", aWantIsMirroredAsAPulseAndTheHaveNamingItEndsIt) } -// TestAnAskRunningBeforeTheStartGetsItsPulse puts a running step in the build module's steps before the watchman starts, and +// anAskRunningBeforeTheStartGetsItsPulse, run by TestWatchmanWhole (the test budget: one whole-module test, DLV-T1), puts a running step in the build module's steps before the watchman starts, and // sees the pulse of its ask once it is up: what stood at the start is mirrored as what happens later is (issue 1909). -func TestAnAskRunningBeforeTheStartGetsItsPulse(t *testing.T) { // 1909 +func anAskRunningBeforeTheStartGetsItsPulse(t *testing.T) { // 1909 dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "w") if err != nil { t.Fatal(err) @@ -406,10 +409,10 @@ func TestAnAskRunningBeforeTheStartGetsItsPulse(t *testing.T) { // 1909 t.Fatalf("no pulse for the ask running before the start (%s)", pk) } -// TestAWantIsMirroredAsAPulseAndTheHaveNamingItEndsIt (1906/D6, ADR 0539 §19): started whole, the watchman mirrors a want of +// aWantIsMirroredAsAPulseAndTheHaveNamingItEndsIt (1906/D6, ADR 0539 §19), run by TestWatchmanWhole: started whole, the watchman mirrors a want of // the assigner as a pulse owed by the node's agent, one that stood before its start as well, and the node-engine's have that // names the want's fingerprint deletes the pulse and writes no stuck key. -func TestAWantIsMirroredAsAPulseAndTheHaveNamingItEndsIt(t *testing.T) { +func aWantIsMirroredAsAPulseAndTheHaveNamingItEndsIt(t *testing.T) { dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "w") if err != nil { t.Fatal(err) diff --git a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go index 0e183670..dd88ae0d 100644 --- a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go +++ b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go @@ -418,6 +418,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro if err := hear(conn, w, dead, logger.Printf); err != nil { return err } + hearConsumed(ctx, js, cred, w, logger.Printf) errs := make(chan error, 16) if healthWatch != nil { diff --git a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main_test.go b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main_test.go index a5804e47..f4c9e742 100644 --- a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main_test.go +++ b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main_test.go @@ -7,9 +7,12 @@ import ( "io" "strings" "testing" + "time" "git.novox.be/novox/mesh-sdk/go/process" + "mesh-delivery-watchman/internal/watch" + "github.com/nats-io/nats.go/jetstream" ) @@ -45,6 +48,29 @@ func TestRunNeedsItsCredential(t *testing.T) { // 1135/AC6 } } }) + t.Run("DLV-W1: the consumed events reach the watchman by their subjects", func(t *testing.T) { + out := &findingsOut{findings: map[string]bool{}} + w := watch.New(out, time.Now) + say := func(string, ...any) {} + // The bus module's own stream is its history, never heard here: only the seat's subject (ruling 13:0x). + consumed(w, "mesh.stream.nats.event.refused", []byte(`{"module":"m","local":"l","why":"no room"}`), say) + if len(out.findings) != 0 { + t.Fatalf("a refused on the bus module's own stream reached %v", out.findings) + } + consumed(w, "mesh.seat.mesh-broker.event.refused", []byte(`{"module":"m","local":"l","why":"no room"}`), say) + if !out.findings[watch.FindingKey(watch.ScopeModule, "m", watch.KindBusStateRefused+"-l")] { + t.Fatalf("refused reached no finding: %v", out.findings) + } + consumed(w, "mesh.mod.mesh-delivery-indexer.event.manifest-accepted", []byte(`{"module":"m","digest":"d","bus-state":[]}`), say) + if len(out.findings) != 0 { + t.Fatalf("an accepted manifest no longer asking for l left %v", out.findings) + } + consumed(w, "mesh.seat.mesh-broker.event.refused", []byte(`{"module":"m","local":"l"}`), say) + consumed(w, "mesh.seat.mesh-broker.event.created", []byte(`{"module":"m","local-name":"l"}`), say) + if len(out.findings) != 0 { + t.Fatalf("created left %v", out.findings) + } + }) err := run(context.Background(), func(string) string { return "" }, io.Discard) if err == nil || !strings.Contains(err.Error(), "MESH_BROKER_FILE") { t.Fatalf("run = %v, want a refusal naming MESH_BROKER_FILE", err) @@ -55,3 +81,13 @@ func TestRunNeedsItsCredential(t *testing.T) { // 1135/AC6 type refusingBus struct{ err error } func (r refusingBus) KeyValue(context.Context, string) (jetstream.KeyValue, error) { return nil, r.err } + +// findingsOut keeps which findings stand. +type findingsOut struct{ findings map[string]bool } + +func (o *findingsOut) PutPresence(string, bool) error { return nil } +func (o *findingsOut) PutPulse(string, []byte, time.Duration) error { return nil } +func (o *findingsOut) DeletePulse(string) error { return nil } +func (o *findingsOut) PutStuck(string, []byte) error { return nil } +func (o *findingsOut) PutFinding(key string, _ []byte) error { o.findings[key] = true; return nil } +func (o *findingsOut) DeleteFinding(key string) error { delete(o.findings, key); return nil } diff --git a/modules/mesh-delivery-watchman/internal/watch/busstate.go b/modules/mesh-delivery-watchman/internal/watch/busstate.go new file mode 100644 index 00000000..0141ca8e --- /dev/null +++ b/modules/mesh-delivery-watchman/internal/watch/busstate.go @@ -0,0 +1,164 @@ +package watch + +import ( + "encoding/json" + "fmt" + "sort" + "strings" + "time" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// **A refused or overdue creation of bus state is loud** (novox/hq ADR 0595 §4; design 25 § 12.2; DLV-W1). The watchman +// hears, through its `consumes`, the indexer's `manifest-accepted` (which carries the module's bus-state entries whole) +// and the bus module's own `created` and `refused`. An entry an accepted manifest asks for that has no `created` or +// `refused` within OverdueBound is the finding bus-state-overdue, naming the module and the entry; a `refused` is the +// finding bus-state-refused, with why. A later `created` clears both, and so does a later accepted manifest of that +// module that no longer asks for the entry. Never the bus module's to raise: it cannot while it is down. +// +// The bound is a row of the signals table: a pulse key per entry with OverdueBound to live, whose expiry is the finding, +// so no loop looks and a pulse that stood across a restart still acts. Both findings outlive a restart (signalKind), +// since what ends them is heard, not read. + +// OverdueBound is how long an accepted entry may wait for the bus module's created or refused. +const OverdueBound = 5 * time.Minute + +// The kinds, and the signals table's row. +const ( + KindBusStateOverdue = "bus-state-overdue" + KindBusStateRefused = "bus-state-refused" + rowBusState = "bus-state" +) + +// BrokerSaid is the part of the bus module's created or refused the watchman reads: the module, the entry, and why a +// create was refused. It stands until the seat mesh-broker's definition is generated in mesh-contracts. +type BrokerSaid struct { + Module string `json:"module"` + // Local is the entry: the contract's `local-name` (mesh-contracts meshbroker.Said), or `local` where a body names it so. + Local string `json:"local,omitempty"` + LocalName string `json:"local-name,omitempty"` + Why string `json:"why,omitempty"` +} + +// busStateID is the signal's and the findings' id of one entry: `.`. +func busStateID(module, local string) string { return module + "." + local } + +func overdueKey(module, local string) string { + return FindingKey(ScopeModule, module, KindBusStateOverdue+"-"+local) +} + +func refusedKey(module, local string) string { + return FindingKey(ScopeModule, module, KindBusStateRefused+"-"+local) +} + +// Accepted is the indexer's manifest-accepted heard: each entry it asks for waits OverdueBound for the bus module's word, +// and every entry of the module this watchman waits on or holds a finding of that it no longer asks for is ended. +func (w *Watchman) Accepted(a delivery.ManifestAccepted) { + w.mu.Lock() + defer w.mu.Unlock() + asked := map[string]bool{} + for _, e := range a.BusState { + if e.Name == "" { + continue + } + asked[e.Name] = true + // One already said created or refused since an earlier verdict waits again: a new verdict asks the bus module again. + w.signal(rowBusState, busStateID(a.Module, e.Name), OverdueBound, signalDue{Do: dueFind, Finding: Finding{ + Scope: ScopeModule, ID: a.Module, Token: KindBusStateOverdue + "-" + e.Name, Kind: KindBusStateOverdue, + Severity: Warning, Source: rowBusState, Since: w.now().Add(OverdueBound).UTC().Truncate(time.Second), + Summary: fmt.Sprintf("the bus module said nothing of %s's %s (%s) within %s of its accepted manifest %s", a.Module, + e.Name, e.Storage, OverdueBound, short(a.Digest)), + Headline: "The bus state " + e.Name + " of " + a.Module + " was not created", + Explanation: "The indexer accepted a manifest of " + a.Module + " asking the bus module for " + e.Name + ", and the bus " + + "module said neither created nor refused within " + OverdueBound.String() + ": it is down, or behind. " + a.Module + + " fails at its start while the entry's binding is missing (novox/hq ADR 0595 §1).", + Needs: "a look at the bus module's health and its process's log; nats_first_view_again asks it to read the verdicts again", + Resolved: "the bus module said " + e.Name + " of " + a.Module + " created, or a later manifest no longer asks for it"}}) + } + for _, local := range w.busStateEntriesOf(a.Module) { + if asked[local] { + continue + } + w.unsignal(rowBusState, busStateID(a.Module, local)) + w.unfind(overdueKey(a.Module, local)) + w.unfind(refusedKey(a.Module, local)) + } +} + +// BrokerCreated is the bus module's created heard: the entry's wait ends, and its findings with it. +func (w *Watchman) BrokerCreated(s BrokerSaid) { + w.mu.Lock() + defer w.mu.Unlock() + w.unsignal(rowBusState, busStateID(s.Module, s.Local)) + w.unfind(overdueKey(s.Module, s.Local)) + w.unfind(refusedKey(s.Module, s.Local)) +} + +// BrokerRefused is the bus module's refused heard: the entry's wait ends in the finding bus-state-refused, until a later +// created. +func (w *Watchman) BrokerRefused(s BrokerSaid) { + w.mu.Lock() + defer w.mu.Unlock() + w.unsignal(rowBusState, busStateID(s.Module, s.Local)) + w.unfind(overdueKey(s.Module, s.Local)) + why := strings.TrimSpace(s.Why) + if why == "" { + why = "it gave no reason" + } + w.find(Finding{Scope: ScopeModule, ID: s.Module, Token: KindBusStateRefused + "-" + s.Local, Kind: KindBusStateRefused, + Severity: Urgent, Source: rowBusState, Since: w.now().UTC().Truncate(time.Second), Said: why, + Summary: fmt.Sprintf("the bus module refused to create %s's %s: %s", s.Module, s.Local, why), + Headline: "The bus state " + s.Local + " of " + s.Module + " was refused", + Explanation: "The bus module could not create " + s.Local + " for " + s.Module + ", so no binding is handed and " + s.Module + + " fails at its start, naming the entry (novox/hq ADR 0595 §1): " + why, + Needs: "a pull request that mends the entry in the manifest of " + s.Module + ", or the bus module's cause fixed", + Resolved: "the bus module said " + s.Local + " of " + s.Module + " created"}) +} + +// busStateEntriesOf are the entries of module this watchman waits on or holds a finding of, by local name, sorted. +func (w *Watchman) busStateEntriesOf(module string) []string { + seen := map[string]bool{} + prefix := signalKey(rowBusState, module) + "." + for pk := range w.signals { + if local, ok := strings.CutPrefix(pk, prefix); ok && !strings.Contains(local, ".") { + seen[local] = true + } + } + for _, kind := range []string{KindBusStateOverdue, KindBusStateRefused} { + p := FindingKey(ScopeModule, module, kind+"-") + for key := range w.findings { + if local, ok := strings.CutPrefix(key, p); ok { + seen[local] = true + } + } + } + out := make([]string, 0, len(seen)) + for l := range seen { + out = append(out, l) + } + sort.Strings(out) + return out +} + +// ReadBrokerSaid reads the bus module's created or refused. +func ReadBrokerSaid(body []byte) (BrokerSaid, error) { + var s BrokerSaid + if err := json.Unmarshal(body, &s); err != nil { + return BrokerSaid{}, fmt.Errorf("the bus module's word cannot be read: %w", err) + } + if s.Local == "" { + s.Local = s.LocalName + } + if s.Module == "" || s.Local == "" { + return BrokerSaid{}, fmt.Errorf("the bus module's word names no module or no entry") + } + return s, nil +} + +func short(digest string) string { + if len(digest) > 12 { + return digest[:12] + } + return digest +} diff --git a/modules/mesh-delivery-watchman/internal/watch/findings_test.go b/modules/mesh-delivery-watchman/internal/watch/findings_test.go index 40e308b2..d9c84281 100644 --- a/modules/mesh-delivery-watchman/internal/watch/findings_test.go +++ b/modules/mesh-delivery-watchman/internal/watch/findings_test.go @@ -6,10 +6,20 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/provisions" ) +func mustEncode(t *testing.T, f Finding) []byte { + t.Helper() + v, err := EncodeFinding(f) + if err != nil { + t.Fatal(err) + } + return v +} + func haveOf(t *testing.T, phase delivery.Phase, health delivery.Health, detail string) []byte { t.Helper() b, err := delivery.EncodeHave(delivery.Have{FP: fpNew, Phase: phase, Health: health, Detail: detail}) @@ -72,6 +82,52 @@ func TestTheWatchmansFindings(t *testing.T) { } w.ModuleVerdict("nats", value(""), Put) }, map[string]string{}}, + {"DLV-W1, 1966/AC37-AC38 (ADR 0595 §4, design 25 § 12.2): an accepted entry the bus module says nothing of in five minutes is overdue until created; a refused one stands until created; a later verdict no longer asking ends both", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { + accepted := func(locals ...string) delivery.ManifestAccepted { + a := delivery.ManifestAccepted{Module: "mesh-knowledge", Digest: strings.Repeat("d", 64)} + for _, l := range locals { + a.BusState = append(a.BusState, catalogue.BusStateEntry{Name: l, Storage: catalogue.StorageLedger}) + } + return a + } + w.Accepted(accepted("records", "drafts", "kept")) + pk := func(local string) string { return signalKey(rowBusState, busStateID("mesh-knowledge", local)) } + if o.ttls[pk("records")] != OverdueBound { + t.Fatalf("no five-minute pulse for records: %v", o.ttls) + } + w.BrokerCreated(BrokerSaid{Module: "mesh-knowledge", Local: "drafts"}) + expire(w, c, pk("drafts"), 5*time.Minute) // created in time: no finding + expire(w, c, pk("records"), 0) + overdue := o.findings[overdueKey("mesh-knowledge", "records")] + if overdue.Kind != KindBusStateOverdue || !strings.Contains(overdue.Summary, "mesh-knowledge's records") { + t.Fatalf("findings = %v", o.findings) + } + w.BrokerCreated(BrokerSaid{Module: "mesh-knowledge", Local: "records"}) + if _, open := o.findings[overdueKey("mesh-knowledge", "records")]; open { + t.Fatal("created did not clear overdue") + } + w.BrokerRefused(BrokerSaid{Module: "mesh-knowledge", Local: "kept", Why: "the cap is above the mesh's bound"}) + if f := o.findings[refusedKey("mesh-knowledge", "kept")]; f.Severity != Urgent || !strings.Contains(f.Summary, "cap is above") { + t.Fatalf("findings = %v", o.findings) + } + // A restart keeps both kinds: what ends them is heard. + if !signalKind(mustEncode(t, o.findings[refusedKey("mesh-knowledge", "kept")])) { + t.Error("a refused creation would be dropped at a restart") + } + // A later verdict asking for kept alone again waits on it again; one no longer asking for it ends it. + w.Accepted(accepted("kept", "fresh")) + w.Accepted(accepted("fresh")) + if _, open := o.findings[refusedKey("mesh-knowledge", "kept")]; open { + t.Fatal("a verdict no longer asking for kept left its finding") + } + if _, waits := o.pulses[pk("kept")]; waits { + t.Fatal("a verdict no longer asking for kept left its wait") + } + w.BrokerCreated(BrokerSaid{Module: "mesh-knowledge", Local: "fresh"}) + // A refused create stands until created. + w.Accepted(accepted("gone")) + w.BrokerRefused(BrokerSaid{Module: "mesh-knowledge", Local: "gone"}) + }, map[string]string{refusedKey("mesh-knowledge", "gone"): Urgent}}, {"ADR 0601 §4: what a node keeps of a module as it runs, for a manifest that does not say it yet, is a warning until the have says none", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { have := func(kept ...string) []byte { b, err := delivery.EncodeHave(delivery.Have{FP: "src2:" + strings.Repeat("a", 64), Phase: delivery.PhaseRunning, Health: delivery.HealthOK, Kept: kept}) diff --git a/modules/mesh-delivery-watchman/internal/watch/watch.go b/modules/mesh-delivery-watchman/internal/watch/watch.go index 35908f75..1e25c8cf 100644 --- a/modules/mesh-delivery-watchman/internal/watch/watch.go +++ b/modules/mesh-delivery-watchman/internal/watch/watch.go @@ -419,7 +419,7 @@ func signalKind(value []byte) bool { return false } switch f.Source { - case "provisioner.failing", "S8", "S9", "S10", "S11": + case "provisioner.failing", "S8", "S9", "S10", "S11", rowBusState: return true } return false diff --git a/modules/mesh-delivery-watchman/manifest_test.go b/modules/mesh-delivery-watchman/manifest_test.go index 5cb3db48..4e72ada3 100644 --- a/modules/mesh-delivery-watchman/manifest_test.go +++ b/modules/mesh-delivery-watchman/manifest_test.go @@ -47,12 +47,12 @@ type manifest struct { Module string `json:"module"` Claims []claim `json:"claims"` Tools []string `json:"tools"` + Consumes []string `json:"consumes"` Moved bool `json:"moved"` Version string `json:"version"` Slug string `json:"slug"` Reads []string `json:"reads"` Invokes []string `json:"invokes"` - Consumes []string `json:"consumes"` State []state `json:"state"` OwnSecrets map[string]string `json:"own-secrets"` SecretsOwner string `json:"secrets-owner"` @@ -106,7 +106,8 @@ func TestManifestStateAndReads(t *testing.T) { // 1135/AC6 if want := []string{"mesh-delivery-assigner.assigner_read_wants", "mesh-delivery-indexer.indexer_read_modules"}; !reflect.DeepEqual(m.Invokes, want) { t.Errorf("invokes = %v, want %v", m.Invokes, want) } - if want := []string{"mesh-delivery-assigner.views.wants", "mesh-delivery-indexer.views.modules"}; !reflect.DeepEqual(m.Consumes, want) { + // DLV-M5 its first views, and DLV-W1 (ADR 0595 §4): the accepted verdicts and the bus module's created and refused, durably. + if want := []string{"mesh-delivery-assigner.views.wants", "mesh-delivery-indexer.views.modules", "mesh-delivery-indexer.manifest-accepted", "mesh-broker.created", "mesh-broker.refused"}; !reflect.DeepEqual(m.Consumes, want) { t.Errorf("consumes = %v, want %v", m.Consumes, want) } // 1952/WM1: the watchman holds the seat mesh-watchdogs at mesh scope, its first seat (novox/hq ADR 0564 §1, §4). diff --git a/modules/mesh-delivery-watchman/module.json b/modules/mesh-delivery-watchman/module.json index 5d0dc5ec..dcac4d8e 100644 --- a/modules/mesh-delivery-watchman/module.json +++ b/modules/mesh-delivery-watchman/module.json @@ -30,7 +30,10 @@ ], "consumes": [ "mesh-delivery-assigner.views.wants", - "mesh-delivery-indexer.views.modules" + "mesh-delivery-indexer.views.modules", + "mesh-delivery-indexer.manifest-accepted", + "mesh-broker.created", + "mesh-broker.refused" ], "tools": [ "watchman_findings", diff --git a/modules/mesh-grants/cmd/mesh-grants/integration_test.go b/modules/mesh-grants/cmd/mesh-grants/integration_test.go index 25a157c0..b60896a0 100644 --- a/modules/mesh-grants/cmd/mesh-grants/integration_test.go +++ b/modules/mesh-grants/cmd/mesh-grants/integration_test.go @@ -6,10 +6,11 @@ import ( "context" "encoding/json" "errors" - "io" "os" "path/filepath" + "slices" "strings" + "sync" "sync/atomic" "testing" "time" @@ -22,6 +23,7 @@ import ( "git.novox.be/novox/mesh-sdk/go/membership" "git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-sdk/go/seal" + "git.novox.be/novox/mesh-sdk/go/views" "mesh-grants/internal/grants" ) @@ -56,7 +58,7 @@ func TestTheGrantsPartWhole(t *testing.T) { defer cancel() kvs := map[string]jetstream.KeyValue{} - for _, state := range grants.Reads { + for _, state := range append(append([]string(nil), grants.Reads...), grants.ViewReads...) { kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: bucketOf(state), History: 5}) if err != nil { t.Fatal(err) @@ -96,9 +98,17 @@ func TestTheGrantsPartWhole(t *testing.T) { verdict, _ := delivery.EncodeManifestVerdict(delivery.ManifestVerdict{Module: module, State: delivery.VerdictAccepted, Judge: "j", At: time.Now()}) put(grants.ReadVerdicts, digest, json.RawMessage(verdict)) } - manifest("nats", `{"module":"nats","version":"1","claims":[{"name":"mesh-broker"}],"own-secrets":{"broker":"/var/lib/nats/broker"}}`) + manifest("nats", `{"module":"nats","version":"1","claims":[{"name":"mesh-broker"}],"own-secrets":{"broker":"/var/lib/nats/broker"},`+ + `"emits":["created","done","dropped","recreated"],"streams":[{"name":"broker-events","events":["created","dropped","recreated"],"max-age-days":30,"max-mib":256}]}`) manifest("mod-c", `{"module":"mod-c","version":"1","own-secrets":{"broker":"/var/lib/c/broker"},"consumes":["nats.done"]}`) - pl, _ := delivery.EncodePlacement(delivery.Placement{Node: "a", Modules: []delivery.PlacedModule{{Module: "nats"}, {Module: "mod-c"}}}) + // The part itself, placed as on the mesh, so its own consumer on EVENTS hears the seat mesh-broker's events (DLV-G1). + own, err := os.ReadFile("../../module.json") + if err != nil { + t.Fatal(err) + } + manifest(grants.Module, string(own)) + pl, _ := delivery.EncodePlacement(delivery.Placement{Node: "a", Modules: []delivery.PlacedModule{{Module: "nats"}, {Module: "mod-c"}, + {Module: grants.Module}}}) if _, err := kvs[grants.ReadPlacement].Put(ctx, "placement.a", pl); err != nil { t.Fatal(err) } @@ -140,6 +150,45 @@ func TestTheGrantsPartWhole(t *testing.T) { t.Fatal(err) } t.Cleanup(func() { _ = sub.Unsubscribe() }) + // DLV-F4r, DLV-F5r (ADR 0615 §4, §6, ADR 0621 §3): the owners answer their first-view tools at once from what they + // hold, with its revision, as the indexer's and the assigner's runtimes of their own do; the part never reads their + // buckets. Started before its owners answer (answering false: each ask refused, as an owner not yet connected), the + // part waits, says so, and composes nothing it would read from them. + var firstViewAsks, refusedAsks atomic.Int32 + var answering atomic.Bool + for _, owner := range []string{grants.IndexerModule, grants.AssignerModule} { + owner := owner + osub, err := nc.Subscribe("mesh.mod."+owner+".>", func(m *nats.Msg) { + for _, fv := range grants.FirstViews { + if fv.Owner != owner || !strings.Contains(m.Subject, "."+fv.State) { + continue + } + if !answering.Load() { + refusedAsks.Add(1) + raw, _ := json.Marshal(map[string]any{"error": "not connected yet", "node": "a"}) + _ = m.Respond(raw) + return + } + firstViewAsks.Add(1) + values, rev := map[string]json.RawMessage{}, uint64(0) + if keys, err := kvs[fv.Read].ListKeys(ctx); err == nil { + for k := range keys.Keys() { + if e, err := kvs[fv.Read].Get(ctx, k); err == nil { + values[k] = e.Value() + rev = max(rev, e.Revision()) + } + } + } + answer(m, views.Answer{V: views.Version, Owner: owner, State: fv.State, Revision: rev, + Created: time.Date(2026, 11, 4, 0, 0, 0, 0, time.UTC), Values: values, At: time.Now()}) + return + } + }) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = osub.Unsubscribe() }) + } // A membership is never said on its subject any more (issue 1126). var said atomic.Int32 if _, err := nc.Subscribe("mesh.assignment.>", func(*nats.Msg) { said.Add(1) }); err != nil { @@ -154,7 +203,8 @@ func TestTheGrantsPartWhole(t *testing.T) { env := map[string]string{"MESH_BROKER_FILE": broker} runCtx, stop := context.WithCancel(ctx) done := make(chan error, 1) - go func() { done <- run(runCtx, func(k string) string { return env[k] }, io.Discard) }() + logged := &lockedLog{} + go func() { done <- run(runCtx, func(k string) string { return env[k] }, logged) }() t.Cleanup(func() { stop(); <-done }) until := func(what string, ok func() bool) { @@ -166,6 +216,14 @@ func TestTheGrantsPartWhole(t *testing.T) { } t.Fatalf("never saw: %s", what) } + until("every first view asked and refused once", func() bool { return refusedAsks.Load() >= int32(len(grants.FirstViews)) }) + until("the part saying it waits on the indexer and the assigner", func() bool { + return logged.has("mesh-delivery-indexer.manifests") && logged.has("mesh-delivery-assigner.placement") && logged.has("is not answered") + }) + if _, err := kvs[grants.Module+"."+grants.StateMemberships].Get(ctx, "a.mod-c"); err == nil { + t.Error("a membership composed before the owners answered its first views") + } + answering.Store(true) until("a.mod-c's membership in the part's own state memberships", func() bool { e, err := kvs[grants.Module+"."+grants.StateMemberships].Get(ctx, "a.mod-c") if err != nil { @@ -189,6 +247,41 @@ func TestTheGrantsPartWhole(t *testing.T) { if n := said.Load(); n != 0 { t.Errorf("%d memberships were said on their subject", n) } + if firstViewAsks.Load() < int32(len(grants.FirstViews)) { + t.Errorf("the part composed with %d first-view asks, want one of each of the %d", firstViewAsks.Load(), len(grants.FirstViews)) + } + // DLV-G1, 1966/AC34 (novox/hq ADR 0595 §1, §7): the bus module's created, said on the seat mesh-broker's subject on + // EVENTS like every seat's event (the lead's ruling, registry milestone 13:0x), reaches the part on its durable consumer + // there, and the binding it carries is in the module's membership; dropped takes it out. + until("the part's durable consumer on EVENTS hearing mesh-broker's created", func() bool { + cons, err := js.Consumer(ctx, "EVENTS", "a_"+grants.Module) + if err != nil { + return false + } + info, err := cons.Info(ctx) + return err == nil && slices.Contains(info.Config.FilterSubjects, "mesh.seat.mesh-broker.event.created") + }) + bindingOf := func() []membership.BusBinding { + e, err := kvs[grants.Module+"."+grants.StateMemberships].Get(ctx, "a.mod-c") + if err != nil { + return nil + } + m, _ := membership.DecodeGrantsMembership(e.Value()) + return m.Bindings + } + sayBroker := func(event string, bind membership.BusBinding) { + body, _ := json.Marshal(grants.BrokerEvent{V: 1, Module: "mod-c", Local: "kept", Storage: "key-value", Template: "state", Binding: bind}) + if _, err := js.Publish(ctx, "mesh.seat.mesh-broker.event."+event, body); err != nil { + t.Fatal(err) + } + } + sayBroker("created", membership.BusBinding{Local: "kept", Storage: "key-value", Template: "state", Bucket: "mod-c_kept"}) + until("mod-c's binding in its membership after created", func() bool { + b := bindingOf() + return len(b) == 1 && b[0].Bucket == "mod-c_kept" + }) + sayBroker("dropped", membership.BusBinding{Local: "kept"}) + until("mod-c's binding gone after dropped", func() bool { return len(bindingOf()) == 0 }) for _, bucket := range []string{"mesh-controller_lease", grants.Module + "_lease", grants.Module + "_takeover"} { if _, err := js.KeyValue(ctx, bucket); !errors.Is(err, jetstream.ErrBucketNotFound) { t.Errorf("the bucket %s: %v, want none", bucket, err) @@ -197,3 +290,21 @@ func TestTheGrantsPartWhole(t *testing.T) { // And what the part composes holds on a server that loads it (one integration test per module, the test budget). t.Run("the composed grants hold on a server that loads them", theBusNodeFollowsTheSealedListUnderTheComposedGrants) } + +// lockedLog is the part's log, read by the test while the part writes it. +type lockedLog struct { + mu sync.Mutex + buf strings.Builder +} + +func (l *lockedLog) Write(p []byte) (int, error) { + l.mu.Lock() + defer l.mu.Unlock() + return l.buf.Write(p) +} + +func (l *lockedLog) has(s string) bool { + l.mu.Lock() + defer l.mu.Unlock() + return strings.Contains(l.buf.String(), s) +} diff --git a/modules/mesh-grants/cmd/mesh-grants/main.go b/modules/mesh-grants/cmd/mesh-grants/main.go index 9cfc0c70..6f7a4cb7 100644 --- a/modules/mesh-grants/cmd/mesh-grants/main.go +++ b/modules/mesh-grants/cmd/mesh-grants/main.go @@ -36,6 +36,9 @@ import ( "git.novox.be/novox/mesh-sdk/go/announce" "git.novox.be/novox/mesh-sdk/go/process" + "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/views" + "mesh-grants/internal/grants" ) @@ -104,12 +107,25 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro return err } bus := newBusReader(ctx, js) - f.Part = &grants.Part{Read: bus, Call: conn, Store: ownStore{conn}, Emit: emitting{conn}, + // The owners' five states by their first views (ADR 0615, ADR 0622): asked through the part's own account, then + // followed on the owners' views families, which the hearing below hands them. + sayCond := sayVia(ctx, js, account.Module) + fv := grants.NewViews(func(tool string) func(context.Context, string) (views.Answer, error) { + return views.AskVia(grants.Unwrapped(conn.Call), tool) + }, sayCond, f.Wake, logger.Printf) + fv.Rest, fv.Watching = bus, bus + byState := map[string]*views.Follower{} + for _, v := range grants.FirstViews { + byState[v.State] = fv.Followers[v.Read] + } + hearViews := views.Hear(ctx, byState, logger.Printf) + f.Part = &grants.Part{Read: fv, Call: conn, Store: ownStore{conn}, Emit: emitting{conn}, Views: fv, FirstViewContext: ctx, Create: creating{ctx: ctx, js: js}, Consumers: creating{ctx: ctx, js: js}, Objects: sealedObjects{ctx: ctx, js: js}, Logf: logger.Printf, Bus: grants.BusAddress{URL: account.URL, Fingerprint: account.Fingerprint}} - f.Watch = bus - f.Part.Say = sayVia(ctx, js, account.Module) + f.Watch = fv + f.Part.Say = sayCond + fv.Start(ctx, f.Wake) f.Asked = func(wake func()) error { return conn.State(grants.StateUserList).Watch(grants.KeyAsked, func(c stdio.StateChange) error { if !c.Current { @@ -121,16 +137,58 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // The part makes this module's own durable consumer on EVENTS from the manifest's consumes (filtered on the two // condition subjects and the vault's account-issued; ADR 0561 §6), as it makes every module's; it is pulled and every message acknowledged, so its pending never grows. Where it is not there yet the pass says so and // hears again at the next change. The vault's bus-accounts is watched beside it, so a hash is composed either way. + // + // Beside EVENTS, the module streams its consumes reach (the bus module's own stream, whose created, recreated and + // dropped carry the bindings: ADR 0595 §1, §7), each on the durable consumer its own membership names; one not + // there yet is opened at the next change, and one opened is never opened twice. + opened := map[string]bool{} f.Hear = func(handle func(string, []byte)) error { - cons, err := js.Consumer(ctx, "EVENTS", account.Node+"_"+account.Module) - if err != nil { - return fmt.Errorf("the consumer %s_%s on EVENTS: %w", account.Node, account.Module, err) + consume := func(stream, name string) error { + if opened[stream] { + return nil + } + cons, err := js.Consumer(ctx, stream, name) + if err != nil { + return fmt.Errorf("the consumer %s on %s: %w", name, stream, err) + } + if _, err := cons.Consume(func(m jetstream.Msg) { + // An owner's views event goes to the follower of its state, which acknowledges it (firstviews.go). + if strings.Contains(m.Subject(), ".event."+views.EventPrefix) { + hearViews(m) + return + } + // The indexer's verdict events go to the verdicts' first view (ADR 0622 §2), which takes each at its revision. + if event, ok := strings.CutPrefix(m.Subject(), "mesh.mod."+grants.IndexerModule+".event."); ok && + (event == delivery.ManifestAcceptedEvent || event == delivery.ManifestRefusedEvent) { + if err := fv.HearVerdict(ctx, event, m.Data(), logger.Printf); err != nil { + logger.Printf("views: %s is not a verdict event, dropped: %v", m.Subject(), err) + _ = m.Term() + return + } + _ = m.Ack() + return + } + handle(m.Subject(), m.Data()) + _ = m.Ack() + }, jetstream.ConsumeErrHandler(func(_ jetstream.ConsumeContext, err error) { logger.Printf("hearing the bus: %v", err) })); err != nil { + return err + } + opened[stream] = true + return nil } - _, err = cons.Consume(func(m jetstream.Msg) { - handle(m.Subject(), m.Data()) - _ = m.Ack() - }, jetstream.ConsumeErrHandler(func(_ jetstream.ConsumeContext, err error) { logger.Printf("hearing the bus: %v", err) })) - return err + if err := consume("EVENTS", account.Node+"_"+account.Module); err != nil { + return err + } + hears, err := grants.OwnHears(ownStore{conn}, account.Node) + if err != nil { + return err + } + for _, h := range hears { + if err := consume(h.Stream, h.Consumer); err != nil { + return err + } + } + return nil } logger.Printf("started on the account %s: publishing memberships, writing grants and the user list sealed to the bus node, which it follows, and creating the account consumers and the declared buckets, logs and streams the bus lacks; a change to a held stream is only said", account.User) f.Run(ctx) diff --git a/modules/mesh-grants/cmd/mesh-grants/tools.go b/modules/mesh-grants/cmd/mesh-grants/tools.go index 5eb4e40d..0b140f3e 100644 --- a/modules/mesh-grants/cmd/mesh-grants/tools.go +++ b/modules/mesh-grants/cmd/mesh-grants/tools.go @@ -1,11 +1,19 @@ package main import ( + "context" "encoding/json" + "fmt" + "maps" + "sort" "strings" + "sync" "time" + "git.novox.be/novox/mesh-sdk/go/views" + stdio "git.novox.be/novox/mesh-sdk/go" + "git.novox.be/novox/mesh-sdk/go/handacts" "mesh-grants/internal/grants" ) @@ -13,12 +21,34 @@ import ( // The part's tools, served through the tool runner from its bundle (ADR 0436: a process never serves). They read and // write through the runtime as the module: its own states, the reads its manifest names, the bus module's read tools. -// runtimeReader reads another part's state through the runtime, `.`. -type runtimeReader struct{} +// runtimeReader reads another part's state through the runtime, `.`; the owners' five states the part +// reads by their first views (grants.ViewReads) it reads by asking each owner's first-view tool, once per tool call +// (fresh), never their buckets (ADR 0615 §1). +type runtimeReader struct{ views *toolViews } -func (runtimeReader) Keys(state string) ([]string, error) { return stdio.State(state).Keys() } +func (r runtimeReader) Keys(state string) ([]string, error) { + if values, mine, err := r.views.of(state); mine { + if err != nil { + return nil, err + } + out := make([]string, 0, len(values)) + for k := range values { + out = append(out, k) + } + sort.Strings(out) + return out, nil + } + return stdio.State(state).Keys() +} -func (runtimeReader) Get(state, key string) ([]byte, bool, error) { +func (r runtimeReader) Get(state, key string) ([]byte, bool, error) { + if values, mine, err := r.views.of(state); mine { + if err != nil { + return nil, false, err + } + v, ok := values[key] + return v, ok, nil + } e, err := stdio.State(state).Get(key) if err != nil || e == nil { return nil, false, err @@ -26,6 +56,63 @@ func (runtimeReader) Get(state, key string) ([]byte, bool, error) { return e.Value, true, nil } +// toolViews are the five first views as one tool call reads them: each asked whole at its first read in the call, kept +// for the rest of it, and forgotten by fresh at the next call. +type toolViews struct { + mu sync.Mutex + held map[string]map[string]json.RawMessage +} + +func (v *toolViews) fresh() { + v.mu.Lock() + v.held = nil + v.mu.Unlock() +} + +// of is the whole picture of a first view's state, whether state is one, and why it could not be had. +func (v *toolViews) of(state string) (map[string]json.RawMessage, bool, error) { + var fv *grants.FirstView + for i := range grants.FirstViews { + if grants.FirstViews[i].Read == state { + fv = &grants.FirstViews[i] + } + } + if fv == nil { + return nil, false, nil + } + v.mu.Lock() + defer v.mu.Unlock() + if held, ok := v.held[state]; ok { + return held, true, nil + } + ask := views.AskVia(grants.Unwrapped(stdio.Ask), fv.Tool) + values := map[string]json.RawMessage{} + first := true + var rev uint64 + for after := ""; ; { + a, err := ask(context.Background(), after) + if err != nil { + return nil, true, fmt.Errorf("%s does not answer: %w", fv.Tool, err) + } + if first { + rev, first = a.Revision, false + } + maps.Copy(values, a.Values) + if a.Next == "" || a.Next == after { + break + } + after = a.Next + } + if rev == 0 { + return nil, true, fmt.Errorf("%s by %s: %w", state, fv.Tool, grants.ErrFirstViewNotYet) + } + if v.held == nil { + v.held = map[string]map[string]json.RawMessage{} + } + v.held[state] = values + return values, true, nil +} + // runtimeStore is the part's own state through the runtime. type runtimeStore struct{} @@ -142,11 +229,36 @@ func toolsOf(p *grants.Part) []stdio.Tool { } return p.AcceptHashes(stdio.CallerOf(a), users, grants.Answer(a, "why")) }}, + {Name: grants.ToolFirstViews, Description: "The grants part's first views of the owners' states (novox/hq ADR 0615 §4 to §6, " + + "ADR 0622): the assigner's placement and wants, the indexer's manifests, modules and verdicts, each with its owner, tool " + + "and the revision its picture holds, when it asked and when it was answered, how many asks and hand acts, and whether " + + "its condition is raised, as the part's process last kept them. Read-only.", + Run: func(map[string]any) (any, error) { return p.FirstViewsOf() }}, + {Name: grants.ToolFirstViewAgain, Description: "Ask one of the grants part's first views once more now (placement, wants, " + + "manifests, modules or verdicts; none for all), by a person's act with why, recorded as a hand act: the step after an " + + "owner's state was written again from its sources (an empty answer at revision 0, condition " + + "mesh-grants.first-view-empty). Asks the part's process, which alone follows them; grants_first_views shows the answer.", + Input: schema(map[string]string{"state": "placement, wants, manifests, modules or verdicts; none for all", "why": "why, kept in the hand act"}, "why"), + Run: func(a map[string]any) (any, error) { + return p.AskFirstViewAgain(stdio.CallerOf(a), grants.Answer(a, "state"), grants.Answer(a, "why"), recordHandAct) + }}, } } +// recordHandAct records a person's act through the seat mesh-hand-acts. +func recordHandAct(what, why string) error { + _, err := handacts.Recorder{Ask: stdio.Ask}.Record(handacts.Request{What: what, Why: why, Cause: "first-view-again"}) + return err +} + // serveTools serves the tools to the runtime until it stops. func serveTools() error { - p := &grants.Part{Read: runtimeReader{}, Call: runtimeBus{}, Store: runtimeStore{}, Emit: runtimeBus{}} - return stdio.Serve("", toolsOf(p)) + tv := &toolViews{} + p := &grants.Part{Read: runtimeReader{views: tv}, Call: runtimeBus{}, Store: runtimeStore{}, Emit: runtimeBus{}} + tools := toolsOf(p) + for i := range tools { + run := tools[i].Run + tools[i].Run = func(a map[string]any) (any, error) { tv.fresh(); return run(a) } + } + return stdio.Serve("", tools) } diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 5967fbc2..7b0aa40d 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -15,6 +15,7 @@ import ( "slices" "strings" "sync" + "sync/atomic" "testing" "time" @@ -25,6 +26,7 @@ import ( "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/membership" "git.novox.be/novox/mesh-sdk/go/seal" + "git.novox.be/novox/mesh-sdk/go/views" "mesh-grants/internal/buscompose" "mesh-grants/internal/grants" @@ -376,14 +378,18 @@ const ( // brokerSeat is one verb of the bus's seat as a membership serves it: nats claims the seat in fixture and no membership // composed there serves it, so a membership or list that drops it leaves the seat with no holder (issue 1945). -func brokerSeat() (seat, verb string) { - s, _ := catalogue.SeatNamed(catalogue.BrokerSeat) - verb = "v" - if len(s.Serves) > 0 { - verb = s.Serves[0].Name - } - return s.Name, verb -} +// heldSeat is a seat the fixture's bus module claims beside mesh-broker (natsHeldManifest), and a verb of it: a seat the +// catalogue defines without verbs, its holder declaring them, so a verb the composition no longer grants is lost, never +// retired. mesh-broker's own verbs are all optional, and the composition grants its reading verbs to every holder, so +// losing one of them loses no holder. +func heldSeat() (seat, verb string) { return heldSeatName, "v" } + +// heldSeatName is that seat, and natsHeldManifest the fixture's bus module claiming it beside mesh-broker. +const ( + heldSeatName = "mesh-dns-resolver" + natsHeldManifest = `{"module":"nats","version":"1","claims":[{"name":"mesh-broker"},{"name":"` + heldSeatName + `"}],` + + `"own-secrets":{"broker":"/var/lib/nats/broker"}}` +) // fixture is a mesh of two machines: a runs the bus and mod-x, b runs mod-y; the bus node's agent loaded a list. func fixture() *fakeBus { @@ -703,8 +709,8 @@ func TestGrants(t *testing.T) { t.Errorf("a reader of a held module's object store lost its grant: %v", heldWeb.Publish) } b.manifestOf("mesh-knowledge", knowledge) // accepted again for the rest of the row - // ADR 0592 §6, §25: the bucket past 75 percent of its cap is said, in one warning, and cleared once below; the history - // stream at 10 percent is not. + // ADR 0595 §6, design 25 § 12.3 (DLV-G3): fill is the bus module's; the part raises no history-full, however full a + // bucket is, and clears the old key once after its start. var said []asks.Condition p.Say = func(c asks.Condition) error { if c.Key == grants.FullCondition { @@ -712,25 +718,15 @@ func TestGrants(t *testing.T) { } return nil } - size := func(stream string, bytes int64) { - for _, s := range b.streams { - if s["name"] == stream { - s["bytes"] = bytes - } + for _, s := range b.streams { + if s["name"] == "OBJ_mesh-knowledge_sections" { + s["bytes"] = int64(3500 << 20) } } - size("OBJ_mesh-knowledge_sections", 3500<<20) - size("VERSIONS", 200<<20) - if res := p.Compose(); len(res.Full) != 1 || res.Full[0].Stream != "OBJ_mesh-knowledge_sections" || res.Full[0].Percent() != 85 || - len(said) != 1 || said[0].State != asks.StateRaised || !strings.Contains(said[0].Summary, "3500 of 4096 MiB (85%)") { - t.Fatalf("full = %+v, said %+v", res.Full, said) - } - if p.Compose(); len(said) != 1 { - t.Errorf("the same fill was said again: %+v", said) - } - size("OBJ_mesh-knowledge_sections", 1<<20) - if res := p.Compose(); len(res.Full) != 0 || len(said) != 2 || said[1].State != asks.StateCleared { - t.Errorf("below its mark: full %+v, said %+v", res.Full, said) + p.Compose() + p.Compose() + if len(said) != 1 || said[0].State != asks.StateCleared { + t.Errorf("history-full: said %+v, want one clear and no raise", said) } }) @@ -1256,7 +1252,9 @@ func TestGrants(t *testing.T) { n := newFakeBus() // 1952/S5 (ADR 0564 Limits 4): the bus module's tools bundle declares its own account and what it may remove. n.manifestOf("nats", strings.Replace(strings.TrimSuffix(natsManifest, "}"), `"broker":"/var/lib/nats/broker"`, `"broker":"/var/lib/nats/broker","broker-nats-tools":"${dir:tools-secret}/broker-nats-tools"`, 1)+ - `,"resources":[{"id":"tools-secret","type":"directory","path":"/var/lib/mesh/nats-tools","mode":"0711"}],"build":{"artifacts":[{"name":"tools","kind":"bundle","language":"go",`+ + `,"resources":[{"id":"tools-secret","type":"directory","path":"/var/lib/mesh/nats-tools","mode":"0711"},`+ + // ADR 0626 §1: the keeper's process declares `keeps`, which its principal carries. + `{"id":"keeper","type":"process","name":"nats-keeper","artifact":"tools","run":["./nats-tools","keep"],"bus":{"state":["asked"],"keeps":true}}],"build":{"artifacts":[{"name":"tools","kind":"bundle","language":"go",`+ `"system":"arch","from":"cmd/nats-tools","binary":"nats-tools","loads":["nats-tools"],"bus":{"removes":["controller-leftovers","mesh-host_acts"]}}]}}`) n.manifestOf("claude-licence-manager", string(licence)) n.assign("novox", "nats") @@ -1276,8 +1274,9 @@ func TestGrants(t *testing.T) { tools = d.Processes } } - if len(tools) != 1 || tools[0].Process != "nats-tools" || !slices.Equal(tools[0].Removes, []string{"controller-leftovers", "mesh-host_acts"}) { - t.Errorf("the bus module's principals = %+v, want nats-tools carrying both removals", tools) + if len(tools) != 2 || tools[0].Process != "keeper" || !tools[0].Keeps || tools[1].Process != "nats-tools" || tools[1].Keeps || + !slices.Equal(tools[1].Removes, []string{"controller-leftovers", "mesh-host_acts"}) { + t.Errorf("the bus module's principals = %+v, want the keeper keeping and nats-tools carrying both removals", tools) } if m := grants.Memberships(recs)["novox.claude-licence-manager"]; len(m.Membership.Seats) == 0 { t.Errorf("the licence manager's membership under the seeded placement carries no seats: %+v", m.Membership) @@ -1569,8 +1568,9 @@ func TestGrants(t *testing.T) { // Issue 1945, 2026-10-27 09:21: a list that would leave a seat the list in force holds with no holder, // while a placed module still claims it, is held, as such a membership is: not written, the warning // raised naming the seat. A list that only adds is written; one whose loaded grants cannot be read is held. + b.manifestOf("nats", natsHeldManifest) inForce := slices.Clone(b.loaded) - seat, verb := brokerSeat() + seat, verb := heldSeat() seatGrant := "mesh.seat." + seat + ".call." + verb + ".*" for i := range b.loaded { if b.loaded[i].User == "a.nats" { @@ -1635,7 +1635,7 @@ func TestGrants(t *testing.T) { t.Run("memberships: published unless a seat would lose its last holder or a holder a capability it asks for", func(t *testing.T) { const key = "a.mod-x" - brokerName, brokerVerb := brokerSeat() + brokerName, brokerVerb := heldSeat() // The telegram module's claim of its channel, as mesh-catalog's telegram manifest has it (c05aa5e6), on mod-x. const telegramClaim = `[{"name":"channel","scope":"mesh","kind":"telegram","capabilities":["deliver","verified-sender"]}]` verified := func(m map[string]any) map[string]any { @@ -1692,6 +1692,7 @@ func TestGrants(t *testing.T) { } { t.Run(row.name, func(t *testing.T) { b := fixture() + b.manifestOf("nats", natsHeldManifest) if row.claims != "" { b.manifestOf("mod-x", strings.Replace(xManifest, `"own-secrets"`, `"claims":`+row.claims+`,"own-secrets"`, 1)) } @@ -1893,6 +1894,7 @@ func TestGrants(t *testing.T) { t.Run("memberships held back: every one is said and listed", func(t *testing.T) { b := fixture() + b.manifestOf("nats", natsHeldManifest) b.loaded = shadowOf(t, b) recs, _ := grants.Records(grants.ReadInputs(b, &grants.Manifests{})) b.own[grants.StateMemberships] = map[string][]byte{} @@ -1900,7 +1902,7 @@ func TestGrants(t *testing.T) { raw, _ := json.Marshal(grants.Memberships(recs)[key].Membership) var m map[string]any _ = json.Unmarshal(raw, &m) - seat, verb := brokerSeat() + seat, verb := heldSeat() m["seats"] = []any{map[string]any{"seat": seat, "verb": verb, "subject": "mesh.seat." + seat + ".tool." + verb}} raw, _ = json.Marshal(m) b.own[grants.StateMemberships][key] = raw @@ -1926,7 +1928,7 @@ func TestGrants(t *testing.T) { <-done held, err := partOn(b).MembershipOf("", "") h, _ := held.(map[string]any) - if seat, _ := brokerSeat(); err != nil || h["count"] != 2 || !strings.Contains(fmt.Sprint(h["held"]), "the last holder of "+seat) { + if seat, _ := heldSeat(); err != nil || h["count"] != 2 || !strings.Contains(fmt.Sprint(h["held"]), "the last holder of "+seat) { t.Errorf("grants_membership with neither node nor module = %v, %v", held, err) } if _, err := partOn(b).MembershipOf("a", ""); err == nil { @@ -2207,6 +2209,273 @@ func TestGrants(t *testing.T) { } }) + // DLV-G1, 1966/AC32-AC33 (novox/hq ADR 0595 §1, §7; design 25 § 12.2): the bus module's created writes the binding into + // the module's membership on every machine it is assigned, one assigned later included; recreated rewrites it; dropped + // removes it; before created, on a refused, or from an emitter holding no mesh-broker, none. + t.Run("bindings: written on created, rewritten on recreated, removed on dropped, kept in memberships", func(t *testing.T) { + b := fixture() + p := partOn(b) + p.Compose() + bindingsOn := func(key string) []membership.BusBinding { + raw, _, found, _ := b.ownGet(grants.StateMemberships, key) + if !found { + t.Fatalf("no membership %s", key) + } + bare, _ := membership.BareGrantsMembership(raw) + var m membership.Membership + _ = json.Unmarshal(bare, &m) + return m.Bindings + } + if got := bindingsOn("a.mod-x"); len(got) != 0 { + t.Fatalf("a binding before created: %+v", got) + } + hear := func(event string, bind membership.BusBinding) bool { + body, _ := json.Marshal(grants.BrokerEvent{V: 1, Module: "mod-x", Local: "records", Storage: "key-value", Template: "state", Binding: bind}) + changed, err := p.HearBroker(event, body, p.Brokers()) + if err != nil { + t.Fatalf("%s: %v", event, err) + } + return changed + } + first := membership.BusBinding{Local: "records", Storage: "key-value", Template: "state", Bucket: "mod-x_records"} + for _, event := range []string{"mesh.seat.mesh-broker.event.refused", "mesh.stream.mod-y.event.created", + "mesh.seat.mesh-broker.event.drifted", "mesh.stream.nats.event.created", "mesh.mod.nats.event.created", "nats.created"} { + if hear(event, first) { + t.Errorf("%s changed the bindings", event) + } + } + // The part's own consumer on the bus module's stream, and every module's on a module stream it hears, is created by + // the part since the controller ended (ADR 0363 §5, ADR 0590): here mod-y's on mod-x's THINGS. + heard := fixture() + heard.manifestOf("mod-y", strings.Replace(yManifest, `"reads"`, `"consumes":["mod-x.thing.added"],"reads"`, 1)) + hp := partOn(heard) + hp.Consumers = heard + hp.Compose() + if !slices.Contains(heard.consumerCreates, "THINGS.b_mod-y") { + t.Errorf("the consumer of a heard module stream was not created: %v", heard.consumerCreates) + } + if !hear("mesh.seat.mesh-broker.event.created", first) || hear("mesh.seat.mesh-broker.event.created", first) { + t.Fatal("created: not kept once") + } + b.assign("b", "mod-x") + p.Compose() + for _, key := range []string{"a.mod-x", "b.mod-x"} { + if got := bindingsOn(key); len(got) != 1 || got[0].Bucket != "mod-x_records" { + t.Errorf("%s after created: %+v", key, got) + } + } + if got := bindingsOn("a.nats"); len(got) != 0 { + t.Errorf("another module's membership carries the binding: %+v", got) + } + again := first + again.Bucket = "mod-x_records-2" + hear("mesh.seat.mesh-broker.event.recreated", again) + p.Compose() + if got := bindingsOn("b.mod-x"); len(got) != 1 || got[0].Bucket != "mod-x_records-2" { + t.Errorf("after recreated: %+v", got) + } + // The seat's own subject, and the contract's field names (mesh-contracts meshbroker.Said), are read alike. + if changed, err := p.HearBroker("mesh.seat.mesh-broker.event.dropped", []byte(`{"module":"mod-x","local-name":"records"}`), p.Brokers()); err != nil || !changed { + t.Fatal("dropped changed nothing") + } + p.Compose() + if got := bindingsOn("a.mod-x"); len(got) != 0 { + t.Errorf("after dropped: %+v", got) + } + }) + + // TLS-5 (A7's request; ADR 0615 §5): the tool runner, which consumes the owners' views events and keeps a findings state, + // gets its durable consumer on each views stream, made by the part, and its own findings bucket, as any manifest's. + t.Run("the tool runner's consumes of the owners' views get their consumers, and its findings state its grant", func(t *testing.T) { + b := fixture() + b.manifestOf("mesh-delivery-indexer", `{"module":"mesh-delivery-indexer","version":"1","own-secrets":{"broker":"/var/lib/i/broker"},`+ + `"emits":["views.manifests","views.modules"],"streams":[{"name":"indexer-views","events":["views.*"],"max-age-days":7,"max-mib":256}]}`) + b.manifestOf("mesh-delivery-assigner", `{"module":"mesh-delivery-assigner","version":"1","own-secrets":{"broker":"/var/lib/as/broker"},`+ + `"emits":["views.placement","views.seat-holders"],"streams":[{"name":"assigner-views","events":["views.*"],"max-age-days":7,"max-mib":256}]}`) + b.manifestOf("node-tools", `{"module":"node-tools","version":"1","tool-runner":true,"own-secrets":{"broker":"/var/lib/nt/broker"},`+ + `"consumes":["mesh-delivery-assigner.views.placement","mesh-delivery-assigner.views.seat-holders","mesh-delivery-indexer.views.manifests",`+ + `"mesh-delivery-indexer.views.modules"],"state":[{"name":"findings","history":5}]}`) + for _, m := range []string{"mesh-delivery-indexer", "mesh-delivery-assigner", "node-tools"} { + b.assign("a", m) + } + p := partOn(b) + p.Consumers = b + p.Compose() + for _, c := range []string{"INDEXER_VIEWS.a_node-tools", "ASSIGNER_VIEWS.a_node-tools"} { + if !slices.Contains(b.consumerCreates, c) { + t.Errorf("the tool runner's consumer %s was not made: %v", c, b.consumerCreates) + } + } + users := shadowOf(t, b) + runner := users[slices.IndexFunc(users, func(u grants.User) bool { return u.User == "a.node-tools" })] + for _, s := range []string{"$JS.API.CONSUMER.MSG.NEXT.INDEXER_VIEWS.a_node-tools", "$KV.node-tools_findings.>"} { + if !slices.ContainsFunc(runner.Publish, func(g string) bool { return buscompose.SubjectsOverlap(g, s) }) { + t.Errorf("the tool runner may not publish %s: %v", s, runner.Publish) + } + } + }) + + // DLV-F4r, DLV-R1 (novox/hq ADR 0615 §4 to §6, ADR 0622; design 25 § 12.2): the five owners' states are read by their + // first views; an answer empty at revision 0 drops no login and raises first-view-empty with the operator's step until an + // answer above 0; an owner down past the bound raises first-view-unanswered, cleared by its answer; an event applies a + // change with no ask; grants_first_view_again refuses a state that is none of the five and records the hand act. + t.Run("first views: the owners' states by their first views, an empty answer at revision 0 dropping no login", func(t *testing.T) { + b := fixture() + b.loaded = shadowOf(t, b) + created := time.Date(2026, 11, 4, 9, 0, 0, 0, time.UTC) + var down, empty atomic.Bool + empty.Store(true) + owner := func(tool string) func(context.Context, string) (views.Answer, error) { + return func(context.Context, string) (views.Answer, error) { + if down.Load() && strings.HasSuffix(tool, ".verdicts") { + return views.Answer{}, errors.New("no responders") + } + for _, fv := range grants.FirstViews { + if fv.Tool != tool { + continue + } + if empty.Load() && fv.State == "placement" { + return views.Answer{Revision: 0, Created: created, Values: map[string]json.RawMessage{}}, nil + } + values := map[string]json.RawMessage{} + b.mu.Lock() + for k, v := range b.states[fv.Read] { + values[k] = v + } + b.mu.Unlock() + return views.Answer{Revision: 7, Created: created, Values: values}, nil + } + return views.Answer{}, errors.New("no such tool " + tool) + } + } + var said []asks.Condition + var sayMu sync.Mutex + say := func(c asks.Condition) error { sayMu.Lock(); said = append(said, c); sayMu.Unlock(); return nil } + fv := grants.NewViews(owner, say, func() {}, nil) + fv.Rest = b + var minutes atomic.Int64 + for _, f := range fv.Followers { + f.Wait = time.Nanosecond + f.Now = func() time.Time { return created.Add(time.Duration(minutes.Add(1)) * time.Minute) } + } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + down.Store(true) + fv.Start(ctx, func() {}) + for _, f := range fv.Followers { + if f.Tool != grants.IndexerModule+".verdicts" { + <-f.Ready() + } + } + raised := func(key string, state string) bool { + sayMu.Lock() + defer sayMu.Unlock() + last := "" + for _, c := range said { + if c.Key == key { + last = c.State + } + } + return last == state + } + waitFor(t, func() bool { + return raised(grants.FirstViewUnanswered+".mesh-delivery-indexer.verdicts", asks.StateRaised) + }) + p := partOn(b) + p.Read, p.Views, p.Say = fv, fv, say + before := p.Compose() + if !slices.ContainsFunc(before.UserList.Gaps, func(g grants.Gap) bool { return g.Input == grants.ReadPlacement && g.Failed }) { + t.Fatalf("placement answered empty at revision 0 was not a failed read: %+v", before.UserList.Gaps) + } + if !raised(grants.FirstViewEmpty+".mesh-delivery-assigner.placement", asks.StateRaised) { + t.Fatalf("no first-view-empty: %+v", said) + } + empty.Store(false) + down.Store(false) + if _, err := fv.Again(ctx, "placement", "the assigner composed its wants again"); err != nil { + t.Fatal(err) + } + waitFor(t, func() bool { return fv.Followers[grants.ReadPlacement].Status().Revision == 7 }) + waitFor(t, func() bool { + return raised(grants.FirstViewUnanswered+".mesh-delivery-indexer.verdicts", asks.StateCleared) + }) + after := p.Compose() + if slices.ContainsFunc(after.UserList.Gaps, func(g grants.Gap) bool { return g.Input == grants.ReadPlacement }) || + !raised(grants.FirstViewEmpty+".mesh-delivery-assigner.placement", asks.StateCleared) { + t.Fatalf("after an answer above 0: gaps %+v, said %+v", after.UserList.Gaps, said) + } + if after.UserList.Users != before.UserList.Users && before.UserList.Users > after.UserList.Users { + t.Errorf("the empty first view dropped users: %d then %d", before.UserList.Users, after.UserList.Users) + } + // An event of the owner's views family changes the picture with no ask. + asksBefore := fv.Followers[grants.ReadPlacement].Status().Asks + raw, _ := json.Marshal(map[string]any{"v": 1, "node": "c"}) + if gap, why := fv.Followers[grants.ReadPlacement].Apply(views.Event{V: 1, Owner: grants.AssignerModule, State: "placement", + Key: "placement.c", Revision: 8, Created: created, Value: raw}); gap { + t.Fatalf("a next revision was a gap: %s", why) + } + if _, found, _ := fv.Get(grants.ReadPlacement, "placement.c"); !found || fv.Followers[grants.ReadPlacement].Status().Asks != asksBefore { + t.Error("the event did not reach the picture, or asked") + } + // The verdicts follow the indexer's manifest-accepted and manifest-refused at the revision each carries (ADR 0622 §2): + // the next revision changes the picture with no ask; one already in it is passed over; one carrying no revision, or + // past the next, asks the first view once more. + verdicts := fv.Followers[grants.ReadVerdicts] + digest := strings.Repeat("ab", 32) + heard := func(event string, body any) { + t.Helper() + raw, _ := json.Marshal(body) + if err := fv.HearVerdict(ctx, event, raw, t.Logf); err != nil { + t.Fatalf("%s: %v", event, err) + } + } + verdictOf := func() delivery.ManifestVerdict { + raw, _, _ := fv.Get(grants.ReadVerdicts, digest) + v, _ := delivery.DecodeManifestVerdict(raw) + return v + } + asked := verdicts.Status().Asks + heard(delivery.ManifestAcceptedEvent, delivery.ManifestAccepted{V: 1, Module: "mod-z", Digest: digest, Judge: "j", Revision: 8}) + if v := verdictOf(); v.State != delivery.VerdictAccepted || v.Module != "mod-z" || verdicts.Status().Revision != 8 { + t.Errorf("manifest-accepted at the next revision: %+v at %d", v, verdicts.Status().Revision) + } + heard(delivery.ManifestRefusedEvent, delivery.ManifestRefused{V: 1, Module: "mod-z", Digest: digest, Judge: "j", Problems: []string{"p"}, Revision: 9}) + heard(delivery.ManifestAcceptedEvent, delivery.ManifestAccepted{V: 1, Module: "mod-z", Digest: digest, Judge: "j", Revision: 9}) + if v := verdictOf(); !v.Refused() || !slices.Equal(v.Problems, []string{"p"}) || verdicts.Status().Asks != asked { + t.Errorf("manifest-refused, then a revision already held: %+v, asks %d (was %d)", v, verdicts.Status().Asks, asked) + } + // A revision below the picture's (ADR 0615 §5; the verdicts made again restart low): the same verdict again is a + // redelivery, passed over; another verdict asks the first view once more. + heard(delivery.ManifestRefusedEvent, delivery.ManifestRefused{V: 1, Module: "mod-z", Digest: digest, Judge: "j", Problems: []string{"p"}, Revision: 3}) + if verdicts.Status().Asks != asked { + t.Errorf("a held verdict redelivered at a lower revision asked: asks %d (was %d)", verdicts.Status().Asks, asked) + } + heard(delivery.ManifestAcceptedEvent, delivery.ManifestAccepted{V: 1, Module: "mod-z", Digest: digest, Judge: "j", Revision: 2}) + waitFor(t, func() bool { return verdicts.Status().Asks > asked }) + waitFor(t, func() bool { s := verdicts.Status(); return !s.Answered.Before(s.Asked) }) + asked = verdicts.Status().Asks + heard(delivery.ManifestAcceptedEvent, delivery.ManifestAccepted{V: 1, Module: "mod-z", Digest: digest, Judge: "j"}) + waitFor(t, func() bool { return verdicts.Status().Asks > asked }) + asked = verdicts.Status().Asks + heard(delivery.ManifestAcceptedEvent, delivery.ManifestAccepted{V: 1, Module: "mod-z", Digest: digest, Judge: "j", Revision: 12}) + waitFor(t, func() bool { return verdicts.Status().Asks > asked }) + if err := fv.HearVerdict(ctx, "manifest-judged", []byte("{}"), t.Logf); err == nil { + t.Error("an event that is no verdict was taken") + } + // The tools: a state that is none of the five is refused; one is asked with its hand act. + var acts []string + record := func(what, why string) error { acts = append(acts, what+": "+why); return nil } + if _, err := p.AskFirstViewAgain("jochen@shanks", "builds", "x", record); err == nil || len(acts) != 0 { + t.Errorf("a state that is no first view was asked: %v %v", err, acts) + } + if _, err := p.AskFirstViewAgain("jochen@shanks", "verdicts", "after the take-in", record); err != nil || len(acts) != 1 { + t.Errorf("verdicts again: %v %v", err, acts) + } + if got, err := p.FirstViewsOf(); err != nil || !strings.Contains(fmt.Sprint(got), "mesh-delivery-indexer.verdicts") { + t.Errorf("grants_first_views = %v, %v", got, err) + } + }) + // 774/AC4 (novox/hq ADR 0561 §2): every account the vault mints that the records compose is written to the state // accounts once, with the bus's address and a module's claims, read by the vault alone; a key is deleted when placement // no longer names its user, and never in a pass whose reads failed. diff --git a/modules/mesh-grants/internal/buscompose/buscompose_test.go b/modules/mesh-grants/internal/buscompose/buscompose_test.go index c57ae568..304636a2 100644 --- a/modules/mesh-grants/internal/buscompose/buscompose_test.go +++ b/modules/mesh-grants/internal/buscompose/buscompose_test.go @@ -50,6 +50,7 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) { t.Run("a declared stream is created as the controller asserted it", declaredStreamsAreCreatedAsTheControllerAssertsThem) t.Run("every node's compositions are read by their named readers alone and written by their node", everyNodesCompositions) t.Run("a module's ledger is written by its ledger account alone", aLedgerIsWrittenByItsLedgerAccountAlone) + t.Run("the bus module's process alone keeps every module's bus state and serves the broker's verbs", theBusModulesProcessKeepsBusState) // ADR 0567 §3, §9: a node reads every key of the seat's placement and its candidates (it composes with the whole // proposal laid over), and none of its open proposals; it writes neither. reads := strings.Join(NodeComposeReads("laptop"), " ") @@ -946,6 +947,10 @@ func everyNodesCompositions(t *testing.T) { perms[u.Username()] = p } view, plex, one := perms["one.mesh-delivery-view"], perms["one.plex"], perms["node.one"] + // ENG-11 (design 51): a node's engine hears the status view's settled, and a module not named does not. + if !MaySubscribe(one, "mesh.mod.mesh-delivery-view.event.settled") || MaySubscribe(plex, "mesh.mod.mesh-delivery-view.event.settled") { + t.Error("the node-engine does not hear the view's settled, or a module not named does") + } for _, node := range []string{"one", "two"} { bucket := "composed-" + node if !MayPublish(view, "$JS.API.DIRECT.GET.KV_"+bucket+".$KV."+bucket+"."+node+".node") || @@ -1056,3 +1061,141 @@ func aLedgerIsWrittenByItsLedgerAccountAlone(t *testing.T) { } } } + +// DLV-G2, 1966/AC35 (novox/hq ADR 0595 §4, §5; design 25 § 12.2, § 12.3): the process of the module holding mesh-broker +// may create, update and delete every module's bus state by its names (a bucket's KV_, an object store's OBJ_, a ledger's +// four parts) and an orphan's the bus module said it created, never a stream of the foundation; it serves the seat's eight +// verbs; and no other principal, the tool runner and the bus module's own account included, holds the acting four. +func theBusModulesProcessKeepsBusState(t *testing.T) { + r := Records{Nodes: []string{"a"}, Assigned: map[string][]Declared{"a": { + {Module: "node-tools", ToolRunner: true}, + {Module: "nats", SnapshotsTheBus: true, Holds: []Seat{{Name: catalogue.BrokerSeat, Serves: BrokerVerbs(), + Emits: []string{"created", "refused"}}}, + Consumes: []string{"mesh-delivery-indexer.manifest-accepted"}, Uses: []Seat{{Name: "operator-channel", Accepts: []string{"condition"}, ByCaller: []string{"condition"}}}, + Streams: []ModuleStream{{Module: "nats", Name: "BROKER_EVENTS", Events: []string{"created", "refused"}, MaxAgeDays: 30, MaxMiB: 256}}, + Processes: []ProcessPrincipal{{Process: "keeper", State: []string{"asked"}, Keeps: true}, {Process: "other"}, + {Process: "nats-tools", Tools: true, Removes: []string{catalogue.RemovesEngineActs}}}}, + {Module: "mod-x", Processes: []ProcessPrincipal{{Process: "sneak", Keeps: true}}, + Streams: []ModuleStream{{Module: "mod-x", Name: "SECRETS", Events: []string{"sealed"}, Restricted: true}}}, + {Module: "admin-ish", Invokes: []string{"*"}}, + }}, + BusState: map[string][]catalogue.BusStateEntry{"mod-x": { + {Name: "records", Storage: catalogue.StorageKeyValue, Template: catalogue.TemplateState}, + {Name: "audit", Storage: catalogue.StorageLedger, Template: catalogue.TemplateLedger, + Types: []catalogue.LedgerType{{Name: "decision", Indexes: []catalogue.LedgerIndex{{Name: "by-status", On: []string{"status"}}}}}}, + {Name: "parts", Storage: catalogue.StorageObjectStore, Template: catalogue.TemplateObjects}}}, + Bound: map[string][]string{"gone": {"KV_gone_kept"}}} + users, err := Users(r) + if err != nil { + t.Fatal(err) + } + perms := map[string]Permissions{} + for _, u := range users { + p, err := PermissionsFor(u) + if err != nil { + t.Fatalf("%s: %v", u.Username(), err) + } + perms[u.Username()] = p + } + keeper := perms["a.nats.keeper"] + for _, u := range []string{"a.nats.keeper", "a.nats.other", "a.mod-x.sneak", "a.node-tools", "a.nats.nats-tools", "a.admin-ish"} { + if _, ok := perms[u]; !ok { + t.Fatalf("no user %s composed: %v", u, slices.Collect(maps.Keys(perms))) + } + } + for _, s := range []string{"KV_mod-x_records", "mod-x_audit", "mod-x_audit_snapshot", "KV_mod-x_audit_state", + "KV_mod-x_audit_index-by-status", "OBJ_mod-x_parts", "KV_gone_kept"} { + for _, op := range []string{"CREATE", "UPDATE", "DELETE", "SNAPSHOT"} { + if !MayPublish(keeper, "$JS.API.STREAM."+op+"."+s) { + t.Errorf("the bus module's process may not %s %s", op, s) + } + // Only the process whose bus declares `keeps`, and only on the module holding mesh-broker (ADR 0626 §1, §3): no + // tool runner, and not the bus module's own tools (ADR 0626 §4), however wide their grant. + for _, other := range []string{"a.nats.other", "a.mod-x.sneak", "a.node-tools", "a.nats.nats-tools", "a.admin-ish"} { + if MayPublish(perms[other], "$JS.API.STREAM."+op+"."+s) { + t.Errorf("%s, which is no keeper of the bus, may %s %s", other, op, s) + } + } + } + } + for _, s := range []string{"EVENTS", "KV_mesh-grants_memberships", "KV_mod-x_other"} { + if MayPublish(keeper, "$JS.API.STREAM.CREATE."+s) || MayPublish(keeper, "$JS.API.STREAM.DELETE."+s) { + t.Errorf("the bus module's process may define %s", s) + } + } + // A process serves nothing (ADR 0436 §3): it hears its module's consumes on its own consumer, says its module's own + // stream's events and raises its conditions through the operator channel. + for _, v := range BrokerVerbs() { + if MaySubscribe(keeper, "mesh.seat.mesh-broker.tool."+v) { + t.Errorf("the bus module's process serves %s", v) + } + } + if keeper.AllowResponses { + t.Error("the bus module's process answers what it was asked") + } + for _, s := range []string{"$JS.API.CONSUMER.MSG.NEXT.EVENTS.a_nats_keeper", "$JS.ACK.EVENTS.a_nats_keeper.x", + "mesh.stream.nats.event.created", "mesh.seat.operator-channel.accept.condition.nats", + // The seat's events on the seat's subjects, as every seat's (the lead's ruling, registry milestone 13:0x), and the + // acknowledgement of a snapshot's chunks of an entry it keeps (ADR 0626 §1). + "mesh.seat.mesh-broker.event.created", "mesh.seat.mesh-broker.event.refused", "$JS.SNAPSHOT.ACK.KV_mod-x_records.x.1.0"} { + if !MayPublish(keeper, s) { + t.Errorf("the bus module's process may not publish %s", s) + } + } + for _, other := range []string{"a.nats.other", "a.mod-x.sneak"} { + if MayPublish(perms[other], "mesh.seat.mesh-broker.event.created") { + t.Errorf("%s, which is no keeper of the bus, says mesh-broker's created", other) + } + } + // ADR 0626, How it is checked: no subscription to a restricted stream's subjects, nor a consumer of it or of any + // stream it keeps; a snapshot's chunks come to its own inbox alone, and only of a stream it keeps. + for _, s := range []string{"mesh.stream.mod-x.event.sealed", "mesh.stream.mod-x.event.>", "mesh.bus.mod-x.records.x"} { + if MaySubscribe(keeper, s) { + t.Errorf("the bus module's process hears %s", s) + } + } + for _, s := range []string{"$JS.API.CONSUMER.CREATE.SECRETS.x.y", "$JS.API.CONSUMER.CREATE.KV_mod-x_records.x", + "$JS.API.CONSUMER.MSG.NEXT.SECRETS.x", "$JS.API.DIRECT.GET.SECRETS", "$JS.API.STREAM.SNAPSHOT.SECRETS", + "$JS.SNAPSHOT.ACK.SECRETS.x.1.0", "$JS.API.STREAM.MSG.GET.KV_mod-x_records"} { + if MayPublish(keeper, s) { + t.Errorf("the bus module's process may publish %s", s) + } + } + for _, s := range keeper.Subscribe { + if !strings.HasPrefix(s, "_INBOX.a.nats.keeper.") && !strings.HasPrefix(s, "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_nats_") && + !strings.HasPrefix(s, "mesh.seat.operator-channel.") { + t.Errorf("the bus module's process subscribes %s, beyond its inbox, its own state's creation and its conditions' answers", s) + } + } + made := false + for _, mc := range AccountConsumers(users) { + if mc.Consumer.Stream == "EVENTS" && mc.Consumer.Name == "a_nats_keeper" && + slices.Equal(mc.Consumer.Filters, []string{"mesh.mod.mesh-delivery-indexer.event.manifest-accepted"}) { + made = true + } + } + if !made { + t.Errorf("the bus module's process's consumer is not made: %+v", AccountConsumers(users)) + } + // Its module's tools serve the reading verbs, and its tools bundle reads the module's own stream back. + if !MaySubscribe(perms["a.node-tools"], "mesh.seat.mesh-broker.tool.list") { + t.Error("nothing serves the seat's list") + } + if !MayPublish(perms["a.nats.nats-tools"], "$JS.API.CONSUMER.CREATE.BROKER_EVENTS.x.y") { + t.Error("the bus module's tools cannot read its events back") + } + for user, p := range perms { + if user == "a.nats.keeper" { + continue + } + for _, v := range BrokerActingVerbs { + subject := "mesh.seat.mesh-broker.tool." + v + if MayPublish(p, subject) || MaySubscribe(p, subject) { + t.Errorf("%s holds %s", user, subject) + } + } + if MayPublish(p, "$JS.API.STREAM.CREATE.KV_mod-x_records") && user != "a.mesh-grants" { + t.Errorf("%s may create mod-x's bus state", user) + } + } +} diff --git a/modules/mesh-grants/internal/buscompose/busstate.go b/modules/mesh-grants/internal/buscompose/busstate.go new file mode 100644 index 00000000..27b73d5b --- /dev/null +++ b/modules/mesh-grants/internal/buscompose/busstate.go @@ -0,0 +1,202 @@ +package buscompose + +import ( + "slices" + "sort" + "strings" + + "git.novox.be/novox/mesh-sdk/go/catalogue" +) + +// **The bus module's process has a bus account of its own** (novox/hq ADR 0595 §4, §5; design 25 § 12.2, § 12.3; +// DLV-G2): composed for the process of the module holding the seat mesh-broker (a seat, never a module's name), it +// creates, updates, reads, snapshots and deletes every module's bus state under that module's names only, each stream +// by its name (a key-value bucket's and an object store's by the stream the bus keeps it in); it hears what its module +// consumes (the indexer's manifest-accepted) on a durable consumer of its own, says its module's own stream's events, +// and raises its conditions through the operator channel. As every process account it serves nothing (ADR 0436 §3): the +// seat's four reading verbs are served by its module's tools, and the four acting verbs by nobody on the bus, a +// person's ask reaching the process through its own state. No account holds the acting verbs' subjects, the tool +// runner's included: each is denied to every principal whose publish would reach it. +// +// The names are one rule (BusStateStreams), read by the bus module where it creates them: until mesh-sdk carries it +// as one function (asked of the mesh-sdk member of the milestone), this is the grants part's side of it. + +// The verbs of the seat mesh-broker (design 25 § 12.3), until its definition is generated in mesh-contracts: the four +// reading verbs any caller its role grants may ask, and the four acting verbs the bus module's process alone serves. +var ( + BrokerReadingVerbs = []string{"list", "show", "check", "orphans"} + BrokerActingVerbs = []string{"create", "apply", "recreate", "drop"} +) + +// BrokerVerbs are the eight. +func BrokerVerbs() []string { + return append(append([]string(nil), BrokerReadingVerbs...), BrokerActingVerbs...) +} + +// brokerSeat is the seat mesh-broker as its verbs' subjects name it: held once, for the mesh. +var brokerSeat = Seat{Name: catalogue.BrokerSeat} + +// BrokerVerbSubjects are where the seat's verbs are asked: each verb's tool subject, and its caller-named form (issue +// 365). +func BrokerVerbSubjects(verbs []string) []string { + var out []string + for _, v := range verbs { + out = append(out, seatToolSubject(brokerSeat, v, ""), seatSubject(brokerSeat, CallKind, v)+".>") + } + sort.Strings(out) + return out +} + +// BrokerActingOnly are the subjects of the seat's acting verbs, which no principal publishes on, however wide its +// grant: the bus module's process serves them, and a person's ask reaches it on its own tool subject instead +// (design 25 § 12.3). +func BrokerActingOnly() []string { return BrokerVerbSubjects(BrokerActingVerbs) } + +// BusStateStreams are the streams one bus-state entry of module is kept in on the bus, by the mesh's names +// `_` (design 25 § 12.1): a stream's own; a key-value bucket's KV_ stream; an object store's OBJ_ +// stream; a ledger's history stream, its snapshot stream, and its state and index buckets' KV_ streams. A name that is +// not one plain subject token names nothing. +func BusStateStreams(module string, e catalogue.BusStateEntry) []string { + base := module + "_" + e.Name + if !safeSubject.MatchString(base) || strings.Contains(e.Name, "_") || strings.Contains(module, "_") { + return nil + } + var out []string + switch e.Storage { + case catalogue.StorageKeyValue: + out = []string{"KV_" + base} + case catalogue.StorageObjectStore: + out = []string{"OBJ_" + base} + case catalogue.StorageLedger: + out = []string{base, base + "_snapshot", "KV_" + base + "_state"} + for _, t := range e.Types { + for _, ix := range t.Indexes { + if safeSubject.MatchString(ix.Name) { + out = append(out, "KV_"+base+"_index-"+ix.Name) + } + } + } + default: + out = []string{base} + } + sort.Strings(out) + return slices.Compact(out) +} + +// keeperConsumer is the durable consumer the bus module's process hears its module's consumes on, on EVENTS: +// `__`, as a process's own consumer is named (QueueConsumer), filtered on the consumed events. +func keeperConsumer(p Principal) (Consumer, bool) { + var filters []string + for _, c := range p.Consumes { + if s, err := consumedSubject(c); err == nil && !strings.HasPrefix(s, moduleStreamPrefix) { + filters = append(filters, s) + } + } + if len(filters) == 0 { + return Consumer{}, false + } + sort.Strings(filters) + return Consumer{Name: p.Node + "_" + p.Module + "_" + p.Process, Stream: "EVENTS", Filters: slices.Compact(filters), + AckWaitSeconds: 30, MaxDeliver: 5, FromNow: true, + Why: "what the bus module's process " + p.Process + " hears of its module's consumes (novox/hq ADR 0595 §4): the " + + "indexer's accepted verdicts, from now, its first view at start holding what came before"}, true +} + +// keeperConsumerGrants are the subjects the process reads and acknowledges its consumer through. +func keeperConsumerGrants(c Consumer) []string { + return []string{"$JS.API.CONSUMER.INFO." + c.Stream + "." + c.Name, "$JS.API.CONSUMER.MSG.NEXT." + c.Stream + "." + c.Name, + "$JS.ACK." + c.Stream + "." + c.Name + ".>"} +} + +// ownStreamReadGrants are what reading a module's own streams back needs (the bus module's tool history): each stream's +// information and an ordered consumer of its own, as a log's reader has (readLogGrants). +func ownStreamReadGrants(module string, streams []ModuleStream) []string { + var out []string + for _, s := range streams { + if s.Module != module || !safeSubject.MatchString(s.Name) { + continue + } + out = append(out, "$JS.API.STREAM.INFO."+s.Name, "$JS.API.CONSUMER.CREATE."+s.Name+".>", + "$JS.API.CONSUMER.MSG.NEXT."+s.Name+".*", "$JS.API.CONSUMER.INFO."+s.Name+".*", "$JS.API.CONSUMER.DELETE."+s.Name+".*") + } + return out +} + +// BusStateNames are every stream the bus module's process may define: those of every entry a manifest asks for now +// (asked), and those the bus module said it created and has not dropped (bound, by module: names of streams), so an +// orphan whose manifest left the trunk can still be dropped. Sorted, once each. +func BusStateNames(asked map[string][]catalogue.BusStateEntry, bound map[string][]string) []string { + var out []string + for module, entries := range asked { + for _, e := range entries { + out = append(out, BusStateStreams(module, e)...) + } + } + for _, names := range bound { + for _, n := range names { + if safeSubject.MatchString(n) { + out = append(out, n) + } + } + } + sort.Strings(out) + return slices.Compact(out) +} + +// busStateKeeperGrants are what the bus module's process publishes to keep the bus state named: each stream's create, +// update, information, deletion and snapshot with the acknowledgement of the snapshot's chunks (ADR 0626 §1; the server +// waits on `$JS.SNAPSHOT.ACK..…` past its window), and the stream list it finds orphans in. Never a consumer of +// one, nor any stream's own subjects: a snapshot carries an entry's data to the keeper's inbox alone, and the keeper +// hears no message of it (ADR 0626 §1, restricted streams included). +func busStateKeeperGrants(names []string) []string { + out := []string{"$JS.API.STREAM.NAMES", "$JS.API.STREAM.LIST"} + for _, n := range names { + for _, op := range []string{"CREATE", "UPDATE", "INFO", "DELETE", "SNAPSHOT"} { + out = append(out, "$JS.API.STREAM."+op+"."+n) + } + out = append(out, "$JS.SNAPSHOT.ACK."+n+".>") + } + return out +} + +// brokerSeatEvents are the subjects of the seat mesh-broker's events, from the held seat's definition: what the bus +// module's keeper says them on, like every seat's holder (the lead's ruling, registry milestone 13:0x). +func brokerSeatEvents(holds []Seat) []string { + var out []string + for _, s := range holds { + if s.Name != catalogue.BrokerSeat { + continue + } + for _, e := range s.Emits { + if safeSubject.MatchString(e) { + out = append(out, seatSubject(s, "event", e)) + } + } + } + return out +} + +// keepsBusState is the bus module's process defining one of the bus-state streams it keeps: the writers table's +// exemption from stream definitions being the grants part's (ADR 0595 §4). +func keepsBusState(p Principal, pattern string) bool { + if p.Kind != KindProcess || len(p.KeepsBusState) == 0 { + return false + } + for _, op := range []string{"CREATE", "UPDATE", "DELETE"} { + if name, ok := strings.CutPrefix(pattern, "$JS.API.STREAM."+op+"."); ok { + return slices.Contains(p.KeepsBusState, name) + } + } + return false +} + +// withoutActing are a holder's served verbs of mesh-broker without the acting four. +func withoutActing(served []string) []string { + var out []string + for _, v := range served { + if !slices.Contains(BrokerActingVerbs, v) { + out = append(out, v) + } + } + return out +} diff --git a/modules/mesh-grants/internal/buscompose/composed.go b/modules/mesh-grants/internal/buscompose/composed.go index b3825726..26d65664 100644 --- a/modules/mesh-grants/internal/buscompose/composed.go +++ b/modules/mesh-grants/internal/buscompose/composed.go @@ -84,8 +84,9 @@ func NodeAgentAsks(node string) ([]string, error) { } // NodeAgentHears are the events a node's node-engine subscribes while it applies a want: the backup holder's word that a -// backup is taken (`backed-up`), from whichever module holds node-backup on its machine (restic today). -var NodeAgentHears = []string{"mesh.mod." + BackupHolderModule + ".event.backed-up"} +// backup is taken (`backed-up`), from whichever module holds node-backup on its machine (restic today); and the status +// view's `settled`, on which it retries a pull from a store's holder that failed (design 51, ENG-11). +var NodeAgentHears = []string{"mesh.mod." + BackupHolderModule + ".event.backed-up", "mesh.mod." + ViewModule + ".event.settled"} // BackupHolderModule is the module that holds node-backup on every node, restic: the one whose word a node-engine hears. // A second holder of the seat is a change of this grant. diff --git a/modules/mesh-grants/internal/buscompose/derived.go b/modules/mesh-grants/internal/buscompose/derived.go index c1d23fbb..8085e2de 100644 --- a/modules/mesh-grants/internal/buscompose/derived.go +++ b/modules/mesh-grants/internal/buscompose/derived.go @@ -229,6 +229,12 @@ func ConsumersOf(users []Principal) []ModuleConsumer { if p.Kind == KindModule { add(p) } + // The bus module's process hears its module's consumes on a consumer of its own (busstate.go). + if p.Kind == KindProcess && p.KeepsBus { + if c, ok := keeperConsumer(p); ok { + out = append(out, ModuleConsumer{Node: p.Node, Module: p.Module, Consumer: c}) + } + } } for _, p := range users { if p.Kind != KindNodeTools { @@ -248,23 +254,30 @@ func ConsumersOf(users []Principal) []ModuleConsumer { } // AccountConsumers are the durable consumers an account needs (novox/hq ADR 0561 §6): of ConsumersOf, each module's on -// EVENTS and each process's on its own work queue, never one on a module stream it hears (those the controller makes with -// the stream's grant). The grants part creates them from the lease write, with these configurations. +// EVENTS, each process's on its own work queue, and each module's on a module stream it hears (ADR 0363 §5), which the +// controller made with the stream's grant until it ended (ADR 0590): a restricted stream's only for a module of its own +// account, as ComposedHears composes it. The grants part creates them, with these configurations. func AccountConsumers(users []Principal) []ModuleConsumer { - queues := map[string]bool{} + queues, heard := map[string]bool{}, map[string]bool{} for _, p := range users { for _, q := range p.Queues { queues[q.Stream] = true } + for _, h := range p.Hears { + heard[h.Stream] = true + } for _, d := range p.Carries { for _, q := range d.Queues { queues[q.Stream] = true } + for _, h := range ComposedHears(d) { + heard[h.Stream] = true + } } } var out []ModuleConsumer for _, mc := range ConsumersOf(users) { - if mc.Consumer.Stream == consumerStream(Principal{Kind: KindModule}) || queues[mc.Consumer.Stream] { + if mc.Consumer.Stream == consumerStream(Principal{Kind: KindModule}) || queues[mc.Consumer.Stream] || heard[mc.Consumer.Stream] { out = append(out, mc) } } diff --git a/modules/mesh-grants/internal/buscompose/nats.go b/modules/mesh-grants/internal/buscompose/nats.go index 7f85009a..e029a421 100644 --- a/modules/mesh-grants/internal/buscompose/nats.go +++ b/modules/mesh-grants/internal/buscompose/nats.go @@ -110,6 +110,14 @@ type Principal struct { // Ledgers are the local names of the ledgers its module asks for in `requires`, for a KindLedger principal alone // (issue 1966 D5): its whole authority is publishing on those ledgers' histories and snapshots (LedgerPublishGrants). Ledgers []string + // KeepsBus says this is the process of the module holding mesh-broker, the bus module's own (busstate.go, ADR 0595 + // §4, §5), and KeepsBusState are the streams it defines, every module's bus state by its name. + KeepsBus bool + KeepsBusState []string + // SaysSeatEvents are the subjects of the seat mesh-broker's events the bus module's keeper says them on (the lead's + // ruling, registry milestone 13:0x): `mesh.seat.mesh-broker.event.` for each event the seat's definition + // names, as a holder's own account would be granted them; set on the keeper alone. + SaysSeatEvents []string // Process is the process resource's id, for a KindProcess principal alone (novox/hq ADR 0436 §2): // Username and inbox are ... Process string @@ -592,6 +600,8 @@ func PermissionsFor(p Principal) (Permissions, error) { if err != nil { return Permissions{}, err } + // The bus module's tools bundle reads its module's own stream back (its tool history, design 25 § 12.3). + pub = append(pub, ownStreamReadGrants(p.Module, p.Streams)...) sort.Strings(pub) if err := CheckWriters(p, pub); err != nil { return Permissions{}, err @@ -894,8 +904,11 @@ func PermissionsFor(p Principal) (Permissions, error) { "$JS.API.CONSUMER.INFO."+consumerStream(p)+"."+consumerDurable(p), "$JS.API.CONSUMER.MSG.NEXT."+consumerStream(p)+"."+consumerDurable(p)) - // 3. Seats it holds: full participation. + // 3. Seats it holds: full participation. But mesh-broker's acting verbs, which nobody serves on the bus (busstate.go). for _, s := range p.Holds { + if s.Name == catalogue.BrokerSeat { + s.Serves = withoutActing(s.Serves) + } if s.isNewTraffic() { // Composed by SeatTrafficOf below, worker and all; only its tools are served here. for _, t := range s.Serves { @@ -991,6 +1004,10 @@ func PermissionsFor(p Principal) (Permissions, error) { // And its own streams' events (novox/hq ADR 0363 §2). pub = append(pub, ownStreamGrants(d.Module, d.Streams)...) for _, s := range d.Holds { + // mesh-broker's acting verbs are served by nobody on the bus (busstate.go). + if s.Name == catalogue.BrokerSeat { + s.Serves = withoutActing(s.Serves) + } for _, t := range s.Serves { sub = append(sub, seatToolSubject(s, t, p.Node)) } @@ -1157,6 +1174,13 @@ func PermissionsFor(p Principal) (Permissions, error) { } } } + // And the seat mesh-broker's acting verbs to every principal whose grant reaches them (ADR 0595 §5): served by the bus + // module's process alone, asked by nobody on the bus. + for _, only := range BrokerActingOnly() { + if slices.ContainsFunc(pub, func(a string) bool { return SubjectsOverlap(a, only) }) { + deny = append(deny, only) + } + } // And the vault's enrolment verbs to every principal but mesh-enrolment's (novox/hq ADR 0587 §1). if p.VaultEnrols && !((p.Kind == KindModule || p.Kind == KindProcess) && p.Module == delivery.EnrolmentModule) { for _, only := range EnrolmentOnly() { diff --git a/modules/mesh-grants/internal/buscompose/objects.go b/modules/mesh-grants/internal/buscompose/objects.go index 0d3e0af3..e064525f 100644 --- a/modules/mesh-grants/internal/buscompose/objects.go +++ b/modules/mesh-grants/internal/buscompose/objects.go @@ -11,8 +11,7 @@ import ( // A module's own object bucket (novox/hq ADR 0592 §5, §9). The grants part creates it once, create-only, kept with no // age limit and refusing a new object when full; only its module writes it; another module reads it only through a // `reads` entry naming it and the owner's trusted config value of the bucket's grant naming that module, read-only, as a -// restricted stream is heard (ADR 0363 §4, §5). The condition when it passes 75 percent of its cap is the grants part's -// (grants.FullCondition). +// restricted stream is heard (ADR 0363 §4, §5). The condition when it fills is the bus module's (ADR 0595 §6). // ObjectBucket is one object bucket a module owns, as the bus holds it. type ObjectBucket struct { diff --git a/modules/mesh-grants/internal/buscompose/process_grants.go b/modules/mesh-grants/internal/buscompose/process_grants.go index 99c7b1d1..e6509a3c 100644 --- a/modules/mesh-grants/internal/buscompose/process_grants.go +++ b/modules/mesh-grants/internal/buscompose/process_grants.go @@ -52,5 +52,19 @@ func processGrants(p Principal) (pub, sub []string, err error) { } pub = append(pub, invoked...) + // 6. The bus module's process (busstate.go, ADR 0595 §4, §5): every module's bus state it keeps, by name; its own + // consumer of what its module consumes; its module's own stream's events; and its conditions through the seats its + // module uses (the operator channel's accept.condition in its module's name). + if p.KeepsBus { + pub = append(pub, busStateKeeperGrants(p.KeepsBusState)...) + if c, ok := keeperConsumer(p); ok { + pub = append(pub, keeperConsumerGrants(c)...) + } + pub = append(pub, ownStreamGrants(p.Module, p.Streams)...) + pub = append(pub, p.SaysSeatEvents...) + tp, ts := seatTrafficGrants(SeatTrafficOf(p.Module, nil, p.Uses, nil)) + pub, sub = append(pub, tp...), append(sub, ts...) + } + return pub, sub, nil } diff --git a/modules/mesh-grants/internal/buscompose/testdata/composed.conf b/modules/mesh-grants/internal/buscompose/testdata/composed.conf index f65a1ba8..87436d7c 100644 --- a/modules/mesh-grants/internal/buscompose/testdata/composed.conf +++ b/modules/mesh-grants/internal/buscompose/testdata/composed.conf @@ -34,7 +34,7 @@ accounts { } } { user: "node.one", password: "$2a$11$nnnnnnnnnnnnnnnnnnnnnn", permissions: { publish: { allow: ["$JS.ACK.NODES.one.>", "$JS.API.CONSUMER.CREATE.KV_composed-one.>", "$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.one.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.one.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.>", "$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_verdicts.>", "$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.one.>", "$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.one.>", "$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.one", "$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.one", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.one.>", "$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node", "$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.one.>", "$JS.API.CONSUMER.CREATE.OBJ_declaration-files-one.*.$O.declaration-files-one.C.*", "$JS.API.CONSUMER.DELETE.KV_composed-one.>", "$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.>", "$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_verdicts.>", "$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.>", "$JS.API.CONSUMER.DELETE.KV_mesh-host_data.>", "$JS.API.CONSUMER.DELETE.KV_mesh-host_health.>", "$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.>", "$JS.API.CONSUMER.DELETE.KV_node-engine_have.>", "$JS.API.CONSUMER.DELETE.OBJ_declaration-files-one.*", "$JS.API.CONSUMER.INFO.NODES.one", "$JS.API.DIRECT.GET.KV_composed-one.>", "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.one.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.one.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.>", "$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_verdicts.>", "$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.one.>", "$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.one.>", "$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.one", "$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.one", "$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.one.>", "$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node", "$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.one.>", "$JS.API.DIRECT.GET.OBJ_declaration-files-one.$O.declaration-files-one.M.*", "$JS.API.STREAM.INFO.KV_composed-one", "$JS.API.STREAM.INFO.KV_mesh-config_resolved", "$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates", "$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement", "$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants", "$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds", "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates", "$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_verdicts", "$JS.API.STREAM.INFO.KV_mesh-host_acts", "$JS.API.STREAM.INFO.KV_mesh-host_data", "$JS.API.STREAM.INFO.KV_mesh-host_health", "$JS.API.STREAM.INFO.KV_mesh-host_packages", "$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions", "$JS.API.STREAM.INFO.KV_node-engine_have", "$JS.FC.KV_composed-one.>", "$JS.FC.KV_mesh-config_resolved.>", "$JS.FC.KV_mesh-delivery-assigner_candidates.>", "$JS.FC.KV_mesh-delivery-assigner_placement.>", "$JS.FC.KV_mesh-delivery-assigner_wants.>", "$JS.FC.KV_mesh-delivery-builder_builds.>", "$JS.FC.KV_mesh-delivery-indexer_candidates.>", "$JS.FC.KV_mesh-delivery-indexer_manifests.>", "$JS.FC.KV_mesh-delivery-indexer_modules.>", "$JS.FC.KV_mesh-delivery-indexer_verdicts.>", "$JS.FC.KV_mesh-host_acts.>", "$JS.FC.KV_mesh-host_data.>", "$JS.FC.KV_mesh-host_health.>", "$JS.FC.KV_mesh-host_packages.>", "$JS.FC.KV_mesh-provisioner_provisions.>", "$JS.FC.KV_node-engine_have.>", "$JS.FC.OBJ_declaration-files-one.>", "$KV.composed-one.>", "$KV.mesh-host_acts.one.>", "$KV.mesh-host_data.one.>", "$KV.mesh-host_health.one", "$KV.mesh-host_packages.one", "$KV.node-engine_have.one.>", "$SRV.INFO", "$SRV.INFO.>", "$SRV.PING", "$SRV.PING.*.one", "$SRV.PING.>", "mesh.control.one.>", "mesh.seat.node-backup.call.backed-up.one.node~one", "mesh.seat.node-backup.call.now.one.node~one", "mesh.seat.node-backup.tool.backed-up.one", "mesh.seat.node-backup.tool.now.one", "mesh.seat.node-engine.event.*.one"] } - subscribe: { allow: ["$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates", "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement", "$SRV.INFO", "$SRV.INFO.node-engine", "$SRV.INFO.node-engine.>", "$SRV.PING", "$SRV.PING.node-engine", "$SRV.PING.node-engine.>", "$SRV.STATS", "$SRV.STATS.node-engine", "$SRV.STATS.node-engine.>", "_DELIVER.one", "_DELIVER.one.>", "_INBOX.node.one.>", "mesh.mod.restic.event.backed-up", "mesh.node.one.ask.access-act", "mesh.node.one.ask.hand-over", "mesh.node.one.ask.judge-directories", "mesh.node.one.ask.move-secret", "mesh.node.one.ask.report", "mesh.node.one.ask.setuid-search", "mesh.node.one.declare", "mesh.seat.node-engine.call.*.one.*", "mesh.seat.node-engine.tool.*.one"] } + subscribe: { allow: ["$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates", "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement", "$SRV.INFO", "$SRV.INFO.node-engine", "$SRV.INFO.node-engine.>", "$SRV.PING", "$SRV.PING.node-engine", "$SRV.PING.node-engine.>", "$SRV.STATS", "$SRV.STATS.node-engine", "$SRV.STATS.node-engine.>", "_DELIVER.one", "_DELIVER.one.>", "_INBOX.node.one.>", "mesh.mod.mesh-delivery-view.event.settled", "mesh.mod.restic.event.backed-up", "mesh.node.one.ask.access-act", "mesh.node.one.ask.hand-over", "mesh.node.one.ask.judge-directories", "mesh.node.one.ask.move-secret", "mesh.node.one.ask.report", "mesh.node.one.ask.setuid-search", "mesh.node.one.declare", "mesh.seat.node-engine.call.*.one.*", "mesh.seat.node-engine.tool.*.one"] } allow_responses: { max: 1, ttl: "1m" } } } { user: "one.mesh-delivery-watchman", password: "$2a$11$wwwwwwwwwwwwwwwwwwwwww", permissions: { diff --git a/modules/mesh-grants/internal/buscompose/testdata/user-lists.json b/modules/mesh-grants/internal/buscompose/testdata/user-lists.json index 842a9753..9de8d078 100644 --- a/modules/mesh-grants/internal/buscompose/testdata/user-lists.json +++ b/modules/mesh-grants/internal/buscompose/testdata/user-lists.json @@ -8,7 +8,7 @@ }, { "Name": "the controller's someRecords", - "List": "# The mesh's users, composed by the grants part. Do not edit: the next\n# composition overwrites it. Permissions are derived from what each module\n# declares and nothing else (novox/hq ADR 0043, design 29 §2).\n\naccounts {\n MESH {\n jetstream: enabled\n imports = [\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.CONNECT\" }, to: \"mesh.bus.connect\" }\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.DISCONNECT\" }, to: \"mesh.bus.disconnect\" }\n { service: { account: \"$SYS\", subject: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\" }, to: \"mesh.bus.connz\" }\n ]\n users = [\n { user: \"node.one\", password: \"$2a$11$node.onenode.onenode.ofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.one\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.one\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.one.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-one.*.$O.declaration-files-one.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-one.*\", \"$JS.API.CONSUMER.INFO.NODES.one\", \"$JS.API.DIRECT.GET.KV_composed-one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.one\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.one\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.one.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.one.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-one.$O.declaration-files-one.M.*\", \"$JS.API.STREAM.INFO.KV_composed-one\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$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_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-one.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-one.\u003e\", \"$KV.composed-one.\u003e\", \"$KV.mesh-host_acts.one.\u003e\", \"$KV.mesh-host_data.one.\u003e\", \"$KV.mesh-host_health.one\", \"$KV.mesh-host_packages.one\", \"$KV.node-engine_have.one.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.*.one\", \"$SRV.PING.\u003e\", \"mesh.control.one.\u003e\", \"mesh.seat.node-backup.call.backed-up.one.node~one\", \"mesh.seat.node-backup.call.now.one.node~one\", \"mesh.seat.node-backup.tool.backed-up.one\", \"mesh.seat.node-backup.tool.now.one\", \"mesh.seat.node-engine.event.*.one\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.one\", \"_DELIVER.one.\u003e\", \"_INBOX.node.one.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.one.ask.access-act\", \"mesh.node.one.ask.hand-over\", \"mesh.node.one.ask.judge-directories\", \"mesh.node.one.ask.move-secret\", \"mesh.node.one.ask.report\", \"mesh.node.one.ask.setuid-search\", \"mesh.node.one.declare\", \"mesh.seat.node-engine.call.*.one.*\", \"mesh.seat.node-engine.tool.*.one\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.two\", password: \"$2a$11$node.twonode.twonode.tfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.two\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.two\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.two.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-two.*.$O.declaration-files-two.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-two.*\", \"$JS.API.CONSUMER.INFO.NODES.two\", \"$JS.API.DIRECT.GET.KV_composed-two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.two\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.two\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.two.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.two.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-two.$O.declaration-files-two.M.*\", \"$JS.API.STREAM.INFO.KV_composed-two\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$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_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-two.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-two.\u003e\", \"$KV.composed-two.\u003e\", \"$KV.mesh-host_acts.two.\u003e\", \"$KV.mesh-host_data.two.\u003e\", \"$KV.mesh-host_health.two\", \"$KV.mesh-host_packages.two\", \"$KV.node-engine_have.two.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.*.two\", \"$SRV.PING.\u003e\", \"mesh.control.two.\u003e\", \"mesh.seat.node-backup.call.backed-up.two.node~two\", \"mesh.seat.node-backup.call.now.two.node~two\", \"mesh.seat.node-backup.tool.backed-up.two\", \"mesh.seat.node-backup.tool.now.two\", \"mesh.seat.node-engine.event.*.two\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.two\", \"_DELIVER.two.\u003e\", \"_INBOX.node.two.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.two.ask.access-act\", \"mesh.node.two.ask.hand-over\", \"mesh.node.two.ask.judge-directories\", \"mesh.node.two.ask.move-secret\", \"mesh.node.two.ask.report\", \"mesh.node.two.ask.setuid-search\", \"mesh.node.two.declare\", \"mesh.seat.node-engine.call.*.two.*\", \"mesh.seat.node-engine.tool.*.two\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"one.telegram\", password: \"$2a$11$one.telegramone.telegrfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.one_telegram.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.one.telegram\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.one_telegram\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_telegram\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.one.telegram\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.telegram\", \"$SRV.INFO.telegram.\u003e\", \"$SRV.PING\", \"$SRV.PING.telegram\", \"$SRV.PING.telegram.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.telegram\", \"$SRV.STATS.telegram.\u003e\", \"_INBOX.one.telegram.\u003e\", \"mesh.mod.telegram.call.\u003e\", \"mesh.mod.telegram.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"person.ada\", password: \"$2a$11$person.adaperson.adapefixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.\u003e\", \"mesh.mod.mesh-catalog.call.catalog_tools.*.person~ada\", \"mesh.mod.mesh-catalog.call.catalog_tools.person~ada\", \"mesh.mod.mesh-catalog.tool.catalog_tools\", \"mesh.mod.mesh-catalog.tool.catalog_tools.*\"] }\n subscribe: { allow: [\"_INBOX.person.ada.\u003e\"] }\n } }\n { user: \"two.shop\", password: \"$2a$11$two.shoptwo.shoptwo.shfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.two_shop.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.two.shop\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.two_shop\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.two_shop\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.two.shop\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"mesh.mod.shop.event.order.placed\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.shop\", \"$SRV.INFO.shop.\u003e\", \"$SRV.PING\", \"$SRV.PING.shop\", \"$SRV.PING.shop.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.shop\", \"$SRV.STATS.shop.\u003e\", \"_INBOX.two.shop.\u003e\", \"mesh.mod.shop.call.\u003e\", \"mesh.mod.shop.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n ]\n }\n \"$SYS\" {\n exports = [\n { stream: \"$SYS.ACCOUNT.MESH.CONNECT\", accounts: [\"MESH\"] }\n { stream: \"$SYS.ACCOUNT.MESH.DISCONNECT\", accounts: [\"MESH\"] }\n { service: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\", accounts: [\"MESH\"] }\n ]\n }\n}\n", + "List": "# The mesh's users, composed by the grants part. Do not edit: the next\n# composition overwrites it. Permissions are derived from what each module\n# declares and nothing else (novox/hq ADR 0043, design 29 §2).\n\naccounts {\n MESH {\n jetstream: enabled\n imports = [\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.CONNECT\" }, to: \"mesh.bus.connect\" }\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.DISCONNECT\" }, to: \"mesh.bus.disconnect\" }\n { service: { account: \"$SYS\", subject: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\" }, to: \"mesh.bus.connz\" }\n ]\n users = [\n { user: \"node.one\", password: \"$2a$11$node.onenode.onenode.ofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.one\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.one\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.one.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-one.*.$O.declaration-files-one.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-one.*\", \"$JS.API.CONSUMER.INFO.NODES.one\", \"$JS.API.DIRECT.GET.KV_composed-one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.one\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.one\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.one.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.one.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-one.$O.declaration-files-one.M.*\", \"$JS.API.STREAM.INFO.KV_composed-one\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$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_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-one.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-one.\u003e\", \"$KV.composed-one.\u003e\", \"$KV.mesh-host_acts.one.\u003e\", \"$KV.mesh-host_data.one.\u003e\", \"$KV.mesh-host_health.one\", \"$KV.mesh-host_packages.one\", \"$KV.node-engine_have.one.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.*.one\", \"$SRV.PING.\u003e\", \"mesh.control.one.\u003e\", \"mesh.seat.node-backup.call.backed-up.one.node~one\", \"mesh.seat.node-backup.call.now.one.node~one\", \"mesh.seat.node-backup.tool.backed-up.one\", \"mesh.seat.node-backup.tool.now.one\", \"mesh.seat.node-engine.event.*.one\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.one\", \"_DELIVER.one.\u003e\", \"_INBOX.node.one.\u003e\", \"mesh.mod.mesh-delivery-view.event.settled\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.one.ask.access-act\", \"mesh.node.one.ask.hand-over\", \"mesh.node.one.ask.judge-directories\", \"mesh.node.one.ask.move-secret\", \"mesh.node.one.ask.report\", \"mesh.node.one.ask.setuid-search\", \"mesh.node.one.declare\", \"mesh.seat.node-engine.call.*.one.*\", \"mesh.seat.node-engine.tool.*.one\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.two\", password: \"$2a$11$node.twonode.twonode.tfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.two\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.two\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.two.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-two.*.$O.declaration-files-two.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-two.*\", \"$JS.API.CONSUMER.INFO.NODES.two\", \"$JS.API.DIRECT.GET.KV_composed-two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.two\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.two\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.two.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.two.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-two.$O.declaration-files-two.M.*\", \"$JS.API.STREAM.INFO.KV_composed-two\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$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_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-two.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-two.\u003e\", \"$KV.composed-two.\u003e\", \"$KV.mesh-host_acts.two.\u003e\", \"$KV.mesh-host_data.two.\u003e\", \"$KV.mesh-host_health.two\", \"$KV.mesh-host_packages.two\", \"$KV.node-engine_have.two.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.*.two\", \"$SRV.PING.\u003e\", \"mesh.control.two.\u003e\", \"mesh.seat.node-backup.call.backed-up.two.node~two\", \"mesh.seat.node-backup.call.now.two.node~two\", \"mesh.seat.node-backup.tool.backed-up.two\", \"mesh.seat.node-backup.tool.now.two\", \"mesh.seat.node-engine.event.*.two\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.two\", \"_DELIVER.two.\u003e\", \"_INBOX.node.two.\u003e\", \"mesh.mod.mesh-delivery-view.event.settled\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.two.ask.access-act\", \"mesh.node.two.ask.hand-over\", \"mesh.node.two.ask.judge-directories\", \"mesh.node.two.ask.move-secret\", \"mesh.node.two.ask.report\", \"mesh.node.two.ask.setuid-search\", \"mesh.node.two.declare\", \"mesh.seat.node-engine.call.*.two.*\", \"mesh.seat.node-engine.tool.*.two\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"one.telegram\", password: \"$2a$11$one.telegramone.telegrfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.one_telegram.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.one.telegram\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.one_telegram\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_telegram\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.one.telegram\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.telegram\", \"$SRV.INFO.telegram.\u003e\", \"$SRV.PING\", \"$SRV.PING.telegram\", \"$SRV.PING.telegram.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.telegram\", \"$SRV.STATS.telegram.\u003e\", \"_INBOX.one.telegram.\u003e\", \"mesh.mod.telegram.call.\u003e\", \"mesh.mod.telegram.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"person.ada\", password: \"$2a$11$person.adaperson.adapefixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.\u003e\", \"mesh.mod.mesh-catalog.call.catalog_tools.*.person~ada\", \"mesh.mod.mesh-catalog.call.catalog_tools.person~ada\", \"mesh.mod.mesh-catalog.tool.catalog_tools\", \"mesh.mod.mesh-catalog.tool.catalog_tools.*\"] }\n subscribe: { allow: [\"_INBOX.person.ada.\u003e\"] }\n } }\n { user: \"two.shop\", password: \"$2a$11$two.shoptwo.shoptwo.shfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.two_shop.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.two.shop\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.two_shop\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.two_shop\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.two.shop\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"mesh.mod.shop.event.order.placed\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.shop\", \"$SRV.INFO.shop.\u003e\", \"$SRV.PING\", \"$SRV.PING.shop\", \"$SRV.PING.shop.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.shop\", \"$SRV.STATS.shop.\u003e\", \"_INBOX.two.shop.\u003e\", \"mesh.mod.shop.call.\u003e\", \"mesh.mod.shop.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n ]\n }\n \"$SYS\" {\n exports = [\n { stream: \"$SYS.ACCOUNT.MESH.CONNECT\", accounts: [\"MESH\"] }\n { stream: \"$SYS.ACCOUNT.MESH.DISCONNECT\", accounts: [\"MESH\"] }\n { service: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\", accounts: [\"MESH\"] }\n ]\n }\n}\n", "Missing": null, "Err": "", "Memberships": { @@ -46,7 +46,7 @@ }, { "Name": "seats, consumers, people and a hashless user", - "List": "# The mesh's users, composed by the grants part. Do not edit: the next\n# composition overwrites it. Permissions are derived from what each module\n# declares and nothing else (novox/hq ADR 0043, design 29 §2).\n\naccounts {\n MESH {\n jetstream: enabled\n imports = [\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.CONNECT\" }, to: \"mesh.bus.connect\" }\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.DISCONNECT\" }, to: \"mesh.bus.disconnect\" }\n { service: { account: \"$SYS\", subject: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\" }, to: \"mesh.bus.connz\" }\n ]\n users = [\n { user: \"node.one\", password: \"$2a$11$node.onenode.onenode.ofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_user-list.*.$KV.mesh-grants_user-list.sealed\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.one\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.one\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.one.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-one.*.$O.declaration-files-one.C.*\", \"$JS.API.CONSUMER.CREATE.OBJ_mesh-grants-user-list.*.$O.mesh-grants-user-list.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_user-list.*\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-one.*\", \"$JS.API.CONSUMER.DELETE.OBJ_mesh-grants-user-list.*\", \"$JS.API.CONSUMER.INFO.NODES.one\", \"$JS.API.DIRECT.GET.KV_composed-one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-grants_user-list.$KV.mesh-grants_user-list.sealed\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.one\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.one\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.one.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.one.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-one.$O.declaration-files-one.M.*\", \"$JS.API.DIRECT.GET.OBJ_mesh-grants-user-list.$O.mesh-grants-user-list.M.*\", \"$JS.API.STREAM.INFO.KV_composed-one\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$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_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-grants_user-list\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-one.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-grants_user-list.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-one.\u003e\", \"$JS.FC.OBJ_mesh-grants-user-list.\u003e\", \"$KV.composed-one.\u003e\", \"$KV.mesh-host_acts.one.\u003e\", \"$KV.mesh-host_data.one.\u003e\", \"$KV.mesh-host_health.one\", \"$KV.mesh-host_packages.one\", \"$KV.node-engine_have.one.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.*.one\", \"$SRV.PING.\u003e\", \"mesh.control.one.\u003e\", \"mesh.seat.node-backup.call.backed-up.one.node~one\", \"mesh.seat.node-backup.call.now.one.node~one\", \"mesh.seat.node-backup.tool.backed-up.one\", \"mesh.seat.node-backup.tool.now.one\", \"mesh.seat.node-engine.event.*.one\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.one\", \"_DELIVER.one.\u003e\", \"_INBOX.node.one.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.one.ask.access-act\", \"mesh.node.one.ask.hand-over\", \"mesh.node.one.ask.judge-directories\", \"mesh.node.one.ask.move-secret\", \"mesh.node.one.ask.report\", \"mesh.node.one.ask.setuid-search\", \"mesh.node.one.declare\", \"mesh.seat.node-engine.call.*.one.*\", \"mesh.seat.node-engine.tool.*.one\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.two\", password: \"$2a$11$node.twonode.twonode.tfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.two\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.two\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.two.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-two.*.$O.declaration-files-two.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-two.*\", \"$JS.API.CONSUMER.INFO.NODES.two\", \"$JS.API.DIRECT.GET.KV_composed-two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.two\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.two\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.two.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.two.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-two.$O.declaration-files-two.M.*\", \"$JS.API.STREAM.INFO.KV_composed-two\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$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_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-two.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-two.\u003e\", \"$KV.composed-two.\u003e\", \"$KV.mesh-host_acts.two.\u003e\", \"$KV.mesh-host_data.two.\u003e\", \"$KV.mesh-host_health.two\", \"$KV.mesh-host_packages.two\", \"$KV.node-engine_have.two.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.*.two\", \"$SRV.PING.\u003e\", \"mesh.control.two.\u003e\", \"mesh.seat.node-backup.call.backed-up.two.node~two\", \"mesh.seat.node-backup.call.now.two.node~two\", \"mesh.seat.node-backup.tool.backed-up.two\", \"mesh.seat.node-backup.tool.now.two\", \"mesh.seat.node-engine.event.*.two\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.two\", \"_DELIVER.two.\u003e\", \"_INBOX.node.two.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.two.ask.access-act\", \"mesh.node.two.ask.hand-over\", \"mesh.node.two.ask.judge-directories\", \"mesh.node.two.ask.move-secret\", \"mesh.node.two.ask.report\", \"mesh.node.two.ask.setuid-search\", \"mesh.node.two.declare\", \"mesh.seat.node-engine.call.*.two.*\", \"mesh.seat.node-engine.tool.*.two\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"one.mesh-grants\", password: \"$2a$11$one.mesh-grantsone.mesfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.one_mesh-grants.\u003e\", \"$JS.API.CONSUMER.CREATE.EVENTS.two_audit\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.one.mesh-grants\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.one_mesh-grants\", \"$JS.API.CONSUMER.INFO.EVENTS.two_audit\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_mesh-grants\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.*.*\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.one.mesh-grants\", \"$JS.API.DIRECT.GET.OBJ_mesh-grants-user-list.\u003e\", \"$JS.API.STREAM.CREATE.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.API.STREAM.INFO.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.MSG.GET.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.PURGE.OBJ_mesh-grants-user-list\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"$O.mesh-grants-user-list.\u003e\", \"mesh.assignment.*.*\", \"mesh.mod.mesh-grants.event.membership-changed\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.mesh-grants\", \"$SRV.INFO.mesh-grants.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-grants\", \"$SRV.PING.mesh-grants.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-grants\", \"$SRV.STATS.mesh-grants.\u003e\", \"_INBOX.one.mesh-grants.\u003e\", \"mesh.mod.mesh-grants.call.\u003e\", \"mesh.mod.mesh-grants.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"one.mesh-watchman\", password: \"$2a$11$one.mesh-watchmanone.mfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.one_mesh-watchman.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.one.mesh-watchman\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.one_mesh-watchman\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_mesh-watchman\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.one.mesh-watchman\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.mesh-watchman\", \"$SRV.INFO.mesh-watchman.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-watchman\", \"$SRV.PING.mesh-watchman.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-watchman\", \"$SRV.STATS.mesh-watchman.\u003e\", \"_INBOX.one.mesh-watchman.\u003e\", \"mesh.mod.mesh-watchman.call.\u003e\", \"mesh.mod.mesh-watchman.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"one.nats\", password: \"$2a$11$one.natsone.natsone.nafixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.API.STREAM.INFO.*\", \"$JS.API.STREAM.NAMES\", \"$JS.API.STREAM.SNAPSHOT.*\", \"$JS.SNAPSHOT.ACK.\u003e\"] }\n subscribe: { allow: [\"_INBOX.one.nats.\u003e\"] }\n } }\n { user: \"one.telegram\", password: \"$2a$11$one.telegramone.telegrfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.one_telegram.\u003e\", \"$JS.ACK.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.one.telegram\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.one_telegram\", \"$JS.API.CONSUMER.INFO.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_telegram\", \"$JS.API.CONSUMER.MSG.NEXT.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.one.telegram\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"mesh.seat.telegram-sender.event.delivered\", \"mesh.seat.telegram-sender.event.failed\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.telegram\", \"$SRV.INFO.telegram.\u003e\", \"$SRV.PING\", \"$SRV.PING.telegram\", \"$SRV.PING.telegram.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.telegram\", \"$SRV.STATS.telegram.\u003e\", \"_INBOX.one.telegram.\u003e\", \"mesh.mod.telegram.call.\u003e\", \"mesh.mod.telegram.tool.\u003e\", \"mesh.seat.telegram-sender.accept.send\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"person.ada\", password: \"$2a$11$person.adaperson.adapefixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.\u003e\", \"mesh.mod.*.call.*.*.person~ada\", \"mesh.mod.*.call.*.person~ada\", \"mesh.mod.*.tool.\u003e\", \"mesh.seat.*.call.*.*.person~ada\", \"mesh.seat.*.call.*.person~ada\", \"mesh.seat.*.tool.\u003e\"], deny: [\"mesh.seat.node-launcher.tool.secret\", \"mesh.seat.node-launcher.tool.secret.*\", \"mesh.mod.*.tool.node-launcher.secret\", \"mesh.mod.*.tool.node-launcher.secret.*\", \"mesh.seat.node-launcher.call.secret.\u003e\", \"mesh.mod.*.call.node-launcher.secret.\u003e\"] }\n subscribe: { allow: [\"_INBOX.person.ada.\u003e\"] }\n } }\n { user: \"person.bob\", password: \"$2a$11$person.bobperson.bobpefixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.\u003e\", \"mesh.mod.shop.call.status.*.person~bob\", \"mesh.mod.shop.call.status.person~bob\", \"mesh.mod.shop.tool.status\", \"mesh.mod.shop.tool.status.*\"] }\n subscribe: { allow: [\"_INBOX.person.bob.\u003e\"] }\n } }\n { user: \"two.shop\", password: \"$2a$11$two.shoptwo.shoptwo.shfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.two_shop.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.two.shop\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.two_shop\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.two_shop\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.two.shop\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"mesh.mod.shop.event.order.placed\", \"mesh.seat.telegram-sender.accept.send\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.shop\", \"$SRV.INFO.shop.\u003e\", \"$SRV.PING\", \"$SRV.PING.shop\", \"$SRV.PING.shop.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.shop\", \"$SRV.STATS.shop.\u003e\", \"_INBOX.two.shop.\u003e\", \"mesh.mod.shop.call.\u003e\", \"mesh.mod.shop.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n ]\n }\n \"$SYS\" {\n exports = [\n { stream: \"$SYS.ACCOUNT.MESH.CONNECT\", accounts: [\"MESH\"] }\n { stream: \"$SYS.ACCOUNT.MESH.DISCONNECT\", accounts: [\"MESH\"] }\n { service: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\", accounts: [\"MESH\"] }\n ]\n }\n}\n", + "List": "# The mesh's users, composed by the grants part. Do not edit: the next\n# composition overwrites it. Permissions are derived from what each module\n# declares and nothing else (novox/hq ADR 0043, design 29 §2).\n\naccounts {\n MESH {\n jetstream: enabled\n imports = [\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.CONNECT\" }, to: \"mesh.bus.connect\" }\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.DISCONNECT\" }, to: \"mesh.bus.disconnect\" }\n { service: { account: \"$SYS\", subject: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\" }, to: \"mesh.bus.connz\" }\n ]\n users = [\n { user: \"node.one\", password: \"$2a$11$node.onenode.onenode.ofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_user-list.*.$KV.mesh-grants_user-list.sealed\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.one\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.one\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.one.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-one.*.$O.declaration-files-one.C.*\", \"$JS.API.CONSUMER.CREATE.OBJ_mesh-grants-user-list.*.$O.mesh-grants-user-list.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_user-list.*\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-one.*\", \"$JS.API.CONSUMER.DELETE.OBJ_mesh-grants-user-list.*\", \"$JS.API.CONSUMER.INFO.NODES.one\", \"$JS.API.DIRECT.GET.KV_composed-one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-grants_user-list.$KV.mesh-grants_user-list.sealed\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.one\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.one\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.one.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.one.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-one.$O.declaration-files-one.M.*\", \"$JS.API.DIRECT.GET.OBJ_mesh-grants-user-list.$O.mesh-grants-user-list.M.*\", \"$JS.API.STREAM.INFO.KV_composed-one\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$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_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-grants_user-list\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-one.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-grants_user-list.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-one.\u003e\", \"$JS.FC.OBJ_mesh-grants-user-list.\u003e\", \"$KV.composed-one.\u003e\", \"$KV.mesh-host_acts.one.\u003e\", \"$KV.mesh-host_data.one.\u003e\", \"$KV.mesh-host_health.one\", \"$KV.mesh-host_packages.one\", \"$KV.node-engine_have.one.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.*.one\", \"$SRV.PING.\u003e\", \"mesh.control.one.\u003e\", \"mesh.seat.node-backup.call.backed-up.one.node~one\", \"mesh.seat.node-backup.call.now.one.node~one\", \"mesh.seat.node-backup.tool.backed-up.one\", \"mesh.seat.node-backup.tool.now.one\", \"mesh.seat.node-engine.event.*.one\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.one\", \"_DELIVER.one.\u003e\", \"_INBOX.node.one.\u003e\", \"mesh.mod.mesh-delivery-view.event.settled\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.one.ask.access-act\", \"mesh.node.one.ask.hand-over\", \"mesh.node.one.ask.judge-directories\", \"mesh.node.one.ask.move-secret\", \"mesh.node.one.ask.report\", \"mesh.node.one.ask.setuid-search\", \"mesh.node.one.declare\", \"mesh.seat.node-engine.call.*.one.*\", \"mesh.seat.node-engine.tool.*.one\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.two\", password: \"$2a$11$node.twonode.twonode.tfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.two\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.two\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.two.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-two.*.$O.declaration-files-two.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-two.*\", \"$JS.API.CONSUMER.INFO.NODES.two\", \"$JS.API.DIRECT.GET.KV_composed-two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.two\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.two\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.two.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.two.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-two.$O.declaration-files-two.M.*\", \"$JS.API.STREAM.INFO.KV_composed-two\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$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_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-two.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-two.\u003e\", \"$KV.composed-two.\u003e\", \"$KV.mesh-host_acts.two.\u003e\", \"$KV.mesh-host_data.two.\u003e\", \"$KV.mesh-host_health.two\", \"$KV.mesh-host_packages.two\", \"$KV.node-engine_have.two.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.*.two\", \"$SRV.PING.\u003e\", \"mesh.control.two.\u003e\", \"mesh.seat.node-backup.call.backed-up.two.node~two\", \"mesh.seat.node-backup.call.now.two.node~two\", \"mesh.seat.node-backup.tool.backed-up.two\", \"mesh.seat.node-backup.tool.now.two\", \"mesh.seat.node-engine.event.*.two\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.two\", \"_DELIVER.two.\u003e\", \"_INBOX.node.two.\u003e\", \"mesh.mod.mesh-delivery-view.event.settled\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.two.ask.access-act\", \"mesh.node.two.ask.hand-over\", \"mesh.node.two.ask.judge-directories\", \"mesh.node.two.ask.move-secret\", \"mesh.node.two.ask.report\", \"mesh.node.two.ask.setuid-search\", \"mesh.node.two.declare\", \"mesh.seat.node-engine.call.*.two.*\", \"mesh.seat.node-engine.tool.*.two\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"one.mesh-grants\", password: \"$2a$11$one.mesh-grantsone.mesfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.one_mesh-grants.\u003e\", \"$JS.API.CONSUMER.CREATE.EVENTS.two_audit\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.one.mesh-grants\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.one_mesh-grants\", \"$JS.API.CONSUMER.INFO.EVENTS.two_audit\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_mesh-grants\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.*.*\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.one.mesh-grants\", \"$JS.API.DIRECT.GET.OBJ_mesh-grants-user-list.\u003e\", \"$JS.API.STREAM.CREATE.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.API.STREAM.INFO.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.MSG.GET.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.PURGE.OBJ_mesh-grants-user-list\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"$O.mesh-grants-user-list.\u003e\", \"mesh.assignment.*.*\", \"mesh.mod.mesh-grants.event.membership-changed\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.mesh-grants\", \"$SRV.INFO.mesh-grants.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-grants\", \"$SRV.PING.mesh-grants.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-grants\", \"$SRV.STATS.mesh-grants.\u003e\", \"_INBOX.one.mesh-grants.\u003e\", \"mesh.mod.mesh-grants.call.\u003e\", \"mesh.mod.mesh-grants.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"one.mesh-watchman\", password: \"$2a$11$one.mesh-watchmanone.mfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.one_mesh-watchman.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.one.mesh-watchman\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.one_mesh-watchman\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_mesh-watchman\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.one.mesh-watchman\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.mesh-watchman\", \"$SRV.INFO.mesh-watchman.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-watchman\", \"$SRV.PING.mesh-watchman.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-watchman\", \"$SRV.STATS.mesh-watchman.\u003e\", \"_INBOX.one.mesh-watchman.\u003e\", \"mesh.mod.mesh-watchman.call.\u003e\", \"mesh.mod.mesh-watchman.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"one.nats\", password: \"$2a$11$one.natsone.natsone.nafixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.API.STREAM.INFO.*\", \"$JS.API.STREAM.NAMES\", \"$JS.API.STREAM.SNAPSHOT.*\", \"$JS.SNAPSHOT.ACK.\u003e\"] }\n subscribe: { allow: [\"_INBOX.one.nats.\u003e\"] }\n } }\n { user: \"one.telegram\", password: \"$2a$11$one.telegramone.telegrfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.one_telegram.\u003e\", \"$JS.ACK.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.one.telegram\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.one_telegram\", \"$JS.API.CONSUMER.INFO.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_telegram\", \"$JS.API.CONSUMER.MSG.NEXT.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.one.telegram\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"mesh.seat.telegram-sender.event.delivered\", \"mesh.seat.telegram-sender.event.failed\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.telegram\", \"$SRV.INFO.telegram.\u003e\", \"$SRV.PING\", \"$SRV.PING.telegram\", \"$SRV.PING.telegram.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.telegram\", \"$SRV.STATS.telegram.\u003e\", \"_INBOX.one.telegram.\u003e\", \"mesh.mod.telegram.call.\u003e\", \"mesh.mod.telegram.tool.\u003e\", \"mesh.seat.telegram-sender.accept.send\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"person.ada\", password: \"$2a$11$person.adaperson.adapefixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.\u003e\", \"mesh.mod.*.call.*.*.person~ada\", \"mesh.mod.*.call.*.person~ada\", \"mesh.mod.*.tool.\u003e\", \"mesh.seat.*.call.*.*.person~ada\", \"mesh.seat.*.call.*.person~ada\", \"mesh.seat.*.tool.\u003e\"], deny: [\"mesh.seat.node-launcher.tool.secret\", \"mesh.seat.node-launcher.tool.secret.*\", \"mesh.mod.*.tool.node-launcher.secret\", \"mesh.mod.*.tool.node-launcher.secret.*\", \"mesh.seat.node-launcher.call.secret.\u003e\", \"mesh.mod.*.call.node-launcher.secret.\u003e\", \"mesh.seat.mesh-broker.call.apply.\u003e\", \"mesh.seat.mesh-broker.call.create.\u003e\", \"mesh.seat.mesh-broker.call.drop.\u003e\", \"mesh.seat.mesh-broker.call.recreate.\u003e\", \"mesh.seat.mesh-broker.tool.apply\", \"mesh.seat.mesh-broker.tool.create\", \"mesh.seat.mesh-broker.tool.drop\", \"mesh.seat.mesh-broker.tool.recreate\"] }\n subscribe: { allow: [\"_INBOX.person.ada.\u003e\"] }\n } }\n { user: \"person.bob\", password: \"$2a$11$person.bobperson.bobpefixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.\u003e\", \"mesh.mod.shop.call.status.*.person~bob\", \"mesh.mod.shop.call.status.person~bob\", \"mesh.mod.shop.tool.status\", \"mesh.mod.shop.tool.status.*\"] }\n subscribe: { allow: [\"_INBOX.person.bob.\u003e\"] }\n } }\n { user: \"two.shop\", password: \"$2a$11$two.shoptwo.shoptwo.shfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.two_shop.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.two.shop\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.two_shop\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.two_shop\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.two.shop\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"mesh.mod.shop.event.order.placed\", \"mesh.seat.telegram-sender.accept.send\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.shop\", \"$SRV.INFO.shop.\u003e\", \"$SRV.PING\", \"$SRV.PING.shop\", \"$SRV.PING.shop.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.shop\", \"$SRV.STATS.shop.\u003e\", \"_INBOX.two.shop.\u003e\", \"mesh.mod.shop.call.\u003e\", \"mesh.mod.shop.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n ]\n }\n \"$SYS\" {\n exports = [\n { stream: \"$SYS.ACCOUNT.MESH.CONNECT\", accounts: [\"MESH\"] }\n { stream: \"$SYS.ACCOUNT.MESH.DISCONNECT\", accounts: [\"MESH\"] }\n { service: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\", accounts: [\"MESH\"] }\n ]\n }\n}\n", "Missing": [ "two.audit" ], @@ -152,7 +152,7 @@ }, { "Name": "state, logs, streams, queues and processes", - "List": "# The mesh's users, composed by the grants part. Do not edit: the next\n# composition overwrites it. Permissions are derived from what each module\n# declares and nothing else (novox/hq ADR 0043, design 29 §2).\n\naccounts {\n MESH {\n jetstream: enabled\n imports = [\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.CONNECT\" }, to: \"mesh.bus.connect\" }\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.DISCONNECT\" }, to: \"mesh.bus.disconnect\" }\n { service: { account: \"$SYS\", subject: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\" }, to: \"mesh.bus.connz\" }\n ]\n users = [\n { user: \"ace.plex\", password: \"$2a$11$ace.plexace.plexace.plfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.ace_plex.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.ace.plex\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.ace_plex\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.ace_plex\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.ace.plex\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.plex\", \"$SRV.INFO.plex.\u003e\", \"$SRV.PING\", \"$SRV.PING.plex\", \"$SRV.PING.plex.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.plex\", \"$SRV.STATS.plex.\u003e\", \"_INBOX.ace.plex.\u003e\", \"mesh.mod.plex.call.\u003e\", \"mesh.mod.plex.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.ace\", password: \"$2a$11$node.acenode.acenode.afixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.ace\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.ace\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-ace.*.$O.declaration-files-ace.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-ace.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-ace.*\", \"$JS.API.CONSUMER.INFO.NODES.ace\", \"$JS.API.DIRECT.GET.KV_composed-ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.ace\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.ace\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.ace.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-ace.$O.declaration-files-ace.M.*\", \"$JS.API.STREAM.INFO.KV_composed-ace\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$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_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-ace.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-ace.\u003e\", \"$KV.composed-ace.\u003e\", \"$KV.mesh-host_acts.ace.\u003e\", \"$KV.mesh-host_data.ace.\u003e\", \"$KV.mesh-host_health.ace\", \"$KV.mesh-host_packages.ace\", \"$KV.node-engine_have.ace.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.*.ace\", \"$SRV.PING.\u003e\", \"mesh.control.ace.\u003e\", \"mesh.mod.plex.call.plex_status.ace.node~ace\", \"mesh.mod.plex.tool.plex_status.ace\", \"mesh.seat.node-backup.call.backed-up.ace.node~ace\", \"mesh.seat.node-backup.call.now.ace.node~ace\", \"mesh.seat.node-backup.tool.backed-up.ace\", \"mesh.seat.node-backup.tool.now.ace\", \"mesh.seat.node-engine.event.*.ace\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.ace\", \"_DELIVER.ace.\u003e\", \"_INBOX.node.ace.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.ace.ask.access-act\", \"mesh.node.ace.ask.hand-over\", \"mesh.node.ace.ask.judge-directories\", \"mesh.node.ace.ask.move-secret\", \"mesh.node.ace.ask.report\", \"mesh.node.ace.ask.setuid-search\", \"mesh.node.ace.declare\", \"mesh.seat.node-engine.call.*.ace.*\", \"mesh.seat.node-engine.tool.*.ace\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.novox\", password: \"$2a$11$node.novoxnode.novoxnofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.novox\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.novox\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-novox.*.$O.declaration-files-novox.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-novox.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-novox.*\", \"$JS.API.CONSUMER.INFO.NODES.novox\", \"$JS.API.DIRECT.GET.KV_composed-novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.novox\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.novox\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.novox.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-novox.$O.declaration-files-novox.M.*\", \"$JS.API.STREAM.INFO.KV_composed-novox\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$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_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-novox.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-novox.\u003e\", \"$KV.composed-novox.\u003e\", \"$KV.mesh-host_acts.novox.\u003e\", \"$KV.mesh-host_data.novox.\u003e\", \"$KV.mesh-host_health.novox\", \"$KV.mesh-host_packages.novox\", \"$KV.node-engine_have.novox.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.*.novox\", \"$SRV.PING.\u003e\", \"mesh.control.novox.\u003e\", \"mesh.seat.node-backup.call.backed-up.novox.node~novox\", \"mesh.seat.node-backup.call.now.novox.node~novox\", \"mesh.seat.node-backup.tool.backed-up.novox\", \"mesh.seat.node-backup.tool.now.novox\", \"mesh.seat.node-engine.event.*.novox\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.novox\", \"_DELIVER.novox.\u003e\", \"_INBOX.node.novox.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.novox.ask.access-act\", \"mesh.node.novox.ask.hand-over\", \"mesh.node.novox.ask.judge-directories\", \"mesh.node.novox.ask.move-secret\", \"mesh.node.novox.ask.report\", \"mesh.node.novox.ask.setuid-search\", \"mesh.node.novox.declare\", \"mesh.seat.node-engine.call.*.novox.*\", \"mesh.seat.node-engine.tool.*.novox\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"novox.mesh-delivery-builder\", password: \"$2a$11$novox.mesh-delivery-bufixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.novox_mesh-delivery-builder.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_presence.*.$KV.mesh-delivery-builder_presence.novox\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.novox.mesh-delivery-builder\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.novox_mesh-delivery-builder\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.novox_mesh-delivery-builder\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_presence.$KV.mesh-delivery-builder_presence.novox\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.novox.mesh-delivery-builder\", \"$JS.API.DIRECT.GET.LOG_mesh-delivery-builder_builds\", \"$JS.API.DIRECT.GET.LOG_mesh-delivery-builder_builds.\u003e\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_presence\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.API.STREAM.INFO.LOG_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_GO\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_IMAGE\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_NODE\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_GO\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_IMAGE\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_NODE\", \"$JS.FC.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"$KV.mesh-delivery-builder_presence.novox\", \"$KV.mesh-delivery-builder_steps.\u003e\", \"mesh.log.mesh-delivery-builder.builds.\u003e\", \"mesh.mod.gitea.call.gitea_commit.*.novox~mesh-delivery-builder\", \"mesh.mod.gitea.call.gitea_commit.novox~mesh-delivery-builder\", \"mesh.mod.gitea.tool.gitea_commit\", \"mesh.mod.gitea.tool.gitea_commit.*\", \"mesh.mod.mesh-delivery-builder.event.built\", \"mesh.mod.mesh-delivery-builder.event.states\", \"mesh.stream.mesh-delivery-builder.event.ask\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_presence\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_steps\", \"$SRV.INFO\", \"$SRV.INFO.mesh-delivery-builder\", \"$SRV.INFO.mesh-delivery-builder.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-delivery-builder\", \"$SRV.PING.mesh-delivery-builder.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-delivery-builder\", \"$SRV.STATS.mesh-delivery-builder.\u003e\", \"_INBOX.novox.mesh-delivery-builder.\u003e\", \"mesh.mod.mesh-delivery-builder.call.\u003e\", \"mesh.mod.mesh-delivery-builder.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"novox.mesh-delivery-builder.worker\", password: \"$2a$11$novox.mesh-delivery-bufixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.asks.novox_mesh-delivery-builder_worker.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.INFO.asks.novox_mesh-delivery-builder_worker\", \"$JS.API.CONSUMER.MSG.NEXT.asks.novox_mesh-delivery-builder_worker\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps\", \"$JS.FC.KV_mesh-delivery-builder_steps.\u003e\", \"$KV.mesh-delivery-builder_steps.\u003e\", \"mesh.mod.gitea.call.gitea_commit.*.novox~mesh-delivery-builder~worker\", \"mesh.mod.gitea.call.gitea_commit.novox~mesh-delivery-builder~worker\", \"mesh.mod.gitea.tool.gitea_commit\", \"mesh.mod.gitea.tool.gitea_commit.*\", \"mesh.mod.mesh-delivery-builder.event.built\", \"mesh.mod.mesh-delivery-builder.event.states\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_steps\", \"_INBOX.novox.mesh-delivery-builder.worker.\u003e\"] }\n } }\n { user: \"novox.mesh-delivery-view\", password: \"$2a$11$novox.mesh-delivery-vifixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.novox_mesh-delivery-view.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_presence.*.$KV.mesh-delivery-builder_presence.novox\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.novox.mesh-delivery-view\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.novox_mesh-delivery-view\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_GO.SEAT_NODE_BUILD_GO_CHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_GO.SEAT_NODE_BUILD_GO_DELIVERY_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_GO.SEAT_NODE_BUILD_GO_RECHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_IMAGE.SEAT_NODE_BUILD_IMAGE_CHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_IMAGE.SEAT_NODE_BUILD_IMAGE_DELIVERY_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_IMAGE.SEAT_NODE_BUILD_IMAGE_RECHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_NODE.SEAT_NODE_BUILD_NODE_CHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_NODE.SEAT_NODE_BUILD_NODE_DELIVERY_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_NODE.SEAT_NODE_BUILD_NODE_RECHECK_worker\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.novox_mesh-delivery-view\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_presence.$KV.mesh-delivery-builder_presence.novox\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.novox.mesh-delivery-view\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_presence\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_GO\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_IMAGE\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_NODE\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_GO\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_IMAGE\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_NODE\", \"$JS.FC.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_presence\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_steps\", \"$SRV.INFO\", \"$SRV.INFO.mesh-delivery-view\", \"$SRV.INFO.mesh-delivery-view.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-delivery-view\", \"$SRV.PING.mesh-delivery-view.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-delivery-view\", \"$SRV.STATS.mesh-delivery-view.\u003e\", \"_INBOX.novox.mesh-delivery-view.\u003e\", \"mesh.mod.mesh-delivery-view.call.\u003e\", \"mesh.mod.mesh-delivery-view.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"novox.mesh-grants\", password: \"$2a$11$novox.mesh-grantsnovoxfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.novox_mesh-grants.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_assertions.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.novox.mesh-grants\", \"$JS.API.CONSUMER.CREATE.asks.novox_mesh-delivery-builder_worker\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_assertions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.novox_mesh-grants\", \"$JS.API.CONSUMER.INFO.asks.novox_mesh-delivery-builder_worker\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.novox_mesh-grants\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.*.*\", \"$JS.API.DIRECT.GET.KV_mesh-grants_assertions.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.novox.mesh-grants\", \"$JS.API.DIRECT.GET.OBJ_mesh-grants-user-list.\u003e\", \"$JS.API.STREAM.CREATE.KV_mesh-delivery-builder_presence\", \"$JS.API.STREAM.CREATE.KV_mesh-delivery-builder_steps\", \"$JS.API.STREAM.CREATE.KV_mesh-grants_assertions\", \"$JS.API.STREAM.CREATE.LOG_mesh-delivery-builder_builds\", \"$JS.API.STREAM.CREATE.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.CREATE.asks\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_presence\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps\", \"$JS.API.STREAM.INFO.KV_mesh-grants_assertions\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.API.STREAM.INFO.LOG_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.INFO.asks\", \"$JS.API.STREAM.MSG.GET.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.PURGE.OBJ_mesh-grants-user-list\", \"$JS.FC.KV_mesh-grants_assertions.\u003e\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"$KV.mesh-grants_assertions.\u003e\", \"$O.mesh-grants-user-list.\u003e\", \"mesh.assignment.*.*\", \"mesh.mod.mesh-grants.event.membership-changed\", \"mesh.mod.mesh-grants.event.states\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-grants_assertions\", \"$SRV.INFO\", \"$SRV.INFO.mesh-grants\", \"$SRV.INFO.mesh-grants.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-grants\", \"$SRV.PING.mesh-grants.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-grants\", \"$SRV.STATS.mesh-grants.\u003e\", \"_INBOX.novox.mesh-grants.\u003e\", \"mesh.mod.mesh-grants.call.\u003e\", \"mesh.mod.mesh-grants.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n ]\n }\n \"$SYS\" {\n exports = [\n { stream: \"$SYS.ACCOUNT.MESH.CONNECT\", accounts: [\"MESH\"] }\n { stream: \"$SYS.ACCOUNT.MESH.DISCONNECT\", accounts: [\"MESH\"] }\n { service: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\", accounts: [\"MESH\"] }\n ]\n }\n}\n", + "List": "# The mesh's users, composed by the grants part. Do not edit: the next\n# composition overwrites it. Permissions are derived from what each module\n# declares and nothing else (novox/hq ADR 0043, design 29 §2).\n\naccounts {\n MESH {\n jetstream: enabled\n imports = [\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.CONNECT\" }, to: \"mesh.bus.connect\" }\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.DISCONNECT\" }, to: \"mesh.bus.disconnect\" }\n { service: { account: \"$SYS\", subject: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\" }, to: \"mesh.bus.connz\" }\n ]\n users = [\n { user: \"ace.plex\", password: \"$2a$11$ace.plexace.plexace.plfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.ace_plex.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.ace.plex\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.ace_plex\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.ace_plex\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.ace.plex\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.plex\", \"$SRV.INFO.plex.\u003e\", \"$SRV.PING\", \"$SRV.PING.plex\", \"$SRV.PING.plex.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.plex\", \"$SRV.STATS.plex.\u003e\", \"_INBOX.ace.plex.\u003e\", \"mesh.mod.plex.call.\u003e\", \"mesh.mod.plex.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.ace\", password: \"$2a$11$node.acenode.acenode.afixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.ace\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.ace\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-ace.*.$O.declaration-files-ace.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-ace.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-ace.*\", \"$JS.API.CONSUMER.INFO.NODES.ace\", \"$JS.API.DIRECT.GET.KV_composed-ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.ace\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.ace\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.ace.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-ace.$O.declaration-files-ace.M.*\", \"$JS.API.STREAM.INFO.KV_composed-ace\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$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_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-ace.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-ace.\u003e\", \"$KV.composed-ace.\u003e\", \"$KV.mesh-host_acts.ace.\u003e\", \"$KV.mesh-host_data.ace.\u003e\", \"$KV.mesh-host_health.ace\", \"$KV.mesh-host_packages.ace\", \"$KV.node-engine_have.ace.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.*.ace\", \"$SRV.PING.\u003e\", \"mesh.control.ace.\u003e\", \"mesh.mod.plex.call.plex_status.ace.node~ace\", \"mesh.mod.plex.tool.plex_status.ace\", \"mesh.seat.node-backup.call.backed-up.ace.node~ace\", \"mesh.seat.node-backup.call.now.ace.node~ace\", \"mesh.seat.node-backup.tool.backed-up.ace\", \"mesh.seat.node-backup.tool.now.ace\", \"mesh.seat.node-engine.event.*.ace\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.ace\", \"_DELIVER.ace.\u003e\", \"_INBOX.node.ace.\u003e\", \"mesh.mod.mesh-delivery-view.event.settled\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.ace.ask.access-act\", \"mesh.node.ace.ask.hand-over\", \"mesh.node.ace.ask.judge-directories\", \"mesh.node.ace.ask.move-secret\", \"mesh.node.ace.ask.report\", \"mesh.node.ace.ask.setuid-search\", \"mesh.node.ace.declare\", \"mesh.seat.node-engine.call.*.ace.*\", \"mesh.seat.node-engine.tool.*.ace\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.novox\", password: \"$2a$11$node.novoxnode.novoxnofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.novox\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.novox\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-novox.*.$O.declaration-files-novox.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-novox.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-novox.*\", \"$JS.API.CONSUMER.INFO.NODES.novox\", \"$JS.API.DIRECT.GET.KV_composed-novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.novox\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.novox\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.novox.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-novox.$O.declaration-files-novox.M.*\", \"$JS.API.STREAM.INFO.KV_composed-novox\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$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_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-novox.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-novox.\u003e\", \"$KV.composed-novox.\u003e\", \"$KV.mesh-host_acts.novox.\u003e\", \"$KV.mesh-host_data.novox.\u003e\", \"$KV.mesh-host_health.novox\", \"$KV.mesh-host_packages.novox\", \"$KV.node-engine_have.novox.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.*.novox\", \"$SRV.PING.\u003e\", \"mesh.control.novox.\u003e\", \"mesh.seat.node-backup.call.backed-up.novox.node~novox\", \"mesh.seat.node-backup.call.now.novox.node~novox\", \"mesh.seat.node-backup.tool.backed-up.novox\", \"mesh.seat.node-backup.tool.now.novox\", \"mesh.seat.node-engine.event.*.novox\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.novox\", \"_DELIVER.novox.\u003e\", \"_INBOX.node.novox.\u003e\", \"mesh.mod.mesh-delivery-view.event.settled\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.novox.ask.access-act\", \"mesh.node.novox.ask.hand-over\", \"mesh.node.novox.ask.judge-directories\", \"mesh.node.novox.ask.move-secret\", \"mesh.node.novox.ask.report\", \"mesh.node.novox.ask.setuid-search\", \"mesh.node.novox.declare\", \"mesh.seat.node-engine.call.*.novox.*\", \"mesh.seat.node-engine.tool.*.novox\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"novox.mesh-delivery-builder\", password: \"$2a$11$novox.mesh-delivery-bufixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.novox_mesh-delivery-builder.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_presence.*.$KV.mesh-delivery-builder_presence.novox\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.novox.mesh-delivery-builder\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.novox_mesh-delivery-builder\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.novox_mesh-delivery-builder\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_presence.$KV.mesh-delivery-builder_presence.novox\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.novox.mesh-delivery-builder\", \"$JS.API.DIRECT.GET.LOG_mesh-delivery-builder_builds\", \"$JS.API.DIRECT.GET.LOG_mesh-delivery-builder_builds.\u003e\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_presence\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.API.STREAM.INFO.LOG_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_GO\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_IMAGE\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_NODE\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_GO\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_IMAGE\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_NODE\", \"$JS.FC.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"$KV.mesh-delivery-builder_presence.novox\", \"$KV.mesh-delivery-builder_steps.\u003e\", \"mesh.log.mesh-delivery-builder.builds.\u003e\", \"mesh.mod.gitea.call.gitea_commit.*.novox~mesh-delivery-builder\", \"mesh.mod.gitea.call.gitea_commit.novox~mesh-delivery-builder\", \"mesh.mod.gitea.tool.gitea_commit\", \"mesh.mod.gitea.tool.gitea_commit.*\", \"mesh.mod.mesh-delivery-builder.event.built\", \"mesh.mod.mesh-delivery-builder.event.states\", \"mesh.stream.mesh-delivery-builder.event.ask\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_presence\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_steps\", \"$SRV.INFO\", \"$SRV.INFO.mesh-delivery-builder\", \"$SRV.INFO.mesh-delivery-builder.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-delivery-builder\", \"$SRV.PING.mesh-delivery-builder.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-delivery-builder\", \"$SRV.STATS.mesh-delivery-builder.\u003e\", \"_INBOX.novox.mesh-delivery-builder.\u003e\", \"mesh.mod.mesh-delivery-builder.call.\u003e\", \"mesh.mod.mesh-delivery-builder.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"novox.mesh-delivery-builder.worker\", password: \"$2a$11$novox.mesh-delivery-bufixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.asks.novox_mesh-delivery-builder_worker.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.INFO.asks.novox_mesh-delivery-builder_worker\", \"$JS.API.CONSUMER.MSG.NEXT.asks.novox_mesh-delivery-builder_worker\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps\", \"$JS.FC.KV_mesh-delivery-builder_steps.\u003e\", \"$KV.mesh-delivery-builder_steps.\u003e\", \"mesh.mod.gitea.call.gitea_commit.*.novox~mesh-delivery-builder~worker\", \"mesh.mod.gitea.call.gitea_commit.novox~mesh-delivery-builder~worker\", \"mesh.mod.gitea.tool.gitea_commit\", \"mesh.mod.gitea.tool.gitea_commit.*\", \"mesh.mod.mesh-delivery-builder.event.built\", \"mesh.mod.mesh-delivery-builder.event.states\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_steps\", \"_INBOX.novox.mesh-delivery-builder.worker.\u003e\"] }\n } }\n { user: \"novox.mesh-delivery-view\", password: \"$2a$11$novox.mesh-delivery-vifixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.novox_mesh-delivery-view.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_presence.*.$KV.mesh-delivery-builder_presence.novox\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.novox.mesh-delivery-view\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.novox_mesh-delivery-view\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_GO.SEAT_NODE_BUILD_GO_CHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_GO.SEAT_NODE_BUILD_GO_DELIVERY_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_GO.SEAT_NODE_BUILD_GO_RECHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_IMAGE.SEAT_NODE_BUILD_IMAGE_CHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_IMAGE.SEAT_NODE_BUILD_IMAGE_DELIVERY_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_IMAGE.SEAT_NODE_BUILD_IMAGE_RECHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_NODE.SEAT_NODE_BUILD_NODE_CHECK_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_NODE.SEAT_NODE_BUILD_NODE_DELIVERY_worker\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_BUILD_NODE.SEAT_NODE_BUILD_NODE_RECHECK_worker\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.novox_mesh-delivery-view\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_presence.$KV.mesh-delivery-builder_presence.novox\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.novox.mesh-delivery-view\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_presence\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_GO\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_IMAGE\", \"$JS.API.STREAM.INFO.SEAT_NODE_BUILD_NODE\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_GO\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_IMAGE\", \"$JS.API.STREAM.MSG.GET.SEAT_NODE_BUILD_NODE\", \"$JS.FC.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_presence\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_steps\", \"$SRV.INFO\", \"$SRV.INFO.mesh-delivery-view\", \"$SRV.INFO.mesh-delivery-view.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-delivery-view\", \"$SRV.PING.mesh-delivery-view.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-delivery-view\", \"$SRV.STATS.mesh-delivery-view.\u003e\", \"_INBOX.novox.mesh-delivery-view.\u003e\", \"mesh.mod.mesh-delivery-view.call.\u003e\", \"mesh.mod.mesh-delivery-view.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"novox.mesh-grants\", password: \"$2a$11$novox.mesh-grantsnovoxfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.novox_mesh-grants.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_assertions.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.novox.mesh-grants\", \"$JS.API.CONSUMER.CREATE.asks.novox_mesh-delivery-builder_worker\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_assertions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.novox_mesh-grants\", \"$JS.API.CONSUMER.INFO.asks.novox_mesh-delivery-builder_worker\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.novox_mesh-grants\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.*.*\", \"$JS.API.DIRECT.GET.KV_mesh-grants_assertions.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.novox.mesh-grants\", \"$JS.API.DIRECT.GET.OBJ_mesh-grants-user-list.\u003e\", \"$JS.API.STREAM.CREATE.KV_mesh-delivery-builder_presence\", \"$JS.API.STREAM.CREATE.KV_mesh-delivery-builder_steps\", \"$JS.API.STREAM.CREATE.KV_mesh-grants_assertions\", \"$JS.API.STREAM.CREATE.LOG_mesh-delivery-builder_builds\", \"$JS.API.STREAM.CREATE.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.CREATE.asks\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_presence\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps\", \"$JS.API.STREAM.INFO.KV_mesh-grants_assertions\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.API.STREAM.INFO.LOG_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.INFO.asks\", \"$JS.API.STREAM.MSG.GET.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.PURGE.OBJ_mesh-grants-user-list\", \"$JS.FC.KV_mesh-grants_assertions.\u003e\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"$KV.mesh-grants_assertions.\u003e\", \"$O.mesh-grants-user-list.\u003e\", \"mesh.assignment.*.*\", \"mesh.mod.mesh-grants.event.membership-changed\", \"mesh.mod.mesh-grants.event.states\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-grants_assertions\", \"$SRV.INFO\", \"$SRV.INFO.mesh-grants\", \"$SRV.INFO.mesh-grants.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-grants\", \"$SRV.PING.mesh-grants.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-grants\", \"$SRV.STATS.mesh-grants.\u003e\", \"_INBOX.novox.mesh-grants.\u003e\", \"mesh.mod.mesh-grants.call.\u003e\", \"mesh.mod.mesh-grants.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n ]\n }\n \"$SYS\" {\n exports = [\n { stream: \"$SYS.ACCOUNT.MESH.CONNECT\", accounts: [\"MESH\"] }\n { stream: \"$SYS.ACCOUNT.MESH.DISCONNECT\", accounts: [\"MESH\"] }\n { service: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\", accounts: [\"MESH\"] }\n ]\n }\n}\n", "Missing": null, "Err": "", "Memberships": { diff --git a/modules/mesh-grants/internal/buscompose/users.go b/modules/mesh-grants/internal/buscompose/users.go index 9df3e1dd..5d521da9 100644 --- a/modules/mesh-grants/internal/buscompose/users.go +++ b/modules/mesh-grants/internal/buscompose/users.go @@ -131,6 +131,11 @@ type ProcessPrincipal struct { // Invokes are the tools and seat verbs this process calls, each . or a role's // seat:., or * for every tool on the mesh. Invokes []string + // Tools says it is a tools bundle's account, run by the node's tool runtime, not a process the node runs. + Tools bool + // Keeps says its bus declares `keeps`: the bus module's keeper (novox/hq ADR 0626 §1), composed the bus's create, + // update, snapshot and delete subjects of every module's bus state, and only on the module holding mesh-broker. + Keeps bool } // Records is what composing a user list needs to know about the mesh, and nothing more. @@ -164,6 +169,11 @@ type Records struct { // Announced is each module's last announcement of the states it keeps (novox/hq ADR 0495, issue 1790), as the // controller keeps it; a module absent has announced nothing yet. Nil where none was read. Announced map[string]announce.States + // BusState is every bus-state entry each manifest asks for in `requires`, by module, and Bound the streams of each + // entry the bus module said it created and has not dropped, by module: what the bus module's process may define + // (busstate.go, ADR 0595 §4). + BusState map[string][]catalogue.BusStateEntry + Bound map[string][]string } // Users is every user the composed file should contain, in the order it will be written. @@ -232,6 +242,18 @@ func Users(r Records) ([]Principal, error) { Emits: pr.Emits, Queues: pr.Queues, State: pr.State, Uses: d.Uses, Asks: pr.Asks, Invokes: pr.Invokes, Removes: pr.Removes, }) + // The process of the module holding mesh-broker whose bus declares `keeps` keeps every module's bus state + // (busstate.go, ADR 0595 §4, ADR 0626 §1); `keeps` on any other module composes nothing (the judge refuses it, + // ADR 0626 §3). Its tools bundle reads its module's own stream back. + if d.SnapshotsTheBus && pr.Keeps && !pr.Tools { + k := &out[len(out)-1] + k.KeepsBus, k.KeepsBusState = true, BusStateNames(r.BusState, r.Bound) + k.Consumes, k.Streams = d.Consumes, d.Streams + k.SaysSeatEvents = brokerSeatEvents(d.Holds) + } + if d.SnapshotsTheBus && len(pr.Removes) > 0 { + out[len(out)-1].Streams = d.Streams + } // The ended controller's leftovers include each node's declaration files' bucket (ADR 0590 clause 6). if slices.Contains(pr.Removes, catalogue.RemovesControllerLeftovers) { out[len(out)-1].FilesFor = sortedCopy(r.Nodes) diff --git a/modules/mesh-grants/internal/buscompose/writers.go b/modules/mesh-grants/internal/buscompose/writers.go index 0fab7628..9143f6a2 100644 --- a/modules/mesh-grants/internal/buscompose/writers.go +++ b/modules/mesh-grants/internal/buscompose/writers.go @@ -40,7 +40,7 @@ type WriterRow struct { // update or a delete (novox/hq ADR 0547 §5; the operator, 2026-10-26 08:54); or the bus module's tools deleting one of // the ended controller's leftover streams (ADR 0590 clause 6). func createsDeclared(p Principal, pattern string) bool { - if removesLeftover(p, pattern) { + if removesLeftover(p, pattern) || keepsBusState(p, pattern) { return true } name, ok := strings.CutPrefix(pattern, "$JS.API.STREAM.CREATE.") @@ -151,7 +151,7 @@ var WritersTable = []WriterRow{ Subjects: []string{"$JS.API.STREAM.CREATE.>", "$JS.API.STREAM.UPDATE.>", "$JS.API.STREAM.DELETE.>", "$JS.API.CONSUMER.DURABLE.CREATE.>"}, Writes: createsDeclared, - Shared: "the grants part creates a stream a manifest declares, by its name, and never updates or deletes one (novox/hq ADR 0547 §5; the operator, 2026-10-26 08:54); the bus module's tools account deletes the ended controller's leftover streams, each by its name, and no other (novox/hq ADR 0590 clause 6)"}, + Shared: "the grants part creates a stream a manifest declares, by its name, and never updates or deletes one (novox/hq ADR 0547 §5; the operator, 2026-10-26 08:54); the bus module's tools account deletes the ended controller's leftover streams, each by its name, and no other (novox/hq ADR 0590 clause 6); the bus module's process creates, updates and deletes every module's bus state, each stream by its name (novox/hq ADR 0595 §4)"}, {State: "builds and their outcomes", Writer: "the build seat's holder", KeptIn: "its own state", Others: "the builder asks", Subjects: []string{"mesh.seat.mesh-build-machine.event.built"}, diff --git a/modules/mesh-grants/internal/grants/bindings.go b/modules/mesh-grants/internal/grants/bindings.go new file mode 100644 index 00000000..a06c7922 --- /dev/null +++ b/modules/mesh-grants/internal/grants/bindings.go @@ -0,0 +1,250 @@ +package grants + +import ( + "encoding/json" + "fmt" + "sort" + "strings" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/membership" + "mesh-grants/internal/buscompose" +) + +// **The binding travels on the bus module's events** (novox/hq ADR 0595 §1, §7; design 25 § 12.2; DLV-G1). The bus +// module (the holder of the seat mesh-broker) says `created` when it created one entry of a module's bus state, +// `recreated` when it rebuilt one, and `dropped` when it removed one; each carries the entry's names on the bus. The +// grants part hears them on its durable consumer of the bus module's own stream (its manifest's `consumes`), writes the +// binding into the module's membership on every machine the module is assigned, rewrites it on `recreated` and removes +// it on `dropped`. What it heard is kept in its own state memberships, one key per module (BoundKey), so a binding +// outlives the 30 days its event is kept and reaches a machine the module is assigned to later. A `refused` create +// hands no binding: only these three events are read. + +// The bus module's events the part hears, by their local names (ADR 0595 §6; ADR 0620 §2: the holder's own events, +// never a seat's subject). +const ( + BrokerCreated = "created" + BrokerRecreated = "recreated" + BrokerDropped = "dropped" +) + +// BoundPrefix starts the key of the part's own state memberships that keeps one module's bindings, `bound.`: +// never a `.` key, since no machine is named `bound` (a membership's runtime reads its own key alone). +const BoundPrefix = "bound." + +// BoundKey is the key keeping module's bindings. +func BoundKey(module string) string { return BoundPrefix + module } + +// BrokerEvent is the body of the bus module's `created`, `recreated` and `dropped`, the fields the part reads. +// +// It stands until the seat mesh-broker's definition is generated in mesh-contracts (ADR 0613 §1, §5; design 26): this +// part then reads the generated type, with the same fields. +type BrokerEvent struct { + V int `json:"v,omitempty"` + Module string `json:"module"` + // LocalName and StorageType are the seat's contract's names (mesh-contracts meshbroker.Said); Local and Storage the + // same read where an older body names them so. + LocalName string `json:"local-name,omitempty"` + Local string `json:"local,omitempty"` + StorageType string `json:"storage-type,omitempty"` + Storage string `json:"storage,omitempty"` + Template string `json:"template,omitempty"` + // Binding is the entry's names on the bus: whole on `created` and `recreated`; on `dropped` only its local name counts. + Binding membership.BusBinding `json:"binding"` +} + +// Bound is the value of BoundKey: every binding of one module the bus module said it created and has not dropped, by +// local name. +type Bound struct { + V int `json:"v"` + Module string `json:"module"` + Bindings map[string]membership.BusBinding `json:"bindings"` +} + +// brokerEvent is the local name of an event heard from the holder of mesh-broker, by its bus subject, and whether it is +// one: the seat's own subject alone (`mesh.seat.mesh-broker.event.`, the lead's ruling, registry milestone 13:0x), +// where only its holder publishes, and only while a module holds the seat. The bus module's own stream is its history +// for its tool, never what the part hears. +func brokerEvent(event string, brokers map[string]bool) (string, bool) { + name, onTheSeat := strings.CutPrefix(event, "mesh.seat."+catalogue.BrokerSeat+".event.") + if !onTheSeat || len(brokers) == 0 { + return "", false + } + switch name { + case BrokerCreated, BrokerRecreated, BrokerDropped: + return name, true + } + return "", false +} + +// HearBroker applies one event heard to the kept bindings: a `created` or `recreated` writes the entry's binding, a +// `dropped` removes it. It answers whether the kept bindings changed (so a pass composes again), or why the event was +// not applied; an event that is no bus module's is no change and no error. brokers are the modules holding mesh-broker. +func (p *Part) HearBroker(event string, body []byte, brokers map[string]bool) (bool, error) { + name, ok := brokerEvent(event, brokers) + if !ok { + return false, nil + } + var e BrokerEvent + if err := json.Unmarshal(body, &e); err != nil { + return false, fmt.Errorf("the bus module's %s does not read: %w", name, err) + } + local := firstOf(e.LocalName, e.Local, e.Binding.Local) + storage := firstOf(e.StorageType, e.Storage) + if e.Module == "" || local == "" || strings.Contains(e.Module, ".") { + return false, fmt.Errorf("the bus module's %s names no module or no entry (%q, %q)", name, e.Module, local) + } + b, err := p.bound(e.Module) + if err != nil { + return false, err + } + was, had := b.Bindings[local] + switch name { + case BrokerDropped: + if !had { + return false, nil + } + delete(b.Bindings, local) + default: + bind := e.Binding + bind.Local = local + if bind.Storage == "" { + bind.Storage = storage + } + if bind.Template == "" { + bind.Template = e.Template + } + if had && sameBinding(was, bind) { + return false, nil + } + b.Bindings[local] = bind + } + raw, _ := json.Marshal(b) + if _, err := p.Store.Put(StateMemberships, BoundKey(e.Module), raw); err != nil { + return false, fmt.Errorf("its own %s %s cannot be written: %w", StateMemberships, BoundKey(e.Module), err) + } + return true, nil +} + +// bound reads the kept bindings of module; none kept is an empty value. +func (p *Part) bound(module string) (Bound, error) { + b := Bound{V: 1, Module: module, Bindings: map[string]membership.BusBinding{}} + raw, _, found, err := p.Store.Get(StateMemberships, BoundKey(module)) + if err != nil { + return b, fmt.Errorf("its own %s %s cannot be read: %w", StateMemberships, BoundKey(module), err) + } + if found { + if err := json.Unmarshal(raw, &b); err != nil { + return b, fmt.Errorf("its own %s %s does not read: %w", StateMemberships, BoundKey(module), err) + } + if b.Bindings == nil { + b.Bindings = map[string]membership.BusBinding{} + } + } + return b, nil +} + +// bindingsOf is module's kept bindings in local-name order, for its memberships; nil when none. +func (p *Part) bindingsOf(module string) ([]membership.BusBinding, error) { + b, err := p.bound(module) + if err != nil || len(b.Bindings) == 0 { + return nil, err + } + locals := make([]string, 0, len(b.Bindings)) + for l := range b.Bindings { + locals = append(locals, l) + } + sort.Strings(locals) + out := make([]membership.BusBinding, 0, len(locals)) + for _, l := range locals { + out = append(out, b.Bindings[l]) + } + return out, nil +} + +func sameBinding(a, b membership.BusBinding) bool { + ra, _ := json.Marshal(a) + rb, _ := json.Marshal(b) + return string(ra) == string(rb) +} + +// OwnHears are the module streams the part's own membership on node names it hearing (its consumes of another module's +// stream, the bus module's among them), each with its durable consumer: read from its own state memberships, which the +// part writes; none yet is an error, so the hearing is opened at the next change. +func OwnHears(s Store, node string) ([]membership.HeardIssued, error) { + key := buscompose.MembershipKey(node, Module) + raw, _, found, err := s.Get(StateMemberships, key) + if err != nil { + return nil, fmt.Errorf("its own membership %s cannot be read: %w", key, err) + } + if !found { + return nil, fmt.Errorf("its own membership %s is not composed yet, so the module streams it hears are not known", key) + } + bare, err := membership.BareGrantsMembership(raw) + if err != nil { + return nil, fmt.Errorf("its own membership %s does not read: %w", key, err) + } + var m membership.Membership + if err := json.Unmarshal(bare, &m); err != nil { + return nil, fmt.Errorf("its own membership %s does not read: %w", key, err) + } + return m.Hears, nil +} + +// boundStreams are, by module, the streams on the bus of every binding kept (BoundKey): what the bus module's process +// may still define, an orphan's among them (buscompose.BusStateNames). +func (p *Part) boundStreams() (map[string][]string, error) { + keys, err := p.Store.Keys(StateMemberships) + if err != nil { + return nil, fmt.Errorf("its own %s cannot be listed: %w", StateMemberships, err) + } + out := map[string][]string{} + for _, key := range keys { + module, ok := strings.CutPrefix(key, BoundPrefix) + if !ok { + continue + } + b, err := p.bound(module) + if err != nil { + return nil, err + } + for _, bind := range b.Bindings { + out[module] = append(out[module], bindingStreams(bind)...) + } + } + return out, nil +} + +// bindingStreams are the streams on the bus a binding names: a stream's, a bucket's KV_, an object store's OBJ_, a ledger's +// history, its snapshot stream and its state and index buckets. +func bindingStreams(b membership.BusBinding) []string { + var out []string + if b.Stream != "" { + out = append(out, b.Stream) + } + if b.Bucket != "" { + out = append(out, "KV_"+b.Bucket) + } + if b.ObjectStore != "" { + out = append(out, "OBJ_"+b.ObjectStore) + } + if b.History != "" { + out = append(out, b.History, b.History+"_snapshot") + } + if b.State != "" { + out = append(out, "KV_"+b.State) + } + for _, ix := range b.Indexes { + out = append(out, "KV_"+ix) + } + return out +} + +func firstOf(values ...string) string { + for _, v := range values { + if v != "" { + return v + } + } + return "" +} diff --git a/modules/mesh-grants/internal/grants/firstviews.go b/modules/mesh-grants/internal/grants/firstviews.go new file mode 100644 index 00000000..e7240e04 --- /dev/null +++ b/modules/mesh-grants/internal/grants/firstviews.go @@ -0,0 +1,438 @@ +package grants + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "slices" + "sort" + "strings" + "sync" + "time" + + "git.novox.be/novox/mesh-sdk/go/asks" + "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/views" +) + +// **The grants part's first views** (novox/hq ADR 0615 §1, §4 to §6, ADR 0621, ADR 0622; design 25 § 12.2; DLV-F4r, +// DLV-R1). The part reads the assigner's placement and wants and the indexer's manifests, modules and verdicts no longer +// from their buckets, but by each owner's first-view tool, asked once at its start, then from the owner's views family +// alone (each event one changed key with its revision), asking once more only on a gap in revisions, never on a timer. +// The verdicts alone are followed on the indexer's manifest-accepted and manifest-refused, which carry the revision +// (ADR 0622 §2), not on its views family (HearVerdict). +// Started while an owner is down it keeps asking and never reads the owner's state; past the bound (two minutes) it +// raises FirstViewUnanswered naming the owner and the tool, cleared by the answer. +// +// **An answer empty at revision 0 drops no login**: until an owner answers above revision 0 the state reads as a failed +// read, so the part keeps the user list it has in force, and FirstViewEmpty stands, naming the owner and the operator's +// step (the owner writes its state again from its sources, then grants_first_view_again); an answer above 0 clears it. + +// FirstView is one owner's state the part reads by its first view: the owner, the tool its `invokes` names, the state, +// and the read it stands for in the part's inputs (`.`). +type FirstView struct { + Owner string `json:"owner"` + Tool string `json:"tool"` + State string `json:"state"` + Read string `json:"read"` +} + +// The owners. +const ( + AssignerModule = "mesh-delivery-assigner" + IndexerModule = "mesh-delivery-indexer" +) + +// FirstViews are the five (ADR 0615 §3 as ADR 0622 §1 amends it: the grants part's placement, wants, manifests, +// modules and verdicts). +var FirstViews = []FirstView{ + {Owner: AssignerModule, Tool: AssignerModule + ".placement", State: "placement", Read: ReadPlacement}, + {Owner: AssignerModule, Tool: AssignerModule + ".wants", State: "wants", Read: ReadWants}, + {Owner: IndexerModule, Tool: IndexerModule + ".manifests", State: "manifests", Read: ReadManifests}, + {Owner: IndexerModule, Tool: IndexerModule + ".modules", State: "modules", Read: ReadModules}, + {Owner: IndexerModule, Tool: IndexerModule + ".verdicts", State: "verdicts", Read: ReadVerdicts}, +} + +// The conditions of the first views, each naming the owner and its state. +const ( + FirstViewUnanswered = "mesh-grants.first-view-unanswered" + FirstViewEmpty = "mesh-grants.first-view-empty" +) + +// KeyFirstViews is the key of the part's own state user-list where its process keeps what each first view holds, which +// grants_first_views reads. +const KeyFirstViews = "first-views" + +// ErrFirstViewNotYet is a read of a first view not answered above revision 0: the part composes as from a failed read. +var ErrFirstViewNotYet = errors.New("not answered above revision 0 yet") + +// Views is the part's reads of the five owners' states through their first views: a Reader and a Watcher for those +// states, every other read going to Rest. +type Views struct { + // Followers are the five, by the read they stand for. + Followers map[string]*views.Follower + // Rest reads every other state, and Watching watches it (nil: watched by nobody, as in a tool). + Rest Reader + Watching Watcher +} + +// NewViews makes the five followers: ask calls an owner's tool (through the part's own account), say says a condition, +// changed wakes a pass. Started by Start. +func NewViews(ask func(tool string) func(context.Context, string) (views.Answer, error), say func(asks.Condition) error, + changed func(), logf func(string, ...any)) *Views { + v := &Views{Followers: map[string]*views.Follower{}} + for _, fv := range FirstViews { + fv := fv + f := &views.Follower{Owner: fv.Owner, Tool: fv.Tool, State: fv.State, Ask: ask(fv.Tool), Logf: logf, + Changed: func(string, json.RawMessage, bool, time.Time) { changed() }} + f.Raise = func(s views.Status) { + if say != nil { + _ = say(unansweredCondition(fv, s, true)) + } + changed() + } + f.Clear = func(s views.Status) { + if say != nil { + _ = say(unansweredCondition(fv, s, false)) + } + changed() + } + v.Followers[fv.Read] = f + } + return v +} + +// Start asks each first view on a goroutine of its own, until it is answered or ctx ends; each answer wakes a pass. +func (v *Views) Start(ctx context.Context, changed func()) { + for _, f := range v.Followers { + f := f + go func() { + if f.Start(ctx) == nil { + changed() + } + }() + } +} + +// Following says the follower of the owner's views event state, or nil. +func (v *Views) Following(owner, state string) *views.Follower { + for _, fv := range FirstViews { + if fv.Owner == owner && fv.State == state { + return v.Followers[fv.Read] + } + } + return nil +} + +// ready is the follower of read, answered above revision 0; nil and why when it is not one of the five, or not yet. +func (v *Views) ready(read string) (*views.Follower, bool, error) { + f, ok := v.Followers[read] + if !ok { + return nil, false, nil + } + select { + case <-f.Ready(): + default: + return f, true, fmt.Errorf("%s by %s: %w", read, f.Tool, ErrFirstViewNotYet) + } + if s := f.Status(); s.Revision == 0 { + return f, true, fmt.Errorf("%s by %s answered empty at revision 0: %w", read, f.Tool, ErrFirstViewNotYet) + } + return f, true, nil +} + +// Get reads one key: from its first view for the five, else from Rest. +func (v *Views) Get(state, key string) ([]byte, bool, error) { + f, mine, err := v.ready(state) + if !mine { + return v.Rest.Get(state, key) + } + if err != nil { + return nil, false, err + } + raw, ok := f.Get(key) + return raw, ok, nil +} + +// Keys lists a state's keys: from its first view for the five, else from Rest. +func (v *Views) Keys(state string) ([]string, error) { + f, mine, err := v.ready(state) + if !mine { + return v.Rest.Keys(state) + } + if err != nil { + return nil, err + } + values := f.Values() + out := make([]string, 0, len(values)) + for k := range values { + out = append(out, k) + } + sort.Strings(out) + return out, nil +} + +// Watch watches Rest's states; the five are followed by their first views and events, never watched. +func (v *Views) Watch(state string, changed func(key string, value []byte), ended func(error)) error { + if _, mine := v.Followers[state]; mine || v.Watching == nil { + return nil + } + return v.Watching.Watch(state, changed, ended) +} + +// Statuses are what each first view holds now, in FirstViews' order. +func (v *Views) Statuses() []views.Status { + out := make([]views.Status, 0, len(FirstViews)) + for _, fv := range FirstViews { + out = append(out, v.Followers[fv.Read].Status()) + } + return out +} + +// Again asks the first view of one state (its state name, `placement`, or its tool) once more, or every one when state is +// empty, each on a goroutine of its own; it answers which it asked, or refuses a state that is none of the five. +func (v *Views) Again(ctx context.Context, state, why string) ([]string, error) { + var asked []string + for _, fv := range FirstViews { + if state != "" && state != fv.State && state != fv.Tool && state != fv.Read { + continue + } + f := v.Followers[fv.Read] + go func() { _ = f.Again(ctx, why) }() + asked = append(asked, fv.Tool) + } + if len(asked) == 0 { + return nil, fmt.Errorf("%q is no first view of the grants part: one of placement, wants, manifests, modules, verdicts, or none for all", state) + } + return asked, nil +} + +// VerdictEvents are the indexer's two events the part follows the verdicts on after their first view (ADR 0622 §2), as its +// manifest's consumes name them. +var VerdictEvents = []string{IndexerModule + "." + delivery.ManifestAcceptedEvent, IndexerModule + "." + delivery.ManifestRefusedEvent} + +// HearVerdict takes one of the indexer's manifest-accepted or manifest-refused (event, its name) into the verdicts' first +// view, as one change of the verdicts state at the revision it carries (ADR 0622 §2): the manifest's digest its key, the +// verdict it says its value. A revision past the one after the picture's is a gap, and so is an event carrying no +// revision (an indexer that does not say it) or one saying no manifest above the picture's revision (the empty accepted +// verdict of a module leaving the trunk): the first view is asked once more on its own goroutine, said in the log. A +// revision already in the picture is passed over. Data that does not read is answered as an error, and nothing is asked. +func (v *Views) HearVerdict(ctx context.Context, event string, data []byte, logf func(string, ...any)) error { + f := v.Followers[ReadVerdicts] + if f == nil { + return nil + } + var verdict delivery.ManifestVerdict + var digest string + var revision uint64 + switch event { + case delivery.ManifestAcceptedEvent: + var a delivery.ManifestAccepted + if err := json.Unmarshal(data, &a); err != nil { + return fmt.Errorf("%s does not read: %w", event, err) + } + verdict = delivery.ManifestVerdict{V: delivery.Version, Module: a.Module, State: delivery.VerdictAccepted, Judge: a.Judge} + digest, revision = a.Digest, a.Revision + case delivery.ManifestRefusedEvent: + var r delivery.ManifestRefused + if err := json.Unmarshal(data, &r); err != nil { + return fmt.Errorf("%s does not read: %w", event, err) + } + verdict = delivery.ManifestVerdict{V: delivery.Version, Module: r.Module, State: delivery.VerdictRefused, Judge: r.Judge, Problems: r.Problems} + digest, revision = r.Digest, r.Revision + default: + return fmt.Errorf("%q is not a verdict event of the indexer", event) + } + held := f.Status() + gap := func(why string) error { + logf("views: a gap in %s (%s): its first view %s is asked once more", f.State, why, f.Tool) + go func() { _ = f.Gap(ctx, why) }() + return nil + } + if revision == 0 { + return gap(fmt.Sprintf("the indexer's %s of %s carries no revision", event, verdict.Module)) + } + key, err := delivery.VerdictsKey(digest) + if err != nil { + if revision <= held.Revision { + return nil + } + return gap(fmt.Sprintf("the indexer's %s of %s at revision %d names no manifest", event, verdict.Module, revision)) + } + // A revision below the picture's is a gap (ADR 0615 §5): the verdict events carry no mark of when the indexer's verdicts + // were made, so one made again shows only as revisions starting low. A redelivery of a verdict the picture already + // holds for its manifest is passed over, so a consumer's backlog after a restart asks nothing. + if revision < held.Revision { + if raw, found := f.Get(key); found { + if was, err := delivery.DecodeManifestVerdict(raw); err == nil && sameVerdict(was, verdict) { + return nil + } + } + return gap(fmt.Sprintf("the indexer's %s of %s at revision %d, below the picture's %d: its verdicts were made again, or this verdict was missed", + event, verdict.Module, revision, held.Revision)) + } + value, err := json.Marshal(verdict) + if err != nil { + return err + } + if g, why := f.Apply(views.Event{V: 1, Owner: f.Owner, State: f.State, Key: key, Revision: revision, Created: held.Created, + Value: value, At: time.Now().UTC()}); g { + return gap(why) + } + return nil +} + +// sameVerdict says two verdicts of one manifest say the same: its module, accepted or refused, the judge and the problems. +func sameVerdict(a, b delivery.ManifestVerdict) bool { + return a.Module == b.Module && a.State == b.State && a.Judge == b.Judge && slices.Equal(a.Problems, b.Problems) +} + +// unansweredCondition is FirstViewUnanswered for one first view, raised or cleared. +func unansweredCondition(fv FirstView, s views.Status, raised bool) asks.Condition { + c := asks.Condition{Key: FirstViewUnanswered + "." + fv.Owner + "." + fv.State, Subject: asks.Scope{Scope: "module", ID: Module}} + if !raised { + c.State = asks.StateCleared + c.Resolved = fv.Tool + " answered at revision " + fmt.Sprint(s.Revision) + return c + } + c.State, c.Severity = asks.StateRaised, asks.SeverityWarning + c.Summary = fmt.Sprintf("the grants part has no first view of %s's %s: %s has not answered since %s (%s)", fv.Owner, fv.State, + fv.Tool, s.Asked.Format(time.RFC3339), strings.TrimSpace(s.Error)) + c.Headline = "The grants part waits for " + fv.Owner + c.Explanation = "The grants part reads " + fv.Owner + "'s " + fv.State + " only through " + fv.Tool + ", and keeps asking it; " + + "it never reads the state itself, and keeps what it composed meanwhile (novox/hq ADR 0615 §6). The answer clears this." + c.Needs = "a look at " + fv.Owner + "'s runtime of its own: is it running and connected?" + return c +} + +// emptyCondition is FirstViewEmpty for one first view: raised while it answered empty at revision 0, with the +// operator's step, cleared by an answer above 0. +func emptyCondition(fv FirstView, raised bool) asks.Condition { + c := asks.Condition{Key: FirstViewEmpty + "." + fv.Owner + "." + fv.State, Subject: asks.Scope{Scope: "module", ID: Module}} + if !raised { + c.State = asks.StateCleared + c.Resolved = fv.Tool + " answered above revision 0" + return c + } + step := "the indexer takes each module in again (indexer_take_in)" + if fv.Owner == AssignerModule { + step = "the assigner composes the wants again at its next composition" + } + c.State, c.Severity = asks.StateRaised, asks.SeverityWarning + c.Summary = fmt.Sprintf("%s answered %s's %s empty at revision 0, so the grants part keeps the user list in force", fv.Tool, fv.Owner, fv.State) + c.Headline = fv.Owner + "'s " + fv.State + " is empty" + c.Explanation = asks.NeedsYou + "a step. The owner holds nothing of " + fv.State + " (its state rebuilt by a recreate that could " + + "not keep the data, or a fresh mesh), so the grants part drops no login and keeps what it holds; a login revoked meanwhile " + + "stays until the owner's first change (novox/hq design 25 § 12.2)." + c.Needs = "let the owner write its state again from its sources (" + step + "), then grants_first_view_again" + return c +} + +// firstViewsSaid is what the part last said and kept of its first views (sayFirstViews). +type firstViewsSaid struct { + mu sync.Mutex + empty map[string]bool + known bool +} + +// FirstViewsState is the value of KeyFirstViews: each first view's status, as the part's process last kept it. +type FirstViewsState struct { + V int `json:"v"` + Views []views.Status `json:"views"` +} + +// sayFirstViews keeps the first views' statuses in the part's own state (for grants_first_views) when they change, and +// raises or clears FirstViewEmpty for each answered at revision 0. Each pass calls it; nil Views is none. +func (p *Part) sayFirstViews(res *Result) { + if p.Views == nil { + return + } + statuses := p.Views.Statuses() + for i := range statuses { + statuses[i].Asked, statuses[i].Answered = statuses[i].Asked.Truncate(time.Second), statuses[i].Answered.Truncate(time.Second) + } + data, _ := json.Marshal(FirstViewsState{V: 1, Views: statuses}) + p.changed(res, StateUserList, KeyFirstViews, data) + p.fvSaid.mu.Lock() + defer p.fvSaid.mu.Unlock() + if p.fvSaid.empty == nil { + p.fvSaid.empty = map[string]bool{} + } + for i, fv := range FirstViews { + s := statuses[i] + empty := !s.Answered.IsZero() && s.Revision == 0 + if p.fvSaid.known && p.fvSaid.empty[fv.Read] == empty { + continue + } + if s.Answered.IsZero() && !p.fvSaid.known { + continue // not answered yet: nothing to say of its emptiness + } + if p.Say == nil || p.Say(emptyCondition(fv, empty)) == nil { + p.fvSaid.empty[fv.Read] = empty + } + } + p.fvSaid.known = true +} + +// FirstViewsOf answers the tool grants_first_views: what the part's process last kept of each first view (owner, tool, +// state, revision, when asked and answered, asks, hand acts, whether its condition is raised). +func (p *Part) FirstViewsOf() (any, error) { + raw, rev, found, err := p.Store.Get(StateUserList, KeyFirstViews) + if err != nil { + return nil, fmt.Errorf("its own %s %s cannot be read: %w", StateUserList, KeyFirstViews, err) + } + if !found { + return map[string]any{"views": []any{}, "said": "the part's process has kept no first view yet: it has not passed since it started"}, nil + } + var st FirstViewsState + if err := json.Unmarshal(raw, &st); err != nil { + return nil, fmt.Errorf("its own %s %s does not read: %w", StateUserList, KeyFirstViews, err) + } + return map[string]any{"views": st.Views, "revision": rev, "first-views": FirstViews}, nil +} + +// AskFirstViewAgain answers the tool grants_first_view_again: it asks the part's process to ask one first view (by its +// state, or every one when state is empty) once more now, and records the hand act through record (the seat +// mesh-hand-acts); a state that is none of the five is refused before anything is asked. +func (p *Part) AskFirstViewAgain(caller, state, why string, record func(what, why string) error) (any, error) { + which := AllFirstViews + if state != "" { + which = "" + for _, fv := range FirstViews { + if state == fv.State || state == fv.Tool || state == fv.Read { + which = fv.State + } + } + if which == "" { + return nil, fmt.Errorf("%q is no first view of the grants part: one of placement, wants, manifests, modules, verdicts, or none for all", state) + } + } + if strings.TrimSpace(why) == "" { + return nil, errors.New("grants_first_view_again needs why: it is recorded as a hand act") + } + if record != nil { + if err := record("grants_first_view_again "+which, why); err != nil { + return nil, fmt.Errorf("the hand act could not be recorded, so nothing was asked: %w", err) + } + } + ask, _ := json.Marshal(Ask{By: caller, At: p.now(), FirstView: which}) + rev, err := p.Store.Put(StateUserList, KeyAsked, ask) + if err != nil { + return nil, fmt.Errorf("the ask could not be written: %w", err) + } + return map[string]any{"asked": rev, "first-view": which, + "says": "the part's process asks it once more at this ask; grants_first_views shows its answer"}, nil +} + +// Unwrapped is a call whose answer is read as the tool's own text where the runtime wraps it as content (unwrap), as +// a first view's answer is decoded (views.AskVia). +func Unwrapped(call func(key string, body any) (json.RawMessage, error)) func(key string, body any) (json.RawMessage, error) { + return func(key string, body any) (json.RawMessage, error) { + raw, err := call(key, body) + if err != nil { + return nil, err + } + return unwrap(raw), nil + } +} diff --git a/modules/mesh-grants/internal/grants/follow.go b/modules/mesh-grants/internal/grants/follow.go index 9e01a74d..b69a8905 100644 --- a/modules/mesh-grants/internal/grants/follow.go +++ b/modules/mesh-grants/internal/grants/follow.go @@ -128,6 +128,15 @@ func (f *Follower) open() { } if f.Hear != nil && !f.heard_ { err := f.Hear(func(event string, body []byte) { + // The bus module's created, recreated or dropped: the binding it carries is kept, and a pass composes the + // memberships with it (bindings.go). + if changed, err := f.Part.HearBroker(event, body, f.Part.Brokers()); err != nil { + f.logf("%v: not applied, said again at its redelivery", err) + return + } else if changed { + f.Wake() + return + } // An account the vault minted. if IsAccountIssued(event) { f.Wake() @@ -241,6 +250,11 @@ func (f *Follower) Run(ctx context.Context) { f.logf("published %d memberships: %v", len(res.Published), res.Published) } f.said(res) + // A consumer this pass created may be the part's own, which its hearing waits on: one more pass opens it, so + // the first event on it is heard without waiting for an unrelated change. + if f.Hear != nil && !f.heard_ && res.Consumers != nil && len(res.Consumers.Created) > 0 { + f.Wake() + } if res.Written { f.logf("the shadow list moved to revision %d: %d users, %d differing from the loaded list", res.Revision, res.UserList.Users, len(res.UserList.Compare.Differing())) diff --git a/modules/mesh-grants/internal/grants/full.go b/modules/mesh-grants/internal/grants/full.go index 872a6498..f563887a 100644 --- a/modules/mesh-grants/internal/grants/full.go +++ b/modules/mesh-grants/internal/grants/full.go @@ -1,95 +1,32 @@ package grants import ( - "fmt" - "strings" - "git.novox.be/novox/mesh-sdk/go/asks" - "git.novox.be/novox/mesh-sdk/go/catalogue" ) -// **A history stream or an object bucket past 75 percent of its cap is said, in one place for every module** (novox/hq -// ADR 0592 §6, §25; ADR 0593 §10 gives the controller's part to this part). Such a stream or bucket refuses a new message -// or object when full rather than dropping its oldest, so filling up is the one way it fails: the part reads every -// stream's size from the bus module's nats_streams at each pass (HeldStreams) and raises the warning FullCondition naming -// each one past catalogue.FullWarnPercent, cleared once none is. +// **The fill of a module's kept data is the bus module's** (novox/hq ADR 0595 §6, design 25 § 12.3): its one periodic +// read raises `full` at its raise mark and clears it below its clear mark, for every entry that refuses when full. The +// grants part no longer reads any stream's size and raises nothing of fill: `mesh-grants.history-full` moved to the bus +// module, its new condition raised first, then this old key cleared. So the part clears the old key once, at its first +// pass after a start (DLV-G3), whether or not one is held: a clear of a condition not raised is a no-op at the keeper. -// FullCondition is the key of the warning raised while a history stream or an object bucket is past its mark. +// FullCondition is the key of the warning this part raised before ADR 0595 §6 moved fill to the bus module: only ever +// cleared now. const FullCondition = "mesh-grants.history-full" -// Fill is how full one history stream or object bucket is: its module, its stream on the bus, its bytes and its cap. -type Fill struct { - Module string `json:"module"` - Stream string `json:"stream"` - Bytes int64 `json:"bytes"` - Cap int64 `json:"cap"` -} - -// Percent is its bytes in percent of its cap, rounded down. -func (f Fill) Percent() int64 { return f.Bytes * 100 / f.Cap } - -// Full is every history stream and object bucket of a placed module past catalogue.FullWarnPercent of its cap, by the -// sizes the bus holds (bytes, by stream name), in module and stream order. One the bus does not hold is not full. Pure. -func Full(in Inputs, bytes map[string]int64) []Fill { - var out []Fill - for _, module := range sortedKeys(in.Manifests) { - if !assigned(in, module) { - continue - } - m := in.Manifests[module] - var capped []Fill - for _, s := range m.Streams { - if s.KeepsHistory() { - capped = append(capped, Fill{Module: module, Stream: s.BusName(), Cap: int64(s.Cap()) << 20}) - } - } - for _, o := range objectsOf(m) { - capped = append(capped, Fill{Module: module, Stream: "OBJ_" + o.Bucket(), Cap: int64(o.MaxMiB) << 20}) - } - for _, f := range capped { - held, ok := bytes[f.Stream] - if !ok || f.Cap <= 0 { - continue - } - f.Bytes = held - if f.Bytes*100 >= catalogue.FullWarnPercent*f.Cap { - out = append(out, f) - } - } - } - return out -} - -// sayFull raises FullCondition while a history stream or an object bucket is past its mark, and clears it once none is: -// said once per set of streams, again when that set changes, and at the first pass after a start. -func (p *Part) sayFull(full []Fill) { +// clearFull clears FullCondition once after a start; a clear refused is tried again at the next pass. +func (p *Part) clearFull() { if p.Say == nil { return } - var names, lines []string - for _, f := range full { - names = append(names, f.Stream) - lines = append(lines, fmt.Sprintf("%s of %s: %d of %d MiB (%d%%)", f.Stream, f.Module, f.Bytes>>20, f.Cap>>20, f.Percent())) - } - said := strings.Join(names, " ") p.holdMu.Lock() defer p.holdMu.Unlock() - if p.fullKnown && said == p.fullSaid { + if p.fullCleared { return } - c := asks.Condition{Key: FullCondition, Subject: asks.Scope{Scope: "module", ID: Module}} - if said == "" { - c.State = asks.StateCleared - } else { - c.Severity, c.State = asks.SeverityWarning, asks.StateRaised - c.Summary = fmt.Sprintf("%d history stream(s) or object bucket(s) past %d%% of their cap: %s", len(full), - catalogue.FullWarnPercent, strings.Join(lines, "; ")) - c.Headline, c.Needs = "A stream that keeps its history is filling up", "a larger cap in the module's manifest, or data removed" - c.Explanation = asks.NeedsYou + "a decision before it is full: such a stream or bucket refuses new messages when full " + - "rather than dropping its oldest (novox/hq ADR 0592 §6). " + strings.Join(lines, "; ") - c.Resolved = "every history stream and object bucket is below its mark" - } + c := asks.Condition{Key: FullCondition, Subject: asks.Scope{Scope: "module", ID: Module}, State: asks.StateCleared, + Resolved: "the fill of a module's kept data is the bus module's own condition now (novox/hq ADR 0595 §6)"} if p.Say(c) == nil { - p.fullSaid, p.fullKnown = said, true + p.fullCleared = true } } diff --git a/modules/mesh-grants/internal/grants/inputs.go b/modules/mesh-grants/internal/grants/inputs.go index 3fd6310c..0b330afa 100644 --- a/modules/mesh-grants/internal/grants/inputs.go +++ b/modules/mesh-grants/internal/grants/inputs.go @@ -47,8 +47,11 @@ const ( // Reads is every state the part reads, in the manifest's order. Not the controller's assignments: the seat's placement // alone places modules (novox/hq ADR 0549 §5, ADR 0571 §10; issue 774). -var Reads = []string{ReadModules, ReadManifests, ReadVerdicts, ReadResolved, ReadHave, ReadWants, ReadBuilds, - ReadAccounts, ReadPlacement, ReadEnrolments, ReadNodeLogins} +var Reads = []string{ReadResolved, ReadHave, ReadBuilds, ReadAccounts, ReadEnrolments, ReadNodeLogins} + +// ViewReads are the owners' states the part reads by their first views and views families, never their buckets +// (firstviews.go, ADR 0615): each read in Reads' place. +var ViewReads = []string{ReadModules, ReadManifests, ReadVerdicts, ReadWants, ReadPlacement} // The bus module's tools the part calls, as its manifest's invokes name them: read-only, each. const ( @@ -58,7 +61,18 @@ const ( ) // Invokes is every tool the part calls. -var Invokes = []string{CallUsers, CallUserCan, CallStreams, CallRootFree} +var Invokes = append([]string{CallUsers, CallUserCan, CallStreams, CallRootFree, CallHandAct}, firstViewTools()...) + +// CallHandAct records a person's act (grants_first_view_again). +const CallHandAct = "seat:mesh-hand-acts.hand-act" + +func firstViewTools() []string { + var out []string + for _, fv := range FirstViews { + out = append(out, fv.Tool) + } + return out +} // Reader reads another part's state, named `.`. A refusal (a read not granted yet, a bucket not there) is // an error the part names as a gap and composes past: it never stops for one. @@ -107,6 +121,9 @@ type Inputs struct { Placed map[string]delivery.Placement // Manifests is each module's manifest at the indexer's trunk, by module. Manifests map[string]catalogue.Manifest + // Bound is, by module, the streams of each bus-state entry the bus module said it created and has not dropped, from + // the part's own state memberships (bindings.go): set by the pass, never read from another part. + Bound map[string][]string // Digests is each module's manifest digest, by module. Digests map[string]string // Fingerprints is the fingerprint of the new form the indexer's trunk value names for each module, by module: what a diff --git a/modules/mesh-grants/internal/grants/part.go b/modules/mesh-grants/internal/grants/part.go index ed644c8d..c23c9498 100644 --- a/modules/mesh-grants/internal/grants/part.go +++ b/modules/mesh-grants/internal/grants/part.go @@ -2,6 +2,7 @@ package grants import ( "bytes" + "context" "crypto/sha256" "encoding/hex" "encoding/json" @@ -12,6 +13,7 @@ import ( "time" "git.novox.be/novox/mesh-sdk/go/asks" + "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/membership" "mesh-grants/internal/buscompose" @@ -96,8 +98,6 @@ func (s UserListState) inputs() string { // Result is what one composition did. type Result struct { - // Full is every history stream and object bucket past its mark at this pass (full.go, ADR 0592 §6). - Full []Fill `json:"full,omitempty"` UserList UserListState `json:"user_list"` Revision uint64 `json:"revision"` Written bool `json:"written"` @@ -183,9 +183,8 @@ type Part struct { behindSaid string behindKnown bool behindSeen map[string]bool - // fullSaid is the history streams and object buckets past their mark last said as a condition, once fullKnown (full.go). - fullSaid string - fullKnown bool + // fullCleared is whether the old history-full key was cleared since the start (full.go, ADR 0595 §6). + fullCleared bool // refusedSaid is the refused principals last said as a condition, once refusedKnown (refused.go). refusedSaid string refusedKnown bool @@ -197,6 +196,38 @@ type Part struct { missingKnown bool // hashHold are the users whose hash changes held the pass's list, or nil (sealed.go). hashHold []string + // brokers are the modules whose manifest claims mesh-broker at the last pass (bindings.go), guarded by mu. + brokers map[string]bool + // Views are the part's first views of the owners' states (firstviews.go), and FirstViewContext the process's life + // they ask in: the process's; nil in the tools. + Views *Views + FirstViewContext context.Context + fvSaid firstViewsSaid +} + +// keepBrokers keeps the modules whose manifest claims the seat mesh-broker: the only emitters whose created, recreated +// and dropped the part applies (bindings.go). +func (p *Part) keepBrokers(in Inputs) { + brokers := map[string]bool{} + for module, m := range in.Manifests { + if m.ClaimsSeat(catalogue.BrokerSeat) { + brokers[module] = true + } + } + p.mu.Lock() + p.brokers = brokers + p.mu.Unlock() +} + +// Brokers are the modules holding mesh-broker as the last pass read them. +func (p *Part) Brokers() map[string]bool { + p.mu.Lock() + defer p.mu.Unlock() + out := map[string]bool{} + for m := range p.brokers { + out[m] = true + } + return out } // Ask is the value of KeyAsked: a tool's ask to compose now, and, from grants_assert, to create one module's missing @@ -208,8 +239,13 @@ type Ask struct { // Consumer is, from grants_consumer_match, one account consumer `.` to bring to the composed // configuration by hand (ADR 0561 §6). Consumer string `json:"consumer,omitempty"` + // FirstView is, from grants_first_view_again, one first view to ask once more by hand (its state), or AllFirstViews. + FirstView string `json:"first-view,omitempty"` } +// AllFirstViews asks every first view once more. +const AllFirstViews = "all" + // asked is a new ask, read once per ask; the zero Ask when none. func (p *Part) asked() Ask { raw, rev, found, err := p.Store.Get(StateUserList, KeyAsked) @@ -242,6 +278,12 @@ func (p *Part) Compose() Result { p.mu.Unlock() }() in := ReadInputs(p.Read, &p.cache) + p.keepBrokers(in) + if bound, err := p.boundStreams(); err != nil { + res.Errors = append(res.Errors, err.Error()) + } else { + in.Bound = bound + } for _, g := range in.Refused { res.Refused = append(res.Refused, g.Input+": "+g.Why) } @@ -345,6 +387,15 @@ func (p *Part) Compose() Result { for _, key := range sortedKeys(ms) { // The last membership is the state's own (ADR 0571 clause 7), never the assignments stream's, which goes with the // controller's leftovers (novox/hq ADR 0590 §6). + // The bus module's bindings of the module, as it said them created (bindings.go, ADR 0595 §1, §7). + if binds, err := p.bindingsOf(ms[key].Module); err != nil { + res.Errors = append(res.Errors, err.Error()) + } else if binds != nil { + withBinds := ms[key] + withBinds.Membership.Bindings = binds + withBinds.Digest = digestOf(withBinds.Membership) + ms[key] = withBinds + } was, _, found, err := p.Store.Get(StateMemberships, key) if err != nil { res.NotPublished = append(res.NotPublished, key+": not published: the membership last published cannot be read from its own "+StateMemberships+": "+err.Error()) @@ -380,11 +431,8 @@ func (p *Part) Compose() Result { res.Retired[key] = m.Retired } } - held, sizes, heldErr := HeldStreamsAndBytes(p.Call) - if heldErr == nil { - res.Full = Full(in, sizes) - p.sayFull(res.Full) - } + p.clearFull() + held, heldErr := HeldStreams(p.Call) var ask Ask if p.Create != nil || p.Consumers != nil { ask = p.asked() @@ -412,6 +460,18 @@ func (p *Part) Compose() Result { } } p.consumers(&res, recs, ask.Consumer) + p.sayFirstViews(&res) + if ask.FirstView != "" && p.Views != nil && p.FirstViewContext != nil { + state := ask.FirstView + if state == AllFirstViews { + state = "" + } + if asked, err := p.Views.Again(p.FirstViewContext, state, "by hand: "+ask.By); err != nil { + res.Errors = append(res.Errors, err.Error()) + } else if p.Logf != nil { + p.Logf("asked the first views %v once more by hand, for %s", asked, ask.By) + } + } return res } diff --git a/modules/mesh-grants/internal/grants/records.go b/modules/mesh-grants/internal/grants/records.go index c93bf116..a7447482 100644 --- a/modules/mesh-grants/internal/grants/records.go +++ b/modules/mesh-grants/internal/grants/records.go @@ -41,7 +41,14 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) { out := buscompose.Records{Assigned: map[string][]buscompose.Declared{}, People: map[string][]string{}, Interchangeable: map[string]bool{}, Enrolling: slices.Clone(in.Enrolling), // And each node's logins (ADR 0583 §1): both while it rotates. - NodeLogins: in.NodeLogins} + NodeLogins: in.NodeLogins, + // What the bus module's process may define: every manifest's bus state, and what it said it created (DLV-G2). + BusState: map[string][]catalogue.BusStateEntry{}, Bound: in.Bound} + for module, m := range in.Manifests { + if len(m.BusState) > 0 { + out.BusState[module] = m.BusState + } + } values := func(provider, setting string) []any { var vs []any for _, node := range in.Nodes { @@ -370,7 +377,7 @@ func processesOf(m catalogue.Manifest) []buscompose.ProcessPrincipal { // what it may remove from the bus (ADR 0564 Limits 4, issue 1952 S5). for _, a := range accounts { pr := buscompose.ProcessPrincipal{Process: a.ID, Emits: a.Bus.Emits, State: a.Bus.State, Asks: a.Bus.Asks, - Invokes: a.Bus.Invokes, Removes: a.Bus.Removes} + Invokes: a.Bus.Invokes, Removes: a.Bus.Removes, Tools: a.Tools, Keeps: a.Bus.Keeps} for _, q := range queues { if q.Process == a.ID { pr.Queues = append(pr.Queues, q) diff --git a/modules/mesh-grants/internal/grants/rootfree.go b/modules/mesh-grants/internal/grants/rootfree.go index 3d4c22e9..e76fbbaa 100644 --- a/modules/mesh-grants/internal/grants/rootfree.go +++ b/modules/mesh-grants/internal/grants/rootfree.go @@ -7,6 +7,7 @@ import ( "sort" "strings" + "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/health" "mesh-grants/internal/buscompose" ) @@ -39,7 +40,24 @@ func FromKeeper(event string) bool { const EventAccountIssued = "mesh-vault." + buscompose.AccountIssuedEvent // Consumes is every event the part hears, as its manifest's consumes name them. -var Consumes = []string{EventKeeperRaised, EventKeeperCleared, EventAccountIssued} +// The seat mesh-broker's created, recreated and dropped are heard as every seat's event, `.` on EVENTS (the +// lead's ruling, registry milestone 13:0x), never on the bus module's own stream. +var Consumes = append([]string{EventKeeperRaised, EventKeeperCleared, EventAccountIssued, catalogue.BrokerSeat + "." + BrokerCreated, + catalogue.BrokerSeat + "." + BrokerDropped, catalogue.BrokerSeat + "." + BrokerRecreated}, viewsEvents()...) + +// viewsEvents are the owners' events the part follows its first views by: `.views.`, but the verdicts by +// the indexer's manifest-accepted and manifest-refused (VerdictEvents, ADR 0622 §2). +func viewsEvents() []string { + var out []string + for _, fv := range FirstViews { + if fv.Read == ReadVerdicts { + out = append(out, VerdictEvents...) + continue + } + out = append(out, fv.Owner+".views."+fv.State) + } + return out +} // IsAccountIssued says an event heard is the vault's account-issued, by its name or its bus subject. func IsAccountIssued(event string) bool { diff --git a/modules/mesh-grants/internal/grants/tools.go b/modules/mesh-grants/internal/grants/tools.go index 6503d91f..a94e32d8 100644 --- a/modules/mesh-grants/internal/grants/tools.go +++ b/modules/mesh-grants/internal/grants/tools.go @@ -23,11 +23,14 @@ const ( ToolAccounts = "grants_accounts" ToolConsumerMatch = "grants_consumer_match" ToolAcceptHashes = "grants_accept_hashes" + // ToolFirstViews and ToolFirstViewAgain read and ask again the part's first views (firstviews.go). + ToolFirstViews = "grants_first_views" + ToolFirstViewAgain = "grants_first_view_again" ) // Tools is every tool, in the manifest's order. var Tools = []string{ToolGrants, ToolMembership, ToolUserList, ToolComposeNow, ToolAssert, ToolAccounts, ToolConsumerMatch, - ToolAcceptHashes} + ToolAcceptHashes, ToolFirstViews, ToolFirstViewAgain} // WritesGrants is what grants says of the part's own grants: written once the list the bus loaded carries a build's // accounts, and the one key the assigner reads (ADR 0571 clause 9, which changes ADR 0547 clause 5). diff --git a/modules/mesh-grants/module.json b/modules/mesh-grants/module.json index f7fae6e5..a982d3c6 100644 --- a/modules/mesh-grants/module.json +++ b/modules/mesh-grants/module.json @@ -4,12 +4,7 @@ "slug": "grants", "reads": [ "mesh-config.resolved", - "mesh-delivery-assigner.wants", "mesh-delivery-builder.builds", - "mesh-delivery-indexer.manifests", - "mesh-delivery-indexer.modules", - "mesh-delivery-indexer.verdicts", - "mesh-delivery-assigner.placement", "node-engine.have", "mesh-vault.bus-accounts", "mesh-enrolment.enrolments", @@ -18,7 +13,16 @@ "consumes": [ "mesh-conditions.condition-raised", "mesh-conditions.condition-cleared", - "mesh-vault.account-issued" + "mesh-vault.account-issued", + "mesh-broker.created", + "mesh-broker.dropped", + "mesh-broker.recreated", + "mesh-delivery-assigner.views.placement", + "mesh-delivery-assigner.views.wants", + "mesh-delivery-indexer.views.manifests", + "mesh-delivery-indexer.views.modules", + "mesh-delivery-indexer.manifest-accepted", + "mesh-delivery-indexer.manifest-refused" ], "uses": [ "operator-channel" @@ -27,7 +31,13 @@ "nats.nats_streams", "nats.nats_user_can", "nats.nats_users", - "seat:node-engine.root-free" + "seat:node-engine.root-free", + "mesh-delivery-assigner.placement", + "mesh-delivery-assigner.wants", + "mesh-delivery-indexer.manifests", + "mesh-delivery-indexer.modules", + "mesh-delivery-indexer.verdicts", + "seat:mesh-hand-acts.hand-act" ], "emits": [ "asserted", @@ -44,7 +54,9 @@ "grants_assert", "grants_accounts", "grants_consumer_match", - "grants_accept_hashes" + "grants_accept_hashes", + "grants_first_views", + "grants_first_view_again" ], "state": [ {