Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bab2e1391a | ||
|
|
663c9cbc34 | ||
|
|
3d916a685e |
@@ -170,21 +170,6 @@ func TryTools(tr Trying) []stdio.Tool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seat is the seat the checker holds (novox/hq ADR 0547 §10, issue 774); its verbs are served as tools named <seat>.<verb>.
|
|
||||||
const Seat = "mesh-delivery-checker"
|
|
||||||
|
|
||||||
// SeatTryTools are the try's tools as the seat's verbs: try, try-read and try-end, run by the same code as checker_try,
|
|
||||||
// checker_try_read and checker_try_end, so the answer is the same. The module tools stay beside them.
|
|
||||||
func SeatTryTools(tr Trying) []stdio.Tool {
|
|
||||||
verbs := map[string]string{check.TryTool: "try", check.TryReadTool: "try-read", check.TryEndTool: "try-end"}
|
|
||||||
var out []stdio.Tool
|
|
||||||
for _, t := range TryTools(tr) {
|
|
||||||
t.Name = Seat + "." + verbs[t.Name]
|
|
||||||
out = append(out, t)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tools are the read tools over the checker's states.
|
// Tools are the read tools over the checker's states.
|
||||||
func Tools(r check.Reader) []stdio.Tool {
|
func Tools(r check.Reader) []stdio.Tool {
|
||||||
return []stdio.Tool{
|
return []stdio.Tool{
|
||||||
@@ -292,7 +277,7 @@ func main() {
|
|||||||
return err
|
return err
|
||||||
}}
|
}}
|
||||||
trying := Trying{Tries: store, Ender: store, Trunks: trunks}
|
trying := Trying{Tries: store, Ender: store, Trunks: trunks}
|
||||||
if err := stdio.Serve("", logged(append(append(Tools(r), TryTools(trying)...), SeatTryTools(trying)...))); err != nil {
|
if err := stdio.Serve("", logged(append(Tools(r), TryTools(trying)...))); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "[mesh-delivery-checker-tools] %v\n", err)
|
fmt.Fprintf(os.Stderr, "[mesh-delivery-checker-tools] %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,24 +80,6 @@ func TestTheTryToolsAskAndReadATry(t *testing.T) { // ADR 0501 §5, the agreed i
|
|||||||
if err != nil || !strings.Contains(string(raw.(json.RawMessage)), `"state":"queued"`) {
|
if err != nil || !strings.Contains(string(raw.(json.RawMessage)), `"state":"queued"`) {
|
||||||
t.Errorf("read = %s, %v", raw, err)
|
t.Errorf("read = %s, %v", raw, err)
|
||||||
}
|
}
|
||||||
// The seat's verbs answer as the tools do (ADR 0547 §10, issue 774): the same ask is the same try, and the read is alike.
|
|
||||||
verbs := SeatTryTools(Trying{Tries: store, Ender: store, Now: func() time.Time { return time.Unix(1, 5) },
|
|
||||||
Trunks: func() (map[string]string, error) {
|
|
||||||
return map[string]string{"novox/mesh-delivery": strings.Repeat("a", 40)}, nil
|
|
||||||
}})
|
|
||||||
if len(verbs) != 3 || verbs[0].Name != Seat+".try" || verbs[1].Name != Seat+".try-read" || verbs[2].Name != Seat+".try-end" {
|
|
||||||
t.Fatalf("verbs = %+v", verbs)
|
|
||||||
}
|
|
||||||
again, err := verbs[0].Run(map[string]any{"repository": "novox/mesh-delivery", "head": strings.Repeat("1", 40), "trunk": "main"})
|
|
||||||
if err != nil || again.(map[string]any)["id"] != id {
|
|
||||||
t.Errorf("verb try = %v, %v, want the waiting try %s", again, err, id)
|
|
||||||
}
|
|
||||||
if viaVerb, err := verbs[1].Run(map[string]any{"id": id}); err != nil || string(viaVerb.(json.RawMessage)) != string(raw.(json.RawMessage)) {
|
|
||||||
t.Errorf("verb try-read = %s, %v; tool = %s", viaVerb, err, raw)
|
|
||||||
}
|
|
||||||
if _, err := verbs[0].Run(map[string]any{"repository": "novox/hq", "head": strings.Repeat("1", 40)}); err == nil {
|
|
||||||
t.Error("the verb tried a repository nothing checks")
|
|
||||||
}
|
|
||||||
// Issue 734: a try whose ask was lost is ended by hand with why, and its head is asked anew.
|
// Issue 734: a try whose ask was lost is ended by hand with why, and its head is asked anew.
|
||||||
ended, err := tools[2].Run(map[string]any{"id": id, "why": "its ask was lost"})
|
ended, err := tools[2].Run(map[string]any{"id": id, "why": "its ask was lost"})
|
||||||
if err != nil || ended.(map[string]any)["state"] != "error" {
|
if err != nil || ended.(map[string]any)["state"] != "error" {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module mesh-delivery-checker
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027220144-3f7957e16972
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027193820-8ad9372b1952
|
||||||
github.com/nats-io/nats-server/v2 v2.11.17
|
github.com/nats-io/nats-server/v2 v2.11.17
|
||||||
github.com/nats-io/nats.go v1.54.0
|
github.com/nats-io/nats.go v1.54.0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027220144-3f7957e16972 h1:iYDYv4HpV1ic20Ban0MaOErryQfNT8ngjlh0bKW6sCU=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027193820-8ad9372b1952 h1:TmveT3Bija643RGWkilvkzP/h8UWlVvHFZZtQfJK5oU=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027220144-3f7957e16972/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027193820-8ad9372b1952/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||||
|
|||||||
@@ -38,11 +38,6 @@ type stream struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type manifest struct {
|
type manifest struct {
|
||||||
Claims []struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Scope string `json:"scope"`
|
|
||||||
Serves []string `json:"serves"`
|
|
||||||
} `json:"claims"`
|
|
||||||
Streams []stream `json:"streams"`
|
Streams []stream `json:"streams"`
|
||||||
Tools []string `json:"tools"`
|
Tools []string `json:"tools"`
|
||||||
Module string `json:"module"`
|
Module string `json:"module"`
|
||||||
@@ -133,12 +128,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
|
|||||||
if want := []string{"checker_kept", "checker_previews", "checker_pull", "checker_status", "checker_try", "checker_try_end", "checker_try_read", "checker_verdicts"}; !reflect.DeepEqual(sorted(m.Tools), want) {
|
if want := []string{"checker_kept", "checker_previews", "checker_pull", "checker_status", "checker_try", "checker_try_end", "checker_try_read", "checker_verdicts"}; !reflect.DeepEqual(sorted(m.Tools), want) {
|
||||||
t.Errorf("tools = %v, want %v", m.Tools, want)
|
t.Errorf("tools = %v, want %v", m.Tools, want)
|
||||||
}
|
}
|
||||||
// The try is the checker's own seat's verbs (ADR 0547 §10, issue 774), served beside the tools by the same code.
|
for _, k := range []string{"logs", "emits", "claims", "provides", "requires", "defines-seats", "seat", "seats", "bus", "config"} {
|
||||||
if len(m.Claims) != 1 || m.Claims[0].Name != "mesh-delivery-checker" || m.Claims[0].Scope != "mesh" ||
|
|
||||||
!reflect.DeepEqual(m.Claims[0].Serves, []string{"try", "try-read", "try-end"}) {
|
|
||||||
t.Errorf("claims = %+v, want the mesh seat mesh-delivery-checker serving try, try-read and try-end", m.Claims)
|
|
||||||
}
|
|
||||||
for _, k := range []string{"logs", "emits", "provides", "requires", "defines-seats", "seat", "seats", "bus", "config"} {
|
|
||||||
if _, ok := top[k]; ok {
|
if _, ok := top[k]; ok {
|
||||||
t.Errorf("the manifest declares %q: the checker has none", k)
|
t.Errorf("the manifest declares %q: the checker has none", k)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,6 @@
|
|||||||
"module": "mesh-delivery-checker",
|
"module": "mesh-delivery-checker",
|
||||||
"version": "1",
|
"version": "1",
|
||||||
"slug": "checker",
|
"slug": "checker",
|
||||||
"claims": [
|
|
||||||
{
|
|
||||||
"name": "mesh-delivery-checker",
|
|
||||||
"scope": "mesh",
|
|
||||||
"serves": [
|
|
||||||
"try",
|
|
||||||
"try-read",
|
|
||||||
"try-end"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"mesh-build-go.toolchain.reported",
|
"mesh-build-go.toolchain.reported",
|
||||||
"mesh-build-node.toolchain.reported"
|
"mesh-build-node.toolchain.reported"
|
||||||
|
|||||||
@@ -214,10 +214,6 @@ func (s ownStore) Put(state, key string, value []byte) (uint64, error) {
|
|||||||
return s.c.State(state).Put(key, json.RawMessage(value))
|
return s.c.State(state).Put(key, json.RawMessage(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s ownStore) Keys(state string) ([]string, error) { return s.c.State(state).Keys() }
|
|
||||||
|
|
||||||
func (s ownStore) Delete(state, key string) error { return s.c.State(state).Delete(key) }
|
|
||||||
|
|
||||||
// emitting is the process connection as the part's emitter: its events, with no options.
|
// emitting is the process connection as the part's emitter: its events, with no options.
|
||||||
type emitting struct{ c *process.Conn }
|
type emitting struct{ c *process.Conn }
|
||||||
|
|
||||||
|
|||||||
@@ -18,41 +18,6 @@ type creating struct {
|
|||||||
js jetstream.JetStream
|
js jetstream.JetStream
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateConsumer creates one durable consumer an account needs, by its stream and name (ADR 0561 §6): the create alone;
|
|
||||||
// one the bus holds answers ErrHeld and is left as it is.
|
|
||||||
func (c creating) CreateConsumer(stream string, cfg jetstream.ConsumerConfig) error {
|
|
||||||
ctx, cancel := context.WithTimeout(c.ctx, requestWithin)
|
|
||||||
defer cancel()
|
|
||||||
if _, err := c.js.Consumer(ctx, stream, cfg.Durable); err == nil {
|
|
||||||
return grants.ErrHeld
|
|
||||||
}
|
|
||||||
_, err := c.js.CreateConsumer(ctx, stream, cfg)
|
|
||||||
switch {
|
|
||||||
case err == nil:
|
|
||||||
return nil
|
|
||||||
case errors.Is(err, jetstream.ErrConsumerExists):
|
|
||||||
return grants.ErrHeld
|
|
||||||
case errors.Is(err, context.DeadlineExceeded):
|
|
||||||
return fmt.Errorf("no answer in %s: a create the bus refuses is answered by none (%w)", requestWithin, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// MatchConsumer brings a held consumer to cfg on a person's ask (grants_consumer_match), keeping where it delivers from:
|
|
||||||
// the server refuses a changed deliver policy, and a consumer moved would replay or lose what it holds.
|
|
||||||
func (c creating) MatchConsumer(stream string, cfg jetstream.ConsumerConfig) error {
|
|
||||||
ctx, cancel := context.WithTimeout(c.ctx, requestWithin)
|
|
||||||
defer cancel()
|
|
||||||
held, err := c.js.Consumer(ctx, stream, cfg.Durable)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
have := held.CachedInfo().Config
|
|
||||||
cfg.DeliverPolicy, cfg.OptStartSeq, cfg.OptStartTime = have.DeliverPolicy, have.OptStartSeq, have.OptStartTime
|
|
||||||
_, err = c.js.UpdateConsumer(ctx, stream, cfg)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c creating) CreateDeclared(cfg jetstream.StreamConfig) error {
|
func (c creating) CreateDeclared(cfg jetstream.StreamConfig) error {
|
||||||
ctx, cancel := context.WithTimeout(c.ctx, requestWithin)
|
ctx, cancel := context.WithTimeout(c.ctx, requestWithin)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|||||||
@@ -62,9 +62,14 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// keptStates is what the part keeps (ADR 0495 §2-§3): each readable, for the view and a session, but the wanted accounts,
|
// keptStates is what the part keeps, each readable (ADR 0495 §2-§3): the view and a session read them.
|
||||||
// by the vault alone (ADR 0561 §2).
|
func keptStates() []announce.State {
|
||||||
func keptStates() []announce.State { return grants.Announced() }
|
var out []announce.State
|
||||||
|
for _, s := range grants.States {
|
||||||
|
out = append(out, announce.State{Name: s, Readable: true})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// run is the process: it returns when ctx ends, or when it has no credential or cannot dial the bus at all. A bucket or
|
// run is the process: it returns when ctx ends, or when it has no credential or cannot dial the bus at all. A bucket or
|
||||||
// read the bus refuses never ends it.
|
// read the bus refuses never ends it.
|
||||||
@@ -112,9 +117,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
|||||||
}
|
}
|
||||||
bus := newBusReader(ctx, js)
|
bus := newBusReader(ctx, js)
|
||||||
f.Part = &grants.Part{Read: bus, Call: conn, Store: ownStore{conn}, Emit: emitting{conn},
|
f.Part = &grants.Part{Read: bus, Call: conn, Store: ownStore{conn}, Emit: emitting{conn},
|
||||||
Publish: memberships{ctx: ctx, nc: nc, js: js}, Create: creating{ctx: ctx, js: js}, Consumers: creating{ctx: ctx, js: js},
|
Publish: memberships{ctx: ctx, nc: nc, js: js}, Create: creating{ctx: ctx, js: js},
|
||||||
Objects: sealedObjects{ctx: ctx, js: js}, Logf: logger.Printf,
|
Objects: sealedObjects{ctx: ctx, js: js}, Logf: logger.Printf}
|
||||||
Bus: grants.BusAddress{URL: account.URL, Fingerprint: account.Fingerprint}}
|
|
||||||
f.Watch = bus
|
f.Watch = bus
|
||||||
// The operator's word on take-over and hand-back is their yes on their channel: the part asks it itself, and acts on
|
// The operator's word on take-over and hand-back is their yes on their channel: the part asks it itself, and acts on
|
||||||
// the warrant once (mesh-sdk asker), after its refusals.
|
// the warrant once (mesh-sdk asker), after its refusals.
|
||||||
@@ -134,9 +138,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
// The controller makes this module's durable consumer on EVENTS from the manifest's consumes (filtered on the two
|
// The controller makes this module's durable consumer on EVENTS from the manifest's consumes (filtered on the two
|
||||||
// condition subjects and the vault's account-issued; ADR 0561 §6 gives that to the part from the lease write); it is
|
// condition subjects); it is pulled and every message acknowledged, so its pending never grows. Where it is not there
|
||||||
// pulled and every message acknowledged, so its pending never grows. Where it is not there yet the pass says so and
|
// yet the pass says so and hears again at the next change.
|
||||||
// hears again at the next change. The vault's bus-accounts is watched beside it, so a hash is composed either way.
|
|
||||||
f.Hear = func(handle func(string, []byte)) error {
|
f.Hear = func(handle func(string, []byte)) error {
|
||||||
cons, err := js.Consumer(ctx, "EVENTS", account.Node+"_"+account.Module)
|
cons, err := js.Consumer(ctx, "EVENTS", account.Node+"_"+account.Module)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strings"
|
|
||||||
|
|
||||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||||
|
|
||||||
@@ -40,10 +39,6 @@ func (runtimeStore) Put(state, key string, value []byte) (uint64, error) {
|
|||||||
return stdio.State(state).Put(key, json.RawMessage(value))
|
return stdio.State(state).Put(key, json.RawMessage(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (runtimeStore) Keys(state string) ([]string, error) { return stdio.State(state).Keys() }
|
|
||||||
|
|
||||||
func (runtimeStore) Delete(state, key string) error { return stdio.State(state).Delete(key) }
|
|
||||||
|
|
||||||
// runtimeBus calls tools and says events through the runtime.
|
// runtimeBus calls tools and says events through the runtime.
|
||||||
type runtimeBus struct{}
|
type runtimeBus struct{}
|
||||||
|
|
||||||
@@ -118,43 +113,6 @@ func toolsOf(p *grants.Part) []stdio.Tool {
|
|||||||
Run: func(a map[string]any) (any, error) {
|
Run: func(a map[string]any) (any, error) {
|
||||||
return p.HandBack(stdio.CallerOf(a), grants.Answer(a, "why"))
|
return p.HandBack(stdio.CallerOf(a), grants.Answer(a, "why"))
|
||||||
}},
|
}},
|
||||||
{Name: grants.ToolAccounts, Description: "The bus accounts the mesh wants (novox/hq ADR 0561 §2, §9): every account the vault " +
|
|
||||||
"mints that placement composes (a module's, a process's, a machine's provisioner client's), as the grants part last " +
|
|
||||||
"wrote it to its state accounts, which the vault alone reads: user, kind, node, module, process, the bus's address and " +
|
|
||||||
"fingerprint, a module's claims with the verbs each seat promises, and when the key was written; one user's or all; " +
|
|
||||||
"what the last pass wrote, deleted or kept; and the durable consumers the accounts need as the last pass found them " +
|
|
||||||
"(missing, differing and why, created; whether the lease is handed away). Never a password. Read-only: " +
|
|
||||||
"grants_compose_now writes them again.",
|
|
||||||
Input: schema(map[string]string{"user": "one bus user, e.g. ace.mesh-build-go"}),
|
|
||||||
Run: func(a map[string]any) (any, error) { return p.Accounts(grants.Answer(a, "user")) }},
|
|
||||||
{Name: grants.ToolConsumerMatch, Description: "Bring one durable consumer an account needs (a module's on EVENTS, a process's on " +
|
|
||||||
"its work queue; novox/hq ADR 0561 §6) to the configuration the bus composer composes, by hand: the recovery when " +
|
|
||||||
"the condition mesh-grants.consumer-differs names it. Asks the part's process, which alone may, and keeps where the " +
|
|
||||||
"consumer delivers from. Refused while the controller holds its lease (the consumers are the controller's then) and " +
|
|
||||||
"for a consumer the records do not compose. grants_user_list's last pass says what became of it.",
|
|
||||||
Input: schema(map[string]string{"stream": "the stream, e.g. EVENTS", "consumer": "the consumer, e.g. ace_mesh-build-go"}, "stream", "consumer"),
|
|
||||||
Run: func(a map[string]any) (any, error) {
|
|
||||||
return p.MatchConsumer(grants.Answer(a, "stream"), grants.Answer(a, "consumer"))
|
|
||||||
}},
|
|
||||||
{Name: grants.ToolAcceptHashes, Description: "Release the hold of a user list that changes several loaded users' hashes at " +
|
|
||||||
"once (novox/hq ADR 0561 §7; the condition mesh-grants.user-list-held names them), by a person's act with why: names " +
|
|
||||||
"exactly the users held, bound to the held list's digest, so the part's process writes that list once at its next pass " +
|
|
||||||
"and says hashes-accepted. It never releases another hold (a failed read, a user dropped for want of a hash, a lost " +
|
|
||||||
"right), and a list composed otherwise since is held again for a new acceptance.",
|
|
||||||
Input: map[string]any{"type": "object", "properties": map[string]any{
|
|
||||||
"users": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "the users held, as the condition names them"},
|
|
||||||
"why": map[string]any{"type": "string", "description": "why their hashes change"}}, "required": []string{"users", "why"}},
|
|
||||||
Run: func(a map[string]any) (any, error) {
|
|
||||||
var users []string
|
|
||||||
if list, ok := a["users"].([]any); ok {
|
|
||||||
for _, u := range list {
|
|
||||||
if s, ok := u.(string); ok {
|
|
||||||
users = append(users, strings.TrimSpace(s))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return p.AcceptHashes(stdio.CallerOf(a), users, grants.Answer(a, "why"))
|
|
||||||
}},
|
|
||||||
{Name: grants.ToolTakeOverState, Description: "Where the switch stands: the list the bus node follows, the ask open now, the request, and " +
|
{Name: grants.ToolTakeOverState, Description: "Where the switch stands: the list the bus node follows, the ask open now, the request, and " +
|
||||||
"every act with what became of it (asked, switched, declined, expired, refused and why).",
|
"every act with what became of it (asked, switched, declined, expired, refused and why).",
|
||||||
Input: schema(nil),
|
Input: schema(nil),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module mesh-grants
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027220144-3f7957e16972
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027232145-7359d187db8d
|
||||||
github.com/nats-io/nats.go v1.54.0
|
github.com/nats-io/nats.go v1.54.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027220144-3f7957e16972 h1:iYDYv4HpV1ic20Ban0MaOErryQfNT8ngjlh0bKW6sCU=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026210326-979644420117 h1:88CNZOoQgu7YPYKW16qkWT8Gw9Ucg07aTrIImA2nYUk=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027220144-3f7957e16972/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026210326-979644420117/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027012704-062c29c3a22f h1:AKzV973UTWe0OYFoIjgGu0f+B26xh8QsAHzCpsTIjM8=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027012704-062c29c3a22f/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027014310-cadd7b1b2f8f h1:mimAGERguQmCGYVNdRTrFcZ1dhpU6GWo+Q3EDhHOrVk=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027014310-cadd7b1b2f8f/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027195758-cebc6519d5b0 h1:9B7B38e3RtjbtX1uGP9dRgU9VY1/J2yAfnb5Ajoq4/s=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027195758-cebc6519d5b0/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027200317-538ffbaa085b h1:LuBK5dtBgm978krhj41N2MKA07Yv4zetRPOdHRTB6L8=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027200317-538ffbaa085b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027201213-7a8e9f970648 h1:XoMsgg9kSgjQWa86NZM8h8OpxJ2kovxcMS2C0i6WSVU=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027201213-7a8e9f970648/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027232145-7359d187db8d h1:DhR7DRKaF0OtA5l/ikIk7gZGeIM1lRz65PUE1DGEkvU=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027232145-7359d187db8d/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"maps"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -58,45 +57,6 @@ type fakeBus struct {
|
|||||||
refuseUsers bool
|
refuseUsers bool
|
||||||
// rootFree answers the controller's root-free verb: nil is no answerer at all (no responders).
|
// rootFree answers the controller's root-free verb: nil is no answerer at all (no responders).
|
||||||
rootFree func(machines []string) (any, error)
|
rootFree func(machines []string) (any, error)
|
||||||
// consumers are the durable consumers the bus holds, by stream, as nats_streams answers them for one stream; and
|
|
||||||
// consumerCreates each consumer the part asked to create, `<stream>.<name>`.
|
|
||||||
consumers map[string][]map[string]any
|
|
||||||
consumerCreates []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// MatchConsumer is the bus's update of a held durable consumer to the configuration given.
|
|
||||||
func (b *fakeBus) MatchConsumer(stream string, cfg jetstream.ConsumerConfig) error {
|
|
||||||
b.mu.Lock()
|
|
||||||
defer b.mu.Unlock()
|
|
||||||
b.consumerCreates = append(b.consumerCreates, "match "+stream+"."+cfg.Durable)
|
|
||||||
for _, c := range b.consumers[stream] {
|
|
||||||
if c["name"] == cfg.Durable {
|
|
||||||
c["max_deliver"] = cfg.MaxDeliver
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return errors.New("consumer not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateConsumer is the bus's create of a durable consumer: held when the bus holds one of that name, else held from now.
|
|
||||||
func (b *fakeBus) CreateConsumer(stream string, cfg jetstream.ConsumerConfig) error {
|
|
||||||
b.mu.Lock()
|
|
||||||
defer b.mu.Unlock()
|
|
||||||
b.consumerCreates = append(b.consumerCreates, stream+"."+cfg.Durable)
|
|
||||||
for _, c := range b.consumers[stream] {
|
|
||||||
if c["name"] == cfg.Durable {
|
|
||||||
return grants.ErrHeld
|
|
||||||
}
|
|
||||||
}
|
|
||||||
filters := append([]string{}, cfg.FilterSubjects...)
|
|
||||||
if cfg.FilterSubject != "" {
|
|
||||||
filters = []string{cfg.FilterSubject}
|
|
||||||
}
|
|
||||||
if b.consumers == nil {
|
|
||||||
b.consumers = map[string][]map[string]any{}
|
|
||||||
}
|
|
||||||
b.consumers[stream] = append(b.consumers[stream], map[string]any{"name": cfg.Durable, "filters": filters, "max_deliver": cfg.MaxDeliver})
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *fakeBus) LastMembership(node, module string) ([]byte, uint64, bool, error) {
|
func (b *fakeBus) LastMembership(node, module string) ([]byte, uint64, bool, error) {
|
||||||
@@ -217,9 +177,6 @@ func (b *fakeBus) Call(key string, body any) (json.RawMessage, error) {
|
|||||||
}
|
}
|
||||||
return json.Marshal(map[string]any{"users": out})
|
return json.Marshal(map[string]any{"users": out})
|
||||||
case grants.CallStreams:
|
case grants.CallStreams:
|
||||||
if stream, _ := args["stream"].(string); stream != "" {
|
|
||||||
return json.Marshal(map[string]any{"detail": []map[string]any{{"name": stream, "consumer_detail": b.consumers[stream]}}})
|
|
||||||
}
|
|
||||||
return json.Marshal(map[string]any{"detail": b.streams})
|
return json.Marshal(map[string]any{"detail": b.streams})
|
||||||
case grants.CallRootFree:
|
case grants.CallRootFree:
|
||||||
if b.rootFree == nil {
|
if b.rootFree == nil {
|
||||||
@@ -260,27 +217,6 @@ func (s ownStore) Put(state, key string, value []byte) (uint64, error) {
|
|||||||
return b.revs[state+"/"+key], nil
|
return b.revs[state+"/"+key], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s ownStore) Keys(state string) ([]string, error) {
|
|
||||||
b := s.b
|
|
||||||
b.mu.Lock()
|
|
||||||
defer b.mu.Unlock()
|
|
||||||
var out []string
|
|
||||||
for k := range b.own[state] {
|
|
||||||
out = append(out, k)
|
|
||||||
}
|
|
||||||
slices.Sort(out)
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s ownStore) Delete(state, key string) error {
|
|
||||||
b := s.b
|
|
||||||
b.mu.Lock()
|
|
||||||
defer b.mu.Unlock()
|
|
||||||
b.writes = append(b.writes, state+" delete")
|
|
||||||
delete(b.own[state], key)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *fakeBus) Emit(event string, _ any) error {
|
func (b *fakeBus) Emit(event string, _ any) error {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
@@ -1366,6 +1302,21 @@ func TestGrants(t *testing.T) {
|
|||||||
if e := effect(); !strings.HasPrefix(e, "switched") {
|
if e := effect(); !strings.HasPrefix(e, "switched") {
|
||||||
t.Errorf("effect once the bus node loaded the written list = %s", e)
|
t.Errorf("effect once the bus node loaded the written list = %s", e)
|
||||||
}
|
}
|
||||||
|
// Steady (issue 1126, 2026-10-28 00:16: the shadow list moved every 3 s, 99270 to 99357, with nothing
|
||||||
|
// differing): the bus node saying again that it loaded the same list, at a later time and the key's
|
||||||
|
// later revision, moves neither the shadow list nor the sealed key.
|
||||||
|
_, sealedRev, _ := p.Sealed()
|
||||||
|
p.Compose()
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
withKey(delivery.UserListWritten{Source: delivery.GrantsUserListSource, Revision: sealedRev + uint64(i+1), Wanted: g.Digest,
|
||||||
|
Digest: g.Digest, Users: g.Users, Follows: delivery.FollowGrants, At: time.Date(2026, 10, 28, 0, 16, i, 0, time.UTC)})
|
||||||
|
if res := p.Compose(); res.Written {
|
||||||
|
t.Errorf("pass %d: the shadow list is written again for the bus node saying the same list again (revision %d)", i, res.Revision)
|
||||||
|
}
|
||||||
|
if _, rev, _ := p.Sealed(); rev != sealedRev {
|
||||||
|
t.Errorf("pass %d: the sealed key is written again with nothing changed: revision %d, was %d", i, rev, sealedRev)
|
||||||
|
}
|
||||||
|
}
|
||||||
withKey(loadedBefore)
|
withKey(loadedBefore)
|
||||||
// After take-over a list composed past a failed read, or one dropping a user of the list in force for
|
// After take-over a list composed past a failed read, or one dropping a user of the list in force for
|
||||||
// want of a hash, is not sealed: the list in force stays, and the pass says why.
|
// want of a hash, is not sealed: the list in force stays, and the pass says why.
|
||||||
@@ -1872,269 +1823,6 @@ func TestGrants(t *testing.T) {
|
|||||||
t.Errorf("the refusal was not said: %v", logged)
|
t.Errorf("the refusal was not said: %v", logged)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 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.
|
|
||||||
t.Run("wanted accounts: written once each, read by the vault alone, retired only when placement drops them", func(t *testing.T) {
|
|
||||||
b := fixture()
|
|
||||||
b.loaded = shadowOf(t, b)
|
|
||||||
p := partOn(b)
|
|
||||||
p.Bus = grants.BusAddress{URL: "nats://a.mesh:4222", Fingerprint: "ab"}
|
|
||||||
p.Compose()
|
|
||||||
p.Compose()
|
|
||||||
wrote := 0
|
|
||||||
for _, w := range b.writes {
|
|
||||||
if w == grants.StateAccounts {
|
|
||||||
wrote++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
held := slices.Sorted(maps.Keys(b.own[grants.StateAccounts]))
|
|
||||||
if !slices.Equal(held, []string{"a.mod-x", "a.nats", "b.mod-y"}) || wrote != 3 {
|
|
||||||
t.Fatalf("accounts %v written %d times in two passes, want the three modules' once each", held, wrote)
|
|
||||||
}
|
|
||||||
nats, err := buscompose.ReadWantedAccount("a.nats", b.own[grants.StateAccounts]["a.nats"])
|
|
||||||
if err != nil || nats.URL != "nats://a.mesh:4222" || nats.Fingerprint != "ab" || len(nats.Claims) != 1 ||
|
|
||||||
nats.Claims[0].Seat != catalogue.BrokerSeat || nats.At.IsZero() {
|
|
||||||
t.Errorf("a.nats is wanted as %+v (%v)", nats, err)
|
|
||||||
}
|
|
||||||
var readers []string
|
|
||||||
for _, s := range grants.Announced() {
|
|
||||||
if s.Name == grants.StateAccounts {
|
|
||||||
if s.Readable {
|
|
||||||
t.Error("the wanted accounts are announced readable by anyone")
|
|
||||||
}
|
|
||||||
readers = s.Readers
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !slices.Equal(readers, []string{"mesh-vault"}) || buscompose.ReaderMay("mesh-delivery-view", buscompose.GrantsAccounts) {
|
|
||||||
t.Errorf("the wanted accounts are read by %v", readers)
|
|
||||||
}
|
|
||||||
// A read that fails retires nothing; once read, the user placement dropped is retired.
|
|
||||||
delete(b.states[grants.ReadAssignments], "b.mod-y")
|
|
||||||
b.refused[grants.ReadResolved] = true
|
|
||||||
if res := p.Compose(); res.Accounts == nil || len(res.Accounts.Deleted) != 0 || b.own[grants.StateAccounts]["b.mod-y"] == nil {
|
|
||||||
t.Fatalf("a pass whose read failed deleted %+v", res.Accounts)
|
|
||||||
}
|
|
||||||
delete(b.refused, grants.ReadResolved)
|
|
||||||
if res := p.Compose(); res.Accounts == nil || !slices.Equal(res.Accounts.Deleted, []string{"b.mod-y"}) {
|
|
||||||
t.Fatalf("the user placement dropped is not retired: %+v", res.Accounts)
|
|
||||||
}
|
|
||||||
// The tool reads them, one and all, each with when it was written.
|
|
||||||
one, err := p.Accounts("a.nats")
|
|
||||||
all, err2 := p.Accounts("")
|
|
||||||
if err != nil || err2 != nil || one.(map[string]any)["count"] != 1 || all.(map[string]any)["count"] != 2 ||
|
|
||||||
one.(map[string]any)["accounts"].([]buscompose.WantedAccount)[0].At.IsZero() {
|
|
||||||
t.Errorf("the tool reads %v and %v (%v %v)", one, all, err, err2)
|
|
||||||
}
|
|
||||||
if _, err := p.Accounts("b.mod-y"); err == nil {
|
|
||||||
t.Error("the tool reads an account no longer wanted")
|
|
||||||
}
|
|
||||||
p.Pass()
|
|
||||||
if all, _ := p.Accounts(""); all.(map[string]any)["consumers"] == nil {
|
|
||||||
t.Errorf("the tool does not say the account consumers in shadow: %v", all)
|
|
||||||
}
|
|
||||||
// With no bus address no account is written, and the pass says why.
|
|
||||||
bare := partOn(fixture())
|
|
||||||
if res := bare.Compose(); res.Accounts != nil || !strings.Contains(strings.Join(res.Errors, " "), "no bus address") {
|
|
||||||
t.Errorf("a part with no bus address wrote %+v: %v", res.Accounts, res.Errors)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 774/AC5 (novox/hq ADR 0561 §1): the vault's account-issued wakes a pass, so the next list carries the new user's hash
|
|
||||||
// within one pass; an event of another kind does not.
|
|
||||||
t.Run("the vault's account-issued composes the list again", func(t *testing.T) {
|
|
||||||
b := fixture()
|
|
||||||
var handle func(string, []byte)
|
|
||||||
p := partOn(b)
|
|
||||||
f := &grants.Follower{Part: p, Watch: &fakeWatcher{opened: make(chan string, 16)}, Logf: func(string, ...any) {},
|
|
||||||
Hear: func(h func(string, []byte)) error { handle = h; return nil }}
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
done := make(chan struct{})
|
|
||||||
go func() { f.Run(ctx); close(done) }()
|
|
||||||
defer func() { cancel(); <-done }()
|
|
||||||
waitFor(t, func() bool { return b.count(grants.CallUsers) > 0 && handle != nil })
|
|
||||||
held := func() bool {
|
|
||||||
for _, c := range p.Last().UserList.Credentials.Users {
|
|
||||||
if c.User == "a.mod-x" {
|
|
||||||
return c.HashHeld
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if held() {
|
|
||||||
t.Fatal("a.mod-x has a hash before the vault made one")
|
|
||||||
}
|
|
||||||
b.mu.Lock()
|
|
||||||
b.states[grants.ReadAccounts] = map[string][]byte{"a.mod-x": []byte(`{"user":"a.mod-x","hash":"$2a$10$x","generation":1}`)}
|
|
||||||
b.mu.Unlock()
|
|
||||||
handle(buscompose.AccountIssuedSubject, []byte(`{"user":"a.mod-x","kind":"module","node":"a","module":"mod-x","generation":1}`))
|
|
||||||
waitFor(t, held)
|
|
||||||
if !slices.Contains(grants.Consumes, grants.EventAccountIssued) || !grants.IsAccountIssued(grants.EventAccountIssued) ||
|
|
||||||
grants.IsAccountIssued("mesh.mod.mesh-vault.event.secret-changed") {
|
|
||||||
t.Errorf("the part hears %v", grants.Consumes)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 774/AC6 (novox/hq ADR 0561 §6): the durable consumers an account needs, with the controller's configuration, are
|
|
||||||
// compared in shadow while the controller holds the lease, and created, only where the bus lacks them, from the write
|
|
||||||
// that hands the lease away; one the bus holds is never changed.
|
|
||||||
t.Run("account consumers: compared while the controller holds the lease, created once it is handed away", func(t *testing.T) {
|
|
||||||
const cManifest = `{"module":"mod-c","version":"1","own-secrets":{"broker":"/var/lib/c/broker"},"consumes":["mod-y.done"]}`
|
|
||||||
b := fixture()
|
|
||||||
b.manifestOf("mod-c", cManifest)
|
|
||||||
b.assign("b", "mod-c")
|
|
||||||
b.loaded = shadowOf(t, b)
|
|
||||||
b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"instance": "novox/1/2026-10-27", "epoch": 57})
|
|
||||||
p := partOn(b)
|
|
||||||
p.Consumers = b
|
|
||||||
res := p.Compose()
|
|
||||||
if res.Consumers == nil || res.Consumers.On || !slices.Contains(res.Consumers.Missing, "EVENTS.b_mod-c") || len(b.consumerCreates) != 0 {
|
|
||||||
t.Fatalf("while the controller holds the lease: %+v, created %v", res.Consumers, b.consumerCreates)
|
|
||||||
}
|
|
||||||
b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"handed-to": "the operator's hand-over"})
|
|
||||||
b.consumers = map[string][]map[string]any{"EVENTS": {{"name": "b_other", "filters": []string{"x"}, "max_deliver": 5}}}
|
|
||||||
res = p.Compose()
|
|
||||||
if !res.Consumers.On || !slices.Equal(res.Consumers.Created, []string{"EVENTS.b_mod-c"}) {
|
|
||||||
t.Fatalf("once handed away: %+v", res.Consumers)
|
|
||||||
}
|
|
||||||
var made map[string]any
|
|
||||||
for _, c := range b.consumers["EVENTS"] {
|
|
||||||
if c["name"] == "b_mod-c" {
|
|
||||||
made = c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if made == nil || made["max_deliver"] != 5 || !slices.Contains(made["filters"].([]string), "mesh.mod.mod-y.event.done") {
|
|
||||||
t.Errorf("b_mod-c was made as %v, not as the controller makes it", made)
|
|
||||||
}
|
|
||||||
res = p.Compose()
|
|
||||||
if len(res.Consumers.Created) != 0 || len(res.Consumers.Missing) != 0 || len(b.consumerCreates) != 1 {
|
|
||||||
t.Errorf("a consumer the bus holds was asked again: %+v, %v", res.Consumers, b.consumerCreates)
|
|
||||||
}
|
|
||||||
// A held consumer that differs is never changed by a pass: it is raised as a condition naming it, and brought to
|
|
||||||
// the composed configuration only by hand (grants_consumer_match), never while the controller holds the lease.
|
|
||||||
var said []asks.Condition
|
|
||||||
p.Say = func(c asks.Condition) error { said = append(said, c); return nil }
|
|
||||||
made["max_deliver"] = 9
|
|
||||||
res = p.Compose()
|
|
||||||
if len(res.Consumers.Differ) != 1 || !strings.Contains(res.Consumers.Differ[0], "EVENTS.b_mod-c: max deliver 9") ||
|
|
||||||
len(said) != 1 || said[0].Key != grants.ConsumerDiffersCondition || said[0].State != asks.StateRaised ||
|
|
||||||
!strings.Contains(said[0].Explanation, "EVENTS.b_mod-c") {
|
|
||||||
t.Fatalf("a differing consumer: %+v, said %+v", res.Consumers, said)
|
|
||||||
}
|
|
||||||
b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"instance": "novox/2/2026-10-27", "epoch": 58})
|
|
||||||
if _, err := p.MatchConsumer("EVENTS", "b_mod-c"); err == nil || !strings.Contains(err.Error(), "controller holds the lease") {
|
|
||||||
t.Errorf("matched by hand under the controller's lease: %v", err)
|
|
||||||
}
|
|
||||||
b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"handed-to": "the operator's hand-over"})
|
|
||||||
if _, err := p.MatchConsumer("EVENTS", "b_nothing"); err == nil {
|
|
||||||
t.Error("a consumer the records do not compose was asked to match")
|
|
||||||
}
|
|
||||||
if _, err := p.MatchConsumer("EVENTS", "b_mod-c"); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
res = p.Compose()
|
|
||||||
if !slices.Contains(b.consumerCreates, "match EVENTS.b_mod-c") || made["max_deliver"] != 5 || len(res.Consumers.Matched) != 1 {
|
|
||||||
t.Errorf("the ask to match: %+v, %v", res.Consumers, b.consumerCreates)
|
|
||||||
}
|
|
||||||
res = p.Compose()
|
|
||||||
if len(res.Consumers.Differ) != 0 || said[len(said)-1].State != asks.StateCleared {
|
|
||||||
t.Errorf("once matched: %+v, said %+v", res.Consumers, said)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 774/AC13 (novox/hq ADR 0561 §7): after take-over a list in which more than one loaded user's hash changes, with no
|
|
||||||
// rotation by the vault naming each, is held: the list in force stays, user-list-held is raised naming those users. One
|
|
||||||
// user's change, or changes the vault's rotation names, are written.
|
|
||||||
t.Run("a list changing several loaded users' hashes is held unless the vault's rotation names each", func(t *testing.T) {
|
|
||||||
b := fixture()
|
|
||||||
b.loaded = shadowOf(t, b)
|
|
||||||
hash := func(user, h string, generation int, rotating bool) {
|
|
||||||
v := map[string]any{"user": user, "hash": h, "generation": generation, "fingerprint": fmt.Sprintf("sha256:%d", generation)}
|
|
||||||
if rotating {
|
|
||||||
v["rotating"] = map[string]any{"login": user + "--b", "generation": generation, "revision": 7}
|
|
||||||
}
|
|
||||||
b.put(grants.ReadAccounts, user, v)
|
|
||||||
}
|
|
||||||
for _, u := range shadowOf(t, b) {
|
|
||||||
hash(u.User, "$2a$10$first", 1, false)
|
|
||||||
}
|
|
||||||
busKey, err := seal.GenerateKeyPair()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK,
|
|
||||||
UserList: &delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Digest: "sha256:aa", Users: 6, Key: busKey.PublicKey}})
|
|
||||||
p := partOn(b)
|
|
||||||
p.Objects = &fakeObjects{}
|
|
||||||
var said []asks.Condition
|
|
||||||
p.Say = func(c asks.Condition) error { said = append(said, c); return nil }
|
|
||||||
sw, _ := json.Marshal(grants.Switch{V: 1, State: grants.TookOver, Acts: []grants.Act{}})
|
|
||||||
if _, err := (ownStore{b}).Put(grants.StateTakeover, grants.KeySwitch, sw); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
p.Compose()
|
|
||||||
inForce, _, _ := p.Sealed()
|
|
||||||
if inForce.Digest == "" {
|
|
||||||
t.Fatal("no list written after take-over")
|
|
||||||
}
|
|
||||||
// One user's hash changes: written.
|
|
||||||
hash("a.mod-x", "$2a$10$second", 2, false)
|
|
||||||
p.Compose()
|
|
||||||
if now, _, _ := p.Sealed(); now.Digest == inForce.Digest {
|
|
||||||
t.Fatal("a list changing one loaded user's hash was not written")
|
|
||||||
}
|
|
||||||
inForce, _, _ = p.Sealed()
|
|
||||||
// Two change at once, no rotation names them: held, said naming both.
|
|
||||||
hash("a.mod-x", "$2a$10$third", 3, false)
|
|
||||||
hash("a.nats", "$2a$10$third", 2, false)
|
|
||||||
said = nil
|
|
||||||
res := p.Compose()
|
|
||||||
if now, _, _ := p.Sealed(); now.Digest != inForce.Digest || res.Sealed == nil || !strings.Contains(res.Sealed.Why, "a.mod-x, a.nats") {
|
|
||||||
t.Fatalf("a list changing two loaded users' hashes: %+v", res.Sealed)
|
|
||||||
}
|
|
||||||
if len(said) != 1 || said[0].Key != grants.HeldCondition || said[0].State != asks.StateRaised || !strings.Contains(said[0].Summary, "a.mod-x, a.nats") {
|
|
||||||
t.Errorf("the hold was said as %+v", said)
|
|
||||||
}
|
|
||||||
// The hold is its own guard, beside take-over's refusals (ADR 0562): the switch stays taken over.
|
|
||||||
if p.SwitchState() != grants.TookOver {
|
|
||||||
t.Errorf("the hold moved the switch to %s", p.SwitchState())
|
|
||||||
}
|
|
||||||
// A person accepts exactly the named users' changes of the held list, with why: refused for another set, with no
|
|
||||||
// why, or once the list changed since; accepted, the next pass writes it once, said as a hand act.
|
|
||||||
if _, err := p.AcceptHashes("person.jochen", []string{"a.mod-x"}, "rotated by hand"); err == nil || !strings.Contains(err.Error(), "a.mod-x, a.nats") {
|
|
||||||
t.Errorf("an acceptance of another set than the held one: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := p.AcceptHashes("person.jochen", []string{"a.mod-x", "a.nats"}, ""); err == nil {
|
|
||||||
t.Error("an acceptance with no why")
|
|
||||||
}
|
|
||||||
if _, err := p.AcceptHashes("person.jochen", []string{"a.nats", "a.mod-x"}, "the vault's store was restored"); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
// The list changed since the acceptance (a.mod-x's hash again): the acceptance is bound to the held list, refused.
|
|
||||||
hash("a.mod-x", "$2a$10$fourth", 4, false)
|
|
||||||
res = p.Compose()
|
|
||||||
if now, _, _ := p.Sealed(); now.Digest != inForce.Digest || res.Sealed == nil || !strings.Contains(res.Sealed.Why, "held") {
|
|
||||||
t.Fatalf("an acceptance of a list that changed since released it: %+v", res.Sealed)
|
|
||||||
}
|
|
||||||
if _, err := p.AcceptHashes("person.jochen", []string{"a.mod-x", "a.nats"}, "the vault's store was restored"); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
b.events = nil
|
|
||||||
p.Compose()
|
|
||||||
if now, _, _ := p.Sealed(); now.Digest == inForce.Digest || said[len(said)-1].State != asks.StateCleared ||
|
|
||||||
!slices.Contains(b.events, grants.EventHashesAccepted) {
|
|
||||||
t.Fatalf("an accepted hold was not released once and said: %+v, events %v", said, b.events)
|
|
||||||
}
|
|
||||||
inForce, _, _ = p.Sealed()
|
|
||||||
// The vault's rotation names each: written, the warning cleared.
|
|
||||||
hash("a.mod-x", "$2a$10$fifth", 5, true)
|
|
||||||
hash("a.nats", "$2a$10$fifth", 3, true)
|
|
||||||
p.Compose()
|
|
||||||
if now, _, _ := p.Sealed(); now.Digest == inForce.Digest || said[len(said)-1].State != asks.StateCleared {
|
|
||||||
t.Errorf("changes the vault's rotation names were held: %+v", said)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type fakeAsker struct {
|
type fakeAsker struct {
|
||||||
|
|||||||
@@ -1,217 +0,0 @@
|
|||||||
package grants
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"slices"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/announce"
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
||||||
)
|
|
||||||
|
|
||||||
// **The accounts the mesh wants, said to the vault** (novox/hq ADR 0561 §2, issue 774). The bus composer derives every
|
|
||||||
// user from placement, the manifests and the wants; for each account the vault mints (a module's, a process's, a
|
|
||||||
// machine's provisioner client's), the part writes one key of its state `accounts`, keyed by the bus user, with the
|
|
||||||
// fields the controller's credential carries beside the password. A key is written only when it differs, and deleted
|
|
||||||
// only when placement no longer names its user and every input of the pass was read: a refused read never retires an
|
|
||||||
// account. The vault alone reads the state (buscompose.NamedReaders); it mints each user it holds no value for.
|
|
||||||
|
|
||||||
// StateAccounts is the part's state of wanted accounts.
|
|
||||||
const StateAccounts = buscompose.AccountsState
|
|
||||||
|
|
||||||
// AccountsReaders are the only modules that may read StateAccounts.
|
|
||||||
var AccountsReaders = buscompose.NamedReaders[buscompose.GrantsAccounts]
|
|
||||||
|
|
||||||
// Announced is what the part says it keeps: every state readable by any reader, but the wanted accounts, by the vault
|
|
||||||
// alone (ADR 0495 §2-§3, ADR 0561 §2).
|
|
||||||
func Announced() []announce.State {
|
|
||||||
var out []announce.State
|
|
||||||
for _, s := range States {
|
|
||||||
if s == StateAccounts {
|
|
||||||
out = append(out, announce.State{Name: s, Readers: slices.Clone(AccountsReaders)})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out = append(out, announce.State{Name: s, Readable: true})
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// BusAddress is where every node on the private network reaches the bus, and its certificate's fingerprint: the part's
|
|
||||||
// own credential says both, since the controller gave every node's account the bus holder's overlay name (its
|
|
||||||
// brokerReachableAt).
|
|
||||||
type BusAddress struct {
|
|
||||||
URL string
|
|
||||||
Fingerprint string
|
|
||||||
}
|
|
||||||
|
|
||||||
// WantedAccounts is every account the vault mints that the records compose, keyed by user, with the bus's address. A
|
|
||||||
// node, the controller, an enrolment user or a person is never one (ADR 0561 §4, §5). Pure.
|
|
||||||
func WantedAccounts(in Inputs, recs buscompose.Records, bus BusAddress) (map[string]buscompose.WantedAccount, error) {
|
|
||||||
principals, err := buscompose.Users(recs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out := map[string]buscompose.WantedAccount{}
|
|
||||||
for _, p := range principals {
|
|
||||||
w := buscompose.WantedAccount{User: p.Username(), Kind: p.Kind, Node: p.Node, Module: p.Module, Process: p.Process,
|
|
||||||
URL: bus.URL, Fingerprint: bus.Fingerprint}
|
|
||||||
switch p.Kind {
|
|
||||||
case buscompose.KindModule, buscompose.KindNodeTools:
|
|
||||||
if m, ok := manifestOn(in, p.Node, p.Module); ok {
|
|
||||||
w.Claims = ClaimsOf(m)
|
|
||||||
}
|
|
||||||
case buscompose.KindProcess, buscompose.KindProvisioner:
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out[w.User] = w
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// manifestOn is the manifest a node runs of module: the build its want names, else the trunk's.
|
|
||||||
func manifestOn(in Inputs, node, module string) (catalogue.Manifest, bool) {
|
|
||||||
if m, ok := in.Running[node+"."+module]; ok {
|
|
||||||
return m, true
|
|
||||||
}
|
|
||||||
m, ok := in.Manifests[module]
|
|
||||||
return m, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClaimsOf is a module's claims as its credential carries them: the controller's claimsFor, over the mesh's own seats
|
|
||||||
// (its seat table is seeded from them): each claim's seat and scope, and the verbs the runtime serves for it, the claim's
|
|
||||||
// own or every verb the seat promises; a seat the mesh does not define keeps the claim's scope and no verbs.
|
|
||||||
func ClaimsOf(m catalogue.Manifest) []buscompose.ClaimedSeat {
|
|
||||||
if len(m.Claims) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
byName := map[string]catalogue.Seat{}
|
|
||||||
for _, s := range catalogue.DefaultSeats() {
|
|
||||||
byName[s.Name] = s
|
|
||||||
}
|
|
||||||
var out []buscompose.ClaimedSeat
|
|
||||||
for _, c := range m.Claims {
|
|
||||||
claimed := buscompose.ClaimedSeat{Seat: c.Name, Scope: c.At()}
|
|
||||||
if s, known := byName[c.Name]; known {
|
|
||||||
claimed.Scope = s.Scope
|
|
||||||
claimed.Serves = c.ServesFor(catalogue.Manifest{Tools: catalogue.VerbNames(s.Serves)})
|
|
||||||
}
|
|
||||||
out = append(out, claimed)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accounts answers the tool accounts (ADR 0561 §9): the wanted accounts as the part last wrote them, one user's or all,
|
|
||||||
// each with when it was written; and what the last pass did to them. Never a password: the state holds none.
|
|
||||||
func (p *Part) Accounts(user string) (any, error) {
|
|
||||||
users := []string{user}
|
|
||||||
if user == "" {
|
|
||||||
keys, err := p.Store.Keys(StateAccounts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("its own %s cannot be listed: %w", StateAccounts, err)
|
|
||||||
}
|
|
||||||
users = keys
|
|
||||||
}
|
|
||||||
out := []buscompose.WantedAccount{}
|
|
||||||
for _, u := range users {
|
|
||||||
raw, _, found, err := p.Store.Get(StateAccounts, u)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("its own %s %s cannot be read: %w", StateAccounts, u, err)
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
if user != "" {
|
|
||||||
return nil, fmt.Errorf("no account %s is wanted: placement composes no module, process or provisioner client of that user", user)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
w, err := buscompose.ReadWantedAccount(u, raw)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out = append(out, w)
|
|
||||||
}
|
|
||||||
answer := map[string]any{"accounts": out, "count": len(out), "read_by": AccountsReaders}
|
|
||||||
if raw, _, found, err := p.Store.Get(StateUserList, KeyPass); err == nil && found {
|
|
||||||
var res Result
|
|
||||||
if json.Unmarshal(raw, &res) == nil && res.Accounts != nil {
|
|
||||||
answer["last_pass"] = res.Accounts
|
|
||||||
}
|
|
||||||
// And the durable consumers the accounts need, as the last pass found them: missing and differing ones are read
|
|
||||||
// here in shadow too, where no condition is raised (ADR 0561 §6, §9).
|
|
||||||
if res.Consumers != nil {
|
|
||||||
answer["consumers"] = res.Consumers
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return answer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccountsWritten is what one pass did to the state of wanted accounts.
|
|
||||||
type AccountsWritten struct {
|
|
||||||
Wanted int `json:"wanted"`
|
|
||||||
Written []string `json:"written,omitempty"`
|
|
||||||
Deleted []string `json:"deleted,omitempty"`
|
|
||||||
// Kept are keys placement no longer names that this pass did not delete, with why.
|
|
||||||
Kept []string `json:"kept,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeAccounts writes each wanted account whose key differs, and deletes the key of a user no longer placed, but only
|
|
||||||
// when every input of the pass was read.
|
|
||||||
func (p *Part) writeAccounts(res *Result, in Inputs, recs buscompose.Records) {
|
|
||||||
if p.Bus.URL == "" {
|
|
||||||
res.Errors = append(res.Errors, "the wanted accounts are not written: the part knows no bus address to give them (its own credential names none)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
wanted, err := WantedAccounts(in, recs, p.Bus)
|
|
||||||
if err != nil {
|
|
||||||
res.Errors = append(res.Errors, "the wanted accounts could not be composed: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
done := &AccountsWritten{Wanted: len(wanted)}
|
|
||||||
res.Accounts = done
|
|
||||||
for _, user := range sortedKeys(wanted) {
|
|
||||||
w := wanted[user]
|
|
||||||
if raw, _, found, err := p.Store.Get(StateAccounts, user); err != nil {
|
|
||||||
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be read: %v", StateAccounts, user, err))
|
|
||||||
continue
|
|
||||||
} else if found {
|
|
||||||
if was, err := buscompose.ReadWantedAccount(user, raw); err == nil && was.Same(w) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w.At = p.now()
|
|
||||||
data, _ := json.Marshal(w)
|
|
||||||
if _, err := p.Store.Put(StateAccounts, user, data); err != nil {
|
|
||||||
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", StateAccounts, user, err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
done.Written = append(done.Written, user)
|
|
||||||
}
|
|
||||||
held, err := p.Store.Keys(StateAccounts)
|
|
||||||
if err != nil {
|
|
||||||
res.Errors = append(res.Errors, fmt.Sprintf("its own %s cannot be listed, so no key of a user gone is deleted: %v", StateAccounts, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
failed := in.ReadFailures()
|
|
||||||
placed := Placed(in)
|
|
||||||
for _, user := range held {
|
|
||||||
if _, still := wanted[user]; still {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch {
|
|
||||||
case placed(user):
|
|
||||||
done.Kept = append(done.Kept, user+": placement still names it (composed with no account this pass)")
|
|
||||||
continue
|
|
||||||
case len(failed) > 0:
|
|
||||||
done.Kept = append(done.Kept, user+": a read of this pass failed, so no account is retired")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := p.Store.Delete(StateAccounts, user); err != nil {
|
|
||||||
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be deleted: %v", StateAccounts, user, err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
done.Deleted = append(done.Deleted, user)
|
|
||||||
}
|
|
||||||
sort.Strings(done.Kept)
|
|
||||||
}
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
package grants
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"slices"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/nats-io/nats.go/jetstream"
|
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
|
||||||
)
|
|
||||||
|
|
||||||
// **The durable consumers an account needs** (novox/hq ADR 0561 §6, issue 774). The controller creates a module's
|
|
||||||
// consumer on EVENTS and each process's on its work queue when it issues the account. From the write that hands its
|
|
||||||
// lease away the grants part creates them, with the configuration the bus composer derives (buscompose.AccountConsumers),
|
|
||||||
// only where the bus lacks one; one the bus holds is never changed or deleted, a difference is only said. Before that
|
|
||||||
// write they are compared in shadow alone, so two parts never create one consumer.
|
|
||||||
|
|
||||||
// ReadLease is the controller's lease as the part reads it (buscompose.NamedReaders names the vault and the part), and
|
|
||||||
// LeaseKey its one key.
|
|
||||||
const (
|
|
||||||
ReadLease = buscompose.ControllerLease
|
|
||||||
LeaseKey = buscompose.ControllerLeaseKey
|
|
||||||
)
|
|
||||||
|
|
||||||
// ConsumerCreator creates one durable consumer the bus does not hold: the part's process alone, whose account is granted
|
|
||||||
// the create of each account's consumer by stream and name. One the bus holds answers ErrHeld.
|
|
||||||
type ConsumerCreator interface {
|
|
||||||
CreateConsumer(stream string, cfg jetstream.ConsumerConfig) error
|
|
||||||
// MatchConsumer brings a held consumer to cfg, keeping where it delivers from: only on a person's ask
|
|
||||||
// (grants_consumer_match), never by a pass.
|
|
||||||
MatchConsumer(stream string, cfg jetstream.ConsumerConfig) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConsumersState is what one pass found and did of the account consumers.
|
|
||||||
type ConsumersState struct {
|
|
||||||
// On says the lease is handed away, so the part creates what the bus lacks; Why says why it is on or off.
|
|
||||||
On bool `json:"on"`
|
|
||||||
Why string `json:"why"`
|
|
||||||
// Wanted is how many the records compose; Missing those the bus lacks (`<stream>.<name>`), Differ those it holds
|
|
||||||
// otherwise, each with how.
|
|
||||||
Wanted int `json:"wanted"`
|
|
||||||
Missing []string `json:"missing,omitempty"`
|
|
||||||
Differ []string `json:"differ,omitempty"`
|
|
||||||
Created []string `json:"created,omitempty"`
|
|
||||||
// Matched is a consumer brought to the composed configuration on a person's ask, or why not.
|
|
||||||
Matched []string `json:"matched,omitempty"`
|
|
||||||
NotCreated []string `json:"not_created,omitempty"`
|
|
||||||
// Unread is a stream whose consumers could not be read: nothing on it is compared or created.
|
|
||||||
Unread []string `json:"unread,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// heldConsumer is one consumer as the bus module's nats_streams reads it for one stream.
|
|
||||||
type heldConsumer struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Filters []string `json:"filters"`
|
|
||||||
MaxDeliver int `json:"max_deliver"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// heldConsumers reads the consumers the bus holds on stream, by name.
|
|
||||||
func heldConsumers(c Caller, stream string) (map[string]heldConsumer, error) {
|
|
||||||
raw, err := c.Call(CallStreams, map[string]any{"stream": stream})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s %s: %w", CallStreams, stream, err)
|
|
||||||
}
|
|
||||||
var got struct {
|
|
||||||
Detail []struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Consumers []heldConsumer `json:"consumer_detail"`
|
|
||||||
} `json:"detail"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(unwrap(raw), &got); err != nil {
|
|
||||||
return nil, fmt.Errorf("%s %s answered no consumers: %w", CallStreams, stream, err)
|
|
||||||
}
|
|
||||||
out := map[string]heldConsumer{}
|
|
||||||
for _, d := range got.Detail {
|
|
||||||
if d.Name != stream {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, c := range d.Consumers {
|
|
||||||
out[c.Name] = c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// leaseHandedAway reads the lease as the part's reads give it.
|
|
||||||
func leaseHandedAway(r Reader) (bool, string) {
|
|
||||||
raw, found, err := r.Get(ReadLease, LeaseKey)
|
|
||||||
return buscompose.LeaseHandedAway(raw, found, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// consumers compares the account consumers the records compose with those the bus holds, and creates the missing ones
|
|
||||||
// once the lease is handed away and the part holds a creator.
|
|
||||||
func (p *Part) consumers(res *Result, recs buscompose.Records, match string) {
|
|
||||||
users, err := buscompose.Users(recs)
|
|
||||||
if err != nil {
|
|
||||||
res.Errors = append(res.Errors, "the account consumers could not be composed: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
st := &ConsumersState{}
|
|
||||||
st.On, st.Why = leaseHandedAway(p.Read)
|
|
||||||
res.Consumers = st
|
|
||||||
wanted := buscompose.AccountConsumers(users)
|
|
||||||
st.Wanted = len(wanted)
|
|
||||||
byStream := map[string][]buscompose.Consumer{}
|
|
||||||
for _, mc := range wanted {
|
|
||||||
byStream[mc.Consumer.Stream] = append(byStream[mc.Consumer.Stream], mc.Consumer)
|
|
||||||
}
|
|
||||||
for _, stream := range sortedKeys(byStream) {
|
|
||||||
held, err := heldConsumers(p.Call, stream)
|
|
||||||
if err != nil {
|
|
||||||
st.Unread = append(st.Unread, stream+": "+err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, c := range byStream[stream] {
|
|
||||||
key := stream + "." + c.Name
|
|
||||||
h, ok := held[c.Name]
|
|
||||||
if ok {
|
|
||||||
why := consumerDiffers(c, h)
|
|
||||||
if why != "" && key == match && st.On && p.Consumers != nil {
|
|
||||||
if err := p.Consumers.MatchConsumer(stream, buscompose.ConsumerConfig(c)); err != nil {
|
|
||||||
st.Matched = append(st.Matched, key+": not matched: "+err.Error())
|
|
||||||
} else {
|
|
||||||
st.Matched = append(st.Matched, key+": matched, asked by hand")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if why != "" {
|
|
||||||
st.Differ = append(st.Differ, key+": "+why)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !st.On || p.Consumers == nil {
|
|
||||||
st.Missing = append(st.Missing, key)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch err := p.Consumers.CreateConsumer(stream, buscompose.ConsumerConfig(c)); {
|
|
||||||
case err == nil:
|
|
||||||
st.Created = append(st.Created, key)
|
|
||||||
p.say(res, EventAsserted, map[string]any{"consumer": c.Name, "stream": stream})
|
|
||||||
case errors.Is(err, ErrHeld):
|
|
||||||
default:
|
|
||||||
st.Missing = append(st.Missing, key)
|
|
||||||
st.NotCreated = append(st.NotCreated, key+": "+err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p.sayDiffer(st)
|
|
||||||
}
|
|
||||||
|
|
||||||
// sayDiffer raises the warning condition mesh-grants.consumer-differs while an account consumer the part owns (from the
|
|
||||||
// lease write) differs from its composed configuration, naming each with the fields that differ, and clears it when they
|
|
||||||
// match. Said once per set of differences, again when it changes. Before the lease write the consumers are the
|
|
||||||
// controller's, and a difference is only said in the pass.
|
|
||||||
func (p *Part) sayDiffer(st *ConsumersState) {
|
|
||||||
if p.Say == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
why := ""
|
|
||||||
if st.On && len(st.Differ) > 0 {
|
|
||||||
why = strings.Join(st.Differ, "; ")
|
|
||||||
}
|
|
||||||
p.holdMu.Lock()
|
|
||||||
defer p.holdMu.Unlock()
|
|
||||||
if p.differKnown && why == p.differSaid {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c := asks.Condition{Key: ConsumerDiffersCondition, Subject: asks.Scope{Scope: "module", ID: "mesh-grants"}}
|
|
||||||
if why == "" {
|
|
||||||
c.State = asks.StateCleared
|
|
||||||
} else {
|
|
||||||
c.Severity, c.State = asks.SeverityWarning, asks.StateRaised
|
|
||||||
c.Summary = fmt.Sprintf("%d durable consumers differ from their composed configuration", len(st.Differ))
|
|
||||||
c.Headline, c.Needs = "A module's consumer is not as composed", "a look, and grants_consumer_match for each"
|
|
||||||
c.Explanation = asks.NeedsYou + "a look. The bus holds these consumers otherwise than the bus composer composes them, " +
|
|
||||||
"and the grants part never changes a held one by itself (its delivery position would be lost): " + why
|
|
||||||
c.Resolved = "every account consumer matches, by grants_consumer_match or otherwise"
|
|
||||||
}
|
|
||||||
if p.Say(c) == nil {
|
|
||||||
p.differSaid, p.differKnown = why, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConsumerDiffersCondition is the key of the warning raised while an account consumer differs (ADR 0561 §6).
|
|
||||||
const ConsumerDiffersCondition = "mesh-grants.consumer-differs"
|
|
||||||
|
|
||||||
// MatchConsumer is the tool consumer-match (ADR 0561 §6, §9): it asks the part's process to bring one account consumer
|
|
||||||
// the records compose to its composed configuration, keeping where it delivers from. Refused while the controller holds
|
|
||||||
// the lease (the consumers are the controller's then), and for a consumer the records do not compose.
|
|
||||||
func (p *Part) MatchConsumer(stream, name string) (any, error) {
|
|
||||||
if stream == "" || name == "" {
|
|
||||||
return nil, errors.New("consumer-match names a stream and a consumer")
|
|
||||||
}
|
|
||||||
if on, why := leaseHandedAway(p.Read); !on {
|
|
||||||
return nil, fmt.Errorf("not asked: %s; the controller makes and matches the consumers while it holds the lease", why)
|
|
||||||
}
|
|
||||||
in := ReadInputs(p.Read, &p.cache)
|
|
||||||
recs, _ := Records(in)
|
|
||||||
users, err := buscompose.Users(recs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
found := false
|
|
||||||
for _, mc := range buscompose.AccountConsumers(users) {
|
|
||||||
if mc.Consumer.Stream == stream && mc.Consumer.Name == name {
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
return nil, fmt.Errorf("the records compose no account consumer %s on %s: the part brings only those to their configuration", name, stream)
|
|
||||||
}
|
|
||||||
ask, _ := json.Marshal(Ask{By: "grants_consumer_match", At: p.now(), Consumer: stream + "." + name})
|
|
||||||
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, "says": "the part's process brings " + stream + "." + name + " to its composed " +
|
|
||||||
"configuration at this ask, keeping where it delivers from; the last pass's consumers.matched says what became of it"}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// consumerDiffers says how a held consumer differs from the composed one in what nats_streams reads back, or "".
|
|
||||||
func consumerDiffers(c buscompose.Consumer, h heldConsumer) string {
|
|
||||||
var why []string
|
|
||||||
want, have := slices.Sorted(slices.Values(c.Filters)), slices.Sorted(slices.Values(h.Filters))
|
|
||||||
if !slices.Equal(want, have) {
|
|
||||||
why = append(why, fmt.Sprintf("filters %v, composed %v", have, want))
|
|
||||||
}
|
|
||||||
maxDeliver := c.MaxDeliver
|
|
||||||
if maxDeliver == 0 {
|
|
||||||
maxDeliver = -1
|
|
||||||
}
|
|
||||||
if h.MaxDeliver != maxDeliver {
|
|
||||||
why = append(why, fmt.Sprintf("max deliver %d, composed %d", h.MaxDeliver, maxDeliver))
|
|
||||||
}
|
|
||||||
sort.Strings(why)
|
|
||||||
return strings.Join(why, "; ")
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,7 +18,7 @@ type Watcher interface {
|
|||||||
// Followed are the states whose change composes again: placement, the indexer's manifests by module, the resolved
|
// Followed are the states whose change composes again: placement, the indexer's manifests by module, the resolved
|
||||||
// config values, the node agents' haves (a machine's first have, and the bus node's loaded list) and the assigner's
|
// config values, the node agents' haves (a machine's first have, and the bus node's loaded list) and the assigner's
|
||||||
// wants (the manifest each machine is wanted to run), and the vault's hashes of the bus accounts.
|
// wants (the manifest each machine is wanted to run), and the vault's hashes of the bus accounts.
|
||||||
var Followed = []string{ReadAssignments, ReadModules, ReadResolved, ReadHave, ReadWants, ReadAccounts, ReadPlacement, ReadLease}
|
var Followed = []string{ReadAssignments, ReadModules, ReadResolved, ReadHave, ReadWants, ReadAccounts, ReadPlacement}
|
||||||
|
|
||||||
// Follower composes again on every change it hears, never on a timer. It never stops for a refusal: a watch it cannot
|
// Follower composes again on every change it hears, never on a timer. It never stops for a refusal: a watch it cannot
|
||||||
// open is said, composed without, and opened again at the next wake (any change it does hear, or a reconnect).
|
// open is said, composed without, and opened again at the next wake (any change it does hear, or a reconnect).
|
||||||
@@ -29,9 +28,9 @@ type Follower struct {
|
|||||||
Logf func(string, ...any)
|
Logf func(string, ...any)
|
||||||
// Asked watches the part's own ask to compose now (a tool's compose-now), waking at each; nil watches none.
|
// Asked watches the part's own ask to compose now (a tool's compose-now), waking at each; nil watches none.
|
||||||
Asked func(wake func()) error
|
Asked func(wake func()) error
|
||||||
// Hear hears the controller seat's condition events and the vault's account-issued (Consumes) and hands each, by event
|
// Hear hears the controller seat's condition events (Consumes) and hands each, by event name, with its body, to
|
||||||
// name, with its body, to handle; nil hears none. A condition of the kind root-not-free raised or cleared wakes a pass:
|
// handle; nil hears none. A condition of the kind root-not-free raised or cleared wakes a pass: a machine's
|
||||||
// a machine's root-free word changed; and so does an account the vault minted (ADR 0561 §1), whose hash the list carries.
|
// root-free word changed.
|
||||||
Hear func(handle func(event string, body []byte)) error
|
Hear func(handle func(event string, body []byte)) error
|
||||||
|
|
||||||
wake chan struct{}
|
wake chan struct{}
|
||||||
@@ -47,8 +46,6 @@ type Follower struct {
|
|||||||
held string
|
held string
|
||||||
notAsserted string
|
notAsserted string
|
||||||
refusedMans string
|
refusedMans string
|
||||||
// leaseOn is whether the lease was handed away at its last change heard, once leaseSeen.
|
|
||||||
leaseOn, leaseSeen bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Follower) init() {
|
func (f *Follower) init() {
|
||||||
@@ -88,15 +85,6 @@ func (f *Follower) logf(format string, args ...any) {
|
|||||||
// heard says whether a change composes again: every change but a have's, and of the haves a machine's first and the
|
// heard says whether a change composes again: every change but a have's, and of the haves a machine's first and the
|
||||||
// bus module's loaded list moving.
|
// bus module's loaded list moving.
|
||||||
func (f *Follower) heard(state, key string, value []byte) bool {
|
func (f *Follower) heard(state, key string, value []byte) bool {
|
||||||
if state == ReadLease {
|
|
||||||
// Renewed every few seconds by its holder: only the write that hands it away, or back, composes again (ADR 0561 §6).
|
|
||||||
on, _ := buscompose.LeaseHandedAway(value, len(value) > 0, nil)
|
|
||||||
f.mu.Lock()
|
|
||||||
defer f.mu.Unlock()
|
|
||||||
was, seen := f.leaseOn, f.leaseSeen
|
|
||||||
f.leaseOn, f.leaseSeen = on, true
|
|
||||||
return !seen || was != on
|
|
||||||
}
|
|
||||||
if state != ReadHave {
|
if state != ReadHave {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -136,7 +124,7 @@ func (f *Follower) open() {
|
|||||||
}
|
}
|
||||||
if f.Hear != nil && !f.heard_ {
|
if f.Hear != nil && !f.heard_ {
|
||||||
err := f.Hear(func(event string, body []byte) {
|
err := f.Hear(func(event string, body []byte) {
|
||||||
if IsAccountIssued(event) || IsRootNotFree(body) {
|
if IsRootNotFree(body) {
|
||||||
f.Wake()
|
f.Wake()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const (
|
|||||||
|
|
||||||
// Reads is every state the part reads, in the manifest's order.
|
// Reads is every state the part reads, in the manifest's order.
|
||||||
var Reads = []string{ReadAssignments, ReadControllerGrant, ReadModules, ReadManifests, ReadResolved, ReadHave, ReadWants, ReadBuilds,
|
var Reads = []string{ReadAssignments, ReadControllerGrant, ReadModules, ReadManifests, ReadResolved, ReadHave, ReadWants, ReadBuilds,
|
||||||
ReadAccounts, ReadPlacement, ReadLease}
|
ReadAccounts, ReadPlacement}
|
||||||
|
|
||||||
// The bus module's tools the part calls, as its manifest's invokes name them: read-only, each.
|
// The bus module's tools the part calls, as its manifest's invokes name them: read-only, each.
|
||||||
const (
|
const (
|
||||||
@@ -139,22 +139,6 @@ type HeldHash struct {
|
|||||||
Generation int `json:"generation,omitempty"`
|
Generation int `json:"generation,omitempty"`
|
||||||
At time.Time `json:"at"`
|
At time.Time `json:"at"`
|
||||||
Adopted bool `json:"adopted,omitempty"`
|
Adopted bool `json:"adopted,omitempty"`
|
||||||
// Fingerprint is the vault's fingerprint of the value the hash is of: with the generation, what says the password
|
|
||||||
// changed, never the hash itself (a hash of the same password differs by its salt).
|
|
||||||
Fingerprint string `json:"fingerprint,omitempty"`
|
|
||||||
// Rotating is the vault's rotation of the account under way: a change of its hash it announces (ADR 0561 §4, §7).
|
|
||||||
Rotating *struct {
|
|
||||||
Login string `json:"login"`
|
|
||||||
} `json:"rotating,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// credentialOf is what says which password a held hash is of, never the hash: adopted, or the vault's generation and the
|
|
||||||
// fingerprint of its value.
|
|
||||||
func credentialOf(h HeldHash) string {
|
|
||||||
if h.Adopted {
|
|
||||||
return "adopted"
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("generation %d %s", h.Generation, h.Fingerprint)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// readHashes reads the vault's bus-accounts; a refusal is a gap, and every user then has no hash held.
|
// readHashes reads the vault's bus-accounts; a refusal is a gap, and every user then has no hash held.
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// States is every state the part keeps.
|
// States is every state the part keeps.
|
||||||
var States = []string{StateAccounts, StateAssertions, StateMemberships, StateTakeover, StateUserList}
|
var States = []string{StateAssertions, StateMemberships, StateTakeover, StateUserList}
|
||||||
|
|
||||||
// The events the part says.
|
// The events the part says.
|
||||||
const (
|
const (
|
||||||
@@ -42,15 +42,12 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Emits is every event the part says.
|
// Emits is every event the part says.
|
||||||
var Emits = []string{EventAsserted, EventHashesAccepted, EventMembership, EventUserList}
|
var Emits = []string{EventAsserted, EventMembership, EventUserList}
|
||||||
|
|
||||||
// Store is the part's own state, by local name: the only thing the part writes.
|
// Store is the part's own state, by local name: the only thing the part writes.
|
||||||
type Store interface {
|
type Store interface {
|
||||||
Get(state, key string) ([]byte, uint64, bool, error)
|
Get(state, key string) ([]byte, uint64, bool, error)
|
||||||
Put(state, key string, value []byte) (uint64, error)
|
Put(state, key string, value []byte) (uint64, error)
|
||||||
// Keys and Delete are for the wanted accounts alone (accounts.go): every other state is only put.
|
|
||||||
Keys(state string) ([]string, error)
|
|
||||||
Delete(state, key string) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emitter says one of the part's events.
|
// Emitter says one of the part's events.
|
||||||
@@ -80,6 +77,13 @@ type UserListState struct {
|
|||||||
|
|
||||||
func (s UserListState) inputs() string {
|
func (s UserListState) inputs() string {
|
||||||
s.At, s.Inputs = time.Time{}, ""
|
s.At, s.Inputs = time.Time{}, ""
|
||||||
|
// When the bus node said it loaded a list is no input: the same list said again, at a later time and the key's later
|
||||||
|
// revision, composes nothing new (issue 1126: written again every 3 s, 2026-10-28).
|
||||||
|
if s.Loaded != nil {
|
||||||
|
l := *s.Loaded
|
||||||
|
l.Revision, l.At = 0, time.Time{}
|
||||||
|
s.Loaded = &l
|
||||||
|
}
|
||||||
raw, _ := json.Marshal(s)
|
raw, _ := json.Marshal(s)
|
||||||
sum := sha256.Sum256(raw)
|
sum := sha256.Sum256(raw)
|
||||||
return "sha256:" + hex.EncodeToString(sum[:])
|
return "sha256:" + hex.EncodeToString(sum[:])
|
||||||
@@ -107,13 +111,9 @@ type Result struct {
|
|||||||
// free, <why>`: a verified sender is composed as from no root-free machine there (issue 1945).
|
// free, <why>`: a verified sender is composed as from no root-free machine there (issue 1945).
|
||||||
RootNotFree []string `json:"root_not_free,omitempty"`
|
RootNotFree []string `json:"root_not_free,omitempty"`
|
||||||
// Sealed is the list sealed to the bus node as the key `user-list.sealed` names it (ADR 0547 clause 7).
|
// Sealed is the list sealed to the bus node as the key `user-list.sealed` names it (ADR 0547 clause 7).
|
||||||
Sealed *SealedState `json:"sealed,omitempty"`
|
Sealed *SealedState `json:"sealed,omitempty"`
|
||||||
// Consumers is what the pass found and created of the durable consumers an account needs (ADR 0561 §6).
|
Errors []string `json:"errors,omitempty"`
|
||||||
Consumers *ConsumersState `json:"consumers,omitempty"`
|
PublishedOrAsserted string `json:"published_or_asserted"`
|
||||||
// Accounts is what the pass wrote of the wanted accounts the vault mints from (ADR 0561 §2).
|
|
||||||
Accounts *AccountsWritten `json:"accounts,omitempty"`
|
|
||||||
Errors []string `json:"errors,omitempty"`
|
|
||||||
PublishedOrAsserted string `json:"published_or_asserted"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NothingTakesEffect is what every composition says it did on the bus beyond its own state and the memberships.
|
// NothingTakesEffect is what every composition says it did on the bus beyond its own state and the memberships.
|
||||||
@@ -136,12 +136,6 @@ type Part struct {
|
|||||||
Logf func(string, ...any)
|
Logf func(string, ...any)
|
||||||
// Objects is the part's object bucket of sealed lists: the process's connection; nil in the tools.
|
// Objects is the part's object bucket of sealed lists: the process's connection; nil in the tools.
|
||||||
Objects Objects
|
Objects Objects
|
||||||
// Consumers creates an account's durable consumer from the lease write (consumers.go): the process's connection;
|
|
||||||
// nil in the tools, which compare only.
|
|
||||||
Consumers ConsumerCreator
|
|
||||||
// Bus is where the nodes reach the bus, which each wanted account carries (accounts.go): the process's own credential's;
|
|
||||||
// empty in the tools, which write no account.
|
|
||||||
Bus BusAddress
|
|
||||||
// Say says a condition to the operator: the process's; nil in the tools.
|
// Say says a condition to the operator: the process's; nil in the tools.
|
||||||
Say func(asks.Condition) error
|
Say func(asks.Condition) error
|
||||||
// Forget takes away the record the asker kept of an ask it failed to publish; Nonce is a test's id suffix.
|
// Forget takes away the record the asker kept of an ask it failed to publish; Nonce is a test's id suffix.
|
||||||
@@ -163,11 +157,6 @@ type Part struct {
|
|||||||
holdMu sync.Mutex
|
holdMu sync.Mutex
|
||||||
heldSaid string
|
heldSaid string
|
||||||
heldKnown bool
|
heldKnown bool
|
||||||
// differSaid is the account consumers' differences last said as a condition, once differKnown (consumers.go).
|
|
||||||
differSaid string
|
|
||||||
differKnown bool
|
|
||||||
// hashHold are the users whose hash changes held the pass's list, or nil (sealed.go).
|
|
||||||
hashHold []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ask is the value of KeyAsked: a tool's ask to compose now, and, from grants_assert, to create one module's missing
|
// Ask is the value of KeyAsked: a tool's ask to compose now, and, from grants_assert, to create one module's missing
|
||||||
@@ -176,21 +165,18 @@ type Ask struct {
|
|||||||
By string `json:"by"`
|
By string `json:"by"`
|
||||||
At time.Time `json:"at"`
|
At time.Time `json:"at"`
|
||||||
Assert string `json:"assert,omitempty"`
|
Assert string `json:"assert,omitempty"`
|
||||||
// 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"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// asked is a new ask, read once per ask; the zero Ask when none.
|
// askedAssert is the module a new ask names to assert by hand, read once per ask; empty when none.
|
||||||
func (p *Part) asked() Ask {
|
func (p *Part) askedAssert() string {
|
||||||
raw, rev, found, err := p.Store.Get(StateUserList, KeyAsked)
|
raw, rev, found, err := p.Store.Get(StateUserList, KeyAsked)
|
||||||
if err != nil || !found || rev == p.askSeen {
|
if err != nil || !found || rev == p.askSeen {
|
||||||
return Ask{}
|
return ""
|
||||||
}
|
}
|
||||||
p.askSeen = rev
|
p.askSeen = rev
|
||||||
var a Ask
|
var a Ask
|
||||||
_ = json.Unmarshal(raw, &a)
|
_ = json.Unmarshal(raw, &a)
|
||||||
return a
|
return a.Assert
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Part) now() time.Time {
|
func (p *Part) now() time.Time {
|
||||||
@@ -254,10 +240,6 @@ func (p *Part) Compose() Result {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// The accounts the vault mints from, said in the state only it reads (ADR 0561 §2).
|
|
||||||
if p.Store != nil {
|
|
||||||
p.writeAccounts(&res, in, recs)
|
|
||||||
}
|
|
||||||
// The list itself, sealed to the bus node and named in `user-list.sealed` (ADR 0547 clause 7).
|
// The list itself, sealed to the bus node and named in `user-list.sealed` (ADR 0547 clause 7).
|
||||||
if list, n, leftOut, err := ComposeList(recs, in.Hashes); err != nil {
|
if list, n, leftOut, err := ComposeList(recs, in.Hashes); err != nil {
|
||||||
res.Errors = append(res.Errors, "the user list could not be composed whole: "+err.Error())
|
res.Errors = append(res.Errors, "the user list could not be composed whole: "+err.Error())
|
||||||
@@ -292,11 +274,10 @@ func (p *Part) Compose() Result {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
held, heldErr := HeldStreams(p.Call)
|
held, heldErr := HeldStreams(p.Call)
|
||||||
var ask Ask
|
forced := ""
|
||||||
if p.Create != nil || p.Consumers != nil {
|
if p.Create != nil {
|
||||||
ask = p.asked()
|
forced = p.askedAssert()
|
||||||
}
|
}
|
||||||
forced := ask.Assert
|
|
||||||
for _, module := range sortedKeys(in.Manifests) {
|
for _, module := range sortedKeys(in.Manifests) {
|
||||||
if !assigned(in, module) {
|
if !assigned(in, module) {
|
||||||
continue
|
continue
|
||||||
@@ -318,7 +299,6 @@ func (p *Part) Compose() Result {
|
|||||||
res.AssertionsWritten++
|
res.AssertionsWritten++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.consumers(&res, recs, ask.Consumer)
|
|
||||||
if _, _, found, err := p.Store.Get(StateTakeover, KeySwitch); err == nil && !found {
|
if _, _, found, err := p.Store.Get(StateTakeover, KeySwitch); err == nil && !found {
|
||||||
data, _ := json.Marshal(Switch{V: 1, State: Shadow, Acts: []Act{}})
|
data, _ := json.Marshal(Switch{V: 1, State: Shadow, Acts: []Act{}})
|
||||||
if _, err := p.Store.Put(StateTakeover, KeySwitch, data); err != nil {
|
if _, err := p.Store.Put(StateTakeover, KeySwitch, data); err != nil {
|
||||||
|
|||||||
@@ -23,17 +23,8 @@ const (
|
|||||||
KindRootNotFree = "root-not-free"
|
KindRootNotFree = "root-not-free"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EventAccountIssued is the vault's word that it minted an account (novox/hq ADR 0561 §1): its hash is in the vault's
|
|
||||||
// bus-accounts, so a pass composes the list with it.
|
|
||||||
const EventAccountIssued = "mesh-vault." + buscompose.AccountIssuedEvent
|
|
||||||
|
|
||||||
// Consumes is every event the part hears, as its manifest's consumes name them.
|
// Consumes is every event the part hears, as its manifest's consumes name them.
|
||||||
var Consumes = []string{EventConditionRaised, EventConditionCleared, EventAccountIssued}
|
var Consumes = []string{EventConditionRaised, EventConditionCleared}
|
||||||
|
|
||||||
// IsAccountIssued says an event heard is the vault's account-issued, by its name or its bus subject.
|
|
||||||
func IsAccountIssued(event string) bool {
|
|
||||||
return event == EventAccountIssued || event == buscompose.AccountIssuedSubject
|
|
||||||
}
|
|
||||||
|
|
||||||
// EventSubject is the bus subject of a controller seat event named `mesh-controller.<event>`.
|
// EventSubject is the bus subject of a controller seat event named `mesh-controller.<event>`.
|
||||||
func EventSubject(event string) string {
|
func EventSubject(event string) string {
|
||||||
|
|||||||
@@ -2,12 +2,10 @@ package grants
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||||
@@ -93,12 +91,11 @@ func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut []string
|
|||||||
why = "no node runs the bus's module, so the list is sealed to nobody"
|
why = "no node runs the bus's module, so the list is sealed to nobody"
|
||||||
case key == "":
|
case key == "":
|
||||||
why = "the bus node's have of the bus module names no key yet (its node-engine says it from this release)"
|
why = "the bus node's have of the bus module names no key yet (its node-engine says it from this release)"
|
||||||
case next.Follow == delivery.FollowGrants && cur.Digest != digest && p.holdsBack(in, leftOut, bus, lost, digest) != "":
|
case next.Follow == delivery.FollowGrants && cur.Digest != digest && p.holdsBack(in, leftOut, bus, lost) != "":
|
||||||
// After take-over the list in force is never replaced by one composed past a failed read, or one that drops a user
|
// After take-over the list in force is never replaced by one composed past a failed read, or one that drops a user
|
||||||
// of it for want of a hash: that user's module would be locked out of the bus (review of the switch).
|
// of it for want of a hash: that user's module would be locked out of the bus (review of the switch).
|
||||||
why = p.holdsBack(in, leftOut, bus, lost, digest)
|
why = p.holdsBack(in, leftOut, bus, lost)
|
||||||
held = why
|
held = why
|
||||||
p.keepHashHold(res, digest)
|
|
||||||
case cur.Digest != digest || cur.SealedTo != delivery.SealingKeyID(key) || cur.Node != in.BusNode:
|
case cur.Digest != digest || cur.SealedTo != delivery.SealingKeyID(key) || cur.Node != in.BusNode:
|
||||||
object, err := delivery.SealUserList(list, key)
|
object, err := delivery.SealUserList(list, key)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -136,8 +133,7 @@ func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut []string
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if next.Digest == digest && cur.Digest != digest {
|
if next.Digest == digest && cur.Digest != digest {
|
||||||
p.keepWrittenUsers(res, list, in.Hashes)
|
p.keepWrittenUsers(res, list)
|
||||||
p.acceptedWritten(res, digest)
|
|
||||||
}
|
}
|
||||||
// The object the key named before is taken away once the key names the new one: the bucket keeps the list in force.
|
// The object the key named before is taken away once the key names the new one: the bucket keeps the list in force.
|
||||||
if had && cur.Object != "" && cur.Object != next.Object {
|
if had && cur.Object != "" && cur.Object != next.Object {
|
||||||
@@ -181,7 +177,16 @@ func (p *Part) sayHeld(why string) {
|
|||||||
const HeldCondition = "mesh-grants.user-list-held"
|
const HeldCondition = "mesh-grants.user-list-held"
|
||||||
|
|
||||||
func sameSealed(a, b delivery.GrantsUserList) bool {
|
func sameSealed(a, b delivery.GrantsUserList) bool {
|
||||||
|
// Its time is no change, and no user left out is the same written as [] or null: the key read back says [] for the
|
||||||
|
// nil a pass composes, which read as a change wrote the key again at every pass, and each write moved the bus node's
|
||||||
|
// report and woke the next pass (issue 1126: the shadow list's revision 99270 to 99357 in 90 s, 2026-10-28).
|
||||||
a.At, b.At = b.At, a.At
|
a.At, b.At = b.At, a.At
|
||||||
|
if len(a.LeftOut) == 0 {
|
||||||
|
a.LeftOut = nil
|
||||||
|
}
|
||||||
|
if len(b.LeftOut) == 0 {
|
||||||
|
b.LeftOut = nil
|
||||||
|
}
|
||||||
x, _ := json.Marshal(a)
|
x, _ := json.Marshal(a)
|
||||||
y, _ := json.Marshal(b)
|
y, _ := json.Marshal(b)
|
||||||
return string(x) == string(y)
|
return string(x) == string(y)
|
||||||
@@ -283,50 +288,11 @@ func (p *Part) followReasons(res Result, in Inputs) []string {
|
|||||||
// composed after take-over must keep.
|
// composed after take-over must keep.
|
||||||
const KeyWrittenUsers = "sealed-users"
|
const KeyWrittenUsers = "sealed-users"
|
||||||
|
|
||||||
// KeyWrittenCredentials is the key of the state user-list naming, per user of the list in force, which password its hash
|
func (p *Part) keepWrittenUsers(res *Result, list []byte) {
|
||||||
// is of (credentialOf: adopted, or the vault's generation and fingerprint), never the hash: what tells a list that changes
|
data, _ := json.Marshal(listUsers(list))
|
||||||
// several loaded users' hashes at once (ADR 0561 §7).
|
|
||||||
const KeyWrittenCredentials = "sealed-credentials"
|
|
||||||
|
|
||||||
func (p *Part) keepWrittenUsers(res *Result, list []byte, hashes map[string]HeldHash) {
|
|
||||||
users := listUsers(list)
|
|
||||||
data, _ := json.Marshal(users)
|
|
||||||
if _, err := p.Store.Put(StateUserList, KeyWrittenUsers, data); err != nil {
|
if _, err := p.Store.Put(StateUserList, KeyWrittenUsers, data); err != nil {
|
||||||
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", StateUserList, KeyWrittenUsers, err))
|
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", StateUserList, KeyWrittenUsers, err))
|
||||||
}
|
}
|
||||||
creds := map[string]string{}
|
|
||||||
for _, u := range users {
|
|
||||||
if h, ok := hashes[u]; ok {
|
|
||||||
creds[u] = credentialOf(h)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data, _ = json.Marshal(creds)
|
|
||||||
if _, err := p.Store.Put(StateUserList, KeyWrittenCredentials, data); err != nil {
|
|
||||||
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", StateUserList, KeyWrittenCredentials, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// changedHashes are the users of the list in force whose hash the list composed now would change with no rotation by the
|
|
||||||
// vault naming them, sorted; none when the part kept no credentials of the list in force (a list written before).
|
|
||||||
func (p *Part) changedHashes(hashes map[string]HeldHash) ([]string, error) {
|
|
||||||
raw, _, found, err := p.Store.Get(StateUserList, KeyWrittenCredentials)
|
|
||||||
if err != nil || !found {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var had map[string]string
|
|
||||||
if err := json.Unmarshal(raw, &had); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var changed []string
|
|
||||||
for user, was := range had {
|
|
||||||
h, ok := hashes[user]
|
|
||||||
if !ok || credentialOf(h) == was || h.Rotating != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
changed = append(changed, user)
|
|
||||||
}
|
|
||||||
slices.Sort(changed)
|
|
||||||
return changed, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var listUser = regexp.MustCompile(`\{\s*user:\s*"([^"]+)"`)
|
var listUser = regexp.MustCompile(`\{\s*user:\s*"([^"]+)"`)
|
||||||
@@ -346,8 +312,7 @@ func listUsers(list []byte) []string {
|
|||||||
// loaded list not read holds it too, since that cannot be ruled out). The list in force is the one the part wrote, by the users
|
// loaded list not read holds it too, since that cannot be ruled out). The list in force is the one the part wrote, by the users
|
||||||
// it kept of it; where it kept none (a list written before this release), the users the bus loaded (bus, nil when
|
// it kept of it; where it kept none (a list written before this release), the users the bus loaded (bus, nil when
|
||||||
// they could not be read). Empty when it may.
|
// they could not be read). Empty when it may.
|
||||||
func (p *Part) holdsBack(in Inputs, leftOut, bus, lost []string, digest string) string {
|
func (p *Part) holdsBack(in Inputs, leftOut, bus, lost []string) string {
|
||||||
p.hashHold = nil
|
|
||||||
if f := in.ReadFailures(); len(f) > 0 {
|
if f := in.ReadFailures(); len(f) > 0 {
|
||||||
return "held: the list in force is kept while a read of this pass failed (" + strings.Join(f, "; ") + ")"
|
return "held: the list in force is kept while a read of this pass failed (" + strings.Join(f, "; ") + ")"
|
||||||
}
|
}
|
||||||
@@ -377,127 +342,5 @@ func (p *Part) holdsBack(in Inputs, leftOut, bus, lost []string, digest string)
|
|||||||
if len(unhashed) > 0 {
|
if len(unhashed) > 0 {
|
||||||
return "held: the list in force is kept, since " + strings.Join(unhashed, ", ") + " would be left out for want of a hash in the vault"
|
return "held: the list in force is kept, since " + strings.Join(unhashed, ", ") + " would be left out for want of a hash in the vault"
|
||||||
}
|
}
|
||||||
// More than one loaded user's hash changing at once, with no rotation by the vault naming each (ADR 0561 §7): a new
|
|
||||||
// writer of hashes must not lock several holders out in one list, as nine seats were cut on 2026-10-27 at 09:21.
|
|
||||||
changed, err := p.changedHashes(in.Hashes)
|
|
||||||
if err != nil {
|
|
||||||
return "held: the credentials of the list in force cannot be read: " + err.Error()
|
|
||||||
}
|
|
||||||
if len(changed) > 1 && !p.accepted(digest, changed) {
|
|
||||||
p.hashHold = changed
|
|
||||||
return "held: the list in force is kept, since the hashes of " + fmt.Sprint(len(changed)) + " loaded users would change at " +
|
|
||||||
"once with no rotation by the vault naming them: " + strings.Join(changed, ", ")
|
|
||||||
}
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// **A person accepts a held list's hash changes** (ADR 0561 §7, §9; tools-are-how-a-module-is-spoken-to): the hold of
|
|
||||||
// several loaded users' hashes changing at once is released only by grants_accept_hashes, naming exactly the users held,
|
|
||||||
// with why, bound to the held list's digest. The acceptance releases this guard alone, once: the other holds (a failed
|
|
||||||
// read, a user dropped for want of a hash, a lost right) stand whatever it says; and a list composed otherwise since is
|
|
||||||
// held again, for a new acceptance.
|
|
||||||
|
|
||||||
// KeyHashHold names the list held for its hash changes (its digest and the users), and KeyHashAccept a person's acceptance.
|
|
||||||
const (
|
|
||||||
KeyHashHold = "hash-hold"
|
|
||||||
KeyHashAccept = "hash-accept"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EventHashesAccepted is said when a list held for its hash changes is written on a person's acceptance: a hand act.
|
|
||||||
const EventHashesAccepted = "hashes-accepted"
|
|
||||||
|
|
||||||
// HashHold is the value of KeyHashHold, and HashAccept of KeyHashAccept.
|
|
||||||
type HashHold struct {
|
|
||||||
Digest string `json:"digest"`
|
|
||||||
Users []string `json:"users"`
|
|
||||||
At time.Time `json:"at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HashAccept struct {
|
|
||||||
Digest string `json:"digest"`
|
|
||||||
Users []string `json:"users"`
|
|
||||||
By string `json:"by"`
|
|
||||||
Why string `json:"why"`
|
|
||||||
At time.Time `json:"at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// keepHashHold writes which list is held for its hash changes, when that is the hold.
|
|
||||||
func (p *Part) keepHashHold(res *Result, digest string) {
|
|
||||||
if p.hashHold == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data, _ := json.Marshal(HashHold{Digest: digest, Users: p.hashHold, At: p.now()})
|
|
||||||
if raw, _, found, err := p.Store.Get(StateUserList, KeyHashHold); err == nil && found {
|
|
||||||
var was HashHold
|
|
||||||
if json.Unmarshal(raw, &was) == nil && was.Digest == digest && slices.Equal(was.Users, p.hashHold) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if _, err := p.Store.Put(StateUserList, KeyHashHold, data); err != nil {
|
|
||||||
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", StateUserList, KeyHashHold, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// accepted says a person accepted exactly these users' hash changes of the list of this digest.
|
|
||||||
func (p *Part) accepted(digest string, users []string) bool {
|
|
||||||
raw, _, found, err := p.Store.Get(StateUserList, KeyHashAccept)
|
|
||||||
if err != nil || !found {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
var a HashAccept
|
|
||||||
return json.Unmarshal(raw, &a) == nil && a.Digest == digest && slices.Equal(a.Users, users)
|
|
||||||
}
|
|
||||||
|
|
||||||
// acceptedWritten says, once, that the list a person accepted is written, and forgets the acceptance and the hold.
|
|
||||||
func (p *Part) acceptedWritten(res *Result, digest string) {
|
|
||||||
raw, _, found, err := p.Store.Get(StateUserList, KeyHashAccept)
|
|
||||||
if err != nil || !found {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var a HashAccept
|
|
||||||
if json.Unmarshal(raw, &a) != nil || a.Digest != digest {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
p.say(res, EventHashesAccepted, map[string]any{"digest": digest, "users": a.Users, "by": a.By, "why": a.Why})
|
|
||||||
for _, k := range []string{KeyHashAccept, KeyHashHold} {
|
|
||||||
if err := p.Store.Delete(StateUserList, k); err != nil {
|
|
||||||
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be taken away: %v", StateUserList, k, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AcceptHashes is the tool accept-hashes: a person accepts the hash changes of the list held for them, naming exactly its
|
|
||||||
// users, with why. Refused when no list is held for its hashes, or for another set of users. Bound to the held list's
|
|
||||||
// digest: a list composed otherwise since is held again.
|
|
||||||
func (p *Part) AcceptHashes(caller string, users []string, why string) (any, error) {
|
|
||||||
if strings.TrimSpace(why) == "" {
|
|
||||||
return nil, errors.New("accept-hashes says why (`why`)")
|
|
||||||
}
|
|
||||||
raw, _, found, err := p.Store.Get(StateUserList, KeyHashHold)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("the held list cannot be read: %w", err)
|
|
||||||
}
|
|
||||||
var h HashHold
|
|
||||||
if !found || json.Unmarshal(raw, &h) != nil || h.Digest == "" {
|
|
||||||
return nil, errors.New("no list is held for its users' hashes changing: there is nothing to accept")
|
|
||||||
}
|
|
||||||
named := slices.Sorted(slices.Values(users))
|
|
||||||
if !slices.Equal(named, h.Users) {
|
|
||||||
return nil, fmt.Errorf("the list held (%s) changes the hashes of %s: an acceptance names exactly those, and %s is not that",
|
|
||||||
short(h.Digest), strings.Join(h.Users, ", "), strings.Join(named, ", "))
|
|
||||||
}
|
|
||||||
data, _ := json.Marshal(HashAccept{Digest: h.Digest, Users: h.Users, By: someoneOr(caller), Why: strings.TrimSpace(why), At: p.now()})
|
|
||||||
rev, err := p.Store.Put(StateUserList, KeyHashAccept, data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("the acceptance could not be written: %w", err)
|
|
||||||
}
|
|
||||||
return map[string]any{"accepted": rev, "digest": h.Digest, "users": h.Users, "says": "the part's process writes the held " +
|
|
||||||
"list at its next pass, once, unless another hold stands or the list composed then is another; it says hashes-accepted"}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func someoneOr(caller string) string {
|
|
||||||
if caller == "" {
|
|
||||||
return "a caller the runtime did not prove"
|
|
||||||
}
|
|
||||||
return caller
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -21,14 +21,10 @@ const (
|
|||||||
ToolTakeOver = "grants_take_over"
|
ToolTakeOver = "grants_take_over"
|
||||||
ToolTakeOverState = "grants_take_over_state"
|
ToolTakeOverState = "grants_take_over_state"
|
||||||
ToolHandBack = "grants_hand_back"
|
ToolHandBack = "grants_hand_back"
|
||||||
ToolAccounts = "grants_accounts"
|
|
||||||
ToolConsumerMatch = "grants_consumer_match"
|
|
||||||
ToolAcceptHashes = "grants_accept_hashes"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Tools is every tool, in the manifest's order.
|
// Tools is every tool, in the manifest's order.
|
||||||
var Tools = []string{ToolGrants, ToolMembership, ToolUserList, ToolComposeNow, ToolAssert, ToolTakeOver, ToolHandBack, ToolTakeOverState,
|
var Tools = []string{ToolGrants, ToolMembership, ToolUserList, ToolComposeNow, ToolAssert, ToolTakeOver, ToolHandBack, ToolTakeOverState}
|
||||||
ToolAccounts, ToolConsumerMatch, ToolAcceptHashes}
|
|
||||||
|
|
||||||
// WritesNoGrant is what grants says of the part's own grants in this milestone.
|
// WritesNoGrant is what grants says of the part's own grants in this milestone.
|
||||||
const WritesNoGrant = "none: the grants part writes no grants.<module>.<fp> before ADR 0547's third milestone (clause 5); the controller's are beside"
|
const WritesNoGrant = "none: the grants part writes no grants.<module>.<fp> before ADR 0547's third milestone (clause 5); the controller's are beside"
|
||||||
|
|||||||
@@ -12,13 +12,11 @@
|
|||||||
"mesh-delivery-indexer.modules",
|
"mesh-delivery-indexer.modules",
|
||||||
"mesh-delivery-indexer.placement",
|
"mesh-delivery-indexer.placement",
|
||||||
"node-engine.have",
|
"node-engine.have",
|
||||||
"mesh-vault.bus-accounts",
|
"mesh-vault.bus-accounts"
|
||||||
"mesh-controller.lease"
|
|
||||||
],
|
],
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"mesh-controller.condition-raised",
|
"mesh-controller.condition-raised",
|
||||||
"mesh-controller.condition-cleared",
|
"mesh-controller.condition-cleared"
|
||||||
"mesh-vault.account-issued"
|
|
||||||
],
|
],
|
||||||
"uses": [
|
"uses": [
|
||||||
"operator-channel"
|
"operator-channel"
|
||||||
@@ -31,7 +29,6 @@
|
|||||||
],
|
],
|
||||||
"emits": [
|
"emits": [
|
||||||
"asserted",
|
"asserted",
|
||||||
"hashes-accepted",
|
|
||||||
"membership-changed",
|
"membership-changed",
|
||||||
"user-list-changed"
|
"user-list-changed"
|
||||||
],
|
],
|
||||||
@@ -43,16 +40,9 @@
|
|||||||
"grants_assert",
|
"grants_assert",
|
||||||
"grants_take_over",
|
"grants_take_over",
|
||||||
"grants_take_over_state",
|
"grants_take_over_state",
|
||||||
"grants_hand_back",
|
"grants_hand_back"
|
||||||
"grants_accounts",
|
|
||||||
"grants_consumer_match",
|
|
||||||
"grants_accept_hashes"
|
|
||||||
],
|
],
|
||||||
"state": [
|
"state": [
|
||||||
{
|
|
||||||
"name": "accounts",
|
|
||||||
"history": 5
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "assertions",
|
"name": "assertions",
|
||||||
"history": 5
|
"history": 5
|
||||||
|
|||||||
Reference in New Issue
Block a user