The tool runner's unit carries the paths modules own, for the holder of node-files. The engine's own archive is unpacked into a place named for its build, so every engine replacement changed that word and restarted the tool runner with nothing of it changed (novox 21:16, 22:25, 22:38 on 2026-11-09). A resource unpacked into a place named for its build now owns the directory holding every build. The restart line now names the unit's directive, word and key that differ, never a value. Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
141 lines
5.4 KiB
Go
141 lines
5.4 KiB
Go
package resolution
|
|
|
|
import (
|
|
"fmt"
|
|
"path"
|
|
"strings"
|
|
|
|
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// declaredPaths is every machine path a module declares it owns: a resource's literal path, and a file
|
|
// the mesh renders for it outside the operator's home. An access is not here — reading is not owning.
|
|
//
|
|
// **Cleaned, because the machine writes the cleaned path**: /etc//hosts, /etc/./hosts, /etc/../etc/hosts and
|
|
// /etc/hosts/ are all /etc/hosts to it, and a seat's path is clean by its own rule, so a declaration compared
|
|
// as written would pass for a path nobody named.
|
|
func declaredPaths(m mcat.Manifest) map[string]bool {
|
|
out := map[string]bool{}
|
|
if m.OwnsPaths != nil {
|
|
// Carried: what its manifest declares, never what runs of it (issue 1126, seed-8: f1-circuits, snake and txt-game
|
|
// on ace owned their /var/lib paths on the node and none on the controller).
|
|
for _, p := range m.OwnsPaths {
|
|
out[p] = true
|
|
}
|
|
return out
|
|
}
|
|
for _, r := range m.Resources {
|
|
if p, ok := r["path"].(string); ok && strings.HasPrefix(p, "/") {
|
|
out[path.Clean(versionedPlaceOwner(r, p))] = true
|
|
}
|
|
}
|
|
for _, f := range m.Facts {
|
|
if !f.Home && strings.HasPrefix(f.Path, "/") {
|
|
out[path.Clean(f.Path)] = true
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// DeclaredPaths is every machine path a module's manifest declares it owns (declaredPaths), sorted and never nil: what
|
|
// a carried module owns (Manifest.OwnsPaths).
|
|
func DeclaredPaths(m mcat.Manifest) []string {
|
|
return append([]string{}, sortedKeys(declaredPaths(m))...)
|
|
}
|
|
|
|
// declaresPath says whether a module declares one path, as a resource or as a file the mesh renders.
|
|
func declaresPath(m mcat.Manifest, p string) bool { return declaredPaths(m)[p] }
|
|
|
|
// pathSeats is which seats name each path: the mesh's own, and then those modules declare, in the order
|
|
// given. A module's seat naming a path a seat already names loses it, and the refusal says so (novox/hq
|
|
// ADR 0287 — as for a seat's name, the second declaration loses).
|
|
func pathSeats(order []string, shelf mcat.Shelf, declaredBy map[string]string) (map[string][]string, []string) {
|
|
named := map[string][]string{}
|
|
for _, s := range mcat.Seats() {
|
|
for _, p := range s.Paths {
|
|
named[p.Path] = append(named[p.Path], s.Name)
|
|
}
|
|
}
|
|
var problems []string
|
|
for _, module := range order {
|
|
for _, s := range shelf[module].DefinesSeats {
|
|
if declaredBy != nil && declaredBy[s.Name] != module {
|
|
continue // a second declaration of the seat's name, refused for that already
|
|
}
|
|
for _, p := range s.Paths {
|
|
if others := named[p]; len(others) > 0 {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s declares the seat %s naming the path %q, which %s already names; no two seats "+
|
|
"name one path (novox/hq ADR 0287)", module, s.Name, p, strings.Join(others, " and ")))
|
|
continue
|
|
}
|
|
named[p] = append(named[p], s.Name)
|
|
}
|
|
}
|
|
}
|
|
return named, problems
|
|
}
|
|
|
|
// unclaimedSeatPaths is each path a module declares that a seat names while it claims none of the seats
|
|
// naming it (novox/hq ADR 0287): only a claimant declares a seat's path, and a module claiming any one of
|
|
// them may.
|
|
func unclaimedSeatPaths(m mcat.Manifest, named map[string][]string) []string {
|
|
claims := map[string]bool{}
|
|
for _, c := range m.Claims {
|
|
claims[canonicalSeat(c.Name)] = true
|
|
}
|
|
var problems []string
|
|
declared := declaredPaths(m)
|
|
for _, p := range sortedKeys(declared) {
|
|
seats := named[p]
|
|
if len(seats) == 0 {
|
|
continue
|
|
}
|
|
claimsOne := false
|
|
for _, s := range seats {
|
|
claimsOne = claimsOne || claims[s]
|
|
}
|
|
if !claimsOne {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s declares the path %q, which the seat %s names, and claims none of them; only a holder of "+
|
|
"a seat naming a path declares it (novox/hq ADR 0287)", m.Module, p, strings.Join(seats, " and ")))
|
|
}
|
|
}
|
|
return problems
|
|
}
|
|
|
|
// seatPathsOnMachine is each path the modules of one machine declare that a seat names, for a module
|
|
// claiming none of the seats naming it (novox/hq ADR 0287). Registration refuses the same of a manifest;
|
|
// this holds what the controller generates for a machine to it too, because a computed module's resources
|
|
// exist only once composed (checkResources is asked them there). The seats are the mesh's own and those the
|
|
// machine's modules declare.
|
|
func seatPathsOnMachine(modules []mcat.Manifest) []string {
|
|
shelf := mcat.Shelf{}
|
|
var order []string
|
|
for _, m := range modules {
|
|
shelf[m.Module] = m
|
|
order = append(order, m.Module)
|
|
}
|
|
named, _ := pathSeats(order, shelf, nil)
|
|
var problems []string
|
|
for _, m := range modules {
|
|
problems = append(problems, unclaimedSeatPaths(m, named)...)
|
|
}
|
|
return problems
|
|
}
|
|
|
|
// versionedPlaceOwner is the path a resource owns: its own, unless the resource is an archive unpacked into a place named
|
|
// for its build (the artifact's digest, short: `.../versions/<version>`, catalogue `${version}`), whose owner is the
|
|
// directory holding every such place, because the place changes with each build and the owned paths are an input of the
|
|
// tool runner's unit (the holder of node-files reads them at its start): a build of the engine itself renamed its place
|
|
// and restarted the tool runner, with nothing of the tool runner changed (issue 1966).
|
|
func versionedPlaceOwner(r map[string]any, p string) string {
|
|
digest, _ := r["digest"].(string)
|
|
version := strings.TrimPrefix(digest, "sha256:")
|
|
base := path.Base(path.Clean(p))
|
|
if r["type"] == "archive" && len(base) >= 8 && strings.HasPrefix(version, base) {
|
|
return path.Dir(path.Clean(p))
|
|
}
|
|
return p
|
|
}
|