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
@@ -0,0 +1,75 @@
package main
import (
"context"
"encoding/json"
"fmt"
"time"
"git.novox.be/novox/mesh-contracts/go/seats/meshbroker"
"github.com/nats-io/nats.go"
"mesh-grants/internal/buscompose"
"mesh-grants/internal/grants"
)
// readAgainAtMost bounds one re-read of the seat mesh-broker's events (grants_bindings_again): a stream holding more of
// them than this is read up to here, and the answer says the rest was not read.
const readAgainAtMost = 20000
// brokerHistory reads the seat mesh-broker's events again from the shared events stream, by sequence and subject
// (STREAM.MSG.GET with next_by_subj, the grant buscompose.SeatEventsReadAgain): a get per event, no consumer, oldest first.
type brokerHistory struct {
ctx context.Context
nc *nats.Conn
}
// msgGetAnswer is the bus's answer to a STREAM.MSG.GET.
type msgGetAnswer struct {
Message *struct {
Subject string `json:"subject"`
Seq uint64 `json:"seq"`
Data []byte `json:"data"`
Time time.Time `json:"time"`
} `json:"message"`
Error *struct {
Code int `json:"code"`
ErrCode int `json:"err_code"`
Description string `json:"description"`
} `json:"error"`
}
// noMessage is the bus's error code for a get past the last message of the subject.
const noMessage = 10037
func (h brokerHistory) Said() ([]grants.SaidEvent, error) {
subject := "mesh.seat." + meshbroker.Seat + ".event.*"
var out []grants.SaidEvent
for seq := uint64(1); ; {
if len(out) >= readAgainAtMost {
return out, fmt.Errorf("the stream holds more than %d of the seat's events: read up to sequence %d, the rest not", readAgainAtMost, seq)
}
body, _ := json.Marshal(map[string]any{"seq": seq, "next_by_subj": subject})
ctx, cancel := context.WithTimeout(h.ctx, requestWithin)
reply, err := h.nc.RequestWithContext(ctx, buscompose.SeatEventsReadAgain, body)
cancel()
if err != nil {
return out, fmt.Errorf("%s at sequence %d: %w", buscompose.SeatEventsReadAgain, seq, err)
}
var a msgGetAnswer
if err := json.Unmarshal(reply.Data, &a); err != nil {
return out, fmt.Errorf("%s answered what does not read: %w", buscompose.SeatEventsReadAgain, err)
}
if a.Error != nil {
if a.Error.ErrCode == noMessage {
return out, nil
}
return out, fmt.Errorf("%s at sequence %d: %s", buscompose.SeatEventsReadAgain, seq, a.Error.Description)
}
if a.Message == nil {
return out, nil
}
out = append(out, grants.SaidEvent{Subject: a.Message.Subject, Data: a.Message.Data, At: a.Message.Time})
seq = a.Message.Seq + 1
}
}
@@ -316,6 +316,24 @@ func TestTheGrantsPartWhole(t *testing.T) {
})
sayBroker("dropped", membership.BusBinding{Local: "kept"})
until("mod-c's binding gone after dropped", func() bool { return len(bindingOf()) == 0 })
// Issue 1966, grants_bindings_again: asked as the tool asks, the process reads the seat's events again from EVENTS by
// sequence (brokerHistory) and keeps mod-c's created and dropped in their order, so the dropped entry stays dropped.
ask, _ := json.Marshal(grants.Ask{By: "jochen@shanks", At: time.Now(), Bindings: "mod-c"})
if _, err := kvs[grants.Module+"."+grants.StateUserList].Put(ctx, grants.KeyAsked, ask); err != nil {
t.Fatal(err)
}
until("mod-c's bindings read again from EVENTS", func() bool {
e, err := kvs[grants.Module+"."+grants.StateUserList].Get(ctx, grants.KeyBindingsAgain)
if err != nil {
return false
}
var again grants.BindingsAgain
return json.Unmarshal(e.Value(), &again) == nil && again.Module == "mod-c" && again.Read == 2 && again.Error == "" &&
slices.Equal(again.Dropped, []string{"kept"}) && len(again.Kept) == 0
})
if b := bindingOf(); len(b) != 0 {
t.Errorf("a dropped binding came back from the re-read: %+v", b)
}
for _, bucket := range []string{"mesh-controller_lease", grants.Module + "_lease", grants.Module + "_takeover"} {
if _, err := js.KeyValue(ctx, bucket); !errors.Is(err, jetstream.ErrBucketNotFound) {
t.Errorf("the bucket %s: %v, want none", bucket, err)
@@ -135,6 +135,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
Objects: sealedObjects{ctx: ctx, js: js}, Purging: ownPurging{ctx: ctx, js: js, module: account.Module}, Logf: logger.Printf,
Bus: grants.BusAddress{URL: account.URL, Fingerprint: account.Fingerprint}}
f.Watch = fv
f.Part.History = brokerHistory{ctx: ctx, nc: nc}
// The seat list: read over the connection and the hub, given to catalogue at every change, and a pass composes again then.
gate := &seatGate{}
gate.follow(seatlist.ProcessBus(conn.Call, hub), f.Wake, logger.Printf)
+17 -4
View File
@@ -256,13 +256,26 @@ func toolsOf(p *grants.Part) []stdio.Tool {
"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")) }},
{Name: grants.ToolBindingsAgain, Description: "Read one module's bindings again from the seat mesh-broker's created, " +
"recreated and dropped that the shared events stream still holds, kept as if heard now (issue 1966), by a person's act " +
"with why, recorded as a hand act: the step when grants_bindings lacks an entry the bus module made. Asks the part's " +
"process; grants_bindings shows what it kept, what was already kept, and when the stream no longer holds the created.",
Input: schema(map[string]string{"module": "the module, e.g. distribution", "why": "why, kept in the hand act"}, "module", "why"),
Run: func(a map[string]any) (any, error) {
return p.AskBindingsAgain(stdio.CallerOf(a), grants.Answer(a, "module"), grants.Answer(a, "why"), recordHandActFor("bindings-again"))
}},
}
}
// recordHandAct records a person's act through the seat mesh-hand-acts.
func recordHandAct(what, why string) error {
_, err := meshhandactsclient.Recorder{Ask: stdio.Ask}.Record(meshhandactsclient.Request{What: what, Why: why, Cause: "first-view-again"})
return err
// recordHandAct records a person's act of grants_first_view_again through the seat mesh-hand-acts.
func recordHandAct(what, why string) error { return recordHandActFor("first-view-again")(what, why) }
// recordHandActFor records a person's act through the seat mesh-hand-acts, with its cause.
func recordHandActFor(cause string) func(what, why string) error {
return func(what, why string) error {
_, err := meshhandactsclient.Recorder{Ask: stdio.Ask}.Record(meshhandactsclient.Request{What: what, Why: why, Cause: cause})
return err
}
}
// serveTools serves the tools to the runtime until it stops.
+55
View File
@@ -3189,6 +3189,56 @@ func TestGrants(t *testing.T) {
if got := bindingsOn("a.mod-x"); len(got) != 0 {
t.Errorf("after dropped: %+v", got)
}
// Issue 1966, grants_bindings_again (the lead's ruling, option b): the process reads the seat's events the shared
// stream still holds again, oldest first, and keeps one module's created, recreated and dropped as if heard now: a
// binding never kept comes back, one dropped since stays dropped, another module's is left; the hand act is recorded
// before the ask, and an ask without why or a module is refused.
event := func(name, module, local, bucket string) grants.SaidEvent {
body, _ := json.Marshal(grants.BrokerEvent{V: 1, Module: module, LocalName: local, StorageType: "key-value", Template: "state",
Binding: membership.BusBinding{Local: local, Storage: "key-value", Template: "state", Bucket: bucket}})
return grants.SaidEvent{Subject: "mesh.seat.mesh-broker.event." + name, Data: body, At: time.Date(2026, 11, 3, 0, 0, 0, 0, time.UTC)}
}
p.Create = b
p.History = saidHistory{event("created", "mod-x", "windows", "mod-x_windows"), event("refused", "mod-x", "mirrors", ""),
event("created", "mod-y", "other", "mod-y_other"), event("created", "mod-x", "gone", "mod-x_gone"),
event("dropped", "mod-x", "gone", "")}
var acts []string
record := func(what, why string) error { acts = append(acts, what+": "+why); return nil }
for _, bad := range [][2]string{{"mod-x", ""}, {"", "x"}, {"a.mod-x", "x"}} {
if _, err := p.AskBindingsAgain("jochen@shanks", bad[0], bad[1], record); err == nil || len(acts) != 0 {
t.Errorf("grants_bindings_again %q %q was asked: %v %v", bad[0], bad[1], err, acts)
}
}
readAgain := func(module string) string {
if _, err := p.AskBindingsAgain("jochen@shanks", module, "distribution's lost bindings", record); err != nil {
t.Fatalf("grants_bindings_again %s: %v", module, err)
}
p.Compose()
got, err := p.BindingsOf(module)
if err != nil {
t.Fatal(err)
}
raw, _ := json.Marshal(got)
return string(raw)
}
if got := readAgain("mod-x"); !strings.Contains(got, `"kept":["windows"],"already":[],"dropped":["gone"]`) || !strings.Contains(got, `"read":3`) {
t.Errorf("the first re-read: %s", got)
}
if got := bindingsOn("a.mod-x"); len(got) != 1 || got[0].Bucket != "mod-x_windows" {
t.Errorf("after the re-read, mod-x's membership carries %+v", got)
}
if raw, _, found, _ := b.ownGet(grants.StateMemberships, grants.BoundKey("mod-y")); found {
t.Errorf("another module's binding was kept by mod-x's re-read: %s", raw)
}
if got := readAgain("mod-x"); !strings.Contains(got, `"kept":[],"already":["windows"]`) {
t.Errorf("the second re-read: %s", got)
}
if got := readAgain("mod-z"); !strings.Contains(got, "holds no created or recreated of mod-z") || !strings.Contains(got, "seat mesh-broker's list") {
t.Errorf("a module whose created the stream no longer holds: %s", got)
}
if len(acts) != 3 || !strings.HasPrefix(acts[0], "grants_bindings_again mod-x: ") {
t.Errorf("hand acts %v", acts)
}
})
// Issue 1966 (F2), the conformance row: an entry asked in `requires` is granted where its old section is. For a
@@ -4625,3 +4675,8 @@ func oldStateReaders(path string, src []byte) ([]string, error) {
}
return out, nil
}
// saidHistory is the seat mesh-broker's events a shared events stream holds, oldest first (grants.BrokerHistory).
type saidHistory []grants.SaidEvent
func (h saidHistory) Said() ([]grants.SaidEvent, error) { return h, nil }
@@ -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).
+2 -1
View File
@@ -62,7 +62,8 @@
"grants_first_view_again",
"grants_seat_list",
"grants_seat_list_again",
"grants_bindings"
"grants_bindings",
"grants_bindings_again"
],
"state": [
{