The seats are called mesh-module-assigner and mesh-node-enroller in mesh-cli, claude-code, the checks, mesh-config, mesh-vault, conditions, self-check and the provisioner client (ADR 0634 §9; K-21; issue 1966)

This commit is contained in:
2026-11-05 14:19:32 +01:00
parent a2272916a0
commit 9fafbcfd58
34 changed files with 93 additions and 93 deletions
+17 -17
View File
@@ -111,17 +111,17 @@ var commands = []command{
// What runs where.
{words: []string{"status"}, does: "what is wrong, what is quiet, what is moving (the delivery view)",
build: fixed("mesh-delivery-view.view_status")},
{words: []string{"nodes"}, does: "the nodes placement holds", build: fixed("mesh-placement.placement"),
{words: []string{"nodes"}, does: "the nodes placement holds", build: fixed("mesh-module-assigner.placement"),
render: renderNodes},
{words: []string{"node", "list"}, does: "the nodes placement holds", build: fixed("mesh-placement.placement"),
{words: []string{"node", "list"}, does: "the nodes placement holds", build: fixed("mesh-module-assigner.placement"),
render: renderNodes},
{words: []string{"node", "show"}, usage: "<node>", does: "one node's placement: what it runs and its decided fields",
build: nodeArg("mesh-placement.placement")},
build: nodeArg("mesh-module-assigner.placement")},
{words: []string{"node", "public-domain"}, usage: "<node> [<domain> | --clear] [--why <text>]",
does: "the domain a node composes its routed names under; with a domain or --clear, proposed to placement",
allowed: []string{"clear", "why"}, build: publicDomain},
{words: []string{"node", "add"}, instead: "a new machine joins through the seat mesh-enrolment: nox enrol issue <node> --why <text>"},
{words: []string{"node", "networks"}, instead: "no seat keeps a node's routed networks yet; mesh-placement.placement shows what a node decides"},
{words: []string{"node", "add"}, instead: "a new machine joins through the seat mesh-node-enroller: nox enrol issue <node> --why <text>"},
{words: []string{"node", "networks"}, instead: "no seat keeps a node's routed networks yet; mesh-module-assigner.placement shows what a node decides"},
{words: []string{"plan"}, usage: "<node>", does: "what the assigner wants the node to run", build: optNode("mesh-delivery-view.assigner_wants")},
{words: []string{"haves"}, usage: "[<node>]", does: "what each node runs, as its node-engine said", build: optNode("mesh-delivery-view.node_haves")},
{words: []string{"switch"}, does: "what each node runs, as its node-engine said", build: fixed("mesh-delivery-view.node_haves")},
@@ -133,9 +133,9 @@ var commands = []command{
return call{l.pos[0] + "/node-engine.composition", map[string]any{"module": l.pos[1]}}, nil
}},
{words: []string{"seats"}, does: "who holds each seat, and where", build: func(line) (call, error) {
return call{"mesh-placement.placement", map[string]any{"seats": "true"}}, nil
return call{"mesh-module-assigner.placement", map[string]any{"seats": "true"}}, nil
}},
{words: []string{"seat"}, instead: "a seat is held by placing its module: nox assign <node> <module> --why <text> (mesh-placement.propose)"},
{words: []string{"seat"}, instead: "a seat is held by placing its module: nox assign <node> <module> --why <text> (mesh-module-assigner.propose)"},
{words: []string{"assign"}, usage: "<node> <module>... --why <text>", does: "propose placing modules on a node",
allowed: []string{"why"}, build: placeModules("place")},
{words: []string{"unassign"}, usage: "<node> <module>... --why <text>", does: "propose taking modules off a node",
@@ -155,9 +155,9 @@ var commands = []command{
}
return propose(l, map[string]any{"op": "unpin", "node": l.pos[0], "provision": l.pos[1]})
}},
{words: []string{"proposals"}, usage: "[<id>]", does: "the open proposals of placement", build: optArg("mesh-placement.proposals", "id")},
{words: []string{"overlay", "show"}, does: "every node's placement, with its tunnel", build: fixed("mesh-placement.placement")},
{words: []string{"overlay"}, instead: "a node's tunnel is a decided field of its placement: mesh-placement.propose with op decide"},
{words: []string{"proposals"}, usage: "[<id>]", does: "the open proposals of placement", build: optArg("mesh-module-assigner.proposals", "id")},
{words: []string{"overlay", "show"}, does: "every node's placement, with its tunnel", build: fixed("mesh-module-assigner.placement")},
{words: []string{"overlay"}, instead: "a node's tunnel is a decided field of its placement: mesh-module-assigner.propose with op decide"},
// Deliveries and builds.
{words: []string{"deliveries"}, usage: "[--state s] [--repository owner/repo] [--group g] [--all]",
@@ -458,7 +458,7 @@ var commands = []command{
return call{"knowledge-base.progress", a}, nil
}},
// Enrolment (the seat mesh-enrolment; `enrol open` is local, in main.go).
// Enrolment (the seat mesh-node-enroller; `enrol open` is local, in main.go).
{words: []string{"enrol", "issue"}, usage: "<node> --why <text> [--expires 1h]", does: "a one-time token for a new machine, on the operator's Approve",
allowed: []string{"why", "expires"}, build: enrolIssue},
{words: []string{"token", "issue"}, usage: "--new <node> --why <text>", does: "the same as nox enrol issue",
@@ -472,15 +472,15 @@ var commands = []command{
}
return enrolIssue(l)
}},
{words: []string{"enrol", "tokens"}, usage: "[<node>]", does: "every open enrolment, or one", build: optArg("mesh-enrolment.tokens", "node")},
{words: []string{"enrol", "tokens"}, usage: "[<node>]", does: "every open enrolment, or one", build: optArg("mesh-node-enroller.tokens", "node")},
{words: []string{"enrol", "revoke"}, usage: "<node> --why <text>", does: "end an enrolment at any stage", allowed: []string{"why"},
build: func(l line) (call, error) {
if len(l.pos) != 1 || !l.has("why") {
return call{}, usageErr("give the node and --why")
}
return call{"mesh-enrolment.revoke", map[string]any{"node": l.pos[0], "why": l.flags["why"]}}, nil
return call{"mesh-node-enroller.revoke", map[string]any{"node": l.pos[0], "why": l.flags["why"]}}, nil
}},
{words: []string{"enrol", "again"}, usage: "<node>", does: "raise an enrolment's ask again", build: needArg("mesh-enrolment.again", "node")},
{words: []string{"enrol", "again"}, usage: "<node>", does: "raise an enrolment's ask again", build: needArg("mesh-node-enroller.again", "node")},
// Any address.
{words: []string{"call"}, usage: "<address> [<json arguments>]", does: "call any seat verb or module tool, e.g. ace/node-service-manager.status",
@@ -630,7 +630,7 @@ func propose(l line, changes ...map[string]any) (call, error) {
for i, c := range changes {
list[i] = c
}
return call{"mesh-placement.propose", map[string]any{"why": l.flags["why"], "changes": list}}, nil
return call{"mesh-module-assigner.propose", map[string]any{"why": l.flags["why"], "changes": list}}, nil
}
func placeModules(op string) func(line) (call, error) {
@@ -649,7 +649,7 @@ func placeModules(op string) func(line) (call, error) {
func publicDomain(l line) (call, error) {
switch {
case len(l.pos) == 1 && !l.has("clear"):
return call{"mesh-placement.placement", map[string]any{"node": l.pos[0]}}, nil
return call{"mesh-module-assigner.placement", map[string]any{"node": l.pos[0]}}, nil
case len(l.pos) == 1:
return propose(l, map[string]any{"op": "decide", "node": l.pos[0], "field": "public-domain", "value": nil})
case len(l.pos) == 2 && !l.has("clear"):
@@ -705,7 +705,7 @@ func enrolIssue(l line) (call, error) {
}
a := pick(l, "why", "expires")
a["node"] = l.pos[0]
return call{"mesh-enrolment.issue", a}, nil
return call{"mesh-node-enroller.issue", a}, nil
}
func addressCall(address string, rest []string) (call, error) {
+2 -2
View File
@@ -2,7 +2,7 @@ package main
// `nox enrol open`: the operator opens an enrolment token at their own terminal (novox/hq ADR 0587 §6, §8).
//
// The seat mesh-enrolment's `tokens` answers the token sealed to the operator key, which only the operator's private key
// The seat mesh-node-enroller's `tokens` answers the token sealed to the operator key, which only the operator's private key
// opens; the mesh never holds it. This subcommand is local: it asks nothing of the node-engine's socket and nothing of
// the controller or the bus. It reads the sealed token (the base64 of the sealed box) on standard input and the private
// key from a file (`--key`, the raw X25519 key in base64 that `operator key make` wrote), opens the box, and prints the
@@ -22,7 +22,7 @@ import (
const enrolUsage = `nox enrol open --key <operator private key file>
Opens an enrolment token sealed to the operator key (the answer of the seat mesh-enrolment's tokens) and prints the token
Opens an enrolment token sealed to the operator key (the answer of the seat mesh-node-enroller's tokens) and prints the token
to type on the new machine. The sealed token is read on standard input; this runs here and asks nothing of the mesh.
`
+13 -13
View File
@@ -87,17 +87,17 @@ func TestEachLineCallsItsOwner(t *testing.T) {
address string
args string
}{
{"node show ace", "mesh-placement.placement", `{"node":"ace"}`},
{"node public-domain ace", "mesh-placement.placement", `{"node":"ace"}`},
{"node public-domain ace example.org --why moved", "mesh-placement.propose", `{"changes":[{"field":"public-domain","node":"ace","op":"decide","value":"example.org"}],"why":"moved"}`},
{"node show ace", "mesh-module-assigner.placement", `{"node":"ace"}`},
{"node public-domain ace", "mesh-module-assigner.placement", `{"node":"ace"}`},
{"node public-domain ace example.org --why moved", "mesh-module-assigner.propose", `{"changes":[{"field":"public-domain","node":"ace","op":"decide","value":"example.org"}],"why":"moved"}`},
{"plan ace", "mesh-delivery-view.assigner_wants", `{"node":"ace"}`},
{"haves", "mesh-delivery-view.node_haves", `{}`},
{"composition ace plex", "ace/node-engine.composition", `{"module":"plex"}`},
{"seats", "mesh-placement.placement", `{"seats":"true"}`},
{"assign ace plex sonarr --why=media", "mesh-placement.propose", `{"changes":[{"module":"plex","node":"ace","op":"place"},{"module":"sonarr","node":"ace","op":"place"}],"why":"media"}`},
{"unassign ace plex --why gone", "mesh-placement.propose", `{"changes":[{"module":"plex","node":"ace","op":"take-off"}],"why":"gone"}`},
{"pin ace postgres-database novox postgres --why one", "mesh-placement.propose", `{"changes":[{"node":"ace","op":"pin","provider":{"module":"postgres","node":"novox"},"provision":"postgres-database"}],"why":"one"}`},
{"unpin ace postgres-database --why back", "mesh-placement.propose", `{"changes":[{"node":"ace","op":"unpin","provision":"postgres-database"}],"why":"back"}`},
{"seats", "mesh-module-assigner.placement", `{"seats":"true"}`},
{"assign ace plex sonarr --why=media", "mesh-module-assigner.propose", `{"changes":[{"module":"plex","node":"ace","op":"place"},{"module":"sonarr","node":"ace","op":"place"}],"why":"media"}`},
{"unassign ace plex --why gone", "mesh-module-assigner.propose", `{"changes":[{"module":"plex","node":"ace","op":"take-off"}],"why":"gone"}`},
{"pin ace postgres-database novox postgres --why one", "mesh-module-assigner.propose", `{"changes":[{"node":"ace","op":"pin","provider":{"module":"postgres","node":"novox"},"provision":"postgres-database"}],"why":"one"}`},
{"unpin ace postgres-database --why back", "mesh-module-assigner.propose", `{"changes":[{"node":"ace","op":"unpin","provision":"postgres-database"}],"why":"back"}`},
{"deliveries --state merged --json", "mesh-delivery.deliveries", `{"state":"merged"}`},
{"delivery novox/mesh-sdk@dff0ce2abb21", "mesh-delivery.show", `{"id":"novox/mesh-sdk@dff0ce2abb21"}`},
{"checks novox/mesh-catalog --number 591", "mesh-delivery.checks", `{"number":"591","repository":"novox/mesh-catalog"}`},
@@ -132,9 +132,9 @@ func TestEachLineCallsItsOwner(t *testing.T) {
{"kb ask what holds about the merge window", "knowledge-base.ask", `{"question":"what holds about the merge window"}`},
{"kb get 587 --reasoning", "knowledge-base.get", `{"number":587,"reasoning":true}`},
{"progress 47", "knowledge-base.progress", `{"of":"design 47"}`},
{"enrol issue new1 --why laptop --expires 2h", "mesh-enrolment.issue", `{"expires":"2h","node":"new1","why":"laptop"}`},
{"token issue --new new1 --why laptop", "mesh-enrolment.issue", `{"node":"new1","why":"laptop"}`},
{"enrol tokens", "mesh-enrolment.tokens", `{}`},
{"enrol issue new1 --why laptop --expires 2h", "mesh-node-enroller.issue", `{"expires":"2h","node":"new1","why":"laptop"}`},
{"token issue --new new1 --why laptop", "mesh-node-enroller.issue", `{"node":"new1","why":"laptop"}`},
{"enrol tokens", "mesh-node-enroller.tokens", `{}`},
{"call ace/node-service-manager.status {\"unit\":\"plex\"}", "ace/node-service-manager.status", `{"unit":"plex"}`},
{"ask mesh-catalog catalog_modules", "mesh-catalog.catalog_modules", `{}`},
} {
@@ -190,13 +190,13 @@ func TestALineWithNoOwnerOrNotReadCallsNothingAndSaysWhy(t *testing.T) {
// What the console says is printed as said: an answer it marks an error on standard error with exit 1, a console that
// does not answer as nothing called, a verb's process refused before anything is called; nodes are one line each.
func TestAnswersRefusalsAndSilenceAreSaid(t *testing.T) {
got := console(t, map[string]string{"mesh-enrolment.tokens": "!nothing serves seat:mesh-enrolment.tokens"})
got := console(t, map[string]string{"mesh-node-enroller.tokens": "!nothing serves seat:mesh-node-enroller.tokens"})
if code, out, errOut := runLine("enrol", "tokens"); code != 1 || out != "" || !strings.Contains(errOut, "nothing serves") {
t.Errorf("an error answer: exit %d, stdout %q, stderr %q", code, out, errOut)
}
placement := `{"keys":{"novox":{"placement":{"decided":{"address":"10.10.0.1","site":"hub"},"modules":[{},{}]}},` +
`"ace":{"placement":{"decided":{"address":"10.10.0.2"},"modules":[{}]}}}}`
console(t, map[string]string{"mesh-placement.placement": placement})
console(t, map[string]string{"mesh-module-assigner.placement": placement})
if code, out, _ := runLine("nodes"); code != 0 || !strings.HasPrefix(out, "ace ") ||
!strings.Contains(out, "10.10.0.1") || !strings.Contains(out, "2 modules") || !strings.HasSuffix(out, "answered by ace\n") {
t.Errorf("nodes: exit %d, %q", code, out)