DLV-F4r, DLV-R1: the grants part reads placement, wants, manifests, modules and verdicts by their owners' first views, then their views families; an answer empty at revision 0 drops no login (ADR 0615 §4 to §6, ADR 0621, ADR 0622)
Each first view is asked at the start until answered, past two minutes raising first-view-unanswered naming the owner and the tool; an empty answer at revision 0 reads as a failed read, so the user list stays in force, and first-view-empty names the operator's step until an answer above 0. Each owner's views event applies one change with no ask; a gap asks once more. The reads of the five buckets leave the manifest, and the tools ask the first views too. Tools grants_first_views and grants_first_view_again (a hand act). The verdicts are followed on the indexer's views family (views.verdicts), not on manifest-accepted and manifest-refused as ADR 0622 §2 words it: those carry no revision. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/views"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/membership"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
@@ -56,7 +57,7 @@ func TestTheGrantsPartWhole(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
kvs := map[string]jetstream.KeyValue{}
|
||||
for _, state := range grants.Reads {
|
||||
for _, state := range append(append([]string(nil), grants.Reads...), grants.ViewReads...) {
|
||||
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: bucketOf(state), History: 5})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -148,6 +149,36 @@ func TestTheGrantsPartWhole(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = sub.Unsubscribe() })
|
||||
// DLV-F4r, DLV-F5r (ADR 0615 §4, ADR 0621 §3): the owners answer their first-view tools at once from what they hold,
|
||||
// with its revision, as the indexer's and the assigner's runtimes of their own do; the part never reads their buckets.
|
||||
var firstViewAsks atomic.Int32
|
||||
for _, owner := range []string{grants.IndexerModule, grants.AssignerModule} {
|
||||
owner := owner
|
||||
osub, err := nc.Subscribe("mesh.mod."+owner+".>", func(m *nats.Msg) {
|
||||
for _, fv := range grants.FirstViews {
|
||||
if fv.Owner != owner || !strings.Contains(m.Subject, "."+fv.State) {
|
||||
continue
|
||||
}
|
||||
firstViewAsks.Add(1)
|
||||
values, rev := map[string]json.RawMessage{}, uint64(0)
|
||||
if keys, err := kvs[fv.Read].ListKeys(ctx); err == nil {
|
||||
for k := range keys.Keys() {
|
||||
if e, err := kvs[fv.Read].Get(ctx, k); err == nil {
|
||||
values[k] = e.Value()
|
||||
rev = max(rev, e.Revision())
|
||||
}
|
||||
}
|
||||
}
|
||||
answer(m, views.Answer{V: views.Version, Owner: owner, State: fv.State, Revision: rev,
|
||||
Created: time.Date(2026, 11, 4, 0, 0, 0, 0, time.UTC), Values: values, At: time.Now()})
|
||||
return
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = osub.Unsubscribe() })
|
||||
}
|
||||
// A membership is never said on its subject any more (issue 1126).
|
||||
var said atomic.Int32
|
||||
if _, err := nc.Subscribe("mesh.assignment.>", func(*nats.Msg) { said.Add(1) }); err != nil {
|
||||
@@ -197,6 +228,9 @@ func TestTheGrantsPartWhole(t *testing.T) {
|
||||
if n := said.Load(); n != 0 {
|
||||
t.Errorf("%d memberships were said on their subject", n)
|
||||
}
|
||||
if firstViewAsks.Load() < int32(len(grants.FirstViews)) {
|
||||
t.Errorf("the part composed with %d first-view asks, want one of each of the %d", firstViewAsks.Load(), len(grants.FirstViews))
|
||||
}
|
||||
// DLV-G1, 1966/AC34 (novox/hq ADR 0595 §1, §7): the bus module's created, said on its own stream, reaches the part on
|
||||
// its durable consumer there, and the binding it carries is in the module's membership; dropped takes it out.
|
||||
until("the part's durable consumer on the bus module's stream", func() bool {
|
||||
|
||||
@@ -36,6 +36,8 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/announce"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/views"
|
||||
|
||||
"mesh-grants/internal/grants"
|
||||
)
|
||||
|
||||
@@ -104,12 +106,25 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
return err
|
||||
}
|
||||
bus := newBusReader(ctx, js)
|
||||
f.Part = &grants.Part{Read: bus, Call: conn, Store: ownStore{conn}, Emit: emitting{conn},
|
||||
// The owners' five states by their first views (ADR 0615, ADR 0622): asked through the part's own account, then
|
||||
// followed on the owners' views families, which the hearing below hands them.
|
||||
sayCond := sayVia(ctx, js, account.Module)
|
||||
fv := grants.NewViews(func(tool string) func(context.Context, string) (views.Answer, error) {
|
||||
return views.AskVia(grants.Unwrapped(conn.Call), tool)
|
||||
}, sayCond, f.Wake, logger.Printf)
|
||||
fv.Rest, fv.Watching = bus, bus
|
||||
byState := map[string]*views.Follower{}
|
||||
for _, v := range grants.FirstViews {
|
||||
byState[v.State] = fv.Followers[v.Read]
|
||||
}
|
||||
hearViews := views.Hear(ctx, byState, logger.Printf)
|
||||
f.Part = &grants.Part{Read: fv, Call: conn, Store: ownStore{conn}, Emit: emitting{conn}, Views: fv, FirstViewContext: ctx,
|
||||
Create: creating{ctx: ctx, js: js}, Consumers: creating{ctx: ctx, js: js},
|
||||
Objects: sealedObjects{ctx: ctx, js: js}, Logf: logger.Printf,
|
||||
Bus: grants.BusAddress{URL: account.URL, Fingerprint: account.Fingerprint}}
|
||||
f.Watch = bus
|
||||
f.Part.Say = sayVia(ctx, js, account.Module)
|
||||
f.Watch = fv
|
||||
f.Part.Say = sayCond
|
||||
fv.Start(ctx, f.Wake)
|
||||
f.Asked = func(wake func()) error {
|
||||
return conn.State(grants.StateUserList).Watch(grants.KeyAsked, func(c stdio.StateChange) error {
|
||||
if !c.Current {
|
||||
@@ -136,6 +151,11 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
return fmt.Errorf("the consumer %s on %s: %w", name, stream, err)
|
||||
}
|
||||
if _, err := cons.Consume(func(m jetstream.Msg) {
|
||||
// An owner's views event goes to the follower of its state, which acknowledges it (firstviews.go).
|
||||
if strings.Contains(m.Subject(), ".event."+views.EventPrefix) {
|
||||
hearViews(m)
|
||||
return
|
||||
}
|
||||
handle(m.Subject(), m.Data())
|
||||
_ = m.Ack()
|
||||
}, jetstream.ConsumeErrHandler(func(_ jetstream.ConsumeContext, err error) { logger.Printf("hearing the bus: %v", err) })); err != nil {
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/views"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/handacts"
|
||||
|
||||
"mesh-grants/internal/grants"
|
||||
)
|
||||
@@ -13,12 +21,34 @@ import (
|
||||
// The part's tools, served through the tool runner from its bundle (ADR 0436: a process never serves). They read and
|
||||
// write through the runtime as the module: its own states, the reads its manifest names, the bus module's read tools.
|
||||
|
||||
// runtimeReader reads another part's state through the runtime, `<module>.<state>`.
|
||||
type runtimeReader struct{}
|
||||
// runtimeReader reads another part's state through the runtime, `<module>.<state>`; the owners' five states the part
|
||||
// reads by their first views (grants.ViewReads) it reads by asking each owner's first-view tool, once per tool call
|
||||
// (fresh), never their buckets (ADR 0615 §1).
|
||||
type runtimeReader struct{ views *toolViews }
|
||||
|
||||
func (runtimeReader) Keys(state string) ([]string, error) { return stdio.State(state).Keys() }
|
||||
func (r runtimeReader) Keys(state string) ([]string, error) {
|
||||
if values, mine, err := r.views.of(state); mine {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]string, 0, len(values))
|
||||
for k := range values {
|
||||
out = append(out, k)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out, nil
|
||||
}
|
||||
return stdio.State(state).Keys()
|
||||
}
|
||||
|
||||
func (runtimeReader) Get(state, key string) ([]byte, bool, error) {
|
||||
func (r runtimeReader) Get(state, key string) ([]byte, bool, error) {
|
||||
if values, mine, err := r.views.of(state); mine {
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
v, ok := values[key]
|
||||
return v, ok, nil
|
||||
}
|
||||
e, err := stdio.State(state).Get(key)
|
||||
if err != nil || e == nil {
|
||||
return nil, false, err
|
||||
@@ -26,6 +56,63 @@ func (runtimeReader) Get(state, key string) ([]byte, bool, error) {
|
||||
return e.Value, true, nil
|
||||
}
|
||||
|
||||
// toolViews are the five first views as one tool call reads them: each asked whole at its first read in the call, kept
|
||||
// for the rest of it, and forgotten by fresh at the next call.
|
||||
type toolViews struct {
|
||||
mu sync.Mutex
|
||||
held map[string]map[string]json.RawMessage
|
||||
}
|
||||
|
||||
func (v *toolViews) fresh() {
|
||||
v.mu.Lock()
|
||||
v.held = nil
|
||||
v.mu.Unlock()
|
||||
}
|
||||
|
||||
// of is the whole picture of a first view's state, whether state is one, and why it could not be had.
|
||||
func (v *toolViews) of(state string) (map[string]json.RawMessage, bool, error) {
|
||||
var fv *grants.FirstView
|
||||
for i := range grants.FirstViews {
|
||||
if grants.FirstViews[i].Read == state {
|
||||
fv = &grants.FirstViews[i]
|
||||
}
|
||||
}
|
||||
if fv == nil {
|
||||
return nil, false, nil
|
||||
}
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
if held, ok := v.held[state]; ok {
|
||||
return held, true, nil
|
||||
}
|
||||
ask := views.AskVia(grants.Unwrapped(stdio.Ask), fv.Tool)
|
||||
values := map[string]json.RawMessage{}
|
||||
first := true
|
||||
var rev uint64
|
||||
for after := ""; ; {
|
||||
a, err := ask(context.Background(), after)
|
||||
if err != nil {
|
||||
return nil, true, fmt.Errorf("%s does not answer: %w", fv.Tool, err)
|
||||
}
|
||||
if first {
|
||||
rev, first = a.Revision, false
|
||||
}
|
||||
maps.Copy(values, a.Values)
|
||||
if a.Next == "" || a.Next == after {
|
||||
break
|
||||
}
|
||||
after = a.Next
|
||||
}
|
||||
if rev == 0 {
|
||||
return nil, true, fmt.Errorf("%s by %s: %w", state, fv.Tool, grants.ErrFirstViewNotYet)
|
||||
}
|
||||
if v.held == nil {
|
||||
v.held = map[string]map[string]json.RawMessage{}
|
||||
}
|
||||
v.held[state] = values
|
||||
return values, true, nil
|
||||
}
|
||||
|
||||
// runtimeStore is the part's own state through the runtime.
|
||||
type runtimeStore struct{}
|
||||
|
||||
@@ -142,11 +229,36 @@ func toolsOf(p *grants.Part) []stdio.Tool {
|
||||
}
|
||||
return p.AcceptHashes(stdio.CallerOf(a), users, grants.Answer(a, "why"))
|
||||
}},
|
||||
{Name: grants.ToolFirstViews, Description: "The grants part's first views of the owners' states (novox/hq ADR 0615 §4 to §6, " +
|
||||
"ADR 0622): the assigner's placement and wants, the indexer's manifests, modules and verdicts, each with its owner, tool " +
|
||||
"and the revision its picture holds, when it asked and when it was answered, how many asks and hand acts, and whether " +
|
||||
"its condition is raised, as the part's process last kept them. Read-only.",
|
||||
Run: func(map[string]any) (any, error) { return p.FirstViewsOf() }},
|
||||
{Name: grants.ToolFirstViewAgain, Description: "Ask one of the grants part's first views once more now (placement, wants, " +
|
||||
"manifests, modules or verdicts; none for all), by a person's act with why, recorded as a hand act: the step after an " +
|
||||
"owner's state was written again from its sources (an empty answer at revision 0, condition " +
|
||||
"mesh-grants.first-view-empty). Asks the part's process, which alone follows them; grants_first_views shows the answer.",
|
||||
Input: schema(map[string]string{"state": "placement, wants, manifests, modules or verdicts; none for all", "why": "why, kept in the hand act"}, "why"),
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
return p.AskFirstViewAgain(stdio.CallerOf(a), grants.Answer(a, "state"), grants.Answer(a, "why"), recordHandAct)
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// recordHandAct records a person's act through the seat mesh-hand-acts.
|
||||
func recordHandAct(what, why string) error {
|
||||
_, err := handacts.Recorder{Ask: stdio.Ask}.Record(handacts.Request{What: what, Why: why, Cause: "first-view-again"})
|
||||
return err
|
||||
}
|
||||
|
||||
// serveTools serves the tools to the runtime until it stops.
|
||||
func serveTools() error {
|
||||
p := &grants.Part{Read: runtimeReader{}, Call: runtimeBus{}, Store: runtimeStore{}, Emit: runtimeBus{}}
|
||||
return stdio.Serve("", toolsOf(p))
|
||||
tv := &toolViews{}
|
||||
p := &grants.Part{Read: runtimeReader{views: tv}, Call: runtimeBus{}, Store: runtimeStore{}, Emit: runtimeBus{}}
|
||||
tools := toolsOf(p)
|
||||
for i := range tools {
|
||||
run := tools[i].Run
|
||||
tools[i].Run = func(a map[string]any) (any, error) { tv.fresh(); return run(a) }
|
||||
}
|
||||
return stdio.Serve("", tools)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
@@ -20,3 +21,5 @@ require (
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
)
|
||||
|
||||
replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk
|
||||
|
||||
@@ -15,11 +15,13 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/views"
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
@@ -2302,6 +2304,122 @@ func TestGrants(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
// DLV-F4r, DLV-R1 (novox/hq ADR 0615 §4 to §6, ADR 0622; design 25 § 12.2): the five owners' states are read by their
|
||||
// first views; an answer empty at revision 0 drops no login and raises first-view-empty with the operator's step until an
|
||||
// answer above 0; an owner down past the bound raises first-view-unanswered, cleared by its answer; an event applies a
|
||||
// change with no ask; grants_first_view_again refuses a state that is none of the five and records the hand act.
|
||||
t.Run("first views: the owners' states by their first views, an empty answer at revision 0 dropping no login", func(t *testing.T) {
|
||||
b := fixture()
|
||||
b.loaded = shadowOf(t, b)
|
||||
created := time.Date(2026, 11, 4, 9, 0, 0, 0, time.UTC)
|
||||
var down, empty atomic.Bool
|
||||
empty.Store(true)
|
||||
owner := func(tool string) func(context.Context, string) (views.Answer, error) {
|
||||
return func(context.Context, string) (views.Answer, error) {
|
||||
if down.Load() && strings.HasSuffix(tool, ".verdicts") {
|
||||
return views.Answer{}, errors.New("no responders")
|
||||
}
|
||||
for _, fv := range grants.FirstViews {
|
||||
if fv.Tool != tool {
|
||||
continue
|
||||
}
|
||||
if empty.Load() && fv.State == "placement" {
|
||||
return views.Answer{Revision: 0, Created: created, Values: map[string]json.RawMessage{}}, nil
|
||||
}
|
||||
values := map[string]json.RawMessage{}
|
||||
b.mu.Lock()
|
||||
for k, v := range b.states[fv.Read] {
|
||||
values[k] = v
|
||||
}
|
||||
b.mu.Unlock()
|
||||
return views.Answer{Revision: 7, Created: created, Values: values}, nil
|
||||
}
|
||||
return views.Answer{}, errors.New("no such tool " + tool)
|
||||
}
|
||||
}
|
||||
var said []asks.Condition
|
||||
var sayMu sync.Mutex
|
||||
say := func(c asks.Condition) error { sayMu.Lock(); said = append(said, c); sayMu.Unlock(); return nil }
|
||||
fv := grants.NewViews(owner, say, func() {}, nil)
|
||||
fv.Rest = b
|
||||
var minutes atomic.Int64
|
||||
for _, f := range fv.Followers {
|
||||
f.Wait = time.Nanosecond
|
||||
f.Now = func() time.Time { return created.Add(time.Duration(minutes.Add(1)) * time.Minute) }
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
down.Store(true)
|
||||
fv.Start(ctx, func() {})
|
||||
for _, f := range fv.Followers {
|
||||
if f.Tool != grants.IndexerModule+".verdicts" {
|
||||
<-f.Ready()
|
||||
}
|
||||
}
|
||||
raised := func(key string, state string) bool {
|
||||
sayMu.Lock()
|
||||
defer sayMu.Unlock()
|
||||
last := ""
|
||||
for _, c := range said {
|
||||
if c.Key == key {
|
||||
last = c.State
|
||||
}
|
||||
}
|
||||
return last == state
|
||||
}
|
||||
waitFor(t, func() bool {
|
||||
return raised(grants.FirstViewUnanswered+".mesh-delivery-indexer.verdicts", asks.StateRaised)
|
||||
})
|
||||
p := partOn(b)
|
||||
p.Read, p.Views, p.Say = fv, fv, say
|
||||
before := p.Compose()
|
||||
if !slices.ContainsFunc(before.UserList.Gaps, func(g grants.Gap) bool { return g.Input == grants.ReadPlacement && g.Failed }) {
|
||||
t.Fatalf("placement answered empty at revision 0 was not a failed read: %+v", before.UserList.Gaps)
|
||||
}
|
||||
if !raised(grants.FirstViewEmpty+".mesh-delivery-assigner.placement", asks.StateRaised) {
|
||||
t.Fatalf("no first-view-empty: %+v", said)
|
||||
}
|
||||
empty.Store(false)
|
||||
down.Store(false)
|
||||
if _, err := fv.Again(ctx, "placement", "the assigner composed its wants again"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
waitFor(t, func() bool { return fv.Followers[grants.ReadPlacement].Status().Revision == 7 })
|
||||
waitFor(t, func() bool {
|
||||
return raised(grants.FirstViewUnanswered+".mesh-delivery-indexer.verdicts", asks.StateCleared)
|
||||
})
|
||||
after := p.Compose()
|
||||
if slices.ContainsFunc(after.UserList.Gaps, func(g grants.Gap) bool { return g.Input == grants.ReadPlacement }) ||
|
||||
!raised(grants.FirstViewEmpty+".mesh-delivery-assigner.placement", asks.StateCleared) {
|
||||
t.Fatalf("after an answer above 0: gaps %+v, said %+v", after.UserList.Gaps, said)
|
||||
}
|
||||
if after.UserList.Users != before.UserList.Users && before.UserList.Users > after.UserList.Users {
|
||||
t.Errorf("the empty first view dropped users: %d then %d", before.UserList.Users, after.UserList.Users)
|
||||
}
|
||||
// An event of the owner's views family changes the picture with no ask.
|
||||
asksBefore := fv.Followers[grants.ReadPlacement].Status().Asks
|
||||
raw, _ := json.Marshal(map[string]any{"v": 1, "node": "c"})
|
||||
if gap, why := fv.Followers[grants.ReadPlacement].Apply(views.Event{V: 1, Owner: grants.AssignerModule, State: "placement",
|
||||
Key: "placement.c", Revision: 8, Created: created, Value: raw}); gap {
|
||||
t.Fatalf("a next revision was a gap: %s", why)
|
||||
}
|
||||
if _, found, _ := fv.Get(grants.ReadPlacement, "placement.c"); !found || fv.Followers[grants.ReadPlacement].Status().Asks != asksBefore {
|
||||
t.Error("the event did not reach the picture, or asked")
|
||||
}
|
||||
// The tools: a state that is none of the five is refused; one is asked with its hand act.
|
||||
var acts []string
|
||||
record := func(what, why string) error { acts = append(acts, what+": "+why); return nil }
|
||||
if _, err := p.AskFirstViewAgain("jochen@shanks", "builds", "x", record); err == nil || len(acts) != 0 {
|
||||
t.Errorf("a state that is no first view was asked: %v %v", err, acts)
|
||||
}
|
||||
if _, err := p.AskFirstViewAgain("jochen@shanks", "verdicts", "after the take-in", record); err != nil || len(acts) != 1 {
|
||||
t.Errorf("verdicts again: %v %v", err, acts)
|
||||
}
|
||||
if got, err := p.FirstViewsOf(); err != nil || !strings.Contains(fmt.Sprint(got), "mesh-delivery-indexer.verdicts") {
|
||||
t.Errorf("grants_first_views = %v, %v", got, err)
|
||||
}
|
||||
})
|
||||
|
||||
// 774/AC4 (novox/hq ADR 0561 §2): every account the vault mints that the records compose is written to the state
|
||||
// accounts once, with the bus's address and a module's claims, read by the vault alone; a key is deleted when placement
|
||||
// no longer names its user, and never in a pass whose reads failed.
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
package grants
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/views"
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
)
|
||||
|
||||
// **The grants part's first views** (novox/hq ADR 0615 §1, §4 to §6, ADR 0621, ADR 0622; design 25 § 12.2; DLV-F4r,
|
||||
// DLV-R1). The part reads the assigner's placement and wants and the indexer's manifests, modules and verdicts no longer
|
||||
// from their buckets, but by each owner's first-view tool, asked once at its start, then from the owner's views family
|
||||
// alone (each event one changed key with its revision), asking once more only on a gap in revisions, never on a timer.
|
||||
// Started while an owner is down it keeps asking and never reads the owner's state; past the bound (two minutes) it
|
||||
// raises FirstViewUnanswered naming the owner and the tool, cleared by the answer.
|
||||
//
|
||||
// **An answer empty at revision 0 drops no login**: until an owner answers above revision 0 the state reads as a failed
|
||||
// read, so the part keeps the user list it has in force, and FirstViewEmpty stands, naming the owner and the operator's
|
||||
// step (the owner writes its state again from its sources, then grants_first_view_again); an answer above 0 clears it.
|
||||
|
||||
// FirstView is one owner's state the part reads by its first view: the owner, the tool its `invokes` names, the state,
|
||||
// and the read it stands for in the part's inputs (`<owner>.<state>`).
|
||||
type FirstView struct {
|
||||
Owner string `json:"owner"`
|
||||
Tool string `json:"tool"`
|
||||
State string `json:"state"`
|
||||
Read string `json:"read"`
|
||||
}
|
||||
|
||||
// The owners.
|
||||
const (
|
||||
AssignerModule = "mesh-delivery-assigner"
|
||||
IndexerModule = "mesh-delivery-indexer"
|
||||
)
|
||||
|
||||
// FirstViews are the five (ADR 0615 §3 as ADR 0622 §1 amends it: the grants part's placement, wants, manifests,
|
||||
// modules and verdicts).
|
||||
var FirstViews = []FirstView{
|
||||
{Owner: AssignerModule, Tool: AssignerModule + ".placement", State: "placement", Read: ReadPlacement},
|
||||
{Owner: AssignerModule, Tool: AssignerModule + ".wants", State: "wants", Read: ReadWants},
|
||||
{Owner: IndexerModule, Tool: IndexerModule + ".manifests", State: "manifests", Read: ReadManifests},
|
||||
{Owner: IndexerModule, Tool: IndexerModule + ".modules", State: "modules", Read: ReadModules},
|
||||
{Owner: IndexerModule, Tool: IndexerModule + ".verdicts", State: "verdicts", Read: ReadVerdicts},
|
||||
}
|
||||
|
||||
// The conditions of the first views, each naming the owner and its state.
|
||||
const (
|
||||
FirstViewUnanswered = "mesh-grants.first-view-unanswered"
|
||||
FirstViewEmpty = "mesh-grants.first-view-empty"
|
||||
)
|
||||
|
||||
// KeyFirstViews is the key of the part's own state user-list where its process keeps what each first view holds, which
|
||||
// grants_first_views reads.
|
||||
const KeyFirstViews = "first-views"
|
||||
|
||||
// ErrFirstViewNotYet is a read of a first view not answered above revision 0: the part composes as from a failed read.
|
||||
var ErrFirstViewNotYet = errors.New("not answered above revision 0 yet")
|
||||
|
||||
// Views is the part's reads of the five owners' states through their first views: a Reader and a Watcher for those
|
||||
// states, every other read going to Rest.
|
||||
type Views struct {
|
||||
// Followers are the five, by the read they stand for.
|
||||
Followers map[string]*views.Follower
|
||||
// Rest reads every other state, and Watching watches it (nil: watched by nobody, as in a tool).
|
||||
Rest Reader
|
||||
Watching Watcher
|
||||
}
|
||||
|
||||
// NewViews makes the five followers: ask calls an owner's tool (through the part's own account), say says a condition,
|
||||
// changed wakes a pass. Started by Start.
|
||||
func NewViews(ask func(tool string) func(context.Context, string) (views.Answer, error), say func(asks.Condition) error,
|
||||
changed func(), logf func(string, ...any)) *Views {
|
||||
v := &Views{Followers: map[string]*views.Follower{}}
|
||||
for _, fv := range FirstViews {
|
||||
fv := fv
|
||||
f := &views.Follower{Owner: fv.Owner, Tool: fv.Tool, State: fv.State, Ask: ask(fv.Tool), Logf: logf,
|
||||
Changed: func(string, json.RawMessage, bool, time.Time) { changed() }}
|
||||
f.Raise = func(s views.Status) {
|
||||
if say != nil {
|
||||
_ = say(unansweredCondition(fv, s, true))
|
||||
}
|
||||
changed()
|
||||
}
|
||||
f.Clear = func(s views.Status) {
|
||||
if say != nil {
|
||||
_ = say(unansweredCondition(fv, s, false))
|
||||
}
|
||||
changed()
|
||||
}
|
||||
v.Followers[fv.Read] = f
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Start asks each first view on a goroutine of its own, until it is answered or ctx ends; each answer wakes a pass.
|
||||
func (v *Views) Start(ctx context.Context, changed func()) {
|
||||
for _, f := range v.Followers {
|
||||
f := f
|
||||
go func() {
|
||||
if f.Start(ctx) == nil {
|
||||
changed()
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// Following says the follower of the owner's views event state, or nil.
|
||||
func (v *Views) Following(owner, state string) *views.Follower {
|
||||
for _, fv := range FirstViews {
|
||||
if fv.Owner == owner && fv.State == state {
|
||||
return v.Followers[fv.Read]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ready is the follower of read, answered above revision 0; nil and why when it is not one of the five, or not yet.
|
||||
func (v *Views) ready(read string) (*views.Follower, bool, error) {
|
||||
f, ok := v.Followers[read]
|
||||
if !ok {
|
||||
return nil, false, nil
|
||||
}
|
||||
select {
|
||||
case <-f.Ready():
|
||||
default:
|
||||
return f, true, fmt.Errorf("%s by %s: %w", read, f.Tool, ErrFirstViewNotYet)
|
||||
}
|
||||
if s := f.Status(); s.Revision == 0 {
|
||||
return f, true, fmt.Errorf("%s by %s answered empty at revision 0: %w", read, f.Tool, ErrFirstViewNotYet)
|
||||
}
|
||||
return f, true, nil
|
||||
}
|
||||
|
||||
// Get reads one key: from its first view for the five, else from Rest.
|
||||
func (v *Views) Get(state, key string) ([]byte, bool, error) {
|
||||
f, mine, err := v.ready(state)
|
||||
if !mine {
|
||||
return v.Rest.Get(state, key)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
raw, ok := f.Get(key)
|
||||
return raw, ok, nil
|
||||
}
|
||||
|
||||
// Keys lists a state's keys: from its first view for the five, else from Rest.
|
||||
func (v *Views) Keys(state string) ([]string, error) {
|
||||
f, mine, err := v.ready(state)
|
||||
if !mine {
|
||||
return v.Rest.Keys(state)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values := f.Values()
|
||||
out := make([]string, 0, len(values))
|
||||
for k := range values {
|
||||
out = append(out, k)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Watch watches Rest's states; the five are followed by their first views and events, never watched.
|
||||
func (v *Views) Watch(state string, changed func(key string, value []byte), ended func(error)) error {
|
||||
if _, mine := v.Followers[state]; mine || v.Watching == nil {
|
||||
return nil
|
||||
}
|
||||
return v.Watching.Watch(state, changed, ended)
|
||||
}
|
||||
|
||||
// Statuses are what each first view holds now, in FirstViews' order.
|
||||
func (v *Views) Statuses() []views.Status {
|
||||
out := make([]views.Status, 0, len(FirstViews))
|
||||
for _, fv := range FirstViews {
|
||||
out = append(out, v.Followers[fv.Read].Status())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Again asks the first view of one state (its state name, `placement`, or its tool) once more, or every one when state is
|
||||
// empty, each on a goroutine of its own; it answers which it asked, or refuses a state that is none of the five.
|
||||
func (v *Views) Again(ctx context.Context, state, why string) ([]string, error) {
|
||||
var asked []string
|
||||
for _, fv := range FirstViews {
|
||||
if state != "" && state != fv.State && state != fv.Tool && state != fv.Read {
|
||||
continue
|
||||
}
|
||||
f := v.Followers[fv.Read]
|
||||
go func() { _ = f.Again(ctx, why) }()
|
||||
asked = append(asked, fv.Tool)
|
||||
}
|
||||
if len(asked) == 0 {
|
||||
return nil, fmt.Errorf("%q is no first view of the grants part: one of placement, wants, manifests, modules, verdicts, or none for all", state)
|
||||
}
|
||||
return asked, nil
|
||||
}
|
||||
|
||||
// unansweredCondition is FirstViewUnanswered for one first view, raised or cleared.
|
||||
func unansweredCondition(fv FirstView, s views.Status, raised bool) asks.Condition {
|
||||
c := asks.Condition{Key: FirstViewUnanswered + "." + fv.Owner + "." + fv.State, Subject: asks.Scope{Scope: "module", ID: Module}}
|
||||
if !raised {
|
||||
c.State = asks.StateCleared
|
||||
c.Resolved = fv.Tool + " answered at revision " + fmt.Sprint(s.Revision)
|
||||
return c
|
||||
}
|
||||
c.State, c.Severity = asks.StateRaised, asks.SeverityWarning
|
||||
c.Summary = fmt.Sprintf("the grants part has no first view of %s's %s: %s has not answered since %s (%s)", fv.Owner, fv.State,
|
||||
fv.Tool, s.Asked.Format(time.RFC3339), strings.TrimSpace(s.Error))
|
||||
c.Headline = "The grants part waits for " + fv.Owner
|
||||
c.Explanation = "The grants part reads " + fv.Owner + "'s " + fv.State + " only through " + fv.Tool + ", and keeps asking it; " +
|
||||
"it never reads the state itself, and keeps what it composed meanwhile (novox/hq ADR 0615 §6). The answer clears this."
|
||||
c.Needs = "a look at " + fv.Owner + "'s runtime of its own: is it running and connected?"
|
||||
return c
|
||||
}
|
||||
|
||||
// emptyCondition is FirstViewEmpty for one first view: raised while it answered empty at revision 0, with the
|
||||
// operator's step, cleared by an answer above 0.
|
||||
func emptyCondition(fv FirstView, raised bool) asks.Condition {
|
||||
c := asks.Condition{Key: FirstViewEmpty + "." + fv.Owner + "." + fv.State, Subject: asks.Scope{Scope: "module", ID: Module}}
|
||||
if !raised {
|
||||
c.State = asks.StateCleared
|
||||
c.Resolved = fv.Tool + " answered above revision 0"
|
||||
return c
|
||||
}
|
||||
step := "the indexer takes each module in again (indexer_take_in)"
|
||||
if fv.Owner == AssignerModule {
|
||||
step = "the assigner composes the wants again at its next composition"
|
||||
}
|
||||
c.State, c.Severity = asks.StateRaised, asks.SeverityWarning
|
||||
c.Summary = fmt.Sprintf("%s answered %s's %s empty at revision 0, so the grants part keeps the user list in force", fv.Tool, fv.Owner, fv.State)
|
||||
c.Headline = fv.Owner + "'s " + fv.State + " is empty"
|
||||
c.Explanation = asks.NeedsYou + "a step. The owner holds nothing of " + fv.State + " (its state rebuilt by a recreate that could " +
|
||||
"not keep the data, or a fresh mesh), so the grants part drops no login and keeps what it holds; a login revoked meanwhile " +
|
||||
"stays until the owner's first change (novox/hq design 25 § 12.2)."
|
||||
c.Needs = "let the owner write its state again from its sources (" + step + "), then grants_first_view_again"
|
||||
return c
|
||||
}
|
||||
|
||||
// firstViewsSaid is what the part last said and kept of its first views (sayFirstViews).
|
||||
type firstViewsSaid struct {
|
||||
mu sync.Mutex
|
||||
empty map[string]bool
|
||||
known bool
|
||||
}
|
||||
|
||||
// FirstViewsState is the value of KeyFirstViews: each first view's status, as the part's process last kept it.
|
||||
type FirstViewsState struct {
|
||||
V int `json:"v"`
|
||||
Views []views.Status `json:"views"`
|
||||
}
|
||||
|
||||
// sayFirstViews keeps the first views' statuses in the part's own state (for grants_first_views) when they change, and
|
||||
// raises or clears FirstViewEmpty for each answered at revision 0. Each pass calls it; nil Views is none.
|
||||
func (p *Part) sayFirstViews(res *Result) {
|
||||
if p.Views == nil {
|
||||
return
|
||||
}
|
||||
statuses := p.Views.Statuses()
|
||||
for i := range statuses {
|
||||
statuses[i].Asked, statuses[i].Answered = statuses[i].Asked.Truncate(time.Second), statuses[i].Answered.Truncate(time.Second)
|
||||
}
|
||||
data, _ := json.Marshal(FirstViewsState{V: 1, Views: statuses})
|
||||
p.changed(res, StateUserList, KeyFirstViews, data)
|
||||
p.fvSaid.mu.Lock()
|
||||
defer p.fvSaid.mu.Unlock()
|
||||
if p.fvSaid.empty == nil {
|
||||
p.fvSaid.empty = map[string]bool{}
|
||||
}
|
||||
for i, fv := range FirstViews {
|
||||
s := statuses[i]
|
||||
empty := !s.Answered.IsZero() && s.Revision == 0
|
||||
if p.fvSaid.known && p.fvSaid.empty[fv.Read] == empty {
|
||||
continue
|
||||
}
|
||||
if s.Answered.IsZero() && !p.fvSaid.known {
|
||||
continue // not answered yet: nothing to say of its emptiness
|
||||
}
|
||||
if p.Say == nil || p.Say(emptyCondition(fv, empty)) == nil {
|
||||
p.fvSaid.empty[fv.Read] = empty
|
||||
}
|
||||
}
|
||||
p.fvSaid.known = true
|
||||
}
|
||||
|
||||
// FirstViewsOf answers the tool grants_first_views: what the part's process last kept of each first view (owner, tool,
|
||||
// state, revision, when asked and answered, asks, hand acts, whether its condition is raised).
|
||||
func (p *Part) FirstViewsOf() (any, error) {
|
||||
raw, rev, found, err := p.Store.Get(StateUserList, KeyFirstViews)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("its own %s %s cannot be read: %w", StateUserList, KeyFirstViews, err)
|
||||
}
|
||||
if !found {
|
||||
return map[string]any{"views": []any{}, "said": "the part's process has kept no first view yet: it has not passed since it started"}, nil
|
||||
}
|
||||
var st FirstViewsState
|
||||
if err := json.Unmarshal(raw, &st); err != nil {
|
||||
return nil, fmt.Errorf("its own %s %s does not read: %w", StateUserList, KeyFirstViews, err)
|
||||
}
|
||||
return map[string]any{"views": st.Views, "revision": rev, "first-views": FirstViews}, nil
|
||||
}
|
||||
|
||||
// AskFirstViewAgain answers the tool grants_first_view_again: it asks the part's process to ask one first view (by its
|
||||
// state, or every one when state is empty) once more now, and records the hand act through record (the seat
|
||||
// mesh-hand-acts); a state that is none of the five is refused before anything is asked.
|
||||
func (p *Part) AskFirstViewAgain(caller, state, why string, record func(what, why string) error) (any, error) {
|
||||
which := AllFirstViews
|
||||
if state != "" {
|
||||
which = ""
|
||||
for _, fv := range FirstViews {
|
||||
if state == fv.State || state == fv.Tool || state == fv.Read {
|
||||
which = fv.State
|
||||
}
|
||||
}
|
||||
if which == "" {
|
||||
return nil, fmt.Errorf("%q is no first view of the grants part: one of placement, wants, manifests, modules, verdicts, or none for all", state)
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(why) == "" {
|
||||
return nil, errors.New("grants_first_view_again needs why: it is recorded as a hand act")
|
||||
}
|
||||
if record != nil {
|
||||
if err := record("grants_first_view_again "+which, 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(), FirstView: which})
|
||||
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, "first-view": which,
|
||||
"says": "the part's process asks it once more at this ask; grants_first_views shows its answer"}, nil
|
||||
}
|
||||
|
||||
// Unwrapped is a call whose answer is read as the tool's own text where the runtime wraps it as content (unwrap), as
|
||||
// a first view's answer is decoded (views.AskVia).
|
||||
func Unwrapped(call func(key string, body any) (json.RawMessage, error)) func(key string, body any) (json.RawMessage, error) {
|
||||
return func(key string, body any) (json.RawMessage, error) {
|
||||
raw, err := call(key, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return unwrap(raw), nil
|
||||
}
|
||||
}
|
||||
@@ -47,8 +47,11 @@ const (
|
||||
|
||||
// Reads is every state the part reads, in the manifest's order. Not the controller's assignments: the seat's placement
|
||||
// alone places modules (novox/hq ADR 0549 §5, ADR 0571 §10; issue 774).
|
||||
var Reads = []string{ReadModules, ReadManifests, ReadVerdicts, ReadResolved, ReadHave, ReadWants, ReadBuilds,
|
||||
ReadAccounts, ReadPlacement, ReadEnrolments, ReadNodeLogins}
|
||||
var Reads = []string{ReadResolved, ReadHave, ReadBuilds, ReadAccounts, ReadEnrolments, ReadNodeLogins}
|
||||
|
||||
// ViewReads are the owners' states the part reads by their first views and views families, never their buckets
|
||||
// (firstviews.go, ADR 0615): each read in Reads' place.
|
||||
var ViewReads = []string{ReadModules, ReadManifests, ReadVerdicts, ReadWants, ReadPlacement}
|
||||
|
||||
// The bus module's tools the part calls, as its manifest's invokes name them: read-only, each.
|
||||
const (
|
||||
@@ -58,7 +61,18 @@ const (
|
||||
)
|
||||
|
||||
// Invokes is every tool the part calls.
|
||||
var Invokes = []string{CallUsers, CallUserCan, CallStreams, CallRootFree}
|
||||
var Invokes = append([]string{CallUsers, CallUserCan, CallStreams, CallRootFree, CallHandAct}, firstViewTools()...)
|
||||
|
||||
// CallHandAct records a person's act (grants_first_view_again).
|
||||
const CallHandAct = "seat:mesh-hand-acts.hand-act"
|
||||
|
||||
func firstViewTools() []string {
|
||||
var out []string
|
||||
for _, fv := range FirstViews {
|
||||
out = append(out, fv.Tool)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Reader reads another part's state, named `<module>.<state>`. A refusal (a read not granted yet, a bucket not there) is
|
||||
// an error the part names as a gap and composes past: it never stops for one.
|
||||
|
||||
@@ -2,6 +2,7 @@ package grants
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
@@ -197,6 +198,11 @@ type Part struct {
|
||||
hashHold []string
|
||||
// brokers are the modules whose manifest claims mesh-broker at the last pass (bindings.go), guarded by mu.
|
||||
brokers map[string]bool
|
||||
// Views are the part's first views of the owners' states (firstviews.go), and FirstViewContext the process's life
|
||||
// they ask in: the process's; nil in the tools.
|
||||
Views *Views
|
||||
FirstViewContext context.Context
|
||||
fvSaid firstViewsSaid
|
||||
}
|
||||
|
||||
// keepBrokers keeps the modules whose manifest claims the seat mesh-broker: the only emitters whose created, recreated
|
||||
@@ -233,8 +239,13 @@ type Ask struct {
|
||||
// Consumer is, from grants_consumer_match, one account consumer `<stream>.<name>` to bring to the composed
|
||||
// configuration by hand (ADR 0561 §6).
|
||||
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"`
|
||||
}
|
||||
|
||||
// AllFirstViews asks every first view once more.
|
||||
const AllFirstViews = "all"
|
||||
|
||||
// asked is a new ask, read once per ask; the zero Ask when none.
|
||||
func (p *Part) asked() Ask {
|
||||
raw, rev, found, err := p.Store.Get(StateUserList, KeyAsked)
|
||||
@@ -449,6 +460,18 @@ func (p *Part) Compose() Result {
|
||||
}
|
||||
}
|
||||
p.consumers(&res, recs, ask.Consumer)
|
||||
p.sayFirstViews(&res)
|
||||
if ask.FirstView != "" && p.Views != nil && p.FirstViewContext != nil {
|
||||
state := ask.FirstView
|
||||
if state == AllFirstViews {
|
||||
state = ""
|
||||
}
|
||||
if asked, err := p.Views.Again(p.FirstViewContext, state, "by hand: "+ask.By); err != nil {
|
||||
res.Errors = append(res.Errors, err.Error())
|
||||
} else if p.Logf != nil {
|
||||
p.Logf("asked the first views %v once more by hand, for %s", asked, ask.By)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
@@ -39,8 +39,17 @@ func FromKeeper(event string) bool {
|
||||
const EventAccountIssued = "mesh-vault." + buscompose.AccountIssuedEvent
|
||||
|
||||
// Consumes is every event the part hears, as its manifest's consumes name them.
|
||||
var Consumes = []string{EventKeeperRaised, EventKeeperCleared, EventAccountIssued, BusModule + "." + BrokerCreated,
|
||||
BusModule + "." + BrokerDropped, BusModule + "." + BrokerRecreated}
|
||||
var Consumes = append([]string{EventKeeperRaised, EventKeeperCleared, EventAccountIssued, BusModule + "." + BrokerCreated,
|
||||
BusModule + "." + BrokerDropped, BusModule + "." + BrokerRecreated}, viewsEvents()...)
|
||||
|
||||
// viewsEvents are the owners' views events the part follows its first views by, `<owner>.views.<state>`.
|
||||
func viewsEvents() []string {
|
||||
var out []string
|
||||
for _, fv := range FirstViews {
|
||||
out = append(out, fv.Owner+".views."+fv.State)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// IsAccountIssued says an event heard is the vault's account-issued, by its name or its bus subject.
|
||||
func IsAccountIssued(event string) bool {
|
||||
|
||||
@@ -23,11 +23,14 @@ const (
|
||||
ToolAccounts = "grants_accounts"
|
||||
ToolConsumerMatch = "grants_consumer_match"
|
||||
ToolAcceptHashes = "grants_accept_hashes"
|
||||
// ToolFirstViews and ToolFirstViewAgain read and ask again the part's first views (firstviews.go).
|
||||
ToolFirstViews = "grants_first_views"
|
||||
ToolFirstViewAgain = "grants_first_view_again"
|
||||
)
|
||||
|
||||
// Tools is every tool, in the manifest's order.
|
||||
var Tools = []string{ToolGrants, ToolMembership, ToolUserList, ToolComposeNow, ToolAssert, ToolAccounts, ToolConsumerMatch,
|
||||
ToolAcceptHashes}
|
||||
ToolAcceptHashes, ToolFirstViews, ToolFirstViewAgain}
|
||||
|
||||
// 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).
|
||||
|
||||
@@ -4,12 +4,7 @@
|
||||
"slug": "grants",
|
||||
"reads": [
|
||||
"mesh-config.resolved",
|
||||
"mesh-delivery-assigner.wants",
|
||||
"mesh-delivery-builder.builds",
|
||||
"mesh-delivery-indexer.manifests",
|
||||
"mesh-delivery-indexer.modules",
|
||||
"mesh-delivery-indexer.verdicts",
|
||||
"mesh-delivery-assigner.placement",
|
||||
"node-engine.have",
|
||||
"mesh-vault.bus-accounts",
|
||||
"mesh-enrolment.enrolments",
|
||||
@@ -21,7 +16,12 @@
|
||||
"mesh-vault.account-issued",
|
||||
"nats.created",
|
||||
"nats.dropped",
|
||||
"nats.recreated"
|
||||
"nats.recreated",
|
||||
"mesh-delivery-assigner.views.placement",
|
||||
"mesh-delivery-assigner.views.wants",
|
||||
"mesh-delivery-indexer.views.manifests",
|
||||
"mesh-delivery-indexer.views.modules",
|
||||
"mesh-delivery-indexer.views.verdicts"
|
||||
],
|
||||
"uses": [
|
||||
"operator-channel"
|
||||
@@ -30,7 +30,13 @@
|
||||
"nats.nats_streams",
|
||||
"nats.nats_user_can",
|
||||
"nats.nats_users",
|
||||
"seat:node-engine.root-free"
|
||||
"seat:node-engine.root-free",
|
||||
"mesh-delivery-assigner.placement",
|
||||
"mesh-delivery-assigner.wants",
|
||||
"mesh-delivery-indexer.manifests",
|
||||
"mesh-delivery-indexer.modules",
|
||||
"mesh-delivery-indexer.verdicts",
|
||||
"seat:mesh-hand-acts.hand-act"
|
||||
],
|
||||
"emits": [
|
||||
"asserted",
|
||||
@@ -47,7 +53,9 @@
|
||||
"grants_assert",
|
||||
"grants_accounts",
|
||||
"grants_consumer_match",
|
||||
"grants_accept_hashes"
|
||||
"grants_accept_hashes",
|
||||
"grants_first_views",
|
||||
"grants_first_view_again"
|
||||
],
|
||||
"state": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user