package main import ( "bytes" "context" "encoding/json" "errors" "fmt" "github.com/nats-io/nats.go/micro" "io" "os" "os/exec" "slices" "sort" "strings" "github.com/novox/mesh-controller/internal/broker" "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/link" ) // The mesh's own verbs, served as the mesh-controller seat's tools (novox/hq ADR 0154, design 33). // // **Each tool runs the command it names, in this same binary, and answers what it printed.** That is // ADR 0035 taken literally: the logic lives once, in the command, and a surface is an adapter with no // decisions in it. Running a fresh process rather than calling the function keeps two things true // that calling it would not — every command opens and closes its own stores the way it does from a // shell, and nothing a command prints to the process's standard output can leak into another call's // answer. It also means a refusal is the same refusal in the same words, because it is the same // output. // verbKey carries the verb a call is for, to the process it runs. type verbKey struct{} // verbAnswer is what a verb answers: what the command printed, whether it succeeded, and — where the // command speaks JSON — the same as data. type verbAnswer struct { Output string `json:"output"` OK bool `json:"ok"` Answer any `json:"answer,omitempty"` } // argvFor is the command line a verb and its arguments become. Only the verbs the seat declares, and // only the arguments each declares: a caller cannot reach a flag the schema did not name. // // **Nothing a caller sends is passed over** (novox/hq issue 244). An argument the verb does not // declare is refused, naming it; a switch that is not "true" or "false" is refused; and an argument // the verb declares but did not use for the command line it composed — given beside another that // wins, or half of a shape — is refused too. On 2026-10-05 a push naming one machine reached the // verb without the machine and ran as a push of every machine behind; a verb that answers "I did // not take that" would have stopped it before anything was sent. func argvFor(verb string, args map[string]any) ([]string, error) { a, err := readArguments(verb, args) if err != nil { return nil, err } argv, err := a.commandLine() if err == nil { err = terminalOnly(argv) } if len(a.misread) > 0 { // The table and the command line disagree: the verb reads an argument no caller can see // in its schema, so no caller could ever pass it. return nil, fmt.Errorf("%s reads %s, which its schema does not declare — this build's verb "+ "table and its command lines disagree", verb, quoteAll(a.misread)) } if err != nil { return nil, err } if unused := a.unused(); len(unused) > 0 { return nil, fmt.Errorf("%s did not use %s together with %s, and an argument a verb would pass over "+ "is refused: nothing was done", verb, quoteAll(unused), quoteAll(a.usedGiven())) } return argv, nil } // verbArguments are one call's arguments, checked against the verb's schema, and which of them the // command line was composed from. type verbArguments struct { verb string given map[string]string used map[string]bool declared map[string]bool misread []string // arguments the command line read that the schema does not declare: a bug here } // controllerVerb is this binary's own definition of a verb: what it runs is what it declares, so the // arguments are checked against the table compiled beside argvFor, not a row a newer or older build // wrote. func controllerVerb(name string) (catalogue.Verb, bool) { for _, v := range catalogue.ControllerVerbs { if v.Name == name { return v, true } } return catalogue.Verb{}, false } // declaredArguments are a schema's properties, and which of them are switches. func declaredArguments(v catalogue.Verb) (names []string, switches map[string]bool) { switches = map[string]bool{} props, _ := v.Input["properties"].(map[string]any) for name, p := range props { names = append(names, name) desc, _ := p.(map[string]any) switch enum := desc["enum"].(type) { case []string: switches[name] = len(enum) == 2 && enum[0] == "true" && enum[1] == "false" case []any: switches[name] = len(enum) == 2 && enum[0] == "true" && enum[1] == "false" } } sort.Strings(names) return names, switches } // isList says a verb's argument is declared a list of text (catalogue's listed): given as a JSON array, it is // read as its items joined by commas, as the same argument given as one text would be. func isList(v catalogue.Verb, name string) bool { props, _ := v.Input["properties"].(map[string]any) p, _ := props[name].(map[string]any) return p != nil && p["type"] == "array" } // readArguments refuses what the verb does not take, before anything is composed. func readArguments(verb string, args map[string]any) (*verbArguments, error) { v, known := controllerVerb(verb) if !known { return nil, fmt.Errorf("%q is not a verb the %s seat serves", verb, catalogue.ControllerSeatName) } names, switches := declaredArguments(v) declared := map[string]bool{} for _, n := range names { declared[n] = true } takes := "none" if len(names) > 0 { takes = quoteAll(names) } a := &verbArguments{verb: verb, given: map[string]string{}, used: map[string]bool{}, declared: declared} keys := make([]string, 0, len(args)) for k := range args { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { if !declared[k] { return nil, fmt.Errorf("%s takes no argument %q — it takes %s; nothing was done", verb, k, takes) } var value string switch x := args[k].(type) { case nil: continue case string: value = strings.TrimSpace(x) case bool: if !switches[k] { return nil, fmt.Errorf("%s: %q is text, not true or false", verb, k) } value = fmt.Sprint(x) case []any: if !isList(v, k) { return nil, fmt.Errorf("%s: %q is text, and was given a list", verb, k) } items := make([]string, 0, len(x)) for _, item := range x { text, ok := item.(string) if !ok || strings.TrimSpace(text) == "" || strings.Contains(text, ",") { return nil, fmt.Errorf("%s: %q is a list of names, and holds %v", verb, k, item) } items = append(items, strings.TrimSpace(text)) } value = strings.Join(items, ",") default: return nil, fmt.Errorf("%s: %q is text, and was given %T", verb, k, x) } if switches[k] { switch value { case "true": case "false", "": continue // said and off: the same as not given, and nothing passed over default: return nil, fmt.Errorf("%s: %q is \"true\" or \"false\", not %q", verb, k, value) } } if value != "" { a.given[k] = value } } return a, nil } // str is one argument's value, marked as used. func (a *verbArguments) str(key string) string { if !a.declared[key] { a.misread = append(a.misread, key) } a.used[key] = true return a.given[key] } // on is a switch, marked as used. func (a *verbArguments) on(key string) bool { return a.str(key) == "true" } // need refuses a call missing a required argument, in the verb's own words. func (a *verbArguments) need(keys ...string) error { for _, k := range keys { if a.str(k) == "" { return fmt.Errorf("%s needs %q", a.verb, k) } } return nil } // unused are the arguments given that the command line was not composed from. func (a *verbArguments) unused() []string { var out []string for k := range a.given { if !a.used[k] { out = append(out, k) } } sort.Strings(out) return out } func (a *verbArguments) usedGiven() []string { var out []string for k := range a.given { if a.used[k] { out = append(out, k) } } sort.Strings(out) if len(out) == 0 { return []string{"nothing"} } return out } func quoteAll(xs []string) string { q := make([]string, len(xs)) for i, x := range xs { if x == "nothing" { q[i] = x continue } q[i] = fmt.Sprintf("%q", x) } return strings.Join(q, ", ") } // refusedAsTheGenericCommand is what the generic `command` verb refuses of a command line, and so what every // line asked through a verb's route refuses: the `command` verb's, and an ordinary line from mesh-cli (novox/hq ADR // 0272 §4). One function, so the two routes cannot drift apart. func refusedAsTheGenericCommand(argv []string) error { if len(argv) == 0 { return errors.New("command names no command") } // A layer is written through the settings verb, never the generic one (novox/hq issue 339): the // settings verb is where what a verb may not set is refused, and one route is one set of words. // The command refuses places and accesses through any verb as well; this says so before it runs. if argv[0] == "settings" && slices.ContainsFunc(argv[1:], func(w string) bool { return w == "set" || w == "clear" }) { return &heldAtTheTerminal{msg: "settings are set and cleared through the settings verb, not the " + "generic command; and places and accesses only at the controller's terminal (novox/hq issue 339). " + "Nothing was done"} } // The generic verb only reads (novox/hq ADR 0266): what writes has a named verb that composes its own // line, or is the operator's at the controller's terminal. if err := commandReads(argv); err != nil { return err } // The generic verb is no way round the hand-act log (novox/hq to-be 45 §7): a repair through // it says why, as it would through its own verb. if repair := repairingCommand(argv); repair != "" && !slices.ContainsFunc(argv, isWhyFlag) { return fmt.Errorf("%s is a repair done by hand, and says why: add --why to the command "+ "line (recorded in the hand-act log). Nothing was done", repair) } return nil } // commandLine composes the command. Every argument it reads is one it uses: a branch that reads an // argument and then drops it would pass it over, which is what the check after it exists to refuse. func (a *verbArguments) commandLine() ([]string, error) { verb, str, on, need := a.verb, a.str, a.on, a.need switch verb { case "command": // The generic verb: the command line as given, split as a shell would split it, with // nothing added — the named verbs add flags a caller cannot reach; this one is the whole // binary and says so in its description (novox/hq ADR 0154, 0175). if err := need("command"); err != nil { return nil, err } argv, err := splitCommandLine(str("command")) if err != nil { return nil, err } if err := refusedAsTheGenericCommand(argv); err != nil { return nil, err } return argv, nil case "tools": return nil, errors.New("tools is answered from the records, not by a command") case "dead-letters": return nil, errors.New("dead-letters is answered by the serving controller, on its own connection, not by a command") case "root-free": return nil, errors.New("root-free is judged by the serving controller, on its own connection, not by a command") case "status": return []string{"status", "--json"}, nil case "nodes": return []string{"node", "list", "--json"}, nil case "node": if err := need("node"); err != nil { return nil, err } return []string{"node", "show", str("node")}, nil case "modules": return []string{"module", "list", "--json"}, nil case "seats": return []string{"seats", "--json"}, nil case "builds": if id := str("log"); id != "" { return []string{"builds", "--log", id}, nil } argv := []string{"builds"} if n := str("limit"); n != "" { argv = append(argv, "-n", n) } if m := str("module"); m != "" { argv = append(argv, m) } return argv, nil case "plans": if r := str("repository"); r != "" { argv := []string{"plans", "--what-if", r} if p := str("paths"); p != "" { argv = append(argv, "--paths", p) } if m := str("modules"); m != "" { argv = append(argv, "--modules", m) } if d := str("module-dirs"); d != "" { argv = append(argv, "--module-dirs", d) } return argv, nil } for _, act := range []string{"stop", "close", "retry", "go"} { if id := str(act); id != "" { argv := []string{"plans", act, id} if act == "retry" { return argv, nil } // Ending a plan by hand says why (novox/hq to-be 45 §7); the command refuses it without. if w := str("why"); w != "" { argv = append(argv, "--why", w) } if c := str("cause"); c != "" { argv = append(argv, "--cause", c) } return argv, nil } } if id := str("id"); id != "" { return []string{"plans", id}, nil } argv := []string{"plans"} if n := str("limit"); n != "" { argv = append(argv, "-n", n) } return argv, nil // The delivery's owner's verbs (novox/hq ADR 0239). case "delivery-plan": if err := need("repository", "paths"); err != nil { return nil, err } argv := []string{"delivery", "plan", "--repository", str("repository"), "--paths", str("paths")} for _, flag := range []string{"head", "base", "module-dirs", "removed"} { if v := str(flag); v != "" { argv = append(argv, "--"+flag, v) } } return argv, nil case "delivery-order": if err := need("members"); err != nil { return nil, err } return []string{"delivery", "order", "--members", str("members")}, nil case "delivery-check": if err := need("members"); err != nil { return nil, err } if g := str("group"); g != "" { return []string{"delivery", "check", "--group", g, "--members", str("members")}, nil } return []string{"delivery", "check", "--members", str("members")}, nil case "deliver": if err := need("plan"); err != nil { return nil, err } argv := []string{"delivery", "go", str("plan"), "--by", catalogue.DeliverySeat} if w := str("why"); w != "" { argv = append(argv, "--why", w) } return argv, nil case "delivery-stop": if err := need("plan", "why"); err != nil { return nil, err } argv := []string{"delivery", "stop", str("plan"), "--why", str("why")} if b := str("by"); b != "" { argv = append(argv, "--by", catalogue.DeliverySeat+" for "+b) } return argv, nil case "delivery-walks": argv := []string{"delivery", "walks"} if id := str("plan"); id != "" { return append(argv, "--plan", id), nil } if n := str("limit"); n != "" { argv = append(argv, "-n", n) } return argv, nil // The build queue (novox/hq ADR 0219). case "queue": return []string{"queue"}, nil case "cancel", "kill": if err := need("id"); err != nil { return nil, err } return []string{verb, str("id")}, nil case "clear": if on("dead") { return []string{"clear", "--dead"}, nil } return []string{"clear"}, nil case "rebuild": if err := need("what"); err != nil { return nil, err } return []string{"rebuild", str("what")}, nil case "replay": if err := need("id"); err != nil { return nil, err } argv := []string{"replay", str("id")} if on("register") { argv = append(argv, "--register") } if on("older") { argv = append(argv, "--older") } return argv, nil case "pause", "resume": if n := str("node"); n != "" { return []string{verb, n}, nil } return []string{verb}, nil case "plan": if err := need("node"); err != nil { return nil, err } if on("files") { return []string{"plan", str("node"), "--files"}, nil } // What a push would change there (novox/hq ADR 0217); diff with files is passed over, and so refused. if on("diff") { return []string{"plan", str("node"), "--diff"}, nil } return []string{"plan", str("node"), "--json"}, nil case "assign", "unassign": if err := need("node", "module"); err != nil { return nil, err } // Several modules comma-separated, judged as one act (novox/hq ADR 0207): the holders of // the seats that apply resources depend on each other and go on together. argv := append([]string{verb, str("node")}, splitModules(str("module"))...) // A module known and not built: its build asked for, the assignment pending on it (novox/hq // issue 325). unassign declares no build, so a call giving it is refused before this. if verb == "assign" && on("build") { argv = append(argv, "--build") } return argv, nil case "pin": if err := need("node", "provision", "from", "module"); err != nil { return nil, err } return []string{"pin", str("node"), str("provision"), str("from"), str("module")}, nil case "unpin": if err := need("node", "provision"); err != nil { return nil, err } return []string{"unpin", str("node"), str("provision")}, nil case "push": // Sent and not waited for: the asker reads `status` for what the machine did, which is // what a person at a shell does too. A tool call that blocked for a push's whole apply would // time out on every machine that takes a minute, and say nothing about the ones that did not. // A push through the seat is a push by hand, and says why (novox/hq to-be 45 §7). if err := need("why"); err != nil { return nil, fmt.Errorf("%w: a push by hand is a repair, recorded in the hand-act log with why", err) } why := []string{"--why", str("why")} if c := str("cause"); c != "" { why = append(why, "--cause", c) } if n := str("node"); n != "" { // behind is not read here: given with a machine, it is refused as passed over — naming // a machine and asking for every machine behind are two requests, and guessing one // would push a machine nobody named, or not push one somebody did. argv := []string{"push", n, "--wait", "0"} // A running module's data moving is sent only when said, per module (novox/hq ADR 0217). if m := str("move"); m != "" { argv = append(argv, "--move", m) } return append(argv, why...), nil } // No machine: the whole mesh, whether or not behind said so. The command's answer says it // first, so a caller who meant one machine reads that it was not one. move is not read: a // machine whose data would move is held and named, and sent by a push that names it. on("behind") return append([]string{"push", "--behind", "--wait", "0"}, why...), nil case "hand-act": if err := need("what", "why", "cause"); err != nil { return nil, err } argv := []string{"hand-act", "record", str("what"), "--why", str("why"), "--cause", str("cause")} if c := str("condition"); c != "" { argv = append(argv, "--condition", c) } return argv, nil case "drill": if err := need("what", "why"); err != nil { return nil, err } argv := []string{"hand-act", "drill", str("what"), "--why", str("why")} if c := str("condition"); c != "" { argv = append(argv, "--condition", c) } return argv, nil case "hand-acts": argv := []string{"hand-acts", "--json"} if d := str("days"); d != "" { argv = append(argv, "--days", d) } return argv, nil case "healers": argv := []string{"healers", "--json"} if d := str("days"); d != "" { argv = append(argv, "--days", d) } return argv, nil case "durations": argv := []string{"durations", "--json"} if k := str("kind"); k != "" { argv = append(argv, "--kind", k) } if d := str("days"); d != "" { argv = append(argv, "--days", d) } return argv, nil case "conditions": // One verb, four shapes, as `plans` (novox/hq to-be 45 §2): a silence, one condition, the // history, or the open ones filtered. if key := str("silence"); key != "" { if err := need("for", "why"); err != nil { return nil, err } argv := []string{"conditions", "silence", key, "--for", str("for"), "--why", str("why")} if c := str("cause"); c != "" { argv = append(argv, "--cause", c) } return argv, nil } if on("history") { argv := []string{"conditions", "history", "--json"} if d := str("days"); d != "" { argv = append(argv, "--days", d) } if k := str("key"); k != "" { argv = append(argv, "--key", k) } return argv, nil } if k := str("key"); k != "" { return []string{"conditions", "show", k, "--json"}, nil } argv := []string{"conditions", "--json"} for _, filter := range []string{"scope", "severity", "machine"} { if v := str(filter); v != "" { argv = append(argv, "--"+filter, v) } } return argv, nil case "retire": answer := str("answer") if answer == "" { return []string{"retire", "--json"}, nil } if answer != "approve" && answer != "reject" { return nil, fmt.Errorf("retire answers approve or reject, not %q", answer) } if err := need("node", "module", "why"); err != nil { return nil, err } argv := []string{"retire", answer, str("node"), str("module"), "--why", str("why")} if c := str("cause"); c != "" { argv = append(argv, "--cause", c) } return argv, nil case "cleanup": consumer, older := str("consumer"), str("older-than") switch { case consumer != "" && older != "": return nil, errors.New("cleanup deletes one consumer or those older than some days, not both") case consumer != "": if err := need("node", "module", "why"); err != nil { return nil, err } argv := []string{"cleanup", "delete", str("node"), str("module"), consumer, "--why", str("why")} if c := str("cause"); c != "" { argv = append(argv, "--cause", c) } return argv, nil case older != "": if err := need("why"); err != nil { return nil, err } argv := []string{"cleanup", "delete", "--older-than", older, "--why", str("why")} if on("confirm") { argv = append(argv, "--confirm") } if c := str("cause"); c != "" { argv = append(argv, "--cause", c) } return argv, nil } return []string{"cleanup", "list", "--json"}, nil // novox/hq ADR 0251. case "artifacts": argv := []string{"artifacts", "--json"} if r := str("repository"); r != "" { argv = append(argv, "--repository", r) } if on("collected") { argv = append(argv, "--collected") } return argv, nil case "collect": argv := []string{"collect", "--json"} if m := str("most"); m != "" { argv = append(argv, "--most", m) } why := str("why") if !on("confirm") { if why != "" { return nil, errors.New("collect takes why only with confirm: without confirm it is a dry run, " + "and a reason for nothing would be recorded nowhere. Nothing was done") } return argv, nil } if err := need("why"); err != nil { return nil, fmt.Errorf("%w: letting go of artifacts is a hand act, which says why. Nothing was done", err) } return append(argv, "--confirm", "--why", why), nil case "images": if err := need("node"); err != nil { return nil, err } return []string{"images", str("node"), "--json"}, nil case "mirrors": // novox/hq ADR 0257. argv := []string{"mirrors", "--json"} record := str("record") why := str("why") if record == "" { if on("confirm") || why != "" { return nil, errors.New("mirrors takes confirm and why only with record: there is nothing " + "else it records. Nothing was done") } return argv, nil } argv = append(argv, "--record", record) if !on("confirm") { if why != "" { return nil, errors.New("mirrors takes why only with confirm: without confirm it is a dry run, " + "and a reason for nothing would be recorded nowhere. Nothing was done") } return argv, nil } if err := need("why"); err != nil { return nil, fmt.Errorf("%w: recording a copy as the mesh's is a hand act, which says why. Nothing was done", err) } return append(argv, "--confirm", "--why", why), nil case "data": argv := []string{"data", "--json"} if m := str("machine"); m != "" { argv = append(argv, "--machine", m) } if on("retired") { argv = append(argv, "--retired") } return argv, nil case "upgrade": if on("backlog") { return []string{"upgrade", "backlog"}, nil } if on("release-backlog") { argv := []string{"upgrade", "release-backlog", "--why", str("why")} if c := str("cause"); c != "" { argv = append(argv, "--cause", c) } return argv, nil } m, policy := str("module"), str("policy") if m == "" { if policy != "" { return nil, errors.New("upgrade takes a policy only for a module") } return []string{"upgrade"}, nil } argv := []string{"upgrade", m} if policy != "" { argv = append(argv, policy) if on("together") { argv = append(argv, "--together") } if w := str("why"); w != "" { argv = append(argv, "--why", w) } } return argv, nil case "bus": if !on("upgrade") { return []string{"bus"}, nil } argv := []string{"bus", "upgrade", "--why", str("why")} if c := str("cause"); c != "" { argv = append(argv, "--cause", c) } if on("reversible") { argv = append(argv, "--reversible") } if on("irreversible") { argv = append(argv, "--irreversible") } if w := str("snapshot-taken"); w != "" { argv = append(argv, "--snapshot-taken", w) } return argv, nil case "doctor": which := 0 argv := []string{"doctor"} for _, sub := range []string{"run", "probes", "signals"} { if on(sub) { which++ argv = append(argv, sub) } } if which > 1 { return nil, errors.New("doctor answers one of run, probes or signals at a time") } if p := str("probe"); p != "" { argv = append(argv, "--probe", p) } return append(argv, "--json"), nil case "give": if err := need("node", "module", "secret", "at"); err != nil { return nil, err } return []string{"secret", "accept", str("node"), str("module"), str("secret"), "--at-desk", str("at")}, nil case "rotate": if p := str("provision"); p != "" { argv := []string{"rotate", p} if c := str("consumer"); c != "" { argv = append(argv, "--consumer", c) } // With a provision, module narrows to one consuming module (novox/hq issue 268); node // and secret stay the other shape's, and are refused as passed over. if m := str("module"); m != "" { argv = append(argv, "--module", m) } return argv, nil } _, node := a.given["node"] _, module := a.given["module"] _, secret := a.given["secret"] if node || module || secret { if err := need("node", "module", "secret"); err != nil { return nil, fmt.Errorf("%w: a module's own secret is named by node, module and secret together", err) } argv := []string{"secret", "rotate", str("node"), str("module"), str("secret")} // Why, recorded in the hand-act log (novox/hq ADR 0228); a cause only beside a why. if w := str("why"); w != "" { argv = append(argv, "--why", w) if c := str("cause"); c != "" { argv = append(argv, "--cause", c) } } return argv, nil } // Neither shape: the command says its usage, which names both, and that is the answer the // caller needs. return []string{"rotate"}, nil case "token": // `token issue` at a shell (novox/hq ADR 0169). Exactly one of node or new; the command // refuses both or neither in its own words. argv := []string{"token", "issue"} if n := str("node"); n != "" { argv = append(argv, "--node", n) } if n := str("new"); n != "" { argv = append(argv, "--new", n) } if k := str("overlay_key"); k != "" { argv = append(argv, "--overlay-key", k) } if d := str("for"); d != "" { argv = append(argv, "--for", d) } if str("adopted") == "true" { argv = append(argv, "--adopted") } return argv, nil case "settings": // Every module's preferences, their defaults and each machine's value (novox/hq ADR 0262): // the one interface for them, so no module builds a settings tool of its own. Asked for by // name, or by naming no module, since a layer is always some module's. if str("module") == "" && str("values") != "" { return nil, errors.New("settings: a module is needed to set values; name it with module") } if str("module") == "" && on("clear") { return nil, errors.New("settings: a module is needed to clear a layer; name it with module") } if list := str("list"); list != "" || str("module") == "" { if list != "" && list != "preferences" { return nil, fmt.Errorf("settings lists %q only; %q is not a listing", "preferences", list) } argv := []string{"settings", "preferences"} if m := str("module"); m != "" { argv = append(argv, m) } if n := str("node"); n != "" { argv = append(argv, "--node", n) } return argv, nil } // `settings set|clear` at a shell (novox/hq issue 198). The values travel as an argument // because a tool has no file to hand the command; the command reads either. if err := need("module"); err != nil { return nil, err } var argv []string switch { case on("clear"): argv = []string{"settings", "clear", str("module")} case str("values") != "": argv = []string{"settings", "set", str("module"), str("values")} // What a set removes is refused unless meant (novox/hq ADR 0217). if on("replace") { argv = append(argv, "--replace") } default: // Neither values nor clear: the layer as it stands, which is what a caller reads before // replacing it (novox/hq ADR 0217) — and with history, the layers it replaced. argv = []string{"settings", "show", str("module")} if on("history") { argv = append(argv, "--history") } } if n := str("node"); n != "" { argv = append(argv, "--node", n) } return argv, nil case "issue": // The same act as `module issue` at a shell (novox/hq design 25 §4): the account is minted // into the mesh's records and delivered at the machine's next push, which is the caller's to // ask for — so the mesh is never pushed as a side effect of a credential. if err := need("node", "module"); err != nil { return nil, err } return []string{"module", "issue", str("module"), "--node", str("node")}, nil case "build": // The command's three shapes (`build --on`, `build --behind`, `build `), one per // call: each branch reads only its own argument, so another given beside it is refused as // passed over rather than dropped. Asked and not waited for, the same as a single build. if b := str("on"); b != "" { return []string{"build", "--on", b, "--wait", "0"}, nil } if on("behind") { return []string{"build", "--behind", "--wait", "0"}, nil } if a.given["repository"] == "" { // No shape named: the command says its usage, which names all three — the answer the // caller needs, and the same as `rotate` given neither of its shapes. return []string{"build"}, nil } // Not waited for: a tool call cannot hold a connection for the minutes a build takes; the // daemon takes the outcome in when it comes and the id follows the build (issue 176). A // repository given without a scheme is a path on the forge holding the git seat. argv := []string{"build", str("repository"), "--wait", "0"} if !strings.Contains(str("repository"), "://") && !strings.HasPrefix(str("repository"), "git@") { argv = append(argv, "--self") } if p := str("path"); p != "" { argv = append(argv, "--path", p) } if r := str("ref"); r != "" { argv = append(argv, "--ref", r) } return argv, nil } return nil, fmt.Errorf("%q is a verb of the %s seat's table that this binary has no command line for", verb, catalogue.ControllerSeatName) } // jsonVerbs are the verbs whose command speaks JSON, so the answer carries it as data as well. var jsonVerbs = map[string]bool{"status": true, "seats": true, "plan": true, "collection": true, "hand-acts": true, "durations": true, "conditions": true, "doctor": true, "retire": true, "cleanup": true, "data": true, // What the records say the registries may keep (novox/hq ADR 0251). "artifacts": true, "collect": true, "images": true, "mirrors": true, // The delivery's owner's verbs answer JSON where they read (plan, order, check, walks) — novox/hq ADR 0239. "delivery": true} // overviewVerbs are the JSON verbs whose answer is said once, as data, and not again as the text the // command printed: the overviews, which grow with the mesh (novox/hq issue 314). Every reader of theirs // reads `answer` first. var overviewVerbs = map[string]bool{"status": true, "conditions": true, "doctor": true} // repairingCommand names a command line that repairs by hand, and so says why: a push, a plan stopped // or closed, a consumer re-made (novox/hq to-be 45 §7). Empty for any other. func repairingCommand(argv []string) string { switch { case argv[0] == "push": return "push" case argv[0] == "plans" && len(argv) > 1 && (argv[1] == "stop" || argv[1] == "close" || argv[1] == "go"): return "plans " + argv[1] case argv[0] == "broker" && len(argv) > 1 && argv[1] == "consumer-reset": return "broker consumer-reset" case argv[0] == "hand-act" && len(argv) > 1 && argv[1] == "drill": return "hand-act drill" case argv[0] == "hand-act": return "hand-act record" case argv[0] == "conditions" && len(argv) > 1 && argv[1] == "silence": return "conditions silence" case argv[0] == "retire" && len(argv) > 1 && (argv[1] == "approve" || argv[1] == "reject"): return "retire " + argv[1] case argv[0] == "cleanup" && len(argv) > 1 && argv[1] == "delete": return "cleanup delete" case argv[0] == "collect" && slices.Contains(argv, "--confirm"): return "collect" case argv[0] == "mirrors" && slices.Contains(argv, "--confirm"): return "mirrors" } return "" } func isWhyFlag(word string) bool { return word == "--why" || word == "-why" || strings.HasPrefix(word, "--why=") || strings.HasPrefix(word, "-why=") } // commandEnvironment is the environment of a command line this controller runs for someone: its own — the // stores' credentials, the bus, the broker, everything a command run from a shell beside it would have, because it // is that — with who asked, and how it came. // // **terminal** is said, never inferred (novox/hq ADR 0272): only a line mesh-cli asked as the controller's terminal // passes true. Its line has no `MESH_VERB`, not the served mark ADR 0266 puts on everything the serving controller // starts, and the terminal's mark (cliTerminalVar), so startedAtTheTerminal reads yes. Every other line names its // verb, and is stripped of the terminal's mark whatever this process's environment holds. func commandEnvironment(caller, verb string, terminal bool) []string { env := make([]string, 0, len(os.Environ())+3) for _, kv := range os.Environ() { if strings.HasPrefix(kv, verbVar+"=") || strings.HasPrefix(kv, link.CallerVar+"=") || strings.HasPrefix(kv, cliTerminalVar+"=") || (terminal && strings.HasPrefix(kv, servedVar+"=")) { continue } env = append(env, kv) } env = append(env, link.CallerVar+"="+caller) if terminal { return append(env, cliTerminalVar+"=1") } return append(env, verbVar+"="+verb) } // runVerb runs this binary with the given command line and gathers what it said. // // **This binary is the image this process runs, never the file at the path it started from** // (novox/hq issue 289): the node-engine's witness moves a running build aside when it places the next // one, into a directory only it may enter, and deletes it once the next is proved — while this process // may still be serving. A verb run from the path then failed with "permission denied" for the seconds // the old controller still answered. selfCommand runs what this process is running, wherever its file // went; a verb it still cannot start is refused as a handover, which the caller asks again. func runVerb(ctx context.Context, argv []string) (verbAnswer, error) { if handingOver.Load() { return verbAnswer{}, fmt.Errorf("%w: this controller is stopping and runs no new command", link.ErrHandingOver) } cmd := selfCommand(ctx, argv) // And who asked, so an act it does by hand is recorded as theirs (novox/hq to-be 45 §7). caller := link.CallerIn(ctx) if caller == "" { caller = "a seat call whose caller the bus did not name" } // And which verb, so the connection it dials says so in the bus's list (novox/hq issue 327). verb, _ := ctx.Value(verbKey{}).(string) if verb == "" { verb = argv[0] } cmd.Env = commandEnvironment(caller+", through the "+catalogue.ControllerSeatName+" seat", verb, false) // Two buffers, one answer. What the command *says* is both streams, in the order a person at // a shell would read them; what it *answers as data* is standard output alone — `status --json` // prints its warnings beside the document, and a JSON parsed from the two together parsed // nothing (2026-09-30, the first status asked through the console had no `answer`). var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr runErr := cmd.Run() answer := answerOf(argv, stdout.Bytes(), stderr.String(), runErr == nil) var exit *exec.ExitError if runErr != nil && !errors.As(runErr, &exit) { // Not the command refusing — the command not running at all, which is this process's fault. if errors.Is(runErr, os.ErrPermission) || errors.Is(runErr, os.ErrNotExist) { // Its own image unreachable: a build replaced under a process that has not yet stopped. // Refused as a handover, so the caller asks the controller that follows. return answer, fmt.Errorf("%w: could not run %s from this controller's own build: %v", link.ErrHandingOver, strings.Join(argv, " "), runErr) } return answer, fmt.Errorf("could not run %s: %w", strings.Join(argv, " "), runErr) } return answer, nil } // answerOf is what a command said, as a verb answers it: both streams as text, and standard output as data // where the command speaks JSON. func answerOf(argv []string, stdout []byte, stderr string, ok bool) verbAnswer { answer := verbAnswer{Output: string(stdout) + stderr, OK: ok} if jsonVerbs[argv[0]] && ok { var parsed any if json.Unmarshal(bytes.TrimSpace(stdout), &parsed) == nil { answer.Answer = parsed if overviewVerbs[argv[0]] { // Once, as data: the same document again as text doubled an answer that already // outgrew one message of the bus (novox/hq issue 314). answer.Output = stderr + "its answer, as data, is `answer`\n" } } } return answer } // readHere answers a verb that only reads the bus in the serving controller itself, on its own connection // and keeper (novox/hq issue 327): the same command, writing to the answer rather than to a process's // output, so the answer is the one the command prints. False for any other command line, which runs as a // command of its own. Every `conditions` call — the operator's channel reads it at least once a minute — // was a process that dialled the bus, logged in and left. func readHere(ctx context.Context, argv []string) (verbAnswer, bool) { var read func(context.Context, []string, io.Writer) error args := argv[1:] // Each where this process holds what it reads: the serving keeper, the hand-act log's connection, the // serving connection. switch argv[0] { case "conditions": sub := "list" if len(args) > 0 && !strings.HasPrefix(args[0], "-") { sub, args = args[0], args[1:] } if conditionsFrom != nil { read = map[string]func(context.Context, []string, io.Writer) error{ "list": listConditions, "show": showCondition, "history": conditionHistory}[sub] } case "hand-acts": if handActConn != nil || servingBus.Load() != nil { read = listHandActs } case "queue": if servingBus.Load() != nil { read = listQueue } } if read == nil { return verbAnswer{}, false } var out bytes.Buffer stderr := "" err := read(ctx, args, &out) if err != nil { // As the command says it when it fails (main). stderr = "mesh-controller: " + err.Error() + "\n" } return answerOf(argv, out.Bytes(), stderr, err == nil), true } // seatToolHandlers are the handlers for every verb the mesh-controller seat declares, from the // store's row, so a verb the row does not carry is not served. A verb it carries that this binary // cannot run is named at start and answers the reason when called — never a refusal to serve, which // would take the whole control plane down for one word (novox/hq ADR 0185). func seatToolHandlers() (map[string]link.ToolHandler, []string, error) { seat, known := catalogue.SeatNamed(catalogue.ControllerSeatName) if !known { return nil, nil, fmt.Errorf("this mesh defines no %s seat", catalogue.ControllerSeatName) } var behind []string handlers := map[string]link.ToolHandler{} for _, v := range seat.Serves { verb := v.Name if inProcess[verb] { handlers[verb] = func(ctx context.Context, raw json.RawMessage) (any, error) { args := map[string]any{} if len(bytes.TrimSpace(raw)) > 0 { if err := json.Unmarshal(raw, &args); err != nil { return nil, fmt.Errorf("the arguments are not a JSON object: %w", err) } } // Refused like any verb's: what a verb does not take is not ignored. a, err := readArguments(verb, args) if err != nil { return nil, err } if verb == "calls" { return callsAnswer(link.Calls, a.given["call"]) } if verb == "dead-letters" { return deadLettersAnswer(ctx, a) } if verb == "root-free" { return rootFreeAnswer(ctx, a.given["machines"], rootClock()) } if verb == "doctor" { // From the serving controller, which runs the self-check and hears the signals // (novox/hq to-be 45 §4): the last verdict at once, or a run now. argv, err := a.commandLine() if err != nil { return nil, err } sub := "" if len(argv) > 2 && !strings.HasPrefix(argv[1], "-") { sub = argv[1] } return doctorAnswer(ctx, sub, a.given["probe"]) } return seatTools(), nil } continue } var policy *heldAtTheTerminal if _, err := argvFor(verb, sampleArguments(v)); err != nil && !errors.As(err, &policy) { // **A row ahead of this binary is not a reason to go silent.** // // The row is the store's and a control plane follows it (novox/hq ADR 0154), so a verb // this build does not know means the row was widened by a newer one — the ordinary // state of a roll-out, and of a push that put an older control plane back. Refusing to // serve at all made that transient fatal: on 2026-10-02 one unknown verb took the whole // mesh off the bus for ten minutes, and the way back was a human running the binary by // hand, because the thing that would have repaired it is the thing that was down // (novox/hq 04-ISSUES/201, ADR 0185). // // So the verbs this binary knows are served, and this one answers the reason instead of // nothing: a caller gets a sentence naming the fault, and everything else keeps working // — including the push that replaces this binary with the one whose verb it is. behind = append(behind, verb) reason := err handlers[verb] = func(context.Context, json.RawMessage) (any, error) { return nil, fmt.Errorf("%s is in this mesh's %s row and the control plane running "+ "here cannot run it: %w. It is a verb of a newer build; this one is behind", verb, catalogue.ControllerSeatName, reason) } continue } handlers[verb] = func(ctx context.Context, raw json.RawMessage) (any, error) { args := map[string]any{} if len(raw) > 0 { if err := json.Unmarshal(raw, &args); err != nil { return nil, fmt.Errorf("the arguments are not a JSON object: %w", err) } } argv, err := argvFor(verb, args) if err != nil { return nil, err } ctx = context.WithValue(ctx, verbKey{}, verb) if verb == "status" && statusFrom != nil { // At once, from the summary the serving controller keeps (novox/hq to-be 45 Phase 0). return statusFrom.answer(ctx) } if !readingVerbs[verb] && !(verb == "plans" && !actsOnAPlan(args)) { // Whatever it did, `status` is composed again once it has. defer statusFrom.nudge() } if answer, read := readHere(ctx, argv); read { return answer, nil } if answersFirst(argv) { // Before anything is sent: a push sends the bus's own machine first, and a broker // reloading its user list forgets the answer it was about to permit (novox/hq issue 265). link.Acknowledge(ctx) } return runVerb(ctx, argv) } } return handlers, behind, nil } // actsOnAPlan is `plans` asked to stop, close or retry one rather than to show them. func actsOnAPlan(args map[string]any) bool { for _, act := range []string{"stop", "close", "retry"} { if v, _ := args[act].(string); strings.TrimSpace(v) != "" { return true } } return false } // inProcess are the verbs answered by this process rather than by a command it runs: `tools` from // the records, `calls` from what this process served. var inProcess = map[string]bool{"tools": true, "calls": true, "doctor": true, // What a consumer gave up on, read and changed on the serving controller's own connection (novox/hq // issue 330). "dead-letters": true, // Whether a machine is root-free, judged live on the serving controller's store and connection (novox/hq ADR // 0259 §8): the router asks it before an approval. "root-free": true} // answersFirst is a command line whose caller is answered before it runs: a push, by its verb or // through `command`. A push sends the machine holding the bus first when its user list changed, the // broker reloads, and a reload forgets every answer the bus was about to permit — so an answer // waiting for the push to end was refused, every time the list had changed (novox/hq issue 265). func answersFirst(argv []string) bool { return len(argv) > 0 && argv[0] == "push" } // callsAnswer is what `calls` answers: the kept calls, newest first, without their answers — or // one call whole. Kept on the bus, so a call a controller before this one served is answered too // (novox/hq to-be 45 §6); where the bus cannot be read, what this process served is answered and // the reason said beside it. func callsAnswer(log *link.CallLog, id string) (any, error) { if id != "" { c, ok, err := log.Shown(id) if err != nil { return nil, fmt.Errorf("call %s is not in this controller's memory, and the calls kept on the "+ "bus could not be read: %w", id, err) } if !ok { if log.IsDurable() { return nil, fmt.Errorf("no call %s is kept: the bus keeps the last %d calls, or %s, and this "+ "is not among them — `calls` lists them", id, broker.KeptCallsDurably, broker.CallsKeptFor) } return nil, fmt.Errorf("no call %s is kept here: calls are kept by the controller that "+ "answered them, the last %d, and not across a restart — `calls` lists them", id, link.KeptCalls) } return c, nil } recent, err := log.Recent() for i := range recent { recent[i].Answer = nil } answer := map[string]any{"calls": recent, "note": "newest first; `calls` with a call's id gives its whole answer"} if log.IsDurable() { answer["kept"] = fmt.Sprintf("the last %d calls, or %s, on the bus — across a restart of the controller", broker.KeptCallsDurably, broker.CallsKeptFor) } else { answer["kept"] = fmt.Sprintf("the last %d calls this controller served, in its memory only", link.KeptCalls) } if err != nil { answer["unread"] = err.Error() } return answer, nil } // seatTools is what `tools` answers: every seat with a protocol, and the tools each serves, from the // mesh's own records — no holder in the path, so it is true while a holder restarts (design 33 §5). func seatTools() map[string]any { var seats []map[string]any for _, s := range catalogue.SeatsWithAProtocol() { if len(s.Serves) == 0 { continue } var tools []map[string]any for _, v := range s.Serves { tool := map[string]any{ "name": v.Name, "description": v.Description, "input": v.Input, "output": v.Output, } // What the verb is the mesh's way to do (novox/hq ADR 0245): read by the console's search and // the agent's instructions. if len(v.Replaces) > 0 { tool["replaces"] = v.Replaces } tools = append(tools, tool) } seats = append(seats, map[string]any{"seat": s.Name, "scope": s.Scope, "tools": tools}) } return map[string]any{"seats": seats} } // sampleArguments is one of every argument a verb's schema requires, so the check at start proves the // verb runnable rather than that it happens to want the arguments the check guessed — and nothing // more, since an argument a verb does not declare is refused. func sampleArguments(v catalogue.Verb) map[string]any { sample := map[string]any{} switch required := v.Input["required"].(type) { case []string: for _, k := range required { sample[k] = "x" } case []any: for _, k := range required { if name, ok := k.(string); ok { sample[name] = "x" } } } return sample } // splitCommandLine splits a command line into words the way a POSIX shell does for the simple // cases a controller command needs: spaces separate, single or double quotes group, a backslash // escapes the next character inside double quotes or outside any. No expansion of anything. func splitCommandLine(line string) ([]string, error) { var words []string var cur strings.Builder inWord := false quote := rune(0) runes := []rune(line) for i := 0; i < len(runes); i++ { r := runes[i] switch { case quote == '\'': if r == '\'' { quote = 0 } else { cur.WriteRune(r) } case quote == '"': if r == '"' { quote = 0 } else if r == '\\' && i+1 < len(runes) { i++ cur.WriteRune(runes[i]) } else { cur.WriteRune(r) } case r == '\'' || r == '"': quote = r inWord = true case r == '\\' && i+1 < len(runes): i++ cur.WriteRune(runes[i]) inWord = true case r == ' ' || r == '\t' || r == '\n': if inWord { words = append(words, cur.String()) cur.Reset() inWord = false } default: cur.WriteRune(r) inWord = true } } if quote != 0 { return nil, fmt.Errorf("command has an unclosed %c quote", quote) } if inWord { words = append(words, cur.String()) } return words, nil } // seatAnnouncement is what the controller says it serves on the bus (novox/hq ADR 0197): the // mesh-controller seat, one endpoint per verb it answers, each with the seat's own description and // argument schema — the same facts `tools` answers from the records, as NATS's services format. func seatAnnouncement(handlers map[string]link.ToolHandler) micro.Info { about := map[string]catalogue.Verb{} for _, s := range catalogue.SeatsWithAProtocol() { if s.Name == catalogue.ControllerSeatName { for _, v := range s.Serves { about[v.Name] = v } } } verbs := make([]string, 0, len(handlers)) for verb := range handlers { verbs = append(verbs, verb) } sort.Strings(verbs) var endpoints []micro.EndpointInfo for _, verb := range verbs { schema, _ := json.Marshal(about[verb].Input) // The same shape every tool runtime announces in (node-tools' announce package): the name is // `__`, as the protocol's characters allow; the metadata is what identifies it. endpoints = append(endpoints, micro.EndpointInfo{ Name: catalogue.ControllerSeatName + "__" + verb, Subject: link.SeatToolSubject(catalogue.ControllerSeatName, verb), QueueGroup: "seat." + catalogue.ControllerSeatName, Metadata: map[string]string{ "kind": "seat", "module": catalogue.ControllerSeatName, "tool": verb, "seat": catalogue.ControllerSeatName, "scope": "mesh", "interchangeable": "false", "description": about[verb].Description, "schema": string(schema), }, }) } return micro.Info{ ServiceIdentity: micro.ServiceIdentity{ Name: catalogue.ControllerSeatName, ID: "controller", Version: "0.1.0", Metadata: map[string]string{"seat": catalogue.ControllerSeatName, "scope": "mesh"}, }, Description: "the mesh's own verbs, answered by the holder of the mesh-controller seat", Endpoints: endpoints, } } // nodeReads are the `node` subcommands a verb may run: the ones that only read. var nodeReads = map[string]bool{"list": true, "show": true} // flagsOnly says a command line's rest names no subcommand: empty, or beginning with a flag. For a command // with no subcommands every word is a flag, its value or a name it reads. func flagsOnly(rest []string) bool { return len(rest) == 0 || strings.HasPrefix(rest[0], "-") } // subIn says the rest begins with one of these subcommands. func subIn(rest []string, subs ...string) bool { return len(rest) > 0 && slices.Contains(subs, rest[0]) } // commandReadForms are the command lines the generic `command` verb may run (novox/hq ADR 0266): **an allow // list of the ones that only read**, judged command by command. Anything else — every command that writes a // record, sends, builds, issues an account or a token, sets a key, accepts, rotates, recovers or exports a // secret — is refused, and a command added later is refused until it is judged a read. Writing has its named // verbs, which compose their own lines and are judged by terminalOnly; the rest is the operator's at the // controller's terminal. var commandReadForms = map[string]func(rest []string) bool{ "status": flagsOnly, "version": flagsOnly, "help": flagsOnly, "seats": flagsOnly, "healers": flagsOnly, "hand-acts": flagsOnly, "durations": flagsOnly, "collection": flagsOnly, "images": flagsOnly, "artifacts": flagsOnly, "data": flagsOnly, "builds": flagsOnly, "queue": flagsOnly, // `plan ` previews a node's declaration; it sends nothing. "plan": func([]string) bool { return true }, // `plans` lists and `plans ` shows one; `plans stop|close|go` acts. // Judged on every word, not the first: a flag before the subcommand (`plans --json go `) still acts. "plans": func(r []string) bool { return !slices.ContainsFunc(r, func(w string) bool { return slices.Contains(plansActs, w) }) }, // `doctor` answers the last run, `probes` and `signals` describe; `doctor run` runs. "doctor": func(r []string) bool { return flagsOnly(r) || subIn(r, "probes", "signals") }, "conditions": func(r []string) bool { return flagsOnly(r) || subIn(r, "list", "show", "history") }, "node": func(r []string) bool { return subIn(r, "list", "show") }, "module": func(r []string) bool { return subIn(r, "list") }, "settings": func(r []string) bool { return subIn(r, "show", "preferences") }, "retire": func(r []string) bool { return subIn(r, "list") }, "cleanup": func(r []string) bool { return subIn(r, "list") }, "delivery": func(r []string) bool { return subIn(r, "plan", "walks") }, // `bus` alone says the bus's step; `bus upgrade` takes one. "bus": func(r []string) bool { return len(r) == 0 }, // `mirrors` lists; --record and --confirm keep a mirror. "mirrors": func(r []string) bool { return flagsOnly(r) && !slices.ContainsFunc(r, func(w string) bool { return w == "--record" || w == "-record" || strings.HasPrefix(w, "--record=") || strings.HasPrefix(w, "-record=") || w == "--confirm" || w == "-confirm" || strings.HasPrefix(w, "--confirm=") }) }, } // plansActs are the `plans` subcommands that act on a walk; no other word of a plans line is one of them. var plansActs = []string{"go", "stop", "close", "retry"} // heldAtTheTerminal is a refusal of policy (novox/hq ADR 0266): the verb is known and served, and this line is // the operator's at the controller's terminal. Never read as a verb this binary is behind on. type heldAtTheTerminal struct{ msg string } func (e *heldAtTheTerminal) Error() string { return e.msg } func terminalRefusal(format string, args ...any) error { return &heldAtTheTerminal{fmt.Sprintf(format, args...)} } // commandReads refuses a line the generic verb may not run, saying what it may. func commandReads(argv []string) error { if read, ok := commandReadForms[argv[0]]; ok && read(argv[1:]) { return nil } return terminalRefusal("%q is not a reading command, and the generic command verb only reads (novox/hq ADR 0266): "+ "whoever may call a verb includes agents, and a line that writes, issues, sets a key or reveals a secret "+ "would be theirs to run. Use the named verb for it, or run it at the controller's terminal. The verb may "+ "run: %s. Nothing was done", strings.Join(argv, " "), commandReadNames()) } func commandReadNames() string { names := make([]string, 0, len(commandReadForms)) for n := range commandReadForms { names = append(names, n) } sort.Strings(names) return strings.Join(names, ", ") + " (each in its reading forms)" } // terminalOnlyCommands are the commands no verb runs, whatever composed them (novox/hq ADR 0266): they set // the operator's key, issue a credential or a token that is answered to the caller, or accept, recover or // export a secret. Their answers or effects hand whoever calls them what the runtime's account holds. var terminalOnlyCommands = map[string]string{ "operator": "the operator's key and credential", "identity": "the mesh's identity keys", "token": "a token a machine joins with, answered to the caller", "broker": "the bus's accounts", "api": "the controller's API keys", "licence": "the licences' secrets", } // givenAtTheDesk is exactly the line the `give` verb composes, and nothing beside it: `secret accept // --at-desk `, with no other word — no value, no file, no provider. func givenAtTheDesk(argv []string) bool { if len(argv) != 7 || argv[0] != "secret" || argv[1] != "accept" || argv[5] != "--at-desk" { return false } for _, w := range argv[2:5] { if w == "" || strings.HasPrefix(w, "-") { return false } } return argv[6] != "" && !strings.HasPrefix(argv[6], "-") } // terminalOnly refuses, through any verb, a command that is the operator's at the controller's terminal // alone (novox/hq ADR 0266). **Every `node` subcommand that is not a read**: `node account` and // `node agent-account` above all. Whoever may call a verb includes agents, and an agent that named itself // the operator account, or cleared the agent account, would have the next send grant it root through the // sudo module's rule. An allow list, so a subcommand added later is refused until it is judged a read. func terminalOnly(argv []string) error { if len(argv) == 0 { return nil } if what, kept := terminalOnlyCommands[argv[0]]; kept { return terminalRefusal("%s is run at the controller's terminal only, never through a verb: it holds %s, and "+ "whoever may call a verb includes agents (novox/hq ADR 0266). Nothing was done", argv[0], what) } // Of a secret's commands only rotation, which seals the new value to the machine that uses it, and the // `give` verb's own line: an own secret typed by the operator into the desk's hidden prompt, sealed to this // call and then to the module's machine, so no value travels in the verb or its answer (hq ADR 0259 §10). if argv[0] == "secret" && (len(argv) < 2 || argv[1] != "rotate") && !givenAtTheDesk(argv) { return terminalRefusal("secret %s is run at the controller's terminal only, never through a verb: accepting, "+ "recovering or exporting a secret hands it to whoever asks, and that includes agents (novox/hq ADR "+ "0266). Nothing was done", strings.Join(argv[1:], " ")) } if argv[0] != "node" { return nil } if len(argv) > 1 && nodeReads[argv[1]] { return nil } sub := "node" if len(argv) > 1 { sub += " " + argv[1] } return terminalRefusal("%s is run at the controller's terminal only, never through a verb: a node's accounts "+ "decide who may become root on it (novox/hq ADR 0266). A verb may run node list and node show. "+ "Nothing was done", sub) }