diff --git a/modules/mesh-grants/cmd/mesh-grants/tools.go b/modules/mesh-grants/cmd/mesh-grants/tools.go index 0edd4c0..830c43c 100644 --- a/modules/mesh-grants/cmd/mesh-grants/tools.go +++ b/modules/mesh-grants/cmd/mesh-grants/tools.go @@ -118,8 +118,8 @@ func toolsOf(p *grants.Part) []stdio.Tool { Run: func(a map[string]any) (any, error) { return p.TakeOver(stdio.CallerOf(a), grants.Answer(a, "why")) }}, - {Name: grants.ToolHandBack, Description: "Ask the operator to switch the bus node back to the controller's list (ADR 0547 clause 8), " + - "by their yes on their channel; in shadow there is nothing to hand back, and it says so. Every act is recorded.", + {Name: grants.ToolHandBack, Description: "Gone: the controller ended by its unassignment (issue 1126), so nothing writes its user list and the grants " + + "part's list is the only one; it says so and asks nothing (ADR 0547 §11).", Input: schema(map[string]string{"why": "why, kept with the act"}, "why"), Run: func(a map[string]any) (any, error) { return p.HandBack(stdio.CallerOf(a), grants.Answer(a, "why")) @@ -129,15 +129,14 @@ func toolsOf(p *grants.Part) []stdio.Tool { "wrote it to its state accounts, which the vault alone reads: user, kind, node, module, process, the bus's address and " + "fingerprint, a module's claims with the verbs each seat promises, and when the key was written; one user's or all; " + "what the last pass wrote, deleted or kept; and the durable consumers the accounts need as the last pass found them " + - "(missing, differing and why, created; whether the lease is handed away). Never a password. Read-only: " + + "(missing, differing and why, created). Never a password. Read-only: " + "grants_compose_now writes them again.", Input: schema(map[string]string{"user": "one bus user, e.g. ace.mesh-build-go"}), Run: func(a map[string]any) (any, error) { return p.Accounts(grants.Answer(a, "user")) }}, {Name: grants.ToolConsumerMatch, Description: "Bring one durable consumer an account needs (a module's on EVENTS, a process's on " + "its work queue; novox/hq ADR 0561 §6) to the configuration the bus composer composes, by hand: the recovery when " + "the condition mesh-grants.consumer-differs names it. Asks the part's process, which alone may, and keeps where the " + - "consumer delivers from. Refused while the controller holds its lease (the consumers are the controller's then) and " + - "for a consumer the records do not compose. grants_user_list's last pass says what became of it.", + "consumer delivers from. Refused for a consumer the records do not compose. grants_user_list's last pass says what became of it.", Input: schema(map[string]string{"stream": "the stream, e.g. EVENTS", "consumer": "the consumer, e.g. ace_mesh-build-go"}, "stream", "consumer"), Run: func(a map[string]any) (any, error) { return p.MatchConsumer(grants.Answer(a, "stream"), grants.Answer(a, "consumer")) diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 09dc244..0056be1 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -11,7 +11,6 @@ import ( "maps" "os" "path/filepath" - "reflect" "slices" "strings" "sync" @@ -540,7 +539,7 @@ func TestGrants(t *testing.T) { gap string }{ {"every record on the bus", func(*fakeBus) {}, - []string{"a.mod-x", "a.nats", "b.mod-y", "controller", "node.a", "node.b"}, "enrolment tokens"}, + []string{"a.mod-x", "a.nats", "b.mod-y", "controller", "node.a", "node.b"}, "people"}, {"a manifest missing is a gap and its user is left out", func(b *fakeBus) { delete(b.states[grants.ReadModules], "mod-y") }, []string{"a.mod-x", "a.nats", "controller", "node.a", "node.b"}, "manifest of mod-y"}, {"a want's built manifest is composed, not the trunk's; the list's records carry both (issue 774)", func(b *fakeBus) { @@ -551,7 +550,7 @@ func TestGrants(t *testing.T) { b.put(grants.ReadWants, "want.a.mod-x", delivery.Want{V: delivery.Version, FP: fp, Composed: "sha256:" + strings.Repeat("ef", 32)}) key, _ := delivery.BuilderBuildKey("mod-x", fp) b.put(grants.ReadBuilds, key, map[string]any{"v": delivery.Version, "state": "done", "manifest": digest}) - }, []string{"a.mod-x", "a.nats", "b.mod-y", "controller", "node.a", "node.b"}, "enrolment tokens"}, + }, []string{"a.mod-x", "a.nats", "b.mod-y", "controller", "node.a", "node.b"}, "people"}, {"placement refused is a gap; the machines come from the haves", func(b *fakeBus) { b.refused[grants.ReadPlacement] = true }, []string{"controller", "node.a", "node.b"}, grants.ReadPlacement}, } { @@ -1316,7 +1315,7 @@ func TestGrants(t *testing.T) { } // Written in shadow, the bus node told to follow the controller's list; every user left out while the vault holds // no hash; a tool's part (no object bucket) writes none. - if g, _, had := p.Sealed(); !had || g.Follow != delivery.FollowController || g.Revision != 1 || g.Node != "a" || + if g, _, had := p.Sealed(); !had || g.Follow != delivery.FollowController || !g.LeaseHanded || g.Revision != 1 || g.Node != "a" || g.Users != 0 || len(g.LeftOut) != 6 || len(objs.objects) != 1 || !strings.Contains(open(g), "accounts {") { t.Errorf("the sealed list in shadow = %+v, %d objects", g, len(objs.objects)) } @@ -1328,14 +1327,15 @@ func TestGrants(t *testing.T) { } else if _, _, had := tp.Sealed(); had { t.Error("a tool's part wrote the sealed list") } - if len(first.Published) != 3 || len(b.publishes) != 3 { - t.Errorf("the first composition published %v, want the three assignments' memberships", first.Published) + // Written to the state alone, never said on their subject, with no lease key (1126/A1). + if len(first.Published) != 3 || first.MembershipsWritten != 3 || len(b.publishes) != 0 { + t.Errorf("the first composition published %v (%d on the subject), want the three assignments' memberships in the state alone", first.Published, len(b.publishes)) } // The second pass says each membership unchanged (published at the first); the third writes nothing at all. if again := p.Compose(); again.Written || again.Revision != 1 || len(again.Published) != 0 || again.AssertionsWritten != 0 { t.Errorf("a composition with no input changed wrote or published: %+v", again) } - if third := p.Compose(); third.Written || third.MembershipsWritten != 0 || len(third.Published) != 0 || len(b.publishes) != 3 { + if third := p.Compose(); third.Written || third.MembershipsWritten != 0 || len(third.Published) != 0 || len(b.publishes) != 0 { t.Errorf("a membership equal to the one last published was published again, or a state written: %+v", third) } // mod-x placed on b too: b.mod-x is added and its first membership published; a.mod-x's loses the module's plain @@ -1402,8 +1402,6 @@ func TestGrants(t *testing.T) { var out any if act == "take-over" { out, err = p.TakeOver("shanks.node-tools", "a row") - } else { - out, err = p.HandBack("shanks.node-tools", "a row") } if err != nil { t.Fatalf("%s: %v", act, err) @@ -1452,7 +1450,6 @@ func TestGrants(t *testing.T) { }, []string{"a.mod-x: 1 read"}}, {"a yes with only added rights and every hash held records the switch, the ask naming them", "take-over", yes, settled(lacks), nil, nil, grants.TookOver, "recorded", nil, []string{"adds 1 right to 1 user and takes none away", "a.mod-x: 1 read"}}, - {"a yes to hand back records the switch back", "hand-back", yes, func() {}, nil, nil, grants.Shadow, "recorded", nil, nil}, } { t.Run(row.name, func(t *testing.T) { row.setup() @@ -1522,8 +1519,8 @@ func TestGrants(t *testing.T) { // The key the bus node follows turns with the switch at once, and the state says switched only once the bus // node loaded the grants part's written list. g, _, _ := p.Sealed() - if want := map[string]string{grants.TookOver: delivery.FollowGrants, grants.Shadow: delivery.FollowController}[p.SwitchState()]; g.Follow != want { - t.Errorf("the key says follow %s with the switch %s", g.Follow, p.SwitchState()) + if want := map[string]string{grants.TookOver: delivery.FollowGrants, grants.Shadow: delivery.FollowController}[p.SwitchState()]; g.Follow != want || !g.LeaseHanded { + t.Errorf("the key says follow %s, lease handed %v, with the switch %s and no lease key (1126/A1/AC2)", g.Follow, g.LeaseHanded, p.SwitchState()) } effect := func() string { st, _ := p.TakeOverState(); return fmt.Sprint(st.(map[string]any)["effect"]) } if row.outcome == "recorded" && row.act == "take-over" { @@ -1663,10 +1660,11 @@ func TestGrants(t *testing.T) { } }) } - t.Run("hand-back in shadow is refused at once, with nothing asked", func(t *testing.T) { + // 1126/A1: the controller ended by its unassignment, so nothing writes its list and a hand-back is gone. + t.Run("hand-back is gone at once, with nothing asked", func(t *testing.T) { n := len(fa.asks) - if _, err := p.HandBack("shanks.node-tools", "a row"); err == nil || !strings.Contains(err.Error(), "nothing to hand back") { - t.Errorf("hand-back in shadow = %v", err) + if _, err := p.HandBack("shanks.node-tools", "a row"); err == nil || !strings.HasPrefix(err.Error(), "gone") { + t.Errorf("hand-back = %v", err) } p.Compose() if len(fa.asks) != n { @@ -1674,6 +1672,8 @@ func TestGrants(t *testing.T) { } }) t.Run("an ask that can stay open is closed every way and can be asked again", func(t *testing.T) { + // The rows above end taken over and a hand-back is gone (1126/A1): asked again from a part in shadow. + delete(b.own[grants.StateTakeover], grants.KeySwitch) // A warrant the process missed is caught up from the router's record, whatever its outcome. id := asked("take-over") p.Compose() @@ -1843,11 +1843,10 @@ func TestGrants(t *testing.T) { if row.names == "" && row.last != nil && !strings.Contains(string(state), "10.0.0.1") { t.Errorf("the private network of the last membership is not carried: %s", state) } + // Written to the state alone, never said on its subject (1126/A1). got, err := membership.DecodeGrantsMembership(state) - var sent membership.Membership - _ = json.Unmarshal(b.stream["mesh.assignment."+key], &sent) - if err != nil || !reflect.DeepEqual(got, sent) || bytes.Contains(state, []byte(`"membership"`)) { - t.Errorf("the state is not the bare membership published (%v): %s", err, state) + if err != nil || got.Node != "a" || got.Module != "mod-x" || bytes.Contains(state, []byte(`"membership"`)) || len(b.stream) != 0 { + t.Errorf("the state is not the bare membership published, or its subject said (%v, %d said): %s", err, len(b.stream), state) } if !strings.Contains(pass, row.names) { t.Errorf("the pass does not name %q: %s", row.names, pass) @@ -1866,23 +1865,6 @@ func TestGrants(t *testing.T) { } }) } - // The assignments stream removed (nats_stream_remove at the lease write) and its subject refused: every membership - // is still written to the state, where each runtime reads it, and the refusal is said (issue 774). - t.Run("the stream gone and its subject refused: memberships still written, the refusal said", func(t *testing.T) { - b := fixture() - b.loaded = shadowOf(t, b) - b.refusePub = true - p := partOn(b) - refused := p.Compose() - if !slices.Equal(refused.Published, []string{"a.mod-x", "a.nats", "b.mod-y"}) || refused.MembershipsWritten != 3 || - !strings.Contains(strings.Join(refused.Errors, "\n"), "Permissions Violation") { - t.Errorf("with the stream gone = %+v: every membership written, the refusal said", refused) - } - b.refusePub = false - if next := p.Compose(); len(next.Published) != 0 || next.MembershipsWritten != 0 { - t.Errorf("the next event published %v again", next.Published) - } - }) t.Run("a tool answers while a pass waits on the bus", func(t *testing.T) { b := fixture() b.loaded = shadowOf(t, b) @@ -2251,7 +2233,7 @@ func TestGrants(t *testing.T) { } return nil } - b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"handed-to": "the operator's hand-over"}) + // With no lease key (1126/A1). // The private network the mesh provides is placed with no fingerprint by construction: never a missing grant. b.assign("b", "mesh-wireguard") fp := "src2:d4afe7a1abab5ccdbef6d8629b2245006a2aec60290f430f7b4756e2930ed60c" @@ -2422,7 +2404,6 @@ func TestGrants(t *testing.T) { b := fixture() b.manifestOf("mesh-enrolment", enrolmentManifest) b.assign("a", "mesh-enrolment") - b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"handed-to": "the operator's hand-over"}) issued := time.Date(2026, 11, 2, 1, 0, 0, 0, time.UTC) for node, stage := range map[string]string{"lab": delivery.EnrolmentLive, "pi": delivery.EnrolmentAsked} { raw, err := delivery.EncodeEnrolment(delivery.Enrolment{Node: node, Address: "10.0.0.7", Why: "joins", @@ -2545,27 +2526,21 @@ func TestGrants(t *testing.T) { } }) - // 774/AC6 (novox/hq ADR 0561 §6): the durable consumers an account needs, with the controller's configuration, are - // compared in shadow while the controller holds the lease, and created, only where the bus lacks them, from the write - // that hands the lease away; one the bus holds is never changed. - t.Run("account consumers: compared while the controller holds the lease, created once it is handed away", func(t *testing.T) { + // 774/AC6 (novox/hq ADR 0561 §6), 1126/A1/AC1: the durable consumers an account needs, with the composed + // configuration, are created where the bus lacks them with no lease key at all (the controller ended by its + // unassignment, never by the lease write); one the bus holds is never changed. + t.Run("account consumers: created where the bus lacks them, with no lease key", func(t *testing.T) { const cManifest = `{"module":"mod-c","version":"1","own-secrets":{"broker":"/var/lib/c/broker"},"consumes":["mod-y.done"]}` b := fixture() b.manifestOf("mod-c", cManifest) b.assign("b", "mod-c") b.loaded = shadowOf(t, b) - b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"instance": "novox/1/2026-10-27", "epoch": 57}) p := partOn(b) p.Consumers = b - res := p.Compose() - if res.Consumers == nil || res.Consumers.On || !slices.Contains(res.Consumers.Missing, "EVENTS.b_mod-c") || len(b.consumerCreates) != 0 { - t.Fatalf("while the controller holds the lease: %+v, created %v", res.Consumers, b.consumerCreates) - } - b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"handed-to": "the operator's hand-over"}) b.consumers = map[string][]map[string]any{"EVENTS": {{"name": "b_other", "filters": []string{"x"}, "max_deliver": 5}}} - res = p.Compose() - if !res.Consumers.On || !slices.Equal(res.Consumers.Created, []string{"EVENTS.b_mod-c"}) { - t.Fatalf("once handed away: %+v", res.Consumers) + res := p.Compose() + if res.Consumers == nil || !slices.Equal(res.Consumers.Created, []string{"EVENTS.b_mod-c"}) { + t.Fatalf("with no lease key: %+v", res.Consumers) } var made map[string]any for _, c := range b.consumers["EVENTS"] { @@ -2581,7 +2556,7 @@ func TestGrants(t *testing.T) { t.Errorf("a consumer the bus holds was asked again: %+v, %v", res.Consumers, b.consumerCreates) } // A held consumer that differs is never changed by a pass: it is raised as a condition naming it, and brought to - // the composed configuration only by hand (grants_consumer_match), never while the controller holds the lease. + // the composed configuration only by hand (grants_consumer_match), with no lease key too. var said []asks.Condition p.Say = func(c asks.Condition) error { if c.Key != grants.GrantMissingCondition && c.Key != grants.RefusedCondition { // their own rows @@ -2596,11 +2571,6 @@ func TestGrants(t *testing.T) { !strings.Contains(said[0].Explanation, "EVENTS.b_mod-c") { t.Fatalf("a differing consumer: %+v, said %+v", res.Consumers, said) } - b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"instance": "novox/2/2026-10-27", "epoch": 58}) - if _, err := p.MatchConsumer("EVENTS", "b_mod-c"); err == nil || !strings.Contains(err.Error(), "controller holds the lease") { - t.Errorf("matched by hand under the controller's lease: %v", err) - } - b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"handed-to": "the operator's hand-over"}) if _, err := p.MatchConsumer("EVENTS", "b_nothing"); err == nil { t.Error("a consumer the records do not compose was asked to match") } diff --git a/modules/mesh-grants/internal/grants/consumers.go b/modules/mesh-grants/internal/grants/consumers.go index 90a5f62..53cd4f8 100644 --- a/modules/mesh-grants/internal/grants/consumers.go +++ b/modules/mesh-grants/internal/grants/consumers.go @@ -38,7 +38,8 @@ type ConsumerCreator interface { // ConsumersState is what one pass found and did of the account consumers. type ConsumersState struct { - // On says the lease is handed away, so the part creates what the bus lacks; Why says why it is on or off. + // On says the part creates what the bus lacks: always, since the controller ended by its unassignment (issue 1126); + // Why says so. On bool `json:"on"` Why string `json:"why"` // Wanted is how many the records compose; Missing those the bus lacks (`.`), Differ those it holds @@ -88,14 +89,13 @@ func heldConsumers(c Caller, stream string) (map[string]heldConsumer, error) { return out, nil } -// leaseHandedAway reads the lease as the part's reads give it. -func leaseHandedAway(r Reader) (bool, string) { - raw, found, err := r.Get(ReadLease, LeaseKey) - return buscompose.LeaseHandedAway(raw, found, err) -} +// ConsumersOn is why the part creates and matches the account consumers with no lease read: the controller ended by its +// unassignment, never by the lease write, so its lease names no holder for good and the part is their only maker +// (issue 1126; mesh-sdk's buscompose.LeaseHandedAway answers false for an absent key, so it is no longer asked here). +const ConsumersOn = "the grants part makes and matches the account consumers: the controller ended by its unassignment (issue 1126)" // consumers compares the account consumers the records compose with those the bus holds, and creates the missing ones -// once the lease is handed away and the part holds a creator. +// where the part holds a creator. func (p *Part) consumers(res *Result, recs buscompose.Records, match string) { users, err := buscompose.Users(recs) if err != nil { @@ -103,7 +103,7 @@ func (p *Part) consumers(res *Result, recs buscompose.Records, match string) { return } st := &ConsumersState{} - st.On, st.Why = leaseHandedAway(p.Read) + st.On, st.Why = true, ConsumersOn res.Consumers = st wanted := buscompose.AccountConsumers(users) st.Wanted = len(wanted) @@ -154,9 +154,8 @@ func (p *Part) consumers(res *Result, recs buscompose.Records, match string) { } // sayDiffer raises the warning condition mesh-grants.consumer-differs while an account consumer the part owns (from the -// lease write) differs from its composed configuration, naming each with the fields that differ, and clears it when they -// match. Said once per set of differences, again when it changes. Before the lease write the consumers are the -// controller's, and a difference is only said in the pass. +// controller's end) differs from its composed configuration, naming each with the fields that differ, and clears it when +// they match. Said once per set of differences, again when it changes. func (p *Part) sayDiffer(st *ConsumersState) { if p.Say == nil { return @@ -190,15 +189,12 @@ func (p *Part) sayDiffer(st *ConsumersState) { const ConsumerDiffersCondition = "mesh-grants.consumer-differs" // MatchConsumer is the tool consumer-match (ADR 0561 §6, §9): it asks the part's process to bring one account consumer -// the records compose to its composed configuration, keeping where it delivers from. Refused while the controller holds -// the lease (the consumers are the controller's then), and for a consumer the records do not compose. +// the records compose to its composed configuration, keeping where it delivers from. Refused for a +// consumer the records do not compose. func (p *Part) MatchConsumer(stream, name string) (any, error) { if stream == "" || name == "" { return nil, errors.New("consumer-match names a stream and a consumer") } - if on, why := leaseHandedAway(p.Read); !on { - return nil, fmt.Errorf("not asked: %s; the controller makes and matches the consumers while it holds the lease", why) - } in := ReadInputs(p.Read, &p.cache) recs, _ := Records(in) users, err := buscompose.Users(recs) diff --git a/modules/mesh-grants/internal/grants/grants.go b/modules/mesh-grants/internal/grants/grants.go index 99a25a4..7ae517d 100644 --- a/modules/mesh-grants/internal/grants/grants.go +++ b/modules/mesh-grants/internal/grants/grants.go @@ -314,15 +314,15 @@ func orSaid(s, none string) string { return s } -// sayMissing raises mesh-grants.grant-missing while a placed module has no grant, once the controller's lease is handed -// away (the part's grants are then the ones the assigner reads), and clears it when every one has one; in shadow the -// pass and the tool say them. Said once per set, again when it changes, and always at the first pass after a start. +// sayMissing raises mesh-grants.grant-missing while a placed module has no grant (the part's grants are the ones the +// assigner reads since the controller's end, issue 1126), and clears it when every one has one. Said once per set, again +// when it changes, and always at the first pass after a start. func (p *Part) sayMissing(missing []string) { if p.Say == nil { return } why := "" - if on, _ := leaseHandedAway(p.Read); on && len(missing) > 0 { + if len(missing) > 0 { why = strings.Join(missing, "; ") } p.holdMu.Lock() diff --git a/modules/mesh-grants/internal/grants/inputs.go b/modules/mesh-grants/internal/grants/inputs.go index ac8e8e1..433d489 100644 --- a/modules/mesh-grants/internal/grants/inputs.go +++ b/modules/mesh-grants/internal/grants/inputs.go @@ -81,8 +81,6 @@ type Gap struct { // Standing gaps: what the controller composes from its own store, which no bus value carries in this milestone. var standingGaps = []Gap{ - {Input: "enrolment tokens", Why: "while the controller holds the lease, its live enrolment tokens are in its store alone: " + - "an enrolment user is composed only from the seat mesh-enrolment's keys"}, {Input: "people", Why: "the people and the tools they may invoke are in the controller's store alone: no person user is composed"}, {Input: "seat holdings", Why: "who holds each seat held once for the mesh is in the controller's store alone: a mesh seat claimed on two machines is composed as held on both"}, {Input: "announced states", Why: "the controller does not announce its states-announced readable, so every read is composed from `reads`, as when announcements cannot be read"}, @@ -131,9 +129,6 @@ type Inputs struct { // NodeLogins is each node's key of the vault's node-logins (ReadNodeLogins, ADR 0583 §1); a node absent logs in as // node. alone. NodeLogins map[string]delivery.NodeLogin - // LeaseAway says the controller's lease is handed away, as ReadInputs read it: an enrolment user then enrols through - // the seat alone (ADR 0587 §6). - LeaseAway bool // Refused is every manifest the indexer holds that the part cannot read or mesh-sdk's reader refuses, assigned or // not, each with why: its module counts as placed nowhere, so it is said loudly, never passed over (issue 1945). Refused []Gap @@ -280,7 +275,6 @@ func ReadInputs(r Reader, cache *Manifests) Inputs { in.Placed[node] = p } } - in.LeaseAway, _ = leaseHandedAway(r) for node, p := range in.Placed { nodes[node] = true for _, m := range p.Assigned() { @@ -448,12 +442,8 @@ func ReadInputs(r Reader, cache *Manifests) Inputs { in.Gaps = append(in.Gaps, Gap{Input: "wanted manifests", Why: "no build record names the manifest at the wanted fingerprint, so the trunk's is used: " + strings.Join(fromTrunk, ", ")}) } in.Hashes = readHashes(r, &in.Gaps) - // The controller's tokens stand as a gap while it holds the lease; from the write the seat's keys are every token. - var enrolmentsRead bool - in.Enrolling, enrolmentsRead = readEnrolling(r, &in.Gaps) - if enrolmentsRead && in.LeaseAway { - in.Gaps = slices.DeleteFunc(in.Gaps, func(g Gap) bool { return g.Input == "enrolment tokens" }) - } + // The seat mesh-enrolment's keys are every token since the controller's end (issue 1126). + in.Enrolling, _ = readEnrolling(r, &in.Gaps) in.NodeLogins = readNodeLogins(r, &in.Gaps) for _, raw := range each(r, ReadResolved, &in.Gaps) { var v resolved diff --git a/modules/mesh-grants/internal/grants/part.go b/modules/mesh-grants/internal/grants/part.go index b7ef545..adce0f4 100644 --- a/modules/mesh-grants/internal/grants/part.go +++ b/modules/mesh-grants/internal/grants/part.go @@ -337,10 +337,8 @@ func (p *Part) Compose() Result { members = append(members, m.Membership) } seated := holdings.ByMemberships(members...) - // From the lease write the memberships are written to the state alone and no longer said on their subject, which - // nothing reads any more (novox/hq ADR 0571 clause 10, issue 774). - notice, _ := leaseHandedAway(p.Read) - notice = !notice + // The memberships are written to the state alone and never said on their subject, which nothing reads any more + // (novox/hq ADR 0571 clause 10, issue 774; unconditional since the controller's end, issue 1126). for _, key := range sortedKeys(ms) { // The last membership is the state's own (ADR 0571 clause 7), never the assignments stream's, which the bus // module's tools remove at the lease write (research 072 row 33): a last read there would freeze every membership @@ -360,11 +358,6 @@ func (p *Part) Compose() Result { m.Published, now = "not published (tried again at the next event): its own "+StateMemberships+" cannot be written", false } else { res.MembershipsWritten++ - if notice { - if err := p.Publish.PublishMembership(m.Node, m.Module, body); err != nil { - res.Errors = append(res.Errors, fmt.Sprintf("the membership of %s is written, its notice on its subject not said: %v", key, err)) - } - } } } // One unchanged is written bare once where the state still holds the wrapper written before 2026-10-30. diff --git a/modules/mesh-grants/internal/grants/records.go b/modules/mesh-grants/internal/grants/records.go index eaba280..ff8531d 100644 --- a/modules/mesh-grants/internal/grants/records.go +++ b/modules/mesh-grants/internal/grants/records.go @@ -39,7 +39,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) { // The enrolment users, from the seat mesh-enrolment's keys, and from the lease write through its redeem alone (ADR 0587 // §4, §6). out := buscompose.Records{Assigned: map[string][]buscompose.Declared{}, People: map[string][]string{}, - Interchangeable: map[string]bool{}, Enrolling: slices.Clone(in.Enrolling), LeaseHandedAway: in.LeaseAway, + Interchangeable: map[string]bool{}, Enrolling: slices.Clone(in.Enrolling), LeaseHandedAway: true, // And each node's logins (ADR 0583 §1): both while it rotates. NodeLogins: in.NodeLogins} values := func(provider, setting string) []any { diff --git a/modules/mesh-grants/internal/grants/sealed.go b/modules/mesh-grants/internal/grants/sealed.go index dbe2c83..8bed292 100644 --- a/modules/mesh-grants/internal/grants/sealed.go +++ b/modules/mesh-grants/internal/grants/sealed.go @@ -95,8 +95,9 @@ func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut, dropped cur, _, had := p.Sealed() next := cur next.Follow = followOf(p.SwitchState()) - // The bus node follows this list alone once the lease is handed (ADR 0571 clause 8, ADR 0582 clause 3). - next.LeaseHanded = p.leaseHanded() + // The bus node follows this list alone (ADR 0571 clause 8): always since the controller ended by its unassignment, + // with no lease written (issue 1126). + next.LeaseHanded = true key := "" if in.Loaded != nil { key = in.Loaded.Key diff --git a/modules/mesh-grants/internal/grants/takeover.go b/modules/mesh-grants/internal/grants/takeover.go index 9d717b6..aa02d7e 100644 --- a/modules/mesh-grants/internal/grants/takeover.go +++ b/modules/mesh-grants/internal/grants/takeover.go @@ -132,14 +132,11 @@ func (p *Part) SwitchState() string { // the refusals of Decide. Asking again while an ask is open is refused. func (p *Part) TakeOver(caller, why string) (any, error) { return p.request(ActTakeOver, caller, why) } -// HandBack is the tool: it asks the operator to switch the bus node back to the controller's list. Once the lease is -// handed to the grants part it is answered as gone: the grants part's list is the only one (ADR 0547 §11). +// HandBack is the tool: it is answered as gone. The controller ended by its unassignment (issue 1126), so nothing +// writes its list and the grants part's list is the only one (ADR 0547 §11). func (p *Part) HandBack(caller, why string) (any, error) { - if p.leaseHanded() { - return nil, errors.New("gone: the controller's lease is handed to mesh-grants, so the grants part's list is the only " + - "one and the user list is not handed back (ADR 0547 §11); lease-hand-back gives the lease back first") - } - return p.request(ActHandBack, caller, why) + return nil, errors.New("gone: the controller ended by its unassignment (issue 1126), so nothing writes its user list; " + + "the grants part's list is the only one and the user list is not handed back (ADR 0547 §11)") } // nonce is four hex digits that keep two asks of one second apart. @@ -546,13 +543,7 @@ func (p *Part) recordWarrant(ask string) { if p.Call == nil { return } - rec := handacts.Recorder{Ask: p.Call.Call, LeaseAway: func() bool { - if p.Read == nil { - return false - } - away, _ := leaseHandedAway(p.Read) - return away - }} + rec := handacts.Recorder{Ask: p.Call.Call} taken, err := rec.RecordWarrant(Module, ask) if p.Logf == nil { return