Author SHA1 Message Date
mesh-tools efdcc9daef Merge pull request 'sdk1: route-proxy moves here; its composition proofs move to the node-engine' (#2) from feat/sdk1 into main 2026-11-02 20:43:16 +00:00
jschoubben f18ec031c4 Pin mesh-sdk at f02f142, the sdk1 head whose tests no longer leak package globals, so the group's check builds against it
mesh/check checking f18ec031c463 on fe0392e05f62 | novox/mesh-networking#2 in group feat/sdk1
mesh/delivery merged: source.main moved to novox/mesh-networking@efdcc9daef2d
mesh/delivery-group group feat/sdk1 merged: every member is merged
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 20:14:16 +01:00
jschoubben 9d67813829 Pin mesh-sdk at the sdk1 group's head 1c7356c, so the group's check builds every module against the narrowed SDK it was written for
mesh/check failed: integration: meshbuildgo/cmd/mesh-build-go.TestAComposeAskIsBuiltOnceAndAnsweredOnTheBus: NOT CHECKED: the node-engine's composer c…
mesh/delivery-group group feat/sdk1 checking: 7 of 7 member(s) ready
mesh/delivery closed: a newer head of the same pull request
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 19:29:53 +01:00
jschoubben f82e0b77b4 Write route-proxy's test plan entry as the contributed form itself, since its encoder left mesh-sdk
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 19:20:18 +01:00
jschoubben a74f9db508 Test route-proxy without composing: its manifest's own facts and a plan entry's port, since the composition left mesh-sdk
The composer's proof that the manifest composes, and that a port is the placement's, is the node-engine's now (ADR 0593 §7).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 19:06:37 +01:00
jschoubben 9c9d03691b Drop route-proxy's stale notes of a contract module of its own, which the move made a package
The go.mod comment and the contract package's comment still described a separate Go module with its own tags. Issue 1126.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:14:32 +01:00
5 changed files with 31 additions and 95 deletions
@@ -54,40 +54,16 @@ func TestRouteProxyKeepsItsPublicAccountDirectory(t *testing.T) {
if slices.Contains(m.Requires, "acme-ca") { if slices.Contains(m.Requires, "acme-ca") {
t.Fatal("route-proxy still asks for acme-ca; the public issuer is its own fact (ADR 0226)") t.Fatal("route-proxy still asks for acme-ca; the public issuer is its own fact (ADR 0226)")
} }
// As a build would leave it: each artifact a container names resolved to an image. Which image does not matter to // The tree read inlines the template the manifest names, a literal with nothing to fill, so this is what it writes.
// the file checked here. An archive resource carries no image: it unpacks a bundle. env := resourceNamed(m.Resources, "acme-env")
for _, res := range m.Resources {
if res["type"] == "archive" {
continue
}
if artifact, ok := res["artifact"].(string); ok {
delete(res, "artifact")
res["image"] = "registry.invalid/route-proxy/" + artifact +
"@sha256:1111111111111111111111111111111111111111111111111111111111111111"
}
}
r := catalogue.Resolution{
Node: "anchor",
Modules: []catalogue.Manifest{m},
Needs: []catalogue.Needed{{
Name: "internal-acme-ca", From: "home", At: "home.internal", For: "route-proxy",
Serves: map[string]any{"port": float64(9000), "path": "/acme/acme/directory", "roots": "/roots.pem"},
}},
}
// Its own broker credential, sealed as the mesh would; what it is does not matter here.
out, err := r.Declaration(catalogue.Rendering{Needed: map[string]map[string]string{"route-proxy": {"broker": "sealed"}}})
if err != nil {
t.Fatalf("route-proxy could not be composed for a machine: %v", err)
}
env := resourceNamed(out, "route-proxy.acme-env")
if env == nil { if env == nil {
t.Fatalf("nothing writes the public issuer's environment: %v", out) t.Fatalf("nothing writes the public issuer's environment: %v", m.Resources)
} }
if got, _ := env["content"].(string); got != renderedBeforeTheFold { if got, _ := env["content"].(string); got != renderedBeforeTheFold {
t.Fatalf("acme.env changed, which moves the proxy's account and reorders every certificate:\ngot %q\nwant %q", t.Fatalf("acme.env changed, which moves the proxy's account and reorders every certificate:\ngot %q\nwant %q",
got, renderedBeforeTheFold) got, renderedBeforeTheFold)
} }
if resourceNamed(out, "route-proxy.bound-acme-ca") != nil { if resourceNamed(m.Resources, "bound-acme-ca") != nil {
t.Error("a binding to acme-ca is still written") t.Error("a binding to acme-ca is still written")
} }
var trust string var trust string
@@ -154,70 +154,33 @@ func TestARouteWithNoInternalNameGetsNoAlias(t *testing.T) {
} }
} }
// A route is dialled where the machine publishes its module's endpoint, as the module's declaration publishes it (issue // A route is dialled at the machine port its plan entry carries (issue 774, research 072 row 34; 2026-10-31: gitea's
// 774, research 072 row 34; 2026-10-31: gitea's 3000, published at 20000, and grafana's 3000 on another machine, // 3000, published at 20000, and grafana's 3000 on another machine, published at 20012, answered 502): over loopback
// published at 20012, answered 502): over loopback beside the proxy, which is the only thing that works without a // when the plan is the one of this proxy's own node, which is the only thing that works without a private network, and
// private network, and at its machine's address otherwise. Each plan is composed by the SDK as the seat // at the address the entry gives for its machine otherwise. The plans here are written as the seat mesh-provisioner
// mesh-provisioner composes it, its ports as compose's portsFor gives them (the placement's for a port the mesh may // carries them; that the port is the placement's and not the software's own is the composition's, proved by the
// move, the manifest's own otherwise), and read as this proxy reads it: the live shapes the old proxy served alike. // node-engine's composer, not here.
func TestAComposedPlanIsDialledWhereTheMachinePublishesIt(t *testing.T) { func TestAPlanEntryIsDialledAtTheMachinePortItCarries(t *testing.T) {
proxy := catalogue.Manifest{Module: "route-proxy", Version: "1",
Provides: []catalogue.Offer{{Name: "route", Scope: catalogue.ScopeMesh, AnsweredBy: catalogue.AnsweredByHelper}}}
world := catalogue.World{Offered: map[string][]catalogue.Provider{
"route": {{Node: "anchor", At: "anchor.internal", Module: "route-proxy"}}}}
for _, c := range []struct { for _, c := range []struct {
name, node, module string name, node, module, at string
port int // the endpoint's port, as listens says it port int
published []any // the container's ports; nil for the host's network target string
placed map[int]int // what mesh-placement holds for the module
target string
}{ }{
{"beside the proxy, moved by the placement (gitea)", "anchor", "gitea", 3000, []any{"3000"}, map[int]int{3000: 20000}, {"beside the proxy (gitea)", "anchor", "gitea", "", 20000, "http://127.0.0.1:20000"},
"http://127.0.0.1:20000"}, {"beside the proxy, though the entry says an address (mailu)", "anchor", "mailu", "anchor.internal", 7080, "http://127.0.0.1:7080"},
{"on another machine, moved by the placement (grafana)", "home", "grafana", 3000, []any{"3000"}, {"on another machine (grafana)", "home", "grafana", "home.internal", 20012, "http://home.internal:20012"},
map[int]int{3000: 20012}, "http://home.internal:20012"}, {"on another machine, a bare port (influxdb)", "home", "influxdb", "home.internal", 8086, "http://home.internal:8086"},
{"a bare port the placement holds none for (influxdb)", "home", "influxdb", 8086, []any{"8086"}, nil,
"http://home.internal:8086"},
{"a fixed mapping (mailu 7080:80)", "anchor", "mailu", 7080, []any{"25", "7080:80"}, nil, "http://127.0.0.1:7080"},
{"a fixed mapping the placement names another port of (photos 9102:9000)", "anchor", "photos", 9102,
[]any{"9102:9000"}, map[int]int{9000: 20011}, "http://127.0.0.1:9102"},
{"the host's network (home-assistant)", "anchor", "home-assistant", 8123, nil, nil, "http://127.0.0.1:8123"},
} { } {
container := map[string]any{"type": "container", "name": c.module, "image": "x"} values, _ := json.Marshal(map[string]any{"name": c.module + ".example.org", "port": c.port})
if c.published != nil { // The form as the seat carries it (version 1): written here, so what the proxy decodes is the shape, not a helper.
container["ports"] = c.published contributed, err := json.Marshal(catalogue.ContributedForm{V: catalogue.ContributedVersion, At: c.at,
} else { Each: []catalogue.ContributedOne{{Values: values}}})
container["network"] = "host"
}
module := catalogue.Manifest{Module: c.module, Version: "1", Requires: []string{"route"},
Listens: []catalogue.Listening{{Name: "web", Port: c.port, Protocol: "tcp", From: "mesh"}},
Contributes: map[string]map[string]any{"route": {"label": c.module, "endpoint": "web"}},
Resources: []map[string]any{container}}
node := catalogue.Node{Name: c.node, At: c.node + ".internal", PublicDomain: "example.org",
Capabilities: map[string]bool{"container-runtime": true}}
r, err := catalogue.Resolve(map[string]catalogue.Manifest{module.Module: module, proxy.Module: proxy},
[]string{module.Module}, node, world)
if err != nil { if err != nil {
t.Fatalf("%s: %v", c.name, err) t.Fatalf("%s: %v", c.name, err)
} }
r.Account = "op" value, err := provisions.EncodePlan(provisions.Plan{Node: c.node, Module: c.module, Plan: catalogue.Plan{
for i, n := range r.Needs { Provisions: []catalogue.PlanProvision{{Provision: contract.Provision, Provider: "anchor",
r.Needs[i].AnsweredByHelper = n.Name == "route" // as the plan sets it (compose's answeredByHelper) ProviderModule: contract.Module, Contributed: contributed}}}})
}
ports := map[int]int{}
for _, l := range module.Listens { // compose's portsFor
where, mayAssign := module.MachineSide(l.Port)
if at, known := c.placed[l.Port]; mayAssign && known {
where = at
}
ports[l.Port] = where
}
composed, err := r.Compose(catalogue.Rendering{Hands: true, Provisioner: "mesh-provisioner-client",
ArtifactStore: "store:5000", Planned: true, Ports: map[string]map[int]int{c.module: ports}})
if err != nil {
t.Fatalf("%s: %v", c.name, err)
}
value, err := provisions.EncodePlan(provisions.Plan{Node: c.node, Module: c.module, Plan: composed.Plans[c.module]})
if err != nil { if err != nil {
t.Fatalf("%s: %v", c.name, err) t.Fatalf("%s: %v", c.name, err)
} }
+3 -4
View File
@@ -3,10 +3,9 @@
// row 34; the operator, 2026-10-31: "the route-proxy should define its own contract for the seat (with the message // row 34; the operator, 2026-10-31: "the route-proxy should define its own contract for the seat (with the message
// contracts of course) which other modules can use"). // contracts of course) which other modules can use").
// //
// **Published by the repository that holds route-proxy, as its own Go module**, versioned by tags of the form // **A package of the route-proxy module**, imported as `route-proxy/contract`: standard library only, built from the
// `modules/route-proxy/contract/vX.Y.Z`, the subdirectory form Go's module system expects, the same way mesh-sdk is // same tree as the proxy, so the two never drift. No other module imports it today; when one must, it moves into the
// published (`go/vX.Y.Z`). A module that asks a route, reads what the proxy serves or hears its events imports this // networking domain's package (research 081).
// package; nothing imports the repository. Standard library only, so a consumer takes nothing else with it.
// //
// **How a route travels.** A module that must be reachable requires `route` and says, in its manifest's // **How a route travels.** A module that must be reachable requires `route` and says, in its manifest's
// `contributes.route`, one Request, or several by local name; Schema is that object's JSON schema. The seat // `contributes.route`, one Request, or several by local name; Schema is that object's JSON schema. The seat
+1 -3
View File
@@ -3,7 +3,7 @@ module route-proxy
go 1.26.0 go 1.26.0
require ( require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
github.com/nats-io/nats.go v1.54.0 github.com/nats-io/nats.go v1.54.0
golang.org/x/crypto v0.57.0 golang.org/x/crypto v0.57.0
) )
@@ -16,5 +16,3 @@ require (
golang.org/x/sys v0.48.0 // indirect golang.org/x/sys v0.48.0 // indirect
golang.org/x/text v0.42.0 // indirect golang.org/x/text v0.42.0 // indirect
) )
// The contract is this repository's own Go module, beside this one (contract/): built from the tree, not fetched.
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=