Merge pull request 'genesis: seal the build agent the forge it raised, in place of the builder's npm-password (hq issue 121, ADR 0336)' (#102) from fix/121-genesis-package-registry into main

This commit was merged in pull request #102.
This commit is contained in:
2026-10-13 03:37:15 +00:00
4 changed files with 120 additions and 60 deletions
-21
View File
@@ -38,27 +38,6 @@ func recording(t *testing.T) (*controlRecorder, controlPlane) {
return c, controlPlane{container: "temp-mesh-controller", run: c.run, timeout: time.Second}
}
func TestTheBuildersPackageRegistryPortIsASettingAndNotTheManifest(t *testing.T) {
c, control := recording(t)
o := Options{Node: "anchor", Ports: FoundationPorts{Packages: 3100}, Wait: time.Second}
if _, err := installModule(context.Background(), o, control, BuilderModule,
[]byte(theBuildersBinding), quietly); err != nil {
t.Fatal(err)
}
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[BuilderModule+"-module.json"]; got != theBuildersBinding {
t.Errorf("the installer changed the builder's manifest:\n%s", got)
}
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)
}
}
func TestTheBuilderOnTheDefaultPackagesPortIsToldNothing(t *testing.T) {
c, control := recording(t)
o := Options{Node: "anchor", Wait: time.Second}
@@ -0,0 +1,65 @@
package bootstrap
import (
"context"
"encoding/json"
"strings"
"testing"
"time"
)
// Genesis seals the build agent the forge it raised (novox/hq ADR 0336 §3, issue 121): the agent's
// npm-package-registry requirement is optional and binds only once the gitea module is assigned, after
// the base, so until then the packages step's genesis document is how the sdk and base steps reach the
// forge.
func TestThePackagesStepSealsTheGenesisDocumentToTheBuildAgent(t *testing.T) {
if BuilderModule != "build-agent" {
t.Fatalf("genesis installs %q; the document is the build agent's", BuilderModule)
}
c, control := recording(t)
o := Options{Node: "anchor", Ports: FoundationPorts{Packages: 3100}, Wait: time.Second}
doc := genesisRegistry(o.Ports.orDefaults(), "builder-password")
if err := deliverGenesisRegistry(context.Background(), o, control, doc, quietly); err != nil {
t.Fatal(err)
}
carried := c.settings["accepting-"+GenesisRegistrySecret]
var got GenesisRegistry
if err := json.Unmarshal([]byte(carried), &got); err != nil {
t.Fatalf("the genesis document carried is not JSON: %q", carried)
}
want := GenesisRegistry{Scheme: "http", At: "127.0.0.1", Port: 3100, NpmPath: "/api/packages/novox/npm/",
As: "mesh-builder", Password: "builder-password"}
if got != want {
t.Fatalf("the genesis document says %+v, want %+v", got, want)
}
accept := c.index("secret accept anchor build-agent genesis-package-registry --from /accepting-genesis-package-registry")
push := c.index("push anchor")
if accept < 0 || push < 0 || accept > push {
t.Fatalf("the genesis document was not sealed to the build agent before the push: %v", c.told)
}
}
// What the retired builder was given and the build agent declares neither of: an `npm-password`
// own-secret and a `serves` setting for its carried binding (ADR 0336 §3).
func TestNoStepGivesTheBuildAgentAServesSettingOrAnNpmPassword(t *testing.T) {
for _, module := range []string{BuilderModule} {
c, control := recording(t)
o := Options{Node: "anchor", Ports: FoundationPorts{Packages: 3100}, Wait: time.Second}
if _, err := installModule(context.Background(), o, control, module, []byte(`{"module":"`+module+`"}`), quietly); err != nil {
t.Fatal(err)
}
if got, set := c.settings[module+"-settings.json"]; set {
t.Errorf("%s was given a setting on a node with a non-default packages port: %s", module, got)
}
if err := deliverGenesisRegistry(context.Background(), o, control,
genesisRegistry(o.Ports.orDefaults(), "p"), quietly); err != nil {
t.Fatal(err)
}
for _, told := range c.told {
if strings.Contains(told, "npm-password") || strings.Contains(told, "serves") {
t.Errorf("genesis told the controller %q", told)
}
}
}
}
+53 -14
View File
@@ -2,6 +2,7 @@ package bootstrap
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
@@ -49,8 +50,8 @@ const (
packagesTeam = "packages"
// giteaAdminUser is the admin the bootstrap creates and the provisioner later authenticates as.
giteaAdminUser = "mesh-admin"
// builderGiteaUser is the gitea account the builder publishes and pulls with at genesis. It is
// the `as` the builder's static package binding names.
// builderGiteaUser is the gitea account the build agent publishes and pulls with at genesis: the
// `as` of the genesis document (novox/hq ADR 0336 §3). Retiring it is issue 602.
builderGiteaUser = "mesh-builder"
// giteaDBRole/giteaDBName is gitea's own database in the foundation store.
giteaDBRole = "mesh_gitea"
@@ -133,8 +134,8 @@ func RaisePackageRegistry(ctx context.Context, o Options, d Deps, control contro
return err
}
say(" delivering the builder its registry credential")
if err := deliverBuilderNpm(ctx, o, control, builderPassword, say); err != nil {
say(" sealing the build agent the genesis document for the forge it raised")
if err := deliverGenesisRegistry(ctx, o, control, genesisRegistry(ports, builderPassword), say); err != nil {
return err
}
@@ -362,27 +363,65 @@ func packagePasswords() (db, admin, builder string, err error) {
return db, admin, builder, nil
}
// deliverBuilderNpm seals the builder's registry password to this node as its `npm-password`
// own-secret, the same way the control plane's store connections are delivered — carry the value in,
// `secret accept`, and the next push writes it sealed where the builder reads it.
func deliverBuilderNpm(ctx context.Context, o Options, control controlPlane, password string,
// GenesisRegistrySecret is the build agent's own-secret the genesis document is sealed as (novox/hq
// ADR 0336 §3). The build-agent manifest declares it, issued outside the mesh: only this installer
// gives one, on genesis's node.
const GenesisRegistrySecret = "genesis-package-registry"
// GenesisRegistry is the genesis document (novox/hq ADR 0336 §3): the forge this step raised, the port
// this node gave the package registry, its npm path, and the account this step made with its password.
// mesh-controller's cmd/mesh-builder reads it under the same field names while the build agent's
// npm-package-registry requirement is unbound, and prefers the binding once the gitea module is
// assigned and it binds.
type GenesisRegistry struct {
Scheme string `json:"scheme"`
At string `json:"at"`
Port int `json:"port"`
NpmPath string `json:"npm-path"`
As string `json:"as"`
Password string `json:"password"`
}
// genesisRegistry is the document for the forge raised here: on the machine's own loopback, where it
// answers on the host network and where its ROOT_URL says it is (raiseGiteaServer), on the port this
// node gave the package registry.
func genesisRegistry(ports FoundationPorts, password string) GenesisRegistry {
return GenesisRegistry{Scheme: "http", At: "127.0.0.1", Port: ports.Packages,
NpmPath: "/api/packages/" + packagesOrg + "/npm/", As: builderGiteaUser, Password: password}
}
// deliverGenesisRegistry seals the genesis document to the build agent on this node as its
// `genesis-package-registry` own-secret (novox/hq ADR 0336 §3), the same way the control plane's store
// connections are delivered — carry the value in, `secret accept`, and the next push writes it sealed
// where the build agent reads it.
//
// **The build agent's one way to the forge until the registry binds.** Its npm-package-registry
// requirement is optional (ADR 0336 §1), and nothing provides it until the gitea module is assigned
// after the base, so the sdk step (15) publishes and the base step (16) resolves with the account in
// this document. It replaces the retired builder's `npm-password` own-secret and the `serves.port`
// setting its carried binding took, neither of which the build agent declares.
func deliverGenesisRegistry(ctx context.Context, o Options, control controlPlane, doc GenesisRegistry,
say func(string)) error {
at := "/accepting-npm-password"
if err := control.carryingSecret(ctx, "mesh-accepting-npm-password", []byte(password), at); err != nil {
raw, err := json.Marshal(doc)
if err != nil {
return err
}
if _, err := control.tell(ctx, "secret", "accept", o.Node, BuilderModule, "npm-password", "--from", at); err != nil {
at := "/accepting-" + GenesisRegistrySecret
if err := control.carryingSecret(ctx, "mesh-accepting-"+GenesisRegistrySecret, raw, at); err != nil {
return err
}
// Push so the sealed secret reaches the builder, which restarts on it and comes back credentialed.
if _, err := control.tell(ctx, "secret", "accept", o.Node, BuilderModule, GenesisRegistrySecret, "--from", at); err != nil {
return err
}
// Push so the sealed document reaches the build agent, which reads it at its next build.
if _, err := control.tell(ctx, "push", o.Node); err != nil {
return err
}
return nil
}
// PublishTheSDK dispatches a build of the SDK to the builder. The builder has its registry
// credential by now, so the build's `npm publish` authenticates; the artifact is a `package`, built
// PublishTheSDK dispatches a build of the SDK to the build agent. The agent has the genesis document by
// now (novox/hq ADR 0336 §7), so the build's `npm publish` authenticates; the artifact is a `package`, built
// on a public base, so this needs no toolchain — which is the whole point of doing it before the base.
func PublishTheSDK(ctx context.Context, o Options, control controlPlane, say func(string)) error {
if o.SDKSource.Repository == "" {
+2 -25
View File
@@ -117,24 +117,6 @@ const busModulePort = 4222
// PortsSetting is the controller's settings key for a module's given ports.
const PortsSetting = "ports"
// ServesSetting is the settings key that carries what a provider serves, inside the one binding a
// module carries rather than resolves — the builder's package binding (novox/hq 04-ISSUES/085).
//
// **A port given at genesis has to be a setting wherever it is read, or something puts it back.**
// The builder reaches the package registry through a binding written in its own manifest, because
// at genesis no module yet provides `package-registry` and so there is nothing for the controller
// to resolve it from. That binding used to be rewritten, as text, when the installer registered the
// builder — which a later registration from the catalogue undid silently. Set as a node's setting
// instead, it is held by the controller rather than by the manifest, so re-registering the builder
// leaves it where it was.
//
// **A settings layer is the module's, not one resource's.** The controller lays it over every file
// of that module which merges as JSON, so `serves` lands in the package binding only because that
// binding is the builder's one mergeable file. A second mergeable file added to the builder would
// be given a `serves` key too, meaning nothing to whatever reads it. Worth knowing before adding
// one.
const ServesSetting = "serves"
// setFoundationSettings tells the controller the ports this node gave a foundation module — and,
// on an adopted node, that the registry is reached from anywhere, as a node pulls from it before it
// has a private-network address (novox/hq ADR 0100). Done after the module is registered and before
@@ -148,13 +130,8 @@ func setFoundationSettings(ctx context.Context, o Options, control controlPlane,
if o.Adopted && module == RegistryModule {
values["expose"] = map[string]string{strconv.Itoa(DefaultPorts().Registry): "anywhere"}
}
if packages := o.Ports.orDefaults().Packages; module == BuilderModule &&
packages != DefaultPorts().Packages {
// The one binding nothing resolves: the builder dials the forge by a number it carries.
// The rest of the binding — the provision, the forge it is from, the account it presents —
// is the manifest's and stays there.
values[ServesSetting] = map[string]int{"port": packages}
}
// No `serves` setting for the build agent: the port given for the package registry reaches it in
// the genesis document (novox/hq ADR 0336 §3), and from the binding once the registry binds.
if len(values) == 0 {
return nil
}