R4e (mesh-controller): deliver from the builder's keys, ask build-agent only for its own modules, follow the forge's protection for checks, announce the controller's states #441

Merged
mesh-tools merged 39 commits from feat/r4e into main 2026-10-22 22:12:59 +00:00
3 changed files with 145 additions and 1 deletions
Showing only changes of commit d5d53893a4 - Show all commits
+69
View File
@@ -0,0 +1,69 @@
package main
import (
"context"
"fmt"
)
// A new controller's own grants reach the bus before it writes (novox/hq issue 1800).
//
// A controller's grants are composed by the controller binary itself (broker.PermissionsFor), so the controller that
// walks a delivery cannot put its successor's grants in the bus's user list: the list that carries them was sent only
// once the new controller passed its gate on its first machine (carryUserList). Until then the new controller wrote
// buckets its account was not granted, and the bus refused them: plans-building on 2026-10-21, declared-packages on
// 2026-10-22, and R4e's states-announced would be the third.
//
// So a controller that starts, holding the lease and before it starts anything that writes its buckets, sends the
// machine holding the bus its user list when the list it composes is not the one that machine was last sent: the grants
// step's send (release.go grantsStep), every build there kept at the one it runs, refused unsent when the declaration
// would change more than the user list (errNotGrantsOnly). No build of a running plan reaches that machine before the
// plan's own tier sends it (ADR 0503 §8). A send that cannot be made is said once, and the controller serves on.
//
// It does not ask heldMachines, as carryUserList does: every reason that gives is a build that would move with the
// list (a policy, a plan not yet sent there, no gate passed), and here none can. The running plan that brought this
// controller holds the bus's machine until its tier sends it; asking would leave the list behind exactly when 1800
// meets it. The refusal unsent (errNotGrantsOnly) is the guard instead.
// ownGrantsCarrier is what the start's send reads and does: whether the bus's list is behind and on which machine, and
// the grants-only send to it. Variables so a test needs no store and no bus.
type ownGrantsCarrier struct {
behind func(ctx context.Context) (holder string, behind bool, err error)
send func(ctx context.Context, holder string) error
}
// carry sends the machine holding the bus the controller's own user list when it is behind, and says what it did.
func (c ownGrantsCarrier) carry(ctx context.Context) string {
holder, behind, err := c.behind(ctx)
switch {
case err != nil:
return fmt.Sprintf("whether the bus's user list carries this controller's grants cannot be read, so it is not sent "+
"at start; a bucket it newly writes may be refused until the gate carries it: %v", err)
case holder == "" || !behind:
return ""
}
if err := c.send(ctx, holder); err != nil {
return fmt.Sprintf("this controller's grants could not be sent to %s, which holds the bus, at its start; a bucket "+
"it newly writes may be refused until the gate carries them: %v", holder, err)
}
return fmt.Sprintf("sent %s, which holds the bus, this controller's user list alone, every build there kept, "+
"before it writes", holder)
}
// startWithOwnGrants carries the controller's own grants to the bus and only then starts what writes (then).
func startWithOwnGrants(ctx context.Context, c ownGrantsCarrier, then func()) {
if said := c.carry(ctx); said != "" {
fmt.Println(said)
}
then()
}
// ownGrantsOf is the carrier of the serving controller: the grants step's reading and its grants-only send.
func ownGrantsOf(open *stores) ownGrantsCarrier {
return ownGrantsCarrier{
behind: func(ctx context.Context) (string, bool, error) { return brokerBehindOf(ctx, open, nil) },
send: func(ctx context.Context, holder string) error {
_, err := sendRollout(keepingEveryBuild(withScope(ctx, sendScope{}), holder), open, []string{holder})
return err
},
}
}
+73
View File
@@ -0,0 +1,73 @@
package main
import (
"context"
"errors"
"strings"
"testing"
)
// 1800 (1): a controller starting with the bus's list behind its own sends the bus's machine once, before anything
// that writes its buckets starts.
func TestAStartingControllerSendsItsGrantsBeforeItWrites(t *testing.T) {
var order []string
c := ownGrantsCarrier{
behind: func(context.Context) (string, bool, error) { return "novox", true, nil },
send: func(_ context.Context, holder string) error { order = append(order, "send "+holder); return nil },
}
startWithOwnGrants(t.Context(), c, func() { order = append(order, "writers") })
if strings.Join(order, ", ") != "send novox, writers" {
t.Fatalf("the start did %v", order)
}
}
// 1800 (3), (4): no send when the list is current or there is no bus with a list; one that cannot be read or sent is
// said once, and the controller goes on.
func TestAStartingControllerSendsNothingWhenTheListIsCurrentAndGoesOnWhenRefused(t *testing.T) {
sends := 0
send := func(context.Context, string) error { sends++; return nil }
for _, c := range []ownGrantsCarrier{
{behind: func(context.Context) (string, bool, error) { return "novox", false, nil }, send: send},
{behind: func(context.Context) (string, bool, error) { return "", true, nil }, send: send},
} {
if said := c.carry(t.Context()); said != "" || sends != 0 {
t.Fatalf("a current list sent %d and said %q", sends, said)
}
}
refused := ownGrantsCarrier{behind: func(context.Context) (string, bool, error) { return "novox", true, nil },
send: func(context.Context, string) error { return errNotGrantsOnly }}
went := false
startWithOwnGrants(t.Context(), refused, func() { went = true })
if !went {
t.Fatal("a refused send stopped the controller")
}
if said := refused.carry(t.Context()); !strings.Contains(said, "could not be sent to novox") ||
!strings.Contains(said, errNotGrantsOnly.Error()) {
t.Fatalf("a refused send is said as %q", said)
}
unread := ownGrantsCarrier{behind: func(context.Context) (string, bool, error) { return "", false, errors.New("no store") },
send: send}
if said := unread.carry(t.Context()); !strings.Contains(said, "cannot be read") || sends != 0 {
t.Fatalf("an unread list: sent %d, said %q", sends, said)
}
}
// 1800 (2): the start's send is the grants step's: composed with every build of the bus's machine kept at the one it
// runs, so the declaration differs from its last only by the user list (and is refused unsent otherwise,
// errNotGrantsOnly, release.go), whatever a running plan registered since.
func TestAStartingControllersSendKeepsEveryBuildOfTheBusMachine(t *testing.T) {
was := sendRollout
t.Cleanup(func() { sendRollout = was })
var kept bool
var named []string
sendRollout = func(ctx context.Context, _ *stores, names []string) ([]string, error) {
kept, named = everyBuildKept(ctx, "novox"), names
return names, nil
}
if err := ownGrantsOf(nil).send(t.Context(), "novox"); err != nil {
t.Fatal(err)
}
if !kept || len(named) != 1 || named[0] != "novox" {
t.Fatalf("the start's send keeps every build: %v, names %v", kept, named)
}
}
+3 -1
View File
@@ -189,7 +189,9 @@ func serve(ctx context.Context) (err error) {
// Open plans move on events (novox/hq ADR 0406 §5, issue 974): a tier is asked on the `built` and the send of the
// tier before it, a walk ends on its machines' reports, a silent machine is a key that expired, and a plan left
// by a replaced controller resumes from the first look, which this asks for. No timer looks every so often.
startRolloutEvents(ctx, open, server.JetStream())
// **Its own grants on the bus before it writes** (novox/hq issue 1800): a bucket this controller newly writes is
// granted before the first write, not after its gate.
startWithOwnGrants(ctx, ownGrantsOf(open), func() { startRolloutEvents(ctx, open, server.JetStream()) })
// And the merge window (novox/hq ADR 0276, ADR 0407): a batch is cut into its walk when the window closes,
// which no merge or outcome says. The window is two keys on the bus that expire and a watcher that hears it;
// nothing polls (issue 973).