mesh/merge-gate checking again: the gate refused `unknown` because the controller judged with a node-engine validator pinned before mesh-host #117; mesh-co…
mesh/repo-check checking again: the gate refused `unknown` because the controller judged with a node-engine validator pinned before mesh-host #117; mesh-co…
mesh/delivery-group group feat/owned-paths checking: 0 of 2 member(s) ready
mesh/delivery superseded: a newer head of the same pull request
Every directory and archive resource carries `unknown`: move-out for a home folder whose program writes nothing there (the node-engine, delivered in mesh-host #117, moves the rest to ~/.local/state/mesh-moved/<module>/ with a list), module for ~/.ssh and ~/.claude, program elsewhere; `knows` keeps what a program or the operator relies on (powerlevel10k's compiled files, i3's scripts and the asus/g14 drop-ins, dunst's slack rule, betterbird's records, node-env's forge credential, issue 1033). Two shared folders are no longer declared. Each module's README says it. checks/owned holds every manifest to it; checks/sshdir stays while main's merge check runs it (issue 1035). Tests that pin a manifest follow the new key only. Reviewed in three rounds.
237 lines
6.4 KiB
Go
237 lines
6.4 KiB
Go
// Command owned judges how the catalogue's directories and archives treat files they did not write (novox/hq
|
|
// ADR 0422 §8, issue 1024). It refuses, naming the module, the resource and the reason:
|
|
//
|
|
// 1. a directory or archive without `unknown`, or with a value but move-out, module or program;
|
|
//
|
|
// 2. `move-out` on a path not under ${machine:account-home} or ${machine:agent-home};
|
|
//
|
|
// 3. `knows` with an empty, absolute, `.`/`..`/empty-component or repeated path;
|
|
//
|
|
// 4. a path one module declares (a resource's path, or a fact with "home": true) lying inside a directory of
|
|
// another module, after ${dir:<id>} is resolved within each module and both homes are read as one root.
|
|
// Archives are not containers for this rule. This holds for ~/.ssh what checks/sshdir held (ADR 0415 §6).
|
|
//
|
|
// go run . <catalogue root>
|
|
//
|
|
// Exits 1 on a finding.
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Home is the one root both machine homes are read as.
|
|
const Home = "${home}"
|
|
|
|
var (
|
|
ofDir = regexp.MustCompile(`\$\{dir:([a-z0-9][a-z0-9_-]*)\}`)
|
|
homeVar = strings.NewReplacer("${machine:account-home}", Home, "${machine:agent-home}", Home)
|
|
values = map[string]bool{"move-out": true, "module": true, "program": true}
|
|
)
|
|
|
|
// Finding is one refusal.
|
|
type Finding struct {
|
|
Rule int
|
|
Module, ID, Why string
|
|
}
|
|
|
|
type resource struct {
|
|
ID, Type, Path, Unknown string
|
|
Knows []string
|
|
HasKnows bool
|
|
}
|
|
|
|
type parsed struct {
|
|
Module string
|
|
Resources []resource
|
|
Facts []string // paths of facts with "home": true, under the home
|
|
}
|
|
|
|
func load(file string) (*parsed, error) {
|
|
raw, err := os.ReadFile(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var m struct {
|
|
Module string `json:"module"`
|
|
Resources []struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
Path string `json:"path"`
|
|
Unknown *string `json:"unknown"`
|
|
Knows []string `json:"knows"`
|
|
} `json:"resources"`
|
|
Facts map[string]struct {
|
|
Path string `json:"path"`
|
|
Home bool `json:"home"`
|
|
} `json:"facts"`
|
|
}
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
return nil, fmt.Errorf("%s: %w", file, err)
|
|
}
|
|
out := &parsed{Module: m.Module}
|
|
if out.Module == "" {
|
|
out.Module = filepath.Base(filepath.Dir(file))
|
|
}
|
|
for _, r := range m.Resources {
|
|
x := resource{ID: r.ID, Type: r.Type, Path: r.Path, Knows: r.Knows, HasKnows: r.Knows != nil}
|
|
if r.Unknown != nil {
|
|
x.Unknown = *r.Unknown
|
|
if x.Unknown == "" {
|
|
x.Unknown = "(empty)"
|
|
}
|
|
}
|
|
out.Resources = append(out.Resources, x)
|
|
}
|
|
keys := make([]string, 0, len(m.Facts))
|
|
for k := range m.Facts {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, k := range keys {
|
|
if f := m.Facts[k]; f.Home && f.Path != "" {
|
|
out.Facts = append(out.Facts, f.Path)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// norm resolves ${dir:<id>} from the module's own resources, reads both homes as one root and cleans the path.
|
|
func norm(p string, dirs map[string]string) string {
|
|
for i := 0; i < 8 && ofDir.MatchString(p); i++ {
|
|
p = ofDir.ReplaceAllStringFunc(p, func(w string) string {
|
|
if to, ok := dirs[ofDir.FindStringSubmatch(w)[1]]; ok {
|
|
return to
|
|
}
|
|
return w
|
|
})
|
|
}
|
|
p = homeVar.Replace(p)
|
|
if p == "" {
|
|
return ""
|
|
}
|
|
return path.Clean(p)
|
|
}
|
|
|
|
func underHome(p string) bool { return strings.HasPrefix(p, Home+"/") }
|
|
|
|
// badKnows says why a knows entry is refused, or "".
|
|
func badKnows(k string) string {
|
|
if k == "" {
|
|
return "is empty"
|
|
}
|
|
if strings.HasPrefix(k, "/") {
|
|
return "is absolute"
|
|
}
|
|
for _, c := range strings.Split(strings.TrimSuffix(k, "/"), "/") {
|
|
if c == "" || c == "." || c == ".." {
|
|
return "has an empty, . or .. component"
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
type declared struct {
|
|
module, id, path string
|
|
}
|
|
|
|
// Check reads every manifest under root/modules and answers what it refuses.
|
|
func Check(root string) ([]Finding, error) {
|
|
files, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sort.Strings(files)
|
|
var out []Finding
|
|
var paths []declared
|
|
type container struct{ module, path string }
|
|
var containers []container
|
|
for _, file := range files {
|
|
m, err := load(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dirs := map[string]string{}
|
|
for _, r := range m.Resources {
|
|
if r.ID != "" && r.Path != "" {
|
|
dirs[r.ID] = r.Path
|
|
}
|
|
}
|
|
for _, r := range m.Resources {
|
|
p := norm(r.Path, dirs)
|
|
if p != "" {
|
|
paths = append(paths, declared{m.Module, r.ID, p})
|
|
}
|
|
if r.Type != "directory" && r.Type != "archive" {
|
|
continue
|
|
}
|
|
if r.Type == "directory" && p != "" {
|
|
containers = append(containers, container{m.Module, p})
|
|
}
|
|
add := func(rule int, why string) { out = append(out, Finding{rule, m.Module, r.ID, why}) }
|
|
switch {
|
|
case r.Unknown == "":
|
|
add(1, "a "+r.Type+" without `unknown` (move-out, module or program)")
|
|
case !values[r.Unknown]:
|
|
add(1, fmt.Sprintf("`unknown` is %q, not move-out, module or program", r.Unknown))
|
|
case r.Unknown == "move-out" && !underHome(p):
|
|
add(2, fmt.Sprintf("`move-out` on %q, which is not under ${machine:account-home} or ${machine:agent-home}", r.Path))
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, k := range r.Knows {
|
|
if why := badKnows(k); why != "" {
|
|
add(3, fmt.Sprintf("`knows` entry %q %s", k, why))
|
|
continue
|
|
}
|
|
c := strings.TrimSuffix(k, "/")
|
|
if seen[c] {
|
|
add(3, fmt.Sprintf("`knows` entry %q is repeated", k))
|
|
}
|
|
seen[c] = true
|
|
}
|
|
if r.HasKnows && len(r.Knows) == 0 {
|
|
add(3, "`knows` is empty")
|
|
}
|
|
}
|
|
for _, f := range m.Facts {
|
|
if p := norm(Home+"/"+f, nil); p != "" {
|
|
paths = append(paths, declared{m.Module, "fact " + f, p})
|
|
}
|
|
}
|
|
}
|
|
for _, d := range paths {
|
|
for _, c := range containers {
|
|
if c.module != d.module && (d.path == c.path || strings.HasPrefix(d.path, c.path+"/")) {
|
|
out = append(out, Finding{4, d.module, d.id, fmt.Sprintf("declares %q, inside a directory of %s (%q)", d.path, c.module, c.path)})
|
|
}
|
|
}
|
|
}
|
|
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: %s (rule %d, novox/hq ADR 0422 §8, issue 1024)\n", f.Module, f.ID, f.Why, f.Rule)
|
|
}
|
|
if len(found) > 0 {
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println("owned paths: every directory and archive says how it treats unknown files; no module declares inside another's directory")
|
|
}
|