From 67e1c7594fc4d030b396d6ab22a9a08c839f64f7 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 9 Nov 2026 13:54:24 +0100 Subject: [PATCH] A tool reads the bindings the grants part keeps, beside the memberships it wrote (issue 1966) bound. 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 --- modules/mesh-grants/cmd/mesh-grants/tools.go | 5 ++ modules/mesh-grants/grants_test.go | 23 ++++++ .../mesh-grants/internal/grants/bindings.go | 71 +++++++++++++++++++ modules/mesh-grants/internal/grants/tools.go | 4 +- modules/mesh-grants/module.json | 3 +- 5 files changed, 104 insertions(+), 2 deletions(-) diff --git a/modules/mesh-grants/cmd/mesh-grants/tools.go b/modules/mesh-grants/cmd/mesh-grants/tools.go index fee5debd..882f2261 100644 --- a/modules/mesh-grants/cmd/mesh-grants/tools.go +++ b/modules/mesh-grants/cmd/mesh-grants/tools.go @@ -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")) }}, } } diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 78b9c19a..27076b65 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -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"} diff --git a/modules/mesh-grants/internal/grants/bindings.go b/modules/mesh-grants/internal/grants/bindings.go index f3cac9df..44f2108f 100644 --- a/modules/mesh-grants/internal/grants/bindings.go +++ b/modules/mesh-grants/internal/grants/bindings.go @@ -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 `.` 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) diff --git a/modules/mesh-grants/internal/grants/tools.go b/modules/mesh-grants/internal/grants/tools.go index 31f46c8d..b7bd1495 100644 --- a/modules/mesh-grants/internal/grants/tools.go +++ b/modules/mesh-grants/internal/grants/tools.go @@ -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). diff --git a/modules/mesh-grants/module.json b/modules/mesh-grants/module.json index c60bfcea..e68a1f00 100644 --- a/modules/mesh-grants/module.json +++ b/modules/mesh-grants/module.json @@ -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": [ {