Judge every path the controller resolves for a directory or file where the plan is composed, so the merge gate refuses a directory in Docker's data (issue 496)
mesh/merge-gate pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without …
mesh/repo-check fail: its merge-check.sh failed: --- FAIL: TestTheResolverAndWhatAsksItComposeOnOneMachine (0.02s)
mesh/delivery superseded: a newer head of the same pull request

This commit is contained in:
2026-10-11 18:19:22 +02:00
parent fe2e5e91b5
commit 174c8b5d53
3 changed files with 192 additions and 1 deletions
+79
View File
@@ -92,6 +92,81 @@ func systemPath(path string) string {
return ""
}
// Where no directory or file the controller resolves for a module may be, from any route (novox/hq issue 496).
//
// mesh-catalog #205 gave docker's `state` directory `"place": "."`, which resolves to <root>/docker: /var/lib/docker,
// every container's filesystem. systemPath judged only the `places` and `accesses` settings, so the default layout
// and a definition's own paths reached the merge gate unjudged; it composed every machine and passed, and only the
// node-engine refused the directory, at apply, failing the walk. So every path a directory or file resolves to is
// judged where the plan is composed, which is also where the gate composes.
//
// Not by systemPath's whole list: a module writes the machine's configuration in /etc and /usr by design (units,
// jails, a launcher), keeps run directories in /run, an account's keys in its .ssh, and the mesh places its own files
// for every module in /var/lib/mesh. Those are the node-engine's to judge with what only the machine knows (owners,
// links, where the runtimes really keep their data). Refused here is what no module ever writes, whoever owns it:
// the trees in neverWritten, the node-engine's own trees but by its own module, and a path that is one of the
// machine's roots or holds one. Each is refused by the node-engine too (its placement guard, issue 339), so nothing
// that applies today is refused here.
var (
neverWritten = []string{"/boot", "/root", "/proc", "/sys", "/dev", "/bin", "/sbin", "/lib", "/lib32", "/lib64",
"/var/spool", "/var/lib/docker", "/var/lib/containers", "/var/lib/containerd", "/opt"}
engineTrees = []string{"/var/lib/mesh-host", "/usr/lib/nox-mesh-host"}
// engineModule is the node-engine's own module, the one that places in engineTrees.
engineModule = "mesh-host"
// usrRoots are the roots below /usr that hold every program and library: never a module's directory itself.
usrRoots = []string{"/usr/bin", "/usr/lib", "/usr/lib64", "/usr/local", "/usr/local/bin", "/usr/local/lib",
"/usr/local/sbin", "/usr/sbin", "/usr/share"}
)
// resolvedSystemPath says why a path resolved for one of module's directories or files is the machine's own, or "".
func resolvedSystemPath(path, module string) string {
if !strings.HasPrefix(path, "/") {
return ""
}
path = filepath.Clean(path)
in := func(tree string) bool { return path == tree || strings.HasPrefix(path, tree+"/") }
for _, tree := range neverWritten {
if in(tree) {
return path + " is in " + tree + ", which no module writes in"
}
}
if module != engineModule {
for _, tree := range engineTrees {
if in(tree) {
return path + " is in " + tree + ", the node-engine's own, which only its module places in"
}
}
}
for _, roots := range [][]string{systemRoots, systemTrees, usrRoots} {
for _, root := range roots {
if path == root {
return path + " is one of the machine's roots, the parent of everything in it"
}
}
}
for _, tree := range append(append([]string(nil), systemTrees...), neverWritten...) {
if strings.HasPrefix(tree, path+"/") {
return path + " holds " + tree + ", the machine's own or the mesh's state"
}
}
return ""
}
// judgeResolved refuses a directory or file whose resolved path resolvedSystemPath refuses (novox/hq issue 496).
func judgeResolved(resource map[string]any, module string) error {
kind := fmt.Sprint(resource["type"])
if kind != "directory" && kind != "file" {
return nil
}
path, _ := resource["path"].(string)
if why := resolvedSystemPath(path, module); why != "" {
return fmt.Errorf("%s's %s %q resolves to %s: %s. The node-engine refuses it at apply, so it is refused "+
"where the plan is composed, and the merge gate with it (novox/hq issue 496)",
module, kind, fmt.Sprint(resource["id"]), filepath.Clean(path), why)
}
return nil
}
// accessRef is how a module names one of its accesses: ${access:<id>}.
var accessRef = regexp.MustCompile(`\$\{access:([a-z0-9][a-z0-9-]*)\}`)
@@ -284,6 +359,10 @@ func accessInto(resource map[string]any, accesses map[string]string, module stri
if resource["path"], err = fill(path); err != nil {
return err
}
// Judged again once the access is in it: dirInto judged it while it still named one (novox/hq issue 496).
if err := judgeResolved(resource, module); err != nil {
return err
}
}
if content, ok := resource["content"].(string); ok {
if resource["content"], err = fill(content); err != nil {