diff --git a/.gitignore b/.gitignore index bab11407..77f4b90c 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,5 @@ modules/mesh-config/cmd/mesh-config/mesh-config modules/forticlient/cmd/forticlient-tools/forticlient-tools modules/networkmanager/uplink-tools checks/words/words +modules/mesh-cli/mesh-cli +modules/mesh-cli/cmd/mesh-cli/mesh-cli diff --git a/modules/mesh-cli/README.md b/modules/mesh-cli/README.md index 74f2387f..2bb2687b 100644 --- a/modules/mesh-cli/README.md +++ b/modules/mesh-cli/README.md @@ -1,12 +1,16 @@ # mesh-cli -The operator's command on every node (novox/hq ADR 0272): `mesh-cli ` asks the controller to run one -of its command lines, from wherever the operator is. In interactive zsh it is also `nox`. +The operator's command on every node: `mesh-cli `, in interactive zsh also `nox`. Each line calls the +seat verb or the module tool that owns its job, through this machine's console, and prints the owner's answer. ``` nox status -nox settings show zsh --node -nox settings set zsh '{"execute":"withhold"}' --node +nox conditions --severity urgent +nox deliveries +nox assign --why +nox issue +nox call /node-service-manager.status '{"unit":"plex"}' +nox help ``` ## What it places @@ -14,57 +18,61 @@ nox settings set zsh '{"execute":"withhold"}' --node | 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 console's address | `/etc/mesh-cli/console` | a file resource from `files/console`: `http://127.0.0.1:${bound:mcp-endpoint:port}/mcp`, from the `mcp-endpoint` the module requires | | 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. +No link and no second copy. `/usr/local/bin` is on every account's `PATH` already. 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. +`mesh-cli` holds no credential and has no bus account of its own (so its manifest declares no `invokes`: an account +nothing dials would only be minted and kept). It calls `mesh_call` on this machine's console, the mesh MCP server the +module node-tools serves on loopback (novox/hq ADR 0152: whoever is on the machine owns the mesh there), and the console +puts the call on the bus, as every agent session on the machine does. The console's address is read from +`/etc/mesh-cli/console`, or `MESH_CONSOLE` when set, or node-tools' declared port when the file is not there yet. -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. A line still running after ten seconds is followed by its call until it ends. `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. When no answer comes in - time it says the line may have run and names its call; "nothing ran" is said only of a line no controller - took. +- **Who judges.** The seat that owns the job: a change of placement is a proposal the operator approves + (`mesh-placement.propose`), a config value is set with its why (`mesh-config.config`), a silence or a hand act is + recorded as one. Until novox/hq issue 1126 the controller ran these lines and judged the account (ADR 0272); with + the controller unassigned, that judgement went with it. +- **The map.** Every line the controller ran, and the seat verb or tool that does its job now, or none, is + `commands.go`'s table (and hq's `.work/1126/nox-map.md` while issue 1126 runs). A line no seat or module owns says + that it was the controller's, that the controller is unassigned, and what to use instead, and exits 1 having called + nothing. A line nox cannot read says how it is written and exits 2. +- **Any address.** `nox call
[json]` calls any seat verb or tool; `nox search ` and + `nox describe
` find one and its arguments. +- **What is printed.** The owner's answer as the console gave it (JSON), on standard output with exit 0; an answer the + console marks an error (a refusal, nothing serving, no answer within its bound) on standard error with exit 1. A + console that does not answer is said as nothing having been called; no answer in time is said as a call that may + have run. +- **`nox status`** is the delivery view's (`mesh-delivery-view.view_status`). When the view does not answer, nox says + so first, shows the open conditions and the deliveries under their own addresses, and exits 1: they are never + passed off as the status. +- **`nox enrol open --key `** runs here and asks nothing of the mesh (ADR 0587 §8). -A process that runs for a verb (it carries `MESH_VERB`) is refused before anything is asked: a verb is never the +A process that runs for a verb (it carries `MESH_VERB`) is refused before anything is called: 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 "$(cat settings.json)" --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. -- **The `MESH_VERB` refusal is a courtesy.** A verb's process is refused before asking, but the gate is the - controller: it judges the account the node-engine vouches for, whatever the program says. -- **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. +- **No formatting beyond `nox nodes`.** Answers are the owners' JSON. +- **Lines with no owner.** Accepting a secret from a person, making or setting the operator key, opening a sealed + secret locally, registering a module from another repository and forgetting a module have no seat verb yet; nox + names what to use meanwhile. ## Tests -`go test ./...`: the module places exactly one file (one archive resource, one Go bundle compiled to the binary -alone); the line is asked as given and the answer printed with its exit; a reason, a refusal, a cut and an outcome not known -are said and a refusal never exits 0; a verb's process is refused before asking; an engine that does not answer -is said. +`go test ./...`: every owned line calls its owner's address with the arguments it reads, against a stand-in console +answering MCP `tools/call` as node-tools does; a line with no owner or not read calls nothing and says why; an error +answer, a silent console and a verb's process are said; the status's fallback is labelled and exits 1; the console's +address is found; `enrol open` opens a sealed token locally; the module places the program and the console's address +and nothing else. ## Files it does not know Each directory or archive this module declares says how it treats a file it did not write (ADR 0422, ADR 0416). - `/usr/local/bin`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2). +- `/etc/mesh-cli`: the program's; it holds the console's address alone. diff --git a/modules/mesh-cli/cmd/mesh-cli/manifest_test.go b/modules/mesh-cli/cmd/mesh-cli/manifest_test.go index dec228c1..c411eab7 100644 --- a/modules/mesh-cli/cmd/mesh-cli/manifest_test.go +++ b/modules/mesh-cli/cmd/mesh-cli/manifest_test.go @@ -6,11 +6,11 @@ import ( "testing" ) -// **The archive places exactly one file, /usr/local/bin/mesh-cli** (review of novox/hq ADR 0272): one archive +// **The archive places exactly one file, /usr/local/bin/mesh-cli**, and the console's address is one file more (review of novox/hq ADR 0272): one archive // resource, unpacking one artifact into /usr/local/bin, and that artifact a compiled Go bundle — the builder writes // such a bundle as its binary alone (`go build -o /`), named mesh-cli — with nothing the runtime would // load or put beside it. A second file in the bundle, or a second resource writing there, fails here. -func TestTheArchivePlacesExactlyOneFile(t *testing.T) { +func TestTheArchivePlacesExactlyOneFileAndTheConsolesAddressOne(t *testing.T) { raw, err := os.ReadFile("../../module.json") if err != nil { t.Fatal(err) @@ -24,12 +24,24 @@ func TestTheArchivePlacesExactlyOneFile(t *testing.T) { if err := json.Unmarshal(raw, &m); err != nil { t.Fatal(err) } - if len(m.Resources) != 1 { - t.Fatalf("the module declares %d resources; it places one file", len(m.Resources)) + // Beside the program, one directory and one file: the console's address, from the mcp-endpoint it requires + // (novox/hq issue 1126). Nothing else. + var r map[string]any + for _, res := range m.Resources { + switch { + case res["type"] == "archive": + if r != nil { + t.Fatalf("two archives: %v", res) + } + r = res + case res["type"] == "directory" && res["path"] == "/etc/mesh-cli": + case res["type"] == "file" && res["path"] == consoleFile && res["template"] == "files/console": + default: + t.Fatalf("the module declares %v beside the program and the console's address", res) + } } - r := m.Resources[0] - if r["type"] != "archive" || r["path"] != "/usr/local/bin" { - t.Fatalf("its resource is %v", r) + if len(m.Resources) != 3 || r == nil || r["path"] != "/usr/local/bin" { + t.Fatalf("its resources are %v", m.Resources) } // The build has a second artifact, the files archive the controller reads the templates back from; it // places no file. The artifact the resource unpacks is the one that does. @@ -66,3 +78,22 @@ func TestTheArchivePlacesExactlyOneFile(t *testing.T) { } } } + +// The console's address is written from the mcp-endpoint the module requires, as claude-code writes it, so it follows +// the port node-tools is bound on. +func TestTheConsolesAddressIsBoundFromTheEndpoint(t *testing.T) { + raw, err := os.ReadFile("../../module.json") + if err != nil { + t.Fatal(err) + } + var m struct { + Requires []string `json:"requires"` + } + if err := json.Unmarshal(raw, &m); err != nil || len(m.Requires) != 1 || m.Requires[0] != "mcp-endpoint" { + t.Fatalf("it requires %v (%v)", m.Requires, err) + } + tpl, err := os.ReadFile("../../files/console") + if err != nil || string(tpl) != "http://127.0.0.1:${bound:mcp-endpoint:port}/mcp\n" { + t.Fatalf("files/console is %q (%v)", tpl, err) + } +} diff --git a/modules/mesh-cli/files/console b/modules/mesh-cli/files/console new file mode 100644 index 00000000..198dbd46 --- /dev/null +++ b/modules/mesh-cli/files/console @@ -0,0 +1 @@ +http://127.0.0.1:${bound:mcp-endpoint:port}/mcp diff --git a/modules/mesh-cli/module.json b/modules/mesh-cli/module.json index 681a3e55..d533c48e 100644 --- a/modules/mesh-cli/module.json +++ b/modules/mesh-cli/module.json @@ -2,6 +2,9 @@ "module": "mesh-cli", "version": "1", "moved": true, + "requires": [ + "mcp-endpoint" + ], "resources": [ { "id": "program", @@ -9,6 +12,20 @@ "artifact": "program", "path": "/usr/local/bin", "unknown": "program" + }, + { + "id": "console-dir", + "type": "directory", + "path": "/etc/mesh-cli", + "mode": "0755", + "unknown": "program" + }, + { + "id": "console", + "type": "file", + "path": "/etc/mesh-cli/console", + "mode": "0644", + "template": "files/console" } ], "items": [