package grants import ( "encoding/json" "errors" "fmt" "sort" "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" ToolHandBack = "grants_hand_back" ) // Tools is every tool, in the manifest's order. var Tools = []string{ToolGrants, ToolMembership, ToolUserList, ToolComposeNow, ToolAssert, ToolTakeOver, ToolHandBack} // WritesNoGrant is what grants says of the part's own grants in this milestone. const WritesNoGrant = "none: the grants part writes no grants.. before ADR 0547's third milestone (clause 5); the controller's are beside" // Grants answers the tool grants: what the part writes of grants (none), and the controller's, one module's or all. func (p *Part) Grants(module string) (any, error) { keys, err := p.Read.Keys(ReadControllerGrant) if err != nil { return map[string]any{"writes": WritesNoGrant, "controller_unread": err.Error()}, nil } type grant struct { Module string `json:"module"` Fingerprint string `json:"fingerprint"` Accounts []string `json:"accounts,omitempty"` } out := []grant{} for _, k := range keys { m, fp, err := delivery.ParseControllerGrantKey(k) if err != nil || (module != "" && m != module) { continue } g := grant{Module: m, Fingerprint: fp} if module != "" { if raw, found, err := p.Read.Get(ReadControllerGrant, k); err == nil && found { if v, err := delivery.DecodeControllerGrant(raw); err == nil { g.Accounts = v.Accounts } } } out = append(out, g) } sort.Slice(out, func(i, j int) bool { return out[i].Module+out[i].Fingerprint < out[j].Module+out[j].Fingerprint }) return map[string]any{"writes": WritesNoGrant, "controller": out, "count": len(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 nil, errors.New("membership names a node and a module") } in := ReadInputs(p.Read, &p.cache) recs, left := Records(in) m, ok := Memberships(recs)[node+"."+module] out := map[string]any{"node": node, "module": module, "gaps": MembershipGaps} 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 } // 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(map[string]any{"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. It asserts nothing. 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) return Assert(module, v.Manifest, m, held, heldErr), 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 "" }