claude-code: write the agent's home as its own account where it has one

On the control node agents are to run as an account that cannot become root
(hq ADR 0266). The runtime keeps running as the operator, so the token, the
identity and the home-scope items go to the agent's home through a helper
started as that account; the licence and the account file follow the move,
what was placed in the operator's home goes once the agent holds the licence,
and the guard refuses every tool of a session running as anyone else,
recording to the journal. An API key is taken only from the agent's home and
only when it is one, here and in the licence manager, so the tool cannot carry
off the runtime's credential. Unchanged where no agent account is named.
This commit is contained in:
jochen
2026-10-09 10:14:18 +02:00
parent 7339ce8556
commit b57e6e8144
17 changed files with 1467 additions and 102 deletions
+30 -23
View File
@@ -419,11 +419,11 @@ func (v *ConfigView) Items() map[string]Item {
// an older snapshot never lands after a newer one.
func (v *ConfigView) writeThroughLocked() bool {
now, _ := indented(v.items)
before, _ := os.ReadFile(v.p.config())
before, _ := fsys.ReadFile(v.p.config())
if string(before) == string(now) {
return false
}
_ = os.WriteFile(v.p.config(), now, 0o600)
_ = fsys.WriteFile(v.p.config(), now, 0o600)
return true
}
@@ -479,8 +479,11 @@ type GuardFiles struct {
Data GuardData
}
// GuardMatcher is what the guard is asked about: the shell, and the agent's own file edits.
const GuardMatcher = "Bash|Edit|Write|MultiEdit|NotebookEdit"
// GuardMatcher is what the guard is asked about: every tool. Its own rules judge the shell and the agent's file
// edits and let every other tool through; the rule of the account agents run as (novox/hq ADR 0266) refuses
// every tool of a session running as another, reading and the mesh's tools included, so that session can do
// nothing at all.
const GuardMatcher = "*"
// GuardCommand is the hook's command line: by its path under the managed directory, root's — never the
// copy the agent may keep of a plugin, which the session could change.
@@ -673,9 +676,13 @@ const deletedByHand = "deleted-by-hand"
// placed that the person deleted stays deleted until the item is unregistered. Answers what it did and what
// it left alone, and why.
func PlaceHome(p Paths, want map[string]string) (done []string, left []string) {
dir := filepath.Join(p.Home, ".claude")
return placeHomeIn(filepath.Join(p.Home, ".claude"), p.placed(), want)
}
// placeHomeIn is PlaceHome for one agent directory and its own record of what was placed there.
func placeHomeIn(dir, record string, want map[string]string) (done []string, left []string) {
var placed Placed
if !readJSON(p.placed(), &placed) {
if !readJSON(record, &placed) {
placed = Placed{}
}
paths := make([]string, 0, len(want))
@@ -690,7 +697,7 @@ func PlaceHome(p Paths, want map[string]string) (done []string, left []string) {
left = append(left, rel+": "+why+", left alone")
continue
}
current, err := os.ReadFile(full)
current, err := fsys.ReadFile(full)
exists := err == nil
ours, wasPlaced := placed[rel]
switch {
@@ -712,7 +719,7 @@ func PlaceHome(p Paths, want map[string]string) (done []string, left []string) {
left = append(left, rel+": changed by hand since it was placed, left alone")
continue
}
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
if err := fsys.MkdirAll(filepath.Dir(full), 0o755); err != nil {
left = append(left, rel+": "+err.Error())
continue
}
@@ -720,7 +727,7 @@ func PlaceHome(p Paths, want map[string]string) (done []string, left []string) {
if isScript(rel, content) {
mode = 0o755
}
if err := os.WriteFile(full, []byte(content), mode); err != nil {
if err := fsys.WriteFile(full, []byte(content), mode); err != nil {
left = append(left, rel+": "+err.Error())
continue
}
@@ -740,13 +747,13 @@ func PlaceHome(p Paths, want map[string]string) (done []string, left []string) {
left = append(left, rel+": no longer registered, but "+why+", left alone")
continue
}
current, err := os.ReadFile(full)
current, err := fsys.ReadFile(full)
if err == nil && digest(string(current)) != ours {
left = append(left, rel+": no longer registered, but changed by hand, left alone")
delete(placed, rel)
continue
}
if err := os.Remove(full); err != nil && !os.IsNotExist(err) {
if err := fsys.Remove(full); err != nil && !os.IsNotExist(err) {
left = append(left, rel+": no longer registered, and could not be removed: "+err.Error())
continue
}
@@ -756,7 +763,7 @@ func PlaceHome(p Paths, want map[string]string) (done []string, left []string) {
done = append(done, rel+": removed")
}
raw, _ := indented(placed)
if err := os.WriteFile(p.placed(), raw, 0o600); err != nil {
if err := fsys.WriteFile(record, raw, 0o600); err != nil {
left = append(left, "the record of what was placed could not be saved: "+err.Error())
}
return done, left
@@ -769,7 +776,7 @@ func linkedParent(dir, rel string) string {
at := dir
for _, seg := range parts[:len(parts)-1] {
at = filepath.Join(at, seg)
if info, err := os.Lstat(at); err == nil && info.Mode()&os.ModeSymlink != 0 {
if info, err := fsys.Lstat(at); err == nil && info.Mode()&os.ModeSymlink != 0 {
return at + " is a symbolic link"
}
}
@@ -779,7 +786,7 @@ func linkedParent(dir, rel string) string {
// removeEmptyParents removes empty directories from dir up to, not including, root.
func removeEmptyParents(root, dir string) {
for dir != root && strings.HasPrefix(dir, root+string(filepath.Separator)) {
if err := os.Remove(dir); err != nil {
if err := fsys.Remove(dir); err != nil {
return
}
dir = filepath.Dir(dir)
@@ -795,7 +802,7 @@ func HomeConflict(p Paths, it Item) string {
if _, ours := placed[rel]; ours {
continue
}
if _, err := os.Stat(filepath.Join(p.Home, ".claude", filepath.FromSlash(rel))); err == nil {
if _, err := fsys.Stat(filepath.Join(p.Home, ".claude", filepath.FromSlash(rel))); err == nil {
return fmt.Sprintf("%s already exists in this home and the mesh did not place it; pick another name, or import it", rel)
}
}
@@ -843,12 +850,12 @@ func HomeItems(p Paths, c Config, loaded Servers) []HomeItem {
}
out = append(out, h)
}
if entries, err := os.ReadDir(filepath.Join(dir, "skills")); err == nil {
if entries, err := fsys.ReadDir(filepath.Join(dir, "skills")); err == nil {
for _, e := range entries {
if !e.IsDir() {
continue
}
body, err := os.ReadFile(filepath.Join(dir, "skills", e.Name(), "SKILL.md"))
body, err := fsys.ReadFile(filepath.Join(dir, "skills", e.Name(), "SKILL.md"))
if err != nil {
continue // the vendor's own synced folders carry none at their top
}
@@ -856,7 +863,7 @@ func HomeItems(p Paths, c Config, loaded Servers) []HomeItem {
}
}
for kind, sub := range map[string]string{KindAgent: "agents", KindCommand: "commands", KindOutputStyle: "output-styles", KindInstructions: "rules"} {
entries, err := os.ReadDir(filepath.Join(dir, sub))
entries, err := fsys.ReadDir(filepath.Join(dir, sub))
if err != nil {
continue
}
@@ -864,11 +871,11 @@ func HomeItems(p Paths, c Config, loaded Servers) []HomeItem {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") {
continue
}
body, _ := os.ReadFile(filepath.Join(dir, sub, e.Name()))
body, _ := fsys.ReadFile(filepath.Join(dir, sub, e.Name()))
add(kind, strings.TrimSuffix(e.Name(), ".md"), sub+"/"+e.Name(), string(body))
}
}
if body, err := os.ReadFile(filepath.Join(dir, "CLAUDE.md")); err == nil {
if body, err := fsys.ReadFile(filepath.Join(dir, "CLAUDE.md")); err == nil {
add(KindMemory, memoryName, "CLAUDE.md", string(body))
}
sort.Slice(out, func(i, j int) bool {
@@ -887,12 +894,12 @@ func ImportFromHome(p Paths, kind, name string) (Item, error) {
switch kind {
case KindSkill:
root := filepath.Join(dir, "skills", name)
err := filepath.WalkDir(root, func(full string, d os.DirEntry, err error) error {
err := fsys.WalkDir(root, func(full string, d os.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
rel, _ := filepath.Rel(root, full)
raw, err := os.ReadFile(full)
raw, err := fsys.ReadFile(full)
if err != nil {
return err
}
@@ -904,7 +911,7 @@ func ImportFromHome(p Paths, kind, name string) (Item, error) {
}
case KindAgent, KindCommand, KindOutputStyle, KindInstructions:
sub := map[string]string{KindAgent: "agents", KindCommand: "commands", KindOutputStyle: "output-styles", KindInstructions: "rules"}[kind]
raw, err := os.ReadFile(filepath.Join(dir, sub, name+".md"))
raw, err := fsys.ReadFile(filepath.Join(dir, sub, name+".md"))
if err != nil {
return Item{}, fmt.Errorf("the %s %s in this home: %w", kind, name, err)
}