H-15: the apply guard and the yields read the node-engine's module from the entry claiming its seat

The module is the one whose catalog entry claims node-engine, else the one this node heard run the build its own
fingerprint names; no name is fixed in the guard or in the compose.

Builds: design 47 G §364, §527, §428

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 14:05:43 +01:00
parent a073a001c5
commit 602ed830e9
9 changed files with 143 additions and 13 deletions
+2
View File
@@ -670,6 +670,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
composerNow.Store(composer) // what the verb compose asks a pass of, this run's alone
session.SetManifestOf(composer.ManifestOf) // the event running names the manifest of the build that runs
go followRunning(run, session, composer, say)
apply.UseEngineModule(composer.EngineModule) // the guard of the engine's own trees fixes no name (§527)
env.Composed, env.SelfComposed, env.Policy, env.ComposedToday = composer, true, composer.Policy, composer.ComposedToday
env.WhyUnmade, env.Waiting = composer.WhyUnmade, composer.Waiting
env.HoldsData = composer.HoldsData // what runs from the kept declaration says Data as its manifest does
@@ -683,6 +684,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
current.set(nil)
composerNow.Store(nil)
session.SetManifestOf(nil)
apply.UseEngineModule(nil)
shadowNow.Store(nil)
if ctx.Err() != nil {
return
+24 -3
View File
@@ -54,6 +54,7 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"git.novox.be/novox/mesh-sdk/go/declaration"
@@ -303,8 +304,28 @@ func splitQuoted(s string) []string {
return out
}
// engineModule is the module whose resources may place in the engine's own trees.
const engineModule = "mesh-host"
// engineModuleOf answers the module whose resources may place in the engine's own trees: the module whose entry of the
// catalog half claims the seat node-engine (design 47 G §527), asked of the node's composer; none until one is given.
var engineModuleOf atomic.Pointer[func() string]
// UseEngineModule gives the apply guard the function that answers the node-engine's own module; nil takes it away. The
// guard fixes no name (§364, §527).
func UseEngineModule(f func() string) {
if f == nil {
engineModuleOf.Store(nil)
return
}
engineModuleOf.Store(&f)
}
// isEngineModule is whether module is the node-engine's own.
func isEngineModule(module string) bool {
f := engineModuleOf.Load()
if f == nil || module == "" {
return false
}
return (*f)() == module
}
// passwdFile is the user database homes are read from. A variable so a test names its own.
var passwdFile = "/etc/passwd"
@@ -531,7 +552,7 @@ func refuseOne(path string, kind placing, module, owner string, declared map[str
return &PlacementRefusedError{Path: path, Why: "nothing is placed in or mounted from " + tree}
}
}
if module != engineModule {
if !isEngineModule(module) {
for _, tree := range engineTrees {
if atOrBelow(path, tree) {
return &PlacementRefusedError{Path: path, Why: tree + " is the node-engine's own, placed in by " +
+20
View File
@@ -193,4 +193,24 @@ func TestAModulesOwnPlacesAreStillPlaced(t *testing.T) {
if err := refusePath("/var/lib/notes/data", placingDirectory, "notes", "1001:2000"); err != nil {
t.Errorf("an owner below /var/lib: %v", err)
}
// Design 47 G §364, §527: the node-engine's own trees are placed in by the module the composer says claims its seat,
// whatever that module is called, and by no other; with none said, by nobody.
tree := engineTrees[0]
t.Cleanup(func() { UseEngineModule(nil) })
for _, c := range []struct {
engine func() string
module string
placed bool
}{
{func() string { return "the-engine" }, "the-engine", true},
{func() string { return "the-engine" }, "notes", false},
{func() string { return "the-engine" }, "", false},
{func() string { return "" }, "", false},
{nil, "the-engine", false},
} {
UseEngineModule(c.engine)
if err := refusePath(tree, placingDirectory, c.module, ""); (err == nil) != c.placed {
t.Errorf("%s for module %q: %v, want placed %v", tree, c.module, err, c.placed)
}
}
}
+35
View File
@@ -160,6 +160,13 @@ type Scope struct {
// composing node's own consumer (design 47 G §199): the passing of a seat reads it, and a module nothing was heard of
// counts by the trunk's entry. Never read from a have key, a want or a call.
Heard map[string]map[string]Heard
// Engine is told, at each composing of the node's input, the node-engine's own module: the one whose entry of the catalog half
// claims the seat node-engine, else the one of the node's heard modules that runs the build OwnFingerprint names (design 47 G
// §527); "" when neither shows it. Nil: told to nobody.
Engine func(module string)
// OwnFingerprint is the fingerprint this node-engine was started at (its MESH_FINGERPRINT); empty when it was started
// without one, as an engine of before G.
OwnFingerprint string
// HeldToday is, in an answer, the modules the node keeps today as they run ("node" for its own composition), each with
// how ("held", "failed", "runs a build of the old form"): a candidate that cannot compose one, or holds one only
// behind one of them, keeps it as today, which is said under CandidateAnswer.Held and is no refusal (issue 1126).
@@ -593,6 +600,31 @@ type Heard struct {
Stopped bool
}
// EngineModule is the node-engine's own module (design 47 G §527): the module whose catalog entry claims the seat node-engine,
// which the validator makes one per revision (§508). With none claiming it, which only a revision of before G shows, as at a
// revert of G, the module this node heard run the build its own fingerprint names; "" when neither shows it.
func EngineModule(in compose.Input, node, ownFingerprint string) string {
for _, name := range sortedKeys(in.Catalogue) {
if resolution.ClaimsEngine(in.Catalogue[name]) {
return name
}
}
if ownFingerprint == "" {
return ""
}
for i := range in.Nodes {
if in.Nodes[i].Name != node {
continue
}
for _, module := range sortedKeys(in.Nodes[i].Heard) {
if in.Nodes[i].Heard[module].Fingerprint == ownFingerprint {
return module
}
}
}
return ""
}
// keepHeard puts, as each node's Heard, what it said it runs of each module it is assigned, with the manifest of the build
// read by the event's digest; a digest that cannot be read leaves the entry unread, and the passing says so (§459).
func keepHeard(in *compose.Input, s Scope, read func(module, digest string) (catalogue.Manifest, error)) {
@@ -643,6 +675,9 @@ func Compose(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]s
if err != nil {
return nil, nil, held, err
}
if s.Engine != nil {
s.Engine(EngineModule(in, s.Node, s.OwnFingerprint))
}
if s.Holds != nil {
holds := map[string]compose.Hold{}
for name := range held {
+14
View File
@@ -19,6 +19,7 @@ import (
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"github.com/novox/mesh-host/internal/compositions"
"maps"
"os"
"reflect"
"slices"
@@ -592,6 +593,19 @@ func TestInput(t *testing.T) {
t.Errorf("anchor's Heard holds %+v, want f2 unread", n.Heard)
}
}
// Design 47 G §527: the node-engine's own module is the one whose entry claims its seat; with none, the one this node heard
// run the build its own fingerprint names; with neither, none.
if got := EngineModule(in, "laptop", "f1"); got != "xterm" {
t.Errorf("by the own fingerprint: %q, want xterm", got)
}
if got := EngineModule(in, "laptop", "other"); got != "" {
t.Errorf("an unheard fingerprint names %q", got)
}
in.Catalogue = maps.Clone(in.Catalogue)
in.Catalogue["the-engine"] = catalogue.Manifest{Module: "the-engine", Claims: []catalogue.Claim{{Name: "node-engine"}}}
if got := EngineModule(in, "laptop", "f1"); got != "the-engine" {
t.Errorf("by the claim: %q, want the-engine", got)
}
// Issue 1126, the seed's refusals. restic and the provisioner's client as the catalogue holds them (mesh-catalog
// c05aa5e6), restic moved with an own secret the vault leaves out (it hands it to the client, mesh-vault ownnode.go
// handed): composed from the indexer's manifest restic is not handed and stands in for that secret, as novox's,
+11 -2
View File
@@ -7,6 +7,7 @@ import (
"sort"
"strings"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
)
@@ -56,8 +57,16 @@ func systemPath(path string) string {
return ""
}
// engineModule is the node-engine's own module, the one that places in engineTrees.
const engineModule = "mesh-host"
// ClaimsEngine is whether a manifest claims the seat node-engine: the node-engine's own module is the one whose entry of
// the catalog half does (design 47 G §527), never a name fixed in code.
func ClaimsEngine(m mcat.Manifest) bool {
for _, c := range m.Claims {
if c.Name == nodeengine.Seat {
return true
}
}
return false
}
// accessRef is how a module names one of its accesses: ${access:<id>}.
var accessRef = regexp.MustCompile(`\$\{access:([a-z0-9][a-z0-9-]*)\}`)
+13 -4
View File
@@ -310,16 +310,25 @@ func TestAModuleOfItsOwnAccountIsServedByARuntimeOfItsOwn(t *testing.T) {
}
gob, img := builder("gob", "node-build-go"), builder("img", "node-build-image")
with.Needed["gob"], with.Needed["img"] = map[string]string{"broker": "own"}, map[string]string{"broker": "own"}
// The node-engine's module is the one whose entry claims its seat (design 47 G §527), whatever it is called; with none in
// the resolution its settings are not found and the default stands.
engine := mcat.Manifest{Module: "mesh-host", Claims: []mcat.Claim{{Name: "node-engine"}}}
for _, c := range []struct {
settings mcat.SettingsBy
share string
noEngine bool
}{
{nil, "50"},
{mcat.SettingsBy{"mesh-host": {{From: "anchor", Values: map[string]any{"build-cpu-share": float64(75)}}}}, "75"},
{mcat.SettingsBy{"mesh-host": {{From: "anchor", Values: map[string]any{"build-cpu-share": float64(0)}}}}, "50"},
{nil, "50", false},
{mcat.SettingsBy{"mesh-host": {{From: "anchor", Values: map[string]any{"build-cpu-share": float64(75)}}}}, "75", false},
{mcat.SettingsBy{"mesh-host": {{From: "anchor", Values: map[string]any{"build-cpu-share": float64(0)}}}}, "50", false},
{mcat.SettingsBy{"mesh-host": {{From: "anchor", Values: map[string]any{"build-cpu-share": float64(75)}}}}, "50", true},
} {
with.Settings = c.settings
runtimes, err := resolutionOwnRuntimes(mcat.Resolution{Node: "anchor", Modules: []mcat.Manifest{gob, img, telegram, goRuntime}}, with)
modules := []mcat.Manifest{gob, img, telegram, goRuntime, engine}
if c.noEngine {
modules = modules[:4]
}
runtimes, err := resolutionOwnRuntimes(mcat.Resolution{Node: "anchor", Modules: modules}, with)
if err != nil {
t.Fatal(err)
}
+3 -3
View File
@@ -21,14 +21,14 @@ func resolutionYieldsOf(r mcat.Resolution, m mcat.Manifest, with mcat.Rendering)
if !yields(m) {
return nil, nil
}
engine := mcat.Manifest{Module: engineModule}
var engine mcat.Manifest // the module claiming the seat node-engine; none in the resolution leaves the default
for _, e := range r.Modules {
if e.Module == engineModule {
if ClaimsEngine(e) {
engine = e
}
}
share := mcat.BuildCPUDefaultPercent
for _, s := range Effective(engine, with.Settings[engineModule]) {
for _, s := range Effective(engine, with.Settings[engine.Module]) {
if s.Key == mcat.BuildCPUShareSetting {
if n, ok := percentOf(s.Value); ok {
share = n
+21 -1
View File
@@ -33,6 +33,7 @@ import (
"github.com/novox/mesh-host/internal/compositions"
engcomp "github.com/novox/mesh-host/internal/compositions"
"maps"
"os"
"reflect"
"slices"
"sort"
@@ -180,6 +181,8 @@ type Composer struct {
// node's own consumer (NoteRunning), under mu; heardRead is whether the first read of the stream ended (RunningRead).
heard map[string]map[string]heardRun
heardRead bool
// engine is the node-engine's own module as the last composing of the node's input found it (Scope.Engine), under mu.
engine string
// rehand asks for a pass that hands again what the agent waits for (Waiting).
rehand chan struct{}
// sealedOwn is each own secret's sealed value as first handed, by module/name and generation: a value sealed again
@@ -1002,7 +1005,7 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
var keptMu sync.Mutex
var holds map[string]compose.Hold
fromManifest := map[string]string{} // by module, why it is composed from the indexer's manifest and not a build
scope := frombus.Scope{Node: c.env.Node, KeptAt: kept, InOwn: inOwn, Running: running, Heard: c.heardNow(), Kept: func(module, words string) {
scope := frombus.Scope{Node: c.env.Node, KeptAt: kept, InOwn: inOwn, Running: running, Heard: c.heardNow(), Engine: c.noteEngine, OwnFingerprint: os.Getenv(engcomp.FingerprintVar), Kept: func(module, words string) {
keptMu.Lock()
defer keptMu.Unlock()
if !slices.Contains(keptRuns[module], words) {
@@ -2351,3 +2354,20 @@ func (c *Composer) heardNow() map[string]map[string]frombus.Heard {
}
return out
}
// noteEngine keeps the node-engine's own module a composing of the node's input found; one that finds none changes nothing.
func (c *Composer) noteEngine(module string) {
if module == "" {
return
}
c.mu.Lock()
c.engine = module
c.mu.Unlock()
}
// EngineModule is the node-engine's own module (design 47 G §527), for the apply guard; "" until a composing found it.
func (c *Composer) EngineModule() string {
c.mu.Lock()
defer c.mu.Unlock()
return c.engine
}