From d86baebe9ab396e03d7393cfddfe3d96a35ea03e Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 3 Oct 2026 22:20:33 +0200 Subject: [PATCH 1/6] A plan settles an asked build from the build records (hq issue 214) A merge to the controller's own repository replaces the controller in its first tier; the build that produced the new one was recorded, the plan never heard it, and it waited for ever with every later plan behind it. The record is the fact: a build recorded after the ask is the tier's outcome, whoever was listening when it came. --- cmd/mesh-controller/release_plan.go | 56 ++++++++++++++++++++++++ cmd/mesh-controller/release_plan_test.go | 37 ++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/cmd/mesh-controller/release_plan.go b/cmd/mesh-controller/release_plan.go index 438ea38..26a082c 100644 --- a/cmd/mesh-controller/release_plan.go +++ b/cmd/mesh-controller/release_plan.go @@ -399,6 +399,24 @@ func advanceOnce(ctx context.Context, open *stores, p *inventory.Plan, } return true, nil } + // **Asked: settle from the build records first** (novox/hq 04-ISSUES/214). An outcome is taken + // in by whichever controller hears it, and a merge to the controller's own repository replaces + // the controller in its first tier: the build that produced the new one is recorded, and the + // plan never hears it. The record is the fact; a build recorded after the ask is that tier's + // outcome, whoever was listening. + recorded := map[string][]inventory.Build{} + for _, m := range tier { + if s := p.Modules[m]; s != nil && s.State == "asked" { + builds, err := inv.Builds(ctx, m, 5) + if err != nil { + return false, err + } + recorded[m] = builds + } + } + if settleFromRecords(p, tier, recorded) { + return true, nil + } // Asked: wait for every build. var latest time.Time for _, m := range tier { @@ -755,3 +773,41 @@ func splitList(s string) []string { } return out } + +// settleFromRecords marks every module of the tier still `asked` built — or failed — from a build +// recorded after it was asked, and says whether it changed anything (novox/hq 04-ISSUES/214). +// Newest first, as Builds answers: the first record after the ask is the outcome of that ask. +func settleFromRecords(p *inventory.Plan, tier []string, recorded map[string][]inventory.Build) bool { + changed := false + for _, m := range tier { + s := p.Modules[m] + if s == nil || s.State != "asked" || s.AskedAt == nil { + continue + } + var outcome *inventory.Build + for i := range recorded[m] { + b := recorded[m][i] + if b.At.Before(*s.AskedAt) { + break + } + outcome = &b + } + if outcome == nil { + continue + } + at := outcome.At + if outcome.Worked() { + s.State = "built" + s.BuiltAt = &at + s.Commit = outcome.Commit + } else { + s.State = "failed" + s.Why = outcome.Failed + p.State = inventory.PlanFailed + p.Note = fmt.Sprintf("%s failed to build in tier %d", m, p.Tier) + } + fmt.Printf("%s: %s settled from the build records as %s (%s)\n", p.ID, m, s.State, outcome.ID) + changed = true + } + return changed +} diff --git a/cmd/mesh-controller/release_plan_test.go b/cmd/mesh-controller/release_plan_test.go index c2eeb27..c4428ca 100644 --- a/cmd/mesh-controller/release_plan_test.go +++ b/cmd/mesh-controller/release_plan_test.go @@ -115,3 +115,40 @@ func TestACycleIsOneLastTierAndSaidSo(t *testing.T) { t.Fatalf("a cycle should be one tier of two, said: %v", tiers) } } + +// novox/hq 04-ISSUES/214: a plan whose build outcome was recorded while no controller followed it — +// the controller rebuilding itself — settles from the build records instead of waiting for ever. +func TestAPlanSettlesAnAskedBuildFromTheRecords(t *testing.T) { + asked := time.Date(2026, 10, 3, 19, 20, 0, 0, time.UTC) + p := inventory.Plan{ID: "plan-1", Tiers: [][]string{{"mesh-controller", "builder"}, {"route-proxy"}}, + Modules: map[string]*inventory.PlanModule{ + "mesh-controller": {State: "asked", AskedAt: &asked}, + "builder": {State: "asked", AskedAt: &asked}, + }} + records := map[string][]inventory.Build{ + // Newest first, as Builds answers: the build after the ask is the outcome. + "mesh-controller": { + {ID: "build-2", Commit: "2ebbb799", At: asked.Add(4 * time.Minute)}, + {ID: "build-1", Commit: "06ea2168", At: asked.Add(-10 * time.Minute)}, + }, + // Only a build from before the ask: not this ask's outcome. + "builder": {{ID: "build-0", Commit: "06ea2168", At: asked.Add(-time.Hour)}}, + } + if !settleFromRecords(&p, p.Tiers[0], records) { + t.Fatal("nothing settled, though the controller's build is recorded after the ask") + } + if s := p.Modules["mesh-controller"]; s.State != "built" || s.Commit != "2ebbb799" || s.BuiltAt == nil { + t.Errorf("the controller's ask is %+v, want built from 2ebbb799", s) + } + if s := p.Modules["builder"]; s.State != "asked" { + t.Errorf("an ask with no record after it was settled: %+v", s) + } + + // A failure recorded after the ask fails the plan, as hearing it would have. + q := inventory.Plan{ID: "plan-2", Tiers: [][]string{{"x"}}, + Modules: map[string]*inventory.PlanModule{"x": {State: "asked", AskedAt: &asked}}} + settleFromRecords(&q, q.Tiers[0], map[string][]inventory.Build{"x": {{ID: "b", Failed: "no", At: asked.Add(time.Minute)}}}) + if q.State != inventory.PlanFailed || q.Modules["x"].State != "failed" { + t.Errorf("a recorded failure did not fail the plan: %+v %+v", q, q.Modules["x"]) + } +} From 6784efae756721a335eb0496ebca35964bed8f09 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 3 Oct 2026 22:22:08 +0200 Subject: [PATCH 2/6] A commit is never a branch to follow (hq issue 215) A build asked at a commit recorded that commit as the module's ref. Every merge after it failed to match the module and its plan left it out without a word, and every plan that rebuilt it asked for the same old commit again. Registration now keeps the branch the module followed (the default branch for a new one); matching and re-asking read a recorded commit as the default branch, which heals records already pinned this way; and a merge says which modules of its repository it leaves out because they follow another branch. --- cmd/mesh-controller/build.go | 9 +++++++ cmd/mesh-controller/build_test.go | 37 +++++++++++++++++++++++++++++ cmd/mesh-controller/order_test.go | 24 +++++++++++++++++++ cmd/mesh-controller/release_plan.go | 3 ++- cmd/mesh-controller/upgrades.go | 28 +++++++++++++++++++++- 5 files changed, 99 insertions(+), 2 deletions(-) diff --git a/cmd/mesh-controller/build.go b/cmd/mesh-controller/build.go index 56f7afb..452a74e 100644 --- a/cmd/mesh-controller/build.go +++ b/cmd/mesh-controller/build.go @@ -530,6 +530,15 @@ func takeIn(ctx context.Context, inv *inventory.Inventory, result link.BuildResu if result.Source != nil && result.Source.Seat != "" { recorded.Repository, recorded.Seat = result.Source.Repository, result.Source.Seat } + // **A build at a commit does not change the branch a module follows** (novox/hq 04-ISSUES/215): + // the commit is built and recorded as what it was built from, and the module keeps following + // what it followed before — the repository's default branch for one new to the catalogue. + if followedBranch(result.Ref) == "" && result.Ref != "" { + recorded.Ref = "" + if was, err := inv.SourceOf(ctx, manifest.Module); err == nil { + recorded.Ref = followedBranch(was.Ref) + } + } if err := namesNoInstallation(manifest); err != nil { return manifest, kept, fmt.Errorf("%s built %s (%s), and the mesh does not register it: %w", result.On, result.Repository, short(result.Commit), err) diff --git a/cmd/mesh-controller/build_test.go b/cmd/mesh-controller/build_test.go index a39ef6e..29d0d73 100644 --- a/cmd/mesh-controller/build_test.go +++ b/cmd/mesh-controller/build_test.go @@ -63,3 +63,40 @@ func TestABuildHeardIsRecordedAndRegistered(t *testing.T) { t.Fatalf("a failure is said in the builder's words: %v", err) } } + +// novox/hq 04-ISSUES/215: a build asked at a commit is recorded as built from that commit, and the +// module keeps following the branch it followed — a new one, the default branch. +func TestABuildAtACommitKeepsTheBranchTheModuleFollows(t *testing.T) { + open := aMesh(t) + ctx := t.Context() + manifest, _ := json.Marshal(map[string]any{"module": "unifi", "version": "1"}) + result := func(id, ref, commit string) link.BuildResult { + return link.BuildResult{ID: id, Repository: "http://forge.internal:20000/novox/mesh-catalog.git", + Path: "modules/unifi", Ref: ref, On: "anchor", Commit: commit, Manifest: manifest, + Source: &link.SourceOnSeat{Seat: "git", Repository: "novox/mesh-catalog"}} + } + if _, _, err := takeIn(ctx, open.inventory, result("b-1", "main", "1111111aaaa")); err != nil { + t.Fatal(err) + } + if _, _, err := takeIn(ctx, open.inventory, result("b-2", "9c97a8a", "9c97a8a1d2c3")); err != nil { + t.Fatal(err) + } + src, err := open.inventory.SourceOf(ctx, "unifi") + if err != nil { + t.Fatal(err) + } + if src.Ref != "main" || src.BuiltFrom != "9c97a8a1d2c3" { + t.Errorf("after a build at a commit the module follows %q, built from %q; want main, 9c97a8a1d2c3", src.Ref, src.BuiltFrom) + } + + // One new to the catalogue, first built at a commit, follows the default branch. + other, _ := json.Marshal(map[string]any{"module": "letta", "version": "1"}) + r := result("b-3", "deadbeef", "deadbeefcafe") + r.Manifest, r.Path = other, "modules/letta" + if _, _, err := takeIn(ctx, open.inventory, r); err != nil { + t.Fatal(err) + } + if src, _ := open.inventory.SourceOf(ctx, "letta"); src.Ref != "" { + t.Errorf("a module first built at a commit follows %q, want the default branch", src.Ref) + } +} diff --git a/cmd/mesh-controller/order_test.go b/cmd/mesh-controller/order_test.go index be2c4d0..2acac30 100644 --- a/cmd/mesh-controller/order_test.go +++ b/cmd/mesh-controller/order_test.go @@ -211,3 +211,27 @@ func TestWhatAHandedOverModuleRecordsAboutItsSource(t *testing.T) { } } } + +// novox/hq 04-ISSUES/215: a module once built at a commit still follows its branch — a merge into it +// matches the module, and a plan re-asks the branch, not the old commit. +func TestAModuleBuiltAtACommitStillFollowsItsBranch(t *testing.T) { + m := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main"} + pinned := inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Ref: "9c97a8a"} + if !sourceIs(pinned, m) { + t.Error("a module whose record names a commit is left out of a merge into its branch") + } + full := inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Ref: "9c97a8a1d2c3b4a5f60718293a4b5c6d7e8f9012"} + if !sourceIs(full, m) { + t.Error("a full commit hash is read as a branch") + } + if got := followedBranch("9c97a8a"); got != "" { + t.Errorf("a plan would re-ask the old commit %q", got) + } + if got := followedBranch("release"); got != "release" { + t.Errorf("a branch is not followed as named: %q", got) + } + // A module that follows another branch is still not this merge's. + if sourceIs(inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Ref: "release"}, m) { + t.Error("a module following another branch was matched") + } +} diff --git a/cmd/mesh-controller/release_plan.go b/cmd/mesh-controller/release_plan.go index 438ea38..3cf4c0c 100644 --- a/cmd/mesh-controller/release_plan.go +++ b/cmd/mesh-controller/release_plan.go @@ -272,7 +272,8 @@ func askTier(ctx context.Context, inv *inventory.Inventory, p *inventory.Plan) e } source := buildSource{Repository: e.Source.Repository, Seat: e.Source.Seat} fmt.Printf(" tier %d: ", p.Tier) - if err := buildOne(ctx, source, e.Source.Path, e.Source.Ref, 0); err != nil { + // The branch it follows, never a commit a build once named (novox/hq 04-ISSUES/215). + if err := buildOne(ctx, source, e.Source.Path, followedBranch(e.Source.Ref), 0); err != nil { state.State = "failed" state.Why = err.Error() p.State = inventory.PlanFailed diff --git a/cmd/mesh-controller/upgrades.go b/cmd/mesh-controller/upgrades.go index 0aa9ffd..b9d9cd2 100644 --- a/cmd/mesh-controller/upgrades.go +++ b/cmd/mesh-controller/upgrades.go @@ -5,6 +5,7 @@ import ( "errors" "flag" "fmt" + "regexp" "strings" "time" @@ -283,6 +284,14 @@ func (f following) SourceMoved(ctx context.Context, m link.SourceMoved) error { if isHistory(m.MergedAt, lastLookAt(entries, m)) { packaging = nil } + // Said, never silent (novox/hq 04-ISSUES/215): a module built from this repository that follows + // another branch is not part of this merge, and whoever is waiting for its change should read why. + for _, e := range entries { + if sameRepository(e.Source.Repository, m) && !sourceIs(e.Source, m) { + fmt.Printf(" %s is built from %s/%s and follows %s, not %s; this merge leaves it out\n", + e.Manifest.Module, m.Owner, m.Repo, e.Source.Ref, m.Base) + } + } touched := whatTheMergeTouched(from, entries, m) for _, e := range touched { if err := inv.SourceMoved(ctx, e.Manifest.Module, m.Commit); err != nil { @@ -345,7 +354,24 @@ func sourceIs(s inventory.Source, m link.SourceMoved) bool { if !sameRepository(s.Repository, m) { return false } - return s.Ref == "" || s.Ref == m.Base + ref := followedBranch(s.Ref) + return ref == "" || ref == m.Base +} + +// commitRef is a ref that names a commit rather than a branch: what `build --ref ` asks for. +var commitRef = regexp.MustCompile(`^[0-9a-f]{7,40}$`) + +// followedBranch is the branch a recorded ref means a module follows (novox/hq 04-ISSUES/215). **A +// commit is never a branch to follow.** A build asked at a commit — to try one, or to pin it during a +// fix — recorded that commit as the module's ref; every merge after it then failed to match the +// module, its plan left it out without saying so, and every plan that rebuilt it asked for that same +// old commit again. A commit recorded so is read as the repository's default branch, which is what +// the module followed before it; a branch is followed as named. +func followedBranch(ref string) string { + if commitRef.MatchString(strings.TrimSpace(ref)) { + return "" + } + return ref } // sameRepository is whether a recorded repository is the one a merge names, in either spelling it From cf2bb3b87d3ee56779cc086c8be5854e37ce5fc5 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 3 Oct 2026 22:25:34 +0200 Subject: [PATCH 3/6] A bundle nothing would deliver is refused at registration (hq issue 216) The composer delivers a bundle when the runtime loads from it, a resource names it, or it is the runtime; one reached by none of them was built, recorded and pushed as success and was simply absent. Seven modules' tools went missing that way. Refused at registration, naming the field that would deliver it. --- internal/catalogue/build.go | 30 ++++++++++++++++++++++++++++++ internal/catalogue/manifest.go | 1 + internal/catalogue/runtime_test.go | 25 +++++++++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/internal/catalogue/build.go b/internal/catalogue/build.go index 1bc31c6..930c69a 100644 --- a/internal/catalogue/build.go +++ b/internal/catalogue/build.go @@ -436,3 +436,33 @@ func BinaryOf(a Artifact) string { } return a.Name } + +// undeliveredBundles says which of a module's bundles nothing would ever put on a machine (novox/hq +// 04-ISSUES/216). A bundle reaches a machine three ways: the node's runtime serves it (it says +// `loads`, or its module declares `tools`), a resource names it (a process, a step, an archive), or +// it is the runtime itself. One reached by none of them was built, recorded and pushed as success, +// and was simply absent — seven modules' tools went missing that way on 2026-10-03. Refused here, +// naming the field that would deliver it. +func undeliveredBundles(m Manifest) []string { + if m.Build == nil || m.Module == RuntimeModule { + return nil + } + named := map[string]bool{} + for _, r := range m.Resources { + if a, ok := r["artifact"].(string); ok && a != "" { + named[a] = true + } + } + var problems []string + for _, a := range m.Build.Artifacts { + if a.Kind != ArtifactBundle || named[a.Name] || len(a.Loads) > 0 || len(m.Tools) > 0 { + continue + } + problems = append(problems, fmt.Sprintf( + "%s: the bundle %q would be built and never reach a machine: nothing loads it, runs it or "+ + "unpacks it. A tools bundle says `loads` (the entrypoints the node's runtime serves) or its "+ + "module lists its `tools`; a daemon or a step is a resource naming it (novox/hq 04-ISSUES/216)", + m.Module, a.Name)) + } + return problems +} diff --git a/internal/catalogue/manifest.go b/internal/catalogue/manifest.go index 01686d5..aef8497 100644 --- a/internal/catalogue/manifest.go +++ b/internal/catalogue/manifest.go @@ -1373,6 +1373,7 @@ func ParseManifest(raw []byte) (Manifest, error) { } } problems = append(problems, m.Build.problems(m.Module)...) + problems = append(problems, undeliveredBundles(m)...) // **What provides the artifact store cannot be delivered through it** (novox/hq 04-ISSUES/029). // // Building publishes to the store, and the builder will not start without one. So a module diff --git a/internal/catalogue/runtime_test.go b/internal/catalogue/runtime_test.go index fa4dc43..b7f2173 100644 --- a/internal/catalogue/runtime_test.go +++ b/internal/catalogue/runtime_test.go @@ -389,3 +389,28 @@ func TestARuntimeCompiledToABinaryRunsItself(t *testing.T) { t.Errorf("the Go runtime is not told what to serve or whose it is: %v %v", env, process["user"]) } } + +// novox/hq 04-ISSUES/216: a bundle nothing loads, runs or unpacks is refused at registration; saying +// `loads`, listing `tools`, or a resource naming it admits it. +func TestABundleNothingDeliversIsRefused(t *testing.T) { + base := func() Manifest { + return Manifest{Module: "baserow", Version: "1", Build: &Build{Artifacts: []Artifact{ + {Name: "tools", Kind: ArtifactBundle, Language: "typescript", Entrypoints: []string{"tools/index.js"}}}}} + } + if p := undeliveredBundles(base()); len(p) != 1 || !strings.Contains(p[0], "never reach a machine") { + t.Fatalf("a bundle nothing delivers was admitted: %v", p) + } + loads := base() + loads.Build.Artifacts[0].Loads = []string{"tools/index.js"} + tools := base() + tools.Tools = []string{"baserow_list_rows"} + run := base() + run.Resources = []map[string]any{{"id": "daemon", "type": "process", "artifact": "tools", "run": []any{"node", "tools/index.js"}}} + runtime := base() + runtime.Module = RuntimeModule + for name, m := range map[string]Manifest{"loads": loads, "tools": tools, "a process": run, "the runtime": runtime} { + if p := undeliveredBundles(m); len(p) != 0 { + t.Errorf("a bundle delivered by %s was refused: %v", name, p) + } + } +} From 8b016cc62b23b77b283751da73c56f49a76b438e Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 3 Oct 2026 22:27:01 +0200 Subject: [PATCH 4/6] A Go tools bundle is served by its binary (hq ADR 0193) A bundle compiled to a binary has no entrypoints, and loads had to name one, so a Go bundle could not be served. Its binary is what the runtime starts: loads names the binary, derived when the module lists tools, and the runtime is told the binary's path, delivered like any tools bundle. --- internal/catalogue/build.go | 11 ++++++++ internal/catalogue/runtime_test.go | 43 ++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/internal/catalogue/build.go b/internal/catalogue/build.go index 1bc31c6..6e3cb08 100644 --- a/internal/catalogue/build.go +++ b/internal/catalogue/build.go @@ -87,6 +87,12 @@ func (m Manifest) Resolve(built []Built) (Manifest, error) { loads := append([]string(nil), a.Loads...) if a.Loads == nil && len(m.Tools) > 0 { loads = append([]string(nil), a.Entrypoints...) + // A bundle compiled to a binary has no entrypoints: the binary is what it is, and what + // the runtime starts to serve it (novox/hq ADR 0193). So a Go tools bundle is served + // as Go — the runtime execs it — exactly as a TypeScript one is through its launcher. + if bin := BinaryOf(a); bin != "" { + loads = []string{bin} + } } // **Kept, never routed** (ADR 0155): the builder publishes to the store at the address // it reached it by, and a manifest carrying that address names an installation — @@ -242,6 +248,11 @@ func (b *Build) problems(module string) []string { for _, e := range a.Entrypoints { found = found || e == load } + // A bundle compiled to a binary is one executable: the runtime loads that or nothing + // (novox/hq ADR 0193). + if bin := BinaryOf(a); bin != "" { + found = load == bin + } if !found { problems = append(problems, fmt.Sprintf( "%s: %q says the runtime loads %q, which is not among its entrypoints — "+ diff --git a/internal/catalogue/runtime_test.go b/internal/catalogue/runtime_test.go index fa4dc43..216c46e 100644 --- a/internal/catalogue/runtime_test.go +++ b/internal/catalogue/runtime_test.go @@ -389,3 +389,46 @@ func TestARuntimeCompiledToABinaryRunsItself(t *testing.T) { t.Errorf("the Go runtime is not told what to serve or whose it is: %v %v", env, process["user"]) } } + +// novox/hq ADR 0193: a Go tools bundle is served — its binary is what the runtime starts, delivered +// like any tools bundle, named to the runtime where a TypeScript bundle names its launcher. +func TestAGoToolsBundleIsServedByItsBinary(t *testing.T) { + with := Rendering{ArtifactStore: "anchor.internal:5101", + Needed: map[string]map[string]string{RuntimeModule: {"broker": "sealed-credential"}}} + lamp := Manifest{Module: "lamp", Version: "1", Tools: []string{"on"}, + Build: &Build{Artifacts: []Artifact{{Name: "tools", Kind: ArtifactBundle, Language: "go", + System: "arch", From: "cmd/lamp-tools"}}}} + if p := lamp.Build.problems("lamp"); len(p) != 0 { + t.Fatalf("a Go tools bundle was refused: %v", p) + } + lamp, err := lamp.Resolve([]Built{{Name: "tools", Kind: ArtifactBundle, + Reference: ArtifactStoreScheme + "lamp/tools/blobs/" + bundleDigest, Digest: bundleDigest}}) + if err != nil { + t.Fatal(err) + } + if fmt.Sprint(lamp.Bundles[0].Loads) != "[lamp-tools]" { + t.Fatalf("the runtime loads %v from a Go bundle, want its binary", lamp.Bundles[0].Loads) + } + out, err := Resolution{Node: "anchor", Account: "ops", Modules: []Manifest{lamp, theRuntime(t)}}.Declaration(with) + if err != nil { + t.Fatal(err) + } + if fileNamed(out, "lamp."+BundleID("tools")) == nil { + t.Errorf("the Go bundle is not delivered: %v", ids(out)) + } + env := fileNamed(out, RuntimeModule+"."+RuntimeProcessID())["env"].(map[string]string) + if env[RuntimeToolModules] != "lamp="+BundlePath("lamp", "tools")+"/lamp-tools" { + t.Errorf("the runtime is told %q, want the binary", env[RuntimeToolModules]) + } + + // An artifact may say it explicitly; naming anything but the binary is refused. + said := Manifest{Module: "lamp", Version: "1", Build: &Build{Artifacts: []Artifact{{Name: "tools", + Kind: ArtifactBundle, Language: "go", System: "arch", Binary: "lamp", Loads: []string{"lamp"}}}}} + if p := said.Build.problems("lamp"); len(p) != 0 { + t.Errorf("loads naming the binary was refused: %v", p) + } + said.Build.Artifacts[0].Loads = []string{"tools/index.js"} + if p := said.Build.problems("lamp"); len(p) == 0 { + t.Error("a Go bundle loading a file it does not contain was admitted") + } +} From ac9c2d57be33ecd65fa324ad89bce2a74fbba69a Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 3 Oct 2026 22:30:55 +0200 Subject: [PATCH 5/6] The node's runtime reads the consumers of the modules it carries (hq ADR 0198) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A module's long-running code is a bundle the runtime launches, and the runtime is its bus: it binds the module's own durable consumer — EVENTS, _, still the controller's to make from the module's principal — and acknowledges what the module's code took. So the runtime principal is granted, for each carried module that consumes, exactly what that module's own principal has for its consumer: its info, its next message, its ack subject. Nothing is pushed to it; it pulls. ADR 0175's "consumes nothing" no longer holds. Memberships need nothing new: the consumer's name is derived, as the module's own runtime derived it. --- internal/broker/nats.go | 20 ++++++++++++++++++-- internal/broker/nats_test.go | 27 +++++++++++++++++++-------- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/internal/broker/nats.go b/internal/broker/nats.go index c5c01c8..86ecace 100644 --- a/internal/broker/nats.go +++ b/internal/broker/nats.go @@ -469,8 +469,24 @@ func PermissionsFor(p Principal) (Permissions, error) { // request once, so the runtime announces everything it carries under its own name. sub = append(sub, announcing(append([]string{RuntimeModule}, serves...)...)...) pub = append(pub, discovering()...) - // Nothing about consumers: it consumes nothing. A module's reactions to events are its - // own long-lived process, which ADR 0175 leaves where it is; what moves here is tools. + // **And it consumes for the modules it carries** (novox/hq ADR 0198, which changes ADR 0175's + // "it consumes nothing"): a module's long-running code is a bundle this runtime launches, and + // the runtime is its bus — it reads the module's own durable consumer and acknowledges what + // the module's code took. Exactly the grants the module's own principal has for that consumer, + // on its name and no other's: asking about it, pulling from it, acknowledging it. The + // consumer is still the controller's to make, from the module's own principal. + for _, d := range p.Carries { + own := Principal{Kind: KindModule, Node: p.Node, Module: d.Module, Emits: d.Emits, + Consumes: d.Consumes, Serves: d.Serves, Holds: d.Holds, Uses: d.Uses, Watches: d.Watches} + if _, consumes := ConsumerFor(own); !consumes { + continue + } + stream, durable := consumerStream(own), consumerDurable(own) + pub = append(pub, + "$JS.API.CONSUMER.INFO."+stream+"."+durable, + "$JS.API.CONSUMER.MSG.NEXT."+stream+"."+durable, + "$JS.ACK."+stream+"."+durable+".>") + } sub = unique(sub) pub = unique(pub) } diff --git a/internal/broker/nats_test.go b/internal/broker/nats_test.go index dad5ed9..404919b 100644 --- a/internal/broker/nats_test.go +++ b/internal/broker/nats_test.go @@ -378,7 +378,7 @@ func TestAModulePullsItsOwnConsumerAndNoOthers(t *testing.T) { // their tools (novox/hq ADR 0175): every carried module's tool namespace, every held seat's verbs // on this node, every module's membership on this node, and a call to anything. Nothing it // consumes, because it reacts to nothing. -func TestTheRuntimeServesTheUnionAndConsumesNothing(t *testing.T) { +func TestTheRuntimeServesTheUnionAndConsumesForItsModules(t *testing.T) { filter := Seat{Name: "node-packet-filter", Scope: "node", Serves: []string{"rules", "reload"}} p := Principal{Kind: KindNodeTools, Node: "anchor", Module: RuntimeModule, Carries: []Declared{ {Module: "nftables", Holds: []Seat{filter}, Serves: []string{"firewall_rules"}}, @@ -408,22 +408,33 @@ func TestTheRuntimeServesTheUnionAndConsumesNothing(t *testing.T) { t.Errorf("the runtime may not publish %s: %v", want, perms.Publish) } } - // Nothing of what a carried module consumes, and no consumer of its own to ack. - for _, s := range perms.Subscribe { - if strings.Contains(s, ".event.") || strings.HasPrefix(s, "_DELIVER.") { - t.Errorf("the runtime was granted a delivery it has no consumer for: %s", s) + // It reads the consumer of every carried module that consumes — that module's, by its name, as + // the module's own principal could (novox/hq ADR 0198) — and of no module that consumes nothing. + for _, want := range []string{ + "$JS.API.CONSUMER.INFO.EVENTS.anchor_zsh", + "$JS.API.CONSUMER.MSG.NEXT.EVENTS.anchor_zsh", + "$JS.ACK.EVENTS.anchor_zsh.>", + } { + if !contains(perms.Publish, want) { + t.Errorf("the runtime may not read zsh's consumer: %s missing from %v", want, perms.Publish) } } for _, s := range perms.Publish { - if strings.HasPrefix(s, "$JS.ACK.") || strings.Contains(s, "CONSUMER") { - t.Errorf("the runtime was granted a consumer's subject and has no consumer: %s", s) + if (strings.HasPrefix(s, "$JS.ACK.") || strings.Contains(s, "CONSUMER")) && !strings.Contains(s, "anchor_zsh") { + t.Errorf("the runtime was granted a consumer no carried module of it consumes on: %s", s) + } + } + // It pulls; nothing is pushed to it, and it subscribes no event subject directly. + for _, s := range perms.Subscribe { + if strings.Contains(s, ".event.") || strings.HasPrefix(s, "_DELIVER.") { + t.Errorf("the runtime was granted a delivery: %s", s) } } if !perms.AllowResponses { t.Error("the runtime answers what it is asked, and may not reply") } if _, needed := ConsumerFor(p); needed { - t.Error("a consumer would be made for the runtime, which consumes nothing") + t.Error("a consumer would be made for the runtime itself; it reads its modules' consumers, never one of its own") } // Each subject once in each list: the file is read as the mesh's authority model. One subject may // stand in both — the runtime answers discovery on `$SRV.INFO` and, as the console, asks it From 74efe8e2e7bc0fb8481b55875f73e668ed2315a9 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 3 Oct 2026 23:15:57 +0200 Subject: [PATCH 6/6] Tests follow the grants and issue 203: a person may ask what answers; the resolver test mints its credential --- cmd/mesh-controller/network_test.go | 6 ++++++ internal/inventory/people_test.go | 11 +++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/cmd/mesh-controller/network_test.go b/cmd/mesh-controller/network_test.go index 6c76bed..adea345 100644 --- a/cmd/mesh-controller/network_test.go +++ b/cmd/mesh-controller/network_test.go @@ -266,6 +266,12 @@ func TestTheResolverIsToldEveryMachineOnTheNetworkAndToldAgainWhenOneLeaves(t *t if _, err := assign(ctx, open, "anchor", "dnsmasq"); err != nil { t.Fatal(err) } + // Its bus credential, as assigning issues it where the bus is reachable (novox/hq issue 203): + // no bus is known to this test, so it is minted here, or composing refuses the placeholder. + if _, err := open.inventory.MintBusPassword(ctx, inventory.BusUser{ + Username: "anchor.dnsmasq", Kind: inventory.BusModule, Node: "anchor", Module: "dnsmasq"}); err != nil { + t.Fatal(err) + } zones := func() string { t.Helper() for _, r := range composed(t, open, "anchor").Resources { diff --git a/internal/inventory/people_test.go b/internal/inventory/people_test.go index a3a4ac6..7df4b15 100644 --- a/internal/inventory/people_test.go +++ b/internal/inventory/people_test.go @@ -44,8 +44,15 @@ func TestAPersonMayCallToolsAndNothingElse(t *testing.T) { } // The one tool, both ways it is addressed (novox/hq ADR 0159): to whichever instance // answers, and to the instance on one machine. Nothing else. - if len(perms.Publish) != 2 || perms.Publish[0] != "mesh.mod.mesh-catalog.tool.catalog_tools" || - perms.Publish[1] != "mesh.mod.mesh-catalog.tool.catalog_tools.*" { + // And asking what answers (novox/hq ADR 0197), which claims nothing and calls nothing. + var tools []string + for _, s := range perms.Publish { + if !strings.HasPrefix(s, "$SRV.") { + tools = append(tools, s) + } + } + if len(tools) != 2 || tools[0] != "mesh.mod.mesh-catalog.tool.catalog_tools" || + tools[1] != "mesh.mod.mesh-catalog.tool.catalog_tools.*" { t.Errorf("ada may publish %v, which should be the one tool, both ways addressed, and nothing else", perms.Publish) } for _, s := range perms.Publish {