Composed without a root-free judgement, a membership would drop a kind's verified-sender and the operator's channel approvals would stop being honoured; any loss now stays a shadow difference, named. A tool no longer waits on a pass's round trip. compose-now says the controller serves no verb to write a module's grant again.
188 lines
7.1 KiB
Go
188 lines
7.1 KiB
Go
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.<module>.<fp> 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 ""
|
|
}
|