From 7da4deb3f11ff9a7955473024e7e09a0b71f88bc Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 14:02:01 +0100 Subject: [PATCH] =?UTF-8?q?What=20a=20node=20keeps=20of=20a=20module=20as?= =?UTF-8?q?=20it=20runs,=20for=20a=20manifest=20that=20does=20not=20yet=20?= =?UTF-8?q?say=20it,=20is=20carried=20on=20the=20module's=20have=20by=20mo?= =?UTF-8?q?dule,=20so=20the=20watchman=20raises=20its=20condition=20(ADR?= =?UTF-8?q?=200601=20=C2=A74;=20review=20round=201,=20finding=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/mesh-host/nodeagent.go | 13 ++++++-- internal/agent/agent.go | 41 ++++++++++++++++++++++++ internal/agent/agent_test.go | 9 ++++++ internal/compose/frombus/frombus.go | 6 ++-- internal/compose/frombus/frombus_test.go | 2 +- internal/compose/frombus/keep.go | 20 ++++++++---- internal/link/have.go | 1 + internal/link/messages.go | 2 ++ internal/selfcompose/selfcompose.go | 21 +++++++++++- 9 files changed, 100 insertions(+), 15 deletions(-) diff --git a/cmd/mesh-host/nodeagent.go b/cmd/mesh-host/nodeagent.go index 68759f5..00b5aa2 100644 --- a/cmd/mesh-host/nodeagent.go +++ b/cmd/mesh-host/nodeagent.go @@ -120,7 +120,7 @@ func (h sessionHaves) Write(ctx context.Context, x agent.Have) { } 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), Judgement: x.Judgement, Waits: x.Waits, - BuildAlone: x.BuildAlone, Sealed: x.Sealed}) + BuildAlone: x.BuildAlone, Kept: x.Kept, Sealed: x.Sealed}) } func (h sessionHaves) Removed(ctx context.Context, module string) { @@ -606,6 +606,12 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus now.NotComposedEach(run, why, held) // a module held alone marks that module, never the others (issue 1126) } }, + // And what it keeps of a module as it runs, for a manifest that does not say it yet: on the module's have (ADR 0601 §4). + kept: func(kept map[string][]string) { + if now := own.get(); now != nil && run.Err() == nil { + now.KeptNow(run, kept) + } + }, // And the vault's authority beside the controller's, and this node's certificate asked of the vault when a // module here asks for one (ADR 0550 §2). vaultAuthority: trust.VaultAuthority, @@ -616,7 +622,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus // And its answer to every placement candidate, placed or not, applying nothing (novox/hq ADR 0559 §4): written into // its own key, `.node`, under candidates. shadowHooks := hooks - shadowHooks.refused, shadowHooks.certify = nil, nil + shadowHooks.refused, shadowHooks.kept, shadowHooks.certify = nil, nil, nil shadowEnv := composerEnv(session, node, say, shadowHooks) shadow := selfcompose.NewShadow(shadowEnv, func(ctx context.Context, answers map[string]delivery.CandidateAnswer) { @@ -958,6 +964,7 @@ type composerHooks struct { running func() []delivery.Composed unseal func(string) ([]byte, error) refused func(why string, held map[string]string) + kept func(kept map[string][]string) held func(module string) (delivery.Seals, []string, error) // vaultAuthority and certify are selfcompose.Env's VaultAuthority and Certify. vaultAuthority func() string @@ -1011,6 +1018,6 @@ func composerEnv(session *link.Session, node string, say func(string), hooks com Hear: session.Hear, // And a moved module's templates archive, in the artifact store, as every archive is fetched (issue 1126). Fetch: apply.Fetch, - Running: hooks.running, Unseal: hooks.unseal, Refused: hooks.refused, Held: hooks.held, + Running: hooks.running, Unseal: hooks.unseal, Refused: hooks.refused, Held: hooks.held, Kept: hooks.kept, VaultAuthority: hooks.vaultAuthority, Certify: hooks.certify} } diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 35d5d42..b9ff6ff 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -17,6 +17,7 @@ import ( "context" "encoding/json" "fmt" + "slices" "sort" "strings" "sync" @@ -60,6 +61,9 @@ type Have struct { Waits string // BuildAlone says, in phase waiting-yes, the move changes the build alone (novox/hq ADR 0601 §1, delivery.Have.BuildAlone). BuildAlone bool + // Kept says, in words, what the node keeps of the module as it runs because its manifest does not yet say it (ADR 0601 + // §4, delivery.Have.Kept): the watchman raises a condition from it. + Kept []string // Sealed is, on a node composing its own, each value the composition it names carries sealed to the node's key, by // slot (delivery.Have.SealedAt): kept in the have so the composer reuses them after a restart (ADR 0547 §3), each at // its own slot alone (issue 1126, C1). @@ -205,6 +209,8 @@ type Agent struct { // keptData is, for each module of the kept declaration whose manifest could be read, whether it declares data // (Env.HoldsData): the Data of what runs of it from the kept declaration. keptData map[string]bool + // kept is, by module, what the composer's last pass keeps of it as it runs (KeptNow, ADR 0601 §4). Under mu. + kept map[string][]string // attempt is the last apply of a module that failed or is held, with the revisions it was made from. attempt map[string]attempt checked map[string]bool @@ -579,6 +585,11 @@ func (a *Agent) NotComposedEach(ctx context.Context, why string, held map[string // said runs, its fingerprint, digest and sealed values, rather than a have naming nothing: what runs is not lost, and // the boxes the composer reuses after a restart stay (review of L2, rounds 2 and 3). func (a *Agent) writeHave(ctx context.Context, h Have) { + if h.Module != NodeModule { + a.mu.Lock() + h.Kept = a.kept[h.Module] + a.mu.Unlock() + } if h.Composed == "" && h.Module != NodeModule { if last, ok := a.env.Haves.Read(ctx, h.Module); ok && last.Composed != "" { h.FP, h.Composed = last.FP, last.Composed @@ -595,6 +606,36 @@ func (a *Agent) writeHave(ctx context.Context, h Have) { a.env.Haves.Write(ctx, h) } +// KeptNow takes in what the composer's last pass keeps of each module as it runs because its manifest does not yet say it +// (ADR 0601 §4), and writes again the have of each module whose words changed, so it carries them; a have waiting for a yes +// takes them at its next write, which keeps what it waits for. +func (a *Agent) KeptNow(ctx context.Context, kept map[string][]string) { + a.mu.Lock() + var changed []string + for m := range a.kept { + if _, still := kept[m]; !still { + changed = append(changed, m) + } + } + for m, words := range kept { + if !slices.Equal(a.kept[m], words) { + changed = append(changed, m) + } + } + a.kept = map[string][]string{} + for m, words := range kept { + a.kept[m] = slices.Clone(words) + } + a.mu.Unlock() + slices.Sort(changed) + for _, m := range changed { + if h, ok := a.env.Haves.Read(ctx, m); ok && m != NodeModule && h.Phase != string(delivery.PhaseWaitingYes) { + h.Module = m + a.writeHave(ctx, h) + } + } +} + func (a *Agent) sealedOf(ctx context.Context, module, fp, composed string) delivery.Seals { a.mu.Lock() if c, ok := a.applied[module]; ok && c.Err == nil && c.Dig == composed { diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go index a6e7b2f..9887661 100644 --- a/internal/agent/agent_test.go +++ b/internal/agent/agent_test.go @@ -1340,6 +1340,15 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes( want("web", fp, "") eventually(t, "the node's own composition was not applied", func() bool { return len(b.appliedList()) == 1 }) eventually(t, "the have does not name the composed digest", func() bool { return b.haves.last("web").Composed == dig }) + // ADR 0601 §4: what the composer keeps of web as it runs is carried on its have, and goes once it keeps none. + b.ag.KeptNow(context.Background(), map[string][]string{"web": {"web runs the tool runner's process here"}}) + if h := b.haves.last("web"); len(h.Kept) != 1 || h.Composed != dig { + t.Errorf("web's have does not carry what is kept as it runs: %+v", h) + } + b.ag.KeptNow(context.Background(), nil) + if h := b.haves.last("web"); len(h.Kept) != 0 || h.Composed != dig { + t.Errorf("web's have still carries what was kept: %+v", h) + } // The same composition handed again is no move. b.comps.put(ck, craw) time.Sleep(50 * time.Millisecond) diff --git a/internal/compose/frombus/frombus.go b/internal/compose/frombus/frombus.go index 244d26a..ce704e6 100644 --- a/internal/compose/frombus/frombus.go +++ b/internal/compose/frombus/frombus.go @@ -156,7 +156,7 @@ type Scope struct { SameValue func(a, b string) (same, known bool) // Kept is told, in words, each module composed as what runs shows because its manifest does not yet say it (the tool // runner, the core's credentials; ADR 0601 §4), so the node says it. Nil: said nowhere. - Kept func(words string) + Kept func(module, words string) } // InOwn is, of the modules p places on its node, those whose resources the node's own composition it runs @@ -235,9 +235,9 @@ func input(ctx context.Context, r Reader, mesh Mesh, s Scope, refuse func(module in, err := b.finish(ctx, r, r.Build, mesh, s, b.in, refuse, wanted) if err == nil { // What runs is kept while a manifest does not yet say what shared code no longer infers (ADR 0601 §4). - for _, words := range keepWhatRuns(&in, s.Node, s.Running) { + for _, k := range keepWhatRuns(&in, s.Node, s.Running) { if s.Kept != nil { - s.Kept(words) + s.Kept(k.Module, k.Words) } } } diff --git a/internal/compose/frombus/frombus_test.go b/internal/compose/frombus/frombus_test.go index abcb742..7e030c1 100644 --- a/internal/compose/frombus/frombus_test.go +++ b/internal/compose/frombus/frombus_test.go @@ -887,7 +887,7 @@ func whatRunsIsKeptWhileAManifestDoesNotSayIt(t *testing.T) { in := input(false) said := keepWhatRuns(&in, "a", running) if !in.Catalogue["runner-x"].ToolRunner || !in.Catalogue["prov-y"].CoreCredentials || in.Catalogue["app"].ToolRunner || - in.Catalogue["app"].CoreCredentials || len(said) != 2 { + in.Catalogue["app"].CoreCredentials || len(said) != 2 || said[0].Module != "prov-y" || said[1].Module != "runner-x" { t.Errorf("what runs was not kept: %+v, said %q", in.Catalogue, said) } in = input(true) diff --git a/internal/compose/frombus/keep.go b/internal/compose/frombus/keep.go index c766ec8..08f11fa 100644 --- a/internal/compose/frombus/keep.go +++ b/internal/compose/frombus/keep.go @@ -23,9 +23,9 @@ import ( // Known by what runs, never by a module's name. // keepWhatRuns marks, in in, each module placed on node that what runs shows is the tool runner or carries the core's -// credentials, where its manifest does not say so; it returns, sorted, what it marked, in words. The tool runner is marked -// only where no placed module's manifest says it is one. -func keepWhatRuns(in *compose.Input, node string, running []delivery.Composed) []string { +// credentials, where its manifest does not say so; it returns, sorted by module, what it marked, in words. The tool runner is +// marked only where no placed module's manifest says it is one. +func keepWhatRuns(in *compose.Input, node string, running []delivery.Composed) []KeptAsRuns { placed := assignedOn(*in, node) if len(placed) == 0 || len(running) == 0 { return nil @@ -60,7 +60,7 @@ func keepWhatRuns(in *compose.Input, node string, running []delivery.Composed) [ } } } - var said []string + var said []KeptAsRuns saysRunner := false for _, m := range placed { if man, ok := manifestOn(*in, node, m); ok && catalogue.IsToolRunner(man) { @@ -68,6 +68,7 @@ func keepWhatRuns(in *compose.Input, node string, running []delivery.Composed) [ } } mark := func(module string, set func(*catalogue.Manifest), words string) { + k := KeptAsRuns{Module: module, Words: words} for i := range in.Nodes { if in.Nodes[i].Name != node { continue @@ -75,14 +76,14 @@ func keepWhatRuns(in *compose.Input, node string, running []delivery.Composed) [ if man, ok := in.Nodes[i].Kept[module]; ok { set(&man) in.Nodes[i].Kept[module] = man - said = append(said, words) + said = append(said, k) return } } if man, ok := in.Catalogue[module]; ok { set(&man) in.Catalogue[module] = man - said = append(said, words) + said = append(said, k) } } if runner != "" && !saysRunner { @@ -95,6 +96,11 @@ func keepWhatRuns(in *compose.Input, node string, running []delivery.Composed) [ "and its manifest does not say they are the core's credentials (core-credentials): they stay sealed, until its manifest says so", m)) } } - sort.Strings(said) + sort.SliceStable(said, func(i, j int) bool { return said[i].Module < said[j].Module }) return said } + +// KeptAsRuns is one module composed as what runs shows, and why, in words (ADR 0601 §4): its have carries the words. +type KeptAsRuns struct { + Module, Words string +} diff --git a/internal/link/have.go b/internal/link/have.go index 7f94aa6..eb13fb0 100644 --- a/internal/link/have.go +++ b/internal/link/have.go @@ -374,6 +374,7 @@ func (q *Session) writeHaves(ctx context.Context, bus Bus) { f := s.modules[module] h := delivery.Have{V: delivery.Version, FP: f.FP, Composed: f.Composed, Build: f.Build, Phase: delivery.PhaseRunning, Health: delivery.HealthUnknown, Detail: f.Detail} + h.Kept = f.Kept if delivery.Phase(f.Phase) == delivery.PhaseWaitingYes { h.Judgement, h.Waits, h.BuildAlone = delivery.Judgement(f.Judgement), f.Waits, f.BuildAlone } diff --git a/internal/link/messages.go b/internal/link/messages.go index 05ec57a..dd65cb3 100644 --- a/internal/link/messages.go +++ b/internal/link/messages.go @@ -641,6 +641,8 @@ type ModuleFact struct { Waits string // BuildAlone says, in phase waiting-yes, the move changes the build alone (ADR 0601 §1, delivery.Have.BuildAlone). BuildAlone bool + // Kept is what the node keeps of the module as it runs, for a manifest that does not say it yet (delivery.Have.Kept). + Kept []string // Sealed is the values the composition it names carries sealed to this node's key, by slot (delivery.Have.SealedAt). Sealed delivery.Seals } diff --git a/internal/selfcompose/selfcompose.go b/internal/selfcompose/selfcompose.go index 8726b82..a8f81a0 100644 --- a/internal/selfcompose/selfcompose.go +++ b/internal/selfcompose/selfcompose.go @@ -78,6 +78,9 @@ type Env struct { // only within its own module. // An error is a have the bus did not answer for, never one absent: the pass is then tried again (BusFailed). Held func(module string) (slots delivery.Seals, legacy []string, err error) + // Kept is told, after each pass that composed, what the node keeps of each module as it runs because its manifest does + // not yet say it (ADR 0601 §4), by module, so each module's have carries it (agent.KeptNow). Nil tells nobody. + Kept func(kept map[string][]string) // Fetch reads an archive at a URL in the artifact store: a moved module's templates archive, whose texts its built // manifest carries (issue 1126). Read once per digest. Nil reads none, and a moved module does not compose. Fetch func(ctx context.Context, url string) ([]byte, error) @@ -844,7 +847,18 @@ func (c *Composer) compose(ctx context.Context) []agent.Update { } // Its own certificate, asked of the vault when a module here asks for one and the record holds none it trusts. c.certifyIfWanted(ctx, r, own) - scope := frombus.Scope{Node: c.env.Node, KeptAt: kept, InOwn: inOwn, Running: running, Kept: c.sayOnceEach} + // What it keeps of a module as it runs, for a manifest that does not say it yet (ADR 0601 §4): said, and carried on the + // module's have, from which the watchman raises its condition. + keptRuns := map[string][]string{} + var keptMu sync.Mutex + scope := frombus.Scope{Node: c.env.Node, KeptAt: kept, InOwn: inOwn, Running: running, Kept: func(module, words string) { + keptMu.Lock() + defer keptMu.Unlock() + if !slices.Contains(keptRuns[module], words) { + keptRuns[module] = append(keptRuns[module], words) + } + c.sayOnceEach(words) + }} c.mu.Lock() c.dataScope = frombus.Scope{Node: c.env.Node, KeptAt: kept, InOwn: inOwn} // the data items are worked out as composed c.mu.Unlock() @@ -906,6 +920,11 @@ func (c *Composer) compose(ctx context.Context) []agent.Update { done := c.waiting("the agent marking each held module's have") c.refused("", held) done() + if c.env.Kept != nil { + keptMu.Lock() + c.env.Kept(keptRuns) + keptMu.Unlock() + } if len(held) == 0 { c.sayOnce("") } else {