Files
mesh-catalog/modules/claude-code/cmd/claude-code/agent_account_test.go
T
jschoubben 31ce47d1cc C-3: a node takes its licence token from the vault (issue 1409)
The agent module asks the seat's current which licence it holds and its key,
then the vault's get for that licence's access secret sealed to its own key:
at its start, on a binding.changed with a newer generation than it applied,
and on the vault's secret-changed for the licence it holds. The applied
binding keeps the licence's key, so the vault's event is matched by the
carried key, never one formed here.

Builds: design 39 bus-and-switch §19, §18, B7, B9, B11

Claude-Session: https://claude.ai/code/session_01NcbgfHb9d4T9nq5Q6WqdiJ
2026-11-11 23:51:00 +01:00

570 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}`)
var asked []string
ask := vaulted(t, Current{Licence: "personal", Key: "personal", Kind: "subscription", Generation: 4,
Identity: &Identity{AccountUUID: "u-1", EmailAddress: "operator@example.org"}}, "at", &asked)
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(30 * 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) > 10*time.Second {
t.Fatalf("the request waited %s", time.Since(start))
}
}