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:
@@ -39,7 +39,7 @@ func TestRefusalsJudgeATree(t *testing.T) {
|
||||
{"a bucket in a test is its data", map[string]string{"modules/e/cmd/e/main_test.go": "package main\n// mesh-controller_lease\n"}, ""},
|
||||
{"a fixture copied from elsewhere", map[string]string{"modules/e/cmd/e/testdata/x.md": "mesh-controller_rollouts\n"}, ""},
|
||||
{"an owner and prose pass", map[string]string{"modules/f/module.json": `{"module":"f","config":{"owners":{"default":["mesh-controller","mesh-controller modules/route-proxy"]}},"why":"the mesh-controller module is gone"}`}, ""},
|
||||
{"the seat's successors pass", map[string]string{"modules/g/module.json": `{"module":"g","reads":["mesh-placement.placement"],"invokes":["seat:mesh-hand-acts.hand-act"]}`}, ""},
|
||||
{"the seat's successors pass", map[string]string{"modules/g/module.json": `{"module":"g","reads":["mesh-module-assigner.placement"],"invokes":["seat:mesh-hand-acts.hand-act"]}`}, ""},
|
||||
} {
|
||||
t.Run(row.name, func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Command noplacement refuses what the repository path of placement left behind (novox/hq ADR 0567 §10, §12).
|
||||
//
|
||||
// Placement, which module runs on which node, is bus state held by the seat mesh-placement; no file of the
|
||||
// Placement, which module runs on which node, is bus state held by the seat mesh-module-assigner; no file of the
|
||||
// catalogue says it, and no manifest reads it from the delivery indexer, which no longer keeps it. It refuses:
|
||||
//
|
||||
// 1. a `placement` directory (or file) at the root of the catalogue;
|
||||
@@ -34,7 +34,7 @@ var Prefixes = []string{"mesh-delivery-indexer.placement", "mesh-delivery-indexe
|
||||
func Findings(root string) ([]string, error) {
|
||||
var out []string
|
||||
if _, err := os.Lstat(filepath.Join(root, "placement")); err == nil {
|
||||
out = append(out, "placement: the repository holds no placement; it is bus state of the seat mesh-placement (novox/hq ADR 0567 §10)")
|
||||
out = append(out, "placement: the repository holds no placement; it is bus state of the seat mesh-module-assigner (novox/hq ADR 0567 §10)")
|
||||
}
|
||||
if _, err := os.Lstat(filepath.Join(root, "particulars")); err == nil {
|
||||
out = append(out, "particulars: the repository lists no node names; the checker hands them to the check (novox/hq ADR 0567 §16)")
|
||||
@@ -55,7 +55,7 @@ func Findings(root string) ([]string, error) {
|
||||
}
|
||||
module := filepath.Base(filepath.Dir(file))
|
||||
for _, s := range named(doc, nil) {
|
||||
out = append(out, fmt.Sprintf("%s: names %q, which the indexer no longer keeps; placement is the seat mesh-placement's (novox/hq ADR 0567 §12)", module, s))
|
||||
out = append(out, fmt.Sprintf("%s: names %q, which the indexer no longer keeps; placement is the seat mesh-module-assigner's (novox/hq ADR 0567 §12)", module, s))
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
|
||||
@@ -19,7 +19,7 @@ func TestFindingsRefuseWhatThePlacementPathLeft(t *testing.T) {
|
||||
{"a read of the indexer's placement", map[string]string{"modules/a/module.json": `{"module":"a","reads":["mesh-delivery-indexer.placement"]}`}, `a: names "mesh-delivery-indexer.placement"`},
|
||||
{"a read of its candidates", map[string]string{"modules/b/module.json": `{"module":"b","reads":["mesh-delivery-indexer.placement-candidates"]}`}, "placement-candidates"},
|
||||
{"its event", map[string]string{"modules/c/module.json": `{"module":"c","on":[{"event":"mesh-delivery-indexer.placement-changed"}]}`}, "placement-changed"},
|
||||
{"the seat's own state is allowed", map[string]string{"modules/d/module.json": `{"module":"d","reads":["mesh-placement.placement"]}`}, ""},
|
||||
{"the seat's own state is allowed", map[string]string{"modules/d/module.json": `{"module":"d","reads":["mesh-module-assigner.placement"]}`}, ""},
|
||||
} {
|
||||
t.Run(row.name, func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
|
||||
@@ -109,7 +109,7 @@ func TestCheckAddressesRefusesTheControllersAddresses(t *testing.T) {
|
||||
{"`mesh-controller.<verb>`", true},
|
||||
{"the mesh-controller module is gone.", false},
|
||||
{"located in mesh-controller#246", false},
|
||||
{"ask mesh-placement.placement", false},
|
||||
{"ask mesh-module-assigner.placement", false},
|
||||
} {
|
||||
got := checkAddresses([]text{{file: "m/a.go", line: 1, s: c.s}})
|
||||
if (len(got) == 1) != c.want {
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
// seats' contract and the modules' manifests say them.
|
||||
func meshForTheGuard() GuardData {
|
||||
defined := []seats.Seat{
|
||||
{Name: "mesh-placement", Scope: "mesh", Serves: []seats.Verb{
|
||||
{Name: "mesh-module-assigner", Scope: "mesh", Serves: []seats.Verb{
|
||||
{Name: "placement", Description: "Read placement: one node's key.", Replaces: []string{"hostnamectl", "uptime"}}}},
|
||||
{Name: "node-service-manager", Scope: "node", Serves: []seats.Verb{
|
||||
{Name: "status", Description: "One unit as the service manager sees it now: its states.", Replaces: []string{"systemctl status", "systemctl is-active"}},
|
||||
@@ -80,14 +80,14 @@ func TestSSHToAMeshMachineIsRefusedNamingTheTool(t *testing.T) {
|
||||
"ssh -o StrictHostKeyChecking=no 10.10.0.1 docker ps -a": "anchor/docker.docker_list",
|
||||
"ssh anchor docker logs --tail 100 nats": "anchor/docker.docker_logs",
|
||||
"ssh laptop docker logs x": "anchor/docker.docker_logs",
|
||||
"ssh a uptime": "mesh-placement.placement",
|
||||
"ssh a uptime": "mesh-module-assigner.placement",
|
||||
"cd /tmp && ssh anchor journalctl": "anchor/node-service-manager.journal",
|
||||
"sudo -u root ssh anchor journalctl": "anchor/node-service-manager.journal",
|
||||
"timeout 10 ssh anchor journalctl": "anchor/node-service-manager.journal",
|
||||
"env LANG=C ssh anchor journalctl": "anchor/node-service-manager.journal",
|
||||
"bash -c 'ssh anchor journalctl -u x'": "anchor/node-service-manager.journal",
|
||||
`echo "$(ssh anchor journalctl -n 5)" | tail`: "anchor/node-service-manager.journal",
|
||||
"for n in anchor laptop; do ssh $n uptime; done; ssh anchor uptime": "mesh-placement.placement",
|
||||
"for n in anchor laptop; do ssh $n uptime; done; ssh anchor uptime": "mesh-module-assigner.placement",
|
||||
"ssh -J anchor 203.0.113.9 journalctl": "anchor/node-service-manager.journal",
|
||||
"ssh via-anchor journalctl": "anchor/node-service-manager.journal",
|
||||
"ssh nas.lan journalctl": "anchor/node-service-manager.journal",
|
||||
|
||||
@@ -39,7 +39,7 @@ func (b aBus) Get(read, key string) ([]byte, bool, error) {
|
||||
// someSeats are two seats as the seats' contract says them: one held once for the mesh, one on every machine.
|
||||
func someSeats() []seats.Seat {
|
||||
return []seats.Seat{
|
||||
{Name: "mesh-placement", Scope: "mesh", Serves: []seats.Verb{
|
||||
{Name: "mesh-module-assigner", Scope: "mesh", Serves: []seats.Verb{
|
||||
{Name: "placement", Description: "Read placement: one node's key.", Replaces: []string{"hostnamectl", "uptime"}},
|
||||
{Name: "propose", Description: "Propose a change."}}},
|
||||
{Name: "node-service-manager", Scope: "node", Serves: []seats.Verb{
|
||||
@@ -49,7 +49,7 @@ func someSeats() []seats.Seat {
|
||||
}
|
||||
|
||||
// aMesh is two machines, anchor (with a public domain) and laptop, docker and zsh placed on anchor, docker's manifest
|
||||
// saying what its tool replaces, as the seat mesh-placement and the indexer keep them on the bus.
|
||||
// saying what its tool replaces, as the seat mesh-module-assigner and the indexer keep them on the bus.
|
||||
func aMesh(t *testing.T) aBus {
|
||||
t.Helper()
|
||||
b := aBus{PlacementRead: {}, ModulesRead: {}, ManifestsRead: {}}
|
||||
@@ -100,7 +100,7 @@ func TestTheMeshsToolsAreReadFromTheBus(t *testing.T) {
|
||||
for _, r := range m.Replacements {
|
||||
addresses = append(addresses, r.Address)
|
||||
}
|
||||
if got := strings.Join(addresses, ","); got != "mesh-placement.placement,node-service-manager.status,node-service-manager.journal,docker.docker_logs" {
|
||||
if got := strings.Join(addresses, ","); got != "mesh-module-assigner.placement,node-service-manager.status,node-service-manager.journal,docker.docker_logs" {
|
||||
t.Errorf("replacements: %s", got)
|
||||
}
|
||||
if len(m.Machines) != 2 || strings.Join(m.Machines[0].Domains, ",") != "anchor.internal,anchor.example" ||
|
||||
@@ -120,7 +120,7 @@ func TestTheInsteadOfTableIsGeneratedFromTheRecords(t *testing.T) {
|
||||
for _, want := range []string{
|
||||
"| `<node>/node-service-manager.` | `status` — `systemctl status`, `systemctl is-active` · `journal` — `journalctl` |",
|
||||
"| `<node>/docker.` | `docker_logs` — `docker logs` |",
|
||||
"| `mesh-placement.` | `placement` — `hostnamectl`, `uptime` |",
|
||||
"| `mesh-module-assigner.` | `placement` — `hostnamectl`, `uptime` |",
|
||||
"Never `ssh` to a mesh machine",
|
||||
} {
|
||||
if !strings.Contains(table, want) {
|
||||
@@ -128,14 +128,14 @@ func TestTheInsteadOfTableIsGeneratedFromTheRecords(t *testing.T) {
|
||||
}
|
||||
}
|
||||
if strings.Index(table, "node-service-manager.") > strings.Index(table, "docker.") ||
|
||||
strings.Index(table, "docker.") > strings.Index(table, "`mesh-placement.`") {
|
||||
strings.Index(table, "docker.") > strings.Index(table, "`mesh-module-assigner.`") {
|
||||
t.Errorf("not the machines' seats, then modules, then the mesh's:\n%s", table)
|
||||
}
|
||||
// What the guard here named most comes first: its row, and in its row its verb.
|
||||
m.Refused = map[string]int{"node-service-manager.journal": 3, "mesh-placement.placement": 1}
|
||||
m.Refused = map[string]int{"node-service-manager.journal": 3, "mesh-module-assigner.placement": 1}
|
||||
ranked := InsteadOf(&m)
|
||||
if !strings.Contains(ranked, "| `<node>/node-service-manager.` | `journal` — `journalctl` · `status`") ||
|
||||
strings.Index(ranked, "`mesh-placement.`") > strings.Index(ranked, "`<node>/docker.`") {
|
||||
strings.Index(ranked, "`mesh-module-assigner.`") > strings.Index(ranked, "`<node>/docker.`") {
|
||||
t.Errorf("not ranked by the guard's record:\n%s", ranked)
|
||||
}
|
||||
m.Refused = nil
|
||||
@@ -182,7 +182,7 @@ func TestTheMeshsToolsAreKeptOnlyWhenTheyChange(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Where a module runs is read from the seat mesh-placement's placement (issue 1126 B13), never the controller's answer.
|
||||
// Where a module runs is read from the seat mesh-module-assigner's placement (issue 1126 B13), never the controller's answer.
|
||||
func TestTheMachinesRunningAModuleAreReadFromPlacement(t *testing.T) {
|
||||
b := aMesh(t)
|
||||
if got, err := NodesRunning(b, "zsh"); err != nil || strings.Join(got, ",") != "anchor" {
|
||||
|
||||
@@ -9,5 +9,5 @@ func instructionsText(node, role string) string {
|
||||
node +
|
||||
"`\n- **Role:** " +
|
||||
role +
|
||||
"\n- The other nodes, their roles and what runs where: ask the placement seat (`mesh-placement.placement`),\n or `mesh_overview` and `mesh_machine`. Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own seats\n include `mesh-placement`, `mesh-delivery`, `mesh-config` and `mesh-conditions`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the seats' verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `<node>/node-engine.composition` for the module says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
|
||||
"\n- The other nodes, their roles and what runs where: ask the placement seat (`mesh-module-assigner.placement`),\n or `mesh_overview` and `mesh_machine`. Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own seats\n include `mesh-module-assigner`, `mesh-delivery`, `mesh-config` and `mesh-conditions`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the seats' verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `<node>/node-engine.composition` for the module says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ package main
|
||||
// Read from the bus at start and every few minutes (issue 1126 B13; the controller that answered this before is
|
||||
// not placed, novox/hq ADR 0590): the seats' verbs from the seats' contract (mesh-contracts), the modules'
|
||||
// own tools from the manifests the indexer keeps (`mesh-delivery-indexer.modules` and `.manifests`), and the
|
||||
// machines and where each module runs from the seat mesh-placement's `placement`. Kept in the module's state so
|
||||
// machines and where each module runs from the seat mesh-module-assigner's `placement`. Kept in the module's state so
|
||||
// a render never waits for the bus, and rendered whenever it changes. Nothing here is written by hand: a verb
|
||||
// that gains a `replaces` is in the next render's table and the guard's next refusal.
|
||||
|
||||
@@ -104,7 +104,7 @@ type ModuleTools struct {
|
||||
Replaces map[string][]string
|
||||
}
|
||||
|
||||
// placements is every node's placement, from the seat mesh-placement's state.
|
||||
// placements is every node's placement, from the seat mesh-module-assigner's state.
|
||||
func placements(st States) (map[string]delivery.Placement, error) {
|
||||
keys, err := st.Keys(PlacementRead)
|
||||
if err != nil {
|
||||
|
||||
@@ -32,11 +32,11 @@
|
||||
"withKey": {
|
||||
"managed-mcp.json": "{\n \"mcpServers\": {\n \"anton\": {\n \"type\": \"stdio\",\n \"command\": \"node\",\n \"args\": [\n \"/a/b.js\"\n ],\n \"env\": {}\n },\n \"docs\": {\n \"type\": \"stdio\",\n \"command\": \"docs-mcp\",\n \"args\": [\n \"--x\"\n ]\n },\n \"mesh\": {\n \"type\": \"http\",\n \"url\": \"http://127.0.0.1:4270/mcp\"\n },\n \"search\": {\n \"type\": \"http\",\n \"url\": \"https://s.example/mcp\"\n }\n }\n}\n",
|
||||
"managed-settings.json": "{\n \"attribution\": {\n \"commit\": \"\",\n \"pr\": \"\"\n },\n \"allowAllClaudeAiMcps\": true,\n \"apiKeyHelper\": \"/state/api-key-helper\"\n}\n",
|
||||
"CLAUDE.md": "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalog, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `workstation`\n- **Role:** the laptop\n- The other nodes, their roles and what runs where: ask the placement seat (`mesh-placement.placement`),\n or `mesh_overview` and `mesh_machine`. Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own seats\n include `mesh-placement`, `mesh-delivery`, `mesh-config` and `mesh-conditions`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the seats' verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `<node>/node-engine.composition` for the module says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
|
||||
"CLAUDE.md": "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalog, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `workstation`\n- **Role:** the laptop\n- The other nodes, their roles and what runs where: ask the placement seat (`mesh-module-assigner.placement`),\n or `mesh_overview` and `mesh_machine`. Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own seats\n include `mesh-module-assigner`, `mesh-delivery`, `mesh-config` and `mesh-conditions`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the seats' verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `<node>/node-engine.composition` for the module says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
|
||||
},
|
||||
"plain": {
|
||||
"managed-mcp.json": "{\n \"mcpServers\": {\n \"mesh\": {\n \"type\": \"http\",\n \"url\": \"http://127.0.0.1:4270/mcp\"\n }\n }\n}\n",
|
||||
"managed-settings.json": "{\n \"attribution\": {\n \"commit\": \"\",\n \"pr\": \"\"\n },\n \"allowAllClaudeAiMcps\": true\n}\n",
|
||||
"CLAUDE.md": "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalog, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `workstation`\n- **Role:** not stated — set it in this module's settings for the node\n- The other nodes, their roles and what runs where: ask the placement seat (`mesh-placement.placement`),\n or `mesh_overview` and `mesh_machine`. Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own seats\n include `mesh-placement`, `mesh-delivery`, `mesh-config` and `mesh-conditions`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the seats' verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `<node>/node-engine.composition` for the module says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
|
||||
"CLAUDE.md": "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalog, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `workstation`\n- **Role:** not stated — set it in this module's settings for the node\n- The other nodes, their roles and what runs where: ask the placement seat (`mesh-module-assigner.placement`),\n or `mesh_overview` and `mesh_machine`. Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own seats\n include `mesh-module-assigner`, `mesh-delivery`, `mesh-config` and `mesh-conditions`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the seats' verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `<node>/node-engine.composition` for the module says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ puts the call on the bus, as every agent session on the machine does. The consol
|
||||
`/etc/mesh-cli/console`, or `MESH_CONSOLE` when set, or node-tools' declared port when the file is not there yet.
|
||||
|
||||
- **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
|
||||
(`mesh-module-assigner.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
|
||||
|
||||
@@ -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,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.
|
||||
`
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -94,7 +94,7 @@ func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) {
|
||||
}
|
||||
}
|
||||
// A seat's finding is about the module the placement says holds the seat on that node (the lead, 2026-10-29): held
|
||||
// while the assigner is delivered on ace, though the seat is named mesh-placement.
|
||||
// while the assigner is delivered on ace, though the seat is named mesh-module-assigner.
|
||||
r := newRig()
|
||||
// Research 072 row 37: the placement in its keys per module and node; a stale legacy key beside them is not read.
|
||||
stale, err := delivery.EncodePlacement(delivery.Placement{Node: "ace"})
|
||||
@@ -103,7 +103,7 @@ func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) {
|
||||
}
|
||||
r.k.Placement("placement.ace", stale, false)
|
||||
placed, err := delivery.PlacementValues(delivery.Placement{Node: "ace", Modules: []delivery.PlacedModule{{Module: "mesh-delivery-assigner"}},
|
||||
Holds: []delivery.Holding{{Claim: "mesh-placement", Scope: "mesh", Module: "mesh-delivery-assigner"}}})
|
||||
Holds: []delivery.Holding{{Claim: "mesh-module-assigner", Scope: "mesh", Module: "mesh-delivery-assigner"}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -111,7 +111,7 @@ func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) {
|
||||
r.k.Placement("ace.node", placed["ace.node"], false)
|
||||
_ = r.k.Want("want.ace.mesh-delivery-assigner", want(ctrlNext, ctrlNextComp), false)
|
||||
_ = r.k.Have("ace.mesh-delivery-assigner", have(ctrlRunning, ctrlComposed, delivery.PhaseRunning, delivery.HealthOK), false)
|
||||
seat, seatRaw := finding(t, selfCheck, health.ScopeSeat, "mesh-placement.ace", "holder-silent", "D3", health.Warning, r.now)
|
||||
seat, seatRaw := finding(t, selfCheck, health.ScopeSeat, "mesh-module-assigner.ace", "holder-silent", "D3", health.Warning, r.now)
|
||||
_ = r.k.Finding(selfCheck, seat, seatRaw, false)
|
||||
if got := r.changes(); got != "" || len(r.k.Held()) != 1 || r.k.Held()[0].Module != "mesh-delivery-assigner" {
|
||||
t.Errorf("a seat's finding while its holder is delivered: said %q, held %+v", got, r.k.Held())
|
||||
|
||||
@@ -54,7 +54,7 @@ var Observers = []string{health.WatchmanModule, health.SelfCheckModule, health.H
|
||||
const (
|
||||
AttemptsRead = health.HealersModule + "." + health.AttemptsState
|
||||
WantsRead = delivery.AssignerModule + ".wants"
|
||||
// PlacementRead is the mesh-placement seat's placement, read for which module holds a seat on a node (hold.go; the
|
||||
// PlacementRead is the mesh-module-assigner seat's placement, read for which module holds a seat on a node (hold.go; the
|
||||
// lead, 2026-10-29, for ADR 0564 clause 2's inputs).
|
||||
PlacementRead = delivery.PlacementRead
|
||||
HavesRead = "node-engine.have"
|
||||
|
||||
@@ -21,7 +21,7 @@ account alone writes its buckets.
|
||||
|
||||
## How a value resolves
|
||||
|
||||
For every module the seat mesh-placement places on a node (`mesh-delivery-assigner.placement`), from the module's
|
||||
For every module the seat mesh-module-assigner places on a node (`mesh-delivery-assigner.placement`), from the module's
|
||||
own manifests as the delivery built them, never from the controller (issue 1953): the trunk's, which the indexer's
|
||||
`modules.<module>` names in its manifests by digest (`mesh-delivery-indexer.modules`, `.manifests`), with the build the
|
||||
node's want names (`mesh-delivery-assigner.wants`, the builder's `mesh-delivery-builder.builds` naming its manifest)
|
||||
|
||||
@@ -146,7 +146,7 @@ func (m *memStore) mirror(t *testing.T, read, key, raw string) {
|
||||
m.states[read][key] = []byte(raw)
|
||||
}
|
||||
|
||||
// assign places a module on a node, in the seat mesh-placement's key for the node.
|
||||
// assign places a module on a node, in the seat mesh-module-assigner's key for the node.
|
||||
func (m *memStore) assign(t *testing.T, node, module string) {
|
||||
t.Helper()
|
||||
m.place(t, node, module, true)
|
||||
|
||||
@@ -148,7 +148,7 @@ type Published struct {
|
||||
Commit string `json:"commit"`
|
||||
}
|
||||
|
||||
// Assignment is a module placed on a node, as the seat mesh-placement's key `placement.<node>` names it.
|
||||
// Assignment is a module placed on a node, as the seat mesh-module-assigner's key `placement.<node>` names it.
|
||||
type Assignment struct {
|
||||
Node string `json:"node"`
|
||||
Module string `json:"module"`
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// node), mesh-config_resolved (each module's value on each node, with its source), mesh-config_predefined (the
|
||||
// published list of predefined keys) and mesh-config_proposals (each value proposed for the operator's Approve,
|
||||
// asked under this module's own name, design 57 K4).
|
||||
// It resolves from where the seat mesh-placement places each module, and each module's declarations as its own
|
||||
// It resolves from where the seat mesh-module-assigner places each module, and each module's declarations as its own
|
||||
// manifests declare them, the trunk's and its wanted build's (the indexer's and the builder's state, issue 1953); the
|
||||
// list of predefined keys comes from the indexer's (ADR 0565 §2); the controller's records are no longer read (issue 774). stdout is the MCP channel; what this module says, it says on stderr.
|
||||
package main
|
||||
|
||||
@@ -22,7 +22,7 @@ const (
|
||||
PredefinedState = "predefined"
|
||||
ProposalsState = "proposals"
|
||||
|
||||
// PlacementRead is the seat mesh-placement's state, held by the assigner (novox/hq ADR 0567 §1, §8): each
|
||||
// PlacementRead is the seat mesh-module-assigner's state, held by the assigner (novox/hq ADR 0567 §1, §8): each
|
||||
// node's key `placement.<node>` with the modules placed there. It is the one source of which module runs where
|
||||
// (issue 1953): the controller's assignments are no longer read.
|
||||
PlacementRead = delivery.PlacementRead
|
||||
|
||||
@@ -13,14 +13,14 @@ import (
|
||||
// Reading the sources, writing the resolved values, and publishing the list of predefined keys.
|
||||
|
||||
// sources reads everything a resolution reads, from the bus as it is now, from the source the switch names
|
||||
// (takeover.go): which modules are placed where from the seat mesh-placement, and each placed module's declarations
|
||||
// (takeover.go): which modules are placed where from the seat mesh-module-assigner, and each placed module's declarations
|
||||
// from its own manifests as built (issue 1953, ADR 0565 §3); the controller's records are no longer read.
|
||||
func (c *Config) sources() (Sources, error) {
|
||||
s := Sources{Declared: map[string]ModuleDeclarations{}, OnNode: map[string]ModuleDeclarations{},
|
||||
Predefined: map[string]Published{}, Values: map[string]Value{}, Renames: map[string]Rename{}}
|
||||
var problems []string
|
||||
if err := c.readPlacement(&s); err != nil {
|
||||
return s, fmt.Errorf("the seat mesh-placement's placement cannot be read: %w", err)
|
||||
return s, fmt.Errorf("the seat mesh-module-assigner's placement cannot be read: %w", err)
|
||||
}
|
||||
if err := c.readDeclarations(&s); err != nil {
|
||||
return s, fmt.Errorf("the modules' declarations cannot be read: %w", err)
|
||||
@@ -63,7 +63,7 @@ func (c *Config) sources() (Sources, error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// readPlacement is every module the seat mesh-placement places on a node (ADR 0567 §1, §9), from the state's keys in
|
||||
// readPlacement is every module the seat mesh-module-assigner places on a node (ADR 0567 §1, §9), from the state's keys in
|
||||
// either form: a node key and its module keys, or the form before, `placement.<node>` (delivery.PlacementsOf;
|
||||
// research 072 row 37). By node in name order, each node's modules in the order its placement names them.
|
||||
func (c *Config) readPlacement(s *Sources) error {
|
||||
|
||||
@@ -5,12 +5,12 @@ P5). Go, one binary (`cmd/mesh-provisioner-client`), run as a service of its own
|
||||
`mesh-provisioner-client`, outside the tool runner, on a bus account of its own, `provisioner.<node>`, delivered as
|
||||
its own secret `provisioner-broker` (mesh-sdk `buscompose/provisioner.go`). The bus grants that account its own
|
||||
machine's keys and, to read alone, the seat mesh-provisioner's whole `plans`, where a provider's consumers on any
|
||||
machine are bound to it (ADR 0373), and the seat mesh-placement's whole `placement`; nothing else.
|
||||
machine are bound to it (ADR 0373), and the seat mesh-module-assigner's whole `placement`; nothing else.
|
||||
|
||||
## What it does
|
||||
|
||||
For each module placed on its node — a plan in the seat mesh-provisioner's `plans` (`mesh-provisioner_plans`,
|
||||
`<node>.<module>`, novox/hq ADR 0570 clause 2); a module the seat mesh-placement still places here stays while the
|
||||
`<node>.<module>`, novox/hq ADR 0570 clause 2); a module the seat mesh-module-assigner still places here stays while the
|
||||
seat holds no plan of it, filled from its last plan, and leaves only when neither names it (issue 1126): the templates archive
|
||||
by reference and digest, the files it fills (path, template, `owner-at-runtime`, how its program takes a change)
|
||||
and the placement values its templates name — the client:
|
||||
|
||||
@@ -37,7 +37,7 @@ import (
|
||||
// The owners it fills from are placement (in the module's plan), config (mesh-config_resolved) and the vault, asked for
|
||||
// each secret the module's plan names (secrets.go), and for each consumer's credential a provider here lists
|
||||
// (consumers.go); and each provider's entries for a consumer here. Every key it writes is its own machine's; it reads
|
||||
// the plans whole, for who binds a provider here (ADR 0373), and the seat mesh-placement's placement, for which
|
||||
// the plans whole, for who binds a provider here (ADR 0373), and the seat mesh-module-assigner's placement, for which
|
||||
// modules stay here: its bus account is granted no other.
|
||||
|
||||
// The buckets the client reads and writes, and the subject it reports on.
|
||||
@@ -45,7 +45,7 @@ const (
|
||||
// PlansBucket is where each module's provisioning plan on each node is read: the seat mesh-provisioner's
|
||||
// `plans` (novox/hq ADR 0570 clause 2), read as an assignment's value carried it (asAssignment).
|
||||
PlansBucket = provisions.PlansBucket
|
||||
// PlacementBucket is the seat mesh-placement's placement (novox/hq ADR 0567): a module it names on this node stays
|
||||
// PlacementBucket is the seat mesh-module-assigner's placement (novox/hq ADR 0567): a module it names on this node stays
|
||||
// here while the seat mesh-provisioner holds no plan of it (nats on novox runs without a want), filled from its
|
||||
// last plan, and leaves only when neither names it (issue 1126 B12; the controller's assignments mirror, read for
|
||||
// this before, is frozen since the controller's end, ADR 0590).
|
||||
@@ -155,7 +155,7 @@ type state struct {
|
||||
plans map[string]Plan
|
||||
// seatPlans is the seat's plan by key, as an assignment's value reads.
|
||||
seatPlans map[string][]byte
|
||||
// placement is the seat mesh-placement's keys as read, and placed the modules it names on this node.
|
||||
// placement is the seat mesh-module-assigner's keys as read, and placed the modules it names on this node.
|
||||
placement *delivery.PlacementKeys
|
||||
placed map[string]bool
|
||||
refused map[string]string // a module whose plan cannot be acted on, and why
|
||||
@@ -1161,7 +1161,7 @@ func (c *Client) forgetItems(ctx context.Context, kvs map[string]jetstream.KeyVa
|
||||
return removed
|
||||
}
|
||||
|
||||
// takePlacement folds one key of the seat mesh-placement's placement in, and answers the modules of this node it moved:
|
||||
// takePlacement folds one key of the seat mesh-module-assigner's placement in, and answers the modules of this node it moved:
|
||||
// one it names now stays here without a plan; one it no longer names, with no plan of the seat, leaves.
|
||||
func (c *Client) takePlacement(s *state, key string, value []byte, revision uint64, gone bool) []string {
|
||||
node, err := s.placement.Put(key, value, revision, gone)
|
||||
|
||||
@@ -97,7 +97,7 @@ func startBus(t *testing.T) *aBus {
|
||||
`"bucket":"mesh-provisioner_collect","key":"<node>.<collection>.<contributor>"}`)
|
||||
}
|
||||
for _, kind := range placementKinds {
|
||||
b.put(t, RegistryBucket, kind, `{"kind":"`+kind+`","domain":"mesh-placement","from":"bucket",`+
|
||||
b.put(t, RegistryBucket, kind, `{"kind":"`+kind+`","domain":"mesh-module-assigner","from":"bucket",`+
|
||||
`"bucket":"mesh-provisioner_plans","key":"<node>.<module>"}`)
|
||||
}
|
||||
return b
|
||||
@@ -122,7 +122,7 @@ func (b *aBus) put(t *testing.T, bucket, key, value string) {
|
||||
}
|
||||
}
|
||||
|
||||
// place writes this node's placement as the seat mesh-placement keeps it, naming exactly these modules.
|
||||
// place writes this node's placement as the seat mesh-module-assigner keeps it, naming exactly these modules.
|
||||
func (b *aBus) place(t *testing.T, modules ...string) {
|
||||
t.Helper()
|
||||
p := delivery.Placement{V: 1, Node: node}
|
||||
@@ -659,7 +659,7 @@ func TestNatsAModuleTakenOffTheNodeLosesItsReport(t *testing.T) {
|
||||
}
|
||||
|
||||
// Issue 1126 B12 (ADR 0590): the controller's assignments are frozen and go, so what places a module here is the seat
|
||||
// mesh-placement's placement alone. A module stays while placement names it, even with no plan of the seat
|
||||
// mesh-module-assigner's placement alone. A module stays while placement names it, even with no plan of the seat
|
||||
// mesh-provisioner (nats on novox runs without a want): its last plan keeps being filled, and it leaves only when
|
||||
// neither its plan nor its placement is there.
|
||||
func TestNatsAModuleStaysWhilePlacementNamesItAndLeavesOnlyWhenNeitherPlacesIt(t *testing.T) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package main
|
||||
|
||||
// The probes of the mesh's machines (novox/hq ADR 0564 §6, issue 1952): each reads its facts where they live on the
|
||||
// bus — placement (the seat mesh-placement's state), who holds which seat (its seat-holders), whether a node is heard
|
||||
// bus — placement (the seat mesh-module-assigner's state), who holds which seat (its seat-holders), whether a node is heard
|
||||
// from (the watchman's presence) — and asks each node's seat holder what only that node knows. Its assertion, kind,
|
||||
// key and words are the controller's (mesh-controller probes.go), so the condition the keeper raises from it is the one
|
||||
// the controller raised.
|
||||
|
||||
@@ -2,14 +2,14 @@ package main
|
||||
|
||||
// The vault's part of enrolment (novox/hq ADR 0587 §3, §5, §8; ADR 0561 §4, §5; issue 774).
|
||||
//
|
||||
// A machine joins with a token a person types. The seat mesh-enrolment decides who may join; the vault holds the secrets.
|
||||
// A machine joins with a token a person types. The seat mesh-node-enroller decides who may join; the vault holds the secrets.
|
||||
// Its verb `enrolment-token` makes the token's one-time secret, keeps it with its operator copy as `enrol/<node>/token`,
|
||||
// writes its bcrypt hash under `bus-accounts` as the user `enrol.<node>`, and answers at issue the public fields alone
|
||||
// (never the token). Asked to show it, it answers the whole token sealed to the operator key, which the operator opens at
|
||||
// their own terminal (`nox enrol open`); asked to drop it, it takes the token and its hash away. Its verb `node-account`
|
||||
// makes the account `node.<node>` of a joining machine at the redeem: once for a node and a set of keys, sealed to the key
|
||||
// the machine presented, with the node's key in `node-logins` naming that login in use and nothing rotating. Only the
|
||||
// seat mesh-enrolment may call either: the runtime proves who asks.
|
||||
// seat mesh-node-enroller may call either: the runtime proves who asks.
|
||||
|
||||
import (
|
||||
"crypto/ecdh"
|
||||
@@ -32,7 +32,7 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// The names enrolment gives: the module that holds the seat mesh-enrolment, the prefix of a token's bus user, and the
|
||||
// The names enrolment gives: the module that holds the seat mesh-node-enroller, the prefix of a token's bus user, and the
|
||||
// vault's state of each node's logins.
|
||||
const (
|
||||
EnrolmentModule = delivery.EnrolmentModule
|
||||
@@ -115,7 +115,7 @@ func (v *Vault) loginKeyRefusal(caller, publicKey string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// fromEnrolment refuses every caller but the seat mesh-enrolment, as the runtime proved it.
|
||||
// fromEnrolment refuses every caller but the seat mesh-node-enroller, as the runtime proved it.
|
||||
func (v *Vault) fromEnrolment(caller, verb string) error {
|
||||
callers := v.EnrolCallers
|
||||
if callers == nil {
|
||||
|
||||
@@ -199,7 +199,7 @@ func TestTheVaultsEnrolmentAnswersAreMeshSDKsWireShapes(t *testing.T) {
|
||||
filled []string // the json fields the holder reads, which must not be empty
|
||||
refused string // a part of the refusal, when refused
|
||||
}{
|
||||
// Only the seat mesh-enrolment is answered: not the controller, a node, a person or a caller not proven.
|
||||
// Only the seat mesh-node-enroller is answered: not the controller, a node, a person or a caller not proven.
|
||||
{"a node asks a token", "enrolment-token", "node.g14", map[string]any{"node": "new1", "tunnel": tunnel}, nil, nil, "mesh-enrolment"},
|
||||
{"the controller asks a token", "enrolment-token", "controller", map[string]any{"node": "new1", "tunnel": tunnel}, nil, nil, "mesh-enrolment"},
|
||||
{"a person asks an account", "node-account", "person.jochen", map[string]any{"node": "new1"}, nil, nil, "mesh-enrolment"},
|
||||
|
||||
@@ -55,7 +55,7 @@ func enrolmentTools(env *Env) []toolDef {
|
||||
node := str("the node's name")
|
||||
return []toolDef{
|
||||
{Name: Seat + ".enrolment-token",
|
||||
Description: "The enrolment token of a joining machine, for the seat mesh-enrolment alone (novox/hq ADR 0587 §3, §5, " +
|
||||
Description: "The enrolment token of a joining machine, for the seat mesh-node-enroller alone (novox/hq ADR 0587 §3, §5, " +
|
||||
"§8). Issue (the default): takes the machine's first tunnel, makes the one-time secret, keeps both with the operator " +
|
||||
"copy, writes the secret's hash as the bus user enrol.<node>, and answers the bus's address, its certificate's " +
|
||||
"fingerprint and the vault's signing key, never the token. sealed: the whole token sealed to the operator key, " +
|
||||
@@ -78,7 +78,7 @@ func enrolmentTools(env *Env) []toolDef {
|
||||
Tunnel: tunnel, Sealed: isTrue(a["sealed"]), Drop: isTrue(a["drop"])})
|
||||
}},
|
||||
{Name: Seat + ".node-account",
|
||||
Description: "A joining node's account node.<node>, for the seat mesh-enrolment alone at the redeem (novox/hq ADR 0587 " +
|
||||
Description: "A joining node's account node.<node>, for the seat mesh-node-enroller alone at the redeem (novox/hq ADR 0587 " +
|
||||
"§3): made once for a node and the keys it presented, answered sealed to the sealing key presented, with the " +
|
||||
"node's key in node-logins naming the login in use. Asked again for the same keys it answers the same account. " +
|
||||
"retire: the account, its value and its key taken away at a revoke.",
|
||||
|
||||
@@ -238,7 +238,7 @@ func watchCredentialAsks(env *Env, kick chan<- struct{}) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// placement reads the seat mesh-placement's placement through its owner's tool and events (placementview.go), never the
|
||||
// placement reads the seat mesh-module-assigner's placement through its owner's tool and events (placementview.go), never the
|
||||
// owner's bucket.
|
||||
var placement = viewPlacement(func() *views.Follower { return placementView })
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package main
|
||||
|
||||
// Where modules stand, from the placement (novox/hq ADR 0549 §3, §5; ADR 0561; issue 774), the seat mesh-placement's
|
||||
// Where modules stand, from the placement (novox/hq ADR 0549 §3, §5; ADR 0561; issue 774), the seat mesh-module-assigner's
|
||||
// state held by the assigner (ADR 0567). The controller's assignments mirror goes with its lease, so the vault reads which
|
||||
// module stands on which node from the seat's placement first, and the assignments beside it while both stand: a module either names stands. The provisioner's plan,
|
||||
// which only an assignment's value carries, is read from the assignments still: its home after the lease is a subtask of
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// PlacementRead is the seat mesh-placement's placement, as the vault's manifest reads it (mesh-sdk's contract).
|
||||
// PlacementRead is the seat mesh-module-assigner's placement, as the vault's manifest reads it (mesh-sdk's contract).
|
||||
const PlacementRead = delivery.PlacementRead
|
||||
|
||||
// PlacementReader reads the seat's placement: every key, and one key's value (nil when there is none).
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// placementsOf is the seat mesh-placement's placement of each node, as the vault reads it.
|
||||
// placementsOf is the seat mesh-module-assigner's placement of each node, as the vault reads it.
|
||||
func placementsOf(t *testing.T, nodes map[string][]string) PlacementReader {
|
||||
t.Helper()
|
||||
values := map[string][]byte{}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package main
|
||||
|
||||
// The vault's reading of the seat mesh-placement's placement, through its owner (novox/hq ADR 0615 §1, §4 to §6, ADR
|
||||
// The vault's reading of the seat mesh-module-assigner's placement, through its owner (novox/hq ADR 0615 §1, §4 to §6, ADR
|
||||
// 0596): the assigner's tool `assigner_read_placement` asked for the whole state at start, the owner's `views.placement`
|
||||
// events after it, and the tool once more only on a gap. Started while the assigner is down it keeps asking and says so;
|
||||
// it never reads the assigner's bucket instead. (The vault's other reads, the node-engines' haves, the grants part's
|
||||
|
||||
@@ -277,7 +277,7 @@ func (v *Vault) Sign(caller string, a SignArgs) (meshvault.SignAnswer, error) {
|
||||
return meshvault.SignAnswer{}, fmt.Errorf("%q is not a kind the vault signs (%s)", a.Kind, strings.Join(delivery.SignKinds, ", "))
|
||||
}
|
||||
|
||||
// redeeming says node has an enrolment token held: a machine joining, which the seat mesh-enrolment redeems.
|
||||
// redeeming says node has an enrolment token held: a machine joining, which the seat mesh-node-enroller redeems.
|
||||
func (v *Vault) redeeming(node string) error {
|
||||
if err := enrolNode(node); err != nil {
|
||||
return err
|
||||
@@ -290,7 +290,7 @@ func (v *Vault) redeeming(node string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// signRedeemingCertificate signs the certificate of a machine that is joining, for the seat mesh-enrolment at the redeem
|
||||
// signRedeemingCertificate signs the certificate of a machine that is joining, for the seat mesh-node-enroller at the redeem
|
||||
// (ADR 0587 §6). The machine has written no record yet, so the serving key is the one it presented at the redeem, which
|
||||
// counts only when the four keys presented have the digest node-account recorded for its account; the name is its
|
||||
// placement's, which mesh-enrolment writes before it asks.
|
||||
@@ -410,7 +410,7 @@ func (v *Vault) internalName(node string) (string, error) {
|
||||
return "", fmt.Errorf(notYetPrefix+"the placement of %s could not be read: %w", node, err)
|
||||
}
|
||||
if !found {
|
||||
return "", fmt.Errorf("%s has no placement of the seat mesh-placement, so it has no name inside the mesh", node)
|
||||
return "", fmt.Errorf("%s has no placement of the seat mesh-module-assigner, so it has no name inside the mesh", node)
|
||||
}
|
||||
if decided.Address == "" {
|
||||
return "", fmt.Errorf("%s is not on the private network (its placement names no address), so it has no name inside the mesh to be certified for", node)
|
||||
|
||||
@@ -78,7 +78,7 @@ type Vault struct {
|
||||
// none.
|
||||
Signers SignersStore
|
||||
// EnrolCallers are the callers of the enrolment verbs `enrolment-token` and `node-account` and of `sign` for an
|
||||
// enrolment: the seat mesh-enrolment (enrolment.go). Nil: that module alone.
|
||||
// enrolment: the seat mesh-node-enroller (enrolment.go). Nil: that module alone.
|
||||
EnrolCallers stdio.Callers
|
||||
// RetireCallers are the callers of the seat's verb `retire`: the vault's trusted config value `retire-callers`, the
|
||||
// holder of the seat mesh-config alone (ownretire.go). Nil: nobody.
|
||||
|
||||
Reference in New Issue
Block a user