claude-code: read where it runs from the controller's JSON answer
mesh/merge-gate pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered

nodesRunningMe looked for printed lines in what is a JSON list, so every register tool's "also on"
hint and every nodes: "all" named no machine.
This commit is contained in:
jochen
2026-10-07 20:28:06 +02:00
parent 24a3fac2ba
commit f72a435487
11 changed files with 61 additions and 28 deletions
+1 -1
View File
@@ -49,7 +49,7 @@ must see, a node that joins later included — kept, so it carries no secret eit
| an MCP server registered through this module | a key in the module's `servers` state — `all.<server>` for every node, `<node>.<server>` for one; every node watches it and renders what applies to it, a node's own entry over the one for every node. A node that joins later, or was off, reads the whole current set at start; unregistering is a delete. An entry with a secret in its `env` or `headers` is refused by the runtime |
| the agent's configuration (hq ADR 0216) | a key in the module's `config` state per registration — `mesh.<kind>.<name>` for every node, `node.<node>.<kind>.<name>` for one, `home.<node>.<kind>.<name>` for one account's own directory — the item and its files in one value, at most 256 KiB. Every node watches it and renders what applies to it, a node item over a mesh item of the same kind and name |
## The mesh's tools first (hq ADR 0244)
## The mesh's tools first (hq ADR 0245)
The agent kept reaching for `ssh <machine> journalctl` while the service manager's `journal` verb existed. So:
@@ -332,7 +332,7 @@ type PluginFile struct {
From string `json:",omitempty"`
}
// GuardDir is where the guard on the agent's shell lives in the plugin (novox/hq ADR 0244): its binary and
// GuardDir is where the guard on the agent's shell lives in the plugin (novox/hq ADR 0245): its binary and
// what it is given. Outside hooks/, so no registered hook's name can be it.
const GuardDir = "guard"
@@ -94,7 +94,7 @@ func TestEachKindLandsInItsOnePlace(t *testing.T) {
} `json:"hooks"`
}
_ = json.Unmarshal([]byte(plugin[root+"hooks/hooks.json"].Content), &hooks)
// The mesh's own guard on the shell first (novox/hq ADR 0244), then the hook registered.
// The mesh's own guard on the shell first (novox/hq ADR 0245), then the hook registered.
if pre := hooks.Hooks["PreToolUse"]; len(pre) != 2 || pre[0].Matcher != GuardMatcher || pre[0].Hooks[0].Command != GuardCommand() ||
pre[1].Matcher != "Bash" || pre[1].Hooks[0].Command != `"${CLAUDE_PLUGIN_ROOT}/hooks/guard"/guard.sh` {
t.Errorf("the hook's command does not name its directory, quoted: %s", plugin[root+"hooks/hooks.json"].Content)
+7 -7
View File
@@ -1,6 +1,6 @@
package main
// The guard on the agent's shell (novox/hq ADR 0244): a PreToolUse hook every session on a machine of the
// The guard on the agent's shell (novox/hq ADR 0245): a PreToolUse hook every session on a machine of the
// mesh runs before a shell command or a file edit, delivered in the module's plugin.
//
// It refuses three work-arounds, each with the mesh tool that does the job when one says it replaces the
@@ -69,7 +69,7 @@ type Verdict struct {
// command or a setting put there (guard_run.go).
const OverrideVar = "MESH_GUARD_OVERRIDE"
// ForgeUser is the forge's ssh account: it runs git, never a shell (stated in ADR 0244).
// ForgeUser is the forge's ssh account: it runs git, never a shell (stated in ADR 0245).
const ForgeUser = "git"
// protectedFiles are the files a session never writes for a mesh name.
@@ -110,7 +110,7 @@ func (g Guard) judgeLine(line string, depth int) Verdict {
}
if strings.Contains(line, OverrideVar) {
return Verdict{Refuse: true, Rule: "override-named", Message: fmt.Sprintf(
"Refused by the mesh's guard (novox/hq ADR 0244): this command names %s, the operator's override. "+
"Refused by the mesh's guard (novox/hq ADR 0245): this command names %s, the operator's override. "+
"It is the operator's alone, set in their own shell before a session starts; a session never sets, "+
"reads or passes it.", OverrideVar)}
}
@@ -310,7 +310,7 @@ func (g Guard) judgeSSH(name string, args []string, line string) Verdict {
}
}
if user == ForgeUser {
return Verdict{} // the forge's account: git and nothing else (stated in ADR 0244)
return Verdict{} // the forge's account: git and nothing else (stated in ADR 0245)
}
if machine, ok := g.meshHost(host); ok {
return g.refuseSSH(machine, host, remote, line)
@@ -573,7 +573,7 @@ func (g Guard) refuseSSH(machine, host, remote, line string) Verdict {
}
var b strings.Builder
var named []string
fmt.Fprintf(&b, "Refused by the mesh's guard (novox/hq ADR 0244): `%s` reaches %s round the mesh's tools.\n", cut(line, 160), who)
fmt.Fprintf(&b, "Refused by the mesh's guard (novox/hq ADR 0245): `%s` reaches %s round the mesh's tools.\n", cut(line, 160), who)
switch {
case strings.TrimSpace(remote) == "":
target := machine
@@ -603,7 +603,7 @@ func (g Guard) refuseSSH(machine, host, remote, line string) Verdict {
func (g Guard) refuseLocal(rule, replaced, what string) Verdict {
var b strings.Builder
fmt.Fprintf(&b, "Refused by the mesh's guard (novox/hq ADR 0244): %s is a work-around for a mesh name. "+
fmt.Fprintf(&b, "Refused by the mesh's guard (novox/hq ADR 0245): %s is a work-around for a mesh name. "+
"A mesh name is the mesh's resolvers' to answer, and a machine's own lines in /etc/hosts are its node-hostname seat's.\n", what)
var named []string
if address := g.exact(replaced); address != "" {
@@ -628,7 +628,7 @@ func cut(s string, n int) string {
return s
}
// commandWordsOf are a command line's words as matched against a replaced command (the console's rule):
// commandWordsOf are a command line's words as matched against a replaced command (the mesh MCP server's search rule):
// lower-cased, a program by path reduced to its name, sudo left out.
func commandWordsOf(line string) []string {
var out []string
@@ -1,6 +1,6 @@
package main
// The guard as the hook runs it (novox/hq ADR 0244): `claude-code guard <guard.json>`, the tool call on
// The guard as the hook runs it (novox/hq ADR 0245): `claude-code guard <guard.json>`, the tool call on
// standard input, exit 2 with the reason on standard error to refuse — the agent reads it — and 0 to let it be.
//
// **The operator's override.** `MESH_GUARD_OVERRIDE=<why>` lets one session through a refusal — never the
@@ -65,7 +65,7 @@ func bash(command string) HookInput {
}
// **ssh to a mesh machine is refused**, by every name and address it has and however the shell spells it,
// and the refusal names the verb that does the job on that machine (novox/hq ADR 0244).
// and the refusal names the verb that does the job on that machine (novox/hq ADR 0245).
func TestSSHToAMeshMachineIsRefusedNamingTheTool(t *testing.T) {
g := guard()
for command, want := range map[string]string{
@@ -1,6 +1,6 @@
package main
// The "instead of" table in the agent's managed instructions (novox/hq ADR 0244): to do this, call that
// The "instead of" table in the agent's managed instructions (novox/hq ADR 0245): to do this, call that
// address, not this shell command — generated on every render from what each seat verb and module tool
// says it replaces, so it cannot drift from the tools the mesh has.
//
@@ -78,7 +78,7 @@ func InsteadOf(m *MeshTools) string {
}
var b strings.Builder
b.WriteString("\n## Instead of a shell command\n\n")
b.WriteString("Generated from what each seat verb and module tool says it replaces (`replaces`, novox/hq ADR 0244),\n")
b.WriteString("Generated from what each seat verb and module tool says it replaces (`replaces`, novox/hq ADR 0245),\n")
b.WriteString("rewritten on every render. Call `<address><verb>` through `mesh_call`, `<node>` being the machine;\n")
b.WriteString("`mesh_search` with the command you would have typed finds it too.\n\n")
b.WriteString("| call | the verb — instead of |\n|---|---|\n")
@@ -68,7 +68,7 @@ func TestTheMeshsToolsAreAskedOfTheController(t *testing.T) {
}
}
// **The table is generated, not written** (novox/hq ADR 0244): what the records say a verb replaces is a row,
// **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)
@@ -137,3 +137,19 @@ func TestTheMeshsToolsAreKeptOnlyWhenTheyChange(t *testing.T) {
t.Errorf("the guard's data: %+v", d)
}
}
// 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)
}
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 := NodesRunning(json.RawMessage(`{"ok":true,"answer":[{"module":"zsh","on":["a"]}]}`), "claude-code"); got != nil {
t.Errorf("a module that runs nowhere: %v", got)
}
}
+28 -11
View File
@@ -163,20 +163,37 @@ func nodesRunningMe() ([]string, error) {
if err != nil {
return nil, err
}
var answer struct {
Output string `json:"output"`
return NodesRunning(raw, "claude-code"), nil
}
// 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"`
}
text := string(raw)
if json.Unmarshal(raw, &answer) == nil && answer.Output != "" {
text = answer.Output
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, "claude-code ") {
if !strings.HasPrefix(line, module+" ") {
continue
}
_, on, ok := strings.Cut(line, " on ")
if !ok || strings.TrimSpace(on) == "nothing" {
return nil, nil
return nil
}
var out []string
for _, n := range strings.Split(on, ",") {
@@ -184,9 +201,9 @@ func nodesRunningMe() ([]string, error) {
out = append(out, n)
}
}
return out, nil
return out
}
return nil, nil
return nil
}
func fingerprintOfFile(path string) any {
@@ -264,7 +281,7 @@ func tools(p Paths, servers ServerState, view *ServerView, config ConfigState, c
return answer, err
}},
{Name: "claude_code_guard",
Description: "The guard on the agent's shell on this machine (novox/hq ADR 0244): what it refuses — ssh to a mesh machine, writing /etc/hosts or /etc/resolv.conf, HOSTALIASES — the machines and the replaced commands it judges with, the \"instead of\" table rendered into the agent's instructions, and its record of what it refused and what the operator's override let through, newest last.",
Description: "The guard on the agent's shell on this machine (novox/hq ADR 0245): what it refuses — ssh to a mesh machine, writing /etc/hosts or /etc/resolv.conf, HOSTALIASES — the machines and the replaced commands it judges with, the \"instead of\" table rendered into the agent's instructions, and its record of what it refused and what the operator's override let through, newest last.",
Input: map[string]any{"last": map[string]any{"type": "integer", "description": "how many lines of the record (default 50)"}},
Run: func(a map[string]any) (any, error) {
last := 50
@@ -370,7 +387,7 @@ func persist(what string, attempt func() error, done func(refusals int)) {
}
func main() {
// The guard on the agent's shell (novox/hq ADR 0244): this binary, run by the agent's hook.
// The guard on the agent's shell (novox/hq ADR 0245): this binary, run by the agent's hook.
if len(os.Args) == 3 && os.Args[1] == "guard" {
os.Exit(runGuard(os.Args[2], os.Stdin, os.Stderr))
}
+1 -1
View File
@@ -1,7 +1,7 @@
package main
// What the mesh says about its own tools and machines, as the agent's instructions and the guard on its
// shell need it (novox/hq ADR 0244): every seat verb and module tool that says what shell command it
// 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`
@@ -103,7 +103,7 @@ func Render(facts Facts, settings Settings, binding *Binding, helperPath string,
return RenderWithMesh(facts, settings, binding, helperPath, registered, config, nil)
}
// RenderWithMesh is Render with what the mesh said about its tools (novox/hq ADR 0244): the "instead of"
// RenderWithMesh is Render with what the mesh said about its tools (novox/hq ADR 0245): the "instead of"
// table, after the mesh's own text and before the sections registered for the agent.
func RenderWithMesh(facts Facts, settings Settings, binding *Binding, helperPath string, registered Servers, config Config,
mesh *MeshTools) map[string]string {