From f52f47e1ff857dca79ac4242a118be6de821821b Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 1 Nov 2026 03:26:59 +0100 Subject: [PATCH] mesh-grants and the assigner read the grants part's key alone, root-free is each engine's word, and the hand-act log is the seat's alone (ADR 0571 clauses 9 and 11, ADR 0565 clause 11, issues 1953 and 1955) Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- .../integration_test.go | 8 ++-- .../cmd/mesh-delivery-assigner/main.go | 13 ++--- .../cmd/mesh-delivery-assigner/pass_test.go | 2 +- .../cmd/mesh-delivery-assigner/world.go | 12 ++--- .../cmd/mesh-delivery-assigner/world_test.go | 22 ++++----- .../mesh-delivery-assigner/manifest_test.go | 8 ++-- modules/mesh-delivery-assigner/module.json | 3 -- modules/mesh-grants/cmd/mesh-grants/tools.go | 6 +-- modules/mesh-grants/grants_test.go | 24 +++++----- modules/mesh-grants/internal/grants/grants.go | 35 -------------- modules/mesh-grants/internal/grants/inputs.go | 19 ++++---- .../mesh-grants/internal/grants/leasecheck.go | 2 +- .../mesh-grants/internal/grants/rootfree.go | 47 ++++++++++++------- .../mesh-grants/internal/grants/takeover.go | 8 +--- modules/mesh-grants/internal/grants/tools.go | 23 ++------- modules/mesh-grants/module.json | 6 +-- 16 files changed, 89 insertions(+), 149 deletions(-) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go index da1fc30..cd1670e 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go @@ -407,7 +407,7 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) { for _, fp := range []string{fNew, fOld} { gkey, _ := delivery.ControllerGrantKey("web", fp) grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{}) - b.putJSON(t, delivery.ControllerGrantsBucket, gkey, grant) + b.putJSON(t, delivery.PartGrantsBucket, gkey, grant) } runs := map[string]string{"ace": fOld, "g14": "build-9", "shanks": fOld} for _, node := range []string{"ace", "g14", "shanks"} { @@ -617,7 +617,7 @@ func TestTheAssignerReplacedResumesWithNothingLostOrRepeated(t *testing.T) { b.putJSON(t, delivery.BuilderBuildsBucket, bkey, built) gkey, _ := delivery.ControllerGrantKey(name, fp) grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{}) - b.putJSON(t, delivery.ControllerGrantsBucket, gkey, grant) + b.putJSON(t, delivery.PartGrantsBucket, gkey, grant) b.compose(t, "ace", name, fp, 'c', delivery.JudgedNeither) b.putJSON(t, provider.AssignmentsBucket, "ace."+name, []byte(fmt.Sprintf(`{"node":"ace","module":%q,"build":"b-%d"}`, name, i))) } @@ -663,7 +663,7 @@ func TestAWantWaitsForItsComposition(t *testing.T) { b.putJSON(t, delivery.BuilderBuildsBucket, bkey, built) gkey, _ := delivery.ControllerGrantKey("web", fNew) grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{}) - b.putJSON(t, delivery.ControllerGrantsBucket, gkey, grant) + b.putJSON(t, delivery.PartGrantsBucket, gkey, grant) on, _ := delivery.EncodePresence(delivery.Presence{Online: true}) b.putJSON(t, delivery.PresenceBucket, "ace", on) b.putJSON(t, provider.AssignmentsBucket, "ace.web", []byte(`{"node":"ace","module":"web","build":"b-1"}`)) @@ -712,7 +712,7 @@ func (b *bed) fixture(t *testing.T, name, fp, manifest string) { b.putJSON(t, delivery.BuilderBuildsBucket, bkey, built) gkey, _ := delivery.ControllerGrantKey(name, fp) grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{}) - b.putJSON(t, delivery.ControllerGrantsBucket, gkey, grant) + b.putJSON(t, delivery.PartGrantsBucket, gkey, grant) on, _ := delivery.EncodePresence(delivery.Presence{Online: true}) b.putJSON(t, delivery.PresenceBucket, "ace", on) b.putJSON(t, provider.AssignmentsBucket, "ace."+name, []byte(`{"node":"ace","module":"`+name+`","build":"b-1"}`)) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go index a3fd2f7..486ac10 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go @@ -59,7 +59,6 @@ func main() { var readBuckets = []string{ buscompose.ResolvedBucket, provider.AssignmentsBucket, - delivery.ControllerGrantsBucket, delivery.PartGrantsBucket, buscompose.LeaseBucket, delivery.ControllerComposedBucket, @@ -219,8 +218,8 @@ func startsWithout(name string) bool { return slices.Contains(seatBuckets, name) || sourceOfGrants(name) } -// sourceOfGrants says the bucket is one of the two that choose the grants' source, the grants part's grants and the -// controller's lease: neither stops the assigner when it is not there (delivery.GrantSource). +// sourceOfGrants says the bucket is the grants part's grants or the controller's lease: neither stops the assigner when it +// is not there. func sourceOfGrants(name string) bool { return name == delivery.PartGrantsBucket || name == buscompose.LeaseBucket } @@ -333,7 +332,6 @@ func (e emitting) Emit(eventType string, body any) error { return e.Conn.Emit(ev var watched = []string{ buscompose.ResolvedBucket, provider.AssignmentsBucket, - delivery.ControllerGrantsBucket, delivery.PartGrantsBucket, buscompose.LeaseBucket, delivery.ControllerComposedBucket, @@ -419,11 +417,10 @@ func follow(ctx context.Context, js jetstream.JetStream, nc process.Subscriber, if e.Operation() == jetstream.KeyValuePut { value = e.Value() } - before := w.grantsBucket() + before := w.handedAway() w.putEntry(name, e.Key(), value, e.Revision(), e.Created()) - if name == buscompose.LeaseBucket && w.grantsBucket() == before { - // The controller renews its lease every few seconds: only a change of where grants are read from - // makes a pass (delivery.GrantSource). + if name == buscompose.LeaseBucket && w.handedAway() == before { + // The controller renews its lease every few seconds: only the lease's being handed away makes a pass. continue } wake() diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go index f9c77cf..41347bd 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go @@ -272,7 +272,7 @@ func putOfTheBus(t *testing.T) { world(delivery.ControllerComposedBucket, ckey, cd) gkey, _ := delivery.ControllerGrantKey("nats", fp) g, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{}) - world(delivery.ControllerGrantsBucket, gkey, g) + world(delivery.PartGrantsBucket, gkey, g) got := step() akey, _ := delivery.RecordAskKey("novox", "nats", digest) if got.Step != delivery.PutPerson || got.Ask != akey { diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go index 542311e..ae1fd8a 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world.go @@ -584,15 +584,9 @@ func (w *world) value(bucket, key string) []byte { return w.values[bucket][key] } -// grantsBucket is the bucket the grants are read from now (delivery.GrantSource): the grants part's while the controller's -// lease is handed away, as the lease bucket's key says it (buscompose.LeaseHandedAway), the controller's in every other -// case, a lease not held here included. -func (w *world) grantsBucket() string { - raw := w.value(buscompose.LeaseBucket, buscompose.ControllerLeaseKey) - away, _ := buscompose.LeaseHandedAway(raw, raw != nil, nil) - bucket, _ := delivery.GrantSource(away) - return bucket -} +// grantsBucket is the bucket the grants are read from: the grants part's state `grants`, always (ADR 0571 clause 9); no +// manifest but the controller's reads the controller's grants. +func (w *world) grantsBucket() string { return delivery.PartGrantsBucket } // has says whether a bucket holds a key now. func (w *world) has(bucket, key string) bool { diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world_test.go index 158912b..d335a7f 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world_test.go @@ -128,24 +128,22 @@ func theWorldCountsTheValuesAKeyTakes(t *testing.T) { if got := w.changed(delivery.HaveBucket, "gone"); got != 0 { t.Errorf("changed = %d for a key never seen", got) } - // Issue 774: grants are read from the grants part's bucket only while the controller's lease is handed away: a grant - // only the part holds lets a node's put through then, and with a controller's lease (or none, or an unreadable one) it - // holds it, a grant only the controller holds being the one that counts instead. + // ADR 0571 clause 9: grants are read from the grants part's bucket in every case, a lease held, handed away, absent or + // unreadable alike, and the controller's own grants count for nothing. fp := delivery.SourcePrefix2 + strings.Repeat("d", 64) gkey, _ := delivery.ControllerGrantKey("web", fp) grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{Accounts: []string{"ace.web"}}) for _, row := range []struct { name, lease string inPart, inController bool - bucket string granted bool }{ - {"handed away, the part's grant", `{"handed-to":"the operator's hand-over"}`, true, false, delivery.PartGrantsBucket, true}, - {"handed away, only the controller's grant", `{"handed-to":"x"}`, false, true, delivery.PartGrantsBucket, false}, - {"a controller holds it, only the part's grant", `{"instance":"novox/1","epoch":3}`, true, false, delivery.ControllerGrantsBucket, false}, - {"a controller holds it, its own grant", `{"instance":"novox/1"}`, false, true, delivery.ControllerGrantsBucket, true}, - {"no lease read", ``, true, false, delivery.ControllerGrantsBucket, false}, - {"an unreadable lease", `[1]`, true, false, delivery.ControllerGrantsBucket, false}, + {"handed away, the part's grant", `{"handed-to":"the operator's hand-over"}`, true, false, true}, + {"handed away, only the controller's grant", `{"handed-to":"x"}`, false, true, false}, + {"a controller holds it, the part's grant", `{"instance":"novox/1","epoch":3}`, true, false, true}, + {"a controller holds it, only its own grant", `{"instance":"novox/1"}`, false, true, false}, + {"no lease read", ``, true, false, true}, + {"an unreadable lease", `[1]`, true, false, true}, } { p := newPasser(buckets{}, time.Now()) if row.lease != "" { @@ -157,8 +155,8 @@ func theWorldCountsTheValuesAKeyTakes(t *testing.T) { if row.inController { p.w.put(delivery.ControllerGrantsBucket, gkey, grant) } - if got := p.w.grantsBucket(); got != row.bucket { - t.Errorf("%s: grants are read from %s, want %s", row.name, got, row.bucket) + if got := p.w.grantsBucket(); got != delivery.PartGrantsBucket { + t.Errorf("%s: grants are read from %s, want the part's", row.name, got) } if _, granted := p.composedFor("ace", "web", fp); granted != row.granted { t.Errorf("%s: granted = %v, want %v", row.name, granted, row.granted) diff --git a/modules/mesh-delivery-assigner/manifest_test.go b/modules/mesh-delivery-assigner/manifest_test.go index 405447b..fd07447 100644 --- a/modules/mesh-delivery-assigner/manifest_test.go +++ b/modules/mesh-delivery-assigner/manifest_test.go @@ -33,7 +33,7 @@ func TestManifest(t *testing.T) { } reads := slices.Clone(m.Reads) slices.Sort(reads) - want := []string{"mesh-build-go.steps", "mesh-conditions.takeover", "mesh-config.kept", "mesh-config.resolved", "mesh-controller.assignments", "mesh-controller.composed", "mesh-controller.grants", "mesh-controller.lease", + want := []string{"mesh-build-go.steps", "mesh-conditions.takeover", "mesh-config.kept", "mesh-config.resolved", "mesh-controller.assignments", "mesh-controller.composed", "mesh-controller.lease", "mesh-delivery-builder.build-log", "mesh-delivery-builder.builds", "mesh-delivery-indexer.candidates", "mesh-delivery-indexer.manifests", "mesh-delivery-indexer.modules", "mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-grants.grants", "mesh-provisioner.provisions", "node-engine.have"} if !reflect.DeepEqual(reads, want) { @@ -59,7 +59,7 @@ func TestManifest(t *testing.T) { t.Errorf("uses = %v, want exactly node-build-go and operator-channel", m.Uses) } // ADR 0546: it serves its own tools (put, release, puts) from its bundle through the tool runner (the artifact loads the - // module), says each put's step as the event put, and calls the controller's warranted and the bus node's backup holder. + // module), says each put's step as the event put, and calls the hand-act seat and the bus node's backup holder. // ADR 0567 §1, §6-§10: it holds the seat mesh-placement, serving its seven verbs; keeps the seat's events on its stream // placement for 30 days; calls the controller's export, its four verbs that keep its assignments in step and its // hand-act log; and reads mesh-config's resolved values to compose every node a proposal touches. @@ -82,9 +82,9 @@ func TestManifest(t *testing.T) { !slices.Equal(slices.Sorted(slices.Values(m.Streams[0].Events)), slices.Sorted(slices.Values(delivery.PlacementEvents))) { t.Errorf("streams = %+v, want placement carrying the seat's four events for %d days", m.Streams, delivery.PlacementStreamDays) } - if want := []string{"seat:mesh-controller.assign", "seat:mesh-controller.hand-act", "seat:mesh-controller.pin", + if want := []string{"seat:mesh-controller.assign", "seat:mesh-controller.pin", "seat:mesh-controller.placement-export", "seat:mesh-controller.unassign", "seat:mesh-controller.unpin", - "seat:mesh-controller.warranted", "seat:mesh-hand-acts.hand-act", "seat:mesh-hand-acts.hand-acts", "seat:mesh-hand-acts.warranted", // ADR 0565 §9, §11: the seat, the controller's only while it has no holder + "seat:mesh-hand-acts.hand-act", "seat:mesh-hand-acts.hand-acts", "seat:mesh-hand-acts.warranted", // ADR 0565 §9, §11: the seat alone "seat:node-backup.backed-up", "seat:node-backup.now"}; !slices.Equal(slices.Sorted(slices.Values(m.Invokes)), want) { t.Errorf("invokes = %v, want %v", m.Invokes, want) } diff --git a/modules/mesh-delivery-assigner/module.json b/modules/mesh-delivery-assigner/module.json index 91ad85a..5ca8293 100644 --- a/modules/mesh-delivery-assigner/module.json +++ b/modules/mesh-delivery-assigner/module.json @@ -32,7 +32,6 @@ "mesh-config.resolved", "mesh-controller.assignments", "mesh-controller.composed", - "mesh-controller.grants", "mesh-controller.lease", "mesh-delivery-builder.build-log", "mesh-delivery-builder.builds", @@ -51,12 +50,10 @@ ], "invokes": [ "seat:mesh-controller.assign", - "seat:mesh-controller.hand-act", "seat:mesh-controller.pin", "seat:mesh-controller.placement-export", "seat:mesh-controller.unassign", "seat:mesh-controller.unpin", - "seat:mesh-controller.warranted", "seat:mesh-hand-acts.hand-act", "seat:mesh-hand-acts.hand-acts", "seat:mesh-hand-acts.warranted", diff --git a/modules/mesh-grants/cmd/mesh-grants/tools.go b/modules/mesh-grants/cmd/mesh-grants/tools.go index 8977fe2..de93298 100644 --- a/modules/mesh-grants/cmd/mesh-grants/tools.go +++ b/modules/mesh-grants/cmd/mesh-grants/tools.go @@ -65,9 +65,9 @@ func schema(properties map[string]string, required ...string) map[string]any { // toolsOf are the part's tools over a part (grants.Tools names them). func toolsOf(p *grants.Part) []stdio.Tool { return []stdio.Tool{ - {Name: grants.ToolGrants, Description: "What the grants part holds of grants (novox/hq ADR 0547 clause 6): in this milestone it " + - "writes none (grants.. from the third milestone, clause 5), and shows beside it the grants the controller " + - "wrote (mesh-controller_grants), all or one module's with the accounts each names. Read-only.", + {Name: grants.ToolGrants, Description: "What the grants part holds of grants (novox/hq ADR 0547 clause 5, ADR 0571 clause 9): the grants it wrote " + + "(grants.. in its state grants, the one key the assigner reads), all or one module's with the accounts each names. " + + "Read-only.", Input: schema(map[string]string{"module": "one module"}), Run: func(a map[string]any) (any, error) { return p.Grants(grants.Answer(a, "module")) }}, {Name: grants.ToolMembership, Description: "One module's membership on one node (ADR 0547 clause 6): as composed now from the " + diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 790d1ce..9f8ec77 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -59,7 +59,7 @@ type fakeBus struct { refuseCreate bool // refuseUsers refuses the bus module's nats_users: the loaded list cannot be read. refuseUsers bool - // rootFree answers the controller's root-free verb: nil is no answerer at all (no responders). + // rootFree answers a machine's engine's root-free verb: nil is no answerer at all (no responders). rootFree func(machines []string) (any, error) // consumers are the durable consumers the bus holds, by stream, as nats_streams answers them for one stream; and // consumerCreates each consumer the part asked to create, `.`. @@ -191,8 +191,13 @@ func (b *fakeBus) Call(key string, body any) (json.RawMessage, error) { } b.mu.Lock() defer b.mu.Unlock() - b.calls = append(b.calls, key) args, _ := body.(map[string]any) + // A node seat's verb is asked of one machine: `@`; the call is kept and judged by the verb alone. + machine := "" + if base, m, ok := strings.Cut(key, "@"); ok { + key, machine = base, m + } + b.calls = append(b.calls, key) switch key { case grants.CallUsers: if b.refuseUsers { @@ -222,7 +227,7 @@ func (b *fakeBus) Call(key string, body any) (json.RawMessage, error) { if b.rootFree == nil { return nil, errors.New("nats: no responders available for request") } - out, err := b.rootFree(args["machines"].([]string)) + out, err := b.rootFree([]string{machine}) if err != nil { return nil, err } @@ -350,7 +355,6 @@ func fixture() *fakeBus { b.put(grants.ReadHave, "b.node", map[string]any{"v": 1}) b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, UserList: &delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Wanted: "sha256:aa", Digest: "sha256:aa", Users: 6}}) - b.put(grants.ReadControllerGrant, "mod-x.0123456789ab0123456789ab0123456789ab0123456789ab0123456789ab0123", delivery.ControllerGrant{V: delivery.Version, Accounts: []string{"a.mod-x"}}) return b } @@ -613,7 +617,7 @@ func TestGrants(t *testing.T) { } }) - t.Run("root-free: the controller's word fills the composition, failing closed (issue 1945)", func(t *testing.T) { + t.Run("root-free: each engine's word fills the composition, failing closed (issue 1945)", func(t *testing.T) { const router = `{"module":"messenger","version":"1","runs-as":"messenger","resources":[{"id":"account","type":"user","name":"messenger","shell":"/usr/bin/nologin","home":"/var/lib/messenger"}],"own-secrets":{"broker":"/var/lib/m/broker"},"secrets-owner":"messenger",` + `"seats":[{"name":"operator-channel","scope":"mesh","serves":["open"]},{"name":"channel","scope":"mesh","kinded":true,"accepts":["show"]}],` + `"claims":[{"name":"operator-channel","scope":"mesh","serves":["open"]}]}` @@ -1317,7 +1321,7 @@ func TestGrants(t *testing.T) { t.Errorf("membership = %v", ms) } gr, _ := p.Grants("mod-x") - if g, _ := gr.(map[string]any); g["writes"] != grants.WritesGrants || g["controller_count"] != 1 { + if g, _ := gr.(map[string]any); g["writes"] != grants.WritesGrants || g["controller"] != nil || g["controller_count"] != nil { t.Errorf("grants = %v", gr) } @@ -2064,9 +2068,6 @@ func TestGrants(t *testing.T) { b.put(grants.ReadAccounts, user, map[string]any{"user": user, "hash": "$2a$10$x", "generation": 1}) } held := func() []string { return slices.Sorted(maps.Keys(b.own[grants.StateGrants])) } - // The controller's grant for this fingerprint, beside the stale one the fixture holds for another. - delete(b.states[grants.ReadControllerGrant], "mod-x.0123456789ab0123456789ab0123456789ab0123456789ab0123456789ab0123") - b.put(grants.ReadControllerGrant, key("mod-x"), delivery.ControllerGrant{V: delivery.Version, Accounts: []string{"a.mod-x"}}) hash("a.mod-x") hash("a.nats") res := p.Compose() @@ -2091,11 +2092,10 @@ func TestGrants(t *testing.T) { if res := p.Compose(); len(res.Grants.Written) != 0 || said() != 2 { t.Errorf("a second pass wrote %v and said %d", res.Grants.Written, said()) } - // The tool: equal on mod-x, and nats is the part's alone (the controller's fixture holds none). + // The tool answers what the part wrote, and reads nothing of the controller's. got, err := p.Grants("") gm, _ := got.(map[string]any) - diff, _ := gm["differences"].([]grants.GrantDifference) - if err != nil || gm["equal"] != false || len(diff) != 1 || diff[0].Key != key("nats") || diff[0].Kind != "only the part's" { + if _, ok := gm["controller"]; err != nil || gm["count"] != 2 || ok { t.Errorf("grants = %+v, %v", got, err) } // A user with a hash later: written once. Without it again: deleted. Unread hashes: nothing deleted. diff --git a/modules/mesh-grants/internal/grants/grants.go b/modules/mesh-grants/internal/grants/grants.go index 2b68fd1..6b903e2 100644 --- a/modules/mesh-grants/internal/grants/grants.go +++ b/modules/mesh-grants/internal/grants/grants.go @@ -469,38 +469,3 @@ func withVersion(g delivery.ControllerGrant) delivery.ControllerGrant { g.V = delivery.Version return g } - -// GrantDifference is one key on which the part's grants and the controller's differ. -type GrantDifference struct { - Key string `json:"key"` - // Kind is `only the controller's`, `only the part's` or `accounts differ`. - Kind string `json:"kind"` - Controller []string `json:"controller_accounts,omitempty"` - Part []string `json:"part_accounts,omitempty"` -} - -// CompareGrants lists every key the two sets differ on, in key order: a key held by one alone, or held by both with other -// accounts. Pure. -func CompareGrants(controller, part map[string]delivery.ControllerGrant) []GrantDifference { - keys := map[string]bool{} - for k := range controller { - keys[k] = true - } - for k := range part { - keys[k] = true - } - out := []GrantDifference{} - for _, k := range sortedKeys(keys) { - c, inC := controller[k] - g, inP := part[k] - switch { - case inC && !inP: - out = append(out, GrantDifference{Key: k, Kind: "only the controller's", Controller: c.Accounts}) - case inP && !inC: - out = append(out, GrantDifference{Key: k, Kind: "only the part's", Part: g.Accounts}) - case !delivery.SameControllerGrant(withVersion(c), withVersion(g)): - out = append(out, GrantDifference{Key: k, Kind: "accounts differ", Controller: c.Accounts, Part: g.Accounts}) - } - } - return out -} diff --git a/modules/mesh-grants/internal/grants/inputs.go b/modules/mesh-grants/internal/grants/inputs.go index 5ed4184..6f804f3 100644 --- a/modules/mesh-grants/internal/grants/inputs.go +++ b/modules/mesh-grants/internal/grants/inputs.go @@ -24,14 +24,13 @@ import ( // The states of other parts the grants part reads, as its manifest's reads name them (`.`). const ( - ReadAssignments = "mesh-controller.assignments" - ReadControllerGrant = "mesh-controller.grants" - ReadModules = "mesh-delivery-indexer.modules" - ReadManifests = "mesh-delivery-indexer.manifests" - ReadHave = "node-engine.have" - ReadResolved = "mesh-config.resolved" - ReadWants = "mesh-delivery-assigner.wants" - ReadBuilds = "mesh-delivery-builder.builds" + ReadAssignments = "mesh-controller.assignments" + ReadModules = "mesh-delivery-indexer.modules" + ReadManifests = "mesh-delivery-indexer.manifests" + ReadHave = "node-engine.have" + ReadResolved = "mesh-config.resolved" + ReadWants = "mesh-delivery-assigner.wants" + ReadBuilds = "mesh-delivery-builder.builds" // ReadAccounts is the vault's bcrypt hashes of the bus accounts it holds, by bus user (ADR 0547 clause 9). ReadAccounts = "mesh-vault.bus-accounts" // ReadPlacement is the seat mesh-placement's placement (ADR 0567 §1, §9): each node's modules and the seats @@ -42,7 +41,7 @@ const ( // Reads is every state the part reads, in the manifest's order. ReadAssignments stays (and in Followed) only while the // controller holds the lease, when the assignments are unioned with the placement (ReadInputs); after the lease write // they place nothing, and the first change after it takes them out (../assignments-readers names the part until then). -var Reads = []string{ReadAssignments, ReadControllerGrant, ReadModules, ReadManifests, ReadResolved, ReadHave, ReadWants, ReadBuilds, +var Reads = []string{ReadAssignments, ReadModules, ReadManifests, ReadResolved, ReadHave, ReadWants, ReadBuilds, ReadAccounts, ReadPlacement, ReadLease, ReadTakeover} // The bus module's tools the part calls, as its manifest's invokes name them: read-only, each. @@ -53,7 +52,7 @@ const ( ) // Invokes is every tool the part calls. -var Invokes = []string{CallUsers, CallUserCan, CallStreams, CallRootFree, CallRecordWarrant, CallRecordWarrantController} +var Invokes = []string{CallUsers, CallUserCan, CallStreams, CallRootFree, CallRecordWarrant} // Reader reads another part's state, named `.`. A refusal (a read not granted yet, a bucket not there) is // an error the part names as a gap and composes past: it never stops for one. diff --git a/modules/mesh-grants/internal/grants/leasecheck.go b/modules/mesh-grants/internal/grants/leasecheck.go index bf27c90..7c500a7 100644 --- a/modules/mesh-grants/internal/grants/leasecheck.go +++ b/modules/mesh-grants/internal/grants/leasecheck.go @@ -103,7 +103,7 @@ var EndedBy = map[string]string{ DependsRead + " mesh-controller.grants": "ADR 0571 §9: no manifest reads the controller's grants; the grants part's key is the one read", DependsInvoke + " seat:mesh-controller.hand-act": "ADR 0565 §11: every recorder calls mesh-hand-acts", DependsInvoke + " seat:mesh-controller.warranted": "ADR 0565 §11: every recorder calls mesh-hand-acts", - DependsInvoke + " seat:mesh-controller.root-free": "ADR 0571 §11: root-free is the self-check's probe", + DependsInvoke + " seat:mesh-controller.root-free": "ADR 0571 §11: each machine's engine answers root-free (seat:node-engine.root-free@machine)", DependsInvoke + " seat:mesh-controller.held": "ADR 0571 §2: held is the seat mesh-artifact-sweep's", DependsRead + " mesh-controller.declared-packages": "ADR 0565 §6: each node agent writes its own node's packages", } diff --git a/modules/mesh-grants/internal/grants/rootfree.go b/modules/mesh-grants/internal/grants/rootfree.go index 568b516..7e981e6 100644 --- a/modules/mesh-grants/internal/grants/rootfree.go +++ b/modules/mesh-grants/internal/grants/rootfree.go @@ -11,9 +11,11 @@ import ( "git.novox.be/novox/mesh-sdk/go/health" ) -// CallRootFree is the controller seat's verb that judges machines root-free (novox/hq ADR 0259 §8): the one judgement -// the composer needs and the part cannot make from the bus (issue 1945). Only the controller answers it, on purpose. -const CallRootFree = "seat:mesh-controller.root-free" +// CallRootFree is the node seat's verb that judges one machine root-free (novox/hq ADR 0259 §8, ADR 0563 §6), asked of +// that machine's own engine as `seat:node-engine.root-free@`: the judgement the composer needs and the part +// cannot make from the bus (issue 1945). Only that machine's engine answers it, and the controller takes no part in it +// (ADR 0571 §11, issue 1955). The messenger asks the same verb the same way. +const CallRootFree = "seat:node-engine.root-free" // The condition events the part hears (never a timer): a machine's root-free word changes with the raising and the // clearing of its `root-not-free` condition. They are the controller seat's until the operator's take-over, and the @@ -99,36 +101,45 @@ func RootFreeNodes(r buscompose.Records) []string { return nodes } -// verdict is one machine's answer of the controller's root-free verb. +// verdict is one machine's answer of its engine's root-free verb. type verdict struct { Machine string `json:"machine"` Free bool `json:"free"` Why string `json:"why"` } -// WithRootFree fills r.RootFree for the machines the composition needs by asking the controller once, and answers a -// line for each machine that is not free, `: `. It fails closed: an error, no answer or a machine -// left out is not free, and says so. Pure but for the one call; the answer is kept for this pass alone. +// WithRootFree fills r.RootFree for the machines the composition needs by asking each machine's own engine about that +// machine, and answers a line for each machine that is not free, `: `. It fails closed: an error, no +// answer or a verdict an engine gives on another machine is not free, and says so. Pure but for the calls; the answers +// are kept for this pass alone. func WithRootFree(r *buscompose.Records, call Caller) []string { nodes := RootFreeNodes(*r) if len(nodes) == 0 { return nil } free := map[string]verdict{} - var failure string - if call == nil { - failure = "no way to ask the controller here" - } else if raw, err := call.Call(CallRootFree, map[string]any{"machines": nodes}); err != nil { - failure = "the controller's root-free word could not be asked: " + err.Error() - } else { + failures := map[string]string{} + for _, n := range nodes { + if call == nil { + failures[n] = "no way to ask the machine's engine here" + continue + } + raw, err := call.Call(CallRootFree+"@"+n, map[string]any{}) + if err != nil { + failures[n] = "the engine's root-free word could not be asked: " + err.Error() + continue + } var body struct { Machines []verdict `json:"machines"` } if err := json.Unmarshal(raw, &body); err != nil { - failure = "the controller's root-free answer could not be read: " + err.Error() + failures[n] = "the engine's root-free answer could not be read: " + err.Error() + continue } for _, v := range body.Machines { - free[v.Machine] = v + if v.Machine == n { + free[n] = v + } } } r.RootFree = map[string]bool{} @@ -138,10 +149,10 @@ func WithRootFree(r *buscompose.Records, call Caller) []string { switch { case answered && v.Free: r.RootFree[n] = true - case failure != "": - notes = append(notes, fmt.Sprintf("%s: not free, %s", n, failure)) + case failures[n] != "": + notes = append(notes, fmt.Sprintf("%s: not free, %s", n, failures[n])) case !answered: - notes = append(notes, fmt.Sprintf("%s: not free, the controller named no verdict for it", n)) + notes = append(notes, fmt.Sprintf("%s: not free, its engine named no verdict for it", n)) default: notes = append(notes, fmt.Sprintf("%s: not free, %s", n, v.Why)) } diff --git a/modules/mesh-grants/internal/grants/takeover.go b/modules/mesh-grants/internal/grants/takeover.go index d5c94f7..448cf45 100644 --- a/modules/mesh-grants/internal/grants/takeover.go +++ b/modules/mesh-grants/internal/grants/takeover.go @@ -519,12 +519,8 @@ func (p *Part) TakeOverState() (any, error) { // Module is the grants part's module, the asker of its take-over and hand-back. const Module = "mesh-grants" -// The hand-act log's verbs the part records its switches by (novox/hq ADR 0565 §11, ADR 0547 §6): the seat's, and the -// controller's while the seat has no holder and the controller holds its lease (handacts.Recorder). -const ( - CallRecordWarrant = "seat:" + handacts.Seat + ".warranted" - CallRecordWarrantController = "seat:mesh-controller.warranted" -) +// The hand-act log's verb the part records its switches by (novox/hq ADR 0565 §11, ADR 0547 §6): the seat's. +const CallRecordWarrant = "seat:" + handacts.Seat + ".warranted" // recordWarrant records, in the hand-act log, that a take-over or a hand-back was made on the operator's warrant for ask. // Said, never fatal: the switch stands either way. diff --git a/modules/mesh-grants/internal/grants/tools.go b/modules/mesh-grants/internal/grants/tools.go index dcc3911..71c0711 100644 --- a/modules/mesh-grants/internal/grants/tools.go +++ b/modules/mesh-grants/internal/grants/tools.go @@ -31,12 +31,11 @@ const ( var Tools = []string{ToolGrants, ToolMembership, ToolUserList, ToolComposeNow, ToolAssert, ToolTakeOver, ToolHandBack, ToolTakeOverState, ToolAccounts, ToolConsumerMatch, ToolAcceptHashes, ToolLeaseCheck} -// WritesGrants is what grants says of the part's own grants in this milestone: written, and read by nobody until the -// controller's lease is handed away (delivery.GrantSource). -const WritesGrants = "in shadow: the grants part writes grants.. into its state grants, which the assigner reads only once the controller's lease is handed away (ADR 0547 clause 5); the controller's are beside, with every difference" +// WritesGrants is what grants says of the part's own grants: written once the list the bus loaded carries a build's +// accounts, and the one key the assigner reads (ADR 0571 clause 9, which changes ADR 0547 clause 5). +const WritesGrants = "the grants part writes grants.. into its state grants, once the list the bus loaded carries that build's accounts; it is the one key the assigner reads (ADR 0571 clause 9)" -// Grants answers the tool grants: the grants the part wrote, the controller's, and every difference between them, one -// module's or all. +// Grants answers the tool grants: the grants the part wrote, one module's or all. func (p *Part) Grants(module string) (any, error) { out := map[string]any{"writes": WritesGrants} read := func(get func() ([]string, func(string) ([]byte, bool, error), error)) (map[string]delivery.ControllerGrant, error) { @@ -65,10 +64,6 @@ func (p *Part) Grants(module string) (any, error) { } return m, nil } - controller, cerr := read(func() ([]string, func(string) ([]byte, bool, error), error) { - keys, err := p.Read.Keys(ReadControllerGrant) - return keys, func(k string) ([]byte, bool, error) { return p.Read.Get(ReadControllerGrant, k) }, err - }) mine, perr := read(func() ([]string, func(string) ([]byte, bool, error), error) { keys, err := p.Store.Keys(StateGrants) return keys, func(k string) ([]byte, bool, error) { @@ -89,21 +84,11 @@ func (p *Part) Grants(module string) (any, error) { } return out } - if cerr != nil { - out["controller_unread"] = cerr.Error() - } else { - out["controller"], out["controller_count"] = list(controller), len(controller) - } if perr != nil { out["written_unread"] = perr.Error() } else { out["written"], out["count"] = list(mine), len(mine) } - if cerr == nil && perr == nil { - diff := CompareGrants(controller, mine) - out["differences"], out["differing"] = diff, len(diff) - out["equal"] = len(diff) == 0 - } if last := p.Last(); last.Grants != nil { out["last_pass"] = last.Grants } diff --git a/modules/mesh-grants/module.json b/modules/mesh-grants/module.json index cc7ecc5..0d8f39c 100644 --- a/modules/mesh-grants/module.json +++ b/modules/mesh-grants/module.json @@ -5,7 +5,6 @@ "reads": [ "mesh-config.resolved", "mesh-controller.assignments", - "mesh-controller.grants", "mesh-delivery-assigner.wants", "mesh-delivery-builder.builds", "mesh-delivery-indexer.manifests", @@ -30,9 +29,8 @@ "nats.nats_streams", "nats.nats_user_can", "nats.nats_users", - "seat:mesh-controller.root-free", - "seat:mesh-hand-acts.warranted", - "seat:mesh-controller.warranted" + "seat:node-engine.root-free", + "seat:mesh-hand-acts.warranted" ], "emits": [ "asserted",