mesh/merge-gate pass: builds claude-code, claude-licence-manager, restic, sudo → ace, g14, novox, shanks; no bus step; every machine composes with the chan…
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery delivered
The managed settings apply to every account's claude, so a key-helper script under the agent's home would run as the operator, and through sudo as root: the command is now carried whole in the root-owned managed settings and reads the key only as the agent. The helper reads only a regular file, bounded, without blocking, and a request it does not answer fails in time instead of holding every later one; a directory entry it answers that is not a name is refused. The guard's record is matched on its binary in the query, a half-named agent account is not taken as the operator's, the kept API key stays until the agent's home holds one, restic refuses to restore beside a directory others may write, and the agent-home check cleans a path before judging it.
574 lines
22 KiB
Go
574 lines
22 KiB
Go
package main
|
|
|
|
// Where the agent runs as an account of its own (novox/hq ADR 0266): the home touched through the helper, the
|
|
// licence that follows the account, what goes from the operator's home, and the guard that refuses a session
|
|
// running as anyone else.
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"io/fs"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// inProcess is a helper served in this process over pipes, counting what it was asked; kill ends it, as a
|
|
// helper that died would.
|
|
type inProcess struct {
|
|
asked atomic.Int64
|
|
mu sync.Mutex
|
|
kill func()
|
|
}
|
|
|
|
func (c *inProcess) die() {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.kill()
|
|
}
|
|
|
|
func helperInProcess(account string, counted *inProcess) *Helper {
|
|
return &Helper{Account: account, Start: func() (io.WriteCloser, io.Reader, func(), error) {
|
|
reqR, reqW := io.Pipe()
|
|
ansR, ansW := io.Pipe()
|
|
counting := &countingReader{r: reqR, n: &counted.asked}
|
|
go func() { _ = ServeHelper(counting, ansW); _ = ansW.Close() }()
|
|
counted.mu.Lock()
|
|
counted.kill = func() { _ = reqR.Close(); _ = ansW.Close() }
|
|
counted.mu.Unlock()
|
|
return reqW, ansR, func() { _ = reqW.Close() }, nil
|
|
}}
|
|
}
|
|
|
|
type countingReader struct {
|
|
r io.Reader
|
|
n *atomic.Int64
|
|
}
|
|
|
|
func (c *countingReader) Read(p []byte) (int, error) {
|
|
n, err := c.r.Read(p)
|
|
c.n.Add(int64(bytes.Count(p[:n], []byte("\n"))))
|
|
return n, err
|
|
}
|
|
|
|
func agentFiles(t *testing.T) (*Files, string, *inProcess) {
|
|
t.Helper()
|
|
home := filepath.Join(t.TempDir(), "agent")
|
|
if err := os.MkdirAll(home, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
counted := &inProcess{}
|
|
return &Files{Home: home, Account: "agent", Helper: helperInProcess("agent", counted)}, home, counted
|
|
}
|
|
|
|
func TestTheAgentsHomeIsTouchedThroughTheHelper(t *testing.T) {
|
|
f, home, counted := agentFiles(t)
|
|
file := filepath.Join(home, ".claude", "skills", "x", "SKILL.md")
|
|
if err := f.MkdirAll(filepath.Dir(file), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := f.WriteFile(file, []byte("hello"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, err := f.ReadFile(file); err != nil || string(got) != "hello" {
|
|
t.Fatalf("read back %q, %v", got, err)
|
|
}
|
|
info, err := f.Stat(file)
|
|
if err != nil || info.Size() != 5 || info.Mode().Perm() != 0o600 || info.IsDir() {
|
|
t.Fatalf("stat: %v %v", info, err)
|
|
}
|
|
if err := f.Chmod(file, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if info, _ := f.Lstat(file); info.Mode().Perm() != 0o644 {
|
|
t.Fatalf("chmod did not take: %v", info.Mode())
|
|
}
|
|
if err := f.CreateExclusive(file, []byte("again"), 0o600); !errors.Is(err, fs.ErrExist) {
|
|
t.Fatalf("a file that is there is said to exist, got %v", err)
|
|
}
|
|
if _, err := f.ReadFile(filepath.Join(home, "absent")); !os.IsNotExist(err) {
|
|
t.Fatalf("an absent file is said not to exist, as the filesystem says it, got %v", err)
|
|
}
|
|
entries, err := f.ReadDir(filepath.Join(home, ".claude", "skills"))
|
|
if err != nil || len(entries) != 1 || entries[0].Name() != "x" || !entries[0].IsDir() {
|
|
t.Fatalf("readdir: %v %v", entries, err)
|
|
}
|
|
var walked []string
|
|
if err := f.WalkDir(filepath.Join(home, ".claude"), func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rel, _ := filepath.Rel(home, path)
|
|
walked = append(walked, rel)
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Join(walked, ",") != ".claude,.claude/skills,.claude/skills/x,.claude/skills/x/SKILL.md" {
|
|
t.Fatalf("walked %v", walked)
|
|
}
|
|
moved := filepath.Join(home, "moved")
|
|
if err := f.Rename(file, moved); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := f.Remove(moved); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := f.RemoveAll(filepath.Join(home, ".claude")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(home, ".claude")); !os.IsNotExist(err) {
|
|
t.Fatal("remove-all left the directory")
|
|
}
|
|
if counted.asked.Load() < 12 {
|
|
t.Fatalf("every operation under the home goes through the helper; it was asked %d times", counted.asked.Load())
|
|
}
|
|
}
|
|
|
|
func TestAPathOutsideTheAgentsHomeIsNeverTheHelpers(t *testing.T) {
|
|
f, home, counted := agentFiles(t)
|
|
state := t.TempDir()
|
|
if err := f.WriteFile(filepath.Join(state, "x"), []byte("y"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := f.ReadFile(filepath.Join(state, "x")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// A sibling whose name begins with the home's is not under it.
|
|
if f.through(home+"-other/x") || !f.through(home) || !f.through(home+"/.claude.json") {
|
|
t.Fatal("the home is the home and nothing beside it")
|
|
}
|
|
if err := f.Rename(filepath.Join(state, "x"), filepath.Join(home, "x")); err == nil {
|
|
t.Fatal("a move between the home and elsewhere is refused: no one account may do both")
|
|
}
|
|
if counted.asked.Load() != 0 {
|
|
t.Fatalf("the helper was asked %d times for paths that are not the agent's", counted.asked.Load())
|
|
}
|
|
if (&Files{}).through(home) {
|
|
t.Fatal("with no agent account of its own, nothing goes through a helper")
|
|
}
|
|
}
|
|
|
|
func TestAHelperThatDiedIsStartedAgain(t *testing.T) {
|
|
f, home, counted := agentFiles(t)
|
|
if err := f.WriteFile(filepath.Join(home, "a"), []byte("1"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
counted.die()
|
|
if got, err := f.ReadFile(filepath.Join(home, "a")); err != nil || string(got) != "1" {
|
|
t.Fatalf("after the helper died the next request is answered by a new one: %q %v", got, err)
|
|
}
|
|
}
|
|
|
|
func TestTheHelperRefusesARelativePathAndAnUnknownOperation(t *testing.T) {
|
|
if a := serveOne(Op{Op: "read", Path: "relative"}); a.Err == "" {
|
|
t.Fatal("a relative path is refused")
|
|
}
|
|
if a := serveOne(Op{Op: "exec", Path: "/"}); !strings.Contains(a.Err, "no such operation") {
|
|
t.Fatalf("only the filesystem's operations are served, got %+v", a)
|
|
}
|
|
}
|
|
|
|
// ---- the paths --------------------------------------------------------------------------------------------
|
|
|
|
func envOf(m map[string]string) func(string) string { return func(k string) string { return m[k] } }
|
|
|
|
func TestTheAgentsAccountAndHomeComeFromTheRuntime(t *testing.T) {
|
|
base := map[string]string{"MESH_CLAUDE_CODE_STATE": "/s", "MESH_CLAUDE_CODE_FACTS": "/s/f", "MESH_CLAUDE_CODE_SETTINGS": "/s/x",
|
|
"MESH_NODE": "anchor", "MESH_OPERATOR_ACCOUNT": "operator", "MESH_OPERATOR_HOME": "/home/operator"}
|
|
p, ok := PathsFrom(envOf(base))
|
|
if !ok || p.Home != "/home/operator" || p.Agent != "operator" || p.Confined() {
|
|
t.Fatalf("a controller that names no agent account: the operator's, as before; got %+v", p)
|
|
}
|
|
with := map[string]string{"MESH_AGENT_ACCOUNT": "agent", "MESH_AGENT_HOME": "/home/agent"}
|
|
for k, v := range base {
|
|
with[k] = v
|
|
}
|
|
p, ok = PathsFrom(envOf(with))
|
|
if !ok || p.Home != "/home/agent" || p.Agent != "agent" || p.OperatorHome != "/home/operator" || !p.Confined() {
|
|
t.Fatalf("an agent account of its own: its home; got %+v", p)
|
|
}
|
|
if p.keyDir() != "/home/agent/.claude/mesh" || p.placed() != "/s/home-placed-agent.json" {
|
|
t.Fatalf("the key and the record follow the agent's account: %s %s", p.keyDir(), p.placed())
|
|
}
|
|
with["MESH_AGENT_HOME"] = ""
|
|
if _, ok := PathsFrom(envOf(with)); ok {
|
|
t.Fatal("the agent's account without its home was taken as the operator's")
|
|
}
|
|
with["MESH_AGENT_ACCOUNT"], with["MESH_AGENT_HOME"] = "operator", "/home/operator"
|
|
if p, _ = PathsFrom(envOf(with)); p.Confined() || p.keyDir() != "/s" || p.placed() != "/s/home-placed.json" {
|
|
t.Fatalf("the agent account named as the operator's is the operator's: %+v", p)
|
|
}
|
|
}
|
|
|
|
// ---- the licence follows the account ----------------------------------------------------------------------
|
|
|
|
func TestABindingAppliedIsFetchedAgainWhenTheAgentsHomeHoldsNoToken(t *testing.T) {
|
|
p, _ := node(t, "anchor")
|
|
writeFile(t, p.binding(), `{"licence":"personal","kind":"subscription","generation":4}`)
|
|
asked := 0
|
|
ask := func(string, any) (json.RawMessage, error) { asked++; return json.RawMessage(`{}`), nil }
|
|
if _, err := OnBinding(p, &BindingState{Licence: "personal", Kind: "subscription", Generation: 4}, ask, writer(map[string]string{})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if asked != 1 {
|
|
t.Fatalf("a home with no token is asked for one, though the binding is the one applied; asked %d", asked)
|
|
}
|
|
writeFile(t, p.credentials(), `{"claudeAiOauth":{"accessToken":"a","expiresAt":1}}`)
|
|
if _, err := OnBinding(p, &BindingState{Licence: "personal", Kind: "subscription", Generation: 4}, ask, writer(map[string]string{})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if asked != 1 {
|
|
t.Fatal("a home that holds the token applied is not asked again")
|
|
}
|
|
}
|
|
|
|
// ---- what goes from the operator's home -------------------------------------------------------------------
|
|
|
|
func confined(t *testing.T) Paths {
|
|
t.Helper()
|
|
p, _ := node(t, "anchor")
|
|
p.Agent, p.Operator = "agent", "operator"
|
|
p.OperatorHome = filepath.Join(filepath.Dir(p.Home), "operator")
|
|
if err := os.MkdirAll(filepath.Join(p.OperatorHome, ".claude", "rules"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func TestTheTokenTheModulePlacedInTheOperatorsHomeGoes(t *testing.T) {
|
|
p := confined(t)
|
|
file := filepath.Join(p.OperatorHome, ".claude", ".credentials.json")
|
|
writeFile(t, file, `{"claudeAiOauth":{"accessToken":"a","expiresAt":1}}`)
|
|
writeFile(t, filepath.Join(p.State, "api-key"), "k\n")
|
|
if said := strings.Join(RetireOperatorHome(p), "\n"); !strings.Contains(said, "until agent's home holds") {
|
|
t.Fatalf("while the agent's home holds no licence the operator's token stays: %s", said)
|
|
}
|
|
if _, err := os.Stat(file); err != nil {
|
|
t.Fatal("the operator's token went before the agent's home held one")
|
|
}
|
|
writeFile(t, p.credentials(), `{"claudeAiOauth":{"accessToken":"b","expiresAt":1}}`)
|
|
said := strings.Join(RetireOperatorHome(p), "\n")
|
|
if _, err := os.Stat(file); !os.IsNotExist(err) {
|
|
t.Fatalf("the access-token-only file is the module's and goes: %s", said)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(p.State, "api-key")); err != nil {
|
|
t.Fatal("the key kept in the state went before the agent's home held one")
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(p.apiKey()), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writeFile(t, p.apiKey(), "k\n")
|
|
RetireOperatorHome(p)
|
|
if _, err := os.Stat(filepath.Join(p.State, "api-key")); !os.IsNotExist(err) {
|
|
t.Fatal("the key kept where the agent's account cannot read it goes once the agent's home holds it")
|
|
}
|
|
}
|
|
|
|
// The managed settings apply to every account's claude, root's included: where the agent runs as an account of
|
|
// its own, the key-helper they name is never a file under any home, and reads the key only as that account.
|
|
func TestTheKeyHelperIsNeverAFileTheAgentOwns(t *testing.T) {
|
|
p := confined(t)
|
|
cmd := p.helperCommand()
|
|
if strings.HasPrefix(cmd, "/") || strings.HasPrefix(cmd, p.Home) || strings.HasPrefix(cmd, filepath.Dir(p.Home)) {
|
|
t.Fatalf("the key-helper is a path the agent may own: %q", cmd)
|
|
}
|
|
if !strings.Contains(cmd, `"$(id -un)" = 'agent'`) || !strings.Contains(cmd, "exec cat -- '"+p.apiKey()+"'") {
|
|
t.Fatalf("the key-helper reads the key only as the agent's account: %q", cmd)
|
|
}
|
|
p.Agent = p.Operator
|
|
if p.helperCommand() != p.helper() {
|
|
t.Fatalf("where the agent runs as the operator the helper stays in the state: %q", p.helperCommand())
|
|
}
|
|
}
|
|
|
|
func TestALoginThePersonMadeInTheOperatorsHomeStays(t *testing.T) {
|
|
p := confined(t)
|
|
file := filepath.Join(p.OperatorHome, ".claude", ".credentials.json")
|
|
writeFile(t, file, `{"claudeAiOauth":{"accessToken":"a","refreshToken":"r","expiresAt":1}}`)
|
|
said := strings.Join(RetireOperatorHome(p), "\n")
|
|
if _, err := os.Stat(file); err != nil || !strings.Contains(said, "login the person made") {
|
|
t.Fatalf("a refresh token is a person's login: left and said; said %q", said)
|
|
}
|
|
}
|
|
|
|
func TestWhatWasPlacedInTheOperatorsHomeGoesUnlessChanged(t *testing.T) {
|
|
p := confined(t)
|
|
dir := filepath.Join(p.OperatorHome, ".claude")
|
|
writeFile(t, filepath.Join(dir, "rules", "a.md"), "placed")
|
|
writeFile(t, filepath.Join(dir, "rules", "b.md"), "changed by hand")
|
|
raw, _ := json.Marshal(Placed{"rules/a.md": digest("placed"), "rules/b.md": digest("placed")})
|
|
writeFile(t, filepath.Join(p.State, "home-placed.json"), string(raw))
|
|
RetireOperatorHome(p)
|
|
if _, err := os.Stat(filepath.Join(dir, "rules", "a.md")); !os.IsNotExist(err) {
|
|
t.Fatal("an item placed and unchanged goes, as unregistering takes it")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, "rules", "b.md")); err != nil {
|
|
t.Fatal("an item the person changed is theirs now and stays")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(p.State, "home-placed.json")); !os.IsNotExist(err) {
|
|
t.Fatal("the operator's home's record goes once it holds nothing")
|
|
}
|
|
}
|
|
|
|
func TestNothingGoesWhereTheAgentRunsAsTheOperator(t *testing.T) {
|
|
p, _ := node(t, "anchor")
|
|
p.OperatorHome = p.Home
|
|
file := p.credentials()
|
|
writeFile(t, file, `{"claudeAiOauth":{"accessToken":"a","expiresAt":1}}`)
|
|
if said := RetireOperatorHome(p); len(said) != 0 {
|
|
t.Fatalf("said %v", said)
|
|
}
|
|
if _, err := os.Stat(file); err != nil {
|
|
t.Fatal("the token stays where the agent reads it")
|
|
}
|
|
}
|
|
|
|
// ---- the guard --------------------------------------------------------------------------------------------
|
|
|
|
func TestASessionNotRunningAsTheAgentsAccountDoesNothing(t *testing.T) {
|
|
g := Guard{Data: GuardData{Node: "anchor", Agent: "agent"}}
|
|
echo := HookInput{ToolName: "Bash", ToolInput: map[string]any{"command": "echo hello"}}
|
|
v := g.JudgeAs("operator", echo)
|
|
if !v.Refuse || v.Rule != "account" || v.Overridable || !strings.Contains(v.Message, "claude-agent") {
|
|
t.Fatalf("a session as the operator is refused whatever it runs, with no override: %+v", v)
|
|
}
|
|
if v := g.JudgeAs("operator", HookInput{ToolName: "Write", ToolInput: map[string]any{"file_path": "/tmp/x"}}); !v.Refuse {
|
|
t.Fatal("a file edit too")
|
|
}
|
|
if v := g.JudgeAs("agent", echo); v.Refuse {
|
|
t.Fatalf("the agent's own account runs as before: %+v", v)
|
|
}
|
|
if v := (Guard{Data: GuardData{Node: "laptop"}}).JudgeAs("operator", echo); v.Refuse {
|
|
t.Fatal("where agents run as the operator, the operator's session runs as before")
|
|
}
|
|
}
|
|
|
|
func TestWhereTheAgentHasItsOwnAccountTheRecordIsTheJournal(t *testing.T) {
|
|
p, _ := node(t, "anchor")
|
|
if d := GuardDataOf(p, nil); d.Agent != "" || strings.HasPrefix(d.Log, JournalRecord) {
|
|
t.Fatalf("as the operator: the module's own file; got %+v", d)
|
|
}
|
|
p.Agent, p.Operator = "agent", "operator"
|
|
d := GuardDataOf(p, nil)
|
|
if d.Agent != "agent" || d.Log != JournalRecord+GuardJournalID {
|
|
t.Fatalf("as an account of its own: the journal, and the account named; got %+v", d)
|
|
}
|
|
|
|
socket := filepath.Join(t.TempDir(), "journal")
|
|
conn, err := net.ListenUnixgram("unixgram", &net.UnixAddr{Name: socket, Net: "unixgram"})
|
|
if err != nil {
|
|
t.Skipf("no unix datagram socket here: %v", err)
|
|
}
|
|
defer conn.Close()
|
|
was := journalSocket
|
|
journalSocket = socket
|
|
t.Cleanup(func() { journalSocket = was })
|
|
if err := RecordGuard(d.Log, GuardEntry{Decision: "refused", Rule: "account", Tool: "Bash", Command: "a\nb"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
buf := make([]byte, 4096)
|
|
n, _, err := conn.ReadFrom(buf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
lines := strings.Split(strings.TrimSuffix(string(buf[:n]), "\n"), "\n")
|
|
if len(lines) != 3 || lines[0] != "SYSLOG_IDENTIFIER="+GuardJournalID || !strings.HasPrefix(lines[2], "MESSAGE={") {
|
|
t.Fatalf("one line per field, the entry as one line of JSON: %q", lines)
|
|
}
|
|
|
|
readWas := readJournal
|
|
t.Cleanup(func() { readJournal = readWas })
|
|
readJournal = func(id string, last int) ([]byte, error) {
|
|
if id != GuardJournalID {
|
|
t.Fatalf("read %s", id)
|
|
}
|
|
msg, _ := json.Marshal(strings.TrimPrefix(lines[2], "MESSAGE="))
|
|
forged, _ := json.Marshal(`{"decision":"overridden","rule":"ssh","tool":"Bash","command":"forged"}`)
|
|
return []byte(`{"MESSAGE":` + string(msg) + `,"_EXE":"` + HelperPath() + `"}` + "\n" +
|
|
`{"MESSAGE":` + string(forged) + `,"_EXE":"/usr/bin/logger"}` + "\n"), nil
|
|
}
|
|
if got := ReadGuardLog(d.Log, 10); len(got) != 1 || got[0].Rule != "account" || got[0].Command != "a\nb" {
|
|
t.Fatalf("the record reads back from the journal: %+v", got)
|
|
}
|
|
if err := RecordGuard(JournalRecord+"x", GuardEntry{}); err != nil && !strings.Contains(err.Error(), "journal") {
|
|
t.Fatal(err)
|
|
}
|
|
journalSocket = filepath.Join(t.TempDir(), "absent")
|
|
if err := RecordGuard(d.Log, GuardEntry{}); err == nil {
|
|
t.Fatal("a journal that cannot be reached is an error, so an override is not honoured unrecorded")
|
|
}
|
|
}
|
|
|
|
// ---- an API key is taken only from the agent's own home ---------------------------------------------------
|
|
|
|
func TestAnAPIKeyFileOutsideTheAgentsHomeIsRefusedAndKept(t *testing.T) {
|
|
p, _ := node(t, "anchor")
|
|
asked := 0
|
|
ask := func(string, any) (json.RawMessage, error) { asked++; return json.RawMessage(`{}`), nil }
|
|
outside := filepath.Join(filepath.Dir(p.Home), "runtime-credential")
|
|
writeFile(t, outside, "sk-ant-api03-secret-0123456789abcdefghij\n")
|
|
inside := filepath.Join(p.Home, "not-a-key")
|
|
writeFile(t, inside, "-----BEGIN OPENSSH PRIVATE KEY-----\nsecret\n")
|
|
linked := filepath.Join(p.Home, "linked")
|
|
if err := os.Symlink(outside, linked); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, file := range []string{outside, p.Home + "/../runtime-credential", inside, linked, "relative", p.Home} {
|
|
_, err := AddAPIKey(p, "api", file, false, ask)
|
|
if err == nil || strings.Contains(err.Error(), "secret") {
|
|
t.Fatalf("%s: refused, without repeating what it holds; got %v", file, err)
|
|
}
|
|
}
|
|
for _, file := range []string{outside, inside} {
|
|
if _, err := os.Stat(file); err != nil {
|
|
t.Fatalf("%s was removed though it was refused", file)
|
|
}
|
|
}
|
|
if asked != 0 {
|
|
t.Fatal("nothing refused reaches the licence manager")
|
|
}
|
|
}
|
|
|
|
func TestTheGuardIsAskedAboutEveryToolAndRefusesEachForAnotherAccount(t *testing.T) {
|
|
if GuardMatcher != "*" {
|
|
t.Fatalf("the guard is asked about every tool, got %q", GuardMatcher)
|
|
}
|
|
g := Guard{Data: GuardData{Node: "anchor", Agent: "agent"}}
|
|
for _, tool := range []string{"Read", "Grep", "WebFetch", "mcp__mesh__mesh_call", "Task"} {
|
|
if v := g.JudgeAs("operator", HookInput{ToolName: tool}); !v.Refuse {
|
|
t.Fatalf("%s as another account is refused", tool)
|
|
}
|
|
if v := g.JudgeAs("agent", HookInput{ToolName: tool}); v.Refuse {
|
|
t.Fatalf("%s as the agent's account passes", tool)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAMoveWritesTheAccountFileBesideTheToken(t *testing.T) {
|
|
p, w := node(t, "anchor")
|
|
writeFile(t, p.binding(), `{"licence":"personal","kind":"subscription","generation":4}`)
|
|
ask := func(address string, args any) (json.RawMessage, error) {
|
|
key := args.(map[string]any)["public_key"].(string)
|
|
g, _ := json.Marshal(Grant{AccessToken: "at", ExpiresAt: now + 3_600_000})
|
|
box, _ := Seal(string(g), key)
|
|
return json.Marshal(Current{Licence: "personal", Kind: "subscription", Generation: 4, Sealed: &box,
|
|
Identity: &Identity{AccountUUID: "u-1", EmailAddress: "operator@example.org"}})
|
|
}
|
|
if _, err := OnBinding(p, &BindingState{Licence: "personal", Kind: "subscription", Generation: 4}, ask, writer(w)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if creds(t, p)["accessToken"] != "at" {
|
|
t.Fatal("the token is in the new home")
|
|
}
|
|
if id := ReadIdentity(p.account()); id == nil || id.AccountUUID != "u-1" {
|
|
t.Fatalf("the account file names the account whose token the new home holds, got %+v", id)
|
|
}
|
|
}
|
|
|
|
func TestAnAnswerPastTheBoundIsRefused(t *testing.T) {
|
|
long := strings.Repeat("x", 100) + "\n"
|
|
if _, err := readLine(bufio.NewReaderSize(strings.NewReader(long), 16), 50); err == nil {
|
|
t.Fatal("a line past the bound is an error")
|
|
}
|
|
if got, err := readLine(bufio.NewReaderSize(strings.NewReader(long), 16), 200); err != nil || string(got) != long {
|
|
t.Fatalf("a line within it is read whole across the buffer: %q %v", got, err)
|
|
}
|
|
}
|
|
|
|
// A FIFO, a device or a link to one in the agent's home is answered at once, an error: it never holds the
|
|
// helper, and every request behind it (review of 2026-10-09).
|
|
func TestTheHelperReadsOnlyARegularFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
fifo := filepath.Join(dir, ".credentials.json")
|
|
if err := syscall.Mkfifo(fifo, 0o600); err != nil {
|
|
t.Skip("no FIFO here:", err)
|
|
}
|
|
zero := filepath.Join(dir, "zero")
|
|
if err := os.Symlink("/dev/zero", zero); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, path := range []string{fifo, zero} {
|
|
done := make(chan Answer, 1)
|
|
go func() { done <- serveOne(Op{Op: "read", Path: path}) }()
|
|
select {
|
|
case a := <-done:
|
|
if a.Err == "" || len(a.Data) != 0 {
|
|
t.Fatalf("%s was read: %q", path, a.Data)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("reading %s held the helper", path)
|
|
}
|
|
}
|
|
big := filepath.Join(dir, "big")
|
|
if err := os.WriteFile(big, make([]byte, helperReadMax+1), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if a := serveOne(Op{Op: "read", Path: big}); a.Err == "" {
|
|
t.Fatal("a file past the bound was read")
|
|
}
|
|
}
|
|
|
|
// helperAnswering is a helper that answers every request with answer, or never when answer is empty.
|
|
func helperAnswering(account, answer string) *Helper {
|
|
return &Helper{Account: account, Start: func() (io.WriteCloser, io.Reader, func(), error) {
|
|
reqR, reqW := io.Pipe()
|
|
ansR, ansW := io.Pipe()
|
|
go func() {
|
|
r := bufio.NewReader(reqR)
|
|
for {
|
|
if _, err := r.ReadBytes('\n'); err != nil {
|
|
_ = ansW.Close()
|
|
return
|
|
}
|
|
if answer != "" {
|
|
_, _ = ansW.Write([]byte(answer + "\n"))
|
|
}
|
|
}
|
|
}()
|
|
return reqW, ansR, func() { _ = reqR.Close(); _ = ansW.Close() }, nil
|
|
}}
|
|
}
|
|
|
|
// What the helper answers is the agent's word: an entry that is not a name in the directory is refused, so no
|
|
// path joined from it leaves the home and is read as the runtime's account.
|
|
func TestAnEntryThatLeavesTheDirectoryIsRefused(t *testing.T) {
|
|
for _, name := range []string{"../operator/.ssh/id_ed25519", "..", ".", "", "a/b"} {
|
|
raw, _ := json.Marshal(Answer{Entries: []*Info{{Name: name}}})
|
|
f := &Files{Home: "/home/agent", Account: "agent", Helper: helperAnswering("agent", string(raw))}
|
|
if _, err := f.ReadDir("/home/agent/.claude"); err == nil {
|
|
t.Fatalf("the entry %q was taken", name)
|
|
}
|
|
}
|
|
raw, _ := json.Marshal(Answer{Entries: []*Info{{Name: "settings.json"}}})
|
|
f := &Files{Home: "/home/agent", Account: "agent", Helper: helperAnswering("agent", string(raw))}
|
|
if got, err := f.ReadDir("/home/agent/.claude"); err != nil || len(got) != 1 {
|
|
t.Fatalf("a name in the directory was refused: %v %v", got, err)
|
|
}
|
|
}
|
|
|
|
// A helper that does not answer fails the request in time, said, and is not asked again for it.
|
|
func TestAHelperThatDoesNotAnswerFailsTheRequestInTime(t *testing.T) {
|
|
was := helperAnswerWithin
|
|
helperAnswerWithin = 200 * time.Millisecond
|
|
defer func() { helperAnswerWithin = was }()
|
|
h := helperAnswering("agent", "")
|
|
start := time.Now()
|
|
_, err := h.Do(Op{Op: "read", Path: "/home/agent/.claude.json"})
|
|
if err == nil || !errors.Is(err, errHelperSlow) {
|
|
t.Fatalf("a silent helper answered: %v", err)
|
|
}
|
|
if time.Since(start) > 2*time.Second {
|
|
t.Fatalf("the request waited %s", time.Since(start))
|
|
}
|
|
}
|