170 lines
11 KiB
Go
170 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
|
|
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>`.
|
|
type runtimeReader struct{}
|
|
|
|
func (runtimeReader) Keys(state string) ([]string, error) { return stdio.State(state).Keys() }
|
|
|
|
func (runtimeReader) Get(state, key string) ([]byte, bool, error) {
|
|
e, err := stdio.State(state).Get(key)
|
|
if err != nil || e == nil {
|
|
return nil, false, err
|
|
}
|
|
return e.Value, 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) }
|
|
|
|
// 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 6): in this milestone it " +
|
|
"writes none (grants.<module>.<fp> from the third milestone, clause 5), and shows beside it the grants the controller " +
|
|
"wrote (mesh-controller_grants), 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 in shadow (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. The user list, assertions and " +
|
|
"grants stay in shadow. It cannot ask the controller to write a module's grant again: the controller serves no such " +
|
|
"verb, and the answer says so.",
|
|
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.ToolTakeOver, Description: "Ask the operator to switch the bus node to the grants part's list (ADR 0547 clause 8). " +
|
|
"The operator's word is their yes on their channel (a warrant), never a caller's name: this tool only asks, once at a time. " +
|
|
"After the yes, the switch is recorded and takes effect when the bus node loads the part's list; it is refused while the loaded list could not be read, a user differs between the lists " +
|
|
"(an enrolment user or a person the part cannot compose among them), or the vault holds no hash for a user, and the " +
|
|
"refusal is said to the operator naming the users. Every act is recorded in the part's state takeover.",
|
|
Input: schema(map[string]string{"why": "why, kept with the act"}, "why"),
|
|
Run: func(a map[string]any) (any, error) {
|
|
return p.TakeOver(stdio.CallerOf(a), grants.Answer(a, "why"))
|
|
}},
|
|
{Name: grants.ToolHandBack, Description: "Ask the operator to switch the bus node back to the controller's list (ADR 0547 clause 8), " +
|
|
"by their yes on their channel; in shadow there is nothing to hand back, and it says so. Every act is recorded.",
|
|
Input: schema(map[string]string{"why": "why, kept with the act"}, "why"),
|
|
Run: func(a map[string]any) (any, error) {
|
|
return p.HandBack(stdio.CallerOf(a), grants.Answer(a, "why"))
|
|
}},
|
|
{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; whether the lease is handed away). 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 while the controller holds its lease (the consumers are the controller's then) and " +
|
|
"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.ToolTakeOverState, Description: "Where the switch stands: the list the bus node follows, the ask open now, the request, and " +
|
|
"every act with what became of it (asked, switched, declined, expired, refused and why).",
|
|
Input: schema(nil),
|
|
Run: func(map[string]any) (any, error) { return p.TakeOverState() }},
|
|
}
|
|
}
|
|
|
|
// serveTools serves the tools to the runtime until it stops.
|
|
func serveTools() error {
|
|
p := &grants.Part{Read: runtimeReader{}, Call: runtimeBus{}, Store: runtimeStore{}, Emit: runtimeBus{}}
|
|
return stdio.Serve("", toolsOf(p))
|
|
}
|