token issue --overlay-key records the key the machine made, binds the token to it, gives the machine its address and makes it a peer of the hub, pushing the hub before the token is shown. The token carries the hub's tunnel and the bus at its address on the private network, and enrolment refuses any other key (novox/hq ADR 0169). Tokens without a key enrol as before until the bus is closed. Also a token verb.
210 lines
11 KiB
Go
210 lines
11 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; " +
|
|
"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: "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",
|
|
"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",
|
|
}, 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: "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: "token", Description: "Issue a one-time token for a machine to join with. Give the public half of the " +
|
|
"tunnel key the machine made (`nox-mesh-host key`): the machine is given its address and made a peer of " +
|
|
"the hub, and joins through the tunnel. The token is shown once, in the answer.",
|
|
Input: schema(map[string]string{
|
|
"node": "a machine the mesh already has a record for",
|
|
"new": "or the name of a machine to create the record for",
|
|
"overlay_key": "the public half of the machine's tunnel key",
|
|
"for": "how long it may be used, as a duration (default 1h)",
|
|
"adopted": "\"true\" when the machine is in use and joins adopted",
|
|
}, nil)},
|
|
{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",
|
|
}, []string{"module"})},
|
|
{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
|
|
}
|