From 91aa7b78fc25517ef2fcdac07856f4905a6e69e1 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 30 Oct 2026 21:34:38 +0100 Subject: [PATCH] Key provisions by one rule the plan, the seat and the assigner share (issue 774) The assigner's rule 8 waited for a key for every `requires`, while the plans named only the needs of handed modules: every new build of grafana, dnsmasq, mesh-knowledge, mesh-mail and the builders waited for a key nobody would make, and grafana, not handed, had no plan, so no route key and no route on route-proxy. catalogue.Keyed is the rule (a need a module answers is keyed when its consumer is handed or the provider's helper answers it), catalogue.Awaited the same rule from manifests for the assigner, and a module not handed now has a plan for what a helper answers of it. Handed modules' plans are unchanged. Quick fix: drop the missing-key check from rule 8. Thorough fix (chosen): one rule in mesh-sdk all three parts use, since the kind (three parts deciding the same set apart) already drifted once and hid failures as waits. --- go/catalogue/declaration.go | 8 ++- go/catalogue/handed.go | 42 +++++++++++++- go/catalogue/keyed.go | 67 +++++++++++++++++++++ go/catalogue/keyed_test.go | 112 ++++++++++++++++++++++++++++++++++++ go/catalogue/resolve.go | 4 ++ go/compose/resolve.go | 11 ++++ 6 files changed, 238 insertions(+), 6 deletions(-) create mode 100644 go/catalogue/keyed.go create mode 100644 go/catalogue/keyed_test.go diff --git a/go/catalogue/declaration.go b/go/catalogue/declaration.go index 962ffbc..69b0a11 100644 --- a/go/catalogue/declaration.go +++ b/go/catalogue/declaration.go @@ -1065,9 +1065,11 @@ func (r Resolution) compose(with Rendering, owner map[string]string, if err != nil { return nil, err } - // **What the mesh calls the module through each provision it requires** (issue 792): the names its binding - // file carries, composed by the same call, handed to the provider through the plan's entry. - if _, ok := handed.plans[m.Module]; ok && handing != nil { + // **A plan for what the seat keys of it all the same** (catalogue.Keyed, issue 774): a module not handed whose + // provider's helper answers a provision of it, its route the commonest, has that provision in a plan of its own, + // so the seat keys it and the helper hears it. + keyedOnly := handing == nil && handed.keyed(r.Node, m, r.Needs) + if _, ok := handed.plans[m.Module]; ok && (handing != nil || keyedOnly) { for _, to := range requirementsContributedTo(m) { own, err := r.ownNames(m, to, with.Settings[m.Module]) if err != nil { diff --git a/go/catalogue/handed.go b/go/catalogue/handed.go index f019c53..0f5e3ec 100644 --- a/go/catalogue/handed.go +++ b/go/catalogue/handed.go @@ -129,11 +129,12 @@ type PlanProvision struct { // provisionsOf is, from a resolution's needs, each provision module requires and its provider, by provision then // local name. A need answered by a record (a licence) or by no module names no provider on a machine and is left // out: the vault has no provider's value to make for it. -func provisionsOf(needs []Needed, module, as string, contributes map[string]map[string]any, +func provisionsOf(needs []Needed, module string, handed bool, as string, contributes map[string]map[string]any, named map[string]bool) []PlanProvision { var out []PlanProvision for _, n := range needs { - if n.For != module || n.ByRecord || n.Module == "" || n.From == "" { + // **The one rule** (Keyed, issue 774): what the seat keys and the want waits for. + if n.For != module || !Keyed(handed, n) { continue } p := PlanProvision{Provision: n.Name, Local: n.Local, Provider: n.From, ProviderModule: n.Module, @@ -589,6 +590,40 @@ type handedFiles struct { required map[string]map[string]bool // names is the names composed for each handed module through each requirement it contributes to (issue 792). names map[string]map[string]map[string]string + // keyedOnly is each module with a plan only for what Keyed keys of a consumer not handed: a provision its + // provider's helper answers (issue 774), such as a route. + keyedOnly map[string]bool +} + +// keyed starts the plan of a module begin started none for, when the composition keys any of its needs as a consumer +// not handed (Keyed, issue 774): one whose provider's helper answers a provision of it (grafana's route). A module +// handed that keeps no template or item (a held one's stub, issue 1126) is not handed here either: what it keeps +// stays the controller's but for what a helper answers. Its plan names provisions alone: no templates, files or +// secrets. Answers whether it started one. +func (h *handedFiles) keyed(node string, m Manifest, needs []Needed) bool { + if _, started := h.plans[m.Module]; started { + return false + } + if !slices.ContainsFunc(needs, func(n Needed) bool { return n.For == m.Module && Keyed(false, n) }) { + return false + } + if h.identities == nil { + h.identities = map[string]string{} + h.required = map[string]map[string]bool{} + } + h.identities[m.Module] = ConsumerIdentity(node, IdentitySource(m.Slug, m.Module)) + h.plans[m.Module] = Plan{} + if h.keyedOnly == nil { + h.keyedOnly = map[string]bool{} + } + h.keyedOnly[m.Module] = true + if len(m.Contributes) > 0 { + if h.contributes == nil { + h.contributes = map[string]map[string]map[string]any{} + } + h.contributes[m.Module] = m.Contributes + } + return true } // name records the names composed for a handed module through one requirement, as its binding file carries them. @@ -768,7 +803,8 @@ func (h *handedFiles) finished(needs []Needed) map[string]Plan { for module, plan := range h.plans { // **Every provision it requires, with its provider** (issue 760); a credential the vault keeps for it alone // is that provision's entry, with its name and file (issue 757). - provisions := provisionsOf(needs, module, h.identities[module], h.contributes[module], h.required[module]) + provisions := provisionsOf(needs, module, !h.keyedOnly[module], h.identities[module], h.contributes[module], + h.required[module]) for _, credential := range h.provisions[module] { i := slices.IndexFunc(provisions, func(p PlanProvision) bool { return p.Provision == credential.Provision && p.Local == credential.Local diff --git a/go/catalogue/keyed.go b/go/catalogue/keyed.go new file mode 100644 index 0000000..6161afd --- /dev/null +++ b/go/catalogue/keyed.go @@ -0,0 +1,67 @@ +package catalogue + +import "slices" + +// The one rule of which provisions the seat mesh-provisioner keys and a want waits for (novox/hq ADR 0570 §1, §2, §4; +// issue 774). The plan composition puts a need in a consumer's plan by it, the holder of mesh-provisioner keys what the +// plans name, and the assigner's rule 8 waits for what Awaited answers: three parts, one rule, so they cannot drift +// again (on 2026-10-30 the assigner waited for every `requires` while the plans named only some, and every new build of +// grafana, dnsmasq, mesh-knowledge, mesh-mail and the builders waited for a key nobody would make). +// +// A need a module answers (not a record, not nothing) is keyed when its consumer is handed to the provisioner, or the +// provider's offer says the provider helper answers it. A need of a consumer not handed whose provider's helper does +// not answer it is still answered by the controller's composition: it is neither keyed nor waited for. + +// HandedByManifest is whether a module is handed to the provisioner as far as its manifest says (Rendering.Handing +// without the machine's own words, and begin's): marked moved, not a held module's stub, with a template its build +// kept or an item. +func (m Manifest) HandedByManifest() bool { + return m.Moved && m.Handed != nil && !m.Stubbed && (len(m.Handed.Templates) > 0 || len(m.Items) > 0) +} + +// Keyed is whether a need is a provision of the seat mesh-provisioner: a key, an entry in its consumer's plan, and a +// wait of the consumer's want. consumerHanded is whether the composition hands the consumer to the provisioner. +func Keyed(consumerHanded bool, n Needed) bool { + if n.ByRecord || n.Module == "" || n.From == "" { + return false + } + return consumerHanded || n.AnsweredByHelper +} + +// Awaited is, of what consumer requires (not optionally), each provision Keyed answers yes for, judged from manifests +// alone, as the assigner reads them: providers answers the modules providing a provision. A provision is a need when a +// provider offers it to the mesh, or one on the consumer's machine serves facts about it (Resolve: one answered on the +// machine with nothing served binds nothing). It is keyed when the consumer is handed, or every provider's offer is +// answered by the helper: where the providers disagree, which one answers is the composition's, and nothing is waited +// for that might never be keyed. Sorted, each once. +func Awaited(consumer Manifest, providers func(provision string) []Manifest) []string { + var out []string + for _, r := range consumer.Requires { + if consumer.OptionalRequires[r] || slices.Contains(out, r) { + continue + } + by := providers(r) + if len(by) == 0 { + continue + } + need, helper := false, true + for _, p := range by { + for _, o := range p.Provides { + if o.Name != r { + continue + } + if o.At() == ScopeMesh || len(p.Serves[r]) > 0 { + need = true + } + } + if !p.AnsweredByHelperFor(r) { + helper = false + } + } + if need && (consumer.HandedByManifest() || helper) { + out = append(out, r) + } + } + slices.Sort(out) + return out +} diff --git a/go/catalogue/keyed_test.go b/go/catalogue/keyed_test.go new file mode 100644 index 0000000..4fffa28 --- /dev/null +++ b/go/catalogue/keyed_test.go @@ -0,0 +1,112 @@ +package catalogue + +import ( + "reflect" + "slices" + "strings" + "testing" +) + +// The one rule replays the live cases of 2026-10-30 (issue 774): what the assigner's rule 8 waits for is what the +// composition keys, judged from the manifests alone. +func TestAwaitedIsWhatTheCompositionKeys(t *testing.T) { + built := &Handed{Source: "store://t", Digest: "sha256:t", Templates: map[string]string{"c": "c"}} + offer := func(module, name string, scope string, helper bool, serves bool) Manifest { + o := Offer{Name: name, Scope: scope} + if helper { + o.AnsweredBy = AnsweredByHelper + } + m := Manifest{Module: module, Provides: []Offer{o}} + if serves { + m.Serves = map[string]map[string]any{name: {"port": 1}} + } + return m + } + gitea, distribution := offer("gitea", "forge-account", ScopeMesh, false, false), offer("distribution", "artifact-store", ScopeMesh, false, false) + goTools, nodeJS := offer("go", "go-toolchain", ScopeNode, false, false), offer("nodejs", "node-toolchain", ScopeNode, false, false) + keycloak, proxy := offer("keycloak", "oidc-client", ScopeMesh, false, false), offer("route-proxy", "route", ScopeMesh, true, false) + adapter := offer("route-adapter", "route", ScopeMesh, false, false) + ollama := offer("ollama", "llm", ScopeNode, false, true) + by := func(ms ...Manifest) func(string) []Manifest { + return func(p string) []Manifest { + var out []Manifest + for _, m := range ms { + if slices.Contains(m.Offers(), p) { + out = append(out, m) + } + } + return out + } + } + for _, c := range []struct { + name string + consumer Manifest + providers func(string) []Manifest + want []string + }{ + {"a handed builder waits for what binds, not for a toolchain on its own machine (mesh-build-go on ace)", + Manifest{Module: "mesh-build-go", Moved: true, Handed: built, + Requires: []string{"forge-account", "go-toolchain", "artifact-store", "node-tools"}}, + by(gitea, distribution, goTools), []string{"artifact-store", "forge-account"}}, + {"node-toolchain alike (mesh-build-node)", + Manifest{Module: "mesh-build-node", Moved: true, Handed: built, Requires: []string{"node-toolchain", "forge-account"}}, + by(gitea, nodeJS), []string{"forge-account"}}, + {"a module not handed waits for its route, which the helper answers, and not for its login the controller still makes (grafana on ace)", + Manifest{Module: "grafana", Requires: []string{"route", "oidc-client", "influxdb-api"}, + OptionalRequires: map[string]bool{"influxdb-api": true}}, + by(keycloak, proxy), []string{"route"}}, + {"where providers disagree on the helper, nothing is waited for that might never be keyed", + Manifest{Module: "grafana", Requires: []string{"route"}}, by(proxy, adapter), nil}, + {"a moved module with no template is not handed (a held stub, issue 1126)", + Manifest{Module: "bot", Moved: true, Handed: &Handed{}, Requires: []string{"oidc-client", "route"}}, + by(keycloak, proxy), []string{"route"}}, + {"a provision on its own machine that serves facts binds, and is keyed for a handed consumer", + Manifest{Module: "chat", Moved: true, Handed: built, Requires: []string{"llm", "llm"}}, by(ollama), []string{"llm"}}, + } { + if got := Awaited(c.consumer, c.providers); !reflect.DeepEqual(got, c.want) { + t.Errorf("%s: awaited %v, want %v", c.name, got, c.want) + } + } +} + +// A module not handed has a plan for what a provider's helper answers of it, and for nothing else (issue 774: grafana +// had no plan, so no route key, and route-proxy served no route for it); a need the controller still answers stays out. +func TestAModuleNotHandedHasAPlanForWhatAHelperAnswers(t *testing.T) { + consumer := notifierConsumer(t, false, false) + consumer.Requires = append(consumer.Requires, "route") + consumer.Contributes = map[string]map[string]any{"route": {"host": "notify.example.org"}} + node := workstation() + node.At = "workstation.internal" + proxy := Manifest{Module: "proxy", Version: "1", Provides: []Offer{{Name: "route", Scope: ScopeMesh, AnsweredBy: AnsweredByHelper}}} + store := Manifest{Module: "store", Version: "1", Provides: []Offer{{Name: "postgres-database", Scope: ScopeMesh, + Credential: &OfferCredential{Form: CredentialFormPair}}}, Serves: map[string]map[string]any{"postgres-database": {"port": 5432}}} + w := twoStores("anchor") + w.Offered["route"] = []Provider{{Node: "anchor", At: "anchor.internal", Module: "proxy"}} + r, err := Resolve(shelf(consumer, proxy, store), []string{consumer.Module}, node, w) + if err != nil { + t.Fatal(err) + } + r.Account = "op" + for i, n := range r.Needs { + // As the plan sets it (compose's answeredByHelper). + r.Needs[i].AnsweredByHelper = n.Name == "route" + } + _, before := composeWith(t, r, Rendering{}) + c, got := composeWith(t, r, handingTo("store:5000")) + if got != before { + t.Fatalf("the plan changed what is composed:\nbefore: %s\nnow: %s", before, got) + } + plan, ok := c.Plans["notify"] + if !ok || len(plan.Provisions) != 1 || plan.Provisions[0].Provision != "route" || plan.Provisions[0].ProviderModule != "proxy" { + t.Fatalf("the plan is %+v (%v), want the route alone", plan, ok) + } + if plan.Templates != (PlanTemplates{}) || plan.Files != nil || plan.Secrets != nil || + !strings.Contains(plan.Provisions[0].As, "notify") || plan.Provisions[0].Contributes["host"] != "notify.example.org" { + t.Fatalf("the plan names %+v", plan) + } + if want := Awaited(consumer, func(p string) []Manifest { + return map[string][]Manifest{"route": {proxy}, "postgres-database": {store}}[p] + }); !reflect.DeepEqual(want, []string{"route"}) { + t.Fatalf("the assigner waits for %v, and the plan keys the route alone", want) + } +} diff --git a/go/catalogue/resolve.go b/go/catalogue/resolve.go index 006646e..f81a39f 100644 --- a/go/catalogue/resolve.go +++ b/go/catalogue/resolve.go @@ -263,6 +263,10 @@ type Needed struct { // then name `${bound::}` and `${secret:}`, which its machine's client fills from the // provider's entry and the vault. Set by the plan, which reads the provider's manifest. ProviderMoved bool + // AnsweredByHelper is set when the providing module's offer says the provider helper answers it (issue 792): a + // provision the seat mesh-provisioner keys whether or not its consumer is handed (Keyed). Set by the plan, which + // reads the provider's manifest. + AnsweredByHelper bool } // Refusal is why a set of assignments cannot become a declaration. diff --git a/go/compose/resolve.go b/go/compose/resolve.go index a481909..356c837 100644 --- a/go/compose/resolve.go +++ b/go/compose/resolve.go @@ -54,6 +54,7 @@ func (c *composer) planFor(nodeName string) (Resolved, error) { // Issued for this holder alone and kept by the vault (issue 757). resolved.Needs[i].IssuedToHolder = issuedToHolder(shelf, n) resolved.Needs[i].ProviderMoved = providerMoved(shelf, n) + resolved.Needs[i].AnsweredByHelper = answeredByHelper(shelf, n) if n.ByRecord { // Answered by something the mesh holds, so there is no pair-wise secret between two machines. licence, _ := c.licence(n.From) @@ -189,6 +190,16 @@ func providerMoved(shelf map[string]catalogue.Manifest, n catalogue.Needed) bool return known && (movedProvider(provider) || provider.AnsweredByHelperFor(n.Name)) } +// answeredByHelper is whether a need's provider offers it answered by the provider helper (issue 792): keyed by the seat +// mesh-provisioner for every consumer, handed or not (catalogue.Keyed, issue 774). +func answeredByHelper(shelf map[string]catalogue.Manifest, n catalogue.Needed) bool { + if n.ByRecord || n.Module == "" { + return false + } + provider, known := shelf[n.Module] + return known && provider.AnsweredByHelperFor(n.Name) +} + // movedProvider is whether a provider's credentials are the vault's: the module is marked moved, or it holds the // mesh-vault seat (issue 793). func movedProvider(m catalogue.Manifest) bool {