// Command agenthome refuses, in every module's manifest, a resource placed under the agent account's home // that the node-engine cannot write without a gap (novox/hq ADR 0266). // // The agent account owns its home and everything in it, so it can put a symbolic or hard link anywhere // there. The node-engine, as root, writes a whole file or a directory below a home through descriptors it // opened itself and refuses a link it finds; it unpacks an archive, and writes into an existing file (`into`: // json or block), only after checking the path, so a link swapped in between the check and the write would // point root's write at any file. No module does either today. This check keeps it so: a resource of type // `archive`, or a `file` with `into`, whose path is under ${machine:agent-home} — written so, reached through // one of the module's own directories (${dir:}), or written as a literal /home path — is refused. The // node-engine refuses the same below the home of any account declared never to become root, whatever the path. // // go run . // // Exits 1 on a finding. package main import ( "encoding/json" "fmt" "os" "path/filepath" "regexp" "sort" "strings" ) // AgentHome is the machine fact naming the agent account's home. const AgentHome = "${machine:agent-home}" // ofDir is where a manifest names one of its own directories: ${dir:}. var ofDir = regexp.MustCompile(`\$\{dir:([a-z0-9][a-z0-9_-]*)\}`) // resolve replaces each ${dir:} with that resource's own path, as far as the manifest says it, so a path // reaching the agent's home through one of the module's directories is seen for what it is. func resolve(p string, dirs map[string]string) string { for i := 0; i < 8 && ofDir.MatchString(p); i++ { p = ofDir.ReplaceAllStringFunc(p, func(word string) string { if to, ok := dirs[ofDir.FindStringSubmatch(word)[1]]; ok { return to } return word }) } return p } // underAHome says whether a resolved path lies under the agent's home: by its machine fact, or written as a // literal home, which no definition may carry anyway (ADR 0112) and which may well be the agent's. func underAHome(p string) bool { // Cleaned first, so //home/x or /./home/x is seen as the /home/x it is. if p != "" { p = filepath.Clean(p) } return strings.HasPrefix(p, AgentHome) || strings.HasPrefix(p, "/home/") || p == "/home" } // Finding is one resource refused. type Finding struct { Module, ID, Why string } // Check reads every manifest under root/modules and answers what it refuses. func Check(root string) ([]Finding, error) { manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json")) if err != nil { return nil, err } sort.Strings(manifests) var out []Finding for _, path := range manifests { raw, err := os.ReadFile(path) if err != nil { return nil, err } var m struct { Module string `json:"module"` Resources []map[string]any `json:"resources"` } if err := json.Unmarshal(raw, &m); err != nil { return nil, fmt.Errorf("%s: %w", path, err) } dirs := map[string]string{} for _, r := range m.Resources { id, _ := r["id"].(string) if p, _ := r["path"].(string); id != "" && p != "" { dirs[id] = p } } for _, r := range m.Resources { p, _ := r["path"].(string) if !underAHome(resolve(p, dirs)) { continue } id, _ := r["id"].(string) kind, _ := r["type"].(string) into, _ := r["into"].(string) switch { case kind == "archive": out = append(out, Finding{m.Module, id, "an archive unpacked under the agent's home"}) case kind == "file" && into != "": out = append(out, Finding{m.Module, id, "a file written into (" + into + ") under the agent's home"}) } } } return out, nil } func main() { root := "." if len(os.Args) > 1 { root = os.Args[1] } found, err := Check(root) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } for _, f := range found { fmt.Printf("%s: %s is %s: the node-engine writes it as root after checking the path, and the agent "+ "account could swap a link in between (novox/hq ADR 0266); place a whole file or a directory instead\n", f.Module, f.ID, f.Why) } if len(found) > 0 { os.Exit(1) } fmt.Println("agent-home: no archive and no file written into under the agent's home") }