package apply import ( "path/filepath" "strings" "github.com/novox/mesh-host/internal/declaration" ) // rootPaths are the files and directories through which an account comes to root, or by which it is told who it // is: the user and group databases, sudo's, doas's and polkit's rules. A file written there changes what the // judge of an agent account reads (novox/hq ADR 0266). var rootPaths = []string{"/etc/passwd", "/etc/shadow", "/etc/group", "/etc/gshadow", "/etc/sudoers", "/etc/sudoers.d", "/etc/doas.conf", "/etc/doas.d", "/etc/polkit-1", "/usr/share/polkit-1"} // MovesRoot says an apply changed something through which root's reach could have changed — an account, a // package, an action run as root, or a file in rootPaths (novox/hq issue 361). The engine then asks for a new // search for setuid programs: root acted, and a complete search is sound only until root acts. Outcomes that // moved nothing (unchanged, held, waiting, held still, skipped, kept, forgotten) do not count. func MovesRoot(outcomes []Outcome) bool { for _, o := range outcomes { // Only what moved the machine: skipped, kept and forgotten touch nothing, and counting them made one // broken module start a search on every reconcile (the review of 2026-10-10). switch o.Action { case "created", "updated", "corrected", "removed": default: continue } switch declaration.Type(o.Type) { case declaration.TypeUser, declaration.TypePackage, declaration.TypeAction: return true case declaration.TypeFile, declaration.TypeDirectory, declaration.TypeArchive: if underRootPath(o.Target) { return true } } } return false } func underRootPath(target string) bool { t := filepath.Clean(target) for _, p := range rootPaths { if t == p || strings.HasPrefix(t, p+"/") { return true } } return false }