Files
mesh-controller/internal/catalogue/verbs.go
T
jschoubben e9df5dccab The mesh's own verbs are the mesh-controller seat's tools
A seat's protocol lives in the store (migration 0047; seeded additively), a served verb carries its
description and schema, holding a mesh seat requires serving its verbs, a node-scoped seat's tool
carries the node, and the control plane serves status, nodes, node, modules, seats, builds, plan,
assign, unassign, push, build and tools on its seat by running the same commands (novox/hq ADR 0132,
ADR 0154, design 33). A grant of * reaches a role's tools; seat:<seat>.<verb> grants one.
2026-09-30 17:39:38 +02:00

134 lines
6.1 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: "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.",
Input: schema(map[string]string{"module": "one module's name; every module when absent"}, 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: "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: "build", Description: "Have the build machine build a repository and record what came out.",
Input: schema(map[string]string{
"repository": "the repository's URL, or its path on the forge holding the git seat",
"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
}
// unservedVerbs is what a seat promises and a claimant's `tools` 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
}