Answer mesh-cli: the control-node's operator is the terminal, everyone else is not (hq ADR 0272)
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request

The records send the operator to "the controller's terminal", and nothing
reached it (hq issue 343). The serving controller now answers each node's
mesh.control.<node>.cli, where only that node's engine may publish, with the
account the engine read from the kernel. A line from the control-node's
operator account runs as the terminal: a fresh process of this binary with
no MESH_VERB, whatever the serving process carries. The same account on any
other node, where agents may run as it, is an ordinary call: the generic
command verb's refusals (one function now, so the two routes cannot drift)
and MESH_VERB=mesh-cli, so a terminal-only change is refused with its
reason. Any other account, root included, runs nothing. Servers are never
run, and an answer over one bus message is cut and says so.

The terminal-only refusals and the usage now name mesh-cli on the
control-node as the way to the terminal.
This commit is contained in:
jochen
2026-10-09 12:10:24 +02:00
parent 4d60628f37
commit b1897a3498
9 changed files with 525 additions and 23 deletions
+4
View File
@@ -216,6 +216,10 @@ func run() error {
func usage() {
fmt.Fprint(os.Stderr, `mesh-controller — the control plane
On a node the operator types these as 'mesh-cli <command>' (in zsh, 'nox <command>'): asked
through the node's engine, and run as the controller's terminal only on the control-node
(novox/hq ADR 0272).
migrate bring each context's schema up to date
prepare the same, asked the way the mesh asks any module (ADR 0135)
node add <name> [--adopted] create a node record; --adopted: the machine is in use
+157
View File
@@ -0,0 +1,157 @@
package main
import (
"bytes"
"context"
"errors"
"fmt"
"os/exec"
"slices"
"strings"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
)
// The operator's command, `mesh-cli` (alias `nox`), answered here (novox/hq ADR 0272).
//
// mesh-cli asks the node-engine on its own machine; the engine reads the asking account from the kernel and asks
// this controller on its own node's subject. Here it is judged — the controller's terminal, an ordinary call, or
// nothing — and run as every verb's line is: a fresh process of this binary, with this process's environment.
//
// **The terminal** is a line from a node's operator account, on the control-node (§4). Phase 2 adds a node whose
// agents run under an account of their own, judged unable to become root (ADR 0266, not built yet); until then no
// other node is the terminal, because agents there run as its operator. **An ordinary call** is a line from that
// account elsewhere: it meets every refusal of the generic `command` verb and runs with `MESH_VERB=mesh-cli`, so a
// terminal-only change is refused with its reason. **Any other account is refused**, root included: those two
// accounts already reach the mesh's verbs through the mesh MCP server, and no other account gains anything here.
// cliVerb is what a line from mesh-cli that is not the terminal runs as: the verb its process names, so every
// terminal-only refusal applies and says it came through mesh-cli.
const cliVerb = "mesh-cli"
// cliServers are commands that serve until stopped. Run for mesh-cli they would hold a second server under this
// one until the call's bound killed it.
var cliServers = map[string]bool{"serve": true, "api": true, "board": true}
// cliVerdict is how a line is run, or why it is not.
type cliVerdict struct {
terminal bool
// why says why the line is or is not the terminal, in words the operator reads under the answer.
why string
// refused says why nothing runs.
refused string
}
// judgeCLI decides how a line from mesh-cli runs (ADR 0272 §4). nodes is every node the mesh knows; control is
// every node the controller's module is assigned to, which is exactly one in a mesh that is well.
func judgeCLI(node string, asked link.CLIAsked, nodes []inventory.Node, control []string) cliVerdict {
var record *inventory.Node
for i := range nodes {
if nodes[i].Name == node {
record = &nodes[i]
break
}
}
switch {
case record == nil:
return cliVerdict{refused: fmt.Sprintf("%s is not a node this mesh knows, so nothing ran", node)}
case asked.UID == 0 || asked.Account == "root":
return cliVerdict{refused: "mesh-cli answers the operator's own account, never root: run it as yourself, " +
"not under sudo. Nothing ran"}
case record.Account == "":
return cliVerdict{refused: fmt.Sprintf("the mesh does not know %s's operator account (`node account <node> <login>`), "+
"so no account there is answered. Nothing ran", node)}
case asked.Account != record.Account:
return cliVerdict{refused: fmt.Sprintf("mesh-cli answers %s's operator account (%s) only, and this line came "+
"from %s. Nothing ran", node, record.Account, asked.Account)}
}
if len(control) != 1 {
return cliVerdict{why: fmt.Sprintf("not the controller's terminal: the mesh names %d control-nodes, and the "+
"terminal is the one control-node's operator account", len(control))}
}
if control[0] == node {
return cliVerdict{terminal: true, why: fmt.Sprintf("the controller's terminal: %s on the control-node %s",
asked.Account, node)}
}
return cliVerdict{why: fmt.Sprintf("not the controller's terminal: agents on %s may run as %s, so a "+
"terminal-only change is made from the control-node %s (novox/hq ADR 0272)", node, asked.Account, control[0])}
}
// controlNodes is every node the controller's module is assigned to.
func controlNodes(ctx context.Context, inv *inventory.Inventory, nodes []inventory.Node) ([]string, error) {
var out []string
for _, n := range nodes {
modules, err := inv.Assigned(ctx, n.Name)
if err != nil {
return nil, err
}
if slices.Contains(modules, controllerModule) {
out = append(out, n.Name)
}
}
return out, nil
}
// controllerModule is the module that runs the controller, and so marks the control-node.
const controllerModule = "mesh-controller"
// answerMeshCLI is the serving controller's answer to mesh-cli.
func answerMeshCLI(inv *inventory.Inventory) link.CLIHandler {
return func(ctx context.Context, node string, asked link.CLIAsked) link.CLIAnswer {
if handingOver.Load() {
return link.CLIRefusal("this controller is stopping and runs no new command; ask again in a moment. Nothing ran")
}
nodes, err := inv.Nodes(ctx)
if err != nil {
return link.CLIRefusal("the mesh's nodes cannot be read, so nothing ran: " + err.Error())
}
control, err := controlNodes(ctx, inv, nodes)
if err != nil {
return link.CLIRefusal("which node is the control-node cannot be read, so nothing ran: " + err.Error())
}
return runForMeshCLI(ctx, node, asked, judgeCLI(node, asked, nodes, control))
}
}
// runForMeshCLI runs a judged line and answers what it said.
func runForMeshCLI(ctx context.Context, node string, asked link.CLIAsked, v cliVerdict) link.CLIAnswer {
if v.refused != "" {
return link.CLIRefusal(v.refused)
}
if cliServers[asked.Line[0]] {
return link.CLIAnswer{Exit: 1, Why: v.why, Refused: fmt.Sprintf("%s serves until stopped, and is not a "+
"command line mesh-cli runs. Nothing ran", asked.Line[0])}
}
verb := ""
if !v.terminal {
if err := refusedAsTheGenericCommand(asked.Line); err != nil {
return link.CLIAnswer{Exit: 1, Why: v.why, Refused: err.Error()}
}
verb = cliVerb
}
cmd := selfCommand(ctx, asked.Line)
cmd.Env = commandEnvironment(fmt.Sprintf("%s through mesh-cli on %s", asked.Account, node), verb)
// No standard input: a command that reads one gets nothing, and fails saying so (ADR 0272 §5).
cmd.Stdin = nil
var stdout, stderr bytes.Buffer
cmd.Stdout, cmd.Stderr = &stdout, &stderr
err := cmd.Run()
answer := link.CLIAnswer{Stdout: stdout.Bytes(), Stderr: stderr.Bytes(), Terminal: v.terminal, Why: v.why}
var exit *exec.ExitError
switch {
case err == nil:
case errors.As(err, &exit):
answer.Exit = exit.ExitCode()
if answer.Exit < 0 {
// Killed: by the call's bound, or by this controller stopping.
answer.Exit = 1
answer.Stderr = append(answer.Stderr, []byte(fmt.Sprintf("\nmesh-cli: the command was stopped (%v)\n",
exit))...)
}
default:
answer.Exit = 1
answer.Refused = fmt.Sprintf("could not run %s from this controller's own build: %v", strings.Join(asked.Line, " "), err)
}
return answer
}
+107
View File
@@ -0,0 +1,107 @@
package main
import (
"context"
"strings"
"testing"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
)
// echoEnvironment makes the test binary, run as a command line, say the verb and caller it was given (TestMain).
const echoEnvironment = "MESH_TEST_ECHO_ENVIRONMENT"
var cliNodes = []inventory.Node{
{Name: "control", Account: "operator"},
{Name: "laptop", Account: "operator"},
{Name: "unnamed"},
}
func asked(account string, uid uint32, line ...string) link.CLIAsked {
return link.CLIAsked{Line: line, Account: account, UID: uid}
}
// Who is the controller's terminal, and who is an ordinary call or refused (novox/hq ADR 0272 §4).
func TestMeshCLIIsTheTerminalOnlyForTheControlNodesOperator(t *testing.T) {
control := []string{"control"}
cases := []struct {
name, node string
asked link.CLIAsked
control []string
terminal bool
refused string
why string
}{
{"the control-node's operator", "control", asked("operator", 1000, "status"), control, true, "", "the controller's terminal"},
{"another node's operator", "laptop", asked("operator", 1000, "status"), control, false, "", "agents on laptop may run as operator"},
{"another account", "control", asked("agent", 1001, "status"), control, false, "operator account (operator) only", ""},
{"root", "control", asked("root", 0, "status"), control, false, "never root", ""},
{"a node with no operator account", "unnamed", asked("operator", 1000, "status"), control, false, "does not know unnamed's operator account", ""},
{"a node the mesh does not know", "elsewhere", asked("operator", 1000, "status"), control, false, "not a node this mesh knows", ""},
{"two control-nodes", "control", asked("operator", 1000, "status"), []string{"control", "laptop"}, false, "", "2 control-nodes"},
}
for _, c := range cases {
v := judgeCLI(c.node, c.asked, cliNodes, c.control)
if v.terminal != c.terminal {
t.Errorf("%s: terminal %v, want %v (%+v)", c.name, v.terminal, c.terminal, v)
}
if c.refused == "" && v.refused != "" || c.refused != "" && !strings.Contains(v.refused, c.refused) {
t.Errorf("%s: refused %q, want %q", c.name, v.refused, c.refused)
}
if c.why != "" && !strings.Contains(v.why, c.why) {
t.Errorf("%s: why %q, want %q", c.name, v.why, c.why)
}
}
}
// The terminal runs its line without MESH_VERB, even where this process carries one; an ordinary call names
// mesh-cli; both record who asked through mesh-cli.
func TestTheTerminalRunsWithoutAVerbAndAnOrdinaryCallNamesMeshCLI(t *testing.T) {
t.Setenv(echoEnvironment, "1")
t.Setenv("MESH_VERB", "leaked-from-the-serving-process")
ctx := context.Background()
a := runForMeshCLI(ctx, "control", asked("operator", 1000, "status"), cliVerdict{terminal: true, why: "the terminal"})
if a.Exit != 0 || a.Refused != "" || !a.Terminal {
t.Fatalf("the terminal's line did not run: %+v", a)
}
if got := string(a.Stdout); !strings.Contains(got, `verb=""`) || !strings.Contains(got, "operator through mesh-cli on control") {
t.Fatalf("the terminal's line ran with %s", got)
}
a = runForMeshCLI(ctx, "laptop", asked("operator", 1000, "status"), cliVerdict{why: "not the terminal"})
if a.Exit != 0 || a.Terminal || a.Why != "not the terminal" {
t.Fatalf("an ordinary line did not run as one: %+v", a)
}
if got := string(a.Stdout); !strings.Contains(got, `verb="mesh-cli"`) {
t.Fatalf("an ordinary line ran with %s", got)
}
}
// An ordinary call meets every refusal of the generic command verb, and nothing runs; a server is never run.
func TestAnOrdinaryCallMeetsTheCommandVerbsRefusals(t *testing.T) {
t.Setenv(echoEnvironment, "1")
ctx := context.Background()
ordinary := cliVerdict{why: "not the terminal"}
a := runForMeshCLI(ctx, "laptop", asked("operator", 1000, "settings", "set", "claude-code", "{}"), ordinary)
if a.Refused == "" || len(a.Stdout) != 0 || a.Exit != 1 {
t.Fatalf("settings set ran as an ordinary call: %+v", a)
}
if !strings.Contains(a.Refused, "controller's terminal") || a.Why != "not the terminal" {
t.Fatalf("the refusal does not say why: %+v", a)
}
if err := refusedAsTheGenericCommand([]string{"settings", "set", "x", "{}"}); err == nil {
t.Fatal("the command verb no longer refuses settings set, and mesh-cli's ordinary call relies on it")
}
for _, server := range []string{"serve", "api", "board"} {
a := runForMeshCLI(ctx, "control", asked("operator", 1000, server), cliVerdict{terminal: true})
if a.Refused == "" || len(a.Stdout) != 0 {
t.Fatalf("%s was run for mesh-cli: %+v", server, a)
}
}
a = runForMeshCLI(ctx, "control", asked("agent", 1001, "status"), cliVerdict{refused: "agent is not answered"})
if a.Refused != "agent is not answered" || len(a.Stdout) != 0 {
t.Fatalf("a refused line ran: %+v", a)
}
}
+2 -2
View File
@@ -1107,7 +1107,7 @@ func refuseTerminalSettingsThroughAVerb(ctx context.Context, inv *inventory.Inve
}
// A trusted mergeable file takes any key, so its module's whole layer is the terminal's (novox/hq issue 340).
if files := catalogue.TrustedMergeable(shelf[module]); len(files) > 0 && !sameLayer(before, after) {
return fmt.Errorf("the settings of %s on %s are set at the controller's terminal only, never through a verb (this "+
return fmt.Errorf("the settings of %s on %s are set at the controller's terminal only (`mesh-cli` on the control-node), never through a verb (this "+
"line came through %q): %s merges whatever key a layer sets into a file root or a consumer trusts, so "+
"any key could point the module at a listener of the caller's, and whoever may call a verb includes "+
"agents (novox/hq issue 340; a file nothing trusts says \"trusted\": false). Nothing was changed",
@@ -1119,7 +1119,7 @@ func refuseTerminalSettingsThroughAVerb(ctx context.Context, inv *inventory.Inve
if string(was) == string(now) {
continue
}
return fmt.Errorf("%s of %s on %s is set at the controller's terminal only, never through a verb (this "+
return fmt.Errorf("%s of %s on %s is set at the controller's terminal only (`mesh-cli` on the control-node), never through a verb (this "+
"line came through %q): it says where root creates and owns a module's directories, which of "+
"the machine's paths are mounted into its container, what the mesh's consumers trust, or what a file "+
"root or a person's session obeys takes, and whoever may call a verb includes agents (novox/hq issue 339; "+
+6
View File
@@ -264,6 +264,12 @@ func serve(ctx context.Context) (err error) {
return err
}
defer stopServing()
// And the operator's command on every node, asked through each node's engine (novox/hq ADR 0272).
stopCLI, err := bus.ServeCLI(answerMeshCLI(open.inventory), log.New(os.Stdout, "", log.LstdFlags))
if err != nil {
return err
}
defer stopCLI()
// And says so on the bus (novox/hq ADR 0197): what it serves, as the NATS services protocol asks.
stopAnnouncing, err := bus.Announce(seatAnnouncement(handlers), log.New(os.Stdout, "", log.LstdFlags))
if err != nil {
+6
View File
@@ -26,6 +26,12 @@ import (
// shape fails the suite, naming its source. The keeper would say such a summary in words at run time;
// this is where the producer is made to say it rightly in the first place.
func TestMain(m *testing.M) {
// The process a mesh-cli test runs as a command line: it says the verb and the caller it was given, and
// ends (meshcli_test.go).
if os.Getenv(echoEnvironment) != "" {
fmt.Printf("verb=%q caller=%q\n", os.Getenv("MESH_VERB"), os.Getenv("MESH_CALLER"))
os.Exit(0)
}
conditions.Unsayable = func(o conditions.Observation, field string, r outward.Refusal) {
unsaid.note(o, field, r)
}
+46 -21
View File
@@ -226,6 +226,30 @@ func quoteAll(xs []string) string {
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 errors.New("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 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 <text> 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) {
@@ -242,22 +266,8 @@ func (a *verbArguments) commandLine() ([]string, error) {
if err != nil {
return nil, err
}
if len(argv) == 0 {
return nil, 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 nil, errors.New("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 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 nil, fmt.Errorf("%s is a repair done by hand, and says why: add --why <text> to the command "+
"line (recorded in the hand-act log). Nothing was done", repair)
if err := refusedAsTheGenericCommand(argv); err != nil {
return nil, err
}
return argv, nil
case "tools":
@@ -911,6 +921,25 @@ 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 the verb it came through. An empty verb is the controller's terminal (novox/hq ADR
// 0272 §4): no `MESH_VERB` at all, whatever this process was started with.
func commandEnvironment(caller, verb string) []string {
env := make([]string, 0, len(os.Environ())+2)
for _, kv := range os.Environ() {
if strings.HasPrefix(kv, verbVar+"=") || strings.HasPrefix(kv, link.CallerVar+"=") {
continue
}
env = append(env, kv)
}
env = append(env, link.CallerVar+"="+caller)
if verb != "" {
env = append(env, verbVar+"="+verb)
}
return env
}
// 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**
@@ -924,21 +953,17 @@ func runVerb(ctx context.Context, argv []string) (verbAnswer, error) {
return verbAnswer{}, fmt.Errorf("%w: this controller is stopping and runs no new command", link.ErrHandingOver)
}
cmd := selfCommand(ctx, argv)
// The same environment: the stores' credentials, the bus, the broker — everything a command run
// from a shell in this container would have, because it is that.
cmd.Env = os.Environ()
// 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"
}
cmd.Env = append(cmd.Env, link.CallerVar+"="+caller+", through the "+catalogue.ControllerSeatName+" seat")
// 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 = append(cmd.Env, verbVar+"="+verb)
cmd.Env = commandEnvironment(caller+", through the "+catalogue.ControllerSeatName+" seat", verb)
// 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
+130
View File
@@ -0,0 +1,130 @@
package link
import (
"context"
"encoding/json"
"fmt"
"log"
"strings"
"github.com/nats-io/nats.go"
)
// mesh-cli asks the controller through the node-engine of the machine it runs on (novox/hq ADR 0272 §3). The engine
// reads the asking account from the kernel and asks on its own node's subject, which only that node's engine may
// publish (its grant is `mesh.control.<node>.>`): so the node is a fact the bus server enforces, and the account a
// fact the kernel gave. The engine's side holds the same field names (mesh-host internal/meshcli, a test on each
// side), as for its health statement.
// CLISubjects is every node's mesh-cli subject, which the serving controller answers.
const CLISubjects = "mesh.control.*.cli"
// CLISubject is one node's.
func CLISubject(node string) string { return "mesh.control." + node + ".cli" }
// CLIAsked is a line mesh-cli was given on a node, and the account the node-engine says is asking.
type CLIAsked struct {
Line []string `json:"line"`
Account string `json:"account"`
UID uint32 `json:"uid"`
}
// CLIAnswer is what the controller answers: what the command printed, how it exited, whether it ran as the
// controller's terminal and why, or why nothing ran. The node-engine hands it to mesh-cli as it is.
type CLIAnswer struct {
Stdout []byte `json:"stdout,omitempty"`
Stderr []byte `json:"stderr,omitempty"`
Exit int `json:"exit"`
Terminal bool `json:"terminal"`
Why string `json:"why,omitempty"`
Refused string `json:"refused,omitempty"`
Cut bool `json:"cut,omitempty"`
}
// CLIRefusal is an answer saying nothing ran, and why.
func CLIRefusal(why string) CLIAnswer { return CLIAnswer{Exit: 1, Refused: why} }
// CLINode is the node a mesh-cli subject names, or false for any other subject.
func CLINode(subject string) (string, bool) {
rest, ok := strings.CutPrefix(subject, "mesh.control.")
if !ok {
return "", false
}
node, tail, ok := strings.Cut(rest, ".")
if !ok || tail != "cli" || node == "" {
return "", false
}
return node, true
}
// CLIHandler answers one line from one node.
type CLIHandler func(ctx context.Context, node string, asked CLIAsked) CLIAnswer
// ServeCLI answers mesh-cli for every node until stopped: one queue group, so of two controllers during a handover
// one answers. Each line on its own goroutine, bounded by HandlerTimeout, and its answer cut to one bus message.
func (b OverNATS) ServeCLI(handle CLIHandler, logger *log.Logger) (func(), error) {
done := make(chan struct{})
bind := func() (*nats.Subscription, error) {
return b.Conn.QueueSubscribe(CLISubjects, "mesh-cli", func(msg *nats.Msg) {
go b.answerCLI(msg, handle, logger)
})
}
sub, err := bind()
if err != nil {
return nil, fmt.Errorf("serving %s: %w", CLISubjects, err)
}
go keepBound(sub, bind, CLISubjects, done, logger)
return func() {
close(done)
_ = sub.Unsubscribe()
}, nil
}
func (b OverNATS) answerCLI(msg *nats.Msg, handle CLIHandler, logger *log.Logger) {
if msg.Reply == "" {
return
}
var answer CLIAnswer
node, ok := CLINode(msg.Subject)
var asked CLIAsked
switch {
case !ok:
answer = CLIRefusal("not a mesh-cli subject: " + msg.Subject)
case json.Unmarshal(msg.Data, &asked) != nil || len(asked.Line) == 0:
answer = CLIRefusal("the node-engine's request could not be read, so nothing ran")
default:
ctx, cancel := context.WithTimeout(context.Background(), HandlerTimeout)
answer = handle(ctx, node, asked)
cancel()
}
body := FitCLIAnswer(answer, b.Conn.MaxPayload())
if err := msg.Respond(body); err != nil && logger != nil {
logger.Printf("mesh-cli on %s: the answer could not be sent: %v", node, err)
}
}
// FitCLIAnswer is the answer as one bus message of at most limit bytes: what the command printed is cut, standard
// output first, and the cut is said (ADR 0272 §5) — never silently short.
func FitCLIAnswer(a CLIAnswer, limit int64) []byte {
body, _ := json.Marshal(a)
if limit <= 0 || int64(len(body)) <= limit {
return body
}
a.Cut = true
// Room for the rest of the answer and JSON's base64 of the streams (4 bytes for every 3).
room := (limit - 4096) * 3 / 4
if room < 0 {
room = 0
}
if int64(len(a.Stderr)) > room/2 {
a.Stderr = a.Stderr[:room/2]
}
if left := room - int64(len(a.Stderr)); int64(len(a.Stdout)) > left {
if left < 0 {
left = 0
}
a.Stdout = a.Stdout[:left]
}
body, _ = json.Marshal(a)
return body
}
+67
View File
@@ -0,0 +1,67 @@
package link
import (
"encoding/json"
"sort"
"strings"
"testing"
)
// The node-engine's request and the answer it hands mesh-cli hold these field names; the engine's side holds the
// same list (mesh-host internal/meshcli, TestTheRequestToTheControllerKeepsItsFieldNames).
func TestTheMeshCLIRequestAndAnswerKeepTheirFieldNames(t *testing.T) {
body, _ := json.Marshal(CLIAsked{Line: []string{"status"}, Account: "a", UID: 1})
if got := keysIn(t, body); got != "account line uid" {
t.Fatalf("the request's fields are %q", got)
}
body, _ = json.Marshal(CLIAnswer{Stdout: []byte("o"), Stderr: []byte("e"), Exit: 1, Terminal: true, Why: "w",
Refused: "r", Cut: true})
if got := keysIn(t, body); got != "cut exit refused stderr stdout terminal why" {
t.Fatalf("the answer's fields are %q", got)
}
}
func keysIn(t *testing.T, body []byte) string {
t.Helper()
var m map[string]any
if err := json.Unmarshal(body, &m); err != nil {
t.Fatal(err)
}
var keys []string
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return strings.Join(keys, " ")
}
// A node's mesh-cli subject names the node, and no other subject does.
func TestTheMeshCLISubjectNamesItsNode(t *testing.T) {
if node, ok := CLINode(CLISubject("laptop")); !ok || node != "laptop" {
t.Fatalf("CLINode(%q) = %q %v", CLISubject("laptop"), node, ok)
}
for _, s := range []string{"mesh.control.laptop.report", "mesh.control..cli", "mesh.control.a.b.cli", "mesh.node.a.cli"} {
if _, ok := CLINode(s); ok {
t.Fatalf("%s was read as a mesh-cli subject", s)
}
}
}
// An answer larger than one bus message is cut to fit, and the cut is said.
func TestALargeMeshCLIAnswerIsCutAndSaysSo(t *testing.T) {
a := CLIAnswer{Stdout: []byte(strings.Repeat("o", 200000)), Stderr: []byte(strings.Repeat("e", 1000)), Why: "the terminal"}
body := FitCLIAnswer(a, 64<<10)
if len(body) > 64<<10 {
t.Fatalf("the answer is %d bytes, over the bound", len(body))
}
var got CLIAnswer
if err := json.Unmarshal(body, &got); err != nil {
t.Fatal(err)
}
if !got.Cut || got.Why != "the terminal" || len(got.Stdout) == 0 || string(got.Stderr) != strings.Repeat("e", 1000) {
t.Fatalf("the cut answer is %+v", got)
}
if small := FitCLIAnswer(CLIAnswer{Stdout: []byte("ok")}, 64<<10); strings.Contains(string(small), `"cut"`) {
t.Fatal("an answer that fits was said to be cut")
}
}