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: "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", }, nil)}, {Name: "plan", Description: "What one machine would run, and why: the declaration the mesh would send it.", Input: schema(map[string]string{"node": "the machine's name"}, []string{"node"})}, {Name: "assign", Description: "Put a module on a machine. Refused with the mesh's own words when it cannot resolve there.", Input: schema(map[string]string{"node": "the machine's name", "module": "the module's name"}, []string{"node", "module"})}, {Name: "unassign", Description: "Take a module off a machine.", Input: schema(map[string]string{"node": "the machine's name", "module": "the module's name"}, []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 a machine everything it should be — or every machine that is behind, when no machine is named.", Input: schema(map[string]string{"node": "the machine's name; every machine behind when absent"}, nil)}, {Name: "rotate", Description: "Replace a credential. A pair credential, by provision (and a consuming machine, " + "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", "secret": "an own secret: its name in the module's definition", }, nil)}, {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. 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 } // 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 }