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) } } } }