claude-code: point the agent at the mesh's tools and guard its shell against ssh to the mesh (hq ADR 0245)

The agent kept running ssh <machine> journalctl while the journal verb existed. The managed
instructions now carry an "instead of" table generated from what each verb says it replaces, and
the plugin carries a PreToolUse guard that refuses ssh to a mesh machine and local work-arounds for a
mesh name, naming the tool or saying one must be created. The operator's override is read from the
session's start environment and recorded.
This commit is contained in:
jochen
2026-10-07 20:27:31 +02:00
parent ba6058abb9
commit 24a3fac2ba
13 changed files with 2033 additions and 14 deletions
@@ -0,0 +1,139 @@
package main
import (
"encoding/json"
"errors"
"os"
"strings"
"testing"
)
// 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 {
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
}
return nil, errors.New("no such verb")
}
}
func resolving(name string) []string {
return map[string][]string{"anchor": {"10.10.0.1"}, "anchor.internal": {"10.10.0.1"}, "anchor.example": {"192.0.2.10"},
"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)
if err != nil {
t.Fatal(err)
}
var addresses []string
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" {
t.Errorf("replacements: %s", got)
}
if len(m.Machines) != 2 || strings.Join(m.Machines[0].Domains, ",") != "anchor.internal,anchor.example" ||
strings.Join(m.Machines[0].Addresses, ",") != "10.10.0.1,192.0.2.10" || strings.Join(m.Machines[1].Addresses, ",") != "10.10.0.4" {
t.Errorf("machines: %+v", m.Machines)
}
if m.Replacements[3].Does != "logs" || !m.Replacements[2].NodeScoped || m.Replacements[0].NodeScoped {
t.Errorf("the rows: %+v", m.Replacements)
}
}
// **The table is generated, not written** (novox/hq ADR 0244): 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)
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` |",
"Never `ssh` to a mesh machine",
} {
if !strings.Contains(table, want) {
t.Errorf("the table lacks %q:\n%s", want, table)
}
}
if strings.Index(table, "node-service-manager.") > strings.Index(table, "docker.") ||
strings.Index(table, "docker.") > strings.Index(table, "`mesh-controller.`") {
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}
ranked := InsteadOf(&m)
if !strings.Contains(ranked, "| `<node>/node-service-manager.` | `journal` — `journalctl` · `status`") ||
strings.Index(ranked, "`mesh-controller.`") > strings.Index(ranked, "`<node>/docker.`") {
t.Errorf("not ranked by the guard's record:\n%s", ranked)
}
m.Refused = nil
if InsteadOf(nil) != "" || InsteadOf(&MeshTools{}) != "" {
t.Error("a table from nothing")
}
// Into the instructions after the mesh's own text, before what was registered.
out := RenderWithMesh(Facts{Node: "w", Console: "x"}, Settings{}, nil, "/h", nil,
Config{Mesh: []Item{{Kind: KindInstructions, Name: "conventions", Scope: ScopeMesh, Files: map[string]string{"conventions.md": "Be brief."}}}}, &m)
md := out["CLAUDE.md"]
if i, j, k := strings.Index(md, "## Conventions"), strings.Index(md, "## Instead of a shell command"), strings.Index(md, "### conventions"); !(i < j && j < k && i >= 0) {
t.Errorf("the table is not between the mesh's text and the registered sections:\n%s", md)
}
if os.Getenv("SHOW_TABLE") != "" {
t.Log(table)
}
// A long list is cut, and says how the rest is found.
var many MeshTools
for i := 0; i < InsteadOfRows+3; i++ {
many.Replacements = append(many.Replacements, Replacement{Address: "s" + string(rune('a'+i)) + ".v", Seat: true, Replaces: []string{"c"}})
}
if t2 := InsteadOf(&many); strings.Count(t2, "| `s") != InsteadOfRows || !strings.Contains(t2, "3 more seats and modules") {
t.Errorf("not cut to %d rows:\n%s", InsteadOfRows, t2)
}
}
// 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)
if !KeepMeshTools(p, m) {
t.Fatal("the first answer was not kept")
}
if KeepMeshTools(p, m) {
t.Fatal("the same answer was kept again")
}
m.Machines = m.Machines[:1]
if !KeepMeshTools(p, m) || len(ReadMeshTools(p).Machines) != 1 {
t.Fatal("a changed answer was not kept")
}
d := GuardDataOf(Paths{State: p.State, Node: "laptop"}, ReadMeshTools(p))
if d.Node != "laptop" || len(d.Replacements) != 4 || !strings.HasSuffix(d.Log, "guard.log") {
t.Errorf("the guard's data: %+v", d)
}
}