Research 031 counted the repairs people made by hand: a push to unstick a plan waiting on a report, a controller restarted to make an object again, a plan closed, a consumer re-made from now. Each was the ordinary path taken again by someone who noticed. The healer registry makes each a registered response to one condition kind, with a budget, a settle and its event: - H1 sent-not-reported: ask the machine's node-engine to report again (mesh.node.<n>.ask.report); if it does not report what it was sent, send it again, never moving a build a policy or a plan holds back - H2 stalled: close a plan whose wait is superseded or finished - H3 holder-silent / consumer-lost: the send's own assertion of the bus's objects (issue 208's note) - H4 consumer-behind: consumer-reset, only for a consumer the stream table marks resettable (the controller's own events consumer) - H5 is the identity provider's own repair (ADR 0224 §5), registered only Success is the observation clearing the condition, never the healer; a spent budget hands the condition to the operator, urgent, with what was tried, and no healer touches it again. Every act is begun in the store before it is made (migration 0070), kept in the condition's tried as "healer Hn" and said as the seat event healer-acted; a heal is never a hand act. More than twelve acts in an hour stop every healer until an hour after the last, said urgently. Only the lease holder heals. S15 is live: a cause repaired by hand twice in a fortnight raises healer-wanted, naming the healer that was not enough where one exists. D6's far-behind finding has its own kind, consumer-behind. Nodes are granted the question; the controller's grant gains healer-acted (genesis lock in mesh-host). `healers` lists the registry, the acts and the brake; status counts the week's heals.
338 lines
21 KiB
Go
338 lines
21 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
)
|
|
|
|
// A Verb is one tool a role serves: its name, what it does, and the schema of its arguments and of
|
|
// its answer (novox/hq ADR 0132, design 33 §2).
|
|
//
|
|
// **A name alone is not callable by something that has never seen the mesh before**, which is the
|
|
// whole population a tool surface exists for. So a seat's protocol carries the definition, in the
|
|
// form an agent protocol already uses — a JSON schema for the input — so nothing translates between
|
|
// a seat's idea of an argument and the caller's.
|
|
//
|
|
// A manifest may still write a bare verb name (`"serves": ["price"]`); that is a Verb with only a
|
|
// name, and the module's runtime answers `tools` with the rest. The two forms read into one type so
|
|
// nothing downstream cares which was written.
|
|
type Verb struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description,omitempty"`
|
|
Input map[string]any `json:"input,omitempty"`
|
|
Output map[string]any `json:"output,omitempty"`
|
|
}
|
|
|
|
func (v *Verb) UnmarshalJSON(raw []byte) error {
|
|
trimmed := bytes.TrimSpace(raw)
|
|
if len(trimmed) > 0 && trimmed[0] == '"' {
|
|
var name string
|
|
if err := json.Unmarshal(trimmed, &name); err != nil {
|
|
return err
|
|
}
|
|
*v = Verb{Name: name}
|
|
return nil
|
|
}
|
|
// Strictly, like the manifest around it: a misspelt key in a tool's definition would otherwise
|
|
// describe a tool nobody can call and refuse nothing.
|
|
type plain Verb
|
|
var p plain
|
|
decoder := json.NewDecoder(bytes.NewReader(trimmed))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&p); err != nil {
|
|
return fmt.Errorf("a served verb is a name or {name, description, input, output}: %w", err)
|
|
}
|
|
if p.Name == "" {
|
|
return fmt.Errorf("a served verb has no name: %s", trimmed)
|
|
}
|
|
*v = Verb(p)
|
|
return nil
|
|
}
|
|
|
|
// VerbNames are the names alone, for the grants and the checks that care about nothing else.
|
|
func VerbNames(verbs []Verb) []string {
|
|
out := make([]string, 0, len(verbs))
|
|
for _, v := range verbs {
|
|
out = append(out, v.Name)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ControllerSeatName is the seat the control plane holds, whose tools are the mesh's own verbs.
|
|
const ControllerSeatName = "mesh-controller"
|
|
|
|
// ControllerVerbs are the mesh's own verbs, as the `mesh-controller` seat's tools (novox/hq ADR 0154).
|
|
//
|
|
// **The same function the command line calls, and nothing the tool adds** (ADR 0035): each of these
|
|
// is a command the controller's binary already answers, run by the holder of the seat with the
|
|
// arguments below and answered with what the command printed. A verb here is a contract every future
|
|
// holder must implement, which is why the list is short and made of what an operator asks weekly.
|
|
// Additive within a version (design 33 §7); a verb that would break a caller takes a new version.
|
|
var ControllerVerbs = []Verb{
|
|
{Name: "tools", Description: "Every seat's tools, from the mesh's own records: what each role " +
|
|
"answers, whether or not its holder is up. The mesh's own verbs are the mesh-controller seat's.",
|
|
Input: schema(nil, nil)},
|
|
{Name: "calls", Description: "The calls this controller answered lately and what came of each — " +
|
|
"one still running, one that finished after its caller was told it was running, one whose answer " +
|
|
"the bus refused — and, given a call's id, its whole answer (novox/hq issue 265). A call that has " +
|
|
"not finished within ten seconds answers that it is running, with its id; this is where it ends.",
|
|
Input: schema(map[string]string{
|
|
"call": "a call's id, as a running answer or `calls` gives it: that call and its whole answer",
|
|
}, nil)},
|
|
{Name: "status", Description: "What is wrong, what is quiet, what is out of date, and which " +
|
|
"machines are behind what the mesh would send them.",
|
|
Input: schema(nil, nil)},
|
|
{Name: "nodes", Description: "Every machine the mesh knows, with whether it is converged or adopted.",
|
|
Input: schema(nil, nil)},
|
|
{Name: "node", Description: "What one machine reported it can do, what it is assigned, and why.",
|
|
Input: schema(map[string]string{"node": "the machine's name"}, []string{"node"})},
|
|
{Name: "modules", Description: "Every module the mesh holds: version, the commit it was built from, " +
|
|
"and which machines run it.",
|
|
Input: schema(nil, nil)},
|
|
{Name: "seats", Description: "Every seat the mesh defines, what it delivers, and who holds it.",
|
|
Input: schema(nil, nil)},
|
|
{Name: "builds", Description: "What has been built lately and what came of it, for every module or for one; " +
|
|
"or, given a build's id, everything the build machine said while building it, line by line, from the bus.",
|
|
Input: schema(map[string]string{
|
|
"module": "one module's name; every module when absent",
|
|
"log": "a build's id (as `builds` lists it): print what the build machine said, line by line",
|
|
"limit": "how many builds to list (default 20); not with log",
|
|
}, nil)},
|
|
{Name: "plans", Description: "What the last merges produced and where each stands (novox/hq ADR 0162): " +
|
|
"the tiers, the tier a plan is at, what it waits for and since when; one plan whole, given its id.",
|
|
Input: schema(map[string]string{
|
|
"id": "a plan's id (as `plans` lists them): that plan, tier by tier",
|
|
"stop": "a plan's id: stop it — what was asked still builds, nothing further is asked",
|
|
"close": "a plan's id: close a plan that will not move again, as failed by hand (novox/hq issue 254)",
|
|
"retry": "a failed plan's id: ask its failed builds again under new ids, and carry the plan on from that tier (novox/hq ADR 0219)",
|
|
"repository": "owner/repository: the plan a merge there would produce, saving nothing (what-if); with paths or modules",
|
|
"paths": "with repository: the files the merge would change, comma-separated, from the repository's root",
|
|
"modules": "with repository: or the modules it would change, comma-separated",
|
|
"limit": "how many plans to list (default 10); only when listing",
|
|
"why": "with stop or close: why it is ended by hand — required, and recorded in the hand-act log (novox/hq to-be 45 §7)",
|
|
"cause": "with stop or close: the cause in a word, or a condition's kind (optional)",
|
|
}, nil)},
|
|
{Name: "plan", Description: "What one machine would run, and why: the declaration the mesh would send it — " +
|
|
"or, with files, the files it would be given.",
|
|
Input: schema(map[string]string{
|
|
"node": "the machine's name",
|
|
"files": "\"true\": the files this machine would be given, instead of the declaration as JSON",
|
|
}, []string{"node"}, "files")},
|
|
{Name: "assign", Description: "Put a module on a machine. Refused with the mesh's own words when it cannot resolve there, " +
|
|
"or when a seat its resources are applied through is held by nothing on the machine (novox/hq ADR 0207).",
|
|
Input: schema(map[string]string{"node": "the machine's name",
|
|
"module": "the module's name; several comma-separated are judged together"}, []string{"node", "module"})},
|
|
{Name: "unassign", Description: "Take a module off a machine. Refused when it holds a seat a module left there depends on.",
|
|
Input: schema(map[string]string{"node": "the machine's name",
|
|
"module": "the module's name; several comma-separated are judged together"}, []string{"node", "module"})},
|
|
{Name: "pin", Description: "Tell a machine which provider answers a provision for it — the module, and the node " +
|
|
"it runs on, both. Asked for when more than one could answer; the refusal lists them.",
|
|
Input: schema(map[string]string{
|
|
"node": "the machine's name",
|
|
"provision": "the provision, as the consumer requires it",
|
|
"from": "the node the chosen provider runs on",
|
|
"module": "the module providing it there",
|
|
}, []string{"node", "provision", "from", "module"})},
|
|
{Name: "unpin", Description: "Take that choice back, putting the question to the mesh again.",
|
|
Input: schema(map[string]string{"node": "the machine's name", "provision": "the provision"}, []string{"node", "provision"})},
|
|
{Name: "push", Description: "Send one machine everything it should be. With no machine named it is a push of the " +
|
|
"WHOLE mesh — every machine that is behind — and the answer says so first; behind says that outright. " +
|
|
"Answers at once that it is running, with a call id: `calls` with that id says what it sent " +
|
|
"(a push can reload the bus, which then refuses any answer still to come). A push by hand is a repair, " +
|
|
"and says why: recorded in the hand-act log (novox/hq to-be 45 §7).",
|
|
Input: schema(map[string]string{
|
|
"node": "the machine's name; without it, every machine that is behind",
|
|
"behind": "\"true\": every machine that is behind, the whole mesh — the same as naming none, said outright; not with node",
|
|
"why": "why this is pushed by hand: recorded in the hand-act log",
|
|
"cause": "the cause in a word, or a condition's kind — the word a second push for the same reason uses (optional)",
|
|
}, []string{"why"}, "behind")},
|
|
{Name: "rotate", Description: "Replace a credential. A pair credential, by provision (and a consuming machine and module, " +
|
|
"else every holder): both ends are re-sent together. Or a module's own secret, by machine, module and " +
|
|
"name: made anew and the machine sent, so the module starts again on it — for a secret its definition " +
|
|
"says it reads at start, whether the mesh made the value or a person gave it (novox/hq ADR 0228); one " +
|
|
"an outside party issued, or one the module applies to a backend, is refused with the reason.",
|
|
Input: schema(map[string]string{
|
|
"provision": "a pair credential: the provision whose credential to replace",
|
|
"consumer": "with provision: only the holder on this machine (optional)",
|
|
"node": "an own secret: the machine",
|
|
"module": "an own secret: the module; with provision: only this consuming module's credential (optional)",
|
|
"secret": "an own secret: its name in the module's definition",
|
|
"why": "an own secret: why it is rotated — recorded in the hand-act log (optional)",
|
|
"cause": "with why: the cause in a word, the word a second rotation for the same reason uses (optional)",
|
|
}, nil)},
|
|
{Name: "issue", Description: "Give a module on a machine its account on the bus: minted, and sealed to the " +
|
|
"machine as the module's own secret named broker, read at the next push of that machine. For a module " +
|
|
"whose definition declares that secret; refused with the reason otherwise. Issued again, it replaces the account.",
|
|
Input: schema(map[string]string{
|
|
"node": "the machine that runs the module",
|
|
"module": "the module's name",
|
|
}, []string{"node", "module"})},
|
|
{Name: "settings", Description: "Set what an assignment is configured with: a module's settings for the whole mesh, " +
|
|
"or for one machine. Replaces that layer whole — what it does not name, it no longer sets — and takes effect " +
|
|
"at the next push. With clear, removes the layer and the module is back to what its definition says.",
|
|
Input: schema(map[string]string{
|
|
"module": "the module's name",
|
|
"values": "the settings as a JSON object, for set",
|
|
"node": "one machine; the whole mesh when absent",
|
|
"clear": "\"true\" to remove the layer instead of setting it; not with values",
|
|
}, []string{"module"}, "clear")},
|
|
{Name: "command", Description: "Run one command line of the controller's own, as you would type it at its " +
|
|
"shell — `node account g14 jochen`, `node show ace`, `module list` — and answer what it printed. The " +
|
|
"generic verb beside the named ones (novox/hq ADR 0154): everything the binary can do, without a verb " +
|
|
"per command. Any node may call any tool (ADR 0175), so nothing is held back here.",
|
|
Input: schema(map[string]string{
|
|
"command": "the command line, as the controller's binary takes it; quotes group a word with spaces",
|
|
}, []string{"command"})},
|
|
// The build queue, controlled by hand (novox/hq ADR 0219). Every verb that drops an ask leaves a
|
|
// failed outcome for it, so a plan waiting on it fails visibly instead of hanging.
|
|
{Name: "queue", Description: "Every ask in the build queue: waiting, in flight (on which machine, for how long), " +
|
|
"and dead (handed out as often as allowed and never settled) — each with its id, repository, path, ref and when it was asked.",
|
|
Input: schema(nil, nil)},
|
|
{Name: "cancel", Description: "Drop one waiting or dead ask from the build queue; its outcome is recorded failed, " +
|
|
"cancelled by hand, and a plan that asked for it fails. One in flight is refused: `kill` ends it where it runs.",
|
|
Input: schema(map[string]string{"id": "the ask's build id, as `queue` lists it"}, []string{"id"})},
|
|
{Name: "clear", Description: "Cancel every waiting ask in the build queue — and with dead, every dead one too — each " +
|
|
"recorded failed, cancelled by hand. Never touches one in flight.",
|
|
Input: schema(map[string]string{"dead": "\"true\" to cancel the dead asks as well"}, nil, "dead")},
|
|
{Name: "rebuild", Description: "Ask a module's current source again under a new id — the branch it follows — or, given a " +
|
|
"build's id, that build's repository, path and ref. A module a plan holds unbuilt or failed joins that plan. Answers the new id.",
|
|
Input: schema(map[string]string{"what": "a module's name, or a build's id"}, []string{"what"})},
|
|
{Name: "replay", Description: "Ask a recorded build's repository and path again at the commit it built, under a new id. " +
|
|
"A dry run unless register: nothing recorded or registered. Registering is refused when a newer build of the module " +
|
|
"is registered — it would roll the older commit out (novox/hq issue 207) — unless older says so.",
|
|
Input: schema(map[string]string{
|
|
"id": "the build's id",
|
|
"register": "\"true\" to register what it builds",
|
|
"older": "\"true\", with register: even though a newer build of the module is registered",
|
|
}, []string{"id"}, "register", "older")},
|
|
{Name: "kill", Description: "End a build where it runs: the machine that took it stops its commands and containers and " +
|
|
"announces it failed, killed by hand — settled, never handed to another machine.",
|
|
Input: schema(map[string]string{"id": "the build's id"}, []string{"id"})},
|
|
{Name: "pause", Description: "The build seat's holder on one machine — or every holder — takes no new build until resumed; " +
|
|
"a build running finishes. Kept across a restart of the holder. A plan waiting on a paused seat says so and is not late.",
|
|
Input: schema(map[string]string{"node": "one machine; every holder when absent"}, nil)},
|
|
{Name: "resume", Description: "The build seat's holder on one machine — or every holder — takes builds again.",
|
|
Input: schema(map[string]string{"node": "one machine; every holder when absent"}, nil)},
|
|
// Acts done by hand, and what the bounds are set from (novox/hq to-be 45 §7, Phase 0).
|
|
{Name: "hand-act", Description: "Record an act done by hand outside the mesh — a container restarted, a file " +
|
|
"edited, a service started on a machine — with why and its cause, in the hand-act log beside the pushes and " +
|
|
"plans ended by hand (novox/hq to-be 45 §7). A cause recorded twice in a fortnight is a healer wanted.",
|
|
Input: schema(map[string]string{
|
|
"what": "what was done, in a line",
|
|
"why": "why it had to be done by hand",
|
|
"cause": "the cause in a word, or a condition's kind — the word a second act for the same reason uses",
|
|
"condition": "the key of the condition it addressed, if any (optional)",
|
|
}, []string{"what", "why", "cause"})},
|
|
{Name: "hand-acts", Description: "What was done by hand lately — pushes, plans ended, consumers re-made, acts " +
|
|
"recorded — who, why and the cause of each, and which causes repeat: each repeat is a healer the mesh lacks.",
|
|
Input: schema(map[string]string{"days": "how many days back (default 14)"}, nil)},
|
|
{Name: "durations", Description: "How long things take, as the controller measured them: a send to its machine's " +
|
|
"report (apply), a machine's silence between words (heartbeat-gap), a plan's tier, a build — per machine, " +
|
|
"repository or module, with median, p90 and max. What the core's bounds are set from (novox/hq to-be 45 Phase 0).",
|
|
Input: schema(map[string]string{
|
|
"kind": "one kind: apply, heartbeat-gap, plan-tier or build; every kind when absent",
|
|
"days": "how many days back (default 14)",
|
|
}, nil)},
|
|
// What is wrong, and the self-check (novox/hq to-be 45 §2, §4).
|
|
{Name: "conditions", Description: "What is wrong with the mesh now: every open condition, urgent first, " +
|
|
"then oldest — raised by the watchdogs of the signals table, the self-check's probes and the providers' " +
|
|
"own words, and cleared when observation says it is resolved, never by hand. Given a key, that one " +
|
|
"whole with its evidence; with history, every transition lately; with silence, stop one's messages " +
|
|
"for a while — a hand act, which says why (novox/hq to-be 45 §2).",
|
|
Input: schema(map[string]string{
|
|
"key": "a condition's key: that one whole; with history, only its transitions",
|
|
"scope": "only this scope: machine, plan, call, build, merge, provider, seat, bus, core, probe or mesh",
|
|
"severity": "only urgent, or only warning",
|
|
"machine": "only those about this machine",
|
|
"history": "\"true\": every raising, change, silence and clearing lately, oldest first",
|
|
"days": "with history: how many days back (default 7, at most 90)",
|
|
"silence": "a condition's key: send no message for it for a while; it stays open and in status",
|
|
"for": "with silence: how long — 30m, 4h, 2d; at most 7d",
|
|
"why": "with silence: why — required, and recorded in the hand-act log",
|
|
"cause": "with silence: the cause in a word (the condition's kind when absent)",
|
|
}, nil, "history")},
|
|
// What the healers did (novox/hq to-be 45 §7).
|
|
{Name: "healers", Description: "The healers (novox/hq to-be 45 §7): each registered response to one kind " +
|
|
"of condition — its repair (the ordinary path again), its budget, what happens when it is spent — every act " +
|
|
"they took lately with its outcome, and the mesh-wide brake. A heal is never a hand act; whether it " +
|
|
"repaired anything is the condition's clearing to say. Each act is also in its condition's tried.",
|
|
Input: schema(map[string]string{"days": "how many days of acts back (default 7)"}, nil)},
|
|
{Name: "doctor", Description: "The self-check (novox/hq to-be 45 §4): the last run's verdict at once — " +
|
|
"each probe of the design's live invariants passed, failed or could not run, and how long ago. With " +
|
|
"run, a run now; with probes, the registry; with signals, every row of the signals table and the age " +
|
|
"of its newest signal. Runs every five minutes on its own; each failure is an open condition.",
|
|
Input: schema(map[string]string{
|
|
"run": "\"true\": run every probe now and answer the verdict",
|
|
"probes": "\"true\": the registry — what each probe asserts, and the condition it raises",
|
|
"signals": "\"true\": the signals table, each row with the age of its newest signal",
|
|
}, nil, "run", "probes", "signals")},
|
|
{Name: "build", Description: "Have the build machine build a repository. Answers at once with the build's id: " +
|
|
"`builds` with that id follows it line by line, and the module is registered when the outcome comes.",
|
|
Input: schema(map[string]string{
|
|
"repository": "the repository's URL, or its path on the forge holding the git seat (owner/name)",
|
|
"path": "the module's directory inside it (optional)",
|
|
"ref": "the branch, tag or commit to build (optional)",
|
|
}, []string{"repository"})},
|
|
}
|
|
|
|
// schema is a JSON schema for an object of string properties, which is every argument the verbs
|
|
// above take. Kept small on purpose: a schema an agent cannot read is a tool it cannot call. The
|
|
// switches are the properties that are "true" or "false" and nothing else, said in the schema as an
|
|
// enum so a caller sees it and the verb can refuse any other word rather than read it as false.
|
|
//
|
|
// **The schema is the whole of what a verb takes** (novox/hq issue 244): an argument it does not
|
|
// name is refused when the verb is called, never passed over.
|
|
func schema(properties map[string]string, required []string, switches ...string) map[string]any {
|
|
isSwitch := map[string]bool{}
|
|
for _, s := range switches {
|
|
if _, declared := properties[s]; !declared {
|
|
panic("a switch that is not a property: " + s)
|
|
}
|
|
isSwitch[s] = true
|
|
}
|
|
props := map[string]any{}
|
|
for name, description := range properties {
|
|
p := map[string]any{"type": "string", "description": description}
|
|
if isSwitch[name] {
|
|
p["enum"] = []string{"true", "false"}
|
|
}
|
|
props[name] = p
|
|
}
|
|
out := map[string]any{"type": "object", "properties": props}
|
|
if len(required) > 0 {
|
|
out["required"] = required
|
|
}
|
|
return out
|
|
}
|
|
|
|
// unpromised is what a claim says it serves and the seat's protocol never promised.
|
|
func unpromised(serves []string, promised []Verb) []string {
|
|
has := map[string]bool{}
|
|
for _, v := range promised {
|
|
has[v.Name] = true
|
|
}
|
|
var extra []string
|
|
for _, s := range serves {
|
|
if !has[s] {
|
|
extra = append(extra, s)
|
|
}
|
|
}
|
|
return extra
|
|
}
|
|
|
|
// unservedVerbs is what a seat promises and a claimant's offer for it does not answer.
|
|
func unservedVerbs(tools []string, promised []Verb) []string {
|
|
has := map[string]bool{}
|
|
for _, t := range tools {
|
|
has[t] = true
|
|
}
|
|
var missing []string
|
|
for _, v := range promised {
|
|
if !has[v.Name] {
|
|
missing = append(missing, v.Name)
|
|
}
|
|
}
|
|
return missing
|
|
}
|