A tool reads one module's bindings again from the seat events the shared stream still holds (issue 1966)

A binding the bus module said created that the part did not keep had no
recovery: distribution's holds, windows, mirrors and events are missing on
the bus node's memberships. grants_bindings_again, a hand act, asks the
process to read the seat mesh-broker's created, recreated and dropped of
one module again from EVENTS by sequence and keep each through HearBroker
in order, so a dropped entry stays dropped. grants_bindings shows what it
kept, and says when the stream no longer holds a created: then the seat's
list answering bindings is the path (the record extending ADR 0595 §7).
The part alone is granted the stream's message get, no consumer.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-09 14:11:26 +01:00
parent 44422f21db
commit aa89032f02
12 changed files with 371 additions and 19 deletions
@@ -600,13 +600,18 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) {
t.Errorf("a one-filter consumer's grants = %v: its update is the no-filter create form", one)
}
for _, u := range users {
if u.Module != GrantsModule || u.Kind != KindModule {
continue
}
perms, err := PermissionsFor(u)
if err != nil {
t.Fatal(err)
}
// Issue 1966: the grants part alone reads the shared events stream by sequence, to read the seat mesh-broker's
// events again (grants_bindings_again).
if may := MayPublish(perms, "$JS.API.STREAM.MSG.GET.EVENTS"); may != (u.Module == GrantsModule && u.Kind == KindModule) {
t.Errorf("%s %s may read EVENTS by sequence: %v", u.Kind, u.Module, may)
}
if u.Module != GrantsModule || u.Kind != KindModule {
continue
}
if !MayPublish(perms, "$JS.API.CONSUMER.CREATE.EVENTS.a_m") || MayPublish(perms, "$JS.API.CONSUMER.CREATE.EVENTS.a_quiet") ||
MayPublish(perms, "$JS.API.CONSUMER.DELETE.EVENTS.a_m") || MayPublish(perms, "$JS.API.CONSUMER.CREATE.EVENTS.a_m.mesh.mod.x.event.happened") {
t.Errorf("the grants part creates consumers as %v", u.CreatesConsumers)
@@ -859,6 +859,10 @@ func PermissionsFor(p Principal) (Permissions, error) {
pub = append(pub, CreateConsumerGrants(p.CreatesConsumers)...)
// And its own object bucket, where it writes the user list sealed to the bus node (ADR 0547 clause 7).
pub = append(pub, GrantsUserListWrites()...)
// And the read of one message of the shared events stream by its sequence and subject, where the part reads the
// seat mesh-broker's created, recreated and dropped again for grants_bindings_again (issue 1966): a get, never a
// consumer, so no other account's consumer is touched.
pub = append(pub, SeatEventsReadAgain)
}
// The view alone reads the build seats' workers for the backlog of asks (issue 1887 A8a).
if p.Module == ViewModule {
@@ -1546,6 +1550,10 @@ func CreateConsumerGrants(consumers []ModuleConsumer) []string {
return out
}
// SeatEventsReadAgain is the grants part's read of the shared events stream by sequence (STREAM.MSG.GET with
// next_by_subj): a binding the bus module said that the part did not keep is read again from it (issue 1966).
const SeatEventsReadAgain = "$JS.API.STREAM.MSG.GET." + EventsStream
// GrantsModule is the one module whose principal publishes the modules' memberships (novox/hq ADR 0547 §5): the grants
// part, which composes them from the manifests and assignments. It defines no stream and is granted no other subject here.
const GrantsModule = "mesh-grants"
File diff suppressed because one or more lines are too long
+168 -4
View File
@@ -10,6 +10,7 @@ import (
"slices"
"sort"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/membership"
"mesh-grants/internal/buscompose"
@@ -245,11 +246,174 @@ func (p *Part) BindingsOf(module string) (any, error) {
}
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
again, err := p.lastBindingsAgain(module)
if err != nil {
return nil, err
}
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
answer := 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"}
if module != "" && len(out) == 0 {
answer["says"] = "the part keeps no binding of " + module + ": the bus module said no created for it that the part heard"
}
if again != nil {
answer["read-again"] = again
}
return answer, nil
}
// **A binding the part did not keep is read again from the shared events stream** (issue 1966; the lead's ruling, option
// b): grants_bindings_again asks the part's process, by a person's act recorded as a hand act, to read the seat
// mesh-broker's events the stream still holds, oldest first, and to keep each of one module's created, recreated and
// dropped through HearBroker, as if heard now, so a binding dropped since is not brought back. The stream keeps its events
// for a bounded age: a created older than that is not there, and the answer says so, naming the path then (the seat's
// list answering each entry's binding, a record extending ADR 0595 §7).
// KeyBindingsAgain is, in StateUserList, the last re-read of one module's bindings: BindingsAgain.
const KeyBindingsAgain = "bindings-again"
// BrokerHistory reads the seat mesh-broker's events again from the shared events stream.
type BrokerHistory interface {
// Said is every event of the seat the stream still holds, oldest first.
Said() ([]SaidEvent, error)
}
// SaidEvent is one event of the seat mesh-broker as the shared events stream holds it.
type SaidEvent struct {
Subject string `json:"subject"`
Data []byte `json:"data"`
At time.Time `json:"at"`
}
// BindingsAgain is what the last re-read of one module's bindings found: the seat's events read, the local names whose
// binding it kept (Kept), those it found already kept as said (Already), those a dropped took away (Dropped), the oldest
// event the stream still holds, and in plain words what it means.
type BindingsAgain struct {
V int `json:"v"`
Module string `json:"module"`
By string `json:"by"`
At time.Time `json:"at"`
Read int `json:"read"`
Kept []string `json:"kept"`
Already []string `json:"already"`
Dropped []string `json:"dropped"`
Oldest time.Time `json:"oldest,omitzero"`
Says string `json:"says"`
Error string `json:"error,omitempty"`
}
// bindingsAgain reads module's bindings again from the seat's events the stream holds, keeps each, and writes what it
// found to its own state; an error is said in the pass's errors and in the kept result.
func (p *Part) bindingsAgain(res *Result, module, by string) {
out := BindingsAgain{V: 1, Module: module, By: by, At: p.now(), Kept: []string{}, Already: []string{}, Dropped: []string{}}
events, err := p.History.Said()
if err != nil {
out.Error = "the shared events stream could not be read: " + err.Error()
}
heardCreate := false
for _, e := range events {
if out.Oldest.IsZero() || (!e.At.IsZero() && e.At.Before(out.Oldest)) {
out.Oldest = e.At
}
name, ok := brokerEvent(e.Subject, p.Brokers())
if !ok {
continue
}
var said BrokerEvent
if json.Unmarshal(e.Data, &said) != nil || said.Module != module {
continue
}
out.Read++
local := firstOf(said.LocalName, said.Local, said.Binding.Local)
changed, err := p.HearBroker(e.Subject, e.Data, p.Brokers())
if err != nil {
out.Error = fmt.Sprintf("the %s of %s could not be kept: %v", name, local, err)
break
}
switch {
case name == BrokerDropped && changed:
out.Dropped = appendOnce(out.Dropped, local)
out.Kept = slices.DeleteFunc(out.Kept, func(l string) bool { return l == local })
case name != BrokerDropped:
heardCreate = true
out.Dropped = slices.DeleteFunc(out.Dropped, func(l string) bool { return l == local })
if changed {
out.Kept = appendOnce(out.Kept, local)
out.Already = slices.DeleteFunc(out.Already, func(l string) bool { return l == local })
} else if !slices.Contains(out.Kept, local) {
out.Already = appendOnce(out.Already, local)
}
}
}
switch {
case out.Error != "":
out.Says = "the re-read of " + module + " stopped: " + out.Error
res.Errors = append(res.Errors, out.Says)
case !heardCreate:
out.Says = fmt.Sprintf("the shared events stream holds no created or recreated of %s (its oldest event: %s): a created older "+
"than the stream keeps is not there, and its binding comes back only by the seat mesh-broker's list answering each "+
"entry's binding (a record extending ADR 0595 §7), not by this tool", module, out.Oldest.Format(time.RFC3339))
default:
out.Says = fmt.Sprintf("read %d of the seat mesh-broker's events of %s again: kept %v, already kept %v, dropped %v; the pass "+
"writes them into its memberships", out.Read, module, out.Kept, out.Already, out.Dropped)
}
if p.Logf != nil {
p.Logf("the bindings of %s read again by hand, for %s: %s", module, by, out.Says)
}
raw, _ := json.Marshal(out)
p.changed(res, StateUserList, KeyBindingsAgain, raw)
}
func appendOnce(list []string, s string) []string {
if slices.Contains(list, s) {
return list
}
return append(list, s)
}
// AskBindingsAgain answers the tool grants_bindings_again: it asks the part's process to read one module's bindings
// again from the seat mesh-broker's events the shared events stream holds, and records the hand act through record (the
// seat mesh-hand-acts) first; a module that is no module's name, or an ask without why, is refused before anything is
// asked. grants_bindings shows what the process found.
func (p *Part) AskBindingsAgain(caller, module, why string, record func(what, why string) error) (any, error) {
if module == "" || strings.ContainsAny(module, ".*> ") {
return nil, fmt.Errorf("grants_bindings_again needs one module, by its name: %q is none", module)
}
if strings.TrimSpace(why) == "" {
return nil, errors.New("grants_bindings_again needs why: it is recorded as a hand act")
}
if record != nil {
if err := record(ToolBindingsAgain+" "+module, why); err != nil {
return nil, fmt.Errorf("the hand act could not be recorded, so nothing was asked: %w", err)
}
}
ask, _ := json.Marshal(Ask{By: caller, At: p.now(), Bindings: module})
rev, err := p.Store.Put(StateUserList, KeyAsked, ask)
if err != nil {
return nil, fmt.Errorf("the ask could not be written: %w", err)
}
return map[string]any{"asked": rev, "module": module,
"says": "the part's process reads the seat mesh-broker's created, recreated and dropped of " + module + " again from the shared " +
"events stream at this ask, and keeps each as if heard now; grants_bindings shows what it kept, what was already kept, " +
"and whether the stream still holds its created"}, nil
}
// lastBindingsAgain is the last re-read kept, when it was of module (or any, for an empty module).
func (p *Part) lastBindingsAgain(module string) (*BindingsAgain, error) {
raw, _, found, err := p.Store.Get(StateUserList, KeyBindingsAgain)
if err != nil {
return nil, fmt.Errorf("its own %s %s cannot be read: %w", StateUserList, KeyBindingsAgain, err)
}
if !found {
return nil, nil
}
var b BindingsAgain
if err := json.Unmarshal(raw, &b); err != nil {
return nil, fmt.Errorf("its own %s %s does not read: %w", StateUserList, KeyBindingsAgain, err)
}
if module != "" && b.Module != module {
return nil, nil
}
return &b, nil
}
func sameBinding(a, b membership.BusBinding) bool {
+14 -4
View File
@@ -174,6 +174,9 @@ type Part struct {
// Say says a condition to the operator: the process's; nil in the tools.
Say func(oc.Condition) error
Now func() time.Time
// History reads the seat mesh-broker's events again from the shared events stream (bindings.go): the process's
// connection; nil in the tools, which ask the process.
History BrokerHistory
// SeatList is the seat list reader, which gates a module whose seats the list has not named (inputs.go, SeatGate); nil
// gates none.
SeatList SeatGate
@@ -258,6 +261,9 @@ type Ask struct {
Consumer string `json:"consumer,omitempty"`
// FirstView is, from grants_first_view_again, one first view to ask once more by hand (its state), or AllFirstViews.
FirstView string `json:"first-view,omitempty"`
// Bindings is, from grants_bindings_again, one module whose bindings the part reads again from the seat mesh-broker's
// events the shared events stream still holds (bindings.go, issue 1966).
Bindings string `json:"bindings,omitempty"`
}
// AllFirstViews asks every first view once more.
@@ -298,6 +304,14 @@ func (p *Part) Compose() Result {
p.Views.SayUnsaid()
in := p.inputs()
p.keepBrokers(in)
var ask Ask
if p.Create != nil || p.Consumers != nil {
ask = p.asked()
}
// A module's bindings read again by hand are kept before the pass reads the kept bindings, so it writes them now.
if ask.Bindings != "" && p.History != nil {
p.bindingsAgain(&res, ask.Bindings, ask.By)
}
if err := p.withBound(&in); err != nil {
res.Errors = append(res.Errors, err.Error())
}
@@ -493,10 +507,6 @@ func (p *Part) Compose() Result {
}
p.clearFull()
held, heldErr := HeldStreams(p.Call)
var ask Ask
if p.Create != nil || p.Consumers != nil {
ask = p.asked()
}
forced := ask.Assert
for _, module := range sortedKeys(in.Manifests) {
if !assigned(in, module) {
+3 -1
View File
@@ -33,11 +33,13 @@ const (
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"
// ToolBindingsAgain reads one module's bindings again from the seat mesh-broker's events the shared events stream holds.
ToolBindingsAgain = "grants_bindings_again"
)
// Tools is every tool, in the manifest's order.
var Tools = []string{ToolGrants, ToolMembership, ToolUserList, ToolComposeNow, ToolAssert, ToolAccounts, ToolConsumerMatch,
ToolAcceptHashes, ToolFirstViews, ToolFirstViewAgain, ToolSeatList, ToolSeatListAgain, ToolBindings}
ToolAcceptHashes, ToolFirstViews, ToolFirstViewAgain, ToolSeatList, ToolSeatListAgain, ToolBindings, ToolBindingsAgain}
// 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).