From 602ed830e9f20562004a4e85816bb61032f51661 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 14:05:43 +0100 Subject: [PATCH] H-15: the apply guard and the yields read the node-engine's module from the entry claiming its seat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- cmd/mesh-host/nodeagent.go | 2 ++ internal/apply/placement_guard.go | 27 ++++++++++++++-- internal/apply/placement_guard_test.go | 20 ++++++++++++ internal/compose/frombus/frombus.go | 35 +++++++++++++++++++++ internal/compose/frombus/frombus_test.go | 14 +++++++++ internal/compose/resolution/placement.go | 13 ++++++-- internal/compose/resolution/runtime_test.go | 17 +++++++--- internal/compose/resolution/yields.go | 6 ++-- internal/selfcompose/selfcompose.go | 22 ++++++++++++- 9 files changed, 143 insertions(+), 13 deletions(-) diff --git a/cmd/mesh-host/nodeagent.go b/cmd/mesh-host/nodeagent.go index 76873b20..5eecb947 100644 --- a/cmd/mesh-host/nodeagent.go +++ b/cmd/mesh-host/nodeagent.go @@ -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 diff --git a/internal/apply/placement_guard.go b/internal/apply/placement_guard.go index 864c4cb4..248dda54 100644 --- a/internal/apply/placement_guard.go +++ b/internal/apply/placement_guard.go @@ -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 " + diff --git a/internal/apply/placement_guard_test.go b/internal/apply/placement_guard_test.go index 156563c3..8bff411d 100644 --- a/internal/apply/placement_guard_test.go +++ b/internal/apply/placement_guard_test.go @@ -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) + } + } } diff --git a/internal/compose/frombus/frombus.go b/internal/compose/frombus/frombus.go index 7af21b84..5e090c09 100644 --- a/internal/compose/frombus/frombus.go +++ b/internal/compose/frombus/frombus.go @@ -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 { diff --git a/internal/compose/frombus/frombus_test.go b/internal/compose/frombus/frombus_test.go index c34bbd50..5ea996e9 100644 --- a/internal/compose/frombus/frombus_test.go +++ b/internal/compose/frombus/frombus_test.go @@ -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, diff --git a/internal/compose/resolution/placement.go b/internal/compose/resolution/placement.go index 1c65ca49..d94f0b34 100644 --- a/internal/compose/resolution/placement.go +++ b/internal/compose/resolution/placement.go @@ -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:}. var accessRef = regexp.MustCompile(`\$\{access:([a-z0-9][a-z0-9-]*)\}`) diff --git a/internal/compose/resolution/runtime_test.go b/internal/compose/resolution/runtime_test.go index ba0932fd..da6ae5bc 100644 --- a/internal/compose/resolution/runtime_test.go +++ b/internal/compose/resolution/runtime_test.go @@ -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) } diff --git a/internal/compose/resolution/yields.go b/internal/compose/resolution/yields.go index 32f70cf8..41076f1d 100644 --- a/internal/compose/resolution/yields.go +++ b/internal/compose/resolution/yields.go @@ -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 diff --git a/internal/selfcompose/selfcompose.go b/internal/selfcompose/selfcompose.go index cba3a1f2..d4c84594 100644 --- a/internal/selfcompose/selfcompose.go +++ b/internal/selfcompose/selfcompose.go @@ -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 +}