Close the review's gaps round the agent account (hq ADR 0266)
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.
This commit is contained in:
jochen
2026-10-09 23:48:15 +02:00
parent b382045721
commit b240085e0a
11 changed files with 295 additions and 33 deletions
@@ -17,7 +17,9 @@ import (
"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
@@ -198,6 +200,10 @@ func TestTheAgentsAccountAndHomeComeFromTheRuntime(t *testing.T) {
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)
@@ -255,8 +261,33 @@ func TestTheTokenTheModulePlacedInTheOperatorsHomeGoes(t *testing.T) {
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")
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())
}
}
@@ -453,3 +484,90 @@ func TestAnAnswerPastTheBoundIsRefused(t *testing.T) {
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))
}
}