Install the build agent at genesis, the module that builds since ADR 0190 (issue 596) #101

Merged
mesh-admin merged 1 commits from fix/596-genesis-installs-the-build-agent into main 2026-10-13 00:52:22 +00:00
2 changed files with 146 additions and 21 deletions
+72 -18
View File
@@ -1,13 +1,16 @@
package bootstrap
import (
"bytes"
"context"
"encoding/json"
"fmt"
"strings"
)
// BuilderModule is the module that lets a mesh produce anything at all.
const BuilderModule = "builder"
// BuilderModule is the module that lets a mesh produce anything at all: the build agent, which holds
// the node seat `node-build-agent` (novox/hq ADR 0190). The one-holder `builder` module it replaced
// left the catalogue on 2026-10-03, and genesis went on asking for it (issue 596).
const BuilderModule = "build-agent"
// BuilderRepository is what its image is called in this mesh's own registry.
const BuilderRepository = "mesh-builder"
@@ -50,10 +53,10 @@ func InstallBuilder(ctx context.Context, o Options, d Deps, control controlPlane
manifest, err := readManifest(o.Catalogue, BuilderModule)
if err != nil {
return out, fmt.Errorf("%w\n"+
"This is the manifest that makes the builder an ordinary module. Without it the mesh "+
"This is the manifest that makes the build agent an ordinary module. Without it the mesh "+
"has the image and no way to run it, so nothing can be built here", err)
}
pinned, places, err := pinPlaceholder(manifest, published.Reference, BuilderModule)
pinned, places, err := pinTheCarriedBuilder(manifest, published.Reference)
if err != nil {
return out, err
}
@@ -66,21 +69,72 @@ func InstallBuilder(ctx context.Context, o Options, d Deps, control controlPlane
}
// Before the push, so the account is in the declaration the machine receives rather than in
// the one after it. A builder that arrives without its credential starts, finds nothing it may
// read, and waits — which looks exactly like a builder with no work.
account := o.Node + "-" + BuilderModule
if _, err := control.tell(ctx, "builder", "issue", account, "--node", o.Node); err != nil {
// Said and carried on. An account that already exists is the ordinary case on a re-run,
// and the push below is what makes either state true on the machine.
if !strings.Contains(err.Error(), "already") {
return out, fmt.Errorf("the builder has no broker account, so it can take no work: %w", err)
}
say(" broker account " + account + " — already issued")
} else {
out.Account = true
say(" broker account " + account + ", scoped to what it consumes and emits")
// the one after it. A build agent that arrives without its credential starts, finds nothing it
// may read, and waits — which looks exactly like an agent with no work. The same act as any
// module's account (`module issue`), which a re-run repeats harmlessly.
if _, err := control.tell(ctx, "module", "issue", BuilderModule, "--node", o.Node); err != nil {
return out, fmt.Errorf("the build agent has no account on the bus, so it can take no work: %w", err)
}
out.Account = true
say(" bus account " + BuilderModule + " on " + o.Node + ", scoped to what it consumes and emits")
out.Installed.Pushed, err = pushNode(ctx, o, control, say)
return out, err
}
// pinTheCarriedBuilder names the image this installer carried and published in the build agent's
// manifest, in place of the artifact the mesh would build for it.
//
// **The one module genesis cannot have built** (novox/hq ADR 0073): the build agent is what builds,
// and at this step the mesh has no build agent. The catalogue's manifest names its server container by
// the artifact it builds (`"artifact": "server"`, an image built from mesh-controller's
// cmd/mesh-builder); the image this installer carries is that build, made from the same program. So
// every container built from that image artifact runs the published reference instead, and the rest
// of the manifest — its build, its seat, its requirements — is the catalogue's as written. The mesh
// rebuilds it from the catalogue like any module from then on.
//
// A manifest still carrying the old placeholder digest is pinned the old way. One naming neither an
// image artifact nor a placeholder is refused: there would be nothing to put the carried image in.
func pinTheCarriedBuilder(manifest []byte, reference string) ([]byte, int, error) {
if bytes.Contains(manifest, []byte(placeholderDigest)) {
return pinPlaceholder(manifest, reference, BuilderModule)
}
var m map[string]any
if err := json.Unmarshal(manifest, &m); err != nil {
return nil, 0, fmt.Errorf("the %s module's manifest is not readable: %w", BuilderModule, err)
}
images := map[string]bool{}
if build, ok := m["build"].(map[string]any); ok {
artifacts, _ := build["artifacts"].([]any)
for _, a := range artifacts {
if art, ok := a.(map[string]any); ok && art["kind"] == "image" {
if name, ok := art["name"].(string); ok {
images[name] = true
}
}
}
}
places := 0
resources, _ := m["resources"].([]any)
for _, r := range resources {
res, ok := r.(map[string]any)
if !ok || res["type"] != "container" {
continue
}
if name, ok := res["artifact"].(string); ok && images[name] {
delete(res, "artifact")
res["image"] = reference
places++
}
}
if places == 0 {
return nil, 0, fmt.Errorf(
"the %s module's manifest runs no container from an image it builds, so there is nowhere to "+
"put the builder this installer carried and published (%s)", BuilderModule, reference)
}
pinned, err := json.MarshalIndent(m, "", " ")
if err != nil {
return nil, 0, err
}
return append(pinned, '\n'), places, nil
}
+74 -3
View File
@@ -2,6 +2,7 @@ package bootstrap
import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
@@ -44,15 +45,15 @@ func TestTheBuildersPackageRegistryPortIsASettingAndNotTheManifest(t *testing.T)
[]byte(theBuildersBinding), quietly); err != nil {
t.Fatal(err)
}
if got := c.settings["builder-settings.json"]; got != `{"serves":{"port":3100}}` {
if got := c.settings[BuilderModule+"-settings.json"]; got != `{"serves":{"port":3100}}` {
t.Errorf("the builder was told %q about the package registry's port", got)
}
// The manifest reaches the mesh as the catalogue wrote it. A port rewritten into it here is a
// port the next registration from the catalogue silently takes back.
if got := c.settings["builder-module.json"]; got != theBuildersBinding {
if got := c.settings[BuilderModule+"-module.json"]; got != theBuildersBinding {
t.Errorf("the installer changed the builder's manifest:\n%s", got)
}
set, push := c.index("settings set builder"), c.index("push anchor")
set, push := c.index("settings set "+BuilderModule), c.index("push anchor")
if set < 0 || push < 0 || set > push {
t.Fatalf("the port was not set before the push that raises the builder: %v", c.told)
}
@@ -167,3 +168,73 @@ func TestAForgeManifestTheCatalogueDoesNotHaveIsNamed(t *testing.T) {
t.Errorf("a missing forge manifest was not explained: %v", err)
}
}
// theCataloguesModule is the catalogue's own manifest of a module, never a copy: the clone beside a merge
// check (MESH_CHECK_BESIDE), where it must be; else a checkout beside this one; else the test skips aloud.
func theCataloguesModule(t *testing.T, module string) []byte {
t.Helper()
inside := filepath.Join("mesh-catalog", "modules", module, "module.json")
if root := os.Getenv("MESH_CHECK_BESIDE"); root != "" {
raw, err := os.ReadFile(filepath.Join(root, inside))
if err != nil {
t.Fatalf("the catalogue beside this check (MESH_CHECK_BESIDE=%s) has no %s: %v", root, inside, err)
}
return raw
}
raw, err := os.ReadFile(filepath.Join("..", "..", "..", inside))
if err != nil {
t.Skipf("NOT JUDGED: no catalogue beside this checkout and no MESH_CHECK_BESIDE; the merge check judges it")
}
return raw
}
// **Genesis installs the build agent the catalogue holds** (novox/hq issue 596, ADR 0190): the
// one-holder `builder` left the catalogue on 2026-10-03, and genesis stopped at step 13 asking for it.
// The catalogue's own manifest is read, and the image the installer carried is put where the manifest
// names the image it builds.
func TestGenesisInstallsTheCataloguesBuildAgent(t *testing.T) {
manifest := theCataloguesModule(t, BuilderModule)
const reference = "127.0.0.1:5000/mesh-builder@sha256:" + "4444444444444444444444444444444444444444444444444444444444444444"
pinned, places, err := pinTheCarriedBuilder(manifest, reference)
if err != nil {
t.Fatal(err)
}
var m struct {
Module string `json:"module"`
Claims []struct {
Name string `json:"name"`
} `json:"claims"`
Resources []map[string]any `json:"resources"`
}
if err := json.Unmarshal(pinned, &m); err != nil {
t.Fatal(err)
}
if m.Module != BuilderModule || len(m.Claims) == 0 || m.Claims[0].Name != "node-build-agent" {
t.Fatalf("genesis installs %q claiming %v; the build agent holds node-build-agent (ADR 0190)", m.Module, m.Claims)
}
running := 0
for _, r := range m.Resources {
if r["type"] != "container" {
continue
}
if _, still := r["artifact"]; still {
t.Errorf("the container %v still names an artifact nothing has built yet", r["name"])
}
if r["image"] == reference {
running++
}
}
if places != 1 || running != 1 {
t.Errorf("the carried builder runs in %d container(s) (%d place(s) said); the build agent runs one", running, places)
}
}
// A manifest that runs nothing from an image it builds has nowhere to put the carried builder, and is
// refused rather than registered running nothing.
func TestABuildAgentManifestWithNowhereForTheCarriedBuilderIsRefused(t *testing.T) {
_, _, err := pinTheCarriedBuilder([]byte(`{"module":"build-agent","resources":[{"id":"server","type":"container","name":"x","artifact":"server"}],"build":{"artifacts":[{"name":"server","kind":"bundle"}]}}`),
"127.0.0.1:5000/mesh-builder@sha256:"+strings.Repeat("4", 64))
if err == nil || !strings.Contains(err.Error(), "nowhere to put") {
t.Fatalf("a manifest with no image artifact was pinned: %v", err)
}
}