node-engine.composition shows the node's own fields and what the last pass held (issue 774)

Reading ace's node composition gave four resources twice and no way to see that one revision named no provisioner
client's account. The verb now shows, for module node, the revision's provisioner account beside what runs names, its
adoption, how many identities, groups and libraries it names, and each module the composer's last pass held with why.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
This commit is contained in:
2026-11-04 01:50:21 +01:00
parent f7abd05981
commit f7129fdb41
5 changed files with 60 additions and 0 deletions
+3
View File
@@ -136,6 +136,9 @@ type Env struct {
// Revisions are a composition key's revisions the bucket keeps, oldest first; nil reads none. The composition a want
// names is taken from them when a newer one replaced it under the same key (ADR 0539 clause 1).
Revisions func(ctx context.Context, key string) ([]Update, error)
// Held is what the node's composer held in its last pass, each module with why, as the verb composition shows it
// for the node's own (issue 774); nil says nothing.
Held func() map[string]string
// Settled is told a module's apply or removal that succeeded, once what runs holds it (Everything), outside the
// agent's lock: the declaration the node applies now names what that composition names, its filled files among
// them. The node-engine's filling tells the provisioner's client then to fill again what it refused (mesh-host
+12
View File
@@ -12,6 +12,7 @@ import (
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/declaration"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -1312,6 +1313,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
defer historyMu.Unlock()
history[key] = append(history[key], Update{Key: key, Value: raw, Revision: uint64(len(history[key]) + 1)})
}
b.env.Held = func() map[string]string { return map[string]string{"mesh-build-go": "its binding is unset"} }
stop := b.run()
defer stop()
unsigned := func(module, fp string, rs ...json.RawMessage) (string, []byte, string) {
@@ -1493,6 +1495,16 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
if _, err := b.ag.Composition(context.Background(), "db", "sha256:"+strings.Repeat("0", 64), false); err == nil {
t.Error("a digest no revision has was answered")
}
// Issue 774 (ace, 2026-11-04 01:29): the node's own revision shows its own fields, the provisioner client's account
// and the adoption among them, and what the composer's last pass held.
ownKey, ownRaw, ownDig := encodedAs(t, delivery.Composed{Node: node, Own: delivery.NodeOwn{Provisioner: "mesh-provisioner-client",
Adoption: &declaration.Adoption{Taken: []string{"web"}}}}, func() string { k, _ := delivery.ComposedNodeKey(node); return k }())
keep(ownKey, ownRaw)
if view, err := b.ag.Composition(context.Background(), NodeModule, ownDig, false); err != nil || view.Own == nil ||
view.Own.Provisioner != "mesh-provisioner-client" || view.Own.Adoption == nil || view.Own.Adoption.Taken[0] != "web" ||
view.Own.Held["mesh-build-go"] == "" {
t.Errorf("the node's own composition reads %+v %+v (%v)", view, view.Own, err)
}
// novox/hq ADR 0593 §6: the engine publishes each composition with its plan in its composition state; the verb reads
// that revision by the digest its have and want name, its resources' (the plan left out of the digest).
planned, _ := delivery.DecodeComposed(draw)
+32
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/declaration"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/judge"
)
@@ -32,6 +33,26 @@ type CompositionView struct {
Judged JudgedView `json:"judged"`
// Diff is every resource that differs from what runs, when asked.
Diff []judge.ResourceChange `json:"diff,omitempty"`
// Own is, of the node's own composition (module node), its own fields beside what runs names (issue 774: ace's
// own composition dropped the provisioner client's account, and nothing read could show it).
Own *OwnView `json:"own,omitempty"`
}
// OwnView is the node's own fields of a composition (delivery.NodeOwn) beside what runs of them, and what the node's
// composer held in its last pass.
type OwnView struct {
// Provisioner is the account the provisioner's client runs as, as the revision read names it; RunsProvisioner as
// what runs names it. Empty: the declaration names none, and every filled write is refused.
Provisioner string `json:"provisioner_account"`
RunsProvisioner string `json:"runs_provisioner_account,omitempty"`
// Adoption is the revision's adoption: the node adopted, and which of its modules are taken; nil for a converged node.
Adoption *declaration.Adoption `json:"adoption,omitempty"`
// Identities, Groups and Libraries count what the revision names of each.
Identities int `json:"identities,omitempty"`
Groups int `json:"groups,omitempty"`
Libraries int `json:"libraries,omitempty"`
// Held is each module the node's composer held in its last pass, with why.
Held map[string]string `json:"held,omitempty"`
}
// WantView is a want as the verb shows it.
@@ -171,6 +192,17 @@ func (a *Agent) Composition(ctx context.Context, module, digest string, diff boo
}
view.Runs = rv
}
if module == NodeModule {
o := read.C.Own
view.Own = &OwnView{Provisioner: o.Provisioner, Adoption: o.Adoption, Identities: len(o.Identities), Groups: len(o.Groups),
Libraries: len(o.Libraries)}
if running != nil {
view.Own.RunsProvisioner = running.Own.Provisioner
}
if a.env.Held != nil {
view.Own.Held = a.env.Held()
}
}
j := judge.JudgeMove(running, read.C)
view.Judged = JudgedView{Judgement: string(j.Judgement), Words: j.Words}
if a.env.SelfComposed && a.env.Policy != nil && module != NodeModule && (!w.Present || w.Want.Yes != read.Dig) {
+7
View File
@@ -218,6 +218,13 @@ type Pass struct {
Earlier bool `json:"earlier,omitempty"`
}
// LastHeld is what the last pass held, each module with why; nil before a pass or when it held none.
func (c *Composer) LastHeld() map[string]string {
c.mu.Lock()
defer c.mu.Unlock()
return maps.Clone(c.last.Held)
}
// askedWithin is how recent a pass answers an ask of the verb compose without a new one: the first wait after a seat
// that did not answer (laterAgain's first).
var askedWithin = 5 * time.Second