Place the bus's users after every push until the bus is adopted (hq issue 608)
mesh/merge-gate head 7283209d1abc merged into trunk 64f0ab405101 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 7283209d1abc merged into trunk 64f0ab405101 — pass: its merge-check.sh passed
mesh/delivery delivered

The bus reads its user list from a file that, until the broker step adopts it as the nats module,
only the installer writes, and the installer wrote it only at enrolment. The build agent's account,
minted with step 13's push, never reached the bus; the agent was refused (Authorization Violation)
and the sdk step's build waited until it was killed. Every push genesis makes now places the composed
list and has the bus re-read it, until the nats module is registered (ADR 0331 §5, ADR 0333 §4).
This commit is contained in:
jochen
2026-10-13 06:58:39 +02:00
parent 64f0ab4051
commit 7283209d1a
5 changed files with 120 additions and 5 deletions
+9
View File
@@ -146,6 +146,15 @@ func pushNode(ctx context.Context, o Options, control controlPlane, say func(str
"already registered and try the push again", err)
}
say(" pushed " + o.Node)
// **Every push may carry a new bus user, and until the adoption the bus hears of it only from here**
// (novox/hq issue 608, ADR 0331 §5, ADR 0333 §4). A module's account is minted into the mesh's
// records and sealed to this node with the push; the bus reads its user list from a file, and until
// the broker step adopts the bus as the nats module nothing but this installer writes that file. The
// list was placed only when the node enrolled, so the build agent's account (step 13) never reached
// the bus and the agent was refused ("Authorization Violation") until the sdk step gave up.
if err := placeUntilAdopted(ctx, control, say); err != nil {
return "", err
}
return strings.TrimSpace(said), nil
}
+4 -5
View File
@@ -413,11 +413,10 @@ func deliverGenesisRegistry(ctx context.Context, o Options, control controlPlane
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
// Push so the sealed document reaches the build agent, which reads it at its next build — and,
// like every push before the adoption, the bus hears the users it carries (issue 608).
_, err = pushNode(ctx, o, control, say)
return err
}
// PublishTheSDK dispatches a build of the SDK to the build agent. The agent has the genesis document by
+6
View File
@@ -239,6 +239,12 @@ func (c *controlRecorder) run(_ context.Context, name string, args ...string) (s
}
if name == "docker" && args[0] == "exec" {
c.told = append(c.told, strings.Join(args[3:], " "))
if strings.Join(args[3:], " ") == "broker accounts" {
return "accounts {}\n", nil // the composed user list, placed after every push (issue 608)
}
}
if name == "sh" {
c.told = append(c.told, "placed the bus's users")
}
return "", nil
}
@@ -0,0 +1,97 @@
package bootstrap
import (
"context"
"strings"
"testing"
"time"
)
// Defends novox/hq issue 608 (ADR 0331 §5, ADR 0333 §4): until the bus is adopted as the nats module,
// every push genesis makes is followed by the installer placing the composed user list where the bus
// reads it, and making the bus re-read it. The build agent's account was minted at step 13 and sealed
// with that push, and the list was placed only at enrolment, so the bus refused the agent
// ("Authorization Violation") and genesis stopped at step 15.
// placing answers the controller as a mesh whose bus is or is not the nats module yet, and records
// what was run, in order.
func placing(adopted bool) (*asked, controlPlane) {
listed := "build-agent 1\n"
if adopted {
listed += "nats 1\n"
}
runtime := &asked{}
runtime.answer = func(name string, args []string) (string, error) {
joined := strings.Join(args, " ")
switch {
case name == "sh":
return "", nil
case strings.HasSuffix(joined, "module list"):
return listed, nil
case strings.HasSuffix(joined, "broker accounts"):
return "accounts {\n MESH { users = [ { user: \"anchor.build-agent\" } ] }\n}\n", nil
}
return "", nil
}
return runtime, controlPlane{container: "mesh-controller", run: runtime.run, timeout: time.Second}
}
// placedAfterThePush is whether the bus's users were placed — written at the module's bus-users path,
// the bus told to re-read them — after the push, and how often.
func placedAfterThePush(t *testing.T, commands []string) int {
t.Helper()
pushed, placed := -1, 0
for i, c := range commands {
if strings.HasSuffix(c, "push anchor") && pushed < 0 {
pushed = i
}
if strings.HasPrefix(c, "sh -c") && strings.Contains(c, busAccounts) &&
strings.Contains(c, "kill -s HUP "+busContainer) {
if pushed < 0 || i < pushed {
t.Fatalf("the bus's users were placed before the push that carries the account: %v", commands)
}
placed++
}
}
if pushed < 0 {
t.Fatalf("nothing was pushed: %v", commands)
}
return placed
}
func TestTheBusHearsTheBuildAgentsAccountAfterItsPush(t *testing.T) {
t.Setenv("TMPDIR", t.TempDir())
runtime, control := placing(false)
o := Options{Node: "anchor", Wait: time.Second}
if _, err := installModule(context.Background(), o, control, BuilderModule, []byte(`{}`), quietly); err != nil {
t.Fatal(err)
}
if n := placedAfterThePush(t, runtime.commands); n != 1 {
t.Fatalf("the bus's users were placed %d time(s) after the build agent's push; once: %v", n, runtime.commands)
}
}
func TestTheGenesisDocumentsPushIsHeardByTheBusToo(t *testing.T) {
t.Setenv("TMPDIR", t.TempDir())
runtime, control := placing(false)
o := Options{Node: "anchor", Wait: time.Second}
if err := deliverGenesisRegistry(context.Background(), o, control, genesisRegistry(DefaultPorts(), "pw"), quietly); err != nil {
t.Fatal(err)
}
if n := placedAfterThePush(t, runtime.commands); n != 1 {
t.Fatalf("the bus's users were placed %d time(s) after the genesis document's push; once: %v", n, runtime.commands)
}
}
// From the adoption on the nats module writes the list, and the installer does not (ADR 0333 §4).
func TestOnceTheBusIsTheNatsModulesThePushPlacesNothing(t *testing.T) {
t.Setenv("TMPDIR", t.TempDir())
runtime, control := placing(true)
o := Options{Node: "anchor", Wait: time.Second}
if _, err := installModule(context.Background(), o, control, BuilderModule, []byte(`{}`), quietly); err != nil {
t.Fatal(err)
}
if n := placedAfterThePush(t, runtime.commands); n != 0 {
t.Fatalf("the installer placed the bus's users over the nats module's: %v", runtime.commands)
}
}
+4
View File
@@ -58,8 +58,12 @@ func aMeshThatAgrees(answers map[string]string) func(string, []string) (string,
}
}
switch {
case name == "sh" && len(args) == 2 && strings.Contains(args[1], "accounts.conf"):
return "", nil // the bus's users placed after a push (issue 608)
case name != "docker":
return "", fmt.Errorf("unexpected program %q", name)
case strings.HasSuffix(joined, "broker accounts"):
return "accounts {}\n", nil
case args[0] == "cp":
return "", nil
case args[0] == "container":