diff --git a/go/announce/announce.go b/go/announce/announce.go index 8cf8867..00e6d35 100644 --- a/go/announce/announce.go +++ b/go/announce/announce.go @@ -24,15 +24,12 @@ const Event = "states" // account may already publish to and the events stream keeps. func Subject(module string) string { return "mesh.mod." + module + ".event." + Event } -// SubjectAll is every module's announcement, what the controller consumes. +// SubjectAll is every module's announcement. const SubjectAll = "mesh.mod.*.event." + Event -// KeptBucket is the controller's bucket of the last announcement of each module, by Key. -const KeptBucket = "mesh-controller_states-announced" - var name = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`) -// Key is module's key in KeptBucket: the module itself, which must be one plain name, since a module's name is part of +// Key is module's key where the last announcement of each module is kept: the module itself, which must be one plain name, since a module's name is part of // its buckets' names (the indexer's judge, novox/hq ADR 0578 §1). func Key(module string) (string, error) { if !name.MatchString(module) { diff --git a/go/announce/announce_test.go b/go/announce/announce_test.go index 7156aea..05f0157 100644 --- a/go/announce/announce_test.go +++ b/go/announce/announce_test.go @@ -42,9 +42,6 @@ func TestAnAnnouncementRoundTripsSortedAndSaysWhatIsReadable(t *testing.T) { if Subject("mesh-controller") != "mesh.mod.mesh-controller.event.states" || SubjectAll != "mesh.mod.*.event.states" { t.Fatalf("the subjects are %q and %q", Subject("mesh-controller"), SubjectAll) } - if KeptBucket != "mesh-controller_states-announced" { - t.Fatalf("the kept bucket is %q", KeptBucket) - } if k, err := Key("mesh-delivery-builder"); err != nil || k != "mesh-delivery-builder" { t.Fatalf("Key = %q, %v", k, err) } diff --git a/go/buscompose/accounts.go b/go/buscompose/accounts.go index 3a40eb5..1402804 100644 --- a/go/buscompose/accounts.go +++ b/go/buscompose/accounts.go @@ -186,68 +186,3 @@ func HeldCredential(w WantedAccount, password string) ([]byte, error) { }{URL: w.URL, Fingerprint: w.Fingerprint, Node: w.Node, Module: w.Module, User: w.User, Password: password, Claims: w.Claims}) } - -// **The lease write** (novox/hq ADR 0561 §3, §6). The controller's lease is the one key `holder` of its bucket -// (LeaseBucket). The operator's credential hands it away by writing a value that names its new holder in `handed-to`; -// while the key holds that, the grants part creates the durable consumers an account needs, the assigner reads the grants -// part's grants (delivery.GrantSource), and the vault retires and rotates accounts. A controller's value, a key deleted or -// expired, or one that cannot be read or parsed, turn those off. No clock and no setting turns it. -// -// **Minting is no longer on the lease** (issue 774, research 072's gap "vault mints after the lease"): the vault is the -// one issuer of every account the grants part wants, whoever holds the lease, and the controller mints none once a vault -// is installed (it forwards `module issue` to the vault's issue-account). One issuer at every moment holds by who mints, -// not by the lease: an account whose value the vault holds is never minted anew, and a value the controller hands or -// moves never replaces one the vault minted. - -// ControllerLease is the lease's bucket as a reader names it; the vault and the grants part alone read it -// (NamedReaders), the controller's announcement marking it readable by nobody. -const ControllerLease = ControllerSeat + ".lease" - -// ControllerLeaseKey is the lease's one key. -const ControllerLeaseKey = leaseKey - -// LeaseHolder is the lease key's value: a controller instance's (mesh-controller internal/lease Holder), or, once handed -// away, one naming its holder in HandedTo. -type LeaseHolder struct { - Instance string `json:"instance,omitempty"` - Host string `json:"host,omitempty"` - Epoch uint64 `json:"epoch,omitempty"` - Taken time.Time `json:"taken,omitzero"` - Renewed time.Time `json:"renewed,omitzero"` - // HandedTo is who holds the lease once the controller handed it away: never a controller instance. - HandedTo string `json:"handed-to,omitempty"` - // HandedBy is the credential that wrote it (the operator's), and Why its reason. - HandedBy string `json:"handed-by,omitempty"` - Why string `json:"why,omitempty"` - // Ask is the operator's ask whose warrant handed it (novox/hq ADR 0582 clause 3: "with the why, the ask and the - // time"), and At when it was written. Neither turns the lease: LeaseHandedAway reads HandedTo alone. - Ask string `json:"ask,omitempty"` - At time.Time `json:"at,omitzero"` -} - -// LeaseHandedAway reads the lease key as a getter answered it (its value, whether it was there, the read's error) and -// says whether it is handed to a holder that is no controller, with why either way. -func LeaseHandedAway(raw []byte, found bool, readErr error) (bool, string) { - switch { - case readErr != nil: - return false, "the controller's lease cannot be read (" + readErr.Error() + "), so it is not handed away" - case !found || len(raw) == 0 || string(raw) == "null": - return false, "no lease is written (expired, deleted, or never taken): a controller may take it, so it is not handed away" - } - var h LeaseHolder - if err := json.Unmarshal(raw, &h); err != nil { - return false, "the controller's lease is not readable as a holder (" + err.Error() + "), so it is not handed away" - } - if strings.TrimSpace(h.HandedTo) == "" { - who := h.Instance - if who == "" { - who = "an instance that names itself not" - } - return false, "the controller holds the lease (" + who + "), so it is not handed away (ADR 0561 §3)" - } - if h.Instance != "" { - return false, "the lease names both a controller instance (" + h.Instance + ") and a holder it is handed to (" + - h.HandedTo + "), so it is not handed away" - } - return true, "the lease is handed to " + h.HandedTo + " (ADR 0561 §3)" -} diff --git a/go/buscompose/announced.go b/go/buscompose/announced.go index 90c57d8..bdd8c49 100644 --- a/go/buscompose/announced.go +++ b/go/buscompose/announced.go @@ -9,39 +9,11 @@ import ( ) // 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 controller -// keeps the last word of each (announce.KeptBucket) and grants a reader's `reads: .` only when the owner +// 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. -// StatesAnnouncedBucket is the controller's bucket of the last announcement of each module (announce.KeptBucket). -const StatesAnnouncedBucket = announce.KeptBucket - -// controllerReadable are the controller's states others may read: the record mirrors (the packages each node declares -// among them, which the pacman module reads, ADR 0507 §5), the chain's buckets, and the grants issued and the -// compositions' digests, which the assigner reads (ADR 0528 clause 2, ADR 0539 clause 6). -var controllerReadable = []string{MirrorAssignments, MirrorDeclaredConfig, MirrorPredefined, ChainBuilds, ChainRollouts, - MirrorDeclaredPackages, ControllerGrants, ControllerComposed} - -// ControllerAnnouncement is what the controller announces it keeps (ADR 0495 clause 2): every bucket of its own by its local -// name, the ones others may read marked readable. It announces itself, as any module does from its code. -func ControllerAnnouncement(node string) announce.States { - s := announce.States{Module: ControllerSeat, Node: node} - readable := map[string]bool{} - for _, n := range controllerReadable { - readable[n] = true - } - for _, bucket := range ControllerBuckets() { - local, mine := strings.CutPrefix(bucket, ControllerSeat+"_") - if !mine { - continue - } - s.States = append(s.States, announce.State{Name: local, Readable: readable[local]}) - } - sort.Slice(s.States, func(i, j int) bool { return s.States[i].Name < s.States[j].Name }) - return s -} - // 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 @@ -56,8 +28,7 @@ func ReadsAnnouncedBy(reader string, reads []string, announced map[string]announ 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 (ADR 0561 §3: the - // controller's lease, which its announcement marks readable by nobody). + // 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) { diff --git a/go/buscompose/buscompose_test.go b/go/buscompose/buscompose_test.go index b896812..d31d1e6 100644 --- a/go/buscompose/buscompose_test.go +++ b/go/buscompose/buscompose_test.go @@ -2,12 +2,10 @@ package buscompose import ( "encoding/json" - "errors" "maps" "os" "path/filepath" "reflect" - "regexp" "slices" "strings" "testing" @@ -16,179 +14,31 @@ import ( "git.novox.be/novox/mesh-sdk/go/announce" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" - "git.novox.be/novox/mesh-sdk/go/membership" - "git.novox.be/novox/mesh-sdk/go/provisions" "github.com/nats-io/nats.go/jetstream" ) -// Defends issue 1377 and novox/hq ADR 0547 §5: this is the controller's bus composer copied, and it composes byte for -// byte what the controller's composes. testdata/controller-user-lists.json was written by the controller's own -// internal/broker (mesh-controller branch fix/1945-mesh-grants-publishes-memberships, and fix/1790-grants-create-buckets for the grants part creating declared streams), lists and memberships, from fixtures_test.go compiled there unchanged; composed.conf is the -// controller's golden of the whole server configuration. A row that differs is the copy drifting from the controller, -// which is a fault while both compose (until the controller loses its lease, ADR 0547 §13), but for the grants the copy -// gives beyond it on purpose (beyondTheControllers): the copy grants every user everything the controller's grants, and -// only those more. -// createdAdvisory is one subscription to a bucket's creation in a composed list, as stateCreatedHeard composes it. -var createdAdvisory = regexp.MustCompile(`"\$JS\.EVENT\.ADVISORY\.STREAM\.CREATED\.KV_[^"]*", `) +// 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" -// beyondTheControllers is what the copy grants beyond the controller's on purpose (novox/hq ADR 0547 §2, ADR 0555 §3, -// issue 1126), by user: a node's account reads every node's own facts, `.node` of the have bucket where the node's -// record rides, and what it composes its own modules from (NodeComposeReads). After the take-over (ADR 0547 §8) the -// grants part's list is the one the bus loads; a hand-back loads the controller's, which lacks these reads, and the nodes -// keep applying without composing their own until the next take-over. Each principal's hearing of its buckets' creation -// (issue 1790) is the other row beyond, matched by createdAdvisory. -// -// And the watchman's dead letters and watchdogs (novox/hq ADR 0564 clauses 4, 5, issue 1952 S5): its own consumer on -// DEAD_LETTER_NOTICES, a read of DEAD_LETTERS, and the words its watchdogs hear, the controller's jobs until the lease -// write. -func beyondTheControllers(user string) map[string]bool { - out := map[string]bool{} - if strings.HasSuffix(user, "."+WatchmanModule) { - for _, s := range append(WatchmanDeadLetterGrants(nil), WatchmanHears()...) { - out[s] = true - } - } - node, ok := strings.CutPrefix(user, "node.") - if !ok { - return out - } - for _, s := range append(append(readGrants("node-engine_have", "*.node"), NodeComposeReads(node)...), NodeComposeAdvisories()...) { - out[s] = true - } - return out -} - -// mayBeyondTheControllers is what the copy may grant beyond the controller's where the records allow it, and need not -// where they do not: a node's ask of the vault's `own` while the vault serves it (issue 1126, ADR 0550 §5). -func mayBeyondTheControllers(user string) map[string]bool { - // A runtime's read of its membership from the grants part's state (novox/hq ADR 0571 clause 7): its own key, or the - // tool runner's node's. - if parts := strings.Split(user, "."); len(parts) == 2 && parts[0] != "node" { - key := user - if parts[1] == RuntimeModule { - key = parts[0] + ".*" - } - out := map[string]bool{} - if membership.GrantsBucket != GrantsMembershipsBucket || membership.GrantsKey("a", "b") != MembershipKey("a", "b") { - panic("the membership package's grants bucket does not agree with buscompose's") // FollowMembership reads it - } - for _, s := range readGrants(GrantsMembershipsBucket, key) { - out[s] = true - } - return out - } - if _, ok := strings.CutPrefix(user, "node."); !ok { - return nil - } - return map[string]bool{VaultOwnCall(Principal{Kind: KindNode, Node: strings.TrimPrefix(user, "node.")}): true, - VaultSecretChanged: true} -} - -// engineBeyond is a node's engine holding its node seat and asking the operator in its own name (novox/hq ADR 0563 §1, -// §3), beyond the controller's on purpose: its own node's verbs, events and discovery, the bus's discovery asked, and -// the router's seat traffic under node-engine-. -func engineBeyond(user, g string) bool { - node, ok := strings.CutPrefix(user, "node.") - if !ok { - return false - } - return strings.HasPrefix(g, "mesh.seat."+EngineSeat+".") && strings.Contains(g, "."+node) || - strings.HasPrefix(g, "$SRV.") || strings.Contains(g, ".node-engine-"+node) || - strings.Contains(g, "KV_"+EngineActsBucket) || strings.HasPrefix(g, "$KV."+EngineActsBucket+"."+node+".") || - // And its node's declared packages (novox/hq ADR 0565 clause 6). - strings.Contains(g, "KV_"+EnginePackagesBucket) || g == "$KV."+EnginePackagesBucket+"."+node || - // And, read, the controller's mirror's key of its node, until the lease write (novox/hq ADR 0565 clause 6). - strings.Contains(g, "KV_"+DeclaredPackagesBucket) && !strings.HasPrefix(g, "$KV.") && - (!strings.Contains(g, "$KV.") || strings.HasSuffix(g, "$KV."+DeclaredPackagesBucket+"."+node)) || - // And its own health report (issue 774). - strings.Contains(g, "KV_"+delivery.NodeHealthBucket) || g == "$KV."+delivery.NodeHealthBucket+"."+node || - // And the provisions of its node's modules, read (novox/hq ADR 0570 clause 6). - strings.Contains(g, "KV_"+provisions.ProvisionsBucket) || - // And its node's placed modules' data items, its own node's keys (novox/hq ADR 0577 clause 1). - strings.Contains(g, "KV_"+EngineDataBucket) || strings.HasPrefix(g, "$KV."+EngineDataBucket+"."+node+".") -} - -// createsAnAccountsConsumer is the grants part's create and read of a durable consumer an account needs, by its stream -// and name (novox/hq ADR 0561 §6): beyond the controller's on purpose, since the controller made them alone. -func createsAnAccountsConsumer(user, g string) bool { - return strings.HasSuffix(user, "."+GrantsModule) && - (strings.HasPrefix(g, "$JS.API.CONSUMER.CREATE.") || strings.HasPrefix(g, "$JS.API.CONSUMER.INFO.")) -} - -// quotedToken is a quoted token of a composed list. -var quotedToken = regexp.MustCompile(`"[^"]*"`) - -// grantsByUser splits a composed list into the text outside its users and each user's quoted tokens (its name, its -// password's hash, every subject it may publish or subscribe to), by user. -func grantsByUser(list string) (string, map[string]map[string]bool) { - var outside strings.Builder - users := map[string]map[string]bool{} - user := "" - for _, line := range strings.SplitAfter(list, "\n") { - trimmed := strings.TrimSpace(line) - if name, ok := strings.CutPrefix(trimmed, `{ user: "`); ok { - user, _, _ = strings.Cut(name, `"`) - users[user] = map[string]bool{} - } else if user != "" && trimmed == "]" { - user = "" - } - if user == "" { - outside.WriteString(line) - continue - } - for _, q := range quotedToken.FindAllString(line, -1) { - users[user][strings.Trim(q, `"`)] = true - } - } - return outside.String(), users -} - -// grantsAsTheControllers fails t unless the copy's list is the controller's, user for user, but for the grants it -// names on purpose (beyondTheControllers, the creation advisories): every grant of the controller's is the copy's, and -// the copy's beyond them are exactly those, every one of them there. -func grantsAsTheControllers(t *testing.T, got, want string) { +// golden compares got with the golden file name, or writes it. +func golden(t *testing.T, name string, got []byte) { t.Helper() - gotOutside, gotUsers := grantsByUser(got) - wantOutside, wantUsers := grantsByUser(want) - if gotOutside != wantOutside { - t.Errorf("the list differs from the controller's outside its users:\n%s", gotOutside) + path := filepath.Join("testdata", name) + if updateGolden { + if err := os.WriteFile(path, got, 0o644); err != nil { + t.Fatal(err) + } + return } - for user, was := range wantUsers { - now, ok := gotUsers[user] - if !ok { - t.Errorf("the copy has no user %s", user) - continue - } - intended := beyondTheControllers(user) - for g := range was { - // A runtime's direct get of its membership from the assignments stream went with the stream (research 072 - // row 33): it reads the grants part's state memberships instead (ADR 0571 clause 7). And it no longer hears - // the subject, where the grants part stops publishing at the lease write (ADR 0571 clause 10, issue 774). - node := strings.SplitN(user, ".", 2)[0] - if !now[g] && !strings.HasPrefix(g, "$JS.API.DIRECT.GET."+AssignmentsStream+".mesh.assignment."+node+".") && - !strings.HasPrefix(g, "mesh.assignment."+node+".") { - t.Errorf("%s: the copy does not grant %s, which the controller's does", user, g) - } - } - for g := range now { - if strings.HasPrefix(g, "$JS.API.DIRECT.GET."+AssignmentsStream+".") && !strings.HasSuffix(user, "."+GrantsModule) { - t.Errorf("%s: the copy grants %s, a read of the assignments stream, which goes at the lease write", user, g) - } - if !was[g] && !intended[g] && !mayBeyondTheControllers(user)[g] && !createdAdvisory.MatchString(`"`+g+`", `) && - !createsAnAccountsConsumer(user, g) && !engineBeyond(user, g) { - t.Errorf("%s: the copy grants %s beyond the controller's, and no decision names it", user, g) - } - } - for g := range intended { - if !now[g] { - t.Errorf("%s: the copy does not grant %s, which a node composing its own modules reads", user, g) - } - } + want, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) } - for user := range gotUsers { - if _, ok := wantUsers[user]; !ok { - t.Errorf("the copy has a user %s the controller's has not", user) - } + 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) } } @@ -209,63 +59,33 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) { got[1] != "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates" { t.Errorf("a node hears the creation of %v", got) } - raw, err := os.ReadFile(filepath.Join("testdata", "controller-user-lists.json")) - if err != nil { - t.Fatal(err) - } - var want []struct { + type list struct { Name, List string Missing []string Err string Memberships json.RawMessage } - if err := json.Unmarshal(raw, &want); err != nil { + 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) } - fixtures := UserListFixtures() - if len(want) != len(fixtures) { - t.Fatalf("%d fixtures, %d lists the controller composed", len(fixtures), len(want)) - } - for i, f := range fixtures { - t.Run(f.Name, func(t *testing.T) { - if want[i].Name != f.Name { - t.Fatalf("fixture %q against the controller's %q", f.Name, want[i].Name) - } - list, missing, err := ComposedFor(f) - got := "" - if err != nil { - got = err.Error() - } - if got != want[i].Err { - t.Fatalf("error %q, the controller's %q", got, want[i].Err) - } - if strings.Join(missing, ",") != strings.Join(want[i].Missing, ",") { - t.Errorf("left out %v, the controller %v", missing, want[i].Missing) - } - // The one row the copy composes beyond the controller's, on purpose (novox/hq issue 1790, 2026-10-26, after - // the take-over of ADR 0547 milestone 2): each principal hears its own and read buckets created, so a state - // missing at its start is bound when it appears. The controller's composer is frozen and its list no longer - // loaded; while it is not, this row is left out of the comparison and nothing else. - grantsAsTheControllers(t, list, want[i].List) - // And every membership the records issue, as the controller's MembershipFor makes it (ADR 0547 §5): the - // grants part publishes them. - raw, err := json.Marshal(MembershipsFor(f)) - if err != nil { - t.Fatal(err) - } - var gotM, wasM any - if err := json.Unmarshal(raw, &gotM); err != nil { - t.Fatal(err) - } - if err := json.Unmarshal(want[i].Memberships, &wasM); err != nil { - t.Fatal(err) - } - heldEventsBeyond(gotM, wasM) - if !reflect.DeepEqual(gotM, wasM) { - t.Errorf("the memberships differ from the controller's:\n%s", raw) - } - }) - } + 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) { @@ -380,13 +200,12 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) { 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` and the lease key, never writing either. + // `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" - leaseGet := "$JS.API.DIRECT.GET.KV_" + LeaseBucket + ".$KV." + LeaseBucket + ".holder" - for subject, want := range map[string]bool{VaultSignCall(one): true, signersGet: true, leaseGet: true, - VaultSignCall(Principal{Kind: KindNode, Node: "two"}): false, "$KV." + SignersBucketName + ".key": false, - "$KV." + LeaseBucket + ".holder": false} { + 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) } @@ -490,12 +309,12 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) { t.Error("node-logins' named readers") } }) - // 774/S40/AC1 (ADR 0587 §4, §6): from the lease write an enrolling node's user may publish the redeem call in its - // own name and nothing else, and subscribe its own inbox alone; before it, the controller's subject beside. + // 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"}} - for _, handed := range []bool{true, false} { - users, err := Users(Records{Nodes: []string{"ace"}, Enrolling: []string{"lab"}, LeaseHandedAway: handed, + { + 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) @@ -514,17 +333,13 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) { t.Fatal(err) } want := []string{"mesh.seat.mesh-enrolment.call.redeem.enrol~lab"} - if !handed { - want = []string{"mesh.control.enrol", "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("handed %v: enrol.lab publishes %v, subscribes %v, answers %v", handed, perms.Publish, perms.Subscribe, - perms.AllowResponses) + t.Errorf("enrol.lab publishes %v, subscribes %v, answers %v", perms.Publish, perms.Subscribe, perms.AllowResponses) } } - // No holder serving redeem and the lease handed: the user may enrol nowhere, refused by name. - if _, err := PermissionsFor(Principal{Kind: KindEnrolment, Node: "lab", LeaseHandedAway: true}); err == nil { + // 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") } }) @@ -570,14 +385,13 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) { t.Error("the enrolments' named readers") } }) - t.Run("the controller's golden server configuration", func(t *testing.T) { + 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: KindController, PasswordHash: "$2a$11$cccccccccccccccccccccc"}, - {Kind: KindEnrolment, Node: "one", PasswordHash: "$2a$11$eeeeeeeeeeeeeeeeeeeeee"}, + {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"}, @@ -592,11 +406,7 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) { if err != nil { t.Fatal(err) } - want, err := os.ReadFile(filepath.Join("testdata", "composed.conf")) - if err != nil { - t.Fatal(err) - } - grantsAsTheControllers(t, got, string(want)) + 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 @@ -632,7 +442,7 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) { {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: KindController, 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 { @@ -702,52 +512,18 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) { t.Error("an account was held with no password") } }) - // 774 subtasks 6 and 8 (novox/hq ADR 0561 §3, §6): minting and the grants part's consumers turn on only while the - // lease is handed to a holder that is no controller; the vault and the grants part alone read the lease, whatever the - // controller announced; a module's consumer is created as the controller's EnsureConsumer creates it. - t.Run("the lease turns minting on only when handed away; a consumer is created as the controller's", func(t *testing.T) { - for _, row := range []struct { - name string - raw string - found bool - err error - on bool - }{ - {"a controller holds it", `{"instance":"novox/1234/2026-10-27T20:00:00Z","epoch":57}`, true, nil, false}, - {"handed away", `{"handed-to":"the operator's hand-over","handed-by":"person.jochen"}`, true, nil, true}, - // ADR 0582 clause 3 (774/S3): the write names the ask and the time, which turn nothing. - {"handed to mesh-grants with its ask and time", `{"handed-to":"mesh-grants","handed-by":"person.jochen","why":"w",` + - `"ask":"lease-write-1","at":"2026-11-02T10:00:00Z"}`, true, nil, true}, - {"expired or deleted", ``, false, nil, false}, - {"unreadable", ``, false, errors.New("refused"), false}, - {"not a holder", `[1]`, true, nil, false}, - {"both a controller and a handed holder", `{"instance":"novox/1","handed-to":"x"}`, true, nil, false}, - } { - if on, why := LeaseHandedAway([]byte(row.raw), row.found, row.err); on != row.on || why == "" { - t.Errorf("%s: on %v (%s), want %v", row.name, on, why, row.on) - } - // The assigner's grants source follows it: the grants part's bucket only for a handed-away lease (issue 774). - on, _ := LeaseHandedAway([]byte(row.raw), row.found, row.err) - if bucket, _ := delivery.GrantSource(on); (bucket == delivery.PartGrantsBucket) != row.on { - t.Errorf("%s: the grants source is %s", row.name, bucket) - } - } - announced := map[string]announce.States{ControllerSeat: ControllerAnnouncement("novox")} - for reader, want := range map[string]bool{VaultSeat: true, GrantsModule: true, "mesh-delivery-assigner": true, "mesh-delivery-view": false} { - granted, _ := ReadsAnnouncedBy(reader, []string{ControllerLease}, announced) - if (len(granted) == 1) != want { - t.Errorf("%s reads the lease: %v, want %v", reader, granted, want) - } - } - // Issue 774 (sweep item 2): Users judges every read for its reader again, whatever split the caller made: the - // controller's own reader-less copy refused the vault the lease and the grants part's accounts it is named for. + // 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{ControllerLease, GrantsAccounts}}, - {Module: "mesh-config", ReadsRefused: []string{ControllerLease}}, + {Module: VaultSeat, ReadsRefused: []string{GrantsAccounts}}, {Module: "restic", ReadsRefused: []string{"disk-load.readings"}}, - {Module: "messenger", Reads: []string{ControllerLease, GrantsAccounts}}}}}) + {Module: "messenger", Reads: []string{GrantsAccounts}}}}}) if err != nil { t.Fatal(err) } @@ -757,7 +533,7 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) { reads[u.Module] = strings.Join(u.Reads, ",") } } - if reads[VaultSeat] != ControllerLease+","+GrantsAccounts || reads["mesh-config"] != ControllerLease || reads["restic"] != "" || reads["messenger"] != "" { + 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. @@ -793,48 +569,6 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) { t.Errorf("the grants part creates consumers as %v", u.CreatesConsumers) } } - // ADR 0582 clause 3 (issue 774, 774/S2): the lease write's cut takes from the controller the lease's key and every - // update, purge and delete of its bucket, and gives the grants part the key holder alone; without it nothing - // changes (the golden lists above are composed without it). - lease := "$KV." + LeaseBucket + ".holder" - for _, cut := range []bool{false, true} { - users, err := Users(Records{Nodes: []string{"a"}, Assigned: map[string][]Declared{"a": {{Module: GrantsModule}}}, - LeaseCut: cut}) - if err != nil { - t.Fatal(err) - } - for _, c := range []struct { - kind Kind - subject string - may bool - }{ - {KindController, lease, !cut}, - {KindController, "$JS.API.STREAM.UPDATE.KV_" + LeaseBucket, !cut}, - {KindController, "$JS.API.STREAM.PURGE.KV_" + LeaseBucket, !cut}, - {KindController, "$JS.API.STREAM.DELETE.KV_" + LeaseBucket, !cut}, - {KindController, "$JS.API.STREAM.MSG.DELETE.KV_" + LeaseBucket, !cut}, - // It still reads the lease, and keeps its other buckets. - {KindController, "$JS.API.DIRECT.GET.KV_" + LeaseBucket + ".$KV." + LeaseBucket + ".holder", true}, - {KindController, "$KV." + CallsBucket + ".x", true}, - {KindController, "$JS.API.STREAM.UPDATE.KV_" + CallsBucket, true}, - {KindModule, lease, cut}, - {KindModule, "$KV." + LeaseBucket + ".other", false}, - {KindModule, "$JS.API.STREAM.UPDATE.KV_" + LeaseBucket, false}, - } { - for _, u := range users { - if u.Kind != c.kind || (c.kind == KindModule && u.Module != GrantsModule) { - continue - } - perms, err := PermissionsFor(u) - if err != nil { - t.Fatalf("cut %v: %s: %v", cut, u.Username(), err) - } - if MayPublish(perms, c.subject) != c.may { - t.Errorf("cut %v: %s may publish %s: %v, want %v", cut, u.Username(), c.subject, !c.may, c.may) - } - } - } - } // 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"}, @@ -848,18 +582,12 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) { // 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{ControllerSeat: ControllerAnnouncement("novox"), + 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", ControllerLease, true}, {"mesh-healers", ControllerLease, true}, {WatchmanModule, ControllerLease, true}, - {"mesh-self-check", ControllerLease, false}, {"messenger", ControllerLease, false}, {ArtifactStoreTools, ControllerLease, true}, - {"mesh-conditions", ControllerConditions, true}, {"mesh-healers", ControllerConditions, false}, {"messenger", ControllerConditions, false}, - {"mesh-healers", ControllerHandActs, true}, {"mesh-conditions", ControllerHandActs, false}, - // ADR 0565 clauses 10, 11 (issue 1953 L5-S4): mesh-hand-acts reads the controller's log beside its own. - {"mesh-hand-acts", ControllerHandActs, true}, {"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}, @@ -872,7 +600,7 @@ func healthChecks(t *testing.T) { 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", ControllerConditions}}, + 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)}}, @@ -903,7 +631,7 @@ func healthChecks(t *testing.T) { {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.RemovesControllerDeadLetterConsumer, catalogue.RemovesEngineActs, catalogue.RemovesAssignmentsStream}}, {Process: "other"}}}, + Removes: []string{catalogue.RemovesEngineActs, catalogue.RemovesControllerLeftovers}}, {Process: "other"}}}, {Module: "shop", Processes: []ProcessPrincipal{{Process: "nats-tools", Removes: []string{catalogue.RemovesEngineActs}}}}, }}}) if err != nil { @@ -943,8 +671,17 @@ func healthChecks(t *testing.T) { "$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", - // ADR 0582 clause 3 (issue 774, 774/S2): the lease's bucket's expiry, read and set by its tool. - "$JS.API.STREAM.INFO.KV_mesh-controller_lease", "$JS.API.STREAM.UPDATE.KV_mesh-controller_lease"}, + // 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). @@ -988,10 +725,11 @@ func healthChecks(t *testing.T) { "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 the lease's bucket alone, and neither purges nor deletes it nor writes its key (ADR 0582 clause 3). + // 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.PURGE.KV_mesh-controller_lease", "$JS.API.STREAM.DELETE.KV_mesh-controller_lease", - "$KV.mesh-controller_lease.holder"}, + "$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"}, @@ -1024,11 +762,9 @@ func healthChecks(t *testing.T) { 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: and it reads the controller's mirror's key of its own node alone, and writes none of it. + // 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") || - MayPublish(perms, "$JS.API.DIRECT.GET.KV_mesh-controller_declared-packages.$KV.mesh-controller_declared-packages.b") || - MayPublish(perms, "$KV.mesh-controller_declared-packages.a")) { + 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") { @@ -1102,10 +838,7 @@ func healthChecks(t *testing.T) { // 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", - // ADR 0582 clauses 4, 6 (issue 774, 774/S22): the lease write's events, and ASSIGNMENTS deleted. - "mesh.seat.mesh-grants.event.lease-altered", "mesh.seat.mesh-grants.event.removal-failed", - "mesh.seat.mesh-grants.event.lease-handed", "$JS.EVENT.ADVISORY.STREAM.DELETED.ASSIGNMENTS"} { + "mesh.mod.nats.event.bus.slow-consumer", "prov.applied.g14.keycloak"} { if !MaySubscribe(perms, subject) { t.Errorf("the watchman may not hear %s", subject) } @@ -1145,52 +878,3 @@ func healthChecks(t *testing.T) { } } } - -// heldEventsBeyond takes out of the copy's memberships the one row they list beyond the controller's on purpose (issue -// 1952): a held seat's own events in its holder's seat traffic, so a module the tool runner carries may say them. The -// controller's composer is frozen; any other difference is still a fault. -func heldEventsBeyond(got, was any) { - gotAll, _ := got.(map[string]any) - wasAll, _ := was.(map[string]any) - for key, m := range gotAll { - gm, _ := m.(map[string]any) - traffic, _ := gm["seat-traffic"].(map[string]any) - if traffic == nil { - continue - } - listedBefore := map[string]bool{} - if wm, _ := wasAll[key].(map[string]any); wm != nil { - if wt, _ := wm["seat-traffic"].(map[string]any); wt != nil { - for _, p := range asStrings(wt["publish"]) { - listedBefore[p] = true - } - } - } - var kept []any - for _, p := range asStrings(traffic["publish"]) { - if parts := strings.Split(p, "."); !listedBefore[p] && len(parts) == 5 && parts[0] == "mesh" && parts[1] == "seat" && parts[3] == "event" { - continue - } - kept = append(kept, p) - } - if kept == nil { - delete(traffic, "publish") - } else { - traffic["publish"] = kept - } - if len(traffic) == 0 { - delete(gm, "seat-traffic") - } - } -} - -func asStrings(v any) []string { - list, _ := v.([]any) - out := make([]string, 0, len(list)) - for _, x := range list { - if s, ok := x.(string); ok { - out = append(out, s) - } - } - return out -} diff --git a/go/buscompose/composed.go b/go/buscompose/composed.go index be03e18..9dd1285 100644 --- a/go/buscompose/composed.go +++ b/go/buscompose/composed.go @@ -1,91 +1,14 @@ package buscompose import ( - "context" "fmt" "git.novox.be/novox/mesh-sdk/go/delivery" - "github.com/nats-io/nats.go/jetstream" ) -// The compositions' buckets (novox/hq ADR 0539 clauses 2 and 4): one per node, `composed-`, holding that node's -// compositions, which only that node's node-engine reads, since a composition can carry a sealed value (ADR 0488 §2's -// reason); and `mesh-controller_composed`, holding each composition's digest and the judgement of its move, which the -// assigner reads. The controller writes both, by compare-and-set while it holds the lease; each keeps five revisions -// of a key. The names and layouts are mesh-sdk's (delivery.ComposedBucketFor, ControllerComposedBucket, ComposedKey). - -// ControllerComposed is the local name of the compositions' digests, as a reader names it in `reads` after -// `mesh-controller.`: delivery.ControllerComposedBucket. -const ControllerComposed = "composed" - -// ComposedValueBytes is the most a composition may hold: a file over 64 KiB travels by digest, so a module's -// composition is far smaller; just under the server's largest message, which also carries the subject and headers. -const ComposedValueBytes = 1<<20 - 8<<10 - -// ComposedBucketBytes is a node's composition bucket's size: five revisions of every module's compositions of the node. -const ComposedBucketBytes = 256 << 20 - -// ComposedDigestsBytes is the size of the bucket of digests: a value of a few hundred bytes per node, module and -// fingerprint kept. -const ComposedDigestsBytes = 64 << 20 - -// ComposedConfig is a node's composition bucket as the controller asserts it. -func ComposedConfig(node string) (jetstream.KeyValueConfig, error) { - bucket, err := delivery.ComposedBucketFor(node) - if err != nil { - return jetstream.KeyValueConfig{}, err - } - return jetstream.KeyValueConfig{ - Bucket: bucket, - Description: "the compositions of " + node + " (novox/hq ADR 0539): each module's resources for this node at one " + - "fingerprint, and the node's own, signed; written by the controller alone, read by this node's node-engine " + - "alone; deleted when outside the module's five newest fingerprints and named by no want", - History: delivery.ComposedHistory, - MaxValueSize: ComposedValueBytes, - MaxBytes: ComposedBucketBytes, - Storage: jetstream.FileStorage, - }, nil -} - -// ComposedDigestsConfig is the bucket of the compositions' digests as the controller asserts it. -func ComposedDigestsConfig() jetstream.KeyValueConfig { - return jetstream.KeyValueConfig{ - Bucket: delivery.ControllerComposedBucket, - Description: "each composition's digest and the judgement of its move from what the node runs (novox/hq ADR 0539 " + - "clauses 2 and 14): written by the controller alone, read by the assigner", - History: delivery.ControllerComposedHistory, - MaxValueSize: 4 << 10, - MaxBytes: ComposedDigestsBytes, - Storage: jetstream.FileStorage, - } -} - -// EnsureComposed creates the bucket of digests and each named node's composition bucket if absent and brings their -// options to match. An update, never a delete. -func EnsureComposed(ctx context.Context, js jetstream.JetStream, nodes ...string) error { - if _, err := js.CreateOrUpdateKeyValue(ctx, ComposedDigestsConfig()); err != nil { - return fmt.Errorf("asserting bucket %s: %w", delivery.ControllerComposedBucket, err) - } - for _, node := range nodes { - cfg, err := ComposedConfig(node) - if err != nil { - return err - } - if _, err := js.CreateOrUpdateKeyValue(ctx, cfg); err != nil { - return fmt.Errorf("asserting bucket %s: %w", cfg.Bucket, err) - } - } - return nil -} - -// ComposedWrites is what the controller publishes to write a node's compositions: the bucket's own subjects. -func ComposedWrites(node string) (string, error) { - bucket, err := delivery.ComposedBucketFor(node) - if err != nil { - return "", err - } - return "$KV." + bucket + ".>", nil -} +// 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 @@ -126,42 +49,6 @@ var NodeAgentHears = []string{"mesh.mod." + BackupHolderModule + ".event.backed- // A second holder of the seat is a change of this grant. const BackupHolderModule = "restic" -// administers says a person may invoke every tool (`*`): the operator. -func administers(invokes []string) bool { - for _, i := range invokes { - if i == "*" { - return true - } - } - return false -} - -// HandPathWrites is what the operator's account publishes for release 6's hand path (ADR 0539 clauses 2 and 10): a want -// of any node, written and read back, in the assigner's bucket; each named node's compositions; the digests; and each -// node's object bucket for a file over 64 KiB, whose put reads an object's metadata and purges a replaced one's chunks. -func HandPathWrites(nodes []string) ([]string, error) { - out := append([]string{"$KV." + delivery.AssignerWantsBucket + ".want.>"}, readGrants(delivery.AssignerWantsBucket, "want.>")...) - out = append(out, "$KV."+delivery.ControllerComposedBucket+".>") - out = append(out, readGrants(delivery.ControllerComposedBucket, "")...) - for _, node := range nodes { - w, err := ComposedWrites(node) - if err != nil { - return nil, err - } - bucket, _ := delivery.ComposedBucketFor(node) - out = append(out, w) - out = append(out, readGrants(bucket, "")...) - files, err := DeclarationFilesWrites(node) - if err != nil { - return nil, err - } - stream := "OBJ_" + DeclarationFilesBucket + node - out = append(out, files, "$JS.API.STREAM.INFO."+stream, "$JS.API.DIRECT.GET."+stream+".>", - "$JS.API.STREAM.MSG.GET."+stream, "$JS.API.STREAM.PURGE."+stream) - } - return out, nil -} - // 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 @@ -182,15 +69,3 @@ func RaiseAfterDeclarations(r Raiser) error { } return AssertMeshConsumers(r) } - -// TakeInConsumer is the durable consumer the controller takes done keys in from, on the stream of each of TakeInBuckets -// (novox/hq ADR 0544 §1). -const TakeInConsumer = "mesh-controller-take-in" - -// TakeInBuckets are the buckets the take-in consumes: the indexer's modules (each module's trunk value) and the builder's -// builds (each build's key). -var TakeInBuckets = []string{delivery.ModulesBucket, delivery.BuilderBuildsBucket} - -// ConfigFollowConsumer is the durable consumer the controller follows mesh-config's resolved values with, on that -// bucket's stream (novox/hq ADR 0545). -const ConfigFollowConsumer = "mesh-controller-config-follow" diff --git a/go/buscompose/controller_buckets.go b/go/buscompose/controller_buckets.go deleted file mode 100644 index 775f1c9..0000000 --- a/go/buscompose/controller_buckets.go +++ /dev/null @@ -1,247 +0,0 @@ -package buscompose - -import ( - "context" - "fmt" - "time" - - "git.novox.be/novox/mesh-sdk/go/delivery" - "github.com/nats-io/nats.go/jetstream" -) - -// The controller's own key-value buckets (novox/hq to-be 45 §1, §6, §7). -// -// **What the controller must remember across its own restart, it keeps on the bus.** A call's -// outcome lived in the memory of the process that served it (novox/hq issue 265), so a controller -// replaced while a push ran answered "no such call" for the one thing its caller had been told to -// ask about. The bus already outlives the controller and is the shape ADR 0201 gives a module's -// current state: one value per key, written by one owner, read by anybody granted it. These are the -// controller's, written by it alone — the writers table of to-be 45 §1 — and asserted on every start -// like the streams, so a bus raised from nothing has them before the first call is served. - -// CallsBucket keeps every call of the mesh's own verbs and what came of it; HandActsBucket every act -// a person did by hand, with why; ConditionsBucket every condition open now (to-be 45 §2), one key -// each, and ConditionHistoryBucket every transition of one — raised, changed, silenced, cleared — -// for ninety days. -// -// **The history is a bucket of its own** because its keys expire and an open condition's must not: -// a bucket has one age for every key, and a condition open longer than the history is kept would -// otherwise vanish from the store while still true. -var ( - CallsBucket = BucketName(ControllerSeat, "calls") - HandActsBucket = BucketName(ControllerSeat, "hand-acts") - ConditionsBucket = BucketName(ControllerSeat, "conditions") - ConditionHistoryBucket = BucketName(ControllerSeat, "condition-history") - // LeaseBucket holds the controller's lease (to-be 45 §6): one key, `holder`, which the instance - // allowed to act writes by compare-and-set and renews; its revision when taken is the epoch. - LeaseBucket = BucketName(ControllerSeat, "lease") - // AskedBucket keeps what the controller asked the operator about its conditions (novox/hq ADR 0259): - // each ask by its id, its options and the actions they stand for, how it ended and whether the - // controller acted on its warrant — so a restart neither asks twice nor acts twice. - AskedBucket = BucketName(ControllerSeat, "asked") - // DeliveryMergesBucket holds the merges mesh-delivery announced it made, each with the ask whose Approve it merged - // on (novox/hq ADR 0420 §7, issue 1025): per merge a key `///` holding the - // announcement, kept DeliveryMergesKeptFor. What lets the build of a moved module register it from its new - // repository: the controller reads the ask's warrant from the router and accepts only the operator's Approve of - // those heads. Written by the controller alone, from the announcement, when it holds the lease. - DeliveryMergesBucket = BucketName(ControllerSeat, "delivery-merges") -) - -// DeliveryMergesKeptFor is how long a merge mesh-delivery announced is kept: a week, as the events are. -const DeliveryMergesKeptFor = 7 * 24 * time.Hour - -// The controller's record mirrors (novox/hq ADR 0289, design 54, design 57 package F3): a bucket per kind of -// record, one key per record, written from the store by the lease holder only when the record changed, with -// one event on the controller's seat per write. Read by whatever names them in `reads` as -// `mesh-controller.` (ADR 0201 §1), written by nobody else. **No secret in either** (ADR 0201): an -// assignment's value names its module and node; a module's declared config is what its manifest, which is -// public, declares. -// -// AssignmentsBucket holds which module is assigned to which node, keyed `.`; and -// DeclaredConfigBucket each registered module's config declarations as its manifest holds them, keyed by -// the module: what mesh-config resolves a config value from (ADR 0348 clauses 2 and 4); and PredefinedBucket the -// list of predefined keys the mesh published (the newest trunk build's of the catalogue, ADR 0348 §7), one key -// `list`: what mesh-config publishes as mesh-config_predefined (design 57 K3, issue 662). -var ( - AssignmentsBucket = BucketName(ControllerSeat, MirrorAssignments) - DeclaredConfigBucket = BucketName(ControllerSeat, MirrorDeclaredConfig) - PredefinedBucket = BucketName(ControllerSeat, MirrorPredefined) - // DeclaredPackagesBucket holds, keyed by the node, each package the modules assigned to it declare, present or absent, - // with the declaring modules: what the pacman module's verbs read to say a package is declared (ADR 0507 §5). - DeclaredPackagesBucket = BucketName(ControllerSeat, MirrorDeclaredPackages) -) - -// The mirrors' local names, as a reader names them in `reads` after `mesh-controller.`. -const ( - MirrorAssignments = "assignments" - MirrorDeclaredConfig = "declared-config" - MirrorPredefined = "predefined" - // MirrorDeclaredPackages is the local name of the packages each node's modules declare (ADR 0507 §5, issue 1840/S3). - MirrorDeclaredPackages = "declared-packages" -) - -// The chain's buckets (novox/hq ADR 0408, issue 1089): builds, rollouts and assignments are one chain kept on the bus -// alone, one bucket each, one key per record, written by the lease holder by compare-and-set. The assignments bucket is -// AssignmentsBucket above, which keeps its name and its readers (ADR 0408 §3). Nothing is removed from them (§4), they keep -// ChainHistory earlier revisions of every key (§5), and a value is never cut (§6). -var ( - BuildsBucket = BucketName(ControllerSeat, ChainBuilds) - RolloutsBucket = BucketName(ControllerSeat, ChainRollouts) -) - -// **The assignments bucket keeps the marker age (MirrorMarkerTTL) it was made with as a mirror.** A stream that allows -// messages an age cannot be made to disallow them (the server refuses the update: "message TTL status can not be -// disabled", nats-server stream.go), so asserting the bucket without the age would fail the controller's start on a -// mesh that has the bucket. The chain never purges or removes a key (ADR 0408 §4), so no marker is ever written and the -// age ends nothing; it is kept only so the stream's configuration stays one the server accepts. -// -// The chain buckets' local names, as a reader names them in `reads` after `mesh-controller.`. -const ( - ChainBuilds = "builds" - ChainRollouts = "rollouts" -) - -// The chain buckets' bounds. ChainHistory is the bus's largest history (ADR 0408 §5). A value is at most ChainValueBytes, -// just under the server's largest message of a mebibyte, which also carries the subject and the headers (ADR 0408 §6): -// the controller writes none larger and the move refuses to. The buckets' size is the arithmetic of the survey of -// issue 981: 8585 builds of 1 to 40 KB, 1049 rollouts of about 2 KB a revision and 64 revisions, so 512 MiB for the two -// that grow, and the assignments, 285 of under a kilobyte, 64 MiB. -const ( - ChainHistory = 64 - ChainValueBytes = 1<<20 - 8<<10 - ChainBuildsBytes = 512 << 20 - ChainRolloutsBytes = 512 << 20 - ChainAssignmentBytes = 64 << 20 -) - -// GrantsBucket holds, keyed `.`, each fingerprint of a module whose bus credential the controller -// has issued (novox/hq ADR 0528 clause 2, ADR 0429 §6 rule 6, issue 1394): written by the controller alone once the bus's -// user list composed with the module's manifest at that fingerprint was sent to the bus's machine; read by the assigner, -// whose sixth rule moves a node's want to a fingerprint only once its grant exists. The name and layout are mesh-sdk's -// (delivery.ControllerGrantsBucket, ControllerGrantKey). -var GrantsBucket = BucketName(ControllerSeat, ControllerGrants) - -// ControllerGrants is the grants bucket's local name, as a reader names it in `reads` after `mesh-controller.`. -const ControllerGrants = "grants" - -// ChainBuckets are the three buckets of the chain: builds, rollouts and assignments. -func ChainBuckets() []string { return []string{BuildsBucket, RolloutsBucket, AssignmentsBucket} } - -// MirrorsWrittenByAnother are the controller's mirrors whose writer is not the controller, each with its reason: the rule -// that a mirror is written by the controller alone (TestTheMirrorsListAndTheWritersTableAgree) has these exceptions and no -// other. -var MirrorsWrittenByAnother = map[string]string{} - -// The mirrors' caps (design 54 §5): one value per key, no age, 64 KiB a value, 64 MiB a bucket. -const ( - MirrorValueBytes = 64 << 10 - MirrorBytes = 64 << 20 - // MirrorMarkerTTL is the longest a purge's marker is kept (design 54 §2): the bucket allows a - // per-message age for it, and the writer gives each marker a day. - MirrorMarkerTTL = 24 * time.Hour -) - -// PlanStateBuckets are the buckets of the plans' state that release 6 deletes (novox/hq ADR 0539 clause 13): the merge -// window's clock, the silent sends' waits and the building plans' hold. The controller deletes each at its first start -// and says it once (composed_start.go); none is asserted, granted or written any more. -var PlanStateBuckets = []string{BucketName(ControllerSeat, "merge-window"), BucketName(ControllerSeat, "rollout-silent"), - BucketName(ControllerSeat, "plans-building")} - -// AskedKeptFor is how long an ask is kept after it was made: a month, as the router keeps its own. -const AskedKeptFor = 30 * 24 * time.Hour - -// LeaseTTL is how long the lease's key lives unrenewed (to-be 45 §6): fifteen seconds, renewed -// every five. The bucket's age, so the bus forgets a holder that stopped renewing. -const LeaseTTL = 15 * time.Second - -// The bounds to-be 45 §6 sets for calls: the last thousand, or fourteen days, whichever is fewer. -// A call is two keys — its record, and its answer apart so a listing does not read every answer — -// so the stream holds twice as many messages as it keeps calls. -const ( - KeptCallsDurably = 1000 - CallsKeptFor = 14 * 24 * time.Hour - // CallAnswerBytes is the most of one answer kept: a whole declaration is far smaller, and an - // answer larger is cut and says so. - CallAnswerBytes = 64 << 10 - // HandActsKeptFor is as long as a condition's history (to-be 45 §2): an act by hand is read - // back beside what it addressed. - HandActsKeptFor = 90 * 24 * time.Hour - // ConditionHistoryKeptFor is how long a condition's transitions are kept (to-be 45 §2). - ConditionHistoryKeptFor = 90 * 24 * time.Hour -) - -// IsControllerBucket says a bucket is the controller's own, not a module's state nothing declares. -func IsControllerBucket(bucket string) bool { - return bucket == CallsBucket || bucket == HandActsBucket || bucket == ConditionsBucket || - bucket == ConditionHistoryBucket || bucket == LeaseBucket || bucket == AskedBucket || - bucket == AssignmentsBucket || bucket == DeclaredConfigBucket || bucket == PredefinedBucket || - bucket == DeclaredPackagesBucket || - bucket == DeliveryMergesBucket || bucket == BuildsBucket || bucket == RolloutsBucket || - bucket == delivery.HaveBucket || bucket == StatesAnnouncedBucket || - bucket == GrantsBucket || bucket == delivery.ControllerComposedBucket -} - -// ControllerBuckets are the controller's own buckets, in the order they are asserted. -func ControllerBuckets() []string { - return []string{LeaseBucket, CallsBucket, HandActsBucket, ConditionsBucket, ConditionHistoryBucket, AskedBucket, - AssignmentsBucket, DeclaredConfigBucket, PredefinedBucket, DeclaredPackagesBucket, - DeliveryMergesBucket, BuildsBucket, RolloutsBucket, delivery.HaveBucket, StatesAnnouncedBucket, GrantsBucket, - delivery.ControllerComposedBucket} -} - -// ControllerBucketsAsserter is what raising the controller's buckets needs of a connection. -type ControllerBucketsAsserter interface { - EnsureControllerBuckets() error -} - -// chainBucket is the options of a bucket of the chain: sixty-four revisions of each key, no age, a value up to -// ChainValueBytes, on disk. -func chainBucket(bucket, why string, bytes int64, markers time.Duration) jetstream.KeyValueConfig { - return jetstream.KeyValueConfig{ - Bucket: bucket, - Description: why, - History: ChainHistory, - MaxValueSize: ChainValueBytes, - MaxBytes: bytes, - Storage: jetstream.FileStorage, - LimitMarkerTTL: markers, - } -} - -// EnsureLeaseBucket creates the lease's bucket if absent and brings its options to match (to-be 45 -// §6). **Before the lease is taken, by any candidate**: it is the lease's own precondition, and asserting -// a bucket that exists changes nothing. File storage, so its revisions — the epochs — outlive a restart of -// the bus; one raised again from nothing is moved past the highest epoch issued when the lease is taken. -func EnsureLeaseBucket(ctx context.Context, js jetstream.JetStream) error { - if _, err := js.CreateOrUpdateKeyValue(ctx, jetstream.KeyValueConfig{ - Bucket: LeaseBucket, - Description: "the controller's lease (novox/hq to-be 45 §6): the key `holder`, written by compare-and-set " + - "by the one controller instance that may act and renewed every five seconds; its revision when taken " + - "is the epoch every declaration and plan write carries", - History: 1, - TTL: LeaseTTL, - MaxValueSize: 4 << 10, - MaxBytes: 1 << 20, - Storage: jetstream.FileStorage, - }); err != nil { - return fmt.Errorf("asserting bucket %s: %w", LeaseBucket, err) - } - return nil -} - -// HaveBucketConfig is the options of the have bucket (novox/hq ADR 0429 §1): delivery.HaveHistory revisions of each -// key, no age, 64 MiB on disk like the assignments. A module's have is JSON of about 500 bytes, but a node's own key -// carries its whole facts (its profile among them), several kilobytes on a desktop: a value is as large as the chain's -// buckets take, since a value is never cut (novox/hq ADR 0408 §6, issue 1162). -func HaveBucketConfig() jetstream.KeyValueConfig { - return jetstream.KeyValueConfig{ - Bucket: delivery.HaveBucket, - Description: "what each machine has of each module, one key `.` (and `.node` for the machine's " + - "own facts) (novox/hq ADR 0429, design 47): written by each machine's node-engine under its own node's keys " + - "alone, never by the controller; read by the delivery view and the watchman", - History: delivery.HaveHistory, - MaxValueSize: ChainValueBytes, - MaxBytes: 64 << 20, - Storage: jetstream.FileStorage, - } -} diff --git a/go/buscompose/engine.go b/go/buscompose/engine.go index 56d93ce..6426db7 100644 --- a/go/buscompose/engine.go +++ b/go/buscompose/engine.go @@ -74,11 +74,9 @@ func engineGrants(p Principal) (pub, sub []string) { 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); and, read, the controller's mirror's key - // of its node, which the node seat's verb packages compares it with until the lease write ends the mirror. + // 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)...) - pub = append(pub, readGrants(DeclaredPackagesBucket, p.Node)...) } return append(pub, tp...), append(sub, ts...) } diff --git a/go/buscompose/health_grants.go b/go/buscompose/health_grants.go index 925b248..29036e9 100644 --- a/go/buscompose/health_grants.go +++ b/go/buscompose/health_grants.go @@ -10,64 +10,94 @@ import ( "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). +// **The health holders' grants** (novox/hq ADR 0564 clauses 2, 5 and 8, Limits 2, 4 and 8; issue 1952 S5; ADR 0590). // -// - The condition keeper reads the controller's open conditions until the lease write, for what clauses 8 and 9 carry -// from them, and mesh-healers the controller's hand-act log, for S15 (Limits 2): both buckets the controller -// announces readable by nobody, so each is read by its named reader alone. The keeper, mesh-healers and the -// watchman read the controller's lease (Limits 8). 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 -// controller's consumer `controller` still stands; it may neither delete nor change that consumer (clause 5). It -// lists and shows DEAD_LETTERS, which is all it does before the lease write. +// - 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 the consumer `controller` on DEAD_LETTER_NOTICES and keys of the -// node-engine's acts bucket (Limits 4), never the backup's account `broker`, which only reads (ADR 0235). +// (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). -// The states the composer names its readers for, beside the lease (NamedReaders). -var ( - // ControllerConditions is the controller's open conditions, read by the keeper alone (ADR 0564 clauses 8, 9). - ControllerConditions = ControllerSeat + ".conditions" - // ControllerHandActs is the controller's hand-act log, read by mesh-healers alone of the four health holders (ADR - // 0564 Limits 2). - ControllerHandActs = ControllerSeat + ".hand-acts" - // HealersWould is what mesh-healers would have done while the lease names a controller: read by nobody (clause 8). - HealersWould = health.HealersModule + "." + health.WouldState -) +// 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 -// BusRemovalGrants are the whole authority of an account whose bus declares removals (ADR 0564 Limits 4, issue 1952 -// notes 6 and 7), each removal its grant: the controller's consumer on DEAD_LETTER_NOTICES read and deleted (the lease -// write's step, clause 5); the node-engine's acts bucket read (STREAM.INFO with a subject filter lists what a removal -// would take, so the tools read before they remove) and its keys removed, which nothing else may empty when it fills; the ASSIGNMENTS stream read and deleted whole (the lease -// write, research 072 row 33). A -// removal outside the set is refused, as the indexer's judge refuses it in a manifest (novox/hq ADR 0578 §1). -func BusRemovalGrants(removes []string) ([]string, error) { +// 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 `.`. +var ControllerLeftoverConsumers = []string{ + "EVENTS." + ControllerName, "EVENTS." + MergesConsumerName, "EVENTS.novox_mesh-delivery", "EVENTS.novox_mesh-catalog", + "CONTROL." + ControllerName, DeadLetterNoticesStream + "." + ControllerName, +} + +// ControllerLeftoverConsumersOn are ControllerLeftoverConsumers and, for each node given, the consumer the controller +// made for the node's engine (`EVENTS._mesh-host`, of restic's backed-up): the engine hears that event without a +// durable consumer, and the grants part composes none for it, so nothing reads it (issue 1126). +func ControllerLeftoverConsumersOn(nodes []string) []string { + out := slices.Clone(ControllerLeftoverConsumers) + for _, n := range nodes { + if safeSubject.MatchString(n) { + out = append(out, "EVENTS."+n+"_"+EngineModule) + } + } + return out +} + +// ControllerLeftoverBuckets are the ended controller's buckets, each by its local name (`KV_mesh-controller_`). +var ControllerLeftoverBuckets = []string{"asked", "assignments", "builds", "calls", "checks-running", "composed", + "condition-history", "conditions", "declared-config", "declared-packages", "delivery-merges", "grants", "hand-acts", + "lease", "predefined", "rollouts", "states-announced"} + +// ControllerLeftoverStreams are the ended controller's streams, by their names: CONTROL, ASSIGNMENTS, each of its buckets' +// and, for each node given, the declaration files' object bucket the controller wrote (ADR 0486 §2). +func ControllerLeftoverStreams(nodes []string) []string { + out := []string{"CONTROL", AssignmentsStream} + for _, b := range ControllerLeftoverBuckets { + out = append(out, "KV_"+BucketName(catalogue.EndedControllerName, b)) + } + for _, n := range nodes { + if safeSubject.MatchString(n) { + out = append(out, "OBJ_"+DeclarationFilesBucket+n) + } + } + return out +} + +// BusRemovalGrants are the whole authority of an account whose bus declares removals (ADR 0564 Limits 4, ADR 0590 clause +// 6), each removal its grant: the node-engine's acts bucket read (STREAM.INFO with a subject filter lists what a removal +// would take, so the tools read before they remove) and its keys removed, which nothing else may empty when it fills; and +// the ended controller's leftovers, each consumer and each stream read and deleted by its name, for the nodes given (the +// declaration files' object buckets). A removal outside the set is refused, as the indexer's judge refuses it in a +// manifest (novox/hq ADR 0578 §1). +func BusRemovalGrants(removes, nodes []string) ([]string, error) { var out []string for _, r := range removes { switch r { - case catalogue.RemovesControllerDeadLetterConsumer: - out = append(out, "$JS.API.CONSUMER.INFO."+DeadLetterNoticesStream+"."+ControllerName, - "$JS.API.CONSUMER.DELETE."+DeadLetterNoticesStream+"."+ControllerName) case catalogue.RemovesEngineActs: out = append(out, "$JS.API.STREAM.INFO.KV_"+EngineActsBucket, "$KV."+EngineActsBucket+".>") - case catalogue.RemovesAssignmentsStream: - out = append(out, "$JS.API.STREAM.INFO."+AssignmentsStream, "$JS.API.STREAM.DELETE."+AssignmentsStream) + case catalogue.RemovesControllerLeftovers: + for _, c := range ControllerLeftoverConsumersOn(nodes) { + out = append(out, "$JS.API.CONSUMER.INFO."+c, "$JS.API.CONSUMER.DELETE."+c) + } + for _, s := range ControllerLeftoverStreams(nodes) { + out = append(out, "$JS.API.STREAM.INFO."+s, "$JS.API.STREAM.DELETE."+s) + } default: - return nil, fmt.Errorf("a bus account removes %q, which is not one of %s (novox/hq ADR 0564 Limits 4)", r, - strings.Join([]string{catalogue.RemovesControllerDeadLetterConsumer, catalogue.RemovesEngineActs, - catalogue.RemovesAssignmentsStream}, ", ")) + return nil, fmt.Errorf("a bus account removes %q, which is not one of %s (novox/hq ADR 0564 Limits 4, ADR 0590)", r, + strings.Join([]string{catalogue.RemovesEngineActs, catalogue.RemovesControllerLeftovers}, ", ")) } } return out, nil } -// removesAssignmentsStream is an account declared to remove the stream ASSIGNMENTS: the writers table's one exemption from -// stream definitions being the controller's, for deleting that one stream at the lease write (research 072 row 33). -func removesAssignmentsStream(p Principal, pattern string) bool { - return p.Kind == KindProcess && slices.Contains(p.Removes, catalogue.RemovesAssignmentsStream) && - pattern == "$JS.API.STREAM.DELETE."+AssignmentsStream +// 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 @@ -129,12 +159,6 @@ func WatchmanHears() []string { "mesh.mod.nats.event.bus.connection-closed", ProvAppliedSubjects, deadLetterPrefix + ">", - // And the lease write's (ADR 0582 clauses 4, 6; issue 774): the seat mesh-grants' three events, and the stream - // ASSIGNMENTS deleted, which with the controller's consumer deleted clears a removal that failed. - "mesh.seat." + catalogue.GrantsSeat + ".event." + catalogue.GrantsEventLeaseHanded, - "mesh.seat." + catalogue.GrantsSeat + ".event." + catalogue.GrantsEventLeaseAltered, - "mesh.seat." + catalogue.GrantsSeat + ".event." + catalogue.GrantsEventRemovalFailed, - "$JS.EVENT.ADVISORY.STREAM.DELETED." + AssignmentsStream, } } @@ -184,6 +208,5 @@ func deadLetterSourcesOf(r Records) []string { // 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 isController(p, pattern) || - (p.Kind == KindModule && p.Module == WatchmanModule && pattern == deadLetterPrefix+">") + return p.Kind == KindModule && p.Module == WatchmanModule && pattern == deadLetterPrefix+">" } diff --git a/go/buscompose/lease_cut.go b/go/buscompose/lease_cut.go deleted file mode 100644 index e1c2cb1..0000000 --- a/go/buscompose/lease_cut.go +++ /dev/null @@ -1,41 +0,0 @@ -package buscompose - -import "slices" - -// **The lease write's grants** (novox/hq ADR 0582 clause 3, issue 774). Composed with Records.LeaseCut, the list takes -// from the controller its write of the lease's key and every update, purge and delete of the lease's bucket, and gives -// the grants part the key `holder`, which it creates naming itself and deletes at a hand-back (clause 5). Whatever the -// records, the bus module's tools account may read and update the lease's bucket, for the tool that removes and -// restores its expiry; no other bucket's. - -// leaseStream is the lease bucket's stream. -var leaseStream = "KV_" + LeaseBucket - -// LeaseHolderWrite is the grants part's write of the lease's one key, from the cut. -func LeaseHolderWrite() string { return "$KV." + LeaseBucket + "." + leaseKey } - -// LeaseCutDenied are the controller's subjects of the lease's bucket that its `$JS.API.>` reaches and the cut refuses: -// the bucket's update (its expiry), purge and delete, and a message's delete (the key's last value). -func LeaseCutDenied() []string { - return []string{"$JS.API.STREAM.UPDATE." + leaseStream, "$JS.API.STREAM.PURGE." + leaseStream, - "$JS.API.STREAM.DELETE." + leaseStream, "$JS.API.STREAM.MSG.DELETE." + leaseStream} -} - -// LeaseExpiryGrants are the bus module's tools account's read and update of the lease's bucket, for its expiry. -func LeaseExpiryGrants() []string { - return []string{"$JS.API.STREAM.INFO." + leaseStream, "$JS.API.STREAM.UPDATE." + leaseStream} -} - -// writesTheLease is the controller holding the lease, or, from the cut, the grants part writing its key holder alone. -func writesTheLease(p Principal, pattern string) bool { - if p.Kind == KindController { - return true - } - return p.Kind == KindModule && p.Module == GrantsModule && p.LeaseCut && pattern == LeaseHolderWrite() -} - -// setsLeaseExpiry is the bus module's tools account updating the lease's bucket: the writers table's one exemption from -// stream definitions being the controller's beside ASSIGNMENTS' removal (ADR 0582 clause 3). -func setsLeaseExpiry(p Principal, pattern string) bool { - return p.Kind == KindProcess && len(p.Removes) > 0 && slices.Contains(LeaseExpiryGrants(), pattern) -} diff --git a/go/buscompose/membership.go b/go/buscompose/membership.go index 4b4b716..8601788 100644 --- a/go/buscompose/membership.go +++ b/go/buscompose/membership.go @@ -53,9 +53,6 @@ func MembershipFor(node string, d Declared, where Placements) Membership { m.Serves = append(m.Serves, Served{Subject: own + ".tool.{tool}", Queue: "serve." + d.Module}) } for _, s := range d.Holds { - if servedOnlyByTheController(s) { - continue // answered by the serving controller alone, never through a membership - } for _, verb := range s.Serves { m.Seats = append(m.Seats, SeatServed{Seat: s.Name, Verb: verb, Subject: seatToolSubject(s, verb, node)}) } diff --git a/go/buscompose/nats.go b/go/buscompose/nats.go index ccdb259..77c52e4 100644 --- a/go/buscompose/nats.go +++ b/go/buscompose/nats.go @@ -15,7 +15,6 @@ package buscompose import ( "errors" "fmt" - "git.novox.be/novox/mesh-sdk/go/announce" "regexp" "slices" "sort" @@ -32,10 +31,9 @@ import ( type Kind string const ( - KindModule Kind = "module" - KindNode Kind = "node" - KindController Kind = "controller" - KindEnrolment Kind = "enrolment" + KindModule Kind = "module" + KindNode Kind = "node" + KindEnrolment Kind = "enrolment" // KindPerson is somebody reaching the mesh's tools from a workstation (design 25 §7). Its // authority is a list of tools and nothing else — not control, not declarations, not builds, // and no ability to answer anything, because a person asks. @@ -108,8 +106,9 @@ type Principal struct { // Username and inbox are ... Process string - // FilesFor are the nodes whose declaration-file buckets this principal writes, for the controller alone (novox/hq ADR 0486 §2): - // every node of the mesh, one bucket each. + // FilesFor are the nodes whose declaration-file buckets (novox/hq ADR 0486 §2) this principal may remove, for the bus + // module's tools removing the ended controller's leftovers alone (ADR 0590 clause 6): every node of the mesh, one + // bucket each. FilesFor []string Emits []string @@ -145,30 +144,15 @@ type Principal struct { // permission and nothing beside it. Invokes []string - // AsksIssued are the tools the controller asks in its own name for a credential a provider's software - // issued, each `.` (novox/hq ADR 0305): on the subject that names its caller, so the tool - // knows the controller asks. The vault's holder is granted the same tools in its own name (issue 723, - // vaultCalls); the controller's grant goes with its path in R1 (issue 682). Composed from the records, so the installer's first user list — a controller - // with no records — carries none, and moves with issue 464 like the rest of the controller's caller-named - // grants. + // AsksIssued are, for the vault's holder, the tools it asks in its own name for a credential a provider's software + // issued, each `.` (novox/hq ADR 0305, issue 723, vaultCalls): on the subject that names its caller, + // so the tool knows the vault asks. AsksIssued []string - // AsksTheVault are the vault's verbs the controller asks in its own name (VerbsTheControllerAsksTheVault, - // issue 675), those the vault's holder serves: on the subject that names its caller, so the vault knows the - // controller asks. Composed from the records like AsksIssued, so the installer's first user list carries none. - AsksTheVault []string - - // AsksTheConfig are the mesh-config verbs the controller asks in its own name (VerbsTheControllerAsksTheConfig, - // issue 925), those mesh-config's holder serves: on the subject that names its caller, so mesh-config takes a - // value as the controller carries it (ADR 0348 §8). Composed from the records like AsksTheVault, so the - // installer's first user list carries none. - AsksTheConfig []string - // AsksTheDesk says this principal is the vault's holder, which asks the desks' secret prompt in its own name // (novox/hq ADR 0349 clause 9, issue 723): `node-launcher.secret` on any machine, on the subject that names // its caller, and nothing else of that seat. With AsksIssued on the same principal, the vault's whole reach - // outside its own seat: it asks each issuing provider's tool in its own name (clause 10). The controller - // keeps its own grant to both until R1 (issue 682), for the modules still filled the old way (clause 17). + // outside its own seat: it asks each issuing provider's tool in its own name (clause 10). AsksTheDesk bool // State is the local names of the state this principal's module keeps, and Reads the state of @@ -202,18 +186,10 @@ type Principal struct { // Removes are, for a process or tools bundle account whose bus declares them, what it may remove from the bus // (novox/hq ADR 0564 Limits 4): its whole authority is then BusRemovalGrants of them. Removes []string - // LeaseCut is Records.LeaseCut on the controller and the grants part (novox/hq ADR 0582 clause 3): the controller - // loses its write of the lease, and the grants part may write the key `holder`. - LeaseCut bool // DeadLetterSources are, for the watchman, the streams a dead letter it keeps may come from, by name // (deadLetterSourcesOf, novox/hq ADR 0564 clause 5): never a bucket's. DeadLetterSources []string - // WitnessesController is a machine principal whose node-engine witnesses the controller's upgrades: - // the machine runs the controller (novox/hq ADR 0236, lease/witness.go). It may read the lease's one - // key, and nothing else of the bucket, so it can judge a new controller build and put the previous - // one back. - WitnessesController bool // Checks are the tools a machine principal's node-engine asks as a declared health check, each // `.` (novox/hq ADR 0240, to-be 48 §3): asked of the instance on its own machine and // nowhere else. @@ -302,17 +278,18 @@ var VerbsTheSelfCheckAsks = []SeatVerb{{Seat: "node-intrusion-prevention", Verb: // takes before it asks its first tier (novox/hq ADR 0404). var VerbsTheBusStepAsks = []SeatVerb{{Seat: "node-backup", Verb: "now"}} -// VerbsTheControllerAsksForASecret are the seat verbs `give` calls (novox/hq ADR 0259 §10): the operator's -// desk opens a prompt that does not show what is typed, and answers it sealed to the controller's call. -var VerbsTheControllerAsksForASecret = []SeatVerb{{Seat: "node-launcher", Verb: "secret"}} +// 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"}} -// ControllerOnly are the subjects the controller alone may publish, 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 controller asks: only the bus makes that true. -func ControllerOnly() []string { +// 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 VerbsTheControllerAsksForASecret { + 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+".*") } @@ -365,42 +342,6 @@ func MaySubscribe(perms Permissions, subject string) bool { return false } -// VerbsTheControllerAsksTheDeliveryOwner are the mesh-delivery seat's verbs the controller calls (novox/hq -// ADR 0239): its self-check reads `stalled`, and healer H2 takes the one transition the table allows -// through `close`. A mesh seat's verb is flat: no machine in the subject. -// -// And, since novox/hq ADR 0259, `release` and `stop`: the controller asks the operator for them about a -// delivery held past its bound, and calls them on the operator's warrant, with its why. -// -// And, since novox/hq ADR 0282, `times`: the self-check reads the delivery times to say a delivery over its budget. -// -// And, since novox/hq ADR 0306, `groups`: a merge heard from a head waits for the window while its delivery group -// has a member still to merge. -var VerbsTheControllerAsksTheDeliveryOwner = []SeatVerb{{Seat: "mesh-delivery", Verb: "stalled"}, - {Seat: "mesh-delivery", Verb: "close"}, {Seat: "mesh-delivery", Verb: "release"}, {Seat: "mesh-delivery", Verb: "stop"}, - {Seat: "mesh-delivery", Verb: "times"}, {Seat: "mesh-delivery", Verb: "groups"}} - -// VerbsTheControllerAsksTheVault are the mesh-vault seat's verbs the controller calls in its own name (novox/hq -// ADR 0349 clauses 8 to 10, design 57's V2, issue 675): it carries a value given at its terminal, typed at a desk -// or issued by a provider's software into the vault, sealed to a key the vault made for the call, and asks a -// rotation. The vault takes these from the controller alone, which it knows from the subject that names its -// caller; granted as Principal.AsksTheVault says. -// -// And `move-asks` (design 57's V3, issue 676): the controller copies its record of asks at a desk, which holds no -// value, into the vault's. -// -// And `get` (novox/hq ADR 0349 §16, issue 755): the controller asks for a core module's own credential sealed to its -// machine's key, which it carries in that machine's declaration and cannot open (mesh-vault's core.go). -var VerbsTheControllerAsksTheVault = []SeatVerb{{Seat: "mesh-vault", Verb: "accept"}, - {Seat: "mesh-vault", Verb: "secret-ask"}, {Seat: "mesh-vault", Verb: "issued"}, {Seat: "mesh-vault", Verb: "rotate"}, - {Seat: "mesh-vault", Verb: "move-asks"}, {Seat: "mesh-vault", Verb: "get"}} - -// VerbsTheControllerAsksTheConfig are the mesh-config seat's verbs the controller calls in its own name (issue 925): -// `config`, the one verb mesh-config serves, which move-config uses to set each value and the settings verb to set, -// clear and propose through mesh-config. mesh-config takes a value as the controller carries it only from the -// subject that names the controller as caller (ADR 0348 §8); granted as Principal.AsksTheConfig says. -var VerbsTheControllerAsksTheConfig = []SeatVerb{{Seat: "mesh-config", Verb: "config"}} - // 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"} @@ -444,124 +385,6 @@ func NodeLoginsReads(node string) []string { return readGrants(delivery.NodeLoginsBucket, node) } -// VerbsTheControllerActsOnAWarrant are the other seat verbs the controller calls when the operator's warrant -// chooses them (novox/hq ADR 0259): a machine's service restarted, and a walk started or stopped through the -// controller's own `plans`, a machine's held builds sent by its own `push` and the builds held after a failed release -// sent by its own `upgrade release-backlog` (novox/hq issue 1732). Named one by one; a node seat's on any machine, a -// mesh seat's flat. -var VerbsTheControllerActsOnAWarrant = []SeatVerb{{Seat: "node-service-manager", Verb: "restart"}, - {Seat: ControllerSeat, Verb: "plans"}, {Seat: ControllerSeat, Verb: "push"}, {Seat: ControllerSeat, Verb: "upgrade"}} - -// VerbsTheControllerAsksTheOperatorChannel are the operator channel's verbs the controller calls (novox/hq -// ADR 0380): `tell`, one plain message with no condition behind it — a module's own secret given, named, -// where and when and by whom, asking nothing. A mesh seat's verb is flat: no machine in the subject. -var VerbsTheControllerAsksTheOperatorChannel = []SeatVerb{{Seat: "operator-channel", Verb: "tell"}} - -// DegradableGrant is a publish grant whose absence from the installer's carried first-user list is not a -// bug in the template: a fallback covers the refusal. -type DegradableGrant struct { - Subject string - Reason string -} - -// DegradableGrants are the only publish grants TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose -// may find missing from the installer's carried list without failing (novox/hq issue 835, the forge check -// of mesh-controller#345): a change that only edits mesh-host's examples can never satisfy a check that -// reads the mesh-host commit the machines actually run, so the carried list lags behind the controller's -// derived one until mesh-host's own module next builds. operator-channel.tell is the one grant the -// controller already has a fallback for: refused (no holder, or the bus refuses it), the controller raises -// the secret-given condition exactly as before (ADR 0380 §9), so an older carried list still comes up and -// works. Every other grant stays required — missing one of those is a controller that comes up and is -// refused on the first thing it tries, with no fallback that covers it. -var DegradableGrants = []DegradableGrant{ - { - Subject: announce.Subject(ControllerSeat), - Reason: "refused: the controller's own announcement waits for its first push, and until it is kept every reader of " + - "its states is granted from its `reads` as before (novox/hq ADR 0495, issue 1790)", - }, - { - Subject: "$KV." + StatesAnnouncedBucket + ".>", - Reason: "refused: no announcement is kept until the controller's first push, and until then every reader is " + - "granted from its `reads` as before (novox/hq ADR 0495, issue 1790)", - }, - { - Subject: "mesh.seat." + ControllerSeat + ".tool.push", - Reason: "refused: the ask of a held move (Release) is answered, and the refusal told, while the operator's own " + - "push at the terminal still sends the machine (issue 1732; ADR 0259)", - }, - { - Subject: "mesh.seat." + ControllerSeat + ".tool.upgrade", - Reason: "refused: the ask of the builds held after a failed release is answered, and the refusal told, while the " + - "operator's own `upgrade release-backlog` at the terminal still releases them (issue 1732; ADR 0259)", - }, - { - Subject: "mesh.seat." + VerbsTheControllerAsksTheOperatorChannel[0].Seat + ".tool." + VerbsTheControllerAsksTheOperatorChannel[0].Verb, - Reason: "refused: the controller raises the secret-given condition as before (ADR 0380 §9)", - }, - { - Subject: "$JS.ACK.EVENTS." + MergesConsumerName + ".>", - Reason: "carried only at genesis: the controller composes its own permissions and pushes them to the bus at " + - "its first push, which is when it acknowledges a merge (ADR 0407 decision 3, issue 973)", - }, - { - Subject: "$KV." + DeclaredPackagesBucket + ".>", - Reason: "carried only at genesis: the controller composes its own permissions and pushes them to the bus at its " + - "first push, and keeps the packages each node declares only after that, holding the lease (ADR 0507 §5, issue 1840)", - }, - { - Subject: "mesh.seat." + ControllerSeat + ".event.artifacts-gone", - Reason: "refused: the take-in is refused all the same and says why; only the builder's rebuild waits for the " + - "controller's first push, and the builder also asks held when a module's value or its own start comes (novox/hq " + - "issue 1937)", - }, - { - Subject: "mesh.seat." + ControllerSeat + ".event.sent", - Reason: "refused: the controller says a tier sent to a machine only for outsiders, and a send that could not " + - "be said is printed and goes on (ADR 0406 §5, issue 974)", - }, - { - Subject: "$KV." + DeliveryMergesBucket + ".>", - Reason: "refused: the controller then keeps no merge mesh-delivery announced, so the build of a moved module is " + - "refused as it is without the operator's Approve — failing closed (ADR 0420 §7, issue 1025)", - }, - { - Subject: "$KV." + BuildsBucket + ".>", - Reason: "carried only at genesis: the controller composes its own permissions and pushes them to the bus at its " + - "first push, and writes the chain only after that, holding the lease (ADR 0408 §2 and §8, issue 1089)", - }, - { - Subject: "mesh.seat." + ControllerSeat + ".event.bucket-deleted", - Reason: "refused: nothing is deleted at genesis; the controller composes its own permissions into the bus module's " + - "composition before a later start's deletions (novox/hq ADR 0539 clause 13)", - }, - { - Subject: "$KV." + delivery.ControllerComposedBucket + ".>", - Reason: "carried only at genesis: the controller composes its own permissions into the bus module's composition, " + - "and writes a composition's digest only after the bus applied it (novox/hq ADR 0539 clauses 2 and 7)", - }, - { - Subject: "$JS.ACK.KV_" + delivery.ModulesBucket + "." + TakeInConsumer + ".>", - Reason: "refused: an acknowledgement refused leaves the key delivered again after its wait, and the take-in's first " + - "batch with something new composes the bus's node, which grants it (novox/hq ADR 0544 §1)", - }, - { - Subject: "$JS.ACK.KV_" + delivery.BuilderBuildsBucket + "." + TakeInConsumer + ".>", - Reason: "refused: an acknowledgement refused leaves the key delivered again after its wait, and the take-in's first " + - "batch with something new composes the bus's node, which grants it (novox/hq ADR 0544 §1)", - }, - { - Subject: "$KV." + GrantsBucket + ".>", - Reason: "carried only at genesis: the controller composes its own permissions and pushes them to the bus at its " + - "first push, and writes a grant only after a send of the bus's user list, which comes after that (ADR 0528 " + - "clause 2, issue 1394)", - }, - { - Subject: "$KV." + RolloutsBucket + ".>", - Reason: "carried only at genesis: the controller composes its own permissions and pushes them to the bus at its " + - "first push, and writes the chain only after that, holding the lease (ADR 0408 §2 and §8, issue 1089)", - }, -} - // 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} @@ -596,8 +419,6 @@ func (p Principal) Username() string { return p.Login } return "node." + p.Node - case KindController: - return "controller" case KindProvisioner: // Named for the machine, as a node is: one client per machine, whichever module runs it, and never // `.`, which is the module's own account or the tool runner's. @@ -666,7 +487,7 @@ func (p Principal) inbox() string { // 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 controller's alone (ControllerOnly), + // 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 @@ -741,13 +562,10 @@ func PermissionsFor(p Principal) (Permissions, error) { } if p.Kind == KindProcess && len(p.Removes) > 0 { - pub, err := BusRemovalGrants(p.Removes) + pub, err := BusRemovalGrants(p.Removes, p.FilesFor) if err != nil { return Permissions{}, err } - // And the lease's bucket's expiry, read and set, by the bus module's tool the grants part calls at the lease - // write and its hand-back (novox/hq ADR 0582 clauses 3, 5): the one bucket whose stream it may update. - pub = append(pub, LeaseExpiryGrants()...) sort.Strings(pub) if err := CheckWriters(p, pub); err != nil { return Permissions{}, err @@ -758,160 +576,6 @@ func PermissionsFor(p Principal) (Permissions, error) { var pub, sub []string switch p.Kind { - case KindController: - // The controller owns the mesh's own traffic and the streams. It is the only writer of - // stream definitions (design 25 §3), so it alone reaches the JetStream API — and it alone - // issues memberships (novox/hq ADR 0160), which it publishes into the assignments stream - // after each push; refused by the server on 2026-10-01 until this line named them. - // - // **Not what the machines say** (novox/hq to-be 45 §1): `mesh.control.>` is subscribed, never - // published — a machine's report has one writer, its node-engine, and the controller granted - // that subject would be a second (the writers table refuses it). It was granted from the first - // composition and nothing ever published under it. - pub = []string{"mesh.node.>", "mesh.assignment.>", "$JS.API.>"} - // **And where its consumers deliver.** A push consumer delivers on `_DELIVER.`, - // and a client bound to it subscribes exactly that; the server refused it for every - // principal the first time one bound a consumer (2026-09-28). Each kind below is granted - // its own consumers' delivery subjects and no other's. - sub = []string{"mesh.control.>", "$JS.API.>", "_DELIVER." + ControllerName, "_DELIVER." + ControllerName + ".>"} - - // Work the mesh's own flows submit to a role, and the outcomes they wait on (ADR 0121). A - // build is the one today: the controller asks, and reads the answer from the seat's event - // like the catalogue does — which is why no holder needs to publish into anybody's inbox. - // A node-scoped seat's work subject carries no node (novox/hq ADR 0190): the ask goes to - // the role, and whichever machine holding it is idle takes it. - // **And what the mesh says it did** (novox/hq ADR 0134). The control plane states its own - // facts under the seat it holds, because a role's events belong to the role and keep their - // address while the holder is replaced. Named one by one rather than as a whole namespace: - // least authority, and a fact nothing states is authority nobody uses. - for _, event := range ControllerStates { - pub = append(pub, seatEventSubject(ControllerSeat, event)) - } - - // Every module's tools: **the control plane is the way in** (novox/hq ADR 0095). A person - // or an agent asks through it and every question passes one process where an audit - // belongs — so it, alone among principals, may call any tool by name. The first `ask` on - // the new bus was refused the publish (2026-09-28). - pub = append(pub, "mesh.mod.*.tool.>") - - // **And the mesh's own verbs, as the seat it holds** (novox/hq ADR 0132, ADR 0154): - // `status`, `push`, `assign` are the mesh-controller seat's tools, served by its holder. The - // whole verb namespace of its own seat rather than a list: the list is the seat's protocol, - // which this package mirrors rather than reads, and a verb the seat does not declare is a - // subject nothing publishes. - sub = append(sub, "mesh.seat."+ControllerSeat+".tool.>") - // And says so (novox/hq ADR 0197): it answers discovery for the seat it serves. - sub = append(sub, announcing(ControllerSeat)...) - // And the verbs the self-check reads with, of any machine's holder (novox/hq to-be 45 §4). - for _, v := range VerbsTheSelfCheckAsks { - pub = append(pub, "mesh.seat."+v.Seat+".tool."+v.Verb+".*") - } - // And the bus's planned step: a snapshot taken now, before the bus is replaced (ADR 0236). - for _, v := range VerbsTheBusStepAsks { - pub = append(pub, "mesh.seat."+v.Seat+".tool."+v.Verb+".*") - } - // And the operator's desk, for a secret given there (ADR 0259 §10). - for _, v := range VerbsTheControllerAsksForASecret { - pub = append(pub, "mesh.seat."+v.Seat+".tool."+v.Verb+".*") - } - // And the operator channel, a mesh seat, asked flat: one message when a secret is given (ADR 0380). - for _, v := range VerbsTheControllerAsksTheOperatorChannel { - pub = append(pub, "mesh.seat."+v.Seat+".tool."+v.Verb) - } - // And the delivery's owner, a mesh seat, asked on its flat subjects (ADR 0239). - for _, v := range VerbsTheControllerAsksTheDeliveryOwner { - pub = append(pub, "mesh.seat."+v.Seat+".tool."+v.Verb) - } - // And the verbs a warrant chooses (novox/hq ADR 0259): a node seat's on any machine, its own flat. - for _, v := range VerbsTheControllerActsOnAWarrant { - if v.Seat == ControllerSeat { - pub = append(pub, "mesh.seat."+v.Seat+".tool."+v.Verb) - continue - } - pub = append(pub, "mesh.seat."+v.Seat+".tool."+v.Verb+".*") - } - // And asking the operator (novox/hq ADR 0259): an ask and its cancel under its own name, its warrants - // heard under its own name, the record of its asks read under its own name — as any user of the seat, - // derived the same way, from the seat its holder declares. - tp, ts := seatTrafficGrants(SeatTrafficOf(ControllerSeat, nil, p.Uses, nil)) - pub = append(pub, tp...) - sub = append(sub, ts...) - // And asks who answers (novox/hq to-be 45 §4, D3): the self-check finds every seat's holder by - // the same discovery the console reads. The question only; the answers come to its own inbox. - pub = append(pub, "$SRV.INFO") - - // The events it reacts to, and its ack subject on the stream they arrive from - // (streams.go). **Each named, not a pattern**: `mesh.mod.*.event.>` would make the - // controller a subscriber to every event in the mesh, and its permission list would stop - // saying what it is for. The one wildcard is the emitter of a provider's standing (ADR - // 0224) — still two named events, from whichever module provides. The ack grant below is scoped per stream because the controller's - // consumer name is the same on both and `$JS.ACK.CONTROL.controller.>` does not cover a - // delivery from EVENTS — a consumer that cannot ack has every message redelivered for - // ever, refused by the list it already has. - sub = append(sub, ControllerFollows...) - pub = append(pub, "$JS.ACK.EVENTS."+ControllerName+".>", "$JS.ACK.EVENTS."+MergesConsumerName+".>") - // And the take-in's two durable consumers on the indexer's and the builder's buckets (novox/hq ADR 0544 §1): - // acknowledged after each key is taken in, and no other consumer of those streams. - for _, bucket := range TakeInBuckets { - pub = append(pub, "$JS.ACK.KV_"+bucket+"."+TakeInConsumer+".>") - } - - // **Where an enrolment's answer goes**, and `allow_responses` does not cover it. That - // permits one reply to the reply subject of a message the user received — and a message a - // JetStream consumer delivers has had that field claimed for the consumer's own ack address - // (design 25 §2), so the address the controller actually answers is the one the request - // carried in its payload, which is not a reply subject as the server understands it. - // - // Verified against a real server before this line existed: the answer was refused with - // "Permissions Violation for Publish to _INBOX.enrol.anchor…", and every enrolment on the - // mesh would have timed out while the controller logged success. - // - // **The enrolment inbox space, not a blanket `_INBOX.>`.** Design 25 §4 refuses that, and - // this is not it: nothing but an enrolling node ever subscribes under this prefix, each - // scoped to its own token's, so the controller publishing here is the mesh answering - // enrolments and can reach nothing else. - pub = append(pub, "_INBOX."+enrolmentPrefix+".>") - - // **And its own buckets** (novox/hq to-be 45 §1): the calls it served and the acts done by - // hand, which it alone writes. A put is a publish to the bucket's subject, which `$JS.API.>` - // does not cover; each bucket named, not `$KV.>`, which would let it write any module's state. - // And says what it keeps, as every module does (novox/hq ADR 0495 clause 2, issue 1790). - pub = append(pub, announce.Subject(ControllerSeat)) - for _, bucket := range ControllerBuckets() { - if bucket == delivery.HaveBucket { - continue // each node's node-engine writes its own keys of it, the controller none (ADR 0429 §1) - } - if bucket == LeaseBucket && p.LeaseCut { - continue // the lease write took it (novox/hq ADR 0582 clause 3): a controller started fails on the lease - } - pub = append(pub, "$KV."+bucket+".>") - } - // **And the object bucket a declaration's large files travel in** (novox/hq ADR 0486 §2, issue 1697): the - // controller alone writes it, as a publish to the bucket's own subjects, which `$JS.API.>` does not cover. - for _, node := range p.FilesFor { - writes, err := DeclarationFilesWrites(node) - if err != nil { - return Permissions{}, err - } - pub = append(pub, writes) - // And each node's compositions, which it alone writes (novox/hq ADR 0539 clause 2). - composed, err := ComposedWrites(node) - if err != nil { - return Permissions{}, err - } - pub = append(pub, composed) - } - // **And what the bus says about itself, read-only** (novox/hq to-be 45 §3, S9): a durable - // consumer that gave up on a message, or one that was deleted. The server already publishes - // both in the mesh's own account; the controller says each as a condition in the mesh's words. - // Named, not `$JS.EVENT.>`: the other advisories are every API call the mesh makes. - sub = append(sub, BusAdvisories...) - // **And what a consumer gave up on, kept and delivered again** (novox/hq issue 330): the notice - // acknowledged once the message is copied, the copy kept, and an event delivered again to the one - // consumer that gave it up. An ask to a seat is not delivered again, so no seat's queue is granted. - pub = append(pub, "$JS.ACK."+DeadLetterNoticesStream+"."+ControllerName+".>", deadLetterPrefix+">", - againPrefix+">") - case KindProvisioner: var err error if pub, sub, err = provisionerGrants(p); err != nil { @@ -935,15 +599,6 @@ func PermissionsFor(p Principal) (Permissions, error) { // 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()...) - // And, for an administrator (`*`, the operator), release 6's hand path (ADR 0539 clauses 2 and 10): a want written - // at the terminal, and a composition made by hand with mesh-hand-compose. - if administers(p.Invokes) { - hand, err := HandPathWrites(p.FilesFor) - if err != nil { - return Permissions{}, err - } - pub = append(pub, hand...) - } case KindEnrolment: // A leaked token is useless for anything but enrolling: it cannot read a declaration, hear @@ -963,20 +618,13 @@ func PermissionsFor(p Principal) (Permissions, error) { "enrolment: a token is issued for a node record, and that record's name is " + "the token's id") } - // **From the lease write a node enrols through the seat mesh-enrolment alone** (novox/hq ADR 0587 §4, §6): its - // user calls redeem in its own name, while the seat's holder serves it, and the controller's enrolment subject is - // kept only while the controller holds the lease. - pub = nil - if !p.LeaseHandedAway { - pub = append(pub, "mesh.control.enrol") - } - if p.Redeems { - pub = append(pub, EnrolmentRedeemCall(p)) - } - if len(pub) == 0 { - return Permissions{}, fmt.Errorf("the enrolment user of %s may enrol nowhere: the lease is handed away and "+ - "no holder of %s serves %s", p.Node, EnrolmentRedeem.Seat, EnrolmentRedeem.Verb) + // **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: @@ -1082,13 +730,11 @@ func PermissionsFor(p Principal) (Permissions, error) { // 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, the vault's public signing - // halves, read and watched, and the controller's lease key, read and watched, which says when the controller's key - // is no longer trusted (ADR 0550 §2). Read only. + // 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, "")...) - pub = append(pub, readGrants(LeaseBucket, leaseKey)...) } // 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). @@ -1129,11 +775,6 @@ func PermissionsFor(p Principal) (Permissions, error) { 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()...) - // And, from the lease write's cut, the lease's one key, which it creates naming itself and deletes at a - // hand-back (novox/hq ADR 0582 clauses 3, 5): never the bucket's other keys or its stream. - if p.LeaseCut { - pub = append(pub, LeaseHolderWrite()) - } } // The view alone reads the build seats' workers for the backlog of asks (issue 1887 A8a). if p.Module == ViewModule { @@ -1227,12 +868,8 @@ func PermissionsFor(p Principal) (Permissions, error) { "$JS.API.CONSUMER.INFO."+consumerStream(p)+"."+consumerDurable(p), "$JS.API.CONSUMER.MSG.NEXT."+consumerStream(p)+"."+consumerDurable(p)) - // 3. Seats it holds: full participation — but the controller's own seat, whose verbs only the serving - // controller answers, on its own connection (servedOnlyByTheController). + // 3. Seats it holds: full participation. for _, s := range p.Holds { - if servedOnlyByTheController(s) { - continue - } if s.isNewTraffic() { // Composed by SeatTrafficOf below, worker and all; only its tools are served here. for _, t := range s.Serves { @@ -1326,9 +963,6 @@ func PermissionsFor(p Principal) (Permissions, error) { // And its own streams' events (novox/hq ADR 0363 §2). pub = append(pub, ownStreamGrants(d.Module, d.Streams)...) for _, s := range d.Holds { - if servedOnlyByTheController(s) { - continue - } for _, t := range s.Serves { sub = append(sub, seatToolSubject(s, t, p.Node)) } @@ -1446,7 +1080,7 @@ func PermissionsFor(p Principal) (Permissions, error) { sub = append(sub, p.inbox()) } - if p.Kind == KindModule || p.Kind == KindNode || p.Kind == KindController { + if p.Kind == KindModule || p.Kind == KindNode { // Its own reply space, and nothing wider. sub = append(sub, p.inbox()) @@ -1459,52 +1093,14 @@ func PermissionsFor(p Principal) (Permissions, error) { } // **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 for one release beside these, - // so callers and runtimes move without a gap; their retirement is hq issue 464. - // - // **Not the controller's, this release.** Its grants are also the installer's first user list - // (TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose), judged against the node-engine the mesh - // runs, so a change to them waits on a node-engine delivery. It calls and serves on the subjects that name - // no caller meanwhile — a caller-named call to its seat reaches nobody and is asked again on those at once — - // and moves with issue 464. + // 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 - if p.Kind != KindController { - pub, sub = callerNamed(p.Username(), pub, sub) - if p.Kind == KindModule { - vaultsOwn = vaultCalls(p) - pub = append(pub, vaultsOwn...) - } - } else { - // **But the tools that answer a software-issued credential** (novox/hq ADR 0305 §2): those answer the - // controller alone, and know it only from the subject's last token, so it asks them in its own name — - // those tools, on any machine, and nothing else under `call`. - for _, key := range p.AsksIssued { - module, tool, ok := strings.Cut(key, ".") - if !ok || module == "" || tool == "" || strings.ContainsAny(key, "*> ") { - continue - } - pub = append(pub, "mesh.mod."+module+"."+CallKind+"."+tool+".*."+CallerToken(p.Username())) - } - // **And the vault's intake verbs** (issue 675): the vault takes a value from the controller alone, known - // from the subject's last token — the verbs its holder serves, flat, and nothing else of its seat. - for _, v := range VerbsTheControllerAsksTheVault { - if slices.Contains(p.AsksTheVault, v.Verb) { - pub = append(pub, "mesh.seat."+v.Seat+"."+CallKind+"."+v.Verb+"."+CallerToken(p.Username())) - } - } - // **And mesh-config's verb** (issue 925): it takes a trusted value from the controller alone, known from the - // subject's last token. - for _, v := range VerbsTheControllerAsksTheConfig { - if slices.Contains(p.AsksTheConfig, v.Verb) { - pub = append(pub, "mesh.seat."+v.Seat+"."+CallKind+"."+v.Verb+"."+CallerToken(p.Username())) - } - } - // And its consumer of mesh-config's resolved values, acknowledged once a change is composed (novox/hq ADR 0545): - // where mesh-config serves, as its verb, so genesis grants nothing of mesh-config. - if len(p.AsksTheConfig) > 0 { - pub = append(pub, "$JS.ACK.KV_"+ConfigResolvedBucket+"."+ConfigFollowConsumer+".>") - } + pub, sub = callerNamed(p.Username(), pub, sub) + if p.Kind == KindModule { + vaultsOwn = vaultCalls(p) + pub = append(pub, vaultsOwn...) } sort.Strings(pub) @@ -1514,23 +1110,16 @@ func PermissionsFor(p Principal) (Permissions, error) { if err := CheckWriters(p, pub); err != nil { return Permissions{}, err } - // What the controller alone may publish is denied to everybody else whose grant reaches it. + // What the vault's holder alone may publish is denied to everybody else whose grant reaches it. var deny []string - // And from the lease write's cut, the lease's bucket to the controller, whose `$JS.API.>` reaches it (novox/hq ADR - // 0582 clause 3): a controller started by mistake fails on the lease instead of taking it back. - if p.Kind == KindController && p.LeaseCut { - deny = append(deny, LeaseCutDenied()...) - } - if p.Kind != KindController { - for _, only := range ControllerOnly() { - for _, a := range pub { - if slices.Contains(vaultsOwn, a) { - continue - } - if SubjectsOverlap(a, only) { - deny = append(deny, only) - break - } + for _, only := range DeskOnly() { + for _, a := range pub { + if slices.Contains(vaultsOwn, a) { + continue + } + if SubjectsOverlap(a, only) { + deny = append(deny, only) + break } } } @@ -1547,13 +1136,13 @@ func PermissionsFor(p Principal) (Permissions, error) { 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 — and so does the controller. A node is asked one + // 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 — the controller reads the engine's words and records nothing. A + // 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 == KindController || p.Kind == KindNodeTools || p.Kind == KindNode || + AllowResponses: p.Kind == KindModule || p.Kind == KindNodeTools || p.Kind == KindNode || p.Kind == KindProvisioner, }, nil } @@ -1569,7 +1158,7 @@ func vaultCalls(p Principal) []string { } var out []string if p.AsksTheDesk { - for _, v := range VerbsTheControllerAsksForASecret { + for _, v := range VerbsTheVaultAsksForASecret { out = append(out, "mesh.seat."+v.Seat+"."+CallKind+"."+v.Verb+".*."+token) } } @@ -1602,13 +1191,6 @@ func seatSubject(s Seat, kind, verb string) string { // 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. -// servedOnlyByTheController says a seat's verbs are answered by the serving controller alone, on its own -// connection (the KindController grant), never by a module claiming the seat or a runtime carrying it: the -// controller's own seat. Its verbs decide what the mesh is — and `root-free` decides whether the router believes -// a verified sender (novox/hq ADR 0259 §8) — so a machine's runtime, whose credential an agent on that machine -// may hold, answering one would be an agent answering it (the confirmation review of 2026-10-09). -func servedOnlyByTheController(s Seat) bool { return s.Name == ControllerSeat } - func seatToolSubject(s Seat, verb, node string) string { base := seatSubject(s, "tool", verb) if s.Scope == "node" && node != "" { @@ -1682,8 +1264,6 @@ func consumerStream(p Principal) string { return "EVENTS" case KindNode: return "NODES" - case KindController: - return "CONTROL" } return "" } @@ -1694,8 +1274,6 @@ func consumerDurable(p Principal) string { return p.Node + "_" + p.Module case KindNode: return p.Node - case KindController: - return "controller" } return "" } @@ -1723,7 +1301,7 @@ func Compose(s Server, principals []Principal) (string, error) { 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 controller. Do not edit: the next composition overwrites it.\n") + 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") @@ -1777,7 +1355,7 @@ func ComposeAccounts(principals []Principal) (string, error) { 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 controller. Do not edit: the next\n") + 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") @@ -1992,15 +1570,9 @@ func discovering() []string { } // WitnessSubjects are the subjects a machine's node-engine publishes to witness the core builds it -// places (novox/hq ADR 0236, lease/witness.go): its own node tools' PING, and the lease's key where it -// runs the controller. Reads only: a write to the bucket is `$KV..>`, the controller's alone -// (the writers table). The answers come to the machine's own inbox. +// places (novox/hq ADR 0236): its own node tools' PING. The answers come to the machine's own inbox. func WitnessSubjects(p Principal) []string { - out := []string{pingSubject(p.Node)} - if p.WitnessesController { - out = append(out, leaseReadSubject(LeaseBucket)) - } - return append(out, CheckSubjects(p)...) + 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, @@ -2024,18 +1596,10 @@ func CheckSubjects(p Principal) []string { return out } -// pingSubject and leaseReadSubject are the controller's lease witness's subjects (mesh-controller internal/lease), copied -// with the composer so the grants it composes name them without the lease's code. +// 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 } -func leaseReadSubject(bucket string) string { - return "$JS.API.DIRECT.GET.KV_" + bucket + ".$KV." + bucket + "." + leaseKey -} - -const ( - nodeToolsService = "node-tools" - leaseKey = "holder" -) +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 { diff --git a/go/buscompose/provisioner.go b/go/buscompose/provisioner.go index fc25434..e279463 100644 --- a/go/buscompose/provisioner.go +++ b/go/buscompose/provisioner.go @@ -41,14 +41,15 @@ var ConfigResolvedBucket = BucketName("mesh-config", "resolved") // 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 controller's assignments mirror, -// where a provider's consumers are bound to it in their own machines' plans, which the provider machine's client -// reads to fill ${consumers:} with each one's credential (novox/hq ADR 0373, issue 679). It holds placement -// and plans, never a secret's value; the client writes none of it. **The seat's plans are read whole for the same +// 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{AssignmentsBucket, provisions.PlansBucket} +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 diff --git a/go/buscompose/raise.go b/go/buscompose/raise.go index f802655..9104f04 100644 --- a/go/buscompose/raise.go +++ b/go/buscompose/raise.go @@ -16,7 +16,7 @@ type Raiser interface { Ensurer } -// Raise asserts the mesh's streams, the controller's own consumers, and one consumer per node. +// 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 diff --git a/go/buscompose/seattraffic.go b/go/buscompose/seattraffic.go index 715c4b3..243f7ac 100644 --- a/go/buscompose/seattraffic.go +++ b/go/buscompose/seattraffic.go @@ -63,9 +63,6 @@ func SeatTrafficOf(module string, holds, uses, watches []Seat) SeatTraffic { // 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. - if servedOnlyByTheController(s) { - continue - } for _, e := range s.Emits { if s.Scope == "node" && perMachineEvents[e] { continue diff --git a/go/buscompose/state.go b/go/buscompose/state.go index e0077a0..b64461e 100644 --- a/go/buscompose/state.go +++ b/go/buscompose/state.go @@ -309,9 +309,9 @@ func RaiseBuckets(a BucketAsserter, buckets []Bucket) (undeclared []string, err 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; so are - // the controller's own buckets (novox/hq to-be 45 §1). - if !declared[n] && !IsCancelledSet(n) && !IsControllerBucket(n) { + // 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) } } diff --git a/go/buscompose/streams.go b/go/buscompose/streams.go index 6af9e61..3544acd 100644 --- a/go/buscompose/streams.go +++ b/go/buscompose/streams.go @@ -144,26 +144,14 @@ func OriginalOfAgain(subject string) (string, bool) { // MeshStreams is the foundation set, in the order a person reads it. // -// **CONTROL names its subjects rather than taking `mesh.control.>`**, because heartbeats live -// under that prefix and must not be persisted: a lost heartbeat is the next heartbeat, and a -// stream of them is a stream of the least valuable messages the mesh sends, competing for the -// same retention as the ones that matter. -// // **EVENTS filters on the `event` token**, which is the reason that token exists. A module's // namespace carries both its events and its tool calls; a filter of `mesh.mod.*.>` would persist // every tool invocation in the mesh, and a tool call must never be persisted (design 25 §3 keeps // tools on core NATS, where a lost call is a timeout the caller already handles). func MeshStreams() []Stream { return []Stream{ - { - Name: "CONTROL", - // A build's outcome is no longer here: it is the build-machine seat's own event, so one - // publish reaches whoever asked, the controller and the catalogue (novox/hq ADR 0121). - Subjects: []string{"mesh.control.*.report", "mesh.control.enrol"}, - Retention: RetentionWorkQueue, - Why: "the store-window guarantee (ADR 0083): the controller naks with a delay while its " + - "store is away and the message is redelivered; nothing is dropped", - }, + // CONTROL, the ended controller's work queue of what nodes reported, is no longer the mesh's (novox/hq ADR 0590 + // clause 6): not declared here, which would make it again; the bus module's tools remove it. { Name: "NODES", Subjects: []string{"mesh.node.*.declare"}, @@ -235,18 +223,6 @@ const ProvAppliedStream = "PROV_APPLIED" // all (ProvisionerAppliedSubject). const ProvAppliedSubjects = "prov.applied.>" -// eventsTheControllerFollows are the events the controller's events consumer takes: all it follows but the forge's -// merges, which the merges consumer takes (MergesConsumerName). -func eventsTheControllerFollows() []string { - var out []string - for _, subject := range ControllerFollows { - if subject != ControllerFollows[3] { - out = append(out, subject) - } - } - return append(out, ControllerHears...) -} - // 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 @@ -321,147 +297,17 @@ func Overlaps() []string { return clashes } -// The mesh's own consumers. -// -// A seat's streams and a module's consumers are derived from declarations (derived.go). These two -// are not: **the controller is not a module and files no manifest**, so its authority and its -// subscriptions cannot come from a declaration that does not exist. They are named here, where the -// mesh's own streams are named, and narrowly — a controller subscribing `mesh.mod.*.event.>` would -// hear every event in the mesh, which it has no business doing and which would make its permission -// list stop explaining anything. - -// ControllerName is the controller's durable consumer on each stream it reads, and the name its -// ack subject is derived from (nats.go: `$JS.ACK..controller.>`). -const ControllerName = "controller" - -// MergesConsumerName is the controller's durable consumer of the forge's merged event on EVENTS, apart from the -// events consumer (novox/hq ADR 0407 decision 3, issue 973). A merge is never dropped, so this consumer has no -// max-deliver, which would send a merge held five times to DEAD_LETTERS to wait for a person, and no healer -// resets it, which would drop the merges it holds. A consumer of its own because both of those are properties of -// a consumer, not of a subject. -const MergesConsumerName = "controller-merges" - -// ControllerSeat is the role the control plane holds, and ControllerStates are the facts it states -// under it (novox/hq ADR 0134). -// -// **Written here as well as in `link`, and a test keeps them agreeing.** `link` imports `broker`, so -// `broker` cannot import `link`; a grant naming a subject the controller never publishes is authority -// nobody uses, and a controller publishing one the grant omits is refused at the moment it has -// something to say. -const ControllerSeat = "mesh-controller" - -var ControllerStates = []string{"applied", "refused", "built-before", - // What is wrong, said as it changes (novox/hq to-be 45 §2): a condition raised, changed in - // severity, resolver or silence, and cleared. The operator-channel's holder and any other surface - // consume them; the controller tells nobody itself. - "condition-raised", "condition-changed", "condition-cleared", - // And the self-check's heartbeat, at the end of every run (to-be 45 §4, S10): watched from a - // machine that is not the control node, so the controller going quiet is itself said. - "doctor-heartbeat", - // And a value given by hand, replaced after its module's first good start (novox/hq ADR 0228). - "secret-replaced", - // And every act a healer takes on a condition (novox/hq to-be 45 §7, Phase 3): a repair the mesh - // made by itself is said like one a person made, never quietly. - "healer-acted", - // And a build put back after its gate failed on its first machine (novox/hq ADR 0236, to-be 45 §8). - "rolled-back", - // And a pull request's merge check, judged (novox/hq to-be 45 §9): what the forge's holder sets as - // the pull request's status, and anybody else may read. - "checked", - // (A walk kept in a new state and a tier sent to a machine are gone with the plans, novox/hq ADR 0539.) - // And a key of a record mirror written or removed (novox/hq ADR 0289, design 54 §3): the assignments and - // each registered module's declared config (design 57 F3). - "assignment-changed", "declared-config-changed", "predefined-changed", - // And a builder's key not taken in because the artifact store does not hold what it names (novox/hq issue 1937). - "artifacts-gone", - // And a bucket of the plans' state deleted at the controller's first start, and the declarations' stream once no node - // reads one (novox/hq ADR 0539 clauses 11 and 13). - "bucket-deleted"} - -// BusAdvisories are what the bus server says about the mesh's own account that the controller -// reads (novox/hq to-be 45 §3, S9): a durable consumer that handed a message over as often as it -// may and gave up on it, and one that was deleted. Read-only: an advisory is the server's to -// publish, and the controller's subscription changes nothing on the bus. -var BusAdvisories = []string{ - "$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.>", - "$JS.EVENT.ADVISORY.CONSUMER.DELETED.>", -} - -// ControllerFollows are the events the controller reacts to: the catalogue saying a module's -// current version moved, and a catalogue that has just started saying it may have missed builds. -// -// **Derived the same way a module's subscription is**, from the emitter and the bare local event -// name, rather than written out. They were written out while the catalogue still spelled its events -// as the old bus's routing keys, and the moment those were converted (novox/hq 04-ISSUES/127) a -// hard-coded pair became a controller listening to a subject nothing publishes — the same fault, from -// the other side. Deriving them means the conversion could not leave these behind. -var ControllerFollows = []string{ - moduleEventSubject("mesh-catalog", "upgraded"), - moduleEventSubject("mesh-catalog", "catching-up"), - // A build's outcome, which is the build-machine role's own event now (ADR 0121) rather than a - // message on the control branch. Same three audiences, one publish: whoever asked, this, and the - // catalogue. - seatEventSubject("node-build-agent", "built"), - // The forge's merges: what moved a source, so the mesh builds what that source produces - // without anybody telling it (novox/hq 04-ISSUES/131). Appended, because the index is a name. - moduleEventSubject("gitea", "pull.merged"), - // The retired build role's outcome too, while the handover runs (novox/hq ADR 0190): the one - // build machine keeps answering on its seat until build-agent replaces it, and the outcome that - // registers build-agent itself comes from there. Appended, for the same reason as above; goes - // with the retired seat row. - seatEventSubject("mesh-build-machine", "built"), - // **Every provider's standing** (novox/hq ADR 0224): a consumer it has failed for minutes, and - // that consumer recovered. The one pattern on this list, and a narrow one — two named events, - // from whichever module provides — because the rule is about every provider, and a list of - // providers here would be a list somebody forgets to extend. On 2026-10-05 the identity provider - // failed every consumer for a day and only its journal said so (issue 179). Appended, because - // the index is a name. - moduleEventSubject("*", ProvisionerFailing), - moduleEventSubject("*", ProvisionerRecovered), - // **What becomes of a consumer the mesh stopped asking for** (novox/hq ADR 0230): retired, waiting - // for a person, re-enabled, deleted — a provider's third word, from whichever module provides. - // Appended, because the index is a name. - moduleEventSubject("*", ProvisionerRetirement), - // **A pull request's head, announced** (novox/hq to-be 45 §9): what the controller asks the build - // seat to check before it merges — every machine of the facts snapshot composed with the change. - // Appended, because the index is a name. - moduleEventSubject("gitea", "pull.updated"), - // **The operator's answers to what the controller asked** (novox/hq ADR 0259): the router's warrant, or - // the end of an ask without one, said to the controller alone under its own name. On the stream, so a - // controller that was away hears what was decided meanwhile. Appended, because the index is a name. - DecidedSubject, -} - -// ControllerHears are events the controller's events consumer takes beside the ones it follows, and that need no -// subscribe grant of their own. **A push consumer delivers onto its deliver subject** (`_DELIVER.controller.>`, already -// granted), so the subject a consumer filters on is never one the controller subscribes: listing this one in -// ControllerFollows would add a subscribe grant the installer's carried user list, which lives in mesh-host, would -// have to carry too (TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose), for nothing the consumer uses. -// -// A merge mesh-delivery made, announced (novox/hq ADR 0420 §7, issue 1025): with the ask whose Approve it merged on and -// the heads that Approve covered, kept a week to read the warrant a moved module's registration needs. -var ControllerHears = []string{DeliveryMergedSubject} - -// DeliveryMergedEvent is the event a module says when it merged a pull request. -const DeliveryMergedEvent = "merged" - -// DeliveryMergedSubject is where the delivery's owner says it merged a pull request: one wildcard subject, the -// module that claims the delivery seat being whichever the catalogue says (novox/hq ADR 0437 §3). Who said it is -// read from the subject, and whether it claims the seat is the controller's to check. -var DeliveryMergedSubject = moduleEventSubject("*", DeliveryMergedEvent) - -// DeliveryMergedSubjectOf is where one module says it merged a pull request. -func DeliveryMergedSubjectOf(module string) string { - return moduleEventSubject(module, DeliveryMergedEvent) -} +// 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. +const ( + ControllerName = "controller" + 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" -// DecidedSubject is where the router says the controller's warrants: the seat's event named by the -// controller as its caller. -var DecidedSubject = seatEventSubject(AsksSeat, "decided."+ControllerSeat) - // 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 ( @@ -484,108 +330,9 @@ func seatEventSubject(seat, verb string) string { // EventsStream holds every module's and every role's events, a build's log among them. const EventsStream = "EVENTS" -// MeshConsumers is what the controller consumes, in the order a person reads it. -// -// **Unlimited redelivery on CONTROL, deliberately.** The store window's bound is the controller's, -// not the server's (window.go): a message is held with a nak-and-delay until the controller either -// takes it or gives up and says so. A max-deliver here would give up on a push that was being -// held through a store restart — the exact message the stream exists to protect — some minutes -// before the controller had finished deciding about it. -func MeshConsumers() []Consumer { - return ControllerConsumers() -} - -// ControllerConsumers are the consumers the controller's own event loop takes from: MeshConsumers without the build -// seat's check workers, which the builders take from and the loop never does (issue 1063). A consumer added to the -// controller is counted here without a change elsewhere. -func ControllerConsumers() []Consumer { - return []Consumer{ - { - Name: ControllerName, - Stream: "CONTROL", - Push: true, - AckWaitSeconds: 30, - Why: "the controller is the single consumer of what nodes say; explicit ack and no " + - "max-deliver, because the store window's bound is the controller's own", - }, - // The forge's merges (novox/hq ADR 0407 decision 3, issue 973): never dropped, so no max-deliver and no - // reset; a merge the controller cannot record yet is held with a growing delay until it can. **Asserted before - // the events consumer**, which stops filtering merges: the order of this list is the order of assertion. - { - Name: MergesConsumerName, - Stream: "EVENTS", - // Within what the controller may subscribe already (`_DELIVER.controller.>`). - DeliverSubject: "_DELIVER." + ControllerName + ".merges", - Filters: []string{ControllerFollows[3]}, - Push: true, - AckWaitSeconds: 30, - MaxAckPending: 1, - FromNow: true, - TakesOverFrom: ControllerName, - Why: "the forge's merges, one at a time; no max-deliver and never reset, because a merge is never " + - "dropped and no person releases one", - }, - { - Name: ControllerName, - Stream: "EVENTS", - Filters: append(eventsTheControllerFollows(), AgainFilter(ControllerName)), - Push: true, - AckWaitSeconds: 30, - MaxDeliver: 5, - // One at a time (novox/hq issue 175): acting on a merge builds for minutes, and an - // announcement handed over behind it must wait on the server, not time out on the - // client and come back to be acted on again. - MaxAckPending: 1, - FromNow: true, - // **The one consumer the mesh resets by itself** (healer H4, issue 248): it fell a week - // behind once and held every merge after it. What a reset drops is caught up elsewhere — a - // build's outcome from the build records a plan settles from (issue 214), a provider's failing - // word by the provider saying it again every quarter of an hour (ADR 0224). **Not the merges**: - // they are the merges consumer's below, which a reset leaves alone (ADR 0407 decision 3). - Resettable: "what it drops is caught up: build outcomes from the build records (issue 214), a " + - "provider's failing word said again (ADR 0224); the forge's merges are not its, they have a " + - "consumer of their own that no reset touches", - Why: "the events the mesh's own controller reacts to, one at a time; after " + - "max-deliver it gives the event up, and the controller keeps it in DEAD_LETTERS until " + - "a person delivers it again or drops it", - }, - // What the server said a consumer gave up on (novox/hq issue 330): copied into DEAD_LETTERS and - // acknowledged. No max-deliver: a notice the controller could not copy is offered again, and said. - { - Name: ControllerName, - Stream: DeadLetterNoticesStream, - Push: true, - AckWaitSeconds: 30, - Why: "the controller copies each message a consumer gave up on into DEAD_LETTERS; no max-deliver, " + - "because a notice it gave up on would lose the message it is about", - }, - } -} - -// NoticesConsumer is the controller's consumer on DEAD_LETTER_NOTICES (novox/hq issue 330). -func NoticesConsumer() Consumer { - for _, c := range MeshConsumers() { - if c.Stream == DeadLetterNoticesStream { - return c - } - } - panic("the mesh's consumers carry none on " + DeadLetterNoticesStream) -} - -// AssertServingConsumers are the controller's own consumers its serving cannot go without: all but the -// one on DEAD_LETTER_NOTICES, which the keeper of dead letters asserts and retries by itself, so a fault -// there never stops the controller serving (novox/hq issue 330). -func AssertServingConsumers(e Ensurer) error { - for _, c := range MeshConsumers() { - if c.Stream == DeadLetterNoticesStream { - continue - } - if err := e.EnsureConsumer(c); err != nil { - return fmt.Errorf("asserting consumer %s on %s: %w", c.Name, c.Stream, err) - } - } - return nil -} +// 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. @@ -593,7 +340,7 @@ type Ensurer interface { EnsureConsumer(c Consumer) error } -// AssertMeshConsumers brings the controller's own consumers into being, and says which one failed. +// 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. diff --git a/go/buscompose/testdata/composed.conf b/go/buscompose/testdata/composed.conf index 7560fd8..0fe0bb6 100644 --- a/go/buscompose/testdata/composed.conf +++ b/go/buscompose/testdata/composed.conf @@ -1,4 +1,4 @@ -# Composed by the mesh controller. Do not edit: the next composition overwrites it. +# 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). @@ -15,7 +15,7 @@ jetstream { store_dir: "/data" } -# The mesh's users, composed by the controller. Do not edit: the next +# 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). @@ -28,23 +28,18 @@ accounts { { service: { account: "$SYS", subject: "$SYS.REQ.ACCOUNT.MESH.CONNZ" }, to: "mesh.bus.connz" } ] users = [ - { user: "controller", password: "$2a$11$cccccccccccccccccccccc", permissions: { - publish: { allow: ["$JS.ACK.CONTROL.controller.>", "$JS.ACK.DEAD_LETTER_NOTICES.controller.>", "$JS.ACK.EVENTS.controller-merges.>", "$JS.ACK.EVENTS.controller.>", "$JS.ACK.KV_mesh-delivery-builder_builds.mesh-controller-take-in.>", "$JS.ACK.KV_mesh-delivery-indexer_modules.mesh-controller-take-in.>", "$JS.API.>", "$KV.mesh-controller_asked.>", "$KV.mesh-controller_assignments.>", "$KV.mesh-controller_builds.>", "$KV.mesh-controller_calls.>", "$KV.mesh-controller_composed.>", "$KV.mesh-controller_condition-history.>", "$KV.mesh-controller_conditions.>", "$KV.mesh-controller_declared-config.>", "$KV.mesh-controller_declared-packages.>", "$KV.mesh-controller_delivery-merges.>", "$KV.mesh-controller_grants.>", "$KV.mesh-controller_hand-acts.>", "$KV.mesh-controller_lease.>", "$KV.mesh-controller_predefined.>", "$KV.mesh-controller_rollouts.>", "$KV.mesh-controller_states-announced.>", "$SRV.INFO", "_INBOX.enrol.>", "mesh.again.>", "mesh.assignment.>", "mesh.events.dead.>", "mesh.mod.*.tool.>", "mesh.mod.mesh-controller.event.states", "mesh.node.>", "mesh.seat.mesh-controller.event.applied", "mesh.seat.mesh-controller.event.artifacts-gone", "mesh.seat.mesh-controller.event.assignment-changed", "mesh.seat.mesh-controller.event.bucket-deleted", "mesh.seat.mesh-controller.event.built-before", "mesh.seat.mesh-controller.event.checked", "mesh.seat.mesh-controller.event.condition-changed", "mesh.seat.mesh-controller.event.condition-cleared", "mesh.seat.mesh-controller.event.condition-raised", "mesh.seat.mesh-controller.event.declared-config-changed", "mesh.seat.mesh-controller.event.doctor-heartbeat", "mesh.seat.mesh-controller.event.healer-acted", "mesh.seat.mesh-controller.event.predefined-changed", "mesh.seat.mesh-controller.event.refused", "mesh.seat.mesh-controller.event.rolled-back", "mesh.seat.mesh-controller.event.secret-replaced", "mesh.seat.mesh-controller.tool.plans", "mesh.seat.mesh-controller.tool.push", "mesh.seat.mesh-controller.tool.upgrade", "mesh.seat.mesh-delivery.tool.close", "mesh.seat.mesh-delivery.tool.groups", "mesh.seat.mesh-delivery.tool.release", "mesh.seat.mesh-delivery.tool.stalled", "mesh.seat.mesh-delivery.tool.stop", "mesh.seat.mesh-delivery.tool.times", "mesh.seat.node-backup.tool.backed-up.*", "mesh.seat.node-backup.tool.now.*", "mesh.seat.node-intrusion-prevention.tool.banned.*", "mesh.seat.node-launcher.tool.secret.*", "mesh.seat.node-service-manager.tool.restart.*", "mesh.seat.operator-channel.tool.tell"] } - subscribe: { allow: ["$JS.API.>", "$JS.EVENT.ADVISORY.CONSUMER.DELETED.>", "$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.>", "$SRV.INFO", "$SRV.INFO.mesh-controller", "$SRV.INFO.mesh-controller.>", "$SRV.PING", "$SRV.PING.mesh-controller", "$SRV.PING.mesh-controller.>", "$SRV.STATS", "$SRV.STATS.mesh-controller", "$SRV.STATS.mesh-controller.>", "_DELIVER.controller", "_DELIVER.controller.>", "_INBOX.controller.>", "mesh.control.>", "mesh.mod.*.event.provisioner.failing", "mesh.mod.*.event.provisioner.recovered", "mesh.mod.*.event.provisioner.retirement", "mesh.mod.gitea.event.pull.merged", "mesh.mod.gitea.event.pull.updated", "mesh.mod.mesh-catalog.event.catching-up", "mesh.mod.mesh-catalog.event.upgraded", "mesh.seat.mesh-build-machine.event.built", "mesh.seat.mesh-controller.tool.>", "mesh.seat.node-build-agent.event.built", "mesh.seat.operator-channel.event.decided.mesh-controller"] } - allow_responses: { max: 1, ttl: "1m" } - } } { user: "enrol.one", password: "$2a$11$eeeeeeeeeeeeeeeeeeeeee", permissions: { - publish: { allow: ["mesh.control.enrol"] } + 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-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.one.>", "$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-delivery-assigner_wants.>", "$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-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.one.>", "$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-delivery-assigner_wants", "$JS.API.STREAM.INFO.KV_node-engine_have", "$JS.FC.KV_composed-one.>", "$JS.FC.KV_mesh-delivery-assigner_wants.>", "$JS.FC.KV_node-engine_have.>", "$JS.FC.OBJ_declaration-files-one.>", "$KV.node-engine_have.one.>", "$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"] } - subscribe: { allow: ["_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"] } + 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.EVENTS.one_mesh-delivery-watchman.>", "$JS.API.CONSUMER.INFO.EVENTS.one_mesh-delivery-watchman", "$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_mesh-delivery-watchman", "$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.one.mesh-delivery-watchman", "mesh.bus.connz"] } - subscribe: { allow: ["$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.assignment.one.mesh-delivery-watchman", "mesh.bus.connect", "mesh.bus.disconnect", "mesh.mod.mesh-delivery-watchman.call.>", "mesh.mod.mesh-delivery-watchman.tool.>"] } + 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: { @@ -52,18 +47,18 @@ accounts { 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.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.ASSIGNMENTS.mesh.assignment.one.telegram", "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.assignment.one.telegram", "mesh.mod.telegram.call.>", "mesh.mod.telegram.tool.>", "mesh.seat.telegram-sender.accept.send"] } + 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.INFO.EVENTS.two_audit", "$JS.API.CONSUMER.MSG.NEXT.EVENTS.two_audit", "$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.two.audit"] } - 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.assignment.two.audit", "mesh.mod.audit.call.>", "mesh.mod.audit.tool.>", "mesh.mod.shop.event.order.placed"] } + 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.INFO.EVENTS.two_shop", "$JS.API.CONSUMER.MSG.NEXT.EVENTS.two_shop", "$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.two.shop", "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.assignment.two.shop", "mesh.mod.shop.call.>", "mesh.mod.shop.tool.>"] } + 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" } } } ] diff --git a/go/buscompose/testdata/controller-user-lists.json b/go/buscompose/testdata/controller-user-lists.json deleted file mode 100644 index a4da9e9..0000000 --- a/go/buscompose/testdata/controller-user-lists.json +++ /dev/null @@ -1,283 +0,0 @@ -[ - { - "Name": "empty", - "List": "# The mesh's users, composed by the controller. 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: \"controller\", password: \"$2a$11$controllercontrollercofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.CONTROL.controller.\u003e\", \"$JS.ACK.DEAD_LETTER_NOTICES.controller.\u003e\", \"$JS.ACK.EVENTS.controller-merges.\u003e\", \"$JS.ACK.EVENTS.controller.\u003e\", \"$JS.ACK.KV_mesh-delivery-builder_builds.mesh-controller-take-in.\u003e\", \"$JS.ACK.KV_mesh-delivery-indexer_modules.mesh-controller-take-in.\u003e\", \"$JS.API.\u003e\", \"$KV.mesh-controller_asked.\u003e\", \"$KV.mesh-controller_assignments.\u003e\", \"$KV.mesh-controller_builds.\u003e\", \"$KV.mesh-controller_calls.\u003e\", \"$KV.mesh-controller_composed.\u003e\", \"$KV.mesh-controller_condition-history.\u003e\", \"$KV.mesh-controller_conditions.\u003e\", \"$KV.mesh-controller_declared-config.\u003e\", \"$KV.mesh-controller_declared-packages.\u003e\", \"$KV.mesh-controller_delivery-merges.\u003e\", \"$KV.mesh-controller_grants.\u003e\", \"$KV.mesh-controller_hand-acts.\u003e\", \"$KV.mesh-controller_lease.\u003e\", \"$KV.mesh-controller_predefined.\u003e\", \"$KV.mesh-controller_rollouts.\u003e\", \"$KV.mesh-controller_states-announced.\u003e\", \"$SRV.INFO\", \"_INBOX.enrol.\u003e\", \"mesh.again.\u003e\", \"mesh.assignment.\u003e\", \"mesh.events.dead.\u003e\", \"mesh.mod.*.tool.\u003e\", \"mesh.mod.mesh-controller.event.states\", \"mesh.node.\u003e\", \"mesh.seat.mesh-controller.event.applied\", \"mesh.seat.mesh-controller.event.artifacts-gone\", \"mesh.seat.mesh-controller.event.assignment-changed\", \"mesh.seat.mesh-controller.event.bucket-deleted\", \"mesh.seat.mesh-controller.event.built-before\", \"mesh.seat.mesh-controller.event.checked\", \"mesh.seat.mesh-controller.event.condition-changed\", \"mesh.seat.mesh-controller.event.condition-cleared\", \"mesh.seat.mesh-controller.event.condition-raised\", \"mesh.seat.mesh-controller.event.declared-config-changed\", \"mesh.seat.mesh-controller.event.doctor-heartbeat\", \"mesh.seat.mesh-controller.event.healer-acted\", \"mesh.seat.mesh-controller.event.predefined-changed\", \"mesh.seat.mesh-controller.event.refused\", \"mesh.seat.mesh-controller.event.rolled-back\", \"mesh.seat.mesh-controller.event.secret-replaced\", \"mesh.seat.mesh-controller.tool.plans\", \"mesh.seat.mesh-controller.tool.push\", \"mesh.seat.mesh-controller.tool.upgrade\", \"mesh.seat.mesh-delivery.tool.close\", \"mesh.seat.mesh-delivery.tool.groups\", \"mesh.seat.mesh-delivery.tool.release\", \"mesh.seat.mesh-delivery.tool.stalled\", \"mesh.seat.mesh-delivery.tool.stop\", \"mesh.seat.mesh-delivery.tool.times\", \"mesh.seat.node-backup.tool.backed-up.*\", \"mesh.seat.node-backup.tool.now.*\", \"mesh.seat.node-intrusion-prevention.tool.banned.*\", \"mesh.seat.node-launcher.tool.secret.*\", \"mesh.seat.node-service-manager.tool.restart.*\", \"mesh.seat.operator-channel.tool.tell\"] }\n subscribe: { allow: [\"$JS.API.\u003e\", \"$JS.EVENT.ADVISORY.CONSUMER.DELETED.\u003e\", \"$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.mesh-controller\", \"$SRV.INFO.mesh-controller.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-controller\", \"$SRV.PING.mesh-controller.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-controller\", \"$SRV.STATS.mesh-controller.\u003e\", \"_DELIVER.controller\", \"_DELIVER.controller.\u003e\", \"_INBOX.controller.\u003e\", \"mesh.control.\u003e\", \"mesh.mod.*.event.provisioner.failing\", \"mesh.mod.*.event.provisioner.recovered\", \"mesh.mod.*.event.provisioner.retirement\", \"mesh.mod.gitea.event.pull.merged\", \"mesh.mod.gitea.event.pull.updated\", \"mesh.mod.mesh-catalog.event.catching-up\", \"mesh.mod.mesh-catalog.event.upgraded\", \"mesh.seat.mesh-build-machine.event.built\", \"mesh.seat.mesh-controller.tool.\u003e\", \"mesh.seat.node-build-agent.event.built\", \"mesh.seat.operator-channel.event.decided.mesh-controller\"] }\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": {} - }, - { - "Name": "the controller's someRecords", - "List": "# The mesh's users, composed by the controller. 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: \"controller\", password: \"$2a$11$controllercontrollercofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.CONTROL.controller.\u003e\", \"$JS.ACK.DEAD_LETTER_NOTICES.controller.\u003e\", \"$JS.ACK.EVENTS.controller-merges.\u003e\", \"$JS.ACK.EVENTS.controller.\u003e\", \"$JS.ACK.KV_mesh-delivery-builder_builds.mesh-controller-take-in.\u003e\", \"$JS.ACK.KV_mesh-delivery-indexer_modules.mesh-controller-take-in.\u003e\", \"$JS.API.\u003e\", \"$KV.composed-one.\u003e\", \"$KV.composed-two.\u003e\", \"$KV.mesh-controller_asked.\u003e\", \"$KV.mesh-controller_assignments.\u003e\", \"$KV.mesh-controller_builds.\u003e\", \"$KV.mesh-controller_calls.\u003e\", \"$KV.mesh-controller_composed.\u003e\", \"$KV.mesh-controller_condition-history.\u003e\", \"$KV.mesh-controller_conditions.\u003e\", \"$KV.mesh-controller_declared-config.\u003e\", \"$KV.mesh-controller_declared-packages.\u003e\", \"$KV.mesh-controller_delivery-merges.\u003e\", \"$KV.mesh-controller_grants.\u003e\", \"$KV.mesh-controller_hand-acts.\u003e\", \"$KV.mesh-controller_lease.\u003e\", \"$KV.mesh-controller_predefined.\u003e\", \"$KV.mesh-controller_rollouts.\u003e\", \"$KV.mesh-controller_states-announced.\u003e\", \"$O.declaration-files-one.\u003e\", \"$O.declaration-files-two.\u003e\", \"$SRV.INFO\", \"_INBOX.enrol.\u003e\", \"mesh.again.\u003e\", \"mesh.assignment.\u003e\", \"mesh.events.dead.\u003e\", \"mesh.mod.*.tool.\u003e\", \"mesh.mod.mesh-controller.event.states\", \"mesh.node.\u003e\", \"mesh.seat.mesh-controller.event.applied\", \"mesh.seat.mesh-controller.event.artifacts-gone\", \"mesh.seat.mesh-controller.event.assignment-changed\", \"mesh.seat.mesh-controller.event.bucket-deleted\", \"mesh.seat.mesh-controller.event.built-before\", \"mesh.seat.mesh-controller.event.checked\", \"mesh.seat.mesh-controller.event.condition-changed\", \"mesh.seat.mesh-controller.event.condition-cleared\", \"mesh.seat.mesh-controller.event.condition-raised\", \"mesh.seat.mesh-controller.event.declared-config-changed\", \"mesh.seat.mesh-controller.event.doctor-heartbeat\", \"mesh.seat.mesh-controller.event.healer-acted\", \"mesh.seat.mesh-controller.event.predefined-changed\", \"mesh.seat.mesh-controller.event.refused\", \"mesh.seat.mesh-controller.event.rolled-back\", \"mesh.seat.mesh-controller.event.secret-replaced\", \"mesh.seat.mesh-controller.tool.plans\", \"mesh.seat.mesh-controller.tool.push\", \"mesh.seat.mesh-controller.tool.upgrade\", \"mesh.seat.mesh-delivery.tool.close\", \"mesh.seat.mesh-delivery.tool.groups\", \"mesh.seat.mesh-delivery.tool.release\", \"mesh.seat.mesh-delivery.tool.stalled\", \"mesh.seat.mesh-delivery.tool.stop\", \"mesh.seat.mesh-delivery.tool.times\", \"mesh.seat.node-backup.tool.backed-up.*\", \"mesh.seat.node-backup.tool.now.*\", \"mesh.seat.node-intrusion-prevention.tool.banned.*\", \"mesh.seat.node-launcher.tool.secret.*\", \"mesh.seat.node-service-manager.tool.restart.*\", \"mesh.seat.operator-channel.tool.tell\"] }\n subscribe: { allow: [\"$JS.API.\u003e\", \"$JS.EVENT.ADVISORY.CONSUMER.DELETED.\u003e\", \"$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.mesh-controller\", \"$SRV.INFO.mesh-controller.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-controller\", \"$SRV.PING.mesh-controller.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-controller\", \"$SRV.STATS.mesh-controller.\u003e\", \"_DELIVER.controller\", \"_DELIVER.controller.\u003e\", \"_INBOX.controller.\u003e\", \"mesh.control.\u003e\", \"mesh.mod.*.event.provisioner.failing\", \"mesh.mod.*.event.provisioner.recovered\", \"mesh.mod.*.event.provisioner.retirement\", \"mesh.mod.gitea.event.pull.merged\", \"mesh.mod.gitea.event.pull.updated\", \"mesh.mod.mesh-catalog.event.catching-up\", \"mesh.mod.mesh-catalog.event.upgraded\", \"mesh.seat.mesh-build-machine.event.built\", \"mesh.seat.mesh-controller.tool.\u003e\", \"mesh.seat.node-build-agent.event.built\", \"mesh.seat.operator-channel.event.decided.mesh-controller\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"enrol.three\", password: \"$2a$11$enrol.threeenrol.threefixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"mesh.control.enrol\"] }\n subscribe: { allow: [\"_INBOX.enrol.three.\u003e\"] }\n } }\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-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.one.\u003e\", \"$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-delivery-assigner_wants.\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-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.one.\u003e\", \"$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-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-one.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-one.\u003e\", \"$KV.node-engine_have.one.\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\"] }\n subscribe: { allow: [\"_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\"] }\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-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.two.\u003e\", \"$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-delivery-assigner_wants.\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-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.two.\u003e\", \"$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-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-two.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-two.\u003e\", \"$KV.node-engine_have.two.\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\"] }\n subscribe: { allow: [\"_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\"] }\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.INFO.EVENTS.one_telegram\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_telegram\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.one.telegram\"] }\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.assignment.one.telegram\", \"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.INFO.EVENTS.two_shop\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.two_shop\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.two.shop\", \"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.assignment.two.shop\", \"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 controller. 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: \"controller\", password: \"$2a$11$controllercontrollercofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.CONTROL.controller.\u003e\", \"$JS.ACK.DEAD_LETTER_NOTICES.controller.\u003e\", \"$JS.ACK.EVENTS.controller-merges.\u003e\", \"$JS.ACK.EVENTS.controller.\u003e\", \"$JS.ACK.KV_mesh-delivery-builder_builds.mesh-controller-take-in.\u003e\", \"$JS.ACK.KV_mesh-delivery-indexer_modules.mesh-controller-take-in.\u003e\", \"$JS.API.\u003e\", \"$KV.composed-one.\u003e\", \"$KV.composed-two.\u003e\", \"$KV.mesh-controller_asked.\u003e\", \"$KV.mesh-controller_assignments.\u003e\", \"$KV.mesh-controller_builds.\u003e\", \"$KV.mesh-controller_calls.\u003e\", \"$KV.mesh-controller_composed.\u003e\", \"$KV.mesh-controller_condition-history.\u003e\", \"$KV.mesh-controller_conditions.\u003e\", \"$KV.mesh-controller_declared-config.\u003e\", \"$KV.mesh-controller_declared-packages.\u003e\", \"$KV.mesh-controller_delivery-merges.\u003e\", \"$KV.mesh-controller_grants.\u003e\", \"$KV.mesh-controller_hand-acts.\u003e\", \"$KV.mesh-controller_lease.\u003e\", \"$KV.mesh-controller_predefined.\u003e\", \"$KV.mesh-controller_rollouts.\u003e\", \"$KV.mesh-controller_states-announced.\u003e\", \"$O.declaration-files-one.\u003e\", \"$O.declaration-files-two.\u003e\", \"$SRV.INFO\", \"_INBOX.enrol.\u003e\", \"mesh.again.\u003e\", \"mesh.assignment.\u003e\", \"mesh.events.dead.\u003e\", \"mesh.mod.*.tool.\u003e\", \"mesh.mod.mesh-controller.event.states\", \"mesh.node.\u003e\", \"mesh.seat.mesh-controller.event.applied\", \"mesh.seat.mesh-controller.event.artifacts-gone\", \"mesh.seat.mesh-controller.event.assignment-changed\", \"mesh.seat.mesh-controller.event.bucket-deleted\", \"mesh.seat.mesh-controller.event.built-before\", \"mesh.seat.mesh-controller.event.checked\", \"mesh.seat.mesh-controller.event.condition-changed\", \"mesh.seat.mesh-controller.event.condition-cleared\", \"mesh.seat.mesh-controller.event.condition-raised\", \"mesh.seat.mesh-controller.event.declared-config-changed\", \"mesh.seat.mesh-controller.event.doctor-heartbeat\", \"mesh.seat.mesh-controller.event.healer-acted\", \"mesh.seat.mesh-controller.event.predefined-changed\", \"mesh.seat.mesh-controller.event.refused\", \"mesh.seat.mesh-controller.event.rolled-back\", \"mesh.seat.mesh-controller.event.secret-replaced\", \"mesh.seat.mesh-controller.tool.plans\", \"mesh.seat.mesh-controller.tool.push\", \"mesh.seat.mesh-controller.tool.upgrade\", \"mesh.seat.mesh-delivery.tool.close\", \"mesh.seat.mesh-delivery.tool.groups\", \"mesh.seat.mesh-delivery.tool.release\", \"mesh.seat.mesh-delivery.tool.stalled\", \"mesh.seat.mesh-delivery.tool.stop\", \"mesh.seat.mesh-delivery.tool.times\", \"mesh.seat.node-backup.tool.backed-up.*\", \"mesh.seat.node-backup.tool.now.*\", \"mesh.seat.node-intrusion-prevention.tool.banned.*\", \"mesh.seat.node-launcher.tool.secret.*\", \"mesh.seat.node-service-manager.tool.restart.*\", \"mesh.seat.operator-channel.tool.tell\"] }\n subscribe: { allow: [\"$JS.API.\u003e\", \"$JS.EVENT.ADVISORY.CONSUMER.DELETED.\u003e\", \"$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.mesh-controller\", \"$SRV.INFO.mesh-controller.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-controller\", \"$SRV.PING.mesh-controller.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-controller\", \"$SRV.STATS.mesh-controller.\u003e\", \"_DELIVER.controller\", \"_DELIVER.controller.\u003e\", \"_INBOX.controller.\u003e\", \"mesh.control.\u003e\", \"mesh.mod.*.event.provisioner.failing\", \"mesh.mod.*.event.provisioner.recovered\", \"mesh.mod.*.event.provisioner.retirement\", \"mesh.mod.gitea.event.pull.merged\", \"mesh.mod.gitea.event.pull.updated\", \"mesh.mod.mesh-catalog.event.catching-up\", \"mesh.mod.mesh-catalog.event.upgraded\", \"mesh.seat.mesh-build-machine.event.built\", \"mesh.seat.mesh-controller.tool.\u003e\", \"mesh.seat.node-build-agent.event.built\", \"mesh.seat.operator-channel.event.decided.mesh-controller\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\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-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_user-list.*.$KV.mesh-grants_user-list.sealed\", \"$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-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_user-list.*\", \"$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-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.one.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-grants_user-list.$KV.mesh-grants_user-list.sealed\", \"$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-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-grants_user-list\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-one.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-grants_user-list.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-one.\u003e\", \"$JS.FC.OBJ_mesh-grants-user-list.\u003e\", \"$KV.node-engine_have.one.\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\"] }\n subscribe: { allow: [\"_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\"] }\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-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.two.\u003e\", \"$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-delivery-assigner_wants.\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-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.two.\u003e\", \"$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-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-two.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-two.\u003e\", \"$KV.node-engine_have.two.\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\"] }\n subscribe: { allow: [\"_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\"] }\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.INFO.EVENTS.one_mesh-grants\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_mesh-grants\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.*.*\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.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.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.MSG.GET.OBJ_mesh-grants-user-list\", \"$JS.API.STREAM.PURGE.OBJ_mesh-grants-user-list\", \"$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.assignment.one.mesh-grants\", \"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.INFO.EVENTS.one_mesh-watchman\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_mesh-watchman\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.one.mesh-watchman\"] }\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.assignment.one.mesh-watchman\", \"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.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.ASSIGNMENTS.mesh.assignment.one.telegram\", \"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.assignment.one.telegram\", \"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: [\"$JS.API.CONSUMER.CREATE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-controller_composed.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_composed-one.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_composed-two.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-controller_composed.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.DIRECT.GET.KV_composed-one.\u003e\", \"$JS.API.DIRECT.GET.KV_composed-two.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-controller_composed.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-one.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-two.\u003e\", \"$JS.API.STREAM.INFO.KV_composed-one\", \"$JS.API.STREAM.INFO.KV_composed-two\", \"$JS.API.STREAM.INFO.KV_mesh-controller_composed\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.OBJ_declaration-files-one\", \"$JS.API.STREAM.INFO.OBJ_declaration-files-two\", \"$JS.API.STREAM.MSG.GET.OBJ_declaration-files-one\", \"$JS.API.STREAM.MSG.GET.OBJ_declaration-files-two\", \"$JS.API.STREAM.PURGE.OBJ_declaration-files-one\", \"$JS.API.STREAM.PURGE.OBJ_declaration-files-two\", \"$JS.FC.KV_composed-one.\u003e\", \"$JS.FC.KV_composed-two.\u003e\", \"$JS.FC.KV_mesh-controller_composed.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$KV.composed-one.\u003e\", \"$KV.composed-two.\u003e\", \"$KV.mesh-controller_composed.\u003e\", \"$KV.mesh-delivery-assigner_wants.want.\u003e\", \"$O.declaration-files-one.\u003e\", \"$O.declaration-files-two.\u003e\", \"$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.INFO.EVENTS.two_shop\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.two_shop\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.two.shop\", \"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.assignment.two.shop\", \"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" - }, - "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 controller. 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.INFO.EVENTS.ace_plex\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.ace_plex\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.ace.plex\"] }\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.assignment.ace.plex\", \"mesh.mod.plex.call.\u003e\", \"mesh.mod.plex.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"controller\", password: \"$2a$11$controllercontrollercofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.CONTROL.controller.\u003e\", \"$JS.ACK.DEAD_LETTER_NOTICES.controller.\u003e\", \"$JS.ACK.EVENTS.controller-merges.\u003e\", \"$JS.ACK.EVENTS.controller.\u003e\", \"$JS.ACK.KV_mesh-delivery-builder_builds.mesh-controller-take-in.\u003e\", \"$JS.ACK.KV_mesh-delivery-indexer_modules.mesh-controller-take-in.\u003e\", \"$JS.API.\u003e\", \"$KV.composed-ace.\u003e\", \"$KV.composed-novox.\u003e\", \"$KV.mesh-controller_asked.\u003e\", \"$KV.mesh-controller_assignments.\u003e\", \"$KV.mesh-controller_builds.\u003e\", \"$KV.mesh-controller_calls.\u003e\", \"$KV.mesh-controller_composed.\u003e\", \"$KV.mesh-controller_condition-history.\u003e\", \"$KV.mesh-controller_conditions.\u003e\", \"$KV.mesh-controller_declared-config.\u003e\", \"$KV.mesh-controller_declared-packages.\u003e\", \"$KV.mesh-controller_delivery-merges.\u003e\", \"$KV.mesh-controller_grants.\u003e\", \"$KV.mesh-controller_hand-acts.\u003e\", \"$KV.mesh-controller_lease.\u003e\", \"$KV.mesh-controller_predefined.\u003e\", \"$KV.mesh-controller_rollouts.\u003e\", \"$KV.mesh-controller_states-announced.\u003e\", \"$O.declaration-files-ace.\u003e\", \"$O.declaration-files-novox.\u003e\", \"$SRV.INFO\", \"_INBOX.enrol.\u003e\", \"mesh.again.\u003e\", \"mesh.assignment.\u003e\", \"mesh.events.dead.\u003e\", \"mesh.mod.*.tool.\u003e\", \"mesh.mod.mesh-controller.event.states\", \"mesh.node.\u003e\", \"mesh.seat.mesh-controller.event.applied\", \"mesh.seat.mesh-controller.event.artifacts-gone\", \"mesh.seat.mesh-controller.event.assignment-changed\", \"mesh.seat.mesh-controller.event.bucket-deleted\", \"mesh.seat.mesh-controller.event.built-before\", \"mesh.seat.mesh-controller.event.checked\", \"mesh.seat.mesh-controller.event.condition-changed\", \"mesh.seat.mesh-controller.event.condition-cleared\", \"mesh.seat.mesh-controller.event.condition-raised\", \"mesh.seat.mesh-controller.event.declared-config-changed\", \"mesh.seat.mesh-controller.event.doctor-heartbeat\", \"mesh.seat.mesh-controller.event.healer-acted\", \"mesh.seat.mesh-controller.event.predefined-changed\", \"mesh.seat.mesh-controller.event.refused\", \"mesh.seat.mesh-controller.event.rolled-back\", \"mesh.seat.mesh-controller.event.secret-replaced\", \"mesh.seat.mesh-controller.tool.plans\", \"mesh.seat.mesh-controller.tool.push\", \"mesh.seat.mesh-controller.tool.upgrade\", \"mesh.seat.mesh-delivery.tool.close\", \"mesh.seat.mesh-delivery.tool.groups\", \"mesh.seat.mesh-delivery.tool.release\", \"mesh.seat.mesh-delivery.tool.stalled\", \"mesh.seat.mesh-delivery.tool.stop\", \"mesh.seat.mesh-delivery.tool.times\", \"mesh.seat.node-backup.tool.backed-up.*\", \"mesh.seat.node-backup.tool.now.*\", \"mesh.seat.node-intrusion-prevention.tool.banned.*\", \"mesh.seat.node-launcher.tool.secret.*\", \"mesh.seat.node-service-manager.tool.restart.*\", \"mesh.seat.operator-channel.tool.tell\"] }\n subscribe: { allow: [\"$JS.API.\u003e\", \"$JS.EVENT.ADVISORY.CONSUMER.DELETED.\u003e\", \"$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.mesh-controller\", \"$SRV.INFO.mesh-controller.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-controller\", \"$SRV.PING.mesh-controller.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-controller\", \"$SRV.STATS.mesh-controller.\u003e\", \"_DELIVER.controller\", \"_DELIVER.controller.\u003e\", \"_INBOX.controller.\u003e\", \"mesh.control.\u003e\", \"mesh.mod.*.event.provisioner.failing\", \"mesh.mod.*.event.provisioner.recovered\", \"mesh.mod.*.event.provisioner.retirement\", \"mesh.mod.gitea.event.pull.merged\", \"mesh.mod.gitea.event.pull.updated\", \"mesh.mod.mesh-catalog.event.catching-up\", \"mesh.mod.mesh-catalog.event.upgraded\", \"mesh.seat.mesh-build-machine.event.built\", \"mesh.seat.mesh-controller.tool.\u003e\", \"mesh.seat.node-build-agent.event.built\", \"mesh.seat.operator-channel.event.decided.mesh-controller\"] }\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-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.ace.\u003e\", \"$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-delivery-assigner_wants.\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-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.ace.\u003e\", \"$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-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-ace.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-ace.\u003e\", \"$KV.node-engine_have.ace.\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\"] }\n subscribe: { allow: [\"_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\"] }\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-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.novox.\u003e\", \"$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-delivery-assigner_wants.\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-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.novox.\u003e\", \"$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-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-novox.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-novox.\u003e\", \"$KV.node-engine_have.novox.\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\"] }\n subscribe: { allow: [\"_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\"] }\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.DELETE.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_steps.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.novox_mesh-delivery-builder\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.novox_mesh-delivery-builder\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.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.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.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\", \"$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: [\"$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.assignment.novox.mesh-delivery-builder\", \"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: [\"_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.DELETE.KV_mesh-delivery-builder_presence.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_steps.\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.ASSIGNMENTS.mesh.assignment.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.STREAM.INFO.KV_mesh-delivery-builder_presence\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_steps\", \"$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\"] }\n subscribe: { allow: [\"$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.assignment.novox.mesh-delivery-view\", \"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.DELETE.KV_mesh-grants_assertions.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.novox_mesh-grants\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.novox_mesh-grants\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.*.*\", \"$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.novox.mesh-grants\", \"$JS.API.DIRECT.GET.KV_mesh-grants_assertions.\u003e\", \"$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.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\", \"$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: [\"$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.assignment.novox.mesh-grants\", \"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 - } - ] - } - } - } -] \ No newline at end of file diff --git a/go/buscompose/testdata/user-lists.json b/go/buscompose/testdata/user-lists.json new file mode 100644 index 0000000..4851ccc --- /dev/null +++ b/go/buscompose/testdata/user-lists.json @@ -0,0 +1,289 @@ +[ + { + "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 index 5eee315..11ea7b9 100644 --- a/go/buscompose/userlist_switch.go +++ b/go/buscompose/userlist_switch.go @@ -7,7 +7,6 @@ import ( "git.novox.be/novox/mesh-sdk/go/announce" "git.novox.be/novox/mesh-sdk/go/delivery" - "git.novox.be/novox/mesh-sdk/go/handacts" "git.novox.be/novox/mesh-sdk/go/health" ) @@ -88,20 +87,9 @@ const ArtifactStoreTools = "artifact-store-tools" 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). -// The controller's lease is read by the parts that turn on at its write (ADR 0561 §3, §6), and by the assigner, which -// reads the catalogue's placement alone from then (ADR 0549 §5, issue 774). -// -// The health holders (novox/hq ADR 0564 Limits 2 and 8, clauses 8 and 9): the condition keeper, mesh-healers and the -// watchman read the lease; the keeper alone the controller's conditions; mesh-healers and mesh-hand-acts its hand-act log -// (ADR 0565 clauses 10, 11: every reader of acts reads both logs); and nobody the healers' `would` (health_grants.go). -var NamedReaders = map[string][]string{VaultBusAccounts: {GrantsModule}, GrantsAccounts: {VaultSeat}, - ControllerLease: {VaultSeat, GrantsModule, delivery.AssignerModule, health.KeeperModule, health.HealersModule, WatchmanModule, - // ADR 0571 clause 2: the artifact store's seat sweeps only once the lease names no controller. - ArtifactStoreTools, - // ADR 0561 §3: mesh-config takes over the config only once the lease is handed away (its takeover.go leaseAway). - ConfigModule}, - ControllerConditions: {health.KeeperModule}, ControllerHandActs: {health.HealersModule, handacts.Module}, HealersWould: {}, +// 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}, diff --git a/go/buscompose/users.go b/go/buscompose/users.go index 647075c..ea1668f 100644 --- a/go/buscompose/users.go +++ b/go/buscompose/users.go @@ -7,6 +7,7 @@ import ( "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" ) @@ -148,44 +149,25 @@ type Records struct { // 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 - // LeaseHandedAway says the controller's lease names a holder that is no controller (ADR 0582 §3), as the grants part - // reads it: an enrolment user then enrols through the seat mesh-enrolment alone (novox/hq ADR 0587 §6). The - // controller's composer never sets it. It is not LeaseCut: the cut is composed before the holder is written, so the - // grants part may write it, and outlasts the holder at a hand-back; the two are true together only in between. - LeaseHandedAway bool // 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 - // LeaseCut composes the list of the lease write (novox/hq ADR 0582 clause 3, issue 774): the controller may neither - // write the lease's key nor update, purge or delete its bucket, and the grants part may write the key `holder`. - // Set by the grants part from the write's second step until a hand-back composes without it (clause 5); the - // controller's composer never sets it. - LeaseCut bool } // Users is every user the composed file should contain, in the order it will be written. -// -// The controller is always first and always present: a mesh whose own controller is not in the file -// is a mesh that cannot be told anything, and there is no state of the records in which that is -// correct. func Users(r Records) ([]Principal, error) { - out := []Principal{{Kind: KindController, Uses: asksSeatOf(r), AsksIssued: issuedOf(r), AsksTheVault: vaultVerbsOf(r), - AsksTheConfig: servedOf(r, VerbsTheControllerAsksTheConfig), FilesFor: sortedCopy(r.Nodes), LeaseCut: r.LeaseCut}} + var out []Principal served := servedReadsOf(r) logs := DeclaredLogs(r) grants := -1 for _, node := range sortedCopy(r.Nodes) { - witness := false var checks []string bus := false for _, d := range r.Assigned[node] { - if d.Module == controllerModule { - witness = true - } if d.SnapshotsTheBus { bus = true } @@ -194,7 +176,7 @@ func Users(r Records) ([]Principal, error) { // **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, WitnessesController: witness, Checks: checks, + 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)}) @@ -238,6 +220,10 @@ func Users(r Records) ([]Principal, error) { Emits: pr.Emits, Queues: pr.Queues, State: pr.State, Uses: d.Uses, Asks: pr.Asks, Invokes: pr.Invokes, Removes: pr.Removes, }) + // The 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 @@ -266,7 +252,6 @@ func Users(r Records) ([]Principal, error) { } if d.Module == GrantsModule { out[len(out)-1].CreatesStreams = declaredStreamsOf(r) - out[len(out)-1].LeaseCut = r.LeaseCut grants = len(out) - 1 } // **The vault's holder asks the desks and the issuing providers itself** (issue 723), under the @@ -289,17 +274,15 @@ func Users(r Records) ([]Principal, error) { out = append(out, Principal{Kind: KindNodeTools, Node: node, Module: RuntimeModule, Carries: carried}) } } - for _, node := range sortedCopy(r.Enrolling) { - out = append(out, Principal{Kind: KindEnrolment, Node: node, Redeems: servesVerb(r, EnrolmentRedeem), - LeaseHandedAway: r.LeaseHandedAway}) + // 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) { - p := Principal{Kind: KindPerson, Module: person, Invokes: r.People[person]} - if administers(p.Invokes) { - // The operator's hand path writes each node's compositions (ADR 0539 clause 10). - p.FilesFor = sortedCopy(r.Nodes) - } - out = append(out, p) + 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). @@ -368,9 +351,6 @@ func sortedNames(in map[string][]string) []string { return out } -// controllerModule is the controller's module: the machine assigned it witnesses its upgrades. -const controllerModule = "mesh-controller" - // 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. @@ -441,10 +421,6 @@ func holdsSeat(d Declared, seat string) bool { return false } -// vaultVerbsOf are the vault's intake verbs its holder serves, as the records say (issue 675): what the controller -// is granted to ask it in its own name. None while no holder serves them. -func vaultVerbsOf(r Records) []string { return servedOf(r, VerbsTheControllerAsksTheVault) } - // 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{} diff --git a/go/buscompose/writers.go b/go/buscompose/writers.go index fe5db6f..8ae64d6 100644 --- a/go/buscompose/writers.go +++ b/go/buscompose/writers.go @@ -25,7 +25,7 @@ type WriterRow struct { 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 (the controller's store, a module's own) or not built yet. + // 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. @@ -34,16 +34,13 @@ type WriterRow struct { Shared string } -// isController, and the other writers' tests, are who a row's writer is as a principal. -func isController(p Principal, _ string) bool { return p.Kind == KindController } - -// createsDeclared is the controller, or 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). +// 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 isController(p, pattern) { - return true - } - if removesAssignmentsStream(p, pattern) || setsLeaseExpiry(p, pattern) { + if removesLeftover(p, pattern) { return true } name, ok := strings.CutPrefix(pattern, "$JS.API.STREAM.CREATE.") @@ -109,17 +106,14 @@ func moduleNamed(module string) func(Principal, string) bool { } } -// ownAnnouncer is a module, a process of it or the node tools carrying it saying what that module keeps, or the -// controller saying what it keeps itself (novox/hq ADR 0495 clause 2): each under its own name alone. +// 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 } - switch p.Kind { - case KindController: - return tokens[2] == ControllerSeat - case KindProcess: + if p.Kind == KindProcess { return p.Module == tokens[2] } return ownModule(p, pattern) @@ -144,88 +138,39 @@ 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 declaration", Writer: "controller (lease holder)", KeptIn: "the bus, last per subject", - Others: "read", Subjects: []string{"mesh.node.*.declare"}, Writes: isController}, - // The operator's hand-over of a directory used as found, asked of the machine's engine at the controller's - // terminal (novox/hq issue 356). One publisher; and because a responder can still reach the subject through a - // reply, the ask is signed with the mesh's key and the engine verifies it — the row bounds who is granted the - // publish, the signature who is believed. - {State: "a hand-over asked of a machine", Writer: "controller, at its terminal", KeptIn: "the machine, beside its state", - Others: "the engine verifies the mesh's signature and records it, or refuses", - Subjects: []string{"mesh.node.*.ask.hand-over"}, Writes: isController}, - // The operator asking a machine's engine for a fresh search for setuid programs, at the controller's - // terminal (novox/hq issue 361): signed as a hand-over is, under a signing context of its own. - {State: "a fresh setuid search asked of a machine", Writer: "controller, at its terminal", - KeptIn: "the machine, which throws its last search away", Others: "the engine verifies the mesh's signature and starts it, or refuses", - Subjects: []string{"mesh.node.*.ask.setuid-search"}, Writes: isController}, - // A read-only judgement of a machine's declared directories asked of its engine (novox/hq issue 358): signed as a - // hand-over is, under a signing context of its own. It changes nothing on the machine. - {State: "a judgement of a machine's directories asked of it", Writer: "controller", - KeptIn: "nowhere: the engine answers it and keeps nothing", Others: "the engine verifies the mesh's signature and answers, or refuses", - Subjects: []string{"mesh.node.*.ask.judge-directories"}, Writes: isController}, {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): the node is what the controller judges the terminal by, so that - // subject is this machine's engine's alone. + // 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: "the controller lease", Writer: "the controller instance holding it", KeptIn: "key-value " + LeaseBucket, - Others: "a candidate waits", Subjects: kvOf(LeaseBucket), Writes: writesTheLease, - Shared: "from the lease write's cut the controller writes it no more and the grants part writes its key holder alone (novox/hq ADR 0582 clause 3)"}, - // The merges mesh-delivery announced, for a moved module's registration (ADR 0420 §7), written by the lease holder. - {State: "the merges mesh-delivery announced", Writer: "controller (lease holder)", - KeptIn: "key-value " + DeliveryMergesBucket, Others: "read at a moved module's registration", - Subjects: kvOf(DeliveryMergesBucket), Writes: isController}, - // And the compositions (novox/hq ADR 0539 clauses 2 and 4): each node's in its own bucket, which only that node's - // node-engine reads, and each digest with the judgement of its move, which the assigner reads. - {State: "the compositions and their digests", Writer: "controller (lease holder), compare-and-set", - KeptIn: "key-value composed- for each node, and " + delivery.ControllerComposedBucket, - Others: "a node-engine reads its own node's; the assigner reads the digests", - Subjects: kvOf(delivery.ControllerComposedBucket), Writes: isControllerOrTheHandPath}, - {State: "conditions", Writer: "controller", KeptIn: "key-value " + ConditionsBucket + " (and its history, " + - ConditionHistoryBucket + "); how often each was observed, when last and its newest evidence in the " + - "controller's store, so a look that changes nothing writes no bucket", Others: "raise or clear only through observations the controller reads", - Subjects: append(kvOf(ConditionsBucket), kvOf(ConditionHistoryBucket)...), Writes: isController}, - {State: "calls and their outcomes", Writer: "controller", KeptIn: "key-value " + CallsBucket, - Others: "read by id", Subjects: kvOf(CallsBucket), Writes: isController}, - {State: "the hand-act log", Writer: "controller, through the verbs that act", KeptIn: "key-value " + HandActsBucket, - Others: "—", Subjects: kvOf(HandActsBucket), Writes: isController}, - // What the healers did (novox/hq to-be 45 §7, Phase 3): the controller's alone, in its store — the - // budgets and the mesh-wide brake are counted from it, so a controller restarting cannot reset them. - {State: "the healers' acts and their brake", Writer: "controller (lease holder), each act begun before it is made", - KeptIn: "the controller's store", Others: "read through healers; each act said as healer-acted and in its condition's tried"}, - {State: "stream definitions and bus permissions", Writer: "controller", KeptIn: "the bus", Others: "—", + {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 stream ASSIGNMENTS at the lease write, and no other (research 072 row 33), and updates the lease's bucket for its expiry alone (novox/hq ADR 0582 clause 3)"}, + 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 controller asks", + 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, kept by - // the controller. + // 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 controller keeps the last of each and grants readers from it", + KeptIn: "the bus (EVENTS)", Others: "the grants part grants readers from it", Subjects: []string{"mesh.mod.*.event.states"}, Writes: ownAnnouncer}, - {State: "the states announced", Writer: "controller, from each module's announcement", - KeptIn: "key-value " + StatesAnnouncedBucket, Others: "the composition of the bus's users reads it", - Subjects: kvOf(StatesAnnouncedBucket), Writes: isController}, {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 controller asks the build seat to check it", Subjects: []string{"mesh.mod.*.event.pull.updated"}, + 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 build seat's holder that ran it, said as the controller's `checked`", + {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 controller keeps the newest word as a condition", + 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}, @@ -241,35 +186,13 @@ var WritersTable = []WriterRow{ {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)}, - {State: "the facts snapshot", Writer: "controller", KeptIn: "the artifact store, facts/latest", - Others: "the build seat reads"}, - // What a consumer gave up on (novox/hq issue 330): copied by the controller from the server's notice, - // and delivered again or dropped only through its verb, with why. - {State: "a message a consumer gave up on", Writer: "controller", KeptIn: "the bus, the stream " + DeadLettersStream, - Others: "read, delivered again or dropped through the controller's dead-letters verb", - Subjects: []string{deadLetterPrefix + ">", againPrefix + ">"}, Writes: keepsDeadLetters, - Shared: "the watchman keeps the copies from the controller's lease write, the controller before it; delivering again stays the controller's (novox/hq ADR 0564 clause 5)"}, - // The record mirrors (novox/hq ADR 0289 §9, design 54 §2): rows of this table, so a grant that lets - // anything else write them is refused at composition. Built so far: the assignments and each registered - // module's declared config (design 57 F3), and the list of predefined keys kept (design 57 K3). - {State: "the assignments mirror", Writer: "controller (lease holder): the verbs that assign, and the sends that record what runs; " + - "a mirror of its store only until the assignments are moved to the bus (novox/hq ADR 0408)", - KeptIn: "key-value " + AssignmentsBucket + ", the record once moved", Others: "read through `reads` as mesh-controller." + MirrorAssignments, - Subjects: kvOf(AssignmentsBucket), Writes: isController}, - // The chain (novox/hq ADR 0408): builds and rollouts beside the assignments above, which keep their bucket. Not - // mirrors of the store but the record, written by the lease holder by compare-and-set and removed by nobody. - {State: "the builds of the chain", Writer: "controller (lease holder), compare-and-set on the key's revision", - KeptIn: "key-value " + BuildsBucket + ", sixty-four revisions of each key, none removed", - Others: "read through `reads` as mesh-controller." + ChainBuilds, Subjects: kvOf(BuildsBucket), Writes: isController}, - {State: "the rollouts of the chain", Writer: "controller (lease holder), compare-and-set on the key's revision", - KeptIn: "key-value " + RolloutsBucket + ", sixty-four revisions of each key, none removed", - Others: "read through `reads` as mesh-controller." + ChainRollouts, Subjects: kvOf(RolloutsBucket), Writes: isController}, - // The grants issued (novox/hq ADR 0528 clause 2, issue 1394): the controller's, read by the assigner for its sixth rule. - {State: "the grants issued", Writer: "controller (lease holder), once the bus's user list carrying a module's manifest at a fingerprint was sent", - KeptIn: "key-value " + GrantsBucket + ", one key a module's fingerprint, none removed", - Others: "read through `reads` as mesh-controller." + ControllerGrants, Subjects: kvOf(GrantsBucket), Writes: isController}, + // 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, never by the controller; the view and the watchman read it. + // 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}, @@ -294,34 +217,23 @@ var WritersTable = []WriterRow{ 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 contents of a declaration's files over 64 KiB (novox/hq ADR 0486 §2): the controller alone writes the bucket, each - // object under the digest of its content; a node-engine reads one by its name, with no list and no write. - {State: "a declaration's large files", Writer: "controller, each object under the sha256 digest of its content; the " + - "operator's account on release 6's hand path alone (ADR 0539 clause 10)", + // 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 isControllerOrTheHandPath(p, pattern) || grantsOwnObjects(p, pattern) + return grantsOwnObjects(p, pattern) }, - Shared: "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)"}, - {State: "the declared-config mirror", Writer: "controller (lease holder), from its store, only on a change", - KeptIn: "key-value " + DeclaredConfigBucket, Others: "read through `reads` as mesh-controller." + MirrorDeclaredConfig, - Subjects: kvOf(DeclaredConfigBucket), Writes: isController}, - {State: "the predefined-keys mirror", Writer: "controller (lease holder), from its store, only on a change", - KeptIn: "key-value " + PredefinedBucket, Others: "read through `reads` as mesh-controller." + MirrorPredefined, - Subjects: kvOf(PredefinedBucket), Writes: isController}, - {State: "the declared-packages mirror", Writer: "controller (lease holder), from the composition of each node, only on a change", - KeptIn: "key-value " + DeclaredPackagesBucket, Others: "read through `reads` as mesh-controller." + MirrorDeclaredPackages + - ", and each node's engine its node's key, until the lease write (ADR 0565 clause 6)", - Subjects: kvOf(DeclaredPackagesBucket), Writes: isController}, + 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 controller reads it at a walk's end and a gate; a failure is that machine's failed apply", + Others: "the watchman reads it; a failure is that machine's failed apply", Subjects: []string{"prov.applied.*.>"}, Writes: ownProvisioner}, } @@ -368,9 +280,3 @@ func SubjectsOverlap(a, b string) bool { } } } - -// isControllerOrTheHandPath is the controller, or the operator's account (a person who may invoke every tool) writing -// release 6's hand path: the one second writer ADR 0539 clauses 2 and 10 name, for a composition made by hand. -func isControllerOrTheHandPath(p Principal, pattern string) bool { - return isController(p, pattern) || (p.Kind == KindPerson && administers(p.Invokes)) -} diff --git a/go/calladdress/calladdress.go b/go/calladdress/calladdress.go new file mode 100644 index 0000000..425ce45 --- /dev/null +++ b/go/calladdress/calladdress.go @@ -0,0 +1,108 @@ +// Package calladdress reads the key a module calls a tool or a seat verb by, as a module's invokes names it, with the +// machine it is asked of after '@': `seat:.[@]` or `.[@]` (novox/hq ADR 0159, +// ADR 0469 §3). Every caller in the SDK parses its key here before it publishes anything, so a malformed key fails at +// once and says why, never as a call nobody serves and a timeout (novox/hq issue 774, the lease write's stop asked on +// `...call.stop@novox-be...`). +package calladdress + +import ( + "fmt" + "strings" +) + +// Address is a parsed key: a seat and its verb, or a module and its tool, and the machine named, if any. +type Address struct { + Seat, Verb string + Module, Tool string + Node string +} + +// Key is the address as a module's invokes names it, without the machine. +func (a Address) Key() string { + if a.Seat != "" { + return "seat:" + a.Seat + "." + a.Verb + } + return a.Module + "." + a.Tool +} + +// Parse reads a key, refusing one with more than one '@', a node that is not one subject token, or a seat, verb, module +// or tool that holds a wildcard, white space or (but a tool) a dot. +func Parse(key string) (Address, error) { + name, node, err := cutNode(key) + if err != nil { + return Address{}, err + } + if rest, ok := strings.CutPrefix(name, "seat:"); ok { + seat, verb, ok := strings.Cut(rest, ".") + if !ok || seat == "" || verb == "" { + return Address{}, fmt.Errorf("%q names no seat and verb (seat:.)", key) + } + if err := tokens(key, seat, verb); err != nil { + return Address{}, err + } + return Address{Seat: seat, Verb: verb, Node: node}, nil + } + module, tool, ok := strings.Cut(name, ".") + if !ok || module == "" || tool == "" { + return Address{}, fmt.Errorf("%q is not ., and does not start seat:.", key) + } + if err := tokens(key, module); err != nil { + return Address{}, err + } + if err := checkTool(key, tool); err != nil { + return Address{}, err + } + return Address{Module: module, Tool: tool, Node: node}, nil +} + +// CheckOwn is Parse for a caller that may also name a tool of its own module by its bare name (the runtime's mesh/ask). +func CheckOwn(key string) error { + name, _, err := cutNode(key) + if err != nil { + return err + } + if !strings.HasPrefix(name, "seat:") && !strings.Contains(name, ".") { + return tokens(key, name) + } + _, err = Parse(key) + return err +} + +// CheckNode refuses a node name that is not one subject token. +func CheckNode(node string) error { + if node == "" || strings.ContainsAny(node, ".*>@ \t\r\n") { + return fmt.Errorf("%q is not a node name (one subject token: no '.', '*', '>', '@' or space)", node) + } + return nil +} + +func cutNode(key string) (name, node string, err error) { + name, node, at := strings.Cut(key, "@") + if !at { + return key, "", nil + } + if strings.Contains(node, "@") { + return "", "", fmt.Errorf("%q holds more than one '@': a key names at most one machine", key) + } + if err := CheckNode(node); err != nil { + return "", "", fmt.Errorf("%q: %w", key, err) + } + return name, node, nil +} + +func tokens(key string, parts ...string) error { + for _, p := range parts { + if p == "" || strings.ContainsAny(p, ".*> \t\r\n") { + return fmt.Errorf("%q: %q is not one subject token (no '.', '*', '>' or space)", key, p) + } + } + return nil +} + +// checkTool refuses a tool name with a wildcard or white space; a dot is kept, as the tool runner serves it. +func checkTool(key, tool string) error { + if strings.ContainsAny(tool, "*> \t\r\n") || strings.HasPrefix(tool, ".") || strings.HasSuffix(tool, ".") { + return fmt.Errorf("%q: %q is not one subject token (no '*', '>' or space)", key, tool) + } + return nil +} diff --git a/go/calladdress/calladdress_test.go b/go/calladdress/calladdress_test.go new file mode 100644 index 0000000..2614350 --- /dev/null +++ b/go/calladdress/calladdress_test.go @@ -0,0 +1,56 @@ +package calladdress + +import ( + "strings" + "testing" +) + +// Issue 774 notes 84-87 (the lease write's stop asked of `seat:node-service-manager.stop@novox-be` published on a subject +// nobody serves and timed out after 30 s): a key names its machine after '@', and every other malformed key is refused +// before anything is published, naming why. +func TestAKeyIsParsedOrRefusedBeforeItIsPublished(t *testing.T) { + for _, row := range []struct { + key, want string // want: "seat|||", "mod|||", or a refusal's words + }{ + {"seat:node-service-manager.stop@novox", "seat|node-service-manager|stop|novox"}, + {"seat:operator-channel.tell", "seat|operator-channel|tell|"}, + {"mesh-build-go.fetch@ace", "mod|mesh-build-go|fetch|ace"}, + {"nats.nats_users", "mod|nats|nats_users|"}, + {"seat:node-service-manager.stop@novox@g14", "more than one '@'"}, + {"seat:node-service-manager.stop@", "not a node name"}, + {"seat:node-service-manager.stop@nov.ox", "not a node name"}, + {"seat:node-files.*", "not one subject token"}, + {"mesh-build-go.fe tch", "not one subject token"}, + {"mesh>build.fetch", "not one subject token"}, + {"seat:noverb", "names no seat and verb"}, + {"nodot", "is not ."}, + {"", "is not ."}, + } { + a, err := Parse(row.key) + got := "" + switch { + case err != nil: + got = err.Error() + if !strings.Contains(got, row.want) { + t.Errorf("Parse(%q) refused %q, want %q", row.key, got, row.want) + } + continue + case a.Seat != "": + got = "seat|" + a.Seat + "|" + a.Verb + "|" + a.Node + default: + got = "mod|" + a.Module + "|" + a.Tool + "|" + a.Node + } + if got != row.want { + t.Errorf("Parse(%q) = %s, want %s", row.key, got, row.want) + } + } + if a, err := Parse("seat:node-files.read@g14"); err != nil || a.Key() != "seat:node-files.read" { + t.Errorf("the key without its machine: %q, %v", a.Key(), err) + } + if err := CheckOwn("list@ace"); err != nil { + t.Errorf("a module's own tool by its bare name was refused: %v", err) + } + if err := CheckOwn("li*st"); err == nil { + t.Error("a bare name that is not one token was accepted") + } +} diff --git a/go/catalogue/control_records_seats.go b/go/catalogue/control_records_seats.go index 5d30fd0..b82b6eb 100644 --- a/go/catalogue/control_records_seats.go +++ b/go/catalogue/control_records_seats.go @@ -24,13 +24,12 @@ func wantsVerbs() []Verb { {Name: "upgrade", Description: "Whether each module's new builds wait for a person: its policy (roll, together or record) " + "and where it comes from (a person, the module, the bus, its irreplaceable data, the default); the bus is never " + "rolled. With module, that one; with module, policy and why, a person's choice for it, recorded as a hand act; " + - "with policy default, the choice taken back. With compare, every module whose policy differs from the one the " + - "controller writes beside its compositions.", + "with policy default, the choice taken back. With compare, each node's own composition and the assigner's findings.", Input: schema(map[string]string{ "module": "one module", "policy": "with module: roll, together, record, or default to take a person's choice back", "why": "with policy: why, required and kept", - "compare": "\"true\": every module whose policy at the assigner differs from the controller's", + "compare": "\"true\": each node's own composition, by its digest, and the assigner's findings", }, nil, "compare")}, {Name: "bus", Description: "What a bus upgrade would do: the fingerprint of the bus module each node runs against the " + "indexer's newest, and the last bus steps from the hand-act log and the assigner's puts of the bus. Read-only: " + diff --git a/go/catalogue/controller_dependence.go b/go/catalogue/controller_dependence.go index 298b8ba..afcb9cf 100644 --- a/go/catalogue/controller_dependence.go +++ b/go/catalogue/controller_dependence.go @@ -7,118 +7,62 @@ import ( "strings" ) -// **What still depends on the controller, and the lease write's check** (novox/hq ADR 0571 §13, ADR 0582 clause 2, -// issue 774): every declaration of a manifest other than the controller's on the controller (a read of one of its -// states, an invoke of one of its verbs or tools, a consume or a wait on one of its events, a use of its seat), judged -// against the named, temporary list of what another accepted record keeps until the lease write. The grants part's -// grants_lease_check runs it over every manifest the indexer holds; ManifestRefusals runs it over a repository's -// manifests, in the test ADR 0571 §13 names in mesh-catalog, mesh-delivery and mesh-tools. A read of the lease key is no -// dependency: it names the lease's holder, whoever that is, and parts read it to learn that the write happened. +// **Nothing may name the controller** (novox/hq ADR 0590 clause 5, issue 1126): the controller ended by its unassignment +// on 2026-11-02 and its code left the mesh, so a manifest that reads one of its states (its lease too), invokes one of its +// verbs or tools, consumes or waits on one of its events, uses, claims or requires it, or is it, depends on nothing that +// answers. ManifestRefusals runs the check over a repository's manifests, in the test each repository of manifests keeps +// (mesh-catalog, mesh-delivery, mesh-tools); a later decision that places a controller again must first lift it. -// ControllerLease is the controller's lease as a reader names it (`.`): no dependency. -const ControllerLease = ControllerSeatName + ".lease" +// EndedControllerName is the name the ended controller had, as its module and as its seat: no manifest may name it. +const EndedControllerName = "mesh-controller" -// The kinds of a dependency on the controller, as a manifest declares them. +// The kinds of a declaration that names the controller. const ( - DependsRead = "read" - DependsInvoke = "invoke" - DependsConsume = "consume" - DependsUses = "uses" - DependsWaits = "waits" + DependsRead = "read" + DependsInvoke = "invoke" + DependsConsume = "consume" + DependsUses = "uses" + DependsWaits = "waits" + DependsClaims = "claims" + DependsRequires = "requires" + DependsIs = "is" ) -// The phases of the lease write's check (ADR 0571 §13): before the lease write's own delivery the whole named list is -// allowed; that delivery drops every entry but the three kept to the write itself, and its check runs with those alone. -const ( - PhaseBefore = "before-delivery" - PhaseWrite = "write" -) - -// Dependency is one declaration of a manifest other than the controller's on the controller: a read of one of its -// states, an invoke of one of its verbs or tools, a consume or a wait on one of its events, or a send to its seat. +// Dependency is one declaration of a manifest that names the controller. type Dependency struct { Module string `json:"module"` // By is the account declaring it when it is not the module's own: `process ` or `tools `. By string `json:"by,omitempty"` Kind string `json:"kind"` What string `json:"what"` - // Kept is the record that keeps it until the lease write, from the named list; empty when none does. - Kept string `json:"kept,omitempty"` - // ToTheWrite says it is one of the three kept to the write itself, which the lease write's own delivery keeps. - ToTheWrite bool `json:"to_the_write,omitempty"` -} - -// KeptDependency is one entry of the named, temporary list ADR 0571 §13 allows: a read, invoke or consume another -// accepted record keeps until the lease write, with that record. ToTheWrite marks the three kept to the write itself. -type KeptDependency struct { - Module, Kind, What, Record string - ToTheWrite bool -} - -// KeepList is the named list. Each entry names the clause that keeps it; a dependency not on it fails the check. It -// holds the three kept to the write itself and nothing else: the lease write's own delivery dropped every other entry -// (ADR 0571 §13; issue 774), so its check runs with these alone in either phase. The third, the bus node agent's -// following of the controller's user list (ADR 0571 §8, ADR 0547 §8), is no manifest's declaration but the -// node-engine's own, so no manifest matches it; it stays named here so the list reads as the record has it. -var KeepList = []KeptDependency{ - {Module: "mesh-delivery-builder", Kind: DependsConsume, What: "mesh-controller.artifacts-gone", Record: "ADR 0571 §4, §13", ToTheWrite: true}, - {Module: "artifact-store-tools", Kind: DependsInvoke, What: "seat:mesh-controller.collect", Record: "ADR 0571 §4, §13", ToTheWrite: true}, - {Module: "node-engine", Kind: DependsRead, What: "the controller's user list of the bus module's composition", Record: "ADR 0571 §8, §13; ADR 0547 §8", ToTheWrite: true}, -} - -// EndedBy names, for a declaration no entry keeps, the record that already ended it, so a refusal says where it moved. -// The controller's `grants` is one: ADR 0571 §9 has no manifest read it from L7's delivery, which changes ADR 0567 §10 -// and ADR 0547 §5's shadow compare, so neither the assigner's nor the grants part's read of it is kept. -var EndedBy = map[string]string{ - DependsRead + " mesh-controller.grants": "ADR 0571 §9: no manifest reads the controller's grants; the grants part's key is the one read", - DependsInvoke + " seat:mesh-controller.hand-act": "ADR 0565 §11: every recorder calls mesh-hand-acts", - DependsInvoke + " seat:mesh-controller.warranted": "ADR 0565 §11: every recorder calls mesh-hand-acts", - DependsInvoke + " seat:mesh-controller.root-free": "ADR 0571 §11: each machine's engine answers root-free (seat:node-engine.root-free@machine)", - DependsInvoke + " seat:mesh-controller.held": "ADR 0571 §2: held is the seat mesh-artifact-sweep's", -} - -// keptFor is the list's entry for one dependency, or nil. -func keptFor(d Dependency) *KeptDependency { - for i, k := range KeepList { - if k.Module == d.Module && k.Kind == d.Kind && k.What == d.What { - return &KeepList[i] - } - } - return nil } // onController says whether a name `.` is the controller's. -func onController(name string) bool { return strings.HasPrefix(name, ControllerSeatName+".") } +func onController(name string) bool { return strings.HasPrefix(name, EndedControllerName+".") } // invokesController says whether an invoke names a verb of the controller's seat or a tool of its module. `*` names none: -// it is the console's grant to every tool, judged by no record as a dependency. +// it is the console's grant to every tool. func invokesController(invoke string) bool { if seat, _, ok := SeatInvoke(invoke); ok { - return seat == ControllerSeatName + return seat == EndedControllerName } return onController(invoke) } -// Dependencies are every declaration on the controller in the manifests given, by module, each with the list's entry -// that keeps it. The controller's own manifest is not judged, nor a read of its lease key: it names the lease's holder, -// whoever that is, and parts read it to learn that the write happened (ADR 0571 §13). +// Dependencies are every declaration naming the controller in the manifests given, by module. func Dependencies(manifests map[string]Manifest) []Dependency { var out []Dependency add := func(module, by, kind, what string) { - d := Dependency{Module: module, By: by, Kind: kind, What: what} - if k := keptFor(d); k != nil { - d.Kept, d.ToTheWrite = k.Record, k.ToTheWrite - } - if !slices.Contains(out, d) { + if d := (Dependency{Module: module, By: by, Kind: kind, What: what}); !slices.Contains(out, d) { out = append(out, d) } } for name, m := range manifests { - if name == ControllerSeatName || m.Module == ControllerSeatName { - continue + if name == EndedControllerName || m.Module == EndedControllerName { + add(name, "", DependsIs, EndedControllerName) } for _, r := range m.Reads { - if onController(r) && r != ControllerLease { + if onController(r) { add(name, "", DependsRead, r) } } @@ -138,10 +82,20 @@ func Dependencies(manifests map[string]Manifest) []Dependency { } } for _, u := range m.Uses { - if u == ControllerSeatName { + if u == EndedControllerName { add(name, "", DependsUses, u) } } + for _, c := range m.Claims { + if c.Name == EndedControllerName { + add(name, "", DependsClaims, c.Name) + } + } + for _, r := range m.Requires { + if r == EndedControllerName || onController(r) { + add(name, "", DependsRequires, r) + } + } for _, a := range m.BusAccounts() { by := "process " + a.ID if a.Tools { @@ -170,69 +124,20 @@ func Dependencies(manifests map[string]Manifest) []Dependency { return out } -// Verdict is the lease write's check in one phase. -type Verdict struct { - Phase string `json:"phase"` - Passes bool `json:"passes"` - Reasons []string `json:"reasons"` -} - -// Coverage is how much of the mesh's manifests the dependencies were derived from: the manifests read, the modules the -// indexer holds (-1 when its keys could not be listed, which Unread then names), and each manifest that could not be -// read. -type Coverage struct { - Read, Indexed int - Unread []Unread -} - -// Unread is one manifest, or one listing of them, that could not be read, and why. -type Unread struct { - Input string `json:"input"` - Why string `json:"why"` -} - -// Judge runs the lease write's check over the dependencies (ADR 0571 §13) in one phase: before the lease write's own -// delivery, every dependency must be on the named list; at the write, only the three kept to the write itself may stay. -// It passes only over every module the indexer holds: no manifest read, fewer read than the indexer holds, or one that -// could not be read fails it, since what was not read could hide a dependency. A lease already handed away leaves -// nothing to write. -func Judge(deps []Dependency, phase string, cov Coverage, handedAway bool, leaseSays string) Verdict { - v := Verdict{Phase: phase, Reasons: []string{}} - if handedAway { - v.Reasons = append(v.Reasons, "there is nothing to write: "+leaseSays) +// ManifestRefusals is the check over a repository's manifests, by module name: one reason per declaration naming the +// controller, none when it passes. None given fails it, since nothing read can hide a declaration. +func ManifestRefusals(manifests map[string]Manifest) []string { + if len(manifests) == 0 { + return []string{"no manifest was read: with nothing read, nothing naming the controller can be judged absent"} } - switch { - case cov.Read == 0: - v.Reasons = append(v.Reasons, fmt.Sprintf("no manifest was read from the indexer (it holds %d modules): with nothing read, nothing on the controller can be judged absent", max(cov.Indexed, 0))) - case cov.Indexed >= 0 && cov.Read < cov.Indexed: - v.Reasons = append(v.Reasons, fmt.Sprintf("only %d of the %d modules the indexer holds had their manifest read, so what the others declare on the controller is not known", cov.Read, cov.Indexed)) - } - for _, d := range deps { + var out []string + for _, d := range Dependencies(manifests) { who := d.Module if d.By != "" { who += " (" + d.By + ")" } - switch { - case d.Kept == "": - why := fmt.Sprintf("%s declares %s %s, which no record keeps until the lease write (ADR 0571 §13)", who, d.Kind, d.What) - if ended := EndedBy[d.Kind+" "+d.What]; ended != "" { - why += "; ended by " + ended - } - v.Reasons = append(v.Reasons, why) - case phase == PhaseWrite && !d.ToTheWrite: - v.Reasons = append(v.Reasons, fmt.Sprintf("%s declares %s %s, kept by %s only until the lease write's own delivery, which drops it", who, d.Kind, d.What, d.Kept)) - } + out = append(out, fmt.Sprintf("%s declares %s %s: the controller ended by its unassignment and nothing may name it "+ + "(ADR 0590 clause 5)", who, d.Kind, d.What)) } - for _, g := range cov.Unread { - v.Reasons = append(v.Reasons, fmt.Sprintf("%s could not be read (%s), so what it declares on the controller is not known", g.Input, g.Why)) - } - v.Passes = len(v.Reasons) == 0 - return v -} - -// ManifestRefusals is the lease write's check over a repository's manifests, by module name, in one phase (PhaseBefore -// or PhaseWrite): each reason the check gives, none when it passes. Every manifest given counts as read: the caller reads -// them from its own tree, and none given fails it. -func ManifestRefusals(manifests map[string]Manifest, phase string) []string { - return Judge(Dependencies(manifests), phase, Coverage{Read: len(manifests), Indexed: -1}, false, "").Reasons + return out } diff --git a/go/catalogue/controller_dependence_test.go b/go/catalogue/controller_dependence_test.go index 410e27f..e6d2ce3 100644 --- a/go/catalogue/controller_dependence_test.go +++ b/go/catalogue/controller_dependence_test.go @@ -6,98 +6,53 @@ import ( "testing" ) -// Issue 774 (ADR 0571 §13, ADR 0582 clause 2): the lease write's check judges every manifest's dependency on the -// controller against the named list, at the write (the three kept to it alone) and before its delivery. Moved with the -// judge from mesh-grants' leasecheck_test.go (TestTheLeaseCheckJudgesEveryDependencyOnTheController's manifest rows and -// TestTheKeepListNamesItsRecordsAndThreeToTheWrite), whose manifests were mesh-grants' fixtures; the conditions row is -// mesh-conditions' manifest at mesh-catalog ce3bedd5 (module.json:31-32). -func TestTheLeaseWritesCheckJudgesEveryDependencyOnTheController(t *testing.T) { - toTheWrite := 0 - for _, k := range KeepList { - if !strings.HasPrefix(k.Record, "ADR ") || k.Module == "" || k.What == "" { - t.Errorf("entry %+v names no record, module or declaration", k) - } - if k.ToTheWrite { - toTheWrite++ +// Issue 1126 B1/AC1, B2/AC1 (ADR 0590 clause 5): the seat set holds no mesh-controller, and the dependence check refuses +// every manifest that reads (the lease too), invokes, consumes, waits on, uses, claims, requires or is mesh-controller. +func TestNothingMayNameTheController(t *testing.T) { + for _, s := range DefaultSeats() { + if s.Name == EndedControllerName { + t.Errorf("the seat set still holds %s", s.Name) } } - if toTheWrite != 3 || len(KeepList) != 3 { - t.Errorf("%d entries, %d kept to the write itself: ADR 0571 §13 keeps the three alone from the write's delivery", len(KeepList), toTheWrite) - } - - type want struct { - deps []string // " ", kept by the record or "unkept" - write string // a reason the check at the write names, or "" when it passes - before string // a reason the check before the delivery names, or "" when it passes + if _, ok := SeatNamed(EndedControllerName); ok { + t.Errorf("SeatNamed still finds %s", EndedControllerName) } for _, row := range []struct { - name string - manifests map[string]string - want want + name, manifest string + want []string // " " }{ - // 774/S4/AC2. - {"a read of the lease alone is no dependency, and the controller's own manifest is not judged", - map[string]string{"mod-l": `{"module":"mod-l","version":"1","reads":["mesh-controller.lease"]}`, - "mesh-controller": `{"module":"mesh-controller","version":"1","invokes":["seat:mesh-controller.status"]}`}, - want{}}, - // 774/S4/AC1, 774/S30/AC1: the keeper's read of the controller's conditions put back is refused in either phase, - // since the lease write's delivery dropped its entry (it was kept by ADR 0564 §9 before). - {"the keeper's read of the controller's conditions, put back, is refused", - map[string]string{"mesh-conditions": `{"module":"mesh-conditions","version":"1","reads":["mesh-controller.conditions","mesh-controller.lease"]}`}, - want{deps: []string{"mesh-conditions read mesh-controller.conditions unkept"}, - write: "mesh-conditions declares read mesh-controller.conditions, which no record keeps until the lease write", - before: "mesh-conditions declares read mesh-controller.conditions, which no record keeps until the lease write"}}, - // 774/S30/AC1: the three kept to the write pass in either phase. - {"the artifact sweep's collect is kept to the write itself", - map[string]string{"artifact-store-tools": `{"module":"artifact-store-tools","version":"1","invokes":["seat:mesh-controller.collect"]}`}, - want{deps: []string{"artifact-store-tools invoke seat:mesh-controller.collect kept by ADR 0571 §4, §13"}}}, - {"the builder's consume of artifacts-gone is kept to the write itself", - map[string]string{"mesh-delivery-builder": `{"module":"mesh-delivery-builder","version":"1","consumes":["mesh-controller.artifacts-gone"]}`}, - want{deps: []string{"mesh-delivery-builder consume mesh-controller.artifacts-gone kept by ADR 0571 §4, §13"}}}, - {"a read no record keeps fails both phases, named", - map[string]string{"mod-u": `{"module":"mod-u","version":"1","reads":["mesh-controller.declared-packages"]}`}, - want{deps: []string{"mod-u read mesh-controller.declared-packages unkept"}, - write: "mod-u declares read mesh-controller.declared-packages, which no record keeps until the lease write", - before: "mod-u declares read mesh-controller.declared-packages, which no record keeps until the lease write"}}, - {"a seat verb and a module tool of the controller's, a consume and a wait are each one; every tool (*) is none", - map[string]string{"mod-i": `{"module":"mod-i","version":"1","invokes":["seat:mesh-controller.root-free","mesh-controller.status","*","nats.nats_users"],` + - `"consumes":["mesh-controller.checked","mod-x.thing"],"uses":["mesh-controller"]}`, - "node-tools": `{"module":"node-tools","version":"1","waits":["mesh-controller.checked"]}`}, - want{deps: []string{"mod-i consume mesh-controller.checked unkept", "mod-i invoke mesh-controller.status unkept", - "mod-i invoke seat:mesh-controller.root-free unkept", "mod-i uses mesh-controller unkept", "node-tools waits mesh-controller.checked unkept"}, - write: "mod-i declares invoke seat:mesh-controller.root-free, which no record keeps until the lease write (ADR 0571 §13); ended by ADR 0571 §11", - before: "node-tools declares waits mesh-controller.checked"}}, - {"a process's own invoke of the controller is its module's dependency, named by the process", - map[string]string{"mod-p": `{"module":"mod-p","version":"1","resources":[{"id":"account","type":"user","name":"mod-p"},{"id":"worker","type":"process","run":["./w"],"user":"mod-p",` + - `"bus":{"invokes":["seat:mesh-controller.held"]}}]}`}, - want{deps: []string{"mod-p invoke seat:mesh-controller.held unkept"}, - write: "mod-p (process worker) declares invoke seat:mesh-controller.held", before: "mod-p (process worker)"}}, - {"no manifest fails both phases", map[string]string{}, want{write: "no manifest was read", before: "no manifest was read"}}, + {"a read of the lease", `{"module":"mod-l","version":"1","reads":["mesh-controller.lease"]}`, []string{"read mesh-controller.lease"}}, + {"the controller's own manifest", `{"module":"mesh-controller","version":"1"}`, []string{"is mesh-controller"}}, + {"a claim of its seat", `{"module":"a-controller","version":"1","claims":[{"name":"mesh-controller","scope":"mesh"}]}`, + []string{"claims mesh-controller"}}, + {"each declaration kind; every tool (*) is none", + `{"module":"mod-i","version":"1","invokes":["seat:mesh-controller.root-free","mesh-controller.status","*","nats.nats_users"],` + + `"consumes":["mesh-controller.artifacts-gone","mod-x.thing"],"uses":["mesh-controller"],"waits":["mesh-controller.checked"],` + + `"requires":["mesh-controller"]}`, + []string{"consume mesh-controller.artifacts-gone", "invoke mesh-controller.status", "invoke seat:mesh-controller.root-free", + "requires mesh-controller", "uses mesh-controller", "waits mesh-controller.checked"}}, + {"a process's own invoke", `{"module":"mod-p","version":"1","resources":[{"id":"account","type":"user","name":"mod-p"},` + + `{"id":"worker","type":"process","run":["./w"],"user":"mod-p","bus":{"invokes":["seat:mesh-controller.held"]}}]}`, + []string{"invoke seat:mesh-controller.held"}}, + {"a module on its own seats", `{"module":"mod-ok","version":"1","reads":["mesh-grants.accounts"],"invokes":["*"]}`, nil}, } { - manifests := map[string]Manifest{} - for name, raw := range row.manifests { - m, err := Decode([]byte(raw)) - if err != nil { - t.Fatalf("%s: %s: %v", row.name, name, err) - } - manifests[name] = m + m, err := Decode([]byte(row.manifest)) + if err != nil { + t.Fatalf("%s: %v", row.name, err) } - var deps []string - for _, d := range Dependencies(manifests) { - kept := "unkept" - if d.Kept != "" { - kept = "kept by " + d.Kept - } - deps = append(deps, d.Module+" "+d.Kind+" "+d.What+" "+kept) + var got []string + for _, d := range Dependencies(map[string]Manifest{m.Module: m}) { + got = append(got, d.Kind+" "+d.What) } - if !slices.Equal(deps, row.want.deps) { - t.Errorf("%s: dependencies %q, want %q", row.name, deps, row.want.deps) + if !slices.Equal(got, row.want) { + t.Errorf("%s: dependencies %q, want %q", row.name, got, row.want) } - for phase, reason := range map[string]string{PhaseWrite: row.want.write, PhaseBefore: row.want.before} { - got := ManifestRefusals(manifests, phase) - if (reason == "") != (len(got) == 0) || !slices.ContainsFunc(got, func(r string) bool { return strings.Contains(r, reason) }) && reason != "" { - t.Errorf("%s: at %s refused %q, want %q", row.name, phase, got, reason) - } + refused := ManifestRefusals(map[string]Manifest{m.Module: m}) + if len(refused) != len(row.want) || len(refused) > 0 && !strings.Contains(refused[0], "ADR 0590") { + t.Errorf("%s: refused %q", row.name, refused) } } + if got := ManifestRefusals(map[string]Manifest{}); len(got) != 1 || !strings.Contains(got[0], "no manifest") { + t.Errorf("no manifest given: %q", got) + } } diff --git a/go/catalogue/delivery_seat_test.go b/go/catalogue/delivery_seat_test.go index 0ea1642..046b93d 100644 --- a/go/catalogue/delivery_seat_test.go +++ b/go/catalogue/delivery_seat_test.go @@ -9,7 +9,6 @@ import ( // The delivery seat answers "what did the mesh's checks say of this pull request?" as a verb of its own, // `checks` (novox/hq ADR 0239): read by repository and a pull request's number or a commit. func TestTheDeliverySeatPromisesChecks(t *testing.T) { - t.Run("the seat mesh-grants promises the lease write", grantsSeatPromisesTheLeaseWrite) seat, ok := SeatNamed(DeliverySeat) if !ok { t.Fatal("the delivery seat is not in the set") diff --git a/go/catalogue/enrolment_seat.go b/go/catalogue/enrolment_seat.go index ee41572..b01a054 100644 --- a/go/catalogue/enrolment_seat.go +++ b/go/catalogue/enrolment_seat.go @@ -84,7 +84,7 @@ func enrolmentVerbs() []Verb { Input: schema(map[string]string{"node": "the node", "why": "why, recorded with the hand act"}, []string{"node", "why"})}, {Name: EnrolmentVerbTokens, Description: "Read every open enrolment, or one: its stage, expiry, ask and revision, " + - "the bucket's fill against its cap, and whether issuing is on and why (the lease). For a node at live, its " + + "the bucket's fill against its cap. For a node at live, its " + "token sealed to the operator key, asked of the vault at that moment and relayed as the vault sealed it. With " + "history, the ended enrolments from the seat's events, as far back as the events are kept.", Input: schema(map[string]string{"node": "one node", "history": "\"true\": the ended enrolments too"}, diff --git a/go/catalogue/grants_seat.go b/go/catalogue/grants_seat.go deleted file mode 100644 index 5d12b55..0000000 --- a/go/catalogue/grants_seat.go +++ /dev/null @@ -1,53 +0,0 @@ -package catalogue - -// **The lease write, a verb of the seat `mesh-grants`** (novox/hq ADR 0582 clause 1, issue 774): the grants part -// holds the seat once for the mesh and, on the operator's warrant, hands the controller's lease to itself and back. -// It serves two verbs and says three events, none carrying a secret: -// -// - lease-handed: the lease names mesh-grants, written on the operator's warrant (clause 3); -// - lease-altered: the lease key or its bucket's expiry differs from what mesh-grants wrote (clause 4); -// - removal-failed: the day-later removal of the controller's consumer or stream failed (clause 6). - -// GrantsSeat is the seat the grants part (mesh-grants) holds. -const GrantsSeat = "mesh-grants" - -// The seat's verbs. -const ( - GrantsVerbLeaseWrite = "lease-write" - GrantsVerbLeaseHandBack = "lease-hand-back" -) - -// The seat's events. -const ( - GrantsEventLeaseHanded = "lease-handed" - GrantsEventLeaseAltered = "lease-altered" - GrantsEventRemovalFailed = "removal-failed" -) - -// grantsDecision is the record that made the seat. -const grantsDecision = "novox/hq ADR 0582" - -// grantsEvents are the seat's events, in the order the write meets them. -func grantsEvents() []string { - return []string{GrantsEventLeaseHanded, GrantsEventLeaseAltered, GrantsEventRemovalFailed} -} - -// grantsVerbs are the seat's verbs (ADR 0582 clauses 1, 2, 3, 5). -func grantsVerbs() []Verb { - return []Verb{ - {Name: GrantsVerbLeaseWrite, Description: "Hand the controller's lease to mesh-grants, kept for good. Refused, " + - "naming the check, until the lease check at phase write passes and every condition it does not judge reads " + - "green. Otherwise the operator is asked once; on the warrant the checks are read again, the controller's " + - "service is stopped, the bus's user list is composed without the controller's write on the lease, the lease's " + - "expiry is removed and the key holder names mesh-grants, said as lease-handed and recorded as a hand act. A no " + - "or an expiry of the ask acts on nothing.", - Input: schema(map[string]string{"why": "why the lease is handed now, shown to the operator and kept"}, - []string{"why"})}, - {Name: GrantsVerbLeaseHandBack, Description: "Give the lease back to the controller, for a day after the write: " + - "asked and warranted the same way, it restores the lease's fifteen-second expiry, deletes the key holder, " + - "gives the controller its rights back once the list is loaded, and starts the controller. Refused once the " + - "day-later removal ran: the controller would start without its consumer and stream.", - Input: schema(map[string]string{"why": "why the lease goes back, shown to the operator and kept"}, - []string{"why"})}, - } -} diff --git a/go/catalogue/grants_seat_test.go b/go/catalogue/grants_seat_test.go deleted file mode 100644 index 62e08b3..0000000 --- a/go/catalogue/grants_seat_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package catalogue - -import ( - "reflect" - "strings" - "testing" -) - -// The seat mesh-grants (novox/hq ADR 0582 clause 1, issue 774, 774/S1/AC2): its holder serves lease-write and -// lease-hand-back, both a condition of holding, and nothing else; the seat says lease-handed, lease-altered and -// removal-failed. A case of TestTheDeliverySeatPromisesChecks, the seat-verb test, within the test budget. -func grantsSeatPromisesTheLeaseWrite(t *testing.T) { - seat, ok := SeatNamed(GrantsSeat) - if !ok { - t.Fatal("the seat mesh-grants is not in the set") - } - if seat.Scope != ScopeMesh || seat.Delivers != "" { - t.Errorf("mesh-grants is at scope %q delivering %q, want mesh and nothing", seat.Scope, seat.Delivers) - } - if got, want := VerbNames(seat.Serves), []string{"lease-write", "lease-hand-back"}; !reflect.DeepEqual(got, want) { - t.Errorf("mesh-grants serves %v, want %v", got, want) - } - if want := []string{"lease-handed", "lease-altered", "removal-failed"}; !reflect.DeepEqual(seat.Emits, want) { - t.Errorf("mesh-grants says %v, want %v", seat.Emits, want) - } - for _, v := range seat.Serves { - if v.Optional { - t.Errorf("%s is optional: the holder ships with the seat, so every verb is a condition of holding", v.Name) - } - props, _ := v.Input["properties"].(map[string]any) - if req, _ := v.Input["required"].([]string); len(props) != 1 || !reflect.DeepEqual(req, []string{"why"}) { - t.Errorf("%s takes %v requiring %v, want only a why", v.Name, props, req) - } - } - - claim := func(serves ...string) Manifest { - return Manifest{Module: "mesh-grants", Claims: []Claim{{Name: GrantsSeat, Scope: ScopeMesh, Serves: serves}}} - } - for _, c := range []struct { - name string - m Manifest - refuse string // empty: held - }{ - {"both verbs", claim("lease-write", "lease-hand-back"), ""}, - {"without the hand-back", claim("lease-write"), "lease-hand-back"}, - {"a verb the seat does not promise", claim("lease-write", "lease-hand-back", "lease-take"), "lease-take"}, - {"at node scope", Manifest{Module: "mesh-grants", Claims: []Claim{{Name: GrantsSeat, Scope: ScopeNode, - Serves: []string{"lease-write", "lease-hand-back"}}}}, "scope"}, - } { - err := CanHold(c.m, seat) - switch { - case c.refuse == "" && err != nil: - t.Errorf("%s: refused: %v", c.name, err) - case c.refuse != "" && (err == nil || !strings.Contains(err.Error(), c.refuse)): - t.Errorf("%s: %v, want a refusal naming %q", c.name, err, c.refuse) - } - } - - // No other seat changed: the set is the one before it with mesh-grants added beside mesh-hand-acts. - var names []string - for _, s := range DefaultSeats() { - names = append(names, s.Name) - } - at := strings.Index(strings.Join(names, " "), HandActsSeat+" "+GrantsSeat+" ") - if at < 0 { - t.Errorf("mesh-grants does not follow mesh-hand-acts in the seat set: %v", names) - } -} diff --git a/go/catalogue/health_seats.go b/go/catalogue/health_seats.go index 79ae2c6..e208f1f 100644 --- a/go/catalogue/health_seats.go +++ b/go/catalogue/health_seats.go @@ -29,7 +29,7 @@ func conditionsVerbs() []Verb { {Name: "conditions", Description: "What is wrong with the mesh now: every open condition, urgent first, then oldest, " + "raised from the observers' findings (the watchman, the self-check, the healers, the assigner) and cleared when " + "the finding goes, never by hand. Given a key, that one whole with its evidence; with history, every transition " + - "lately, the controller's before the take-over included.", + "lately.", Input: schema(map[string]string{ "key": "a condition's key: that one whole; with history, only its transitions", "scope": "only this scope: machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module", @@ -46,17 +46,6 @@ func conditionsVerbs() []Verb { "why": "why, required and recorded", "cause": "the cause in a word (the condition's kind when absent)", }, []string{"key", "for", "why"})}, - {Name: "compare", Description: "The keeper's open conditions against the controller's: every key open in one and not " + - "the other, or open in both with another severity, each with its reason; the kinds that end with the controller " + - "and the keys held while their module is delivered listed apart and not counted. The take-over waits for it empty.", - Input: schema(map[string]string{}, nil)}, - {Name: "take-over", Description: "Ask the operator's Approve to switch who the router, the grants part and " + - "mesh-watcher hear from the controller to the keeper; refused while compare shows a difference. On the warrant " + - "the keeper writes its take-over key.", - Input: schema(map[string]string{"why": "why now, shown to the operator"}, []string{"why"})}, - {Name: "hand-back", Description: "Switch the readers back to the controller's conditions, until the controller's " + - "lease is handed away; refused after it.", - Input: schema(map[string]string{"why": "why, kept with the switch"}, []string{"why"})}, {Name: "resync", Description: "Read every observer's findings again and raise and clear what differs from the open " + "conditions: the recovery of a keeper that missed a change.", Input: schema(map[string]string{}, nil)}, @@ -78,8 +67,7 @@ func watchdogsVerbs() []Verb { Input: schema(map[string]string{"machine": "one machine"}, nil)}, {Name: "dead-letters", Description: "Every message a consumer on the bus gave up on, kept in DEAD_LETTERS: whose " + "consumer, the subject, how often it was handed over and when it was given up, newest first. With id: that one " + - "whole. With deliver or drop, a hand act with why, taken only once the controller's lease is handed away; " + - "before, refused naming the controller's verb.", + "whole. With deliver or drop, a hand act with why.", Input: schema(map[string]string{ "id": "a dead letter's id, as the list gives it: that one whole, with its message", "consumer": "a consumer's name, or .: only what that one gave up on", @@ -114,8 +102,7 @@ func selfCheckVerbs() []Verb { func healersVerbs() []Verb { return []Verb{ {Name: "healers", Description: "The healers: each registered response to one kind of condition, its repair, its " + - "budget and what happens when it is spent; every act taken lately with its outcome, the acts it would have " + - "taken while the controller's lease names a controller, and the mesh-wide brake.", + "budget and what happens when it is spent; every act taken lately with its outcome, and the mesh-wide brake.", Input: schema(map[string]string{"days": "how many days of acts back (default 7)"}, nil)}, {Name: "heal", Description: "Take one healer's act for one open condition now, within its budget and the brake.", Input: schema(map[string]string{ diff --git a/go/catalogue/placement_seat.go b/go/catalogue/placement_seat.go index 9bb9f51..4b7af9b 100644 --- a/go/catalogue/placement_seat.go +++ b/go/catalogue/placement_seat.go @@ -12,15 +12,13 @@ package catalogue // PlacementSeat is the seat the assigner holds for placement. const PlacementSeat = "mesh-placement" -// The seat's seven verbs (ADR 0567 §10). +// The seat's five verbs (ADR 0567 §10). const ( PlacementVerbPlacement = "placement" PlacementVerbProposals = "proposals" PlacementVerbPropose = "propose" PlacementVerbWithdraw = "withdraw" PlacementVerbAgain = "again" - PlacementVerbAdopt = "adopt" - PlacementVerbHandBack = "hand-back" ) // placementVerbs are the seat's verbs: what reads all it keeps and does by hand all it does by itself (ADR 0567 §10). @@ -74,13 +72,5 @@ func placementVerbs() []Verb { "stand, and raise its ask again when its verdict passes and the operator's channel does not hold the ask. " + "Nothing more: the recovery of a proposal whose judgement or ask was lost.", Input: schema(map[string]string{"id": "the proposal's id"}, []string{"id"})}, - {Name: PlacementVerbAdopt, Optional: true, Description: "Take placement over from the controller: its " + - "placement-export is proposed whole as the seed, every node's key, which every node must answer as the same. " + - "Its Approve is the take-over; at the warrant the export is read again and a changed one is proposed anew.", - Input: schema(map[string]string{"why": "why, shown to the operator"}, []string{"why"})}, - {Name: PlacementVerbHandBack, Optional: true, Description: "The operator's alone, recorded as a hand act: " + - "delete every placement key, the history kept, so every node returns to the controller's compositions. " + - "Refused after the lease write.", - Input: schema(map[string]string{"why": "why, recorded with the hand act"}, []string{"why"})}, } } diff --git a/go/catalogue/process_bus.go b/go/catalogue/process_bus.go index 347e244..4037284 100644 --- a/go/catalogue/process_bus.go +++ b/go/catalogue/process_bus.go @@ -33,15 +33,16 @@ type ProcessBus struct { Removes []string `json:"removes,omitempty"` } -// What a bus account may remove (novox/hq ADR 0564 Limits 4), each its own grant: the controller's consumer on -// DEAD_LETTER_NOTICES (the lease write's step, clause 5), and keys of the node-engine's acts bucket. A further consumer -// or bucket is a change of this set, through a decision. +// What a bus account may remove (novox/hq ADR 0564 Limits 4), each its own grant: keys of the node-engine's acts +// bucket, and the ended controller's leftovers on the bus (ADR 0590 clause 6): its consumers, its CONTROL and ASSIGNMENTS +// streams, its buckets and the declaration files' object buckets, each by its name. A further consumer, stream or bucket +// is a change of this set, through a decision. const ( - RemovesControllerDeadLetterConsumer = "DEAD_LETTER_NOTICES.controller" - RemovesEngineActs = "mesh-host_acts" - // RemovesAssignmentsStream is the stream ASSIGNMENTS, removed whole at the lease write (novox/hq research 072 row 33): - // its 350 messages have no consumer, and the placement is the provisioner's from then on. - RemovesAssignmentsStream = "ASSIGNMENTS" + RemovesEngineActs = "mesh-host_acts" + // RemovesControllerLeftovers is the closed set of the ended controller's leftovers (buscompose.ControllerLeftovers). + // The word does not name the controller's module, so the check that refuses any manifest naming it (ADR 0590 clause + // 5) holds over the bus module's manifest too. + RemovesControllerLeftovers = "controller-leftovers" ) // BusAccount is one account of a module's own beside its `broker`: a process's (ADR 0436) or a tools bundle's (ADR 0564 diff --git a/go/catalogue/replaces_test.go b/go/catalogue/replaces_test.go index 5815795..2f10676 100644 --- a/go/catalogue/replaces_test.go +++ b/go/catalogue/replaces_test.go @@ -13,7 +13,6 @@ func TestTheVerbsWorkedAroundSayWhatTheyReplace(t *testing.T) { ServiceManagerSeat + ".restart": "systemctl restart", ServiceManagerSeat + ".status": "systemctl status", "node-hostname.add": "edit /etc/hosts", - ControllerSeatName + ".node": "hostnamectl", } for _, s := range DefaultSeats() { for _, v := range s.Serves { diff --git a/go/catalogue/seats.go b/go/catalogue/seats.go index 727f4c2..84bc33c 100644 --- a/go/catalogue/seats.go +++ b/go/catalogue/seats.go @@ -100,37 +100,6 @@ type Receivable struct { // // In the order a person reads it: the mesh's own, then a node's. var defaultSeats = append([]Seat{ - // The control plane states what it did under the seat it holds (novox/hq ADR 0134): a role's - // events belong to the role, so they keep their address while the holder is replaced. No accepts, - // so no work queue is raised for it — only what its holder may say. - // And it serves the mesh's own verbs as the seat's tools (novox/hq ADR 0154): `status`, `push`, - // `assign` and the rest are a role's interface, not a container's, and stay addressable while - // the control plane is replaced. - {Name: ControllerSeatName, Scope: ScopeMesh, Decision: "novox/hq ADR 0079", - // And what is wrong, as it changes, and the self-check's heartbeat (novox/hq to-be 45 §2, §4). - Emits: []string{"applied", "refused", "built-before", - "condition-raised", "condition-changed", "condition-cleared", "doctor-heartbeat", - // A value given by hand, replaced after its module's first good start (novox/hq ADR 0228). - "secret-replaced", - // Every act a healer takes (novox/hq to-be 45 §7). - "healer-acted", - // A build put back after its gate failed (novox/hq ADR 0236, to-be 45 §8). - "rolled-back", - // A pull request's merge check, judged (novox/hq to-be 45 §9). - "checked", - // A walk kept in a new state, and a tier sent to a machine: kept until the catalogue's main consumes them no - // more; nothing emits them (ADR 0539). - "plan-moved", - "sent", - // A key of a record mirror written or removed (novox/hq ADR 0289, design 54 §3; design 57 F3). - "assignment-changed", "declared-config-changed", "predefined-changed", - // A builder's key not taken in because the artifact store does not hold an artifact it names (novox/hq - // issue 1937): the builder builds it again. - "artifacts-gone", - // A bucket of a state this release deleted, deleted at the controller's first start (novox/hq ADR 0539 - // clause 13): the merge window's, the silent sends' and the building plans' hold, each said once. - "bucket-deleted"}, - Serves: ControllerVerbs}, // The store's first verbs (novox/hq ADR 0159): the smallest set that makes the store askable, // served by whichever module holds the seat with tools of these names. {Name: "mesh-store", Scope: ScopeMesh, Delivers: "postgres-database", Decision: "novox/hq ADR 0079", @@ -294,21 +263,8 @@ var defaultSeats = append([]Seat{ Serves: []Verb{ {Name: "config", Optional: true, Description: "Set, clear, list or read the history of config values: " + "mesh config or node config for a predefined key, or a module's own value on one node, each set " + - "with why, validated, and naming each module on each node whose value moves. A trusted value only " + - "from the controller's terminal.", + "with why, validated, and naming each module on each node whose value moves.", Input: configInput()}, - // The switch of config to its own sources (novox/hq ADR 0565 §4): optional until every mesh-config serves it. - {Name: "compare", Optional: true, Description: "Every difference between config's own sources and the " + - "controller's records, each with its reason: a module's declared config against the controller's mirror, " + - "the indexer's predefined list against the mirror's, and each open pull request's config candidate against " + - "what the controller told. The take-over waits for none.", - Input: schema(map[string]string{}, nil)}, - {Name: "take-over", Optional: true, Description: "Ask the operator's Approve to switch config to its own " + - "sources (the indexer); refused while compare shows a difference.", - Input: schema(map[string]string{"why": "why, in a sentence"}, []string{"why"})}, - {Name: "hand-back", Optional: true, Description: "Switch config back to the controller's mirrors, until the " + - "controller hands its lease away; refused after it.", - Input: schema(map[string]string{"why": "why, in a sentence"}, []string{"why"})}, }}, // The provisioner's server (novox/hq ADR 0349 clause 3, design 57 P4): its holder keeps the registry of which // domain answers which kind of placeholder, and the buckets mesh-provisioner_needs, _applied and _collect, @@ -348,9 +304,6 @@ var defaultSeats = append([]Seat{ // policy and the bus step at the assigner, and the hand-act log. {Name: WantsSeat, Scope: ScopeMesh, Decision: controlRecordsDecision, Emits: []string{"policy-changed"}, Serves: wantsVerbs()}, {Name: HandActsSeat, Scope: ScopeMesh, Decision: controlRecordsDecision, Emits: []string{"act-recorded"}, Serves: handActsVerbs()}, - // The lease write (novox/hq ADR 0582 clause 1, grants_seat.go): the grants part hands the controller's lease to - // itself and back on the operator's warrant. - {Name: GrantsSeat, Scope: ScopeMesh, Decision: grantsDecision, Emits: grantsEvents(), Serves: grantsVerbs()}, // Joining a machine (novox/hq ADR 0587 §1, enrolment_seat.go): held once by the module mesh-enrolment. {Name: EnrolmentSeat, Scope: ScopeMesh, Decision: enrolmentDecision, Emits: EnrolmentEvents, Serves: enrolmentVerbs()}, // The artifact sweep (novox/hq ADR 0571 clause 2, artifact_sweep_seat.go). diff --git a/go/catalogue/verbs.go b/go/catalogue/verbs.go index 9c1bdac..0c3c9f0 100644 --- a/go/catalogue/verbs.go +++ b/go/catalogue/verbs.go @@ -76,521 +76,6 @@ func VerbNames(verbs []Verb) []string { return out } -// RemovedControllerVerbs are the controller seat's verbs release 6 removed (novox/hq ADR 0539, ADR 0429 §8): no -// delivery plan, batch, tier or push exists, so nothing starts, stops, retries, follows or reads one. Each stays in -// ControllerVerbs as a row with no input, so a caller is answered by name with its replacement, given here. -var RemovedControllerVerbs = map[string]string{ - "plans": "the delivery view (nox-delivery): each machine's want and have, read from the assigner's wants and the " + - "node-engine's haves", - "plan": "the node's compositions: mesh-controller_composed holds each module's digest and judgement per node, and " + - "mesh-hand-compose prints one module's composition for one node", - "push": "a want: the assigner writes want.. by its rules, and by hand the operator's credential writes " + - "it, naming a kept composition (ADR 0539 clause 10)", - "deliver": "nothing to start: a merged change reaches each machine by its want", - "delivery-stop": "a bad fingerprint is held and its canary set back by the assigner (ADR 0429 §6, ADR 0539 clause 18)", - "delivery-backups-again": "the assigner's backup ask, answered on the phone or in the delivery view (ADR 0539 clause 16)", - "delivery-walks": "the delivery view (nox-delivery) and the wants, haves and compositions on the bus", - "delivery-order": "no order to keep: each module reaches its machines by its own want, its providers first by rule 5", -} - -// removedInR6 is the row of a verb release 6 removed from the controller's seat: no input, refused by name. -func removedInR6(name string) Verb { - return Verb{Name: name, Description: "Removed in R6 (ADR 0539): refused by name, naming its replacement: " + - RemovedControllerVerbs[name], Input: schema(nil, nil)} -} - -// ControllerSeatName is the seat the control plane holds, whose tools are the mesh's own verbs. -const ControllerSeatName = "mesh-controller" - -// ControllerVerbs are the mesh's own verbs, as the `mesh-controller` seat's tools (novox/hq ADR 0154). -// -// **The same function the command line calls, and nothing the tool adds** (ADR 0035): each of these -// is a command the controller's binary already answers, run by the holder of the seat with the -// arguments below and answered with what the command printed. A verb here is a contract every future -// holder must implement, which is why the list is short and made of what an operator asks weekly. -// Additive within a version (design 33 §7); a verb that would break a caller takes a new version. -var ControllerVerbs = []Verb{ - {Name: "tools", Description: "Every seat's tools, from the mesh's own records: what each role " + - "answers, whether or not its holder is up. The mesh's own verbs are the mesh-controller seat's.", - Input: schema(nil, nil)}, - {Name: "calls", Description: "The calls this controller answered lately and what came of each — " + - "one still running, one that finished after its caller was told it was running, one whose answer " + - "the bus refused — and, given a call's id, its whole answer (novox/hq issue 265). A call that has " + - "not finished within ten seconds answers that it is running, with its id; this is where it ends.", - Input: schema(map[string]string{ - "call": "a call's id, as a running answer or `calls` gives it: that call and its whole answer", - }, nil)}, - {Name: "status", Description: "What is wrong, what is quiet, what is out of date, and which " + - "machines are behind what the mesh would send them.", - Input: schema(nil, nil)}, - {Name: "nodes", Description: "Every machine the mesh knows, with whether it is converged or adopted.", - Input: schema(nil, nil)}, - {Name: "node", Description: "What one machine reported it can do, what it is assigned, and why.", - Input: schema(map[string]string{"node": "the machine's name"}, []string{"node"}), - Replaces: []string{"hostnamectl", "uptime"}}, - {Name: "placement-export", Description: "Each machine's placement as the catalogue's placement file says it (novox/hq ADR 0555 " + - "§6, issue 1126): what a person decided of the machine (endpoint, site, hub, address on the private network, public " + - "domain, accounts, adoption, the tunnel found at adoption) and where its modules run (in order, with host ports, " + - "bindings, config placement layers, pins and holdings), read by the controller's own code from its own store, as JSON. " + - "Read-only; the one verb the controller gains for its exit (the operator, 2026-10-26 19:06:40), read once by the seed " + - "of the placement files.", - Input: schema(map[string]string{"node": "one machine's name; every machine when absent"}, nil)}, - {Name: "modules", Description: "Every module the mesh holds: version, the commit it was built from, " + - "and which machines run it.", - Input: schema(nil, nil)}, - {Name: "seats", Description: "Every seat the mesh defines, what it delivers, and who holds it.", - Input: schema(nil, nil)}, - {Name: "builds", Description: "What has been built lately and what came of it, for every module or for one; " + - "or, given a build's id, everything the build machine said while building it, line by line, from the bus.", - Input: schema(map[string]string{ - "module": "one module's name; every module when absent", - "log": "a build's id (as `builds` lists it): print what the build machine said, line by line", - "limit": "how many builds to list (default 20); not with log", - }, nil)}, - removedInR6("plans"), - // The verbs the delivery's owner asks with (novox/hq ADR 0239): the planner's answer, a group's order and - // composed check, and the start and end of a walk. Callable by a person too; mesh-delivery is who needs them. - {Name: "delivery-plan", Description: "Removed in R4e (ADR 0501 §5 / ADR 0503 §3): refused by name, naming its replacement", - Input: schema(nil, nil)}, - removedInR6("delivery-order"), - {Name: "delivery-check", Description: "Answers that the checker (mesh-delivery-checker) judges a group (novox/hq " + - "ADR 0503 §3); asks nothing. Its inputs stay, since hq's repository check still names it (ADR 0513 §4).", - Input: schema(map[string]string{"group": "the group's id (none for one head checked again)", - "members": "the members, as JSON, as delivery-order takes them", - "ready": "\"true\" when the pull request is marked ready: its check is queued before every other check (novox/hq ADR 0406 §3)"}, - []string{"members"})}, - removedInR6("deliver"), - removedInR6("delivery-stop"), - removedInR6("delivery-backups-again"), - removedInR6("delivery-walks"), - removedInR6("plan"), - {Name: "assign", Description: "Put a module on a machine. Refused with the mesh's own words when it cannot resolve there, " + - "or when a seat its resources are applied through is held by nothing on the machine (novox/hq ADR 0207). " + - "A module not registered yet is looked for (novox/hq issue 325, ADR 0261): while its build runs — a merge " + - "that adds a module asks for one — the assignment is kept as pending and the controller makes it when the " + - "build registers it; `status` lists it, and if the build fails it expires with why and raises a condition. " + - "A module known and not built is said, and build asks for its build; a name with no module and no build " + - "request kept is refused with the closest names.", - Input: schema(map[string]string{"node": "the machine's name", - "module": "the module's name; several comma-separated are judged together", - "build": "\"true\": for a module known and not built (its last build failed, or was never asked), ask " + - "for its build and keep this assignment pending until the build registers it"}, - []string{"node", "module"}, "build")}, - {Name: "unassign", Description: "Take a module off a machine. Refused when it holds a seat a module left there depends on.", - Input: schema(map[string]string{"node": "the machine's name", - "module": "the module's name; several comma-separated are judged together"}, []string{"node", "module"})}, - {Name: "pin", Description: "Tell a machine which provider answers a provision for it — the module, and the node " + - "it runs on, both. Asked for when more than one could answer; the refusal lists them.", - Input: schema(map[string]string{ - "node": "the machine's name", - "provision": "the provision, as the consumer requires it", - "from": "the node the chosen provider runs on", - "module": "the module providing it there", - }, []string{"node", "provision", "from", "module"})}, - {Name: "unpin", Description: "Take that choice back, putting the question to the mesh again.", - Input: schema(map[string]string{"node": "the machine's name", "provision": "the provision"}, []string{"node", "provision"})}, - removedInR6("push"), - {Name: "rotate", Description: "Replace a credential. A pair credential, by provision (and a consuming machine and module, " + - "else every holder): both ends are re-sent together. Or a module's own secret, by machine, module and " + - "name: made anew and the machine sent, so the module starts again on it — for a secret its definition " + - "says it reads at start, whether the mesh made the value or a person gave it (novox/hq ADR 0228); one " + - "an outside party issued, or one the module applies to a backend, is refused with the reason.", - Input: schema(map[string]string{ - "provision": "a pair credential: the provision whose credential to replace", - "consumer": "with provision: only the holder on this machine (optional)", - "node": "an own secret: the machine", - "module": "an own secret: the module; with provision: only this consuming module's credential (optional)", - "secret": "an own secret: its name in the module's definition", - "why": "an own secret: why it is rotated — recorded in the hand-act log (optional)", - "cause": "with why: the cause in a word, the word a second rotation for the same reason uses (optional)", - }, nil)}, - {Name: "give", Description: "Take a module's own secret from the operator at their desk (novox/hq ADR 0259 " + - "§10): the same as secret-ask with the desk named. A prompt that does not show what is typed opens on " + - "the machine named by at, its answer comes back sealed to this call alone, and is sealed to the " + - "module's machine as `secret accept` seals it. The value is never an argument and never in the answer: " + - "the answer says it was taken, or why not. Recorded in the hand-act log as a value given at the desk. " + - "The prompt waits 25 seconds; dismissed or unanswered, nothing changes. Then push the machine.", - Input: schema(map[string]string{ - "node": "the machine the module runs on, which the secret is sealed to", - "module": "the module's name", - "secret": "the own secret's name in the module's definition", - "at": "the machine the operator sits at, where the prompt opens", - }, []string{"node", "module", "secret", "at"})}, - {Name: "issued", Description: "Take the credential a provider's software issued for a provision — a media " + - "server's sign-in token — and keep it as that provision's one credential for every consumer (novox/hq ADR " + - "0305): the controller asks the provider's own tool on its machine, in the controller's own name, for the " + - "value sealed to a key made for this call alone, seals it to every holder and forgets it, and sends their " + - "machines when it changed. Carries no value and answers none; a value every holder already holds is not " + - "sealed again, so asking again restarts nothing. Refused for a provision no offer says its software issues.", - Input: schema(map[string]string{ - "provision": "the provision whose provider's software issues its credential, e.g. plex-api", - }, []string{"provision"})}, - {Name: "secret-move", Description: "Move every secret the controller holds into the vault, as it is (novox/hq " + - "ADR 0349 clause 7, design 57's V3): each machine's node-engine opens what was sealed to it and hands it to " + - "the vault, which takes it from that machine alone, never over a value it holds, and reads it back; the " + - "record of asks at a desk is copied once. No value is rotated, in the arguments or in the answer, and " + - "nothing points at the vault because of it. Answers, per machine, what moved, what was there already and " + - "what did not move and why; a machine not answering is moved when it is next heard from. Run again, it " + - "moves what is left.", - Input: schema(map[string]string{ - "node": "only this machine's secrets (optional; every machine's without it)", - }, nil)}, - {Name: "secret-ask", Description: "Ask the operator for a module's own secret (novox/hq ADR 0277): a prompt " + - "that shows nothing of what is typed opens at the desk — the module's machine, or the one at names — " + - "naming the module, the secret and who asked; the operator types the value there, never on a channel " + - "and never in a verb's argument; the answer comes back sealed to this call alone and is sealed to the " + - "module's machine as `secret accept` seals it. The answer says the value was taken, or why not. " + - "Refused for a secret the mesh issues itself (the bus account, one the mesh may make) and for a module " + - "that runs as an account of its own, whose value is typed at the controller's terminal. Bounded: one " + - "open prompt per secret, three asks an hour. Recorded in the hand-act log, with who asked, and " + - "announced on every channel. Then push the machine.", - Input: schema(map[string]string{ - "node": "the machine the module runs on, which the secret is sealed to", - "module": "the module's name", - "secret": "the own secret's name in the module's definition", - "at": "the machine the operator sits at, where the prompt opens; the module's machine when absent", - }, []string{"node", "module", "secret"})}, - {Name: "issue", Description: "Give a module on a machine its account on the bus: minted, and sealed to the " + - "machine as the module's own secret named broker, read at the next push of that machine. For a module " + - "whose definition declares that secret; refused with the reason otherwise. Issued again, it replaces the account.", - Input: schema(map[string]string{ - "node": "the machine that runs the module", - "module": "the module's name", - }, []string{"node", "module"})}, - {Name: "token", Description: "Refused through a verb since novox/hq ADR 0266: the one-time token a machine " + - "joins with is answered to its caller, and whoever may call a verb includes agents. Issue it at the " + - "controller's terminal: `mesh-controller token issue --new --overlay-key `.", - Input: schema(map[string]string{ - "node": "a machine the mesh already has a record for", - "new": "or the name of a machine to create the record for", - "overlay_key": "the public half of the machine's tunnel key", - "for": "how long it may be used, as a duration (default 1h)", - "adopted": "\"true\" when the machine is in use and joins adopted", - }, nil, "adopted"), - Replaces: []string{"mesh-controller token issue", "wg set"}}, - {Name: "settings", Description: "Read or set what an assignment is configured with: a module's settings for the whole " + - "mesh, or for one machine. Without values or clear, answers the layer as it stands — read it before setting it — " + - "then every value the module gives a default or a layer sets, with where it comes from: the module's default, " + - "the mesh, or the machine (novox/hq ADR 0262); with list \"preferences\", or with no module, every " + - "module's preferences and each machine's value; " + - "with history, the layers it replaced. Setting replaces that layer whole and answers each key it adds (+), " + - "changes (~) and removes (-); a set that would remove a key is refused unless replace says it is meant " + - "(novox/hq ADR 0217). Takes effect at the next push. With clear, removes the layer and the module is back to " + - "what its definition says; a cleared or replaced layer is kept in the history. A trusted setting — " + - "places, accesses, what a provider serves, what a trusted file asks for, the whole layer of a module " + - "with a trusted mergeable file — is refused through this verb (novox/hq issue 339, 340) and PROPOSED " + - "instead (novox/hq ADR 0277): with propose, the values (or clear) are put to the operator on a channel " + - "that proves who answers, with every key and its exact new value, and the layer is set only on their " + - "Approve; the answer names the proposal, its fingerprint and when it expires, and nothing changes until " + - "then. With proposals, every proposal and where each stands; with proposal, one whole. Once move-config has " + - "run, a module in the config form forwards to mesh-config (issue 664): show lists what config lists, set and " + - "clear set and clear each value as the module's own on the machine (none for the whole mesh), placement " + - "staying beside the assignment, and propose proposes each value through mesh-config.", - Input: schema(map[string]string{ - "module": "the module's name; with list, only that module's", - "values": "the settings as a JSON object, for set or propose", - "node": "one machine; the whole mesh when absent", - "clear": "\"true\" to remove the layer instead of setting it, or to propose its removal; not with values", - "replace": "\"true\": with values, the set (or the proposal) is meant to remove the keys the layer had and it does not name", - "history": "\"true\": without values or clear, the layers this one replaced, the latest first", - "list": "\"preferences\": every module's preferences — key, default and why — and the value on each " + - "machine it is assigned to with where it comes from; module and node narrow it (novox/hq ADR 0262)", - "propose": "\"true\": propose the values (or clear) to the operator instead of setting them (novox/hq ADR 0277)", - "proposals": "\"true\": every settings proposal, newest first, and where each stands", - "proposal": "a proposal's id: that proposal whole, its values and the layer it was shown against", - }, nil, "clear", "replace", "history", "propose", "proposals")}, - {Name: "command", Description: "Run one reading command line of the controller's own, as you would type it at " + - "its shell — `node show ace`, `module list`, `plans`, `conditions show ` — and answer what it " + - "printed. The generic verb beside the named ones (novox/hq ADR 0154), and since ADR 0266 it only reads: " + - "status, version, help, seats, healers, hand-acts, durations, collection, images, artifacts, data, builds, " + - "plan, plans (not stop/close/go), doctor (not run), conditions list/show/history, node list/show, " + - "module list, settings show/preferences, retire list, cleanup list, delivery plan/walks, bus, mirrors (not " + - "--record/--confirm). What writes has its named verb; what sets a key, issues a credential or a token, or " + - "accepts, recovers or exports a secret is the controller's terminal's alone.", - Input: schema(map[string]string{ - "command": "the command line, as the controller's binary takes it; quotes group a word with spaces", - }, []string{"command"})}, - // The build queue went with build-agent (novox/hq ADR 0525 §9): asks are the build seats' streams, their backlog the - // streams' own state, shown in the delivery view. The verbs are refused by name for one release, naming that. - {Name: "queue", Description: "Removed in R4g (ADR 0525 §9): refused by name, naming its replacement: the build seats' backlog in the delivery view", - Input: schema(nil, nil)}, - {Name: "cancel", Description: "Removed in R4g (ADR 0525 §9): refused by name, naming its replacement", Input: schema(nil, nil)}, - {Name: "clear", Description: "Removed in R4g (ADR 0525 §9): refused by name, naming its replacement", Input: schema(nil, nil)}, - {Name: "replay", Description: "Removed in R4g (ADR 0525 §9): refused by name, naming its replacement", Input: schema(nil, nil)}, - // novox/hq issue 686: a builder ran a branch's store tests for the first time in its pull request's merge check. - {Name: "try", Description: "Try a pushed branch or commit before its pull request (novox/hq issue 686): the " + - "checker (mesh-delivery-checker) merges the head onto the repository's trunk and runs its levels on the merge, " + - "as it judges a pull request (ADR 0503 §3), its levels asks on the build seats (ADR 0525 §1); no merge-check.sh is run. Answers at " + - "once with an id; asked again with that id it waits up to 20 s and answers pass, fail or error, how long it " + - "took and the lines naming what failed — or that it is still waiting or running, to ask again. Sets no " + - "status on the forge, registers and builds nothing for delivery. One try of a commit at a time — a second " + - "ask answers the first's id. Only for a repository of the mesh; the ref must be on the forge.", - Input: schema(map[string]string{ - "repository": "the repository on the forge, owner/name; with ref", - "ref": "the branch or commit to try, as pushed to the forge", - "base": "the branch it is merged into (the repository's default branch when none is named)", - "tests": "only the tests this `go test -run` pattern names", - "store-only": "\"true\": only the integration level", - "id": "instead of a repository: the id a try answered, to read what came of it", - }, nil, "store-only"), - Replaces: []string{"sh merge-check.sh", "docker run postgres"}}, - {Name: "kill", Description: "Removed in R4g (ADR 0525 §9): refused by name, naming its replacement: a build seat's ask ends where it runs", Input: schema(nil, nil)}, - {Name: "pause", Description: "Removed in R4g (ADR 0525 §9): refused by name, naming its replacement: /node-build-.pause", Input: schema(nil, nil)}, - {Name: "resume", Description: "Removed in R4g (ADR 0525 §9): refused by name, naming its replacement: /node-build-.resume", Input: schema(nil, nil)}, - // Acts done by hand, and what the bounds are set from (novox/hq to-be 45 §7, Phase 0). - {Name: "hand-act", Description: "Record an act done by hand outside the mesh — a container restarted, a file " + - "edited, a service started on a machine — with why and its cause, in the hand-act log beside the pushes and " + - "plans ended by hand (novox/hq to-be 45 §7). A cause recorded twice in a fortnight is a healer wanted.", - Input: schema(map[string]string{ - "what": "what was done, in a line", - "why": "why it had to be done by hand", - "cause": "the cause in a word, or a condition's kind — the word a second act for the same reason uses", - "condition": "the key of the condition it addressed, if any (optional)", - }, []string{"what", "why", "cause"})}, - {Name: "drill", Description: "Record a drill — something broken on purpose to see the mesh raise and clear it: " + - "a module stopped, a process killed — with what it tests, in the hand-act log. A drill is a person's " + - "deliberate test, never a repair: no healer is wanted for it however often it is run (S15).", - Input: schema(map[string]string{ - "what": "what was done on purpose, in a line", - "why": "what the drill tests", - "condition": "the key of the condition the drill is meant to raise, if any (optional)", - }, []string{"what", "why"})}, - {Name: "warranted", Description: "Record in the hand-act log what a module did on the operator's warrant (novox/hq " + - "ADR 0274, ADR 0259): who chose, through which channel, with which proofs and which answer are read from the " + - "router's own record of that module's ask, never from the caller; recorded once per ask however often it is " + - "asked; the caller names the ask and says nothing else. Refused for an ask still open, ended without a choice, " + - "or not that module's.", - Input: schema(map[string]string{ - "asker": "the module that asked, e.g. claude-code", - "ask": "its ask's id", - }, []string{"asker", "ask"})}, - {Name: "hand-acts", Description: "What was done by hand lately — pushes, plans ended, consumers re-made, acts " + - "recorded — who, why and the cause of each, and which causes repeat: each repeat is a healer the mesh lacks.", - Input: schema(map[string]string{"days": "how many days back (default 14)"}, nil)}, - {Name: "durations", Description: "How long things take, as the controller measured them: a send to its machine's " + - "report (apply), a plan's tier, a build, and each phase of an " + - "ended walk per class (walk-phase, novox/hq ADR 0282) — per machine, repository, module or class/phase, with " + - "median, p90 and max. What the core's bounds are set from (novox/hq to-be 45 Phase 0).", - Input: schema(map[string]string{ - "kind": "one kind: apply, plan-tier, build or walk-phase; every kind when absent", - "days": "how many days back (default 14)", - }, nil)}, - // What is wrong, and the self-check (novox/hq to-be 45 §2, §4). - {Name: "conditions", Description: "What is wrong with the mesh now: every open condition, urgent first, " + - "then oldest — raised by the watchdogs of the signals table, the self-check's probes and the providers' " + - "own words, and cleared when observation says it is resolved, never by hand. Given a key, that one " + - "whole with its evidence; with history, every transition lately; with silence, stop one's messages " + - "for a while — a hand act, which says why (novox/hq to-be 45 §2).", - Input: schema(map[string]string{ - "key": "a condition's key: that one whole; with history, only its transitions", - "scope": "only this scope: machine, plan, call, build, merge, provider, seat, bus, core, probe or mesh", - "severity": "only urgent, or only warning", - "machine": "only those about this machine", - "history": "\"true\": every raising, change, silence and clearing lately, oldest first", - "days": "with history: how many days back (default 7, at most 90)", - "silence": "a condition's key: send no message for it for a while; it stays open and in status", - "for": "with silence: how long — 30m, 4h, 2d; at most 7d", - "why": "with silence: why — required, and recorded in the hand-act log", - "cause": "with silence: the cause in a word (the condition's kind when absent)", - }, nil, "history")}, - // What the healers did (novox/hq to-be 45 §7). - {Name: "healers", Description: "The healers (novox/hq to-be 45 §7): each registered response to one kind " + - "of condition — its repair (the ordinary path again), its budget, what happens when it is spent — every act " + - "they took lately with its outcome, and the mesh-wide brake. A heal is never a hand act; whether it " + - "repaired anything is the condition's clearing to say. Each act is also in its condition's tried.", - Input: schema(map[string]string{"days": "how many days of acts back (default 7)"}, nil)}, - {Name: "doctor", Description: "The self-check (novox/hq to-be 45 §4): the last run's verdict at once — " + - "each probe of the design's live invariants passed, failed or could not run, and how long ago. With " + - "run, a run now; with probes, the registry; with signals, every row of the signals table and the age " + - "of its newest signal. Runs every five minutes on its own; each failure is an open condition.", - Input: schema(map[string]string{ - "run": "\"true\": run every probe now and answer the verdict", - "probes": "\"true\": the registry — what each probe asserts, and the condition it raises", - "signals": "\"true\": the signals table, each row with the age of its newest signal", - "probe": "one probe's id (D14): its verdict alone, with every finding — the verdict shows at most twenty a probe", - }, nil, "run", "probes", "signals")}, - // How a module's new builds reach its machines, and the bus's planned step (novox/hq ADR 0236). - {Name: "upgrade", Description: "Whether each module's new builds wait for a person (novox/hq ADR 0236, ADR 0539 clause 14): " + - "for a module of policy record, the assigner writes no want naming a composition judged to recreate a container or move " + - "data until the operator answers its ask; with where the policy comes from (a person, the module, the bus, its " + - "irreplaceable data, the default) and why. With module, that one; with policy, a person's choice for it — roll-out, " + - "record (with why) or default to take the choice back. The bus is never moved by the assigner (ADR 0539 clause 15).", - Input: schema(map[string]string{ - "module": "one module", - "policy": "with module: roll-out, record or default", - "why": "with policy: why — required for record, kept", - }, nil)}, - {Name: "bus", Description: "The bus as a person's step (novox/hq to-be 45 §8, ADR 0236, ADR 0539 clause 15): what a bus " + - "upgrade would do — the bus's build on each machine against the one the mesh holds — and how the last step went. " + - "With upgrade, check one: a person's act with why, after the streams are snapshotted (snapshot-taken says where, " + - "while the mesh takes none itself), saying first whether it can be reverted; then it answers the want the operator " + - "writes with the operator's credential, want.., recorded — nothing is sent, and the assigner never " + - "moves the bus.", - Input: schema(map[string]string{ - "upgrade": "\"true\": start the bus's upgrade", - "why": "with upgrade: why — required, recorded in the hand-act log", - "cause": "with upgrade: the cause in a word (default bus-upgrade)", - "reversible": "\"true\": with upgrade, the new version can be undone by putting the old one back", - "irreversible": "\"true\": with upgrade, it cannot — your explicit word that it runs anyway", - "snapshot-taken": "with upgrade: where the streams' snapshot you took is", - }, nil, "upgrade", "reversible", "irreversible")}, - // A consumer the mesh stopped asking for: retired, waiting for a person, deleted only by one - // (novox/hq ADR 0230). - {Name: "retire", Description: "A consumer the mesh stops asking for is retired by its provider — access " + - "disabled, data kept — after the same answer in five passes; more than three at once, or more than half " + - "of those held, waits for a person. With no answer, every provider that waits and what it would retire. " + - "With answer approve or reject, a node and a module: retire exactly what that provider waits with, or " + - "keep it active — a hand act, which says why (novox/hq ADR 0230).", - Input: schema(map[string]string{ - "answer": "approve or reject: answer what the provider waits with (needs node, module and why)", - "node": "with answer: the machine the provider runs on", - "module": "with answer: the provider module", - "why": "with answer: why — required, and recorded in the hand-act log", - "cause": "with answer: the cause in a word (retire-waiting when absent)", - }, nil)}, - {Name: "cleanup", Description: "Every consumer a provider holds retired — its age, its size where the " + - "backend knows, and why it was retired — and every module's own data retired on its machine (ADR 0233). " + - "With consumer (and node, module): the provider deletes that one retired consumer — never an active one; " + - "for a module's own retired item, consumer names the item and the machine's backup holder takes a last " + - "restore point of it, then deletes it. With older-than: every retired consumer older than that many " + - "days, listed; deleted only with confirm. Deleting is a hand act, which says why (novox/hq ADR 0230).", - Input: schema(map[string]string{ - "node": "with consumer: the machine the provider runs on", - "module": "with consumer: the provider module", - "consumer": "delete this retired consumer (needs node, module and why)", - "older-than": "delete every consumer retired more than this many days (needs why; lists only without confirm)", - "confirm": "\"true\": with older-than, delete what is listed", - "why": "with consumer or older-than: why — required, and recorded in the hand-act log", - "cause": "with consumer or older-than: the cause in a word (cleanup-waiting when absent)", - }, nil, "confirm")}, - {Name: "judge-directories", Description: "Read-only: which of the directories one machine is declared its " + - "node-engine's walk that follows no link another account could make would refuse (novox/hq issue 358), one line " + - "each: placed, would be refused and why (the link and who could replace it), refused already by the placement " + - "guard, or below a home. The engine judges the declaration it keeps and makes, chmods and chowns nothing.", - Input: schema(map[string]string{ - "node": "the machine's name", - }, []string{"node"})}, - // What a consumer gave up on (novox/hq issue 330): kept in DEAD_LETTERS until a person acts on it. - {Name: "root-free", Description: "Whether each machine named is root-free now (novox/hq ADR 0259 §8): no agent " + - "there can become root without a person. Judged when asked, never from a condition: free only when the machine " + - "names an account its agents run as, its node-engine judged that account unable to become root within the " + - "last 15 minutes, and the login shell's execute is not served there. Anything else, a read that failed " + - "included, is not free and says why. The router asks it before an answer from a channel proving its sender " + - "may approve. Only reads.", - Input: listed(schema(map[string]string{ - "machines": "the machines to judge, by name: a list, or one text separated by commas", - }, []string{"machines"}), "machines")}, - {Name: "dead-letters", Description: "Every message a consumer on the bus gave up on after handing it over " + - "as often as it may, kept in DEAD_LETTERS: whose consumer, the subject, how often it was handed over and " + - "when it was given up, newest first. With id: that one whole, with what it said. With deliver: hand it " + - "again to the consumer that gave it up, and nobody else; with drop: let it go for good. Delivering and " + - "dropping are hand acts, which say why (novox/hq issue 330).", - Input: schema(map[string]string{ - "id": "a dead letter's id, as the list gives it: that one whole, with its message", - "consumer": "a consumer's name, or .: only what that one gave up on", - "limit": "how many to list, newest first (default 50); only when listing", - "deliver": "a dead letter's id: deliver it again to the consumer that gave it up (needs why)", - "drop": "a dead letter's id: drop it for good (needs why)", - "why": "with deliver or drop: why — required, and recorded in the hand-act log", - "cause": "with deliver or drop: the cause in a word (dead-letter when absent)", - }, nil)}, - // The data every machine declares (novox/hq ADR 0233). - {Name: "data", Description: "Every item of data every machine declares, as the self-check last measured it: " + - "its class (irreplaceable, rebuildable, cache), where it is, its size, its newest write, its newest good " + - "backup and the bound on it — and what is retired: kept after its module left the machine, removed only by " + - "`cleanup delete` (novox/hq ADR 0233).", - Input: schema(map[string]string{ - "machine": "one machine (optional)", - "retired": "\"true\": only what is retired", - }, nil, "retired")}, - {Name: "build", Description: "Removed in R4e (ADR 0501 §5 / ADR 0503 §3): refused by name, naming its replacement", - Input: schema(nil, nil)}, - // What the records say the registries may keep, asked on demand (novox/hq ADR 0251, to-be 51). - {Name: "artifacts", Description: "Every artifact the mesh recorded making that lives in its artifact store, " + - "each with its repository, digest, kind (image or archive) and state: kept — and why: a definition names " + - "it, or one of the five most recent builds of a module the mesh holds — or eligible: made, kept for no " + - "reason, not yet let go. Collected ones are counted, and listed only with collected. Reads only " + - "(novox/hq ADR 0251).", - Input: schema(map[string]string{ - "repository": "only this repository's references, as / (the counts stay whole)", - "collected": "\"true\": list the collected references too (they only grow; counted always)", - }, nil, "collected")}, - // Whether the artifact store holds what a builder's done key names, before the builder reuses it (novox/hq issue - // 1937): the store's collector deletes a blob nothing holds, and a key the controller never took in holds nothing. - {Name: "held", Description: "Whether the artifact store holds each reference given, asked with one HEAD each: " + - "held, gone (the store answered 404) or unknown with why. The builder asks it before it reuses a done key, and " + - "builds a key whose artifact is gone again. Reads only: nothing is held or written (novox/hq issue 1937).", - Input: listed(schema(map[string]string{ - "references": "the artifacts' references, artifact-store:////blobs/sha256:: a list, " + - "or one text separated by commas", - }, []string{"references"}), "references")}, - {Name: "collect", Description: "The artifact store's sweep, now: what the mesh made and keeps for no reason " + - "is let go of. Without confirm a dry run — every kept archive asked about with HEADs, what would be let go " + - "listed, nothing held or deleted. With confirm and why: every kept archive held first, then each eligible " + - "artifact let go of, oldest first, and recorded collected — a hand act, recorded with its why. A confirmed " + - "collect lets an index go with the platform manifests no kept index names (read first; any read that " + - "fails lets nothing go); the sweep after a build leaves an eligible index for such a collect (novox/hq ADR " + - "0257). Bounded by " + - "most (500 by default, at most 5000) and 45 seconds; what is left is said. Bytes are reclaimed by the " + - "store's nightly collector. Never touches a digest the mesh did not record (novox/hq ADR 0189, ADR 0251).", - Input: schema(map[string]string{ - "confirm": "\"true\": let go of what is eligible (needs why); without it nothing is done", - "why": "with confirm: why — required, and recorded in the hand-act log", - "most": "let go of at most this many (500 by default, at most 5000)", - }, nil, "confirm")}, - {Name: "move-placement", Description: "The move of placement out of the settings layers, run once after the " + - "delivery that holds it (novox/hq ADR 0348 clause 11, issue 736): every placement key a settings layer still " + - "holds — endpoints, expose, reach and ports, which a layer set before then keeps among its settings — is put " + - "beside the assignment, where places, accesses and networks already are. Without confirm it says which layers " + - "hold which keys and moves nothing. With confirm and why: in one transaction, each such layer is kept whole " + - "in its history first (why: moved to placement), its keys are laid over the placement beside it, and the " + - "layer keeps its other keys, when it was set and who set it — and the act is recorded in the hand-act log. " + - "Answers each layer moved: its machine (empty for the whole mesh), its module and its keys. Nothing a " + - "machine is told changes: composition reads the keys as placement wherever they are. Asked again, it " + - "moves nothing.", - Input: schema(map[string]string{ - "confirm": "\"true\": move them (needs why); without it nothing is moved", - "why": "with confirm: why — required, and recorded in the hand-act log", - }, nil, "confirm")}, - {Name: "move-config", Description: "The move of config: every config value a settings layer holds is set in " + - "mesh-config, run once after the delivery that holds it and after move-placement (novox/hq ADR 0348 §9, " + - "issue 664). A machine's layer becomes the module's own value on that machine; a whole-mesh layer becomes " + - "mesh config for the predefined key its value falls back to where that module alone falls back to it, and " + - "otherwise the module's own value on each machine it is assigned to, laid under that machine's layer; a key " + - "a layer says by its file's name is set under its config key. Without confirm it says each value it would " + - "set, where from, each layer that stays (its module is in the settings form) and anything that refuses the " + - "move, and sets nothing. With confirm and why: each value is set through mesh-config in the controller's " + - "name, every module's resolved values are read back and compared with what its layers lay today, and only " + - "then, in one transaction, is each layer moved kept whole in its history first (why: moved to mesh-config) " + - "and emptied, and the controller switched to read mesh-config where it read the layers — the act recorded " + - "in the hand-act log. Nothing a machine is told changes. Asked again, it sets nothing set already.", - Input: schema(map[string]string{ - "confirm": "\"true\": set the values and switch (needs why); without it nothing is set", - "why": "with confirm: why — required, and recorded in the hand-act log and with each value", - }, nil, "confirm")}, - {Name: "mirrors", Description: "The bases the mesh copied into its artifact store for builds to stand on, as " + - "the records hold them: each with its state — kept because a kept build stood on it, or eligible — and " + - "which build or person recorded it. With record: references of copies no record names (a copy made " + - "before copies were recorded, or by a build that failed before then) to record as the mesh's, so the " + - "sweep keeps or lets go of them like any other; only a repository the mirror writes is taken " + - "(upstream//, or a module's /on-), and each must be in the store. Without " + - "confirm a dry run; with confirm and why, recorded — a hand act. A recorded copy is eligible: the next " + - "sweep lets it go unless a kept build stood on it. Records only: this verb deletes nothing " + - "(novox/hq ADR 0257).", - Input: schema(map[string]string{ - "record": "references to record, artifact-store://@sha256:, separated by spaces or commas", - "confirm": "\"true\": record them (needs why); without it nothing is recorded", - "why": "with confirm: why — required, and recorded with each copy and in the hand-act log", - }, nil, "confirm")}, - {Name: "images", Description: "Every container image one machine's declaration names — the declaration the " + - "mesh would send it now and the one it was last sent — each with the resources naming it: what the " + - "machine keeps when it prunes its images. sent_known is false when the last one sent is not kept with " + - "its images. Reads only (novox/hq ADR 0251).", - Input: schema(map[string]string{ - "node": "the machine's name", - }, []string{"node"})}, -} - // schema is a JSON schema for an object of string properties, which is every argument the verbs // above take. Kept small on purpose: a schema an agent cannot read is a tool it cannot call. The // switches are the properties that are "true" or "false" and nothing else, said in the schema as an diff --git a/go/catalogue/verbs_r4g_test.go b/go/catalogue/verbs_r4g_test.go deleted file mode 100644 index 41e1876..0000000 --- a/go/catalogue/verbs_r4g_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package catalogue - -import ( - "strings" - "testing" -) - -// 1887/A9b: the build queue's verbs are refused by name, naming what replaced them, and the verbs R4e removed are gone. -func TestTheBuildQueueVerbsAreRefusedByName(t *testing.T) { - rows := map[string]Verb{} - for _, v := range ControllerVerbs { - rows[v.Name] = v - } - for _, gone := range []string{"rebuild", "forget-checks"} { - if _, ok := rows[gone]; ok { - t.Errorf("the controller still lists %s", gone) - } - } - for _, refused := range []string{"queue", "cancel", "clear", "kill", "pause", "resume", "replay"} { - v, ok := rows[refused] - if !ok || !strings.HasPrefix(v.Description, "Removed in R4g (ADR 0525 §9): refused by name") { - t.Errorf("%s = %q, want refused by name", refused, v.Description) - } - } - if !strings.Contains(rows["queue"].Description, "delivery view") || !strings.Contains(rows["pause"].Description, "node-build-") { - t.Errorf("queue and pause do not name their replacements: %q, %q", rows["queue"].Description, rows["pause"].Description) - } - if strings.Contains(rows["try"].Description, "build-agent") { - t.Errorf("try still names build-agent: %q", rows["try"].Description) - } -} diff --git a/go/catalogue/verbs_r6_test.go b/go/catalogue/verbs_r6_test.go index 72d8f6f..546d8fe 100644 --- a/go/catalogue/verbs_r6_test.go +++ b/go/catalogue/verbs_r6_test.go @@ -5,58 +5,6 @@ import ( "testing" ) -// 1943/AC4: the controller's seat no longer offers a verb that starts, stops, retries, follows or reads a delivery -// plan, nor a push: each is a row with no input whose description refuses it by name and names its replacement, and -// each is in the removed list the holder answers from. upgrade keeps its policy and drops the plan's backlog. -func TestTheControllerSeatHasNoPlanVerbs(t *testing.T) { - rows := map[string]Verb{} - for _, v := range ControllerVerbs { - rows[v.Name] = v - } - gone := []string{"plans", "plan", "push", "deliver", "delivery-stop", "delivery-backups-again", "delivery-walks", "delivery-order"} - for _, name := range gone { - v, ok := rows[name] - if !ok { - t.Errorf("%s is not in the table, so a caller is not answered by name", name) - continue - } - replacement, removed := RemovedControllerVerbs[name] - if !removed || replacement == "" { - t.Errorf("%s is not in the removed list with its replacement", name) - } - if !strings.HasPrefix(v.Description, "Removed in R6 (ADR 0539): refused by name") || !strings.Contains(v.Description, replacement) { - t.Errorf("%s = %q, want refused by name, naming %q", name, v.Description, replacement) - } - if props, _ := v.Input["properties"].(map[string]any); len(props) > 0 { - t.Errorf("%s still takes %v", name, props) - } - } - props, _ := rows["upgrade"].Input["properties"].(map[string]any) - for _, arg := range []string{"together", "backlog", "release-backlog", "cause"} { - if _, has := props[arg]; has { - t.Errorf("upgrade still takes %s, a plan's", arg) - } - } - for _, arg := range []string{"module", "policy", "why"} { - if _, has := props[arg]; !has { - t.Errorf("upgrade no longer takes %s", arg) - } - } - for _, word := range []string{"rolled", "gate", "backlog", "push"} { - if strings.Contains(rows["upgrade"].Description, word) { - t.Errorf("upgrade's description still speaks of %q: %q", word, rows["upgrade"].Description) - } - } - if !strings.Contains(rows["bus"].Description, "want") { - t.Errorf("bus does not say the operator writes the bus's want: %q", rows["bus"].Description) - } - for name := range RemovedControllerVerbs { - if v := rows[name]; !strings.HasPrefix(v.Description, "Removed in R6") { - t.Errorf("%s is in the removed list and its row says %q", name, v.Description) - } - } -} - // 1943/AC4: the delivery seat keeps the merge's verbs and no longer offers times, what-if, release, stop or // retire-history: each is a row with no input, optional so the holder of before and the holder of release 6 both hold // the seat, refusing it by name; mark-ready, served by nobody since issue 1022, is gone. @@ -101,15 +49,3 @@ func TestTheDeliverySeatHasNoPlanVerbs(t *testing.T) { t.Errorf("the gate of release 5 cannot hold the seat: %v", err) } } - -// 1943 (for 1945/AC14): the controller's seat says a bucket it deleted at its first start as its event, bucket-deleted -// (ADR 0539 clause 13). -func TestTheControllerSaysABucketItDeleted(t *testing.T) { - seat, ok := SeatNamed(ControllerSeatName) - if !ok { - t.Fatal("no controller seat") - } - if !SeatSays(seat.Emits, "bucket-deleted") { - t.Errorf("the controller's seat does not emit bucket-deleted: %v", seat.Emits) - } -} diff --git a/go/catalogue/verbs_test.go b/go/catalogue/verbs_test.go index 9a2b973..da85cec 100644 --- a/go/catalogue/verbs_test.go +++ b/go/catalogue/verbs_test.go @@ -68,21 +68,3 @@ func TestHoldingAMeshSeatRequiresServingItsVerbs(t *testing.T) { t.Fatalf("a holder serving every verb was refused: %v", err) } } - -// The mesh's own verbs are declared in full: an agent cannot call a name without a schema. -func TestEveryControllerVerbIsDescribedWithASchema(t *testing.T) { - seen := map[string]bool{} - for _, v := range ControllerVerbs { - if v.Description == "" || v.Input == nil || v.Input["type"] != "object" { - t.Errorf("%s: no description or no object schema", v.Name) - } - if seen[v.Name] { - t.Errorf("%s declared twice", v.Name) - } - seen[v.Name] = true - } - seat, _ := SeatNamed(ControllerSeatName) - if len(seat.Serves) != len(ControllerVerbs) { - t.Fatalf("the compiled mesh-controller seat serves %d verbs, the table has %d", len(seat.Serves), len(ControllerVerbs)) - } -} diff --git a/go/cli/cli.go b/go/cli/cli.go deleted file mode 100644 index 3a6bd10..0000000 --- a/go/cli/cli.go +++ /dev/null @@ -1,201 +0,0 @@ -// Package cli is the local protocol between `mesh-cli` and the node-engine on its machine (novox/hq ADR 0272). -// -// `mesh-cli ` asks the controller to run one of its command lines. It holds no credential and -// reads no file of the mesh: it connects to the node-engine's socket, writes one Request, and reads one Answer. -// The node-engine reads the connecting process's account from the kernel and asks the controller over its own -// link; that request is the node-engine's and the controller's, and lives in neither this package nor any SDK -// (ADR 0039 as ADR 0188 reads it: the transport lives in the runtime). -// -// One request and one answer per connection, each one line of JSON. Standard library only. -package cli - -import ( - "bufio" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net" - "time" -) - -// Socket is where the node-engine listens for mesh-cli: a file under its runtime directory, with no network -// address (ADR 0272 §3). -const Socket = "/run/nox-mesh-host/cli.sock" - -// MaxStdin bounds the standard input mesh-cli carries for a line: a secret, a key, a settings file — kilobytes. More -// is refused by mesh-cli before anything is asked, and by the node-engine and the controller again. -const MaxStdin = 64 << 10 - -// MaxRequest bounds what the node-engine reads of a request: a command line of a few hundred bytes, settings given -// inline of a few kilobytes, and standard input of at most MaxStdin, which JSON writes as base64 (4 bytes for every 3). -const MaxRequest = 64<<10 + MaxStdin*4/3 + 4 - -// MaxAnswer bounds what mesh-cli reads of an answer. The controller cuts what it runs to fit one bus message, and -// says so; this is the local bound above that. -const MaxAnswer = 16 << 20 - -// AskWithin is how long mesh-cli waits for its answer: the controller's own bound on a call (five minutes), -// and a minute for the link on either side. -const AskWithin = 6 * time.Minute - -// Request is what mesh-cli asks: the controller's command line, as words, and what mesh-cli's own standard input -// held when it was not a terminal. -// -// **Stdin is for the controller's terminal alone** (novox/hq ADR 0259 §10, ADR 0272): a secret given with -// `secret accept … --from -`, so it never stands on a command line. The controller gives it to a line that runs as -// its terminal and refuses any other line that carries it; nobody keeps it — not the node-engine, not the -// controller's record of calls, not its journal. -type Request struct { - Line []string `json:"line"` - Stdin []byte `json:"stdin,omitempty"` -} - -// Answer is what comes back: what the command printed, how it exited, and whether it ran as the controller's -// terminal and why. Refused is set when nothing ran at all, and says why; Exit is then 1. -type Answer struct { - Stdout []byte `json:"stdout,omitempty"` - Stderr []byte `json:"stderr,omitempty"` - Exit int `json:"exit"` - Terminal bool `json:"terminal"` - // Why says whether the line ran as the terminal, and why or why not, in the controller's words. - Why string `json:"why,omitempty"` - // Refused says why nothing ran: the account, the node, the controller unreachable. - Refused string `json:"refused,omitempty"` - // Cut says the output was longer than one bus message and was cut there. - Cut bool `json:"cut,omitempty"` - // Unknown says the line was asked and its outcome is not known: no answer came in time, and it may have - // run. Never said as a refusal, which means nothing ran. - Unknown string `json:"unknown,omitempty"` -} - -// Refusal is an answer saying that nothing ran, and why. -func Refusal(why string) Answer { return Answer{Exit: 1, Refused: why} } - -// NotKnown is an answer saying the line was asked and what came of it is not known: it may have run. call is -// the controller's call the line runs as, when it is known, which `calls` answers. -func NotKnown(call, why string) Answer { - where := "the controller's `calls` lists what came of each line" - if call != "" { - where = "see the call " + call + " (`calls " + call + "`)" - } - return Answer{Exit: 1, Unknown: why + "; the line may have run — " + where} -} - -// ErrTooLong is a request or an answer over its bound. -var ErrTooLong = errors.New("longer than the protocol allows") - -// WriteRequest writes one request, as one line. -func WriteRequest(w io.Writer, r Request) error { return writeLine(w, r) } - -// ReadRequest reads one request, refusing one over MaxRequest or one that names no command. -func ReadRequest(r io.Reader) (Request, error) { - var req Request - if err := readLine(r, MaxRequest, &req); err != nil { - return Request{}, err - } - if len(req.Line) == 0 { - return Request{}, errors.New("the request names no command") - } - if len(req.Stdin) > MaxStdin { - return Request{}, fmt.Errorf("%w: standard input of more than %d bytes", ErrTooLong, MaxStdin) - } - return req, nil -} - -// WriteAnswer writes one answer, as one line. -func WriteAnswer(w io.Writer, a Answer) error { return writeLine(w, a) } - -// ReadAnswer reads one answer, refusing one over MaxAnswer. -func ReadAnswer(r io.Reader) (Answer, error) { - var a Answer - err := readLine(r, MaxAnswer, &a) - return a, err -} - -// Ask is mesh-cli's side: connect to the socket, ask, and wait for the answer within AskWithin or ctx. -func Ask(ctx context.Context, socket string, line []string) (Answer, error) { - return AskWith(ctx, socket, Request{Line: line}) -} - -// AskWith asks a whole request: a line, and the standard input it carries, at most MaxStdin. -func AskWith(ctx context.Context, socket string, req Request) (Answer, error) { - line := req.Line - if len(line) == 0 { - return Answer{}, errors.New("no command given") - } - if len(req.Stdin) > MaxStdin { - return Answer{}, fmt.Errorf("standard input is more than %d bytes, which mesh-cli does not carry; nothing was asked", MaxStdin) - } - ctx, cancel := context.WithTimeout(ctx, AskWithin) - defer cancel() - var d net.Dialer - conn, err := d.DialContext(ctx, "unix", socket) - if err != nil { - return Answer{}, fmt.Errorf("the node-engine on this machine does not answer at %s: %w", socket, err) - } - defer conn.Close() - if deadline, ok := ctx.Deadline(); ok { - _ = conn.SetDeadline(deadline) - } - if err := WriteRequest(conn, req); err != nil { - // The engine may have answered before it read the request — a refusal (busy, an unknown account) written - // and the connection closed — and the write then fails on the close. That answer is already waiting, and - // it says why where the write error says only that the pipe broke (novox/hq issue 444, found under issue - // 434). So read it, within the deadline the connection already has, and only when none came say the write. - // Only a refusal is taken: the request was never wholly written, so nothing can have run, and any other - // answer here is not one a correct engine gives — passing it on as success would hide the failure. - a, rerr := ReadAnswer(conn) - if rerr != nil { - return Answer{}, fmt.Errorf("asking the node-engine: %w; and no answer came (%v)", err, rerr) - } - if a.Refused == "" { - return Answer{}, fmt.Errorf("asking the node-engine: %w; and the answer that came is not a refusal, "+ - "though the request was never wholly written", err) - } - return a, nil - } - a, err := ReadAnswer(conn) - if err != nil { - return Answer{}, fmt.Errorf("reading the node-engine's answer: %w", err) - } - return a, nil -} - -func writeLine(w io.Writer, v any) error { - body, err := json.Marshal(v) - if err != nil { - return err - } - _, err = w.Write(append(body, '\n')) - return err -} - -// readLine reads one line of JSON of at most limit bytes. A line over the limit is refused rather than read -// whole: the reader is the side that pays for it. -func readLine(r io.Reader, limit int, into any) error { - br := bufio.NewReaderSize(io.LimitReader(r, int64(limit)+1), 64<<10) - var body []byte - for { - chunk, err := br.ReadSlice('\n') - body = append(body, chunk...) - if len(body) > limit { - return fmt.Errorf("%w: more than %d bytes", ErrTooLong, limit) - } - if err == nil { - break - } - if errors.Is(err, bufio.ErrBufferFull) { - continue - } - if errors.Is(err, io.EOF) && len(body) > 0 { - break - } - return err - } - if err := json.Unmarshal(body, into); err != nil { - return fmt.Errorf("not one: %w", err) - } - return nil -} diff --git a/go/cli/cli_test.go b/go/cli/cli_test.go deleted file mode 100644 index 92c5050..0000000 --- a/go/cli/cli_test.go +++ /dev/null @@ -1,141 +0,0 @@ -package cli - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "net" - "path/filepath" - "strings" - "testing" -) - -// A request and an answer survive a round trip, bytes and all. -func TestARequestAndAnAnswerSurviveTheirRoundTrip(t *testing.T) { - var b bytes.Buffer - line := []string{"settings", "set", "zsh", `{"execute":"withhold"}`, "--node", "a"} - if err := WriteRequest(&b, Request{Line: line}); err != nil { - t.Fatal(err) - } - got, err := ReadRequest(&b) - if err != nil { - t.Fatal(err) - } - if strings.Join(got.Line, "\x00") != strings.Join(line, "\x00") { - t.Fatalf("the line came back as %q", got.Line) - } - - b.Reset() - sent := Answer{Stdout: []byte("ok\n\xff"), Stderr: []byte("said"), Exit: 3, Terminal: true, Why: "the terminal", Cut: true, - Unknown: "no answer; the line may have run — see the call call-1"} - if err := WriteAnswer(&b, sent); err != nil { - t.Fatal(err) - } - a, err := ReadAnswer(&b) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(a.Stdout, sent.Stdout) || string(a.Stderr) != "said" || a.Exit != 3 || !a.Terminal || a.Why != sent.Why || !a.Cut || - a.Unknown != sent.Unknown { - t.Fatalf("the answer came back as %+v", a) - } -} - -// A request over the bound is refused, not read whole; one naming no command is refused. -func TestARequestOverTheBoundOrNamingNothingIsRefused(t *testing.T) { - long := Request{Line: []string{strings.Repeat("x", MaxRequest)}} - big := Request{Line: []string{"secret"}, Stdin: bytes.Repeat([]byte("s"), MaxStdin+1)} - var bb bytes.Buffer - if err := WriteRequest(&bb, big); err != nil { - t.Fatal(err) - } - if _, err := ReadRequest(&bb); !errors.Is(err, ErrTooLong) { - t.Fatalf("standard input over its bound was read: %v", err) - } - if _, err := AskWith(context.Background(), "/nonexistent.sock", big); err == nil || !strings.Contains(err.Error(), "nothing was asked") { - t.Fatalf("mesh-cli would ask with standard input over its bound: %v", err) - } - var b bytes.Buffer - if err := WriteRequest(&b, long); err != nil { - t.Fatal(err) - } - if _, err := ReadRequest(&b); !errors.Is(err, ErrTooLong) { - t.Fatalf("a request over the bound was read: %v", err) - } - if _, err := ReadRequest(strings.NewReader(`{"line":[]}` + "\n")); err == nil { - t.Fatal("a request naming no command was taken") - } - if _, err := ReadRequest(strings.NewReader("not json\n")); err == nil { - t.Fatal("a request that is not one was taken") - } -} - -// Ask speaks the protocol over a socket, as mesh-cli does to the node-engine. -func TestAskSpeaksTheProtocolOverASocket(t *testing.T) { - socket := filepath.Join(t.TempDir(), "cli.sock") - l, err := net.Listen("unix", socket) - if err != nil { - t.Skipf("no unix socket here: %v", err) - } - defer l.Close() - go func() { - conn, err := l.Accept() - if err != nil { - return - } - defer conn.Close() - req, err := ReadRequest(conn) - if err != nil { - _ = WriteAnswer(conn, Refusal(err.Error())) - return - } - _ = WriteAnswer(conn, Answer{Stdout: []byte(strings.Join(req.Line, " "))}) - }() - a, err := Ask(context.Background(), socket, []string{"status"}) - if err != nil { - t.Fatal(err) - } - if string(a.Stdout) != "status" || a.Exit != 0 || a.Refused != "" { - t.Fatalf("asked status and heard %+v", a) - } - if _, err := Ask(context.Background(), filepath.Join(t.TempDir(), "none.sock"), []string{"status"}); err == nil || - !strings.Contains(err.Error(), "does not answer") { - t.Fatalf("a missing socket was not said: %v", err) - } -} - -// An outcome not known is never a refusal: it says the line may have run, and exits non-zero. -func TestAnUnknownOutcomeIsNotARefusal(t *testing.T) { - a := NotKnown("call-7", "no answer within 6m0s") - if a.Refused != "" || a.Exit == 0 || !strings.Contains(a.Unknown, "may have run") || !strings.Contains(a.Unknown, "call-7") { - t.Fatalf("an unknown outcome reads as %+v", a) - } - if b := NotKnown("", "no answer"); strings.Contains(b.Unknown, "call ") { - t.Fatalf("an unknown outcome with no call names one: %+v", b) - } -} - -// Standard input up to its bound travels whole, beside the line: a secret given with --from -. -func TestStandardInputTravelsWithItsLine(t *testing.T) { - given := append(bytes.Repeat([]byte{0, 0xff, '\n', 'x'}, MaxStdin/4-1), "end"...) - var b bytes.Buffer - if err := WriteRequest(&b, Request{Line: []string{"secret", "accept", "anchor", "telegram", "telegram-token", "--from", "-"}, - Stdin: given}); err != nil { - t.Fatal(err) - } - if b.Len() > MaxRequest { - t.Fatalf("a request with standard input at its bound is %d bytes, over MaxRequest %d", b.Len(), MaxRequest) - } - got, err := ReadRequest(&b) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(got.Stdin, given) || got.Line[1] != "accept" { - t.Fatalf("standard input did not travel whole: %d bytes, want %d", len(got.Stdin), len(given)) - } - none, _ := json.Marshal(Request{Line: []string{"status"}}) - if strings.Contains(string(none), "stdin") { - t.Fatalf("a request without standard input says it has one: %s", none) - } -} diff --git a/go/cli/refusal_test.go b/go/cli/refusal_test.go deleted file mode 100644 index d05ec87..0000000 --- a/go/cli/refusal_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package cli - -import ( - "context" - "errors" - "net" - "os" - "path/filepath" - "strings" - "testing" - "time" -) - -// anEngineThatClosesFirst listens like the node-engine before mesh-host#80: it writes its answer, if any, and -// closes without reading the request (novox/hq issue 434). It answers one connection. -func anEngineThatClosesFirst(t *testing.T, answer *Answer) string { - t.Helper() - socket := filepath.Join(t.TempDir(), "cli.sock") - l, err := net.Listen("unix", socket) - if err != nil { - t.Skipf("no unix socket here: %v", err) - } - t.Cleanup(func() { l.Close() }) - go func() { - conn, err := l.Accept() - if err != nil { - return - } - if answer != nil { - _ = WriteAnswer(conn, *answer) - } - conn.Close() - }() - return socket -} - -// aLineTheSocketCannotHold is a request larger than a unix socket buffers, so its write cannot finish before -// the engine reads it — and an engine that closes first makes it fail every time, not only when it loses a race. -func aLineTheSocketCannotHold() []string { return []string{"status", strings.Repeat("x", 4<<20)} } - -// An engine that refuses and closes before reading the request is heard: its refusal reaches the caller, not -// the broken pipe its close left the client's write (novox/hq issue 444). -func TestARefusalWrittenBeforeTheRequestIsReadReachesTheCaller(t *testing.T) { - busy := Refusal("the node-engine is busy: too many lines at once") - socket := anEngineThatClosesFirst(t, &busy) - a, err := Ask(context.Background(), socket, aLineTheSocketCannotHold()) - if err != nil { - t.Fatalf("the engine said %q and the caller heard: %v", busy.Refused, err) - } - if a.Refused != busy.Refused || a.Exit != 1 { - t.Fatalf("the engine said %q and the caller heard %+v", busy.Refused, a) - } -} - -// When the write fails and no answer is waiting, the error says both: what failed, and that no answer came. -func TestAFailedWriteWithNoAnswerSaysBoth(t *testing.T) { - socket := anEngineThatClosesFirst(t, nil) - start := time.Now() - _, err := Ask(context.Background(), socket, aLineTheSocketCannotHold()) - if err == nil { - t.Fatal("a request the engine never read, and never answered, came back without an error") - } - if !strings.Contains(err.Error(), "asking the node-engine") || !strings.Contains(err.Error(), "no answer came") { - t.Fatalf("the error does not say what failed and that no answer came: %v", err) - } - if waited := time.Since(start); waited > 30*time.Second { - t.Fatalf("waited %s on a connection the engine had closed", waited) - } -} - -// After a failed write only a refusal is taken: nothing can have run, so an answer saying it ran and exited 0 -// is an error, never a success (novox/hq issue 444). -func TestAnAnswerOtherThanARefusalAfterAFailedWriteIsAnError(t *testing.T) { - ran := Answer{Exit: 0, Stdout: []byte("ok")} - socket := anEngineThatClosesFirst(t, &ran) - a, err := Ask(context.Background(), socket, aLineTheSocketCannotHold()) - if err == nil { - t.Fatalf("a request never wholly written came back as success: %+v", a) - } - if !strings.Contains(err.Error(), "not a refusal") { - t.Fatalf("the error does not say the answer was not a refusal: %v", err) - } -} - -// Reading after a failed write never waits longer than the ask already allowed: a peer that never reads and -// never answers is given up on at the context's deadline, not later (novox/hq issue 444). -func TestAFailedWriteWaitsNoLongerThanTheDeadline(t *testing.T) { - socket := filepath.Join(t.TempDir(), "cli.sock") - l, err := net.Listen("unix", socket) - if err != nil { - t.Skipf("no unix socket here: %v", err) - } - t.Cleanup(func() { l.Close() }) - held := make(chan net.Conn, 1) - go func() { - conn, err := l.Accept() - if err != nil { - return - } - held <- conn // neither read nor answered, nor closed, until the test ends - }() - t.Cleanup(func() { - select { - case c := <-held: - c.Close() - default: - } - }) - ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) - defer cancel() - start := time.Now() - _, err = Ask(ctx, socket, aLineTheSocketCannotHold()) - waited := time.Since(start) - if err == nil { - t.Fatal("a peer that never read nor answered gave no error") - } - if !errors.Is(err, os.ErrDeadlineExceeded) { - t.Fatalf("the error is not the deadline: %v", err) - } - if waited > time.Second { - t.Fatalf("waited %s with a deadline of 50ms", waited) - } -} diff --git a/go/cmd/mesh-hand-compose/main.go b/go/cmd/mesh-hand-compose/main.go deleted file mode 100644 index 624b0b2..0000000 --- a/go/cmd/mesh-hand-compose/main.go +++ /dev/null @@ -1,210 +0,0 @@ -// mesh-hand-compose composes one module for one node by hand, from the inputs the controller keeps, and signs it with the -// operator's key, for the hand path of novox/hq ADR 0539 clause 10: a fingerprint the controller never composed (a -// hand build), while the controller cannot compose its own fix. It composes with mesh-sdk's composition, the one the -// controller writes with (compose.Compositions), and prints the composition, its key and its digest; only with -write -// does it write them, with the operator's credential: the composition into the node's bucket composed-, each -// file over 64 KiB into the node's object bucket, and the digest into mesh-controller_composed. The want is the -// operator's next act, naming that digest; each act is recorded in the hand-act log. -// -// mesh-hand-compose -input -node -module -fp -key -// [-write -nats -creds ] -// -// The inputs are a compose.Input as JSON. The key is the operator's ed25519 private key, PKCS#8 in PEM; the -// node-engine trusts it beside the controller's (ADR 0539 clause 3). -package main - -import ( - "context" - "crypto/ed25519" - "crypto/x509" - "encoding/json" - "encoding/pem" - "errors" - "flag" - "fmt" - "io" - "os" - "time" - - "github.com/nats-io/nats.go" - "github.com/nats-io/nats.go/jetstream" - - "git.novox.be/novox/mesh-sdk/go/compose" - "git.novox.be/novox/mesh-sdk/go/declaration" - "git.novox.be/novox/mesh-sdk/go/delivery" - "git.novox.be/novox/mesh-sdk/go/kvlimit" -) - -func main() { - if err := run(os.Args[1:], os.Stdout); err != nil { - fmt.Fprintln(os.Stderr, "mesh-hand-compose:", err) - os.Exit(1) - } -} - -// printed is what mesh-hand-compose prints: where the composition goes, its digest, the composition, and whether it was -// written. -type printed struct { - Bucket string `json:"bucket"` - Key string `json:"key"` - Digest string `json:"digest"` - Composition json.RawMessage `json:"composition"` - Objects []string `json:"objects,omitempty"` - Written bool `json:"written"` - Next string `json:"next"` -} - -func run(args []string, stdout io.Writer) error { - fs := flag.NewFlagSet("mesh-hand-compose", flag.ContinueOnError) - input := fs.String("input", "", "the controller's kept inputs, a compose.Input as JSON") - node := fs.String("node", "", "the node to compose for") - module := fs.String("module", "", "the module to compose") - fp := fs.String("fp", "", "the module's fingerprint, src2: and 64 hex digits") - keyFile := fs.String("key", "", "the operator's ed25519 private key, PKCS#8 in PEM") - write := fs.Bool("write", false, "write the composition, its files and its digest, with the operator's credential") - url := fs.String("nats", nats.DefaultURL, "with -write: the bus") - creds := fs.String("creds", "", "with -write: the operator's credential file") - if err := fs.Parse(args); err != nil { - return err - } - switch { - case *input == "" || *node == "" || *module == "" || *fp == "": - return errors.New("-input, -node, -module and -fp are each required") - case *keyFile == "": - return errors.New("-key is required: a composition made by hand is signed by the operator's key, and the node-engine applies no other") - case *write && *creds == "": - return errors.New("-write needs -creds: the operator's credential is the only hand writer of a composition") - } - key, err := readKey(*keyFile) - if err != nil { - return err - } - key2, err := delivery.ComposedKey(*node, *module, *fp) - if err != nil { - return err - } - bucket, err := delivery.ComposedBucketFor(*node) - if err != nil { - return err - } - raw, err := os.ReadFile(*input) - if err != nil { - return err - } - var in compose.Input - if err := json.Unmarshal(raw, &in); err != nil { - return fmt.Errorf("the inputs in %s are not a compose.Input: %w", *input, err) - } - all, _, err := compose.Compositions(in, *node, map[string]string{*module: *fp}, compose.Reading) - if err != nil { - return err - } - c, ok := all[key2] - if !ok { - return fmt.Errorf("%s does not run %s: its composition holds none of it", *node, *module) - } - c, objects, err := c.ByDigest() - if err != nil { - return err - } - if c, err = delivery.SignComposed(c, key); err != nil { - return err - } - encoded, err := delivery.EncodeComposed(c) - if err != nil { - return err - } - digest, err := delivery.ComposedDigestOf(c) - if err != nil { - return err - } - out := printed{Bucket: bucket, Key: key2, Digest: digest, Composition: encoded, - Next: fmt.Sprintf("write want.%s.%s naming fp %s and composed %s with the operator's credential, and record each act in the hand-act log", *node, *module, *fp, digest)} - for d := range objects { - out.Objects = append(out.Objects, d) - } - if *write { - if err := writeIt(*url, *creds, *node, bucket, key2, encoded, digest, objects); err != nil { - return err - } - out.Written = true - } - enc := json.NewEncoder(stdout) - enc.SetIndent("", " ") - return enc.Encode(out) -} - -func readKey(path string) (ed25519.PrivateKey, error) { - raw, err := os.ReadFile(path) - if err != nil { - return nil, err - } - block, _ := pem.Decode(raw) - if block == nil { - return nil, fmt.Errorf("%s holds no PEM block", path) - } - parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes) - if err != nil { - return nil, fmt.Errorf("%s: %w", path, err) - } - key, ok := parsed.(ed25519.PrivateKey) - if !ok { - return nil, fmt.Errorf("%s is not an ed25519 key", path) - } - return key, nil -} - -// writeIt writes the files by digest, then the composition, then its digest: a reader that finds the digest finds the -// composition and its files already there. -func writeIt(url, creds, node, bucket, key string, encoded []byte, digest string, objects map[string][]byte) error { - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - nc, err := nats.Connect(url, nats.UserCredentials(creds), nats.Name("mesh-hand-compose")) - if err != nil { - return err - } - defer nc.Close() - js, err := jetstream.New(nc) - if err != nil { - return err - } - if len(objects) > 0 { - filesBucket, err := declaration.FileBucketFor(node) - if err != nil { - return err - } - store, err := js.ObjectStore(ctx, filesBucket) - if err != nil { - return fmt.Errorf("the node's object bucket %s: %w", filesBucket, err) - } - for d, body := range objects { - name, err := declaration.ObjectName(d) - if err != nil { - return err - } - if _, err := store.PutBytes(ctx, name, body); err != nil { - return fmt.Errorf("the file %s: %w", d, err) - } - } - } - composed, err := js.KeyValue(ctx, bucket) - if err != nil { - return fmt.Errorf("the bucket %s: %w", bucket, err) - } - if _, err := kvlimit.Put(ctx, composed, key, encoded); err != nil { - return fmt.Errorf("%s in %s: %w", key, bucket, err) - } - value, err := delivery.EncodeComposedDigest(delivery.ComposedDigest{Digest: digest, Judgement: delivery.JudgedCannot, - Words: "composed by hand with mesh-hand-compose (ADR 0539 clause 10): the operator writes the want"}) - if err != nil { - return err - } - digests, err := js.KeyValue(ctx, delivery.ControllerComposedBucket) - if err != nil { - return fmt.Errorf("the bucket %s: %w", delivery.ControllerComposedBucket, err) - } - if _, err := kvlimit.Put(ctx, digests, key, value); err != nil { - return fmt.Errorf("%s in %s: %w", key, delivery.ControllerComposedBucket, err) - } - return nil -} diff --git a/go/cmd/mesh-hand-compose/main_test.go b/go/cmd/mesh-hand-compose/main_test.go deleted file mode 100644 index 96ffd48..0000000 --- a/go/cmd/mesh-hand-compose/main_test.go +++ /dev/null @@ -1,145 +0,0 @@ -package main - -import ( - "bytes" - "crypto/ed25519" - "crypto/rand" - "crypto/x509" - "encoding/base64" - "encoding/json" - "encoding/pem" - "os" - "path/filepath" - "sort" - "strings" - "testing" - - "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/compose" - "git.novox.be/novox/mesh-sdk/go/delivery" - "git.novox.be/novox/mesh-sdk/go/overlay" -) - -// aMesh is the Input of mesh-controller's aMesh, as mesh-sdk's compose tests make it (compose/fixture_test.go): two -// machines on the private network, the module the mesh provides for itself assigned to both. The manifest is the one -// the controller provides (overlay.Manifest), read back as the catalogue reads it. -func aMesh(t *testing.T) compose.Input { - t.Helper() - var m catalogue.Manifest - b, _ := json.Marshal(overlay.Manifest()) - if err := json.Unmarshal(b, &m); err != nil { - t.Fatal(err) - } - in := compose.Input{Catalogue: map[string]catalogue.Manifest{overlay.Name: m}} - for i, name := range []string{"anchor", "laptop"} { - pub, _, _ := ed25519.GenerateKey(rand.Reader) - in.Nodes = append(in.Nodes, compose.Node{Name: name, - Place: compose.Place{Key: base64.StdEncoding.EncodeToString(pub), Endpoint: name + ".example:51820", Site: "here", Hub: name == "anchor", Address: []string{"10.77.0.1", "10.77.0.2"}[i]}, - Assigned: []string{overlay.Name}, Capabilities: map[string]bool{"container-runtime": true, "wireguard": true, "systemd": true}}) - } - return in -} - -func writeKey(t *testing.T, dir string) ed25519.PublicKey { - t.Helper() - pub, priv, _ := ed25519.GenerateKey(rand.Reader) - der, err := x509.MarshalPKCS8PrivateKey(priv) - if err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(dir, "operator.key"), pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}), 0o600); err != nil { - t.Fatal(err) - } - return pub -} - -// 1943/AC5: mesh-hand-compose, given the kept inputs, a node, a module and its fingerprint, prints that module's -// composition for that node signed by the operator's key and its digest: its resources are the controller's for that -// module on that node (compose.Compose), and it verifies against the operator's key. Without --write it writes nothing. -func TestMeshComposeGivesTheControllersResourcesSignedByTheOperator(t *testing.T) { - dir := t.TempDir() - in := aMesh(t) - raw, err := json.Marshal(in) - if err != nil { - t.Fatal(err) - } - input := filepath.Join(dir, "input.json") - if err := os.WriteFile(input, raw, 0o600); err != nil { - t.Fatal(err) - } - operator := writeKey(t, dir) - fp := delivery.SourcePrefix2 + strings.Repeat("a", 64) - - var out bytes.Buffer - if err := run([]string{"-input", input, "-node", "laptop", "-module", overlay.Name, "-fp", fp, - "-key", filepath.Join(dir, "operator.key")}, &out); err != nil { - t.Fatal(err) - } - var printed struct { - Key string `json:"key"` - Bucket string `json:"bucket"` - Digest string `json:"digest"` - Composition json.RawMessage `json:"composition"` - Written bool `json:"written"` - } - if err := json.Unmarshal(out.Bytes(), &printed); err != nil { - t.Fatalf("%s: %v", out.String(), err) - } - c, err := delivery.DecodeComposed(printed.Composition) - if err != nil { - t.Fatal(err) - } - if digest, err := delivery.VerifyComposed(c, []ed25519.PublicKey{operator}); err != nil || digest != printed.Digest { - t.Errorf("the composition verifies %q, %v against the operator's key; printed %s", digest, err, printed.Digest) - } - wantKey, _ := delivery.ComposedKey("laptop", overlay.Name, fp) - if printed.Key != wantKey || printed.Bucket != "composed-laptop" || printed.Written { - t.Errorf("printed key %s in %s, written %v", printed.Key, printed.Bucket, printed.Written) - } - - d, _, err := compose.Compose(in, "laptop") - if err != nil { - t.Fatal(err) - } - var want, got []string - for _, r := range d.Resources { - want = append(want, r["id"].(string)) - } - for _, r := range c.Resources { - var head struct{ ID string } - _ = json.Unmarshal(r, &head) - got = append(got, head.ID) - } - sort.Strings(want) - sort.Strings(got) - if len(got) == 0 || strings.Join(got, " ") != strings.Join(want, " ") { - t.Errorf("mesh-hand-compose gives %v, the controller's composition %v", got, want) - } -} - -// 1943/AC5: mesh-hand-compose refuses what it cannot compose, naming why: no key, a fingerprint not of the new form, a -// module the node does not run, a node the mesh does not have. -func TestMeshComposeRefuses(t *testing.T) { - dir := t.TempDir() - raw, _ := json.Marshal(aMesh(t)) - input := filepath.Join(dir, "input.json") - _ = os.WriteFile(input, raw, 0o600) - writeKey(t, dir) - key := filepath.Join(dir, "operator.key") - fp := delivery.SourcePrefix2 + strings.Repeat("a", 64) - for _, c := range []struct { - args []string - why string - }{ - {[]string{"-input", input, "-node", "laptop", "-module", overlay.Name, "-fp", fp}, "-key"}, - {[]string{"-input", input, "-node", "laptop", "-module", overlay.Name, "-fp", "src2:1", "-key", key}, "fingerprint"}, - {[]string{"-input", input, "-node", "laptop", "-module", "mesh-mail", "-fp", fp, "-key", key}, "does not run mesh-mail"}, - {[]string{"-input", input, "-node", "nowhere", "-module", overlay.Name, "-fp", fp, "-key", key}, "nowhere"}, - {[]string{"-input", input, "-node", "laptop", "-module", overlay.Name, "-fp", fp, "-key", key, "-write"}, "-creds"}, - } { - var out bytes.Buffer - if err := run(c.args, &out); err == nil || !strings.Contains(err.Error(), c.why) { - t.Errorf("mesh-hand-compose %v = %v, want a refusal saying %q", c.args, err, c.why) - } - } -} diff --git a/go/compose/composed.go b/go/compose/composed.go index fd15ffe..07258d3 100644 --- a/go/compose/composed.go +++ b/go/compose/composed.go @@ -18,7 +18,7 @@ import ( // the controller composes it, split into each module's composition at the fingerprint fps gives it and the node's own // (`.node`), by key (delivery.ComposedKey). Each resource says its module, whatever the node reported it reads. // A module fps gives no fingerprint for is not composed and is named in skipped, so a caller composing one module -// (mesh-hand-compose) gets that one; but for a module the node runs inside its own composition (Node.InOwn, and the +// gets that one; but for a module the node runs inside its own composition (Node.InOwn, and the // private network's always), which fps names at no fingerprint of the new form: its resources go into the node's own, // their module taken off and their ids still naming it, and its filled paths with them, as the controller's composed // writer folds them (issue 1126). Each module's Data says whether its manifest declares data. The compositions are not diff --git a/go/delivery/assigner.go b/go/delivery/assigner.go index 5587977..1c9cdf0 100644 --- a/go/delivery/assigner.go +++ b/go/delivery/assigner.go @@ -140,7 +140,7 @@ const ( RuleStep = 4 // at most step other nodes are ahead (step 0: no limit, ADR 0528 §3) RuleProviders = 5 // every provider the module requires runs healthy on that node, or at its canary RuleGrant = 6 // the module's credential for F exists (grants..) - RuleComposed = 7 // the module's composition for that node at F exists (mesh-controller_composed, ADR 0539 §6) + RuleComposed = 7 // the module's composition for that node at F exists (ADR 0539 §6) // RuleProvisions: every provision the module requires on that node is provided (novox/hq ADR 0570 §4): a want's // words name the key and its state that hold it, as "the database of keycloak on novox is asked of postgres". RuleProvisions = 8 diff --git a/go/delivery/assigner_test.go b/go/delivery/assigner_test.go index 77d7d2a..e11920d 100644 --- a/go/delivery/assigner_test.go +++ b/go/delivery/assigner_test.go @@ -268,58 +268,39 @@ func TestCompositionVerdictRefused(t *testing.T) { // 1394/B1b: a grant key round-trips; a want or held key is refused, naming it; a grant round-trips and one naming an // empty account is refused. -func TestControllerGrant(t *testing.T) { +func TestTheGrantsIssued(t *testing.T) { fp := fp2of('c') - key, err := ControllerGrantKey("mesh-delivery-assigner", fp) + key, err := PartGrantKey("mesh-delivery-assigner", fp) if err != nil || key != "mesh-delivery-assigner."+strings.Repeat("c", 64) { - t.Fatalf("ControllerGrantKey = %q, %v", key, err) + t.Fatalf("PartGrantKey = %q, %v", key, err) } - module, back, err := ParseControllerGrantKey(key) + module, back, err := ParsePartGrantKey(key) if err != nil || module != "mesh-delivery-assigner" || back != fp { - t.Errorf("ParseControllerGrantKey = %q, %q, %v", module, back, err) + t.Errorf("ParsePartGrantKey = %q, %q, %v", module, back, err) } held, _ := HeldKey("mesh-mail", fp) for _, bad := range []string{"want.ace.mesh-mail", held, "ace.mesh-mail", ""} { - if _, _, err := ParseControllerGrantKey(bad); err == nil || !strings.Contains(err.Error(), "\""+bad+"\"") { - t.Errorf("ParseControllerGrantKey(%q) = %v, want a refusal naming it", bad, err) + if _, _, err := ParsePartGrantKey(bad); err == nil || !strings.Contains(err.Error(), "\""+bad+"\"") { + t.Errorf("ParsePartGrantKey(%q) = %v, want a refusal naming it", bad, err) } } - for _, g := range []ControllerGrant{{}, {Accounts: []string{"UABC", "UDEF"}}} { - data, err := EncodeControllerGrant(g) + for _, g := range []Grant{{}, {Accounts: []string{"UABC", "UDEF"}}} { + data, err := EncodeGrant(g) if err != nil { t.Fatal(err) } - got, err := DecodeControllerGrant(data) + got, err := DecodeGrant(data) g.V = Version - if err != nil || !SameControllerGrant(got, g) { + if err != nil || !SameGrant(got, g) { t.Errorf("grant %+v came back %+v, %v", g, got, err) } } - if _, err := EncodeControllerGrant(ControllerGrant{Accounts: []string{""}}); err == nil { + if _, err := EncodeGrant(Grant{Accounts: []string{""}}); err == nil { t.Errorf("a grant naming an empty account was accepted") } - // The grants part's keys are the controller's: the same key round-trips; the source is the part's bucket only for a - // handed-away lease (issue 774). - if pk, err := PartGrantKey("mesh-delivery-assigner", fp); err != nil || pk != key { - t.Errorf("PartGrantKey = %q, %v, want the controller's %q", pk, err, key) - } - if m, f, err := ParsePartGrantKey(key); err != nil || m != "mesh-delivery-assigner" || f != fp { - t.Errorf("ParsePartGrantKey = %q, %q, %v", m, f, err) - } + g := Grant{Accounts: []string{"ace.mesh-build-go", "shanks.mesh-mail.reader"}} for _, c := range []struct { - away bool - bucket, read string - }{ - {false, "mesh-controller_grants", "mesh-controller.grants"}, - {true, "mesh-grants_grants", "mesh-grants.grants"}, - } { - if b, r := GrantSource(c.away); b != c.bucket || r != c.read { - t.Errorf("GrantSource(%v) = %q, %q, want %q, %q", c.away, b, r, c.bucket, c.read) - } - } - g := ControllerGrant{Accounts: []string{"ace.mesh-build-go", "shanks.mesh-mail.reader"}} - for _, c := range []struct { - g ControllerGrant + g Grant node, module string names bool }{ @@ -327,7 +308,7 @@ func TestControllerGrant(t *testing.T) { {g, "shanks", "mesh-mail", true}, {g, "g14", "mesh-build-go", false}, {g, "ace", "mesh-build", false}, - {ControllerGrant{}, "novox", "nats", true}, + {Grant{}, "novox", "nats", true}, } { if got := c.g.NamesNode(c.node, c.module); got != c.names { t.Errorf("%v names %s.%s: %v, want %v", c.g.Accounts, c.node, c.module, got, c.names) diff --git a/go/delivery/builder.go b/go/delivery/builder.go index 447196d..c940019 100644 --- a/go/delivery/builder.go +++ b/go/delivery/builder.go @@ -10,8 +10,8 @@ import ( // The builder's keys (novox/hq ADR 0429 clauses 5 and 16, ADR 0437 clause 5, ADR 0405 clauses 2 and 7, ADR 0453 // clause 4): a build is `.` in the builder's bucket, one key per fingerprint, since equal // fingerprints are equal content and the same source is never built twice. They are written only by the builder part -// from R4c and read by the checker, the assigner and the view, and from R4e by the controller. They are not the -// controller's build records (buildrecord.go: BuildsBucket, BuildKey), which stay as they are. +// from R4c and read by the checker, the assigner and the view. They are not the ended controller's build records +// (buildrecord.go: BuildKey). // // **The rules its writer keeps.** A builder claims a fingerprint that has no build key by creating it queued or // building, a create that fails when the key exists, and moves it by compare-and-set; a builder reclaims only a build diff --git a/go/delivery/builder_test.go b/go/delivery/builder_test.go index cdcb8f5..de480ae 100644 --- a/go/delivery/builder_test.go +++ b/go/delivery/builder_test.go @@ -166,10 +166,7 @@ func TestBuilderLayoutDoesNotCollideWithTheControllers(t *testing.T) { if BuilderBuildsBucket != "mesh-delivery-builder_builds" || BuilderBuildsHistory != 5 || BuilderModule != "mesh-delivery-builder" { t.Errorf("bucket %s history %d module %s", BuilderBuildsBucket, BuilderBuildsHistory, BuilderModule) } - if BuilderBuildsBucket == BuildsBucket || BuilderModule == "mesh-controller" { - t.Error("the builder's bucket is the controller's") - } - if BuildsBucket != "mesh-controller_builds" || BuildKey("m", "7") != "m.build.7" || NewestKey("m") != "m.newest" { + if BuildKey("m", "7") != "m.build.7" || NewestKey("m") != "m.newest" { t.Error("the controller's layout changed") } // the two key forms are never read as each other diff --git a/go/delivery/buildrecord.go b/go/delivery/buildrecord.go index 1bccc22..08fd35f 100644 --- a/go/delivery/buildrecord.go +++ b/go/delivery/buildrecord.go @@ -2,8 +2,8 @@ package delivery import "time" -// BuildRecord is the layout mesh-controller internal/chain/records.go Build writes (line 91), the fields a reader outside -// the controller reads (novox/hq ADR 0437 §4 §5, issue 1223). The controller's record carries more; a decode ignores +// BuildRecord is the layout the ended controller's chain wrote (mesh-controller internal/chain/records.go Build, line 91), +// the fields a reader outside it read (novox/hq ADR 0437 §4 §5, issue 1223). The controller's record carries more; a decode ignores // what is not here. Path is the module's directory in its repository, as the controller writes it (Build.Path, records.go:100, // set from the build request trimmed of slashes at internal/inventory/chain_requests.go:19); empty for a module whose // manifest is the repository's root. SourceInputs has no json keys of its own: it is written under its Go field names, as the controller @@ -32,11 +32,6 @@ type Artifact struct { Reference string `json:"reference"` } -// BuildsBucket is the bucket the builds are kept in: mesh-controller internal/broker/controller_buckets.go:101, -// BucketName(ControllerSeat, ChainBuilds), with ControllerSeat "mesh-controller" (internal/broker/streams.go:359) and -// ChainBuilds "builds" (controller_buckets.go:113). -const BuildsBucket = "mesh-controller_builds" - // BuildKey is a build's record under its module, a build that knew none under the request's key: copied from // mesh-controller internal/chain/keys.go:35-40 (BuildKey, with RequestKey at line 42). func BuildKey(module, id string) string { diff --git a/go/delivery/buildrecord_test.go b/go/delivery/buildrecord_test.go index 2a1d4f0..a50ad6d 100644 --- a/go/delivery/buildrecord_test.go +++ b/go/delivery/buildrecord_test.go @@ -114,9 +114,6 @@ func TestBuildRecordWithoutInputs(t *testing.T) { // 1223/AC2 func TestBuildKeys(t *testing.T) { // 1223/AC3 // Literal values of mesh-controller internal/broker/controller_buckets.go:101 (BucketName(ControllerSeat, ChainBuilds) // is "mesh-controller_builds") and internal/chain/keys.go:35-44. - if BuildsBucket != "mesh-controller_builds" { - t.Errorf("BuildsBucket = %q", BuildsBucket) - } if got := BuildKey("build-agent", "build-1"); got != "build-agent.build.build-1" { t.Errorf("BuildKey = %q", got) } diff --git a/go/delivery/buildrun/asks.go b/go/delivery/buildrun/asks.go index a03bbbf..fdaf305 100644 --- a/go/delivery/buildrun/asks.go +++ b/go/delivery/buildrun/asks.go @@ -32,8 +32,8 @@ type AskWaiter interface { type Asker struct { Publisher AskPublisher Waiter AskWaiter - // Bound is how long a step may run once a build node took the ask (its key written running) before the asker stops - // waiting; zero waits as long as ctx. An ask waiting in its stream is not bounded: the stream's backlog is no fault + // Bound is how long a running step (a build node took the ask, its key written running) may stand still, with no new phase, + // before the asker stops waiting; a step that keeps moving is waited for up to 3 Bounds from its start (issue 1930); zero waits as long as ctx. An ask waiting in its stream is not bounded: the stream's backlog is no fault // (ADR 0525 §6, §9), and the bound starts again when another node takes the ask. Bound time.Duration } @@ -68,20 +68,33 @@ func (k Asker) Wait(ctx context.Context, a delivery.StepAsk) (delivery.Step, err if err != nil { return delivery.Step{}, err } - var bound <-chan time.Time - var timer *time.Timer + var bound, outerCap <-chan time.Time + var timer, capTimer *time.Timer defer func() { if timer != nil { timer.Stop() } + if capTimer != nil { + capTimer.Stop() + } }() - running, since := "", time.Time{} + running, since, phase, phaseAt := "", time.Time{}, "", time.Time{} + standStill := func() error { + if phase == "" { + return fmt.Errorf("the ask %s's step on %s did not end within %s of its start: it stood still, saying no phase", a.ID, running, k.Bound) + } + return fmt.Errorf("the ask %s's step on %s did not end within %s: it stood still in %q since %s", + a.ID, running, k.Bound, phase, phaseAt.UTC().Format(time.RFC3339)) + } for { select { case <-ctx.Done(): return delivery.Step{}, ctx.Err() case <-bound: - return delivery.Step{}, fmt.Errorf("the ask %s's step on %s did not end within %s of its start", a.ID, running, k.Bound) + return delivery.Step{}, standStill() + case <-outerCap: + return delivery.Step{}, fmt.Errorf("the ask %s's step on %s did not end within %s of its start: it kept moving (last phase %q at %s) but never ended", + a.ID, running, 3*k.Bound, phase, phaseAt.UTC().Format(time.RFC3339)) case step, ok := <-steps: if !ok { return delivery.Step{}, errors.New("the watch of the ask's step ended") @@ -92,21 +105,42 @@ func (k Asker) Wait(ctx context.Context, a delivery.StepAsk) (delivery.Step, err if step.State != delivery.StepRunning { return step, nil } - if k.Bound <= 0 || (step.Node == running && step.Started.Equal(since)) { + if k.Bound <= 0 { + continue + } + newRun := step.Node != running || !step.Started.Equal(since) + if !newRun && step.Phase == phase && step.PhaseAt.Equal(phaseAt) { continue } // A node took the ask, or took it again after its run stopped (another node, or the same node's build module - // replaced mid-run: a new start): its step is bounded from its own start. - running, since = step.Node, step.Started + // replaced mid-run: a new start), or its step entered a phase. The bound is on standing still: it runs from the + // step's last movement (its phase's start, else its start), so a step that keeps moving is waited for, up to + // three bounds from its start (issue 1930). + running, since, phase, phaseAt = step.Node, step.Started, step.Phase, step.PhaseAt + last := step.Started + if step.PhaseAt.After(last) { + last = step.PhaseAt + } left := k.Bound - if !step.Started.IsZero() { - left = time.Until(step.Started.Add(k.Bound)) + if !last.IsZero() { + left = time.Until(last.Add(k.Bound)) } if timer != nil { timer.Stop() } timer = time.NewTimer(max(left, 0)) bound = timer.C + if newRun { + if capTimer != nil { + capTimer.Stop() + } + outer := 3 * k.Bound + if !step.Started.IsZero() { + outer = time.Until(step.Started.Add(3 * k.Bound)) + } + capTimer = time.NewTimer(max(outer, 0)) + outerCap = capTimer.C + } } } } diff --git a/go/delivery/buildrun/asks_test.go b/go/delivery/buildrun/asks_test.go index 7453b63..5403a54 100644 --- a/go/delivery/buildrun/asks_test.go +++ b/go/delivery/buildrun/asks_test.go @@ -53,6 +53,41 @@ func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) { if len(g.Asks()) != 0 { t.Fatal("a wait put an ask") } + // Issue 1930: the bound is on a step that stands still, not on its age. A step that enters a new phase inside the bound is + // waited for until it ends; one that stays in a phase the bound fails at once, naming the phase; one that never stops moving + // ends at three bounds, said as such. + k = Asker{Publisher: g, Waiter: g, Bound: 60 * time.Millisecond} + started := time.Now() + go func() { + for i := range 8 { + g.Write(delivery.Step{ID: "mvl0frv18d1980", Node: "ace", State: delivery.StepRunning, Started: started, Phase: "p" + string(rune('a'+i)), PhaseAt: time.Now()}) + time.Sleep(15 * time.Millisecond) + } + g.Write(delivery.Step{ID: "mvl0frv18d1980", Node: "ace", State: delivery.StepPassed}) + }() + if step, err := k.Wait(context.Background(), goAsk("mvl0frv18d1980")); err != nil || step.State != delivery.StepPassed { + t.Fatalf("a step that kept moving past its bound: %+v, %v", step, err) + } + g.Write(delivery.Step{ID: "mvl0frv18d1981", Node: "ace", State: delivery.StepRunning, Started: time.Now(), Phase: "integration: tests", PhaseAt: time.Now()}) + if _, err := k.Wait(context.Background(), goAsk("mvl0frv18d1981")); err == nil || !strings.Contains(err.Error(), `stood still in "integration: tests"`) { + t.Fatalf("a step that stood still in a phase: %v", err) + } + loop := make(chan struct{}) + defer close(loop) + started = time.Now() + go func() { + for i := 0; ; i++ { + select { + case <-loop: + return + case <-time.After(20 * time.Millisecond): + g.Write(delivery.Step{ID: "mvl0frv18d1982", Node: "ace", State: delivery.StepRunning, Started: started, Phase: "q" + string(rune('a'+i%26)), PhaseAt: time.Now()}) + } + } + }() + if _, err := k.Wait(context.Background(), goAsk("mvl0frv18d1982")); err == nil || !strings.Contains(err.Error(), "kept moving") { + t.Fatalf("a step that never stops moving: %v", err) + } // Issue 734: an ask the build seat gave up on (DEAD_LETTERS keeps its copy with the ask's id) ends its wait as failed // with the words, though a node had taken it; under NotTakenBound one no node takes ends at the bound; a step written // first is answered as the node wrote it. @@ -157,7 +192,7 @@ func TestARedeliveryToTheSameNodeRestartsTheBound(t *testing.T) { first := time.Now() f.Write(delivery.Step{ID: id, Node: "ace", State: delivery.StepRunning, Started: first}) go func() { - time.Sleep(40 * time.Millisecond) + time.Sleep(15 * time.Millisecond) f.Write(delivery.Step{ID: id, Node: "ace", State: delivery.StepRunning, Started: time.Now()}) time.Sleep(50 * time.Millisecond) // past the first run's bound, inside the second's f.Write(delivery.Step{ID: id, Node: "ace", State: delivery.StepPassed, Started: time.Now()}) diff --git a/go/delivery/composed.go b/go/delivery/composed.go index fa771cf..659a19b 100644 --- a/go/delivery/composed.go +++ b/go/delivery/composed.go @@ -7,29 +7,17 @@ import ( "strings" ) -// The controller's compositions (novox/hq ADR 0539 clauses 1 to 4). A **composition** is one module's resources -// composed for one node at one fingerprint, written by the controller, which keeps the inputs. It lives in a bucket of -// each node, `composed-`, which only that node's node-engine reads (a composition can carry a sealed value), under -// `..`, and the node's own resources (adoption, firewall openings, accounts, the -// resolver, identities and groups) under `.node`. Beside it, under the same key in `mesh-controller_composed`, -// which the assigner reads, the controller writes the composition's digest and its judgement of the move from the -// composition the node runs now (ComposedDigest). The controller writes both by compare-and-set while it holds the -// lease; the operator's credential is a second writer on the hand path alone (clause 10, mesh-hand-compose). -// -// A composition is derived state (clause 4): its key and its digest are deleted by the controller when its fingerprint -// is no longer among the module's five newest and no want names it, and each bucket keeps five revisions of a key. +// The compositions (novox/hq ADR 0539 clauses 1 to 4). A **composition** is one module's resources composed for one node +// at one fingerprint: a node composes its own (ADR 0547 §2), and the ended controller wrote them into a bucket of each +// node, `composed-`, which nothing writes since it ended (ADR 0590), under `..`, and +// the node's own resources (adoption, firewall openings, accounts, the resolver, identities and groups) under +// `.node`. A composition's digest and its judgement of the move from the composition the node runs now is +// ComposedDigest. const ( // ComposedBucketPrefix starts the name of a node's bucket of compositions, ComposedBucketFor(node). ComposedBucketPrefix = "composed-" // ComposedHistory is the revisions a node's composition bucket keeps per key (ADR 0539 clause 4). ComposedHistory = 5 - // ControllerModule is the module of the controller, the writer of the compositions. - ControllerModule = "mesh-controller" - // ControllerComposedBucket is the controller's bucket of each composition's digest and judgement, `_`, read by the assigner (ADR 0539 clauses 2 and 6). - ControllerComposedBucket = ControllerModule + "_composed" - // ControllerComposedHistory is the revisions that bucket keeps per key. - ControllerComposedHistory = 5 // ComposedKeep is how many of a module's newest fingerprints keep their compositions though no want names them // (ADR 0539 clause 4). ComposedKeep = 5 @@ -142,8 +130,8 @@ func checkDigest(digest, of string) error { // // Policy is the module's upgrade policy as the controller holds it (ADR 0236, ADR 0539 clause 14), with PolicyFrom // where it comes from and PolicyWhy why: the assigner holds a move of a module of policy record from it, the operator's -// own choice (`upgrade` with policy) included. Rewritten when the policy changes. Empty on a composition made by hand -// (mesh-hand-compose), whose want the operator writes. +// own choice (`upgrade` with policy) included. Rewritten when the policy changes. Empty on a composition made by hand, +// whose want the operator wrote. type ComposedDigest struct { V int `json:"v"` Digest string `json:"digest"` diff --git a/go/delivery/composed_signature.go b/go/delivery/composed_signature.go index ad3d042..0b4abd7 100644 --- a/go/delivery/composed_signature.go +++ b/go/delivery/composed_signature.go @@ -6,9 +6,9 @@ import ( "fmt" ) -// A composition's signature (novox/hq ADR 0539 clause 3): the controller signs each composition it writes with its -// key, and the operator signs one made by hand with mesh-hand-compose with theirs. The node-engine trusts exactly these two -// signers and applies nothing of a composition whose signature fails or whose signer is neither. The signature is an +// A composition's signature (novox/hq ADR 0539 clause 3): the signer of a composition signs it with its key (the ended +// controller signed those it wrote, the operator one made by hand). The node-engine trusts the signers it is given and +// applies nothing of a composition whose signature fails or whose signer is none of them. The signature is an // ed25519 signature, as a declaration's is, of the composition's digest (ComposedDigestOf) after a context line, so a // signature made for anything else never verifies here. const composedSigningContext = "novox composition v1\n" @@ -84,7 +84,7 @@ func VerifyComposed(c Composed, trusted []ed25519.PublicKey) (string, error) { } } if key == nil { - return "", fmt.Errorf("delivery: the composition of %q on %s is signed by %s, which is not a trusted signer (the controller's key or the operator's)", + return "", fmt.Errorf("delivery: the composition of %q on %s is signed by %s, which is not a trusted signer (the vault's key or the operator's)", c.Module, c.Node, c.Signature.Signer) } digest, err := ComposedDigestOf(c) diff --git a/go/delivery/composed_signature_test.go b/go/delivery/composed_signature_test.go index 3098e27..3a531d0 100644 --- a/go/delivery/composed_signature_test.go +++ b/go/delivery/composed_signature_test.go @@ -108,7 +108,7 @@ func TestACompositionIsSignedByAKeptKey(t *testing.T) { // signers a node trusts and names which; an unknown key and another kind's context are refused. vaultPub, vault := aSigningKey(t) controllerPub, controller := aSigningKey(t) - trusted := []Signer{{Name: SignerController, Key: controllerPub}, {Name: SignerVault, Key: vaultPub}} + trusted := []Signer{{Name: "operator", Key: controllerPub}, {Name: SignerVault, Key: vaultPub}} enrol, _ := SignContext(KindEnrolment) certCtx, _ := SignContext(KindNodeCertificate) msg := []byte("the reply") @@ -120,7 +120,7 @@ func TestACompositionIsSignedByAKeptKey(t *testing.T) { want string }{ {"the vault's key verifies", vault, enrol, enrol, SignerVault}, - {"the controller's key verifies", controller, enrol, enrol, SignerController}, + {"the controller's key verifies", controller, enrol, enrol, "operator"}, {"an unknown key is refused", other, enrol, enrol, ""}, {"the wrong context is refused", vault, certCtx, enrol, ""}, } { @@ -148,7 +148,7 @@ func TestACompositionIsSignedByAKeptKey(t *testing.T) { } } if got := Trusts(trusted, nil); len(got) != 2 || !TrustsKey(got, SignerVault, SignerFingerprint(vaultPub)) || - !TrustsKey(got, SignerController, SignerFingerprint(controllerPub)) { + !TrustsKey(got, "operator", SignerFingerprint(controllerPub)) { t.Errorf("trusts: %v", got) } // A certificate the vault's authority signs for a node's name and serving key checks against a bundle holding that diff --git a/go/delivery/delivery.go b/go/delivery/delivery.go index 595bcc2..3b72bb7 100644 --- a/go/delivery/delivery.go +++ b/go/delivery/delivery.go @@ -367,8 +367,7 @@ type Have struct { // holds now. The bus reloads the file itself when its digest changes (the module's entrypoint), so Digest is the list // the bus loads. Wanted differs from Digest only while a write failed, and Error says why. type UserListWritten struct { - // Source is the key the list was taken from: the controller's composition of the bus module for this node - // (`..`), or the grants part's `user-list` once it took over. + // Source is the key the list was taken from: the grants part's `user-list` (GrantsUserListSource). Source string `json:"source"` Revision uint64 `json:"revision"` // Wanted is the digest of the list the source holds, `sha256:`. @@ -378,8 +377,8 @@ type UserListWritten struct { Users int `json:"users"` At time.Time `json:"at"` Error string `json:"error,omitempty"` - // Follows is which list the node follows now (FollowController or FollowGrants, novox/hq ADR 0547 clause 8), empty - // from a node-engine before the switch. + // Follows is which list the node follows now (FollowGrants, novox/hq ADR 0547 clause 8), empty from a node-engine + // before the switch. Follows string `json:"follows,omitempty"` // Key is the bus node's sealing key, its public half as SPKI PEM: the key the grants part seals its list to // (clause 7). Public, as the vault holds it. @@ -417,19 +416,19 @@ type HaveNode struct { // its shadow composer whenever an answer changes, removed when the candidate key goes. The node applies nothing of a // candidate. Candidates map[string]CandidateAnswer `json:"candidates,omitempty"` - // Composes is whose compositions the node applies: its own (ComposesOwn, placement places it) or the - // controller's (ComposesController); ComposesWhy says why in words. Written by the node agent at each start, so a - // node returning to the controller's compositions when its placement key is deleted says so (issue 1126). Empty - // from an engine older than the field. + // Composes is whose compositions the node applies: its own (ComposesOwn, placement places it), or none, empty, with + // ComposesWhy saying why in words. Written by the node agent at each start (issue 1126). An engine from before the + // controller ended may still say `controller` (composedByTheEndedController), which is read and never written. Composes string `json:"composes,omitempty"` ComposesWhy string `json:"composes-why,omitempty"` } -// Whose compositions a node applies (HaveNode.Composes). -const ( - ComposesOwn = "own" - ComposesController = "controller" -) +// ComposesOwn is a node applying its own compositions (HaveNode.Composes). +const ComposesOwn = "own" + +// composedByTheEndedController is what an engine from before the controller ended (ADR 0590) wrote in Composes while +// it applied the controller's compositions: still read, so its have is not refused, and written by no engine now. +const composedByTheEndedController = "controller" // Presence is whether a node or a part's instance is online; the entry's own time says since when. The watchman // writes it. @@ -526,9 +525,9 @@ func DecodeHave(data []byte) (Have, error) { func (n HaveNode) check() error { switch n.Composes { - case "", ComposesOwn, ComposesController: + case "", ComposesOwn, composedByTheEndedController: default: - return fmt.Errorf("delivery: a node composes %q, neither %s nor %s", n.Composes, ComposesOwn, ComposesController) + return fmt.Errorf("delivery: a node composes %q, not %s", n.Composes, ComposesOwn) } for group, a := range n.Candidates { if group == "" { diff --git a/go/delivery/delivery_test.go b/go/delivery/delivery_test.go index 8bdc8bd..328fad7 100644 --- a/go/delivery/delivery_test.go +++ b/go/delivery/delivery_test.go @@ -242,29 +242,21 @@ func TestRoundTrip(t *testing.T) { t.Fatal(err) } back, err := DecodeGrantsUserList(raw) - if err != nil || back.Digest != digest || back.LeftOut == nil || back.Follow != FollowGrants || back.LeaseHanded { + if err != nil || back.Digest != digest || back.LeftOut == nil || back.Follow != FollowGrants { t.Fatalf("decoded %+v, %v", back, err) } - // ADR 0582 clause 3 (issue 774, 774/S3): a key written before the flag existed reads not handed; one written with - // it reads handed. + // Issue 1126 B4a (ADR 0590): a key written with the lease's flag still reads; the controller's list is no source. if old, err := DecodeGrantsUserList([]byte(`{"v":1,"follow":"grants","digest":"","object":"","sealed_to":"",` + - `"node":"novox","revision":3,"users":40,"left_out":[],"at":"2026-10-30T10:00:00Z"}`)); err != nil || old.LeaseHanded { - t.Errorf("a key from before the lease write decodes %+v, %v", old, err) + `"node":"novox","revision":3,"users":40,"left_out":[],"at":"2026-10-30T10:00:00Z","lease-handed":true}`)); err != nil || old.Revision != 3 { + t.Errorf("a key with the lease's flag decodes %+v, %v", old, err) } - g.LeaseHanded = true - if raw, err := EncodeGrantsUserList(g); err != nil || !bytes.Contains(raw, []byte(`"lease-handed":true`)) { - t.Errorf("the handed flag is written as %s, %v", raw, err) - } else if back, err := DecodeGrantsUserList(raw); err != nil || !back.LeaseHanded { - t.Errorf("the handed flag reads back %+v, %v", back, err) - } - g.LeaseHanded = false - for _, bad := range []GrantsUserList{{Follow: "both", Digest: digest, Object: g.Object}, + for _, bad := range []GrantsUserList{{Follow: "both", Digest: digest, Object: g.Object}, {Follow: "controller"}, {Follow: FollowGrants, Digest: digest, Object: "other"}, {Follow: FollowGrants, Digest: "md5:x", Object: "md5:x"}} { if _, err := EncodeGrantsUserList(bad); err == nil { t.Errorf("%+v was written", bad) } } - if _, err := EncodeGrantsUserList(GrantsUserList{Follow: FollowController}); err != nil { + if _, err := EncodeGrantsUserList(GrantsUserList{Follow: FollowGrants}); err != nil { t.Errorf("a switch with no list yet refused: %v", err) } object, err := SealUserList(list, node.PublicKey) diff --git a/go/delivery/grants.go b/go/delivery/grants.go index 9da49a0..3ed03e6 100644 --- a/go/delivery/grants.go +++ b/go/delivery/grants.go @@ -7,41 +7,44 @@ import ( "strings" ) -// The controller's grants (novox/hq ADR 0528 clause 2, ADR 0429 §6 rule 6): when the controller has issued a module's -// bus credential for a fingerprint, it writes `.` in its bucket grants. The assigner reads -// it: a node's want moves to a fingerprint only once its grant exists. Only the controller writes the bucket. -// -// The names say Controller, since the catalogue's GrantKey (catalogue/streams.go) is a stream's grant pattern, unrelated. +// **The grants issued** (novox/hq ADR 0528 clause 2, ADR 0429 §6 rule 6, ADR 0547 §4, §5, ADR 0590): when the grants +// part has issued a module's bus credentials for a fingerprint, it writes `.` in its state +// `grants` (PartGrantsBucket). The assigner reads it: a node's want moves to a fingerprint only once its grant exists. +// Only the grants part writes the bucket. const ( - // ControllerGrantsBucket is the controller's bucket of issued credentials by fingerprint. - ControllerGrantsBucket = "mesh-controller_grants" - // ControllerGrantsHistory is the revisions the bucket keeps per key. - ControllerGrantsHistory = 5 + // PartGrantsState is the grants part's state of issued credentials by fingerprint, as its manifest names it. + PartGrantsState = "grants" + // PartGrantsBucket is that state's bucket, `_`. + PartGrantsBucket = "mesh-grants_" + PartGrantsState + // PartGrantsRead is that state as a reader names it in `reads`. + PartGrantsRead = "mesh-grants." + PartGrantsState + // PartGrantsHistory is the revisions the bucket keeps per key. + PartGrantsHistory = 5 ) -// ControllerGrantKey is the key, in ControllerGrantsBucket, of a module's credential issued for a fingerprint: -// `.`, the hex being the fingerprint's key part (FingerprintKeyPart). -func ControllerGrantKey(module, fingerprint string) (string, error) { +// PartGrantKey is the key, in PartGrantsBucket, of a module's credential issued for a fingerprint: `.`, +// the hex being the fingerprint's key part (FingerprintKeyPart). +func PartGrantKey(module, fingerprint string) (string, error) { return BuilderBuildKey(module, fingerprint) } -// ParseControllerGrantKey is the inverse of ControllerGrantKey; it refuses a want or held key, naming it. -func ParseControllerGrantKey(key string) (module, fingerprint string, err error) { +// ParsePartGrantKey is the inverse of PartGrantKey; it refuses a want or held key, naming it. +func ParsePartGrantKey(key string) (module, fingerprint string, err error) { if module, fingerprint, err = ParseBuilderBuildKey(key); err != nil { return "", "", fmt.Errorf("delivery: %q is not a grant key, .: %w", key, err) } return module, fingerprint, nil } -// ControllerGrant is the value of a grant key: the bus accounts (their public keys) the module's processes were issued +// Grant is the value of a grant key: the bus accounts (their public keys) the module's processes were issued // for that fingerprint, none for a module whose grants the tool runner's account carries, so a reissue for another // account is a new revision. The entry's own time says when it was issued. -type ControllerGrant struct { +type Grant struct { V int `json:"v"` Accounts []string `json:"accounts,omitempty"` } -func (g ControllerGrant) check() error { +func (g Grant) check() error { for _, a := range g.Accounts { if strings.TrimSpace(a) == "" || strings.ContainsAny(a, " \t\n") { return fmt.Errorf("delivery: a grant names an empty or spaced account %q", a) @@ -50,8 +53,8 @@ func (g ControllerGrant) check() error { return nil } -// EncodeControllerGrant is g as JSON at Version; it refuses an empty or spaced account. -func EncodeControllerGrant(g ControllerGrant) ([]byte, error) { +// EncodeGrant is g as JSON at Version; it refuses an empty or spaced account. +func EncodeGrant(g Grant) ([]byte, error) { if err := g.check(); err != nil { return nil, err } @@ -59,26 +62,26 @@ func EncodeControllerGrant(g ControllerGrant) ([]byte, error) { return json.Marshal(g) } -// DecodeControllerGrant reads a grant; it refuses an unknown version (ErrUnknownVersion) and an empty or spaced account. -func DecodeControllerGrant(data []byte) (ControllerGrant, error) { - var g ControllerGrant +// DecodeGrant reads a grant; it refuses an unknown version (ErrUnknownVersion) and an empty or spaced account. +func DecodeGrant(data []byte) (Grant, error) { + var g Grant if err := decode(data, &g); err != nil { - return ControllerGrant{}, err + return Grant{}, err } if err := g.check(); err != nil { - return ControllerGrant{}, err + return Grant{}, err } return g, nil } -// SameControllerGrant says whether two grants are the same value. -func SameControllerGrant(a, b ControllerGrant) bool { return reflect.DeepEqual(a, b) } +// SameGrant says whether two grants are the same value. +func SameGrant(a, b Grant) bool { return reflect.DeepEqual(a, b) } // NamesNode says whether the grant names module's accounts on node: its own `.` or a process's // `..`. A grant that names no account (a module that does not speak on the bus) names every // node. The assigner wants a node only once the grant names its account (issue 1945), and a put waits for it (ADR // 0546 clause 4). -func (g ControllerGrant) NamesNode(node, module string) bool { +func (g Grant) NamesNode(node, module string) bool { if len(g.Accounts) == 0 { return true } @@ -90,39 +93,3 @@ func (g ControllerGrant) NamesNode(node, module string) bool { } return false } - -// **The grants part's grants** (novox/hq ADR 0547 §4, §5, ADR 0561 §2, §6; issue 774). The grants part writes the same -// keys, with the same value, into its own state `grants` (the bucket PartGrantsBucket), beside the controller's: first in -// shadow, which nobody reads, so a tool can compare the two before anything switches. The assigner reads the part's keys -// only once the controller's lease is handed away (GrantSource), and the controller's until then. -const ( - // PartGrantsState is the grants part's state of issued credentials by fingerprint, as its manifest names it. - PartGrantsState = "grants" - // PartGrantsBucket is that state's bucket, `_`. - PartGrantsBucket = "mesh-grants_" + PartGrantsState - // PartGrantsRead is that state as a reader names it in `reads`. - PartGrantsRead = "mesh-grants." + PartGrantsState - // PartGrantsHistory is the revisions the bucket keeps per key. - PartGrantsHistory = ControllerGrantsHistory -) - -// PartGrantKey is the key, in PartGrantsBucket, of a module's credential issued for a fingerprint: the layout of the -// controller's key (ControllerGrantKey), so a key of one is the key of the other and a compare is by key. -func PartGrantKey(module, fingerprint string) (string, error) { - return ControllerGrantKey(module, fingerprint) -} - -// ParsePartGrantKey is the inverse of PartGrantKey. -func ParsePartGrantKey(key string) (module, fingerprint string, err error) { - return ParseControllerGrantKey(key) -} - -// GrantSource names the bucket the assigner reads grants from, and the `reads` name that grants it: the grants part's -// once the controller's lease is handed away (buscompose.LeaseHandedAway, which refuses every other value of the lease, -// an unreadable one included), the controller's in every other case. -func GrantSource(leaseHandedAway bool) (bucket, read string) { - if leaseHandedAway { - return PartGrantsBucket, PartGrantsRead - } - return ControllerGrantsBucket, "mesh-controller.grants" -} diff --git a/go/delivery/held.go b/go/delivery/held.go index 77e243a..3cd8711 100644 --- a/go/delivery/held.go +++ b/go/delivery/held.go @@ -8,19 +8,19 @@ import ( ) // Whether the artifact store still holds what a done key names (novox/hq issue 1937). A build module pushes a bundle or -// an archive as a bare blob (ADR 0498 §1), and the store's nightly collector deletes every blob no manifest names; only -// the controller's take-in and sweep hold one by its manifest. A done key the controller has not taken in can therefore -// name a blob that is gone, and reusing it sends a definition every machine fails on. So the builder asks before it -// reuses a done key, and the controller says when a take-in found a gone blob. +// an archive as a bare blob (ADR 0498 §1), and the store's nightly collector deletes every blob no manifest names. A done +// key can therefore name a blob that is gone, and reusing it sends a definition every machine fails on. So the builder +// asks before it reuses a done key, and the artifact sweep says when it found a gone blob. Both are the seat +// mesh-artifact-sweep's (novox/hq ADR 0571 §2, ADR 0590): the controller that served them first has ended. const ( - // HeldVerb is the controller seat's verb the builder asks, as a module's invokes names it: references in, each + // HeldVerb is the artifact sweep seat's verb the builder asks, as a module's invokes names it: references in, each // answered held or gone. - HeldVerb = "seat:mesh-controller.held" - // ArtifactsGoneEvent is the controller seat's event said when a take-in is refused because the store does not hold - // an artifact the key names: as a consumer names it, `mesh-controller.artifacts-gone`. + HeldVerb = "seat:mesh-artifact-sweep.held" + // ArtifactsGoneEvent is the artifact sweep seat's event said when the store does not hold an artifact a key names: + // as a consumer names it, `mesh-artifact-sweep.artifacts-gone`. ArtifactsGoneEvent = "artifacts-gone" // ArtifactsGoneSubject is where it is published (the seat's event subject). - ArtifactsGoneSubject = "mesh.seat.mesh-controller.event." + ArtifactsGoneEvent + ArtifactsGoneSubject = "mesh.seat.mesh-artifact-sweep.event." + ArtifactsGoneEvent ) // HeldAnswer is the answer of HeldVerb: the references the store holds, and those it answered 404 for. A reference the @@ -69,7 +69,7 @@ func DecodeHeldAnswer(raw []byte, asked []string) (HeldAnswer, error) { return a, nil } -// ArtifactsGone is the body of ArtifactsGoneEvent: the module whose builder's key the controller would not take in, +// ArtifactsGone is the body of ArtifactsGoneEvent: the module whose builder's key names a gone artifact, // the key and its fingerprint, and every reference of it the store does not hold. type ArtifactsGone struct { Event string `json:"event"` diff --git a/go/delivery/held_test.go b/go/delivery/held_test.go index f5b71c4..7b8a0c4 100644 --- a/go/delivery/held_test.go +++ b/go/delivery/held_test.go @@ -1,6 +1,7 @@ package delivery import ( + "slices" "strings" "testing" "time" @@ -55,30 +56,13 @@ func TestArtifactsGoneRoundTrip(t *testing.T) { } } -// The verb and the event are on the controller seat's contract under the names the builder uses. -func TestHeldOnTheControllerSeat(t *testing.T) { - if HeldVerb != "seat:"+catalogue.ControllerSeatName+".held" { - t.Fatalf("HeldVerb %q", HeldVerb) +// The verb and the event are on the artifact sweep seat's contract under the names the builder uses (ADR 0571 §2). +func TestHeldOnTheArtifactSweepSeat(t *testing.T) { + seat, ok := catalogue.SeatNamed(catalogue.ArtifactSweepSeat) + if !ok || HeldVerb != "seat:"+seat.Name+".held" || ArtifactsGoneSubject != "mesh.seat."+seat.Name+".event."+ArtifactsGoneEvent { + t.Fatalf("HeldVerb %q, ArtifactsGoneSubject %q", HeldVerb, ArtifactsGoneSubject) } - found := false - for _, v := range catalogue.ControllerVerbs { - if v.Name == "held" { - found = true - } + if !slices.ContainsFunc(seat.Serves, func(v catalogue.Verb) bool { return v.Name == "held" }) || !slices.Contains(seat.Emits, ArtifactsGoneEvent) { + t.Fatalf("the sweep seat serves %v and emits %v", seat.Serves, seat.Emits) } - if !found { - t.Fatal("the controller seat declares no verb held") - } - for _, s := range catalogue.DefaultSeats() { - if s.Name != catalogue.ControllerSeatName { - continue - } - for _, e := range s.Emits { - if e == ArtifactsGoneEvent { - return - } - } - t.Fatalf("the controller seat emits %v, not %s", s.Emits, ArtifactsGoneEvent) - } - t.Fatal("no controller seat") } diff --git a/go/delivery/placement_candidate.go b/go/delivery/placement_candidate.go index cef4c32..fe97e98 100644 --- a/go/delivery/placement_candidate.go +++ b/go/delivery/placement_candidate.go @@ -127,10 +127,9 @@ type CandidateAnswer struct { Held []ModuleRefused `json:"held,omitempty"` // Refused is why the node could not compose with the candidate at all; Refusals is then empty. Refused string `json:"refused,omitempty"` - // Returns is, for a proposal that takes the node's placement key away, what the node does then: it returns to the - // controller's compositions (ReturnsToController), as a node with no placement key composes none of its own. Same, Differs, Added and Removed then compare the controller's compositions at - // the node's wants with what it runs, and the verdict is theirs; a want the controller holds no composition for is - // refused (Refused, without Returns). + // Returns is, for a proposal that takes the node's placement key away, what the node does then (ReturnsToNothing): a + // node with no placement key composes nothing (ADR 0590), so Removed names every module it runs and the verdict is + // theirs. Returns string `json:"returns,omitempty"` // Note is what the node says of how it answered, in words, beside the verdict: a pass that ran over a minute, inputs // it waited for (issue 1126: the shadow shows its work). It changes no verdict. @@ -141,8 +140,8 @@ type CandidateAnswer struct { ValuesDiffer int `json:"values_differ,omitempty"` } -// ReturnsToController is what a node answers a proposal that takes its placement key away. -const ReturnsToController = "the proposal takes this node's placement away: it returns to the controller's compositions" +// ReturnsToNothing is what a node answers a proposal that takes its placement key away. +const ReturnsToNothing = "the proposal takes this node's placement away: it composes nothing then" // ModuleDiffers is one module the node runs and the candidate still places whose composition is not the one it runs: // both digests (Composed with the candidate, Running what runs), the first resource that differs by its id, or empty @@ -227,8 +226,7 @@ func (a CandidateAnswer) check() error { return fmt.Errorf("delivery: a candidate the node could not compose names modules") } if a.Returns != "" && (a.Refused != "" || len(a.Refusals) > 0) { - return fmt.Errorf("delivery: a candidate the node returns to the controller's compositions on is compared, never refused: " + - "a composition the controller holds none of is refused without returning") + return fmt.Errorf("delivery: a candidate that takes the node's placement away is compared, never refused") } for _, d := range a.Differs { if d.Module == "" || d.Why == "" { diff --git a/go/delivery/placement_test.go b/go/delivery/placement_test.go index d4a5f96..6e662c4 100644 --- a/go/delivery/placement_test.go +++ b/go/delivery/placement_test.go @@ -388,8 +388,8 @@ func TestPlacement(t *testing.T) { "a value": {Proposal: id, Revision: 1, Verdict: CandidateRefused, Same: []string{"node"}, Refusals: []ModuleRefused{{Module: "db", Why: "stands in for db/password"}}}, // A head that deletes the node's placement file: the node returns to the controller's compositions (issue 1126, // the seed's revert, mesh-catalog #526). - "returns": {Proposal: id, Revision: 1, Verdict: CandidateSame, Returns: ReturnsToController, Same: []string{"app", "node"}}, - "returns, the controller's differs": {Proposal: id, Revision: 1, Verdict: CandidateDiffers, Returns: ReturnsToController, Same: []string{"node"}, + "returns": {Proposal: id, Revision: 1, Verdict: CandidateSame, Returns: ReturnsToNothing, Same: []string{"app", "node"}}, + "returns, differs": {Proposal: id, Revision: 1, Verdict: CandidateDiffers, Returns: ReturnsToNothing, Same: []string{"node"}, Differs: []ModuleDiffers{{Module: "app", Resource: "file:/etc/app", Why: "the resource differs"}}, Removed: []string{"old"}}, } n := HaveNode{Composed: "sha256:" + strings.Repeat("a", 64), Sealed: []string{"box"}, Candidates: answers, @@ -407,11 +407,15 @@ func TestPlacement(t *testing.T) { t.Error("a node's have without its answers counts as the same") } if SameHaveNode(back, HaveNode{V: Version, Composed: n.Composed, Sealed: n.Sealed, Candidates: answers, - Composes: ComposesController, ComposesWhy: n.ComposesWhy}) { + ComposesWhy: n.ComposesWhy}) { t.Error("a node's have composing otherwise counts as the same") } if _, err := EncodeHaveNode(HaveNode{Composes: "both"}); err == nil { - t.Error("a node composing neither its own nor the controller's was taken") + t.Error("a node composing neither its own nor none was taken") + } + // Issue 1126 B4a: a have an engine from before the controller ended wrote still reads. + if _, err := DecodeHaveNode([]byte(`{"v":1,"composes":"controller"}`)); err != nil { + t.Errorf("an older engine's have is refused: %v", err) } // CompareComposed: the bus's user list is left out on both sides; anything else that differs is named. file := func(id, content string) json.RawMessage { @@ -605,8 +609,8 @@ func TestPlacement(t *testing.T) { "same with an added": {Proposal: "p-x1", Revision: 1, Verdict: CandidateSame, Added: []ModuleAdded{{Module: "a"}}}, "differs refusing": {Proposal: "p-x1", Revision: 1, Verdict: CandidateDiffers, Removed: []string{"a"}, Refusals: []ModuleRefused{{Module: "b", Why: "x"}}}, "refused and modules": {Proposal: "p-x1", Revision: 1, Verdict: CandidateRefused, Refused: "x", Same: []string{"a"}}, - "returns refusing": {Proposal: "p-x1", Revision: 1, Verdict: CandidateRefused, Returns: ReturnsToController, Refusals: []ModuleRefused{{Module: "b", Why: "x"}}}, - "returns refused": {Proposal: "p-x1", Revision: 1, Verdict: CandidateRefused, Returns: ReturnsToController, Refused: "x"}, + "returns refusing": {Proposal: "p-x1", Revision: 1, Verdict: CandidateRefused, Returns: ReturnsToNothing, Refusals: []ModuleRefused{{Module: "b", Why: "x"}}}, + "returns refused": {Proposal: "p-x1", Revision: 1, Verdict: CandidateRefused, Returns: ReturnsToNothing, Refused: "x"}, } { if _, err := EncodeHaveNode(HaveNode{Candidates: map[string]CandidateAnswer{"g": a}}); err == nil { t.Errorf("%s: taken", name) diff --git a/go/delivery/signers.go b/go/delivery/signers.go index d98f430..04c86bf 100644 --- a/go/delivery/signers.go +++ b/go/delivery/signers.go @@ -24,9 +24,8 @@ import ( // serving key from its record), and says `signed` naming the caller, the kind, the node and the digest, never the key. // // A node trusts the vault's key through the bus grants alone (the lead's design limit, 2026-10-28): only the vault may -// write `signers`, the same trust the wants and the placement rest on. Until the controller's lease is handed away the -// node trusts the controller's key beside it; from then on the vault's alone (LeaseHandedAway). What a node trusts now -// it states in its record, NodeRecord.Trusts, so the gate before the lease write reads it from the bus. +// write `signers`, the same trust the wants and the placement rest on. A node trusts the vault's key alone since the +// controller ended (ADR 0590). What a node trusts now it states in its record, NodeRecord.Trusts. const ( // VaultModule is the module holding the seat mesh-vault, whose state `signers` is. @@ -54,12 +53,11 @@ const ( // KindAuthority is the vault's own authority certificate, which the vault signs for itself. KindAuthority = "authority" - // SignerController and SignerVault name the two signers a node may trust. - SignerController = "controller" - SignerVault = "vault" + // SignerVault names the signer a node trusts. + SignerVault = "vault" - // CertificateLife is how long a certificate the vault signs for a node lasts: the controller's ten years - // (identity.forever), renewed by asking again, never by a clock running out unseen. + // CertificateLife is how long a certificate the vault signs for a node lasts: ten years, renewed by asking again, + // never by a clock running out unseen. CertificateLife = 10 * 365 * 24 * time.Hour ) @@ -166,7 +164,7 @@ func DecodeSignerAuthority(raw []byte) (SignerAuthority, error) { return a, nil } -// Signer is one key a node trusts, by who holds it (SignerController, SignerVault, or the operator's). +// Signer is one key a node trusts, by who holds it (SignerVault, or the operator's). type Signer struct { Name string Key ed25519.PublicKey diff --git a/go/delivery/userlist.go b/go/delivery/userlist.go index ea27e77..be703a1 100644 --- a/go/delivery/userlist.go +++ b/go/delivery/userlist.go @@ -21,11 +21,10 @@ import ( // list's digest (UserListObjectName). // - It writes the key GrantsUserListKey of its state `user-list` (bucket GrantsUserListBucket): the GrantsUserList // below, which names the object, the list's digest, its revision, its user count and the users left out, and which -// source the bus node follows (Follow: FollowGrants from take-over, FollowController in shadow and after hand-back). -// - The node-engine of the bus node watches that key alone. While it says FollowGrants it opens the object, checks -// the digest, writes the bus's user-list file (mode 0600) only when the file's digest differs, and says what it -// loaded in its have of the bus module (UserListWritten, Source GrantsUserListSource); otherwise it follows the -// controller's compositions as before (clause 8). Never both. +// source the bus node follows (Follow: FollowGrants, the one source since the controller ended, ADR 0590). +// - The node-engine of the bus node watches that key alone. It opens the object, checks the digest, writes the bus's +// user-list file (mode 0600) only when the file's digest differs, and says what it loaded in its have of the bus +// module (UserListWritten, Source GrantsUserListSource). // The names of the grants part's user list on the bus. const ( @@ -41,16 +40,13 @@ const ( GrantsUserListSource = GrantsUserListBucket + "." + GrantsUserListKey ) -// Which list the bus node follows. -const ( - FollowController = "controller" - FollowGrants = "grants" -) +// FollowGrants is the list the bus node follows: the grants part's, the one source (ADR 0590). +const FollowGrants = "grants" // GrantsUserList is the value of the grants part's key `user-list.sealed`. type GrantsUserList struct { V int `json:"v"` - // Follow is which list the bus node follows: FollowGrants from take-over, FollowController otherwise. + // Follow is which list the bus node follows: FollowGrants, the one value it takes. Follow string `json:"follow"` // Digest is the list's digest, `sha256:` of the file's bytes as the bus loads them. Digest string `json:"digest"` @@ -65,10 +61,6 @@ type GrantsUserList struct { // LeftOut are the users the list leaves out for want of a hash in the vault. LeftOut []string `json:"left_out"` At time.Time `json:"at"` - // LeaseHanded says the controller's lease is handed to the grants part (novox/hq ADR 0582 clause 3, ADR 0571 clause - // 8): from then on the bus node follows this list alone and never falls back to the controller's. A key written - // before it reads false. - LeaseHanded bool `json:"lease-handed,omitempty"` } // EncodeGrantsUserList is g as JSON at Version; it refuses a value the bus node could not follow. @@ -96,8 +88,9 @@ func DecodeGrantsUserList(data []byte) (GrantsUserList, error) { } func (g GrantsUserList) check() error { - if g.Follow != FollowGrants && g.Follow != FollowController { - return fmt.Errorf("delivery: the user list's follow %q is not %s or %s", g.Follow, FollowGrants, FollowController) + if g.Follow != FollowGrants { + return fmt.Errorf("delivery: the user list's follow %q is not %s, the one list since the controller ended (ADR 0590)", + g.Follow, FollowGrants) } if g.Digest == "" { return nil // a switch with no list written yet: the bus node keeps what it follows diff --git a/go/handacts/handacts.go b/go/handacts/handacts.go index 26c1514..1288986 100644 --- a/go/handacts/handacts.go +++ b/go/handacts/handacts.go @@ -21,8 +21,6 @@ const ( WarrantsState = "warrants" // EventRecorded is said on the seat for every act, with the act as its body. EventRecorded = "act-recorded" - // ControllerActs is the controller's log as a reader names it, read until its lease ends. - ControllerActs = "mesh-controller.hand-acts" ) // KeptFor is how long an act is kept. @@ -63,15 +61,12 @@ type Act struct { RequestedBy string `json:"requested-by,omitempty"` // Outcome is what came of an act recorded after it was done. Outcome string `json:"outcome,omitempty"` - // Log is which log holds it, set when it is read (LogSeat or LogController); never written. + // Log is which log holds it, set when it is read (LogSeat); never written. Log string `json:"log,omitempty"` } -// The logs an act may be held in. -const ( - LogSeat = Seat - LogController = "mesh-controller" -) +// LogSeat is the log an act is held in: the seat's (the ended controller's log is read from its backup, ADR 0590). +const LogSeat = Seat // Repair says the act counts toward a cause repeated (S15): not a drill, not an operator's answer, not recorded builds. func (a Act) Repair() bool { @@ -99,9 +94,6 @@ type Taken struct { type Recorder struct { // Ask calls a seat verb by its address (`seat:.`): stdio.Ask through the runtime. Ask func(key string, body any) (json.RawMessage, error) - // LeaseAway is no longer read: nothing falls back to the controller. Kept so a caller that sets it builds; it goes - // once no module sets it. - LeaseAway func() bool } // NoResponder says err is the bus finding nobody to answer: the seat has no holder now. diff --git a/go/handacts/handacts_test.go b/go/handacts/handacts_test.go index 6ffb90d..11b7cea 100644 --- a/go/handacts/handacts_test.go +++ b/go/handacts/handacts_test.go @@ -31,7 +31,6 @@ func TestAnActIsRecordedInTheLogThatAnswers(t *testing.T) { name string seat error ctrl error - away bool log string refused string asked string @@ -47,7 +46,7 @@ func TestAnActIsRecordedInTheLogThatAnswers(t *testing.T) { {name: "a warrant recorded", warrants: true, log: LogSeat, asked: "seat:mesh-hand-acts.warranted"}, } { var asked []string - r := Recorder{LeaseAway: func() bool { return c.away }, Ask: func(key string, body any) (json.RawMessage, error) { + r := Recorder{Ask: func(key string, body any) (json.RawMessage, error) { asked = append(asked, key) if strings.HasPrefix(key, "seat:mesh-hand-acts.") { return json.RawMessage(`{}`), c.seat diff --git a/go/health/condition.go b/go/health/condition.go index 987cf1c..e5076b4 100644 --- a/go/health/condition.go +++ b/go/health/condition.go @@ -182,13 +182,9 @@ func (c Condition) Began() time.Time { return c.Raised } -// ShowKeeper is the verb that shows more about a condition the keeper says (ADR 0564 Limits 9); a condition carried from -// the controller's bucket keeps the controller's (ShowController) until the lease write. +// ShowKeeper is the verb that shows more about a condition the keeper says (ADR 0564 Limits 9). func ShowKeeper(key string) string { return "mesh-conditions.conditions key=" + key } -// ShowController is the controller's verb that shows more about a condition it raised. -func ShowController(key string) string { return "mesh-controller.conditions key=" + key } - // SilenceAction is the action that stops a condition's messages for a week, as the keeper offers it (ADR 0564 Limits 9: // `mesh-conditions.silence`). An acknowledgement (ADR 0234 §8); its cause marks it as an answer. func SilenceAction(key string) Action { diff --git a/go/health/condition_test.go b/go/health/condition_test.go index 066f6e3..f6decea 100644 --- a/go/health/condition_test.go +++ b/go/health/condition_test.go @@ -165,7 +165,7 @@ func TestTheHealthLayoutsHoldTheKeepersRules(t *testing.T) { t.Errorf("a finding under the old name read as %+v, %v", old, err) } if FindingsBucket(SelfCheckModule) != "mesh-self-check_findings" || Bucket(HealersModule, AttemptsState) != "mesh-healers_attempts" || - Bucket(KeeperModule, TakeoverState) != "mesh-conditions_takeover" { + KeeperSeat != KeeperModule || SelfCheckSeat != SelfCheckModule { t.Errorf("the buckets are named %s, %s", FindingsBucket(SelfCheckModule), Bucket(HealersModule, AttemptsState)) } @@ -174,37 +174,6 @@ func TestTheHealthLayoutsHoldTheKeepersRules(t *testing.T) { (Tried{At: at, What: "asserted the grants", Outcome: "escalated: budget spent", By: "healer H3"}) { t.Errorf("an attempt is tried as %+v", tr) } - - // The take-over key (clause 9): absent follows the controller; the keeper's names who chose; anything else refused. - for _, c := range []struct { - raw string - found bool - seat, events string - fails bool - }{ - {found: false, seat: "mesh-controller", events: "mesh-controller.doctor-heartbeat"}, - {raw: `{"v":1,"follow":"controller","by":"the operator"}`, found: true, seat: "mesh-controller", events: "mesh-controller.doctor-heartbeat"}, - {raw: `{"v":1,"follow":"keeper","by":"the operator","ask":"ask-1"}`, found: true, seat: "mesh-conditions", events: "mesh-self-check.checked"}, - {raw: `{"v":1,"follow":"keeper"}`, found: true, fails: true}, - {raw: `{"v":1,"follow":"both","by":"x"}`, found: true, fails: true}, - {raw: `{"v":2,"follow":"keeper","by":"x"}`, found: true, fails: true}, - } { - got, err := DecodeTakeover([]byte(c.raw), c.found) - if c.fails != (err != nil) { - t.Errorf("%q: read as %+v, %v", c.raw, got, err) - continue - } - if c.fails { - continue - } - seat, event := got.SelfCheckEvent() - if got.Seat() != c.seat || seat+"."+event != c.events { - t.Errorf("%q follows %s and %s.%s", c.raw, got.Seat(), seat, event) - } - if b, err := EncodeTakeover(got); err != nil || !strings.Contains(string(b), `"v":1`) { - t.Errorf("%q encodes as %s, %v", c.raw, b, err) - } - } } // credentialNamed is the first field of a JSON value whose name ends as mesh-tools' credentialEndings do, or "". diff --git a/go/health/findings.go b/go/health/findings.go index 00a5acd..2ae46c3 100644 --- a/go/health/findings.go +++ b/go/health/findings.go @@ -25,10 +25,6 @@ const ( FindingsState = "findings" // ConditionsState is the keeper's open conditions, one key each, without their looks (clause 3). ConditionsState = "conditions" - // TakeoverState is the keeper's take-over, one key TakeoverKey (clause 9). - TakeoverState = "takeover" - // TakeoverKey is the take-over state's one key. - TakeoverKey = "switch" // AttemptsState is the healers' acts begun, each created before it is made (clause 7); WouldState the acts they // would have taken while the lease names a controller (clause 8), which the keeper never reads. AttemptsState = "attempts" @@ -272,79 +268,10 @@ type Would struct { Why string `json:"why"` } -// Which source the readers of conditions follow (clause 9). +// The readers of conditions (the router, the grants part and mesh-watcher) hear the keeper's condition events on +// KeeperSeat and the self-check's run as `checked` on SelfCheckSeat: the controller's events and the take-over key that +// chose between the two sources ended with it (novox/hq ADR 0590 clause 3). const ( - FollowController = "controller" - FollowKeeper = "keeper" + KeeperSeat = "mesh-conditions" + SelfCheckSeat = "mesh-self-check" ) - -// TakeoverVersion is the version of the take-over key's value. -const TakeoverVersion = 1 - -// Takeover is the value of the keeper's key `takeover.switch` (clause 9): which source the router, the grants part and -// mesh-watcher hear, the controller's condition events and `doctor-heartbeat`, or the keeper's events and the -// self-check's `checked`. Written by the keeper alone, on the operator's warrant for a take-over and by `hand-back`. An -// absent key is FollowController. -type Takeover struct { - V int `json:"v"` - Follow string `json:"follow"` - At time.Time `json:"at"` - // By is who chose (the operator, as the warrant proves), and Why why; Ask the approve ask it was chosen on. - By string `json:"by"` - Why string `json:"why,omitempty"` - Ask string `json:"ask,omitempty"` -} - -// EncodeTakeover is t as JSON at TakeoverVersion; it refuses a source the readers could not follow. -func EncodeTakeover(t Takeover) ([]byte, error) { - t.V = TakeoverVersion - if err := t.check(); err != nil { - return nil, err - } - return json.Marshal(t) -} - -// DecodeTakeover reads the key as a getter answered it: an absent key follows the controller. -func DecodeTakeover(data []byte, found bool) (Takeover, error) { - if !found || len(data) == 0 { - return Takeover{V: TakeoverVersion, Follow: FollowController}, nil - } - var t Takeover - if err := json.Unmarshal(data, &t); err != nil { - return Takeover{}, fmt.Errorf("health: the take-over key is not readable: %w", err) - } - if t.V != TakeoverVersion { - return Takeover{}, fmt.Errorf("health: the take-over key is of version %d, and this reads %d", t.V, TakeoverVersion) - } - if err := t.check(); err != nil { - return Takeover{}, err - } - return t, nil -} - -func (t Takeover) check() error { - if t.Follow != FollowController && t.Follow != FollowKeeper { - return fmt.Errorf("health: the take-over follows %q, not %s or %s", t.Follow, FollowController, FollowKeeper) - } - if t.Follow == FollowKeeper && strings.TrimSpace(t.By) == "" { - return fmt.Errorf("health: a take-over by the keeper names who chose it") - } - return nil -} - -// Seat is the seat a reader hears condition events on while it follows t: mesh-controller or mesh-conditions. -func (t Takeover) Seat() string { - if t.Follow == FollowKeeper { - return "mesh-conditions" - } - return "mesh-controller" -} - -// SelfCheckEvent is the seat and event a reader of the self-check's run hears while it follows t: the controller's -// `doctor-heartbeat`, or mesh-self-check's `checked`. -func (t Takeover) SelfCheckEvent() (seat, event string) { - if t.Follow == FollowKeeper { - return "mesh-self-check", EventChecked - } - return "mesh-controller", "doctor-heartbeat" -} diff --git a/go/process/call.go b/go/process/call.go index 95cce50..6579ea7 100644 --- a/go/process/call.go +++ b/go/process/call.go @@ -11,6 +11,7 @@ import ( "github.com/nats-io/nats.go" "git.novox.be/novox/mesh-sdk/go/busretry" + "git.novox.be/novox/mesh-sdk/go/calladdress" ) // CallTimeout bounds a call to a tool or a seat verb (mesh-tools node-tools/internal/bus/bus.go:35, @@ -24,9 +25,10 @@ var CallTimeout = 30 * time.Second // verb, or "." for a module's own tool, as a process's bus.invokes names it. The reply is // {result, error, node} (mesh-tools node-tools/internal/bus/bus.go answerOf); a non-empty error is the // call's failure. It offers no way to answer a call — a process reaches the bus only as itself, and -// never serves anything. +// never serves anything. A key naming its machine after '@' (`seat:node-service-manager.stop@novox`) is CallOn of that +// machine; any other malformed key is refused before anything is published (calladdress; novox/hq issue 774). func (c *Conn) Call(key string, body any) (json.RawMessage, error) { - called, fallback, err := callSubjects(key, c.callerToken()) + called, fallback, err := callSubjectsOn(key, "", c.callerToken()) if err != nil { return nil, err } @@ -39,7 +41,7 @@ func (c *Conn) Call(key string, body any) (json.RawMessage, error) { // (mesh-tools node-tools/internal/bus/caller.go:15-19). node is one subject token: an empty one, or one // holding '.', '*', '>' or white space, is refused before anything is published. func (c *Conn) CallOn(node, key string, body any) (json.RawMessage, error) { - if err := checkNode(node); err != nil { + if err := calladdress.CheckNode(node); err != nil { return nil, err } called, fallback, err := callSubjectsOn(key, node, c.callerToken()) @@ -99,33 +101,28 @@ func callSubjects(key, token string) (called, fallback string, err error) { return callSubjectsOn(key, "", token) } -// checkNode refuses a node name that is not one subject token. -func checkNode(node string) error { - if node == "" || strings.ContainsAny(node, ".*> \t\r\n") { - return fmt.Errorf("%q is not a node name (one subject token: no '.', '*', '>' or space)", node) - } - return nil -} - -// callSubjectsOn is callSubjects for one named node ("" names none): the node is a token between the tool and -// the caller on the called subject, and the last one of the fallback. +// callSubjectsOn is callSubjects for one named node ("" names none, and then the key's own '@' names it): the node is a +// token between the tool and the caller on the called subject, and the last one of the fallback. A key naming another +// machine than node is refused. func callSubjectsOn(key, node, token string) (called, fallback string, err error) { + a, err := calladdress.Parse(key) + if err != nil { + return "", "", err + } + switch { + case a.Node != "" && node != "" && a.Node != node: + return "", "", fmt.Errorf("%q names the machine %s, and it is asked of %s", key, a.Node, node) + case a.Node != "": + node = a.Node + } at := "" if node != "" { at = "." + node } - if rest, ok := strings.CutPrefix(key, "seat:"); ok { - seat, verb, ok := strings.Cut(rest, ".") - if !ok || seat == "" || verb == "" { - return "", "", fmt.Errorf("%q names no seat and verb (seat:.)", key) - } - return "mesh.seat." + seat + ".call." + verb + at + "." + token, "mesh.seat." + seat + ".tool." + verb + at, nil + if a.Seat != "" { + return "mesh.seat." + a.Seat + ".call." + a.Verb + at + "." + token, "mesh.seat." + a.Seat + ".tool." + a.Verb + at, nil } - module, tool, ok := strings.Cut(key, ".") - if !ok || module == "" || tool == "" { - return "", "", fmt.Errorf("%q is not ., and does not start seat:.", key) - } - return "mesh.mod." + module + ".call." + tool + at + "." + token, "mesh.mod." + module + ".tool." + tool + at, nil + return "mesh.mod." + a.Module + ".call." + a.Tool + at + "." + token, "mesh.mod." + a.Module + ".tool." + a.Tool + at, nil } // request asks one subject once and reads its reply ({result, error, node}); a reply naming an error is diff --git a/go/process/call_integration_test.go b/go/process/call_integration_test.go index 708e38f..09345a4 100644 --- a/go/process/call_integration_test.go +++ b/go/process/call_integration_test.go @@ -237,7 +237,8 @@ func TestReleaseAcceptanceAProcessDoesExactlyItsBusDeclaration(t *testing.T) { } // 1470/AC4: two answerers of the same verb on the subjects of nodes ace and g14; CallOn naming g14 gets g14's -// answer, and ace's answerer hears nothing. +// answer, and ace's answerer hears nothing. Issue 774 notes 84-87: Call with the key `@g14` is published on the +// same subject, never on one holding the '@' that nobody serves. func TestCallOnReachesTheNamedNodeAlone(t *testing.T) { url, account := grantedSendProcess(t) adminNC, _ := adminJS(t, url) @@ -271,6 +272,9 @@ func TestCallOnReachesTheNamedNodeAlone(t *testing.T) { if string(result) != `{"node":"g14"}` { t.Fatalf("result: %s, want g14's answer", result) } + if result, err = conn.Call("seat:operator-channel.tell@g14", map[string]any{}); err != nil || string(result) != `{"node":"g14"}` { + t.Fatalf("Call with @g14: %s, %v, want g14's answer", result, err) + } select { case <-aceHeard: t.Error("ace's answerer heard a call named for g14") diff --git a/go/process/call_test.go b/go/process/call_test.go index a892874..e56ed26 100644 --- a/go/process/call_test.go +++ b/go/process/call_test.go @@ -73,17 +73,31 @@ func TestCallSubjectsOnANodeForASeatVerb(t *testing.T) { if called != "mesh.seat.node-files.call.read.g14.tok" || fallback != "mesh.seat.node-files.tool.read.g14" { t.Errorf("called %q, fallback %q", called, fallback) } + // Issue 774 notes 84-87: the machine named in the key after '@' is the same subject, never a token holding '@'. + called, fallback, err = callSubjects("seat:node-files.read@g14", "tok") + if err != nil || called != "mesh.seat.node-files.call.read.g14.tok" || fallback != "mesh.seat.node-files.tool.read.g14" { + t.Errorf("@g14: called %q, fallback %q, %v", called, fallback, err) + } + if _, _, err := callSubjectsOn("seat:node-files.read@ace", "g14", "tok"); err == nil { + t.Error("a key naming ace was asked of g14") + } } // 1470/AC3: a node that is not one token is refused before anything is published (the Conn has no // connection, so only the name check can answer with this error). func TestCallOnRefusesANodeThatIsNotOneToken(t *testing.T) { c := &Conn{} - for _, node := range []string{"", "a.b", "*", ">", "a b", "a\tb"} { + for _, node := range []string{"", "a.b", "*", ">", "a b", "a\tb", "a@b"} { if _, err := c.CallOn(node, "mesh-build-go.fetch", nil); err == nil || !strings.Contains(err.Error(), "not a node name") { t.Errorf("CallOn(%q) = %v, want the refusal of the name", node, err) } } + // Issue 774 notes 84-87: Call refuses a malformed key the same way, before anything is published. + for _, key := range []string{"seat:node-service-manager.stop@novox-be.lan", "seat:a.b@x@y", "seat:a.*", "a.b@"} { + if _, err := c.Call(key, nil); err == nil { + t.Errorf("Call(%q) was published", key) + } + } } // The review of 2026-10-30 (issue 774): a callee's own answer is delivered, whatever its words, and is never asked diff --git a/go/provider/provider.go b/go/provider/provider.go index df315a5..8871ca0 100644 --- a/go/provider/provider.go +++ b/go/provider/provider.go @@ -50,10 +50,6 @@ import ( "git.novox.be/novox/mesh-sdk/go/provisions" ) -// AssignmentsBucket is the controller's assignments mirror, `.`, whose value carries the plan of a -// module handed to the provisioner. -const AssignmentsBucket = "mesh-controller_assignments" - // BucketOf is a providing module's own bucket of entries, one per consumer: `_bound`, the module's state // `bound`. func BucketOf(module string) string { return module + "_bound" } @@ -374,6 +370,31 @@ func (p *Provider) passIn(ctx context.Context, in Inputs) ([]Outcome, error) { gated[key] = pr } } + // **A key no plan names is bound from the entry it carries** (issue 1126): the seat keys a consumer's provisions + // from its wanted and its newest builds, and keeps the plan at its wanted build alone, so a requirement the newest + // build adds has a key before any plan names it, and the want that waits for it waits on this provider. The plan + // wins where both say it. A key with no entry is said, never left without a word. + planned := map[string]bool{} + for _, b := range binds { + planned[b.Key()] = true + } + for _, key := range sortedKeys(gated) { + pr := gated[key] + if planned[key] || pr.State == provisions.Retiring || unknown[consumerOf(key)] { + continue + } + if pr.Plan == nil { + o := Outcome{Key: key, Status: Waiting, Why: "asked, but no plan names it and its key carries no plan entry, " + + "so whom it binds is not known; mesh-provisioner composes the entry"} + out = append(out, o) + p.say(key, o.Status+" "+o.Why) + continue + } + b := *pr.Plan + binds = append(binds, Entry{Provider: p.opts.Node, ProviderModule: p.opts.Module, Provision: pr.Provision, + Consumer: pr.Consumer.Node, Module: pr.Consumer.Module, Local: pr.Local, Contributes: b.Contributes, At: b.At, + From: b.From, As: b.As, Names: b.Names, CredentialForm: b.CredentialForm, Name: b.Name}) + } want := map[string]bool{} for _, b := range binds { key := b.Key() diff --git a/go/provider/provider_test.go b/go/provider/provider_test.go index eec9d1b..da8d7f4 100644 --- a/go/provider/provider_test.go +++ b/go/provider/provider_test.go @@ -955,3 +955,48 @@ func TestTheSeatsKeysSayWhenAProviderActsAndWhenItLetsGo(t *testing.T) { t.Errorf("gone %v, made %v: let go once, on the warrant, and made from the vault's value", m.gone, m.made) } } + +// Issue 1126 review 1 (mcp-endpoint asked on ace and shanks, 2026-11-02 11:56): a consumer's key composed from its newest +// build carries the plan entry it was composed from, though its wanted build's plan names no such provision; the +// provider binds it from the key and provides it, so the want that waits for it moves. A key of this provider that +// carries no entry and no plan names is said, never bound silently or left without a word. +func TestAKeyNoPlanNamesIsBoundFromTheEntryItCarries(t *testing.T) { + bus, m := newStandIn(), &maker{} + v := &vault{values: map[string]string{"pair/novox/postgres-database/ace/letta": "letta-secret-1"}} + // letta's wanted plan names nothing of postgres; its key, from its newest build, carries the entry. + planned(t, bus, "ace", "letta") + entry := catalogue.PlanProvision{Provision: "postgres-database", Provider: "novox", ProviderModule: "postgres", As: "mesh_ace_letta"} + pr := provisions.Provision{Provision: "postgres-database", Consumer: provisions.End{Module: "letta", Node: "ace"}, + Provider: provisions.End{Module: "postgres", Node: "novox"}, State: provisions.Asked, Plan: &entry} + raw, err := provisions.Encode(pr) + if err != nil { + t.Fatal(err) + } + _ = bus.Put(context.Background(), provisions.ProvisionsBucket, "ace.letta.postgres-database", raw) + // And umami's key carries none: asked, but no plan names it. + bare := provisions.Provision{Provision: "postgres-database", Consumer: provisions.End{Module: "umami", Node: "g14"}, + Provider: provisions.End{Module: "postgres", Node: "novox"}, State: provisions.Asked} + raw, _ = provisions.Encode(bare) + _ = bus.Put(context.Background(), provisions.ProvisionsBucket, "g14.umami.postgres-database", raw) + + out, err := postgres(bus, v, m).Pass(context.Background()) + if err != nil { + t.Fatal(err) + } + if e := entryOf(t, bus, "ace.letta.postgres-database"); e.Consumer != "ace" || e.Module != "letta" || e.Refused != "" { + t.Errorf("the entry %+v", e) + } + said := map[string]Outcome{} + for _, o := range out { + said[o.Key] = o + } + if said["ace.letta.postgres-database"].Status != Answered { + t.Errorf("letta: %+v", said["ace.letta.postgres-database"]) + } + if o := said["g14.umami.postgres-database"]; o.Status != Waiting || !strings.Contains(o.Why, "no plan names it") { + t.Errorf("umami: %+v", o) + } + if bus.get(BucketOf("postgres"), "g14.umami.postgres-database") != nil { + t.Error("a key with no entry and no plan was bound") + } +} diff --git a/go/provider/statebus_test.go b/go/provider/statebus_test.go index 1e5f391..7e20dff 100644 --- a/go/provider/statebus_test.go +++ b/go/provider/statebus_test.go @@ -99,7 +99,7 @@ func TestStateBusMapsTheProvidersBucketsToTheRuntimesStates(t *testing.T) { rt.send(map[string]any{"jsonrpc": "2.0", "id": d["id"], "result": map[string]any{}}) rt.next() - for _, bucket := range []string{"other_bound", "elsewhere", AssignmentsBucket} { + for _, bucket := range []string{"other_bound", "elsewhere", "mesh-controller_assignments"} { if err := bus.Watch(context.Background(), bucket, func(string, []byte) {}); err == nil || !strings.Contains(err.Error(), "not a bucket") { t.Fatalf("a watch of %s gave %v", bucket, err) } diff --git a/go/provisions/provisions.go b/go/provisions/provisions.go index 376b9bc..3889dc3 100644 --- a/go/provisions/provisions.go +++ b/go/provisions/provisions.go @@ -214,9 +214,13 @@ type Provision struct { Why string `json:"why,omitempty"` Reference *Reference `json:"reference,omitempty"` // Ask is a retiring provision's ask of the operator; Warrant the answer, once given. - Ask string `json:"ask,omitempty"` - Warrant *Warrant `json:"warrant,omitempty"` - At time.Time `json:"at"` + Ask string `json:"ask,omitempty"` + Warrant *Warrant `json:"warrant,omitempty"` + // Plan is the plan entry the key was composed from, at the consumer's wanted build or its newest (issue 1126): the + // provider binds the consumer from it, so a requirement the newest build adds is provided before the want that + // waits for it moves, though the plan the seat keeps at the wanted build names it not yet. + Plan *catalogue.PlanProvision `json:"plan,omitempty"` + At time.Time `json:"at"` } // Key is the provision's key. @@ -260,12 +264,12 @@ func Encode(p Provision) ([]byte, error) { return json.Marshal(p) } -// Decode reads a key's value strictly: a field the layout does not name is refused, so no value, sealed or not, rides in -// a provision (clause 1, ADR 0201 §13). +// Decode reads a key's value. **A field the layout does not name is read past and dropped** (issue 1126 review 1): a +// reader on an older mesh-sdk must not refuse a key a newer writer adds a field to. No value rides in a provision all +// the same (clause 1, ADR 0201 §13): Encode writes the layout alone, so what a reader does not know is never kept. func Decode(data []byte) (Provision, error) { var p Provision dec := json.NewDecoder(bytes.NewReader(data)) - dec.DisallowUnknownFields() if err := dec.Decode(&p); err != nil { return Provision{}, fmt.Errorf("provisions: a provision is not readable: %w", err) } diff --git a/go/provisions/provisions_test.go b/go/provisions/provisions_test.go index a5c49ad..f9e5ab8 100644 --- a/go/provisions/provisions_test.go +++ b/go/provisions/provisions_test.go @@ -56,10 +56,16 @@ func TestAProvisionIsAReferenceUnderItsConsumersKey(t *testing.T) { t.Errorf("%s: %s parses as %s %s %s %s, %v", c.name, key, node, consumer, provision, local, err) } } - // A value never rides in a provision: a field the layout does not name is refused. - if _, err := Decode([]byte(`{"v":1,"provision":"p","consumer":{"module":"c","node":"n"},"provider":{"module":"p","node":"n"},` + - `"state":"provided","at":"2026-10-29T22:00:00Z","password":"hunter2"}`)); err == nil { - t.Error("a provision carrying a password was read") + // A field a later layout adds is read past, never kept (issue 1126 review 1: a reader on an older mesh-sdk must not + // refuse a key a newer writer adds a field to). A value still never rides in a provision: Encode writes the layout + // alone, so what a reader does not know is dropped, and encoding it again carries none of it. + later, err := Decode([]byte(`{"v":1,"provision":"p","consumer":{"module":"c","node":"n"},"provider":{"module":"p","node":"n"},` + + `"state":"provided","at":"2026-10-29T22:00:00Z","a-later-field":{"x":1},"password":"hunter2"}`)) + if err != nil || later.Provision != "p" || later.State != Provided { + t.Errorf("a provision with a field of a later layout: %+v, %v", later, err) + } + if again, _ := Encode(later); strings.Contains(string(again), "hunter2") || strings.Contains(string(again), "a-later-field") { + t.Errorf("a field the layout does not name was kept: %s", again) } plan := Plan{Node: "novox", Module: "restic", Plan: catalogue.Plan{}} raw, err := EncodePlan(plan) diff --git a/go/stdio.go b/go/stdio.go index 71ab95b..62d0053 100644 --- a/go/stdio.go +++ b/go/stdio.go @@ -24,6 +24,8 @@ import ( "strings" "sync" "time" + + "git.novox.be/novox/mesh-sdk/go/calladdress" ) // Protocol is the MCP version this speaks; the runtime says the same. @@ -471,8 +473,12 @@ func Subscribe(pattern string, handle func(Envelope) error) error { } // Ask calls a tool on the mesh as this bundle's module, through the runtime (ADR 0198), and returns -// its result as JSON. The key is `.` as the runtime calls it. +// its result as JSON. The key is `.` or `seat:.`, with `@` for one machine's, as the +// runtime calls it; a malformed key is refused here, before the runtime is asked (calladdress; novox/hq issue 774). func Ask(key string, body any) (json.RawMessage, error) { + if err := calladdress.CheckOwn(key); err != nil { + return nil, err + } c, err := live() if err != nil { return nil, err diff --git a/go/stdio_test.go b/go/stdio_test.go index e882534..29a64db 100644 --- a/go/stdio_test.go +++ b/go/stdio_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "io" + "strings" "sync" "testing" "time" @@ -160,6 +161,12 @@ func TestABundleSubscribesAndAsksThroughTheRuntime(t *testing.T) { if !TopicMatches("a.#", "a.b.c") || TopicMatches("a.*", "a.b.c") || !TopicMatches("#", "x") { t.Error("topic matching differs from the TypeScript SDK's") } + // Issue 774 notes 84-87: a malformed key is refused before the runtime is asked. + for _, key := range []string{"seat:node-service-manager.stop@novox@g14", "seat:a.*", "postgres.list@", "postgres.list@a.b"} { + if _, err := Ask(key, nil); err == nil || strings.Contains(err.Error(), "Serve") { + t.Errorf("Ask(%q) = %v, want the key refused", key, err) + } + } } // A subscribe the runtime refused leaves no handler behind: asked again until the runtime takes it, a