bootstrap: the rest of the pivot — enrol, registry, publish, reinstall, retire

Steps 6 to 10, which turn a substrate into a mesh that can maintain itself
(novox/hq ADR 0067).

 6 enrol      a node record, a token, `mesh-host enrol`, and the host agent
              running. Proved by the mesh having HEARD from the node, not by a
              process existing: a host that cannot reach the broker looks exactly
              like a successful install until the first push applies nothing.
 7 registry   the module that gives this mesh an image store, registered from a
              --catalog checkout, assigned and pushed. Its image is upstream and
              never built (04-ISSUES/029) — a placeholder digest there is refused.
              Verified by asking `/v2/`, because a container that is up is not a
              registry that serves.
 8 publish    the carried image pushed into that registry, which assigns it the
              first manifest digest it has ever had. This is the hinge: without
              it the mesh works and can never upgrade itself.
 9 control    the control plane registered as an ordinary module pinned to that
              digest, with the substrate's own store connections delivered
              through `secret accept` — read out of the bundle that made them,
              because the mesh cannot invent a credential that predates it.
10 retire     the temporary control plane dropped from the bundle and removed by
              the host's ordinary removal pass.

Every step asks before it acts and reports "already done". No step leaves the
machine without a control plane: steps 9 and 10 overlap deliberately, and two
stateless control planes are untidy rather than broken.

mesh-control's `internal/builder`.PublishImage is mirrored rather than imported —
tier 0 depends on nothing that must be installed first — with one correction: the
digest is chosen from RepoDigests by repository instead of taken as element zero,
so an image pushed to two registries cannot silently pin this mesh to the wrong
one.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
2026-09-10 23:59:57 +02:00
parent af953dbb0d
commit f534cf8b42
15 changed files with 2910 additions and 34 deletions
+52 -1
View File
@@ -105,9 +105,60 @@ func TestTheUsageTextAndTheFlagsAgree(t *testing.T) {
t.Errorf("--%s exists and the usage text does not mention it", name)
}
}
for _, promised := range []string{"bundle", "out", "state", "system", "timeout", "wait", "dry-run", "json"} {
for _, promised := range []string{
"bundle", "out", "state", "system", "timeout", "wait", "dry-run", "json",
"catalog", "node", "registry", "host", "host-service", "host-in-background",
} {
if !declared[promised] {
t.Errorf("the usage text promises --%s and no such flag exists", promised)
}
}
}
// The pivot's defaults are the documented ones too, and the one that has no default is the one
// that decides whether the pivot happens at all.
func TestThePivotsDefaultsAreTheDocumentedOnes(t *testing.T) {
_, opts, _, err := parseArgs(nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if opts.Registry != defaultRegistry {
t.Errorf("default registry is %q; the usage text says %q", opts.Registry, defaultRegistry)
}
if opts.Host != defaultHost {
t.Errorf("default host binary is %q; the usage text says %q", opts.Host, defaultHost)
}
if opts.HostService != defaultService {
t.Errorf("default host service is %q; the usage text says %q",
opts.HostService, defaultService)
}
if opts.HostInBackground {
t.Error("the host is started unsupervised by default, and no real machine should")
}
// **No default, deliberately.** A catalogue this installer went looking for on its own would
// be a checkout somebody else made, at whatever commit they left it on — and it decides which
// image the mesh's control plane is pinned to for ever after.
if opts.Catalogue != "" {
t.Errorf("--catalog defaults to %q; without one the installer stops at the substrate",
opts.Catalogue)
}
// The machine's own name, because that is what a person already calls it.
if opts.Node == "" {
t.Error("no default node name; this machine can say what it is called")
}
}
// --node overrides the machine's own name rather than being ignored because a default was already
// computed. The name is what the mesh's records, tokens, assignments and pushes all name.
func TestTheNodeNameCanBeSaid(t *testing.T) {
_, opts, _, err := parseArgs([]string{"--node", "anchor", "--catalog", "/somewhere"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if opts.Node != "anchor" {
t.Errorf("--node anchor parsed as %q", opts.Node)
}
if opts.Catalogue != "/somewhere" {
t.Errorf("--catalog parsed as %q", opts.Catalogue)
}
}