The node composes its own modules once the catalogue places it: one composer per node drains a mark set by any input's change, the agent applies what it composed when it differs, and a record move waits for a yes (issue 1126, ADR 0547 §2, §3)
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 02d9d7b0538f on ff9386dcccc2 | novox/mesh-host#149 in group feat/1126-nodes-compose

This commit is contained in:
2026-10-26 18:44:58 +01:00
parent 7fc0ea5478
commit 02d9d7b053
9 changed files with 866 additions and 5 deletions
+42 -1
View File
@@ -13,11 +13,14 @@ import (
"sync"
"time"
"git.novox.be/novox/mesh-sdk/go/compose/frombus"
"git.novox.be/novox/mesh-sdk/go/declaration"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/overlay"
"github.com/novox/mesh-host/internal/agent"
"github.com/novox/mesh-host/internal/apply"
"github.com/novox/mesh-host/internal/link"
"github.com/novox/mesh-host/internal/selfcompose"
"github.com/novox/mesh-host/internal/store"
)
@@ -73,7 +76,7 @@ func (h sessionHaves) Write(ctx context.Context, x agent.Have) {
phase = ""
}
h.session.NoteModule(ctx, link.ModuleFact{Module: x.Module, FP: x.FP, Composed: x.Composed, Phase: phase,
Failed: phase == string(delivery.PhaseFailed), Detail: shorten(x.Detail)})
Failed: phase == string(delivery.PhaseFailed), Detail: shorten(x.Detail), Judgement: x.Judgement, Waits: x.Waits})
}
func (h sessionHaves) Removed(ctx context.Context, module string) {
@@ -476,6 +479,11 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, sess
return res
},
}
// The node composes its own modules once the catalogue places it (novox/hq ADR 0547 §2, ADR 0549, issue 1126): its
// placement key is read at the agent's start; until then it applies the controller's compositions as before.
if composer := selfComposer(linked, session, node, say); composer != nil {
env.Composed, env.SelfComposed, env.Trusted, env.Policy = composer, true, nil, composer.Policy
}
ag = agent.New(env)
current.set(ag)
err = ag.Run(linked)
@@ -756,3 +764,36 @@ const applyBound = 30 * time.Minute
func applyContext(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(context.WithoutCancel(ctx), applyBound)
}
// selfComposer is the node's own composer when the catalogue places the node (its key `placement.<node>` in the
// indexer's state), and nil, said once, while it does not or the key cannot be read.
func selfComposer(ctx context.Context, session *link.Session, node string, say func(string)) *selfcompose.Composer {
key, err := delivery.PlacementKey(node)
if err != nil {
say("this node applies the controller's compositions: " + err.Error())
return nil
}
got, err := session.KVHistory(ctx, delivery.PlacementBucket, key)
if err != nil || len(got) == 0 || got[len(got)-1].Deleted {
why := "the catalogue does not place it yet"
if err != nil {
why = "its placement cannot be read: " + err.Error()
}
say("this node applies the controller's compositions: " + why)
return nil
}
say("this node composes its own modules: the catalogue places it (" + key + ")")
return selfcompose.New(selfcompose.Env{Node: node,
Watch: func(ctx context.Context, bucket, pattern string) (<-chan agent.Update, error) {
return bucketWatch{session, bucket}.Watch(ctx, pattern)
},
Get: func(ctx context.Context, bucket, key string) ([]byte, error) {
h, err := session.KVHistory(ctx, bucket, key)
if err != nil || len(h) == 0 || h[len(h)-1].Deleted {
return nil, err
}
return h[len(h)-1].Value, nil
},
Mesh: frombus.Mesh{InternalSuffix: overlay.Suffix()},
Say: say})
}