// Package grants is the grants part in shadow (novox/hq ADR 0547 clauses 5 to 8, issue 1945): it composes the bus's user // list, each module's membership and each manifest's buckets, logs and streams from what the bus holds, with the bus // composer the controller uses (mesh-sdk buscompose), and compares them with what the bus loaded and holds. Beyond its own // state it takes effect in two things alone, each by a grant the operator allowed: it publishes memberships (2026-10-25 // 22:31), and creates a bucket, log or stream a manifest declares that the bus lacks (2026-10-26 08:54, issue 1790). It // changes no stream the bus holds, writes no grant and no user list the bus node reads, until the operator's take-over, // which the operator asks on their channel and the part runs on their yes, after its refusals. package grants import ( "crypto/sha256" "encoding/json" "fmt" "slices" "sort" "strings" "sync" "time" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/overlay" ) // The states of other parts the grants part reads, as its manifest's reads name them (`.`). const ( ReadAssignments = "mesh-controller.assignments" ReadControllerGrant = "mesh-controller.grants" ReadModules = "mesh-delivery-indexer.modules" ReadManifests = "mesh-delivery-indexer.manifests" ReadHave = "node-engine.have" ReadResolved = "mesh-config.resolved" ReadWants = "mesh-delivery-assigner.wants" ReadBuilds = "mesh-delivery-builder.builds" // ReadAccounts is the vault's bcrypt hashes of the bus accounts it holds, by bus user (ADR 0547 clause 9). ReadAccounts = "mesh-vault.bus-accounts" ) // Reads is every state the part reads, in the manifest's order. var Reads = []string{ReadAssignments, ReadControllerGrant, ReadModules, ReadManifests, ReadResolved, ReadHave, ReadWants, ReadBuilds, ReadAccounts} // The bus module's tools the part calls, as its manifest's invokes name them: read-only, each. const ( CallUsers = "nats.nats_users" CallUserCan = "nats.nats_user_can" CallStreams = "nats.nats_streams" ) // Invokes is every tool the part calls. var Invokes = []string{CallUsers, CallUserCan, CallStreams, CallRootFree} // Reader reads another part's state, named `.`. A refusal (a read not granted yet, a bucket not there) is // an error the part names as a gap and composes past: it never stops for one. type Reader interface { Keys(state string) ([]string, error) Get(state, key string) ([]byte, bool, error) } // Caller calls another module's tool, `.`, and answers its result. type Caller interface { Call(key string, body any) (json.RawMessage, error) } // Gap is an input the shadow cannot read from the bus, named with why: never guessed. type Gap struct { Input string `json:"input"` Why string `json:"why"` // Failed says the gap is a read that failed this pass, not one that stands (ADR 0547 clause 7: after take-over a // list composed past a failed read is not sealed). Failed bool `json:"failed,omitempty"` } // Standing gaps: what the controller composes from its own store, which no bus value carries in this milestone. var standingGaps = []Gap{ {Input: "enrolment tokens", Why: "the live enrolment tokens are in the controller's store alone: no enrolment user is composed"}, {Input: "people", Why: "the people and the tools they may invoke are in the controller's store alone: no person user is composed"}, {Input: "seat holdings", Why: "who holds each seat held once for the mesh is in the controller's store alone: a mesh seat claimed on two machines is composed as held on both"}, {Input: "announced states", Why: "the controller does not announce its states-announced readable, so every read is composed from `reads`, as when announcements cannot be read"}, } // assignment is a key of the controller's assignments as composing needs it (mesh-controller internal/chain Assignment): // a value that names no time assigned (a module pulled in), has ended, or waits in a state, is no assignment. type assignment struct { Node string `json:"node"` Module string `json:"module"` Assigned time.Time `json:"assigned,omitzero"` Ended time.Time `json:"ended,omitzero"` State string `json:"state,omitempty"` } func (a assignment) stands() bool { return !a.Assigned.IsZero() && a.Ended.IsZero() && a.State == "" } // resolved is a key of mesh-config's resolved values, `..`. type resolved struct { Node string `json:"node"` Module string `json:"module"` Key string `json:"key"` Value json.RawMessage `json:"value,omitempty"` } // Inputs is what the shadow composes from, read from the bus. type Inputs struct { // Nodes is every machine seen: in a node agent's have or a standing assignment. Nodes []string // Assigned is each machine's standing assignments, sorted. Assigned map[string][]string // Manifests is each module's manifest at the indexer's trunk, by module. Manifests map[string]catalogue.Manifest // Digests is each module's manifest digest, by module. Digests map[string]string // Running is the manifest a machine is wanted to run, by `.`: the one the builder built at the // fingerprint the assigner's want names. An assignment with none is composed from the trunk's manifest. Running map[string]catalogue.Manifest // Values is each resolved config value, by `//`. Values map[string]any // BusNode is the machine running the bus's module, and Loaded what its node agent says it loaded. BusNode string Loaded *delivery.UserListWritten // Hashes are the vault's hashes of the bus accounts, by bus user (ReadAccounts); kept in memory, never written. Hashes map[string]HeldHash // Refused is every manifest the indexer holds that the part cannot read or mesh-sdk's reader refuses, assigned or // not, each with why: its module counts as placed nowhere, so it is said loudly, never passed over (issue 1945). Refused []Gap Gaps []Gap } // HeldHash is one key of the vault's bus-accounts: the hash, and what it is of. The hash is never said or written by // the part in shadow. type HeldHash struct { Hash string `json:"hash"` Generation int `json:"generation,omitempty"` At time.Time `json:"at"` Adopted bool `json:"adopted,omitempty"` } // readHashes reads the vault's bus-accounts; a refusal is a gap, and every user then has no hash held. func readHashes(r Reader, gaps *[]Gap) map[string]HeldHash { out := map[string]HeldHash{} for user, raw := range each(r, ReadAccounts, gaps) { var h HeldHash if json.Unmarshal(raw, &h) != nil || h.Hash == "" { *gaps = append(*gaps, Gap{Input: ReadAccounts + " " + user, Why: "holds no hash"}) continue } out[user] = h } return out } // BusModule is the bus's module: it holds mesh-broker. const BusModule = "nats" // Manifests caches manifests by digest: a digest names one manifest for ever. type Manifests struct { mu sync.Mutex parsed map[string]catalogue.Manifest } func (c *Manifests) get(r Reader, digest string) (catalogue.Manifest, error) { // The lock guards the map alone, never a read on the bus: a tool never waits on a pass's round trip. c.mu.Lock() m, ok := c.parsed[digest] c.mu.Unlock() if ok { return m, nil } raw, found, err := r.Get(ReadManifests, digest) if err != nil { return catalogue.Manifest{}, err } if !found { return catalogue.Manifest{}, fmt.Errorf("the indexer holds no manifest %s", digest) } v, err := delivery.DecodeManifest(raw) if err != nil { return catalogue.Manifest{}, err } // Read for its inputs, as every delivery part reads a manifest (issue 1751): the merge gate judged its config values, // and the part reads its seats, buckets, logs and streams, never a value; so a value falling back to a predefined key // is taken as it declares itself, where ParseManifest refused it with no list and placed its module nowhere (issue 1945). m, err = catalogue.ParseManifestForInputs([]byte(v.JSON)) if err != nil { return catalogue.Manifest{}, fmt.Errorf("the manifest %s is refused by mesh-sdk's reader: %w", digest, err) } c.mu.Lock() defer c.mu.Unlock() if c.parsed == nil { c.parsed = map[string]catalogue.Manifest{} } c.parsed[digest] = m return m, nil } // each reads every current value of a state; a refusal is a gap and an empty map. func each(r Reader, state string, gaps *[]Gap) map[string][]byte { keys, err := r.Keys(state) if err != nil { *gaps = append(*gaps, Gap{Input: state, Why: "refused or absent: " + err.Error(), Failed: true}) return map[string][]byte{} } out := map[string][]byte{} for _, k := range keys { v, found, err := r.Get(state, k) if err != nil { *gaps = append(*gaps, Gap{Input: state + " " + k, Why: err.Error(), Failed: true}) continue } if found { out[k] = v } } return out } // ReadInputs reads what the shadow composes from. It never fails: what it cannot read is a gap. func ReadInputs(r Reader, cache *Manifests) Inputs { in := Inputs{Assigned: map[string][]string{}, Manifests: map[string]catalogue.Manifest{}, Digests: map[string]string{}, Running: map[string]catalogue.Manifest{}, Values: map[string]any{}, Gaps: append([]Gap(nil), standingGaps...)} nodes := map[string]bool{} for _, raw := range each(r, ReadAssignments, &in.Gaps) { var a assignment if json.Unmarshal(raw, &a) != nil || !a.stands() || a.Node == "" || a.Module == "" { continue } nodes[a.Node] = true in.Assigned[a.Node] = append(in.Assigned[a.Node], a.Module) if a.Module == BusModule { in.BusNode = a.Node } } haveKeys, err := r.Keys(ReadHave) if err != nil { in.Gaps = append(in.Gaps, Gap{Input: ReadHave, Why: "refused or absent: " + err.Error(), Failed: true}) } for _, k := range haveKeys { if node, _, isNode, err := delivery.ParseHaveKey(k); err == nil && isNode { nodes[node] = true } } in.Gaps = append(in.Gaps, Gap{Input: "machines", Why: "read from the node agents' haves and the assignments: a machine with neither is not seen"}) for n := range nodes { in.Nodes = append(in.Nodes, n) } sort.Strings(in.Nodes) if in.BusNode != "" { if raw, found, err := r.Get(ReadHave, in.BusNode+"."+BusModule); err != nil { in.Gaps = append(in.Gaps, Gap{Input: "the loaded list's have", Why: err.Error(), Failed: true}) } else if found { if h, err := delivery.DecodeHave(raw); err == nil { in.Loaded = h.UserList } } } wanted := map[string]bool{} for node := range in.Assigned { sort.Strings(in.Assigned[node]) for _, m := range in.Assigned[node] { wanted[m] = true } } modules := make([]string, 0, len(wanted)) for m := range wanted { modules = append(modules, m) } sort.Strings(modules) // The seats every module declares, across the whole catalogue: read every module the indexer holds, not only the // assigned ones, since a seat is declared by one module and held by another. indexed, err := r.Keys(ReadModules) if err != nil { in.Gaps = append(in.Gaps, Gap{Input: ReadModules, Why: "refused or absent: " + err.Error(), Failed: true}) } for _, module := range indexed { raw, found, err := r.Get(ReadModules, module) if err != nil { in.Gaps = append(in.Gaps, Gap{Input: ReadModules + " " + module, Why: err.Error(), Failed: true}) continue } if !found { continue } v, err := delivery.DecodeModule(raw) if err == nil && v.Manifest == "" { err = fmt.Errorf("the indexer's value names no manifest digest") } var m catalogue.Manifest if err == nil { m, err = cache.get(r, v.Manifest) } if err != nil { // Every manifest, assigned or not: one refused declares no seat, so its holders compose as placed nowhere. g := Gap{Input: "manifest of " + module, Why: err.Error()} in.Refused = append(in.Refused, g) in.Gaps = append(in.Gaps, g) continue } in.Manifests[module] = m in.Digests[module] = v.Manifest } // A module the mesh provides itself has no repository the indexer reads: its manifest comes with mesh-sdk, and the // controller's Provide overwrites any row of it at every start, so the provided one always wins here too (issue 1945, // the private network on every machine). An indexer manifest of the same name is said as a gap, never used. if slices.Contains(modules, overlay.Name) { raw, err := json.Marshal(overlay.Manifest()) var m catalogue.Manifest if err == nil { m, err = catalogue.ParseManifestForInputs(raw) } in.Gaps = slices.DeleteFunc(in.Gaps, func(g Gap) bool { return g.Input == "manifest of "+overlay.Name }) in.Refused = slices.DeleteFunc(in.Refused, func(g Gap) bool { return g.Input == "manifest of "+overlay.Name }) if _, held := in.Digests[overlay.Name]; held { in.Gaps = append(in.Gaps, Gap{Input: "manifest of " + overlay.Name, Why: "the indexer also holds one, which is not used: the mesh provides this module's manifest itself, and the controller overwrites any row of it"}) } if err != nil { g := Gap{Input: "manifest of " + overlay.Name, Why: "the manifest mesh-sdk provides is refused by its own reader: " + err.Error()} in.Refused = append(in.Refused, g) in.Gaps = append(in.Gaps, g) delete(in.Manifests, overlay.Name) delete(in.Digests, overlay.Name) } else { in.Manifests[overlay.Name] = m in.Digests[overlay.Name] = fmt.Sprintf("provided:sha256:%x", sha256.Sum256(raw)) } } for _, module := range modules { if _, ok := in.Manifests[module]; !ok && !gapNamed(in.Gaps, "manifest of "+module) { in.Gaps = append(in.Gaps, Gap{Input: "manifest of " + module, Why: "assigned, and the indexer holds no manifest of it at the trunk (a module registered from outside the indexed repositories): composed with no authority"}) } } // What each machine is wanted to run: the want's fingerprint, the builder's build of it, the manifest it built. var fromTrunk []string for key, raw := range each(r, ReadWants, &in.Gaps) { node, module, err := delivery.ParseWantKey(key) if err != nil || !slices.Contains(in.Assigned[node], module) { continue } w, err := delivery.DecodeWant(raw) if err != nil { fromTrunk = append(fromTrunk, node+"."+module) continue } if w.Removed || w.FP == "" { continue } bk, err := delivery.BuilderBuildKey(module, w.FP) if err != nil { continue } braw, found, err := r.Get(ReadBuilds, bk) if err != nil || !found { fromTrunk = append(fromTrunk, node+"."+module) continue } b, err := delivery.DecodeBuilderBuild(braw) if err != nil || b.Manifest == "" { fromTrunk = append(fromTrunk, node+"."+module) continue } if m, err := cache.get(r, b.Manifest); err == nil { in.Running[node+"."+module] = m } else { fromTrunk = append(fromTrunk, node+"."+module) } } if len(fromTrunk) > 0 { sort.Strings(fromTrunk) in.Gaps = append(in.Gaps, Gap{Input: "wanted manifests", Why: "no build record names the manifest at the wanted fingerprint, so the trunk's is used: " + strings.Join(fromTrunk, ", ")}) } in.Hashes = readHashes(r, &in.Gaps) for _, raw := range each(r, ReadResolved, &in.Gaps) { var v resolved if json.Unmarshal(raw, &v) != nil || v.Node == "" || v.Module == "" || v.Key == "" || len(v.Value) == 0 { continue } var value any if json.Unmarshal(v.Value, &value) == nil { in.Values[v.Node+"/"+v.Module+"/"+v.Key] = value } } sortGaps(in.Gaps) return in } func gapNamed(gaps []Gap, input string) bool { for _, g := range gaps { if g.Input == input { return true } } return false } func sortGaps(g []Gap) { sort.SliceStable(g, func(i, j int) bool { return strings.Compare(g[i].Input, g[j].Input) < 0 }) } // ReadFailures names every read this pass could not make and every manifest refused: what a list composed now may lack. func (in Inputs) ReadFailures() []string { var out []string for _, g := range in.Gaps { if g.Failed { out = append(out, g.Input+": "+g.Why) } } for _, g := range in.Refused { out = append(out, g.Input+": "+g.Why) } return out }