H-17: the node-engine's own name is read from its applied record or first composing

Discovery, the finding writer and the vault login names read it; no fixed name.

Builds: design 47 G §527, §634 (A-149)
This commit is contained in:
2026-11-12 04:58:28 +01:00
parent 4d656fe4e1
commit ac3422f914
17 changed files with 346 additions and 41 deletions
+27 -6
View File
@@ -131,6 +131,28 @@ func (l *natsLink) ServeEngineSeat(handle EngineFunc) {
if handle == nil || l.conn == nil {
return
}
// Discovery names the module whose code answers: the engine's own, read from §527 and no fixed name (§634). Known at
// once from the applied record; on a first start it is served once the first composing names it.
if l.own == nil || l.own.Get() != "" {
l.serveEngineSeat(handle, l.own.Get())
return
}
ctx, cancel := context.WithCancel(context.Background())
l.engineMu.Lock()
l.stopWait = cancel
l.engineMu.Unlock()
go func() {
if name, err := l.own.Wait(ctx); err == nil {
l.serveEngineSeat(handle, name)
}
}()
}
// UseOwnName gives the link the engine's own name its discovery reads; before ServeEngineSeat.
func (l *natsLink) UseOwnName(o *OwnName) { l.own = o }
// serveEngineSeat serves the seat as the module module.
func (l *natsLink) serveEngineSeat(handle EngineFunc, module string) {
svc, err := micro.AddService(l.conn, micro.Config{Name: EngineSeat, Version: "1.0.0",
Description: "the node seat node-engine of " + l.node + " (novox/hq ADR 0563)", QueueGroupDisabled: true,
Metadata: map[string]string{"seat": EngineSeat, "scope": "node", "node": l.node}})
@@ -141,13 +163,15 @@ func (l *natsLink) ServeEngineSeat(handle EngineFunc) {
for _, v := range ServedEngineSeat().Serves {
verb := v.Name
answer := EngineAnswer(handle, verb, l.node, apart)
meta := EngineEndpointMetadata(v, l.node)
meta := EngineEndpointMetadata(v, l.node, module)
_ = svc.AddEndpoint(verb, answer, micro.WithEndpointSubject(EngineToolSubject(verb, l.node)),
micro.WithEndpointMetadata(meta))
_ = svc.AddEndpoint(verb+"-called", answer, micro.WithEndpointSubject(EngineCallPattern(verb, l.node)),
micro.WithEndpointMetadata(meta))
}
l.engineMu.Lock()
l.engine = svc
l.engineMu.Unlock()
}
// answeredApart are the verbs answered off the subscription that delivered them: each endpoint's subscription hands its
@@ -184,20 +208,17 @@ func EngineAnswer(handle EngineFunc, verb, node string, apart chan struct{}) mic
}
}
// EngineModule is the module whose code answers the seat, as discovery names it: the node-engine's manifest's.
const EngineModule = "mesh-host"
// EngineEndpointMetadata is what discovery reads of one verb's endpoint, in the shape every tool runner and the
// controller announce a seat's verb in (mesh-tools node-tools' announce package): its kind, the seat, its scope, the
// module whose code answers, the verb, the node, its description and its schema. Without the kind and the scope the
// console listed none of it, and `<node>/node-engine.<verb>` answered that nothing in the mesh is called node-engine
// (the sweep of 2026-10-30).
func EngineEndpointMetadata(v catalogue.Verb, node string) map[string]string {
func EngineEndpointMetadata(v catalogue.Verb, node, module string) map[string]string {
schema, err := json.Marshal(v.Input)
if err != nil || v.Input == nil {
schema = []byte("{}")
}
return map[string]string{"kind": "seat", "seat": EngineSeat, "scope": "node", "module": EngineModule,
return map[string]string{"kind": "seat", "seat": EngineSeat, "scope": "node", "module": module,
"tool": v.Name, "node": node, "interchangeable": "false", "description": v.Description, "schema": string(schema)}
}
+10
View File
@@ -8,6 +8,7 @@ import (
"os"
"regexp"
"strings"
"sync"
"time"
"github.com/nats-io/nats.go"
@@ -48,6 +49,10 @@ type natsLink struct {
moving *nats.Subscription
// engine serves the node seat node-engine, and warrants hears the operator's answers to its asks (ADR 0563).
engine micro.Service
engineMu sync.Mutex
// own is the engine's own module, which discovery names (§634); stopWait ends a wait for it when the link closes.
own *OwnName
stopWait context.CancelFunc
warrants *nats.Subscription
node string
// user is the bus user this link authenticated as (node.<n> or node.<n>--b, ADR 0583 §1).
@@ -295,9 +300,14 @@ func (l *natsLink) Close() {
if l.judging != nil {
_ = l.judging.Unsubscribe()
}
l.engineMu.Lock()
if l.stopWait != nil {
l.stopWait()
}
if l.engine != nil {
_ = l.engine.Stop()
}
l.engineMu.Unlock()
if l.warrants != nil {
_ = l.warrants.Unsubscribe()
}
+61
View File
@@ -0,0 +1,61 @@
package link
import (
"context"
"errors"
"sync"
)
// OwnName is the module that is this node-engine: the module whose catalog entry claims the seat node-engine, which the
// engine learns first from its applied record of the build whose fingerprint is its own and, on a first start with no such
// record, from its first composing (design 47 G §527, §634, A-149). Its discovery, its finding writer and its login names
// in the vault read it here and fix none.
type OwnName struct {
mu sync.Mutex
name string
ready chan struct{}
}
// NewOwnName makes an own name not yet known.
func NewOwnName() *OwnName { return &OwnName{ready: make(chan struct{})} }
// Set keeps the name; an empty one changes nothing. The first name opens Wait for everyone.
func (o *OwnName) Set(name string) {
if o == nil || name == "" {
return
}
o.mu.Lock()
defer o.mu.Unlock()
if o.name == "" {
close(o.ready)
}
o.name = name
}
// Get is the name, "" while it is not known.
func (o *OwnName) Get() string {
if o == nil {
return ""
}
o.mu.Lock()
defer o.mu.Unlock()
return o.name
}
// Wait returns the name once it is known, or ctx's error.
func (o *OwnName) Wait(ctx context.Context) (string, error) {
if o == nil {
return "", errors.New("this session names no own module")
}
select {
case <-o.ready:
return o.Get(), nil
case <-ctx.Done():
return "", ctx.Err()
}
}
// OwnNamed is a link that reads the node-engine's own module from an OwnName when it serves its seat.
type OwnNamed interface {
UseOwnName(*OwnName)
}
+32
View File
@@ -0,0 +1,32 @@
package link
import (
"context"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// Design 47 G §527, §634: discovery names the module the engine reads of itself, Wait holds until it is known, and an
// empty name changes nothing.
func TestTheOwnNameIsWaitedForAndDiscoveryNamesIt(t *testing.T) {
o := NewOwnName()
o.Set("")
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
defer cancel()
if _, err := o.Wait(ctx); err == nil || o.Get() != "" {
t.Fatalf("an unnamed engine's wait ended with %v, name %q", err, o.Get())
}
o.Set("the-engine")
if got, err := o.Wait(context.Background()); err != nil || got != "the-engine" {
t.Fatalf("waited %q, %v", got, err)
}
if got := EngineEndpointMetadata(catalogue.Verb{Name: "composition"}, "n1", o.Get())["module"]; got != "the-engine" {
t.Fatalf("discovery names the module %q", got)
}
var none *OwnName
if _, err := none.Wait(context.Background()); err == nil || none.Get() != "" {
t.Fatalf("a session naming none waited: %v", err)
}
}
+3
View File
@@ -217,6 +217,9 @@ func serve(ctx context.Context, link Link, m Membership, queue *Session, say Ann
// link, and what was answered while no link was open caught up from the router's records.
if queue.Acts != nil {
if s, ok := link.(EngineSeats); ok {
if o, named := link.(OwnNamed); named {
o.UseOwnName(queue.Own)
}
s.ServeEngineSeat(queue.Acts.Call)
// And what it reads of a manifest, said once for this link (novox/hq ADR 0618 §4).
queue.SayReads()
+3
View File
@@ -26,6 +26,9 @@ import (
// **It applies nothing and orders nothing.** A module is applied by the node agent from its want. Nothing here
// receives, orders or reconciles a whole declaration.
type Session struct {
// Own is the module that is this node-engine (§527, §634): its discovery, finding writer and vault login names read it.
// Nil names none, as a test's session does.
Own *OwnName
// Fallback is the engine's fall back to its old login its health report carries now (Rotator.Fallback); nil says none.
Fallback func() *nodeengine.LoginFallback
// linkedUser is the bus user the link open now authenticated as, guarded by mu (WhenLinkedAs).
+4 -2
View File
@@ -25,7 +25,9 @@ const HeldKind = "module-held"
type HeldKeeper struct {
Node string
Observer string
Writer Writer
// ObserverOf, when set, answers the observer at each write (see Waiter).
ObserverOf func() string
Writer Writer
// Ctx is the run's context for the writer's acts; nil is the background.
Ctx context.Context
// Now is the clock; nil is time.Now.
@@ -78,7 +80,7 @@ func (k *HeldKeeper) Reconcile(held map[string]string) {
continue
}
id := HeldID(m, k.Node)
f := condseat.Finding{Observer: k.Observer, Kind: HeldKind, Scope: scopeModule, ID: id, Severity: condseat.FindingSeverityWarning,
f := condseat.Finding{Observer: observerOf(k.Observer, k.ObserverOf), Kind: HeldKind, Scope: scopeModule, ID: id, Severity: condseat.FindingSeverityWarning,
Source: HeldKind,
Subject: condseat.Subject{Scope: scopeModule, ID: id, Machine: k.Node},
Summary: fmt.Sprintf("%s is held on %s: %s", m, k.Node, why),
+15 -4
View File
@@ -65,9 +65,12 @@ func stands(w Writer, scope, id, kind string) bool {
type Waiter struct {
Node string
Observer string
Reader Reader
Writer Writer
Clock Clock
// ObserverOf, when set, answers the observer at each write: the node-engine's own module, which a first start learns
// from its first composing (design 47 G §527, §634).
ObserverOf func() string
Reader Reader
Writer Writer
Clock Clock
// Ctx is the run's context for the writer's acts; nil is the background.
Ctx context.Context
// Say tells the node's journal; nil says nothing.
@@ -143,7 +146,7 @@ func (w *Waiter) expired() {
if w.stands() {
return
}
f := condseat.Finding{Observer: w.Observer, Kind: Kind, Scope: scopeMachine, ID: w.Node, Severity: condseat.FindingSeverityWarning,
f := condseat.Finding{Observer: observerOf(w.Observer, w.ObserverOf), Kind: Kind, Scope: scopeMachine, ID: w.Node, Severity: condseat.FindingSeverityWarning,
Source: Kind, Subject: condseat.Subject{Scope: scopeMachine, ID: w.Node, Machine: w.Node},
Summary: fmt.Sprintf("%s has read no complete seat list since %s: %s", w.Node, w.since.UTC().Format(time.RFC3339), st.Health()),
Headline: fmt.Sprintf("%s waits for the seat list", w.Node),
@@ -161,3 +164,11 @@ func (w *Waiter) say(s string) {
w.Say(s)
}
}
// observerOf is of() when it is given, else fixed.
func observerOf(fixed string, of func() string) string {
if of != nil {
return of()
}
return fixed
}
+8 -1
View File
@@ -160,7 +160,10 @@ const VaultSecretChanged = "mesh.mod.mesh-vault.event.secret-changed"
// Composer composes the node and is the agent's watcher of its compositions (agent.Env.Composed).
type Composer struct {
env Env
// OnEngine is told the node-engine's own module each time a composing finds it (design 47 G §527, §634); set before the
// composer runs.
OnEngine func(module string)
env Env
mu sync.Mutex
wants map[string]meshwants.WantsByWantNodeModuleValue // by module, the node's own under agent.NodeModule
@@ -2453,7 +2456,11 @@ func (c *Composer) noteEngine(module string) {
}
c.mu.Lock()
c.engine = module
on := c.OnEngine
c.mu.Unlock()
if on != nil {
on(module)
}
}
// EngineModule is the node-engine's own module (design 47 G §527), for the apply guard; "" until a composing found it.