package main import ( "bytes" "encoding/json" "os" "path/filepath" "strconv" "strings" "testing" ) // 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. 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{ {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"}}, }} } // noSSH reads a destination from the command line alone; aliases reads one alias as ssh's configuration would. func aliases(options []string, destination string) (string, string, []string) { host, user := hostOf(destination) switch host { case "a": return "anchor.example", "operator", nil case "via-anchor": return "203.0.113.5", "operator", []string{"anchor"} case "forge": return "git.anchor" + InternalSuffix, "git", nil } if user == "" { user = "operator" } for i, o := range options { if o == "-l" && i+1 < len(options) { user = options[i+1] } } return host, user, nil } func guard() Guard { return Guard{Data: meshForTheGuard(), SSH: aliases, Resolve: func(name string) []string { if name == "nas.lan" { return []string{"192.0.2.10"} } return nil }} } func bash(command string) HookInput { return HookInput{ToolName: "Bash", ToolInput: map[string]any{"command": command}} } // **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 0245). 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 '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", "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", "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", "ssh ssh://anchor:2222 journalctl": "anchor/node-service-manager.journal", } { v := g.Judge(bash(command)) if !v.Refuse || v.Rule != "ssh" { t.Errorf("%q was not refused: %+v", command, v) continue } if !strings.Contains(v.Message, want) { t.Errorf("%q: the refusal does not name %s:\n%s", command, want, v.Message) } } } // What no tool replaces is refused all the same, saying a tool is created, never worked around; a shell // on a machine is pointed at what the machine serves; copying a file off one is refused too. func TestSSHWithNoToolForItSaysCreateOne(t *testing.T) { g := guard() for command, want := range map[string]string{ "ssh anchor cat /var/lib/thing/state.json": "a missing tool is created in the module", "ssh anchor": "mesh_machine anchor", "ssh build.internal ls": "a missing tool is created", "scp anchor:/etc/nats/nats.conf /tmp/": "mesh_machine anchor", "rsync -av root@10.10.0.4:/srv/ ./srv/": "mesh_machine laptop", "sftp anchor.example": "mesh_machine anchor", "mosh anchor": "mesh_machine anchor", "autossh -M 0 -N -L 5432:localhost:5432 anchor": "mesh_machine anchor", } { v := g.Judge(bash(command)) if !v.Refuse { t.Errorf("%q was not refused", command) continue } if !strings.Contains(v.Message, want) { t.Errorf("%q: want %q in:\n%s", command, want, v.Message) } } } // **What is not a work-around passes**: git over ssh to the forge above all, and ssh beyond the mesh, and // every command that merely mentions ssh or a machine. func TestWhatIsNotAWorkAroundPasses(t *testing.T) { g := guard() for _, command := range []string{ "git push -u origin feat/x", "git clone ssh://git@git.anchor.internal:222/novox/hq.git", "git fetch ssh://git@git.anchor.internal:222/novox/hq.git main", `GIT_SSH_COMMAND="ssh -i ~/.ssh/forge -o IdentitiesOnly=yes" git push origin HEAD`, "git -c core.sshCommand='ssh -p 222' pull", "ssh -T git@git.anchor.internal -p 222", "ssh forge", "scp git@git.anchor.internal:novox/hq.git .", "ssh github.com", "ssh -T git@github.com", "ssh user@203.0.113.7 uptime", "ssh -V", "ssh-keygen -t ed25519 -f ~/.ssh/x", "ssh-add -l", "man ssh", "grep -r 'ssh anchor' docs/", `git commit -m "Stop running ssh anchor journalctl"`, "echo anchor && journalctl --user -n 5", "cat /etc/hosts", "getent hosts anchor.internal", "cp /etc/hosts /tmp/hosts.copy", "sed -n 1,5p /etc/hosts", "curl -s http://anchor.internal:8080/health", "go test ./...", } { if v := g.Judge(bash(command)); v.Refuse { t.Errorf("%q was refused:\n%s", command, v.Message) } } if v := g.Judge(HookInput{ToolName: "Read", ToolInput: map[string]any{"file_path": "/etc/hosts"}}); v.Refuse { t.Errorf("reading /etc/hosts was refused") } if v := g.Judge(HookInput{ToolName: "Write", ToolInput: map[string]any{"file_path": "/tmp/hosts"}}); v.Refuse { t.Errorf("writing a file named hosts elsewhere was refused") } } // **The local work-arounds for a mesh name are refused** with the machine's own hosts verb: writing // /etc/hosts by any means, the agent's own Edit and Write included, and HOSTALIASES. func TestTheLocalWorkAroundsForAMeshNameAreRefused(t *testing.T) { g := guard() for _, command := range []string{ "echo '10.10.0.1 anchor' | sudo tee -a /etc/hosts", "sudo sh -c 'echo 10.10.0.1 anchor >> /etc/hosts'", "echo x >>/etc/hosts", "sudo sed -i 's/old/new/' /etc/hosts", "sudo sed -Ei.bak 's/a/b/' /etc/hosts", "sudo vim /etc/hosts", "sudo cp /tmp/hosts /etc/hosts", "sudo install -m 644 hosts /etc/hosts", "HOSTALIASES=~/.hosts curl http://anchor/", "export HOSTALIASES=/tmp/aliases", } { v := g.Judge(bash(command)) if !v.Refuse { t.Errorf("%q was not refused", command) continue } if !strings.Contains(v.Message, "laptop/node-hostname.add") { t.Errorf("%q: the refusal does not name this machine's hosts verb:\n%s", command, v.Message) } } for _, tool := range []string{"Edit", "Write", "MultiEdit"} { v := g.Judge(HookInput{ToolName: tool, ToolInput: map[string]any{"file_path": "/etc/../etc/hosts"}}) if !v.Refuse || !strings.Contains(v.Message, "node-hostname.add") { t.Errorf("%s of /etc/hosts was not refused with the verb: %+v", tool, v) } } // The resolver file has no verb that replaces writing it: the refusal says to create one. v := g.Judge(bash("echo nameserver 192.0.2.53 | sudo tee /etc/resolv.conf")) if !v.Refuse || !strings.Contains(v.Message, "a missing tool is created") { t.Errorf("writing the resolver file: %+v", v) } } // The agent never names the operator's override, and no override lifts that refusal. func TestACommandNamingTheOverrideIsRefused(t *testing.T) { g := guard() for _, command := range []string{ "export " + OverrideVar + "=because", OverrideVar + "=x claude -p 'ssh anchor uptime'", "env " + OverrideVar + "=1 bash", } { v := g.Judge(bash(command)) if !v.Refuse || v.Rule != "override-named" || v.Overridable { t.Errorf("%q: %+v", command, v) } } } // Without the mesh's answer — a controller not asked yet — the mesh's own names are still refused. func TestWithoutTheMeshsAnswerItsNamesAreStillRefused(t *testing.T) { g := Guard{} if v := g.Judge(bash("ssh anchor.internal journalctl")); !v.Refuse || !strings.Contains(v.Message, "a missing tool is created") { t.Errorf("an internal name without data: %+v", v) } if v := g.Judge(bash("echo x | sudo tee -a /etc/hosts")); !v.Refuse { t.Errorf("the hosts file without data: %+v", v) } } // fakeProc lays out a process tree: the agent started with env, a shell under it, the hook under that. func fakeProc(t *testing.T, agentEnv []string) (root string, hookParent int) { t.Helper() root = t.TempDir() write := func(pid, ppid int, cmdline []string, env []string) { dir := filepath.Join(root, strconv.Itoa(pid)) _ = os.MkdirAll(dir, 0o755) _ = os.WriteFile(filepath.Join(dir, "cmdline"), []byte(strings.Join(cmdline, "\x00")+"\x00"), 0o644) _ = os.WriteFile(filepath.Join(dir, "environ"), []byte(strings.Join(env, "\x00")), 0o644) _ = os.WriteFile(filepath.Join(dir, "stat"), []byte(strconv.Itoa(pid)+" (some (odd) name) S "+strconv.Itoa(ppid)+" 1 1"), 0o644) } write(100, 1, []string{"/opt/claude-code/bin/claude", "--resume"}, agentEnv) write(200, 100, []string{"/bin/sh", "-c", "hook"}, []string{OverrideVar + "=set-by-the-session"}) return root, 200 } // **The override is the operator's, from the session as it was started, and recorded** — and a value a // command or a setting put in the session's later environment is not it. func TestTheOverrideIsTheOperatorsAndRecorded(t *testing.T) { data := meshForTheGuard() data.Log = filepath.Join(t.TempDir(), "guard.log") path := filepath.Join(t.TempDir(), "guard.json") raw, _ := json.Marshal(data) _ = os.WriteFile(path, raw, 0o644) was, wasPID := procRoot, parentPID t.Cleanup(func() { procRoot, parentPID = was, wasPID }) run := func(command string) (int, string) { var stderr bytes.Buffer in, _ := json.Marshal(bash(command)) return runGuard(path, bytes.NewReader(in), &stderr), stderr.String() } // Not set where the session started: the later shell's value is not the operator's. root, hook := fakeProc(t, []string{"HOME=/home/operator"}) procRoot, parentPID = root, func() int { return hook } if code, says := run("ssh anchor journalctl"); code != 2 || !strings.Contains(says, "anchor/node-service-manager.journal") { t.Fatalf("refused with %d: %s", code, says) } // Set by the operator before the session: let through, with why on record. root, hook = fakeProc(t, []string{"HOME=/home/operator", OverrideVar + "=reading the broker's log by hand while the systemd module is down"}) procRoot, parentPID = root, func() int { return hook } if code, says := run("ssh anchor journalctl -u nats"); code != 0 { t.Fatalf("the operator's override was not honoured: %d %s", code, says) } // Never for a command naming the override itself. if code, _ := run("export " + OverrideVar + "=x"); code != 2 { t.Fatalf("a command naming the override passed under it: %d", code) } record := ReadGuardLog(data.Log, 10) if len(record) != 3 || record[0].Decision != "refused" || record[1].Decision != "overridden" || !strings.Contains(record[1].Why, "systemd module is down") || record[1].Machine != "anchor" || record[2].Rule != "override-named" { t.Fatalf("the record: %+v", record) } // An override that cannot be recorded is not honoured. data.Log = filepath.Join(t.TempDir(), "no", "such", "dir", "guard.log") raw, _ = json.Marshal(data) _ = os.WriteFile(path, raw, 0o644) if code, says := run("ssh anchor journalctl"); code != 2 || !strings.Contains(says, "could not be recorded") { t.Fatalf("an override not recorded was honoured: %d %s", code, says) } } // What passes is not recorded, and a guard that cannot read the call says so and blocks nothing. func TestTheHookExitsAsTheAgentReadsIt(t *testing.T) { path := filepath.Join(t.TempDir(), "guard.json") _ = os.WriteFile(path, []byte(`{}`), 0o644) var stderr bytes.Buffer in, _ := json.Marshal(bash("git push")) if code := runGuard(path, bytes.NewReader(in), &stderr); code != 0 || stderr.Len() != 0 { t.Errorf("git push: %d %q", code, stderr.String()) } if code := runGuard(path, strings.NewReader("not json"), &stderr); code != 1 { t.Errorf("an unreadable call: %d", code) } } func TestTheAgentsProcessIsKnown(t *testing.T) { for cmdline, want := range map[string]bool{ "/opt/claude-code/bin/claude\x00--resume\x00": true, "claude\x00": true, "node\x00/usr/lib/node_modules/@anthropic-ai/claude-code/cli.js\x00": true, "/usr/bin/node\x00/home/x/.local/bin/claude\x00": true, "/bin/zsh\x00": false, "node\x00server.js\x00": false, "/usr/bin/claude-code-tools\x00": false, } { if isAgent([]byte(cmdline)) != want { t.Errorf("%q: want %v", cmdline, want) } } }