Files
mesh-catalog/checks/paths/main.go
T
jochen 0d54ecc764
mesh/merge-gate pass: builds asus-zephyrus-g14, i3status-rust, power → ace, g14, novox, shanks; no bus step; every machine composes with the change as it d…
mesh/repo-check fail: 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 superseded: a newer head of the same pull request
Keep the value a watch hands over at once, watch a dead watch again, and let the path lists grow only in the open (hq ADR 0260)
The SDK hands the current value over before Watch returns, and the follower had no
entry for it yet, so the first value was dropped. A silent watch is now told from an
expired key and watched again rather than polled, each write takes its own temporary
file, drop-in directories are no module's, and an entry added to the path check's lists
is refused unless its line says why.
2026-10-08 16:12:37 +02:00

279 lines
9.0 KiB
Go

// Command paths holds the catalogue's modules to talking over the bus (novox/hq ADR 0260): no module
// names a path that another module's manifest places under /run, /var/lib, /usr/local/lib or /etc.
// Such a path is that module's state or configuration on its machine, and a second module reading or
// writing it is two modules talking through a file, which the bus exists to replace.
//
// A module owns every `path` of its resources under those roots. Every other module's manifest, and
// every file of its own but its documentation, its tests and its binaries, is searched for each as a
// whole path: followed by nothing, a slash, or a character no path takes. A path a container sees
// inside itself (the target of a `volumes` entry) is the container's, and is not searched.
//
// **A drop-in directory is no module's.** A directory whose name ends in `.d` (`/etc/sysctl.d`,
// `/etc/systemd/system/<unit>.d`, `/etc/X11/xorg.conf.d`) is a place several modules each put their own
// file, so naming it is not reaching into one module's state; the files in it are each their placer's.
//
// Two lists beside this file narrow it, each line a path or a finding and its reason after a `#`:
//
// - shared: paths every program on a machine reads as the machine's, not as a message from their
// owner — /etc/hosts, which every resolver reads.
// - known: the findings standing on the day the roots were widened (2026-10-08). The check fails on a
// finding not listed, and on a listed one that no longer occurs, so the list only shrinks.
//
// **Neither list may grow unseen.** With `-base <dir>` holding the base branch's copies of both, a line
// the change adds is refused unless its reason says `allow:` and why — so an addition is a decision a
// reviewer reads in the diff, not a way past the check.
//
// **What it cannot see:** a path built in code from pieces (`filepath.Join("/etc", name)`), one read
// from the environment or a setting, and one reached through a shell the module runs. It reads text,
// not what a program does; review and the runtime's grants cover the rest.
//
// go run . <catalogue root>
//
// Exits 1 on a finding.
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io/fs"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
// stateRoots are where a machine keeps a module's state and configuration.
var stateRoots = []string{"/run/", "/var/lib/", "/usr/local/lib/", "/etc/"}
// Finding is one module naming another's path.
type Finding struct {
Module, File, Path, Owner string
}
func (f Finding) String() string {
return fmt.Sprintf("%s names %s in %s, and the path is %s's; modules talk over the bus (novox/hq ADR 0260)",
f.Module, f.Path, f.File, f.Owner)
}
// Key is how a finding is listed in `known`: the module, the path and the file.
func (f Finding) Key() string { return f.Module + " " + f.Path + " " + f.File }
// containerTargets drops what a container sees inside itself from a manifest before it is searched:
// the target half of every `volumes` entry of a container resource.
func containerTargets(m map[string]any) {
resources, _ := m["resources"].([]any)
for _, raw := range resources {
r, ok := raw.(map[string]any)
if !ok || r["type"] != "container" {
continue
}
volumes, _ := r["volumes"].([]any)
for i, v := range volumes {
if s, ok := v.(string); ok {
if src, _, two := strings.Cut(s, ":"); two {
volumes[i] = src
}
}
if o, ok := v.(map[string]any); ok {
delete(o, "target")
delete(o, "container")
}
}
}
}
// Check is every finding in the catalogue at root, before the lists narrow it.
func Check(root string) ([]Finding, error) {
files, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return nil, err
}
owner := map[string]string{}
modules := map[string]string{}
for _, f := range files {
raw, err := os.ReadFile(f)
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", f, err)
}
modules[m.Module] = filepath.Dir(f)
for _, r := range m.Resources {
p, _ := r["path"].(string)
if r["type"] == "directory" && strings.HasSuffix(filepath.Base(p), ".d") {
continue // a drop-in directory: several modules' place, no one module's
}
for _, root := range stateRoots {
if strings.HasPrefix(p, root) && len(p) > len(root) {
owner[strings.TrimRight(p, "/")] = m.Module
}
}
}
}
paths := make([]string, 0, len(owner))
for p := range owner {
paths = append(paths, p)
}
sort.Strings(paths)
patterns := map[string]*regexp.Regexp{}
for _, p := range paths {
patterns[p] = regexp.MustCompile(regexp.QuoteMeta(p) + `(?:$|[^A-Za-z0-9._-])`)
}
names := make([]string, 0, len(modules))
for n := range modules {
names = append(names, n)
}
sort.Strings(names)
var out []Finding
for _, module := range names {
err := filepath.WalkDir(modules[module], func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
name := d.Name()
if strings.HasSuffix(name, ".md") || strings.HasSuffix(name, "_test.go") ||
name == "go.sum" || name == "package-lock.json" {
return nil
}
raw, err := os.ReadFile(path)
if err != nil {
return err
}
if bytes.IndexByte(raw, 0) >= 0 {
return nil // a binary: what it says is in its source
}
if name == "module.json" {
var m map[string]any
if json.Unmarshal(raw, &m) == nil {
containerTargets(m)
raw, _ = json.Marshal(m)
}
}
text := string(raw)
for _, p := range paths {
if owner[p] != module && patterns[p].MatchString(text) {
rel, _ := filepath.Rel(root, path)
out = append(out, Finding{Module: module, File: rel, Path: p, Owner: owner[p]})
}
}
return nil
})
if err != nil {
return nil, err
}
}
return out, nil
}
// list reads one of the lists beside this check: each line's entry before a `#`, trimmed, with its
// reason after it.
func list(path string) (map[string]bool, map[string]string, error) {
out, why := map[string]bool{}, map[string]string{}
f, err := os.Open(path)
if err != nil {
return nil, nil, err
}
defer f.Close()
s := bufio.NewScanner(f)
for s.Scan() {
line, reason, _ := strings.Cut(s.Text(), "#")
if line = strings.TrimSpace(line); line != "" {
out[line] = true
why[line] = strings.TrimSpace(reason)
}
}
return out, why, s.Err()
}
// Grown is every entry a list has that its base copy does not, and whose reason does not say `allow:`.
func Grown(name string, now map[string]bool, why map[string]string, base map[string]bool) []string {
var out []string
for entry := range now {
if !base[entry] && !strings.Contains(why[entry], "allow:") {
out = append(out, fmt.Sprintf("checks/paths/%s gains %q, which the base branch's copy does not hold; "+
"say why after `# allow:` on its line, or keep it out", name, entry))
}
}
sort.Strings(out)
return out
}
// Judge narrows the findings by the lists: what is left to refuse, and the known findings that no
// longer occur, which must leave the list.
func Judge(found []Finding, shared, known map[string]bool) (refused []Finding, gone []string) {
seen := map[string]bool{}
for _, f := range found {
if shared[f.Path] {
continue
}
seen[f.Key()] = true
if !known[f.Key()] {
refused = append(refused, f)
}
}
for k := range known {
if !seen[k] {
gone = append(gone, k)
}
}
sort.Strings(gone)
return refused, gone
}
func main() {
root, baseDir := ".", ""
args := os.Args[1:]
if len(args) >= 2 && args[0] == "-base" {
baseDir, args = args[1], args[2:]
}
if len(args) > 0 {
root = args[0]
}
found, err := Check(root)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
here := filepath.Join(root, "checks", "paths")
shared, sharedWhy, err1 := list(filepath.Join(here, "shared"))
known, knownWhy, err2 := list(filepath.Join(here, "known"))
if err1 != nil || err2 != nil {
fmt.Fprintln(os.Stderr, "the lists beside the check cannot be read:", err1, err2)
os.Exit(2)
}
var grown []string
if baseDir == "" {
fmt.Println("NOT COMPARED: no base branch's lists were given, so additions to them were not judged")
} else {
baseShared, _, err1 := list(filepath.Join(baseDir, "shared"))
baseKnown, _, err2 := list(filepath.Join(baseDir, "known"))
if err1 != nil || err2 != nil {
fmt.Fprintln(os.Stderr, "the base branch's lists cannot be read:", err1, err2)
os.Exit(2)
}
grown = append(Grown("shared", shared, sharedWhy, baseShared), Grown("known", known, knownWhy, baseKnown)...)
}
refused, gone := Judge(found, shared, known)
for _, g := range grown {
fmt.Println(g)
}
for _, f := range refused {
fmt.Println(f)
}
for _, k := range gone {
fmt.Printf("%q is in checks/paths/known and no longer occurs: take it out, so it cannot come back\n", k)
}
if len(refused) > 0 || len(gone) > 0 || len(grown) > 0 {
os.Exit(1)
}
fmt.Printf("paths: no module names another's state on its machine (%d known, listed to go)\n", len(known))
}