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:
@@ -94,8 +94,8 @@ another node. So these tools refuse, and say why and how it is done instead:
|
||||
|
||||
- `claude_code_hook_register`, `claude_code_settings_set`, `claude_code_permission_add`, `claude_code_permission_remove`:
|
||||
the operator sets hooks, permission rules and every other managed setting in this module's `managed_settings`
|
||||
setting, `mesh-controller settings set claude-code '{"managed_settings": {…}}' [--node <node>]`, typed at the
|
||||
controller's terminal. The controller refuses `managed_settings`, `mcp_servers` and `role` through any verb: a
|
||||
setting, `mesh-cli settings set claude-code '{"managed_settings": {…}}' [--node <node>]`, typed at the
|
||||
controller's terminal (`mesh-cli`, or `nox`, on the control-node; hq ADR 0272). The controller refuses `managed_settings`, `mcp_servers` and `role` through any verb: a
|
||||
mergeable file asks for every key its own content names;
|
||||
- `claude_code_mcp_register`: the operator sets tool servers in the `mcp_servers` setting the same way;
|
||||
- a skill, subagent or command that would run something by itself: a shell line the agent runs when it loads one
|
||||
@@ -117,7 +117,7 @@ decision of 2026-10-09; a surface of the operator's own may update it later). In
|
||||
skills, commands and subagents at the `mesh` or `node` scope are refused through the tools, and the import tool is
|
||||
refused. They are keys of this module's settings, each text a list of its lines, because a setting holds no line
|
||||
break: `instructions`, `output_styles`, `commands` and `agents` map a name to its text; `skills` maps a name to its
|
||||
files, each a list of lines. For example `mesh-controller settings set claude-code '{"instructions": {"conventions":
|
||||
files, each a list of lines. For example `mesh-cli settings set claude-code '{"instructions": {"conventions":
|
||||
["Commit in the imperative.", "", "Test before pushing."]}}'`. They land where a registration did, laid over what was
|
||||
registered before; what the operator writes there is not judged, but an item that is not well formed is left out and
|
||||
said in the render's answer. What was registered before is still rendered and may be unregistered.
|
||||
@@ -156,8 +156,8 @@ A new session takes a change; a running one at `/reload-plugins`.
|
||||
|
||||
## Settings
|
||||
|
||||
Per node or for the whole mesh, at the controller's terminal alone: `mesh-controller settings set claude-code '<json>'
|
||||
[--node <node>]`. The controller refuses all three keys through any verb, `mesh-controller.settings` included, because
|
||||
Per node or for the whole mesh, at the controller's terminal alone: `mesh-cli settings set claude-code '<json>'
|
||||
[--node <node>]` on the control-node (in zsh, `nox`; hq ADR 0272). The controller refuses all three keys through any verb, `mesh-controller.settings` included, because
|
||||
each reaches every session on the node and a verb's caller may be an agent (hq issue 340). Reading them through the
|
||||
verb still answers.
|
||||
|
||||
|
||||
@@ -86,8 +86,8 @@ var nodeName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}$`)
|
||||
|
||||
// AtTheTerminal is how a hook, a permission rule or any other managed setting is set now (novox/hq issue 340):
|
||||
// in this module's `managed_settings` setting, at the controller's terminal, which refuses it through a verb.
|
||||
const AtTheTerminal = "mesh-controller settings set claude-code '{\"managed_settings\": {...}}' [--node <node>], " +
|
||||
"typed by the operator at the controller's terminal"
|
||||
const AtTheTerminal = "mesh-cli settings set claude-code '{\"managed_settings\": {...}}' [--node <node>], " +
|
||||
"typed by the operator at the controller's terminal: mesh-cli (nox) on the control-node (novox/hq ADR 0272)"
|
||||
|
||||
// RunsInASession says why a kind is never registered through this module: what it holds runs in, or is obeyed
|
||||
// by, every session it reaches, as whoever starts that session (novox/hq issue 340).
|
||||
@@ -935,7 +935,7 @@ func SharedTextAtTheTerminal(kind string) string {
|
||||
KindOutputStyle: "output_styles", KindInstructions: "instructions"}[kind]
|
||||
return "a " + kind + " for every node or some reaches every session there as the mesh's own word, and this tool " +
|
||||
"cannot tell an agent from the operator. It is set at the controller's terminal only, under `" + key +
|
||||
"` in this module's settings, its text as a list of lines: mesh-controller settings set claude-code " +
|
||||
"` in this module's settings, its text as a list of lines, on the control-node: mesh-cli settings set claude-code " +
|
||||
"'{\"" + key + "\": {\"<name>\": [\"line\", …]}}' [--node <node>] (novox/hq issue 340). The home scope, " +
|
||||
"one account's own directory, is still registered here. Nothing was registered"
|
||||
}
|
||||
|
||||
@@ -604,7 +604,7 @@ func OnServerChange(v *ServerView, c ServerChange, p Paths, write WriteManaged)
|
||||
const ServerAtTheTerminal = "a tool server is started, or handed what the agent asks it, by every session on the nodes it " +
|
||||
"reaches, as whoever starts that session — the operator, whose account holds root — and the tool runner is not " +
|
||||
"told who called it, so an agent on another node could register one. It is set at the controller's terminal " +
|
||||
"only, in this module's mcp_servers setting: mesh-controller settings set claude-code '{\"mcp_servers\": {...}}' " +
|
||||
"only, in this module's mcp_servers setting, on the control-node: mesh-cli settings set claude-code '{\"mcp_servers\": {...}}' " +
|
||||
"[--node <node>] (novox/hq issue 340). Nothing was registered"
|
||||
|
||||
// RegisterServer unregisters a server: a delete per scope in the `servers` state, taken into this node's view at
|
||||
|
||||
@@ -130,7 +130,7 @@ func TestWhatASessionRunsIsRefusedThroughEveryTool(t *testing.T) {
|
||||
|
||||
// The settings and the tool server refusals name the terminal's line.
|
||||
text, _ := s.call(t, "claude_code_settings_set", map[string]any{"settings": map[string]any{"model": "x"}})
|
||||
if !strings.Contains(text, "mesh-controller settings set claude-code") {
|
||||
if !strings.Contains(text, "mesh-cli settings set claude-code") {
|
||||
t.Errorf("the refusal does not say how it is done: %s", text)
|
||||
}
|
||||
text, _ = s.call(t, "claude_code_mcp_register", map[string]any{"name": "x", "url": "https://x.example"})
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# mesh-cli
|
||||
|
||||
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, from wherever the operator is. In interactive zsh it is also `nox`.
|
||||
|
||||
```
|
||||
nox status
|
||||
nox settings show zsh --node <node>
|
||||
nox settings set zsh '{"execute":"withhold"}' --node <node>
|
||||
```
|
||||
|
||||
## What it places
|
||||
|
||||
| What | Where | How |
|
||||
|---|---|---|
|
||||
| the program | `/usr/local/bin/mesh-cli` | the `program` bundle, unpacked by an `archive` resource: the node-engine moves the one file in, refuses to write over a file it did not place, and takes out only that file when the module goes |
|
||||
| the alias `nox` | the zsh module's mesh block in `~/.zshrc` | a `shell` contribution in the `normal` slot (ADR 0204) |
|
||||
|
||||
No link and no second copy. `/usr/local/bin` is on every account's `PATH` already, so the module contributes no
|
||||
environment. The alias is interactive zsh only: a script, `sudo`, a cron line and any document meant to work
|
||||
everywhere say `mesh-cli`. In interactive zsh the alias hides any other `nox` (Arch's `python-nox` installs
|
||||
`/usr/bin/nox`); `command nox` still reaches it.
|
||||
|
||||
## How it is answered
|
||||
|
||||
`mesh-cli` holds no credential and reads no file of the mesh. It writes one request to the node-engine's socket
|
||||
(the SDK's `go/cli`: the line, as words) and prints the one answer.
|
||||
|
||||
1. The node-engine reads which account is asking from the kernel, never from the program, and asks the controller
|
||||
on its own node's subject.
|
||||
2. The controller judges (ADR 0272 §4):
|
||||
- **the controller's terminal**: the node's operator account, on the control-node. The line runs as the
|
||||
operator typed it there.
|
||||
- **an ordinary call**: the node's operator account on any other node, where agents may run as that account.
|
||||
It meets every refusal of the generic `command` verb, and a change made at the terminal alone is refused with
|
||||
the reason.
|
||||
- **refused**: any other account, root included. Run it as yourself, not under `sudo`.
|
||||
3. `mesh-cli` prints standard output and standard error as the command wrote them, says why a line was not the
|
||||
terminal or was refused, says when the output was cut to fit one bus message, and exits with the command's
|
||||
status.
|
||||
|
||||
A process that runs for a verb (it carries `MESH_VERB`) is refused before anything is asked: a verb is never the
|
||||
operator.
|
||||
|
||||
## What it does not do yet
|
||||
|
||||
- **No standard input.** A command that reads one gets nothing, and fails saying so.
|
||||
- **No local files.** A path in a line names a file where the controller runs, read by the controller's account.
|
||||
Give settings inline: `nox settings set <module> "$(cat settings.json)" --node <node>`.
|
||||
- **No streaming.** The answer arrives when the command ends, within the controller's bound on a call (five
|
||||
minutes). Interrupting `mesh-cli` stops the wait, not the command.
|
||||
- **No other node is the terminal.** Phase 2 of ADR 0272 adds a node whose agents run under an account of their
|
||||
own (ADR 0266), judged unable to become root.
|
||||
|
||||
## Tests
|
||||
|
||||
`go test ./...`: the line is asked as given and the answer printed with its exit; a reason, a refusal and a cut
|
||||
are said and a refusal never exits 0; a verb's process is refused before asking; an engine that does not answer
|
||||
is said.
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
module mesh-cli
|
||||
|
||||
go 1.22
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261009100314-bfe82315f42c
|
||||
@@ -0,0 +1,2 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261009100314-bfe82315f42c h1:8hjQ3FYt7mGdZv+HX0Vk+n3pdaS3W5EIlwsrvSXXhRY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261009100314-bfe82315f42c/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"module": "mesh-cli",
|
||||
"version": "1",
|
||||
"resources": [
|
||||
{
|
||||
"id": "program",
|
||||
"type": "archive",
|
||||
"artifact": "program",
|
||||
"path": "/usr/local/bin"
|
||||
}
|
||||
],
|
||||
"shell": [
|
||||
{
|
||||
"for": "zsh",
|
||||
"slot": "normal",
|
||||
"code": "# The operator's command (module mesh-cli, novox/hq ADR 0272): nox is mesh-cli, in interactive zsh only.\n# A script, sudo and a document meant to work everywhere say mesh-cli.\nalias nox=mesh-cli\n"
|
||||
}
|
||||
],
|
||||
"build": {
|
||||
"artifacts": [
|
||||
{
|
||||
"name": "program",
|
||||
"kind": "bundle",
|
||||
"language": "go",
|
||||
"system": "arch",
|
||||
"from": "cmd/mesh-cli",
|
||||
"binary": "mesh-cli"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -107,7 +107,7 @@ into the container.
|
||||
| `machine` | `host.docker.internal` | how the predecessor's proxy reaches this machine |
|
||||
|
||||
```
|
||||
mesh-controller settings set route-adapter settings.json --node <node>
|
||||
mesh-cli settings set route-adapter "$(cat settings.json)" --node <node>
|
||||
```
|
||||
|
||||
**One thing does not follow the setting yet, and it fails loudly rather than quietly.** The
|
||||
|
||||
Reference in New Issue
Block a user