Files
mesh-delivery/modules/mesh-grants/internal/grants/inputs.go
T
jschoubben d4aa1fe11b Hear the keeper alone and stop reading the controller's lease in mesh-grants (issue 1126, B7b)
ADR 0590 §3 ends the choice of a condition source by the keeper's
take-over key: the controller is unassigned and the keeper is the one
source. The part still read the take-over key, fell back to the
controller's condition events when it was absent, and read and watched
the controller's lease, which nothing writes any more. It now hears the
keeper's events alone and reads neither.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 11:27:19 +01:00

567 lines
22 KiB
Go

// Package grants is the grants part (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
// (mesh-sdk buscompose), and compares them with what the bus loaded and holds. It publishes memberships (2026-10-25
// 22:31), creates a bucket, log or stream a manifest declares that the bus lacks (2026-10-26 08:54, issue 1790), writes
// the grants the assigner reads and the user list the bus node follows alone, and makes the account consumers, since the
// controller ended by its unassignment (ADR 0590, issue 1126). It changes no stream the bus holds.
package grants
import (
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"slices"
"sort"
"strings"
"sync"
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// The states of other parts the grants part reads, as its manifest's reads name them (`<module>.<state>`).
const (
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"
// ReadPlacement is the seat mesh-placement's placement (ADR 0567 §1, §9): each node's modules and the seats
// they hold.
ReadPlacement = delivery.PlacementRead
// ReadEnrolments is the seat mesh-enrolment's open enrolments, read as a named reader (ADR 0587 §2, §4): a key at
// live or redeeming composes the user enrol.<node>.
ReadEnrolments = delivery.EnrolmentsRead
// ReadNodeLogins is the vault's node-logins, read as its named reader (ADR 0583 §1 insight): each node's login in use
// and the one rotating, both composed as users of the node's permissions.
ReadNodeLogins = delivery.NodeLoginsRead
)
// Reads is every state the part reads, in the manifest's order. Not the controller's assignments: the seat's placement
// alone places modules (novox/hq ADR 0549 §5, ADR 0571 §10; issue 774).
var Reads = []string{ReadModules, ReadManifests, ReadResolved, ReadHave, ReadWants, ReadBuilds,
ReadAccounts, ReadPlacement, ReadEnrolments, ReadNodeLogins}
// 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 `<module>.<state>`. 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, `<module>.<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: 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: "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"},
}
// resolved is a key of mesh-config's resolved values, `<node>.<module>.<key>`.
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 the placement.
Nodes []string
// Assigned is what the seat's placement puts on each machine, sorted (issue 774: never the controller's assignments).
Assigned map[string][]string
// Placed is each node's placement as the seat mesh-placement holds it (ADR 0567 §1); empty before any is read.
// Where it is, a seat held once for the mesh is held where placement says, not wherever a module claiming it runs.
Placed map[string]delivery.Placement
// 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
// Fingerprints is the fingerprint of the new form the indexer's trunk value names for each module, by module: what a
// grant is written for (grants.go).
Fingerprints map[string]string
// NoFingerprint is why the indexer's value of a module names no fingerprint of the new form, by module (its none2, or
// that it names none): a module here gets no grant, and that is said (grants.go MissingGrants).
NoFingerprint map[string]string
// Running is the manifest a machine is wanted to run, by `<node>.<module>`: 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 `<node>/<module>/<key>`.
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
// Enrolling is every node whose enrolment key is at live or redeeming (ReadEnrolments, ADR 0587 §4, §5), sorted: one
// user enrol.<node> each. A key at asked has no token yet; an ended or expired key is gone from the bucket.
Enrolling []string
// NodeLogins is each node's key of the vault's node-logins (ReadNodeLogins, ADR 0583 §1); a node absent logs in as
// node.<n> alone.
NodeLogins map[string]delivery.NodeLogin
// 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 by the part, and
// written only into the list sealed to the bus node.
type HeldHash struct {
Hash string `json:"hash"`
Generation int `json:"generation,omitempty"`
At time.Time `json:"at"`
Adopted bool `json:"adopted,omitempty"`
// Fingerprint is the vault's fingerprint of the value the hash is of: with the generation, what says the password
// changed, never the hash itself (a hash of the same password differs by its salt).
Fingerprint string `json:"fingerprint,omitempty"`
// Rotating is the vault's rotation of the account under way: a change of its hash it announces (ADR 0561 §4, §7).
Rotating *struct {
Login string `json:"login"`
} `json:"rotating,omitempty"`
}
// credentialOf is what says which password a held hash is of, never the hash: adopted, or the vault's generation and the
// fingerprint of its value.
func credentialOf(h HeldHash) string {
if h.Adopted {
return "adopted"
}
return fmt.Sprintf("generation %d %s", h.Generation, h.Fingerprint)
}
// 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
}
// Decoded, never judged (novox/hq ADR 0578 §3): the indexer's judge judged it, and its verdict is taken where the
// module's value is read (Inputs); a field this part does not know is set aside.
m, err = catalogue.Decode([]byte(v.JSON))
if err != nil {
return catalogue.Manifest{}, fmt.Errorf("the manifest %s does not decode: %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{}, Fingerprints: map[string]string{},
NoFingerprint: map[string]string{}, Running: map[string]catalogue.Manifest{},
Values: map[string]any{}, Gaps: append([]Gap(nil), standingGaps...)}
nodes := map[string]bool{}
// **The seat's placement alone places modules** (novox/hq ADR 0549 §5, ADR 0567 §9; issue 774): the controller's
// assignments are read no more, and the seats held once for the mesh are held where it says. Not read is a failed
// read: nothing is placed, so the list composed past it is not sealed.
if keys, err := r.Keys(ReadPlacement); err != nil {
in.Gaps = append(in.Gaps, Gap{Input: ReadPlacement, Why: "refused or absent: nothing is placed (" + err.Error() + ")", Failed: true})
} else {
// Each node's keys in either form, assembled by the seat's one reader (delivery.PlacementsOf; research 072
// row 37): its node key and a key per module, or its legacy key `placement.<node>`.
values := map[string][]byte{}
for _, k := range keys {
raw, found, err := r.Get(ReadPlacement, k)
if err != nil || !found {
continue
}
if _, _, _, err := delivery.ParsePlacementKey(k); err != nil {
in.Gaps = append(in.Gaps, Gap{Input: ReadPlacement + " " + k, Why: "not a placement key, so it places nothing"})
continue
}
values[k] = raw
}
placed, err := delivery.PlacementsOf(values)
if err != nil {
in.Gaps = append(in.Gaps, Gap{Input: ReadPlacement, Why: "a key does not read, so its node is composed from what does: " + err.Error()})
}
for node, p := range placed {
if in.Placed == nil {
in.Placed = map[string]delivery.Placement{}
}
in.Placed[node] = p
}
}
for node, p := range in.Placed {
nodes[node] = true
for _, m := range p.Assigned() {
if !slices.Contains(in.Assigned[node], m) {
in.Assigned[node] = append(in.Assigned[node], m)
}
if m == BusModule && in.BusNode == "" {
in.BusNode = node
}
}
}
if len(in.Placed) > 0 {
in.Gaps = slices.DeleteFunc(in.Gaps, func(g Gap) bool { return g.Input == "seat holdings" })
}
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 placement: 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")
}
if err == nil && strings.HasPrefix(v.FP2, delivery.SourcePrefix2) {
in.Fingerprints[module] = v.FP2
} else if err == nil {
why := v.None2
if why == "" {
why = "the indexer's value names no fingerprint of the new form"
}
in.NoFingerprint[module] = why
}
var m catalogue.Manifest
if err == nil {
// The indexer's refusal, carried in its value (ADR 0578 §3): held and named as a refused manifest.
if verdict := v.VerdictOf(module); verdict != nil && verdict.Refused() {
err = errors.New(verdict.Why())
}
}
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.
for _, name := range delivery.ProvidedModules() {
if !slices.Contains(modules, name) {
continue
}
m, raw, err := delivery.ProvidedManifest(name)
in.Gaps = slices.DeleteFunc(in.Gaps, func(g Gap) bool { return g.Input == "manifest of "+name })
in.Refused = slices.DeleteFunc(in.Refused, func(g Gap) bool { return g.Input == "manifest of "+name })
if _, held := in.Digests[name]; held {
in.Gaps = append(in.Gaps, Gap{Input: "manifest of " + 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 " + name, Why: err.Error()}
in.Refused = append(in.Refused, g)
in.Gaps = append(in.Gaps, g)
delete(in.Manifests, name)
delete(in.Digests, name)
} else {
in.Manifests[name] = m
in.Digests[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)
// The seat mesh-enrolment's keys are every token since the controller's end (issue 1126).
in.Enrolling, _ = readEnrolling(r, &in.Gaps)
in.NodeLogins = readNodeLogins(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
}
// readEnrolling is every node with an enrolment key at live or redeeming (ADR 0587 §4, §5), and whether the seat's state
// was read. Not on the bus yet is a standing gap, never a failed read: the list is still composed and sealed, with no
// enrolment user. A key that does not read is named and composes no user.
func readEnrolling(r Reader, gaps *[]Gap) ([]string, bool) {
keys, err := r.Keys(ReadEnrolments)
if err != nil {
*gaps = append(*gaps, Gap{Input: ReadEnrolments, Why: "not read yet: no enrolment user is composed (" + err.Error() + ")"})
return nil, false
}
var out []string
for _, k := range keys {
node, isNode := delivery.EnrolmentKeyNode(k)
if !isNode {
continue
}
raw, found, err := r.Get(ReadEnrolments, k)
if err != nil {
*gaps = append(*gaps, Gap{Input: ReadEnrolments + " " + k, Why: err.Error(), Failed: true})
continue
}
if !found {
continue
}
e, err := delivery.DecodeEnrolment(raw)
if err != nil || e.Node != node {
*gaps = append(*gaps, Gap{Input: ReadEnrolments + " " + k, Why: "does not read as the enrolment of " + node +
", so it composes no user"})
continue
}
if e.Stage == delivery.EnrolmentLive || e.Stage == delivery.EnrolmentRedeeming {
out = append(out, node)
}
}
sort.Strings(out)
return out, true
}
// readNodeLogins is every node's key of the vault's node-logins (ADR 0583 §1). Not on the bus yet is a standing gap,
// never a failed read: every node then logs in as node.<n> alone, as before any rotation. A key that does not read is
// named and leaves its node at node.<n>; a read that fails is a failed read, since a rotating node's new login would be
// left out.
func readNodeLogins(r Reader, gaps *[]Gap) map[string]delivery.NodeLogin {
keys, err := r.Keys(ReadNodeLogins)
if err != nil {
*gaps = append(*gaps, Gap{Input: ReadNodeLogins, Why: "not read yet: every node logs in as node.<node> (" + err.Error() + ")"})
return nil
}
out := map[string]delivery.NodeLogin{}
for _, node := range keys {
raw, found, err := r.Get(ReadNodeLogins, node)
if err != nil {
*gaps = append(*gaps, Gap{Input: ReadNodeLogins + " " + node, Why: err.Error(), Failed: true})
continue
}
if !found {
continue
}
l, err := delivery.DecodeNodeLogin(raw)
if err != nil || l.Node != node {
*gaps = append(*gaps, Gap{Input: ReadNodeLogins + " " + node, Why: "does not read as the logins of " + node +
", so it logs in as node." + node})
continue
}
out[node] = l
}
return out
}
// Rotating are the logins the vault's node-logins names in a rotation under way, as a set: a hash of one changes by the
// vault's rotation (ADR 0583 §2), never counted as a writer changing several hashes at once.
func (in Inputs) Rotating() map[string]bool {
out := map[string]bool{}
for _, l := range in.NodeLogins {
if l.Rotates() {
out[l.Login], out[l.Rotating] = true, true
}
}
return out
}
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
}