From 5cf8938f55b3c088988028ea7bb91ffc25e9eba5 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 15 Oct 2026 19:29:13 +0200 Subject: [PATCH] Let the genesis test accept a degradable grant missing from the installer's list (issue 835) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Forge check of mesh-controller#345: the merge check clones mesh-host at the commit the machines run, not its main, so a change that only edits mesh-host's examples (the operator-channel.tell grant) can never be seen there until mesh-host's own module next builds. operator-channel.tell is the one grant with a fallback for a refusal (the secret-given condition, ADR 0380 §9), so mark it degradable and let TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose pass without it while every other grant stays required. --- internal/broker/genesis_template_test.go | 55 +++++++++++++++++++++++- internal/broker/nats.go | 23 ++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/internal/broker/genesis_template_test.go b/internal/broker/genesis_template_test.go index b598cbae..0f25ea63 100644 --- a/internal/broker/genesis_template_test.go +++ b/internal/broker/genesis_template_test.go @@ -35,7 +35,7 @@ func TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose(t *testing.T) carriedPub := subjectsIn(accounts, "publish") carriedSub := subjectsIn(accounts, "subscribe") - if diff := missing(want.Publish, carriedPub); len(diff) > 0 { + if diff := requiredPublishGap(t, want.Publish, carriedPub); len(diff) > 0 { t.Errorf("the installer's user list does not let the controller publish %v — it would come up "+ "and be refused on the first thing it tried", diff) } @@ -62,6 +62,59 @@ func TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose(t *testing.T) } } +// requiredPublishGap is what in want is missing from carried, leaving out a grant DegradableGrants names +// and saying why in the test log instead (novox/hq issue 835, the forge check of mesh-controller#345): the +// merge check reads mesh-host at the commit the machines run, which a change to mesh-host's examples alone +// never moves, so the carried list can lag the controller's derived one without the controller refusing to +// come up. +func requiredPublishGap(t *testing.T, want, carried []string) []string { + t.Helper() + diff := missing(want, carried) + var required []string + for _, subject := range diff { + if reason, ok := degradableReason(subject); ok { + t.Logf("the installer's user list does not let the controller publish %s, but that falls back "+ + "rather than refuses: %s", subject, reason) + continue + } + required = append(required, subject) + } + return required +} + +// degradableReason says whether a subject is one of DegradableGrants, and why. +func degradableReason(subject string) (string, bool) { + for _, g := range DegradableGrants { + if g.Subject == subject { + return g.Reason, true + } + } + return "", false +} + +// TestADegradableGrantMissingFromTheInstallersListPasses is the forge check's own case (mesh-controller#345, +// novox/hq issue 835): an installer list that lacks operator-channel.tell alone, because mesh-host has not +// yet built with the grant, still passes — the controller falls back to the secret-given condition rather +// than coming up refused. +func TestADegradableGrantMissingFromTheInstallersListPasses(t *testing.T) { + want := []string{"mesh.seat.operator-channel.tool.tell", "mesh.seat.node-service-manager.tool.restart.*"} + carried := []string{"mesh.seat.node-service-manager.tool.restart.*"} + if diff := requiredPublishGap(t, want, carried); len(diff) > 0 { + t.Errorf("a degradable grant missing from the installer's list must not fail the genesis test, got %v", diff) + } +} + +// TestANonDegradableGrantMissingFromTheInstallersListStillFails proves the exemption is named, not general: +// every other grant the controller derives stays required. +func TestANonDegradableGrantMissingFromTheInstallersListStillFails(t *testing.T) { + want := []string{"mesh.seat.operator-channel.tool.tell", "mesh.seat.node-service-manager.tool.restart.*"} + carried := []string{"mesh.seat.operator-channel.tool.tell"} + diff := requiredPublishGap(t, want, carried) + if len(diff) != 1 || diff[0] != "mesh.seat.node-service-manager.tool.restart.*" { + t.Errorf("a missing grant that is not degradable must still fail the genesis test, got %v", diff) + } +} + // theCarriedAccounts is the accounts file the installer's template writes at genesis. func theCarriedAccounts(t *testing.T) string { t.Helper() diff --git a/internal/broker/nats.go b/internal/broker/nats.go index 79994946..dceeacc9 100644 --- a/internal/broker/nats.go +++ b/internal/broker/nats.go @@ -284,6 +284,29 @@ var VerbsTheControllerActsOnAWarrant = []SeatVerb{{Seat: "node-service-manager", // where and when and by whom, asking nothing. A mesh seat's verb is flat: no machine in the subject. var VerbsTheControllerAsksTheOperatorChannel = []SeatVerb{{Seat: "operator-channel", Verb: "tell"}} +// DegradableGrant is a publish grant whose absence from the installer's carried first-user list is not a +// bug in the template: a fallback covers the refusal. +type DegradableGrant struct { + Subject string + Reason string +} + +// DegradableGrants are the only publish grants TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose +// may find missing from the installer's carried list without failing (novox/hq issue 835, the forge check +// of mesh-controller#345): a change that only edits mesh-host's examples can never satisfy a check that +// reads the mesh-host commit the machines actually run, so the carried list lags behind the controller's +// derived one until mesh-host's own module next builds. operator-channel.tell is the one grant the +// controller already has a fallback for: refused (no holder, or the bus refuses it), the controller raises +// the secret-given condition exactly as before (ADR 0380 §9), so an older carried list still comes up and +// works. Every other grant stays required — missing one of those is a controller that comes up and is +// refused on the first thing it tries, with no fallback that covers it. +var DegradableGrants = []DegradableGrant{ + { + Subject: "mesh.seat." + VerbsTheControllerAsksTheOperatorChannel[0].Seat + ".tool." + VerbsTheControllerAsksTheOperatorChannel[0].Verb, + Reason: "refused: the controller raises the secret-given condition as before (ADR 0380 §9)", + }, +} + // perMachineEvents are a node-scoped seat's events about the holder itself, whose last token is the // holder's machine (novox/hq ADR 0219): `paused.`, the build agent saying whether it takes work. var perMachineEvents = map[string]bool{"paused.*": true}