Refuse the grants step when its send would change more than the bus's user list (review of #230)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without …
mesh/repo-check fail: its merge-check.sh failed: --- FAIL: TestTheGrantsStepSendsTheUserListAndNothingElse (1.68s)

Kept builds are not the whole declaration: a new assignment, settings or
a provision's logins went out as the mesh holds them now. The step now
compares what it would send with the last send's summary and fails, said,
when anything but the user list differs; ungatedIn reads the kept builds
instead of passing the step unread.
This commit is contained in:
2026-10-11 19:09:23 +02:00
parent e1367d185a
commit fbeffb608d
3 changed files with 285 additions and 12 deletions
+106 -5
View File
@@ -2,6 +2,7 @@ package main
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
@@ -239,11 +240,6 @@ var machineMoves = func(ctx context.Context, open *stores, f moveFacts, node str
// ungatedIn refuses a send whose machines would move a build no gate has seen, outside its scope: the
// machines named, and why; the machine holding the bus, added only for its user list, is left instead.
func ungatedIn(ctx context.Context, open *stores, names []string, addedHolder string) ([]string, error) {
// The grants step moves no build: every module is composed at the build its machine runs (novox/hq
// issue 490), so there is nothing here for a gate to judge.
if everyBuildKept(ctx) {
return names, nil
}
scope := scopeOf(ctx)
if scope.person {
return names, nil
@@ -258,8 +254,19 @@ func ungatedIn(ctx context.Context, open *stores, names []string, addedHolder st
if err != nil {
return nil, err
}
// The grants step composes every module at the build its machine was last sent (novox/hq issue 490):
// a move it keeps is not made. Read, not assumed, so a move it could not keep is still refused here.
var keeps map[string]string
if everyBuildKept(ctx) {
if keeps, err = recordedKept(ctx, open, n); err != nil {
return nil, err
}
}
var waiting []string
for _, mv := range moves {
if was, kept := keeps[mv.Module]; kept && sameCommit(was, mv.From) {
continue
}
if !scope.judged[n] && !scope.modules[mv.Module] {
waiting = append(waiting, fmt.Sprintf("%s %s → %s", mv.Module, short(mv.From), short(mv.To)))
}
@@ -961,6 +968,19 @@ func grantsStep(ctx context.Context, open *stores, node string) *inventory.Grant
// list is the one already sent.
return nil
}
// **The user list and nothing else**: checked before it is sent, against what the machine was last sent.
// Kept builds are not the whole of a declaration — a new assignment, a settings change, an assignment
// taken away, a provision's logins are composed as the mesh holds them now — and a step that carried any
// of it would be the unjudged change this step exists to avoid.
if other, err := grantsOnlyOf(ctx, open, holder); err != nil || other != "" {
why := other
if err != nil {
why = err.Error()
}
fmt.Printf("grants step: %s is not sent its user list ahead of %s, which is sent without it: %s\n",
holder, node, why)
return &inventory.GrantsStep{Node: holder, Failed: why}
}
if _, err := sendRollout(keepingEveryBuild(withScope(ctx, sendScope{})), open, []string{holder}); err != nil {
fmt.Printf("grants step: the bus's user list could not be sent to %s ahead of %s, which is sent without "+
"it — the bus may refuse what the send newly grants: %v\n", holder, node, err)
@@ -972,6 +992,87 @@ func grantsStep(ctx context.Context, open *stores, node string) *inventory.Grant
return &inventory.GrantsStep{Node: holder, At: &now}
}
// grantsOnlyOf is grantsOnly: a variable so a test of the walk needs no store.
var grantsOnlyOf = grantsOnly
// grantsOnly is what the grants step's send to the machine holding the bus would change there besides its
// user list, against what it was last sent (novox/hq issue 490); empty when nothing else. Composed as the
// step's send composes it, without allocating anything (Foreseeing), and compared resource by resource
// with the summary the last send kept (ADR 0217): exact, rather than a list of the cases that can differ.
// A module there that was never sent is a new assignment, and refused too, whatever its resources.
func grantsOnly(ctx context.Context, open *stores, holder string) (string, error) {
inv := open.inventory
kept := keepingEveryBuild(keepingRecorded(ctx))
plan, settings, err := planFor(kept, open, holder)
if err != nil {
return "", err
}
sent, known, err := inv.SentBuilds(ctx, holder)
if err != nil {
return "", err
}
if !known {
return fmt.Sprintf("what %s was last sent is not known", holder), nil
}
var other []string
listID := ""
for _, m := range plan.Modules {
if _, was := sent[m.Module]; !was {
other = append(other, m.Module+" newly assigned")
}
if m.BusUsers != "" && m.ClaimsSeat(catalogue.BrokerSeat) {
listID = m.Module + "." + catalogue.BusUsersID()
}
}
gens, err := generators(kept, open)
if err != nil {
return "", err
}
declared, err := declarationWith(kept, open, holder, plan, settings, gens, Foreseeing)
if err != nil {
return "", err
}
for _, f := range declared.foreseen {
other = append(other, f+" would be minted")
}
body, err := declared.Body()
if err != nil {
return "", err
}
after, err := summarize(body)
if err != nil {
return "", err
}
prev, err := inv.SentSummary(ctx, holder)
if err != nil {
return "", err
}
if len(prev) == 0 {
return fmt.Sprintf("what %s was last sent is not kept for comparison", holder), nil
}
var before []sentResource
if err := json.Unmarshal(prev, &before); err != nil {
return "", fmt.Errorf("what %s was last sent is kept in a form this version does not read: %w", holder, err)
}
d := diffSent(before, after)
for _, id := range d.Added {
other = append(other, "+"+id)
}
for _, id := range d.Removed {
other = append(other, "-"+id)
}
for _, c := range d.Changed {
if c.ID == listID {
continue
}
other = append(other, fmt.Sprintf("~%s (%s)", c.ID, strings.Join(c.Fields, ", ")))
}
if len(other) == 0 {
return "", nil
}
return "its send would change more than the bus's user list: " + strings.Join(other, "; "), nil
}
// sendJudged is a gated send's sends: the grants step when the user list changed, then the machine judged.
func sendJudged(ctx context.Context, open *stores, node string) ([]string, *inventory.GrantsStep, error) {
grants := grantsStep(ctx, open, node)