From 018243a86ce593ca87ca80eb99cc8ac3aa9c8fa0 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 2 Nov 2026 18:48:13 +0100 Subject: [PATCH] =?UTF-8?q?buscompose=20keeps=20only=20the=20bus's=20names?= =?UTF-8?q?=20and=20shapes;=20the=20composer=20of=20the=20user=20list=20is?= =?UTF-8?q?=20the=20grants=20part's=20own=20(ADR=200593=20=C2=A71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- go/buscompose/accounts.go | 19 - go/buscompose/agreement.go | 125 -- go/buscompose/announced.go | 74 - go/buscompose/buildbacklog.go | 47 - go/buscompose/buscompose_test.go | 880 ----------- go/buscompose/callers.go | 117 -- go/buscompose/cancelled.go | 11 - go/buscompose/composed.go | 71 - go/buscompose/create.go | 115 -- go/buscompose/create_test.go | 46 - go/buscompose/creates_streams_test.go | 155 -- go/buscompose/declaration_files.go | 103 -- go/buscompose/derived.go | 322 ----- go/buscompose/doc.go | 15 +- go/buscompose/engine.go | 70 - go/buscompose/fixtures_test.go | 119 -- go/buscompose/health_grants.go | 113 -- go/buscompose/log.go | 155 -- go/buscompose/membership.go | 178 --- go/buscompose/modstreams.go | 234 --- go/buscompose/names_test.go | 21 + go/buscompose/nats.go | 1287 ----------------- go/buscompose/onnats.go | 5 - go/buscompose/process_grants.go | 56 - go/buscompose/provisioner.go | 229 --- go/buscompose/raise.go | 73 - go/buscompose/readiness.go | 139 -- go/buscompose/sdk_membership.go | 30 - go/buscompose/seattraffic.go | 312 ---- go/buscompose/state.go | 319 ---- go/buscompose/streams.go | 158 -- go/buscompose/testdata/composed.conf | 73 - .../testdata/controller-credentials.json | 21 - go/buscompose/testdata/user-lists.json | 289 ---- go/buscompose/userlist_switch.go | 174 --- .../userlist_switch_integration_test.go | 213 --- go/buscompose/users.go | 463 ------ go/buscompose/waits.go | 76 - go/buscompose/writers.go | 282 ---- 39 files changed, 25 insertions(+), 7164 deletions(-) delete mode 100644 go/buscompose/agreement.go delete mode 100644 go/buscompose/announced.go delete mode 100644 go/buscompose/buildbacklog.go delete mode 100644 go/buscompose/buscompose_test.go delete mode 100644 go/buscompose/callers.go delete mode 100644 go/buscompose/cancelled.go delete mode 100644 go/buscompose/composed.go delete mode 100644 go/buscompose/create.go delete mode 100644 go/buscompose/create_test.go delete mode 100644 go/buscompose/creates_streams_test.go delete mode 100644 go/buscompose/fixtures_test.go delete mode 100644 go/buscompose/membership.go create mode 100644 go/buscompose/names_test.go delete mode 100644 go/buscompose/onnats.go delete mode 100644 go/buscompose/process_grants.go delete mode 100644 go/buscompose/provisioner.go delete mode 100644 go/buscompose/raise.go delete mode 100644 go/buscompose/readiness.go delete mode 100644 go/buscompose/testdata/composed.conf delete mode 100644 go/buscompose/testdata/controller-credentials.json delete mode 100644 go/buscompose/testdata/user-lists.json delete mode 100644 go/buscompose/userlist_switch.go delete mode 100644 go/buscompose/userlist_switch_integration_test.go delete mode 100644 go/buscompose/writers.go diff --git a/go/buscompose/accounts.go b/go/buscompose/accounts.go index 1402804..924afc7 100644 --- a/go/buscompose/accounts.go +++ b/go/buscompose/accounts.go @@ -8,31 +8,12 @@ import ( "time" ) -// **The accounts the grants part wants, and the one shape a held account takes** (novox/hq ADR 0561 §1, §2; issue 774). -// -// The bus composer derives every user from placement, the manifests and the wants. For each account the mesh mints (a -// module's, a module's process's, a machine's provisioner client's), the grants part writes one key of its state -// `accounts`, keyed by the bus user, holding the account's fields that are not secret: what the controller's credential -// carries today beside the password. The vault watches that state, mints the user it holds no value for, and keeps the -// value as the module's own secret on that node, built by HeldCredential, which gives byte for byte the controller's -// shape, so a runtime reads it unchanged. The state is read by the vault alone (NamedReaders), as `bus-accounts` is read -// by the grants part alone. -// -// The node accounts, the controller's own, an enrolment user and a person are never wanted here: the vault mints none -// of them (ADR 0561 §4, §5). - // AccountsState is the grants part's state of wanted accounts, by bus user. const AccountsState = "accounts" // GrantsAccounts is that state as a reader names it (`.`). const GrantsAccounts = GrantsModule + "." + AccountsState -// AccountsBucket is the bucket the state lives in. -var AccountsBucket = BucketName(GrantsModule, AccountsState) - -// AccountsSubject is the subject of one wanted account's key: `$KV.mesh-grants_accounts.`. -func AccountsSubject(user string) string { return "$KV." + AccountsBucket + "." + user } - // AccountIssuedEvent is what the vault says when it minted an account: AccountIssued, never a value or a hash. const AccountIssuedEvent = "account-issued" diff --git a/go/buscompose/agreement.go b/go/buscompose/agreement.go deleted file mode 100644 index d4083ca..0000000 --- a/go/buscompose/agreement.go +++ /dev/null @@ -1,125 +0,0 @@ -package buscompose - -import ( - "fmt" - "sort" - "strings" -) - -// Do the emitters and the consumers of a catalogue agree? -// -// **The check that was missing** (novox/hq 04-ISSUES/127). Every manifest was individually -// well-formed and every derivation individually correct, and no cross-module subscription in the -// mesh matched anything: a consumer's declaration derived into a namespace nobody publishes to. -// Nothing failed, because a subscription that matches nothing is not an error — it is silence. -// -// The comparison has to be over the whole catalogue, because the two halves live in different -// manifests, and it cannot simply demand that every consumed event have a live emitter: a module -// may be installed long before the one whose events it wants. So the rule is narrower and still -// catches this: **where the emitter is present, it must emit what the consumer asked for.** - -// AConsumer is one module's interest in another's events, as this check needs it. -type AConsumer struct { - Module string - Consumes []string -} - -// AnEmitter is one module's events. -type AnEmitter struct { - Module string - Emits []string -} - -// Disagreements are the consumed events whose emitter is in the catalogue and does not emit them. -// -// Returned as sentences rather than as structs: every one of them is read by a person deciding -// whether a manifest or a catalogue is wrong, and a pair of names without the reason is a puzzle. -func Disagreements(emitters []AnEmitter, consumers []AConsumer, seats []DeclaredSeat) []string { - emits := map[string]map[string]bool{} - for _, e := range emitters { - if emits[e.Module] == nil { - emits[e.Module] = map[string]bool{} - } - for _, name := range e.Emits { - emits[e.Module][name] = true - } - } - // A seat's events are published by its holder under the seat's name, so a consumer naming the - // seat is naming something real even though no module declares it as its own. - for _, s := range seats { - if len(s.Emits) == 0 { - continue - } - if emits[s.Name] == nil { - emits[s.Name] = map[string]bool{} - } - for _, name := range s.Emits { - emits[s.Name][name] = true - } - } - - var out []string - for _, c := range consumers { - for _, pattern := range c.Consumes { - emitter, event, named := strings.Cut(pattern, ".") - // Every event from everyone, or every event from one module: both are deliberate and - // neither names a particular event to check. - if !named || emitter == "*" || emitter == catalogueTheRest || event == catalogueTheRest { - continue - } - known, present := emits[emitter] - if !present { - // Not installed here, which is ordinary: a module lives in its own repository and - // may be registered later. Nothing to compare, so nothing to say. - continue - } - if matchesAny(event, known) { - continue - } - out = append(out, fmt.Sprintf( - "%s consumes %q and %s emits %s — so that subscription would match nothing, and "+ - "nothing would report it", - c.Module, pattern, emitter, listOf(known))) - } - } - sort.Strings(out) - return out -} - -// matchesAny says whether one of an emitter's event names satisfies a consumer's pattern. -func matchesAny(pattern string, emitted map[string]bool) bool { - want := strings.Split(pattern, ".") - for name := range emitted { - if matches(want, strings.Split(name, ".")) { - return true - } - } - return false -} - -func matches(pattern, name []string) bool { - for i, part := range pattern { - if part == catalogueTheRest { - return i < len(name) - } - if i >= len(name) { - return false - } - if part != "*" && part != name[i] { - return false - } - } - return len(pattern) == len(name) -} - -func listOf(names map[string]bool) string { - if len(names) == 0 { - return "nothing" - } - out := make([]string, 0, len(names)) - for n := range names { - out = append(out, n) - } - sort.Strings(out) - return strings.Join(out, ", ") -} diff --git a/go/buscompose/announced.go b/go/buscompose/announced.go deleted file mode 100644 index bdd8c49..0000000 --- a/go/buscompose/announced.go +++ /dev/null @@ -1,74 +0,0 @@ -package buscompose - -import ( - "slices" - "sort" - "strings" - - "git.novox.be/novox/mesh-sdk/go/announce" -) - -// Who may read a module's state is what the module announced (novox/hq ADR 0495 clauses 2 and 3, ADR 0513 §3; issue -// 1790). A module says on the bus which states it keeps and which others may read (announce.Subject); the composer is -// given the last word of each (Records.Announced) and grants a reader's `reads: .` only when the owner -// announced that state readable. An owner that has announced nothing yet is read as before, from the reader's `reads` -// (the transition, until every module announces), and is named as not yet announced. - -// ReadsAnnouncedBy splits one reader module's `reads` (`.`) by what each owner announced: granted when the -// owner announced the state readable (by this reader, where it named its readers: announce.State.Readers), when the -// composer names the reader among the state's readers (NamedReaders, ADR 0547 clause 9), or when the owner announced -// nothing yet (the transition); refused otherwise, and a read named for other modules is refused. There is no judgement -// without the reader (issue 774): one made without it refused every named reader, and Users judges again for the -// reader whatever split it is given (withAnnouncedReads). An empty reader is judged by the readable mark alone. -func ReadsAnnouncedBy(reader string, reads []string, announced map[string]announce.States) (granted, refused []string) { - for _, r := range reads { - owner, name, ok := strings.Cut(r, ".") - s, said := announced[owner] - switch { - case reader != "" && !ReaderMay(reader, r): - refused = append(refused, r) - case reader != "" && namedReader(reader, r): - // A state the composer names its readers for is theirs whatever its owner announced. - granted = append(granted, r) - case reader != "" && ok && said && s.Keeps(name): - if s.ReadableBy(name, reader) { - granted = append(granted, r) - } else { - refused = append(refused, r) - } - case !ok || !said || s.Readable(name): - granted = append(granted, r) - default: - refused = append(refused, r) - } - } - return granted, refused -} - -// NotAnnounced are the owners some module reads the state of and that have announced nothing yet, sorted: read from the -// readers' `reads` as before, until they announce. -func NotAnnounced(r Records) []string { - seen := map[string]bool{} - var out []string - for _, ds := range r.Assigned { - for _, d := range ds { - for _, read := range append(append([]string(nil), d.Reads...), d.ReadsRefused...) { - owner, _, ok := strings.Cut(read, ".") - if !ok || seen[owner] { - continue - } - if _, said := r.Announced[owner]; !said { - seen[owner] = true - out = append(out, owner) - } - } - } - } - sort.Strings(out) - return out -} - -// namedReader says the composer names module among the readers of read (NamedReaders). -func namedReader(module, read string) bool { - return slices.Contains(NamedReaders[strings.TrimSpace(read)], module) -} diff --git a/go/buscompose/buildbacklog.go b/go/buscompose/buildbacklog.go deleted file mode 100644 index f62cdae..0000000 --- a/go/buscompose/buildbacklog.go +++ /dev/null @@ -1,47 +0,0 @@ -package buscompose - -import "git.novox.be/novox/mesh-sdk/go/delivery" - -// The view alone reads the build seats' queues (novox/hq ADR 0525 §2, issue 1887 A8a): the shared workers' counts and the -// oldest waiting ask, to show the backlog. It may not publish an ask: only the builder and the checker ask, by their seat use. -// The watchman is granted nothing here: a waiting ask has no pulse, a taken one is read from the build module's steps (issue -// 1908). Nothing is a wildcard over a stream or a worker. -const ( - // ViewModule is the module of the delivery view (mesh-delivery-view). - ViewModule = "mesh-delivery-view" - // BuilderModule is the module of the builder (mesh-delivery-builder). - BuilderModule = "mesh-delivery-builder" -) - -// BuildAskReads are the API subjects the builder requests when it takes over, at its start, a key naming an ask (issue -// 1910): each build seat's stream's information and its messages by sequence, to tell whether the ask is on its stream. -// Reads only: no consumer, no pull, no acknowledgement; the builder puts its asks by its seat use, as before. -func BuildAskReads() []string { - var out []string - for _, lang := range delivery.AskLanguages { - if stream, err := delivery.AskStream(lang); err == nil { - out = append(out, "$JS.API.STREAM.INFO."+stream, "$JS.API.STREAM.MSG.GET."+stream) - } - } - return out -} - -// BuildBacklogReads are the API subjects the view requests: the information of each build seat's stream and of each of its -// shared workers (delivery.AskStream, delivery.AskWorker), and the message of the stream by sequence. Reads only: no -// message is pulled, none acknowledged, none published. -func BuildBacklogReads() []string { - var out []string - for _, lang := range delivery.AskLanguages { - stream, err := delivery.AskStream(lang) - if err != nil { - continue - } - out = append(out, "$JS.API.STREAM.INFO."+stream, "$JS.API.STREAM.MSG.GET."+stream) - for _, kind := range delivery.AskKinds { - if w, err := delivery.AskWorker(lang, kind); err == nil { - out = append(out, "$JS.API.CONSUMER.INFO."+stream+"."+w) - } - } - } - return out -} diff --git a/go/buscompose/buscompose_test.go b/go/buscompose/buscompose_test.go deleted file mode 100644 index d31d1e6..0000000 --- a/go/buscompose/buscompose_test.go +++ /dev/null @@ -1,880 +0,0 @@ -package buscompose - -import ( - "encoding/json" - "maps" - "os" - "path/filepath" - "reflect" - "slices" - "strings" - "testing" - "time" - - "git.novox.be/novox/mesh-sdk/go/announce" - "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" - "github.com/nats-io/nats.go/jetstream" -) - -// Issue 1126 B3a (novox/hq ADR 0590): the composed user lists and the server configuration are golden files this -// composer wrote (testdata/user-lists.json, testdata/composed.conf), first from the lists the ended controller composed -// with its user and the lease's grants taken out. A change to what any user is granted shows here as a diff to read; -// MESH_SDK_UPDATE_GOLDEN=1 writes them anew. -var updateGolden = os.Getenv("MESH_SDK_UPDATE_GOLDEN") == "1" - -// golden compares got with the golden file name, or writes it. -func golden(t *testing.T, name string, got []byte) { - t.Helper() - path := filepath.Join("testdata", name) - if updateGolden { - if err := os.WriteFile(path, got, 0o644); err != nil { - t.Fatal(err) - } - return - } - want, err := os.ReadFile(path) - if err != nil { - t.Fatal(err) - } - if string(got) != string(want) { - t.Errorf("%s differs from what is composed now; read the diff and, if it is meant, write it with MESH_SDK_UPDATE_GOLDEN=1", path) - } -} - -const SignersBucketName = "mesh-vault_signers" - -func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) { - // 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"), " ") - for bucket, granted := range map[string]bool{delivery.PlacementBucket: true, delivery.PlacementCandidatesBucket: true, - delivery.PlacementProposalsBucket: false} { - if strings.Contains(reads, "$JS.API.DIRECT.GET.KV_"+bucket+".>") != granted || strings.Contains(reads, "$KV."+bucket+".laptop") { - t.Errorf("a node is granted %s: %v, want %v", bucket, !granted, granted) - } - } - // It hears the creation of those two buckets, and of no other. - if got := NodeComposeAdvisories(); len(got) != 2 || got[0] != "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement" || - got[1] != "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates" { - t.Errorf("a node hears the creation of %v", got) - } - type list struct { - Name, List string - Missing []string - Err string - Memberships json.RawMessage - } - var lists []list - for _, f := range UserListFixtures() { - l, missing, err := ComposedFor(f) - row := list{Name: f.Name, List: l, Missing: missing} - if err != nil { - row.Err = err.Error() - } - if row.Memberships, err = json.Marshal(MembershipsFor(f)); err != nil { - t.Fatal(err) - } - // B3a/AC1: no list holds a user for the ended controller. - if strings.Contains(l, `user: "controller"`) { - t.Errorf("%s: the list holds the controller's user", f.Name) - } - lists = append(lists, row) - } - raw, err := json.MarshalIndent(lists, "", " ") - if err != nil { - t.Fatal(err) - } - golden(t, "user-lists.json", append(raw, '\n')) - // An asker on a seat pulled by kind hears what that seat's workers gave up on, and no other stream's dead letters - // (novox/hq issue 734: a check ask given up during a bus reload left its try running for ever). - t.Run("an asker reads its seat's dead letters", func(t *testing.T) { - build := Seat{Name: "node-build-go", Scope: "node", Accepts: []string{"delivery", "check", "recheck"}, PulledByKind: true} - perms, err := PermissionsFor(Principal{Kind: KindModule, Node: "one", Module: "mesh-delivery-checker", Uses: []Seat{build}}) - if err != nil { - t.Fatal(err) - } - for subject, want := range map[string]bool{ - "$JS.API.CONSUMER.CREATE.DEAD_LETTERS.x1.mesh.events.dead.SEAT_NODE_BUILD_GO.>": true, - "$JS.API.CONSUMER.CREATE.DEAD_LETTERS.x1.mesh.events.dead.SEAT_NODE_BUILD_GO.w.next": true, - "$JS.API.CONSUMER.DELETE.DEAD_LETTERS.x1": true, - "$JS.API.CONSUMER.MSG.NEXT.DEAD_LETTERS.x1": true, - "$JS.API.CONSUMER.CREATE.DEAD_LETTERS.x1.mesh.events.dead.EVENTS.>": false, - "$JS.API.CONSUMER.CREATE.DEAD_LETTERS.x1": false, - "$JS.API.STREAM.PURGE.DEAD_LETTERS": false, - "$JS.FC.DEAD_LETTERS.x1": false, - } { - if got := MayPublish(perms, subject); got != want { - t.Errorf("may publish %s: %v, want %v", subject, got, want) - } - } - plain, err := PermissionsFor(Principal{Kind: KindModule, Node: "one", Module: "shop", - Uses: []Seat{{Name: "telegram-sender", Accepts: []string{"send"}}}}) - if err != nil { - t.Fatal(err) - } - if MayPublish(plain, "$JS.API.CONSUMER.DELETE.DEAD_LETTERS.x1") { - t.Error("a user of a seat not pulled by kind reads DEAD_LETTERS") - } - }) - // ADR 0563 §1, §3, §6: each node's engine alone answers its own node's node-engine verbs and says its events, any - // runtime may call them, and it asks the operator under node-engine- alone. - t.Run("a node's engine holds its node seat and asks in its own name", func(t *testing.T) { - router := Seat{Name: AsksSeat, Accepts: []string{"ask", "cancel"}, Emits: []string{"decided"}, - ByCaller: []string{"ask", "cancel", "decided"}, Records: []string{"messenger_asks"}} - users, err := Users(Records{Nodes: []string{"one", "two"}, Assigned: map[string][]Declared{ - "one": {{Module: "messenger", Holds: []Seat{router}, Invokes: []string{"seat:" + EngineSeat + ".root-free"}}}, - "two": {{Module: RuntimeModule}, {Module: "shop", Invokes: []string{"*"}}}}}) - if err != nil { - t.Fatal(err) - } - for _, u := range users { - perms, err := PermissionsFor(u) - if err != nil { - t.Fatal(err) - } - answers := MaySubscribe(perms, "mesh.seat."+EngineSeat+".tool.root-free.one") || - MaySubscribe(perms, "mesh.seat."+EngineSeat+".call.root-free.one.messenger~one") - if engineOne := u.Kind == KindNode && u.Node == "one"; answers != engineOne { - t.Errorf("%s answers one's root-free: %v", u.Username(), answers) - } - switch { - case u.Kind == KindNode && u.Node == "one": - for subject, want := range map[string]bool{ - "mesh.seat.operator-channel.accept.ask.node-engine-one": true, - "mesh.seat.operator-channel.accept.cancel.node-engine-one": true, - "mesh.seat.operator-channel.accept.ask.node-engine-two": false, - "$JS.API.DIRECT.GET.KV_messenger_asks.$KV.messenger_asks.node-engine-one.hand-over-1": true, - "$JS.API.DIRECT.GET.KV_messenger_asks.$KV.messenger_asks.node-engine-two.hand-over-1": false, - "mesh.seat." + EngineSeat + ".event.done.one": true, - "mesh.seat." + EngineSeat + ".event.done.two": false, - "$SRV.INFO": true, - } { - if got := MayPublish(perms, subject); got != want { - t.Errorf("node one may publish %s: %v, want %v", subject, got, want) - } - } - if !MaySubscribe(perms, "mesh.seat.operator-channel.event.decided.node-engine-one") || - MaySubscribe(perms, "mesh.seat.operator-channel.event.decided.node-engine-two") || - !MaySubscribe(perms, "$SRV.INFO."+EngineSeat) || MaySubscribe(perms, "mesh.seat."+EngineSeat+".tool.hand-over.two") { - t.Errorf("node one hears %v", perms.Subscribe) - } - // A verb or an event the seat's contract gains later is its engine's on its own node without a release of - // the grants part (2026-10-31: mesh-host #184's optional verb composition was refused on every node, the - // grants part's mesh-sdk pin predating it): the seat's subjects on its node, any verb, any event. - for _, verb := range []string{"composition", "compose", "a-verb-added-later"} { - if !MaySubscribe(perms, "mesh.seat."+EngineSeat+".tool."+verb+".one") || - !MaySubscribe(perms, "mesh.seat."+EngineSeat+".call."+verb+".one.messenger~one") || - MaySubscribe(perms, "mesh.seat."+EngineSeat+".tool."+verb+".two") || - MaySubscribe(perms, "mesh.seat."+EngineSeat+".call."+verb+".two.messenger~one") { - t.Errorf("node one does not answer %s on its own node alone: %v", verb, perms.Subscribe) - } - } - if !MayPublish(perms, "mesh.seat."+EngineSeat+".event.an-event-added-later.one") || - MayPublish(perms, "mesh.seat."+EngineSeat+".event.an-event-added-later.two") { - t.Errorf("node one does not say a later event on its own node alone: %v", perms.Publish) - } - case u.Kind == KindNodeTools || u.Module == "messenger": - if !MayPublish(perms, "mesh.seat."+EngineSeat+".tool.root-free.one") { - t.Errorf("%s cannot call one's root-free", u.Username()) - } - } - } - }) - // Issue 1126 (ADR 0550 §5): a node's engine asks the vault's `own` in its own name while the vault serves it, and no - // other node's. - t.Run("a node asks the vault its own secrets", func(t *testing.T) { - one := Principal{Kind: KindNode, Node: "one", AsksItsOwnSecrets: true} - perms, err := PermissionsFor(one) - if err != nil { - t.Fatal(err) - } - if !MayPublish(perms, VaultOwnCall(one)) || MayPublish(perms, VaultOwnCall(Principal{Kind: KindNode, Node: "two"})) { - t.Errorf("node one asks own as %v, and as two %v", MayPublish(perms, VaultOwnCall(one)), - MayPublish(perms, VaultOwnCall(Principal{Kind: KindNode, Node: "two"}))) - } - if plain, _ := PermissionsFor(Principal{Kind: KindNode, Node: "one"}); MayPublish(plain, VaultOwnCall(one)) { - t.Error("a node asks own while the vault does not serve it") - } - if !slices.Contains(perms.Subscribe, VaultSecretChanged) { - t.Errorf("node one does not hear the vault's secret-changed: %v", perms.Subscribe) - } - // L3 s6 (ADR 0550 §2): while the vault serves `sign`, a node asks it in its own name alone, and reads the vault's - // `signers`, never writing it; the ended controller's lease it reads no more (ADR 0590). - signs, _ := PermissionsFor(Principal{Kind: KindNode, Node: "one", AsksTheVaultToSign: true}) - signersGet := "$JS.API.DIRECT.GET.KV_" + SignersBucketName + ".$KV." + SignersBucketName + ".key" - for subject, want := range map[string]bool{VaultSignCall(one): true, signersGet: true, - "$JS.API.DIRECT.GET.KV_mesh-controller_lease.$KV.mesh-controller_lease.holder": false, - VaultSignCall(Principal{Kind: KindNode, Node: "two"}): false, "$KV." + SignersBucketName + ".key": false} { - if MayPublish(signs, subject) != want { - t.Errorf("a node asking sign may publish %s: %v, want %v", subject, !want, want) - } - } - if MayPublish(perms, VaultSignCall(one)) || MayPublish(perms, signersGet) { - t.Error("a node asks sign or reads signers while the vault does not serve sign") - } - // And what a module on another node serves from its settings, that key alone (mailu's smtp domain, issue 1126). - users, err := Users(Records{Nodes: []string{"one", "two"}, Assigned: map[string][]Declared{ - "two": {{Module: "mailer", Served: []string{"domain", "not.a.token"}}}}}) - if err != nil { - t.Fatal(err) - } - get := func(key string) string { - return "$JS.API.DIRECT.GET.KV_" + ResolvedBucket + ".$KV." + ResolvedBucket + "." + key - } - for _, u := range users { - if u.Kind != KindNode { - continue - } - perms, err := PermissionsFor(u) - if err != nil { - t.Fatal(err) - } - if u.Node == "one" && (!reflect.DeepEqual(u.ServedReads, []string{"two.mailer.domain"}) || !MayPublish(perms, get("two.mailer.domain")) || - MayPublish(perms, get("two.mailer.sitename"))) { - t.Errorf("node one reads %v of two's resolved values", u.ServedReads) - } - if u.Node == "two" && len(u.ServedReads) != 0 { - t.Errorf("node two is granted its own served keys apart: %v", u.ServedReads) - } - } - }) - // 774/S39 (ADR 0583 §1): while a node rotates, both its logins are users with the node's permissions and its one - // inbox, each calling in its own name; every node reads its own node-logins key alone; with no rotation the list - // is the one above (AC2: the controller's golden lists, unchanged). - t.Run("a node rotating has two logins of one node", func(t *testing.T) { - vault := Seat{Name: VaultSeat, Serves: []string{"rotate-node", "node-account-confirm", "sign", "own"}} - since := time.Date(2026, 11, 2, 1, 0, 0, 0, time.UTC) - users, err := Users(Records{Nodes: []string{"ace", "g14"}, - Assigned: map[string][]Declared{"ace": {{Module: "mesh-vault", Holds: []Seat{vault}}}}, - NodeLogins: map[string]delivery.NodeLogin{ - "g14": {Node: "g14", Login: "node.g14", Rotating: "node.g14--b", Stage: delivery.RotationBegun, Since: since}, - // A node that rotated before and rotates no more logs in as its second login alone. - "ace": {Node: "ace", Login: "node.ace--b", Since: since}}}) - if err != nil { - t.Fatal(err) - } - perms := map[string]Permissions{} - for _, u := range users { - if u.Kind == KindNode { - p, err := PermissionsFor(u) - if err != nil { - t.Fatal(err) - } - perms[u.Username()] = p - } - } - if len(perms) != 3 || perms["node.g14"].Publish == nil || perms["node.g14--b"].Publish == nil || perms["node.ace--b"].Publish == nil { - t.Fatalf("the node users are %v", slices.Sorted(maps.Keys(perms))) - } - // Equal, but for the caller each names in its calls (the bus's proof of which login called, ADR 0583 §3). - own := func(p Permissions, login string) Permissions { - swap := func(in []string) []string { - out := make([]string, len(in)) - for i, s := range in { - out[i] = strings.ReplaceAll(s, CallerToken(login), "") - } - return out - } - return Permissions{Publish: swap(p.Publish), PublishDeny: swap(p.PublishDeny), Subscribe: swap(p.Subscribe), - AllowResponses: p.AllowResponses} - } - if a, b := own(perms["node.g14"], "node.g14"), own(perms["node.g14--b"], "node.g14--b"); !reflect.DeepEqual(a, b) { - t.Errorf("the two logins' permissions differ:\n%v\n%v", a, b) - } - for login, p := range perms { - node, _ := delivery.LoginNode(login) - if !slices.Contains(p.Subscribe, "_INBOX.node."+node+".>") || slices.Contains(p.Subscribe, "_INBOX."+login+".>") && login != "node."+node { - t.Errorf("%s's inbox: %v", login, p.Subscribe) - } - } - // AC3: g14 reads node-logins key g14 and not ace, and writes neither; it confirms in its own login's name. - get := func(key string) string { - return "$JS.API.DIRECT.GET.KV_" + delivery.NodeLoginsBucket + ".$KV." + delivery.NodeLoginsBucket + "." + key - } - confirm := func(login string) string { - return "mesh.seat.mesh-vault.call.node-account-confirm." + CallerToken(login) - } - for subject, want := range map[string]bool{get("g14"): true, get("ace"): false, "$KV." + delivery.NodeLoginsBucket + ".g14": false, - confirm("node.g14--b"): true, confirm("node.g14"): false} { - if MayPublish(perms["node.g14--b"], subject) != want { - t.Errorf("node.g14--b may publish %s: %v, want %v", subject, !want, want) - } - } - if plain, _ := PermissionsFor(Principal{Kind: KindNode, Node: "g14"}); MayPublish(plain, get("g14")) { - t.Error("a node reads node-logins while the vault does not rotate") - } - // And nothing but the grants part names node-logins among its reads. - if !ReaderMay(GrantsModule, delivery.NodeLoginsRead) || ReaderMay("shop", delivery.NodeLoginsRead) { - t.Error("node-logins' named readers") - } - }) - // 774/S40/AC1 (ADR 0587 §4, §6, ADR 0590): an enrolling node's user may publish the redeem call in its own name and - // nothing else, and subscribe its own inbox alone. - t.Run("an enrolling node redeems at the seat alone", func(t *testing.T) { - holder := Seat{Name: catalogue.EnrolmentSeat, Serves: []string{"issue", "redeem", "confirm", "revoke", "tokens", "again"}} - { - users, err := Users(Records{Nodes: []string{"ace"}, Enrolling: []string{"lab"}, - Assigned: map[string][]Declared{"ace": {{Module: "mesh-enrolment", Holds: []Seat{holder}}}}}) - if err != nil { - t.Fatal(err) - } - var lab *Principal - for i := range users { - if users[i].Username() == "enrol.lab" { - lab = &users[i] - } - } - if lab == nil { - t.Fatal("no user enrol.lab") - } - perms, err := PermissionsFor(*lab) - if err != nil { - t.Fatal(err) - } - want := []string{"mesh.seat.mesh-enrolment.call.redeem.enrol~lab"} - if !reflect.DeepEqual(perms.Publish, want) || !reflect.DeepEqual(perms.Subscribe, []string{"_INBOX.enrol.lab.>"}) || - perms.AllowResponses { - t.Errorf("enrol.lab publishes %v, subscribes %v, answers %v", perms.Publish, perms.Subscribe, perms.AllowResponses) - } - } - // No holder serving redeem: the user may enrol nowhere, refused by name. - if _, err := PermissionsFor(Principal{Kind: KindEnrolment, Node: "lab"}); err == nil { - t.Error("an enrolment user that may enrol nowhere was composed") - } - }) - // 774/S41/AC1 (ADR 0587 §1-§3, ADR 0561 §5): mesh-enrolment writes its bucket (with per-key expiry) and calls the - // vault's enrolment-token, node-account and sign in its own name; no other module or runtime may call those two, - // however wide its grant; and its bucket is read by the grants part alone besides it. - t.Run("mesh-enrolment alone asks the vault to enrol", func(t *testing.T) { - vault := Seat{Name: VaultSeat, Serves: []string{"sign", "enrolment-token", "node-account"}} - holder := Seat{Name: catalogue.EnrolmentSeat, Serves: []string{"issue", "redeem", "confirm", "revoke", "tokens", "again"}} - calls := []string{"seat:mesh-vault.enrolment-token", "seat:mesh-vault.node-account", "seat:mesh-vault.sign"} - users, err := Users(Records{Nodes: []string{"ace"}, Assigned: map[string][]Declared{"ace": { - {Module: "mesh-vault", Holds: []Seat{vault}}, - // One process with a bus account of its own (ADR 0587 §1): never carried by the tool runner. - {Module: "mesh-enrolment", RunsAs: "mesh-enrolment", Holds: []Seat{holder}, Invokes: calls, - State: []Bucket{{Module: "mesh-enrolment", Name: "enrolments", KeyTTL: true}}}, - {Module: "shop", Invokes: calls[:2]}, - {Module: RuntimeModule}, {Module: "agent-tools", Invokes: []string{"*"}}}}}) - if err != nil { - t.Fatal(err) - } - call := func(verb, user string) string { return "mesh.seat.mesh-vault.call." + verb + "." + CallerToken(user) } - for _, u := range users { - perms, err := PermissionsFor(u) - if err != nil { - t.Fatal(err) - } - name := u.Username() - for _, verb := range []string{"enrolment-token", "node-account"} { - enrols := name == "ace.mesh-enrolment" - if MayPublish(perms, call(verb, name)) != enrols || (!enrols && MayPublish(perms, "mesh.seat.mesh-vault.tool."+verb)) { - t.Errorf("%s may call %s: %v", name, verb, !enrols) - } - } - if name == "ace.mesh-enrolment" && (!MayPublish(perms, call("sign", name)) || - !MayPublish(perms, "$KV."+delivery.EnrolmentsBucket+".node.lab") || !MayPublish(perms, "$KV."+delivery.EnrolmentsBucket+".address.10.0.0.7")) { - t.Errorf("mesh-enrolment does not sign or write its bucket: %v", perms.Publish) - } - if name != "ace.mesh-enrolment" && MayPublish(perms, "$KV."+delivery.EnrolmentsBucket+".node.lab") { - t.Errorf("%s writes the enrolments", name) - } - } - if !ReaderMay(GrantsModule, delivery.EnrolmentsRead) || ReaderMay("shop", delivery.EnrolmentsRead) { - t.Error("the enrolments' named readers") - } - }) - t.Run("the golden server configuration", func(t *testing.T) { - seat := Seat{Name: "telegram-sender", Accepts: []string{"send"}, Emits: []string{"delivered", "failed"}} - got, err := Compose( - Server{ClientPort: 4222, MonitoringPort: 8222, StoreDir: "/data", - TLSCert: "/tls/tls.crt", TLSKey: "/tls/tls.key", TLSCA: "/tls/ca.crt"}, - []Principal{ - {Kind: KindEnrolment, Node: "one", Redeems: true, PasswordHash: "$2a$11$eeeeeeeeeeeeeeeeeeeeee"}, - {Kind: KindNode, Node: "one", PasswordHash: "$2a$11$nnnnnnnnnnnnnnnnnnnnnn"}, - {Kind: KindModule, Node: "one", Module: "telegram", Holds: []Seat{seat}, - Serves: []string{"status"}, PasswordHash: "$2a$11$tttttttttttttttttttttt"}, - {Kind: KindModule, Node: "two", Module: "shop", Uses: []Seat{seat}, - Emits: []string{"order.placed"}, PasswordHash: "$2a$11$ssssssssssssssssssssss"}, - {Kind: KindModule, Node: "two", Module: "audit", - Consumes: []string{"shop.order.placed"}, PasswordHash: "$2a$11$aaaaaaaaaaaaaaaaaaaaaa"}, - {Kind: KindModule, Node: "one", Module: WatchmanModule, PasswordHash: "$2a$11$wwwwwwwwwwwwwwwwwwwwww"}, - {Kind: KindModule, Node: "one", Module: "nats", SnapshotsTheBus: true, - Serves: []string{"nats_streams"}, PasswordHash: "$2a$11$bbbbbbbbbbbbbbbbbbbbbb"}, - }) - if err != nil { - t.Fatal(err) - } - golden(t, "composed.conf", []byte(got)) - }) - // 774/AC2 (novox/hq ADR 0561 §2): a wanted account round-trips through the grants part's state `accounts`, keyed by - // its user, at the subject `$KV.mesh-grants_accounts.`, read by the vault alone; and the vault's account-issued - // names the account, never a value. - t.Run("a wanted account is kept by its user and read by the vault alone", func(t *testing.T) { - for _, w := range []WantedAccount{ - {User: "ace.mesh-build-go", Kind: KindModule, Node: "ace", Module: "mesh-build-go", URL: "nats://novox.mesh:4222", - Fingerprint: "ab", Claims: []ClaimedSeat{{Seat: "node-build-go", Scope: "node", Serves: []string{"build"}}}}, - {User: "novox.mesh-mail.send", Kind: KindProcess, Node: "novox", Module: "mesh-mail", Process: "send", - URL: "nats://novox.mesh:4222"}, - {User: "provisioner.g14", Kind: KindProvisioner, Node: "g14", Module: "mesh-provisioner-client", - URL: "nats://novox.mesh:4222", Fingerprint: "ab"}, - } { - if err := w.Valid(); err != nil { - t.Fatalf("%s: %v", w.User, err) - } - raw, err := json.Marshal(w) - if err != nil { - t.Fatal(err) - } - back, err := ReadWantedAccount(w.User, raw) - if err != nil || !back.Same(w) { - t.Errorf("%s does not round-trip: %+v, %v", w.User, back, err) - } - if got := AccountsSubject(w.User); got != "$KV.mesh-grants_accounts."+w.User { - t.Errorf("%s is kept at %s", w.User, got) - } - if _, err := ReadWantedAccount("someone.else", raw); err == nil { - t.Errorf("%s was read under another user's key", w.User) - } - } - for _, bad := range []WantedAccount{ - {User: "ace.x", Kind: KindModule, Node: "ace", Module: "y", URL: "nats://a:1"}, - {User: "ace.x.p", Kind: KindProcess, Node: "ace", Module: "x", URL: "nats://a:1"}, - {User: "node.ace", Kind: KindNode, Node: "ace", URL: "nats://a:1"}, - {User: "controller", Kind: "controller", URL: "nats://a:1"}, - {User: "ace.x", Kind: KindModule, Node: "ace", Module: "x"}, - } { - if bad.Valid() == nil { - t.Errorf("%+v is taken as a wanted account", bad) - } - } - if ReaderMay("mesh-delivery-assigner", GrantsAccounts) || !ReaderMay(VaultSeat, GrantsAccounts) { - t.Errorf("%s is read by %v alone, want the vault's", GrantsAccounts, NamedReaders[GrantsAccounts]) - } - e := AccountIssued{User: "ace.x", Kind: KindModule, Node: "ace", Module: "x", Generation: 1} - raw, _ := json.Marshal(e) - if strings.Contains(string(raw), "password") || strings.Contains(string(raw), "hash") || - AccountIssuedSubject != "mesh.mod.mesh-vault.event.account-issued" { - t.Errorf("account-issued says %s at %s", raw, AccountIssuedSubject) - } - }) - // 774/AC3 (novox/hq ADR 0561 §2): the account the vault seals is, byte for byte, what the controller hands for each of - // its three kinds: testdata/controller-credentials.json was written by the controller's own code (its "how"). - t.Run("a held account is the controller's shape byte for byte", func(t *testing.T) { - raw, err := os.ReadFile(filepath.Join("testdata", "controller-credentials.json")) - if err != nil { - t.Fatal(err) - } - var captured struct { - Rows []struct{ Name, Held string } - } - if err := json.Unmarshal(raw, &captured); err != nil { - t.Fatal(err) - } - vault := ClaimedSeat{Seat: "mesh-vault", Scope: "mesh", Serves: []string{"get", "accept", "secret-ask", "issued", - "rotate", "move-in", "node-key", "own", "move-asks"}} - controller := ClaimedSeat{Seat: "mesh-controller", Scope: "mesh", Serves: strings.Split("tools,calls,status,nodes,"+ - "node,placement-export,modules,seats,builds,plans,delivery-plan,delivery-order,delivery-check,deliver,"+ - "delivery-stop,delivery-backups-again,delivery-walks,plan,assign,unassign,pin,unpin,push,rotate,give,issued,"+ - "secret-move,secret-ask,issue,token,settings,command,queue,cancel,clear,replay,try,kill,pause,resume,hand-act,"+ - "drill,warranted,hand-acts,durations,conditions,healers,doctor,upgrade,bus,retire,cleanup,judge-directories,"+ - "root-free,dead-letters,data,build,artifacts,held,collect,move-placement,move-config,mirrors,images", ",")} - at := "nats://10.0.0.1:4222" - wanted := map[string]struct { - w WantedAccount - password string - }{ - "module": {WantedAccount{User: "laptop.cap-bot", Kind: KindModule, Node: "laptop", Module: "cap-bot", URL: at, - Fingerprint: "ab:cd", Claims: []ClaimedSeat{vault, {Seat: "node-service-manager", Scope: "node", - Serves: []string{"status"}}, controller, {Seat: "no-such-seat", Scope: "node"}}}, "pw-module"}, - "module-no-claims-no-fingerprint": {WantedAccount{User: "laptop.cap-plain", Kind: KindModule, Node: "laptop", - Module: "cap-plain", URL: at}, "pw-plain"}, - "provisioner": {WantedAccount{User: "provisioner.laptop", Kind: KindProvisioner, Node: "laptop", Module: "nats", - URL: at, Fingerprint: "ab:cd"}, "pw-prov"}, - "process": {WantedAccount{User: "laptop.mesh-mail.send", Kind: KindProcess, Node: "laptop", Module: "mesh-mail", - Process: "send", URL: at, Fingerprint: "ab:cd"}, "pw-proc"}, - } - if len(captured.Rows) != len(wanted) { - t.Fatalf("%d rows captured, %d wanted", len(captured.Rows), len(wanted)) - } - for _, row := range captured.Rows { - c, ok := wanted[row.Name] - if !ok { - t.Fatalf("no wanted account for the captured %s", row.Name) - } - got, err := HeldCredential(c.w, c.password) - if err != nil || string(got) != row.Held { - t.Errorf("%s:\n got %s (%v)\nwant %s", row.Name, got, err, row.Held) - } - } - if _, err := HeldCredential(wanted["module"].w, ""); err == nil { - t.Error("an account was held with no password") - } - }) - // 774 subtask 8 (novox/hq ADR 0561 §6): a module's consumer is created as the grants part creates it, and every read is - // judged for its reader. - t.Run("every read is judged for its reader; a consumer is created as the grants part creates it", func(t *testing.T) { - announced := map[string]announce.States{} - // Issue 774 (sweep item 2): Users judges every read for its reader again, whatever split the caller made: a - // reader-less split refused the vault the grants part's accounts it is named for. - announced["mesh-grants"] = announce.States{Module: "mesh-grants", States: []announce.State{{Name: "accounts", Readers: []string{VaultSeat}}}} - announced["disk-load"] = announce.States{Module: "disk-load", States: []announce.State{{Name: "readings"}}} - judged, err := Users(Records{Nodes: []string{"a"}, Announced: announced, Assigned: map[string][]Declared{"a": { - {Module: VaultSeat, ReadsRefused: []string{GrantsAccounts}}, - {Module: "restic", ReadsRefused: []string{"disk-load.readings"}}, - {Module: "messenger", Reads: []string{GrantsAccounts}}}}}) - if err != nil { - t.Fatal(err) - } - reads := map[string]string{} - for _, u := range judged { - if u.Kind == KindModule { - reads[u.Module] = strings.Join(u.Reads, ",") - } - } - if reads[VaultSeat] != GrantsAccounts || reads["restic"] != "" || reads["messenger"] != "" { - t.Errorf("the reads granted for their readers: %v", reads) - } - // AccountsEntry is the very lines ComposeAccounts writes for a user, so a writer of kept users repeats none. - filled, _ := WithPasswords(judged, map[string]string{judged[0].Username(): "$2a$11$h"}) - list, err := ComposeAccounts(filled[:1]) - perms, perr := PermissionsFor(filled[0]) - if err != nil || perr != nil || !strings.Contains(list, " users = [\n"+AccountsEntry(filled[0].Username(), "$2a$11$h", perms)+" ]\n") { - t.Errorf("the accounts entry is not ComposeAccounts' own: %v %v\n%s", err, perr, list) - } - c, _ := ConsumerFor(Principal{Kind: KindModule, Node: "a", Module: "m", Consumes: []string{"x.happened"}}) - cfg := ConsumerConfig(c) - if cfg.Durable != "a_m" || cfg.AckPolicy != jetstream.AckExplicitPolicy || cfg.AckWait != 30*time.Second || - cfg.MaxDeliver != 5 || cfg.DeliverPolicy != jetstream.DeliverNewPolicy || len(cfg.FilterSubjects) != 2 || - cfg.Description == "" { - t.Errorf("a module's consumer is created as %+v", cfg) - } - // The grants part is granted the create of each account's consumer by its stream and name, and no other. - users, err := Users(Records{Nodes: []string{"a"}, Assigned: map[string][]Declared{"a": { - {Module: GrantsModule}, {Module: "m", Consumes: []string{"x.happened"}}, {Module: "quiet"}}}}) - if err != nil { - t.Fatal(err) - } - for _, u := range users { - if u.Module != GrantsModule || u.Kind != KindModule { - continue - } - perms, err := PermissionsFor(u) - if err != nil { - t.Fatal(err) - } - if !MayPublish(perms, "$JS.API.CONSUMER.CREATE.EVENTS.a_m") || MayPublish(perms, "$JS.API.CONSUMER.CREATE.EVENTS.a_quiet") || - MayPublish(perms, "$JS.API.CONSUMER.DELETE.EVENTS.a_m") || MayPublish(perms, "$JS.API.CONSUMER.CREATE.EVENTS.a_m.mesh.mod.x.event.happened") { - t.Errorf("the grants part creates consumers as %v", u.CreatesConsumers) - } - } - // 1952 S5 (novox/hq ADR 0564 clauses 2, 5, 8; Limits 2, 4, 8): the health holders' credentials. - healthChecks(t) - q := ConsumerConfig(QueueConsumer(Principal{Kind: KindProcess, Node: "a", Module: "m", Process: "p"}, - QueueOwner{Process: "p", Stream: "M_WORK", MaxDeliver: 3})) - if q.Durable != "a_m_p" || q.FilterSubject != "" || q.MaxDeliver != 3 || q.DeliverPolicy != jetstream.DeliverAllPolicy { - t.Errorf("a process's queue consumer is created as %+v", q) - } - }) -} - -// healthChecks are the health holders' credential cases (ADR 0564 clauses 2, 5, 8; Limits 2, 4, 8; issue 1952 S5). -func healthChecks(t *testing.T) { - t.Helper() - announced := map[string]announce.States{ - "mesh-healers": {Module: "mesh-healers", States: []announce.State{{Name: "attempts", Readable: true}, {Name: "would", Readable: true}}}} - for _, c := range []struct { - reader, read string - may bool - }{ - {"mesh-conditions", "mesh-healers.attempts", true}, {"mesh-conditions", HealersWould, false}, {"mesh-delivery-view", HealersWould, false}, - // ADR 0577 clause 1: the node-engine's data items are disk-load's alone to read. - {"disk-load", delivery.EngineDataRead, true}, {"restic", delivery.EngineDataRead, false}, - } { - granted, _ := ReadsAnnouncedBy(c.reader, []string{c.read}, announced) - if (len(granted) == 1) != c.may { - t.Errorf("%s reads %s: %v, want %v", c.reader, c.read, granted, c.may) - } - } - bucket := func(m, n string) Bucket { return Bucket{Module: m, Name: n} } - users, err := Users(Records{Nodes: []string{"a"}, Assigned: map[string][]Declared{"a": { - {Module: "mesh-conditions", State: []Bucket{bucket("mesh-conditions", "conditions"), bucket("mesh-conditions", "takeover")}, - Reads: []string{"mesh-self-check.findings", "mesh-healers.attempts"}}, - {Module: "mesh-self-check", State: []Bucket{bucket("mesh-self-check", "findings")}, Reads: []string{"restic.good"}}, - // Issue 1955 L7: the grants part keeps the hearings it withholds; the self-check reads them. - {Module: GrantsModule, State: []Bucket{bucket(GrantsModule, GrantsWithheldState)}}, - {Module: "mesh-healers", State: []Bucket{bucket("mesh-healers", "findings"), bucket("mesh-healers", "attempts"), bucket("mesh-healers", "would")}}, - {Module: WatchmanModule, State: []Bucket{bucket(WatchmanModule, "findings")}, Reads: []string{delivery.NodeHealthRead}}, - {Module: "mesh-hand-acts", State: []Bucket{bucket("mesh-hand-acts", "warrants")}, Logs: []Log{{Module: "mesh-hand-acts", Name: "acts"}}}, - // ADR 0570 clauses 1, 8 (issue 1954 L6-P5): mesh-provisioner keeps provisions and plans; the vault and a provider - // read them. - {Module: "mesh-provisioner", State: []Bucket{bucket("mesh-provisioner", "provisions"), bucket("mesh-provisioner", "plans"), - bucket("mesh-provisioner", "registry")}}, - {Module: "mesh-vault", Reads: []string{"mesh-provisioner.provisions", "mesh-provisioner.plans"}}, - {Module: "postgres", Reads: []string{"mesh-provisioner.provisions", "mesh-provisioner.plans"}}, - // The node's provisioner client reads its machine's plans and records no needs (ADR 0570 clause 2; its V3). - {Module: "mesh-provisioner-client", ProvisionsNode: true}, - // ADR 0571 clauses 2, 4 (issue 1955): the seat mesh-artifact-sweep says artifacts-gone; the assigner keeps seat-holders. - {Module: "artifact-store-tools", Holds: []Seat{{Name: "mesh-artifact-sweep", Emits: []string{"artifacts-gone"}}}, - Reads: []string{delivery.SeatHoldersRead}}, - {Module: delivery.AssignerModule, State: []Bucket{bucket(delivery.AssignerModule, delivery.SeatHoldersState)}}, - // ADR 0571 clause 3: disk-load keeps the readings; the backup holder reads them. - // ADR 0577 clause 1: and it reads its own node's keys of the node-engine's data items, which restic may not. - {Module: "disk-load", State: []Bucket{bucket("disk-load", "readings")}, Reads: []string{delivery.EngineDataRead}}, - {Module: "restic", Reads: []string{"disk-load.readings", delivery.EngineDataRead}, State: []Bucket{bucket("restic", "good")}}, - // ADR 0571 clauses 1, 9: the indexer reads the builder's log through its own consumer; the catalogue module asks - // the builder's tool. - {Module: delivery.BuilderModule, Logs: []Log{{Module: delivery.BuilderModule, Name: delivery.BuilderLog, MaxAgeDays: 30}}}, - {Module: delivery.IndexerModule, Reads: []string{delivery.BuildLogRead}}, - {Module: "mesh-catalog", Invokes: []string{delivery.BuilderModule + "." + delivery.BuildsSinceTool}}, - {Module: "telegram", Holds: []Seat{{Name: "telegram-sender", Accepts: []string{"send"}}}, - Processes: []ProcessPrincipal{{Process: "w", Queues: []QueueOwner{{Process: "w", Stream: "M_WORK"}, {Process: "w", Stream: "KV_x"}}}}}, - {Module: "nats", SnapshotsTheBus: true, Processes: []ProcessPrincipal{{Process: "nats-tools", - Removes: []string{catalogue.RemovesEngineActs, catalogue.RemovesControllerLeftovers}}, {Process: "other"}}}, - {Module: "shop", Processes: []ProcessPrincipal{{Process: "nats-tools", Removes: []string{catalogue.RemovesEngineActs}}}}, - }}}) - if err != nil { - t.Fatal(err) - } - may := map[string][]string{ - "a.mesh-conditions": {"$KV.mesh-conditions_conditions.machine.g14.silent"}, - "a.mesh-provisioner": {"$KV.mesh-provisioner_provisions.a.keycloak.postgres-database", "$KV.mesh-provisioner_plans.a.keycloak"}, - "a.mesh-vault": {"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.a.keycloak.postgres-database", - "$JS.API.DIRECT.GET.KV_mesh-provisioner_plans.$KV.mesh-provisioner_plans.a.keycloak"}, - "a.mesh-hand-acts": {"$KV.mesh-hand-acts_warrants.mesh-config.ask-1", "mesh.log.mesh-hand-acts.acts.act-1"}, - "a.disk-load": {"$KV.disk-load_readings.a.pg.data", "$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.a.pg", - "$JS.API.CONSUMER.CREATE.KV_mesh-host_data.c1.$KV.mesh-host_data.a.*"}, - "provisioner.a": {"$JS.API.DIRECT.GET.KV_mesh-provisioner_plans.$KV.mesh-provisioner_plans.a.keycloak", - "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_plans.c1.$KV.mesh-provisioner_plans.a.keycloak", - // Every machine's plans, as the client V3 watches them (issue 774, 2026-10-30). - "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_plans.c1.$KV.mesh-provisioner_plans.>", - "$JS.API.DIRECT.GET.KV_mesh-provisioner_plans.$KV.mesh-provisioner_plans.b.keycloak", - // The needs, for the client before V3, until the lease write (review round 3). - "$KV." + ProvisionerNeedsBucket + ".a.keycloak.db.x"}, - // ADR 0571 clause 3: restic's bundle writes each item's last good night as its own state; the self-check reads it. - "a.mesh-self-check": {"$JS.API.DIRECT.GET.KV_restic_good.$KV.restic_good.a.pg.data"}, - "a.mesh-grants": {"$KV.mesh-grants_withheld.reader.mail.a"}, - "a.restic": {"$JS.API.DIRECT.GET.KV_disk-load_readings.$KV.disk-load_readings.a.pg.data", "$KV.restic_good.a.pg.data", - // ADR 0571 clause 7: its own membership from the grants part's state. - "$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.a.restic"}, - "a.mesh-delivery-indexer": {"$JS.API.CONSUMER.CREATE.LOG_mesh-delivery-builder_build-log.r1.mesh.log.mesh-delivery-builder.build-log.letta", - "$JS.API.CONSUMER.MSG.NEXT.LOG_mesh-delivery-builder_build-log.r1", "$JS.API.CONSUMER.DELETE.LOG_mesh-delivery-builder_build-log.r1", - "$JS.API.DIRECT.GET.LOG_mesh-delivery-builder_build-log.mesh.log.mesh-delivery-builder.build-log.letta"}, - "a.mesh-delivery-builder": {"mesh.log.mesh-delivery-builder.build-log.letta"}, - "a.mesh-catalog": {"mesh.mod.mesh-delivery-builder.tool.builder_builds_since"}, - WatchmanModule: {"$JS.API.CONSUMER.CREATE.DEAD_LETTER_NOTICES." + WatchmanModule, "$JS.API.CONSUMER.MSG.NEXT.DEAD_LETTER_NOTICES." + WatchmanModule, - "$JS.API.CONSUMER.INFO.DEAD_LETTER_NOTICES.controller", "$JS.API.STREAM.MSG.GET.DEAD_LETTERS"}, - WatchmanModule + " sources": {"$JS.API.STREAM.MSG.GET.EVENTS", "$JS.API.STREAM.INFO.SEAT_TELEGRAM_SENDER", - "$JS.API.STREAM.MSG.GET.M_WORK", "$JS.API.STREAM.INFO.EVENTS", "mesh.events.dead.EVENTS.a_m"}, - "a.nats.nats-tools": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$JS.API.CONSUMER.INFO.DEAD_LETTER_NOTICES.controller", - "$KV.mesh-host_acts.g14.a1", "$JS.API.STREAM.INFO.KV_mesh-host_acts", - // Research 072 row 33: the ASSIGNMENTS stream is read and removed at the lease write. - "$JS.API.STREAM.INFO.ASSIGNMENTS", "$JS.API.STREAM.DELETE.ASSIGNMENTS", - // Issue 1126 B5 (ADR 0590 clause 6): every leftover of the ended controller, each by its name, read and deleted. - "$JS.API.STREAM.INFO.KV_mesh-controller_lease", "$JS.API.STREAM.DELETE.KV_mesh-controller_lease", - "$JS.API.STREAM.DELETE.KV_mesh-controller_rollouts", "$JS.API.STREAM.DELETE.KV_mesh-controller_states-announced", - "$JS.API.STREAM.DELETE.CONTROL", "$JS.API.STREAM.DELETE.OBJ_declaration-files-a", - "$JS.API.CONSUMER.DELETE.EVENTS.controller", "$JS.API.CONSUMER.DELETE.EVENTS.controller-merges", - "$JS.API.CONSUMER.DELETE.EVENTS.novox_mesh-delivery", "$JS.API.CONSUMER.DELETE.CONTROL.controller", - // And the consumers no part composes that the controller made before it ended: mesh-catalog's of the - // controller's built-before, and each node's engine's of restic's backed-up (issue 1126). - "$JS.API.CONSUMER.INFO.EVENTS.novox_mesh-catalog", "$JS.API.CONSUMER.DELETE.EVENTS.novox_mesh-catalog", - "$JS.API.CONSUMER.INFO.EVENTS.a_mesh-host", "$JS.API.CONSUMER.DELETE.EVENTS.a_mesh-host", - "$JS.API.CONSUMER.INFO.CONTROL.controller"}, - } - mayNot := map[string][]string{ - // The keeper writes no findings and no attempts; no observer writes the keeper's buckets (clause 2). - "a.mesh-conditions": {"$KV.mesh-self-check_findings.x", "$KV.mesh-healers_attempts.x", "$KV.mesh-controller_conditions.x"}, - "a.mesh-self-check": {"$KV.mesh-conditions_conditions.x", "$KV.mesh-conditions_takeover.switch", "$KV.restic_good.a.pg.data", - "$KV.mesh-grants_withheld.reader.mail.a", "$SRV.INFO", "$SRV.PING.node-tools", - "$JS.API.DIRECT.GET.KV_mesh-grants_withheld.$KV.mesh-grants_withheld.reader.mail.a"}, - // Only mesh-provisioner writes provisions and plans, and it writes none of the grants part's states (ADR 0570 - // clauses 1, 8). - "a.mesh-provisioner": {"$KV.mesh-grants_accounts.a.m", "$KV.mesh-grants_user-list.sealed"}, - "a.mesh-vault": {"$KV.mesh-provisioner_provisions.a.keycloak.postgres-database", "$KV.mesh-provisioner_plans.a.keycloak", - "mesh.seat.mesh-artifact-sweep.event.artifacts-gone"}, - // Each writes its own state alone, and these users are composed. - "a.disk-load": {"$KV.restic_good.a.pg.data", "$KV.mesh-host_data.a.pg", - "$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.b.pg", "$JS.API.CONSUMER.CREATE.KV_mesh-host_data.c1.$KV.mesh-host_data.b.*"}, - "a.mesh-delivery-builder": {"$KV.disk-load_readings.a.pg.data"}, - // It reads every machine's plans (a provider's consumers are bound there, ADR 0373) but writes none, nor another - // machine's needs, which it keeps for the client before V3 until the lease write. - "provisioner.a": {"$KV.mesh-provisioner_plans.a.keycloak", "$KV." + ProvisionerNeedsBucket + ".b.keycloak.db.x"}, - // Only the assigner writes seat-holders. - "a.artifact-store-tools": {"$KV.mesh-delivery-assigner_seat-holders.mesh-broker"}, - // Only disk-load writes its readings. - "a.restic": {"$KV.disk-load_readings.a.pg.data", "$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.a.pg"}, - "a.postgres": {"$KV.mesh-provisioner_provisions.a.keycloak.postgres-database", "$KV.mesh-provisioner_plans.a.keycloak"}, - // Nor mesh-hand-acts' log and warrants, which it alone writes (ADR 0565 clause 10). - "a.mesh-healers": {"$KV.mesh-conditions_conditions.x", "$KV.mesh-conditions_takeover.switch", - "$KV.mesh-hand-acts_warrants.x", "mesh.log.mesh-hand-acts.acts.x"}, - // The watchman never deletes or changes the controller's consumer (clause 5). - // Its reads and deletes of a message are on the streams a dead letter comes from, named, never a bucket's - // (the lead, 2026-10-29). - WatchmanModule + " sources": {"$JS.API.STREAM.MSG.GET.KV_mesh-vault_bus-accounts", "$JS.API.STREAM.MSG.GET.KV_x", - "$JS.API.STREAM.MSG.DELETE.KV_mesh-host_acts", "$JS.API.STREAM.MSG.GET.OBJ_mesh-grants-user-list", "$JS.API.STREAM.MSG.GET.OTHER", - // In L4 it neither delivers again nor drops (clause 5, the lead 2026-10-29). - "$JS.API.STREAM.MSG.DELETE.EVENTS", "$JS.API.STREAM.MSG.DELETE.DEAD_LETTERS", "$JS.API.CONSUMER.CREATE.DEAD_LETTERS.x", - "mesh.again.a_m.x"}, - WatchmanModule: {"$KV.mesh-conditions_conditions.x", "$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", - "$JS.API.CONSUMER.CREATE.DEAD_LETTER_NOTICES.controller", "$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES." + WatchmanModule}, - // The bus module's tools remove the one consumer and the acts' keys, nothing else (Limits 4); its other accounts - // remove nothing; an account is granted the removals its bus declares and no other (the catalogue refuses them - // outside the bus module). - "a.nats.nats-tools": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.other", "$JS.API.CONSUMER.DELETE.EVENTS.a_m", - "$KV.mesh-conditions_conditions.x", "$JS.API.STREAM.DELETE.KV_mesh-host_acts", "$JS.API.STREAM.PURGE.KV_mesh-host_acts", - "$JS.API.STREAM.DELETE.EVENTS", "$JS.API.STREAM.PURGE.ASSIGNMENTS", "$JS.API.STREAM.DELETE.ASSIGNMENTS_X", - // It updates nothing, purges nothing, deletes no stream but the leftovers' and writes no key of them (ADR 0590). - "$JS.API.STREAM.UPDATE.KV_mesh-host_acts", "$JS.API.STREAM.UPDATE.KV_mesh-controller_calls", "$JS.API.STREAM.UPDATE.EVENTS", - "$JS.API.STREAM.UPDATE.KV_mesh-controller_lease", "$JS.API.STREAM.PURGE.KV_mesh-controller_lease", - "$JS.API.STREAM.DELETE.KV_mesh-controller_other", "$JS.API.STREAM.DELETE.OBJ_declaration-files-zz", - "$JS.API.CONSUMER.DELETE.EVENTS.a_mesh-delivery", "$KV.mesh-controller_lease.holder"}, - "a.nats.other": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-host_acts.g14.a1", - "$JS.API.STREAM.UPDATE.KV_mesh-controller_lease"}, - "a.nats": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-host_acts.g14.a1"}, - "a.shop.nats-tools": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-conditions_conditions.x", "$JS.API.STREAM.DELETE.ASSIGNMENTS"}, - } - // ADR 0571 clause 1: a read naming another module's log is issued to the reader as a log it reads, never writes. - if m := MembershipFor("a", Declared{Module: delivery.IndexerModule, Reads: []string{delivery.BuildLogRead, "x.y"}}, - Placements{Logs: map[string]bool{delivery.BuildLogRead: true}}); len(m.Logs) != 1 || m.Logs[0].Writes || - m.Logs[0].Stream != "LOG_mesh-delivery-builder_build-log" || m.Logs[0].Name != delivery.BuildLogRead { - t.Errorf("the indexer's membership issues the logs %+v", m.Logs) - } - // ADR 0565 clauses 6, 7 (issue 1953 L5-S4): a node's engine writes its own packages key and no other node's; only the - // assigner writes its policies. - for _, u := range users { - perms, err := PermissionsFor(u) - if err != nil { - continue - } - // ADR 0570 clause 6: a node's engine reads its own node's provision keys, and no other node's, and writes none. - if u.Kind == KindNode && (!MayPublish(perms, "$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.a.keycloak.db") || - MayPublish(perms, "$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.b.keycloak.db") || - MayPublish(perms, "$KV.mesh-provisioner_provisions.a.keycloak.db")) { - t.Errorf("node %s reads the provisions %v", u.Node, perms.Publish) - } - // Issue 774: a node's engine writes its own health report's key and no other node's. - if u.Kind == KindNode && (!MayPublish(perms, "$KV.mesh-host_health.a") || MayPublish(perms, "$KV.mesh-host_health.b")) { - t.Errorf("node %s writes the health reports %v", u.Node, perms.Publish) - } - if u.Kind == KindModule && u.Module == WatchmanModule && (!MayPublish(perms, "$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.b") || - MayPublish(perms, "$KV.mesh-host_health.b")) { - t.Errorf("the watchman reads the health reports %v, %v", MayPublish(perms, "$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.b"), perms.Publish) - } - // ADR 0565 clause 6, ADR 0590: and the ended controller's mirror it reads no more. - if u.Kind == KindNode && (!MayPublish(perms, "$KV.mesh-host_packages.a") || MayPublish(perms, "$KV.mesh-host_packages.b") || - MayPublish(perms, "$JS.API.DIRECT.GET.KV_mesh-controller_declared-packages.$KV.mesh-controller_declared-packages.a")) { - t.Errorf("node %s writes the packages keys %v", u.Node, perms.Publish) - } - if u.Kind == KindModule && MayPublish(perms, "$KV.mesh-delivery-assigner_policies.x") { - t.Errorf("%s writes the assigner's policies", u.Username()) - } - // ADR 0571 clause 10 (issue 774, 774/S5/AC2): nobody hears memberships on mesh.assignment; they are read from - // the grants part's state. - if MaySubscribe(perms, "mesh.assignment.a.restic") { - t.Errorf("%s hears mesh.assignment", u.Username()) - } - } - // Nor the runtime carrying the node's modules, which reads each one's membership from the state. - if perms, err := PermissionsFor(Principal{Kind: KindNodeTools, Node: "a", Module: RuntimeModule, - Carries: []Declared{{Module: "restic"}}}); err != nil || MaySubscribe(perms, "mesh.assignment.a.restic") || - !MayPublish(perms, "$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.a.restic") { - t.Errorf("the runtime hears mesh.assignment or reads no membership: %v", err) - } - // 1953 S-age (ADR 0565 §10): a log that says an age keeps its entries that long and no longer; one that says none, as - // long as the log (ADR 0297). - if aged, kept := (Log{Module: "mesh-hand-acts", Name: "acts", MaxAgeDays: 400}).Config(), (Log{Module: "m", Name: "l"}).Config(); aged.MaxAge != 400*24*time.Hour || kept.MaxAge != 0 || !aged.DenyDelete { - t.Errorf("an aged log's stream keeps %s, an unaged one %s", aged.MaxAge, kept.MaxAge) - } - // The tools account's secret is the module's own broker-, which its manifest's own-secrets names and the - // vault keeps by that name (AccountSecret). - if w := (WantedAccount{User: "a.nats.nats-tools", Kind: KindProcess, Node: "a", Module: "nats", Process: "nats-tools", - URL: "nats://a:4222"}); w.Valid() != nil || w.OwnSecret() != "broker-nats-tools" { - t.Errorf("the tools account is held in %q (%v)", w.OwnSecret(), w.Valid()) - } - // Issue 1952: a module that holds a seat says its own seat's events, also when the tool runner carries it, so the - // self-check's `checked`, the keeper's condition events and the healers' `healer-acted` reach their hearers. Its - // membership lists them, the runtime is granted them; a user of the seat is not, nor a machine's event on a node seat - // (granted per machine, to the holder's own account only). - selfCheck := Declared{Module: "mesh-self-check", Holds: []Seat{{Name: "mesh-self-check", Scope: "mesh", Emits: []string{"checked"}}}} - for _, c := range []struct { - name string - d Declared - subject string - may bool - }{ - {"the self-check says checked", selfCheck, "mesh.seat.mesh-self-check.event.checked", true}, - {"the keeper says a condition raised", Declared{Module: "mesh-conditions", Holds: []Seat{{Name: "mesh-conditions", Scope: "mesh", - Emits: []string{"condition-raised", "condition-cleared"}}}}, "mesh.seat.mesh-conditions.event.condition-cleared", true}, - {"a user does not say the seat's events", Declared{Module: "mesh-delivery-view", Uses: []Seat{{Name: "mesh-self-check", - Scope: "mesh", Emits: []string{"checked"}}}}, "mesh.seat.mesh-self-check.event.checked", false}, - {"the holder says no other seat's", selfCheck, "mesh.seat.mesh-conditions.event.condition-raised", false}, - {"a machine's event on a node seat is not composed here", Declared{Module: "pause", Holds: []Seat{{Name: "node-power", - Scope: "node", Emits: []string{"paused.*"}}}}, "mesh.seat.node-power.event.paused.b", false}, - } { - m := MembershipFor("a", c.d, Placements{}) - listed := m.SeatTraffic != nil && slices.ContainsFunc(m.SeatTraffic.Publish, func(p string) bool { return p == c.subject }) - perms, err := PermissionsFor(Principal{Kind: KindNodeTools, Node: "a", Carries: []Declared{c.d}}) - if err != nil { - t.Fatalf("%s: %v", c.name, err) - } - if listed != c.may || MayPublish(perms, c.subject) != c.may { - t.Errorf("%s: the membership lists %s %v and the runtime may publish it %v, want %v", c.name, c.subject, listed, - MayPublish(perms, c.subject), c.may) - } - } - seen := map[string]bool{} - for _, u := range users { - name := u.Username() - if u.Module == WatchmanModule && u.Kind == KindModule { - name = WatchmanModule - } - perms, err := PermissionsFor(u) - if err != nil { - t.Fatalf("%s: %v", name, err) - } - if name == WatchmanModule { - // It hears its watchdogs' words on core subjects (Limits 3, 11). - for _, subject := range []string{"mesh.mod.power.event.sleeping", "mesh.control.g14.tools-alive", - "mesh.seat.mesh-self-check.event.checked", "mesh.mod.keycloak.event.provisioner.failing", - "mesh.mod.nats.event.bus.slow-consumer", "prov.applied.g14.keycloak"} { - if !MaySubscribe(perms, subject) { - t.Errorf("the watchman may not hear %s", subject) - } - } - // Never the bus's API audit, which carries every API answer in the account (the lead, 2026-10-29). - if MaySubscribe(perms, "$JS.EVENT.ADVISORY.API") || MaySubscribe(perms, "$JS.EVENT.ADVISORY.API.x") { - t.Error("the watchman hears the bus's API audit") - } - if !MayPublish(perms, "$JS.API.DIRECT.GET.PROV_APPLIED.prov.applied.g14.keycloak") { - t.Error("the watchman may not read the newest provisioner report") - } - } - subjects := append(append([]string(nil), may[name]...), may[name+" sources"]...) - denied := append(append([]string(nil), mayNot[name]...), mayNot[name+" sources"]...) - if len(may[name+" sources"]) > 0 { - seen[name+" sources"] = true - } - for _, subject := range subjects { - seen[name] = true - if !MayPublish(perms, subject) { - t.Errorf("%s may not publish %s", name, subject) - } - } - for _, subject := range denied { - seen[name] = true - if MayPublish(perms, subject) { - t.Errorf("%s may publish %s", name, subject) - } - } - } - // Every row names a user that was composed, in either table: a row for a user absent would pass for nothing. - for _, table := range []map[string][]string{may, mayNot} { - for name := range table { - if !seen[name] { - t.Errorf("no user %s was composed", name) - } - } - } -} diff --git a/go/buscompose/callers.go b/go/buscompose/callers.go deleted file mode 100644 index 5af25dd..0000000 --- a/go/buscompose/callers.go +++ /dev/null @@ -1,117 +0,0 @@ -package buscompose - -import ( - "regexp" - "strings" -) - -// A tool call names its caller (novox/hq issue 365, by ADR 0259 §3's precedent for asks). -// -// mesh.mod..call.[.]. -// mesh.seat..call.[.]. -// -// The last token is the bus user that published the call, and each user is granted these subjects with its own -// name there and no other — the caller is a fact the server enforces, and the tool runtime hands it to the -// module from the subject a call arrived on (mesh-tools node-tools internal/bus caller.go holds the same shape). -// -// **A kind of its own, `call`, not the `tool` subject with a token appended.** Every grant to call a tool is a -// wildcard over the `tool` kind — `.tool..*` for the instance on any machine, `mesh.mod.*.tool.>` for every -// tool — and either would match a `tool` subject with any caller appended, so a caller could name another. -// Under `call` nothing is granted but the caller-named subjects. No stream's filter covers it: a tool call is -// never persisted (design 25 §3). -// -// **Derived from the `tool` grants, in one place** (callerNamed): wherever a principal may call or answer a -// tool, it may call it in its own name, or answer it naming any caller — so no kind of principal is left -// unable to call in its own name, and a new grant to call is one in both shapes by construction. -// -// The `tool` grants stay beside these for one release, so every caller and every runtime moves without a gap: -// their retirement is hq issue 464. -const CallKind = "call" - -var userName = regexp.MustCompile(`^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$`) - -// CallerToken is a bus user's name as the last token of a call it publishes: each dot written `~`, which no part -// of a user's name may hold (safeSubject), so the token names that user and no other. "" for a name that is not -// a user's. -func CallerToken(user string) string { - if !userName.MatchString(user) { - return "" - } - return strings.ReplaceAll(user, ".", "~") -} - -// toolGrant splits a grant on the `tool` kind — `mesh.mod..tool.` or `mesh.seat..tool.`, any -// part possibly a wildcard — at the kind; ok is false for any other subject. -func toolGrant(subject string) (head, rest string, ok bool) { - parts := strings.SplitN(subject, ".", 5) - if len(parts) != 5 || parts[0] != "mesh" || (parts[1] != "mod" && parts[1] != "seat") || parts[3] != "tool" || - parts[2] == "" || parts[4] == "" { - return "", "", false - } - return parts[0] + "." + parts[1] + "." + parts[2], parts[4], true -} - -// CalledSubject is where a call to a tool subject goes naming its caller; "" when the subject is not a tool's -// or the user is not a bus user. -func CalledSubject(subject, user string) string { - head, rest, ok := toolGrant(subject) - token := CallerToken(user) - if !ok || token == "" || strings.ContainsAny(rest, "*>") { - return "" - } - return head + "." + CallKind + "." + rest + "." + token -} - -// CalledPattern is what a holder answering a tool subject also subscribes, to hear the calls that name their -// caller: the same address under the `call` kind, any caller last. "" for a subject that is not a tool's. -func CalledPattern(subject string) string { - head, rest, ok := toolGrant(subject) - if !ok || strings.ContainsAny(rest, "*>") { - return "" - } - return head + "." + CallKind + "." + rest + ".*" -} - -// calledPublish is a grant to call on the `tool` kind, as the same grant in the caller's own name: its last -// token the caller's, and nothing that reaches past it. A `>` is every tail a call carries — the tool alone or -// the tool and the machine — so it becomes both, each ending in the caller. -func calledPublish(grant, token string) []string { - head, rest, ok := toolGrant(grant) - if !ok || token == "" { - return nil - } - base := head + "." + CallKind + "." - switch { - case rest == ">": - return []string{base + "*." + token, base + "*.*." + token} - case strings.HasSuffix(rest, ".>"): - return []string{base + strings.TrimSuffix(rest, ">") + "*." + token} - } - return []string{base + rest + "." + token} -} - -// calledSubscribe is a grant to answer on the `tool` kind, as the same grant for the calls naming any caller. -func calledSubscribe(grant string) []string { - head, rest, ok := toolGrant(grant) - if !ok { - return nil - } - base := head + "." + CallKind + "." - if strings.HasSuffix(rest, ">") { - return []string{base + rest} - } - return []string{base + rest + ".*"} -} - -// callerNamed adds, beside a principal's grants on the `tool` kind, the same grants under `call`: to publish in -// its own name, to subscribe naming anybody. -func callerNamed(user string, pub, sub []string) ([]string, []string) { - token := CallerToken(user) - for _, g := range pub { - pub = append(pub, calledPublish(g, token)...) - } - for _, g := range sub { - sub = append(sub, calledSubscribe(g)...) - } - return unique(pub), unique(sub) -} diff --git a/go/buscompose/cancelled.go b/go/buscompose/cancelled.go deleted file mode 100644 index f7fa097..0000000 --- a/go/buscompose/cancelled.go +++ /dev/null @@ -1,11 +0,0 @@ -package buscompose - -import "strings" - -// A seat's cancelled set (novox/hq ADR 0219) is a bucket the mesh no longer raises or grants (R4g, ADR 0525 clause 6): the -// controller asks no work queue, so nothing is cancelled. A bus that still holds one is not reported as state nothing declares. - -// IsCancelledSet says a bucket is a seat's cancelled set rather than a module's state. -func IsCancelledSet(bucket string) bool { - return strings.HasPrefix(bucket, "SEAT_") && strings.HasSuffix(bucket, "_cancelled") -} diff --git a/go/buscompose/composed.go b/go/buscompose/composed.go deleted file mode 100644 index 9dd1285..0000000 --- a/go/buscompose/composed.go +++ /dev/null @@ -1,71 +0,0 @@ -package buscompose - -import ( - "fmt" - - "git.novox.be/novox/mesh-sdk/go/delivery" -) - -// The compositions' buckets (novox/hq ADR 0539 clauses 2 and 4): one per node, `composed-`, holding the -// compositions the ended controller made for that node, which only that node's node-engine reads. Nothing writes them -// since the controller ended (ADR 0590); a node composes its own modules (ADR 0547 §2). - -// ComposedReads is what a node's node-engine publishes to follow its wants and compositions (ADR 0539 clauses 1, 2 and -// 9): every key of its own composition bucket, and its own wants (`want..` and `want..node`) in the -// assigner's bucket; nothing of another node's, nor the bucket of digests. -func ComposedReads(node string) ([]string, error) { - bucket, err := delivery.ComposedBucketFor(node) - if err != nil { - return nil, err - } - if !safeSubject.MatchString(node) { - return nil, fmt.Errorf("%q cannot be part of a subject: a permission is a subject pattern, and this would widen it", node) - } - return append(readGrants(bucket, ""), readGrants(delivery.AssignerWantsBucket, "want."+node+".>")...), nil -} - -// VerbsTheNodeAgentAsks are the seat verbs a node's node-engine calls of its own machine while it applies a want (ADR -// 0539 clause 16, ADR 0429 §14): its backup holder backs a data module up before it moves, and says when it was. Asked -// of its own machine only, `mesh.seat..tool..`. -var VerbsTheNodeAgentAsks = []SeatVerb{{Seat: "node-backup", Verb: "now"}, {Seat: "node-backup", Verb: "backed-up"}} - -// NodeAgentAsks are the subjects of VerbsTheNodeAgentAsks for one node. -func NodeAgentAsks(node string) ([]string, error) { - if !safeSubject.MatchString(node) { - return nil, fmt.Errorf("%q cannot be part of a subject: a permission is a subject pattern, and this would widen it", node) - } - var out []string - for _, v := range VerbsTheNodeAgentAsks { - out = append(out, "mesh.seat."+v.Seat+".tool."+v.Verb+"."+node) - } - return out, nil -} - -// 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"} - -// 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. -const BackupHolderModule = "restic" - -// DeclarationsStream is the stream of each node's last declaration, `mesh.node..declare` (issue 107). Release 6 -// sends none (ADR 0539 clauses 5 and 11): the stream is kept, with each node's consumer, only so a node away during the -// switch finds its last declaration and with it the new node-engine; the controller deletes it once every node's have -// names the new node-engine, and from then on neither asserts it nor any node's consumer on it; nor makes it again once -// it is gone from the bus for any reason, since a stream made anew would hold no node's last declaration (issue 1945). -const DeclarationsStream = "NODES" - -// RaiseAfterDeclarations is Raise once the declarations' stream is retired: the mesh's streams and consumers but the -// declarations' stream, and no node's consumer of a declaration. -func RaiseAfterDeclarations(r Raiser) error { - for _, s := range MeshStreams() { - if s.Name == DeclarationsStream { - continue - } - if err := r.EnsureStream(s); err != nil { - return fmt.Errorf("asserting stream %s: %w", s.Name, err) - } - } - return AssertMeshConsumers(r) -} diff --git a/go/buscompose/create.go b/go/buscompose/create.go deleted file mode 100644 index 0147716..0000000 --- a/go/buscompose/create.go +++ /dev/null @@ -1,115 +0,0 @@ -package buscompose - -import ( - "time" - - "github.com/nats-io/nats.go/jetstream" -) - -// The streams a manifest declares, as the server is asked to create them (novox/hq ADR 0547 §5, issue 1790): the -// grants part creates each one the bus does not hold yet, with what the controller asserts at its start, so the two -// make the same stream. Pure. Only a create is ever asked with these: a stream the bus holds is never updated by the -// grants part (CreateStreamGrants grants no update). - -// BucketStreamConfig is a module's bucket as the stream the controller's CreateOrUpdateKeyValue makes of its options -// (mesh-controller internal/broker BucketConfig, with no marker age held): the client library's key-value shape -// (rollup, no delete, direct reads, discard new, a duplicate window of two minutes or the age when shorter) with the -// mesh's caps, file storage and one replica. -func BucketStreamConfig(b Bucket) jetstream.StreamConfig { - history := b.History - if history < 1 { - history = 1 - } - age := time.Duration(b.TTLSeconds) * time.Second - dupes := 2 * time.Minute - if age > 0 && age < dupes { - dupes = age - } - cfg := jetstream.StreamConfig{ - Name: "KV_" + b.Bucket(), - Description: b.Why(), - Subjects: []string{"$KV." + b.Bucket() + ".>"}, - MaxMsgsPerSubject: int64(history), - MaxBytes: StateMaxBytes, - MaxAge: age, - MaxMsgSize: StateMaxValueBytes, - Storage: jetstream.FileStorage, - Replicas: 1, - AllowRollup: true, - DenyDelete: true, - Duplicates: dupes, - MaxMsgs: -1, - MaxConsumers: -1, - AllowDirect: true, - Discard: jetstream.DiscardNew, - } - if b.KeyTTL { - cfg.AllowMsgTTL, cfg.SubjectDeleteMarkerTTL = true, KeyTTLMarkerTTL - } - return cfg -} - -// ModuleStreamConfig is a module's own stream as the controller asserts it (mesh-controller internal/broker -// StreamConfigFor of ModuleStream.Stream). -func ModuleStreamConfig(m ModuleStream) jetstream.StreamConfig { - s := m.Stream() - cfg := jetstream.StreamConfig{ - Name: s.Name, - Subjects: s.Subjects, - Retention: jetstream.LimitsPolicy, - MaxAge: time.Duration(s.MaxAge) * time.Second, - MaxMsgsPerSubject: int64(s.MaxMsgsPerSubject), - Description: s.Why, - AllowDirect: s.Direct, - AllowMsgTTL: s.AllowMsgTTL, - } - if s.Retention == RetentionWorkQueue { - cfg.Retention = jetstream.WorkQueuePolicy - } - if s.Restricted || s.WorkQueue { - cfg.Metadata = map[string]string{} - if s.Restricted { - cfg.Metadata[RestrictedMetadata] = "true" - } - if s.WorkQueue { - cfg.Metadata[WorkQueueMetadata] = "true" - } - } - if s.MaxBytes > 0 { - cfg.MaxBytes = s.MaxBytes - } - if s.DiscardNew { - cfg.Discard = jetstream.DiscardNew - } - if s.DuplicatesSeconds > 0 { - cfg.Duplicates = time.Duration(s.DuplicatesSeconds) * time.Second - } - return cfg -} - -// ConsumerConfig is a pull consumer as the controller's EnsureConsumer creates it (mesh-controller internal/broker -// JetStream.EnsureConsumer, a consumer not there yet): its durable name, explicit acks, its ack wait, max deliver and max -// ack pending, its description, one filter or several, and from the stream's end when it is made FromNow. The grants part -// creates a module's and a process's consumers with it from the lease write (novox/hq ADR 0561 §6): a create alone, -// never an update of one the bus holds. A push consumer or a queue group is the controller's own and never made here. -func ConsumerConfig(c Consumer) jetstream.ConsumerConfig { - cfg := jetstream.ConsumerConfig{ - Durable: c.Name, - AckPolicy: jetstream.AckExplicitPolicy, - AckWait: time.Duration(c.AckWaitSeconds) * time.Second, - MaxDeliver: c.MaxDeliver, - MaxAckPending: c.MaxAckPending, - Description: c.Why, - } - switch len(c.Filters) { - case 0: - case 1: - cfg.FilterSubject = c.Filters[0] - default: - cfg.FilterSubjects = c.Filters - } - if c.FromNow { - cfg.DeliverPolicy = jetstream.DeliverNewPolicy - } - return cfg -} diff --git a/go/buscompose/create_test.go b/go/buscompose/create_test.go deleted file mode 100644 index 68cd04b..0000000 --- a/go/buscompose/create_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package buscompose - -import ( - "testing" - "time" - - "github.com/nats-io/nats.go/jetstream" -) - -// Defends issue 1790 (ADR 0547 §5): the grants part creates a declared bucket and stream as the controller asserts them. -func TestDeclaredStreamsAreCreatedAsTheControllerAssertsThem(t *testing.T) { - plain := BucketStreamConfig(Bucket{Module: "mesh-vault", Name: "bus-accounts"}) - keyed := BucketStreamConfig(Bucket{Module: "m", Name: "presence", History: 5, TTLSeconds: 60, KeyTTL: true}) - queue := ModuleStreamConfig(ModuleStream{Module: "b", Name: "ASKS", Events: []string{"ask"}, MaxAgeDays: 7, MaxMiB: 32, - WorkQueue: true, Duplicates: 1}) - for _, row := range []struct { - what string - got, want any - }{ - {"bucket name", plain.Name, "KV_mesh-vault_bus-accounts"}, - {"bucket subjects", plain.Subjects[0], "$KV.mesh-vault_bus-accounts.>"}, - {"bucket history defaults to one", plain.MaxMsgsPerSubject, int64(1)}, - {"bucket cap", plain.MaxBytes, int64(StateMaxBytes)}, - {"bucket value cap", plain.MaxMsgSize, int32(StateMaxValueBytes)}, - {"bucket duplicate window", plain.Duplicates, 2 * time.Minute}, - {"bucket discards new", plain.Discard, jetstream.DiscardNew}, - {"bucket direct and rollup, no delete", plain.AllowDirect && plain.AllowRollup && plain.DenyDelete, true}, - {"bucket without key ttl", plain.AllowMsgTTL, false}, - {"history", keyed.MaxMsgsPerSubject, int64(5)}, - {"age", keyed.MaxAge, time.Minute}, - {"duplicate window under the age", keyed.Duplicates, time.Minute}, - {"key ttl markers", keyed.SubjectDeleteMarkerTTL, KeyTTLMarkerTTL}, - {"key ttl", keyed.AllowMsgTTL, true}, - {"queue retention", queue.Retention, jetstream.WorkQueuePolicy}, - {"queue subjects", queue.Subjects[0], "mesh.stream.b.event.ask"}, - {"queue age", queue.MaxAge, 7 * 24 * time.Hour}, - {"queue cap", queue.MaxBytes, int64(32 << 20)}, - {"queue discards new", queue.Discard, jetstream.DiscardNew}, - {"queue duplicates", queue.Duplicates, time.Hour}, - {"queue metadata", queue.Metadata[WorkQueueMetadata] + queue.Metadata[RestrictedMetadata], "truetrue"}, - } { - if row.got != row.want { - t.Errorf("%s: %v, want %v", row.what, row.got, row.want) - } - } -} diff --git a/go/buscompose/creates_streams_test.go b/go/buscompose/creates_streams_test.go deleted file mode 100644 index efc3efb..0000000 --- a/go/buscompose/creates_streams_test.go +++ /dev/null @@ -1,155 +0,0 @@ -package buscompose - -import "testing" - -// Defends issue 1790 and novox/hq ADR 0547 §5 (the operator, 2026-10-26 08:54): the grants part may create the stream -// of each bucket, log and stream a placed module's manifest declares, by its own name, and read it back; nothing -// wider, nothing updated or deleted, and no other principal so. Held byte-equal in the controller's copy. And ADR 0547 -// clause 7 (issue 1945, the operator's second exception): the grants part writes its own object bucket of sealed user -// lists, and the node-engine of the node holding mesh-broker alone reads its one key and an object of it by name. -func TestTheGrantsPartCreatesOnlyDeclaredStreams(t *testing.T) { - records := Records{ - Nodes: []string{"novox", "ace"}, - Assigned: map[string][]Declared{"ace": {{Module: "plex"}}, "novox": { - {Module: "nats", SnapshotsTheBus: true}, - // mesh-sdk's copy alone (ADR 0547 clause 9, named readers): the vault's hashes are read by the grants part - // alone, whatever another module's reads say; the controller's copy grants any module that declares it. - {Module: "mesh-vault", State: []Bucket{{Module: "mesh-vault", Name: "bus-accounts"}}}, - {Module: "spy", Serves: []string{"spy_x"}, Reads: []string{VaultBusAccounts}}, - {Module: "mesh-delivery-builder", - State: []Bucket{{Module: "mesh-delivery-builder", Name: "steps", History: 5}}, - Logs: []Log{{Module: "mesh-delivery-builder", Name: "builds"}}, - Streams: []ModuleStream{{Module: "mesh-delivery-builder", Name: "BUILDASKS", Events: []string{"ask"}}}}, - {Module: GrantsModule, State: []Bucket{{Module: GrantsModule, Name: "assertions"}}, Reads: []string{VaultBusAccounts}}, - {Module: RuntimeModule}, - // The node-engine's manifest as a placement names it (issue 774, met twice on 2026-10-30): no account of its - // own, so the runner would carry it, and its state is the node's to write alone. - {Module: EngineModule, NoAccount: true, Consumes: []string{"restic.backed-up"}, - State: []Bucket{{Module: EngineModule, Name: "health"}, {Module: EngineModule, Name: EngineActsState}}}, - }}, - // The node-engine's own manifest, placed on every node though no node is assigned it (novox/hq ADR 0563 §2, §3). - Engine: &Declared{Module: EngineModule, State: []Bucket{{Module: EngineModule, Name: EngineActsState}}}, - } - users, err := Users(records) - if err != nil { - t.Fatal(err) - } - perms := map[string]Permissions{} - for _, u := range users { - if u.Module == EngineModule { - t.Errorf("the node-engine's manifest composed a user of its own: %s", u.Username()) - } - if u.Kind == KindNode || u.Kind == KindNodeTools { - p, err := PermissionsFor(u) - if err != nil { - t.Fatalf("%s: %v", u.Username(), err) - } - perms[u.Username()] = p - continue - } - if u.Kind != KindModule { - continue - } - p, err := PermissionsFor(u) - if err != nil { - t.Fatalf("%s: %v", u.Username(), err) - } - perms[u.Module] = p - } - for _, row := range []struct { - who, subject string - may bool - }{ - {GrantsModule, "$JS.API.STREAM.CREATE.KV_mesh-delivery-builder_steps", true}, - {GrantsModule, "$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps", true}, - {GrantsModule, "$JS.API.STREAM.CREATE.LOG_mesh-delivery-builder_builds", true}, - {GrantsModule, "$JS.API.STREAM.CREATE.BUILDASKS", true}, - {GrantsModule, "$JS.API.STREAM.CREATE.KV_mesh-grants_assertions", true}, - {GrantsModule, "$JS.API.STREAM.UPDATE.KV_mesh-delivery-builder_steps", false}, - {GrantsModule, "$JS.API.STREAM.DELETE.KV_mesh-delivery-builder_steps", false}, - {GrantsModule, "$JS.API.STREAM.PURGE.KV_mesh-delivery-builder_steps", false}, - {GrantsModule, "$JS.API.STREAM.CREATE.KV_mesh-delivery-builder_undeclared", false}, - {GrantsModule, "$JS.API.STREAM.CREATE.EVENTS", false}, - {GrantsModule, "$JS.API.STREAM.CREATE.ASSIGNMENTS", false}, - // Research 072 row 33: a runtime reads its membership from the grants part's state, never the assignments stream. - {"mesh-delivery-builder", "$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.novox.mesh-delivery-builder", false}, - {GrantsModule, "$JS.API.STREAM.INFO.EVENTS", false}, - {"mesh-delivery-builder", "$JS.API.STREAM.CREATE.KV_mesh-delivery-builder_steps", false}, - // The node-engine's acts: created by the grants part, written by each node's engine under its own keys alone. - {GrantsModule, "$JS.API.STREAM.CREATE.KV_mesh-host_acts", true}, - {"node.ace", "$JS.API.STREAM.CREATE.KV_mesh-host_acts", false}, - {"node.ace", "$KV.mesh-host_acts.ace.act.hand-over-1", true}, - {"node.ace", "$KV.mesh-host_acts.ace.open.hand-over.abc", true}, - {"node.ace", "$KV.mesh-host_acts.novox.act.hand-over-1", false}, - {"node.ace", "$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.ace.ask.hand-over-1", true}, - {"node.ace", "$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.novox.ask.hand-over-1", false}, - {"novox.node-tools", "$KV.mesh-host_acts.novox.act.hand-over-1", false}, - // The runner never carries the engine (issue 774): its health report is the node's own key alone. - {"novox.node-tools", "$KV.mesh-host_health.novox", false}, - {"novox.node-tools", "$KV.mesh-host_acts.novox.ask.x", false}, - {"node.novox", "$KV.mesh-host_health.novox", true}, - {"node.novox", "$KV.mesh-host_health.ace", false}, - // The sealed user list (ADR 0547 clause 7): the grants part writes its own bucket and no other. - {GrantsModule, "$JS.API.STREAM.CREATE.OBJ_mesh-grants-user-list", true}, - {GrantsModule, "$O.mesh-grants-user-list.C.abc", true}, - {GrantsModule, "$JS.API.STREAM.PURGE.OBJ_mesh-grants-user-list", true}, - {GrantsModule, "$O.declaration-files-novox.C.abc", false}, - {GrantsModule, "$JS.API.STREAM.DELETE.OBJ_mesh-grants-user-list", false}, - {"mesh-delivery-builder", "$O.mesh-grants-user-list.C.abc", false}, - // The bus node's engine reads the one key and an object by name; nothing of the bucket's list, no write. - {"node.novox", "$JS.API.DIRECT.GET.KV_mesh-grants_user-list.$KV.mesh-grants_user-list.sealed", true}, - {"node.novox", "$JS.API.CONSUMER.CREATE.KV_mesh-grants_user-list.abc.$KV.mesh-grants_user-list.sealed", true}, - {"node.novox", "$JS.API.DIRECT.GET.OBJ_mesh-grants-user-list.$O.mesh-grants-user-list.M.abc", true}, - {"node.novox", "$JS.API.CONSUMER.CREATE.OBJ_mesh-grants-user-list.abc.$O.mesh-grants-user-list.C.abc", true}, - {"node.novox", "$JS.API.DIRECT.GET.KV_mesh-grants_user-list.$KV.mesh-grants_user-list.list", false}, - {"node.novox", "$JS.API.STREAM.INFO.OBJ_mesh-grants-user-list", false}, - {"node.novox", "$JS.API.STREAM.MSG.GET.OBJ_mesh-grants-user-list", false}, - {"node.novox", "$O.mesh-grants-user-list.C.abc", false}, - {"node.ace", "$JS.API.DIRECT.GET.KV_mesh-grants_user-list.$KV.mesh-grants_user-list.sealed", false}, - {"node.ace", "$JS.API.DIRECT.GET.OBJ_mesh-grants-user-list.$O.mesh-grants-user-list.M.abc", false}, - {GrantsModule, "$JS.API.DIRECT.GET.KV_mesh-vault_bus-accounts.$KV.mesh-vault_bus-accounts.x", true}, - {"spy", "$JS.API.DIRECT.GET.KV_mesh-vault_bus-accounts.$KV.mesh-vault_bus-accounts.x", false}, - {"novox.node-tools", "$JS.API.DIRECT.GET.KV_mesh-vault_bus-accounts.$KV.mesh-vault_bus-accounts.x", false}, - } { - if got := MayPublish(perms[row.who], row.subject); got != row.may { - t.Errorf("%s may publish %s: %v, want %v", row.who, row.subject, got, row.may) - } - } - // Issue 1790 (2026-10-26): a module hears the bus create each bucket it may bind, its own and those it reads, so a - // state missing at its start is bound when it appears; no other bucket's, and no other advisory. - for _, row := range []struct { - who, subject string - may bool - }{ - {"mesh-delivery-builder", "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-builder_steps", true}, - {GrantsModule, "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-vault_bus-accounts", true}, - {"mesh-delivery-builder", "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-grants_assertions", false}, - {"mesh-delivery-builder", "$JS.EVENT.ADVISORY.STREAM.CREATED.LOG_mesh-delivery-builder_builds", false}, - {"mesh-delivery-builder", "$JS.EVENT.ADVISORY.STREAM.DELETED.KV_mesh-delivery-builder_steps", false}, - } { - if got := MaySubscribe(perms[row.who], row.subject); got != row.may { - t.Errorf("%s may hear %s: %v, want %v", row.who, row.subject, got, row.may) - } - } - // A name that is no plain token is never granted, and a module other than the grants part is never granted one. - if got := CreateStreamGrants([]string{"*", ">", "KV_a.b", "KV_a_b"}); len(got) != 2 { - t.Errorf("grants for wildcard names: %v", got) - } - other, err := PermissionsFor(Principal{Kind: KindModule, Node: "novox", Module: "shop", CreatesStreams: []string{"KV_shop_x"}}) - if err != nil { - t.Fatal(err) - } - if MayPublish(other, "$JS.API.STREAM.CREATE.KV_shop_x") { - t.Error("a module other than the grants part was granted a create") - } - // The writers table refuses the grants part an update, and a create of a stream it was not given. - g := Principal{Kind: KindModule, Node: "novox", Module: GrantsModule, CreatesStreams: []string{"KV_a_b"}} - if err := CheckWriters(g, []string{"$JS.API.STREAM.CREATE.KV_a_b"}); err != nil { - t.Errorf("the declared create refused: %v", err) - } - for _, refused := range []string{"$JS.API.STREAM.UPDATE.KV_a_b", "$JS.API.STREAM.CREATE.KV_a_c", "$JS.API.STREAM.CREATE.*"} { - if CheckWriters(g, []string{refused}) == nil { - t.Errorf("%s was not refused", refused) - } - } -} diff --git a/go/buscompose/declaration_files.go b/go/buscompose/declaration_files.go index af20264..4adef01 100644 --- a/go/buscompose/declaration_files.go +++ b/go/buscompose/declaration_files.go @@ -1,111 +1,8 @@ package buscompose import ( - "context" - "fmt" - "time" - "git.novox.be/novox/mesh-sdk/go/declaration" - "github.com/nats-io/nats.go/jetstream" ) -// The object buckets a declaration's large files travel in (novox/hq ADR 0486, issues 1696 and 1697): **one bucket for each -// node**, `declaration-files-`. -// -// **The controller alone writes them, and a node-engine reads an object by its name from its own bucket** (ADR 0486 §2): its -// account may neither list the bucket nor write it, no account may touch another node's, and no other account has any access. -// Why a bucket each: a subject pattern cannot tell a name from a wildcard. A grant of `...M.*` matches a publish to the literal -// subject `...M.*` too (probed on nats-server 2.11.17 by the review of this issue: a node created a consumer filtered on -// `$O..C.*` and read every object of the bucket), so in one shared bucket a node could read every other node's files. -// With a bucket each, the same trick reads only the node's own files, which are the files of its own declaration. -// -// A bucket of the bus's object store is a stream, OBJ_, whose messages are the objects' metadata -// (`$O..M.`) and chunks (`$O..C.`). - // DeclarationFilesBucket is the prefix of the buckets' names, which mesh-sdk fixes for the node-engine and the controller alike. const DeclarationFilesBucket = declaration.FileBucketPrefix - -// DeclarationFilesBucketOf is the bucket a node's files are in: mesh-sdk's FileBucketFor, the one place the name is composed. A -// node name it refuses (one that could carry a subject wildcard or separator into a grant) is an error, never a bucket. -func DeclarationFilesBucketOf(node string) (string, error) { return declaration.FileBucketFor(node) } - -// DeclarationFilesStreamOf is the stream under a node's bucket: mesh-sdk's FileStreamFor. -func DeclarationFilesStreamOf(node string) (string, error) { return declaration.FileStreamFor(node) } - -// DeclarationFilesMaxBytes is a bucket's size: the objects are immutable and regenerable, and one node's hold well under a hundred -// megabytes. -const DeclarationFilesMaxBytes = 1 << 30 - -// DeclarationFilesKeptFor is how long an object stays after no node's latest declaration names it (ADR 0486 §3). -const DeclarationFilesKeptFor = 24 * time.Hour - -// DeclarationFilesWrites is the one pattern the controller may publish to write a node's bucket: every object's metadata and -// chunks. The server's API for the stream is the controller's `$JS.API.>`. -func DeclarationFilesWrites(node string) (string, error) { - bucket, err := DeclarationFilesBucketOf(node) - if err != nil { - return "", err - } - return "$O." + bucket + ".>", nil -} - -// DeclarationFilesReads is what a node-engine's account may publish to read an object of its own bucket by its name, and nothing -// more. The set is the one node-engine's own call (mesh-host internal/link FileObject) makes, for B the node's bucket and S its -// stream (issue 1697, notes 4 and 5): -// -// - the object's metadata, by a direct get of the last message of the one subject that names it. **Only the suffixed -// form**: the bare `$JS.API.DIRECT.GET.S` takes a sequence number and would read any message of the stream. -// - the ordered consumer on that object's chunks: `$JS.API.CONSUMER.CREATE.S..$O.B.C.`. **Not `S.>`**: -// a consumer filtered on `$O.B.M.>` would read every object's name. -// - the flow-control replies of that consumer, without which a large object stalls at the flow-control window. -// - the consumer's deletion, best effort: the consumer's inactivity threshold removes it if this is refused. -// -// **Never granted:** STREAM.INFO (a `subjects_filter` of `>` lists every name), STREAM.MSG.GET (any message by its sequence), a -// publish to `$O.B.>` (a write), and any subject of another node's stream. A literal `*` in a subject matches the grant's `*`: it -// reaches the objects of this node's own bucket and no other (above). -func DeclarationFilesReads(node string) ([]string, error) { - bucket, err := DeclarationFilesBucketOf(node) - if err != nil { - return nil, err - } - stream, err := DeclarationFilesStreamOf(node) - if err != nil { - return nil, err - } - return []string{ - "$JS.API.DIRECT.GET." + stream + ".$O." + bucket + ".M.*", - "$JS.API.CONSUMER.CREATE." + stream + ".*.$O." + bucket + ".C.*", - "$JS.API.CONSUMER.DELETE." + stream + ".*", - "$JS.FC." + stream + ".>", - }, nil -} - -// EnsureDeclarationFiles is the same on a JetStream the caller holds. -func EnsureDeclarationFiles(ctx context.Context, js jetstream.JetStream, nodes ...string) error { - for _, node := range nodes { - cfg, err := DeclarationFilesConfig(node) - if err != nil { - return err - } - if _, err := js.CreateOrUpdateObjectStore(ctx, cfg); err != nil { - return fmt.Errorf("asserting the object bucket %s: %w", cfg.Bucket, err) - } - } - return nil -} - -// DeclarationFilesConfig is a node's bucket as the controller asserts it. -func DeclarationFilesConfig(node string) (jetstream.ObjectStoreConfig, error) { - bucket, err := DeclarationFilesBucketOf(node) - if err != nil { - return jetstream.ObjectStoreConfig{}, err - } - return jetstream.ObjectStoreConfig{ - Bucket: bucket, - Description: "the contents of the files over 64 KiB of the declaration of " + node + ", each under the hex of its sha256 " + - "digest (novox/hq ADR 0486): written by the controller alone, read by the node-engine of that node by name, removed " + - "a day after the node's latest declaration no longer names it", - MaxBytes: DeclarationFilesMaxBytes, - Storage: jetstream.FileStorage, - }, nil -} diff --git a/go/buscompose/derived.go b/go/buscompose/derived.go index a727e84..1885a93 100644 --- a/go/buscompose/derived.go +++ b/go/buscompose/derived.go @@ -1,21 +1,5 @@ package buscompose -import ( - "fmt" - "sort" - "strings" -) - -// Streams and consumers derived from what modules declare. -// -// The mesh's own four exist before any module does (streams.go). Everything here is the other -// half: a seat's stream comes into being when the module declaring it is **registered**, and a -// consumer when a module is **assigned** — which is why ADR 0116's task 1.4 had to be narrowed to -// the foundation set. Neither has happened at genesis. -// -// All of it is a pure function of declarations. The controller is still the only writer; this is -// only what it writes. - // A Consumer is a durable subscription the controller creates on a module's behalf. A module // declares what it reacts to, never how delivery works, so it does not name these and cannot // misconfigure them. @@ -68,314 +52,8 @@ type Consumer struct { Resettable string } -// seatStreamName is the stream holding a seat's inbound work. Named after the seat rather than -// the module holding it, because the holder can change and the queued work must not care — which -// is the whole reason a caller addresses a seat instead of a module. -func seatStreamName(seat string) string { return "SEAT_" + upperSnake(seat) } - -// SeatStreams is one work queue per declared seat, created when the declaring module is -// registered rather than when it is assigned. -// -// **The stream exists before anyone holds the seat, and that is the point.** Work queues until a -// holder appears, so installing the telegram module a week after something started sending to it -// flushes the backlog instead of having lost it. A stream created at assignment would make "the -// holder is not here yet" mean "your messages are gone". -func SeatStreams(seats []DeclaredSeat) []Stream { - sorted := append([]DeclaredSeat(nil), seats...) - sort.Slice(sorted, func(i, j int) bool { return sorted[i].Name < sorted[j].Name }) - - var out []Stream - for _, s := range sorted { - if len(s.Accepts) == 0 { - // A seat that only emits and serves needs no stream: its events ride EVENTS and its - // tools are core request/reply, which is never persisted. - continue - } - retain := s.RetainSeconds - if retain == 0 { - retain = 7 * 24 * 60 * 60 - } - out = append(out, Stream{ - Name: seatStreamName(s.Name), - Subjects: []string{"mesh.seat." + s.Name + ".accept.>"}, - Retention: RetentionWorkQueue, - MaxAge: retain, - Why: fmt.Sprintf("work submitted to the %s seat; one holder consumes it, and it "+ - "queues while nobody does", s.Name), - }) - } - return out -} - -// A DeclaredSeat is a seat as the catalogue knows it. Mirrored here rather than imported so this -// package stays free of the catalogue's own types — the same reason the host mirrors the -// contracts instead of importing the sdk. -type DeclaredSeat struct { - Name string - Accepts []string - // Emits are the verbs the seat's holder publishes under the seat's own name. An event about a - // role belongs here rather than in the holder's namespace, because the name then outlives - // whoever fills it (novox/hq ADR 0121, 04-ISSUES/127). - Emits []string - // Serves are the verbs the holder answers, request and reply. - Serves []string - RetainSeconds int - // PulledByKind is Seat's: a worker per accepted kind, derived from the holders' seat traffic, not here. - PulledByKind bool -} - -// ConsumerFor is the durable consumer a module's declarations imply, or false when it subscribes -// to nothing and needs none. -// -// One per module, with every consumed subject as a filter, because its ack permission is derived -// from its name: a module with a consumer per event would need an ack permission per consumer, -// and the permission list would stop being derivable from the declaration. -func ConsumerFor(p Principal) (Consumer, bool) { - // A module that reacts to anything — a module's events or a role's (novox/hq ADR 0121). Watching - // a role was missing here, so the one module that does it got no consumer at all: it started, - // connected, and its graph stayed empty with nothing anywhere reporting why. - // And one that hears its own answers on a seat it uses (novox/hq ADR 0259 §3): an asker's warrants. - hearsItsOwn := len(SeatTrafficOf(p.Module, nil, p.Uses, nil).Subscribe) > 0 - hearsAStream := false - for _, h := range p.Hears { - if len(h.Filters) > 0 { - hearsAStream = true - } - } - if p.Kind != KindModule || (len(p.Consumes) == 0 && len(p.Watches) == 0 && !hearsItsOwn && !hearsAStream) { - return Consumer{}, false - } - perms, err := PermissionsFor(p) - if err != nil { - return Consumer{}, false - } - // Events, wherever they live: a module's own namespace, and the namespace of any role it watches - // (novox/hq ADR 0121). Tool subjects and inboxes are subscribed directly and are not a consumer's - // business, which is why this is a filter and not the whole list. - var filters []string - for _, s := range perms.Subscribe { - // Not a module stream's, which its own consumer on that stream hears (novox/hq ADR 0363). - if strings.Contains(s, ".event.") && !strings.HasPrefix(s, moduleStreamPrefix) { - filters = append(filters, s) - } - } - // **A module that hears only a module's stream still has one here** (novox/hq ADR 0363 §7): what it - // gives up on there is delivered again under `mesh.again..` on EVENTS, and only this filters it. - if len(filters) == 0 && !hearsAStream { - return Consumer{}, false - } - sort.Strings(filters) - // And the events given up on and delivered again to this consumer alone (novox/hq issue 330). - filters = append(filters, AgainFilter(consumerDurable(p))) - return Consumer{ - Name: consumerDurable(p), - Stream: consumerStream(p), - Filters: filters, - AckWaitSeconds: 30, - MaxDeliver: 5, - // From now, as the controller's own consumers have been since issue 248: a module made on a stream that - // keeps a week replays none of it. What a part needs of the present it reads from its owner - // (novox/hq ADR 0443 §1, §3). A consumer that exists keeps its place whatever this says. - FromNow: true, - Why: "what " + p.Module + " declared it consumes; after max-deliver it gives an event up, and the " + - "controller keeps it in DEAD_LETTERS until a person delivers it again or drops it", - }, true -} - // ModuleConsumer is one module's durable consumer, with the module and node it is for. type ModuleConsumer struct { Node, Module string Consumer Consumer } - -// ConsumersOf is every module's durable consumer the composed users imply: each module user's, and -// each module a runtime carries — a carried module with no account of its own is no user (novox/hq -// issue 195), and its consumer is still the controller's to make, since the runtime reads it on the -// module's behalf (ADR 0198). One per module and node, whichever of the two named it first. -func ConsumersOf(users []Principal) []ModuleConsumer { - var out []ModuleConsumer - seen := map[string]bool{} - add := func(p Principal) { - key := p.Node + "/" + p.Module - if seen[key] { - return - } - c, needed := ConsumerFor(p) - // **A module with a work-queue consumer of its own needs no other reason to be visited here** - // (novox/hq ADR 0436 §1, §2, §6): mesh-mail's sender consumes and watches nothing of its own, so - // ConsumerFor alone would have found nothing needed and skipped it before its one queue consumer - // was ever composed. - if !needed && len(p.Queues) == 0 { - return - } - seen[key] = true - if needed { - out = append(out, ModuleConsumer{Node: p.Node, Module: p.Module, Consumer: c}) - // And one on each module stream it hears (novox/hq ADR 0363 §5). - for _, h := range p.Hears { - if len(h.Filters) == 0 { - continue - } - out = append(out, ModuleConsumer{Node: p.Node, Module: p.Module, Consumer: heardConsumer(p, h)}) - } - } - // And the one durable consumer of each of its own work-queue streams, for the one process its - // bus.queues names (novox/hq ADR 0436 §1, §2, §6). - for _, q := range p.Queues { - out = append(out, ModuleConsumer{Node: p.Node, Module: p.Module, Consumer: QueueConsumer(p, q)}) - } - } - for _, p := range users { - if p.Kind == KindModule { - add(p) - } - } - for _, p := range users { - if p.Kind != KindNodeTools { - continue - } - for _, d := range p.Carries { - // **A carried module's work-queue consumer is still the controller's to make** (novox/hq ADR - // 0436 §1, §2, §6), the same reason its ordinary consumer is (ADR 0198): mesh-mail has no - // account of its own (NoAccount) and is carried here, but its process does, and the consumer - // it reads through must exist and survive PruneHeard whether or not the module itself speaks. - add(Principal{Kind: KindModule, Node: p.Node, Module: d.Module, Emits: d.Emits, - Consumes: d.Consumes, Serves: d.Serves, Holds: d.Holds, Uses: d.Uses, Watches: d.Watches, - Streams: d.Streams, Hears: ComposedHears(d), Queues: d.Queues}) - } - } - return out -} - -// 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. -func AccountConsumers(users []Principal) []ModuleConsumer { - queues := map[string]bool{} - for _, p := range users { - for _, q := range p.Queues { - queues[q.Stream] = true - } - for _, d := range p.Carries { - for _, q := range d.Queues { - queues[q.Stream] = true - } - } - } - var out []ModuleConsumer - for _, mc := range ConsumersOf(users) { - if mc.Consumer.Stream == consumerStream(Principal{Kind: KindModule}) || queues[mc.Consumer.Stream] { - out = append(out, mc) - } - } - return out -} - -// HolderConsumerFor is the worker a seat's holders share on that seat's work queue. -// -// **One worker for every holder, and each holder pulls one ask when it is idle** (novox/hq ADR -// 0190). The seat is *authority* — who may be the telegram sender — and the worker is *delivery*, -// kept separate so that relaxing one changes nothing about the other: a node-scoped seat has a -// holder per machine, and all of them take from this one consumer, so the work is shared without -// any holder knowing about the others. Pulled rather than pushed because a push consumer hands the -// next ask to whichever subscriber the server picks, busy or not, and a pulled one is asked for by -// a holder that has just become free. Which is also what ends the race issue 186 describes — asks -// delivered behind the one being worked, expiring unacknowledged and dropped after the fifth -// redelivery: nothing is delivered that nobody asked for. A long build keeps its own ask alive -// (stillWorking); the ack wait is for a holder that died. -func HolderConsumerFor(node, module string, seat DeclaredSeat) (Consumer, bool) { - if len(seat.Accepts) == 0 || seat.PulledByKind { - // A seat pulled by kind has a worker per kind, which its holders' seat traffic derives (SeatTrafficObjects): one - // worker filtered on every accept would overlap them on the work queue. - return Consumer{}, false - } - return Consumer{ - Name: "SEAT_" + upperSnake(seat.Name) + "_worker", - Stream: seatStreamName(seat.Name), - Filters: []string{"mesh.seat." + seat.Name + ".accept.>"}, - AckWaitSeconds: 60, - MaxDeliver: 5, - // As many in flight as there are holders working, which pulling bounds by itself: a holder - // fetches one and fetches again only after it acknowledged. The server's default stands. - Why: fmt.Sprintf("%s on %s holds %s; every holder pulls one ask at a time from this worker "+ - "and acknowledges after the work is done, so a crash mid-work redelivers rather than "+ - "loses and an idle holder is the one that takes the next ask", module, node, seat.Name), - }, true -} - -// NodeConsumer is the durable consumer a node reads its own declaration through. -// -// **Derived from a node existing, and created by the controller, because a host cannot create it.** -// A host's account may subscribe its own declaration subject and publish its own ack subject, and -// reaches no part of the JetStream API — which is correct (the controller is the only writer of -// consumer definitions, design 25 §3) and means the consumer must be waiting before the host binds -// to it. Named after the node, because the node's ack grant is `$JS.ACK.NODES..>` and a -// consumer named anything else is one the host cannot acknowledge a delivery from. -// -// **No max-deliver, and a long ack wait.** A declaration is settled only after the node has applied -// it and reported, which is minutes on a machine pulling images; and a declaration the mesh cannot -// get a node to accept is not one to give up on, because the stream keeps only the newest per node -// anyway — so there is exactly one message per node to redeliver, for as long as that node is away. -func NodeConsumer(node string) Consumer { - return Consumer{ - Name: node, - Stream: "NODES", - Filters: []string{"mesh.node." + node + ".declare"}, - Push: true, - AckWaitSeconds: 300, - Why: "how " + node + " hears what it should be; last-per-subject, so a node that was away " + - "gets exactly the current declaration and nothing older", - } -} - -// AssertNodeConsumers brings every known node's declaration consumer into being. -// -// Asserted on start as well as created at enrolment, for the reason the streams are: a mesh raised -// from a restored backup, or one whose bus was recreated, has node records and no consumers, and a -// node whose consumer is missing hears nothing while everything else about it looks correct. -func AssertNodeConsumers(e Ensurer, nodes []string) error { - for _, n := range nodes { - if err := e.EnsureConsumer(NodeConsumer(n)); err != nil { - return fmt.Errorf("asserting how %s hears its declaration: %w", n, err) - } - } - return nil -} - -// AllOverlaps reports subject filters claimed by more than one stream, across the mesh's own and -// every derived one. -// -// NATS refuses an overlapping stream rather than merging it (verified against nats-server 2.10: -// "subjects overlap with an existing stream"), so this is not a subtle divergence — it is a -// registration that fails. Catching it here names both streams, before a half-applied mesh does. -func AllOverlaps(seats []DeclaredSeat) []string { - all := append(MeshStreams(), SeatStreams(seats)...) - seen := map[string]string{} - var clashes []string - for _, s := range all { - for _, subject := range s.Subjects { - if first, ok := seen[subject]; ok { - clashes = append(clashes, fmt.Sprintf("%s and %s both claim %s", first, s.Name, subject)) - continue - } - seen[subject] = s.Name - } - } - sort.Strings(clashes) - return clashes -} - -// upperSnake makes a stream name from a seat name. NATS stream names may not contain a dot, -// a space or a wildcard, and a hyphen is legal but reads badly beside the mesh's own. -func upperSnake(s string) string { - out := []rune(s) - for i, r := range out { - switch { - case r >= 'a' && r <= 'z': - out[i] = r - 32 - case r == '-' || r == '.': - out[i] = '_' - } - } - return string(out) -} diff --git a/go/buscompose/doc.go b/go/buscompose/doc.go index fcd2338..42db725 100644 --- a/go/buscompose/doc.go +++ b/go/buscompose/doc.go @@ -1,12 +1,5 @@ -// Package buscompose is the bus's composer: who may say what on the bus, as the user list and the server configuration -// the bus loads, derived from the mesh's records (Users, WithPasswords, ComposeAccounts, Compose), with the grants, -// streams, state and logs it composes them from. -// -// **A copy of the controller's internal/broker, composer half** (issue 1377, novox/hq ADR 0547 §5), taken at -// mesh-controller 7cd4d18b with the grants part's membership grant of f67d1553 (ADR 0547 §5) and its grant to create the streams manifests declare (issue 1790): the bus client (jetstream.go), the broker's address from the environment (broker.go, onnats.go -// but its one constant) and the two methods on that client are left out; the lease's two subjects are copied in -// (nats.go). The controller keeps its own copy, unchanged, until it loses its lease at ADR 0547's third milestone, so -// for that time two composers state who may say what and must say the same: TestTheCopiedComposerComposesAsTheControllers -// holds this copy to lists the controller's own code wrote from the same fixtures. A change to what is granted is made -// here and, while the controller composes, in the controller's copy too, never in one alone. +// Package buscompose is the bus's names and shapes every part reads (novox/hq ADR 0593 §1): the mesh's streams and +// dead letters, a module's log, stream and buckets as the bus names them, the account a part asks for, a principal's +// shape, and the grants part's memberships key. The composer of the bus's user list, which reads these and makes every +// user's permissions, is the grants part's own (mesh-delivery modules/mesh-grants internal/buscompose). package buscompose diff --git a/go/buscompose/engine.go b/go/buscompose/engine.go index 6426db7..1f52f8c 100644 --- a/go/buscompose/engine.go +++ b/go/buscompose/engine.go @@ -1,18 +1,5 @@ package buscompose -import ( - "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/provisions" -) - -// The node seat node-engine (novox/hq ADR 0563 §1): every node's engine holds it on its own bus account, and no module -// claims it. Its verbs and events are the catalogue's (catalogue.EngineVerbs), read here rather than copied. - -// EngineSeat is the seat. -const EngineSeat = catalogue.EngineSeat - // EngineModule is the node-engine's manifest's module name, and EngineActsState its state the acts asked through the // node seat and its own asks are kept in (ADR 0563 §2, §3: "the engine's own state"): the bucket mesh-host_acts, keys // `.act.`, `.open..` and `.ask.`, each written by that node's engine alone. @@ -23,60 +10,3 @@ const ( // EngineActsBucket is that state's bucket. var EngineActsBucket = BucketName(EngineModule, EngineActsState) - -// EnginePackagesBucket is the engine's state of its node's declared packages (novox/hq ADR 0565 clause 6): one key, the -// node's name, written by that node's engine alone. -var EnginePackagesBucket = BucketName(EngineModule, delivery.EnginePackagesState) - -// EngineDataBucket is the engine's state of its node's placed modules' data items (novox/hq ADR 0577 clause 1): one -// key `.` per module, written by that node's engine alone. -var EngineDataBucket = delivery.EngineDataBucket - -// EnginePackagesGrants is what a node's engine publishes to keep its node's key of the packages bucket: the key alone, -// and its read. -func EnginePackagesGrants(node string) []string { - return append([]string{"$KV." + EnginePackagesBucket + "." + node}, readGrants(EnginePackagesBucket, node)...) -} - -// engineGrants is what a node's engine is granted to hold its node seat and to ask the operator (ADR 0563 §1, §3): it -// answers its own node's verbs (the caller-named subjects are added beside them with every principal's), says the -// seat's events about its node, answers discovery for the seat, asks who answers what (the login shell's execute, ADR -// 0563 §6), and asks the operator under asks.EngineAsker(node) on the router's seat as its holder declares it, as any -// user of that seat is granted. -// -// Its verbs and events are granted as any verb and any event of the seat on its own node, not as the contract's list: -// the list is compiled into whichever mesh-sdk the grants part pins, so a verb the engine gained before the grants part -// was released was refused on every node and the engine reopened its link every few minutes (2026-10-31, the optional -// verb composition of mesh-host #184). As a module answers `mesh.mod..tool.>`, nothing is given away: no other -// principal may subscribe this seat's subjects of this node, and a caller is still granted each verb by name. -func engineGrants(p Principal) (pub, sub []string) { - sub = append(sub, "mesh.seat."+EngineSeat+".tool.*."+p.Node) - pub = append(pub, "mesh.seat."+EngineSeat+".event.*."+p.Node) - sub = append(sub, announcing(EngineSeat)...) - pub = append(pub, discovering()...) - tp, ts := seatTrafficGrants(SeatTrafficOf(asks.EngineAsker(p.Node), nil, p.Uses, nil)) - // And its acts and asks, under its own node's keys alone; never the bucket's creation, which is the grants part's. - if own, err := NodeKeysGrants(EngineActsBucket, p.Node); err == nil { - pub = append(pub, own...) - } - // And the provisions its node's modules consume, its own node's keys alone, read (novox/hq ADR 0570 clause 6: the - // consuming engine checks the vault's name and generation of what it is handed). - if safeSubject.MatchString(p.Node) { - pub = append(pub, readGrants(provisions.ProvisionsBucket, p.Node+".>")...) - } - // And its node's own health report, its key `` alone (issue 774, the operator's alert table): what the watchman - // reads for the machine's alerts. - if safeSubject.MatchString(p.Node) { - pub = append(pub, append([]string{"$KV." + delivery.NodeHealthBucket + "." + p.Node}, - readGrants(delivery.NodeHealthBucket, p.Node)...)...) - } - // And its node's placed modules' data items, its own node's keys alone (ADR 0577 clause 1). - if own, err := NodeKeysGrants(EngineDataBucket, p.Node); err == nil { - pub = append(pub, own...) - } - // And its node's declared packages, its own key alone (ADR 0565 clause 6). - if safeSubject.MatchString(p.Node) { - pub = append(pub, EnginePackagesGrants(p.Node)...) - } - return append(pub, tp...), append(sub, ts...) -} diff --git a/go/buscompose/fixtures_test.go b/go/buscompose/fixtures_test.go deleted file mode 100644 index bfeda2f..0000000 --- a/go/buscompose/fixtures_test.go +++ /dev/null @@ -1,119 +0,0 @@ -package buscompose - -// The fixtures the copied bus composer is held to (issue 1377, ADR 0547 §5): the same source is compiled into the -// controller's internal/broker to write the expected lists, and into mesh-sdk's buscompose to compare against them. - -// UserListFixture is one mesh's records, and the hashes its accounts carry. -type UserListFixture struct { - Name string - Records Records - // Hashless are users given no hash, so they are left out and named. - Hashless []string -} - -func fixtureTelegramSeat() Seat { - return Seat{Name: "telegram-sender", Accepts: []string{"send"}, Emits: []string{"delivered", "failed"}} -} - -// UserListFixtures are the meshes, from the controller's own tests' records outward. -func UserListFixtures() []UserListFixture { - return []UserListFixture{ - {Name: "empty"}, - {Name: "the controller's someRecords", Records: Records{ - Nodes: []string{"two", "one"}, - Assigned: map[string][]Declared{ - "one": {{Module: "telegram", Serves: []string{"status"}}}, - "two": {{Module: "shop", Emits: []string{"order.placed"}}}, - }, - Enrolling: []string{"three"}, - People: map[string][]string{"ada": {"mesh-catalog.catalog_tools"}}, - }}, - {Name: "seats, consumers, people and a hashless user", Hashless: []string{"two.audit"}, Records: Records{ - Nodes: []string{"one", "two"}, - Assigned: map[string][]Declared{ - "one": { - {Module: "telegram", Holds: []Seat{fixtureTelegramSeat()}, Serves: []string{"status"}}, - {Module: "nats", SnapshotsTheBus: true, Serves: []string{"nats_streams"}}, - {Module: "mesh-watchman"}, - {Module: "mesh-grants", Emits: []string{"membership-changed"}}, - }, - "two": { - {Module: "shop", Uses: []Seat{fixtureTelegramSeat()}, Emits: []string{"order.placed"}}, - {Module: "audit", Consumes: []string{"shop.order.placed"}}, - }, - }, - People: map[string][]string{"ada": {"*"}, "bob": {"shop.status"}}, - Interchangeable: map[string]bool{"shop": true}, - }}, - {Name: "state, logs, streams, queues and processes", Records: Records{ - Nodes: []string{"ace", "novox"}, - Assigned: map[string][]Declared{ - "novox": { - {Module: "mesh-delivery-builder", Emits: []string{"built"}, - State: []Bucket{{Module: "mesh-delivery-builder", Name: "steps", History: 5}, - {Module: "mesh-delivery-builder", Name: "presence", PerMachine: true, KeyTTL: true, TTLSeconds: 60}}, - Logs: []Log{{Module: "mesh-delivery-builder", Name: "builds", MaxMiB: 64}}, - Streams: []ModuleStream{{Module: "mesh-delivery-builder", Name: "asks", Events: []string{"ask"}, MaxAgeDays: 7, MaxMiB: 32, WorkQueue: true}}, - Queues: []QueueOwner{{Process: "worker", Stream: "asks", MaxDeliver: 5}}, - Processes: []ProcessPrincipal{{Process: "worker", Emits: []string{"built"}, - Queues: []QueueOwner{{Process: "worker", Stream: "asks", MaxDeliver: 5}}, - State: []string{"steps"}, Invokes: []string{"gitea.gitea_commit"}}}, - Invokes: []string{"gitea.gitea_commit"}, - }, - {Module: "mesh-delivery-view", Reads: []string{"mesh-delivery-builder.steps"}, - KeyedReads: []KeyedRead{{Read: "mesh-delivery-builder.presence", Key: "novox"}}}, - // The grants part, which may create every stream declared here (ADR 0547 §5, issue 1790). - {Module: "mesh-grants", Emits: []string{"membership-changed"}, - State: []Bucket{{Module: "mesh-grants", Name: "assertions", History: 5}}}, - }, - "ace": { - {Module: "plex", Serves: []string{"plex_status"}, Checks: []string{"plex.plex_status"}}, - {Module: "silent", NoAccount: true}, - }, - }, - RootFree: map[string]bool{"ace": true}, - }}, - } -} - -// HashOf is the stand-in hash a fixture's user carries. -func HashOf(user string) string { - h := "$2a$11$" - for len(h) < 29 { - h += user - } - return h[:29] + "fixturefixturefixturefixture00" -} - -// ComposedFor is a fixture's user list as the composer writes it, and the users left out. -func ComposedFor(f UserListFixture) (string, []string, error) { - users, err := Users(f.Records) - if err != nil { - return "", nil, err - } - hashes := map[string]string{} - skip := map[string]bool{} - for _, u := range f.Hashless { - skip[u] = true - } - for _, u := range users { - if !skip[u.Username()] { - hashes[u.Username()] = HashOf(u.Username()) - } - } - filled, missing := WithPasswords(users, hashes) - list, err := ComposeAccounts(filled) - return list, missing, err -} - -// MembershipsFor is every membership a fixture's records issue, by `.`, as the composer makes them. -func MembershipsFor(f UserListFixture) map[string]Membership { - where := PlacementsOf(f.Records, f.Records.Interchangeable) - out := map[string]Membership{} - for node, ds := range f.Records.Assigned { - for _, d := range ds { - out[node+"."+d.Module] = MembershipFor(node, d, where) - } - } - return out -} diff --git a/go/buscompose/health_grants.go b/go/buscompose/health_grants.go index 29036e9..a06f64d 100644 --- a/go/buscompose/health_grants.go +++ b/go/buscompose/health_grants.go @@ -3,27 +3,12 @@ package buscompose import ( "fmt" "slices" - "sort" "strings" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/health" ) -// **The health holders' grants** (novox/hq ADR 0564 clauses 2, 5 and 8, Limits 2, 4 and 8; issue 1952 S5; ADR 0590). -// -// - Nobody reads mesh-healers' `would` (clause 8): what the healers would have done reaches no condition. Every -// observer's `findings`, the healers' `attempts`, the wants and the haves are read through `reads` as any announced -// state is; and no module writes another's state, so an observer can never write the keeper's buckets (clause 2). -// - The watchman makes its own consumer on DEAD_LETTER_NOTICES, by its own name, pulled, and reads whether the ended -// controller's consumer `controller` still stands; it may neither delete nor change that consumer (clause 5). -// - The bus module's tools run on an account of their own, declared by their bundle's `bus` with `removes` -// (catalogue.BusAccounts), which may remove only keys of the node-engine's acts bucket (Limits 4) and the ended -// controller's leftovers (ADR 0590 clause 6), never the backup's account `broker`, which only reads (ADR 0235). - -// HealersWould is what mesh-healers would have done while a controller was placed: read by nobody (ADR 0564 clause 8). -var HealersWould = health.HealersModule + "." + health.WouldState - // ControllerLeftoverConsumers are the ended controller's durable consumers left on the bus, one leftover of the module // mesh-delivery placed nowhere (issue 1126, milestone section 3), and mesh-catalog's of the controller's built-before, // which its manifest no longer consumes and no part composes; each `.`. @@ -92,55 +77,6 @@ func BusRemovalGrants(removes, nodes []string) ([]string, error) { return out, nil } -// removesLeftover is an account declared to remove the ended controller's leftovers deleting one of its streams: the -// writers table's one exemption from stream definitions being the grants part's (ADR 0590 clause 6). -func removesLeftover(p Principal, pattern string) bool { - name, ok := strings.CutPrefix(pattern, "$JS.API.STREAM.DELETE.") - return ok && p.Kind == KindProcess && slices.Contains(p.Removes, catalogue.RemovesControllerLeftovers) && - slices.Contains(ControllerLeftoverStreams(p.FilesFor), name) -} - -// removesEngineActs is an account declared to remove keys of the node-engine's acts bucket: the writers table's one -// exemption from a node's engine being the only writer of its keys, for removal (ADR 0564 Limits 4). -func removesEngineActs(p Principal, pattern string) bool { - return p.Kind == KindProcess && slices.Contains(p.Removes, catalogue.RemovesEngineActs) && pattern == "$KV."+EngineActsBucket+".>" -} - -// WatchmanDeadLetterConsumer is the watchman's own consumer on DEAD_LETTER_NOTICES, made only once the lease names no -// controller and `controller` is gone (ADR 0564 clause 5). -const WatchmanDeadLetterConsumer = WatchmanModule - -// WatchmanDeadLetterGrants are what the watchman may publish for the dead letters in L4 (ADR 0564 clause 5), at least -// privilege: its own pulled consumer on DEAD_LETTER_NOTICES (made, read, pulled, acknowledged); whether `controller` -// still stands (read only); a read of DEAD_LETTERS (its information and one kept message); a copy kept -// (`mesh.events.dead.>`); and on each stream a notice can name (sources: EVENTS, the seats' work queues and the -// modules' own), its information, for a source's age (ADR 0376), and the message given up on, each stream by its name. -// Never a wildcard over the streams and never a bucket's or an object store's stream: a read of any message would read -// every bucket (the lead, 2026-10-29, on issue 1952 S5). Delivering again and dropping stay refused in L4, so it may -// neither delete a message nor publish `mesh.again.>`. -func WatchmanDeadLetterGrants(sources []string) []string { - own := DeadLetterNoticesStream + "." + WatchmanDeadLetterConsumer - out := []string{ - "$JS.API.STREAM.INFO." + DeadLetterNoticesStream, - "$JS.API.CONSUMER.INFO." + DeadLetterNoticesStream + "." + ControllerName, - "$JS.API.CONSUMER.CREATE." + own, - "$JS.API.CONSUMER.INFO." + own, - "$JS.API.CONSUMER.MSG.NEXT." + own, - "$JS.ACK." + own + ".>", - "$JS.API.STREAM.INFO." + DeadLettersStream, - "$JS.API.STREAM.MSG.GET." + DeadLettersStream, - deadLetterPrefix + ">", - // And the provisioner clients' reports, the newest per subject read at its start (S21, ADR 0362 §3). - "$JS.API.STREAM.INFO." + ProvAppliedStream, - "$JS.API.STREAM.MSG.GET." + ProvAppliedStream, - "$JS.API.DIRECT.GET." + ProvAppliedStream + ".>", - } - for _, s := range DeadLetterSources(sources) { - out = append(out, "$JS.API.STREAM.INFO."+s, "$JS.API.STREAM.MSG.GET."+s) - } - return out -} - // WatchmanHears are the core subjects the watchman subscribes, with no durable consumer (ADR 0564 clauses 4, 5 and // Limits 3, 11): the power seat's words (S1, ADR 0211), the providers' failing and recovered words (S8), each tool // runner's beat (S11), the self-check's checked (S10), a consumer deleted and the bus module's slow consumers and closed @@ -161,52 +97,3 @@ func WatchmanHears() []string { deadLetterPrefix + ">", } } - -// DeadLetterSources are the streams of names a dead letter may come from, sorted, none twice: each a plain name, never a -// bucket's (KV_) or an object store's (OBJ_) stream, the dead letters' own two streams, or a pattern. -func DeadLetterSources(names []string) []string { - seen := map[string]bool{} - var out []string - for _, n := range names { - switch { - case seen[n], !safeSubject.MatchString(n), strings.HasPrefix(n, "KV_"), strings.HasPrefix(n, "OBJ_"), - n == DeadLettersStream, n == DeadLetterNoticesStream: - continue - } - seen[n] = true - out = append(out, n) - } - sort.Strings(out) - return out -} - -// deadLetterSourcesOf are the streams the records' consumers give up on: EVENTS, the work queue of every seat that -// accepts work, and every module's and process's own work queue. -func deadLetterSourcesOf(r Records) []string { - out := []string{EventsStream} - for _, mods := range r.Assigned { - for _, d := range mods { - for _, s := range append(append([]Seat(nil), d.Holds...), d.Uses...) { - if len(s.Accepts) > 0 { - out = append(out, seatStreamName(s.Name)) - } - } - for _, q := range d.Queues { - out = append(out, q.Stream) - } - for _, pr := range d.Processes { - for _, q := range pr.Queues { - out = append(out, q.Stream) - } - } - } - } - return DeadLetterSources(out) -} - -// keepsDeadLetters is the watchman keeping the dead letters: a second writer of the copies beside the controller, which -// keeps them until its lease is handed away and the watchman after (ADR 0564 clause 5). Delivering again stays the -// controller's in L4. -func keepsDeadLetters(p Principal, pattern string) bool { - return p.Kind == KindModule && p.Module == WatchmanModule && pattern == deadLetterPrefix+">" -} diff --git a/go/buscompose/log.go b/go/buscompose/log.go index d179b2b..ef26963 100644 --- a/go/buscompose/log.go +++ b/go/buscompose/log.go @@ -2,27 +2,11 @@ package buscompose import ( "fmt" - "sort" - "strings" "time" "github.com/nats-io/nats.go/jetstream" ) -// A module's log on the bus (novox/hq ADR 0297): its record of operations, each entry appended under -// a key and kept as long as the log is. -// -// A log follows a bucket in every respect (ADR 0201): the module names it locally, the mesh derives -// its stream and subjects, the controller creates it from the catalogue on every raise and from -// registration, never a module, and **no path of the mesh removes one** — a log whose declaration -// is gone is reported, as a bucket is. Only its owner appends to it. Another module reads it when its manifest's -// `reads` names it, `.` (novox/hq ADR 0571 clause 1; the lead, 2026-10-29): by direct gets, and through an -// ordered consumer of its own it creates and lets go of to read from a time or the newest per key and then follow new -// entries; never a durable consumer, never a write. -// -// Pure, but for the stream's configuration, which is the server's own type so that what is tested -// is exactly what is sent; jetstream.go is the part that asks a server. - // The mesh's caps on a log: what an entry's value may weigh, what a message on the log's stream may // weigh — the value and its headers, which the server counts in a message's size, so a value of the // full size still fits with the expected-last-sequence header an append carries — and what a log holds @@ -31,7 +15,6 @@ const ( LogMaxEntryBytes = 256 * 1024 LogMaxMessageBytes = LogMaxEntryBytes + 4*1024 LogDefaultMiB = 1024 - LogMostMiB = 8192 ) // A Log is one module's declared log as the bus holds it. @@ -102,141 +85,3 @@ func (l Log) Config() jetstream.StreamConfig { Replicas: 1, } } - -// logsIssuedFor is every log a module's code may reach, as its membership lists them: its own. -func logsIssuedFor(d Declared) []LogIssued { - var out []LogIssued - for _, l := range d.Logs { - if !safeSubject.MatchString(d.Module) || !safeSubject.MatchString(l.Name) { - continue - } - out = append(out, LogIssued{Name: l.Name, Stream: LogStreamName(d.Module, l.Name), - Subject: LogSubject(d.Module, l.Name), Writes: true}) - } - return out -} - -// logGrants is what a principal publishes to reach the logs its module keeps: for each, appending -// under the log's subjects, binding to its stream, and reading it directly. Nothing more — the -// runtime reads a log by direct gets alone and makes no consumer on it — and nothing of any other -// module's log, because a log is its owner's alone. Replies come on the principal's inbox, as for a -// bucket. -func logGrants(module string, names []string) []string { - if !safeSubject.MatchString(module) { - return nil - } - var out []string - for _, name := range names { - if !safeSubject.MatchString(name) { - continue - } - stream := LogStreamName(module, name) - out = append(out, - LogSubject(module, name)+".>", - "$JS.API.STREAM.INFO."+stream, - "$JS.API.DIRECT.GET."+stream, - "$JS.API.DIRECT.GET."+stream+".>") - } - return out -} - -// DeclaredLogs is every log a module placed anywhere declares, as a read names it: `.`. -func DeclaredLogs(r Records) map[string]bool { - out := map[string]bool{} - for _, ds := range r.Assigned { - for _, d := range ds { - for _, l := range d.Logs { - out[d.Module+"."+l.Name] = true - } - } - } - return out -} - -// withReadLogs is d with ReadLogs set: of its reads, those naming a log of logs. -func withReadLogs(d Declared, logs map[string]bool) Declared { - d.ReadLogs = nil - for _, r := range d.Reads { - if logs[strings.TrimSpace(r)] { - d.ReadLogs = append(d.ReadLogs, strings.TrimSpace(r)) - } - } - return d -} - -// readLogGrants is what a reader of other modules' logs publishes, for each `.` read: the stream's info, -// direct gets (an entry by sequence, the last of a key), and an ordered consumer of its own (created with any name and -// filter, pulled, read and deleted). No append, no purge, no delete of an entry: the log's stream refuses those anyway. -func readLogGrants(reads []string) []string { - var out []string - for _, r := range reads { - module, name, ok := strings.Cut(r, ".") - if !ok || !safeSubject.MatchString(module) || !safeSubject.MatchString(name) { - continue - } - stream := LogStreamName(module, name) - out = append(out, "$JS.API.STREAM.INFO."+stream, "$JS.API.DIRECT.GET."+stream, "$JS.API.DIRECT.GET."+stream+".>", - "$JS.API.CONSUMER.CREATE."+stream+".>", "$JS.API.CONSUMER.MSG.NEXT."+stream+".*", - "$JS.API.CONSUMER.INFO."+stream+".*", "$JS.API.CONSUMER.DELETE."+stream+".*") - } - return out -} - -// readLogsIssuedFor is every other module's log a module's code reads, as its membership lists them: by the read's -// name, never written. -func readLogsIssuedFor(d Declared) []LogIssued { - var out []LogIssued - for _, r := range d.ReadLogs { - module, name, _ := strings.Cut(r, ".") - out = append(out, LogIssued{Name: r, Stream: LogStreamName(module, name), Subject: LogSubject(module, name)}) - } - return out -} - -// logNames is the local names of a module's logs. -func logNames(logs []Log) []string { - out := make([]string, 0, len(logs)) - for _, l := range logs { - out = append(out, l.Name) - } - return out -} - -// A LogAsserter is the part of a JetStream connection log assertion needs. It has no way to remove a -// log, by design: nothing the mesh runs asks for one. -type LogAsserter interface { - // EnsureLog creates the log's stream if absent and brings its configuration to match if present, - // never deleting or recreating it. - EnsureLog(l Log) error - // LogStreams is every log stream on the server: every stream named with LogStreamPrefix. - LogStreams() ([]string, error) -} - -// RaiseLogs asserts every declared log and answers the log streams on the server that nothing -// declares any more. -// -// **Those are reported, never removed** (novox/hq ADR 0297 §2, ADR 0201 §15–16): a log is a module's -// record, and a manifest edited, a module renamed or unassigned is an ordinary day's work that must -// not take a record with it. Removing one is a person's act, outside the mesh. -func RaiseLogs(a LogAsserter, logs []Log) (undeclared []string, err error) { - sorted := append([]Log(nil), logs...) - sort.Slice(sorted, func(i, j int) bool { return sorted[i].Stream() < sorted[j].Stream() }) - declared := map[string]bool{} - for _, l := range sorted { - if err := a.EnsureLog(l); err != nil { - return nil, fmt.Errorf("asserting %s's log %q: %w", l.Module, l.Name, err) - } - declared[l.Stream()] = true - } - names, err := a.LogStreams() - if err != nil { - return nil, fmt.Errorf("listing the bus's logs: %w", err) - } - for _, n := range names { - if strings.HasPrefix(n, LogStreamPrefix) && !declared[n] { - undeclared = append(undeclared, n) - } - } - sort.Strings(undeclared) - return undeclared, nil -} diff --git a/go/buscompose/membership.go b/go/buscompose/membership.go deleted file mode 100644 index 8601788..0000000 --- a/go/buscompose/membership.go +++ /dev/null @@ -1,178 +0,0 @@ -package buscompose - -import ( - "slices" - "sort" - "strings" -) - -// What the mesh issues an assignment to serve and to reach (novox/hq ADR 0160). -// -// A module's code names its tools and its events; **where they land is the mesh's to decide**, and -// it decided it twice — once in the runtime, once here, by one rule compiled into both. Now the -// controller composes a membership for every module on every machine and publishes it to a subject -// only that assignment reads; the runtime serves exactly what the membership says, and the account's -// grant is the same composition read the other way. The shape issued today is the shape the mesh -// already had, so nothing moves when a membership first arrives; only who decides it moves. - -// Placements is where every module runs, for deciding which instance answers for the module. -type Placements struct { - // Nodes is each module's machines. - Nodes map[string][]string - // Interchangeable is each module whose definition says its instances are the same anywhere, - // so the module's plain subject is issued to all of them in one queue. - Interchangeable map[string]bool - // Kinds is every kind held of a kinded bench, by whom and with what capabilities (ADR 0259 §5). - Kinds []KindHeld - // Logs is every log a module placed anywhere declares, `.` (DeclaredLogs): a read naming one is issued - // as a log read (novox/hq ADR 0571 clause 1). - Logs map[string]bool -} - -// AnswersForTheModule says whether an instance of a module on one machine is issued the module's -// plain subject: when it is the only instance, or when the definition says instances are -// interchangeable. A stateful module on two machines gets only its machines' subjects, so a call -// that names none reaches nothing rather than the wrong store. -func (p Placements) AnswersForTheModule(module string) bool { - return len(p.Nodes[module]) <= 1 || p.Interchangeable[module] -} - -// MembershipFor composes one assignment's membership from what it declared and where everything -// runs. The subjects are the ones PermissionsFor grants, derived here once more only until the -// grant itself is read from the membership — which is the next step, not this one. -func MembershipFor(node string, d Declared, where Placements) Membership { - own := "mesh.mod." + d.Module - m := Membership{ - Node: node, Module: d.Module, - Emits: own + ".event.{event}", - Tools: own + ".tool.tools", - } - // This machine's address always; the module's when this instance answers for the module. - m.Serves = append(m.Serves, Served{Subject: own + ".tool.{tool}." + node}) - if where.AnswersForTheModule(d.Module) { - m.Serves = append(m.Serves, Served{Subject: own + ".tool.{tool}", Queue: "serve." + d.Module}) - } - for _, s := range d.Holds { - for _, verb := range s.Serves { - m.Seats = append(m.Seats, SeatServed{Seat: s.Name, Verb: verb, Subject: seatToolSubject(s, verb, node)}) - } - } - m.State = stateIssuedFor(d, node) - m.Logs = append(logsIssuedFor(d), readLogsIssuedFor(withReadLogs(d, where.Logs))...) - m.Streams = streamsIssuedFor(d) - m.Hears = HeardIssuedFor(node, d) - m.Waits = waitsIssuedFor(d) - t := SeatTrafficOf(d.Module, d.Holds, d.Uses, d.Watches) - for _, s := range append(append([]Seat{}, d.Uses...), d.Watches...) { - if !s.Kinded { - continue - } - for _, k := range where.Kinds { - if k.Seat == s.Name && !kindListed(t.Kinds, k) { - t.Kinds = append(t.Kinds, k) - } - } - } - if len(t.Publish)+len(t.Subscribe)+len(t.Answers)+len(t.Workers)+len(t.Records)+len(t.Kinds) > 0 { - m.SeatTraffic = &t - } - if len(d.Invokes) > 0 { - m.Reaches = map[string][]string{} - for _, t := range d.Invokes { - if t == "*" || strings.HasPrefix(t, "seat:") { - continue // every tool, or a role's: addressed by name, not resolved per instance - } - module, tool, ok := strings.Cut(t, ".") - if !ok { - continue - } - var reach []string - if where.AnswersForTheModule(module) { - reach = append(reach, "mesh.mod."+module+".tool."+tool) - } - nodes := append([]string{}, where.Nodes[module]...) - sort.Strings(nodes) - for _, n := range nodes { - reach = append(reach, "mesh.mod."+module+".tool."+tool+"."+n) - } - m.Reaches[t] = reach - } - } - return m -} - -// PlacementsOf reads where everything runs from the records the bus's accounts are composed from. -func PlacementsOf(r Records, interchangeable map[string]bool) Placements { - p := Placements{Nodes: map[string][]string{}, Interchangeable: interchangeable, Logs: DeclaredLogs(r)} - for node, declared := range r.Assigned { - for _, d := range declared { - p.Nodes[d.Module] = append(p.Nodes[d.Module], node) - } - } - for _, nodes := range p.Nodes { - sort.Strings(nodes) - } - // Where the router runs: a verified sender is believed only while its machine is root-free too. A router - // placed nowhere, or on more than one machine, frees nothing. - var routerNodes []string - for node, declared := range r.Assigned { - for _, d := range declared { - for _, s := range d.Holds { - if s.Name == routerSeat && !slices.Contains(routerNodes, node) { - routerNodes = append(routerNodes, node) - } - } - } - } - routerFree := len(routerNodes) == 1 && r.RootFree[routerNodes[0]] - for node, declared := range r.Assigned { - for _, d := range declared { - for _, s := range d.Holds { - if s.Kinded && s.Kind != "" { - p.Kinds = append(p.Kinds, KindHeld{Seat: s.Name, Kind: s.Kind, Module: d.Module, Node: node, - Capabilities: placedCapabilities(s.Capabilities, d.RunsAs, routerFree && r.RootFree[node])}) - } - } - } - } - sort.Slice(p.Kinds, func(i, j int) bool { - a, b := p.Kinds[i], p.Kinds[j] - if a.Seat != b.Seat { - return a.Seat < b.Seat - } - if a.Kind != b.Kind { - return a.Kind < b.Kind - } - return a.Node < b.Node - }) - return p -} - -// routerSeat is the seat the router of asks holds (novox/hq ADR 0259 §3). -const routerSeat = "operator-channel" - -// placedCapabilities is what a kind's claim promises, as far as its placement lets the router believe it -// (novox/hq ADR 0259 §8): `verified-sender` only from a holder that runs as an account of its own, on a bus -// account of its own — never one the machine's runtime carries as the operator's account — and only while -// its machine and the router's were root-free when composed (rootFree; the review of 2026-10-09, H3). The -// membership carrying it is composed at a push, so it can outlive a pass that later fails: the router asks -// the controller's root-free verb again before it honours an approval, and that is the check that holds. -func placedCapabilities(declared []string, runsAs string, rootFree bool) []string { - var out []string - for _, c := range declared { - if c == "verified-sender" && (runsAs == "" || !rootFree) { - continue - } - out = append(out, c) - } - return out -} - -func kindListed(list []KindHeld, k KindHeld) bool { - for _, x := range list { - if x.Seat == k.Seat && x.Kind == k.Kind && x.Module == k.Module && x.Node == k.Node { - return true - } - } - return false -} diff --git a/go/buscompose/modstreams.go b/go/buscompose/modstreams.go index 956d2ed..8505473 100644 --- a/go/buscompose/modstreams.go +++ b/go/buscompose/modstreams.go @@ -3,18 +3,8 @@ package buscompose import ( "fmt" "sort" - "strings" ) -// A module's own stream on the bus (novox/hq ADR 0363). -// -// A module declares a stream as it declares a log: named locally, created by the controller from the -// catalogue on every raise and every send, never removed by the mesh. Its events are published on -// `mesh.stream..event.`, a subject space the shared EVENTS stream (`mesh.mod.*.event.>`) -// does not take — the bus refuses two streams claiming one subject, so a module's stream cannot share -// EVENTS' — and a consumer hears it through a durable consumer of its own on that stream, named as on -// EVENTS. Pure; the controller's raise asserts what is composed here. - // moduleStreamPrefix starts every subject a module's own stream takes. const moduleStreamPrefix = "mesh.stream." @@ -24,20 +14,6 @@ func ModuleStreamSubject(module, event string) string { return moduleStreamPrefix + module + ".event." + event } -// OnAModuleStream says a subject is one a module's own stream takes. -func OnAModuleStream(subject string) bool { return strings.HasPrefix(subject, moduleStreamPrefix) } - -// WirePattern is a local event pattern as the bus spells it: `**` is `>`. -func WirePattern(local string) string { - parts := strings.Split(local, ".") - for i, p := range parts { - if p == "**" { - parts[i] = ">" - } - } - return strings.Join(parts, ".") -} - // A ModuleStream is one module's declared stream as the bus holds it. type ModuleStream struct { Module string @@ -120,33 +96,6 @@ type QueueOwner struct { MaxDeliver int } -// QueueConsumer is the durable consumer the one process a module's bus.queues names is granted on its own -// work-queue stream (novox/hq ADR 0436 §1, §2, §6): no filter, since it is the stream's only consumer — -// unlike heardConsumer, which is one of possibly several hearing a module's ordinary stream. -func QueueConsumer(p Principal, q QueueOwner) Consumer { - return Consumer{ - Name: p.Node + "_" + p.Module + "_" + q.Process, - Stream: q.Stream, - AckWaitSeconds: 30, - MaxDeliver: q.MaxDeliver, - Why: fmt.Sprintf("%s's process %s is the one consumer %s's bus.queues names for its work-queue "+ - "stream %s (novox/hq ADR 0436 §6); after max-deliver %d it gives a message up, and the "+ - "controller keeps it in %s, offered only drop", p.Module, q.Process, p.Module, q.Stream, - q.MaxDeliver, DeadLettersStream), - } -} - -// ModuleStreams is every declared module stream as the controller asserts it, in name order. -func ModuleStreams(declared []ModuleStream) []Stream { - sorted := append([]ModuleStream(nil), declared...) - sort.Slice(sorted, func(i, j int) bool { return sorted[i].Name < sorted[j].Name }) - out := make([]Stream, 0, len(sorted)) - for _, s := range sorted { - out = append(out, s.Stream()) - } - return out -} - // Heard is a module's stream another module hears, and the subjects it hears of it: its `consumes` // within the stream and, for a restricted stream, within what the provider's trusted config value grants it // (ADR 0363 §5). Composed only with at least one subject. @@ -158,186 +107,3 @@ type Heard struct { // is heard only by a module on an account of its own (ADR 0376 §1, ComposedHears). Restricted bool } - -// ComposedHears is what of a module's hearing the controller composes (novox/hq ADR 0376 §1): every stream it -// hears, but a restricted one only for a module that runs in a runtime of its own on a bus account of its own -// (`runs-as`, ADR 0259). **The tool runner never hears a restricted stream**: a module it carries reaches the bus -// through the tool runner's account, which every agent session on that machine reaches the bus as too, so a -// consumer, its grants or its bind word composed there would hand the stream to any session. A module that -// declares no `runs-as` and keeps its own container credential gets none either: only `runs-as` is an account -// the mesh keeps from agents. -func ComposedHears(d Declared) []Heard { - if d.RunsAs != "" { - return d.Hears - } - var out []Heard - for _, h := range d.Hears { - if !h.Restricted { - out = append(out, h) - } - } - return out -} - -// Withheld is a restricted stream a grant gives a module that the controller composes nothing for, because the -// module has no account of its own (novox/hq ADR 0376 §2): said in `doctor` and in the controller's log, never -// silent. -type Withheld struct { - Node, Module, Stream, Emitter string -} - -// Said is a withheld hearing in the operator's words: the module, the stream and the remedy. -func (w Withheld) Said() string { - return fmt.Sprintf("%s on %s is granted part of %s's restricted stream %s, and hears none of it: it is carried "+ - "by the machine's tool runner, whose bus account every agent session there shares, so nothing is composed "+ - "for it. Declare runs-as in %s's manifest, so it runs as an account of its own (novox/hq ADR 0376)", - w.Module, w.Node, w.Emitter, w.Stream, w.Module) -} - -// WithheldOf is every granted hearing of a restricted stream composed for nothing, by machine and module. -func WithheldOf(r Records) []Withheld { - var out []Withheld - for _, node := range sortedCopy(r.Nodes) { - for _, d := range r.Assigned[node] { - if d.RunsAs != "" { - continue - } - for _, h := range d.Hears { - if h.Restricted && len(h.Filters) > 0 { - out = append(out, Withheld{Node: node, Module: d.Module, Stream: h.Stream, Emitter: h.Emitter}) - } - } - } - } - return out -} - -// heardConsumer is the durable consumer a module hears a module's stream through: named as its consumer -// on EVENTS, on that stream, with the subjects it was granted as its filters. -func heardConsumer(p Principal, h Heard) Consumer { - filters := append([]string(nil), h.Filters...) - sort.Strings(filters) - return Consumer{ - Name: consumerDurable(p), - Stream: h.Stream, - Filters: filters, - AckWaitSeconds: 30, - MaxDeliver: 5, - // From now, like a module's consumer on EVENTS (novox/hq ADR 0443 §1). - FromNow: true, - Why: "what " + p.Module + " consumes of " + h.Emitter + "'s stream " + h.Stream + " and is granted " + - "(novox/hq ADR 0363); after max-deliver it gives an event up, and the controller keeps it in " + - "DEAD_LETTERS until a person delivers it again or drops it", - } -} - -// heardGrants are what a principal publishes to read and acknowledge the consumers it hears module streams -// through: asking about each, pulling from it, acknowledging it — its own name on each stream, no other's. -func heardGrants(p Principal, hears []Heard) []string { - var out []string - durable := consumerDurable(p) - for _, h := range hears { - if !safeSubject.MatchString(h.Stream) || len(h.Filters) == 0 { - continue - } - out = append(out, - "$JS.API.CONSUMER.INFO."+h.Stream+"."+durable, - "$JS.API.CONSUMER.MSG.NEXT."+h.Stream+"."+durable, - "$JS.ACK."+h.Stream+"."+durable+".>") - } - return out -} - -// ownStreamGrants are the subjects a module publishes its own streams' events on, and no other's. -func ownStreamGrants(module string, streams []ModuleStream) []string { - var out []string - for _, s := range streams { - if s.Module != module { - continue - } - out = append(out, s.Subjects()...) - } - return out -} - -// streamsIssuedFor is every stream of its own a module's membership names. -func streamsIssuedFor(d Declared) []StreamIssued { - var out []StreamIssued - for _, s := range d.Streams { - if s.Module != d.Module || !safeSubject.MatchString(d.Module) { - continue - } - out = append(out, StreamIssued{Stream: s.Name, Events: append([]string(nil), s.Events...), - Subject: ModuleStreamSubject(d.Module, "{event}")}) - } - return out -} - -// HeardIssuedFor is every module stream an assignment hears, as its membership names it. -func HeardIssuedFor(node string, d Declared) []HeardIssued { - var out []HeardIssued - // A restricted stream's bind word only for a module of its own account (novox/hq ADR 0376 §1). - for _, h := range ComposedHears(d) { - if len(h.Filters) == 0 { - continue - } - out = append(out, HeardIssued{Stream: h.Stream, Consumer: node + "_" + d.Module}) - } - sort.Slice(out, func(i, j int) bool { return out[i].Stream < out[j].Stream }) - return out -} - -// Pruner is the part of a JetStream connection that removes a module's consumer on a module's stream it -// may no longer hear (ADR 0363 §6). Optional: what only records what it is asked to assert has none. -type Pruner interface { - ConsumerNames(stream string) ([]string, error) - RemoveConsumer(stream, name string) error -} - -// PruneHeard removes, from each module stream, every durable consumer the composition no longer asks for, -// and answers what it removed as `/`. A consumer that loses its grant loses what it reads -// through, whatever its runtime still holds; a stream that does not exist yet has nothing to remove. -func PruneHeard(p Pruner, streams []ModuleStream, wanted []Consumer) ([]string, error) { - keep := map[string]bool{} - for _, c := range wanted { - keep[c.Stream+"/"+c.Name] = true - } - var removed []string - for _, s := range streams { - names, err := p.ConsumerNames(s.Name) - if err != nil { - return removed, fmt.Errorf("listing the consumers on %s: %w", s.Name, err) - } - for _, n := range names { - if keep[s.Name+"/"+n] { - continue - } - if err := p.RemoveConsumer(s.Name, n); err != nil { - return removed, fmt.Errorf("removing %s's consumer on %s, which is no longer granted: %w", n, s.Name, err) - } - removed = append(removed, s.Name+"/"+n) - } - } - return removed, nil -} - -// EnsureHeard asserts a consumer on a module's stream (ADR 0363 §5, §6). **On a restricted stream, a consumer -// the bus will not bring to match is removed**: a pull consumer delivers to whoever pulls it, so one left with -// its wider filters after a narrowing was refused would keep handing over what the grant no longer names, -// whatever the user list now says. The next assertion makes it again with the granted filters. The refusal is -// returned either way, saying what was done, so the send says it and raises it. -func EnsureHeard(r Raiser, c Consumer, restricted bool) error { - err := r.EnsureConsumer(c) - if err == nil || !restricted { - return err - } - p, prunes := r.(Pruner) - if !prunes { - return fmt.Errorf("%w; it could not be removed here, and may still deliver more than it is granted", err) - } - if rerr := p.RemoveConsumer(c.Stream, c.Name); rerr != nil { - return fmt.Errorf("%w; REMOVING IT FAILED TOO, so it may still deliver more than it is granted: %v", err, rerr) - } - return fmt.Errorf("%w; REMOVED, so it delivers nothing it is no longer granted: the next assertion makes it "+ - "again with its granted filters (novox/hq ADR 0363 §6)", err) -} diff --git a/go/buscompose/names_test.go b/go/buscompose/names_test.go new file mode 100644 index 0000000..dd76d2a --- /dev/null +++ b/go/buscompose/names_test.go @@ -0,0 +1,21 @@ +package buscompose + +import ( + "testing" + + "git.novox.be/novox/mesh-sdk/go/membership" +) + +// The names this package keeps for the grants part's composer (mesh-grants internal/buscompose, ADR 0593 §1) agree with +// what the runtime reads: the memberships bucket, and a log's stream and subject. +func TestTheBusNamesAgree(t *testing.T) { + if GrantsMembershipsBucket != membership.GrantsBucket { + t.Fatalf("the memberships bucket is %q here and %q in membership", GrantsMembershipsBucket, membership.GrantsBucket) + } + if MembershipKey("ace", "netcheck") != "ace.netcheck" { + t.Fatal("a membership's key is not .") + } + if LogStreamName("mesh-delivery-builder", "build-log") == "" || LogSubject("mesh-delivery-builder", "build-log") == "" { + t.Fatal("a log has no stream or subject") + } +} diff --git a/go/buscompose/nats.go b/go/buscompose/nats.go index 77c52e4..72cd670 100644 --- a/go/buscompose/nats.go +++ b/go/buscompose/nats.go @@ -13,16 +13,8 @@ package buscompose import ( - "errors" - "fmt" "regexp" - "slices" - "sort" - "strings" "time" - - "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // A Kind is what a principal is, which decides the shape of its authority rather than its @@ -54,13 +46,6 @@ const ( KindProcess Kind = "process" ) -// RuntimeModule is the module that IS the node's tool runtime (novox/hq ADR 0175). Where it is -// assigned, the mesh composes one runtime principal for the machine in place of that module's own, -// and the per-module containers that served tools until then stop being the way tools reach a node. -// Mirrored in the catalogue package, which the agreement test holds to the same string; one -// constant, so a rename is one edit and the two packages cannot drift. -const RuntimeModule = "node-tools" - // Seat is a role on the bus as a principal relates to it: the subjects it accepts, and those it // emits (novox/hq ADR 0118, design 29 §5). type Seat struct { @@ -257,138 +242,6 @@ type Principal struct { PasswordHash string } -// SeatVerb is one verb of one seat, on every machine holding it. -type SeatVerb struct{ Seat, Verb string } - -// VerbsTheSelfCheckAsks are the seat verbs the controller's self-check and watchdogs call (novox/hq -// to-be 45 §4): D8 reads every machine's ban list. **Named one by one, and the test that holds them -// to the probe registry is the reason they cannot drift** — a probe that calls a verb its grant does -// not name is refused by the bus on every run (found live on 2026-10-06: D8 timed out on each -// machine, refused). Asked of any machine (`.*`), read-only verbs, nothing else of the seat. -// -// D13 reads every machine's backup holder: what it measured of the data declared there (novox/hq ADR -// 0233). -var VerbsTheSelfCheckAsks = []SeatVerb{{Seat: "node-intrusion-prevention", Verb: "banned"}, - {Seat: "node-backup", Verb: "backed-up"}} - -// VerbsTheBusStepAsks are the seat verbs the bus's planned step calls (novox/hq to-be 45 §8, ADR 0236): -// the bus machine's backup holder takes the bus's snapshot now, before the bus is replaced (ADR 0235's -// dump), and says when it is taken (`backed-up`, granted with the self-check's). The one verb of -// the controller's grant that acts: through the step a person starts, and through the backups a delivery plan -// takes before it asks its first tier (novox/hq ADR 0404). -var VerbsTheBusStepAsks = []SeatVerb{{Seat: "node-backup", Verb: "now"}} - -// VerbsTheVaultAsksForASecret are the seat verbs the vault's holder calls for a secret typed at a desk (novox/hq ADR -// 0259 §10, ADR 0349 clause 9): the operator's desk opens a prompt that does not show what is typed, and answers it -// sealed to the vault's call. -var VerbsTheVaultAsksForASecret = []SeatVerb{{Seat: "node-launcher", Verb: "secret"}} - -// DeskOnly are the subjects the vault's holder alone may publish in its own name, however wide another's grant (the -// review of 2026-10-09, M4): the desk's hidden prompt, on its seat's subjects and on any holder's own module subjects. A -// grant of every tool — the runtime's, which carries every agent's calls, or a person's `*` — would otherwise reach it, -// and the prompt says the vault asks: only the bus makes that true. -func DeskOnly() []string { - var out []string - for _, v := range VerbsTheVaultAsksForASecret { - for _, base := range []string{"mesh.seat." + v.Seat + ".tool." + v.Verb, "mesh.mod.*.tool." + v.Seat + "." + v.Verb} { - out = append(out, base, base+".*") - } - // And where a call names its caller (novox/hq issue 365): any machine, any caller. - for _, base := range []string{"mesh.seat." + v.Seat + "." + CallKind + "." + v.Verb, "mesh.mod.*." + CallKind + "." + v.Seat + "." + v.Verb} { - out = append(out, base+".>") - } - } - return out -} - -// VerbsTheEnrolmentAsksTheVault are the vault's verbs the seat mesh-enrolment's holder alone calls (novox/hq ADR 0587 -// §1, §3; ADR 0561 §5): a token and its hash made and dropped, a joining node's account minted and retired. -var VerbsTheEnrolmentAsksTheVault = []SeatVerb{{Seat: VaultSeat, Verb: delivery.EnrolmentTokenVerb}, - {Seat: VaultSeat, Verb: delivery.NodeAccountVerb}} - -// EnrolmentOnly are the subjects the holder of mesh-enrolment alone may publish, however wide another's grant: the -// vault's two enrolment verbs, by the tool's and the caller-named shape (Principal.VaultEnrols denies them). -func EnrolmentOnly() []string { - var out []string - for _, v := range VerbsTheEnrolmentAsksTheVault { - out = append(out, "mesh.seat."+v.Seat+".tool."+v.Verb, "mesh.seat."+v.Seat+".tool."+v.Verb+".>", - "mesh.seat."+v.Seat+"."+CallKind+"."+v.Verb+".>") - } - return out -} - -// MayPublish says whether permissions let a principal publish one subject: an allow covers it and no deny does. -func MayPublish(perms Permissions, subject string) bool { - for _, d := range perms.PublishDeny { - if SubjectsOverlap(d, subject) { - return false - } - } - for _, a := range perms.Publish { - if SubjectsOverlap(a, subject) { - return true - } - } - return false -} - -// MaySubscribe says whether a principal with these permissions may subscribe to (and so answer) a subject. -func MaySubscribe(perms Permissions, subject string) bool { - for _, a := range perms.Subscribe { - if SubjectsOverlap(a, subject) { - return true - } - } - return false -} - -// VaultMoveIn is the vault's verb a machine's node-engine asks in its own name to hand in a value sealed to its -// machine (design 57's V3, issue 676); granted as Principal.MovesIntoTheVault says. -var VaultMoveIn = SeatVerb{Seat: "mesh-vault", Verb: "move-in"} - -// VaultNodeKey is the vault's verb a machine's node-engine asks in its own name to state its sealing key (issue 786, -// novox/hq ADR 0372); granted as Principal.StatesItsKeyToTheVault says. -var VaultNodeKey = SeatVerb{Seat: "mesh-vault", Verb: "node-key"} - -// VaultOwn is the vault's verb a machine's node-engine asks in its own name for its modules' own secrets sealed to its -// key (issue 1126, ADR 0550 §5); granted as Principal.AsksItsOwnSecrets says. -var VaultOwn = SeatVerb{Seat: "mesh-vault", Verb: "own"} - -// VaultSign is the vault's verb a machine's node-engine asks in its own name for its own node's certificate (novox/hq -// ADR 0550 §2); granted as Principal.AsksTheVaultToSign says. -var VaultSign = SeatVerb{Seat: "mesh-vault", Verb: delivery.SignVerb} - -// VaultRotateNode is the vault's verb that begins a node's account rotation; while its holder serves it, every node -// principal reads its own node's key of node-logins (novox/hq ADR 0583 §1, §2). VaultNodeAccountConfirm is the verb a -// node-engine confirms a switch with, in its new login's name (§3). -var ( - VaultRotateNode = SeatVerb{Seat: "mesh-vault", Verb: delivery.RotateNodeVerb} - VaultNodeAccountConfirm = SeatVerb{Seat: "mesh-vault", Verb: delivery.NodeAccountConfirmVerb} -) - -// EnrolmentRedeem is the seat mesh-enrolment's verb a joining node-engine calls as enrol. (novox/hq ADR 0587 §4, -// §6). -var EnrolmentRedeem = SeatVerb{Seat: catalogue.EnrolmentSeat, Verb: catalogue.EnrolmentVerbRedeem} - -// EnrolmentRedeemCall is the one subject an enrolment user may publish once it enrols through the seat: redeem, in its -// own name. -func EnrolmentRedeemCall(p Principal) string { - return "mesh.seat." + EnrolmentRedeem.Seat + "." + CallKind + "." + EnrolmentRedeem.Verb + "." + CallerToken(p.Username()) -} - -// NodeLoginsReads is what a node's engine is granted of the vault's node-logins: its own node's key, read and watched, -// and no other (novox/hq ADR 0583 §1 insight). -func NodeLoginsReads(node string) []string { - if !safeSubject.MatchString(node) { - return nil - } - return readGrants(delivery.NodeLoginsBucket, node) -} - -// perMachineEvents are a node-scoped seat's events about the holder itself, whose last token is the -// holder's machine (novox/hq ADR 0219): `paused.`, the build agent saying whether it takes work. -var perMachineEvents = map[string]bool{"paused.*": true} - // enrolmentPrefix is the space every enrolling node's user and inbox live under, so the one place the // controller may answer an enrolment is derived from the same constant the user is named from. const enrolmentPrefix = "enrol" @@ -438,40 +291,6 @@ func (p Principal) Username() string { return "" } -// AskReportSubject is where the mesh asks one machine's node-engine to say again what it last applied -// (novox/hq to-be 45 §6): its answer is an ordinary report, on its own report subject. -func AskReportSubject(node string) string { return "mesh.node." + node + ".ask.report" } - -// AskHandOverSubject is where the controller's terminal asks one machine's node-engine to hand a directory it -// uses as found to the mesh (novox/hq issue 356, issue 339): a request on core NATS, answered once on the reply -// it carries. Only the controller is granted a publish here (the writers table holds it), but **that is not who -// the engine hears**: the bus lets any principal allowed to answer reply to a message it received, on the reply -// subject that message named, so a message can arrive here from any responder. The ask is therefore signed with -// the mesh's key (link.SignedHandOver), and the engine verifies it before reading anything out of it. -func AskHandOverSubject(node string) string { return "mesh.node." + node + ".ask.hand-over" } - -// AskSetuidSearchSubject is where the controller's terminal asks one machine's node-engine to throw its last -// search for setuid programs away and start a full one (novox/hq issue 361): a request answered once, signed as -// a hand-over is (link.SetuidSearchContext), for the same reason. -func AskSetuidSearchSubject(node string) string { return "mesh.node." + node + ".ask.setuid-search" } - -// AskJudgeDirectoriesSubject is where the controller asks one machine's node-engine to judge, read-only, which of -// its declared directories the walk that follows no link another account could make would refuse (novox/hq issue -// 358): a request answered once, signed as a hand-over is (link.JudgeDirectoriesContext), for the same reason. -func AskJudgeDirectoriesSubject(node string) string { - return "mesh.node." + node + ".ask.judge-directories" -} - -// AskMoveSecretSubject is where the controller asks one machine's node-engine to move one value sealed to it into -// the vault (design 57's V3, issue 676): a request answered once, signed as a hand-over is (link.MoveSecretContext), -// for the same reason. -func AskMoveSecretSubject(node string) string { return "mesh.node." + node + ".ask.move-secret" } - -// AskAccessActSubject is where the controller asks one machine's node-engine to run an act on one access's folder (novox/hq -// ADR 0328 §7): a request answered once, signed as a hand-over is (the node-engine's link.AccessActContext). The -// node-engine has subscribed to it since ADR 0325; the grant was missing, so each connect logged a permissions violation. -func AskAccessActSubject(node string) string { return "mesh.node." + node + ".ask.access-act" } - // inbox is a principal's own reply space. No user is ever granted a bare `_INBOX.>` (design 25 // §4): with one account, inbox privacy is the permission list or it is nothing, so each user's // inbox is derived from its own identity and its permissions name that prefix and no other. @@ -484,25 +303,6 @@ func (p Principal) inbox() string { return "_INBOX." + p.Username() + ".>" } -// Permissions is what a principal may publish and subscribe, and whether it may answer. -type Permissions struct { - Publish []string - // PublishDeny are subjects refused although an allow covers them: the vault's holder's alone (DeskOnly), - // denied to everybody whose grant is wide enough to reach them. The server's deny outranks its allow. - PublishDeny []string - Subscribe []string - // AllowResponses lets a principal reply to a request it received, on the reply subject that - // request carried, once. - // - // **This is what makes scoped inboxes possible at all**, and design 25 §4 did not say it. If - // every user's inbox is private to it, a module serving a tool cannot publish the answer — - // the answer goes to the *caller's* inbox, which the responder has no permission for. The two - // ways out are granting responders `_INBOX.>`, which is precisely the blanket grant §4 - // refuses, or this: the server itself permits one reply to the subject of a message the user - // actually received, and nothing else. The authority is bounded by having been asked. - AllowResponses bool -} - // ResponseTTL is how long the bus lets a principal answer a request it received. Its one answer has // to come inside this, and a seat's holder answers within link.AnswerWithin — inside it by design. // **A broker reloading its user list forgets every answer it was about to permit**, whatever this @@ -510,933 +310,6 @@ type Permissions struct { // refused, which is why a holder answers before it does what can reload it. const ResponseTTL = time.Minute -// BusSnapshotGrants is the whole authority of the bus's own module (novox/hq ADR 0235): the -// snapshot API and what it needs, and nothing that changes a stream. -// -// **Read-only, and the writers table proves it**: none of these overlaps a subject a write of the -// mesh's state is a publish to — not a stream's definition, not a bucket, not a message. A snapshot is -// the server reading its own blocks out to the asker, while it goes on taking writes; it neither -// pauses nor reconfigures the stream. Named one by one rather than `$JS.API.>`, which would be the -// controller's authority over every stream: -// -// - the stream names, and one stream's information (whether it is on disk at all — a memory stream -// cannot be snapshotted and is said as skipped); -// - the snapshot request itself, for any stream: the archive comes to the asker's own inbox; -// - the acknowledgement the server waits for past its window: a publish to the subject the server -// put on each chunk, `$JS.SNAPSHOT.ACK....`, which only the server's -// own internal subscription hears. -// -// Restoring is not here: a restore creates a stream, which is the controller's to define, and the -// mesh restores into a new store beside the live one, swapped in by a person (to-be 43). -var BusSnapshotGrants = struct{ Publish []string }{Publish: []string{ - "$JS.API.STREAM.NAMES", - "$JS.API.STREAM.INFO.*", - "$JS.API.STREAM.SNAPSHOT.*", - "$JS.SNAPSHOT.ACK.>", -}} - -// PermissionsFor derives a principal's authority. Pure, and the only place authority is decided: -// a permission that cannot be derived from a declaration is a permission nobody can explain. -func PermissionsFor(p Principal) (Permissions, error) { - for _, part := range []struct{ what, value string }{ - {"node", p.Node}, {"module", p.Module}, {"process", p.Process}, - } { - if part.value == "" { - continue - } - if !safeSubject.MatchString(part.value) { - return Permissions{}, fmt.Errorf( - "%q cannot be part of a subject: a permission is a subject pattern, and this would widen it", part.value) - } - } - - if p.Kind == KindModule && p.SnapshotsTheBus { - pub := append([]string(nil), BusSnapshotGrants.Publish...) - sort.Strings(pub) - if err := CheckWriters(p, pub); err != nil { - return Permissions{}, err - } - // Its own inbox for the answers and the archive's chunks, and nothing else: nothing is asked - // of it, so it answers nothing. - return Permissions{Publish: pub, Subscribe: []string{p.inbox()}}, nil - } - - if p.Kind == KindProcess && len(p.Removes) > 0 { - pub, err := BusRemovalGrants(p.Removes, p.FilesFor) - if err != nil { - return Permissions{}, err - } - sort.Strings(pub) - if err := CheckWriters(p, pub); err != nil { - return Permissions{}, err - } - // Its own inbox for the answers, and nothing else (Limits 4). - return Permissions{Publish: pub, Subscribe: []string{p.inbox()}}, nil - } - - var pub, sub []string - switch p.Kind { - case KindProvisioner: - var err error - if pub, sub, err = provisionerGrants(p); err != nil { - return Permissions{}, err - } - - case KindProcess: - var err error - if pub, sub, err = processGrants(p); err != nil { - return Permissions{}, err - } - - case KindPerson: - // Tools, and nothing else. Every subject a person may publish is a tool call; a person - // who could publish an event would be able to claim a module said something. - invoked, err := invokedSubjects(p.Invokes) - if err != nil { - return Permissions{}, err - } - pub = append(pub, invoked...) - // And may ask what answers (novox/hq ADR 0197): a question every service answers about - // itself, its replies to the asker's own inbox. - pub = append(pub, discovering()...) - - case KindEnrolment: - // A leaked token is useless for anything but enrolling: it cannot read a declaration, hear - // an event, or subscribe any inbox but the one its own token derives (design 25 §6). - // - // **The inbox was missing and the handshake could not have completed without it.** An - // enrolling node publishes its request and waits on an address it states in the payload; - // with nothing to subscribe it waits out its timeout against a mesh that answered. Its own - // and no wider: `_INBOX.enrol..>`, so what is sealed to one machine cannot be read by - // another enrolling beside it. - if p.Node == "" { - // Refused rather than composed into `_INBOX.enrol..>`, which is a subject with an empty - // token in it — and worse, one every nameless enrolment user would share. A shared - // enrolment inbox is one machine able to read the credentials sealed to another. - return Permissions{}, errors.New( - "an enrolment user names no node, so its inbox would be shared with every other " + - "enrolment: a token is issued for a node record, and that record's name is " + - "the token's id") - } - // **A node enrols through the seat mesh-enrolment alone** (novox/hq ADR 0587 §4, §6, ADR 0590): its user calls - // redeem in its own name, while the seat's holder serves it. - if !p.Redeems { - return Permissions{}, fmt.Errorf("the enrolment user of %s may enrol nowhere: no holder of %s serves %s", - p.Node, EnrolmentRedeem.Seat, EnrolmentRedeem.Verb) - } - pub = []string{EnrolmentRedeemCall(p)} - sub = []string{p.inbox()} - - case KindNode: - // A host publishes its own node's control traffic and subscribes its own declaration — - // and nothing of any other node's. - // And binding to its consumer, which asks the server about it (CONSUMER.INFO) — the one - // thing the host does that nothing granted. Found the first time a machine dialled a - // permissioned server: "this node cannot read its declarations" (2026-09-28). The ack and - // the inbox are granted below with every principal's. - pub = []string{ - "mesh.control." + p.Node + ".>", - "$JS.API.CONSUMER.INFO.NODES." + p.Node, - } - // The deliver subject carries the stream as well as the consumer's name, so what a - // subscriber is permitted has to carry it too (novox/hq 04-ISSUES/146). The bare name - // stays: an existing consumer keeps delivering where it always did until the controller's - // next assertion moves it, and a permission that only allowed the new shape would refuse - // every node in the mesh for exactly as long as that took. - sub = []string{"mesh.node." + p.Node + ".declare", - "_DELIVER." + p.Node, "_DELIVER." + p.Node + ".>", - // And the mesh asking it to say again what it last applied (novox/hq to-be 45 §6, the - // `report` verb healer H1 asks): its own machine's, on core NATS and off any stream. It - // answers through its report, the one thing it already says — no reply to anybody's inbox. - AskReportSubject(p.Node), - // And the controller's terminal asking it to hand a directory used as found to the mesh (novox/hq - // issue 356), which it answers on the request's reply: the one request a node is asked. - AskHandOverSubject(p.Node), - // And asking it for a fresh search for setuid programs (novox/hq issue 361), answered the same way. - AskSetuidSearchSubject(p.Node), - // And asking it to judge its declared directories, read-only (novox/hq issue 358), answered the same way. - AskJudgeDirectoriesSubject(p.Node), - // And asking it to move a value sealed to it into the vault (design 57's V3, issue 676), answered the - // same way. - AskMoveSecretSubject(p.Node), - // And asking it to run an act on an access's folder (novox/hq ADR 0328 §7), answered the same way. - AskAccessActSubject(p.Node)} - // The node-engine witnesses the core builds it places (novox/hq to-be 45 §8, ADR 0236; the - // contract is lease/witness.go): it asks its own machine's node tools PING, and, where the - // machine runs the controller, reads the lease's one key — read, never written. - pub = append(pub, WitnessSubjects(p)...) - // And the keys of its own node in the have bucket, which it alone writes (novox/hq ADR 0429 §1, issue 1136): - // what the node has of each module, read by the view and the watchman. Never the controller's. - haveKeys, err := NodeKeysGrants(delivery.HaveBucket, p.Node) - if err != nil { - return Permissions{}, err - } - pub = append(pub, haveKeys...) - // And every node's own facts, `.node`, its record among them (issue 1126, ADR 0555 §3): a node agent composing - // its own modules reads the other nodes' public records, which its firewall and resolver need (ADR 0547 §2). Read - // only: each node still writes its own keys alone. - pub = append(pub, readGrants(delivery.HaveBucket, "*."+delivery.HaveNodeWord)...) - // And what it composes its own modules from (ADR 0547 §2, ADR 0567 §9): the seat mesh-placement's placement and - // candidates, the indexer's modules and manifests, the builder's builds (the manifest of a build its want names), and mesh-config's resolved values of - // its own node alone. Read only. - pub = append(pub, NodeComposeReads(p.Node)...) - // And, of the other nodes' resolved values, the keys their modules serve from (issue 1126), each one key. - served, err := ServedReadGrants(p.ServedReads) - if err != nil { - return Permissions{}, err - } - pub = append(pub, served...) - // And an object of the declaration bucket by its name, and nothing of the bucket's list or writing - // (novox/hq ADR 0486 §2, issue 1697). - fileReads, err := DeclarationFilesReads(p.Node) - if err != nil { - return Permissions{}, err - } - pub = append(pub, fileReads...) - // And its own compositions and wants, which it follows (novox/hq ADR 0539 clauses 1, 2 and 9), and nothing of another - // node's. - composedReads, err := ComposedReads(p.Node) - if err != nil { - return Permissions{}, err - } - pub = append(pub, composedReads...) - // And its own machine's backup holder, before a data module moves (ADR 0539 clause 16). - backups, err := NodeAgentAsks(p.Node) - if err != nil { - return Permissions{}, err - } - pub = append(pub, backups...) - // And the backup holder's word that it backed the module up (ADR 0539 clause 16), whichever module holds - // node-backup: one event, by its name. - sub = append(sub, NodeAgentHears...) - // And the creation of the seat mesh-placement's two states it composes from (ADR 0567 §9), so a node agent started - // before they exist binds each when the bus says it was made, with no timer; no other bucket's. - sub = append(sub, NodeComposeAdvisories()...) - // And, while the vault serves it, the vault's move-in in its own name and nobody else's (issue 676): the - // values sealed to this machine, which only it opens, handed in sealed to the vault's key for the call. - if p.MovesIntoTheVault { - pub = append(pub, "mesh.seat."+VaultMoveIn.Seat+"."+CallKind+"."+VaultMoveIn.Verb+"."+CallerToken(p.Username())) - } - // And, while the vault serves it, its node-key in its own name (issue 786): the machine's sealing key, stated - // by the engine that holds it, which the vault takes from no other caller. - if p.StatesItsKeyToTheVault { - pub = append(pub, "mesh.seat."+VaultNodeKey.Seat+"."+CallKind+"."+VaultNodeKey.Verb+"."+CallerToken(p.Username())) - } - // And, while the vault serves it, its modules' own secrets in its own name (issue 1126): sealed to its key, for the - // compositions it makes itself. - if p.AsksItsOwnSecrets { - pub = append(pub, VaultOwnCall(p)) - // And the vault's word that a secret changed (names and generations, never a value), so the node composes again - // on it rather than at the next unrelated change (ADR 0547 §2). - sub = append(sub, VaultSecretChanged) - } - // And, while the vault serves `sign`, its own node's certificate in its own name, and the vault's public signing - // halves, read and watched (ADR 0550 §2). Read only. - if p.AsksTheVaultToSign { - pub = append(pub, VaultSignCall(p)) - pub = append(pub, readGrants(delivery.SignersBucket, "")...) - } - // And, while the vault rotates node accounts, its own node's key of node-logins, read and watched, and no other - // key (novox/hq ADR 0583 §1 insight): the login it dials as; and the confirmation of a switch in its own name (§3). - if p.RotatesItsLogin { - pub = append(pub, NodeLoginsReads(p.Node)...) - pub = append(pub, "mesh.seat."+VaultNodeAccountConfirm.Seat+"."+CallKind+"."+VaultNodeAccountConfirm.Verb+"."+ - CallerToken(p.Username())) - } - // And its node seat, node-engine, and its asks to the operator in its own name (novox/hq ADR 0563 §1, §3). - ep, es := engineGrants(p) - pub = append(pub, ep...) - sub = append(sub, es...) - // And, on the node holding mesh-broker, the grants part's sealed user list it follows from take-over (novox/hq ADR - // 0547 clause 7): one key, and an object by its name. - if p.FollowsGrantsUserList { - pub = append(pub, GrantsUserListFollows()...) - } - - case KindModule: - // 0. The watchman alone hears who connects and disconnects, and asks who is connected (ADR 0431 §2). - if p.Module == WatchmanModule { - sub = append(sub, BusConnectSubject, BusDisconnectSubject) - pub = append(pub, BusConnzSubject) - // And the dead letters, from its own consumer, never the controller's (novox/hq ADR 0564 clause 5). - pub = append(pub, WatchmanDeadLetterGrants(p.DeadLetterSources)...) - // And the words its watchdogs hear (ADR 0564 clause 4, Limits 3, 11). - sub = append(sub, WatchmanHears()...) - } - // The grants part alone publishes the modules' memberships into the assignments stream (novox/hq ADR 0547 §5; - // the operator, 2026-10-25 22:31): one per subject, `mesh.assignment..`, reading the last one of each - // first so it publishes only what changed, and no stream it defines. - if p.Module == GrantsModule { - pub = append(pub, "mesh.assignment.*.*", "$JS.API.DIRECT.GET."+AssignmentsStream+".mesh.assignment.*.*") - // And creating each stream a manifest declares, by its own name (the operator, 2026-10-26 08:54; issue - // 1790): the create and the read of that one stream, never an update, a delete or a stream not declared. - pub = append(pub, CreateStreamGrants(p.CreatesStreams)...) - // And creating each durable consumer an account needs, by its stream and name (ADR 0561 §6). - pub = append(pub, CreateConsumerGrants(p.CreatesConsumers)...) - // And its own object bucket, where it writes the user list sealed to the bus node (ADR 0547 clause 7). - pub = append(pub, GrantsUserListWrites()...) - } - // The view alone reads the build seats' workers for the backlog of asks (issue 1887 A8a). - if p.Module == ViewModule { - pub = append(pub, BuildBacklogReads()...) - } - // The builder alone reads the build seats' streams by sequence, to put again at its start an ask a key names that no - // stream holds (issue 1910). - if p.Module == BuilderModule { - pub = append(pub, BuildAskReads()...) - } - // 1. Its own namespace: it publishes its events there and serves its tools there. Nothing - // else may publish into it, so an event's source is a fact the server enforces rather - // than a claim in the body (design 29 §2). - own := "mesh.mod." + p.Module - for _, e := range p.Emits { - pub = append(pub, own+".event."+e) - } - // And its own streams' events, on their own subjects (novox/hq ADR 0363 §2). - pub = append(pub, ownStreamGrants(p.Module, p.Streams)...) - // Every tool under its own name, not a list: the tools a module serves are what its code - // answers, and a second copy of that list in the manifest would be a second source of - // truth for the mesh to keep in step (2026-09-28: every module that served a tool was - // refused the subscription, because none had written the list twice). Nothing is given - // away — no other principal may subscribe this namespace, and a caller's authority is - // still granted per tool, by name, on the publish side. - sub = append(sub, own+".tool.>") - // It says what it serves (novox/hq ADR 0197): discovery for its own name and every seat it - // holds a verb of, answered by the runtime that serves them. - announced := []string{p.Module} - for _, s := range p.Holds { - if len(s.Serves) > 0 { - announced = append(announced, s.Name) - } - } - sub = append(sub, announcing(announced...)...) - // Its own membership (ADR 0160), from the grants part's state memberships, its own key (novox/hq ADR 0571 clause - // 7): its one read of it, since the assignments stream's direct get went with the stream (research 072 row 33), - // and no longer heard on its subject, where the grants part stops publishing at the lease write (ADR 0571 clause - // 10, issue 774). - pub = append(pub, readGrants(GrantsMembershipsBucket, MembershipKey(p.Node, p.Module))...) - - // 1b. The tools it calls, if its manifest says it calls any (novox/hq ADR 0152). The same - // grant a person gets and derived the same way, so "what may this module ask" is - // answered by the one list that answers it for everybody. Publish only: an answer - // arrives on its own inbox, which every principal has below. - invoked, err := invokedSubjects(p.Invokes) - if err != nil { - return Permissions{}, err - } - pub = append(pub, invoked...) - - // 2. What it consumes, by the emitter's own subject — an event is addressed to its - // emitter, because the emitter's identity is the meaning (ADR 0118). - for _, c := range p.Consumes { - subject, err := consumedSubject(c) - if err != nil { - return Permissions{}, err - } - sub = append(sub, subject) - } - - // 2a. What it hears of a module's own stream, as it was granted (novox/hq ADR 0363 §5): the subjects, - // and its own consumer on that stream, asked about, pulled from and acknowledged. - for _, h := range p.Hears { - if len(h.Filters) == 0 { - continue - } - sub = append(sub, h.Filters...) - } - pub = append(pub, heardGrants(p, p.Hears)...) - - // 2b. Events of a role it watches, under the seat's own namespace. Subscribe only: watching a - // role is hearing what it announced, not taking part in it. - for _, w := range p.Watches { - if w.Kinded { - continue // composed by SeatTrafficOf below - } - for _, e := range w.Emits { - sub = append(sub, seatSubject(w, "event", e)) - } - } - - // 2c. Its own consumer, which it **pulls**: the runtime asks for the next message and is - // answered on its own inbox, so what it needs is to ask about the consumer and to ask it - // for messages — its own consumer's name, and no other's. Pulled rather than pushed - // because that is the one shape a runtime's client binds without creating anything; the - // controller and the hosts are pushed to. Named here rather than through ConsumerFor, - // which asks for these permissions to build the consumer and would ask forever. A - // subject for a consumer that turns out not to exist grants nothing anybody can use. - pub = append(pub, - "$JS.API.CONSUMER.INFO."+consumerStream(p)+"."+consumerDurable(p), - "$JS.API.CONSUMER.MSG.NEXT."+consumerStream(p)+"."+consumerDurable(p)) - - // 3. Seats it holds: full participation. - for _, s := range p.Holds { - if s.isNewTraffic() { - // Composed by SeatTrafficOf below, worker and all; only its tools are served here. - for _, t := range s.Serves { - sub = append(sub, seatToolSubject(s, t, p.Node)) - } - continue - } - // Taking work from the role's queue: the worker consumer every holder shares (asked - // about, pulled from, acknowledged), on the seat's own stream (novox/hq ADR 0190). A - // holder pulls — asks the consumer for its next message, answered on its own inbox — - // so what it needs is MSG.NEXT on that worker and nothing delivered to it. The first - // machine to take work over the new bus was refused the asking (2026-09-28). - worker := "SEAT_" + upperSnake(s.Name) + "_worker" - stream := seatStreamName(s.Name) - pub = append(pub, - "$JS.API.CONSUMER.INFO."+stream+"."+worker, - "$JS.API.CONSUMER.MSG.NEXT."+stream+"."+worker, - "$JS.ACK."+stream+"."+worker+".>") - for _, a := range s.Accepts { - sub = append(sub, seatSubject(s, "accept", a)) - } - for _, e := range s.Emits { - // **A machine says its own state and no other's** (novox/hq ADR 0219): on a node-scoped - // seat, an event about the holder itself carries the machine as its last token, and - // each holder is granted its own machine's alone. - if s.Scope == "node" && p.Node != "" && perMachineEvents[e] { - pub = append(pub, seatSubject(s, "event", strings.TrimSuffix(e, "*")+p.Node)) - continue - } - pub = append(pub, seatSubject(s, "event", e)) - } - for _, t := range s.Serves { - sub = append(sub, seatToolSubject(s, t, p.Node)) - } - } - - // 4. Seats it uses: publish only, and only the accepts half. A caller cannot subscribe a - // seat's inbound subject and watch other modules' traffic, nor publish its outbound - // events and lie about outcomes (design 29 §2). - for _, s := range p.Uses { - for _, a := range plainVerbs(s, s.Accepts) { - pub = append(pub, seatSubject(s, "accept", a)) - } - for _, t := range s.Serves { - pub = append(pub, seatToolSubject(s, t, "*")) - } - } - - // 5. Its state, and the state of others it reads (novox/hq ADR 0201): every one read and - // watched, its own written too. - kept := stateAccess{Module: p.Module, Node: p.Node, Keeps: p.State, PerMachine: p.PerMachine, Reads: p.Reads, - KeyedReads: p.KeyedReads} - pub = append(pub, stateGrants(kept)...) - // And it hears each of those buckets created, so one not on the bus at its start is bound when it appears - // (novox/hq issue 1790; mesh-sdk process.AwaitBucket). - sub = append(sub, stateCreatedHeard(kept)...) - // And its logs (novox/hq ADR 0297): appended to and read directly, its own alone. - pub = append(pub, logGrants(p.Module, p.Logs)...) - pub = append(pub, readLogGrants(p.ReadLogs)...) - - // 6. Its traffic on seats that name their caller or their kind, ask proofs or keep records - // (novox/hq ADR 0259 §3). - tp, ts := seatTrafficGrants(SeatTrafficOf(p.Module, p.Holds, p.Uses, p.Watches)) - pub = append(pub, tp...) - sub = append(sub, ts...) - - case KindNodeTools: - // **One process serves what every module on the machine would have served for itself** - // (novox/hq ADR 0175). Each carried module's whole tool namespace — the same grant that - // module's own principal has, for the same reason: the tools a module serves are what its - // code answers, and a list here would be a second copy of it. Each held seat's verbs on - // this node, as the holder's own principal would be granted them. - var serves []string - for _, d := range p.Carries { - if !safeSubject.MatchString(d.Module) { - return Permissions{}, fmt.Errorf( - "%q cannot be part of a subject: a permission is a subject pattern, and this would widen it", d.Module) - } - serves = append(serves, d.Module) - for _, s := range d.Holds { - serves = append(serves, s.Name) - } - own := "mesh.mod." + d.Module - sub = append(sub, own+".tool.>") - // A tool that emits an event is the module's code and emits under the module's name - // (ADR 0042); the runtime carrying that code may publish what the module declared it - // emits, and nothing it did not. - for _, e := range d.Emits { - pub = append(pub, own+".event."+e) - } - // And its own streams' events (novox/hq ADR 0363 §2). - pub = append(pub, ownStreamGrants(d.Module, d.Streams)...) - for _, s := range d.Holds { - for _, t := range s.Serves { - sub = append(sub, seatToolSubject(s, t, p.Node)) - } - } - } - // Every assigned module's membership on this node (ADR 0160): one per module, read and watched from the - // grants part's state memberships (ADR 0571 clause 7), and no longer heard on `mesh.assignment`, where the grants - // part stops publishing at the lease write (ADR 0571 clause 10, issue 774). This node's and no other's — the - // one token that varies is the module, so the pattern is the machine's own assignments. - pub = append(pub, readGrants(GrantsMembershipsBucket, p.Node+".*")...) - // And that it is there (novox/hq to-be 45 §3, S11): its own heartbeat, under its machine's - // name and no other's, on core NATS like the host's. - pub = append(pub, "mesh.control."+p.Node+".tools-alive") - // And every tool on the mesh (ADR 0175, decision 5): any node may call any tool on any - // node, as the console already could — the runtime is the console's serving mode. - invoked, err := invokedSubjects([]string{"*"}) - if err != nil { - return Permissions{}, err - } - pub = append(pub, invoked...) - // And its own machine's provisioner client, for a carried module's config and non-secret bound values, - // naming the module from the bundle that asks (novox/hq ADR 0370 §3, §4): the ask on its own machine's - // subject alone, and what the client says moved, so a module's code's watches are asked again. - pub = append(pub, ProvisionerAskSubject(p.Node)) - sub = append(sub, "prov.changed."+p.Node+".>") - // It says what it serves and may ask what answers (novox/hq ADR 0197): the runtime answers - // discovery for each module and seat it carries, and the console it is asks the bus. - // One service per runtime process, named for the runtime: the bus lets a principal answer each - // request once, so the runtime announces everything it carries under its own name. - sub = append(sub, announcing(append([]string{RuntimeModule}, serves...)...)...) - pub = append(pub, discovering()...) - // **And it consumes for the modules it carries** (novox/hq ADR 0198, which changes ADR 0175's - // "it consumes nothing"): a module's long-running code is a bundle this runtime launches, and - // the runtime is its bus — it reads the module's own durable consumer and acknowledges what - // the module's code took. Exactly the grants the module's own principal has for that consumer, - // on its name and no other's: asking about it, pulling from it, acknowledging it. The - // consumer is still the controller's to make, from the module's own principal. - for _, d := range p.Carries { - own := Principal{Kind: KindModule, Node: p.Node, Module: d.Module, Emits: d.Emits, - Consumes: d.Consumes, Serves: d.Serves, Holds: d.Holds, Uses: d.Uses, Watches: d.Watches, - Streams: d.Streams, Hears: ComposedHears(d)} - // And the module streams it hears, each through its own consumer there (novox/hq ADR 0363 §8) — never - // a restricted one, which only a module on an account of its own hears (ADR 0376 §1). - pub = append(pub, heardGrants(own, own.Hears)...) - if _, consumes := ConsumerFor(own); !consumes { - continue - } - stream, durable := consumerStream(own), consumerDurable(own) - pub = append(pub, - "$JS.API.CONSUMER.INFO."+stream+"."+durable, - "$JS.API.CONSUMER.MSG.NEXT."+stream+"."+durable, - "$JS.ACK."+stream+"."+durable+".>") - } - // **And it keeps and reads state for the modules it carries** (novox/hq ADR 0201): the union - // of what each may do with a bucket — an owner's write, a reader's read. That one module's code - // does not write another's bucket through it is the runtime's to keep, from the membership - // each assignment is issued, as it keeps each module's events under that module's own name. - for _, d := range p.Carries { - pub = append(pub, stateGrants(stateAccess{Module: d.Module, Node: p.Node, Keeps: stateNames(d.State), - PerMachine: perMachineNames(d.State), Reads: d.Reads, KeyedReads: d.KeyedReads})...) - } - // **And it keeps the logs of the modules it carries** (novox/hq ADR 0297): each module's own, the - // union over them. That one module's code does not append to another's log through it is the - // runtime's to keep, from the logs each membership lists. - for _, d := range p.Carries { - pub = append(pub, logGrants(d.Module, logNames(d.Logs))...) - pub = append(pub, readLogGrants(d.ReadLogs)...) - } - // **And the event families a session may wait on** (novox/hq ADR 0367 §2): the tool runner's module's own, - // named one by one in its manifest, as a plain subscription — no consumer is made for them, and no - // call or setting adds one. - waits, err := waitGrants(p.Carries) - if err != nil { - return Permissions{}, err - } - sub = append(sub, waits...) - // **Never the traffic of a trusted holder** (novox/hq ADR 0259 §8): the machine's runtime runs as the - // operator's account, which every agent runs as, so a module saying warrants or speaking for a kind - // that proves its sender is never composed into it — refused here, naming it, whatever registration - // let through. - for _, d := range p.Carries { - if why := trustedTraffic(d); why != "" { - return Permissions{}, fmt.Errorf("%s on %s is carried by the machine's runtime, and %s: it runs "+ - "as an account of its own, never the runtime's (novox/hq ADR 0259)", d.Module, p.Node, why) - } - } - // **And the seat traffic of the modules it carries** (novox/hq ADR 0259 §3): a bundle reaches the - // bus only through its runtime, so the runtime is granted the union. That one module's code does - // not publish under another's name or kind through it is the runtime's to keep, from the seat - // traffic each module's membership lists. - for _, d := range p.Carries { - tp, ts := seatTrafficGrants(SeatTrafficOf(d.Module, d.Holds, d.Uses, d.Watches)) - pub = append(pub, tp...) - sub = append(sub, ts...) - } - sub = unique(sub) - pub = unique(pub) - } - - if p.Kind == KindPerson { - // An inbox to hear answers in, and nothing else. No ack subject: a person has no durable - // consumer, because nothing is delivered to a person — they ask and are answered. - sub = append(sub, p.inbox()) - } - if p.Kind == KindNodeTools { - // Its reply space, so the answers to what its tools call come back to it. No ack subject - // for the same reason a person has none: nothing is delivered to it. - sub = append(sub, p.inbox()) - } - if p.Kind == KindProcess { - // Its own reply space, and nothing wider (novox/hq ADR 0436 §1, §3): a process's queue consumer is - // pulled and acknowledged by publishing, granted above with its own stream and consumer name, never - // a generic EVENTS consumer's — consumerStream/consumerDurable below are a module's own, not a - // process's. - sub = append(sub, p.inbox()) - } - - if p.Kind == KindModule || p.Kind == KindNode { - // Its own reply space, and nothing wider. - sub = append(sub, p.inbox()) - - // Acking a JetStream delivery is a publish to that consumer's own ack address — a - // different subject from anything the consumer subscribes. Without it every message a - // module received would be redelivered forever, refused by the permission list it already - // has (design 25 §4). Scoped to this principal's own consumer name, so it can ack its own - // deliveries and no other's. - pub = append(pub, "$JS.ACK."+consumerStream(p)+"."+consumerDurable(p)+".>") - } - - // **And every tool grant again, naming its caller** (novox/hq issue 365): a call in this principal's own - // name, and an answer to a call naming anybody. The `tool` grants above stay beside these, so callers and runtimes - // move without a gap; their retirement is hq issue 464. - // The vault's calls in its own name (issue 723), kept apart so the desk's grant is not denied below. - var vaultsOwn []string - pub, sub = callerNamed(p.Username(), pub, sub) - if p.Kind == KindModule { - vaultsOwn = vaultCalls(p) - pub = append(pub, vaultsOwn...) - } - - sort.Strings(pub) - sort.Strings(sub) - // One writer per piece of state (novox/hq to-be 45 §1): a grant that would make a second is - // refused here, at composition, naming the state and its writer. - if err := CheckWriters(p, pub); err != nil { - return Permissions{}, err - } - // What the vault's holder alone may publish is denied to everybody else whose grant reaches it. - var deny []string - for _, only := range DeskOnly() { - for _, a := range pub { - if slices.Contains(vaultsOwn, a) { - continue - } - if SubjectsOverlap(a, only) { - deny = append(deny, only) - break - } - } - } - // 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() { - if slices.ContainsFunc(pub, func(a string) bool { return SubjectsOverlap(a, only) }) { - deny = append(deny, only) - } - } - } - return Permissions{ - Publish: pub, - PublishDeny: deny, - Subscribe: sub, - // A module answers what it was asked — a tool call reaches it on its own namespace, so the - // authority is bounded by having been asked. A node is asked one - // thing, a hand-over on its own subject (novox/hq issue 356), and answers that: the node is its - // machine's engine, root there already, and it is delivered only its own subjects. What it answers is - // never trusted for being an answer. A - // provisioner client is asked by its own machine's tool runner alone, on its own machine's subject (novox/hq - // ADR 0370 §3), and answers that. A person is never asked anything, and is granted nothing here. - AllowResponses: p.Kind == KindModule || p.Kind == KindNodeTools || p.Kind == KindNode || - p.Kind == KindProvisioner, - }, nil -} - -// vaultCalls are the calls the vault's holder makes in its own name (novox/hq ADR 0349 clauses 9 and 10, issue -// 723): the desks' secret prompt on any machine, and each tool that answers a credential its provider's software -// issued, on any machine — each on the subject whose last token is the vault's own name, which the bus lets it -// alone publish, so the prompt and the tool know the vault asks. Nothing else outside its seat. -func vaultCalls(p Principal) []string { - token := CallerToken(p.Username()) - if token == "" { - return nil - } - var out []string - if p.AsksTheDesk { - for _, v := range VerbsTheVaultAsksForASecret { - out = append(out, "mesh.seat."+v.Seat+"."+CallKind+"."+v.Verb+".*."+token) - } - } - for _, key := range p.AsksIssued { - module, tool, ok := strings.Cut(key, ".") - if !ok || module == "" || tool == "" || strings.ContainsAny(key, "*> ") { - continue - } - out = append(out, "mesh.mod."+module+"."+CallKind+"."+tool+".*."+token) - } - return out -} - -// seatSubject places a seat's verb under the kind of traffic it is. -// -// **The kind token is load-bearing, not decoration.** A stream is defined by a subject filter, so -// without it a stream over a seat or a module's namespace would capture that namespace's *tool* -// traffic too — and a tool call must never be persisted (design 25 §3: tools stay on core NATS). -// Found while defining the streams: the first draft of design 29 had one namespace per module -// with no kind, which reads well and cannot be filtered. -// -// A seat serving more than its current protocol version carries the version as a token -// (design 29 §8): the seat stays one role, and v1 and v2 run beside each other until nothing is -// bound to the old one. -func seatSubject(s Seat, kind, verb string) string { - return "mesh.seat." + s.Name + "." + kind + "." + verb -} - -// seatToolSubject is where a role's tool is asked. Mesh-wide for a mesh-scoped seat; a node-scoped -// seat carries the node it is asked of, because a flat subject would reach every machine's holder -// and the queue group would silently pick a winner (novox/hq ADR 0132, design 33 §4). A holder -// subscribes its own node's; a user publishes any node's (`*`) and names the machine in the subject. -func seatToolSubject(s Seat, verb, node string) string { - base := seatSubject(s, "tool", verb) - if s.Scope == "node" && node != "" { - return base + "." + node - } - return base -} - -// consumerStream and consumerDurable are the two halves of a consumer's identity, and they are -// two functions because conflating them was a real bug. -// -// **A durable name may not contain a dot; an ack subject is built from two names that do.** The -// server acknowledges on `$JS.ACK...…`, so a single string "EVENTS.one_audit" -// reads correctly inside the permission and is rejected as a consumer name — *nats: invalid -// consumer name*. Caught against a running server, and worth the comment because the shape of -// the failure if it had not been is the one design 25 §4 warns about: a consumer that cannot ack -// has every message redelivered forever, and its permission list looks right while it happens. -// -// They are derived here, beside the permission that must match them, because two places deriving -// the same name is how a module ends up unable to ack its own deliveries. -// consumedSubject is where a consumed event lands, from the local pattern a module declared. -// -// **The mesh's wildcards become this transport's** (design 29 §1): `*` is one name on both, and `**` -// — the rest — is `>` here. A module writes neither transport's spelling, so a manifest stays correct -// when the wire changes, which is the whole reason names are local. -// -// `**` on its own is every event from every module: the emitter is any, the event is anything. An -// audit logger wants exactly that and says so in one token. -func consumedSubject(pattern string) (string, error) { - if pattern == catalogueTheRest { - return "mesh.mod.*.event.>", nil - } - emitter, event, named := strings.Cut(pattern, ".") - if !named || emitter == "" || event == "" { - return "", fmt.Errorf( - "%q does not name an emitter and an event: a consumed event is ., or "+ - "%q for every event", pattern, catalogueTheRest) - } - if emitter == catalogueTheRest { - return "", fmt.Errorf("%q stands for the rest of a name, so it cannot name the emitter", catalogueTheRest) - } - // Each name is checked before it becomes a subject: a name carrying a dot would add a token and - // silently widen the permission, which is the whole reason safeSubject exists. - var out []string - for _, part := range strings.Split(event, ".") { - switch part { - case catalogueTheRest: - out = append(out, ">") - case "*": - out = append(out, "*") - default: - if !safeSubject.MatchString(part) { - return "", fmt.Errorf("%q cannot be part of a subject: it would widen the permission", part) - } - out = append(out, part) - } - } - if emitter != "*" && !safeSubject.MatchString(emitter) { - return "", fmt.Errorf("%q cannot name an emitter: it would widen the permission", emitter) - } - return "mesh.mod." + emitter + ".event." + strings.Join(out, "."), nil -} - -// catalogueTheRest is the mesh's wildcard for "the rest of a name", duplicated from the catalogue -// package for the one direction of dependency the build queue's name is duplicated for. -const catalogueTheRest = "**" - -func consumerStream(p Principal) string { - switch p.Kind { - case KindModule: - return "EVENTS" - case KindNode: - return "NODES" - } - return "" -} - -func consumerDurable(p Principal) string { - switch p.Kind { - case KindModule: - return p.Node + "_" + p.Module - case KindNode: - return p.Node - } - return "" -} - -// Server is everything the composed file needs that is not a principal. -type Server struct { - // ClientPort carries TLS itself. There is no plaintext port beside it: a bus reachable - // without TLS is one a module can reach without TLS by mistake. - ClientPort int - MonitoringPort int - TLSCert string - TLSKey string - TLSCA string - // StoreDir is a host directory bind, not a named volume — issue 115 is resolved and converted - // four modules away from named volumes; the bus's own data is not the place to bring one back. - StoreDir string -} - -// Compose renders the server's whole configuration. The order is stable and the output is -// deterministic, because the file's digest is what the module's entrypoint watches to decide -// whether to reload: a composer that reordered a map on each run would signal a reload every time -// the controller restarted, for a file that had not changed. -func Compose(s Server, principals []Principal) (string, error) { - sorted := append([]Principal(nil), principals...) - sort.Slice(sorted, func(i, j int) bool { return sorted[i].Username() < sorted[j].Username() }) - - var b strings.Builder - b.WriteString("# Composed by the mesh. Do not edit: the next composition overwrites it.\n") - b.WriteString("# Accounts and permissions are derived from what each module declares and nothing\n") - b.WriteString("# else (novox/hq ADR 0043, design 29 §2).\n\n") - - fmt.Fprintf(&b, "port: %d\n", s.ClientPort) - fmt.Fprintf(&b, "http: 127.0.0.1:%d\n\n", s.MonitoringPort) - - // **No `verify`, and it said `verify: true` until this configuration was run.** That setting - // makes the server demand a *client* certificate, and nothing in the mesh presents one: a host - // pins this server's exact certificate and authenticates with the password the mesh minted - // (ADR 0004, design 25 §4), and so does a module's runtime. With it on, every connection in the - // mesh is refused at the TLS handshake before any password is looked at, and the error — - // "client didn't provide a certificate" — reads as a fault in the client. - // - // TLS is still required: the block is what requires it, and verify only decides whether client - // certificates are checked. - b.WriteString("tls {\n") - fmt.Fprintf(&b, " cert_file: %q\n", s.TLSCert) - fmt.Fprintf(&b, " key_file: %q\n", s.TLSKey) - fmt.Fprintf(&b, " ca_file: %q\n", s.TLSCA) - b.WriteString("}\n\n") - - b.WriteString("jetstream {\n") - fmt.Fprintf(&b, " store_dir: %q\n", s.StoreDir) - b.WriteString("}\n\n") - - accounts, err := ComposeAccounts(sorted) - if err != nil { - return "", err - } - b.WriteString(accounts) - return b.String(), nil -} - -// ComposeAccounts is the accounts block alone — every user, and nothing about the server. -// -// **This is the only part of the configuration the mesh writes, and the split is deliberate.** A -// server's ports, its TLS paths and its store directory are properties of the container the module -// raises: they live in its image and its mounts, and they change when it does. The controller has no -// business knowing them, and a controller that did would have to be kept in step with a Dockerfile -// it never sees. What only the mesh knows is *who may connect*, so that is what it writes, and the -// module's own configuration includes it. -// -// Four things checked against a running server before this shape was committed to: a user in an -// included file authenticates; an unknown user is refused, so the include is the whole authority -// rather than an addition to something; a publish outside a user's grant is refused; and rewriting -// this file alone and signalling a reload makes a new user appear **without dropping the connection -// the mesh already has** — which is what makes every later account, permission or person change cost -// nothing (task 1.2's payoff). -func ComposeAccounts(principals []Principal) (string, error) { - sorted := append([]Principal(nil), principals...) - sort.Slice(sorted, func(i, j int) bool { return sorted[i].Username() < sorted[j].Username() }) - - var b strings.Builder - b.WriteString("# The mesh's users, composed by the grants part. Do not edit: the next\n") - b.WriteString("# composition overwrites it. Permissions are derived from what each module\n") - b.WriteString("# declares and nothing else (novox/hq ADR 0043, design 29 §2).\n\n") - - // One account for the mesh: accounts in NATS isolate subject spaces entirely, and the mesh is - // one space (design 25 §4). The cost of that — that permissions are the only isolation — is - // paid in the scoping of every inbox and every ack subject. - // JetStream is enabled per account once accounts exist at all: with only the global block set, - // a user in MESH is told "JetStream not enabled for account" the first time it binds a - // consumer, which is the first thing every host does (2026-09-28). - b.WriteString("accounts {\n MESH {\n jetstream: enabled\n") - b.WriteString(meshSystemImports) - b.WriteString(" users = [\n") - for _, p := range sorted { - perms, err := PermissionsFor(p) - if err != nil { - return "", err - } - if p.PasswordHash == "" { - return "", fmt.Errorf("%s has no password hash: a user without one is a user anybody is", p.Username()) - } - b.WriteString(AccountsEntry(p.Username(), p.PasswordHash, perms)) - } - b.WriteString(" ]\n }\n") - b.WriteString(systemAccount) - b.WriteString("}\n") - return b.String(), nil -} - -// AccountsEntry is one user of the accounts block, as ComposeAccounts writes each: its name, its password hash and its -// permissions, in the lines the bus reads. Exported so a writer that keeps a user from grants it holds rather than -// composes (mesh-grants' refused users) writes the same lines, never a copy of them (issue 774). -func AccountsEntry(user, passwordHash string, perms Permissions) string { - var b strings.Builder - fmt.Fprintf(&b, " { user: %q, password: %q, permissions: {\n", user, passwordHash) - if len(perms.PublishDeny) > 0 { - fmt.Fprintf(&b, " publish: { allow: [%s], deny: [%s] }\n", quoted(perms.Publish), quoted(perms.PublishDeny)) - } else { - fmt.Fprintf(&b, " publish: { allow: [%s] }\n", quoted(perms.Publish)) - } - fmt.Fprintf(&b, " subscribe: { allow: [%s] }\n", quoted(perms.Subscribe)) - if perms.AllowResponses { - fmt.Fprintf(&b, " allow_responses: { max: 1, ttl: \"%dm\" }\n", int(ResponseTTL/time.Minute)) - } - b.WriteString(" } }\n") - return b.String() -} - -// WatchmanModule is the one module whose principal hears the bus's connect and disconnect events and asks its list of -// connections (novox/hq ADR 0431 §2, issue 1147): the watchman of the repository mesh-delivery, which writes presence -// from them. No other principal is granted the subjects below, and nothing is a wildcard over them. -const WatchmanModule = "mesh-delivery-watchman" - -// CreateStreamGrants is what creating each named stream needs, and nothing wider (novox/hq ADR 0547 §5; the operator, -// 2026-10-26 08:54): its create, and its information to read back what it holds. Each by its own name: a name that -// is not one plain subject token is left out, since a wildcard there would be every stream. -func CreateStreamGrants(streams []string) []string { - var out []string - for _, s := range streams { - if !safeSubject.MatchString(s) { - continue - } - out = append(out, "$JS.API.STREAM.CREATE."+s, "$JS.API.STREAM.INFO."+s) - } - return out -} - -// CreateConsumerGrants are the subjects that create each consumer by its stream and name, and read its info back: the -// create of the client library's CreateConsumer for a consumer with no filter or several, never one filter's form (a -// wildcard in a filter would widen the grant), an update or a delete. -func CreateConsumerGrants(consumers []ModuleConsumer) []string { - var out []string - for _, mc := range consumers { - c := mc.Consumer - if !safeSubject.MatchString(c.Stream) || !safeSubject.MatchString(c.Name) || len(c.Filters) == 1 { - continue - } - out = append(out, "$JS.API.CONSUMER.CREATE."+c.Stream+"."+c.Name, "$JS.API.CONSUMER.INFO."+c.Stream+"."+c.Name) - } - return out -} - // GrantsModule is the one module whose principal publishes the modules' memberships (novox/hq ADR 0547 §5): the grants // part, which composes them from the manifests and placement. It defines no stream and is granted no other subject here. const GrantsModule = "mesh-grants" @@ -1450,167 +323,7 @@ const GrantsModule = "mesh-grants" const ( GrantsMembershipsState = "memberships" GrantsMembershipsBucket = GrantsModule + "_" + GrantsMembershipsState - GrantsMembershipsRead = GrantsModule + "." + GrantsMembershipsState ) // MembershipKey is an assignment's key in the grants part's memberships: `.`. func MembershipKey(node, module string) string { return node + "." + module } - -// The bus's own events, imported into MESH from the default system account `$SYS` under subjects of MESH's own -// (novox/hq ADR 0431 §1, issue 1147): the server says an account's connections opening and closing only to the system -// account, and the list of an account's connections only to a request made there. `$SYS` holds no user, so nobody -// gains its other requests and events; it exports exactly these three, to MESH alone. -const ( - BusConnectSubject = "mesh.bus.connect" - BusDisconnectSubject = "mesh.bus.disconnect" - BusConnzSubject = "mesh.bus.connz" -) - -// meshSystemImports is what MESH imports from $SYS: the two event streams and the one service. -const meshSystemImports = " imports = [\n" + - " { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.CONNECT\" }, to: \"" + BusConnectSubject + "\" }\n" + - " { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.DISCONNECT\" }, to: \"" + BusDisconnectSubject + "\" }\n" + - " { service: { account: \"$SYS\", subject: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\" }, to: \"" + BusConnzSubject + "\" }\n" + - " ]\n" - -// systemAccount is the account `$SYS`, the server's default system account by its own name (a config that declares an -// account of that name makes it the system account), with no user and the three exports to MESH. Quoted, because an -// unquoted `$` begins a variable in the server's config grammar. -const systemAccount = " \"$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" - -// unique is a sorted list with each subject once. Two carried modules holding seats with the same -// verb, or the runtime module itself carried beside the others, would otherwise write a grant twice -// — harmless to the server, and noise in a file that is read as the mesh's authority model. -func unique(values []string) []string { - sort.Strings(values) - out := values[:0] - for i, v := range values { - if i == 0 || v != values[i-1] { - out = append(out, v) - } - } - return out -} - -func quoted(values []string) string { - if len(values) == 0 { - return "" - } - out := make([]string, len(values)) - for i, v := range values { - out[i] = fmt.Sprintf("%q", v) - } - return strings.Join(out, ", ") -} - -// invokedSubjects is the publish side of a grant to call tools: one subject per `.`, -// or the whole tool namespace for `*`. A person's authority and a module's `invokes` are both this -// (novox/hq ADR 0152), so a malformed entry is refused in one place, before it could be widened into -// something that happens to parse. -func invokedSubjects(invokes []string) ([]string, error) { - var out []string - for _, t := range invokes { - if t == "*" { - // Every module's tools and every role's (novox/hq ADR 0132): a role's verb is a tool - // like any other, addressed to the seat instead of a module. - out = append(out, "mesh.mod.*.tool.>", "mesh.seat.*.tool.>") - continue - } - if rest, isSeat := strings.CutPrefix(t, "seat:"); isSeat { - // A role's tool, `seat:.`. Both address shapes, because the grant is - // written without knowing the seat's scope: a mesh seat's verb is flat and a node - // seat's carries the machine (design 33 §4). - seat, verb, ok := strings.Cut(rest, ".") - if !ok || seat == "" || verb == "" { - return nil, fmt.Errorf( - "%q does not name a role's tool: one invokes seat:.", t) - } - out = append(out, "mesh.seat."+seat+".tool."+verb, "mesh.seat."+seat+".tool."+verb+".*") - continue - } - module, tool, ok := strings.Cut(t, ".") - if !ok || module == "" || tool == "" { - return nil, fmt.Errorf( - "%q does not name a tool: one invokes ., seat:., or * for every one", t) - } - // Both ways a module's tool is addressed (novox/hq ADR 0159): to whichever instance - // answers, and to the instance on one machine, which is the same subject with the machine - // as its last token. - out = append(out, "mesh.mod."+module+".tool."+tool, "mesh.mod."+module+".tool."+tool+".*") - } - return out, nil -} - -// announcing is what a principal that serves tools subscribes to answer the NATS services -// protocol's discovery (novox/hq ADR 0197): the questions asked of every service, and those asked of -// each name it serves — its own and no other's, so it cannot answer for a service it is not. -func announcing(names ...string) []string { - out := []string{"$SRV.PING", "$SRV.INFO", "$SRV.STATS"} - for _, n := range names { - if !safeSubject.MatchString(n) { - continue - } - for _, verb := range []string{"PING", "INFO", "STATS"} { - out = append(out, "$SRV."+verb+"."+n, "$SRV."+verb+"."+n+".>") - } - } - return out -} - -// discovering is what a principal publishes to ask what answers (novox/hq ADR 0197): the services -// protocol's discovery requests, whose replies come to its own inbox. -func discovering() []string { - return []string{"$SRV.PING", "$SRV.PING.>", "$SRV.INFO", "$SRV.INFO.>"} -} - -// WitnessSubjects are the subjects a machine's node-engine publishes to witness the core builds it -// places (novox/hq ADR 0236): its own node tools' PING. The answers come to the machine's own inbox. -func WitnessSubjects(p Principal) []string { - return append([]string{pingSubject(p.Node)}, CheckSubjects(p)...) -} - -// CheckSubjects are the tools a machine's node-engine asks as declared health checks (novox/hq ADR 0240, -// to-be 48 §3): each `.` on this machine's instance — `mesh.mod..tool..` -// — and never the plain subject, which any machine's instance may answer. Sorted and once each. -func CheckSubjects(p Principal) []string { - seen := map[string]bool{} - var out []string - for _, c := range p.Checks { - module, tool, ok := strings.Cut(c, ".") - if !ok || !safeSubject.MatchString(module) || !safeSubject.MatchString(tool) { - continue - } - subject := "mesh.mod." + module + ".tool." + tool + "." + p.Node - if !seen[subject] { - seen[subject] = true - out = append(out, subject) - } - } - sort.Strings(out) - return out -} - -// pingSubject is a node-engine's witness's ping of its own node's tools (novox/hq ADR 0236). -func pingSubject(node string) string { return "$SRV.PING." + nodeToolsService + "." + node } - -const nodeToolsService = "node-tools" - -// VaultOwnCall is the subject a machine principal's node-engine asks the vault's `own` on, in its own name. -func VaultOwnCall(p Principal) string { - return "mesh.seat." + VaultOwn.Seat + "." + CallKind + "." + VaultOwn.Verb + "." + CallerToken(p.Username()) -} - -// VaultSignCall is the subject a machine principal's node-engine asks the vault's `sign` on, in its own name. -func VaultSignCall(p Principal) string { - return "mesh.seat." + VaultSign.Seat + "." + CallKind + "." + VaultSign.Verb + "." + CallerToken(p.Username()) -} - -// VaultSecretChanged is the vault's event that a secret changed (a name and its generation, never a value), which a -// node composing its own hears to compose again (issue 1126, ADR 0547 §2). -var VaultSecretChanged = "mesh.mod." + VaultOwn.Seat + ".event.secret-changed" diff --git a/go/buscompose/onnats.go b/go/buscompose/onnats.go deleted file mode 100644 index ee39eda..0000000 --- a/go/buscompose/onnats.go +++ /dev/null @@ -1,5 +0,0 @@ -package buscompose - -// NATSVar names the environment variable that says the controller is on the bus (mesh-controller internal/broker -// onnats.go); kept with the composer for the readiness it composes. -const NATSVar = "MESH_BUS_NATS" diff --git a/go/buscompose/process_grants.go b/go/buscompose/process_grants.go deleted file mode 100644 index 99c7b1d..0000000 --- a/go/buscompose/process_grants.go +++ /dev/null @@ -1,56 +0,0 @@ -package buscompose - -// The bus account of one process of a module that declares bus (novox/hq ADR 0436 §1, §2, §3, §5). -// -// **Never the module's own account's grants, never the tool runner's.** A process's authority is read -// straight from its bus declaration — its emits, its queue's consumer, its state, whether it reads the -// records of its module's own asks, and the tools and seat verbs it calls — and nothing else: it holds no -// seat, serves no tool, hears no module stream, and answers nothing a caller asked (§3). Modelled on -// provisionerGrants, the other kind of process account the mesh mints (ADR 0349 clauses 6 and 11). - -// processGrants is one process's whole authority: to publish, and to subscribe. -func processGrants(p Principal) (pub, sub []string, err error) { - // 1. Its own events, under its module's own namespace — the same subjects the module's own principal - // would publish them on, had it declared them there instead (novox/hq ADR 0436 §1). - own := "mesh.mod." + p.Module - for _, e := range p.Emits { - pub = append(pub, own+".event."+e) - } - - // 2. The one durable consumer of each work-queue stream its bus names (novox/hq ADR 0436 §1, §2, §6): - // asked about, pulled from and acknowledged, by the same name ConsumersOf composes the consumer under - // (QueueConsumer) — and no other stream's. - for _, q := range p.Queues { - consumer := p.Node + "_" + p.Module + "_" + q.Process - pub = append(pub, - "$JS.API.CONSUMER.INFO."+q.Stream+"."+consumer, - "$JS.API.CONSUMER.MSG.NEXT."+q.Stream+"."+consumer, - "$JS.ACK."+q.Stream+"."+consumer+".>") - } - - // 3. Its module's own state, by the local names its bus names alone — never the whole of what the - // module keeps (novox/hq ADR 0436 §1), read and written as stateGrants gives any module's own bucket. - kept := stateAccess{Module: p.Module, Node: p.Node, Keeps: p.State} - pub = append(pub, stateGrants(kept)...) - // And it hears each of them created, so one not on the bus at its start is bound when it appears (novox/hq issue - // 1790; process.AwaitBucket). - sub = append(sub, stateCreatedHeard(kept)...) - - // 4. The router's records of its module's own asks (novox/hq ADR 0436 §1, ADR 0259 §3): the same - // direct-get subjects a module's own principal reads for the seats its Uses names that keep them — - // read here from the one list, so the two cannot disagree. - if p.Asks { - pub = append(pub, SeatTrafficOf(p.Module, nil, p.Uses, nil).Records...) - } - - // 5. The tools and seat verbs it calls (novox/hq ADR 0152, ADR 0436 §1): the same derivation a - // module's own principal's invokes takes, so `callerNamed` (applied to every non-controller principal) - // adds the caller-named `call` variants for it exactly as it would for a module. - invoked, err := invokedSubjects(p.Invokes) - if err != nil { - return nil, nil, err - } - pub = append(pub, invoked...) - - return pub, sub, nil -} diff --git a/go/buscompose/provisioner.go b/go/buscompose/provisioner.go deleted file mode 100644 index e279463..0000000 --- a/go/buscompose/provisioner.go +++ /dev/null @@ -1,229 +0,0 @@ -package buscompose - -import ( - "errors" - "fmt" - "strings" - - "git.novox.be/novox/mesh-sdk/go/delivery" - "git.novox.be/novox/mesh-sdk/go/provisions" - - "github.com/nats-io/nats.go" -) - -// The provisioner client's own bus account (novox/hq ADR 0349 clauses 6 and 11, design 57 package P3). -// -// **Not the tool runner's account.** The client fills a machine's files and asks the vault for the secrets that -// machine's declaration names; the vault hands a secret only to an account of its own, because a call under the -// tool runner's account cannot be told from an agent's on that machine (ADR 0305 §3, §12). So each machine that -// runs the client has a principal of its own, KindProvisioner, named for the machine, and granted what the client -// does and nothing else: read its own machine's keys in each domain's bucket, write its own machine's keys of the -// provisioner server's needs, applied and collections (design 57 P4), read the server's registry, say what it -// applied on its own machine, and ask the vault in its own name. - -// NodeProvisionerSeat is the seat the provisioner client holds on every machine (ADR 0349 clause 3). -const NodeProvisionerSeat = "node-provisioner" - -// ProvisionerSecret is the own secret of the module running the client that its bus account is delivered in, as -// `broker` is a module's own (novox/hq issue 195): a module that claims NodeProvisionerSeat and declares no own -// secret of this name has nowhere to read an account, and is given none. -const ProvisionerSecret = "provisioner-broker" - -// ConfigResolvedBucket is the config domain's resolved values, keyed `..` (ADR 0348, research -// 051): mesh-config's state `resolved`, written by mesh-config alone. -var ConfigResolvedBucket = BucketName("mesh-config", "resolved") - -// ProvisionerReads are the domains' buckets a provisioner client reads its own machine's keys of, each keyed -// `.…` (research 051): placement, the controller's assignments mirror (`.`), whose value carries -// the provisioner's plan of a module handed to it (inventory.Provisioning, design 57 P5); and config, -// mesh-config's resolved values; and the seat mesh-provisioner's plans of its machine, `.` (novox/hq ADR -// 0570 clause 2). **Named one by one**, as the controller's asks are: a domain that moves to the -// provisioner adds its bucket here, and a bucket not named is one the client cannot read. -var ProvisionerReads = []string{ConfigResolvedBucket} - -// ProvisionerReadsWhole are the buckets a provisioner client reads every key of: the seat mesh-placement's placement, -// where a provider's consumers are placed on their own machines, which the provider machine's client reads to fill -// ${consumers:} with each one's credential (novox/hq ADR 0373, issue 679; ADR 0567, issue 1126: the -// controller's assignments mirror it read before is gone). It holds placement, never a secret's value; the client -// writes none of it. **The seat's plans are read whole for the same -// reason** (2026-10-30 recovery, issue 774): the client V3 watches every machine's plans to bind a provider's -// consumers, and granted only its own machine's keys it read none. ADR 0570 clause 3 has providers act on the seat's -// provision keys instead; once the client does, plans go back to its own machine's keys. -var ProvisionerReadsWhole = []string{delivery.PlacementBucket, provisions.PlansBucket} - -// BoundState is a providing module's state of entries, one per consumer (mesh-sdk go/provider, issue 678): a -// consumer's machine's client reads its own keys of it, `..[.]`, to fill -// ${bound::} (issue 679). -const BoundState = "bound" - -// ProvisionerServerModule is the module holding the seat mesh-provisioner once for the mesh (ADR 0349 clause 3, -// design 57 P4), whose buckets the clients write their own machine's keys of. -const ProvisionerServerModule = "mesh-provisioner" - -// The provisioner server's buckets (ADR 0349 clause 3): its state, created by the controller from its manifest -// like any module's, and written by its own account whole. -var ( - // ProvisionerRegistryBucket is which domain answers which kind of placeholder, keyed by the kind; the - // server's alone, read whole by every client. - ProvisionerRegistryBucket = BucketName(ProvisionerServerModule, "registry") - // ProvisionerNeedsBucket is what each module on each machine needs, `...`. - ProvisionerNeedsBucket = BucketName(ProvisionerServerModule, "needs") - // ProvisionerAppliedBucket is what is applied where, `.`. - ProvisionerAppliedBucket = BucketName(ProvisionerServerModule, "applied") - // ProvisionerCollectBucket is the items of every collection, `..`. - ProvisionerCollectBucket = BucketName(ProvisionerServerModule, "collect") -) - -// ProvisionerWrites are the server's buckets a client writes, and reads, its own machine's keys of (`.>`) -// and no other's: the needs it records, what it applied, and the items of the collections on its machine. A client -// cannot write another machine's needs, or say another machine applied a file. -// -// **The needs stay granted for one release, and are removed after the lease write** (review round 3 of design 47's -// release): the client's V3 reads its machine's plans from the seat mesh-provisioner's `plans` (novox/hq ADR 0570 clause -// 2) and records no needs, but the client before it still runs on a node until its delivery reaches it, and it writes -// them. -var ProvisionerWrites = []string{ProvisionerNeedsBucket, ProvisionerAppliedBucket, ProvisionerCollectBucket} - -// VerbsTheProvisionerAsksTheVault are the vault's verbs a provisioner client asks in its own name (ADR 0349 -// clause 11): `get`, a secret its machine's declaration names, answered sealed to the asker's key. On the subject -// that names its caller, so the vault knows which machine's client asks from the bus's word. -var VerbsTheProvisionerAsksTheVault = []SeatVerb{{Seat: "mesh-vault", Verb: "get"}} - -// VaultSecretChangedSubject is where the vault says a secret it holds changed (`secret-changed`: which, by whom, -// when, its generation; never a value, ADR 0349 clause 11), as link.EventSubject addresses mesh-vault's event. A -// provisioner client listens to it so a rotated secret reaches its file without a new declaration (design 57 V4): -// it asks the vault again, in its own name, for a secret its machine's declaration names. -const VaultSecretChangedSubject = "mesh.mod.mesh-vault.event.secret-changed" - -// ProvisionerAppliedSubject is where a machine's client says what it applied for one module, applied or failed -// with the reason (ADR 0349 clauses 4 and 15): `prov.applied..`. -func ProvisionerAppliedSubject(node, module string) string { - return "prov.applied." + node + "." + module -} - -// ProvisionerAskSubject is where a machine's tool runner asks that machine's provisioner client for a config value -// or a non-secret bound value of a module it carries, naming the module from the bundle that asks (novox/hq ADR -// 0370 §1, §3, §4; issue 685): `prov.ask.`, which the client subscribes and the tool runner's account of the -// same machine alone may publish. The bus proves only that the tool runner asks; the module is the runtime's word, -// so the client answers no secret here (ADR 0370 §2). -func ProvisionerAskSubject(node string) string { return "prov.ask." + node } - -// ProvisionerChangedSubject is where a machine's client says that a value of one module it answers moved, so the -// tool runner asks again for the module's code's watches (ADR 0349 §14, ADR 0370 §1): `prov.changed..`, -// carrying which value and never the value itself, heard by that machine's tool runner alone. -func ProvisionerChangedSubject(node, module string) string { - return "prov.changed." + node + "." + module -} - -// provisionerGrants is a provisioner client's whole authority: to publish, and to subscribe. -func provisionerGrants(p Principal) (pub, sub []string, err error) { - if p.Node == "" { - // Refused rather than composed into `$KV...>`, a pattern no machine's keys are under, and a - // name every nameless client would share. - return nil, nil, errors.New("a provisioner client names no machine, so it would have no keys of its own " + - "to read and an account it shared with every other") - } - for _, bucket := range ProvisionerReads { - pub = append(pub, machineKeysRead(bucket, p.Node)...) - } - for _, bucket := range ProvisionerReadsWhole { - pub = append(pub, wholeBucketRead(bucket)...) - } - for _, bucket := range p.BoundBuckets { - module, name, ok := strings.Cut(bucket, "_") - if !ok || name != BoundState || !safeSubject.MatchString(module) || strings.Contains(module, "_") { - return nil, nil, fmt.Errorf("%q is not a provider's bucket of entries, _%s", bucket, BoundState) - } - pub = append(pub, machineKeysRead(bucket, p.Node)...) - } - // The server's buckets (design 57 P4): its own machine's keys of the needs, the applied and the - // collections, read and written, `$KV...>` and nothing wider; and the registry, read whole, - // which says which domain answers which kind and is the same for every machine. - for _, bucket := range ProvisionerWrites { - pub = append(pub, machineKeysRead(bucket, p.Node)...) - pub = append(pub, "$KV."+bucket+"."+p.Node+".>") - } - pub = append(pub, wholeBucketRead(ProvisionerRegistryBucket)...) - // Its own node's own have, `.node`, read and never written, and no other key of the bucket: it fills every - // module again when the node-engine moves to a new composition of the node's own (issue 1945), since an apply can - // leave a filled file as the composition had it. - pub = append(pub, readGrants(delivery.HaveBucket, p.Node+".node")...) - pub = append(pub, "prov.applied."+p.Node+".>") - token := CallerToken(p.Username()) - if token == "" { - return nil, nil, fmt.Errorf("%q cannot name a caller", p.Username()) - } - for _, v := range VerbsTheProvisionerAsksTheVault { - pub = append(pub, "mesh.seat."+v.Seat+"."+CallKind+"."+v.Verb+"."+token) - } - // What moved of a value it answers its own machine's tool runner (ADR 0370 §1), under its own machine's name. - pub = append(pub, "prov.changed."+p.Node+".>") - // Its own reply space, for the vault's answers and its reads' and watches' deliveries; what the vault says - // changed; and its own machine's tool runner asking it for a module's config and non-secret bound values (ADR - // 0370 §3), which it answers on the ask's reply and nowhere else. - return pub, []string{p.inbox(), VaultSecretChangedSubject, ProvisionerAskSubject(p.Node)}, nil -} - -// machineKeysRead is reading every key of a bucket under one machine's name, `.>`, and no other key: a -// direct get of a key's own subject, and a consumer whose filter is under it — the server's create-with-filter -// form, which it holds to the filter the subject names (as stateGrants does for one key, novox/hq ADR 0260). -// Binding, deleting a consumer and flow control cannot be narrowed by key and read no value. -func machineKeysRead(bucket, node string) []string { - stream := "KV_" + bucket - keys := "$KV." + bucket + "." + node + ".>" - return []string{ - "$JS.API.STREAM.INFO." + stream, - "$JS.API.DIRECT.GET." + stream + "." + keys, - "$JS.API.CONSUMER.CREATE." + stream + ".*." + keys, - "$JS.API.CONSUMER.DELETE." + stream + ".>", - "$JS.FC." + stream + ".>", - } -} - -// wholeBucketRead is reading every key of a bucket and writing none: what stateGrants grants a module for a -// bucket it `reads`. -func wholeBucketRead(bucket string) []string { - stream := "KV_" + bucket - return []string{ - "$JS.API.STREAM.INFO." + stream, - "$JS.API.DIRECT.GET." + stream + ".>", - "$JS.API.CONSUMER.CREATE." + stream + ".>", - "$JS.API.CONSUMER.DELETE." + stream + ".>", - "$JS.FC." + stream + ".>", - } -} - -// LastProvApplied is what a machine's provisioner client last said of one module's files, as it published it on -// ProvisionerAppliedSubject and ProvAppliedStream keeps it (design 57 P6): false when it has said nothing of it in -// the stream's week. -func LastProvApplied(js nats.JetStreamContext, node, module string) ([]byte, bool, error) { - msg, err := js.GetLastMsg(ProvAppliedStream, ProvisionerAppliedSubject(node, module)) - if errors.Is(err, nats.ErrMsgNotFound) { - return nil, false, nil - } - if err != nil { - return nil, false, fmt.Errorf("what %s's provisioner client said of %s cannot be read: %w", node, module, err) - } - return msg.Data, true, nil -} - -// ProvAppliedReports is the newest word of every machine's provisioner client on every module it said one of, kept by -// ProvAppliedStream, by subject (`prov.applied..`). -func ProvAppliedReports(js nats.JetStreamContext) (map[string][]byte, error) { - info, err := js.StreamInfo(ProvAppliedStream, &nats.StreamInfoRequest{SubjectsFilter: ProvAppliedSubjects}) - if err != nil { - return nil, fmt.Errorf("the provisioner clients' reports cannot be listed: %w", err) - } - out := map[string][]byte{} - for subject := range info.State.Subjects { - msg, err := js.GetLastMsg(ProvAppliedStream, subject) - if errors.Is(err, nats.ErrMsgNotFound) { - continue // aged out between the two reads - } - if err != nil { - return nil, fmt.Errorf("the provisioner client's report on %s cannot be read: %w", subject, err) - } - out[subject] = msg.Data - } - return out, nil -} diff --git a/go/buscompose/raise.go b/go/buscompose/raise.go deleted file mode 100644 index 9104f04..0000000 --- a/go/buscompose/raise.go +++ /dev/null @@ -1,73 +0,0 @@ -package buscompose - -import "fmt" - -// Bringing the bus's own objects into being, in the one order that works. -// -// **Asserted on every start rather than created once at genesis.** A stream somebody deleted, a mesh -// raised from a restored backup, or a bus whose data directory was replaced all have records and no -// objects — and a node whose consumer is missing hears nothing while everything else about it looks -// correct. Idempotence is the whole requirement, and the parts are already idempotent; this is the -// order they have to be asked in. - -// Raiser is everything asserting the bus's objects needs of a connection to it. -type Raiser interface { - Asserter - Ensurer -} - -// Raise asserts the mesh's streams, its own consumers, and one consumer per node. -// -// **The order is not a preference.** A consumer on a stream that does not exist is refused, and the -// refusal names the stream rather than the order — so somebody reading it goes looking for a deleted -// stream instead of a reversed pair of lines. Nodes last, because the one a node reads lives on a -// stream the mesh's own set defines. -func Raise(r Raiser, nodes []string) error { - if err := AssertMeshStreams(r); err != nil { - return err - } - if err := AssertMeshConsumers(r); err != nil { - return err - } - if err := AssertNodeConsumers(r, nodes); err != nil { - return err - } - return nil -} - -// RaiseSeats asserts one work queue per declared seat, and the worker of whoever holds it. -// -// Separate from Raise because it is answered by a different question: the mesh's own objects exist -// because the mesh does, and a seat's exist because a module declaring one was registered. Kept -// beside it so the order is visible — a holder's worker needs the seat's stream, and a seat's stream -// needs nothing. -func RaiseSeats(r Raiser, seats []DeclaredSeat, holders map[string]Holder) error { - for _, s := range SeatStreams(seats) { - if err := r.EnsureStream(s); err != nil { - return fmt.Errorf("asserting the work queue for %s: %w", s.Name, err) - } - } - for _, s := range seats { - h, held := holders[s.Name] - if !held { - // **The stream exists and the consumer does not, on purpose.** Work queues until a - // holder appears, so installing the module a week after something started sending to it - // flushes the backlog instead of having lost it. - continue - } - c, needed := HolderConsumerFor(h.Node, h.Module, s) - if !needed { - continue - } - if err := r.EnsureConsumer(c); err != nil { - return fmt.Errorf("asserting how %s on %s works %s: %w", h.Module, h.Node, s.Name, err) - } - } - return nil -} - -// Holder is which module on which machine holds a seat. -type Holder struct { - Node string - Module string -} diff --git a/go/buscompose/readiness.go b/go/buscompose/readiness.go deleted file mode 100644 index 5fb24ec..0000000 --- a/go/buscompose/readiness.go +++ /dev/null @@ -1,139 +0,0 @@ -package buscompose - -import ( - "fmt" - "sort" - "strings" -) - -// Whether a mesh could move its bus, and what is missing if not. -// -// **Asked before anything moves, and answerable from records alone.** The rollout moves every node at -// once (novox/hq ADR 0116 step 5), so there is no partial state to inspect afterwards and no half to -// roll back: either the mesh was ready or it was not. That makes a readiness question the most -// valuable thing here — it costs nothing, it can be asked of a running mesh any number of times, and -// every answer is a thing somebody can go and fix. -// -// Deliberately pure. It is handed what the mesh knows and returns sentences; nothing here connects to -// anything, so it can be asked on a workstation about a mesh it has never reached. - -// Readiness is what the mesh knows about its own ability to move. -type Readiness struct { - // TheBus is the address the mesh's own traffic would move to, empty when nothing names one. - TheBus string - // ServerStanding is whether a bus is reachable at that address, as somebody checked. - ServerStanding bool - // Holder is the node running the module that holds the bus seat, empty when nothing does. - Holder string - // AccountsComposed is whether that node has been sent the composed user list. - AccountsComposed bool - // Nodes is every machine the mesh knows. - Nodes []string - // Credentialled is which of them has a credential for the new bus. - Credentialled map[string]bool - // Modules is every assigned module, as `/`. - Modules []string - // ModuleCredentialled is which of those has one. - ModuleCredentialled map[string]bool -} - -// NotReady is every reason this mesh cannot move its bus yet, in the order somebody would fix them. -// -// Empty means ready. **Each entry names one thing and what to do about it**, because a readiness check -// that says "not ready" is a check nobody can act on — and this is read at the point where the next -// step is irreversible. -func NotReady(r Readiness) []string { - var why []string - - if strings.TrimSpace(r.TheBus) == "" { - why = append(why, "nothing names the bus to move to: set "+NATSVar+" on the control node "+ - "to the address the new server answers on") - } - if !r.ServerStanding { - why = append(why, "no bus is answering at that address. Step 2 of the change raises it beside "+ - "the one the mesh is on, carrying nothing — assign the module that holds "+ - "mesh-broker and push the machine that runs it") - } - if r.Holder == "" { - why = append(why, "no machine holds mesh-broker, so nothing would compose the bus's user "+ - "list. Assign the module that claims it") - } else if !r.AccountsComposed { - why = append(why, fmt.Sprintf( - "%s holds mesh-broker and has not been sent the composed user list, so the bus would "+ - "refuse every connection. `push %s`", r.Holder, r.Holder)) - } - - // A node with no credential cannot come back after the move, and a node that cannot come back is - // a machine the mesh has lost until somebody goes to it. - var missing []string - for _, n := range r.Nodes { - if !r.Credentialled[n] { - missing = append(missing, n) - } - } - sort.Strings(missing) - if len(missing) > 0 { - why = append(why, fmt.Sprintf( - "%d machine(s) have no credential for the new bus and would not come back: %s. Each needs "+ - "one minted before the move, not after — after, there is no bus to ask over", - len(missing), strings.Join(missing, ", "))) - } - - // A module without one keeps running and stops being reachable, which is a smaller fault and still - // one somebody should choose rather than discover. - var quiet []string - for _, m := range r.Modules { - if !r.ModuleCredentialled[m] { - quiet = append(quiet, m) - } - } - sort.Strings(quiet) - if len(quiet) > 0 { - why = append(why, fmt.Sprintf( - "%d module(s) have no credential for the new bus: %s. Each keeps serving and stops "+ - "answering tools and hearing events until it is issued one", - len(quiet), strings.Join(quiet, ", "))) - } - - return why -} - -// WhatMoves is what the rollout would do, in order, for somebody reading before they commit. -// -// **Written out rather than summarised.** This is the one step with nothing to inspect afterwards, so -// the last useful moment to disagree with it is while reading this. -func WhatMoves(r Readiness) []string { - out := []string{ - fmt.Sprintf("compose the bus's user list and send it to %s", holderOr(r.Holder)), - fmt.Sprintf("move this control plane to %s, and confirm it is heard", busOr(r.TheBus)), - } - nodes := append([]string(nil), r.Nodes...) - sort.Strings(nodes) - for _, n := range nodes { - out = append(out, fmt.Sprintf("move %s, and confirm it reports", n)) - } - if len(r.Modules) > 0 { - out = append(out, fmt.Sprintf("move %d module runtime(s), and confirm each answers", - len(r.Modules))) - } - // **The old broker goes, and it goes last** (novox/hq ADR 0131). AMQP is not a provision, so once - // every machine reports on the new bus nothing of the mesh is left speaking to it, and its module - // is unassigned. Said as a step so nobody reads the move as leaving a second bus behind. - out = append(out, "then unassign the old broker's module: AMQP is not a provision (ADR 0131), and "+ - "once every machine reports on the new bus nothing of the mesh speaks to it") - return out -} - -func holderOr(node string) string { - if node == "" { - return "whichever machine holds mesh-broker" - } - return node -} - -func busOr(address string) string { - if address == "" { - return "the new bus" - } - return address -} diff --git a/go/buscompose/sdk_membership.go b/go/buscompose/sdk_membership.go index d9ad34c..215f145 100644 --- a/go/buscompose/sdk_membership.go +++ b/go/buscompose/sdk_membership.go @@ -2,37 +2,7 @@ package buscompose import ( "git.novox.be/novox/mesh-sdk/go/membership" - "git.novox.be/novox/mesh-sdk/go/process" -) - -// The membership the mesh issues an assignment (novox/hq ADR 0160), its subject, its stream, the bare address and the -// pin are mesh-sdk's, public so that a module's program outside this Go module, the route proxy, reads them too -// (novox/hq ADR 0480 clause 4, issue 1633). This package keeps no second copy: what follows only names them where the -// controller already did. -type ( - Membership = membership.Membership - Served = membership.Served - SeatServed = membership.SeatServed - StateIssued = membership.StateIssued - LogIssued = membership.LogIssued - Worker = membership.Worker - SeatTraffic = membership.SeatTraffic - KindHeld = membership.KindHeld - StreamIssued = membership.StreamIssued - HeardIssued = membership.HeardIssued - WaitIssued = membership.WaitIssued ) // AssignmentsStream holds every assignment's membership, the newest per subject. const AssignmentsStream = membership.AssignmentsStream - -var ( - // MembershipSubject is where an assignment's own membership is published. - MembershipSubject = membership.Subject - // CredentialIn reads the user and password out of a bus address, and the address without them. - CredentialIn = membership.CredentialIn - // BareAddress is a bus address with any credential stripped. - BareAddress = membership.BareAddress - // PinnedToFingerprint accepts exactly the certificate with this SHA-256 and no other. - PinnedToFingerprint = process.PinnedToFingerprint -) diff --git a/go/buscompose/seattraffic.go b/go/buscompose/seattraffic.go index 243f7ac..fe5b9e8 100644 --- a/go/buscompose/seattraffic.go +++ b/go/buscompose/seattraffic.go @@ -1,319 +1,7 @@ package buscompose -import ( - "fmt" - "sort" -) - -// What a module may say and take on the seats it holds, uses and watches, beyond tools (novox/hq ADR -// 0259 §3, to-be 46 §10). -// -// Three rules are added to the ones a seat always had, each a subject whose last token names who may -// publish it, granted to that publisher alone — the way a node seat's event about a machine carries the -// machine (ADR 0219): -// -// - **A verb named by its caller** (`by-caller`). A user of the seat submits that accept, and hears that -// event, under its own module's name and no other: `accept.ask.`, `event.decided.`. The -// holder takes every caller's accept and says the event to any caller. So an ask's asker is a fact the -// server enforces, and a warrant reaches only the asker it is for. -// - **A kinded bench** (ADR 0234 §2). A holder claims one kind and takes its own kind's accepts, says its -// own kind's events and asks its own kind's proofs, and nothing of another kind; a user submits to any -// kind. Each kind has its own worker on the seat's queue, so a holder that is away keeps its work and -// holds up no other kind. -// - **A proof** (`proofs`): core request and reply on `mesh.seat..proof..`. No stream's -// subjects cover it, so what travels there — a code typed by the operator — is never persisted. A kinded -// holder asks with its own kind; the modules that watch the seat answer. -// -// And two reads: **a holder's records**, a bucket the seat names, read by each user under its own name only -// (`$KV...>`), so an asker reads the state of its own asks and no other asker's; and on a seat pulled by -// kind, **what its workers gave up on**, the copies DEAD_LETTERS keeps of the seat's stream (DeadLetterReads, issue 734). - -// BuildAskMaxDeliver bounds the deliveries of a build seat's ask (issue 1907): mesh-sdk delivery.AskMaxDeliver, written -// here as the broker mirrors the catalogue rather than importing it. -const BuildAskMaxDeliver = 5 - -// KindedBenches are the seats that may be kinded (ADR 0234 §2): making another is a decision, recorded. -var KindedBenches = map[string]bool{"channel": true, "intake": true} - -// WorkerName is the worker a seat's holders pull from: one for the seat, or one per kind on a kinded bench. -func WorkerName(seat, kind string) string { - if kind == "" { - return "SEAT_" + upperSnake(seat) + "_worker" - } - return "SEAT_" + upperSnake(seat) + "_" + upperSnake(kind) + "_worker" -} - -func namesVerb(list []string, s string) bool { - for _, x := range list { - if x == s { - return true - } - } - return false -} - -// SeatTrafficOf derives one module's seat traffic from the seats it holds, uses and watches. Only seats -// carrying one of the rules above are read: every other seat is composed as it always was. -func SeatTrafficOf(module string, holds, uses, watches []Seat) SeatTraffic { - var t SeatTraffic - for _, s := range holds { - if !s.isNewTraffic() { - // **A holder says its own seat's events** (novox/hq ADR 0564 clause 6, issue 1952): its own account is - // granted them by the held seats' rule (PermissionsFor, step 3), and a module the tool runner carries had - // no way to, so the self-check's `checked` was never said. Listed here, the membership names them for the - // runtime and the runtime is granted them. A machine's event on a node seat carries the machine, which - // only the holder's own account is composed with, so it is left to that rule. - for _, e := range s.Emits { - if s.Scope == "node" && perMachineEvents[e] { - continue - } - t.Publish = append(t.Publish, seatSubject(s, "event", e)) - } - continue - } - kind := "" - if s.Kinded { - kind = s.Kind - if kind == "" || !safeSubject.MatchString(kind) { - // A kinded claim without a usable kind is refused at registration; here it is granted - // nothing, which is the same answer at the last place it could be asked. - continue - } - } - if len(s.Accepts) > 0 && s.PulledByKind { - // One worker per accepted kind, which every holder pulls in the order of Accepts (ADR 0525 §4, ADR 0536 §4): - // `SEAT___worker`, filtered to `accept.` (mesh-sdk delivery.AskWorker). - for _, a := range s.Accepts { - t.Workers = append(t.Workers, Worker{Stream: seatStreamName(s.Name), Consumer: WorkerName(s.Name, a), - Filter: "mesh.seat." + s.Name + ".accept." + a}) - } - } else if len(s.Accepts) > 0 { - stream := seatStreamName(s.Name) - filter := "mesh.seat." + s.Name + ".accept.>" - if kind != "" { - filter = "mesh.seat." + s.Name + ".accept.*." + kind - } - t.Workers = append(t.Workers, Worker{Stream: stream, Consumer: WorkerName(s.Name, kind), Filter: filter}) - } - for _, a := range s.Accepts { - switch { - case kind != "": - t.Subscribe = append(t.Subscribe, seatSubject(s, "accept", a+"."+kind)) - case namesVerb(s.ByCaller, a): - t.Subscribe = append(t.Subscribe, seatSubject(s, "accept", a+".*")) - } - } - for _, e := range s.Emits { - switch { - case kind != "": - t.Publish = append(t.Publish, seatSubject(s, "event", e+"."+kind)) - case namesVerb(s.ByCaller, e): - t.Publish = append(t.Publish, seatSubject(s, "event", e+".*")) - } - } - if kind != "" { - for _, v := range s.Proofs { - t.Publish = append(t.Publish, seatSubject(s, "proof", v+"."+kind)) - } - } - } - for _, s := range uses { - if !s.isNewTraffic() { - continue - } - for _, a := range s.Accepts { - switch { - case namesVerb(s.ByCaller, a): - t.Publish = append(t.Publish, seatSubject(s, "accept", a+"."+module)) - case s.Kinded && module == s.DeclaredBy: - // Work for a kind is put on its queue by the bench's own router, and by no other user. - t.Publish = append(t.Publish, seatSubject(s, "accept", a+".*")) - } - } - for _, e := range s.Emits { - if namesVerb(s.ByCaller, e) { - t.Subscribe = append(t.Subscribe, seatSubject(s, "event", e+"."+module)) - } - } - for _, b := range s.Records { - if !safeSubject.MatchString(b) { - continue - } - t.Records = append(t.Records, "$JS.API.DIRECT.GET.KV_"+b+".$KV."+b+"."+module+".>") - } - if s.PulledByKind && len(s.Accepts) > 0 && safeSubject.MatchString(s.Name) { - t.Records = append(t.Records, DeadLetterReads(seatStreamName(s.Name))...) - } - } - for _, w := range watches { - if w.Kinded { - for _, e := range w.Emits { - t.Subscribe = append(t.Subscribe, seatSubject(w, "event", e+".*")) - } - if module == w.DeclaredBy { - // A code is answered by the bench's own router, and by no other watcher. - for _, v := range w.Proofs { - t.Answers = append(t.Answers, seatSubject(w, "proof", v+".*")) - } - } - } - } - t.Publish = unique(t.Publish) - t.Subscribe = unique(t.Subscribe) - t.Answers = unique(t.Answers) - t.Records = unique(t.Records) - sort.Slice(t.Workers, func(i, j int) bool { return t.Workers[i].Consumer < t.Workers[j].Consumer }) - return t -} - -// DeadLetterReads is what a user of a seat pulled by kind is granted to hear which of the asks it put the seat's workers -// gave up on (novox/hq issue 734): an ordered consumer on DEAD_LETTERS filtered to the copies the controller keeps of that -// seat's stream (DeadLetterSubject(stream, *)), pulled, and its deletion. A copy carries the ask's own message id -// (Mesh-Dead-Msg-Id), so the asker ends the step it waits for instead of waiting for ever; it reads the seat's dead -// letters and no other stream's, and changes none. -func DeadLetterReads(stream string) []string { - return []string{ - "$JS.API.STREAM.INFO." + DeadLettersStream, - "$JS.API.CONSUMER.CREATE." + DeadLettersStream + ".*." + DeadLetterSubject(stream, ">"), - "$JS.API.CONSUMER.MSG.NEXT." + DeadLettersStream + ".*", - "$JS.API.CONSUMER.DELETE." + DeadLettersStream + ".*", - } -} - -// seatTrafficGrants is the bus permissions seat traffic needs: the subjects themselves, and the JetStream API a -// worker is pulled and acknowledged through and a record is read through. -func seatTrafficGrants(t SeatTraffic) (pub, sub []string) { - pub = append(pub, t.Publish...) - sub = append(sub, t.Subscribe...) - sub = append(sub, t.Answers...) - for _, w := range t.Workers { - pub = append(pub, - "$JS.API.CONSUMER.INFO."+w.Stream+"."+w.Consumer, - "$JS.API.CONSUMER.MSG.NEXT."+w.Stream+"."+w.Consumer, - "$JS.ACK."+w.Stream+"."+w.Consumer+".>") - } - pub = append(pub, t.Records...) - return pub, sub -} - // isNewTraffic says whether a seat carries any of the rules above, so a seat that carries none is // composed exactly as before them. func (s Seat) isNewTraffic() bool { return s.Kinded || len(s.ByCaller) > 0 || len(s.Proofs) > 0 || len(s.Records) > 0 || s.PulledByKind } - -// plainVerbs is a seat's accepts or emits with those the rules above compose taken out: a verb named by -// its caller and every verb of a kinded bench are composed by SeatTrafficOf and nowhere else. -func plainVerbs(s Seat, verbs []string) []string { - if s.Kinded { - return nil - } - var out []string - for _, v := range verbs { - if !namesVerb(s.ByCaller, v) { - out = append(out, v) - } - } - return out -} - -// SeatTrafficObjects is the work queues and workers the seat traffic of every composed user implies -// (novox/hq ADR 0259 §3): a queue for each seat a holder takes work from, and each holder's worker on it — -// one per kind on a kinded bench, filtered to that kind, so the kinds never take each other's work. Only -// seats carrying the rules above; the mesh's own seats' queues are RaiseSeats'. -func SeatTrafficObjects(users []Principal) ([]Stream, []Consumer) { - streams := map[string]Stream{} - consumers := map[string]Consumer{} - add := func(module string, holds []Seat) { - for _, w := range SeatTrafficOf(module, holds, nil, nil).Workers { - seat, byKind := "", false - for _, s := range holds { - if seatStreamName(s.Name) == w.Stream { - seat, byKind = s.Name, s.PulledByKind - } - } - streams[w.Stream] = Stream{ - Name: w.Stream, - Subjects: []string{"mesh.seat." + seat + ".accept.>"}, - Retention: RetentionWorkQueue, - MaxAge: 7 * 24 * 60 * 60, - Why: "work submitted to the " + seat + " seat; its holders take it, each kind its own, and it queues while nobody does", - } - c := Consumer{ - Name: w.Consumer, - Stream: w.Stream, - Filters: []string{w.Filter}, - AckWaitSeconds: 60, - // No bound on redelivery: a channel away for a day keeps its work, offered again later and - // later by its holder's runtime (novox/hq ADR 0259; the correctness review of 2026-10-08). - MaxDeliver: 0, - Why: module + " holds " + seat + "; it pulls one ask at a time and acknowledges once it has " + - "recorded it, so a crash redelivers rather than loses", - } - if byKind { - // A build seat's ask is offered a bounded number of times (issue 1907): the holder that takes it past - // mesh-sdk delivery.AskRuns ends it failed saying it is poison, and the bound stops a run that kills - // every node it reaches even when that holder dies too. - c.MaxDeliver = BuildAskMaxDeliver - c.Why = module + " holds " + seat + "; each holder pulls an ask when it has a free slot, and an ask that " + - "no run ends is offered at most " + fmt.Sprint(BuildAskMaxDeliver) + " times, its step failed as poison" - } - consumers[w.Consumer] = c - } - } - for _, p := range users { - switch p.Kind { - case KindModule: - add(p.Module, p.Holds) - case KindNodeTools: - for _, d := range p.Carries { - add(d.Module, d.Holds) - } - } - } - var ss []Stream - for _, s := range streams { - ss = append(ss, s) - } - sort.Slice(ss, func(i, j int) bool { return ss[i].Name < ss[j].Name }) - var cs []Consumer - for _, c := range consumers { - cs = append(cs, c) - } - sort.Slice(cs, func(i, j int) bool { return cs[i].Name < cs[j].Name }) - return ss, cs -} - -// trustedTraffic is why a module's seat traffic is the trusted holder's (novox/hq ADR 0259 §8), or "": it -// says a seat's event to one caller each (a warrant), or holds a kind of a kinded bench that proves its sender. -func trustedTraffic(d Declared) string { - for _, s := range d.Holds { - for _, e := range s.Emits { - if namesVerb(s.ByCaller, e) { - return "it says " + s.Name + "'s " + e + " to one caller each" - } - } - if s.Kinded && namesVerb(s.Capabilities, "verified-sender") { - return "it holds " + s.Name + " of kind " + s.Kind + ", which proves its sender" - } - } - return "" -} - -// TrafficQueues is the work queue of every seat naming its caller or its kind that accepts work, held or not -// (novox/hq ADR 0259 §3): what is submitted before a holder is assigned waits for it. -func TrafficQueues(seats []Seat) []Stream { - var out []Stream - seen := map[string]bool{} - for _, s := range seats { - if len(s.Accepts) == 0 || !s.isNewTraffic() || seen[s.Name] { - continue - } - seen[s.Name] = true - out = append(out, Stream{Name: seatStreamName(s.Name), Subjects: []string{"mesh.seat." + s.Name + ".accept.>"}, - Retention: RetentionWorkQueue, MaxAge: 7 * 24 * 60 * 60, - Why: "work submitted to the " + s.Name + " seat; its holders take it, each kind its own, and it queues while nobody does"}) - } - sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) - return out -} diff --git a/go/buscompose/state.go b/go/buscompose/state.go index b64461e..2f090aa 100644 --- a/go/buscompose/state.go +++ b/go/buscompose/state.go @@ -2,29 +2,12 @@ package buscompose import ( "fmt" - "git.novox.be/novox/mesh-sdk/go/announce" - "git.novox.be/novox/mesh-sdk/go/delivery" - "sort" - "strings" - "time" ) -// A module's state on the bus (novox/hq ADR 0201, design 32 §4, design 25 §3). -// -// A module names the state it keeps (`state`) and the state of others it reads (`reads`), and each -// is a key-value bucket: the server's own last-per-subject stream with direct reads, delete markers -// and watches, which is the state relationship the mesh already uses for declarations, opened to -// modules. The controller creates every bucket from the catalogue — from registration, like a -// seat's stream, so a reader may watch before the owner runs anywhere — and no module can. -// -// Pure, like everything else in this package that decides what the bus holds; jetstream.go is the -// part that asks a server. - // The mesh's caps on a bucket, the same for every module: a value is a piece of state, not a file, // and a bucket that grew without bound would be one module filling the bus's disk for everyone. const ( StateMaxValueBytes = 256 * 1024 - StateMaxBytes = 64 * 1024 * 1024 ) // A Bucket is one module's declared state as the bus holds it. @@ -43,9 +26,6 @@ type Bucket struct { KeyTTL bool } -// KeyTTLMarkerTTL is how long the marker an expired key leaves is kept: long enough for a watcher to hear it. -const KeyTTLMarkerTTL = time.Minute - // BucketName is the bucket a module's state lives in: the module and the local name joined by an // underscore, which neither may contain, so two modules can never derive one bucket. func BucketName(module, name string) string { return module + "_" + name } @@ -61,310 +41,11 @@ func (b Bucket) Why() string { b.Module, b.Name, b.Module, b.Module) } -// bucketOfRead is the bucket a read names, `.`, or false when it names none. -func bucketOfRead(read string) (string, bool) { - at := strings.LastIndex(read, ".") - if at <= 0 || at == len(read)-1 { - return "", false - } - module, name := read[:at], read[at+1:] - if !safeSubject.MatchString(module) || !safeSubject.MatchString(name) { - return "", false - } - return BucketName(module, name), true -} - -// readGrants is what reaching a bucket's keys needs: binding to it, a direct get, an ordered consumer for listing and -// watching and the deletion of that consumer and its flow control. key is one key's subject tail, a pattern such as -// `one.>` for a prefix of keys, or empty for every key. The bucket is made with AllowDirect, so a get is the direct get -// and never the stream's message get. -func readGrants(bucket, key string) []string { - stream := "KV_" + bucket - out := []string{"$JS.API.STREAM.INFO." + stream} - if key == "" { - out = append(out, - "$JS.API.DIRECT.GET."+stream+".>", - "$JS.API.CONSUMER.CREATE."+stream+".>") - } else { - out = append(out, - "$JS.API.DIRECT.GET."+stream+".$KV."+bucket+"."+key, - "$JS.API.CONSUMER.CREATE."+stream+".*.$KV."+bucket+"."+key) - } - return append(out, - "$JS.API.CONSUMER.DELETE."+stream+".>", - "$JS.FC."+stream+".>") -} - -// NodeKeysGrants is what a node's node-engine is granted of a bucket it writes by node, `.>` and no other key -// (novox/hq ADR 0429 §1: each key one writer, enforced by credentials): the writes of those keys and what a get, -// create, update and watch of them need. The node name is checked as every subject token is. -func NodeKeysGrants(bucket, node string) ([]string, error) { - if !safeSubject.MatchString(node) { - return nil, fmt.Errorf("%q cannot be part of a subject: a permission is a subject pattern, and this would widen it", node) - } - return append([]string{"$KV." + bucket + "." + node + ".>"}, readGrants(bucket, node+".>")...), nil -} - -// stateGrants is what a principal publishes to reach the state its modules keep and read: for every -// bucket, binding to it, reading a key directly, and an ordered consumer for listing and watching, -// created and deleted on the bucket's own stream, with its flow control answered; for a bucket an -// owner keeps, writing under the bucket's own subjects too. -// -// **Measured against a running server, 2026-10-04** (novox/hq research 024), and each one is there -// because leaving it out failed: without STREAM.INFO nothing binds; without DIRECT.GET nothing is -// read; without CONSUMER.CREATE no key is listed and nothing is watched; without CONSUMER.DELETE a -// watch cannot be stopped and lingers on the server. A write outside these is refused by the server -// — and reaches the writer as a timeout, not a refusal, which is why the runtime refuses first. -// -// **One key, where the state is per machine** (novox/hq ADR 0260). The owner's instance on a machine -// writes and reads the key named for that machine, and a holder granted a read for its machine reads -// that key: a direct get of the key's own subject, and a consumer whose filter is that subject — the -// server's create-with-filter form, which a client uses for a watch of one key and which the server -// holds to the filter it names. What cannot be narrowed by key: binding (STREAM.INFO), deleting a -// consumer (named at random by the client) and flow control; none of them reads or writes a value. -func stateGrants(a stateAccess) []string { - var out []string - read := func(bucket, key string) { out = append(out, readGrants(bucket, key)...) } - machineKey := "" - if safeSubject.MatchString(a.Node) { - machineKey = a.Node - } - perMachine := map[string]bool{} - for _, n := range a.PerMachine { - perMachine[n] = true - } - for _, name := range a.Keeps { - if !safeSubject.MatchString(name) { - continue - } - bucket := BucketName(a.Module, name) - if perMachine[name] { - if machineKey == "" { - continue // a machine with no usable name reaches none of it - } - read(bucket, machineKey) - out = append(out, "$KV."+bucket+"."+machineKey) - continue - } - read(bucket, "") - out = append(out, "$KV."+bucket+".>") - } - // **And it says what it keeps** (novox/hq ADR 0495 clause 2, issue 1790): a module keeping state announces it on its - // own event subject, from its code; refused, its readers are read as before. - if len(a.Keeps) > 0 && safeSubject.MatchString(a.Module) { - out = append(out, announce.Subject(a.Module)) - } - for _, r := range a.Reads { - if bucket, ok := bucketOfRead(r); ok { - if NodeKeyedReads[r] { - if machineKey != "" { - read(bucket, machineKey+".>") - } - continue - } - read(bucket, "") - } - } - for _, k := range a.KeyedReads { - if bucket, ok := bucketOfRead(k.Read); ok && safeSubject.MatchString(k.Key) { - read(bucket, k.Key) - } - } - return out -} - -// NodeKeyedReads are the states keyed `.>` whose reader reads its own machine's keys alone, whatever its `reads` -// names: the node-engine's data items, each node's disk-load reading its own node's (novox/hq ADR 0577 clause 1). A -// machine with no usable name reaches none of them. -var NodeKeyedReads = map[string]bool{delivery.EngineDataRead: true} - -// StreamCreatedAdvisory is the subject the bus says a stream was created on (the server's own advisory), here for a -// bucket's stream KV_. -func StreamCreatedAdvisory(bucket string) string { - return "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_" + bucket -} - -// stateCreatedHeard is what a principal subscribes to so that a bucket it may bind and that is not on the bus yet is -// bound when it appears (novox/hq issue 1790, 2026-10-26: since the grants part creates a manifest's buckets, ADR 0547, -// a module's delivery can start it before its new state exists): the bus's advisory that the bucket's stream was -// created, for each bucket stateGrants lets it bind, and no other. Read-only: the server publishes it in the mesh's own -// account, and hearing it changes nothing. -func stateCreatedHeard(a stateAccess) []string { - var out []string - seen := map[string]bool{} - add := func(bucket string) { - if !seen[bucket] { - seen[bucket] = true - out = append(out, StreamCreatedAdvisory(bucket)) - } - } - perMachine := map[string]bool{} - for _, n := range a.PerMachine { - perMachine[n] = true - } - for _, name := range a.Keeps { - if !safeSubject.MatchString(name) || (perMachine[name] && !safeSubject.MatchString(a.Node)) { - continue - } - add(BucketName(a.Module, name)) - } - for _, r := range a.Reads { - if bucket, ok := bucketOfRead(r); ok { - add(bucket) - } - } - for _, k := range a.KeyedReads { - if bucket, ok := bucketOfRead(k.Read); ok && safeSubject.MatchString(k.Key) { - add(bucket) - } - } - return out -} - // KeyedRead is one key of another module's state a principal reads, and no other key of it. type KeyedRead struct { Read string `json:"read"` Key string `json:"key"` } -// stateAccess is what one principal may do with state: the module's own buckets (some per machine), -// what it reads whole, and what it reads for its machine's key alone. -type stateAccess struct { - Module, Node string - Keeps, PerMachine []string - Reads []string - KeyedReads []KeyedRead -} - -// perMachineNames is the local names of a module's buckets keyed by machine. -func perMachineNames(buckets []Bucket) []string { - var out []string - for _, b := range buckets { - if b.PerMachine { - out = append(out, b.Name) - } - } - return out -} - -// stateIssuedFor is every bucket a module's code may reach on a machine, as its membership lists them. -func stateIssuedFor(d Declared, node string) []StateIssued { - var out []StateIssued - for _, b := range d.State { - issued := StateIssued{Name: b.Name, Bucket: BucketName(d.Module, b.Name), Writes: true} - if b.PerMachine { - issued.Key = node - } - out = append(out, issued) - } - for _, r := range d.Reads { - if bucket, ok := bucketOfRead(r); ok { - out = append(out, StateIssued{Name: r, Bucket: bucket}) - } - } - for _, k := range d.KeyedReads { - if bucket, ok := bucketOfRead(k.Read); ok { - out = append(out, StateIssued{Name: k.Read, Bucket: bucket, Key: k.Key}) - } - } - return out -} - -// stateNames is the local names of a module's own buckets. -func stateNames(buckets []Bucket) []string { - out := make([]string, 0, len(buckets)) - for _, b := range buckets { - out = append(out, b.Name) - } - return out -} - -// A BucketAsserter is the part of a JetStream connection bucket assertion needs. -type BucketAsserter interface { - // EnsureBucket creates the bucket if absent and brings its options to match if present, never - // discarding what it holds. - EnsureBucket(b Bucket) error - // BucketNames is every key-value bucket on the server. - BucketNames() ([]string, error) -} - -// RaiseBuckets asserts every declared bucket and answers the buckets on the server that nothing -// declares any more. -// -// **Those are reported, never removed** (novox/hq ADR 0201, ADR 0030): what a module stored is -// data, and a manifest edited, a module renamed or a catalogue entry dropped is an ordinary day's -// work that must not take data with it. Removing one is a person's act. -func RaiseBuckets(a BucketAsserter, buckets []Bucket) (undeclared []string, err error) { - sorted := append([]Bucket(nil), buckets...) - sort.Slice(sorted, func(i, j int) bool { return sorted[i].Bucket() < sorted[j].Bucket() }) - declared := map[string]bool{} - for _, b := range sorted { - if err := a.EnsureBucket(b); err != nil { - return nil, fmt.Errorf("asserting %s's state %q: %w", b.Module, b.Name, err) - } - declared[b.Bucket()] = true - } - names, err := a.BucketNames() - if err != nil { - return nil, fmt.Errorf("listing the bus's state: %w", err) - } - for _, n := range names { - // A seat's cancelled set is the mesh's own (novox/hq ADR 0219), not a module's state. The ended controller's - // buckets are undeclared like any other until they are removed (ADR 0590 clause 6). - if !declared[n] && !IsCancelledSet(n) { - undeclared = append(undeclared, n) - } - } - sort.Strings(undeclared) - return undeclared, nil -} - // ResolvedBucket is mesh-config's state of resolved values, keyed `..`. const ResolvedBucket = "mesh-config_resolved" - -// NodeComposeReads is what a node's agent reads to compose its own modules (novox/hq ADR 0547 §2, ADR 0567 §3, §9, -// issue 1126): every key of the seat mesh-placement's placement and candidates (both the assigner's states), of the -// indexer's modules and manifests and of the builder's builds, and of mesh-config's resolved values only its own node's, -// `.>`. Every candidate key, not only `*.`: a node answering its candidate composes with the whole -// proposal laid over (the seed places every node at once, and a binding crosses nodes), and nothing in a placement is -// secret. Reads only; the node is checked as every subject token is by the caller's principal. -func NodeComposeReads(node string) []string { - var out []string - for _, bucket := range []string{delivery.PlacementBucket, delivery.PlacementCandidatesBucket, delivery.ModulesBucket, - delivery.ManifestsBucket, delivery.BuilderBuildsBucket} { - out = append(out, readGrants(bucket, "")...) - } - return append(out, readGrants(ResolvedBucket, node+".>")...) -} - -// NodeComposeAdvisories are the stream-created advisories of the seat mesh-placement's placement and candidates -// (ADR 0567 §9): what a node agent hears to bind each once it is made. Those two and no wider. -func NodeComposeAdvisories() []string { - return []string{StreamCreatedAdvisory(delivery.PlacementBucket), StreamCreatedAdvisory(delivery.PlacementCandidatesBucket)} -} - -// ServedReadGrants is the read of each key of mesh-config's resolved values named, `..` -// (Principal.ServedReads, issue 1126): a get and a watch of that key alone. Each part is checked as every subject token -// is, so a key cannot widen the grant. -func ServedReadGrants(keys []string) ([]string, error) { - var out []string - seen := map[string]bool{} - for _, k := range keys { - parts := strings.Split(k, ".") - if len(parts) != 3 { - return nil, fmt.Errorf("%q is not ..", k) - } - for _, part := range parts { - if !safeSubject.MatchString(part) { - return nil, fmt.Errorf("%q cannot be part of a subject: a permission is a subject pattern, and this would widen it", part) - } - } - for _, g := range readGrants(ResolvedBucket, k) { - if !seen[g] { - seen[g] = true - out = append(out, g) - } - } - } - return out, nil -} diff --git a/go/buscompose/streams.go b/go/buscompose/streams.go index 3544acd..637793d 100644 --- a/go/buscompose/streams.go +++ b/go/buscompose/streams.go @@ -1,23 +1,9 @@ package buscompose import ( - "fmt" - "sort" "strings" ) -// The mesh's own streams. -// -// **These and no more** (novox/hq ADR 0116 task 1.4, as revised by ADR 0118; the two that keep what a -// consumer gave up on added for issue 330). An earlier -// reading had the controller create *every* stream at genesis, from a fixed set. That is only the -// mesh's own half: a seat's streams are created when the module declaring it is registered, and a -// module's durable consumers when it is assigned — neither of which has happened at genesis. What -// is here is the foundation, which exists before any module does. -// -// The controller is the only writer of stream definitions (design 25 §3). A module declares -// nothing about them and cannot reach the JetStream API to make one. - // Retention is how a stream decides what to keep, which is the whole of what distinguishes the // mesh's four relationships on the wire (design 29 §4). type Retention string @@ -117,31 +103,6 @@ func DeadLetterOf(subject string) (stream, consumer string, ok bool) { return stream, consumer, ok && stream != "" && consumer != "" && !strings.Contains(consumer, ".") } -// AgainSubject is where an event given up on is delivered again to the one consumer that gave it up, -// and to nobody else: `mesh.again..` and the original subject without its `mesh.`. Every -// consumer on EVENTS filters its own (AgainFilter), and a module's runtime reads the event's key from -// the tokens around `.event.`, so the handler sees the same key it saw the first time. -func AgainSubject(consumer, original string) string { - return againPrefix + consumer + "." + strings.TrimPrefix(original, "mesh.") -} - -// AgainFilter is the one consumer's share of the subjects events are delivered again on. -func AgainFilter(consumer string) string { return againPrefix + consumer + ".>" } - -// OriginalOfAgain is the subject an event delivered again was first published on; false for a subject -// that is not one delivered again. -func OriginalOfAgain(subject string) (string, bool) { - rest, found := strings.CutPrefix(subject, againPrefix) - if !found { - return "", false - } - _, original, ok := strings.Cut(rest, ".") - if !ok || original == "" { - return "", false - } - return "mesh." + original, true -} - // MeshStreams is the foundation set, in the order a person reads it. // // **EVENTS filters on the `event` token**, which is the reason that token exists. A module's @@ -223,80 +184,6 @@ const ProvAppliedStream = "PROV_APPLIED" // all (ProvisionerAppliedSubject). const ProvAppliedSubjects = "prov.applied.>" -// DeliverSubjectFor is where a push consumer's messages land. -// -// **Per consumer, which means per stream as well as per name** (novox/hq 04-ISSUES/146). A push -// consumer delivers onto an ordinary subject, and everything subscribed to that subject gets a -// copy. The controller holds a consumer called `controller` on CONTROL and another called -// `controller` on EVENTS; while both were given `_DELIVER.controller`, the one process holding -// both subscriptions acted on every message twice — a joining machine was enrolled twice from one -// request, and the second enrolment minted a credential that replaced the one the machine had just -// been handed. Every report and every followed event doubled the same way, silently: nothing is -// redelivered, no count is wrong, the work simply happens twice. -// -// The stream belongs in it because the pair is what identifies a consumer — the server scopes a -// durable's name to its stream, and this subject was the one place that scoping was dropped. It -// stays inside what a controller may already subscribe (`_DELIVER.controller.>`). -func DeliverSubjectFor(c Consumer) string { - if c.DeliverSubject != "" { - return c.DeliverSubject - } - return "_DELIVER." + c.Name + "." + c.Stream -} - -// An Asserter is the part of a JetStream connection stream assertion needs. Narrow on purpose: it -// keeps this testable without a server, and keeps the client library out of everything that only -// wants to know what the streams are. -type Asserter interface { - // EnsureStream creates the stream if absent and updates it to match if present. It must be - // idempotent: the controller asserts on every start, not only at genesis. - EnsureStream(s Stream) error -} - -// AssertMeshStreams brings the foundation set into being, in order, and says which one failed -// rather than that something did. -// -// Asserted on every start rather than created once at genesis, because a stream that was deleted, -// or a mesh raised from a restored backup, must converge rather than run without the guarantee -// its messages assume. Idempotence is the whole requirement. -func AssertMeshStreams(a Asserter) error { - for _, s := range MeshStreams() { - if err := a.EnsureStream(s); err != nil { - return fmt.Errorf("asserting stream %s: %w", s.Name, err) - } - } - return nil -} - -// Overlaps reports subject filters claimed by more than one stream. -// -// **Corrected against the server**: an earlier version of this comment said NATS accepts -// overlapping streams and stores the message twice. It does not — it refuses the second stream -// with "subjects overlap with an existing stream" (verified against nats-server 2.10). The check -// still earns its place, for a different reason: the server's refusal arrives when the controller -// is applying, naming one stream, at a moment when the mesh is half-configured. This one arrives -// where the set is written, names both, and cannot reach a running mesh. -// -// It also decides a design question. Because overlap is refused rather than merged, a shared -// EVENTS stream and a per-module stream cannot coexist — the module's would be refused — so -// "one stream for most, its own for a module that wants different retention" is not an option -// the server allows. It is all of one or all of the other. -func Overlaps() []string { - seen := map[string]string{} - var clashes []string - for _, s := range MeshStreams() { - for _, subject := range s.Subjects { - if first, ok := seen[subject]; ok { - clashes = append(clashes, fmt.Sprintf("%s and %s both claim %s", first, s.Name, subject)) - continue - } - seen[subject] = s.Name - } - } - sort.Strings(clashes) - return clashes -} - // The ended controller's consumers' names (novox/hq ADR 0590 clause 6): kept only so its leftovers are named for their // removal (ControllerLeftoverConsumers) and the watchman can read whether `controller` on DEAD_LETTER_NOTICES still // stands. @@ -305,50 +192,5 @@ const ( MergesConsumerName = "controller-merges" ) -// AsksSeat is the seat an ask is made on and its warrant heard from (novox/hq ADR 0259): the router's. -const AsksSeat = "operator-channel" - -// The provider standing events, by their local names. Written here as well as in the catalogue -// (catalogue.ProvisionerEvents), which this package cannot import; a test keeps them agreeing. -const ( - ProvisionerFailing = "provisioner.failing" - ProvisionerRecovered = "provisioner.recovered" - ProvisionerRetirement = "provisioner.retirement" -) - -// moduleEventSubject is where one module's event lands. The same derivation PermissionsFor uses, so -// what the controller subscribes and what the emitter is permitted to publish cannot drift apart. -func moduleEventSubject(module, event string) string { - return "mesh.mod." + module + ".event." + event -} - -// seatEventSubject is where a role's own event lands, derived the same way a holder's permission is. -func seatEventSubject(seat, verb string) string { - return "mesh.seat." + seat + ".event." + verb -} - // EventsStream holds every module's and every role's events, a build's log among them. const EventsStream = "EVENTS" - -// MeshConsumers are the mesh's own durable consumers, beside those derived from declarations: none since the controller -// ended (novox/hq ADR 0590), whose consumers they were. -func MeshConsumers() []Consumer { return nil } - -// Ensurer is the part of a JetStream connection consumer assertion needs, narrow for the reason -// Asserter is. -type Ensurer interface { - EnsureConsumer(c Consumer) error -} - -// AssertMeshConsumers brings the mesh's own consumers into being, and says which one failed. -// -// After the streams, necessarily: a consumer on a stream that does not exist is refused, and the -// refusal names the stream rather than the order. -func AssertMeshConsumers(e Ensurer) error { - for _, c := range MeshConsumers() { - if err := e.EnsureConsumer(c); err != nil { - return fmt.Errorf("asserting consumer %s on %s: %w", c.Name, c.Stream, err) - } - } - return nil -} diff --git a/go/buscompose/testdata/composed.conf b/go/buscompose/testdata/composed.conf deleted file mode 100644 index 0fe0bb6..0000000 --- a/go/buscompose/testdata/composed.conf +++ /dev/null @@ -1,73 +0,0 @@ -# Composed by the mesh. Do not edit: the next composition overwrites it. -# Accounts and permissions are derived from what each module declares and nothing -# else (novox/hq ADR 0043, design 29 §2). - -port: 4222 -http: 127.0.0.1:8222 - -tls { - cert_file: "/tls/tls.crt" - key_file: "/tls/tls.key" - ca_file: "/tls/ca.crt" -} - -jetstream { - store_dir: "/data" -} - -# The mesh's users, composed by the grants part. Do not edit: the next -# composition overwrites it. Permissions are derived from what each module -# declares and nothing else (novox/hq ADR 0043, design 29 §2). - -accounts { - MESH { - jetstream: enabled - imports = [ - { stream: { account: "$SYS", subject: "$SYS.ACCOUNT.MESH.CONNECT" }, to: "mesh.bus.connect" } - { stream: { account: "$SYS", subject: "$SYS.ACCOUNT.MESH.DISCONNECT" }, to: "mesh.bus.disconnect" } - { service: { account: "$SYS", subject: "$SYS.REQ.ACCOUNT.MESH.CONNZ" }, to: "mesh.bus.connz" } - ] - users = [ - { user: "enrol.one", password: "$2a$11$eeeeeeeeeeeeeeeeeeeeee", permissions: { - publish: { allow: ["mesh.seat.mesh-enrolment.call.redeem.enrol~one"] } - subscribe: { allow: ["_INBOX.enrol.one.>"] } - } } - { 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_manifests.>", "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.>", "$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_manifests.>", "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.>", "$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_manifests.>", "$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.>", "$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_manifests", "$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules", "$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_manifests.>", "$JS.FC.KV_mesh-delivery-indexer_modules.>", "$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.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.>", "$SRV.PING.node-tools.one", "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"] } - allow_responses: { max: 1, ttl: "1m" } - } } - { user: "one.mesh-delivery-watchman", password: "$2a$11$wwwwwwwwwwwwwwwwwwwwww", permissions: { - publish: { allow: ["$JS.ACK.DEAD_LETTER_NOTICES.mesh-delivery-watchman.>", "$JS.ACK.EVENTS.one_mesh-delivery-watchman.>", "$JS.API.CONSUMER.CREATE.DEAD_LETTER_NOTICES.mesh-delivery-watchman", "$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.one.mesh-delivery-watchman", "$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.>", "$JS.API.CONSUMER.INFO.DEAD_LETTER_NOTICES.controller", "$JS.API.CONSUMER.INFO.DEAD_LETTER_NOTICES.mesh-delivery-watchman", "$JS.API.CONSUMER.INFO.EVENTS.one_mesh-delivery-watchman", "$JS.API.CONSUMER.MSG.NEXT.DEAD_LETTER_NOTICES.mesh-delivery-watchman", "$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_mesh-delivery-watchman", "$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.one.mesh-delivery-watchman", "$JS.API.DIRECT.GET.PROV_APPLIED.>", "$JS.API.STREAM.INFO.DEAD_LETTERS", "$JS.API.STREAM.INFO.DEAD_LETTER_NOTICES", "$JS.API.STREAM.INFO.KV_mesh-grants_memberships", "$JS.API.STREAM.INFO.PROV_APPLIED", "$JS.API.STREAM.MSG.GET.DEAD_LETTERS", "$JS.API.STREAM.MSG.GET.PROV_APPLIED", "$JS.FC.KV_mesh-grants_memberships.>", "mesh.bus.connz", "mesh.events.dead.>"] } - subscribe: { allow: ["$JS.EVENT.ADVISORY.CONSUMER.DELETED.>", "$SRV.INFO", "$SRV.INFO.mesh-delivery-watchman", "$SRV.INFO.mesh-delivery-watchman.>", "$SRV.PING", "$SRV.PING.mesh-delivery-watchman", "$SRV.PING.mesh-delivery-watchman.>", "$SRV.STATS", "$SRV.STATS.mesh-delivery-watchman", "$SRV.STATS.mesh-delivery-watchman.>", "_INBOX.one.mesh-delivery-watchman.>", "mesh.bus.connect", "mesh.bus.disconnect", "mesh.control.*.tools-alive", "mesh.events.dead.>", "mesh.mod.*.event.provisioner.failing", "mesh.mod.*.event.provisioner.recovered", "mesh.mod.mesh-delivery-watchman.call.>", "mesh.mod.mesh-delivery-watchman.tool.>", "mesh.mod.nats.event.bus.connection-closed", "mesh.mod.nats.event.bus.slow-consumer", "mesh.mod.power.event.*", "mesh.seat.mesh-self-check.event.checked", "prov.applied.>"] } - allow_responses: { max: 1, ttl: "1m" } - } } - { user: "one.nats", password: "$2a$11$bbbbbbbbbbbbbbbbbbbbbb", permissions: { - publish: { allow: ["$JS.API.STREAM.INFO.*", "$JS.API.STREAM.NAMES", "$JS.API.STREAM.SNAPSHOT.*", "$JS.SNAPSHOT.ACK.>"] } - subscribe: { allow: ["_INBOX.one.nats.>"] } - } } - { user: "one.telegram", password: "$2a$11$tttttttttttttttttttttt", permissions: { - publish: { allow: ["$JS.ACK.EVENTS.one_telegram.>", "$JS.ACK.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker.>", "$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.one.telegram", "$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.>", "$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.>", "mesh.seat.telegram-sender.event.delivered", "mesh.seat.telegram-sender.event.failed"] } - subscribe: { allow: ["$SRV.INFO", "$SRV.INFO.telegram", "$SRV.INFO.telegram.>", "$SRV.PING", "$SRV.PING.telegram", "$SRV.PING.telegram.>", "$SRV.STATS", "$SRV.STATS.telegram", "$SRV.STATS.telegram.>", "_INBOX.one.telegram.>", "mesh.mod.telegram.call.>", "mesh.mod.telegram.tool.>", "mesh.seat.telegram-sender.accept.send"] } - allow_responses: { max: 1, ttl: "1m" } - } } - { user: "two.audit", password: "$2a$11$aaaaaaaaaaaaaaaaaaaaaa", permissions: { - publish: { allow: ["$JS.ACK.EVENTS.two_audit.>", "$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.two.audit", "$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.>", "$JS.API.CONSUMER.INFO.EVENTS.two_audit", "$JS.API.CONSUMER.MSG.NEXT.EVENTS.two_audit", "$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.two.audit", "$JS.API.STREAM.INFO.KV_mesh-grants_memberships", "$JS.FC.KV_mesh-grants_memberships.>"] } - subscribe: { allow: ["$SRV.INFO", "$SRV.INFO.audit", "$SRV.INFO.audit.>", "$SRV.PING", "$SRV.PING.audit", "$SRV.PING.audit.>", "$SRV.STATS", "$SRV.STATS.audit", "$SRV.STATS.audit.>", "_INBOX.two.audit.>", "mesh.mod.audit.call.>", "mesh.mod.audit.tool.>", "mesh.mod.shop.event.order.placed"] } - allow_responses: { max: 1, ttl: "1m" } - } } - { user: "two.shop", password: "$2a$11$ssssssssssssssssssssss", permissions: { - publish: { allow: ["$JS.ACK.EVENTS.two_shop.>", "$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.two.shop", "$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.>", "$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.>", "mesh.mod.shop.event.order.placed", "mesh.seat.telegram-sender.accept.send"] } - subscribe: { allow: ["$SRV.INFO", "$SRV.INFO.shop", "$SRV.INFO.shop.>", "$SRV.PING", "$SRV.PING.shop", "$SRV.PING.shop.>", "$SRV.STATS", "$SRV.STATS.shop", "$SRV.STATS.shop.>", "_INBOX.two.shop.>", "mesh.mod.shop.call.>", "mesh.mod.shop.tool.>"] } - allow_responses: { max: 1, ttl: "1m" } - } } - ] - } - "$SYS" { - exports = [ - { stream: "$SYS.ACCOUNT.MESH.CONNECT", accounts: ["MESH"] } - { stream: "$SYS.ACCOUNT.MESH.DISCONNECT", accounts: ["MESH"] } - { service: "$SYS.REQ.ACCOUNT.MESH.CONNZ", accounts: ["MESH"] } - ] - } -} diff --git a/go/buscompose/testdata/controller-credentials.json b/go/buscompose/testdata/controller-credentials.json deleted file mode 100644 index ab11cf1..0000000 --- a/go/buscompose/testdata/controller-credentials.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "how": "Written by mesh-controller 0fbb5ef's own issueWith, sealProvisionerClient and heldProcessAccount (cmd/mesh-controller), run unchanged in a scratch integration test against a local store with the default seats seeded, on 2026-10-27 (issue 774): each row is the exact value the controller handed the vault. Never edit a row by hand: capture it again.", - "rows": [ - { - "name": "module", - "held": "{\"url\":\"nats://10.0.0.1:4222\",\"fingerprint\":\"ab:cd\",\"node\":\"laptop\",\"module\":\"cap-bot\",\"user\":\"laptop.cap-bot\",\"password\":\"pw-module\",\"claims\":[{\"seat\":\"mesh-vault\",\"scope\":\"mesh\",\"serves\":[\"get\",\"accept\",\"secret-ask\",\"issued\",\"rotate\",\"move-in\",\"node-key\",\"own\",\"move-asks\"]},{\"seat\":\"node-service-manager\",\"scope\":\"node\",\"serves\":[\"status\"]},{\"seat\":\"mesh-controller\",\"scope\":\"mesh\",\"serves\":[\"tools\",\"calls\",\"status\",\"nodes\",\"node\",\"placement-export\",\"modules\",\"seats\",\"builds\",\"plans\",\"delivery-plan\",\"delivery-order\",\"delivery-check\",\"deliver\",\"delivery-stop\",\"delivery-backups-again\",\"delivery-walks\",\"plan\",\"assign\",\"unassign\",\"pin\",\"unpin\",\"push\",\"rotate\",\"give\",\"issued\",\"secret-move\",\"secret-ask\",\"issue\",\"token\",\"settings\",\"command\",\"queue\",\"cancel\",\"clear\",\"replay\",\"try\",\"kill\",\"pause\",\"resume\",\"hand-act\",\"drill\",\"warranted\",\"hand-acts\",\"durations\",\"conditions\",\"healers\",\"doctor\",\"upgrade\",\"bus\",\"retire\",\"cleanup\",\"judge-directories\",\"root-free\",\"dead-letters\",\"data\",\"build\",\"artifacts\",\"held\",\"collect\",\"move-placement\",\"move-config\",\"mirrors\",\"images\"]},{\"seat\":\"no-such-seat\",\"scope\":\"node\"}]}" - }, - { - "name": "module-no-claims-no-fingerprint", - "held": "{\"url\":\"nats://10.0.0.1:4222\",\"node\":\"laptop\",\"module\":\"cap-plain\",\"user\":\"laptop.cap-plain\",\"password\":\"pw-plain\"}" - }, - { - "name": "provisioner", - "held": "{\"url\":\"nats://10.0.0.1:4222\",\"fingerprint\":\"ab:cd\",\"node\":\"laptop\",\"user\":\"provisioner.laptop\",\"password\":\"pw-prov\"}" - }, - { - "name": "process", - "held": "{\"url\":\"nats://10.0.0.1:4222\",\"fingerprint\":\"ab:cd\",\"node\":\"laptop\",\"module\":\"mesh-mail\",\"user\":\"laptop.mesh-mail.send\",\"password\":\"pw-proc\"}" - } - ] -} diff --git a/go/buscompose/testdata/user-lists.json b/go/buscompose/testdata/user-lists.json deleted file mode 100644 index 4851ccc..0000000 --- a/go/buscompose/testdata/user-lists.json +++ /dev/null @@ -1,289 +0,0 @@ -[ - { - "Name": "empty", - "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 ]\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": {} - }, - { - "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_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\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_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\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_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\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_manifests\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules\", \"$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_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\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.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.\u003e\", \"$SRV.PING.node-tools.one\", \"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_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\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_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\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_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\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_manifests\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules\", \"$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_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\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.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.\u003e\", \"$SRV.PING.node-tools.two\", \"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", - "Missing": null, - "Err": "", - "Memberships": { - "one.telegram": { - "node": "one", - "module": "telegram", - "serves": [ - { - "subject": "mesh.mod.telegram.tool.{tool}.one" - }, - { - "subject": "mesh.mod.telegram.tool.{tool}", - "queue": "serve.telegram" - } - ], - "emits": "mesh.mod.telegram.event.{event}", - "tools": "mesh.mod.telegram.tool.tools" - }, - "two.shop": { - "node": "two", - "module": "shop", - "serves": [ - { - "subject": "mesh.mod.shop.tool.{tool}.two" - }, - { - "subject": "mesh.mod.shop.tool.{tool}", - "queue": "serve.shop" - } - ], - "emits": "mesh.mod.shop.event.{event}", - "tools": "mesh.mod.shop.tool.tools" - } - } - }, - { - "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_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\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_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\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_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\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_manifests\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules\", \"$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_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\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.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.\u003e\", \"$SRV.PING.node-tools.one\", \"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_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\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_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\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_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\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_manifests\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules\", \"$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_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\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.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.\u003e\", \"$SRV.PING.node-tools.two\", \"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", - "Missing": [ - "two.audit" - ], - "Err": "", - "Memberships": { - "one.mesh-grants": { - "node": "one", - "module": "mesh-grants", - "serves": [ - { - "subject": "mesh.mod.mesh-grants.tool.{tool}.one" - }, - { - "subject": "mesh.mod.mesh-grants.tool.{tool}", - "queue": "serve.mesh-grants" - } - ], - "emits": "mesh.mod.mesh-grants.event.{event}", - "tools": "mesh.mod.mesh-grants.tool.tools" - }, - "one.mesh-watchman": { - "node": "one", - "module": "mesh-watchman", - "serves": [ - { - "subject": "mesh.mod.mesh-watchman.tool.{tool}.one" - }, - { - "subject": "mesh.mod.mesh-watchman.tool.{tool}", - "queue": "serve.mesh-watchman" - } - ], - "emits": "mesh.mod.mesh-watchman.event.{event}", - "tools": "mesh.mod.mesh-watchman.tool.tools" - }, - "one.nats": { - "node": "one", - "module": "nats", - "serves": [ - { - "subject": "mesh.mod.nats.tool.{tool}.one" - }, - { - "subject": "mesh.mod.nats.tool.{tool}", - "queue": "serve.nats" - } - ], - "emits": "mesh.mod.nats.event.{event}", - "tools": "mesh.mod.nats.tool.tools" - }, - "one.telegram": { - "node": "one", - "module": "telegram", - "serves": [ - { - "subject": "mesh.mod.telegram.tool.{tool}.one" - }, - { - "subject": "mesh.mod.telegram.tool.{tool}", - "queue": "serve.telegram" - } - ], - "emits": "mesh.mod.telegram.event.{event}", - "tools": "mesh.mod.telegram.tool.tools", - "seat-traffic": { - "publish": [ - "mesh.seat.telegram-sender.event.delivered", - "mesh.seat.telegram-sender.event.failed" - ] - } - }, - "two.audit": { - "node": "two", - "module": "audit", - "serves": [ - { - "subject": "mesh.mod.audit.tool.{tool}.two" - }, - { - "subject": "mesh.mod.audit.tool.{tool}", - "queue": "serve.audit" - } - ], - "emits": "mesh.mod.audit.event.{event}", - "tools": "mesh.mod.audit.tool.tools" - }, - "two.shop": { - "node": "two", - "module": "shop", - "serves": [ - { - "subject": "mesh.mod.shop.tool.{tool}.two" - }, - { - "subject": "mesh.mod.shop.tool.{tool}", - "queue": "serve.shop" - } - ], - "emits": "mesh.mod.shop.event.{event}", - "tools": "mesh.mod.shop.tool.tools" - } - } - }, - { - "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_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\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_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\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_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\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_manifests\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules\", \"$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_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\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.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.\u003e\", \"$SRV.PING.node-tools.ace\", \"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_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\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_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\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_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\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_manifests\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules\", \"$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_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\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.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.\u003e\", \"$SRV.PING.node-tools.novox\", \"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", - "Missing": null, - "Err": "", - "Memberships": { - "ace.plex": { - "node": "ace", - "module": "plex", - "serves": [ - { - "subject": "mesh.mod.plex.tool.{tool}.ace" - }, - { - "subject": "mesh.mod.plex.tool.{tool}", - "queue": "serve.plex" - } - ], - "emits": "mesh.mod.plex.event.{event}", - "tools": "mesh.mod.plex.tool.tools" - }, - "ace.silent": { - "node": "ace", - "module": "silent", - "serves": [ - { - "subject": "mesh.mod.silent.tool.{tool}.ace" - }, - { - "subject": "mesh.mod.silent.tool.{tool}", - "queue": "serve.silent" - } - ], - "emits": "mesh.mod.silent.event.{event}", - "tools": "mesh.mod.silent.tool.tools" - }, - "novox.mesh-delivery-builder": { - "node": "novox", - "module": "mesh-delivery-builder", - "serves": [ - { - "subject": "mesh.mod.mesh-delivery-builder.tool.{tool}.novox" - }, - { - "subject": "mesh.mod.mesh-delivery-builder.tool.{tool}", - "queue": "serve.mesh-delivery-builder" - } - ], - "emits": "mesh.mod.mesh-delivery-builder.event.{event}", - "reaches": { - "gitea.gitea_commit": [ - "mesh.mod.gitea.tool.gitea_commit" - ] - }, - "tools": "mesh.mod.mesh-delivery-builder.tool.tools", - "state": [ - { - "name": "steps", - "bucket": "mesh-delivery-builder_steps", - "writes": true - }, - { - "name": "presence", - "bucket": "mesh-delivery-builder_presence", - "writes": true, - "key": "novox" - } - ], - "logs": [ - { - "name": "builds", - "stream": "LOG_mesh-delivery-builder_builds", - "subject": "mesh.log.mesh-delivery-builder.builds", - "writes": true - } - ], - "streams": [ - { - "stream": "asks", - "events": [ - "ask" - ], - "subject": "mesh.stream.mesh-delivery-builder.event.{event}" - } - ] - }, - "novox.mesh-delivery-view": { - "node": "novox", - "module": "mesh-delivery-view", - "serves": [ - { - "subject": "mesh.mod.mesh-delivery-view.tool.{tool}.novox" - }, - { - "subject": "mesh.mod.mesh-delivery-view.tool.{tool}", - "queue": "serve.mesh-delivery-view" - } - ], - "emits": "mesh.mod.mesh-delivery-view.event.{event}", - "tools": "mesh.mod.mesh-delivery-view.tool.tools", - "state": [ - { - "name": "mesh-delivery-builder.steps", - "bucket": "mesh-delivery-builder_steps" - }, - { - "name": "mesh-delivery-builder.presence", - "bucket": "mesh-delivery-builder_presence", - "key": "novox" - } - ] - }, - "novox.mesh-grants": { - "node": "novox", - "module": "mesh-grants", - "serves": [ - { - "subject": "mesh.mod.mesh-grants.tool.{tool}.novox" - }, - { - "subject": "mesh.mod.mesh-grants.tool.{tool}", - "queue": "serve.mesh-grants" - } - ], - "emits": "mesh.mod.mesh-grants.event.{event}", - "tools": "mesh.mod.mesh-grants.tool.tools", - "state": [ - { - "name": "assertions", - "bucket": "mesh-grants_assertions", - "writes": true - } - ] - } - } - } -] diff --git a/go/buscompose/userlist_switch.go b/go/buscompose/userlist_switch.go deleted file mode 100644 index 11ea7b9..0000000 --- a/go/buscompose/userlist_switch.go +++ /dev/null @@ -1,174 +0,0 @@ -package buscompose - -import ( - "slices" - "strings" - "time" - - "git.novox.be/novox/mesh-sdk/go/announce" - "git.novox.be/novox/mesh-sdk/go/delivery" - "git.novox.be/novox/mesh-sdk/go/health" -) - -// **The grants part's user list, sealed to the bus node** (novox/hq ADR 0547 clause 7, issue 1945). The grants part writes -// the list it composed as one object of its own object bucket, sealed to the key of the node holding mesh-broker, and -// names it in one key of its own state `user-list`. The node-engine of that node follows the key when the switch says -// so (clause 8), opens the object and writes the bus's user-list file. -// -// Who may do what, and nothing more: -// -// - the grants part writes its object bucket (create, put, purge of an object's old chunks, delete) and its own state -// as before; -// - the node-engine of the node holding mesh-broker reads that one key of the state, watched, and an object of the -// bucket by its name: never the bucket's list of names, never a write, never another key of the grants part's state; -// - no other principal touches either. - -// GrantsUserListBucket, GrantsUserListKey, GrantsUserListObjects and GrantsUserListObjectStream are delivery's names, -// here where the grants are composed. -const ( - GrantsUserListBucket = delivery.GrantsUserListBucket - GrantsUserListKey = delivery.GrantsUserListKey - GrantsUserListObjects = delivery.GrantsUserListObjects - GrantsUserListObjectStream = delivery.GrantsUserListObjectStream -) - -// GrantsUserListFollows is what the node-engine of the node holding mesh-broker may publish to follow the grants part's -// list: the one key `user-list.sealed` read and watched (its own ordered consumer, filtered on that key alone), and an -// object of the bucket by its name, as a node reads its declaration's files (ADR 0486 §2: never STREAM.INFO of the object -// stream, which lists every name, and never STREAM.MSG.GET). -func GrantsUserListFollows() []string { - kv := "KV_" + GrantsUserListBucket - key := "$KV." + GrantsUserListBucket + "." + GrantsUserListKey - obj, s := GrantsUserListObjects, GrantsUserListObjectStream - return []string{ - "$JS.API.DIRECT.GET." + kv + "." + key, - "$JS.API.CONSUMER.CREATE." + kv + ".*." + key, - "$JS.API.CONSUMER.DELETE." + kv + ".*", - "$JS.API.STREAM.INFO." + kv, - "$JS.FC." + kv + ".>", - "$JS.API.DIRECT.GET." + s + ".$O." + obj + ".M.*", - "$JS.API.CONSUMER.CREATE." + s + ".*.$O." + obj + ".C.*", - "$JS.API.CONSUMER.DELETE." + s + ".*", - "$JS.FC." + s + ".>", - } -} - -// GrantsUserListWrites is what the grants part may publish to keep its object bucket: create it and read its info back, -// put an object (its chunks and its metadata, the old chunks of a name purged), read an object's metadata, and delete -// one. Its own state `user-list` it writes already, as every module writes its own. -func GrantsUserListWrites() []string { - obj, s := GrantsUserListObjects, GrantsUserListObjectStream - return []string{ - "$JS.API.STREAM.CREATE." + s, - "$JS.API.STREAM.INFO." + s, - "$JS.API.STREAM.PURGE." + s, - "$JS.API.DIRECT.GET." + s + ".>", - "$JS.API.STREAM.MSG.GET." + s, - "$O." + obj + ".>", - } -} - -// grantsOwnObjects is the grants part writing its own object bucket, and nothing of another. -func grantsOwnObjects(p Principal, pattern string) bool { - return p.Kind == KindModule && p.Module == GrantsModule && - (pattern == "$O."+GrantsUserListObjects+".>" || pattern == "$JS.API.STREAM.CREATE."+GrantsUserListObjectStream) -} - -// **Named readers** (novox/hq ADR 0547 clause 9, progressive insight of 2026-10-26; issue 1945). A state named here is -// read by the modules named and by no other, whatever another module's `reads` says: the vault's `bus-accounts` holds -// every bus account's bcrypt hash, and from take-over the grants part alone may read it. A module's announcement may name -// readers too (announce.State.Readers); both are honoured. -const VaultBusAccounts = "mesh-vault.bus-accounts" - -// ArtifactStoreTools is the module holding the seat mesh-artifact-sweep (novox/hq ADR 0571 clause 2). -const ArtifactStoreTools = "artifact-store-tools" - -// ConfigModule is the module that holds the seat mesh-config, a reader of the controller's lease (NamedReaders). -const ConfigModule = "mesh-config" - -// NamedReaders are the states only the modules named may read, by `.`: the grants part's wanted accounts -// are read by the vault alone (novox/hq ADR 0561 §2); nobody reads the healers' `would` (ADR 0564 clause 8, -// health_grants.go). -var NamedReaders = map[string][]string{VaultBusAccounts: {GrantsModule}, GrantsAccounts: {VaultSeat}, HealersWould: {}, - // ADR 0577 clause 1: the node-engine's data items are read by disk-load alone, each node's its own node's keys - // (NodeKeyedReads); the engine announces nothing of its own, so its readers are named here. - delivery.EngineDataRead: {health.DiskLoadModule}, - // ADR 0583 §1 insight: every node's login is read by the grants part alone, each engine its own node's key through its - // machine principal (NodeLoginsReads), never through `reads`. - delivery.NodeLoginsRead: {GrantsModule}, - // ADR 0587 §2: the open enrolments are read by their holder and, as a named reader, by the grants part. - delivery.EnrolmentsRead: {GrantsModule}} - -// ReaderMay says whether module may read the state read (`.`) by the named readers: always, for a state -// that names none. -func ReaderMay(module, read string) bool { - readers, named := NamedReaders[read] - return !named || slices.Contains(readers, module) -} - -// readsNamed splits a module's reads by the named readers: those it may read, and those named for others. -func readsNamed(module string, reads []string) (granted, refused []string) { - for _, r := range reads { - if ReaderMay(module, strings.TrimSpace(r)) { - granted = append(granted, r) - } else { - refused = append(refused, r) - } - } - return granted, refused -} - -// withAnnouncedReads is d with its reads judged for d's module (ReadsAnnouncedBy) against what the owners announced: -// every read its manifest names, granted or refused by the caller, judged again here, so a caller that split them -// without the reader (the controller's own copy of a reader-less judgement, issue 774: the vault, the assigner, the -// grants part and the artifact store's tools refused the lease they are named for) cannot refuse a read the reader is -// named for, nor grant one named for another. With no announcements read it is withNamedReaders. -func withAnnouncedReads(d Declared, announced map[string]announce.States) Declared { - if announced == nil { - return withNamedReaders(d) - } - all := append(append([]string(nil), d.Reads...), d.ReadsRefused...) - if len(all) == 0 { - return d - } - d.Reads, d.ReadsRefused = ReadsAnnouncedBy(d.Module, all, announced) - return d -} - -// withNamedReaders is d with the reads named for other modules moved to its refused reads. -func withNamedReaders(d Declared) Declared { - granted, refused := readsNamed(d.Module, d.Reads) - if len(refused) == 0 { - return d - } - d.Reads = granted - d.ReadsRefused = append(append([]string(nil), d.ReadsRefused...), refused...) - return d -} - -// **The hearings the grants part withholds, on the bus** (issue 1955 L7, mesh-delivery e564eaf): a hearing of a restricted -// stream a module asks in the tool runner, withheld because only an account of its own may hear it (ADR 0376 §2), kept -// in the grants part's state `withheld`, one key `..` (the controller's condition id -// of it), written on change only and deleted once heard; the grants part's tools read it (D-heard was dropped on -// 2026-10-30, so no other module reads it). -const ( - GrantsWithheldState = "withheld" - GrantsWithheldBucket = GrantsModule + "_" + GrantsWithheldState - GrantsWithheldRead = GrantsModule + "." + GrantsWithheldState - WithheldVersion = 1 -) - -// WithheldHearing is one withheld hearing: the machine, the module, the restricted stream and its emitter, the plain -// words of it and the remedy (Withheld.Said), and when the grants part first wrote it as it stands. -type WithheldHearing struct { - V int `json:"v"` - Node string `json:"node"` - Module string `json:"module"` - Stream string `json:"stream"` - Emitter string `json:"emitter"` - Said string `json:"said"` - At time.Time `json:"at"` -} - -// WithheldKey is a withheld hearing's key: `..`. -func WithheldKey(w Withheld) string { return w.Module + "." + strings.ToLower(w.Stream) + "." + w.Node } diff --git a/go/buscompose/userlist_switch_integration_test.go b/go/buscompose/userlist_switch_integration_test.go deleted file mode 100644 index d8eb3fd..0000000 --- a/go/buscompose/userlist_switch_integration_test.go +++ /dev/null @@ -1,213 +0,0 @@ -//go:build integration - -package buscompose - -import ( - "bytes" - "context" - "encoding/base64" - "encoding/json" - "os" - "path/filepath" - "testing" - "time" - - "git.novox.be/novox/mesh-sdk/go/delivery" - "git.novox.be/novox/mesh-sdk/go/seal" - "github.com/nats-io/nats-server/v2/server" - "github.com/nats-io/nats.go" - "github.com/nats-io/nats.go/jetstream" -) - -// Defends novox/hq ADR 0547 clause 7 on a real server (issue 1945): with the list this composer writes, the grants part -// creates its object bucket and its state, puts the sealed list and names it in `user-list.sealed`; the node-engine of -// the node holding mesh-broker watches that one key, reads the object by its name and opens it with its key, and may -// neither list the bucket, nor read another key of the state, nor write; another node reads nothing of either. -func TestTheBusNodeFollowsTheGrantsPartsSealedListUnderTheComposedGrants(t *testing.T) { - records := Records{ - Nodes: []string{"novox", "ace"}, - Assigned: map[string][]Declared{ - "novox": {{Module: "nats", SnapshotsTheBus: true}, - {Module: GrantsModule, State: []Bucket{{Module: GrantsModule, Name: "user-list"}}}}, - "ace": {{Module: "plex"}}, - }, - } - users, err := Users(records) - if err != nil { - t.Fatal(err) - } - for i := range users { - users[i].PasswordHash = "pw-" + users[i].Username() - } - accounts, err := ComposeAccounts(users) - if err != nil { - t.Fatal(err) - } - dir := t.TempDir() - conf := filepath.Join(dir, "server.conf") - if err := os.WriteFile(conf, []byte("listen: 127.0.0.1:-1\njetstream { store_dir: \""+dir+"\" }\n"+accounts), 0o600); err != nil { - t.Fatal(err) - } - opts, err := server.ProcessConfigFile(conf) - if err != nil { - t.Fatalf("the composed list is not the server's: %v", err) - } - opts.NoLog, opts.NoSigs = true, true - s, err := server.NewServer(opts) - if err != nil { - t.Fatal(err) - } - go s.Start() - if !s.ReadyForConnections(30 * time.Second) { - t.Fatal("the bus did not come up") - } - t.Cleanup(s.Shutdown) - dial := func(user string) *nats.Conn { - nc, err := nats.Connect(s.ClientURL(), nats.UserInfo(user, "pw-"+user), nats.CustomInboxPrefix("_INBOX."+user)) - if err != nil { - t.Fatalf("%s: %v", user, err) - } - t.Cleanup(nc.Close) - return nc - } - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - // The grants part: its state and its object bucket, the sealed list put and named. - grants, err := jetstream.New(dial("novox." + GrantsModule)) - if err != nil { - t.Fatal(err) - } - // Its state is made as the grants part makes a declared state, by its stream's name (CreateKeyValue would ask the - // account's info, which no module is granted). - if _, err := grants.CreateStream(ctx, jetstream.StreamConfig{Name: "KV_" + GrantsUserListBucket, - Subjects: []string{"$KV." + GrantsUserListBucket + ".>"}, MaxMsgsPerSubject: 1, AllowDirect: true, - AllowRollup: true, DenyDelete: true, Discard: jetstream.DiscardNew}); err != nil { - t.Fatalf("the grants part's state: %v", err) - } - kv, err := grants.KeyValue(ctx, GrantsUserListBucket) - if err != nil { - t.Fatalf("the grants part binds its state: %v", err) - } - objects, err := grants.CreateObjectStore(ctx, jetstream.ObjectStoreConfig{Bucket: GrantsUserListObjects}) - if err != nil { - t.Fatalf("the grants part's object bucket: %v", err) - } - node, err := seal.GenerateKeyPair() - if err != nil { - t.Fatal(err) - } - list := []byte(accounts) - digest := delivery.UserListDigest(list) - object, err := delivery.SealUserList(list, node.PublicKey) - if err != nil { - t.Fatal(err) - } - name := delivery.UserListObjectName(digest) - for range 2 { // put twice: a second put of a name purges its old chunks - if _, err := objects.PutBytes(ctx, name, object); err != nil { - t.Fatalf("the grants part put its object: %v", err) - } - } - value, err := delivery.EncodeGrantsUserList(delivery.GrantsUserList{Follow: delivery.FollowGrants, Digest: digest, - Object: name, SealedTo: delivery.SealingKeyID(node.PublicKey), Node: "novox", Revision: 1, Users: len(users)}) - if err != nil { - t.Fatal(err) - } - if _, err := kv.Put(ctx, GrantsUserListKey, value); err != nil { - t.Fatal(err) - } - if _, err := kv.Put(ctx, "list", []byte(`{"v":1}`)); err != nil { - t.Fatal(err) - } - - // The bus node's engine: the key watched, the object by name (as mesh-host's link reads a declaration's file). - busNode := dial("node.novox") - js, err := busNode.JetStream(nats.MaxWait(5 * time.Second)) - if err != nil { - t.Fatal(err) - } - keys, err := js.KeyValue(GrantsUserListBucket) - if err != nil { - t.Fatalf("the bus node binds the grants part's state: %v", err) - } - w, err := keys.Watch(GrantsUserListKey) - if err != nil { - t.Fatalf("the bus node watches the key: %v", err) - } - defer w.Stop() - var got delivery.GrantsUserList - select { - case e := <-w.Updates(): - if e == nil { - t.Fatal("the watch found no value") - } - if got, err = delivery.DecodeGrantsUserList(e.Value()); err != nil { - t.Fatal(err) - } - case <-ctx.Done(): - t.Fatal("the watch said nothing") - } - read := func(js nats.JetStreamContext, name string) ([]byte, error) { - meta, err := js.GetLastMsg(GrantsUserListObjectStream, "$O."+GrantsUserListObjects+".M."+ - base64.URLEncoding.EncodeToString([]byte(name)), nats.DirectGet()) - if err != nil { - return nil, err - } - var info struct { - NUID string `json:"nuid"` - Size int `json:"size"` - } - if err := json.Unmarshal(meta.Data, &info); err != nil { - return nil, err - } - sub, err := js.SubscribeSync("$O."+GrantsUserListObjects+".C."+info.NUID, nats.OrderedConsumer(), - nats.BindStream(GrantsUserListObjectStream)) - if err != nil { - return nil, err - } - defer sub.Unsubscribe() - var body []byte - for len(body) < info.Size { - m, err := sub.NextMsg(5 * time.Second) - if err != nil { - return nil, err - } - body = append(body, m.Data...) - } - return body, nil - } - body, err := read(js, got.Object) - if err != nil { - t.Fatalf("the bus node reads the object by its name: %v", err) - } - opened, err := delivery.OpenUserList(body, got.Digest, func(b seal.Box) ([]byte, error) { - s, err := seal.Open(b, node.PrivateKey) - return []byte(s), err - }) - if err != nil || !bytes.Equal(opened, list) { - t.Fatalf("the bus node opened %d bytes, %v", len(opened), err) - } - - // And nothing beyond: no other key, no list of the bucket, no write. - if _, err := keys.Get("list"); err == nil { - t.Error("the bus node read another key of the grants part's state") - } - if _, err := js.StreamInfo(GrantsUserListObjectStream); err == nil { - t.Error("the bus node read the object stream's info, which lists every name") - } - if _, err := js.Publish("$O."+GrantsUserListObjects+".M.x", []byte("x")); err == nil { - t.Error("the bus node wrote the grants part's bucket") - } - // Another node reads nothing of it. - other, err := dial("node.ace").JetStream(nats.MaxWait(2 * time.Second)) - if err != nil { - t.Fatal(err) - } - if _, err := read(other, got.Object); err == nil { - t.Error("another node read the object") - } - if _, err := other.GetLastMsg("KV_"+GrantsUserListBucket, "$KV."+GrantsUserListBucket+"."+GrantsUserListKey, nats.DirectGet()); err == nil { - t.Errorf("another node read the key: %v", err) - } -} diff --git a/go/buscompose/users.go b/go/buscompose/users.go index ea1668f..0eba1c7 100644 --- a/go/buscompose/users.go +++ b/go/buscompose/users.go @@ -1,28 +1,5 @@ package buscompose -import ( - "fmt" - "slices" - "sort" - "strings" - - "git.novox.be/novox/mesh-sdk/go/announce" - "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" -) - -// Every user the composed file should contain, derived from what the mesh knows. -// -// **The list is derived, never kept.** A stored user list would be a second account of who may -// reach the bus, able to disagree with the records it came from — and the disagreement would be -// invisible, because both would look internally consistent. So this is a pure function of the -// mesh's records, run again every time the file is written. -// -// Records are mirrored into this package's own types rather than imported from the catalogue, for -// the reason DeclaredSeat is: composing authority is a different job from parsing a manifest, and -// this package stays free of the other's types so a change to a manifest field cannot quietly widen -// a permission. - // Declared is one module on one node, as composing its authority needs it. type Declared struct { Module string @@ -124,445 +101,5 @@ type ProcessPrincipal struct { Invokes []string } -// Records is what composing a user list needs to know about the mesh, and nothing more. -type Records struct { - // Nodes is every machine the mesh knows. Each gets a host user. - Nodes []string - // Assigned is the modules on each node, as they declare themselves. - Assigned map[string][]Declared - // Enrolling is every node with a live token — one enrolment user each, because the inbox an - // answer goes to is scoped to the token and a shared one is one machine reading another's - // sealed credentials (design 25 §6). The grants part fills it from the seat mesh-enrolment's keys at live or - // redeeming (novox/hq ADR 0587 §4), the controller from its store. - Enrolling []string - // People is each person's name against the tools they may invoke, `*` for an administrator. - People map[string][]string - // Interchangeable is each module whose definition says its instances are the same anywhere - // (ADR 0160), which decides whether the module's plain subject is issued to every instance. - Interchangeable map[string]bool - // RootFree is each machine judged root-free when this was composed (novox/hq ADR 0259 §8): it names an - // account agents run as, judged unable to become root by its node-engine, and serves no login shell - // execute. A machine absent is not free: no record of a pass is no pass. - RootFree map[string]bool - // Engine is the node-engine's own declaration, from its manifest (EngineModule): placed on every node though no node is - // assigned it, since every node runs it (novox/hq ADR 0563 §2, §3). Its state, logs and streams are created by the - // grants part as a placed module's are; it adds no principal, since each node's engine speaks as the node's account. - // Nil where its manifest was not read. - Engine *Declared - // NodeLogins is each node's key of the vault's node-logins (novox/hq ADR 0583 §1): the login in use and the one - // rotating, each composed as a user of the node's permissions. A node absent logs in as `node.` alone. - NodeLogins map[string]delivery.NodeLogin - // 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 -} - -// Users is every user the composed file should contain, in the order it will be written. -func Users(r Records) ([]Principal, error) { - var out []Principal - - served := servedReadsOf(r) - logs := DeclaredLogs(r) - grants := -1 - for _, node := range sortedCopy(r.Nodes) { - var checks []string - bus := false - for _, d := range r.Assigned[node] { - if d.SnapshotsTheBus { - bus = true - } - checks = append(checks, d.Checks...) - } - // **One user per login the node has** (novox/hq ADR 0583 §1): the one in use, and while it rotates the other too, - // each with the node's permissions. - for _, login := range nodeLoginsOf(r, node) { - out = append(out, Principal{Kind: KindNode, Node: node, Login: login, Checks: checks, - MovesIntoTheVault: servesMoveIn(r), StatesItsKeyToTheVault: servesVerb(r, VaultNodeKey), - AsksItsOwnSecrets: servesVerb(r, VaultOwn), AsksTheVaultToSign: servesVerb(r, VaultSign), Uses: asksSeatOf(r), - FollowsGrantsUserList: bus, ServedReads: servedOn(served, node), RotatesItsLogin: servesVerb(r, VaultRotateNode)}) - } - // **And the machine's provisioner client, on an account of its own** (novox/hq ADR 0349 clause 11): one - // per machine that runs it, delivered to the module running it. - for _, d := range r.Assigned[node] { - if d.ProvisionsNode { - out = append(out, Principal{Kind: KindProvisioner, Node: node, Module: d.Module, - BoundBuckets: boundBucketsOf(r)}) - break - } - } - // **Where the runtime is assigned, the machine gets one runtime principal in place of the - // runtime module's own** (novox/hq ADR 0175, to-be 38). It carries every module on the - // node: its serving grants are the union of theirs. Every other module keeps its own - // principal — a module still serving tools from its own container holds its own - // credential until it moves, and the two serve side by side in the meantime. - runtimeHere := false - for _, d := range r.Assigned[node] { - if d.Module == RuntimeModule { - runtimeHere = true - } - } - for _, d := range r.Assigned[node] { - // **The node-engine's manifest is no placed module's** (novox/hq ADR 0563 §2, §3; issue 774): where a placement - // names it, it adds no user, no process and nothing the runner carries; its grants are the node's account's. - if !Composed(d) { - continue - } - // A state with named readers is read by them alone (ADR 0547 clause 9), and a read is granted what its owner - // announced (ADR 0495 clause 3): judged here, for this reader, whatever split the caller made (issue 774). - d = withReadLogs(withAnnouncedReads(d, r.Announced), logs) - // **A process account is its own, delivered in its own secret** (broker-, novox/hq - // ADR 0436 §1, §2), never the module's `broker` — so it is composed whether or not the module - // itself has an account, and is never carried by the machine's runtime, which runs as the - // operator's account (ADR 0436 §5): added here, before either of those is judged. - for _, pr := range d.Processes { - out = append(out, Principal{ - Kind: KindProcess, Node: node, Module: d.Module, Process: pr.Process, - Emits: pr.Emits, Queues: pr.Queues, State: pr.State, Uses: d.Uses, Asks: pr.Asks, - Invokes: pr.Invokes, Removes: pr.Removes, - }) - // 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) - } - } - if runtimeHere && d.Module == RuntimeModule { - continue - } - // **A module with nowhere to read an account is no user** (novox/hq issue 195). `module - // issue` refuses it one (issue 078: an account nothing reads is an orphan), so its user - // could only ever be left out of the file for want of a password — and every such module - // was named, on every status and plan, as a credential the mesh had not minted. Where the - // runtime is, it speaks for the module; where it is not, the module cannot speak at all, - // and a user would not change that. - if d.NoAccount { - continue - } - out = append(out, Principal{ - Kind: KindModule, Node: node, Module: d.Module, - Emits: d.Emits, Consumes: d.Consumes, Serves: d.Serves, - Holds: d.Holds, Uses: d.Uses, Watches: d.Watches, Invokes: d.Invokes, - State: stateNames(d.State), Reads: d.Reads, ReadLogs: d.ReadLogs, SnapshotsTheBus: d.SnapshotsTheBus, - PerMachine: perMachineNames(d.State), KeyedReads: d.KeyedReads, - Logs: logNames(d.Logs), Streams: d.Streams, Hears: ComposedHears(d), Queues: d.Queues, - }) - // **The grants part creates what a manifest declares** (novox/hq ADR 0547 §5; the operator, 2026-10-26 - // 08:54): every placed module's buckets, logs and streams, each by its stream's name. - if d.Module == WatchmanModule { - out[len(out)-1].DeadLetterSources = deadLetterSourcesOf(r) - } - if d.Module == GrantsModule { - out[len(out)-1].CreatesStreams = declaredStreamsOf(r) - grants = len(out) - 1 - } - // **The vault's holder asks the desks and the issuing providers itself** (issue 723), under the - // account its own runtime runs on (ADR 0259 §8's runs-as): the one module granted it. - if holdsSeat(d, VaultSeat) { - out[len(out)-1].AsksTheDesk = true - out[len(out)-1].AsksIssued = issuedOf(r) - } - } - if runtimeHere { - var carried []Declared - for _, d := range r.Assigned[node] { - if CarriedByRunner(d) { - d = withReadLogs(withAnnouncedReads(d, r.Announced), logs) - // Never a restricted stream for what the tool runner carries (novox/hq ADR 0376 §1). - d.Hears = ComposedHears(d) - carried = append(carried, d) - } - } - out = append(out, Principal{Kind: KindNodeTools, Node: node, Module: RuntimeModule, Carries: carried}) - } - } - // A node enrols through the seat mesh-enrolment alone (ADR 0587 §6, ADR 0590): while no holder serves redeem, an - // enrolling node has nowhere to enrol, and gets no user. - if servesVerb(r, EnrolmentRedeem) { - for _, node := range sortedCopy(r.Enrolling) { - out = append(out, Principal{Kind: KindEnrolment, Node: node, Redeems: true}) - } - } - for _, person := range sortedNames(r.People) { - out = append(out, Principal{Kind: KindPerson, Module: person, Invokes: r.People[person]}) - } - - // The vault's enrolment verbs are mesh-enrolment's alone, however wide another's grant (novox/hq ADR 0587 §1). - if slices.ContainsFunc(VerbsTheEnrolmentAsksTheVault, func(v SeatVerb) bool { return servesVerb(r, v) }) { - for i := range out { - out[i].VaultEnrols = true - } - } - - // The grants part creates the durable consumers the accounts need (novox/hq ADR 0561 §6), by stream and name. - if grants >= 0 { - out[grants].CreatesConsumers = AccountConsumers(out) - } - - // Refused here rather than discovered by the server. Two users with one name is a file the - // server reads as one of them, and which one depends on the order — so a module assigned to a - // node twice, or a person named after nothing, is a composition that must not be written. - seen := map[string]string{} - for _, p := range out { - name := p.Username() - if name == "" || name == "." { - return nil, fmt.Errorf("a %s user has no name, so nothing could authenticate as it", p.Kind) - } - if first, already := seen[name]; already { - return nil, fmt.Errorf( - "two users would be called %q (a %s and a %s): the server would read the file as "+ - "one of them, and which one depends on the order", name, first, p.Kind) - } - seen[name] = string(p.Kind) - } - return out, nil -} - -// WithPasswords fills each user's hash from what the mesh minted, and says which users have none. -// -// **Separated from Users because they fail differently.** A user missing from the records is a bug -// in deriving them; a user with no password is a step that has not happened yet — a module assigned -// but never given a credential, a node enrolled before this existed. The second is ordinary and its -// remedy is to mint one, so it is named rather than returned as an error, and the caller decides -// whether a partial composition is worth writing. -func WithPasswords(principals []Principal, hashes map[string]string) (filled []Principal, missing []string) { - for _, p := range principals { - hash, ok := hashes[p.Username()] - if !ok || hash == "" { - missing = append(missing, p.Username()) - continue - } - p.PasswordHash = hash - filled = append(filled, p) - } - return filled, missing -} - -func sortedCopy(in []string) []string { - out := append([]string(nil), in...) - sort.Strings(out) - return out -} - -func sortedNames(in map[string][]string) []string { - out := make([]string, 0, len(in)) - for k := range in { - out = append(out, k) - } - sort.Strings(out) - return out -} - -// asksSeatOf is the seat an ask is made on, as its holder declares it (novox/hq ADR 0259): the controller -// asks the operator through it like any other user, and is granted what its declaration names for a caller. -// None while nothing holds it. -// issuedOf is every tool of a module assigned anywhere that answers a credential its software issued, as -// `.` (novox/hq ADR 0305): what the controller asks in its own name, and nothing beside. -func issuedOf(r Records) []string { - seen := map[string]bool{} - var out []string - for _, node := range sortedCopy(r.Nodes) { - for _, d := range r.Assigned[node] { - for _, t := range d.Issued { - if key := d.Module + "." + t; !seen[key] { - seen[key] = true - out = append(out, key) - } - } - } - } - sort.Strings(out) - return out -} - -func asksSeatOf(r Records) []Seat { - for _, node := range sortedCopy(r.Nodes) { - for _, d := range r.Assigned[node] { - for _, s := range d.Holds { - if s.Name == AsksSeat && namesVerb(s.ByCaller, "ask") { - seat := s - seat.Kind, seat.Capabilities = "", nil - return []Seat{seat} - } - } - } - } - return nil -} - -// nodeLoginsOf are the logins composed for node, as Principal.Login names them ("" for `node.`): the one its -// node-logins key says is in use and the one rotating, in that order; `node.` alone when the key is absent or does -// not hold (a key naming another node's login is no login of this node's). -func nodeLoginsOf(r Records, node string) []string { - l, ok := r.NodeLogins[node] - if !ok || l.Node != node || (l.Login != delivery.FirstLogin(node) && l.Login != delivery.SecondLogin(node)) { - return []string{""} - } - login := func(name string) string { - if name == delivery.FirstLogin(node) { - return "" - } - return name - } - out := []string{login(l.Login)} - if l.Rotates() && l.Rotating == delivery.OtherLogin(node, l.Login) { - out = append(out, login(l.Rotating)) - } - return out -} - // VaultSeat is the vault's seat: its holder alone asks the desks and the issuing providers in its own name. const VaultSeat = "mesh-vault" - -func holdsSeat(d Declared, seat string) bool { - for _, s := range d.Holds { - if s.Name == seat { - return true - } - } - return false -} - -// servedOf are the verbs of asked that a seat's holder serves, as the records say, by name. -func servedOf(r Records, asked []SeatVerb) []string { - seen := map[string]bool{} - var out []string - for _, node := range sortedCopy(r.Nodes) { - for _, d := range r.Assigned[node] { - for _, s := range d.Holds { - for _, v := range asked { - if s.Name == v.Seat && slices.Contains(s.Serves, v.Verb) && !seen[v.Verb] { - seen[v.Verb] = true - out = append(out, v.Verb) - } - } - } - } - } - sort.Strings(out) - return out -} - -// servesMoveIn says the vault's holder serves move-in, as the records say (issue 676): what lets each machine's -// node-engine ask it in its own name. -func servesMoveIn(r Records) bool { return servesVerb(r, VaultMoveIn) } - -// servesVerb says a holder of verb's seat serves it, as the records say. -func servesVerb(r Records, verb SeatVerb) bool { - for _, node := range r.Nodes { - for _, d := range r.Assigned[node] { - for _, s := range d.Holds { - if s.Name == verb.Seat && slices.Contains(s.Serves, verb.Verb) { - return true - } - } - } - } - return false -} - -// boundBucketsOf is every provider's bucket of entries, `_bound` (mesh-sdk go/provider, issue 678): the state -// named bound of every module placed anywhere, once each, in name order. A consumer on any machine may be bound to -// it, so each machine's provisioner client reads its own keys of each (issue 679). -func boundBucketsOf(r Records) []string { - seen := map[string]bool{} - for _, mods := range r.Assigned { - for _, d := range mods { - for _, b := range d.State { - if b.Name == BoundState { - seen[b.Bucket()] = true - } - } - } - } - out := make([]string, 0, len(seen)) - for b := range seen { - out = append(out, b) - } - sort.Strings(out) - return out -} - -// Composed says a placed module is composed as one: its own user, its processes and what the runner carries of it. -// The node-engine's manifest (EngineModule) is not, though a placement may name it: each node's engine speaks as the -// node's account, which alone writes the engine's state under its own key (engineGrants, novox/hq ADR 0563 §2, §3), and -// Records.Engine creates that state. Granted to another account it is a second writer, and the writers table refuses -// the whole list (issue 774: met for mesh-host_acts and then mesh-host_health, 2026-10-30). -func Composed(d Declared) bool { return d.Module != EngineModule } - -// CarriedByRunner says the machine's tool runner carries this placed module: a module composed at all (Composed) that -// runs as no account of its own and has something for the runner to serve (runnerServes). The one test of what the -// runner's grants are composed from. -func CarriedByRunner(d Declared) bool { return Composed(d) && d.RunsAs == "" && runnerServes(d) } - -// runnerServes says the machine's tool runner has something of this module to serve (novox/hq ADR 0436 §1, -// ADR 0437 §4, issue 1329): tools or loads it answers, a seat it holds, event families a session waits on, -// a bundle the runtime loads (Loads), or a check its health asks. A module with none of these is a process of its own on its own credential, and -// carrying it would hand every session on the machine the runner's account with that module's state, seat -// traffic and streams. A module with no account of its own (NoAccount) is spoken for by the runner, so it -// stays carried, and so does the runtime module itself. Whether the module has its own broker secret is not -// the test: a module with tools and its own credential is still carried (ADR 0175). -func runnerServes(d Declared) bool { - return d.Module == RuntimeModule || d.NoAccount || d.Loads || len(d.Serves) > 0 || len(d.Holds) > 0 || len(d.Waits) > 0 || len(d.Checks) > 0 -} - -// declaredStreamsOf is the stream of every bucket, log and stream a module placed anywhere declares, once each, in name -// order (novox/hq ADR 0547 §5, issue 1790): `KV__`, `LOG__` and each module stream's own -// name. What the grants part may create, and nothing beside. -func declaredStreamsOf(r Records) []string { - seen := map[string]bool{} - placed := [][]Declared{} - for _, mods := range r.Assigned { - placed = append(placed, mods) - } - // The node-engine's manifest is placed on every node (ADR 0563): its declared state is created as a placed module's. - if r.Engine != nil { - placed = append(placed, []Declared{*r.Engine}) - } - for _, mods := range placed { - for _, d := range mods { - for _, b := range d.State { - seen["KV_"+b.Bucket()] = true - } - for _, l := range d.Logs { - seen[l.Stream()] = true - } - for _, s := range d.Streams { - seen[s.Name] = true - } - } - } - out := make([]string, 0, len(seen)) - for s := range seen { - out = append(out, s) - } - sort.Strings(out) - return out -} - -// servedReadsOf is every key of mesh-config's resolved values a module assigned on a node serves from, -// `..`, sorted: each part a subject token, so a key that cannot be one is granted nothing (and the -// node composing a consumer of it refuses, naming it), never a wider read. -func servedReadsOf(r Records) []string { - var out []string - for node, declared := range r.Assigned { - for _, d := range declared { - for _, k := range d.Served { - if safeSubject.MatchString(node) && safeSubject.MatchString(d.Module) && safeSubject.MatchString(k) { - out = append(out, node+"."+d.Module+"."+k) - } - } - } - } - sort.Strings(out) - return slices.Compact(out) -} - -// servedOn is, of the served keys, those of other nodes than node: its own slice it reads whole. -func servedOn(served []string, node string) []string { - var out []string - for _, k := range served { - if !strings.HasPrefix(k, node+".") { - out = append(out, k) - } - } - return out -} diff --git a/go/buscompose/waits.go b/go/buscompose/waits.go index c92300e..b10e6c0 100644 --- a/go/buscompose/waits.go +++ b/go/buscompose/waits.go @@ -1,19 +1,5 @@ package buscompose -import ( - "fmt" - "strings" -) - -// What a session may wait on through the mesh MCP server (novox/hq ADR 0367). -// -// **A grant apart from `consumes`, and the tool runner's alone.** The tool runner's module names in its -// manifest the event families a session may wait on, one by one; the runtime's account is granted a plain -// subscription on those subjects and nothing wider — no durable consumer, no queue of its own — and its -// membership names the same list, from which `mesh_wait` refuses any other. Both are read from this one -// composition, so the grant and what the tool runner offers cannot disagree, and nothing but the manifest -// adds a family (§6). - // Wait is one event family the tool runner's module declares: its name as the manifest gives it, and the // emitter — a seat or a module — and the event, as the inventory resolved them. type Wait struct { @@ -30,65 +16,3 @@ type Wait struct { // on with it given, and refused without. Last string } - -// WaitSubject is where a family's events land. Each token is checked before it becomes a subject: a -// wildcard, a dot inside a name or an empty token would widen the grant. -func WaitSubject(w Wait) (string, error) { - if !safeSubject.MatchString(w.Emitter) { - return "", fmt.Errorf("%q cannot name an emitter of a waited event: it would widen the subject", w.Emitter) - } - for _, t := range strings.Split(w.Event, ".") { - if !safeSubject.MatchString(t) { - return "", fmt.Errorf("%q cannot name a waited event: each of its names is plain, never a wildcard "+ - "(novox/hq ADR 0367 §2)", w.Event) - } - } - if w.Last != "" && !strings.Contains(w.Event, ".") { - return "", fmt.Errorf("%s names no %s: %s's events are said for each %s, so a family is %s.%s.<%s>; "+ - "without it no event would ever be heard (novox/hq ADR 0367 §5)", - w.Name, w.Last, w.Emitter, w.Last, w.Emitter, w.Event, w.Last) - } - if w.Seat { - return "mesh.seat." + w.Emitter + ".event." + w.Event, nil - } - return "mesh.mod." + w.Emitter + ".event." + w.Event, nil -} - -// waitGrants is the subscription the tool runner's account is granted for its module's families, and an -// error for a carried module other than the tool runner's that declares any (ADR 0367 §2). -func waitGrants(carries []Declared) ([]string, error) { - var sub []string - for _, d := range carries { - if len(d.Waits) == 0 { - continue - } - if d.Module != RuntimeModule { - return nil, fmt.Errorf("%s declares what a session waits on; only %s, the tool runner's module, "+ - "does (novox/hq ADR 0367 §2)", d.Module, RuntimeModule) - } - for _, w := range d.Waits { - s, err := WaitSubject(w) - if err != nil { - return nil, err - } - sub = append(sub, s) - } - } - return sub, nil -} - -// waitsIssuedFor is the families a membership names: the tool runner's module's, and nobody else's. -func waitsIssuedFor(d Declared) []WaitIssued { - if d.Module != RuntimeModule { - return nil - } - var out []WaitIssued - for _, w := range d.Waits { - s, err := WaitSubject(w) - if err != nil { - continue // refused where the account is composed, which names it - } - out = append(out, WaitIssued{Event: w.Name, Subject: s}) - } - return out -} diff --git a/go/buscompose/writers.go b/go/buscompose/writers.go deleted file mode 100644 index 8ae64d6..0000000 --- a/go/buscompose/writers.go +++ /dev/null @@ -1,282 +0,0 @@ -package buscompose - -import ( - "fmt" - "slices" - "strings" - - "git.novox.be/novox/mesh-sdk/go/delivery" -) - -// The writers table (novox/hq to-be 45 §1, ADR 0227 rule 1), compiled in. -// -// **Every kind of state the core keeps has one writer; anyone else asks it.** The table is the design's, -// row for row, with what each row writes on the bus where it writes there. Two things read it: the -// composition of every principal's grants (PermissionsFor), which **refuses a grant that lets a -// principal publish on a subject another writes** — so a second writer cannot be granted by accident, -// and the composition says which state and whose — and the test beside it, which walks the rows -// against the design's list and the composition of a whole mesh. Changing a writer is a change to -// this table, through a decision. - -// WriterRow is one row of the writers table. -type WriterRow struct { - State string - Writer string - KeptIn string - Others string - // Subjects are the bus subjects a write of this state is a publish to; none for state kept off the - // bus (a module's own store) or not built yet. - Subjects []string - // Writes says a principal granted a publish pattern overlapping Subjects is this state's writer — - // given the pattern, because a machine writes its own report and no other's. - Writes func(p Principal, pattern string) bool - // Shared says why more than one principal may publish here; empty for one writer. - Shared string -} - -// createsDeclared, and the other writers' tests, are who a row's writer is as a principal. -// -// createsDeclared is the grants part creating one stream a manifest declares, by its own name: the create alone, never an -// 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) { - return true - } - name, ok := strings.CutPrefix(pattern, "$JS.API.STREAM.CREATE.") - if ok && p.Kind == KindModule && p.Module == GrantsModule && name == GrantsUserListObjectStream { - return true - } - return ok && p.Kind == KindModule && p.Module == GrantsModule && slices.Contains(p.CreatesStreams, name) && - safeSubject.MatchString(name) -} - -// ownMachine is a node writing a subject whose third token is its own name, and no wildcard there. -func ownMachine(p Principal, pattern string) bool { - tokens := strings.Split(pattern, ".") - return p.Kind == KindNode && len(tokens) > 2 && tokens[2] == p.Node -} - -// holdsSeatOf is a principal holding the seat a `mesh.seat..…` pattern names: its module's own -// principal, or the node tools carrying a module that holds it (ADR 0175, the runtime is its modules). -func holdsSeatOf(p Principal, pattern string) bool { - tokens := strings.Split(pattern, ".") - if len(tokens) < 3 { - return false - } - seat := tokens[2] - holds := func(seats []Seat) bool { - return slices.ContainsFunc(seats, func(s Seat) bool { return s.Name == seat }) - } - switch p.Kind { - case KindModule: - return holds(p.Holds) - case KindNodeTools: - return slices.ContainsFunc(p.Carries, func(d Declared) bool { return holds(d.Holds) }) - } - return false -} - -// ownModule is a module publishing under its own name, or the node tools carrying it. -func ownModule(p Principal, pattern string) bool { - tokens := strings.Split(pattern, ".") - if len(tokens) < 3 { - return false - } - module := tokens[2] - switch p.Kind { - case KindModule: - return p.Module == module - case KindNodeTools: - return slices.ContainsFunc(p.Carries, func(d Declared) bool { return d.Module == module }) - } - return false -} - -// moduleNamed is the writer that is module: its own principal, a process of it, or the node tools carrying it. -func moduleNamed(module string) func(Principal, string) bool { - return func(p Principal, _ string) bool { - switch p.Kind { - case KindModule, KindProcess: - return p.Module == module - case KindNodeTools: - return slices.ContainsFunc(p.Carries, func(d Declared) bool { return d.Module == module }) - } - return false - } -} - -// ownAnnouncer is a module, a process of it or the node tools carrying it saying what that module keeps (novox/hq ADR -// 0495 clause 2): each under its own name alone. -func ownAnnouncer(p Principal, pattern string) bool { - tokens := strings.Split(pattern, ".") - if len(tokens) < 3 { - return false - } - if p.Kind == KindProcess { - return p.Module == tokens[2] - } - return ownModule(p, pattern) -} - -// ownProvisioner is a machine's provisioner client saying a report about its own machine: the third token of -// `prov.applied..…` is its machine (novox/hq ADR 0349 §4, design 57 P3). -func ownProvisioner(p Principal, pattern string) bool { - tokens := strings.Split(pattern, ".") - return p.Kind == KindProvisioner && len(tokens) > 3 && tokens[2] == p.Node -} - -// ownNodeKeys is a node writing the have keys of its own node: `$KV...…`, the node in the third token -// and no wildcard there (novox/hq ADR 0429 §1). -func ownNodeKeys(p Principal, pattern string) bool { - tokens := strings.Split(pattern, ".") - return p.Kind == KindNode && len(tokens) > 2 && tokens[2] == p.Node -} - -// kvOf is a bucket's write subjects. -func kvOf(bucket string) []string { return []string{"$KV." + bucket + ".>"} } - -// WritersTable is to-be 45 §1, in its order. -var WritersTable = []WriterRow{ - {State: "a machine's applied state and its report", Writer: "the node-engine's apply queue", - KeptIn: "the machine; the report on the bus", Others: "the reconcile and a delivery enqueue, never apply", - // And its health statement between reports (novox/hq ADR 0240): the same writer stating the same - // machine, inside the grant it already had (`mesh.control..>`). And a line mesh-cli was given - // on the machine (novox/hq ADR 0272): that subject is this machine's engine's alone. - Subjects: []string{"mesh.control.*.report", "mesh.control.*.health", "mesh.control.*.cli"}, Writes: ownMachine}, - {State: "stream definitions and bus permissions", Writer: "the grants part", KeptIn: "the bus", Others: "—", - // A stream's definition, and a durable consumer's by the API that names it so. Not every - // consumer create: a module watching its own bucket makes and deletes an ordered consumer on the - // bucket's stream (ADR 0201), which defines nothing the mesh keeps. - 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)"}, - {State: "builds and their outcomes", Writer: "the build seat's holder", KeptIn: "its own state", - Others: "the builder asks", - Subjects: []string{"mesh.seat.node-build-agent.event.built", "mesh.seat.mesh-build-machine.event.built"}, - Writes: holdsSeatOf, - Shared: "every machine holding the build seat answers the asks it took; each outcome names its ask"}, - // What a module keeps and who may read it (novox/hq ADR 0495 clauses 2 and 3, issue 1790): said by the module. - {State: "a module's states announced", Writer: "the module itself, from its code, under its own name", - KeptIn: "the bus (EVENTS)", Others: "the grants part grants readers from it", - Subjects: []string{"mesh.mod.*.event.states"}, Writes: ownAnnouncer}, - {State: "a merge announced", Writer: "one announcer per forge (the hook, or the poll when the hook is absent — never both)", - KeptIn: "the bus", Others: "—", Subjects: []string{"mesh.mod.*.event.pull.merged"}, Writes: ownModule}, - // A pull request's head, before it merges (novox/hq to-be 45 §9): the same one announcer. - {State: "a pull request's head announced", Writer: "the forge's announcer, the one that announces its merges", - KeptIn: "the bus", Others: "the checker checks it", Subjects: []string{"mesh.mod.*.event.pull.updated"}, - Writes: ownModule}, - {State: "a pull request's merge check", Writer: "the checker that ran it", - KeptIn: "the bus", Others: "the forge's holder sets it as the pull request's status"}, - {State: "a provider's standing", Writer: "the provider", KeptIn: "the provider's events", - Others: "the watchman keeps the newest word as a condition", - Subjects: []string{"mesh.mod.*.event.provisioner.failing", "mesh.mod.*.event.provisioner.recovered", - "mesh.mod.*.event.provisioner.retirement"}, - Writes: ownModule}, - {State: "the operator-channel's open messages", Writer: "the seat's holder", KeptIn: "its own key-value state", - Others: "—"}, - // The seat mesh-enrolment's open enrolments (novox/hq ADR 0587 §2, issue 774): its holder alone writes them, each a - // compare-and-set at the revision read; the grants part reads them as a named reader. - {State: "the open enrolments", Writer: "the seat mesh-enrolment's holder, compare-and-set", - KeptIn: "key-value " + delivery.EnrolmentsBucket + ", with per-key expiry", - Others: "the grants part reads them (a named reader) and composes each enrol. user from them", - Subjects: kvOf(delivery.EnrolmentsBucket), Writes: moduleNamed(delivery.EnrolmentModule)}, - // Which login each node uses (novox/hq ADR 0583 §1 insight): the vault alone writes it. - {State: "a node's logins", Writer: "the vault", KeptIn: "key-value " + delivery.NodeLoginsBucket, - Others: "the grants part reads every key, each node's engine its own node's key alone", - Subjects: kvOf(delivery.NodeLoginsBucket), Writes: moduleNamed(delivery.VaultModule)}, - // What a consumer gave up on (novox/hq issue 330): copied by the watchman from the server's notice (ADR 0564 - // clause 5, ADR 0590 clause 2). - {State: "a message a consumer gave up on", Writer: "the watchman", KeptIn: "the bus, the stream " + DeadLettersStream, - Others: "read through the watchman's dead-letters; delivered again by nobody until the hand-act log's home is delivered", - Subjects: []string{deadLetterPrefix + ">", againPrefix + ">"}, Writes: keepsDeadLetters}, - // What each machine has of each module (novox/hq ADR 0429 §1, design 47): written by that machine's node-engine under - // its own node's keys; the view and the watchman read it. - {State: "a machine's have", Writer: "the node-engine of the machine, under its own keys `.>`", - KeptIn: "key-value " + delivery.HaveBucket + ", twenty revisions of each key", - Others: "read through `reads` as node-engine.have", Subjects: kvOf(delivery.HaveBucket), Writes: ownNodeKeys}, - // The acts a node's engine was asked through its node seat, and its own asks (novox/hq ADR 0563 §2, §3): the - // node-engine's declared state, created by the grants part, written by each node's engine under its own keys. - // A node's own health report (issue 774): its engine's own key, read by the watchman. - {State: "a machine's health report", Writer: "the node-engine of the machine, its key `` alone", - KeptIn: "key-value " + delivery.NodeHealthBucket, Others: "read by the watchman through `reads`", - Subjects: kvOf(delivery.NodeHealthBucket), Writes: ownNodeKeys}, - // A node's declared packages (novox/hq ADR 0565 clause 6): its engine's own key, read by pacman's verbs. - {State: "a machine's declared packages", Writer: "the node-engine of the machine, its key `` alone", - KeptIn: "key-value " + EnginePackagesBucket, Others: "read by pacman through `reads`", - Subjects: kvOf(EnginePackagesBucket), Writes: ownNodeKeys}, - // A node's placed modules' data items (novox/hq ADR 0577 clause 1): its engine's own keys, read by that node's disk-load. - {State: "a machine's data items", Writer: "the node-engine of the machine, under its own keys `.>`", - KeptIn: "key-value " + EngineDataBucket, Others: "read by the node's disk-load through `reads`, its own node's keys alone", - Subjects: kvOf(EngineDataBucket), Writes: ownNodeKeys}, - {State: "a machine's hand acts", Writer: "the node-engine of the machine, under its own keys `.>`, compare-and-set", - KeptIn: "key-value " + EngineActsBucket + ", the node-engine's declared state acts", - Others: "read through the node seat's verb acts", Subjects: kvOf(EngineActsBucket), - Writes: func(p Principal, pattern string) bool { - return ownNodeKeys(p, pattern) || removesEngineActs(p, pattern) - }, - Shared: "the bus module's tools account removes keys of a full bucket, for removal only (novox/hq ADR 0564 Limits 4)"}, - // The object buckets (novox/hq ADR 0486 §2, ADR 0547 clause 7): the declaration files' buckets the ended controller - // wrote are written by nobody (ADR 0590); the grants part writes its own. - {State: "an object bucket", Writer: "the grants part, its own bucket alone", - KeptIn: "the object bucket " + DeclarationFilesBucket + "- of each node, an object removed a day after the " + - "node's latest declaration no longer names it", - Others: "a node-engine reads an object of its own bucket by its name and may neither list the bucket nor write it", - Subjects: []string{"$O.*.>"}, Writes: func(p Principal, pattern string) bool { - return grantsOwnObjects(p, pattern) - }, - Shared: "nobody else: the grants part writes its own object bucket " + GrantsUserListObjects + ", the user list sealed to the bus node, and no other (novox/hq ADR 0547 clause 7)"}, - // What a machine's provisioner client placed of a module's files (novox/hq ADR 0349 §4, design 57 P6): its own - // machine's word alone, which a walk's end and a gate wait for (ADR 0362, ADR 0349 §15). Kept newest per module - // in the stream ProvAppliedStream, and by the client in the provisioner server's `applied` bucket. - {State: "a module's files placed on a machine", Writer: "that machine's provisioner client", - KeptIn: "the bus, the stream " + ProvAppliedStream + ", newest per module; and key-value " + - ProvisionerAppliedBucket, - Others: "the watchman reads it; a failure is that machine's failed apply", - Subjects: []string{"prov.applied.*.>"}, Writes: ownProvisioner}, -} - -// CheckWriters refuses a grant that lets a principal publish on a subject the writers table gives -// another writer (to-be 45 §1): which state, whose, and the pattern that would make a second writer. -func CheckWriters(p Principal, publish []string) error { - var problems []string - for _, pattern := range publish { - for _, row := range WritersTable { - if row.Writes == nil { - continue - } - for _, subject := range row.Subjects { - if !SubjectsOverlap(pattern, subject) || row.Writes(p, pattern) { - continue - } - problems = append(problems, fmt.Sprintf("%s may publish %s, which writes %s (%s), whose writer is %s", - p.Username(), pattern, row.State, subject, row.Writer)) - } - } - } - if len(problems) == 0 { - return nil - } - return fmt.Errorf("a second writer would be granted (novox/hq to-be 45 §1, one writer per piece of state):\n %s", - strings.Join(problems, "\n ")) -} - -// SubjectsOverlap says some subject matches both patterns: `*` is one token, `>` one or more to the end. -func SubjectsOverlap(a, b string) bool { - x, y := strings.Split(a, "."), strings.Split(b, ".") - for i := 0; ; i++ { - switch { - case i == len(x) && i == len(y): - return true - case i == len(x) || i == len(y): - return false - case x[i] == ">" || y[i] == ">": - return true - case x[i] == "*" || y[i] == "*" || x[i] == y[i]: - continue - default: - return false - } - } -}