273 lines
13 KiB
Go
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)
|
|
}
|