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:
@@ -11,6 +11,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -83,7 +84,14 @@ func writeManagedTree(name, content string) (string, error) {
|
||||
if f.Executable {
|
||||
mode = 0o755
|
||||
}
|
||||
if err := os.WriteFile(full, []byte(f.Content), mode); err != nil {
|
||||
content := []byte(f.Content)
|
||||
if f.From != "" {
|
||||
var err error
|
||||
if content, err = os.ReadFile(f.From); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(full, content, mode); err != nil {
|
||||
return "", err
|
||||
}
|
||||
_ = os.Chmod(full, mode)
|
||||
@@ -116,7 +124,13 @@ func sameTree(dir string, files map[string]PluginFile) bool {
|
||||
}
|
||||
raw, err := os.ReadFile(full)
|
||||
info, ierr := d.Info()
|
||||
if err != nil || ierr != nil || string(raw) != f.Content || (info.Mode()&0o111 != 0) != f.Executable {
|
||||
want := []byte(f.Content)
|
||||
if f.From != "" {
|
||||
if want, err = os.ReadFile(f.From); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err != nil || ierr != nil || !bytes.Equal(raw, want) || (info.Mode()&0o111 != 0) != f.Executable {
|
||||
return errors.New("differs")
|
||||
}
|
||||
found++
|
||||
@@ -237,10 +251,27 @@ 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.",
|
||||
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).",
|
||||
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"
|
||||
} else {
|
||||
KeepMeshTools(p, m)
|
||||
}
|
||||
out, err := RenderNow(p, writeManaged)
|
||||
return map[string]any{"rendered": out}, err
|
||||
answer["rendered"] = out
|
||||
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.",
|
||||
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
|
||||
if n, ok := a["last"].(float64); ok && n > 0 {
|
||||
last = int(n)
|
||||
}
|
||||
return GuardStatus(p, last), nil
|
||||
}},
|
||||
{Name: "claude_code_pull",
|
||||
Description: "Ask the licence manager for this node's current token now and apply it, rather than waiting for its binding to change.",
|
||||
@@ -339,6 +370,10 @@ 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.
|
||||
if len(os.Args) == 3 && os.Args[1] == "guard" {
|
||||
os.Exit(runGuard(os.Args[2], os.Stdin, os.Stderr))
|
||||
}
|
||||
p, launched := PathsFrom(os.Getenv)
|
||||
if !launched {
|
||||
// Outside a launch — a build, a check — it serves nothing and says why.
|
||||
@@ -360,6 +395,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
go refreshMeshTools(p)
|
||||
servers := stateOf{stdio.State("servers")}
|
||||
view := NewServerView(p)
|
||||
config := stateOf{stdio.State("config")}
|
||||
|
||||
Reference in New Issue
Block a user