mesh-cli requires the console's endpoint and writes its address
nox now reaches the mesh through this machine's console, so the module requires mcp-endpoint and writes the bound address to /etc/mesh-cli/console rather than trusting a fixed port. No invokes: mesh-cli has no bus account, and one nothing dials would only be minted. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
+46
-38
@@ -1,12 +1,16 @@
|
||||
# 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`.
|
||||
The operator's command on every node: `mesh-cli <command line>`, 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 <node>
|
||||
nox settings set zsh '{"execute":"withhold"}' --node <node>
|
||||
nox conditions --severity urgent
|
||||
nox deliveries
|
||||
nox assign <node> <module> --why <text>
|
||||
nox issue <number>
|
||||
nox call <node>/node-service-manager.status '{"unit":"plex"}'
|
||||
nox help
|
||||
```
|
||||
|
||||
## What it places
|
||||
@@ -14,57 +18,61 @@ nox settings set zsh '{"execute":"withhold"}' --node <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 <address> [json]` calls any seat verb or tool; `nox search <words>` and
|
||||
`nox describe <address>` 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 <file>`** 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 <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.
|
||||
- **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.
|
||||
|
||||
@@ -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 <out>/<binary>`), 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
http://127.0.0.1:${bound:mcp-endpoint:port}/mcp
|
||||
@@ -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": [
|
||||
|
||||
Reference in New Issue
Block a user