Keep holding a membership that would drop a capability its holder still asks for (issue 1945)
mesh/delivery merged: source.main moved to novox/mesh-delivery@da514816204b
mesh/check being carried to ec0b7e2dcc06 on da514816204b | from 24cb379434f5
mesh/delivery merged: source.main moved to novox/mesh-delivery@da514816204b
mesh/check being carried to ec0b7e2dcc06 on da514816204b | from 24cb379434f5
A dropped verified-sender comes from a root-free word that failed for one pass, not from a release, and losing it cut the operator's approvals on 2026-10-27. Holdings.CapabilitiesLost holds such a membership with its own reason while the holder is assigned and its manifest still asks for it; a dropped tool or field and a moved seat stay published.
This commit is contained in:
@@ -86,7 +86,8 @@ func toolsOf(p *grants.Part) []stdio.Tool {
|
||||
"the assertions, publish each membership that differs from the one last published, and keep what changed in its own " +
|
||||
"state (one revision when an input changed, none when not), saying user-list-changed and membership-changed. Answers " +
|
||||
"the ask and the last pass; grants_user_list shows the pass that answers it. A membership is published unless it would " +
|
||||
"leave a seat a placed module claims with no holder, which stays a shadow difference, named. The user list, assertions and " +
|
||||
"leave a seat a placed module claims with no holder, or drop a capability its holder still asks for (verified-sender), " +
|
||||
"which stays a shadow difference, named. The user list, assertions and " +
|
||||
"grants stay in shadow. It cannot ask the controller to write a module's grant again: the controller serves no such " +
|
||||
"verb, and the answer says so.",
|
||||
Run: func(a map[string]any) (any, error) { return p.AskCompose(stdio.CallerOf(a)) }},
|
||||
|
||||
@@ -1314,9 +1314,16 @@ func TestGrants(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("memberships: published unless a seat would lose its last holder", func(t *testing.T) {
|
||||
t.Run("memberships: published unless a seat would lose its last holder or a holder a capability it asks for", func(t *testing.T) {
|
||||
const key = "a.mod-x"
|
||||
brokerName, brokerVerb := brokerSeat()
|
||||
// The telegram module's claim of its channel, as mesh-catalog's telegram manifest has it (c05aa5e6), on mod-x.
|
||||
const telegramClaim = `[{"name":"channel","scope":"mesh","kind":"telegram","capabilities":["deliver","verified-sender"]}]`
|
||||
verified := func(m map[string]any) map[string]any {
|
||||
m["seat-traffic"] = map[string]any{"kinds": []any{map[string]any{"seat": "channel", "kind": "telegram", "module": "mod-x", "node": "a",
|
||||
"capabilities": []any{"deliver", "verified-sender"}}}}
|
||||
return m
|
||||
}
|
||||
composed := func(b *fakeBus) map[string]any {
|
||||
recs, _ := grants.Records(grants.ReadInputs(b, &grants.Manifests{}))
|
||||
raw, _ := json.Marshal(grants.Memberships(recs)[key].Membership)
|
||||
@@ -1329,38 +1336,46 @@ func TestGrants(t *testing.T) {
|
||||
last func(map[string]any) map[string]any
|
||||
published bool
|
||||
names string
|
||||
// claims, when set, are mod-x's claims in its manifest for the row.
|
||||
claims string
|
||||
}{
|
||||
{"a first membership is published", nil, true, ""},
|
||||
{"a first membership is published", nil, true, "", ""},
|
||||
{"an additive change is published, the private network carried", func(m map[string]any) map[string]any {
|
||||
delete(m, "state")
|
||||
m["mesh"] = []any{"10.0.0.1"}
|
||||
return m
|
||||
}, true, ""},
|
||||
// The lead's decision in review round 1 of the fix of issue 1945: only a seat left with no holder holds a
|
||||
// membership back; a dropped tool, field or capability is published.
|
||||
}, true, "", ""},
|
||||
// The lead's decision in review round 1 of the fix of issue 1945: only a seat left with no holder, or a
|
||||
// capability its holder still asks for (verified-sender, dropped by a root-free word that failed), holds a
|
||||
// membership back; a dropped tool or field is published.
|
||||
{"a change that drops a tool is published", func(m map[string]any) map[string]any {
|
||||
m["serves"] = append(m["serves"].([]any), map[string]any{"subject": "mesh.mod.mod-x.tool.gone"})
|
||||
m["mesh"] = []any{"10.0.0.1"}
|
||||
return m
|
||||
}, true, ""},
|
||||
}, true, "", ""},
|
||||
{"a change that drops verified-sender its holder still asks for is not published, and named", verified, false, "channel telegram verified-sender of a/mod-x", telegramClaim},
|
||||
{"a change that drops verified-sender its holder no longer asks for is published", verified, true, `"published":"published"`, `[{"name":"channel","scope":"mesh","kind":"telegram"}]`},
|
||||
{"a change that takes the last holder from a seat a placed module claims is not published, the seat named", func(m map[string]any) map[string]any {
|
||||
m["seats"] = []any{map[string]any{"seat": brokerName, "verb": brokerVerb, "subject": "mesh.seat." + brokerName + ".tool." + brokerVerb}}
|
||||
return m
|
||||
}, false, "the last holder of " + brokerName},
|
||||
}, false, "the last holder of " + brokerName, ""},
|
||||
// A verb the seat catalogue retired on purpose (mark-ready, mesh-sdk d506f21, ADR 0421) is dropped and named; a
|
||||
// verb of a seat the catalogue does not define is still a loss (issue 1945).
|
||||
{"a change that drops only a retired seat verb is published, the verb named", func(m map[string]any) map[string]any {
|
||||
m["seats"] = []any{map[string]any{"seat": "mesh-delivery", "verb": "mark-ready", "subject": "mesh.seat.mesh-delivery.tool.mark-ready"}}
|
||||
return m
|
||||
}, true, "mesh-delivery mark-ready"},
|
||||
}, true, "mesh-delivery mark-ready", ""},
|
||||
{"a seat no placed module claims dropped is published", func(m map[string]any) map[string]any {
|
||||
m["seats"] = []any{map[string]any{"seat": "mesh-delivery", "verb": "mark-ready", "subject": "mesh.seat.mesh-delivery.tool.mark-ready"},
|
||||
map[string]any{"seat": "telegram-sender", "verb": "send", "subject": "mesh.seat.telegram-sender.tool.send"}}
|
||||
return m
|
||||
}, true, "mesh-delivery mark-ready"},
|
||||
}, true, "mesh-delivery mark-ready", ""},
|
||||
} {
|
||||
t.Run(row.name, func(t *testing.T) {
|
||||
b := fixture()
|
||||
if row.claims != "" {
|
||||
b.manifestOf("mod-x", strings.Replace(xManifest, `"own-secrets"`, `"claims":`+row.claims+`,"own-secrets"`, 1))
|
||||
}
|
||||
b.loaded = shadowOf(t, b)
|
||||
if row.last != nil {
|
||||
raw, _ := json.Marshal(row.last(composed(b)))
|
||||
|
||||
@@ -104,7 +104,8 @@ func publish(p Publisher, m Membership, h Holdings, held map[string]bool) (Membe
|
||||
}
|
||||
// A membership is held back only when it would take the last holder from a seat some placed module still claims (the
|
||||
// 09:21 kind of issue 1945; the lead's decision in review round 1 of its fix, replacing the hold on any loss of review
|
||||
// 1ff5049): a dropped tool or verb, or a seat handed to another node, is published.
|
||||
// 1ff5049), or drop a capability a kind's holder still asks for (verified-sender, dropped by a root-free word that
|
||||
// failed): a dropped tool or verb, or a seat handed to another node, is published.
|
||||
if found {
|
||||
kept, retired := WithoutRetired(was)
|
||||
m.Retired = retired
|
||||
@@ -115,6 +116,10 @@ func publish(p Publisher, m Membership, h Holdings, held map[string]bool) (Membe
|
||||
m.Published = ShadowDifference + "the last holder of " + strings.Join(lost, ", ")
|
||||
return m, false
|
||||
}
|
||||
if lost := h.CapabilitiesLost(last, m.Membership); len(lost) > 0 {
|
||||
m.Published = CapabilityDifference + strings.Join(lost, "; ")
|
||||
return m, false
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := p.PublishMembership(m.Node, m.Module, body, seq); err != nil {
|
||||
@@ -200,12 +205,14 @@ const ShadowDifference = "not published, a shadow difference: publishing would l
|
||||
type Holdings struct {
|
||||
Claimed map[string]bool
|
||||
Scopes map[string]string
|
||||
// Asked is every capability an assigned module's claim of a kind asks for, `<node>/<module>/<seat>/<kind>/<capability>`.
|
||||
Asked map[string]bool
|
||||
}
|
||||
|
||||
// HoldingsOf is the holdings the assigned modules claim now, by their running manifest or the trunk's, each scope as the
|
||||
// seat's declaration says (the seat catalogue, or the manifest defining it), else the claim's. Pure.
|
||||
func HoldingsOf(in Inputs) Holdings {
|
||||
h := Holdings{Claimed: map[string]bool{}, Scopes: map[string]string{}}
|
||||
h := Holdings{Claimed: map[string]bool{}, Scopes: map[string]string{}, Asked: map[string]bool{}}
|
||||
for _, m := range in.Manifests {
|
||||
for _, d := range m.DefinesSeats {
|
||||
if d.Scope != "" {
|
||||
@@ -228,6 +235,9 @@ func HoldingsOf(in Inputs) Holdings {
|
||||
h.Scopes[seat] = c.At()
|
||||
}
|
||||
h.Claimed[h.key(seat, node)] = true
|
||||
for _, capability := range c.Capabilities {
|
||||
h.Asked[askedKey(node, module, seat, c.Kind, capability)] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,3 +311,40 @@ func (h Holdings) LastHolders(before, now map[string]bool) []string {
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func askedKey(node, module, seat, kind, capability string) string {
|
||||
return node + "/" + module + "/" + canonicalSeat(seat) + "/" + kind + "/" + capability
|
||||
}
|
||||
|
||||
// CapabilitiesLost is every capability a kind's holder carries in the membership in force (its seat traffic's kinds) that
|
||||
// the composed one lacks while that holder is still assigned and its manifest's claim still asks for it, `<seat> <kind>
|
||||
// <capability> of <node>/<module>`, sorted. Such a drop comes from a word that failed for one pass (the controller's
|
||||
// root-free verb drops verified-sender), not from a release, and losing verified-sender stops the operator's approvals by
|
||||
// a channel (issue 1945; review 1ff5049's protection, kept by the lead in review round 1 of its fix). Pure.
|
||||
func (h Holdings) CapabilitiesLost(last, now membership.Membership) []string {
|
||||
if last.SeatTraffic == nil {
|
||||
return nil
|
||||
}
|
||||
has := map[string]bool{}
|
||||
if now.SeatTraffic != nil {
|
||||
for _, k := range now.SeatTraffic.Kinds {
|
||||
for _, c := range k.Capabilities {
|
||||
has[askedKey(k.Node, k.Module, k.Seat, k.Kind, c)] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
var out []string
|
||||
for _, k := range last.SeatTraffic.Kinds {
|
||||
for _, c := range k.Capabilities {
|
||||
if key := askedKey(k.Node, k.Module, k.Seat, k.Kind, c); !has[key] && h.Asked[key] {
|
||||
out = append(out, k.Seat+" "+k.Kind+" "+c+" of "+k.Node+"/"+k.Module)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return slices.Compact(out)
|
||||
}
|
||||
|
||||
// CapabilityDifference opens what a membership's pass says when it was not published because it would drop a capability
|
||||
// its holder still asks for.
|
||||
const CapabilityDifference = ShadowDifference + "a capability its holder still asks for: "
|
||||
|
||||
@@ -110,7 +110,7 @@ type Result struct {
|
||||
}
|
||||
|
||||
// NothingTakesEffect is what every composition says it did on the bus beyond its own state and the memberships.
|
||||
const NothingTakesEffect = "memberships (on the assignments stream, unless one would leave a seat a placed module claims with no holder, which stays a shadow difference), and each bucket, log or stream a manifest declares that the bus lacks, created once with the manifest's limits (the operator, 2026-10-26 08:54); the user list, the grants and any change to a stream the bus holds stay in shadow until take-over"
|
||||
const NothingTakesEffect = "memberships (on the assignments stream, unless one would leave a seat a placed module claims with no holder, or drop a capability its holder still asks for, which stays a shadow difference), and each bucket, log or stream a manifest declares that the bus lacks, created once with the manifest's limits (the operator, 2026-10-26 08:54); the user list, the grants and any change to a stream the bus holds stay in shadow until take-over"
|
||||
|
||||
// Part is the grants part: it reads, calls and writes through these, and nothing else.
|
||||
type Part struct {
|
||||
|
||||
Reference in New Issue
Block a user