From ca6d8cd8eb9e183ea05a92a51c78d67e08c9c2a6 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 01:21:09 +0100 Subject: [PATCH] Fix 9.7e: route-proxy names the seats it reads (seat:mesh-provisioner, seat:mesh-module-assigner), no module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds: design 47 G A-116 (§636, §616) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- .../route-proxy/cmd/route-provider/main.go | 17 ++++--- .../cmd/route-provider/manifest_test.go | 4 +- .../cmd/route-provider/provider.go | 7 +-- .../cmd/route-provider/provider_test.go | 11 +++-- .../route-provider/provisioner_names_test.go | 19 +++++--- modules/route-proxy/cmd/route-proxy/bus.go | 45 +++++++++++++++---- .../route-proxy/cmd/route-proxy/names_test.go | 30 ++++++++++++- modules/route-proxy/go.mod | 1 + modules/route-proxy/module.json | 9 ++-- 9 files changed, 109 insertions(+), 34 deletions(-) diff --git a/modules/route-proxy/cmd/route-provider/main.go b/modules/route-proxy/cmd/route-provider/main.go index 3e294ca..fdc73bc 100644 --- a/modules/route-proxy/cmd/route-provider/main.go +++ b/modules/route-proxy/cmd/route-provider/main.go @@ -12,7 +12,10 @@ import ( "time" "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider" + meshprovisioner "git.novox.be/novox/mesh-contracts/provisioning/go/seats/meshprovisioner" stdio "git.novox.be/novox/mesh-sdk/go" + "git.novox.be/novox/mesh-sdk/go/busretry" + "git.novox.be/novox/mesh-sdk/go/seatlist" "route-proxy/contract" ) @@ -26,11 +29,19 @@ func main() { if node == "" { say("MESH_NODE is not set; answering no consumers") } else { - p := newProvider(node, provider.StateBus(ModuleName, provisionerModule), say) go func() { // The helper's watches go through the runtime's channel, which Serve holds: one asked before it does is // refused ("before Serve"), Run returns and the helper stays stopped until the process restarts. <-stdio.Served() + // The holder of the seat mesh-provisioner is read from the seat that places modules, not named here (A-116). + holder, err := busretry.DoRepeatable("who holds "+meshprovisioner.Seat, func() (string, error) { + return seatlist.HolderOf(stdio.Ask, meshprovisioner.Seat) + }, nil) + if err != nil { + say("the provider helper cannot start: %v", err) + return + } + p := newProvider(node, holder, provider.StateBus(ModuleName, holder), say) if err := p.Run(context.Background()); err != nil { say("the provider helper stopped: %v", err) } @@ -43,7 +54,3 @@ func main() { os.Exit(1) } } - -// provisionerModule is the holder of the seat mesh-provisioner, named from this module's manifest (its `reads` of plans and -// provisions; TestTheProvisionerIsTheManifests). -const provisionerModule = "mesh-provisioner" diff --git a/modules/route-proxy/cmd/route-provider/manifest_test.go b/modules/route-proxy/cmd/route-provider/manifest_test.go index 8d54a5b..6c9c6b6 100644 --- a/modules/route-proxy/cmd/route-provider/manifest_test.go +++ b/modules/route-proxy/cmd/route-provider/manifest_test.go @@ -88,8 +88,8 @@ func TestRouteProxyKeepsItsPublicAccountDirectory(t *testing.T) { // seat mesh-provisioner's plans alone (issue 774, research 072 row 34): it reads them, and receives no file. func TestRouteProxyRunsItsMountedProgramOnTheTrustImage(t *testing.T) { m := ownManifest(t) - if !slices.Contains(m.Reads, "mesh-provisioner.plans") { - t.Errorf("route-proxy does not read mesh-provisioner.plans, where its routes are: %v", m.Reads) + if !slices.Contains(m.Reads, "seat:mesh-provisioner.plans") { + t.Errorf("route-proxy does not read seat:mesh-provisioner.plans, where its routes are: %v", m.Reads) } if len(m.Receives) > 0 { t.Errorf("route-proxy receives %v: a route is served from the seat's plans, never a received file", m.Receives) diff --git a/modules/route-proxy/cmd/route-provider/provider.go b/modules/route-proxy/cmd/route-provider/provider.go index efb6761..63dbdab 100644 --- a/modules/route-proxy/cmd/route-provider/provider.go +++ b/modules/route-proxy/cmd/route-provider/provider.go @@ -22,12 +22,13 @@ const ( ModuleName = contract.Module ) -// newProvider is route-proxy answering its consumers over bus on node: the route, served as the manifest serves it +// newProvider is route-proxy answering its consumers over bus on node, the plans and provisions being those of provisioner, the +// module holding the seat mesh-provisioner (read by seatlist.HolderOf; design 47 G A-116): the route, served as the manifest serves it // (nothing of its own), with no credential, and nothing to make or retire for a consumer. -func newProvider(node string, bus provider.Bus, logf func(string, ...any)) *provider.Provider { +func newProvider(node, provisioner string, bus provider.Bus, logf func(string, ...any)) *provider.Provider { return provider.New(provider.Options{ Node: node, - Module: ModuleName, Provisioner: provisionerModule, + Module: ModuleName, Provisioner: provisioner, Offers: map[string]provider.Offer{ProvisionName: { Credential: provider.NoCredential, Serves: map[string]any{}, diff --git a/modules/route-proxy/cmd/route-provider/provider_test.go b/modules/route-proxy/cmd/route-provider/provider_test.go index 9289486..6a06d2f 100644 --- a/modules/route-proxy/cmd/route-provider/provider_test.go +++ b/modules/route-proxy/cmd/route-provider/provider_test.go @@ -63,7 +63,7 @@ func TestAConsumerOfARouteIsToldItsNamesFromItsPlanEntry(t *testing.T) { t.Fatal(err) } key := "g14." + module - if err := bus.Put(context.Background(), meshprovisionerclient.PlansBucketOf(provisionerModule), key, plan); err != nil { + if err := bus.Put(context.Background(), meshprovisionerclient.PlansBucketOf(theProvisioner), key, plan); err != nil { t.Fatal(err) } asked, err := json.Marshal(meshprovisionerclient.Provision{V: meshprovisionerclient.Version, Provision: ProvisionName, @@ -72,11 +72,11 @@ func TestAConsumerOfARouteIsToldItsNamesFromItsPlanEntry(t *testing.T) { if err != nil { t.Fatal(err) } - if err := bus.Put(context.Background(), meshprovisionerclient.ProvisionsBucketOf(provisionerModule), key+"."+ProvisionName, asked); err != nil { + if err := bus.Put(context.Background(), meshprovisionerclient.ProvisionsBucketOf(theProvisioner), key+"."+ProvisionName, asked); err != nil { t.Fatal(err) } } - p := newProvider("novox", bus, t.Logf) + p := newProvider("novox", theProvisioner, bus, t.Logf) out, err := p.Pass(context.Background()) if err != nil { t.Fatal(err) @@ -127,7 +127,7 @@ func TestRouteProxyIsNotMovedAndKeepsItsOwnFiles(t *testing.T) { t.Fatalf("route is not said to be answered by the helper: %v", provides) } // Its states and its tools are what it says on the bus, not what it runs or writes (issue 774). - for _, k := range []string{"build", "provides", "state", "reads", "tools", "replaces"} { + for _, k := range []string{"build", "provides", "state", "reads", "invokes", "tools", "replaces"} { delete(m, k) } // Refreshed on purpose for the events its contract names (`emits`: route.served, route.withdrawn, route.refused), and for the @@ -158,3 +158,6 @@ func TestRouteProxyIsNotMovedAndKeepsItsOwnFiles(t *testing.T) { } } } + +// theProvisioner is the module a test has hold the seat mesh-provisioner: the code reads it, and names none. +const theProvisioner = "a-provisioner" diff --git a/modules/route-proxy/cmd/route-provider/provisioner_names_test.go b/modules/route-proxy/cmd/route-provider/provisioner_names_test.go index 9a3325a..a9b0289 100644 --- a/modules/route-proxy/cmd/route-provider/provisioner_names_test.go +++ b/modules/route-proxy/cmd/route-provider/provisioner_names_test.go @@ -5,24 +5,31 @@ import ( "os" "slices" "testing" + + "git.novox.be/novox/mesh-contracts/provisioning/go/seats/meshprovisioner" + "git.novox.be/novox/mesh-sdk/go/seatlist" ) -// The module holding the seat mesh-provisioner, which this provider reads the plans and provisions of, is the one its manifest's -// `reads` name: no client names a holder (novox/hq ADR 0625 §4). +// The seat mesh-provisioner, whose plans and provisions this provider reads, is named by the seat in its manifest's `reads` +// (`seat:.`), and the manifest invokes the verb that says who holds it: no module is named (design 47 G A-116). func TestTheProvisionerIsTheManifests(t *testing.T) { raw, err := os.ReadFile("../../module.json") if err != nil { t.Fatal(err) } var m struct { - Reads []string `json:"reads"` + Reads []string `json:"reads"` + Invokes []string `json:"invokes"` } if err := json.Unmarshal(raw, &m); err != nil { t.Fatal(err) } - for _, state := range []string{"plans", "provisions"} { - if !slices.Contains(m.Reads, provisionerModule+"."+state) { - t.Errorf("the provider reads %s.%s, its manifest's reads are %v", provisionerModule, state, m.Reads) + for _, state := range []string{meshprovisioner.StatePlans, meshprovisioner.StateProvisions} { + if want := "seat:" + meshprovisioner.Seat + "." + state; !slices.Contains(m.Reads, want) { + t.Errorf("the provider reads %s, its manifest's reads are %v", want, m.Reads) } } + if want := seatlist.HolderKey(); !slices.Contains(m.Invokes, want) { + t.Errorf("the provider asks %s for the holder, its manifest invokes %v", want, m.Invokes) + } } diff --git a/modules/route-proxy/cmd/route-proxy/bus.go b/modules/route-proxy/cmd/route-proxy/bus.go index f36a2ef..faf78f2 100644 --- a/modules/route-proxy/cmd/route-proxy/bus.go +++ b/modules/route-proxy/cmd/route-proxy/bus.go @@ -11,19 +11,39 @@ import ( "github.com/nats-io/nats.go" "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient" + "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner" + meshprovisioner "git.novox.be/novox/mesh-contracts/provisioning/go/seats/meshprovisioner" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/announce" + "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/process" + "git.novox.be/novox/mesh-sdk/go/seatlist" "route-proxy/contract" ) -// assignmentsRead is the seat mesh-module-assigner's state assignments as this module's manifest reads it (`reads`); no seat's -// client names a holder (novox/hq ADR 0625 §4), so the name is kept here and held to the manifest by TestTheReadIsTheManifests. -const assignmentsRead = "mesh-delivery-assigner.assignments" +// The states this proxy reads are those of the holders of two seats, named in the manifest's `reads` by the seat +// (`seat:.`, design 47 G A-116): assignmentsRead the seat mesh-module-assigner's assignments, plansRead the seat +// mesh-provisioner's plans. The holder of each is read where the state is read (holderRead), so no module is named here; +// TestTheReadIsTheManifests holds both to the manifest. +const ( + assignmentsRead = "seat:" + meshmoduleassigner.Seat + "." + meshmoduleassigner.StateAssignments + plansRead = "seat:" + meshprovisioner.Seat + "." + meshprovisioner.StatePlans +) -// plansRead is the seat mesh-provisioner's state plans as this module's manifest reads it (`reads`; TestTheReadIsTheManifests). -const plansRead = "mesh-provisioner.plans" +// holderRead is a read in the seat form as the bus names it, `.`: the module on record as the holder of the seat, +// read by seatlist.HolderOf through call. +func holderRead(call seatlist.HolderCaller, read string) (string, error) { + seat, state, ok, err := catalogue.SeatRead(read) + if err != nil || !ok { + return "", fmt.Errorf("%q is not a read of a seat's state: %v", read, err) + } + holder, err := seatlist.HolderOf(call, seat) + if err != nil { + return "", err + } + return holder + "." + state, nil +} // keptStates is every state route-proxy keeps, as both its halves write them: the routes and the refusals this // proxy writes, readable by others, and the bound entries the provider helper writes. @@ -80,10 +100,17 @@ func followOnce(path string, held *table) error { } } // Who is inside first, so the first table is served to the machines it is meant for. - if err := followInside(conn.Read(assignmentsRead), held); err != nil { - log.Printf("cannot follow %s, so an internal name is served to this machine alone: %v", assignmentsRead, err) + if inside, err := holderRead(conn.CallKept, assignmentsRead); err != nil { + log.Printf("cannot read who holds %s, so an internal name is served to this machine alone: %v", assignmentsRead, err) + } else if err := followInside(conn.Read(inside), held); err != nil { + log.Printf("cannot follow %s, so an internal name is served to this machine alone: %v", inside, err) } - if err := followPlans(conn.Read(plansRead), newPlanned(account.Node, say), held, say); err != nil { + plans, err := holderRead(conn.CallKept, plansRead) + if err != nil { + conn.Close() + return fmt.Errorf("reading who holds %s: %w", plansRead, err) + } + if err := followPlans(conn.Read(plans), newPlanned(account.Node, say), held, say); err != nil { conn.Close() return err } @@ -110,7 +137,7 @@ func followPlans(plans planWatch, given *planned, held *table, say func(string, return nil }) if err != nil { - return fmt.Errorf("watching %s: %w", plansRead, err) + return fmt.Errorf("watching the plans: %w", err) } read.Store(true) serve(given, held, say) diff --git a/modules/route-proxy/cmd/route-proxy/names_test.go b/modules/route-proxy/cmd/route-proxy/names_test.go index a42b9ab..285fa06 100644 --- a/modules/route-proxy/cmd/route-proxy/names_test.go +++ b/modules/route-proxy/cmd/route-proxy/names_test.go @@ -2,19 +2,22 @@ package main import ( "encoding/json" + "errors" "os" "slices" "testing" ) -// The state the proxy follows to know who is inside is one its manifest reads. +// The states the proxy follows are read in the seat form its manifest reads (`seat:.`; design 47 G A-116), and +// the manifest invokes the verb that says who holds the seat. func TestTheReadIsTheManifests(t *testing.T) { raw, err := os.ReadFile("../../module.json") if err != nil { t.Fatal(err) } var m struct { - Reads []string `json:"reads"` + Reads []string `json:"reads"` + Invokes []string `json:"invokes"` } if err := json.Unmarshal(raw, &m); err != nil { t.Fatal(err) @@ -24,4 +27,27 @@ func TestTheReadIsTheManifests(t *testing.T) { t.Errorf("the proxy follows %s, its manifest reads %v", read, m.Reads) } } + if !slices.Contains(m.Invokes, "seat:mesh-module-assigner.assignments") { + t.Errorf("the proxy asks who holds a seat, its manifest invokes %v", m.Invokes) + } +} + +// A read in the seat form is the bus's `.` of the holder on record, whoever it is. +func TestHolderReadResolvesTheSeat(t *testing.T) { + call := func(holder string) func(string, any) (json.RawMessage, error) { + return func(string, any) (json.RawMessage, error) { + return json.RawMessage(`{"seats":[{"seat":"mesh-provisioner","holders":[{"module":"` + holder + `"}]}]}`), nil + } + } + for _, holder := range []string{"one", "another"} { + if got, err := holderRead(call(holder), plansRead); err != nil || got != holder+".plans" { + t.Errorf("holderRead = %q, %v; want %s.plans", got, err, holder) + } + } + if _, err := holderRead(func(string, any) (json.RawMessage, error) { return nil, errors.New("down") }, plansRead); err == nil { + t.Errorf("a failed ask was taken as a holder") + } + if _, err := holderRead(call("x"), "mesh-provisioner.plans"); err == nil { + t.Errorf("a read naming a module was taken as a seat's") + } } diff --git a/modules/route-proxy/go.mod b/modules/route-proxy/go.mod index db6debc..51ccfeb 100644 --- a/modules/route-proxy/go.mod +++ b/modules/route-proxy/go.mod @@ -6,6 +6,7 @@ require ( git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/provisioning/go/seats/meshprovisioner v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 github.com/nats-io/nats.go v1.54.0 golang.org/x/crypto v0.57.0 diff --git a/modules/route-proxy/module.json b/modules/route-proxy/module.json index 17a6d2b..227ca3e 100644 --- a/modules/route-proxy/module.json +++ b/modules/route-proxy/module.json @@ -265,8 +265,11 @@ ] }, "reads": [ - "mesh-delivery-assigner.assignments", - "mesh-provisioner.plans", - "mesh-provisioner.provisions" + "seat:mesh-module-assigner.assignments", + "seat:mesh-provisioner.plans", + "seat:mesh-provisioner.provisions" + ], + "invokes": [ + "seat:mesh-module-assigner.assignments" ] }