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 — only for a secret its " + "definition says it reads at start; a value given to the mesh, 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", }, 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")}, {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 }