From 5062c36fc9efe159aa9706c0ca2c873351ef1ce0 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 14 Sep 2026 21:11:37 +0200 Subject: [PATCH] A binding answered on this very node still carries an address MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two sibling branches resolve a provision answered by the consumer's own machine. The one for a node-scoped provider falls back to loopback when the machine is on no private network, with a comment saying why and a test holding it. The one for a mesh-scoped provider passed node.At straight through, and nothing noticed because nothing had yet composed a host out of it. The mesh's own artifact store is mesh-scoped and sits on the same machine as the builder that pushes to it. Give the builder the address from its binding and it gets MESH_REGISTRY=:5000 — a name with no host, written into its environment without complaint. It surfaces much later as ":5000/mesh-tools/build" is not a valid repository/tag which is a message about a tag for a fault in how a binding was resolved, on a machine several steps from the decision. A machine off the private network still reaches itself, which is what the neighbouring branch already said. The test fails without the fix, showing the empty address rather than only the symptom. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx --- internal/catalogue/resolve.go | 13 +++++- internal/catalogue/resolve_loopback_test.go | 52 +++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 internal/catalogue/resolve_loopback_test.go diff --git a/internal/catalogue/resolve.go b/internal/catalogue/resolve.go index b92f350..55c00b2 100644 --- a/internal/catalogue/resolve.go +++ b/internal/catalogue/resolve.go @@ -287,8 +287,19 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world if satisfied[want] && !isModule(catalogue, want) { if brokered[want] { // Answered here, and still a need: the provider is this node. + // + // **The same loopback fallback as the branch below, and it was missing here.** A + // machine with no private network has no `at`, and this branch passed that through + // — so a consumer whose file says `${bound::at}:${bound:...:port}` was + // handed `:5000`, a name with no host, written into its environment without + // complaint. The sibling case a few lines down had the fallback and the reasoning + // for it; only this one did not. A machine off the network still reaches itself. + at := node.At + if at == "" { + at = "127.0.0.1" + } needs = append(needs, Needed{ - Name: want, From: node.Name, At: node.At, + Name: want, From: node.Name, At: at, Serves: servedHere(catalogue, chosen, want), For: because[want]}) } else if served := servedHere(catalogue, chosen, want); len(served) > 0 { // Answered here with no credential to mint, but the provider serves facts the diff --git a/internal/catalogue/resolve_loopback_test.go b/internal/catalogue/resolve_loopback_test.go new file mode 100644 index 0000000..d9ca22c --- /dev/null +++ b/internal/catalogue/resolve_loopback_test.go @@ -0,0 +1,52 @@ +package catalogue + +import "testing" + +// A MESH-SCOPED provider on the consumer's own node must deliver a usable address too. +// +// The sibling case — a node-scoped provider minting no credential — has had this fallback and a +// test for it for some time. This branch did not, and the difference is invisible until something +// puts the address into a string: the mesh's own artifact store is mesh-scoped and lives on the +// same machine as the builder that pushes to it, so the builder's environment was written as +// `MESH_REGISTRY=:5000`. Nothing refused it. The runtime did, several steps later, with +// `":5000/mesh-tools/build" is not a valid repository/tag` — a message about a tag, for a fault in +// how a binding was resolved. +// +// A machine off the private network still reaches itself. +func TestAMeshScopedProviderOnThisNodeStillCarriesAnAddress(t *testing.T) { + provider := Manifest{Module: "registry", + Provides: []Offer{{Name: "artifact-store", Scope: ScopeMesh}}, + Serves: map[string]map[string]any{ + "artifact-store": {"port": 5000}}} + consumer := Manifest{Module: "builder", Requires: []string{"artifact-store"}} + + // No `At`: a mesh with no private network raised, which is every mesh before somebody places + // its nodes on one — including the moment just after it is installed. + got, err := Resolve( + map[string]Manifest{"registry": provider, "builder": consumer}, + []string{"builder", "registry"}, + Node{Name: "anchor"}, + World{}) + if err != nil { + t.Fatalf("a mesh-scoped provision answered on this very node was refused: %v", err) + } + + var found *Needed + for i := range got.Needs { + if got.Needs[i].Name == "artifact-store" && got.Needs[i].For == "builder" { + found = &got.Needs[i] + } + } + if found == nil { + t.Fatalf("the store was not delivered to the builder at all: %+v", got.Needs) + } + if found.At == "" { + t.Fatalf("the binding carries no address, so anything composing a host from it gets none: %+v", found) + } + if found.At != "127.0.0.1" { + t.Fatalf("off the private network the address must fall back to loopback, got %q", found.At) + } + if got, want := plainly(found.Serves["port"]), "5000"; got != want { + t.Fatalf("the port was not delivered: got %q want %q", got, want) + } +}