package grants import ( "encoding/json" "errors" "fmt" "slices" "strings" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/membership" "mesh-grants/internal/buscompose" ) // The part's tools, as its manifest lists them. The part claims no seat: the seat mesh-grants served the lease write // alone, which went with the controller (novox/hq ADR 0590 §3, issue 1126). const ( ToolGrants = "grants_grants" ToolMembership = "grants_membership" ToolUserList = "grants_user_list" ToolComposeNow = "grants_compose_now" ToolAssert = "grants_assert" 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, ToolAccounts, ToolConsumerMatch, ToolAcceptHashes} // WritesGrants is what grants says of the part's own grants: written once the list the bus loaded carries a build's // accounts, and the one key the assigner reads (ADR 0571 clause 9, which changes ADR 0547 clause 5). const WritesGrants = "the grants part writes grants.. into its state grants, once the list the bus loaded carries that build's accounts; it is the one key the assigner reads (ADR 0571 clause 9)" // Grants answers the tool grants: the grants the part wrote, 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.Grant, error) { keys, one, err := get() if err != nil { return nil, err } m := map[string]delivery.Grant{} for _, k := range keys { mod, _, err := delivery.ParsePartGrantKey(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.DecodeGrant(raw) if err != nil { return nil, fmt.Errorf("%s: %w", k, err) } m[k] = g } return m, nil } 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.Grant) []grant { out := []grant{} for _, k := range sortedKeys(m) { mod, _, _ := delivery.ParsePartGrantKey(k) out = append(out, grant{Key: k, Module: mod, Accounts: m[k].Accounts}) } return out } if perr != nil { out["written_unread"] = perr.Error() } else { out["written"], out["count"] = list(mine), len(mine) } 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 } // The state holds the bare membership last published (mesh-sdk's codec, issue 774); what became of it at the last // pass is the pass's, kept in user-list at KeyPass. key := node + "." + module if raw, rev, found, err := p.Store.Get(StateMemberships, key); err != nil { out["published"] = "not readable: " + err.Error() } else if !found { out["published"] = "no pass of the part's process has published it yet" } else if was, err := membership.DecodeGrantsMembership(raw); err != nil { out["published"] = fmt.Sprintf("revision %d is not readable: %v", rev, err) } else { out["published"] = map[string]any{"revision": rev, "digest": digestOf(was), "membership": was} } out["last_pass"] = p.passOf(key) return out, nil } // passOf is what the last pass the part's process kept says of one membership: published, unchanged or why not, and // the seat verbs it dropped as retired. func (p *Part) passOf(key string) any { raw, rev, found, err := p.Store.Get(StateUserList, KeyPass) if err != nil { return "not readable: " + err.Error() } if !found { return "no pass of the part's process has been kept yet" } var res Result if err := json.Unmarshal(raw, &res); err != nil { return "not readable: " + err.Error() } said := MembershipUnchanged if slices.Contains(res.Published, key) { said = MembershipPublished } for _, n := range res.NotPublished { if k, why, _ := strings.Cut(n, ": "); k == key { said = why } } out := map[string]any{"revision": rev, "published": said} if r := res.Retired[key]; len(r) > 0 { out["retired"] = r } return out } // Held answers every membership the part's process held back at its last pass, each `.` 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 } // 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"} 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": InForce} 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) var nodes []string if module == buscompose.EngineModule { nodes = ReadInputs(p.Read, &p.cache).Nodes // every node's bucket of compositions (ADR 0593 §6) } a := Assert(module, v.Manifest, m, nodes, 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 "" }