Files
mesh-delivery/modules/mesh-grants/internal/grants/tools.go
T
jschoubben 3618831992 mesh-grants: write grants.<module>.<fp> in shadow into the state grants, say grant-written, and show every difference from the controller's in grants_grants (issue 774, ADR 0547 §5)
Why: the assigner switches its grants source when the controller's lease is handed away; until then the written keys are compared with the controller's, so the switch rests on a measured equality. Needs the group's mesh-sdk pin (PartGrantKey) before it builds.
2026-10-28 21:54:47 +01:00

293 lines
11 KiB
Go

package grants
import (
"encoding/json"
"errors"
"fmt"
"strings"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// The part's tools, as its manifest lists them. Module tools in this milestone: a `mesh-*` seat must be in mesh-sdk's own
// seat list, which the running controller's composer does not know, so the part claims no seat until take-over.
const (
ToolGrants = "grants_grants"
ToolMembership = "grants_membership"
ToolUserList = "grants_user_list"
ToolComposeNow = "grants_compose_now"
ToolAssert = "grants_assert"
ToolTakeOver = "grants_take_over"
ToolTakeOverState = "grants_take_over_state"
ToolHandBack = "grants_hand_back"
ToolAccounts = "grants_accounts"
ToolConsumerMatch = "grants_consumer_match"
ToolAcceptHashes = "grants_accept_hashes"
)
// Tools is every tool, in the manifest's order.
var Tools = []string{ToolGrants, ToolMembership, ToolUserList, ToolComposeNow, ToolAssert, ToolTakeOver, ToolHandBack, ToolTakeOverState,
ToolAccounts, ToolConsumerMatch, ToolAcceptHashes}
// WritesGrants is what grants says of the part's own grants in this milestone: written, and read by nobody until the
// controller's lease is handed away (delivery.GrantSource).
const WritesGrants = "in shadow: the grants part writes grants.<module>.<fp> into its state grants, which the assigner reads only once the controller's lease is handed away (ADR 0547 clause 5); the controller's are beside, with every difference"
// Grants answers the tool grants: the grants the part wrote, the controller's, and every difference between them, one
// module's or all.
func (p *Part) Grants(module string) (any, error) {
out := map[string]any{"writes": WritesGrants}
read := func(get func() ([]string, func(string) ([]byte, bool, error), error)) (map[string]delivery.ControllerGrant, error) {
keys, one, err := get()
if err != nil {
return nil, err
}
m := map[string]delivery.ControllerGrant{}
for _, k := range keys {
mod, _, err := delivery.ParseControllerGrantKey(k)
if err != nil || (module != "" && mod != module) {
continue
}
raw, found, err := one(k)
if err != nil {
return nil, err
}
if !found {
continue
}
g, err := delivery.DecodeControllerGrant(raw)
if err != nil {
return nil, fmt.Errorf("%s: %w", k, err)
}
m[k] = g
}
return m, nil
}
controller, cerr := read(func() ([]string, func(string) ([]byte, bool, error), error) {
keys, err := p.Read.Keys(ReadControllerGrant)
return keys, func(k string) ([]byte, bool, error) { return p.Read.Get(ReadControllerGrant, k) }, err
})
mine, perr := read(func() ([]string, func(string) ([]byte, bool, error), error) {
keys, err := p.Store.Keys(StateGrants)
return keys, func(k string) ([]byte, bool, error) {
raw, _, found, err := p.Store.Get(StateGrants, k)
return raw, found, err
}, err
})
type grant struct {
Key string `json:"key"`
Module string `json:"module"`
Accounts []string `json:"accounts,omitempty"`
}
list := func(m map[string]delivery.ControllerGrant) []grant {
out := []grant{}
for _, k := range sortedKeys(m) {
mod, _, _ := delivery.ParseControllerGrantKey(k)
out = append(out, grant{Key: k, Module: mod, Accounts: m[k].Accounts})
}
return out
}
if cerr != nil {
out["controller_unread"] = cerr.Error()
} else {
out["controller"], out["controller_count"] = list(controller), len(controller)
}
if perr != nil {
out["written_unread"] = perr.Error()
} else {
out["written"], out["count"] = list(mine), len(mine)
}
if cerr == nil && perr == nil {
diff := CompareGrants(controller, mine)
out["differences"], out["differing"] = diff, len(diff)
out["equal"] = len(diff) == 0
}
if last := p.Last(); last.Grants != nil {
out["last_pass"] = last.Grants
}
return out, nil
}
// MembershipOf answers the tool membership: one module on one node as composed now, and as last published by the
// part's process (its state memberships says what became of each at the last pass).
func (p *Part) MembershipOf(node, module string) (any, error) {
if node == "" && module == "" {
return p.Held()
}
if node == "" || module == "" {
return nil, errors.New("membership names a node and a module, or neither for every membership held back")
}
in := ReadInputs(p.Read, &p.cache)
recs, left := Records(in)
notFree := WithRootFree(&recs, p.Call)
m, ok := Memberships(recs)[node+"."+module]
out := map[string]any{"node": node, "module": module, "gaps": MembershipGaps, "root_free": recs.RootFree, "root_not_free": notFree}
if ok {
out["composed"] = m
} else {
why := fmt.Sprintf("%s is not assigned to %s on the bus", module, node)
for _, g := range left {
if g.Input == module+" on "+node {
why = g.Why
}
}
out["composed"] = why
}
if raw, rev, found, err := p.Store.Get(StateMemberships, node+"."+module); err != nil {
out["published"] = "not readable: " + err.Error()
} else if found {
out["published"] = map[string]any{"revision": rev, "last_pass": json.RawMessage(raw)}
} else {
out["published"] = "no pass of the part's process has composed it yet"
}
return out, nil
}
// Held answers every membership the part's process held back at its last pass, each `<node>.<module>` with why (what
// publishing would lose), from the pass it keeps (issue 1945).
func (p *Part) Held() (any, error) {
raw, rev, found, err := p.Store.Get(StateUserList, KeyPass)
if err != nil {
return nil, fmt.Errorf("the last pass cannot be read: %w", err)
}
if !found {
return map[string]any{"held": []any{}, "says": "no pass of the part's process has been kept yet"}, nil
}
var res Result
if err := json.Unmarshal(raw, &res); err != nil {
return nil, fmt.Errorf("the last pass cannot be read: %w", err)
}
type held struct {
Membership string `json:"membership"`
Why string `json:"why"`
}
out := []held{}
for _, n := range res.NotPublished {
key, why, _ := strings.Cut(n, ": ")
out = append(out, held{Membership: key, Why: why})
}
refused := res.Refused
if refused == nil {
refused = []string{}
}
return map[string]any{"held": out, "count": len(out), "pass": rev, "published": res.Published, "refused": refused}, nil
}
// Pass composes and keeps the result as the last pass, for the tools to read.
func (p *Part) Pass() Result {
res := p.Compose()
data, _ := json.Marshal(res)
if _, err := p.Store.Put(StateUserList, KeyPass, data); err != nil {
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", StateUserList, KeyPass, err))
}
return res
}
// NoGrantAgain is what compose-now says of the controller's grants: the controller serves no verb that writes a module's
// grant again (its issue replaces the account, which is not that), so a grant write it lost stays lost until it composes
// again; the part writes grants only from ADR 0547's third milestone.
const NoGrantAgain = "missing: a verb to ask the controller to write a module's grant again (mesh-controller serves none; its issue replaces the bus account, which is not that), so compose-now cannot ask it"
// AskCompose is the tool compose-now: it asks the part's process to compose and publish now (only the process holds the
// grant to publish a membership), and answers the last pass, whose revision moves when the process answered.
func (p *Part) AskCompose(caller string) (any, error) {
ask, _ := json.Marshal(Ask{By: caller, At: p.now()})
rev, err := p.Store.Put(StateUserList, KeyAsked, ask)
if err != nil {
return nil, fmt.Errorf("the ask could not be written: %w", err)
}
out := map[string]any{"asked": rev, "says": "the part's process composes and publishes at this ask; grants_user_list's last pass says what it published",
"controller_grants": NoGrantAgain}
if raw, prev, found, err := p.Store.Get(StateUserList, KeyPass); err == nil && found {
out["last_pass"] = map[string]any{"revision": prev, "result": json.RawMessage(raw)}
}
return out, nil
}
// UserList answers the tool user-list: the shadow list as last written, beside what the bus node says it loaded now.
func (p *Part) UserList() (any, error) {
out := map[string]any{"mode": Shadow}
raw, rev, found, err := p.Store.Get(StateUserList, KeyList)
switch {
case err != nil:
out["written"] = "not readable: " + err.Error()
case !found:
out["written"] = "nothing composed yet: grants_compose_now composes it"
default:
var st UserListState
if err := json.Unmarshal(raw, &st); err != nil {
return nil, fmt.Errorf("its own user list cannot be read: %w", err)
}
out["revision"], out["written"] = rev, st
if raw, prev, found, err := p.Store.Get(StateUserList, KeyPass); err == nil && found {
out["last_pass"] = map[string]any{"revision": prev, "result": json.RawMessage(raw)}
}
if st.BusNode != "" {
if raw, found, err := p.Read.Get(ReadHave, st.BusNode+"."+BusModule); err == nil && found {
if h, err := delivery.DecodeHave(raw); err == nil {
out["loaded_now"] = h.UserList
}
}
}
}
return out, nil
}
// AssertOne answers the tool assert: one module's buckets, logs and streams as composed from its manifest at the
// indexer's trunk, beside what the bus holds, and asks the part's process (which alone holds the grant to create) to
// create what the bus lacks now, even one it created at this manifest and that is gone since (issue 1790). It changes no
// stream the bus holds. Here, in the tools, it creates nothing itself: the answer names the ask.
func (p *Part) AssertOne(module string) (any, error) {
if module == "" {
return nil, errors.New("assert names a module")
}
raw, found, err := p.Read.Get(ReadModules, module)
if err != nil {
return nil, fmt.Errorf("the indexer's %s cannot be read: %w", module, err)
}
if !found {
return nil, fmt.Errorf("the indexer holds no module %s", module)
}
v, err := delivery.DecodeModule(raw)
if err != nil || v.Manifest == "" {
return nil, fmt.Errorf("the indexer names no manifest of %s", module)
}
m, err := p.cache.get(p.Read, v.Manifest)
if err != nil {
return nil, err
}
held, heldErr := HeldStreams(p.Call)
a := Assert(module, v.Manifest, m, held, heldErr)
out := map[string]any{"assertion": a}
missing := 0
for _, it := range a.Items {
if it.Held == nil {
missing++
}
}
if missing == 0 {
out["says"] = "the bus holds every bucket, log and stream the manifest declares: nothing to create; a difference is only said"
return out, nil
}
ask, _ := json.Marshal(Ask{By: "grants_assert", At: p.now(), Assert: module})
rev, err := p.Store.Put(StateUserList, KeyAsked, ask)
if err != nil {
return nil, fmt.Errorf("the ask to create could not be written: %w", err)
}
out["asked"] = rev
out["says"] = fmt.Sprintf("%d the bus lacks: the part's process creates them at this ask if %s is placed (its grant names only placed "+
"modules' streams) and says asserted; grants_assert again shows what the bus holds then", missing, module)
return out, nil
}
// Answer is a tool's arguments read as text.
func Answer(args map[string]any, name string) string {
switch v := args[name].(type) {
case string:
return strings.TrimSpace(v)
case bool:
return fmt.Sprint(v)
}
return ""
}