A tool reads the bindings the grants part keeps, beside the memberships it wrote (issue 1966)

bound.<module> had no reader: a binding lost or kept but not yet written into
a membership could only be guessed from grants_membership. grants_bindings
names each kept binding and, by membership, the ones it lacks.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-09 14:01:41 +01:00
parent 4d75646bf4
commit 67e1c7594f
5 changed files with 104 additions and 2 deletions
@@ -251,6 +251,11 @@ func toolsOf(p *grants.Part) []stdio.Tool {
Run: func(a map[string]any) (any, error) {
return p.AskFirstViewAgain(stdio.CallerOf(a), grants.Answer(a, "state"), grants.Answer(a, "why"), recordHandAct)
}},
{Name: grants.ToolBindings, Description: "The bindings the grants part keeps of a module's kept data on the bus (novox/hq " +
"ADR 0595 §1, §7): each entry the bus module said created or recreated, by local name with its names on the bus, beside " +
"every membership of the module the part wrote, and the kept bindings a membership lacks. One module, or every one. Read-only.",
Input: schema(map[string]string{"module": "one module, e.g. distribution; none for every one"}),
Run: func(a map[string]any) (any, error) { return p.BindingsOf(grants.Answer(a, "module")) }},
}
}
+23
View File
@@ -3117,6 +3117,29 @@ func TestGrants(t *testing.T) {
if !hear("mesh.seat.mesh-broker.event.created", first) || hear("mesh.seat.mesh-broker.event.created", first) {
t.Fatal("created: not kept once")
}
// Issue 1966: grants_bindings reads what the part keeps: a binding kept before the pass that writes it into the
// memberships is named lacking there, and no longer once the pass has written it; a module with none says so.
lacking := func() string {
got, err := p.BindingsOf("mod-x")
if err != nil {
t.Fatalf("grants_bindings: %v", err)
}
raw, _ := json.Marshal(got)
if !strings.Contains(string(raw), `"bucket":"mod-x_records"`) {
t.Errorf("grants_bindings names no kept binding: %s", raw)
}
return string(raw)
}
if got := lacking(); !strings.Contains(got, `"lacking":{"a.mod-x":["records"]}`) {
t.Errorf("a binding kept and not written is not named lacking: %s", got)
}
p.Compose()
if got := lacking(); strings.Contains(got, `"lacking"`) || !strings.Contains(got, `"a.mod-x":["records"]`) {
t.Errorf("a binding written is named lacking: %s", got)
}
if got, err := p.BindingsOf("mod-y"); err != nil || !strings.Contains(fmt.Sprint(got), "keeps no binding of mod-y") {
t.Errorf("grants_bindings of a module with none = %v, %v", got, err)
}
// Issue 1966 (F3): two created of one module heard at once are both kept: the second writer comes between the
// first's read and its write, and the first, refused at its revision, reads again and keeps both.
holds := membership.BusBinding{Local: "holds", Storage: "key-value", Template: "state", Bucket: "mod-x_holds"}
@@ -7,6 +7,7 @@ import (
"git.novox.be/novox/mesh-contracts/go/seats/meshbroker"
stdio "git.novox.be/novox/mesh-sdk/go"
"slices"
"sort"
"strings"
@@ -181,6 +182,76 @@ func (p *Part) bindingsOf(module string) ([]membership.BusBinding, error) {
return out, nil
}
// BindingsOf answers the tool grants_bindings: every binding the part keeps of one module, or of every module when module
// is empty (its own state memberships, key BoundKey), each beside the memberships `<node>.<module>` it wrote, and by
// membership the kept local names its bindings lack, so a binding kept but not yet written into a membership is named.
func (p *Part) BindingsOf(module string) (any, error) {
keys, err := p.Store.Keys(StateMemberships)
if err != nil {
return nil, fmt.Errorf("its own %s cannot be listed: %w", StateMemberships, err)
}
type kept struct {
Module string `json:"module"`
Bindings []membership.BusBinding `json:"bindings"`
Memberships map[string][]string `json:"memberships"`
Lacking map[string][]string `json:"lacking,omitempty"`
}
out := []kept{}
for _, key := range keys {
mod, ok := strings.CutPrefix(key, BoundPrefix)
if !ok || (module != "" && mod != module) {
continue
}
binds, err := p.bindingsOf(mod)
if err != nil {
return nil, err
}
k := kept{Module: mod, Bindings: binds, Memberships: map[string][]string{}}
if k.Bindings == nil {
k.Bindings = []membership.BusBinding{}
}
for _, mk := range keys {
if _, of, _ := strings.Cut(mk, "."); strings.HasPrefix(mk, BoundPrefix) || of != mod {
continue
}
raw, _, found, err := p.Store.Get(StateMemberships, mk)
if err != nil {
return nil, fmt.Errorf("its own membership %s cannot be read: %w", mk, err)
}
if !found {
continue
}
bare, err := membership.BareGrantsMembership(raw)
if err != nil {
return nil, fmt.Errorf("its own membership %s does not read: %w", mk, err)
}
var m membership.Membership
if err := json.Unmarshal(bare, &m); err != nil {
return nil, fmt.Errorf("its own membership %s does not read: %w", mk, err)
}
written := []string{}
for _, b := range m.Bindings {
written = append(written, b.Local)
}
k.Memberships[mk] = written
for _, b := range binds {
if !slices.Contains(written, b.Local) {
if k.Lacking == nil {
k.Lacking = map[string][]string{}
}
k.Lacking[mk] = append(k.Lacking[mk], b.Local)
}
}
}
out = append(out, k)
}
if module != "" && len(out) == 0 {
return map[string]any{"modules": out, "says": "the part keeps no binding of " + module + ": the bus module said no created for it that the part heard"}, nil
}
return map[string]any{"modules": out,
"says": "each binding the part kept from the bus module's created and recreated, beside the memberships it wrote; lacking names a kept binding a membership does not carry yet"}, nil
}
func sameBinding(a, b membership.BusBinding) bool {
ra, _ := json.Marshal(a)
rb, _ := json.Marshal(b)
+3 -1
View File
@@ -31,11 +31,13 @@ const (
// ToolSeatList and ToolSeatListAgain read and read again the seat list the part composes from (mesh-sdk seatlist.Tools).
ToolSeatList = "grants_seat_list"
ToolSeatListAgain = "grants_seat_list_again"
// ToolBindings reads the bindings the part keeps from the bus module's events (bindings.go, issue 1966).
ToolBindings = "grants_bindings"
)
// Tools is every tool, in the manifest's order.
var Tools = []string{ToolGrants, ToolMembership, ToolUserList, ToolComposeNow, ToolAssert, ToolAccounts, ToolConsumerMatch,
ToolAcceptHashes, ToolFirstViews, ToolFirstViewAgain, ToolSeatList, ToolSeatListAgain}
ToolAcceptHashes, ToolFirstViews, ToolFirstViewAgain, ToolSeatList, ToolSeatListAgain, ToolBindings}
// 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).
+2 -1
View File
@@ -61,7 +61,8 @@
"grants_first_views",
"grants_first_view_again",
"grants_seat_list",
"grants_seat_list_again"
"grants_seat_list_again",
"grants_bindings"
],
"state": [
{