Files
mesh-delivery/modules/mesh-grants/cmd/mesh-grants/tools.go
T

273 lines
13 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"maps"
"os"
"sort"
"strings"
"sync"
"time"
"git.novox.be/novox/mesh-sdk/go/seatlist"
"git.novox.be/novox/mesh-sdk/go/views"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
stdio "git.novox.be/novox/mesh-sdk/go"
"mesh-grants/internal/grants"
)
// The part's tools, served through the tool runner from its bundle (ADR 0436: a process never serves). They read and
// write through the runtime as the module: its own states, the reads its manifest names, the bus module's read tools.
// runtimeReader reads another part's state through the runtime, `<module>.<state>`; the owners' five states the part
// reads by their first views (grants.ViewReads) it reads by asking each owner's first-view tool, once per tool call
// (fresh), never their buckets (ADR 0615 §1).
type runtimeReader struct{ views *toolViews }
func (r runtimeReader) Keys(state string) ([]string, error) {
if values, mine, err := r.views.of(state); mine {
if err != nil {
return nil, err
}
out := make([]string, 0, len(values))
for k := range values {
out = append(out, k)
}
sort.Strings(out)
return out, nil
}
return stdio.State(state).Keys()
}
func (r runtimeReader) Get(state, key string) ([]byte, bool, error) {
if values, mine, err := r.views.of(state); mine {
if err != nil {
return nil, false, err
}
v, ok := values[key]
return v, ok, nil
}
e, err := stdio.State(state).Get(key)
if err != nil || e == nil {
return nil, false, err
}
return e.Value, true, nil
}
// toolViews are the five first views as one tool call reads them: each asked whole at its first read in the call, kept
// for the rest of it, and forgotten by fresh at the next call.
type toolViews struct {
mu sync.Mutex
held map[string]map[string]json.RawMessage
}
func (v *toolViews) fresh() {
v.mu.Lock()
v.held = nil
v.mu.Unlock()
}
// of is the whole picture of a first view's state, whether state is one, and why it could not be had.
func (v *toolViews) of(state string) (map[string]json.RawMessage, bool, error) {
var fv *grants.FirstView
for i := range grants.FirstViews {
if grants.FirstViews[i].Read == state {
fv = &grants.FirstViews[i]
}
}
if fv == nil {
return nil, false, nil
}
v.mu.Lock()
defer v.mu.Unlock()
if held, ok := v.held[state]; ok {
return held, true, nil
}
ask := views.AskVia(grants.Unwrapped(stdio.Ask), fv.Tool)
values := map[string]json.RawMessage{}
first := true
var rev uint64
for after := ""; ; {
a, err := ask(context.Background(), after)
if err != nil {
return nil, true, fmt.Errorf("%s does not answer: %w", fv.Tool, err)
}
if first {
rev, first = a.Revision, false
}
maps.Copy(values, a.Values)
if a.Next == "" || a.Next == after {
break
}
after = a.Next
}
if rev == 0 {
return nil, true, fmt.Errorf("%s by %s: %w", state, fv.Tool, grants.ErrFirstViewNotYet)
}
if v.held == nil {
v.held = map[string]map[string]json.RawMessage{}
}
v.held[state] = values
return values, true, nil
}
// runtimeStore is the part's own state through the runtime.
type runtimeStore struct{}
func (runtimeStore) Get(state, key string) ([]byte, uint64, bool, error) {
e, err := stdio.State(state).Get(key)
if err != nil || e == nil {
return nil, 0, false, err
}
return e.Value, e.Revision, true, nil
}
func (runtimeStore) Put(state, key string, value []byte) (uint64, error) {
return stdio.State(state).Put(key, json.RawMessage(value))
}
func (runtimeStore) Keys(state string) ([]string, error) { return stdio.State(state).Keys() }
func (runtimeStore) Delete(state, key string) error { return stdio.State(state).Delete(key) }
func (runtimeStore) PutTTL(state, key string, value []byte, ttl time.Duration) (uint64, error) {
return stdio.State(state).PutTTL(key, json.RawMessage(value), ttl)
}
// runtimeBus calls tools and says events through the runtime.
type runtimeBus struct{}
func (runtimeBus) Call(key string, body any) (json.RawMessage, error) { return stdio.Ask(key, body) }
func (runtimeBus) Emit(event string, body any) error { return stdio.Emit(event, body) }
func schema(properties map[string]string, required ...string) map[string]any {
props := map[string]any{}
for name, description := range properties {
props[name] = map[string]any{"type": "string", "description": description}
}
out := map[string]any{"type": "object", "properties": props}
if len(required) > 0 {
out["required"] = required
}
return out
}
// toolsOf are the part's tools over a part (grants.Tools names them).
func toolsOf(p *grants.Part) []stdio.Tool {
return []stdio.Tool{
{Name: grants.ToolGrants, Description: "What the grants part holds of grants (novox/hq ADR 0547 clause 5, ADR 0571 clause 9): the grants it wrote " +
"(grants.<module>.<fp> in its state grants, the one key the assigner reads), all or one module's with the accounts each names. " +
"Read-only.",
Input: schema(map[string]string{"module": "one module"}),
Run: func(a map[string]any) (any, error) { return p.Grants(grants.Answer(a, "module")) }},
{Name: grants.ToolMembership, Description: "One module's membership on one node (ADR 0547 clause 6): as composed now from the " +
"bus's records with the bus composer, and what the part's process published of it at its last pass (published, " +
"unchanged, or why not: a shadow difference names what publishing would lose; verbs the seat catalogue retired are " +
"dropped and named), with what the composition cannot read. With neither node nor module: every membership the last " +
"pass held back, each with what it would lose. Read-only.",
Input: schema(map[string]string{"node": "the machine (with module)", "module": "the module (with node)"}),
Run: func(a map[string]any) (any, error) {
return p.MembershipOf(grants.Answer(a, "node"), grants.Answer(a, "module"))
}},
{Name: grants.ToolUserList, Description: "The bus's user list (ADR 0547 clauses 6 and 8): the list the grants part " +
"composed (digest, revision, users), what the bus node says it loaded (source, revision, digest, users, error), and how " +
"they differ: users added, users dropped (and of those, gone from placement), each user's grant differences, the " +
"assignments left out, every input the bus does not carry, named; and per user whether the vault holds its hash " +
"(mesh-vault.bus-accounts, ADR 0547 clause 9) and how its line would differ from the loaded one (same, adds, left-out " +
"for want of a credential, loses-login), never a hash. Read-only.",
Run: func(map[string]any) (any, error) { return p.UserList() }},
{Name: grants.ToolComposeNow, Description: "Compose again now (ADR 0547 clause 6), as the part does on every change it hears: asks " +
"the part's process (which alone holds the grant to publish a membership) to compose the memberships, the user list and " +
"the assertions, publish each membership that differs from the one last published, and keep what changed in its own " +
"state (one revision when an input changed, none when not), saying user-list-changed and membership-changed. Answers " +
"the ask and the last pass; grants_user_list shows the pass that answers it. A membership is published unless it would " +
"leave a seat a placed module claims with no holder, or drop a capability its holder still asks for (verified-sender), " +
"which stays a shadow difference, named.",
Run: func(a map[string]any) (any, error) { return p.AskCompose(stdio.CallerOf(a)) }},
{Name: grants.ToolAssert, Description: "One module's buckets, logs and streams (ADR 0547 clause 6), composed from its manifest at " +
"the indexer's trunk with their limits, beside what the bus holds (through the bus module's nats_streams). When the " +
"bus lacks one, asks the part's process (which alone holds the grant) to create it now with the manifest's limits, " +
"as it does by itself when a manifest is registered (issue 1790), and says asserted; one the bus holds is never " +
"changed, a difference is only said. Answers the comparison and the ask; call again to see what the bus holds.",
Input: schema(map[string]string{"module": "the module"}, "module"),
Run: func(a map[string]any) (any, error) { return p.AssertOne(grants.Answer(a, "module")) }},
{Name: grants.ToolAccounts, Description: "The bus accounts the mesh wants (novox/hq ADR 0561 §2, §9): every account the vault " +
"mints that placement composes (a module's, a process's, a machine's provisioner client's), as the grants part last " +
"wrote it to its state accounts, which the vault alone reads: user, kind, node, module, process, the bus's address and " +
"fingerprint, a module's claims with the verbs each seat promises, and when the key was written; one user's or all; " +
"what the last pass wrote, deleted or kept; and the durable consumers the accounts need as the last pass found them " +
"(missing, differing and why, created). Never a password. Read-only: " +
"grants_compose_now writes them again.",
Input: schema(map[string]string{"user": "one bus user, e.g. ace.mesh-build-go"}),
Run: func(a map[string]any) (any, error) { return p.Accounts(grants.Answer(a, "user")) }},
{Name: grants.ToolConsumerMatch, Description: "Bring one durable consumer an account needs (a module's on EVENTS, a process's on " +
"its work queue; novox/hq ADR 0561 §6) to the configuration the bus composer composes, by hand: the recovery when " +
"the condition mesh-grants.consumer-differs names it. Asks the part's process, which alone may, and keeps where the " +
"consumer delivers from. Refused for a consumer the records do not compose. grants_user_list's last pass says what became of it.",
Input: schema(map[string]string{"stream": "the stream, e.g. EVENTS", "consumer": "the consumer, e.g. ace_mesh-build-go"}, "stream", "consumer"),
Run: func(a map[string]any) (any, error) {
return p.MatchConsumer(grants.Answer(a, "stream"), grants.Answer(a, "consumer"))
}},
{Name: grants.ToolAcceptHashes, Description: "Release the hold of a user list that changes several loaded users' hashes at " +
"once (novox/hq ADR 0561 §7; the condition mesh-grants.user-list-held names them), by a person's act with why: names " +
"exactly the users held, bound to the held list's digest, so the part's process writes that list once at its next pass " +
"and says hashes-accepted. It never releases another hold (a failed read, a user dropped for want of a hash, a lost " +
"right), and a list composed otherwise since is held again for a new acceptance.",
Input: map[string]any{"type": "object", "properties": map[string]any{
"users": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "the users held, as the condition names them"},
"why": map[string]any{"type": "string", "description": "why their hashes change"}}, "required": []string{"users", "why"}},
Run: func(a map[string]any) (any, error) {
var users []string
if list, ok := a["users"].([]any); ok {
for _, u := range list {
if s, ok := u.(string); ok {
users = append(users, strings.TrimSpace(s))
}
}
}
return p.AcceptHashes(stdio.CallerOf(a), users, grants.Answer(a, "why"))
}},
{Name: grants.ToolFirstViews, Description: "The grants part's first views of the owners' states (novox/hq ADR 0615 §4 to §6, " +
"ADR 0622): the assigner's placement and wants, the indexer's manifests, modules and verdicts, each with its owner, tool " +
"and the revision its picture holds, when it asked and when it was answered, how many asks and hand acts, and whether " +
"its condition is raised, as the part's process last kept them. Read-only.",
Run: func(map[string]any) (any, error) { return p.FirstViewsOf() }},
{Name: grants.ToolFirstViewAgain, Description: "Ask one of the grants part's first views once more now (placement, wants, " +
"manifests, modules or verdicts; none for all), by a person's act with why, recorded as a hand act: the step after an " +
"owner's state was written again from its sources (an empty answer at revision 0, condition " +
"mesh-grants.first-view-empty). Asks the part's process, which alone follows them; grants_first_views shows the answer.",
Input: schema(map[string]string{"state": "placement, wants, manifests, modules or verdicts; none for all", "why": "why, kept in the hand act"}, "why"),
Run: func(a map[string]any) (any, error) {
return p.AskFirstViewAgain(stdio.CallerOf(a), grants.Answer(a, "state"), grants.Answer(a, "why"), recordHandAct)
}},
}
}
// recordHandAct records a person's act through the seat mesh-hand-acts.
func recordHandAct(what, why string) error {
_, err := meshhandactsclient.Recorder{Ask: stdio.Ask}.Record(meshhandactsclient.Request{What: what, Why: why, Cause: "first-view-again"})
return err
}
// serveTools serves the tools to the runtime until it stops.
func serveTools() error {
tv := &toolViews{}
gate := &seatGate{}
p := &grants.Part{Read: runtimeReader{views: tv}, Call: runtimeBus{}, Store: runtimeStore{}, Emit: runtimeBus{}, SeatList: gate}
// The reader asks through the runtime, which serves once Serve has its channel.
go func() {
<-stdio.Served()
gate.follow(seatlist.StdioBus(), nil, func(f string, a ...any) { fmt.Fprintf(os.Stderr, "mesh-grants: "+f+"\n", a...) })
}()
tools := append(toolsOf(p), gate.tools()...)
for i := range tools {
run := tools[i].Run
tools[i].Run = func(a map[string]any) (any, error) { tv.fresh(); return run(a) }
}
return stdio.Serve("", tools)
}