mesh-cli: the operator's command on every node, nox in zsh (hq ADR 0272)
mesh/merge-gate pass: builds claude-code, mounts, route-adapter, new: modules/mesh-cli → ace, g14, novox, shanks; no bus step; every machine composes with …
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds claude-code, mounts, route-adapter, new: modules/mesh-cli → ace, g14, novox, shanks; no bus step; every machine composes with …
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
The records send the operator to "the controller's terminal", and on the control-node nothing answered (hq issue 343). mesh-cli is a module of its own, assigned to every node and built from its own directory: its program is unpacked into /usr/local/bin by an archive resource (one file, no link, no copy), and its zsh contribution makes nox its alias. It holds no credential: it asks the node-engine on its machine over the SDK's go/cli socket, and prints the controller's answer with its exit. The agent module's refusals and README, and the route adapter's README, now name `mesh-cli settings set …` on the control-node instead of a mesh-controller command nobody could type; a settings file is passed inline, since a path would be read where the controller runs.
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
// Command mesh-cli is the operator's command on every node (novox/hq ADR 0272): `mesh-cli <command line>` asks the
|
||||
// controller to run one of its command lines, and prints what it said. In interactive zsh it is also `nox`.
|
||||
//
|
||||
// It holds no credential and reads no file of the mesh. It asks the node-engine on this machine over a local
|
||||
// socket; the engine reads which account is asking from the kernel and asks the controller. The controller decides:
|
||||
// on the control-node, from the operator's own account, the line runs as the controller's terminal; elsewhere it is
|
||||
// an ordinary call, and a change made at the terminal alone is refused with the reason; from any other account
|
||||
// nothing runs.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/cli"
|
||||
)
|
||||
|
||||
// socket is where the node-engine answers; a variable so a test can name its own.
|
||||
var socket = cli.Socket
|
||||
|
||||
const usage = `mesh-cli — the controller's command lines, from this node (novox/hq ADR 0272)
|
||||
|
||||
mesh-cli <command line> e.g. mesh-cli status, mesh-cli settings show zsh --node <node>
|
||||
nox <command line> the same, in interactive zsh
|
||||
|
||||
Run it as yourself, never under sudo: the node-engine tells the controller which account asks.
|
||||
On the control-node, from its operator's account, the line runs as the controller's terminal.
|
||||
Anywhere else it is an ordinary call, and a terminal-only change is refused with the reason.
|
||||
'mesh-cli help' asks the controller for its own list of command lines.
|
||||
`
|
||||
|
||||
func main() {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
code := run(ctx, os.Args[1:], os.Stdout, os.Stderr)
|
||||
stop()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
||||
if len(args) == 0 || args[0] == "-h" || args[0] == "--help" {
|
||||
fmt.Fprint(stderr, usage)
|
||||
return 2
|
||||
}
|
||||
// A verb's own process carries the verb it runs for; a verb is never a person, so it is never the terminal.
|
||||
if verb := os.Getenv("MESH_VERB"); verb != "" {
|
||||
fmt.Fprintf(stderr, "mesh-cli: this process runs for the verb %q, and a verb is never the operator. "+
|
||||
"Nothing was asked\n", verb)
|
||||
return 1
|
||||
}
|
||||
a, err := cli.Ask(ctx, socket, args)
|
||||
if err != nil {
|
||||
if errors.Is(ctx.Err(), context.Canceled) {
|
||||
fmt.Fprintln(stderr, "mesh-cli: stopped waiting; the line may still be running on the controller")
|
||||
return 130
|
||||
}
|
||||
fmt.Fprintf(stderr, "mesh-cli: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
_, _ = stdout.Write(a.Stdout)
|
||||
_, _ = stderr.Write(a.Stderr)
|
||||
if a.Cut {
|
||||
fmt.Fprintln(stderr, "mesh-cli: the output was longer than one bus message, and was cut there")
|
||||
}
|
||||
if !a.Terminal && a.Why != "" {
|
||||
fmt.Fprintln(stderr, "mesh-cli: "+a.Why)
|
||||
}
|
||||
if a.Refused != "" {
|
||||
fmt.Fprintln(stderr, "mesh-cli: "+a.Refused)
|
||||
if a.Exit == 0 {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return a.Exit
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/cli"
|
||||
)
|
||||
|
||||
// engine answers one connection on a socket of the test's own as the node-engine would, and says what it was asked.
|
||||
func engine(t *testing.T, answer cli.Answer) <-chan []string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "cli.sock")
|
||||
l, err := net.Listen("unix", path)
|
||||
if err != nil {
|
||||
t.Skipf("no unix socket here: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { l.Close() })
|
||||
before := socket
|
||||
socket = path
|
||||
t.Cleanup(func() { socket = before })
|
||||
asked := make(chan []string, 1)
|
||||
go func() {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
req, err := cli.ReadRequest(conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
asked <- req.Line
|
||||
_ = cli.WriteAnswer(conn, answer)
|
||||
}()
|
||||
return asked
|
||||
}
|
||||
|
||||
// The line is asked as given, what the controller printed is printed, and its exit is mesh-cli's.
|
||||
func TestTheLineIsAskedAndTheAnswerIsPrinted(t *testing.T) {
|
||||
t.Setenv("MESH_VERB", "")
|
||||
asked := engine(t, cli.Answer{Stdout: []byte("out\n"), Stderr: []byte("err\n"), Exit: 3, Terminal: true, Why: "the terminal"})
|
||||
var out, errOut bytes.Buffer
|
||||
code := run(context.Background(), []string{"settings", "set", "zsh", `{"execute":"withhold"}`}, &out, &errOut)
|
||||
if line := <-asked; strings.Join(line, "|") != `settings|set|zsh|{"execute":"withhold"}` {
|
||||
t.Fatalf("asked %q", line)
|
||||
}
|
||||
if code != 3 || out.String() != "out\n" || errOut.String() != "err\n" {
|
||||
t.Fatalf("exit %d, stdout %q, stderr %q", code, out.String(), errOut.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Not the terminal, refused, or cut: each is said, and a refusal never exits 0.
|
||||
func TestWhyARefusalAndACutAreSaid(t *testing.T) {
|
||||
t.Setenv("MESH_VERB", "")
|
||||
engine(t, cli.Answer{Why: "not the controller's terminal: agents on laptop may run as operator",
|
||||
Refused: "set at the controller's terminal only", Cut: true})
|
||||
var out, errOut bytes.Buffer
|
||||
code := run(context.Background(), []string{"settings", "set", "x", "{}"}, &out, &errOut)
|
||||
said := errOut.String()
|
||||
if code == 0 || !strings.Contains(said, "not the controller's terminal") || !strings.Contains(said, "terminal only") ||
|
||||
!strings.Contains(said, "cut") {
|
||||
t.Fatalf("exit %d, said %q", code, said)
|
||||
}
|
||||
}
|
||||
|
||||
// A verb's own process is never the operator: refused before anything is asked.
|
||||
func TestAVerbsProcessIsRefusedBeforeAsking(t *testing.T) {
|
||||
t.Setenv("MESH_VERB", "command")
|
||||
before := socket
|
||||
socket = filepath.Join(t.TempDir(), "nothing-listens.sock")
|
||||
t.Cleanup(func() { socket = before })
|
||||
var out, errOut bytes.Buffer
|
||||
if code := run(context.Background(), []string{"status"}, &out, &errOut); code != 1 ||
|
||||
!strings.Contains(errOut.String(), "never the operator") {
|
||||
t.Fatalf("exit %d, said %q", code, errOut.String())
|
||||
}
|
||||
}
|
||||
|
||||
// An engine that does not answer is said, with where it was asked.
|
||||
func TestAnEngineThatDoesNotAnswerIsSaid(t *testing.T) {
|
||||
t.Setenv("MESH_VERB", "")
|
||||
before := socket
|
||||
socket = filepath.Join(t.TempDir(), "nothing-listens.sock")
|
||||
t.Cleanup(func() { socket = before })
|
||||
var out, errOut bytes.Buffer
|
||||
if code := run(context.Background(), []string{"status"}, &out, &errOut); code != 1 ||
|
||||
!strings.Contains(errOut.String(), "does not answer") {
|
||||
t.Fatalf("exit %d, said %q", code, errOut.String())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user