Let claude-code read its tools and machines from the bus, not the controller

The controller is not placed (ADR 0590), so its tools, modules, nodes and
node verbs answer nothing. claude-code now reads the seats' verbs from the
seats' contract, the placed modules' replaces from the indexer's manifests,
and the machines and where modules run from mesh-placement's placement; the
rendered instructions name the seats that own each job (issue 1126 B13).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-02 12:29:56 +01:00
parent b44e600e87
commit f723d4372f
8 changed files with 311 additions and 226 deletions
+4 -2
View File
@@ -120,8 +120,10 @@ nothing else of the home. A project is one entry of the projects directory and a
The agent kept reaching for `ssh <machine> journalctl` while the service manager's `journal` verb existed. So:
- **The "instead of" table.** Every seat verb and module tool may say which shell commands it replaces
(`replaces`, in the seat's definition or the module's manifest). The module asks the controller — `tools`,
`modules`, `nodes`, `node` — at start, every ten minutes and on `claude_code_render`, keeps the answer in its
(`replaces`, in the seat's definition or the module's manifest). The module reads the bus — the seats' verbs from the
seats' contract, the placed modules' manifests from `mesh-delivery-indexer.modules` and `.manifests`, the
machines and where each module runs from `mesh-delivery-assigner.placement` (issue 1126: the controller that
answered this is not placed) — at start, every ten minutes and on `claude_code_render`, keeps it in its
state (`mesh-tools.json`), and renders it into `CLAUDE.md` as one row per seat or module: `<node>/<seat>.` and
each verb with the commands it replaces. Generated, never written by hand: a verb that gains `replaces` is in
the next render. Ordered by what the guard here refused most, then the machines' seats, the modules, the
@@ -8,22 +8,26 @@ import (
"strconv"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// meshForTheGuard is a mesh of two machines and the verbs that replace what an agent runs over ssh, as the
// controller's records say them.
// seats' contract and the modules' manifests say them.
func meshForTheGuard() GuardData {
tools := json.RawMessage(`{"seats":[
{"seat":"mesh-controller","scope":"mesh","tools":[{"name":"node","description":"What one machine reported it can do.","replaces":["hostnamectl","uptime"]}]},
{"seat":"node-service-manager","scope":"node","tools":[
{"name":"status","description":"One unit as the service manager sees it now: its states.","replaces":["systemctl status","systemctl is-active"]},
{"name":"restart","description":"Restart one unit.","replaces":["systemctl restart"]},
{"name":"journal","description":"The last lines of one unit's journal.","replaces":["journalctl"]}]},
{"seat":"node-hostname","scope":"node","tools":[
{"name":"add","description":"Add one address and its names to the operator's lines of /etc/hosts.","replaces":["edit /etc/hosts","HOSTALIASES"]}]}]}`)
modules := json.RawMessage(`[{"module":"docker","on":["anchor"],"replaces":{"docker_logs":["docker logs"],"docker_list":["docker ps"]}},
{"module":"systemd","on":["anchor","laptop"]}]`)
return GuardData{Node: "laptop", Replacements: ReplacementsOf(tools, modules), Machines: []Machine{
seats := []catalogue.Seat{
{Name: "mesh-placement", Scope: catalogue.ScopeMesh, Serves: []catalogue.Verb{
{Name: "placement", Description: "Read placement: one node's key.", Replaces: []string{"hostnamectl", "uptime"}}}},
{Name: "node-service-manager", Scope: catalogue.ScopeNode, Serves: []catalogue.Verb{
{Name: "status", Description: "One unit as the service manager sees it now: its states.", Replaces: []string{"systemctl status", "systemctl is-active"}},
{Name: "restart", Description: "Restart one unit.", Replaces: []string{"systemctl restart"}},
{Name: "journal", Description: "The last lines of one unit's journal.", Replaces: []string{"journalctl"}}}},
{Name: "node-hostname", Scope: catalogue.ScopeNode, Serves: []catalogue.Verb{
{Name: "add", Description: "Add one address and its names to the operator's lines of /etc/hosts.", Replaces: []string{"edit /etc/hosts", "HOSTALIASES"}}}},
}
modules := []ModuleTools{{Module: "docker", On: []string{"anchor"}, Replaces: map[string][]string{"docker_logs": {"docker logs"}, "docker_list": {"docker ps"}}},
{Module: "systemd", On: []string{"anchor", "laptop"}}}
return GuardData{Node: "laptop", Replacements: ReplacementsOf(seats, modules), Machines: []Machine{
{Name: "anchor", Domains: []string{"anchor" + InternalSuffix, "anchor.example"}, Addresses: []string{"192.0.2.10", "10.10.0.1"}},
{Name: "laptop", Domains: []string{"laptop" + InternalSuffix}, Addresses: []string{"10.10.0.4"}},
}}
@@ -69,21 +73,21 @@ func bash(command string) HookInput {
func TestSSHToAMeshMachineIsRefusedNamingTheTool(t *testing.T) {
g := guard()
for command, want := range map[string]string{
"ssh anchor journalctl -u mesh-controller -n 50": "anchor/node-service-manager.journal",
"ssh anchor journalctl -u mesh-config -n 50": "anchor/node-service-manager.journal",
"ssh anchor 'sudo journalctl -fu sshd'": "anchor/node-service-manager.journal",
"ssh -p 22 root@anchor.example systemctl status nats": "anchor/node-service-manager.status",
"ssh anchor.internal systemctl restart nats": "anchor/node-service-manager.restart",
"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-controller.node",
"ssh a uptime": "mesh-placement.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-controller.node",
"for n in anchor laptop; do ssh $n uptime; done; ssh anchor uptime": "mesh-placement.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",
@@ -1,43 +1,82 @@
package main
import (
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// askingAController answers the controller's verbs as the seat does: `tools` in-process, the rest as the
// command's output and its JSON.
func askingAController(t *testing.T) Ask {
// aBus is the states claude-code reads, as a test holds them: by read, key and value.
type aBus map[string]map[string][]byte
func (b aBus) Keys(read string) ([]string, error) {
if b[read] == nil {
return nil, errors.New("no such state")
}
var keys []string
for k := range b[read] {
keys = append(keys, k)
}
return keys, nil
}
func (b aBus) Get(read, key string) ([]byte, bool, error) {
v, ok := b[read][key]
return v, ok, nil
}
// someSeats are two seats as the seats' contract says them: one held once for the mesh, one on every machine.
func someSeats() []catalogue.Seat {
return []catalogue.Seat{
{Name: "mesh-placement", Scope: catalogue.ScopeMesh, Serves: []catalogue.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: catalogue.ScopeNode, Serves: []catalogue.Verb{
{Name: "status", Description: "One unit as the service manager sees it now: its states, whether it starts at boot.", Replaces: []string{"systemctl status", "systemctl is-active"}},
{Name: "journal", Description: "The last lines of one unit's journal.", Replaces: []string{"journalctl"}}}},
}
}
// 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.
func aMesh(t *testing.T) aBus {
t.Helper()
wrap := func(output string, answer any) json.RawMessage {
raw, _ := json.Marshal(map[string]any{"ok": true, "output": output, "answer": answer})
return raw
}
return func(address string, args any) (json.RawMessage, error) {
switch address {
case "seat:mesh-controller.tools":
return json.RawMessage(`{"seats":[
{"seat":"mesh-controller","scope":"mesh","tools":[{"name":"node","description":"What one machine reported it can do, what it is assigned, and why.","replaces":["hostnamectl","uptime"]},{"name":"nodes","description":"Every machine."}]},
{"seat":"node-service-manager","scope":"node","tools":[
{"name":"status","description":"One unit as the service manager sees it now: its states, whether it starts at boot.","replaces":["systemctl status","systemctl is-active"]},
{"name":"journal","description":"The last lines of one unit's journal.","replaces":["journalctl"]}]}]}`), nil
case "seat:mesh-controller.modules":
return wrap("[...]", []any{map[string]any{"module": "docker", "on": []string{"anchor"},
"replaces": map[string][]string{"docker_logs": {"docker logs"}}}, map[string]any{"module": "zsh", "on": []string{"anchor"}}}), nil
case "seat:mesh-controller.nodes":
return wrap("", []any{map[string]any{"name": "anchor"}, map[string]any{"name": "laptop"}}), nil
case "seat:mesh-controller.node":
node, _ := args.(map[string]any)["node"].(string)
if node == "anchor" {
return wrap("anchor\n mode converged\n public domain Anchor.Example\n\n what it runs\n", nil), nil
}
return wrap("laptop\n mode converged\n", nil), nil
b := aBus{PlacementRead: {}, ModulesRead: {}, ManifestsRead: {}}
for _, p := range []delivery.Placement{
{V: 1, Node: "anchor", Decided: delivery.NodeDecided{PublicDomain: "Anchor.Example"},
Modules: []delivery.PlacedModule{{Module: "docker"}, {Module: "zsh"}}},
{V: 1, Node: "laptop"},
} {
values, err := delivery.PlacementValues(p)
if err != nil {
t.Fatal(err)
}
for k, v := range values {
b[PlacementRead][k] = v
}
return nil, errors.New("no such verb")
}
for module, manifest := range map[string]string{
"docker": `{"module":"docker","replaces":{"docker_logs":["docker logs"]}}`,
"zsh": `{"module":"zsh"}`,
} {
digest := fmt.Sprintf("%x", sha256.Sum256([]byte(manifest)))
mkey, _ := delivery.ManifestsKey(digest)
raw, err := json.Marshal(delivery.Manifest{V: 1, Module: module, JSON: manifest})
if err != nil {
t.Fatal(err)
}
b[ManifestsRead][mkey] = raw
key, _ := delivery.ModulesKey(module)
b[ModulesRead][key], _ = json.Marshal(delivery.Module{V: 1, None: "nothing is built of it", Manifest: digest, Repository: "novox/mesh-catalog", Commit: "c"})
}
return b
}
func resolving(name string) []string {
@@ -45,10 +84,11 @@ func resolving(name string) []string {
"laptop": {"127.0.0.1", "10.10.0.4"}}[name]
}
// What the controller says of its tools and machines is read whole: the seats' verbs, the modules' own tools,
// every machine with its domains and addresses — a loopback address never one of them.
func TestTheMeshsToolsAreAskedOfTheController(t *testing.T) {
m, err := AskMeshTools(askingAController(t), resolving)
// Issue 1126 B13: what the mesh says of its tools and machines is read from the bus whole, never asked of the
// controller: the seats' verbs, the placed modules' own tools, every machine with its domains and addresses — a
// loopback address never one of them.
func TestTheMeshsToolsAreReadFromTheBus(t *testing.T) {
m, err := ReadMeshToolsFrom(aMesh(t), someSeats(), resolving)
if err != nil {
t.Fatal(err)
}
@@ -56,7 +96,7 @@ func TestTheMeshsToolsAreAskedOfTheController(t *testing.T) {
for _, r := range m.Replacements {
addresses = append(addresses, r.Address)
}
if got := strings.Join(addresses, ","); got != "mesh-controller.node,node-service-manager.status,node-service-manager.journal,docker.docker_logs" {
if got := strings.Join(addresses, ","); got != "mesh-placement.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" ||
@@ -71,12 +111,12 @@ func TestTheMeshsToolsAreAskedOfTheController(t *testing.T) {
// **The table is generated, not written** (novox/hq ADR 0245): what the records say a verb replaces is a row,
// the machines' seats first, and nothing is rendered where the records say nothing.
func TestTheInsteadOfTableIsGeneratedFromTheRecords(t *testing.T) {
m, _ := AskMeshTools(askingAController(t), resolving)
m, _ := ReadMeshToolsFrom(aMesh(t), someSeats(), resolving)
table := InsteadOf(&m)
for _, want := range []string{
"| `<node>/node-service-manager.` | `status` — `systemctl status`, `systemctl is-active` · `journal` — `journalctl` |",
"| `<node>/docker.` | `docker_logs` — `docker logs` |",
"| `mesh-controller.` | `node` — `hostnamectl`, `uptime` |",
"| `mesh-placement.` | `placement` — `hostnamectl`, `uptime` |",
"Never `ssh` to a mesh machine",
} {
if !strings.Contains(table, want) {
@@ -84,14 +124,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-controller.`") {
strings.Index(table, "docker.") > strings.Index(table, "`mesh-placement.`") {
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-controller.node": 1}
m.Refused = map[string]int{"node-service-manager.journal": 3, "mesh-placement.placement": 1}
ranked := InsteadOf(&m)
if !strings.Contains(ranked, "| `<node>/node-service-manager.` | `journal` — `journalctl` · `status`") ||
strings.Index(ranked, "`mesh-controller.`") > strings.Index(ranked, "`<node>/docker.`") {
strings.Index(ranked, "`mesh-placement.`") > strings.Index(ranked, "`<node>/docker.`") {
t.Errorf("not ranked by the guard's record:\n%s", ranked)
}
m.Refused = nil
@@ -121,7 +161,7 @@ func TestTheInsteadOfTableIsGeneratedFromTheRecords(t *testing.T) {
// Asked again with the same answer, nothing is written and nothing renders.
func TestTheMeshsToolsAreKeptOnlyWhenTheyChange(t *testing.T) {
p := Paths{State: t.TempDir()}
m, _ := AskMeshTools(askingAController(t), resolving)
m, _ := ReadMeshToolsFrom(aMesh(t), someSeats(), resolving)
if !KeepMeshTools(p, m) {
t.Fatal("the first answer was not kept")
}
@@ -138,18 +178,32 @@ func TestTheMeshsToolsAreKeptOnlyWhenTheyChange(t *testing.T) {
}
}
// Where claude-code runs is read from the controller's `modules` answer as it comes — JSON, from `module list
// --json` — and from the printed list too.
func TestTheMachinesRunningTheModuleAreReadFromTheControllersAnswer(t *testing.T) {
asJSON := json.RawMessage(`{"ok":true,"output":"[...]","answer":[{"module":"zsh","on":["a"]},{"module":"claude-code","on":["a","b"]}]}`)
if got := strings.Join(NodesRunning(asJSON, "claude-code"), ","); got != "a,b" {
t.Errorf("from JSON: %q", got)
// Where a module runs is read from the seat mesh-placement'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" {
t.Errorf("zsh runs on %v (%v)", got, err)
}
printed := json.RawMessage(`{"ok":true,"output":"zsh 1 built x on a\nclaude-code 1 built y on a, b\n"}`)
if got := strings.Join(NodesRunning(printed, "claude-code"), ","); got != "a,b" {
t.Errorf("from the printed list: %q", got)
if got, err := NodesRunning(b, "claude-code"); err != nil || got != nil {
t.Errorf("a module that runs nowhere: %v (%v)", got, err)
}
if got := NodesRunning(json.RawMessage(`{"ok":true,"answer":[{"module":"zsh","on":["a"]}]}`), "claude-code"); got != nil {
t.Errorf("a module that runs nowhere: %v", got)
if _, err := NodesRunning(aBus{}, "zsh"); err == nil {
t.Error("an unreadable placement was read as no machine")
}
}
// Issue 1126 B13 AC1 (ADR 0590): over the seats the mesh defines, the rendered instructions, their table among them,
// name no address of the controller, which is not placed: each job is named by the seat that owns it now.
func TestTheRenderedInstructionsNameNoControllerAddress(t *testing.T) {
m, err := ReadMeshToolsFrom(aMesh(t), catalogue.Seats(), resolving)
if err != nil {
t.Fatal(err)
}
md := RenderWithMesh(Facts{Node: "w", Console: "x"}, Settings{}, nil, "/h", nil, Config{}, &m)["CLAUDE.md"]
if !strings.Contains(md, "## Instead of a shell command") {
t.Fatalf("no table rendered:\n%s", md)
}
if strings.Contains(md, "mesh-controller") {
t.Errorf("the instructions name the controller:\n%s", md)
}
}
@@ -2,11 +2,12 @@ package main
// instructionsText is the managed instruction file, generated from the TypeScript renderer it replaced so
// the file under the agent's managed directory did not change by a byte when the module moved to Go;
// a test holds it to that renderer's own output (testdata/rendered-by-typescript.json).
// a test holds it to that renderer's output (testdata/rendered-by-typescript.json), changed since only
// where the controller's verbs were named (issue 1126 B13: the controller is not placed, ADR 0590).
func instructionsText(node, role string) string {
return "# 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:** `" +
node +
"`\n- **Role:** " +
role +
"\n- The other nodes, their roles and what runs where: ask the controller (`mesh-controller.nodes`,\n `mesh-controller.node`). 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 verbs\n are `mesh-controller.<verb>`: `status`, `plan`, `node`, `assign`, `push`, `settings`);\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 controller's 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, `mesh-controller.plan` for the node 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-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"
}
+16 -48
View File
@@ -23,6 +23,7 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asks"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
func say(format string, args ...any) {
@@ -204,54 +205,21 @@ func (s stateOf) Get(key string) (json.RawMessage, bool, error) {
return e.Value, true, nil
}
// nodesRunningMe is the nodes claude-code runs on, from the controller's list of modules — for the register
// nodesRunningMe is the nodes claude-code runs on, from the seat mesh-placement's placement — for the register
// tool's question.
func nodesRunningMe() ([]string, error) {
raw, err := ask("seat:mesh-controller.modules", map[string]any{})
if err != nil {
return nil, err
}
return NodesRunning(raw, "claude-code"), nil
}
func nodesRunningMe() ([]string, error) { return NodesRunning(busStates{}, "claude-code") }
// NodesRunning reads where a module runs from the controller's `modules` answer. The verb runs `module list
// --json`, so the answer is a JSON list of {module, on}: the lines of the printed list it was once read as
// never came, and every "also on" hint and every `nodes: "all"` named no machine. The printed form, `<module>
// … on a, b`, is still read when that is what came.
func NodesRunning(raw json.RawMessage, module string) []string {
var listed []struct {
Module string `json:"module"`
On []string `json:"on"`
// busStates reads the states the manifest declares in its reads through the runtime.
type busStates struct{}
func (busStates) Keys(read string) ([]string, error) { return stdio.State(read).Keys() }
func (busStates) Get(read, key string) ([]byte, bool, error) {
e, err := stdio.State(read).Get(key)
if err != nil || e == nil {
return nil, false, err
}
if json.Unmarshal(verbAnswer(raw), &listed) == nil {
for _, m := range listed {
if m.Module == module {
return m.On
}
}
return nil
}
text := outputOf(raw)
if text == "" {
text = string(raw)
}
for _, line := range strings.Split(text, "\n") {
if !strings.HasPrefix(line, module+" ") {
continue
}
_, on, ok := strings.Cut(line, " on ")
if !ok || strings.TrimSpace(on) == "nothing" {
return nil
}
var out []string
for _, n := range strings.Split(on, ",") {
if n = strings.TrimSpace(n); n != "" {
out = append(out, n)
}
}
return out
}
return nil
return e.Value, true, nil
}
func fingerprintOfFile(path string) any {
@@ -320,11 +288,11 @@ func tools(p Paths, servers ServerState, view *ServerView, config ConfigState, c
Description: "Claude Code on this machine as the mesh configured it: the licence it holds and when its token expires, what it reports holding, the managed files, the MCP servers registered here. Fingerprints only, never a token.",
Run: func(map[string]any) (any, error) { return status(p), nil }},
{Name: "claude_code_render",
Description: "Write Claude Code's managed directory now, from the mesh's facts, this module's settings and the servers registered here — after asking the controller again what each tool replaces and which machines the mesh has (the instructions' table and the shell's guard).",
Description: "Write Claude Code's managed directory now, from the mesh's facts, this module's settings and the servers registered here — after reading the bus again for what each tool replaces and which machines the mesh has (the instructions' table and the shell's guard).",
Run: func(map[string]any) (any, error) {
answer := map[string]any{}
if m, err := AskMeshTools(ask, lookup); err != nil {
answer["asked"] = "the controller did not answer in full (" + err.Error() + "); what was kept is rendered"
if m, err := ReadMeshToolsFrom(busStates{}, catalogue.Seats(), lookup); err != nil {
answer["asked"] = "the bus could not be read in full (" + err.Error() + "); what was kept is rendered"
} else {
KeepMeshTools(p, m)
}
+160 -107
View File
@@ -4,21 +4,27 @@ package main
// shell need it (novox/hq ADR 0245): every seat verb and module tool that says what shell command it
// replaces, and the mesh's machines by every name and address a session could reach one by.
//
// Asked of the controller — `tools` for the seats' verbs, `modules` for the modules' own tools, `nodes`
// and `node` for the machines — at start and every few minutes, 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` in the records is in the next render's table and the guard's next refusal.
// 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-sdk catalogue), 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
// 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.
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// Replacement is one verb or tool and the shell commands it is the mesh's way to do.
@@ -41,11 +47,11 @@ type Machine struct {
Addresses []string `json:"addresses,omitempty"`
}
// MeshTools is what the controller said, as kept in the module's state.
// MeshTools is what the bus said, as kept in the module's state.
type MeshTools struct {
Replacements []Replacement `json:"replacements"`
Machines []Machine `json:"machines"`
// Asked is when the controller last answered.
// Asked is when the bus was last read whole.
Asked string `json:"asked,omitempty"`
// Refused is how often the guard here named each tool instead of a work-around, from its record: the
// table's order. Never kept — read from the record at each render.
@@ -78,42 +84,120 @@ func ReadMeshTools(p Paths) *MeshTools {
return &m
}
// verbAnswer is a controller verb's answer: the command's printed output, and its JSON when it printed one.
func verbAnswer(raw json.RawMessage) json.RawMessage {
var r struct {
Output string `json:"output"`
Answer json.RawMessage `json:"answer"`
// The states read, each declared in the manifest's reads.
const (
PlacementRead = delivery.PlacementRead
ModulesRead = delivery.IndexerModule + ".modules"
ManifestsRead = delivery.IndexerModule + ".manifests"
)
// States reads the module states the manifest declares in its reads: a state's keys, and one key's value.
type States interface {
Keys(read string) ([]string, error)
Get(read, key string) ([]byte, bool, error)
}
// ModuleTools is one module's own tools that replace a command, as its manifest says (`replaces`), and where it runs.
type ModuleTools struct {
Module string
On []string
Replaces map[string][]string
}
// placements is every node's placement, from the seat mesh-placement's state.
func placements(st States) (map[string]delivery.Placement, error) {
keys, err := st.Keys(PlacementRead)
if err != nil {
return nil, fmt.Errorf("%s: %w", PlacementRead, err)
}
if json.Unmarshal(raw, &r) != nil {
return raw
}
if len(r.Answer) > 0 && string(r.Answer) != "null" {
return r.Answer
}
if r.Output != "" {
t := strings.TrimSpace(r.Output)
for _, open := range []string{"[", "{"} {
if strings.HasPrefix(t, open) {
return json.RawMessage(t)
}
if i := strings.Index(t, "\n"+open); i >= 0 {
return json.RawMessage(strings.TrimSpace(t[i+1:]))
}
read := delivery.NewPlacementKeys()
for _, k := range keys {
v, found, err := st.Get(PlacementRead, k)
if err != nil {
return nil, fmt.Errorf("%s %s: %w", PlacementRead, k, err)
}
if found {
_, _ = read.Put(k, v, 0, false) // a key that does not read places nothing
}
return json.RawMessage(nil)
}
return raw
return read.All(), nil
}
func outputOf(raw json.RawMessage) string {
var r struct {
Output string `json:"output"`
// runsOn is, by module, the nodes placement puts it on, sorted.
func runsOn(placed map[string]delivery.Placement) map[string][]string {
out := map[string][]string{}
for node, p := range placed {
for _, m := range p.Modules {
out[m.Module] = append(out[m.Module], node)
}
}
_ = json.Unmarshal(raw, &r)
return r.Output
for _, nodes := range out {
sort.Strings(nodes)
}
return out
}
var publicDomain = regexp.MustCompile(`(?m)^\s*public domain\s+(\S+)\s*$`)
// NodesRunning is the nodes placement puts module on.
func NodesRunning(st States, module string) ([]string, error) {
placed, err := placements(st)
if err != nil {
return nil, err
}
return runsOn(placed)[module], nil
}
// modulesTools is every placed module's own tools that replace a command, by module, from the manifest the indexer
// keeps of it. A module whose manifest cannot be read is left out and said in the error.
func modulesTools(st States, on map[string][]string) ([]ModuleTools, error) {
var out []ModuleTools
var problems []error
modules := make([]string, 0, len(on))
for m := range on {
modules = append(modules, m)
}
sort.Strings(modules)
for _, module := range modules {
key, err := delivery.ModulesKey(module)
if err != nil {
continue
}
raw, found, err := st.Get(ModulesRead, key)
if err != nil || !found {
if err != nil {
problems = append(problems, fmt.Errorf("%s %s: %w", ModulesRead, module, err))
}
continue
}
m, err := delivery.DecodeModule(raw)
if err != nil || m.Manifest == "" {
continue
}
mkey, err := delivery.ManifestsKey(m.Manifest)
if err != nil {
continue
}
raw, found, err = st.Get(ManifestsRead, mkey)
if err != nil || !found {
if err != nil {
problems = append(problems, fmt.Errorf("%s of %s: %w", ManifestsRead, module, err))
}
continue
}
kept, err := delivery.DecodeManifest(raw)
if err != nil {
problems = append(problems, fmt.Errorf("the manifest of %s: %w", module, err))
continue
}
var man struct {
Replaces map[string][]string `json:"replaces"`
}
if json.Unmarshal([]byte(kept.JSON), &man) != nil || len(man.Replaces) == 0 {
continue
}
out = append(out, ModuleTools{Module: module, On: on[module], Replaces: man.Replaces})
}
return out, errors.Join(problems...)
}
// firstSentence is what a verb does, short: its description to the first full stop, at most 70 characters.
func firstSentence(s string) string {
@@ -131,52 +215,33 @@ func firstSentence(s string) string {
return s
}
// ReplacementsOf reads the controller's `tools` and `modules` answers: the seats' verbs first, in the
// records' order, then the modules' own tools by module and tool.
func ReplacementsOf(tools, modules json.RawMessage) []Replacement {
// ReplacementsOf is every seat verb and module tool that says it replaces a command: the seats' verbs first, in the
// seats' order, then the modules' own tools by module and tool.
func ReplacementsOf(seats []catalogue.Seat, modules []ModuleTools) []Replacement {
var out []Replacement
var seats struct {
Seats []struct {
Seat string `json:"seat"`
Scope string `json:"scope"`
Tools []struct {
Name string `json:"name"`
Description string `json:"description"`
Replaces []string `json:"replaces"`
} `json:"tools"`
} `json:"seats"`
}
if json.Unmarshal(tools, &seats) == nil {
for _, s := range seats.Seats {
for _, t := range s.Tools {
if len(t.Replaces) == 0 {
continue
}
out = append(out, Replacement{Address: s.Seat + "." + t.Name, NodeScoped: s.Scope == "node", Seat: true,
Does: firstSentence(t.Description), Replaces: t.Replaces})
for _, s := range seats {
for _, v := range s.Serves {
if len(v.Replaces) == 0 {
continue
}
out = append(out, Replacement{Address: s.Name + "." + v.Name, NodeScoped: s.Scope == catalogue.ScopeNode, Seat: true,
Does: firstSentence(v.Description), Replaces: v.Replaces})
}
}
var listed []struct {
Module string `json:"module"`
On []string `json:"on"`
Replaces map[string][]string `json:"replaces"`
}
if json.Unmarshal(modules, &listed) == nil {
sort.SliceStable(listed, func(i, j int) bool { return listed[i].Module < listed[j].Module })
for _, m := range listed {
names := make([]string, 0, len(m.Replaces))
for t := range m.Replaces {
names = append(names, t)
}
sort.Strings(names)
for _, t := range names {
if len(m.Replaces[t]) == 0 {
continue
}
out = append(out, Replacement{Address: m.Module + "." + t, NodeScoped: true, Replaces: m.Replaces[t], On: m.On,
Does: strings.ReplaceAll(strings.TrimPrefix(t, strings.ReplaceAll(m.Module, "-", "_")+"_"), "_", " ")})
sorted := append([]ModuleTools(nil), modules...)
sort.SliceStable(sorted, func(i, j int) bool { return sorted[i].Module < sorted[j].Module })
for _, m := range sorted {
names := make([]string, 0, len(m.Replaces))
for t := range m.Replaces {
names = append(names, t)
}
sort.Strings(names)
for _, t := range names {
if len(m.Replaces[t]) == 0 {
continue
}
out = append(out, Replacement{Address: m.Module + "." + t, NodeScoped: true, Replaces: m.Replaces[t], On: m.On,
Does: strings.ReplaceAll(strings.TrimPrefix(t, strings.ReplaceAll(m.Module, "-", "_")+"_"), "_", " ")})
}
}
return out
@@ -190,36 +255,24 @@ func lookup(name string) []string {
return addrs
}
// AskMeshTools asks the controller now. A part that does not answer is left out and said in the error;
// what did answer is kept.
func AskMeshTools(ask Ask, resolve func(string) []string) (MeshTools, error) {
// ReadMeshToolsFrom reads the bus now: the seats' verbs, the placed modules' own tools, and every node placement names
// with its domains and addresses. A part that cannot be read is left out and said in the error; what was read is kept.
func ReadMeshToolsFrom(st States, seats []catalogue.Seat, resolve func(string) []string) (MeshTools, error) {
var m MeshTools
tools, terr := ask("seat:mesh-controller.tools", map[string]any{})
modules, merr := ask("seat:mesh-controller.modules", map[string]any{})
if terr != nil && merr != nil {
return m, terr
}
m.Replacements = ReplacementsOf(verbAnswer(tools), verbAnswer(modules))
nodesRaw, err := ask("seat:mesh-controller.nodes", map[string]any{})
placed, err := placements(st)
if err != nil {
m.Replacements = ReplacementsOf(seats, nil)
return m, err
}
var nodes []struct {
Name string `json:"name"`
}
_ = json.Unmarshal(verbAnswer(nodesRaw), &nodes)
for _, n := range nodes {
if n.Name == "" {
continue
}
machine := Machine{Name: n.Name, Domains: []string{n.Name + InternalSuffix}}
if shown, err := ask("seat:mesh-controller.node", map[string]any{"node": n.Name}); err == nil {
if d := publicDomain.FindStringSubmatch(outputOf(shown)); d != nil {
machine.Domains = append(machine.Domains, strings.ToLower(d[1]))
}
modules, merr := modulesTools(st, runsOn(placed))
m.Replacements = ReplacementsOf(seats, modules)
for node, p := range placed {
machine := Machine{Name: node, Domains: []string{node + InternalSuffix}}
if d := strings.ToLower(strings.TrimSpace(p.Decided.PublicDomain)); d != "" {
machine.Domains = append(machine.Domains, d)
}
seen := map[string]bool{}
for _, name := range append([]string{n.Name}, machine.Domains...) {
for _, name := range append([]string{node}, machine.Domains...) {
for _, a := range resolve(name) {
if !seen[a] && !net.ParseIP(a).IsLoopback() {
seen[a] = true
@@ -231,7 +284,7 @@ func AskMeshTools(ask Ask, resolve func(string) []string) (MeshTools, error) {
m.Machines = append(m.Machines, machine)
}
sort.Slice(m.Machines, func(i, j int) bool { return m.Machines[i].Name < m.Machines[j].Name })
return m, nil
return m, merr
}
// KeepMeshTools writes what was asked to the module's state when it differs from what was kept, and
@@ -281,19 +334,19 @@ func guardFiles(p Paths, m *MeshTools) *GuardFiles {
return &GuardFiles{Exe: exe, Data: GuardDataOf(p, m), WaitsDir: WaitsDirPath(p)}
}
// MeshToolsEvery is how often the controller is asked again.
// MeshToolsEvery is how often the bus is read again.
const MeshToolsEvery = 10 * time.Minute
// refreshMeshTools asks the controller at start and every few minutes, and renders when what it said changed.
// refreshMeshTools reads the bus at start and every few minutes, and renders when what it said changed.
func refreshMeshTools(p Paths) {
for {
m, err := AskMeshTools(ask, lookup)
m, err := ReadMeshToolsFrom(busStates{}, catalogue.Seats(), lookup)
if err != nil {
say("asking the controller what each tool replaces: %v", err)
say("reading what each tool replaces and which machines the mesh has: %v", err)
}
if (err == nil || len(m.Replacements) > 0) && KeepMeshTools(p, m) {
if _, err := RenderNow(p, writeManaged); err != nil {
say("rendering what the controller said of its tools: %v", err)
say("rendering what the bus said of the tools: %v", err)
} else {
say("the mesh's tools changed: %d replace a command, %d machines; rendered", len(m.Replacements), len(m.Machines))
}
@@ -306,7 +359,7 @@ func refreshMeshTools(p Paths) {
func GuardStatus(p Paths, last int) map[string]any {
m := ReadMeshTools(p)
d := GuardDataOf(p, m)
asked := "never: the controller has not answered yet"
asked := "never: the bus has not been read whole yet"
if m != nil {
asked = m.Asked
}
@@ -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 controller (`mesh-controller.nodes`,\n `mesh-controller.node`). 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 verbs\n are `mesh-controller.<verb>`: `status`, `plan`, `node`, `assign`, `push`, `settings`);\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 controller's 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, `mesh-controller.plan` for the node 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-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"
},
"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 controller (`mesh-controller.nodes`,\n `mesh-controller.node`). 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 verbs\n are `mesh-controller.<verb>`: `status`, `plan`, `node`, `assign`, `push`, `settings`);\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 controller's 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, `mesh-controller.plan` for the node 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-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"
}
}
+4 -1
View File
@@ -35,7 +35,10 @@
"waits"
],
"reads": [
"claude-licence-manager.bindings"
"claude-licence-manager.bindings",
"mesh-delivery-assigner.placement",
"mesh-delivery-indexer.modules",
"mesh-delivery-indexer.manifests"
],
"tools": [
"claude_code_status",