From 2fe8d01e86c9670c601b7c6d6ab709a5c07d49ee Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 15:05:12 +0100 Subject: [PATCH] Join item 1: the rewrite map, asks/asker, the vault's types and compose-candidate to mesh-contracts mesh-sdk holds no go/asks, go/asker, vault answers or compose-candidate types (A1d, J1): each module now names the operator-channel contract and client, the channel benches (mesh-sdk/go/channel), mesh-vault's contract, and node-engine's contract and client (a1-progress.md, the A1d and J1 rewrite maps). - assigner, teller, view: delivery.Ask.Ask and AssignerAsk.Ask are the router's ask as JSON; the refusals mesh-sdk dropped (an id other than AskingID(key, asking), no option) are each reader's and writer's: assign.EncodeAsk/DecodeAsk (askjson.go), the teller's routerAskOf (teller.go) and the view's routerAsk (view.go); the teller refuses an option the asking does not offer and one whose level is not acknowledge (viewanswers.go), the two refusals ViewAnswerFits gave. Row: subtest of TestTheHoldAskIsValid (another id, no option, no ask; each written before). - enrolment, grants: the vault's verbs and answers from meshvault; asks' conditions from the contract and client. - checker, assigner's judge: compose-candidate from nodeengine/nodeengineclient (nec.Own for "node"). - mesh-grants' user-lists golden: the node-engine's users now subscribe to mesh-delivery-view's settled (mesh-sdk's NodeAgentHears, A7's ENG-8 retry). go.mod: each module requires the mesh-contracts modules it imports (and nodeengineclient, which mesh-sdk imports) at the zero version, for the group pin. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- .../integration_test.go | 43 ++--- .../cmd/mesh-delivery-assigner/main.go | 13 +- .../cmd/mesh-delivery-assigner/main_test.go | 4 +- .../cmd/mesh-delivery-assigner/pass.go | 26 +-- .../cmd/mesh-delivery-assigner/pass_test.go | 112 +++++++----- .../cmd/mesh-delivery-assigner/puts.go | 21 +-- .../cmd/mesh-delivery-assigner/seat.go | 17 +- modules/mesh-delivery-assigner/go.mod | 6 + .../internal/assign/askjson.go | 74 ++++++++ .../internal/assign/asks.go | 119 ++++++------ .../internal/assign/asks_test.go | 131 +++++++++----- .../internal/assign/findings.go | 13 +- .../internal/assign/placement.go | 13 +- .../internal/assign/policy.go | 8 +- .../internal/assign/put.go | 27 +-- .../internal/assign/seat.go | 31 ++-- .../internal/assign/seat_test.go | 8 +- .../internal/judge/judge.go | 5 +- .../cmd/mesh-delivery-checker/engines.go | 14 +- .../mesh-delivery-checker/integration_test.go | 18 +- .../cmd/mesh-delivery-checker/settings.go | 4 +- .../mesh-delivery-checker/settings_test.go | 4 +- modules/mesh-delivery-checker/go.mod | 2 + .../internal/check/engines.go | 50 +++--- .../internal/check/engines_test.go | 42 ++--- .../cmd/mesh-delivery-teller/assignerasks.go | 49 +++-- .../mesh-delivery-teller/assignerasks_test.go | 25 +-- .../cmd/mesh-delivery-teller/decided.go | 11 +- .../mesh-delivery-teller/integration_test.go | 21 +-- .../cmd/mesh-delivery-teller/main.go | 4 +- .../cmd/mesh-delivery-teller/room_test.go | 19 +- .../cmd/mesh-delivery-teller/teller.go | 170 +++++++++++------- .../cmd/mesh-delivery-teller/teller_test.go | 163 +++++++++-------- .../cmd/mesh-delivery-teller/viewanswers.go | 16 +- .../mesh-delivery-teller/viewanswers_test.go | 15 +- modules/mesh-delivery-teller/go.mod | 5 + .../cmd/mesh-delivery-cli/main_test.go | 39 ++-- .../mesh-delivery-view/integration_test.go | 25 +-- .../cmd/mesh-delivery-view/main_test.go | 13 ++ modules/mesh-delivery-view/go.mod | 5 + .../internal/view/answers_test.go | 13 +- .../internal/view/asks_test.go | 27 ++- .../mesh-delivery-view/internal/view/holds.go | 4 +- .../mesh-delivery-view/internal/view/view.go | 62 +++++-- .../cmd/mesh-enrolment/end_test.go | 6 +- .../cmd/mesh-enrolment/holder.go | 15 +- .../cmd/mesh-enrolment/integration_test.go | 29 +-- .../cmd/mesh-enrolment/issue.go | 34 ++-- .../cmd/mesh-enrolment/issue_test.go | 22 +-- .../cmd/mesh-enrolment/kit_test.go | 27 +-- .../mesh-enrolment/cmd/mesh-enrolment/main.go | 14 +- .../cmd/mesh-enrolment/vault.go | 29 +-- modules/mesh-enrolment/go.mod | 4 + modules/mesh-grants/cmd/mesh-grants/asking.go | 11 +- modules/mesh-grants/go.mod | 4 + modules/mesh-grants/grants_test.go | 91 +++++----- .../mesh-grants/internal/buscompose/engine.go | 6 +- .../mesh-grants/internal/buscompose/nats.go | 7 +- .../buscompose/testdata/user-lists.json | 2 +- .../mesh-grants/internal/grants/consumers.go | 19 +- .../mesh-grants/internal/grants/firstviews.go | 23 +-- modules/mesh-grants/internal/grants/full.go | 5 +- modules/mesh-grants/internal/grants/grants.go | 11 +- modules/mesh-grants/internal/grants/held.go | 11 +- modules/mesh-grants/internal/grants/part.go | 4 +- .../mesh-grants/internal/grants/refused.go | 11 +- modules/mesh-grants/internal/grants/sealed.go | 11 +- 67 files changed, 1134 insertions(+), 753 deletions(-) create mode 100644 modules/mesh-delivery-assigner/internal/assign/askjson.go diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go index 8f615fbf..2441d816 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go @@ -21,9 +21,10 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/announce" - "git.novox.be/novox/mesh-sdk/go/asks" "git.novox.be/novox/mesh-sdk/go/buscompose" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" @@ -418,15 +419,15 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) { // DLV-O5 (ADR 0615 §5, ADR 0619 §2): each want is said on the assigner's views family, its value whole at its revision, // and the first view at the mark followed by those events holds it as the bucket does; a waiter on put never wakes. b.viewsHold(t, "wants", wkey) - var neither asks.Condition + var neither oc.Condition for deadline := time.Now().Add(10 * time.Second); time.Now().Before(deadline) && neither.Key == ""; time.Sleep(100 * time.Millisecond) { for k, c := range b.conditionsSaid(t) { - if strings.HasSuffix(k, ".neither") && c.State == asks.StateRaised { + if strings.HasSuffix(k, ".neither") && c.State == occ.StateRaised { neither = c } } } - if neither.Subject.Machine != "g14" || !strings.HasPrefix(neither.Subject.ID, "g14.web") || neither.Severity != asks.SeverityUrgent || !strings.Contains(neither.Explanation, "canary") { + if neither.Subject.Machine != "g14" || !strings.HasPrefix(neither.Subject.ID, "g14.web") || neither.Severity != occ.SeverityUrgent || !strings.Contains(neither.Explanation, "canary") { t.Errorf("the condition of web on neither list on g14 = %+v", neither) } if keys, err := b.buckets[delivery.AssignerDifferencesBucket].Keys(b.ctx); err == nil && len(keys) != 0 { @@ -446,7 +447,7 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) { cleared := false for deadline := time.Now().Add(10 * time.Second); time.Now().Before(deadline) && !cleared; time.Sleep(100 * time.Millisecond) { c, ok := b.conditionsSaid(t)[neither.Key] - cleared = ok && c.State == asks.StateCleared + cleared = ok && c.State == occ.StateCleared } if !cleared { t.Errorf("the condition of web on neither list on g14 was not cleared once g14 got its want") @@ -668,7 +669,7 @@ func TestAHeldFingerprintIsAskedAbout(t *testing.T) { t.Fatalf("held = %+v, %v", h, err) } akey, _ := delivery.HoldAskKey("web", fNew) - a, err := delivery.DecodeAssignerAsk(akey, b.get(t, delivery.AssignerAsksBucket, akey)) + a, err := decodeAsk(akey, b.get(t, delivery.AssignerAsksBucket, akey)) if err != nil || a.Asking <= 0 || a.Ask.ID != delivery.AskingID(akey, a.Asking) || len(a.Ask.Options) != 2 { t.Fatalf("the ask = %+v, %v", a, err) } @@ -677,27 +678,27 @@ func TestAHeldFingerprintIsAskedAbout(t *testing.T) { // conditionsSaid reads what the assigner said of its conditions, by finding key: each a finding in its own bucket, which // the condition keeper carries (novox/hq ADR 0590 §3, issue 1126), raised while it stands and cleared once deleted, each // with the words its last finding said and the subject the finding names (`[.]`). -func (b *bed) conditionsSaid(t *testing.T) map[string]asks.Condition { +func (b *bed) conditionsSaid(t *testing.T) map[string]oc.Condition { t.Helper() w, err := b.buckets[assign.FindingsBucket].WatchAll(b.ctx, jetstream.IncludeHistory()) if err != nil { t.Fatal(err) } defer w.Stop() - out := map[string]asks.Condition{} + out := map[string]oc.Condition{} for e := range w.Updates() { if e == nil { return out } c := out[e.Key()] - c.Key, c.State = e.Key(), asks.StateCleared + c.Key, c.State = e.Key(), occ.StateCleared if e.Operation() == jetstream.KeyValuePut { f, err := health.DecodeFinding(e.Value()) if err != nil { t.Fatal(err) } - c = asks.Condition{Key: e.Key(), State: asks.StateRaised, Severity: string(f.Severity), Explanation: f.Explanation, - Resolved: f.Resolved, Subject: asks.Scope{Scope: string(f.Subject.Scope), ID: f.Subject.ID, Machine: f.Subject.Machine}} + c = oc.Condition{Key: e.Key(), State: occ.StateRaised, Severity: string(f.Severity), Explanation: f.Explanation, + Resolved: f.Resolved, Subject: oc.ConditionSubject{Scope: string(f.Subject.Scope), ID: f.Subject.ID, Machine: f.Subject.Machine}} } out[e.Key()] = c } @@ -724,7 +725,7 @@ func TestAFailedDataModuleIsAskedToRestore(t *testing.T) { b.start(t) akey, _ := delivery.RestoreAskKey("ace", "mail", fNew) - a, err := delivery.DecodeAssignerAsk(akey, b.get(t, delivery.AssignerAsksBucket, akey)) + a, err := decodeAsk(akey, b.get(t, delivery.AssignerAsksBucket, akey)) if err != nil || len(a.Ask.Options) != 2 || a.Ask.Options[0].ID != "restore" || a.Ask.Options[1].ID != "leave-stopped" { t.Fatalf("the restore ask = %+v, %v", a, err) } @@ -734,7 +735,7 @@ func TestAFailedDataModuleIsAskedToRestore(t *testing.T) { key := "" for deadline := time.Now().Add(10 * time.Second); time.Now().Before(deadline) && key == ""; time.Sleep(100 * time.Millisecond) { for k, c := range b.conditionsSaid(t) { - if c.State == asks.StateRaised && strings.HasPrefix(c.Subject.ID, "mail.") && strings.HasSuffix(k, ".held") { + if c.State == occ.StateRaised && strings.HasPrefix(c.Subject.ID, "mail.") && strings.HasSuffix(k, ".held") { if c.Subject.Machine != "ace" || !strings.Contains(c.Explanation, "connection refused") { t.Fatalf("the condition = %+v, want it to name ace and the check's words", c) } @@ -852,7 +853,7 @@ func (b *bed) wantNow(t *testing.T, node, module, what string, ok func(delivery. // answer says the teller wrote the operator's answer to the ask that stands at key (its warrant names that asking's id). func (b *bed) answer(t *testing.T, key, option string) { t.Helper() - a, err := delivery.DecodeAssignerAsk(key, b.get(t, delivery.AssignerAsksBucket, key)) + a, err := decodeAsk(key, b.get(t, delivery.AssignerAsksBucket, key)) if err != nil { t.Fatal(err) } @@ -902,7 +903,7 @@ func TestAReleaseAnswerWritesTheWantAgain(t *testing.T) { for deadline := time.Now().Add(10 * time.Second); time.Now().Before(deadline); time.Sleep(100 * time.Millisecond) { cleared := false for k, c := range b.conditionsSaid(t) { - cleared = cleared || (strings.HasPrefix(k, "module.web.") && c.State == asks.StateCleared && strings.HasSuffix(k, ".held")) + cleared = cleared || (strings.HasPrefix(k, "module.web.") && c.State == occ.StateCleared && strings.HasSuffix(k, ".held")) } if cleared { return @@ -912,17 +913,17 @@ func TestAReleaseAnswerWritesTheWantAgain(t *testing.T) { } // askNow waits for the backup ask at key to be the one numbered asking and answers it. -func (b *bed) askNow(t *testing.T, key string, asking int) delivery.AssignerAsk { +func (b *bed) askNow(t *testing.T, key string, asking int) decodedAsk { t.Helper() for deadline := time.Now().Add(15 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { if e, err := b.buckets[delivery.AssignerAsksBucket].Get(b.ctx, key); err == nil { - if a, err := delivery.DecodeAssignerAsk(key, e.Value()); err == nil && a.Asking == asking { + if a, err := decodeAsk(key, e.Value()); err == nil && a.Asking == asking { return a } } } t.Fatalf("the ask at %s never became asking %d", key, asking) - return delivery.AssignerAsk{} + return decodedAsk{} } // 1906/D4 (ADR 0539 §16), whole on the bus: a module held on a node because its backup failed is asked about, and the answer @@ -992,7 +993,7 @@ func TestAHoldIsLiftedOnceEveryNodeRunsItHealthy(t *testing.T) { continue } for k, c := range b.conditionsSaid(t) { - if strings.HasPrefix(k, "module.web.") && c.State == asks.StateCleared && strings.HasSuffix(k, ".held") { + if strings.HasPrefix(k, "module.web.") && c.State == occ.StateCleared && strings.HasSuffix(k, ".held") { if w := b.wantNow(t, "ace", "web", "the want at the lifted fingerprint", func(w delivery.Want) bool { return w.FP == fNew }); w.Rule != 0 { t.Errorf("the want = %+v", w) } @@ -1017,7 +1018,7 @@ func TestAHoldWrittenAgainAfterALiftAndARestartIsAskedAgain(t *testing.T) { _, stop := b.startStoppable(t) hkey, _ := delivery.HeldKey("web", fNew) akey, _ := delivery.HoldAskKey("web", fNew) - first, err := delivery.DecodeAssignerAsk(akey, b.get(t, delivery.AssignerAsksBucket, akey)) + first, err := decodeAsk(akey, b.get(t, delivery.AssignerAsksBucket, akey)) if err != nil { t.Fatal(err) } @@ -1032,7 +1033,7 @@ func TestAHoldWrittenAgainAfterALiftAndARestartIsAskedAgain(t *testing.T) { b.answerTo(t, akey, "keep", first.Ask.ID) // an answer to the first hold, which the later hold must not take b.putJSON(t, delivery.HaveBucket, "ace.web", failed) b.start(t) - again, err := delivery.DecodeAssignerAsk(akey, b.get(t, delivery.AssignerAsksBucket, akey)) + again, err := decodeAsk(akey, b.get(t, delivery.AssignerAsksBucket, akey)) if err != nil { t.Fatal(err) } diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go index e9bc747e..ca7b8012 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go @@ -21,8 +21,9 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/announce" - "git.novox.be/novox/mesh-sdk/go/asks" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/health" "git.novox.be/novox/mesh-sdk/go/process" @@ -295,7 +296,7 @@ var watched = []string{ // follow watches every bucket a pass reads and makes a pass at start, once every watch handed what stands, and after // each change; changes that come during a pass make one more. It returns when ctx ends or a watch ends. -func follow(ctx context.Context, js jetstream.JetStream, nc process.Subscriber, kvs buckets, say func(asks.Condition) error, findings assign.Findings, bus caller, iv *indexerViews, logger *log.Logger) error { +func follow(ctx context.Context, js jetstream.JetStream, nc process.Subscriber, kvs buckets, say func(oc.Condition) error, findings assign.Findings, bus caller, iv *indexerViews, logger *log.Logger) error { w := newWorld() dirty := make(chan struct{}, 1) wake := func() { @@ -510,13 +511,13 @@ var PutsReopenEvery = time.Minute // sayVia publishes a condition on the operator-channel seat's one subject for conditions, under the assigner's own name // (ADR 0437 clause 9). -func sayVia(ctx context.Context, js jetstream.JetStream) func(asks.Condition) error { - return func(c asks.Condition) error { - subject, err := asks.ConditionSubject(delivery.AssignerModule) +func sayVia(ctx context.Context, js jetstream.JetStream) func(oc.Condition) error { + return func(c oc.Condition) error { + subject, err := occ.ConditionSubject(delivery.AssignerModule) if err != nil { return err } - data, err := asks.EncodeCondition(c) + data, err := occ.EncodeCondition(c) if err != nil { return err } diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main_test.go index b411f949..db989e12 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main_test.go @@ -18,7 +18,7 @@ import ( "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/asks" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -113,7 +113,7 @@ func TestOpensWhatItsManifestNames(t *testing.T) { } kvs[delivery.AssignerPutsBucket].(*watchKV).closed = true kvs[delivery.AssignerWantsBucket].(*watchKV).wrote = wrote - go follow(ctx, nil, nil, kvs, func(asks.Condition) error { return nil }, assign.Findings{}, nil, nil, log.New(io.Discard, "", 0)) + go follow(ctx, nil, nil, kvs, func(oc.Condition) error { return nil }, assign.Findings{}, nil, nil, log.New(io.Discard, "", 0)) select { case key := <-wrote: if key != "want.ace.node" { diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go index e560a24d..2fb7907d 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "git.novox.be/novox/mesh-sdk/go/asks" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/provisions" @@ -29,7 +29,7 @@ type passer struct { sdk string // say publishes a condition to the operator; said is, by condition key and state, what this process has said, so a // pass says a condition once and a restart says it again (the router keeps one per key). - say func(asks.Condition) error + say func(oc.Condition) error said map[string]bool // findings is the assigner's findings bucket (its said conditions, ADR 0564 §2). findings assign.Findings @@ -554,7 +554,7 @@ func (p *passer) holdAsk(ctx context.Context, module, fp string, fresh *delivery asking := int(now.Unix()) h := p.heldHold(module, fp, fresh) a := assign.HoldAsk(key, module, fp, h.Node, h.Words, asking, now) - p.put(ctx, delivery.AssignerAsksBucket, key, func() ([]byte, error) { return delivery.EncodeAssignerAsk(key, a) }) + p.put(ctx, delivery.AssignerAsksBucket, key, func() ([]byte, error) { return assign.EncodeAsk(key, a) }) } // lift takes away a hold the assigner lifted by itself, since every node that runs the module runs the held fingerprint healthy @@ -737,7 +737,7 @@ func (p *passer) restoreAsk(ctx context.Context, module, node, fp string) { } words := p.heldHold(module, fp, nil).Words a := assign.RestoreAsk(key, node, module, fp, words, time.Now()) - p.put(ctx, delivery.AssignerAsksBucket, key, func() ([]byte, error) { return delivery.EncodeAssignerAsk(key, a) }) + p.put(ctx, delivery.AssignerAsksBucket, key, func() ([]byte, error) { return assign.EncodeAsk(key, a) }) } // answeredWith says whether the teller wrote an answer to the ask at key as it stands now and the option chosen is option. The @@ -749,12 +749,12 @@ func (p *passer) answeredWith(key, option string) bool { if !ok || a.Option != option { return false } - ask, err := delivery.DecodeAssignerAsk(key, p.w.value(delivery.AssignerAsksBucket, key)) + _, ask, err := assign.DecodeAsk(key, p.w.value(delivery.AssignerAsksBucket, key)) if err != nil { return false } id, _, _ := strings.Cut(a.Warrant, "@") - return id == ask.Ask.ID + return id == ask.ID } // clearHeld clears, once, the condition of a hold the operator released. @@ -779,7 +779,7 @@ func (p *passer) recordAsk(ctx context.Context, module, fp string, need assign.R return } a := assign.RecordAsk(key, need.Node, module, fp, need, time.Now()) - p.put(ctx, delivery.AssignerAsksBucket, key, func() ([]byte, error) { return delivery.EncodeAssignerAsk(key, a) }) + p.put(ctx, delivery.AssignerAsksBucket, key, func() ([]byte, error) { return assign.EncodeAsk(key, a) }) } // backups sees to the modules held on a node because the backup before an apply failed (the have's phase held, ADR 0539 §16): @@ -810,7 +810,7 @@ func (p *passer) backups(ctx context.Context) { if err != nil { continue } - current, there := delivery.DecodeAssignerAsk(akey, p.w.read(delivery.AssignerAsksBucket)[akey]) + current, currentAsk, there := assign.DecodeAsk(akey, p.w.read(delivery.AssignerAsksBucket)[akey]) if there != nil || assign.BackupAskDigest(current) != want.Composed { after := -1 if there == nil { @@ -820,7 +820,7 @@ func (p *passer) backups(ctx context.Context) { continue } answer, answered := p.w.answer(akey) - if router, _, _ := strings.Cut(answer.Warrant, "@"); !answered || router != current.Ask.ID || + if router, _, _ := strings.Cut(answer.Warrant, "@"); !answered || router != currentAsk.ID || (answer.Option != delivery.BackupAgain && answer.Option != delivery.BackupSkip) { continue // not answered yet: the ask stands } @@ -831,7 +831,7 @@ func (p *passer) backups(ctx context.Context) { if wkey, err := delivery.WantKey(node, module); err == nil { w := want p.put(ctx, delivery.AssignerWantsBucket, wkey, func() ([]byte, error) { return delivery.EncodeWant(w) }) - p.answeredAt[current.Ask.ID] = haveChanges + p.answeredAt[currentAsk.ID] = haveChanges } continue } @@ -839,8 +839,8 @@ func (p *passer) backups(ctx context.Context) { continue } // The answer is in the want. The node retried and the backup failed again when the have took another value since. - if seen, known := p.answeredAt[current.Ask.ID]; !known { - p.answeredAt[current.Ask.ID] = haveChanges // after a restart: wait for the next failure + if seen, known := p.answeredAt[currentAsk.ID]; !known { + p.answeredAt[currentAsk.ID] = haveChanges // after a restart: wait for the next failure } else if haveChanges > seen { p.backupAsk(ctx, akey, node, module, want, have.Detail, assign.NextBackupAsking(current.Asking)) } @@ -850,7 +850,7 @@ func (p *passer) backups(ctx context.Context) { // backupAsk writes the backup ask at its asking and raises the condition of the failed backup, once. func (p *passer) backupAsk(ctx context.Context, key, node, module string, want delivery.Want, words string, asking int) { a := assign.BackupAsk(key, node, module, want.FP, want.Composed, words, asking, time.Now()) - p.put(ctx, delivery.AssignerAsksBucket, key, func() ([]byte, error) { return delivery.EncodeAssignerAsk(key, a) }) + p.put(ctx, delivery.AssignerAsksBucket, key, func() ([]byte, error) { return assign.EncodeAsk(key, a) }) c := assign.BackupCondition(node, module, want.FP, words, true) if p.say == nil || p.said[c.Key] { return diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go index 560572a4..6ad7309d 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go @@ -13,7 +13,8 @@ import ( "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/busretry" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" @@ -94,7 +95,7 @@ func TestAHoldAskedAgainAfterALiftAndARestartHasANewID(t *testing.T) { held := &delivery.Held{Node: "ace", Words: "health failed"} first := newPasser(kvs, time.Date(2026, 10, 25, 1, 0, 0, 0, time.UTC)) first.holdAsk(context.Background(), "web", fpTest, held) - a1, err := delivery.DecodeAssignerAsk(key, asksKV.values[key]) + a1, err := decodeAsk(key, asksKV.values[key]) if err != nil { t.Fatal(err) } @@ -104,7 +105,7 @@ func TestAHoldAskedAgainAfterALiftAndARestartHasANewID(t *testing.T) { } again := newPasser(kvs, time.Date(2026, 10, 25, 2, 0, 0, 0, time.UTC)) // started again: it remembers nothing again.holdAsk(context.Background(), "web", fpTest, held) - a2, err := delivery.DecodeAssignerAsk(key, asksKV.values[key]) + a2, err := decodeAsk(key, asksKV.values[key]) if err != nil { t.Fatal(err) } @@ -128,12 +129,12 @@ func TestAnAnswerCountsOnlyForTheAskingItAnswered(t *testing.T) { p := newPasser(buckets{}, time.Now()) put := func(asking int) string { a := assign.HoldAsk(key, "web", fpTest, "ace", "health failed", asking, time.Now()) - v, err := delivery.EncodeAssignerAsk(key, a) + v, err := assign.EncodeAsk(key, a) if err != nil { t.Fatal(err) } p.w.put(delivery.AssignerAsksBucket, key, v) - return a.Ask.ID + return mustAskID(a) } first := put(100) v, _ := delivery.EncodeAnswer(delivery.Answer{Option: "release", Warrant: first + "@2026-10-25T01:00:00Z", Words: "the operator"}) @@ -397,8 +398,8 @@ func putOfTheBus(t *testing.T) { if got.Step != delivery.PutPerson || got.Ask != akey { t.Fatalf("composed, the put stands at %s (%s), ask %q", got.Step, got.Words, got.Ask) } - a, err := delivery.DecodeAssignerAsk(akey, asksKV.values[akey]) - if err != nil || a.Ask.Options[0].Level != asks.Approve { + a, err := decodeAsk(akey, asksKV.values[akey]) + if err != nil || a.Ask.Options[0].Level != occ.Approve { t.Fatalf("the ask is %+v, %v; want a yes that approves", a, err) } if _, there := wantsKV.values["want.novox.nats"]; there { @@ -446,7 +447,7 @@ func putOfTheBus(t *testing.T) { t.Fatal(err) } again := step() - a2, err := delivery.DecodeAssignerAsk(akey, asksKV.values[akey]) + a2, err := decodeAsk(akey, asksKV.values[akey]) if again.Step != delivery.PutPerson || err != nil || a2.Ask.ID == a.Ask.ID { t.Fatalf("put again, it stands at %s (%s); its ask %q, %v; the first ask was %q", again.Step, again.Words, a2.Ask.ID, err, a.Ask.ID) } @@ -480,9 +481,9 @@ func TestAModuleOnNeitherListIsRaisedOnceAndClearedOnce(t *testing.T) { aWantWaitsOnTheVerbEdgesTheGraphKeeps(t) t.Run("gathers", testThePassGathersTheModulesOnNeitherList) p := newPasser(buckets{}, time.Date(2026, 10, 25, 15, 0, 0, 0, time.UTC)) - var said []asks.Condition + var said []oc.Condition failing := true - p.say = func(c asks.Condition) error { + p.say = func(c oc.Condition) error { if failing { failing = false return errors.New("the channel is away") @@ -497,7 +498,7 @@ func TestAModuleOnNeitherListIsRaisedOnceAndClearedOnce(t *testing.T) { p.sayNeither(one, nil, nil) // the say fails p.sayNeither(one, nil, nil) p.sayNeither(one, nil, nil) - if len(said) != 1 || said[0].State != asks.StateRaised || said[0].Subject.Machine != "g14" || said[0].Subject.ID != "screen-lock" { + if len(said) != 1 || said[0].State != occ.StateRaised || said[0].Subject.Machine != "g14" || said[0].Subject.ID != "screen-lock" { t.Fatalf("said = %+v, want one raised condition of screen-lock on g14", said) } if !strings.Contains(strings.Join(logged, "\n"), "screen-lock") { @@ -506,7 +507,7 @@ func TestAModuleOnNeitherListIsRaisedOnceAndClearedOnce(t *testing.T) { wants := map[string]string{"g14.screen-lock": ""} p.sayNeither(map[string]neitherOne{}, nil, wants) p.sayNeither(map[string]neitherOne{}, nil, wants) - if len(said) != 2 || said[1].State != asks.StateCleared || said[1].Key != said[0].Key { + if len(said) != 2 || said[1].State != occ.StateCleared || said[1].Key != said[0].Key { t.Errorf("said = %+v, want the condition cleared once under its key", said) } if last := logged[len(logged)-1]; last != "screen-lock on g14 has a want again" { @@ -516,11 +517,11 @@ func TestAModuleOnNeitherListIsRaisedOnceAndClearedOnce(t *testing.T) { // on neither list by a rule as any module (ADR 0601 §1), nothing for the operator to do. bus := map[string]neitherOne{"novox.nats": {node: "novox", module: "nats", holding: assign.Holding{Rule: delivery.RuleGrant, Words: "no credential"}}} p.sayNeither(bus, nil, nil) - if c := said[len(said)-1]; c.State != asks.StateRaised || !strings.HasPrefix(c.Explanation, asks.NothingToDo) || !strings.Contains(c.Explanation, "rule 6 holds its first want back: no credential") { + if c := said[len(said)-1]; c.State != occ.StateRaised || !strings.HasPrefix(c.Explanation, occ.NothingToDo) || !strings.Contains(c.Explanation, "rule 6 holds its first want back: no credential") { t.Errorf("the bus's neither condition = %+v", c) } p.sayNeither(map[string]neitherOne{}, nil, map[string]string{"novox.nats": "it is held, and its held condition says so"}) - if c := said[len(said)-1]; c.State != asks.StateCleared || strings.Contains(c.Resolved, "has a want") { + if c := said[len(said)-1]; c.State != occ.StateCleared || strings.Contains(c.Resolved, "has a want") { t.Errorf("a module that left neither list with no want was cleared as %+v", c) } if last := logged[len(logged)-1]; last != "nats on novox is no longer on neither list and has no want: it is held, and its held condition says so" { @@ -582,16 +583,16 @@ func testThePassGathersTheModulesOnNeitherList(t *testing.T) { } // 1496, ADR 0557 §1: a failure that is the node's own (its fingerprint healthy elsewhere, not held) raises the node's condition // once, naming the node, the module, F and the have's words, and clears it once when it ends. - var conds []asks.Condition - p.say = func(c asks.Condition) error { conds = append(conds, c); return nil } + var conds []oc.Condition + p.say = func(c oc.Condition) error { conds = append(conds, c); return nil } failing := map[string]assign.NodeFailure{"shanks": {Others: []string{"ace"}, Words: "health failed, phase running: w"}} for i, c := range []struct { failed map[string]assign.NodeFailure state string // the condition said in this pass, "" for none }{ - {failing, asks.StateRaised}, + {failing, occ.StateRaised}, {failing, ""}, - {nil, asks.StateCleared}, + {nil, occ.StateCleared}, {nil, ""}, } { conds = nil @@ -599,7 +600,7 @@ func testThePassGathersTheModulesOnNeitherList(t *testing.T) { switch { case c.state == "" && len(conds) != 0, c.state != "" && (len(conds) != 1 || conds[0].State != c.state): t.Errorf("node failed, pass %d said %+v, want %q", i, conds, c.state) - case c.state == asks.StateRaised && (conds[0].Key != "mesh-delivery-assigner.failed-alone.shanks.rofi" || + case c.state == occ.StateRaised && (conds[0].Key != "mesh-delivery-assigner.failed-alone.shanks.rofi" || !strings.Contains(conds[0].Explanation, "failed on shanks (health failed, phase running: w), while it runs healthy on ace")): t.Errorf("node failed, raised %+v", conds[0]) } @@ -610,7 +611,7 @@ func testThePassGathersTheModulesOnNeitherList(t *testing.T) { p.sayNodeFailed("rofi", fpTest, []string{"shanks"}, assign.Nodes{}, assign.Outcome{NodeFailed: failing}, false) p.sayNodeFailed("rofi", fpTest, nil, assign.Nodes{}, assign.Outcome{}, false) p.sayNodeFailed("rofi", fpTest, nil, assign.Nodes{}, assign.Outcome{}, false) - if len(conds) != 2 || conds[1].State != asks.StateCleared || conds[1].Key != "mesh-delivery-assigner.failed-alone.shanks.rofi" { + if len(conds) != 2 || conds[1].State != occ.StateCleared || conds[1].Key != "mesh-delivery-assigner.failed-alone.shanks.rofi" { t.Errorf("a node leaving the module said %+v, want one raise and one clear", conds) } known := assign.Nodes{Online: map[string]bool{"ace": true, "g14": false}, @@ -626,11 +627,11 @@ func testThePassGathersTheModulesOnNeitherList(t *testing.T) { conds = nil fresh := newPasser(buckets{}, time.Date(2026, 10, 26, 22, 0, 0, 0, time.UTC)) fresh.logf = func(string, ...any) {} - fresh.say = func(c asks.Condition) error { conds = append(conds, c); return nil } + fresh.say = func(c oc.Condition) error { conds = append(conds, c); return nil } fresh.sayNodeFailed("rofi", fpTest, nil, c.known, assign.Outcome{}, true) var keys []string for _, cond := range conds { - if cond.State == asks.StateCleared { + if cond.State == occ.StateCleared { keys = append(keys, cond.Key) } } @@ -647,8 +648,8 @@ func TestTheFirstPassAfterAStartClearsWhatAPreviousProcessRaised(t *testing.T) { t.Run("a held module", testAHeldModuleRaisesNoNeitherAndClearsAStandingOne) wantsKV := &memKV{values: map[string][]byte{}} p := newPasser(buckets{delivery.AssignerWantsBucket: wantsKV, delivery.AssignerAsksBucket: &memKV{values: map[string][]byte{}}}, time.Now()) - var said []asks.Condition - p.say = func(c asks.Condition) error { said = append(said, c); return nil } + var said []oc.Condition + p.say = func(c oc.Condition) error { said = append(said, c); return nil } mod, _ := delivery.EncodeModule(delivery.Module{FP2: fpTest, Repository: "novox/web", Commit: strings.Repeat("1", 40), Path: delivery.RootPath}) p.w.put(delivery.ModulesBucket, "web", mod) placeOn(t, p.w, "g14", "web") @@ -662,7 +663,7 @@ func TestTheFirstPassAfterAStartClearsWhatAPreviousProcessRaised(t *testing.T) { p.wants(context.Background()) cleared := map[string]bool{} for _, c := range said { - if c.State != asks.StateCleared { + if c.State != occ.StateCleared { t.Errorf("the first pass raised %+v", c) } cleared[c.Key] = true @@ -683,10 +684,10 @@ func TestTheFirstPassAfterAStartClearsWhatAPreviousProcessRaised(t *testing.T) { // neither condition for it, a neither condition this process raised for it before is cleared, and so is one a previous process // raised (the first pass after a start), each once. func testAHeldModuleRaisesNoNeitherAndClearsAStandingOne(t *testing.T) { - setup := func() (*passer, *[]asks.Condition) { + setup := func() (*passer, *[]oc.Condition) { p := newPasser(buckets{delivery.AssignerWantsBucket: &memKV{values: map[string][]byte{}}, delivery.AssignerAsksBucket: &memKV{values: map[string][]byte{}}}, time.Now()) - said := &[]asks.Condition{} - p.say = func(c asks.Condition) error { *said = append(*said, c); return nil } + said := &[]oc.Condition{} + p.say = func(c oc.Condition) error { *said = append(*said, c); return nil } mod, _ := delivery.EncodeModule(delivery.Module{FP2: fpTest, Repository: "novox/resolved", Commit: strings.Repeat("1", 40), Path: delivery.RootPath}) p.w.put(delivery.ModulesBucket, "systemd-resolved", mod) placeOn(t, p.w, "shanks", "systemd-resolved") @@ -700,12 +701,12 @@ func testAHeldModuleRaisesNoNeitherAndClearsAStandingOne(t *testing.T) { return p, said } key := assign.NeitherCondition("shanks", "systemd-resolved", assign.Holding{}, false).Key - neithers := func(said []asks.Condition) (raised, cleared int) { + neithers := func(said []oc.Condition) (raised, cleared int) { for _, c := range said { if c.Key != key { continue } - if c.State == asks.StateRaised { + if c.State == occ.StateRaised { raised++ } else { cleared++ @@ -764,8 +765,8 @@ func TestAsksOfAFingerprintTheTrunkLeftBehindAreTakenAway(t *testing.T) { t.Run(c.name, func(t *testing.T) { asksKV, wantsKV := &memKV{values: map[string][]byte{}}, &memKV{values: map[string][]byte{}} p := newPasser(buckets{delivery.AssignerAsksBucket: asksKV, delivery.AssignerWantsBucket: wantsKV}, time.Now()) - var said []asks.Condition - p.say = func(c asks.Condition) error { said = append(said, c); return nil } + var said []oc.Condition + p.say = func(c oc.Condition) error { said = append(said, c); return nil } for _, key := range all { k, _ := delivery.ParseAnyAskKey(key) var a delivery.AssignerAsk @@ -779,7 +780,7 @@ func TestAsksOfAFingerprintTheTrunkLeftBehindAreTakenAway(t *testing.T) { default: a = assign.BackupAsk(key, k.Node, k.Module, k.FP, digOld, "w", 2, time.Now()) } - v, err := delivery.EncodeAssignerAsk(key, a) + v, err := assign.EncodeAsk(key, a) if err != nil { t.Fatal(err) } @@ -793,13 +794,13 @@ func TestAsksOfAFingerprintTheTrunkLeftBehindAreTakenAway(t *testing.T) { heldOld, _ := delivery.HeldKey("web", fpOld) wantsKV.values[heldOld] = []byte("x") p.w.put(delivery.AssignerWantsBucket, heldOld, []byte("x")) - firstAsk, _ := delivery.DecodeAssignerAsk(holdOld, asksKV.values[holdOld]) + firstAsk, _ := decodeAsk(holdOld, asksKV.values[holdOld]) p.staleAsks(context.Background(), c.trunk) // the held record and condition of a fingerprint whose hold ask went are taken away with it heldOld, _ = delivery.HeldKey("web", fpOld) _, heldThere := wantsKV.values[heldOld] - cleared := slices.ContainsFunc(said, func(c asks.Condition) bool { - return c.Key == assign.HeldCondition("web", fpOld, "", "", false, false).Key && c.State == asks.StateCleared + cleared := slices.ContainsFunc(said, func(c oc.Condition) bool { + return c.Key == assign.HeldCondition("web", fpOld, "", "", false, false).Key && c.State == occ.StateCleared }) if gone := slices.Contains(c.gone, holdOld); !heldThere || cleared != gone { t.Errorf("hold ask of the old fingerprint gone = %v: its held record stays = %v (must), its condition cleared = %v", gone, heldThere, cleared) @@ -816,8 +817,8 @@ func TestAsksOfAFingerprintTheTrunkLeftBehindAreTakenAway(t *testing.T) { p.now = func() time.Time { return time.Now().Add(time.Hour) } p.holdAsk(context.Background(), "web", fpOld, nil) p.heldCondition(context.Background(), "web", fpOld, nil, false) - again, err := delivery.DecodeAssignerAsk(holdOld, asksKV.values[holdOld]) - raised := slices.ContainsFunc(said, func(c asks.Condition) bool { return c.State == asks.StateRaised }) + again, err := decodeAsk(holdOld, asksKV.values[holdOld]) + raised := slices.ContainsFunc(said, func(c oc.Condition) bool { return c.State == occ.StateRaised }) if err != nil || again.Ask.ID == firstAsk.Ask.ID || !raised { t.Errorf("the trunk returned to the held fingerprint: ask %q (first %q, %v), condition raised again = %v", again.Ask.ID, firstAsk.Ask.ID, err, raised) } @@ -828,9 +829,9 @@ func TestAsksOfAFingerprintTheTrunkLeftBehindAreTakenAway(t *testing.T) { t.Run("a clear that failed to be said is said at the next pass, and the ask goes after it", func(t *testing.T) { asksKV, wantsKV := &memKV{values: map[string][]byte{}}, &memKV{values: map[string][]byte{}} p := newPasser(buckets{delivery.AssignerAsksBucket: asksKV, delivery.AssignerWantsBucket: wantsKV}, time.Now()) - var said []asks.Condition + var said []oc.Condition fail := true - p.say = func(c asks.Condition) error { + p.say = func(c oc.Condition) error { if fail { fail = false return errors.New("router unreachable") @@ -839,7 +840,7 @@ func TestAsksOfAFingerprintTheTrunkLeftBehindAreTakenAway(t *testing.T) { return nil } a := assign.HoldAsk(holdOld, "web", fpOld, "novox", "w", 1, time.Now()) - v, err := delivery.EncodeAssignerAsk(holdOld, a) + v, err := assign.EncodeAsk(holdOld, a) if err != nil { t.Fatal(err) } @@ -850,7 +851,7 @@ func TestAsksOfAFingerprintTheTrunkLeftBehindAreTakenAway(t *testing.T) { t.Fatalf("after a failed clear: ask left = %v, conditions said = %d; want the ask kept and nothing said", there, len(said)) } p.staleAsks(context.Background(), trunkOf(fpNew)) - if _, there := asksKV.values[holdOld]; there || len(said) != 1 || said[0].State != asks.StateCleared { + if _, there := asksKV.values[holdOld]; there || len(said) != 1 || said[0].State != occ.StateCleared { t.Errorf("at the next pass: ask left = %v, conditions said = %v; want the ask gone and the clear said", there, said) } }) @@ -1039,7 +1040,7 @@ func aPlacedNodesWantsNameTheBuildAloneAndItsYesComesFromItsHave(t *testing.T) { if wantsKV.puts != wantPuts { t.Fatalf("a want put unchanged reached the bucket") } - a, err := delivery.DecodeAssignerAsk(akey, asksKV.values[akey]) + a, err := decodeAsk(akey, asksKV.values[akey]) if err != nil { t.Fatal(err) } @@ -1076,8 +1077,8 @@ func TestAnApprovedPlacementCallsNoControllerVerb(t *testing.T) { t.Fatalf("calls = %d, said = %v", len(bus.calls), said) } - var conds []asks.Condition - p.say = func(c asks.Condition) error { conds = append(conds, c); return nil } + var conds []oc.Condition + p.say = func(c oc.Condition) error { conds = append(conds, c); return nil } fails := 1 p.findings.Keys = func() ([]string, error) { if fails > 0 { @@ -1089,7 +1090,7 @@ func TestAnApprovedPlacementCallsNoControllerVerb(t *testing.T) { p.clearStaleCalls() p.clearStaleCalls() p.clearStaleCalls() - if len(conds) != 2 || conds[0].State != asks.StateCleared || conds[0].Key != "mesh-delivery-assigner.controller-call.anchor.web" || + if len(conds) != 2 || conds[0].State != occ.StateCleared || conds[0].Key != "mesh-delivery-assigner.controller-call.anchor.web" || conds[1].Key != "mesh-delivery-assigner.controller-pin.laptop.db" { t.Fatalf("the old conditions cleared = %+v", conds) } @@ -1152,3 +1153,22 @@ func aWantWaitsOnTheVerbEdgesTheGraphKeeps(t *testing.T) { } } } + +// decodedAsk is an asks-state value with the router's ask it carries decoded (mesh-sdk keeps it as raw JSON). +type decodedAsk struct { + Asking int + Ask oc.Ask +} + +func decodeAsk(key string, data []byte) (decodedAsk, error) { + a, ask, err := assign.DecodeAsk(key, data) + return decodedAsk{Asking: a.Asking, Ask: ask}, err +} + +func mustAskID(a delivery.AssignerAsk) string { + ask, err := assign.AskOf(a) + if err != nil { + panic(err) + } + return ask.ID +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/puts.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/puts.go index e493328c..a89fb78a 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/puts.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/puts.go @@ -10,7 +10,7 @@ import ( "sync" "time" - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/handacts" @@ -195,14 +195,15 @@ func (p *passer) putWorld(node, module string, put delivery.Put, manifest catalo w.Recorded = policy == catalogue.PolicyRecord if akey, err := delivery.RecordAskKey(node, module, d.Digest); err == nil && put.Ask == akey { // Only this put's own ask counts: an ask (and its answer) of an earlier put under the same key is another asking. - if a, err := delivery.DecodeAssignerAsk(akey, p.w.value(delivery.AssignerAsksBucket, akey)); err == nil && a.Asking == assign.PutAsking(put) { + if a, ask, err := assign.DecodeAsk(akey, p.w.value(delivery.AssignerAsksBucket, akey)); err == nil && a.Asking == assign.PutAsking(put) { switch { case p.answeredWith(akey, assign.OptionYes): w.Answer = assign.OptionYes case p.answeredWith(akey, assign.OptionNo): w.Answer = assign.OptionNo default: - w.AskExpired = !a.Ask.Expires.IsZero() && w.Now.After(a.Ask.Expires) + expires := assign.ExpiresAt(ask) + w.AskExpired = !expires.IsZero() && w.Now.After(expires) } } } @@ -242,13 +243,13 @@ func (p *passer) actOn(ctx context.Context, node, module, id string, w assign.Pu // The put's own ask, written again over one an earlier put left under the key (its asking differs), so the teller // raises it anew under its own id. akey := next.Put.Ask - current, err := delivery.DecodeAssignerAsk(akey, p.w.value(delivery.AssignerAsksBucket, akey)) + current, _, err := assign.DecodeAsk(akey, p.w.value(delivery.AssignerAsksBucket, akey)) if err != nil || current.Asking != assign.PutAsking(next.Put) { a, err := assign.PutAsk(akey, node, module, next.Put, *w.Composed, w.Bus, w.Now) if err != nil { p.logf("the ask about putting %s on %s is not written: %v", module, node, err) } else { - p.put(ctx, delivery.AssignerAsksBucket, akey, func() ([]byte, error) { return delivery.EncodeAssignerAsk(akey, a) }) + p.put(ctx, delivery.AssignerAsksBucket, akey, func() ([]byte, error) { return assign.EncodeAsk(akey, a) }) } } } @@ -257,15 +258,15 @@ func (p *passer) actOn(ctx context.Context, node, module, id string, w assign.Pu } } -// performs checks that the act about to be performed is the one the yes of the ask at key bound (asks.Option.Performs). -func (p *passer) performs(key string, act asks.Act) error { - a, err := delivery.DecodeAssignerAsk(key, p.w.value(delivery.AssignerAsksBucket, key)) +// performs checks that the act about to be performed is the one the yes of the ask at key bound (occ.Performs). +func (p *passer) performs(key string, act occ.Act) error { + _, ask, err := assign.DecodeAsk(key, p.w.value(delivery.AssignerAsksBucket, key)) if err != nil { return fmt.Errorf("the ask %s cannot be read: %w", key, err) } - for _, o := range a.Ask.Options { + for _, o := range ask.Options { if o.ID == assign.OptionYes { - return o.Performs(act) + return occ.Performs(o, act) } } return fmt.Errorf("the ask %s offers no yes", key) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat.go index ceb25001..77ffeb92 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat.go @@ -15,7 +15,7 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/handacts" "git.novox.be/novox/mesh-sdk/go/process" @@ -124,7 +124,7 @@ func (p *passer) sayWaits(unbound []string) { if p.say == nil || p.said[c.Key] && p.saidWaits == state { return } - if c.State == asks.StateCleared && p.saidWaits == "" { + if c.State == occ.StateCleared && p.saidWaits == "" { return // nothing was raised by this process } if err := p.say(c); err != nil { @@ -243,11 +243,16 @@ func (p *passer) ask(ctx context.Context, prop delivery.PlacementProposal, akey, p.logf("the ask about the proposal %s is not written: %v", prop.ID, err) return prop, false } - p.put(ctx, delivery.AssignerAsksBucket, akey, func() ([]byte, error) { return delivery.EncodeAssignerAsk(akey, a) }) - if pk, err := delivery.PulseAskKey(prop.ID); err == nil { - p.pulse(ctx, pk, a.Ask.Expires.Sub(p.clock())) + placed, err := assign.AskOf(a) + if err != nil { + p.logf("the ask about the proposal %s is not written: %v", prop.ID, err) + return prop, false } - prop.State, prop.Ask = delivery.ProposalAsked, a.Ask.ID + p.put(ctx, delivery.AssignerAsksBucket, akey, func() ([]byte, error) { return assign.EncodeAsk(akey, a) }) + if pk, err := delivery.PulseAskKey(prop.ID); err == nil { + p.pulse(ctx, pk, assign.ExpiresAt(placed).Sub(p.clock())) + } + prop.State, prop.Ask = delivery.ProposalAsked, placed.ID return prop, true } diff --git a/modules/mesh-delivery-assigner/go.mod b/modules/mesh-delivery-assigner/go.mod index f1ce8f99..ba3e6520 100644 --- a/modules/mesh-delivery-assigner/go.mod +++ b/modules/mesh-delivery-assigner/go.mod @@ -23,5 +23,11 @@ require ( require git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000 +require ( + git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-00010101000000-000000000000 +) + replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk diff --git a/modules/mesh-delivery-assigner/internal/assign/askjson.go b/modules/mesh-delivery-assigner/internal/assign/askjson.go new file mode 100644 index 00000000..9c1a2b1a --- /dev/null +++ b/modules/mesh-delivery-assigner/internal/assign/askjson.go @@ -0,0 +1,74 @@ +package assign + +import ( + "encoding/json" + "fmt" + "time" + + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// assignerAsk is the asking-th ask a, as the asks state holds it: the router's ask as JSON (delivery.AssignerAsk.Ask). +func assignerAsk(asking int, a oc.Ask) delivery.AssignerAsk { + raw, err := json.Marshal(a) + if err != nil { // an oc.Ask holds only strings, bools and a slice of them: it cannot fail + panic(fmt.Sprintf("assign: the ask %q does not marshal: %v", a.ID, err)) + } + return delivery.AssignerAsk{Asking: asking, Ask: raw} +} + +// AskOf is the router's ask an asks-state value carries. +func AskOf(a delivery.AssignerAsk) (oc.Ask, error) { + var ask oc.Ask + if err := json.Unmarshal(a.Ask, &ask); err != nil { + return oc.Ask{}, fmt.Errorf("assign: the value carries no router's ask: %w", err) + } + return ask, nil +} + +// EncodeAsk is delivery.EncodeAssignerAsk at key, after the checks mesh-sdk no longer makes, since it cannot name the +// router's ask (ADR 0627 §4): the ask's id is delivery.AskingID(key, asking), and it offers at least one option. +func EncodeAsk(key string, a delivery.AssignerAsk) ([]byte, error) { + if _, err := checkedAsk(key, a); err != nil { + return nil, err + } + return delivery.EncodeAssignerAsk(key, a) +} + +func checkedAsk(key string, a delivery.AssignerAsk) (oc.Ask, error) { + ask, err := AskOf(a) + if err != nil { + return oc.Ask{}, err + } + if want := delivery.AskingID(key, a.Asking); ask.ID != want { + return oc.Ask{}, fmt.Errorf("assign: the ask's id %q is not the asking's %q", ask.ID, want) + } + if len(ask.Options) == 0 { + return oc.Ask{}, fmt.Errorf("assign: the ask %q offers no option", ask.ID) + } + return ask, nil +} + +// DecodeAsk reads the ask at key (delivery.DecodeAssignerAsk) with the router's ask it carries. +func DecodeAsk(key string, data []byte) (delivery.AssignerAsk, oc.Ask, error) { + a, err := delivery.DecodeAssignerAsk(key, data) + if err != nil { + return delivery.AssignerAsk{}, oc.Ask{}, err + } + ask, err := checkedAsk(key, a) + if err != nil { + return delivery.AssignerAsk{}, oc.Ask{}, err + } + return a, ask, nil +} + +// ExpiresAt is when the ask ends unanswered; the zero time for an ask that names none. +func ExpiresAt(a oc.Ask) time.Time { + t, err := occ.Time(a.Expires) + if err != nil { + return time.Time{} + } + return t +} diff --git a/modules/mesh-delivery-assigner/internal/assign/asks.go b/modules/mesh-delivery-assigner/internal/assign/asks.go index 70651e77..c7fd22d6 100644 --- a/modules/mesh-delivery-assigner/internal/assign/asks.go +++ b/modules/mesh-delivery-assigner/internal/assign/asks.go @@ -6,7 +6,8 @@ import ( "strings" "time" - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -15,21 +16,21 @@ import ( // record's: the machine whose health check failed and the hold's words (issue 1906). asking numbers the writing: 0 the first, a // larger number an ask written again after a hold lifted by itself, so the router takes it as new. func HoldAsk(key, module, fp, node, words string, asking int, now time.Time) delivery.AssignerAsk { - return delivery.AssignerAsk{Asking: asking, Ask: asks.Ask{ + return assignerAsk(asking, oc.Ask{ ID: delivery.AskingID(key, asking), Headline: fmt.Sprintf("%s is held at %s", module, short(fp)), Explanation: fmt.Sprintf("The health check of %s failed on %s at %s (%s), so the assigner holds that version and wants "+ "no node to move to it. It lifts the hold by itself once every node that runs %s runs it healthy; releasing it "+ "lets the wants move to it now.", module, node, short(fp), words, module), - Options: []asks.Option{ - {ID: "keep", Label: "Keep it held", Does: "the version stays held", Level: asks.Acknowledge}, - {ID: "release", Label: "Release it", Does: "the version is released and the wants move to it by the rules again", Level: asks.Acknowledge}, + Options: []oc.Option{ + {ID: "keep", Label: "Keep it held", Does: "the version stays held", Level: occ.Acknowledge}, + {ID: "release", Label: "Release it", Does: "the version is released and the wants move to it by the rules again", Level: occ.Acknowledge}, }, - Who: asks.Operator, - Expires: now.Add(7 * 24 * time.Hour), + Who: occ.Operator, + Expires: occ.FormatTime(now.Add(7 * 24 * time.Hour)), OnExpiry: "the version stays held", About: key, - }} + }) } // The options of the restore ask (ADR 0537 §3, ADR 0539 §19), and what each does on the node. @@ -42,41 +43,41 @@ const ( // not set back by itself, since a set-back would lose the writes made since. words is the check's words. Restore sets the // node back to the last healthy fingerprint and loses the newer writes; leaving it stopped keeps the data as it is. func RestoreAsk(key, node, module, fp, words string, now time.Time) delivery.AssignerAsk { - return delivery.AssignerAsk{Ask: asks.Ask{ + return assignerAsk(0, oc.Ask{ ID: delivery.AskingID(key, 0), Headline: fmt.Sprintf("%s failed its health check on %s", module, node), Explanation: fmt.Sprintf("%s holds data, and its health check failed on %s at %s: %s. Setting it back to the version that ran healthy "+ "would lose the writes made since; leaving it stopped keeps the data as it is.", module, node, short(fp), words), - Options: []asks.Option{ - {ID: OptionRestore, Label: "Restore it", Does: "the node is set back to the last healthy version and the newer writes are lost", Level: asks.Acknowledge}, - {ID: OptionLeaveStopped, Label: "Leave it stopped", Does: "the module stays as it is on the node and the data is kept", Level: asks.Acknowledge}, + Options: []oc.Option{ + {ID: OptionRestore, Label: "Restore it", Does: "the node is set back to the last healthy version and the newer writes are lost", Level: occ.Acknowledge}, + {ID: OptionLeaveStopped, Label: "Leave it stopped", Does: "the module stays as it is on the node and the data is kept", Level: occ.Acknowledge}, }, - Who: asks.Operator, - Expires: now.Add(7 * 24 * time.Hour), + Who: occ.Operator, + Expires: occ.FormatTime(now.Add(7 * 24 * time.Hour)), OnExpiry: "the module stays as it is on the node", About: key, - }} + }) } // HeldCondition is the condition the assigner raises when it holds a fingerprint of a module (ADR 0429 §6, ADR 0539 §19), or // clears (raised false) when the hold is released. It names the module, the node whose health check failed and the check's // words, under the assigner's own name (ADR 0437 clause 9), keyed by the held fingerprint. needsYou is true for a module holding // data, whose restore ask waits for the operator. -func HeldCondition(module, fp, node, words string, raised, needsYou bool) asks.Condition { +func HeldCondition(module, fp, node, words string, raised, needsYou bool) oc.Condition { key := delivery.AssignerModule + ".held." + module + "." + strings.TrimPrefix(fp, delivery.SourcePrefix2) - subject := asks.Scope{Scope: "module", ID: module, Machine: node} + subject := oc.ConditionSubject{Scope: "module", ID: module, Machine: node} if !raised { - return asks.Condition{Key: key, Subject: subject, State: asks.StateCleared} + return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared} } - c := asks.Condition{Key: key, Severity: asks.SeverityWarning, Subject: subject, State: asks.StateRaised, + c := oc.Condition{Key: key, Severity: occ.SeverityWarning, Subject: subject, State: occ.StateRaised, Summary: fmt.Sprintf("%s is held at %s", module, short(fp)), Headline: fmt.Sprintf("%s is held at %s", module, short(fp))} what := fmt.Sprintf("The health check of %s failed on %s at %s: %s. The assigner holds that version and wants no node to move to it.", module, node, short(fp), words) if needsYou { c.Needs = "answer whether to restore " + module + " on " + node - c.Explanation = asks.NeedsYou + c.Needs + ". " + what + c.Explanation = occ.NeedsYou + c.Needs + ". " + what } else { - c.Explanation = asks.NothingToDo + " " + what + " Nodes that wanted it are set back to the version that ran healthy." + c.Explanation = occ.NothingToDo + " " + what + " Nodes that wanted it are set back to the version that ran healthy." } return c } @@ -85,22 +86,22 @@ func HeldCondition(module, fp, node, words string, raised, needsYou bool) asks.C // node while other nodes run it healthy (ADR 0557 §1, issue 1496): the failure is the node's, so nothing is held or set back and // the node keeps its want. It names the node, the module, fp and the have's words, keyed by node and module, and is cleared // (raised false) once the node no longer fails alone. -func NodeFailedCondition(node, module, fp string, f NodeFailure, raised bool) asks.Condition { +func NodeFailedCondition(node, module, fp string, f NodeFailure, raised bool) oc.Condition { key := delivery.AssignerModule + ".failed-alone." + node + "." + module - subject := asks.Scope{Scope: "module", ID: module, Machine: node} + subject := oc.ConditionSubject{Scope: "module", ID: module, Machine: node} if !raised { - return asks.Condition{Key: key, Subject: subject, State: asks.StateCleared, + return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared, Resolved: fmt.Sprintf("%s on %s no longer fails its health check alone.", module, node)} } headline := fmt.Sprintf("%s fails on %s alone", module, node) - if n := len([]rune(headline)); n > asks.HeadlineLength { - headline = string([]rune(headline)[:asks.HeadlineLength]) + if n := len([]rune(headline)); n > occ.HeadlineLength { + headline = string([]rune(headline)[:occ.HeadlineLength]) } - return asks.Condition{Key: key, Severity: asks.SeverityWarning, Subject: subject, State: asks.StateRaised, + return oc.Condition{Key: key, Severity: occ.SeverityWarning, Subject: subject, State: occ.StateRaised, Summary: headline, Headline: headline, Explanation: fmt.Sprintf("%s The health check of %s at %s failed on %s (%s), while it runs healthy on %s. The failure is %s's, "+ "so the assigner holds nothing and sets no node back, and %s keeps its want. This clears once %s runs it healthy or "+ - "another version is the newest.", asks.NothingToDo, module, short(fp), node, f.Words, strings.Join(f.Others, ", "), node, + "another version is the newest.", occ.NothingToDo, module, short(fp), node, f.Words, strings.Join(f.Others, ", "), node, node, node)} } @@ -108,28 +109,28 @@ func NodeFailedCondition(node, module, fp string, f NodeFailure, raised bool) as // gives up every module with a fingerprint of the new form, so nothing applies it there and its files are gone. It is raised (urgent) // naming the module, the node and the rule holding its first want back with the rule's words, under the assigner's own name keyed by // node and module, and cleared (raised false) when the node has a want for it again. -func NeitherCondition(node, module string, h Holding, raised bool) asks.Condition { +func NeitherCondition(node, module string, h Holding, raised bool) oc.Condition { key := delivery.AssignerModule + ".neither." + node + "." + module - subject := asks.Scope{Scope: "module", ID: module, Machine: node} + subject := oc.ConditionSubject{Scope: "module", ID: module, Machine: node} if !raised { - return asks.Condition{Key: key, Subject: subject, State: asks.StateCleared, + return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared, Resolved: fmt.Sprintf("%s on %s has a want again, so its own composition is applied there.", module, node)} } headline := fmt.Sprintf("%s on %s is on no list", module, node) - if n := len([]rune(headline)); n > asks.HeadlineLength { - headline = string([]rune(headline)[:asks.HeadlineLength]) + if n := len([]rune(headline)); n > occ.HeadlineLength { + headline = string([]rune(headline)[:occ.HeadlineLength]) } - return asks.Condition{Key: key, Severity: asks.SeverityUrgent, Subject: subject, State: asks.StateRaised, + return oc.Condition{Key: key, Severity: occ.SeverityUrgent, Subject: subject, State: occ.StateRaised, Summary: headline, Headline: headline, Explanation: fmt.Sprintf("%s %s is assigned to %s, but the node holds no want for it and the node's own composition does not "+ "hold it either, so nothing applies it there. The assigner's rule %d holds its first want back: %s. The assigner writes the want once the "+ - "rule holds, and clears this.", asks.NothingToDo, module, node, h.Rule, h.Words)} + "rule holds, and clears this.", occ.NothingToDo, module, node, h.Rule, h.Words)} } // NeitherLeftCondition clears the neither condition of module on node when it is no longer on neither list: why is "" when the // node has a live want for it again, else why it has none (it is held, its move waits for an ask, the node no longer runs it), so // the clear never says a want the node does not hold (issue 1945: "nats on novox has a want again" while it had none). -func NeitherLeftCondition(node, module, why string) asks.Condition { +func NeitherLeftCondition(node, module, why string) oc.Condition { c := NeitherCondition(node, module, Holding{}, false) if why != "" { c.Resolved = fmt.Sprintf("%s on %s is no longer on neither list and has no want: %s.", module, node, why) @@ -138,7 +139,7 @@ func NeitherLeftCondition(node, module, why string) asks.Condition { } // LiftedCondition clears the condition of a held fingerprint the assigner lifted by itself, saying why (issue 1906). -func LiftedCondition(module, fp string) asks.Condition { +func LiftedCondition(module, fp string) oc.Condition { c := HeldCondition(module, fp, "", "", false, false) c.Resolved = LiftedWords(module, fp) + "." return c @@ -161,20 +162,20 @@ func RecordAsk(key, node, module, fp string, need RecordNeed, now time.Time) del delivery.JudgedMovesData: "moves the module's data", delivery.JudgedCannot: "cannot be judged: " + need.Words, }[need.Judgement] - return delivery.AssignerAsk{Ask: asks.Ask{ + return assignerAsk(0, oc.Ask{ ID: delivery.AskingID(key, 0), Headline: fmt.Sprintf("%s waits for your yes on %s", module, node), Explanation: fmt.Sprintf("%s is recorded: a person takes each of its moves. The next one on %s (version %s) %s. "+ "The node is told to apply it only after you say yes.", module, node, short(fp), what), - Options: []asks.Option{ - {ID: OptionYes, Label: "Yes, move it", Does: "the node is told to apply the new composition of " + module, Level: asks.Acknowledge}, - {ID: OptionNo, Label: "No, not now", Does: "the node keeps what it runs", Level: asks.Acknowledge}, + Options: []oc.Option{ + {ID: OptionYes, Label: "Yes, move it", Does: "the node is told to apply the new composition of " + module, Level: occ.Acknowledge}, + {ID: OptionNo, Label: "No, not now", Does: "the node keeps what it runs", Level: occ.Acknowledge}, }, - Who: asks.Operator, - Expires: now.Add(7 * 24 * time.Hour), + Who: occ.Operator, + Expires: occ.FormatTime(now.Add(7 * 24 * time.Hour)), OnExpiry: "the node keeps what it runs", About: key, - }} + }) } // NextBackupAsking is the number of the asking that follows the one numbered after (a negative number: none yet). Numbers 0 and 1 @@ -192,7 +193,11 @@ var backupDigest = regexp.MustCompile(`\(composition (sha256:[0-9a-f]{64})\)`) // none. An answer is taken for the composition of the ask it answers, and a new revision at the same fingerprint is asked again // under a new number (ADR 0539 §16). func BackupAskDigest(a delivery.AssignerAsk) string { - if m := backupDigest.FindStringSubmatch(a.Ask.Explanation); m != nil { + ask, err := AskOf(a) + if err != nil { + return "" + } + if m := backupDigest.FindStringSubmatch(ask.Explanation); m != nil { return m[1] } return "" @@ -208,33 +213,33 @@ func BackupAnswerWords(option string, asking int) string { // without a backup. digest is the composition about to be applied and asking the number of the asking, 0 the first; words are the // backup's own words, from the node's have. func BackupAsk(key, node, module, fp, digest, words string, asking int, now time.Time) delivery.AssignerAsk { - return delivery.AssignerAsk{Asking: asking, Ask: asks.Ask{ + return assignerAsk(asking, oc.Ask{ ID: delivery.AskingID(key, asking), Headline: fmt.Sprintf("The backup of %s failed on %s", module, node), Explanation: fmt.Sprintf("The backup before applying %s at %s on %s failed: %s. %s keeps running what it has until you answer. (composition %s)", module, short(fp), node, words, module, digest), - Options: []asks.Option{ - {ID: OptionBackupAgain, Label: "Back up again", Does: "the node tries the backup again before it applies", Level: asks.Acknowledge}, - {ID: OptionBackupSkip, Label: "Go on without a backup", Does: "the node applies the new version without a backup", Level: asks.Acknowledge}, + Options: []oc.Option{ + {ID: OptionBackupAgain, Label: "Back up again", Does: "the node tries the backup again before it applies", Level: occ.Acknowledge}, + {ID: OptionBackupSkip, Label: "Go on without a backup", Does: "the node applies the new version without a backup", Level: occ.Acknowledge}, }, - Who: asks.Operator, - Expires: now.Add(7 * 24 * time.Hour), + Who: occ.Operator, + Expires: occ.FormatTime(now.Add(7 * 24 * time.Hour)), OnExpiry: "the module keeps running what it has", About: key, - }} + }) } // BackupCondition is the condition raised when a backup before an apply fails and the module is held on the node (ADR 0539 §16), // naming the node, the module and the backup's words; cleared under the same key. -func BackupCondition(node, module, fp, words string, raised bool) asks.Condition { +func BackupCondition(node, module, fp, words string, raised bool) oc.Condition { key := delivery.AssignerModule + ".backup." + node + "." + module + "." + strings.TrimPrefix(fp, delivery.SourcePrefix2) - subject := asks.Scope{Scope: "module", ID: module, Machine: node} + subject := oc.ConditionSubject{Scope: "module", ID: module, Machine: node} if !raised { - return asks.Condition{Key: key, Subject: subject, State: asks.StateCleared} + return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared} } headline := fmt.Sprintf("The backup of %s failed on %s", module, node) needs := "answer whether to back up again or go on without a backup" - return asks.Condition{Key: key, Severity: asks.SeverityWarning, Subject: subject, State: asks.StateRaised, + return oc.Condition{Key: key, Severity: occ.SeverityWarning, Subject: subject, State: occ.StateRaised, Summary: headline, Headline: headline, Needs: needs, - Explanation: asks.NeedsYou + needs + ". The backup before applying " + module + " at " + short(fp) + " on " + node + " failed: " + words + "."} + Explanation: occ.NeedsYou + needs + ". The backup before applying " + module + " at " + short(fp) + " on " + node + " failed: " + words + "."} } diff --git a/modules/mesh-delivery-assigner/internal/assign/asks_test.go b/modules/mesh-delivery-assigner/internal/assign/asks_test.go index 324002a0..0c5b5f20 100644 --- a/modules/mesh-delivery-assigner/internal/assign/asks_test.go +++ b/modules/mesh-delivery-assigner/internal/assign/asks_test.go @@ -1,12 +1,14 @@ package assign import ( + "fmt" "slices" "strings" "testing" "time" - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/health" ) @@ -20,27 +22,42 @@ func TestTheHoldAskIsValid(t *testing.T) { } now := time.Now() a := HoldAsk(key, "mail", fNew, "ace", "health failed, phase running: connection refused", 0, now) - if err := a.Ask.Check(now); err != nil { + if err := occ.Check(mustAsk(t, a), now); err != nil { t.Errorf("the router refuses the ask: %v", err) } - if _, err := delivery.EncodeAssignerAsk(key, a); err != nil { + if _, err := EncodeAsk(key, a); err != nil { t.Errorf("the ask cannot be written: %v", err) } // 1906/H3: the ask names the machine whose check failed and the hold's words. - if !strings.Contains(a.Ask.Explanation, "on ace") || !strings.Contains(a.Ask.Explanation, "connection refused") { - t.Errorf("the ask does not name the node and the hold's words: %q", a.Ask.Explanation) + if !strings.Contains(mustAsk(t, a).Explanation, "on ace") || !strings.Contains(mustAsk(t, a).Explanation, "connection refused") { + t.Errorf("the ask does not name the node and the hold's words: %q", mustAsk(t, a).Explanation) } // 1906/H2: an ask written again after a lift is another asking, under an id of its own. again := HoldAsk(key, "mail", fNew, "ace", "x", 7, now) - if again.Asking != 7 || again.Ask.ID != delivery.AskingID(key, 7) || again.Ask.ID == a.Ask.ID { - t.Errorf("the asking after a lift = %d %s", again.Asking, again.Ask.ID) + if again.Asking != 7 || mustAsk(t, again).ID != delivery.AskingID(key, 7) || mustAsk(t, again).ID == mustAsk(t, a).ID { + t.Errorf("the asking after a lift = %d %s", again.Asking, mustAsk(t, again).ID) } - if _, err := delivery.EncodeAssignerAsk(key, again); err != nil { + if _, err := EncodeAsk(key, again); err != nil { t.Errorf("the ask asked again cannot be written: %v", err) } if _, err := delivery.ViewAnswerKey(key); err != nil { t.Errorf("the view may not answer it: %v", err) } + // The join (A1d ruling 1): mesh-sdk no longer refuses another id, no option or no ask in an assigner's ask; EncodeAsk does. + t.Run("EncodeAsk refuses what mesh-sdk no longer does", func(t *testing.T) { + good := HoldAsk(key, "mail", fNew, "ace", "x", 0, time.Unix(100, 0)) + other := good + other.Asking = 3 // its id is the asking 0's + empty := good + ask := mustAsk(t, good) + ask.Options = nil + empty.Ask = assignerAsk(0, ask).Ask + for name, a := range map[string]delivery.AssignerAsk{"another id": other, "no option": empty, "no ask": {}} { + if _, err := EncodeAsk(key, a); err == nil { + t.Errorf("%s: written", name) + } + } + }) } // 1906/D3 (ADR 0539 §19): the ask about a data module held on a node is a valid ask of the router and of mesh-sdk's layout at @@ -52,24 +69,24 @@ func TestTheRestoreAskIsValid(t *testing.T) { } now := time.Now() a := RestoreAsk(key, "ace", "mail", fNew, "connection refused", now) - if err := a.Ask.Check(now); err != nil { + if err := occ.Check(mustAsk(t, a), now); err != nil { t.Errorf("the router refuses the ask: %v", err) } - if _, err := delivery.EncodeAssignerAsk(key, a); err != nil { + if _, err := EncodeAsk(key, a); err != nil { t.Errorf("the ask cannot be written: %v", err) } if _, err := delivery.ViewAnswerKey(key); err != nil { t.Errorf("the view may not answer it: %v", err) } var ids []string - for _, o := range a.Ask.Options { + for _, o := range mustAsk(t, a).Options { ids = append(ids, o.ID) } if len(ids) != 2 || ids[0] != OptionRestore || ids[1] != OptionLeaveStopped { t.Errorf("options = %v", ids) } - if !strings.Contains(a.Ask.Explanation, "connection refused") || !strings.Contains(a.Ask.Explanation, "ace") { - t.Errorf("the ask does not name the node and the check's words: %q", a.Ask.Explanation) + if !strings.Contains(mustAsk(t, a).Explanation, "connection refused") || !strings.Contains(mustAsk(t, a).Explanation, "ace") { + t.Errorf("the ask does not name the node and the check's words: %q", mustAsk(t, a).Explanation) } } @@ -83,13 +100,13 @@ func TestTheAssignersConditions(t *testing.T) { func heldConditionNamesModuleNodeAndWords(t *testing.T) { c := HeldCondition("mail", fNew, "ace", "connection refused", true, true) - if _, err := asks.EncodeCondition(c); err != nil { + if _, err := occ.EncodeCondition(c); err != nil { t.Fatalf("the condition is refused: %v", err) } - if c.State != asks.StateRaised || c.Subject.ID != "mail" || c.Subject.Machine != "ace" || !strings.Contains(c.Explanation, "connection refused") || !strings.Contains(c.Key, "mail") { + if c.State != occ.StateRaised || c.Subject.ID != "mail" || c.Subject.Machine != "ace" || !strings.Contains(c.Explanation, "connection refused") || !strings.Contains(c.Key, "mail") { t.Errorf("condition = %+v", c) } - if got := HeldCondition("mail", fNew, "", "", false, false); got.State != asks.StateCleared || got.Key != c.Key { + if got := HeldCondition("mail", fNew, "", "", false, false); got.State != occ.StateCleared || got.Key != c.Key { t.Errorf("cleared = %+v", got) } } @@ -97,10 +114,10 @@ func heldConditionNamesModuleNodeAndWords(t *testing.T) { // 1906/H2: a hold lifted by itself clears the held condition under its key, saying why. func liftedHoldClearsItsConditionSayingWhy(t *testing.T) { c := LiftedCondition("mail", fNew) - if _, err := asks.EncodeCondition(c); err != nil { + if _, err := occ.EncodeCondition(c); err != nil { t.Fatalf("the condition is refused: %v", err) } - if c.State != asks.StateCleared || c.Key != HeldCondition("mail", fNew, "ace", "x", true, false).Key || !strings.Contains(c.Resolved, "lifted its hold by itself") { + if c.State != occ.StateCleared || c.Key != HeldCondition("mail", fNew, "ace", "x", true, false).Key || !strings.Contains(c.Resolved, "lifted its hold by itself") { t.Errorf("condition = %+v", c) } } @@ -112,7 +129,7 @@ func liftedHoldClearsItsConditionSayingWhy(t *testing.T) { func saidConditionsAreFindings(t *testing.T) { at := time.Date(2026, 10, 29, 21, 0, 0, 0, time.UTC) for _, c := range []struct { - raised, cleared asks.Condition + raised, cleared oc.Condition key string }{ {HeldCondition("mail", fNew, "ace", "refused", true, true), LiftedCondition("mail", fNew), "module.mail." + strings.TrimPrefix(fNew, delivery.SourcePrefix2) + ".held"}, @@ -127,7 +144,7 @@ func saidConditionsAreFindings(t *testing.T) { puts[k] = got return err }, Delete: func(k string) error { deletes = append(deletes, k); return nil }, Now: func() time.Time { return at }} - say := f.Say(func(asks.Condition) error { said++; return nil }) + say := f.Say(func(oc.Condition) error { said++; return nil }) if err := say(c.raised); err != nil { t.Fatalf("%s: %v", c.key, err) } @@ -139,11 +156,11 @@ func saidConditionsAreFindings(t *testing.T) { if err := say(c.cleared); err != nil || !slices.Equal(deletes, []string{c.key}) || said != 0 { t.Fatalf("%s: deleted %v, said %d (%v)", c.key, deletes, said, err) } - if err := (Findings{}).Say(func(asks.Condition) error { said++; return nil })(c.raised); err != nil || said != 1 { + if err := (Findings{}).Say(func(oc.Condition) error { said++; return nil })(c.raised); err != nil || said != 1 { t.Fatalf("%s: with no findings bucket said %d (%v), want straight once", c.key, said, err) } } - if _, _, _, err := FindingOf(asks.Condition{Key: "another.x", State: asks.StateRaised}, at); err == nil { + if _, _, _, err := FindingOf(oc.Condition{Key: "another.x", State: occ.StateRaised}, at); err == nil { t.Error("a condition not the assigner's is a finding") } } @@ -164,24 +181,24 @@ func TestTheRecordAndBackupAsksAreValid(t *testing.T) { dgst := "sha256:" + strings.Repeat("b", 64) b := BackupAsk(bkey, "ace", "mail", fNew, dgst, "no space left", 0, now) for key, a := range map[string]delivery.AssignerAsk{rkey: r, bkey: b} { - if err := a.Ask.Check(now); err != nil { + if err := occ.Check(mustAsk(t, a), now); err != nil { t.Errorf("%s: the router refuses the ask: %v", key, err) } - if _, err := delivery.EncodeAssignerAsk(key, a); err != nil { + if _, err := EncodeAsk(key, a); err != nil { t.Errorf("%s: cannot be written: %v", key, err) } if _, err := delivery.ViewAnswerKey(key); err != nil { t.Errorf("%s: the view may not answer it: %v", key, err) } } - if !strings.Contains(r.Ask.Explanation, "gone") || r.Ask.Options[0].ID != OptionYes { - t.Errorf("the record ask = %+v", r.Ask) + if !strings.Contains(mustAsk(t, r).Explanation, "gone") || mustAsk(t, r).Options[0].ID != OptionYes { + t.Errorf("the record ask = %+v", mustAsk(t, r)) } - if !strings.Contains(b.Ask.Explanation, "no space left") || b.Ask.Options[0].ID != delivery.BackupAgain || b.Ask.Options[1].ID != delivery.BackupSkip { - t.Errorf("the backup ask = %+v", b.Ask) + if !strings.Contains(mustAsk(t, b).Explanation, "no space left") || mustAsk(t, b).Options[0].ID != delivery.BackupAgain || mustAsk(t, b).Options[1].ID != delivery.BackupSkip { + t.Errorf("the backup ask = %+v", mustAsk(t, b)) } // the id binds the ask to the composition and to its number (review round 1, items 3 and 4) - if again := BackupAsk(bkey, "ace", "mail", fNew, dgst, "x", NextBackupAsking(0), now); again.Ask.ID == b.Ask.ID { + if again := BackupAsk(bkey, "ace", "mail", fNew, dgst, "x", NextBackupAsking(0), now); mustAsk(t, again).ID == mustAsk(t, b).ID { t.Errorf("asked again under the same id") } if NextBackupAsking(-1) != 0 || NextBackupAsking(0) != 2 || NextBackupAsking(2) != 3 { @@ -194,7 +211,7 @@ func TestTheRecordAndBackupAsksAreValid(t *testing.T) { t.Errorf("two answers of the same option are one want value") } c := BackupCondition("ace", "mail", fNew, "no space left", true) - if _, err := asks.EncodeCondition(c); err != nil { + if _, err := occ.EncodeCondition(c); err != nil { t.Errorf("the backup condition is refused: %v", err) } // ADR 0546 clauses 7 and 8: a put's ask is valid at the record ask's key; for the bus its yes approves and binds the act, @@ -206,32 +223,39 @@ func TestTheRecordAndBackupAsksAreValid(t *testing.T) { if err != nil { t.Fatalf("bus %v: %v", bus, err) } - if err := a.Ask.Check(now); err != nil { + if err := occ.Check(mustAsk(t, a), now); err != nil { t.Errorf("bus %v: the router refuses the put's ask: %v", bus, err) } - if _, err := delivery.EncodeAssignerAsk(rkey, a); err != nil { + if _, err := EncodeAsk(rkey, a); err != nil { t.Errorf("bus %v: the put's ask cannot be written: %v", bus, err) } - viewed := delivery.ViewAnswerFits(delivery.ViewAnswer{Asking: a.Ask.ID, Option: OptionYes}, a) - yes := a.Ask.Options[0] + viewed := delivery.ViewAnswerFits(delivery.ViewAnswer{Asking: mustAsk(t, a).ID, Option: OptionYes}, rkey, a) + yes := mustAsk(t, a).Options[0] + // mesh-sdk gives no refusal by level any more: the teller refuses an option a view cannot answer (its level is not + // acknowledge), as it did through the sdk before. + if viewed == nil { + if o, ok := occ.OptionOf(mustAsk(t, a), OptionYes); ok && o.Level != "" && o.Level != occ.Acknowledge { + viewed = fmt.Errorf("only a channel that proves who answered takes the option %s", o.ID) + } + } switch { - case !strings.Contains(a.Ask.Explanation, "the bus's user list"): - t.Errorf("bus %v: the put's ask does not say why: %s", bus, a.Ask.Explanation) - case bus && (yes.Level != asks.Approve || yes.Binds == "" || viewed == nil || a.Ask.Expires.After(now.Add(asks.ApproveLasts))): - t.Errorf("bus: the yes is %+v, the view's answer %v, expiring %s", yes, viewed, a.Ask.Expires) - case !bus && (yes.Level != asks.Acknowledge || viewed != nil): + case !strings.Contains(mustAsk(t, a).Explanation, "the bus's user list"): + t.Errorf("bus %v: the put's ask does not say why: %s", bus, mustAsk(t, a).Explanation) + case bus && (yes.Level != occ.Approve || yes.Binds == "" || viewed == nil || expiresAt(t, a).After(now.Add(occ.ApproveLasts))): + t.Errorf("bus: the yes is %+v, the view's answer %v, expiring %s", yes, viewed, expiresAt(t, a)) + case !bus && (yes.Level != occ.Acknowledge || viewed != nil): t.Errorf("a recorded module: the yes is %+v, the view's answer %v", yes, viewed) } later := put later.At = now.Add(time.Minute) b, _ := PutAsk(rkey, "ace", "mail", later, d, bus, now) - if b.Ask.ID == a.Ask.ID || b.Asking != PutAsking(later) { - t.Errorf("bus %v: a later put's ask is asking %d under id %q, the earlier's %q", bus, b.Asking, b.Ask.ID, a.Ask.ID) + if mustAsk(t, b).ID == mustAsk(t, a).ID || b.Asking != PutAsking(later) { + t.Errorf("bus %v: a later put's ask is asking %d under id %q, the earlier's %q", bus, b.Asking, mustAsk(t, b).ID, mustAsk(t, a).ID) } - if err := yes.Performs(PutAct("ace", "mail", fNew, dgst)); err != nil { + if err := occ.Performs(yes, PutAct("ace", "mail", fNew, dgst)); err != nil { t.Errorf("bus %v: the yes does not perform the act it bound: %v", bus, err) } - if err := yes.Performs(PutAct("ace", "mail", fNew, dig(fOld))); bus && err == nil { + if err := occ.Performs(yes, PutAct("ace", "mail", fNew, dig(fOld))); bus && err == nil { t.Errorf("bus: the yes performs another composition's act") } } @@ -241,14 +265,29 @@ func TestTheRecordAndBackupAsksAreValid(t *testing.T) { // under the assigner's own name keyed by node and module, and is cleared under the same key. func TestTheNeitherConditionNamesModuleNodeAndRule(t *testing.T) { c := NeitherCondition("g14", "i3status-rust", Holding{Rule: delivery.RuleProviders, Words: "no provider of x11-display runs healthy on g14 or at its canary"}, true) - if _, err := asks.EncodeCondition(c); err != nil { + if _, err := occ.EncodeCondition(c); err != nil { t.Fatalf("the condition is refused: %v", err) } - if c.State != asks.StateRaised || c.Severity != asks.SeverityUrgent || c.Subject.ID != "i3status-rust" || c.Subject.Machine != "g14" || + if c.State != occ.StateRaised || c.Severity != occ.SeverityUrgent || c.Subject.ID != "i3status-rust" || c.Subject.Machine != "g14" || !strings.Contains(c.Explanation, "x11-display") || !strings.Contains(c.Explanation, "rule 5") || c.Key != "mesh-delivery-assigner.neither.g14.i3status-rust" { t.Errorf("condition = %+v", c) } - if got := NeitherCondition("g14", "i3status-rust", Holding{}, false); got.State != asks.StateCleared || got.Key != c.Key { + if got := NeitherCondition("g14", "i3status-rust", Holding{}, false); got.State != occ.StateCleared || got.Key != c.Key { t.Errorf("cleared = %+v", got) } } + +// mustAsk is the router's ask an asks-state value carries. +func mustAsk(t *testing.T, a delivery.AssignerAsk) oc.Ask { + t.Helper() + ask, err := AskOf(a) + if err != nil { + t.Fatal(err) + } + return ask +} + +func expiresAt(t *testing.T, a delivery.AssignerAsk) time.Time { + t.Helper() + return ExpiresAt(mustAsk(t, a)) +} diff --git a/modules/mesh-delivery-assigner/internal/assign/findings.go b/modules/mesh-delivery-assigner/internal/assign/findings.go index 0840a0bd..bc20d30f 100644 --- a/modules/mesh-delivery-assigner/internal/assign/findings.go +++ b/modules/mesh-delivery-assigner/internal/assign/findings.go @@ -6,7 +6,8 @@ import ( "strings" "time" - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/health" ) @@ -24,7 +25,7 @@ var FindingsBucket = health.FindingsBucket(delivery.AssignerModule) // FindingOf is a said condition as a finding: its key `..`, from the condition's own key // `mesh-delivery-assigner..` (the subject's id when the key names none), its words unchanged. raised is false for // a clear, whose key is to be deleted; value is nil then. -func FindingOf(c asks.Condition, at time.Time) (key string, value []byte, raised bool, err error) { +func FindingOf(c oc.Condition, at time.Time) (key string, value []byte, raised bool, err error) { rest, ok := strings.CutPrefix(c.Key, delivery.AssignerModule+".") if !ok || rest == "" { return "", nil, false, fmt.Errorf("the condition %q is not the assigner's", c.Key) @@ -38,10 +39,10 @@ func FindingOf(c asks.Condition, at time.Time) (key string, value []byte, raised scope = health.ScopeModule } key = health.FindingKey(scope, id, kind) - if c.State == asks.StateCleared { + if c.State == occ.StateCleared { return key, nil, false, nil } - if c.State != asks.StateRaised { + if c.State != occ.StateRaised { return "", nil, false, fmt.Errorf("the condition %s is neither raised nor cleared", c.Key) } summary := c.Summary @@ -70,8 +71,8 @@ type Findings struct { // Say is say with each condition written as a finding (or its finding deleted), which the condition keeper carries, the // one source since the controller's end (novox/hq ADR 0590 §3); said straight to the router only where there is no // findings bucket at all. A finding that cannot be written is the error. -func (f Findings) Say(say func(asks.Condition) error) func(asks.Condition) error { - return func(c asks.Condition) error { +func (f Findings) Say(say func(oc.Condition) error) func(oc.Condition) error { + return func(c oc.Condition) error { var errs []error if f.Put != nil { now := time.Now diff --git a/modules/mesh-delivery-assigner/internal/assign/placement.go b/modules/mesh-delivery-assigner/internal/assign/placement.go index 5019a0e9..d6dc84d5 100644 --- a/modules/mesh-delivery-assigner/internal/assign/placement.go +++ b/modules/mesh-delivery-assigner/internal/assign/placement.go @@ -3,20 +3,21 @@ package assign import ( "strings" - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/delivery" ) // SeatWaitsCondition is the assigner's word on the seat mesh-placement while a state of the seat is not bound (ADR 0567 // §1): raised naming each, cleared when none is left. -func SeatWaitsCondition(unbound []string) asks.Condition { +func SeatWaitsCondition(unbound []string) oc.Condition { key := delivery.AssignerModule + ".seat-states" - subject := asks.Scope{Scope: "module", ID: delivery.AssignerModule} + subject := oc.ConditionSubject{Scope: "module", ID: delivery.AssignerModule} if len(unbound) == 0 { - return asks.Condition{Key: key, Subject: subject, State: asks.StateCleared, Resolved: "the seat mesh-placement holds all its states."} + return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared, Resolved: "the seat mesh-placement holds all its states."} } why := "the seat waits for its states: " + strings.Join(unbound, ", ") + " are not on the bus or not granted yet" - return asks.Condition{Key: key, Severity: asks.SeverityWarning, Subject: subject, State: asks.StateRaised, + return oc.Condition{Key: key, Severity: occ.SeverityWarning, Subject: subject, State: occ.StateRaised, Summary: "the seat waits for its states", Headline: "the seat waits for its states", - Explanation: asks.NothingToDo + " " + why + "; each is bound at the next pass once it is, with no restart."} + Explanation: occ.NothingToDo + " " + why + "; each is bound at the next pass once it is, with no restart."} } diff --git a/modules/mesh-delivery-assigner/internal/assign/policy.go b/modules/mesh-delivery-assigner/internal/assign/policy.go index fab4630b..a4ce3cd1 100644 --- a/modules/mesh-delivery-assigner/internal/assign/policy.go +++ b/modules/mesh-delivery-assigner/internal/assign/policy.go @@ -7,7 +7,7 @@ import ( "strings" "time" - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/health" @@ -70,7 +70,7 @@ func ProvisionFailedFinding(key string, p provisions.Provision, at time.Time) ([ return health.EncodeFinding(health.Finding{Key: ProvisionFailedKey(key), Kind: KindProvisionFailed, Subject: health.Subject{Scope: health.ScopeProvider, ID: key, Machine: p.Consumer.Node, Also: also(p.Provider.Node, p.Consumer.Node)}, Severity: health.Warning, Summary: summary, Headline: clipHeadline(p.Consumer.Module + " waits for a provision that failed"), - Explanation: asks.NothingToDo + " " + summary + ". It clears once the provider provides it.", + Explanation: occ.NothingToDo + " " + summary + ". It clears once the provider provides it.", Resolved: p.Provision + " for " + p.Consumer.Module + " is provided", Evidence: []health.Evidence{{At: at.UTC(), Said: p.Why}}, Source: "assigner." + KindProvisionFailed, Observer: delivery.AssignerModule, At: at.UTC()}) } @@ -102,7 +102,7 @@ func ProvisionMissingFinding(key string, bound time.Duration, at time.Time) ([]b return health.EncodeFinding(health.Finding{Key: ProvisionMissingKey(key), Kind: KindProvisionMissing, Subject: health.Subject{Scope: health.ScopeProvider, ID: key, Machine: node}, Severity: health.Warning, Summary: summary, Headline: clipHeadline(module + " on " + node + " lacks " + provision), - Explanation: asks.NothingToDo + " " + summary + ". It clears once the provision has its key.", + Explanation: occ.NothingToDo + " " + summary + ". It clears once the provision has its key.", Resolved: provision + " for " + module + " on " + node + " has its key", Evidence: []health.Evidence{{At: at.UTC(), Said: summary}}, Source: "assigner." + KindProvisionMissing, Observer: delivery.AssignerModule, At: at.UTC()}) } @@ -122,7 +122,7 @@ func UnplacedFinding(node string, at time.Time) ([]byte, error) { return health.EncodeFinding(health.Finding{Key: UnplacedKey(node), Kind: KindUnplaced, Subject: health.Subject{Scope: health.ScopeMachine, ID: node, Machine: node}, Severity: health.Warning, Summary: summary, Headline: clipHeadline(node + " has no placement"), Needs: "place it with mesh-placement.propose.", - Explanation: asks.NeedsYou + "place it with mesh-placement.propose. " + summary + ".", + Explanation: occ.NeedsYou + "place it with mesh-placement.propose. " + summary + ".", Resolved: node + " is placed", Evidence: []health.Evidence{{At: at.UTC(), Said: summary}}, Source: "assigner." + KindUnplaced, Observer: delivery.AssignerModule, At: at.UTC()}) } diff --git a/modules/mesh-delivery-assigner/internal/assign/put.go b/modules/mesh-delivery-assigner/internal/assign/put.go index 46f993ce..2c99034f 100644 --- a/modules/mesh-delivery-assigner/internal/assign/put.go +++ b/modules/mesh-delivery-assigner/internal/assign/put.go @@ -4,7 +4,8 @@ import ( "fmt" "time" - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -210,15 +211,15 @@ func PutAsking(p delivery.Put) int { } // PutAct is the act a bus put's yes binds and that is performed when its want is written. -func PutAct(node, module, fp, composed string) asks.Act { - return asks.Act{"put": "want", "node": node, "module": module, "fp": fp, "composed": composed} +func PutAct(node, module, fp, composed string) occ.Act { + return occ.Act{"put": "want", "node": node, "module": module, "fp": fp, "composed": composed} } // PutAsk is the ask about a put's move (ADR 0546 clauses 7 and 8), under the record ask's key of the composition so the // teller relays it and its answer as any record ask's. For the bus its yes is of level approve and binds the act, so only a // channel that proves who answered takes it (ADR 0259), never the view. func PutAsk(key, node, module string, p delivery.Put, d delivery.ComposedDigest, bus bool, now time.Time) (delivery.AssignerAsk, error) { - yes := asks.Option{ID: OptionYes, Label: "Yes, put it", Does: "the node is told to apply " + module + " at " + short(p.FP), Level: asks.Acknowledge} + yes := oc.Option{ID: OptionYes, Label: "Yes, put it", Does: "the node is told to apply " + module + " at " + short(p.FP), Level: occ.Acknowledge} what := map[delivery.Judgement]string{ delivery.JudgedRecreates: "recreates a container", delivery.JudgedMovesData: "moves the module's data", @@ -227,26 +228,26 @@ func PutAsk(key, node, module string, p delivery.Put, d delivery.ComposedDigest, delivery.JudgedUnchanged: "changes nothing the node runs", }[d.Judgement] explanation := fmt.Sprintf("Asked to put %s at version %s on %s: %s. The move %s.", module, short(p.FP), node, p.Why, what) - expires := now.Add(7 * 24 * time.Hour) + expires := occ.FormatTime(now.Add(7 * 24 * time.Hour)) if bus { - binds, err := asks.ActDigest(PutAct(node, module, p.FP, d.Digest)) + binds, err := occ.ActDigest(PutAct(node, module, p.FP, d.Digest)) if err != nil { return delivery.AssignerAsk{}, err } - yes.Level, yes.Binds = asks.Approve, binds + yes.Level, yes.Binds = occ.Approve, binds yes.Does = "the bus's streams are snapshotted, then " + node + " is told to apply the bus at " + short(p.FP) explanation += " It is the mesh's bus: every module reconnects while it restarts, and its streams are snapshotted first." - expires = now.Add(asks.ApproveLasts) + expires = occ.FormatTime(now.Add(occ.ApproveLasts)) } - return delivery.AssignerAsk{Ask: asks.Ask{ + return assignerAsk(PutAsking(p), oc.Ask{ ID: delivery.AskingID(key, PutAsking(p)), Headline: fmt.Sprintf("Put %s on %s?", module, node), Explanation: explanation, - Options: []asks.Option{yes, - {ID: OptionNo, Label: "No", Does: "the node keeps what it runs, and the put is refused", Level: asks.Acknowledge}}, - Who: asks.Operator, + Options: []oc.Option{yes, + {ID: OptionNo, Label: "No", Does: "the node keeps what it runs, and the put is refused", Level: occ.Acknowledge}}, + Who: occ.Operator, Expires: expires, OnExpiry: "the node keeps what it runs", About: key, - }, Asking: PutAsking(p)}, nil + }), nil } diff --git a/modules/mesh-delivery-assigner/internal/assign/seat.go b/modules/mesh-delivery-assigner/internal/assign/seat.go index 1ee398c1..3dd39970 100644 --- a/modules/mesh-delivery-assigner/internal/assign/seat.go +++ b/modules/mesh-delivery-assigner/internal/assign/seat.go @@ -7,7 +7,8 @@ import ( "strings" "time" - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -170,15 +171,15 @@ const ( // CallClearedCondition clears the condition of a refused controller call a previous process raised, for its pair (node and // module, or node and provision for a pin). -func CallClearedCondition(kind, node, module string) asks.Condition { - return asks.Condition{Key: delivery.AssignerModule + "." + kind + "." + node + "." + module, - Subject: asks.Scope{Scope: "module", ID: module, Machine: node}, State: asks.StateCleared, +func CallClearedCondition(kind, node, module string) oc.Condition { + return oc.Condition{Key: delivery.AssignerModule + "." + kind + "." + node + "." + module, + Subject: oc.ConditionSubject{Scope: "module", ID: module, Machine: node}, State: occ.StateCleared, Resolved: "the assigner calls no verb of the controller any more: placement is the one source."} } // PlacementAct is the act an approved proposal's yes binds: writing exactly its changes at its base revisions. -func PlacementAct(p delivery.PlacementProposal) asks.Act { - return asks.Act{"placement": "write", "proposal": p.ID, "changes": delivery.ChangesDigest(p)} +func PlacementAct(p delivery.PlacementProposal) occ.Act { + return occ.Act{"placement": "write", "proposal": p.ID, "changes": delivery.ChangesDigest(p)} } // PlacementAsking is a proposal's asking number: each judging that raises its ask again (again, a key moved) is @@ -190,29 +191,29 @@ func PlacementAsking(p delivery.PlacementProposal, at time.Time) int { // PlacementAsk is the authorising ask about a proposal (§5): an Approve that binds its act, so only a channel that // proves who answered takes it, with the brief of the verdict by node (judge.Brief) as its explanation. func PlacementAsk(key string, p delivery.PlacementProposal, brief string, asking int, now time.Time) (delivery.AssignerAsk, error) { - binds, err := asks.ActDigest(PlacementAct(p)) + binds, err := occ.ActDigest(PlacementAct(p)) if err != nil { return delivery.AssignerAsk{}, err } nodes := p.Nodes() headline := fmt.Sprintf("Change placement on %s?", strings.Join(nodes, ", ")) - if n := len([]rune(headline)); n > asks.HeadlineLength { + if n := len([]rune(headline)); n > occ.HeadlineLength { headline = fmt.Sprintf("Change placement on %d machines?", len(nodes)) } - return delivery.AssignerAsk{Ask: asks.Ask{ + return assignerAsk(asking, oc.Ask{ ID: delivery.AskingID(key, asking), Headline: headline, Explanation: brief, - Options: []asks.Option{ + Options: []oc.Option{ {ID: OptionYes, Label: "Approve", Does: "the placement of " + strings.Join(nodes, ", ") + " is written as proposed", - Level: asks.Approve, Binds: binds}, - {ID: OptionNo, Label: "Decline", Does: "nothing is written, and the proposal ends", Level: asks.Acknowledge}, + Level: occ.Approve, Binds: binds}, + {ID: OptionNo, Label: "Decline", Does: "nothing is written, and the proposal ends", Level: occ.Acknowledge}, }, - Who: asks.Operator, - Expires: now.Add(asks.ApproveLasts), + Who: occ.Operator, + Expires: occ.FormatTime(now.Add(occ.ApproveLasts)), OnExpiry: "nothing is written, and the proposal ends", About: key, - }, Asking: asking}, nil + }), nil } // Rest is what a proposal leaves to write once the keys of written are: its changes of the other nodes, to be laid again diff --git a/modules/mesh-delivery-assigner/internal/assign/seat_test.go b/modules/mesh-delivery-assigner/internal/assign/seat_test.go index cd5bcfa6..8e6c9d71 100644 --- a/modules/mesh-delivery-assigner/internal/assign/seat_test.go +++ b/modules/mesh-delivery-assigner/internal/assign/seat_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -81,7 +81,7 @@ func TestThePlacementSeatDecides(t *testing.T) { // The seat says it waits while one of its states is not bound, naming it, and clears once all are. for _, unbound := range [][]string{{delivery.PlacementPulsesBucket}, nil} { c := SeatWaitsCondition(unbound) - if _, err := asks.EncodeCondition(c); err != nil || (len(unbound) > 0) != (c.State == asks.StateRaised) || + if _, err := occ.EncodeCondition(c); err != nil || (len(unbound) > 0) != (c.State == occ.StateRaised) || (len(unbound) > 0 && !strings.Contains(c.Explanation, "the seat waits for its states: "+delivery.PlacementPulsesBucket)) { t.Errorf("waiting for %v: %+v, %v", unbound, c, err) } @@ -92,12 +92,12 @@ func TestThePlacementSeatDecides(t *testing.T) { t.Fatalf("writes = %+v, %v", writes, err) } a, err := PlacementAsk("placement.p-one", p, "move web (proposed by operator@laptop)", 7, time.Unix(100, 0)) - if err != nil || a.Ask.Check(time.Unix(100, 0)) != nil || a.Ask.Options[0].Level != asks.Approve || a.Ask.Options[0].Performs(PlacementAct(p)) != nil { + if err != nil || occ.Check(mustAsk(t, a), time.Unix(100, 0)) != nil || mustAsk(t, a).Options[0].Level != occ.Approve || occ.Performs(mustAsk(t, a).Options[0], PlacementAct(p)) != nil { t.Fatalf("the ask = %+v, %v", a, err) } q := p q.Changes = append(slices.Clone(p.Changes), delivery.PlacementProposalChange{Op: delivery.OpUnpin, Node: "laptop", Provision: "x"}) - if a.Ask.Options[0].Performs(PlacementAct(q)) == nil { + if occ.Performs(mustAsk(t, a).Options[0], PlacementAct(q)) == nil { t.Error("the ask's yes binds changes it was not asked about") } } diff --git a/modules/mesh-delivery-assigner/internal/judge/judge.go b/modules/mesh-delivery-assigner/internal/judge/judge.go index c30b9392..0694f550 100644 --- a/modules/mesh-delivery-assigner/internal/judge/judge.go +++ b/modules/mesh-delivery-assigner/internal/judge/judge.go @@ -14,14 +14,15 @@ import ( "strings" "time" + nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-sdk/go/delivery" ) // AnswerBound is how long the holder waits for a node's answer after its candidate was written: the time to live of the // node's pulse key (delivery.PulseAnswerKey), whose expiry names it as giving no answer, never passed. The same answer -// bound a check gives a node (ADR 0593 §8, §7: 60 seconds, delivery.ComposeAnswerBound). A variable so a test on the bus +// bound a check gives a node (ADR 0593 §8, §7: 60 seconds, nec.AnswerBound). A variable so a test on the bus // can shorten it. -var AnswerBound = delivery.ComposeAnswerBound +var AnswerBound = nec.AnswerBound // Intended is, by node, the modules whose placed entry the proposal changes there, and the nodes that answer it: every // node (ADR 0593 §8, changing ADR 0567 §4's list of the nodes that must answer), sorted. current is the placement as it diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/engines.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/engines.go index e16b49e0..3442c8b6 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/engines.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/engines.go @@ -11,6 +11,8 @@ import ( "github.com/nats-io/nats.go/jetstream" + nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" "git.novox.be/novox/mesh-sdk/go/buildstep" "git.novox.be/novox/mesh-sdk/go/catalogue" @@ -23,14 +25,14 @@ import ( // composition code, the composer built from its head run on the Go build seat. // composeCandidateKey is the seat verb's call key, asked of one node (process.Conn.CallOn). -var composeCandidateKey = "seat:" + catalogue.EngineSeat + "." + delivery.EngineComposeCandidateVerb +var composeCandidateKey = "seat:" + catalogue.EngineSeat + "." + ne.VerbComposeCandidate // engineAsks asks one node's engine over the bus, its answer waited for at most ctx's deadline (the answer bound). type engineAsks struct { call func(node, key string, body any) (json.RawMessage, error) } -func (e engineAsks) ComposeCandidate(ctx context.Context, node string, ask delivery.ComposeCandidateAsk) (delivery.ComposeCandidateAnswer, error) { +func (e engineAsks) ComposeCandidate(ctx context.Context, node string, ask ne.ComposeCandidateInput) (ne.ComposeCandidateAnswer, error) { type answer struct { raw json.RawMessage err error @@ -42,12 +44,12 @@ func (e engineAsks) ComposeCandidate(ctx context.Context, node string, ask deliv }() select { case <-ctx.Done(): - return delivery.ComposeCandidateAnswer{}, fmt.Errorf("no answer within its bound: %w", ctx.Err()) + return ne.ComposeCandidateAnswer{}, fmt.Errorf("no answer within its bound: %w", ctx.Err()) case a := <-got: if a.err != nil { - return delivery.ComposeCandidateAnswer{}, a.err + return ne.ComposeCandidateAnswer{}, a.err } - return delivery.DecodeComposeCandidateAnswer(a.raw) + return nec.DecodeAnswer(a.raw) } } @@ -59,7 +61,7 @@ type placedNodes struct { } func (p placedNodes) Placed() ([]string, error) { - ctx, cancel := context.WithTimeout(context.Background(), delivery.ComposeAnswerBound) + ctx, cancel := context.WithTimeout(context.Background(), nec.AnswerBound) defer cancel() kv, err := p.kv(ctx) if err != nil { diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go index 1e98288d..871a7a31 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go @@ -23,8 +23,10 @@ import ( "github.com/nats-io/nats.go/jetstream" gobuild "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" + nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry" "git.novox.be/novox/mesh-contracts/go/seats/meshdeliverychecker" + ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" "git.novox.be/novox/mesh-delivery/sdk/firstview/firstviewtest" @@ -308,7 +310,7 @@ func (b *bed) said(t *testing.T, words string) { // engine places each node and serves compose-candidate for the first one, as its node-engine does on the bus (answered on // the seat verb's caller-named and plain subjects); the others never answer. It answers what it was asked. -func (b *bed) engine(t *testing.T, nodes ...string) func() []delivery.ComposeCandidateAsk { +func (b *bed) engine(t *testing.T, nodes ...string) func() []ne.ComposeCandidateInput { t.Helper() kv, err := b.js.CreateKeyValue(b.ctx, jetstream.KeyValueConfig{Bucket: delivery.PlacementBucket, History: 1}) if err != nil { @@ -327,9 +329,9 @@ func (b *bed) engine(t *testing.T, nodes ...string) func() []delivery.ComposeCan } t.Cleanup(nc.Close) var mu sync.Mutex - var asked []delivery.ComposeCandidateAsk + var asked []ne.ComposeCandidateInput answer := func(m *nats.Msg) { - ask, err := delivery.DecodeComposeCandidateAsk(m.Data) + ask, err := nec.DecodeAsk(m.Data) if err != nil { _ = m.Respond([]byte(`{"error":"` + err.Error() + `"}`)) return @@ -337,9 +339,9 @@ func (b *bed) engine(t *testing.T, nodes ...string) func() []delivery.ComposeCan mu.Lock() asked = append(asked, ask) mu.Unlock() - a := delivery.ComposeCandidateAnswer{Node: nodes[0], Candidate: ask.Candidate, Verdict: delivery.ComposeCandidateComposed, - Own: delivery.ModuleComposed{Module: "node", State: delivery.ModuleComposedState, Digest: "sha256:" + strings.Repeat("b", 64)}} - raw, err := delivery.EncodeComposeCandidateAnswer(a) + a := ne.ComposeCandidateAnswer{Node: nodes[0], Candidate: ask.Candidate, Verdict: nec.Composed, + Own: ne.Module{Module: nec.Own, State: nec.ModuleComposed, Digest: "sha256:" + strings.Repeat("b", 64)}} + raw, err := nec.EncodeAnswer(a) if err != nil { t.Error(err) } @@ -351,7 +353,7 @@ func (b *bed) engine(t *testing.T, nodes ...string) func() []delivery.ComposeCan t.Fatal(err) } } - return func() []delivery.ComposeCandidateAsk { + return func() []ne.ComposeCandidateInput { mu.Lock() defer mu.Unlock() return slices.Clone(asked) @@ -680,7 +682,7 @@ func TestCheckerJudgesACandidateAndOwesItsStatus(t *testing.T) { // N6/AC5, 1300 t.Fatalf("verdict = %+v", v) } if c := v.Composition; c == nil || c.State != delivery.CompositionPass || !slices.Equal(c.Machines, []string{"n1"}) || - !strings.Contains(c.Words, "n2: "+delivery.ComposeNoAnswer) || len(v.Machines) != 1 || v.Machines[0] != "n1" { + !strings.Contains(c.Words, "n2: "+nec.NoAnswer) || len(v.Machines) != 1 || v.Machines[0] != "n1" { t.Errorf("composition %+v machines %v", v.Composition, v.Machines) } if v.Composition != nil && strings.Contains(v.Composition.Words, "did not end") { diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/settings.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/settings.go index 19d895f3..c198376e 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/settings.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/settings.go @@ -8,7 +8,7 @@ import ( "os" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-sdk/go/process" "mesh-delivery-checker/internal/check" @@ -28,7 +28,7 @@ type settings struct { // readSettings reads the settings file at path: each value's default where no file or no value is there; a value that is // no whole number of seconds within its range is refused, naming it. func readSettings(path string) (settings, error) { - out := settings{answer: delivery.ComposeAnswerBound, buildWait: check.DefaultBuildWait} + out := settings{answer: nec.AnswerBound, buildWait: check.DefaultBuildWait} if path == "" { return out, nil } diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/settings_test.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/settings_test.go index 02e1cc14..73e8b431 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/settings_test.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/settings_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-sdk/go/process" "mesh-delivery-checker/internal/check" @@ -22,7 +22,7 @@ func TestTheAnswerBoundIsTheCheckersSetting(t *testing.T) { want time.Duration err bool buildWait time.Duration - }{{"", delivery.ComposeAnswerBound, false, check.DefaultBuildWait}, {`{}`, delivery.ComposeAnswerBound, false, check.DefaultBuildWait}, + }{{"", nec.AnswerBound, false, check.DefaultBuildWait}, {`{}`, nec.AnswerBound, false, check.DefaultBuildWait}, {`{"compose-answer-bound": 90}`, 90 * time.Second, false, check.DefaultBuildWait}, {`{"compose-answer-bound": 0}`, 0, true, 0}, {`{"compose-answer-bound": 1.5}`, 0, true, 0}, {`{"compose-answer-bound": 60, "build-wait-bound": 600}`, 60 * time.Second, false, 10 * time.Minute}, diff --git a/modules/mesh-delivery-checker/go.mod b/modules/mesh-delivery-checker/go.mod index 8781e9a9..31013cf3 100644 --- a/modules/mesh-delivery-checker/go.mod +++ b/modules/mesh-delivery-checker/go.mod @@ -11,6 +11,8 @@ require ( require git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a require ( + git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/seats/meshconfig v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/seats/meshdeliverychecker v0.0.0-00010101000000-000000000000 diff --git a/modules/mesh-delivery-checker/internal/check/engines.go b/modules/mesh-delivery-checker/internal/check/engines.go index 7b7f1909..e6d73e9f 100644 --- a/modules/mesh-delivery-checker/internal/check/engines.go +++ b/modules/mesh-delivery-checker/internal/check/engines.go @@ -9,6 +9,8 @@ import ( "sync" "time" + nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-sdk/go/composer" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -23,12 +25,12 @@ import ( // // **A group that changes the node-engine's composition code** (a member of the node-engine's repository) is not judged by // the running engines, which cannot judge their own next code: each node's engine answers the composition input it -// composed from (ComposeCandidateAsk.Input), and the composer program built from that member's head composes it in a +// composed from (ne.ComposeCandidateInput.Input), and the composer program built from that member's head composes it in a // build module's sandbox (Sandbox), with every manifest of the catalogue as the case leaves it and the indexer's verdicts. // EngineAsker asks one node's engine to compose a check's candidate (the verb node-engine.compose-candidate on that node). type EngineAsker interface { - ComposeCandidate(ctx context.Context, node string, ask delivery.ComposeCandidateAsk) (delivery.ComposeCandidateAnswer, error) + ComposeCandidate(ctx context.Context, node string, ask ne.ComposeCandidateInput) (ne.ComposeCandidateAnswer, error) } // Sandbox runs a compose job on the Go build seat and answers the composer's verdict. @@ -42,7 +44,7 @@ type EngineCompositions struct { // Placed names every node in placement now (the seat mesh-placement's state), or why it cannot be read: then no node // can be asked, and the composition is not made (an error), never passed with nobody asked. Placed func() ([]string, error) - // Bound is the answer bound (the setting compose-answer-bound); zero is delivery.ComposeAnswerBound. + // Bound is the answer bound (the setting compose-answer-bound); zero is nec.AnswerBound. Bound time.Duration // Composer is the commit of the node-engine the mesh runs, named as what composed (ADR 0548 §6's running commit). Composer func() string @@ -78,11 +80,11 @@ func (e EngineCompositions) Compose(ctx context.Context, c *Case) (delivery.Comp } bound := e.Bound if bound <= 0 { - bound = delivery.ComposeAnswerBound + bound = nec.AnswerBound } head := composerHead(c) sandboxed := head != "" && e.Sandbox != nil - answers := map[string]delivery.ComposeCandidateAnswer{} + answers := map[string]ne.ComposeCandidateAnswer{} silent := map[string]string{} var mu sync.Mutex var wg sync.WaitGroup @@ -92,13 +94,13 @@ func (e EngineCompositions) Compose(ctx context.Context, c *Case) (delivery.Comp defer wg.Done() at, cancel := context.WithTimeout(ctx, bound) defer cancel() - a, err := e.Asker.ComposeCandidate(at, node, delivery.ComposeCandidateAsk{Candidate: key, Input: sandboxed}) - if err == nil && sandboxed && a.Verdict != delivery.ComposeCandidateUnready { + a, err := e.Asker.ComposeCandidate(at, node, ne.ComposeCandidateInput{Candidate: key, Input: sandboxed}) + if err == nil && sandboxed && a.Verdict != nec.Unready { // The composer's build and run are the check's to bound, not the engine's answer bound. A composer that // cannot run is no silent node (ADR 0593 §9 lets only that pass): the node's composition fails, naming // the head. if composed, serr := e.inSandbox(ctx, c, head, node, a); serr != nil { - a = delivery.ComposeCandidateAnswer{V: a.V, Node: node, Candidate: a.Candidate, Verdict: delivery.ComposeCandidateRefused, + a = ne.ComposeCandidateAnswer{V: a.V, Node: node, Candidate: a.Candidate, Verdict: nec.Refused, Refused: "the composer at the node-engine's head " + head + " could not compose it: " + serr.Error()} } else { a = composed @@ -109,7 +111,7 @@ func (e EngineCompositions) Compose(ctx context.Context, c *Case) (delivery.Comp switch { case err != nil: silent[node] = err.Error() - case a.Verdict == delivery.ComposeCandidateUnready: + case a.Verdict == nec.Unready: silent[node] = "unready" default: answers[node] = a @@ -135,8 +137,8 @@ func (e EngineCompositions) Compose(ctx context.Context, c *Case) (delivery.Comp // JudgeEngines is the check's composition verdict from the engines' answers (ADR 0593 §7, §9): failed when any node's // answer is refused, naming each node and each module refused with its words (the first refusing node is the machine); // passed otherwise. Every node in nodes without an answer (silent, by node with why, for the journal only) is named -// with delivery.ComposeNoAnswer as a warning. composer names what composed, for the line. Pure. -func JudgeEngines(group, heads, composerName string, nodes []string, answers map[string]delivery.ComposeCandidateAnswer, silent map[string]string) delivery.CompositionVerdict { +// with nec.NoAnswer as a warning. composer names what composed, for the line. Pure. +func JudgeEngines(group, heads, composerName string, nodes []string, answers map[string]ne.ComposeCandidateAnswer, silent map[string]string) delivery.CompositionVerdict { v := delivery.CompositionVerdict{Group: group, Heads: heads, State: delivery.CompositionPass, SDK: composerName} sorted := append([]string(nil), nodes...) sort.Strings(sorted) @@ -144,17 +146,17 @@ func JudgeEngines(group, heads, composerName string, nodes []string, answers map for _, node := range sorted { a, answered := answers[node] if !answered { - v.Warnings = append(v.Warnings, node+": "+delivery.ComposeNoAnswer) + v.Warnings = append(v.Warnings, node+": "+nec.NoAnswer) continue } - if a.Verdict == delivery.ComposeCandidateNotJudged { + if a.Verdict == nec.NotJudged { // Composed in the sandbox and not judged (ADR 0593 §9): named as no answer, with the composer's words; decided by the // verdict, never by the words (ADR 0601 §4's kind). - v.Warnings = append(v.Warnings, node+": "+delivery.ComposeNoAnswer+" ("+a.Refused+")") + v.Warnings = append(v.Warnings, node+": "+nec.NoAnswer+" ("+a.Refused+")") continue } v.Machines = append(v.Machines, node) - if a.Verdict != delivery.ComposeCandidateRefused { + if a.Verdict != nec.Refused { continue } if v.Machine == "" { @@ -164,8 +166,8 @@ func JudgeEngines(group, heads, composerName string, nodes []string, answers map if a.Refused != "" { words = append(words, a.Refused) } - for _, m := range append(append([]delivery.ModuleComposed(nil), a.Modules...), a.Own) { - if m.State == delivery.ModuleRefusedState { + for _, m := range append(append([]ne.Module(nil), a.Modules...), a.Own) { + if m.State == nec.ModuleRefused { words = append(words, m.Module+": "+m.Words) } } @@ -186,7 +188,7 @@ func JudgeEngines(group, heads, composerName string, nodes []string, answers map // inSandbox composes a node's answer again with the composer built from the node-engine member's head, over the input the // node's engine composed from: the node's answer becomes the sandbox's, refused with the composer's words where the // composer refuses the node. -func (e EngineCompositions) inSandbox(ctx context.Context, c *Case, head, node string, a delivery.ComposeCandidateAnswer) (delivery.ComposeCandidateAnswer, error) { +func (e EngineCompositions) inSandbox(ctx context.Context, c *Case, head, node string, a ne.ComposeCandidateAnswer) (ne.ComposeCandidateAnswer, error) { if len(a.Input) == 0 { return a, fmt.Errorf("the engine of %s answered no composition input, so the node-engine's head cannot compose it", node) } @@ -232,25 +234,25 @@ func (e EngineCompositions) inSandbox(ctx context.Context, c *Case, head, node s if err != nil { return a, fmt.Errorf("the composer at the node-engine's head %s did not answer for %s: %w", head, node, err) } - out := delivery.ComposeCandidateAnswer{V: a.V, Node: node, Candidate: a.Candidate, Verdict: delivery.ComposeCandidateComposed} + out := ne.ComposeCandidateAnswer{V: a.V, Node: node, Candidate: a.Candidate, Verdict: nec.Composed} notJudged := "" for _, cv := range verdict.Cases { if m, ok := cv.Machines[node]; ok && m.NotJudged != "" { notJudged = m.NotJudged // the composer did not judge the node: neither composed nor refused (ADR 0593 §9) } else if ok && !m.Composes { - out.Verdict = delivery.ComposeCandidateRefused + out.Verdict = nec.Refused out.Refused = strings.Join(append(append([]string{m.Reason}, m.Problems...), m.Refusals...), "; ") } for name, read := range cv.Manifests { if !read.Accepted { - out.Verdict = delivery.ComposeCandidateRefused - out.Modules = append(out.Modules, delivery.ModuleComposed{Module: name, State: delivery.ModuleRefusedState, Words: read.Refused}) + out.Verdict = nec.Refused + out.Modules = append(out.Modules, ne.Module{Module: name, State: nec.ModuleRefused, Words: read.Refused}) } } } sort.Slice(out.Modules, func(i, j int) bool { return out.Modules[i].Module < out.Modules[j].Module }) - if notJudged != "" && out.Verdict == delivery.ComposeCandidateComposed { - out.Verdict, out.Refused = delivery.ComposeCandidateNotJudged, notJudged + if notJudged != "" && out.Verdict == nec.Composed { + out.Verdict, out.Refused = nec.NotJudged, notJudged } return out, nil } diff --git a/modules/mesh-delivery-checker/internal/check/engines_test.go b/modules/mesh-delivery-checker/internal/check/engines_test.go index 18f5ac10..5489310d 100644 --- a/modules/mesh-delivery-checker/internal/check/engines_test.go +++ b/modules/mesh-delivery-checker/internal/check/engines_test.go @@ -9,6 +9,8 @@ import ( "testing" "time" + nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-sdk/go/composer" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -16,26 +18,26 @@ import ( // fakeEngines answers compose-candidate by node from memory; a node it holds no answer for never answers. type fakeEngines struct { mu sync.Mutex - answers map[string]delivery.ComposeCandidateAnswer + answers map[string]ne.ComposeCandidateAnswer errs map[string]error - asked map[string]delivery.ComposeCandidateAsk + asked map[string]ne.ComposeCandidateInput } -func (f *fakeEngines) ComposeCandidate(ctx context.Context, node string, ask delivery.ComposeCandidateAsk) (delivery.ComposeCandidateAnswer, error) { +func (f *fakeEngines) ComposeCandidate(ctx context.Context, node string, ask ne.ComposeCandidateInput) (ne.ComposeCandidateAnswer, error) { f.mu.Lock() if f.asked == nil { - f.asked = map[string]delivery.ComposeCandidateAsk{} + f.asked = map[string]ne.ComposeCandidateInput{} } f.asked[node] = ask a, ok := f.answers[node] err := f.errs[node] f.mu.Unlock() if err != nil { - return delivery.ComposeCandidateAnswer{}, err + return ne.ComposeCandidateAnswer{}, err } if !ok { <-ctx.Done() - return delivery.ComposeCandidateAnswer{}, ctx.Err() + return ne.ComposeCandidateAnswer{}, ctx.Err() } return a, nil } @@ -72,10 +74,10 @@ func (d digests) ManifestDigests() map[string]string { return d } // warning, decided by the verdict and never by its words; a group with a node-engine member is composed by the composer // built at that member's head, over the input each node's engine composed from. func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) { - composed := func(node string) delivery.ComposeCandidateAnswer { - return delivery.ComposeCandidateAnswer{Node: node, Verdict: delivery.ComposeCandidateComposed, - Modules: []delivery.ModuleComposed{{Module: "web", State: delivery.ModuleComposedState, Digest: "sha256:" + strings.Repeat("a", 64)}}, - Own: delivery.ModuleComposed{Module: "node", State: delivery.ModuleComposedState, Digest: "sha256:" + strings.Repeat("b", 64)}} + composed := func(node string) ne.ComposeCandidateAnswer { + return ne.ComposeCandidateAnswer{Node: node, Verdict: nec.Composed, + Modules: []ne.Module{{Module: "web", State: nec.ModuleComposed, Digest: "sha256:" + strings.Repeat("a", 64)}}, + Own: ne.Module{Module: nec.Own, State: nec.ModuleComposed, Digest: "sha256:" + strings.Repeat("b", 64)}} } cs := caseOf(t, pullOf(repoA, 7, head1)) key, _ := delivery.CompositionKey(cs.Group, cs.HeadsHash) @@ -92,10 +94,10 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) { t.Run("one node refusing fails the composition, naming the node and the module", func(t *testing.T) { refusing := composed("laptop") - refusing.Verdict = delivery.ComposeCandidateRefused - refusing.Modules = []delivery.ModuleComposed{{Module: "web", State: delivery.ModuleRefusedState, Words: "a required value is unset"}, - {Module: "mail", State: delivery.ModuleHeldState, Words: "held as today"}} - e := &fakeEngines{answers: map[string]delivery.ComposeCandidateAnswer{"anchor": composed("anchor"), "laptop": refusing}} + refusing.Verdict = nec.Refused + refusing.Modules = []ne.Module{{Module: "web", State: nec.ModuleRefused, Words: "a required value is unset"}, + {Module: "mail", State: nec.ModuleHeld, Words: "held as today"}} + e := &fakeEngines{answers: map[string]ne.ComposeCandidateAnswer{"anchor": composed("anchor"), "laptop": refusing}} v := compose(e, "anchor", "laptop") if v.State != delivery.CompositionFail || v.Machine != "laptop" || !strings.Contains(v.Words, "not composed on laptop: web: a required value is unset") || strings.Contains(v.Words, "mail") || len(v.Machines) != 2 { @@ -112,8 +114,8 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) { }) t.Run("a silent, unready or unreachable node is no answer, and the check passes naming it", func(t *testing.T) { - unready := delivery.ComposeCandidateAnswer{Node: "g14", Verdict: delivery.ComposeCandidateUnready, Refused: "a module refused it"} - e := &fakeEngines{answers: map[string]delivery.ComposeCandidateAnswer{"anchor": composed("anchor"), "g14": unready}, + unready := ne.ComposeCandidateAnswer{Node: "g14", Verdict: nec.Unready, Refused: "a module refused it"} + e := &fakeEngines{answers: map[string]ne.ComposeCandidateAnswer{"anchor": composed("anchor"), "g14": unready}, errs: map[string]error{"ace": errors.New("no responders")}} began := time.Now() v := compose(e, "anchor", "laptop", "g14", "ace") @@ -124,14 +126,14 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) { t.Fatalf("verdict = %+v", v) } for _, node := range []string{"ace", "g14", "laptop"} { - if !strings.Contains(strings.Join(v.Warnings, "; "), node+": "+delivery.ComposeNoAnswer) { + if !strings.Contains(strings.Join(v.Warnings, "; "), node+": "+nec.NoAnswer) { t.Errorf("%s is not named: %v", node, v.Warnings) } } var ver delivery.Verdict ver.State = delivery.VerdictPass withComposition(&ver, v) - if ver.State != delivery.VerdictPass || !strings.Contains(composition(ver.Composition, 0, false), "laptop: "+delivery.ComposeNoAnswer) { + if ver.State != delivery.VerdictPass || !strings.Contains(composition(ver.Composition, 0, false), "laptop: "+nec.NoAnswer) { t.Errorf("the line = %q", composition(ver.Composition, 0, false)) } }) @@ -144,7 +146,7 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) { host := CaseOf(c, map[string]string{repoA: trunkA, nodeEngineRepo: trunkA}) withInput := composed("anchor") withInput.Input = map[string]any{"nodes": []any{map[string]any{"name": "anchor"}}} - e := &fakeEngines{answers: map[string]delivery.ComposeCandidateAnswer{"anchor": withInput}} + e := &fakeEngines{answers: map[string]ne.ComposeCandidateAnswer{"anchor": withInput}} sb := &fakeSandbox{v: func(node string) composer.Verdict { return composer.Verdict{V: composer.Version, Cases: []composer.CaseVerdict{{Name: "change", Machines: map[string]composer.Machine{node: {Composes: false, Reason: "the new composer refuses it"}}}}} @@ -182,7 +184,7 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) { return composer.Verdict{V: composer.Version, Cases: []composer.CaseVerdict{{Name: "change", Machines: map[string]composer.Machine{node: row.m}}}} } v, err = ec.Compose(context.Background(), host) - named := len(v.Warnings) == 1 && strings.Contains(v.Warnings[0], "anchor: "+delivery.ComposeNoAnswer+" (its node-engine sends no running state for web)") + named := len(v.Warnings) == 1 && strings.Contains(v.Warnings[0], "anchor: "+nec.NoAnswer+" (its node-engine sends no running state for web)") if err != nil || (v.State == delivery.CompositionPass) != row.pass || named != row.pass { t.Errorf("%+v: verdict %+v, %v", row.m, v, err) } diff --git a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/assignerasks.go b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/assignerasks.go index 04f1bfc5..1c3e743e 100644 --- a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/assignerasks.go +++ b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/assignerasks.go @@ -3,11 +3,12 @@ package main import ( "encoding/json" "fmt" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "slices" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/asks" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -21,10 +22,10 @@ import ( // of these asks needs proof of who answered (mesh-sdk go/delivery AskNeedsProof). // mostAssignerOpen is how many of the assigner's asks the teller keeps open at the router at once. The router keeps at most -// asks.MostOpen open asks of one asker, and the teller asks both the gate's merge asks and the assigner's as one asker, so a burst +// occ.MostOpen open asks of one asker, and the teller asks both the gate's merge asks and the assigner's as one asker, so a burst // of holds (seven at once on 2026-10-25) would take all the room and a merge Approve could not be asked; one place is always // left for a merge ask. An assigner ask beyond these waits in the wait for room and is asked when one of them ends. -const mostAssignerOpen = asks.MostOpen - 1 +const mostAssignerOpen = occ.MostOpen - 1 // assignerOpenRecord is the key of the router ids of the assigner's asks the teller asked. const assignerOpenRecord = "assigner.open" @@ -92,21 +93,41 @@ func isAssignerKey(key string) bool { } // assignerAsk reads the assigner's ask at key: found false when the key holds none. -func (t *Teller) assignerAsk(key string) (delivery.AssignerAsk, bool, error) { +func (t *Teller) assignerAsk(key string) (assignerAsked, bool, error) { if t.Assigner == nil { - return delivery.AssignerAsk{}, false, fmt.Errorf("the teller reads no assigner asks") + return assignerAsked{}, false, fmt.Errorf("the teller reads no assigner asks") } raw, found, err := t.Assigner.Get(key) if err != nil || !found { - return delivery.AssignerAsk{}, false, err + return assignerAsked{}, false, err } - a, err := delivery.DecodeAssignerAsk(key, raw) + a, err := decodeAssignerAsk(key, raw) if err != nil { - return delivery.AssignerAsk{}, false, fmt.Errorf("the assigner's key %s holds no ask: %w", key, err) + return assignerAsked{}, false, fmt.Errorf("the assigner's key %s holds no ask: %w", key, err) } return a, true, nil } +// assignerAsked is the assigner's ask with the router's ask decoded from its JSON. +type assignerAsked struct { + Asking int + Ask oc.Ask +} + +// decodeAssignerAsk reads the assigner's ask at key as delivery.DecodeAssignerAsk did before the ask was JSON: it refuses an +// id other than the one the key and the asking give, and an ask with no option (routerAskOf). +func decodeAssignerAsk(key string, data []byte) (assignerAsked, error) { + a, err := delivery.DecodeAssignerAsk(key, data) + if err != nil { + return assignerAsked{}, err + } + ask, err := routerAskOf(a.Ask, key, a.Asking) + if err != nil { + return assignerAsked{}, err + } + return assignerAsked{Asking: a.Asking, Ask: ask}, nil +} + // raiseAssignerAsk asks the operator the assigner's ask at key, once per router id: an ask answered already (from the view or // the phone) is not asked; an earlier asking still open is taken back first; an asking already made is only caught up with // at the router, and one that ended is not asked again, for the router drops a second ask under an id it keeps. @@ -188,8 +209,8 @@ func (t *Teller) OnAssignerAskGone(key string) { // answerAssigner is the act of a warrant for one of the assigner's asks: the ask must still stand at its key under the asking // and the words the operator answered, and then the answer is written under the ask's key, once. Its options bind no act: -// the assigner reads the option chosen (asker.Client.Take has checked the option, its level and the ask's digest). -func (t *Teller) answerAssigner(key string, o asks.Option, w asks.Warrant) error { +// the assigner reads the option chosen (occ.Client.Take has checked the option, its level and the ask's digest). +func (t *Teller) answerAssigner(key string, o oc.Option, w oc.Warrant) error { a, found, err := t.assignerAsk(key) if err != nil { return err @@ -202,12 +223,12 @@ func (t *Teller) answerAssigner(key string, o asks.Option, w asks.Warrant) error t.logf("%s: the ask %s was answered, and the assigner has asked again under %s: an answer to an earlier asking changes nothing", key, w.Ask, a.Ask.ID) return nil } - if a.Ask.Digest() != w.AskDigest { - return fmt.Errorf("the assigner's ask %s is no longer the ask the operator answered (its digest is %s, the warrant's %s)", key, a.Ask.Digest(), w.AskDigest) + if occ.Digest(a.Ask) != w.AskDigest { + return fmt.Errorf("the assigner's ask %s is no longer the ask the operator answered (its digest is %s, the warrant's %s)", key, occ.Digest(a.Ask), w.AskDigest) } - body, err := delivery.EncodeAnswer(delivery.Answer{Option: o.ID, Warrant: warrantID(w), Words: w.Says()}) + body, err := delivery.EncodeAnswer(delivery.Answer{Option: o.ID, Warrant: warrantID(w), Words: occ.Says(w)}) if err != nil { return err } - return t.writeAnswer(key, w.Ask, body, w.Says()) + return t.writeAnswer(key, w.Ask, body, occ.Says(w)) } diff --git a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/assignerasks_test.go b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/assignerasks_test.go index 1cbed7e7..e1017c9c 100644 --- a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/assignerasks_test.go +++ b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/assignerasks_test.go @@ -2,10 +2,11 @@ package main import ( "encoding/json" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "strings" "testing" - "git.novox.be/novox/mesh-sdk/go/asks" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -18,15 +19,15 @@ func TestAnAssignerAskIsRaisedOnTheOperatorChannelOnce(t *testing.T) { assigner[key] = value b.t.OnAssignerAsk(key) b.t.OnAssignerAsk(key) - asked := b.askedOf(asks.AskSubject(delivery.TellerModule)) + asked := b.askedOf(oc.AcceptSubjectAsk(delivery.TellerModule)) if len(asked) != 1 { t.Fatalf("published %d asks, want one; log:\n%s", len(asked), b.logs.String()) } - var got asks.Ask + var got oc.Ask if err := json.Unmarshal(asked[0].body, &got); err != nil { t.Fatal(err) } - if got.ID != ask.ID || got.Digest() != ask.Digest() { + if got.ID != ask.ID || occ.Digest(got) != occ.Digest(ask) { t.Errorf("the router's ask %+v differs from the assigner's", got) } } @@ -39,7 +40,7 @@ func TestAnAssignerAskIsRaisedWithoutTheView(t *testing.T) { key, value, _ := holdAsk(t) assigner[key] = value b.t.OnAssignerAsk(key) - if n := len(b.askedOf(asks.AskSubject(delivery.TellerModule))); n != 1 { + if n := len(b.askedOf(oc.AcceptSubjectAsk(delivery.TellerModule))); n != 1 { t.Fatalf("published %d asks, want one; log:\n%s", n, b.logs.String()) } } @@ -85,7 +86,7 @@ func TestAnAnswerToAnAssignerAskTakenAwayWritesNothing(t *testing.T) { // itself) is taken back too; an ask answered before it is heard is never raised. func TestAnAssignerAskAnsweredInTheViewOrTakenAwayIsTakenBack(t *testing.T) { cancelled := func(b *bed, id string) bool { - for _, p := range b.askedOf(asks.CancelSubject(delivery.TellerModule)) { + for _, p := range b.askedOf(oc.AcceptSubjectCancel(delivery.TellerModule)) { if string(p.body) == `{"id":"`+id+`"}` { return true } @@ -118,7 +119,7 @@ func TestAnAssignerAskAnsweredInTheViewOrTakenAwayIsTakenBack(t *testing.T) { assigner[key] = value view[key+".view"] = viewAnswer(t, "keep", "") b.t.OnAssignerAsk(key) - if n := len(b.askedOf(asks.AskSubject(delivery.TellerModule))); n != 0 { + if n := len(b.askedOf(oc.AcceptSubjectAsk(delivery.TellerModule))); n != 0 { t.Errorf("an ask answered in the view was raised %d times", n) } } @@ -152,14 +153,14 @@ func TestAnAssignerAskWaitsForRoomAndARefusalIsSaidAsADeliveryAsk(t *testing.T) } // ackFor is the router's warrant for an option of level acknowledge, as the assigner's asks offer. -func ackFor(a asks.Ask, option string) asks.Warrant { +func ackFor(a oc.Ask, option string) oc.Warrant { w := warrantFor(a, option) - w.Level, w.Label = asks.Acknowledge, option + w.Level, w.Label = occ.Acknowledge, option return w } // holdAskOf is the assigner's hold ask of module plex at the n-th fingerprint, shaped as holdAsk. -func holdAskOf(t *testing.T, n int) (string, []byte, asks.Ask) { +func holdAskOf(t *testing.T, n int) (string, []byte, oc.Ask) { t.Helper() key, err := delivery.HoldAskKey("plex", "src2:"+strings.Repeat(string(rune('0'+n)), 64)) if err != nil { @@ -167,7 +168,7 @@ func holdAskOf(t *testing.T, n int) (string, []byte, asks.Ask) { } _, _, ask := holdAsk(t) ask.ID = delivery.AskID(key) - value, err := delivery.EncodeAssignerAsk(key, delivery.AssignerAsk{Ask: ask}) + value, err := delivery.EncodeAssignerAsk(key, delivery.AssignerAsk{Ask: rawAsk(t, ask)}) if err != nil { t.Fatal(err) } @@ -179,7 +180,7 @@ func holdAskOf(t *testing.T, n int) (string, []byte, asks.Ask) { func TestABurstOfHoldsLeavesRoomForAMergeAsk(t *testing.T) { b := newBed(t) assigner, _ := withView(b) - var held []asks.Ask + var held []oc.Ask for n := 0; n < 7; n++ { key, value, ask := holdAskOf(t, n) assigner[key] = value diff --git a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/decided.go b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/decided.go index 18c2b3d8..c46c60b5 100644 --- a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/decided.go +++ b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/decided.go @@ -4,13 +4,14 @@ import ( "context" "encoding/json" "errors" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "time" "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/asks" "git.novox.be/novox/mesh-sdk/go/process" ) @@ -96,13 +97,13 @@ func pullDecided(ctx context.Context, cons jetstream.Consumer, logf func(string, } // sayVia publishes a condition on the seat's one subject for conditions, under the module's own name. -func sayVia(ctx context.Context, js jetstream.JetStream, module string) func(c asks.Condition) error { - return func(c asks.Condition) error { - subject, err := asks.ConditionSubject(module) +func sayVia(ctx context.Context, js jetstream.JetStream, module string) func(c oc.Condition) error { + return func(c oc.Condition) error { + subject, err := occ.ConditionSubject(module) if err != nil { return err } - data, err := asks.EncodeCondition(c) + data, err := occ.EncodeCondition(c) if err != nil { return err } diff --git a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/integration_test.go b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/integration_test.go index 5ea0dc00..d04702aa 100644 --- a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/integration_test.go +++ b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/integration_test.go @@ -6,6 +6,8 @@ import ( "context" "encoding/json" "fmt" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-delivery/sdk/firstview/firstviewtest" "os" "path/filepath" @@ -18,7 +20,6 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/asks" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/process" @@ -407,8 +408,8 @@ func TestTellerCarriesAnAskAndWritesTheAnswer(t *testing.T) { // 1343/AC1, AC2 if raw == nil { t.Fatalf("no ask on the seat's queue: %v\n%s", err, logs.String()) } - var asked asks.Ask - if err := json.Unmarshal(raw.Data, &asked); err != nil || asked.Digest() != ga.Ask.Digest() { + var asked oc.Ask + if err := json.Unmarshal(raw.Data, &asked); err != nil || occ.Digest(asked) != occ.Digest(ga.Ask) { t.Fatalf("the ask on the queue is %+v (%v), want the gate's", asked, err) } // The router says the warrant. @@ -490,7 +491,7 @@ func TestTellerAsksAgainAnAskRefusedForWantOfRoom(t *testing.T) { // 1610/AC1 if err != nil || raw.Sequence <= after { continue } - var a asks.Ask + var a oc.Ask if json.Unmarshal(raw.Data, &a) == nil && a.ID == id { return raw.Sequence } @@ -498,7 +499,7 @@ func TestTellerAsksAgainAnAskRefusedForWantOfRoom(t *testing.T) { // 1610/AC1 t.Fatalf("the ask %s is not on the seat's queue after sequence %d\n%s", id, after, logs.String()) return 0 } - say := func(w asks.Warrant) { + say := func(w oc.Warrant) { t.Helper() raw, _ := json.Marshal(w) if err := b.ctl.Publish(seatDecided, raw); err != nil { @@ -511,7 +512,7 @@ func TestTellerAsksAgainAnAskRefusedForWantOfRoom(t *testing.T) { // 1610/AC1 t.Fatal(err) } first := lastAsk(held.Ask.ID, 0) - say(asks.Warrant{Ask: held.Ask.ID, Asker: delivery.TellerModule, Outcome: asks.OutcomeRefused, Words: roomRefusal, At: now}) + say(oc.Warrant{Ask: held.Ask.ID, Asker: delivery.TellerModule, Outcome: occ.OutcomeRefused, Words: roomRefusal, At: occ.FormatTime(now)}) for deadline := time.Now().Add(10 * time.Second); !strings.Contains(logs.String(), "for want of room"); time.Sleep(50 * time.Millisecond) { if time.Now().After(deadline) { t.Fatalf("the refusal was not heard\n%s", logs.String()) @@ -524,7 +525,7 @@ func TestTellerAsksAgainAnAskRefusedForWantOfRoom(t *testing.T) { // 1610/AC1 } second := lastAsk(ga2.Ask.ID, first) // The other ask lapses at the router: there is room, and the refused ask is asked again. - say(asks.Warrant{Ask: ga2.Ask.ID, Asker: delivery.TellerModule, Outcome: asks.OutcomeExpired, At: now, AskDigest: ga2.Ask.Digest()}) + say(oc.Warrant{Ask: ga2.Ask.ID, Asker: delivery.TellerModule, Outcome: occ.OutcomeExpired, At: occ.FormatTime(now), AskDigest: occ.Digest(ga2.Ask)}) lastAsk(held.Ask.ID, second) } @@ -641,8 +642,8 @@ func TestTellerRaisesAnAssignerAskAndWritesTheOperatorsAnswer(t *testing.T) { if raw == nil { t.Fatalf("the assigner's ask is not on the seat's queue: %v\n%s", err, logs.String()) } - var asked asks.Ask - if err := json.Unmarshal(raw.Data, &asked); err != nil || asked.ID != ask.ID || asked.Digest() != ask.Digest() { + var asked oc.Ask + if err := json.Unmarshal(raw.Data, &asked); err != nil || asked.ID != ask.ID || occ.Digest(asked) != occ.Digest(ask) { t.Fatalf("the ask on the queue is %+v (%v), want the assigner's", asked, err) } w, _ := json.Marshal(ackFor(ask, "release")) @@ -672,7 +673,7 @@ func TestTellerRaisesAnAssignerAskAndWritesTheOperatorsAnswer(t *testing.T) { } ask2 := ask ask2.ID = delivery.AskID(key2) - value2, err := delivery.EncodeAssignerAsk(key2, delivery.AssignerAsk{Ask: ask2}) + value2, err := delivery.EncodeAssignerAsk(key2, delivery.AssignerAsk{Ask: rawAsk(t, ask2)}) if err != nil { t.Fatal(err) } diff --git a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/main.go b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/main.go index 183f57ef..fd69428a 100644 --- a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/main.go +++ b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/main.go @@ -11,6 +11,7 @@ import ( "context" "errors" "fmt" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" "io" "log" "os" @@ -25,7 +26,6 @@ import ( "git.novox.be/novox/mesh-delivery/sdk/firstview" - "git.novox.be/novox/mesh-sdk/go/asker" "git.novox.be/novox/mesh-sdk/go/delivery" "mesh-delivery-teller/internal/ends" @@ -76,7 +76,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // Essential: the router's warrants are how an answer reaches the teller. return fmt.Errorf("the consumer %s on %s cannot be read (%v): the teller hears no answer without it, so it stops; where the bus refused it or did not answer, this module's consumer (made with its grant for the seat it uses) has not reached the bus yet", cred.EventConsumerName(), process.EventsStream, err) } - client := &asker.Client{Module: delivery.TellerModule, State: state, Publish: publish, + client := &occ.Client{Module: delivery.TellerModule, State: state, Publish: publish, Record: recordVia(ctx, conn, delivery.TellerModule), Subscribe: decidedVia(pullDecided(ctx, cons, logger.Printf), logger.Printf)} t := &Teller{Asker: client, Store: state, Answers: kvAnswers{ctx: ctx, kv: kvs[0]}, Gate: kvGate{ctx: ctx, kv: gate}, Say: sayVia(ctx, js, delivery.TellerModule), Logf: logger.Printf, Publish: publish} diff --git a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/room_test.go b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/room_test.go index abc12e82..2884b08c 100644 --- a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/room_test.go +++ b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/room_test.go @@ -2,14 +2,15 @@ package main import ( "encoding/json" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "strings" "testing" - "git.novox.be/novox/mesh-sdk/go/asks" "git.novox.be/novox/mesh-sdk/go/delivery" ) -// roomRefusal is the router's words when an asker already holds asks.MostOpen open asks, as the live router said them to +// roomRefusal is the router's words when an asker already holds occ.MostOpen open asks, as the live router said them to // the teller on 2026-10-21 at 11:21:08 local (novox journal of mesh-delivery-teller.service: "the router refused the ask // merge-design_61_print_and_scan-8cc11c5b8c32-d916c290f50f: its asker already has 3 open asks"; mesh-catalog // modules/messenger cmd/messenger/asks.go:529-531). @@ -21,7 +22,7 @@ func headsN(n int) []delivery.Head { } // askOf puts the gate's key for n and hands it to the teller, as the watch would. -func (b *bed) askOf(t *testing.T, n int, rev uint64) (string, delivery.Ask) { +func (b *bed) askOf(t *testing.T, n int, rev uint64) (string, gateAsked) { t.Helper() key, value, ga := gateAsk(t, headsN(n), headsN(n)) b.gate[key] = value @@ -35,19 +36,19 @@ func (b *bed) refuse(t *testing.T, id, words string) { t.Helper() delete(b.seenIDs, id) delete(b.records, id) - b.deliver(t, asks.Warrant{Ask: id, Asker: delivery.TellerModule, Outcome: asks.OutcomeRefused, Words: words, At: now}) + b.deliver(t, oc.Warrant{Ask: id, Asker: delivery.TellerModule, Outcome: occ.OutcomeRefused, Words: words, At: occ.FormatTime(now)}) } // expire is the router's word that an ask of the teller's lapsed with nobody's answer: one of its asks ended. -func (b *bed) expire(t *testing.T, ga delivery.Ask) { +func (b *bed) expire(t *testing.T, ga gateAsked) { t.Helper() - b.deliver(t, asks.Warrant{Ask: ga.Ask.ID, Asker: delivery.TellerModule, Outcome: asks.OutcomeExpired, At: now, AskDigest: ga.Ask.Digest()}) + b.deliver(t, oc.Warrant{Ask: ga.Ask.ID, Asker: delivery.TellerModule, Outcome: occ.OutcomeExpired, At: occ.FormatTime(now), AskDigest: occ.Digest(ga.Ask)}) } // askedIDs is the router ids of every ask the bus carried, in order. func (b *bed) askedIDs() []string { var out []string - for _, p := range b.askedOf(asks.AskSubject(delivery.TellerModule)) { + for _, p := range b.askedOf(oc.AcceptSubjectAsk(delivery.TellerModule)) { var a struct { ID string `json:"id"` } @@ -77,7 +78,7 @@ func TestAnAskRefusedForWantOfRoomIsAskedAgainWhenAnotherEnds(t *testing.T) { // if len(got) != 5 || got[4] != fourth.Ask.ID { t.Fatalf("asks = %v, want the refused %s asked again once the first was answered", got, fourth.Ask.ID) } - pubs := b.askedOf(asks.AskSubject(delivery.TellerModule)) + pubs := b.askedOf(oc.AcceptSubjectAsk(delivery.TellerModule)) if pubs[4].id == pubs[3].id { t.Errorf("asked again under the bus message id %s of its first asking: the bus drops it as a duplicate", pubs[4].id) } @@ -145,7 +146,7 @@ func TestAnAskRefusedForWantOfRoomTooOftenIsSaidAsACondition(t *testing.T) { // t.Errorf("asked %d times, want %d (once, and again after each refusal but the last)", again, mostRoomRefusals) } asked := len(b.askedIDs()) - if len(b.conds) != 1 || b.conds[0].State != asks.StateRaised || !strings.Contains(b.conds[0].Summary, "open asks") { + if len(b.conds) != 1 || b.conds[0].State != occ.StateRaised || !strings.Contains(b.conds[0].Summary, "open asks") { t.Fatalf("conditions = %+v", b.conds) } _, last := b.askOf(t, 20, 20) diff --git a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/teller.go b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/teller.go index a9006267..debb6aed 100644 --- a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/teller.go +++ b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/teller.go @@ -4,6 +4,8 @@ import ( "encoding/json" "errors" "fmt" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "slices" "sort" "strings" @@ -11,8 +13,6 @@ import ( "time" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/asker" - "git.novox.be/novox/mesh-sdk/go/asks" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -23,19 +23,61 @@ import ( // warrant that does not hold writes no answer and is said as a condition. It holds no timer and serves no request: // every reaction is a revision of the gate's key or a warrant. +// gateAsked is the gate's ask with the router's ask decoded from its JSON (delivery.Ask.Ask is operator-channel's ask as +// JSON, novox/hq ADR 0627 §4). +type gateAsked struct { + Group string + Heads []delivery.Head + Asking int + Ask oc.Ask +} + +// routerAskOf decodes the router's ask a gate or assigner value carries and holds it to what mesh-sdk refused before it +// named no seat's types: an id other than the one the key and the asking give, and an ask with no option. +func routerAskOf(raw json.RawMessage, key string, asking int) (oc.Ask, error) { + var a oc.Ask + if err := json.Unmarshal(raw, &a); err != nil { + return oc.Ask{}, fmt.Errorf("the value carries no router's ask: %w", err) + } + if want := delivery.AskingID(key, asking); a.ID != want { + return oc.Ask{}, fmt.Errorf("the ask's id %q is not %q, the id its key and asking %d give", a.ID, want, asking) + } + if len(a.Options) == 0 { + return oc.Ask{}, fmt.Errorf("the ask has no option") + } + return a, nil +} + +// decodeGateAsk reads the gate's ask value and the router's ask in it, as delivery.DecodeAsk did before the ask was JSON. +func decodeGateAsk(value []byte) (gateAsked, error) { + ga, err := delivery.DecodeAsk(value) + if err != nil { + return gateAsked{}, err + } + key, err := delivery.MergeAskKey(ga.Group, ga.Heads) + if err != nil { + return gateAsked{}, err + } + a, err := routerAskOf(ga.Ask, key, ga.Asking) + if err != nil { + return gateAsked{}, err + } + return gateAsked{Group: ga.Group, Heads: ga.Heads, Asking: ga.Asking, Ask: a}, nil +} + // store is where the teller keeps what it asked: the asker's own records (`ask.`) and, beside them, which gate key // each ask was made for and at which revision of it (`key.`), and which conditions it has raised (`cond.`). type store interface { - asker.Store + occ.Store Delete(key string) error } -// routerAsks is the asker.Client as the teller uses it. +// routerAsks is the occ.Client as the teller uses it. type routerAsks interface { - Ask(a asks.Ask) error + Ask(a oc.Ask) error Cancel(id string) error - Where(id string) (state string, w *asks.Warrant, err error) - Take(w asks.Warrant, act func(asks.Option, asks.Warrant) error) (asker.Handled, error) + Where(id string) (state string, w *oc.Warrant, err error) + Take(w oc.Warrant, act func(oc.Option, oc.Warrant) error) (occ.Handled, error) } // answers is the teller's bucket answers. @@ -63,7 +105,7 @@ type Teller struct { Assigner gateAsks View gateAsks // Say publishes a condition to the operator (mesh.seat.operator-channel.accept.condition.). - Say func(c asks.Condition) error + Say func(c oc.Condition) error Logf func(format string, args ...any) // Publish is the bus the asker publishes on, used to ask again an ask the router refused for want of room under a bus // message id of its own (the bus drops a second message under an id it holds). @@ -118,12 +160,12 @@ func (t *Teller) seenAt(rk string) (seen, *stdio.StateEntry, error) { // seenOf is the record of a router id: the gate key it was asked for. func (t *Teller) seenOf(id string) (seen, *stdio.StateEntry, error) { return t.seenAt(recordKey(id)) } -func (t *Teller) keptOf(id string) (*asker.Kept, uint64, error) { +func (t *Teller) keptOf(id string) (*occ.Kept, uint64, error) { e, err := t.Store.Get(keptKey(id)) if err != nil || e == nil { return nil, 0, err } - var k asker.Kept + var k occ.Kept if err := json.Unmarshal(e.Value, &k); err != nil { return nil, 0, fmt.Errorf("the record %s cannot be read: %w", keptKey(id), err) } @@ -131,7 +173,7 @@ func (t *Teller) keptOf(id string) (*asker.Kept, uint64, error) { } // open says whether an ask is made and neither answered nor ended. -func open(k *asker.Kept) bool { return k != nil && !k.Acted && k.Ended == "" } +func open(k *occ.Kept) bool { return k != nil && !k.Acted && k.Ended == "" } // note keeps that the asking with this router id is the latest of the gate key, made at revision rev of it. func (t *Teller) note(id, key string, rev uint64, prior *stdio.StateEntry) error { @@ -157,7 +199,7 @@ func (t *Teller) note(id, key string, rev uint64, prior *stdio.StateEntry) error func (t *Teller) OnAsk(key string, rev uint64, value []byte) { t.mu.Lock() defer t.mu.Unlock() - ga, err := delivery.DecodeAsk(value) + ga, err := decodeGateAsk(value) if err != nil { t.logf("the gate's key %s at revision %d is not an ask, and is left: %v", key, rev, err) return @@ -195,7 +237,7 @@ func (t *Teller) OnAsk(key string, rev uint64, value []byte) { } if k != nil { switch { - case open(k) && k.Ask.Digest() == ga.Ask.Digest(): + case open(k) && occ.Digest(k.Ask) == occ.Digest(ga.Ask): _ = t.note(id, key, rev, grec) t.catchUp(id) case open(k): @@ -238,7 +280,7 @@ func (t *Teller) cancelOpen(key, id string) { t.logf("%s: the ask %s could not be cancelled: %v", key, id, err) return } - k.Ended = asks.OutcomeCancelled + k.Ended = occ.OutcomeCancelled if _, err := t.Store.Update(keptKey(id), k, krev); err != nil && !errors.Is(err, stdio.ErrChanged) { t.logf("%s: the cancelled ask %s could not be marked: %v", key, id, err) } @@ -290,13 +332,13 @@ func (t *Teller) catchUp(id string) { // mesh-delivery.settle for a person's settle ask, novox/hq ADR 0556 clause 5), this ask, the answer, and the heads it covers. // It is spelled as the gate composes it (mesh-catalog modules/mesh-delivery-gate approval.go mergeAct, from // modules/mesh-delivery approval.go:63-65) and as mesh-controller verifies it (cmd/mesh-controller/moved.go:98-106). -func mergeAct(verb, ask, option string, heads map[string]string) asks.Act { +func mergeAct(verb, ask, option string, heads map[string]string) occ.Act { var out []string for id, sha := range heads { out = append(out, id+"="+sha) } sort.Strings(out) - return asks.Act{"verb": verb, "ask": ask, "option": option, "heads": strings.Join(out, ",")} + return occ.Act{"verb": verb, "ask": ask, "option": option, "heads": strings.Join(out, ",")} } // headsOf is the heads an ask covers as an act names them: a delivery's id, `owner/repository@}`). -func holdAsk(t *testing.T) (key string, value []byte, ask asks.Ask) { +func holdAsk(t *testing.T) (key string, value []byte, ask oc.Ask) { t.Helper() key, err := delivery.HoldAskKey("plex", fpHeld) if err != nil { t.Fatal(err) } - ask = asks.Ask{ID: delivery.AskID(key), Headline: "plex is held after a failed health check", Explanation: "ace failed the check.", Who: asks.Operator, - Expires: now.Add(asks.ApproveLasts), OnExpiry: "the fingerprint stays held", - Options: []asks.Option{{ID: "keep", Label: "Keep it held", Does: "keeps it held", Level: asks.Acknowledge}, {ID: "release", Label: "Release it", Does: "lets the move go on", Level: asks.Acknowledge}}} - value, err = delivery.EncodeAssignerAsk(key, delivery.AssignerAsk{Ask: ask}) + ask = oc.Ask{ID: delivery.AskID(key), Headline: "plex is held after a failed health check", Explanation: "ace failed the check.", Who: occ.Operator, + Expires: occ.FormatTime(now.Add(occ.ApproveLasts)), OnExpiry: "the fingerprint stays held", + Options: []oc.Option{{ID: "keep", Label: "Keep it held", Does: "keeps it held", Level: occ.Acknowledge}, {ID: "release", Label: "Release it", Does: "lets the move go on", Level: occ.Acknowledge}}} + value, err = delivery.EncodeAssignerAsk(key, delivery.AssignerAsk{Ask: rawAsk(t, ask)}) if err != nil { t.Fatal(err) } @@ -195,7 +196,7 @@ func (v timedView) GetAt(key string) ([]byte, time.Time, bool, error) { // teller replaces the answer of that asking with "release", whose warrant names the asking and the view's time; a view answer // written before the phone's answer, the same option, or an ask of another kind replaces nothing. func TestALaterViewAnswerChangesAHoldsAnswer(t *testing.T) { - setup := func(viewAt time.Time, option string) (*bed, string, asks.Ask) { + setup := func(viewAt time.Time, option string) (*bed, string, oc.Ask) { b := newBed(t) assigner := memGate{} view := timedView{memGate: memGate{}, at: map[string]time.Time{}} diff --git a/modules/mesh-delivery-teller/go.mod b/modules/mesh-delivery-teller/go.mod index add68f57..81e64235 100644 --- a/modules/mesh-delivery-teller/go.mod +++ b/modules/mesh-delivery-teller/go.mod @@ -21,6 +21,11 @@ require ( golang.org/x/time v0.15.0 // indirect ) +require ( + git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-00010101000000-000000000000 +) + require git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000 replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-cli/main_test.go b/modules/mesh-delivery-view/cmd/mesh-delivery-cli/main_test.go index a9ae081f..ba1c0610 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-cli/main_test.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-cli/main_test.go @@ -10,7 +10,8 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/delivery" "mesh-delivery-view/internal/view" @@ -125,10 +126,10 @@ func TestPrintsTheOpenAsksInTheTableAndTheJSON(t *testing.T) { // 1345/AC2 if err != nil { t.Fatal(err) } - ask := asks.Ask{ID: delivery.AskID(key), Headline: "Merge into main: mesh-delivery#12", Explanation: "Merges one pull request.", Who: asks.Operator, - Expires: time.Now().Add(time.Hour), OnExpiry: "the merge stays held", - Options: []asks.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: asks.Approve, Binds: "sha256:00"}}} - value, err := delivery.EncodeAsk(delivery.Ask{Group: "feat/x", Heads: heads, Ask: ask}) + ask := oc.Ask{ID: delivery.AskID(key), Headline: "Merge into main: mesh-delivery#12", Explanation: "Merges one pull request.", Who: occ.Operator, + Expires: occ.FormatTime(time.Now().Add(time.Hour)), OnExpiry: "the merge stays held", + Options: []oc.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: occ.Approve, Binds: "sha256:00"}}} + value, err := delivery.EncodeAsk(delivery.Ask{Group: "feat/x", Heads: heads, Ask: rawAsk(t, ask)}) if err != nil { t.Fatal(err) } @@ -201,17 +202,17 @@ func TestTheAnswerCommandAnswersAnAskNeedingNoProof(t *testing.T) { // 1297/B9b if err != nil { t.Fatal(err) } - hold := asks.Ask{ID: delivery.AskID(key), Headline: "plex is held", Explanation: "ace failed the check.", Who: asks.Operator, Expires: time.Now().Add(time.Hour), OnExpiry: "stays held", - Options: []asks.Option{{ID: "release", Label: "Release", Does: "goes on", Level: asks.Approve}, {ID: "keep", Label: "Keep", Does: "stays", Level: asks.Approve}}} - value, _ := delivery.EncodeAssignerAsk(key, delivery.AssignerAsk{Ask: hold}) + hold := oc.Ask{ID: delivery.AskID(key), Headline: "plex is held", Explanation: "ace failed the check.", Who: occ.Operator, Expires: occ.FormatTime(time.Now().Add(time.Hour)), OnExpiry: "stays held", + Options: []oc.Option{{ID: "release", Label: "Release", Does: "goes on", Level: occ.Approve}, {ID: "keep", Label: "Keep", Does: "stays", Level: occ.Approve}}} + value, _ := delivery.EncodeAssignerAsk(key, delivery.AssignerAsk{Ask: rawAsk(t, hold)}) heads := []delivery.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: "0123456789abcdef0123456789abcdef01234567"}} mkey, err := delivery.MergeAskKey("feat/x", heads) if err != nil { t.Fatal(err) } - merge := asks.Ask{ID: delivery.AskID(mkey), Headline: "Merge", Explanation: "Merges.", Who: asks.Operator, Expires: time.Now().Add(time.Hour), OnExpiry: "held", - Options: []asks.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: asks.Approve, Binds: "sha256:00"}}} - mv, err := delivery.EncodeAsk(delivery.Ask{Group: "feat/x", Heads: heads, Ask: merge}) + merge := oc.Ask{ID: delivery.AskID(mkey), Headline: "Merge", Explanation: "Merges.", Who: occ.Operator, Expires: occ.FormatTime(time.Now().Add(time.Hour)), OnExpiry: "held", + Options: []oc.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: occ.Approve, Binds: "sha256:00"}}} + mv, err := delivery.EncodeAsk(delivery.Ask{Group: "feat/x", Heads: heads, Ask: rawAsk(t, merge)}) if err != nil { t.Fatal(err) } @@ -253,9 +254,9 @@ func TestTheHoldCommandsListAndAnswerHolds(t *testing.T) { if err != nil { t.Fatal(err) } - hold := asks.Ask{ID: delivery.AskID(key), Headline: "systemd-resolved is held", Explanation: "ace failed the check.", Who: asks.Operator, Expires: time.Now().Add(time.Hour), OnExpiry: "stays held", - Options: []asks.Option{{ID: "keep", Label: "Keep", Does: "stays", Level: asks.Acknowledge}, {ID: "release", Label: "Release", Does: "goes on", Level: asks.Acknowledge}}} - value, _ := delivery.EncodeAssignerAsk(key, delivery.AssignerAsk{Ask: hold}) + hold := oc.Ask{ID: delivery.AskID(key), Headline: "systemd-resolved is held", Explanation: "ace failed the check.", Who: occ.Operator, Expires: occ.FormatTime(time.Now().Add(time.Hour)), OnExpiry: "stays held", + Options: []oc.Option{{ID: "keep", Label: "Keep", Does: "stays", Level: occ.Acknowledge}, {ID: "release", Label: "Release", Does: "goes on", Level: occ.Acknowledge}}} + value, _ := delivery.EncodeAssignerAsk(key, delivery.AssignerAsk{Ask: rawAsk(t, hold)}) hkey, _ := delivery.HeldKey("systemd-resolved", fp) held, _ := delivery.EncodeHeld(delivery.Held{Node: "ace", Words: "health failed, phase running: dns timeout"}) ans, _ := delivery.EncodeAnswer(delivery.Answer{Option: "keep", Warrant: hold.ID + "@2026-10-25T04:00:00Z", Words: "the operator chose keep"}) @@ -287,3 +288,13 @@ func TestTheHoldCommandsListAndAnswerHolds(t *testing.T) { t.Errorf("hold-keep without a fingerprint: exit %d", code) } } + +// rawAsk is the router's ask as the JSON a gate's or an assigner's value carries (mesh-sdk keeps it raw, ADR 0627 section 4). +func rawAsk(t *testing.T, a oc.Ask) json.RawMessage { + t.Helper() + b, err := json.Marshal(a) + if err != nil { + t.Fatal(err) + } + return b +} diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view/integration_test.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view/integration_test.go index 44cb4fac..4d1f43f4 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view/integration_test.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view/integration_test.go @@ -16,7 +16,8 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/process" @@ -383,10 +384,10 @@ func TestProcessWholeShowsTheOpenAsksAndDropsTheAnsweredOnes(t *testing.T) { // if err != nil { t.Fatal(err) } - ask := asks.Ask{ID: delivery.AskID(key), Headline: "Merge into main: mesh-delivery#12", Explanation: "Merges one pull request.", Who: asks.Operator, - Expires: time.Now().Add(time.Hour), OnExpiry: "the merge stays held", - Options: []asks.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: asks.Approve, Binds: "sha256:00"}}} - value, err := delivery.EncodeAsk(delivery.Ask{Group: "feat/x", Heads: heads, Ask: ask}) + ask := oc.Ask{ID: delivery.AskID(key), Headline: "Merge into main: mesh-delivery#12", Explanation: "Merges one pull request.", Who: occ.Operator, + Expires: occ.FormatTime(time.Now().Add(time.Hour)), OnExpiry: "the merge stays held", + Options: []oc.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: occ.Approve, Binds: "sha256:00"}}} + value, err := delivery.EncodeAsk(delivery.Ask{Group: "feat/x", Heads: heads, Ask: rawAsk(t, ask)}) if err != nil { t.Fatal(err) } @@ -564,10 +565,10 @@ func TestProcessWholeWritesTheAnswerToAnAskNeedingNoProof(t *testing.T) { // 129 if err != nil { t.Fatal(err) } - ask := asks.Ask{ID: delivery.AskID(key), Headline: "plex is held after a failed health check", Explanation: "ace failed the check.", Who: asks.Operator, - Expires: time.Now().Add(time.Hour), OnExpiry: "the fingerprint stays held", - Options: []asks.Option{{ID: "release", Label: "Release", Does: "lets the move go on", Level: asks.Approve}, {ID: "keep", Label: "Keep held", Does: "keeps it held", Level: asks.Approve}}} - value, _ := delivery.EncodeAssignerAsk(key, delivery.AssignerAsk{Ask: ask}) + ask := oc.Ask{ID: delivery.AskID(key), Headline: "plex is held after a failed health check", Explanation: "ace failed the check.", Who: occ.Operator, + Expires: occ.FormatTime(time.Now().Add(time.Hour)), OnExpiry: "the fingerprint stays held", + Options: []oc.Option{{ID: "release", Label: "Release", Does: "lets the move go on", Level: occ.Approve}, {ID: "keep", Label: "Keep held", Does: "keeps it held", Level: occ.Approve}}} + value, _ := delivery.EncodeAssignerAsk(key, delivery.AssignerAsk{Ask: rawAsk(t, ask)}) if _, err := askBucket.Put(ctx, key, value); err != nil { t.Fatal(err) } @@ -604,9 +605,9 @@ func TestProcessWholeWritesTheAnswerToAnAskNeedingNoProof(t *testing.T) { // 129 if err != nil { t.Fatal(err) } - merge := asks.Ask{ID: delivery.AskID(mkey), Headline: "Merge", Explanation: "Merges.", Who: asks.Operator, Expires: time.Now().Add(time.Hour), OnExpiry: "held", - Options: []asks.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: asks.Approve, Binds: "sha256:00"}}} - mv, err := delivery.EncodeAsk(delivery.Ask{Group: "feat/x", Heads: heads, Ask: merge}) + merge := oc.Ask{ID: delivery.AskID(mkey), Headline: "Merge", Explanation: "Merges.", Who: occ.Operator, Expires: occ.FormatTime(time.Now().Add(time.Hour)), OnExpiry: "held", + Options: []oc.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: occ.Approve, Binds: "sha256:00"}}} + mv, err := delivery.EncodeAsk(delivery.Ask{Group: "feat/x", Heads: heads, Ask: rawAsk(t, merge)}) if err != nil { t.Fatal(err) } diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view/main_test.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view/main_test.go index e60edff4..120a2944 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view/main_test.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view/main_test.go @@ -2,9 +2,12 @@ package main import ( "context" + "encoding/json" "io" "strings" "testing" + + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" ) func TestRunNeedsItsCredential(t *testing.T) { @@ -13,3 +16,13 @@ func TestRunNeedsItsCredential(t *testing.T) { t.Fatalf("run = %v, want a refusal naming MESH_BROKER_FILE", err) } } + +// rawAsk is the router's ask as the JSON a gate's or an assigner's value carries (mesh-sdk keeps it raw, ADR 0627 section 4). +func rawAsk(t *testing.T, a oc.Ask) json.RawMessage { + t.Helper() + b, err := json.Marshal(a) + if err != nil { + t.Fatal(err) + } + return b +} diff --git a/modules/mesh-delivery-view/go.mod b/modules/mesh-delivery-view/go.mod index 46742fd5..6ab33fe9 100644 --- a/modules/mesh-delivery-view/go.mod +++ b/modules/mesh-delivery-view/go.mod @@ -21,6 +21,11 @@ require ( golang.org/x/time v0.15.0 // indirect ) +require ( + git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-00010101000000-000000000000 +) + require git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000 replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk diff --git a/modules/mesh-delivery-view/internal/view/answers_test.go b/modules/mesh-delivery-view/internal/view/answers_test.go index a40aba06..b936c375 100644 --- a/modules/mesh-delivery-view/internal/view/answers_test.go +++ b/modules/mesh-delivery-view/internal/view/answers_test.go @@ -10,7 +10,8 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -33,16 +34,16 @@ func (w *memWriter) Put(key string, value []byte) error { // holdAsk is a key of the assigner's asks: the ask about a held fingerprint, as a fixture (in release 5 the assigner writes // none yet; the value is `{"v":1,"ask":}`, as the gate's minus group and heads). -func holdAsk(t *testing.T) (Entry, asks.Ask) { +func holdAsk(t *testing.T) (Entry, oc.Ask) { t.Helper() key, err := delivery.HoldAskKey("plex", fpNew) if err != nil { t.Fatal(err) } - a := asks.Ask{ID: delivery.AskID(key), Headline: "plex is held after a failed health check", Explanation: "ace failed the check on the new build.", Who: asks.Operator, - Expires: t0.Add(23 * time.Hour), OnExpiry: "the fingerprint stays held", - Options: []asks.Option{{ID: "release", Label: "Release", Does: "lets the move go on", Level: asks.Approve}, {ID: "keep", Label: "Keep held", Does: "keeps it held", Level: asks.Approve}}} - v, err := delivery.EncodeAssignerAsk(key, delivery.AssignerAsk{Ask: a}) + a := oc.Ask{ID: delivery.AskID(key), Headline: "plex is held after a failed health check", Explanation: "ace failed the check on the new build.", Who: occ.Operator, + Expires: occ.FormatTime(t0.Add(23 * time.Hour)), OnExpiry: "the fingerprint stays held", + Options: []oc.Option{{ID: "release", Label: "Release", Does: "lets the move go on", Level: occ.Approve}, {ID: "keep", Label: "Keep held", Does: "keeps it held", Level: occ.Approve}}} + v, err := delivery.EncodeAssignerAsk(key, delivery.AssignerAsk{Ask: rawAsk(t, a)}) if err != nil { t.Fatal(err) } diff --git a/modules/mesh-delivery-view/internal/view/asks_test.go b/modules/mesh-delivery-view/internal/view/asks_test.go index d5e9c084..6fbf270b 100644 --- a/modules/mesh-delivery-view/internal/view/asks_test.go +++ b/modules/mesh-delivery-view/internal/view/asks_test.go @@ -9,7 +9,8 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -24,9 +25,9 @@ func askEntry(t *testing.T, group, headline, words string) Entry { if err != nil { t.Fatal(err) } - a := asks.Ask{ID: delivery.AskID(key), Headline: headline, Explanation: words, Who: asks.Operator, Expires: t0.Add(23 * time.Hour), OnExpiry: "the merge stays held", - Options: []asks.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: asks.Approve, Binds: "sha256:00"}}} - v, err := delivery.EncodeAsk(delivery.Ask{Group: group, Heads: heads, Ask: a}) + a := oc.Ask{ID: delivery.AskID(key), Headline: headline, Explanation: words, Who: occ.Operator, Expires: occ.FormatTime(t0.Add(23 * time.Hour)), OnExpiry: "the merge stays held", + Options: []oc.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: occ.Approve, Binds: "sha256:00"}}} + v, err := delivery.EncodeAsk(delivery.Ask{Group: group, Heads: heads, Ask: rawAsk(t, a)}) if err != nil { t.Fatal(err) } @@ -168,10 +169,10 @@ func TestAnAnswerToAnEarlierAskingDoesNotHideTheNextAsking(t *testing.T) { // R2 } // The gate rewrites the key at asking 2; only asking 1's answer exists. heads := []delivery.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: askHeadA}} - a := asks.Ask{ID: delivery.AskingID(e.Key, 2), Headline: "Merge into main: mesh-delivery#12", Explanation: "Merges one pull request.", Who: asks.Operator, - Expires: t0.Add(23 * time.Hour), OnExpiry: "the merge stays held", - Options: []asks.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: asks.Approve, Binds: "sha256:00"}}} - v, err := delivery.EncodeAsk(delivery.Ask{Group: "feat/x", Heads: heads, Asking: 2, Ask: a}) + a := oc.Ask{ID: delivery.AskingID(e.Key, 2), Headline: "Merge into main: mesh-delivery#12", Explanation: "Merges one pull request.", Who: occ.Operator, + Expires: occ.FormatTime(t0.Add(23 * time.Hour)), OnExpiry: "the merge stays held", + Options: []oc.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: occ.Approve, Binds: "sha256:00"}}} + v, err := delivery.EncodeAsk(delivery.Ask{Group: "feat/x", Heads: heads, Asking: 2, Ask: rawAsk(t, a)}) if err != nil { t.Fatal(err) } @@ -184,3 +185,13 @@ func TestAnAnswerToAnEarlierAskingDoesNotHideTheNextAsking(t *testing.T) { // R2 t.Errorf("asking 2 is answered and listed: %+v", got) } } + +// rawAsk is the router's ask as the JSON a gate's or an assigner's value carries (mesh-sdk keeps it raw, ADR 0627 section 4). +func rawAsk(t *testing.T, a oc.Ask) json.RawMessage { + t.Helper() + b, err := json.Marshal(a) + if err != nil { + t.Fatal(err) + } + return b +} diff --git a/modules/mesh-delivery-view/internal/view/holds.go b/modules/mesh-delivery-view/internal/view/holds.go index d5fa69f9..a2b2f524 100644 --- a/modules/mesh-delivery-view/internal/view/holds.go +++ b/modules/mesh-delivery-view/internal/view/holds.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "git.novox.be/novox/mesh-sdk/go/asks" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -128,7 +128,7 @@ func (m *Model) AnswerHold(module, fp, option, words string) (HoldRow, error) { m.mu.Lock() a, w := m.aasks[r.AskKey], m.writer m.mu.Unlock() - if !slices.ContainsFunc(a.ask.Options, func(o asks.Option) bool { return o.ID == option }) { + if !slices.ContainsFunc(a.ask.Options, func(o oc.Option) bool { return o.ID == option }) { return r, fmt.Errorf("the hold ask %s offers no option %q", r.AskKey, option) } if w == nil { diff --git a/modules/mesh-delivery-view/internal/view/view.go b/modules/mesh-delivery-view/internal/view/view.go index 1b3cd75b..e17432ad 100644 --- a/modules/mesh-delivery-view/internal/view/view.go +++ b/modules/mesh-delivery-view/internal/view/view.go @@ -11,7 +11,8 @@ import ( "sync" "time" - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -330,7 +331,7 @@ type sourceEntry struct { } type assignerAskRecord struct { - ask asks.Ask + ask oc.Ask err string } @@ -341,6 +342,7 @@ type moduleEntry struct { type askRecord struct { value delivery.Ask + ask oc.Ask // the router's ask the gate's value carries err string } @@ -554,7 +556,11 @@ func (m *Model) apply(e Entry) { return } a, err := delivery.DecodeAsk(e.Value) - m.asks[e.Key] = askRecord{value: a, err: errText(err)} + var ask oc.Ask + if err == nil { + ask, err = routerAsk(e.Key, a.Asking, a.Ask) + } + m.asks[e.Key] = askRecord{value: a, ask: ask, err: errText(err)} case delivery.AssignerAsksBucket: if e.Deleted { delete(m.aasks, e.Key) @@ -923,11 +929,11 @@ const ( func (m *Model) askRows() []AskRow { out := []AskRow{} for key, a := range m.asks { - if a.err != "" || m.answers[key] == a.value.Ask.ID { + if a.err != "" || m.answers[key] == a.ask.ID { continue } - row := AskRow{Key: key, Group: a.value.Group, Headline: a.value.Ask.Headline, Words: a.value.Ask.Explanation, - Expires: a.value.Ask.Expires, How: AnswerOnThePhone, Pulls: []string{}} + row := AskRow{Key: key, Group: a.value.Group, Headline: a.ask.Headline, Words: a.ask.Explanation, + Expires: expiry(a.ask), How: AnswerOnThePhone, Pulls: []string{}} for _, h := range a.value.Heads { head := h.Head if len(head) > 12 { @@ -941,7 +947,7 @@ func (m *Model) askRows() []AskRow { if a.err != "" || m.answers[key] == a.ask.ID { continue } - row := AskRow{Key: key, Headline: a.ask.Headline, Words: a.ask.Explanation, Expires: a.ask.Expires, How: AnswerInTheView, Pulls: []string{}, + row := AskRow{Key: key, Headline: a.ask.Headline, Words: a.ask.Explanation, Expires: expiry(a.ask), How: AnswerInTheView, Pulls: []string{}, Chosen: m.chosenOf(key, a.ask)} for _, o := range a.ask.Options { row.Options = append(row.Options, o.ID) @@ -952,18 +958,48 @@ func (m *Model) askRows() []AskRow { return out } +// routerAsk reads the router's ask a gate's or an assigner's value carries (operator-channel's ask as JSON). mesh-sdk keeps +// no check of what the ask says (novox/hq ADR 0627 section 4): its id is the asking's (AskingID of key and asking) and it +// offers an option, as the old decoding refused. +func routerAsk(key string, asking int, raw json.RawMessage) (oc.Ask, error) { + var a oc.Ask + if err := json.Unmarshal(raw, &a); err != nil { + return oc.Ask{}, fmt.Errorf("the router's ask is not readable: %w", err) + } + if want := delivery.AskingID(key, asking); a.ID != want { + return oc.Ask{}, fmt.Errorf("the ask's id %q is not its asking's %q", a.ID, want) + } + if len(a.Options) == 0 { + return oc.Ask{}, fmt.Errorf("the ask offers no option") + } + return a, nil +} + +// expiry is when the ask lapses; zero when it says none or says it unreadably. +func expiry(a oc.Ask) time.Time { + t, err := occ.Time(a.Expires) + if err != nil { + return time.Time{} + } + return t +} + // decodeAssignerAsk reads a key of the assigner's asks (mesh-sdk go/delivery DecodeAssignerAsk): the router's ask under its -// asking's id, of a kind that needs no proof, offering an option. -func decodeAssignerAsk(key string, value []byte) (asks.Ask, error) { +// asking's id, offering an option. +func decodeAssignerAsk(key string, value []byte) (oc.Ask, error) { a, err := delivery.DecodeAssignerAsk(key, value) if err != nil { - return asks.Ask{}, fmt.Errorf("the assigner's ask %s is not readable: %w", key, err) + return oc.Ask{}, fmt.Errorf("the assigner's ask %s is not readable: %w", key, err) } - return a.Ask, nil + ask, err := routerAsk(key, a.Asking, a.Ask) + if err != nil { + return oc.Ask{}, fmt.Errorf("the assigner's ask %s is not readable: %w", key, err) + } + return ask, nil } // chosenOf is the option the view wrote for this asking of the ask: an answer to another asking (a stale one) is none. -func (m *Model) chosenOf(key string, ask asks.Ask) string { +func (m *Model) chosenOf(key string, ask oc.Ask) string { if a, ok := m.chosen[key]; ok && a.Asking == ask.ID { return a.Option } @@ -1002,7 +1038,7 @@ func (m *Model) Answer(askKey, option, words string) error { if !open { return fmt.Errorf("the view holds no open ask %s", askKey) } - if !slices.ContainsFunc(a.ask.Options, func(o asks.Option) bool { return o.ID == option }) { + if !slices.ContainsFunc(a.ask.Options, func(o oc.Option) bool { return o.ID == option }) { ids := []string{} for _, o := range a.ask.Options { ids = append(ids, o.ID) diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/end_test.go b/modules/mesh-enrolment/cmd/mesh-enrolment/end_test.go index a341180f..0f1802cc 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/end_test.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/end_test.go @@ -3,7 +3,7 @@ package main import ( "encoding/json" "errors" - "git.novox.be/novox/mesh-sdk/go/asks" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/delivery" "reflect" "slices" @@ -113,13 +113,13 @@ func revokeEndsAtAnyStageAndAnExpiryMarkerSaysExpired(t *testing.T) { _ = r.st.Delete(NodeKey(node)) // and the key is gone by the next pass }, false}, {"after a decline", func(r *rig, node string) { - _ = r.h.Warranted(asks.Option{ID: "no"}, asks.Warrant{Ask: "ask-" + node}) + _ = r.h.Warranted(oc.Option{ID: "no"}, oc.Warrant{Ask: "ask-" + node}) r.asker.answer("ask-"+node, "no") }, false}, {"after a refused add", func(r *rig, node string) { delete(r.rd["mesh-delivery-assigner.placement"], node+".node") // the add is tried, and refused r.placeErr = errors.New("mesh-placement did not take the node: the proposal is refused: change 1: an address taken") - _ = r.h.Warranted(yes, asks.Warrant{Ask: "ask-" + node}) + _ = r.h.Warranted(yes, oc.Warrant{Ask: "ask-" + node}) r.asker.answer("ask-"+node, "yes") r.placeErr = nil if !slices.Contains(r.events(), EventRefused) { diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/holder.go b/modules/mesh-enrolment/cmd/mesh-enrolment/holder.go index 3251ef43..c6503f10 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/holder.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/holder.go @@ -11,7 +11,8 @@ import ( "sync" "time" - "git.novox.be/novox/mesh-sdk/go/asks" + mv "git.novox.be/novox/mesh-contracts/go/seats/meshvault" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -21,26 +22,26 @@ type Vault interface { // Token asks the vault to make the token and its hash for node, expiring at expires, and answers what the joining // machine must trust (the bus's address and fingerprint, the vault's signing key). No token is in the answer. // The token carries tunnel, the node's first tunnel built at issue (ADR 0587 §3, insight of 2026-11-02). - Token(node string, expires time.Time, tunnel delivery.TokenTunnel) (delivery.EnrolmentTokenAnswer, error) + Token(node string, expires time.Time, tunnel delivery.TokenTunnel) (mv.EnrolmentTokenAnswer, error) // Sealed asks the vault for node's token sealed to the operator key, as the vault sealed it. Sealed(node string) (string, error) // Drop asks the vault to drop node's token and its hash; it answers only once both are gone. Drop(node string) error // Account asks for node's account sealed to the sealing key; the same node and keys again answer the same account. - Account(node, sealingKey, keysDigest string) (delivery.NodeAccountAnswer, error) + Account(node, sealingKey, keysDigest string) (mv.NodeAccountAnswer, error) // Retire asks the vault to retire the account it minted for node. Retire(node string) error // SignCertificate and SignReply sign through the vault's `sign`; the certificate's call carries every public key the node // presented, which the vault checks against the digest node-account was minted for. - SignCertificate(node string, keys delivery.JoiningKeys) (delivery.SignAnswer, error) - SignReply(node string, reply []byte) (delivery.SignAnswer, error) + SignCertificate(node string, keys delivery.JoiningKeys) (mv.SignAnswer, error) + SignReply(node string, reply []byte) (mv.SignAnswer, error) // Signer is the vault's public signing key in base64, which the redeem answer names for the node to check it against its token. Signer() (string, error) } -// Asker is the operator's channel as the holder asks it (asker.Client is one). +// Asker is the operator's channel as the holder asks it (occ.Client is one). type Asker interface { - Ask(asks.Ask) error + Ask(oc.Ask) error Cancel(id string) error } diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/integration_test.go b/modules/mesh-enrolment/cmd/mesh-enrolment/integration_test.go index c544016f..bd80387f 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/integration_test.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/integration_test.go @@ -23,8 +23,9 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" + mv "git.novox.be/novox/mesh-contracts/go/seats/meshvault" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/asks" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/testbus" ) @@ -155,12 +156,12 @@ func (v *standInVault) note(c string) { } // Token makes the token as the vault does: the bus it states, its signing key, a secret, and the first tunnel it is handed. -func (v *standInVault) Token(node string, expires time.Time, tunnel delivery.TokenTunnel) (delivery.EnrolmentTokenAnswer, error) { +func (v *standInVault) Token(node string, expires time.Time, tunnel delivery.TokenTunnel) (mv.EnrolmentTokenAnswer, error) { v.mu.Lock() v.tokens[node] = delivery.Token{Version: 1, Node: node, Broker: "bus:4222", Fingerprint: "f", Signer: make([]byte, 32), Secret: "secret-" + node, Tunnel: &tunnel} v.mu.Unlock() v.note("token " + node) - return delivery.EnrolmentTokenAnswer{Node: node, Broker: "bus:4222", Fingerprint: "f", Signer: "s"}, nil + return mv.EnrolmentTokenAnswer{Node: node, Broker: "bus:4222", Fingerprint: "f", Signer: "s"}, nil } func (v *standInVault) Sealed(node string) (string, error) { return "sealed-" + node, nil } func (v *standInVault) Drop(node string) error { @@ -170,22 +171,22 @@ func (v *standInVault) Drop(node string) error { v.note("drop " + node) return nil } -func (v *standInVault) Account(node, sealing, digest string) (delivery.NodeAccountAnswer, error) { +func (v *standInVault) Account(node, sealing, digest string) (mv.NodeAccountAnswer, error) { raw, err := delivery.EncodeNodeLogin(delivery.NodeLogin{Node: node, Login: delivery.FirstLogin(node), Since: time.Now().UTC()}) if err != nil { - return delivery.NodeAccountAnswer{}, err + return mv.NodeAccountAnswer{}, err } if _, err := v.logins.Put(v.ctx, node, raw); err != nil { - return delivery.NodeAccountAnswer{}, err + return mv.NodeAccountAnswer{}, err } - return delivery.NodeAccountAnswer{Node: node, Login: delivery.FirstLogin(node), Account: "account-" + node}, nil + return mv.NodeAccountAnswer{Node: node, Login: delivery.FirstLogin(node), Account: "account-" + node}, nil } func (v *standInVault) Retire(node string) error { return nil } -func (v *standInVault) SignCertificate(node string, _ delivery.JoiningKeys) (delivery.SignAnswer, error) { - return delivery.SignAnswer{Kind: delivery.KindNodeCertificate, Node: node, Certificate: "PEM"}, nil +func (v *standInVault) SignCertificate(node string, _ delivery.JoiningKeys) (mv.SignAnswer, error) { + return mv.SignAnswer{Kind: delivery.KindNodeCertificate, Node: node, Certificate: "PEM"}, nil } -func (v *standInVault) SignReply(node string, reply []byte) (delivery.SignAnswer, error) { - return delivery.SignAnswer{Kind: delivery.KindEnrolment, Node: node, Signature: "sig", Signer: "fp"}, nil +func (v *standInVault) SignReply(node string, reply []byte) (mv.SignAnswer, error) { + return mv.SignAnswer{Kind: delivery.KindEnrolment, Node: node, Signature: "sig", Signer: "fp"}, nil } func (v *standInVault) Signer() (string, error) { return "vault-key", nil } @@ -341,7 +342,7 @@ func TestMeshEnrolmentWholeOnARealBus(t *testing.T) { if err != nil { t.Fatal(err) } - if err := h.Warranted(asks.Option{ID: "yes"}, asks.Warrant{Ask: a.Ask}); err != nil { + if err := h.Warranted(oc.Option{ID: "yes"}, oc.Warrant{Ask: a.Ask}); err != nil { t.Fatal(err) } waitFor("the user enrol.ace is composed", func() bool { return hasUser("enrol.ace") }) @@ -395,7 +396,7 @@ func TestMeshEnrolmentWholeOnARealBus(t *testing.T) { if _, err := h.Issue("person.operator", "g14", "a second machine", tunnelKey, 2*time.Second); err != nil { t.Fatal(err) } - if err := h.Warranted(asks.Option{ID: "yes"}, asks.Warrant{Ask: asker.asked[len(asker.asked)-1]}); err != nil { + if err := h.Warranted(oc.Option{ID: "yes"}, oc.Warrant{Ask: asker.asked[len(asker.asked)-1]}); err != nil { t.Fatal(err) } waitFor("enrolment-expired is said for g14", func() bool { return slices.Contains(events(), EventExpired) }) @@ -420,7 +421,7 @@ func TestMeshEnrolmentWholeOnARealBus(t *testing.T) { if err != nil { t.Fatal(err) } - if err := h.Warranted(asks.Option{ID: "yes"}, asks.Warrant{Ask: b.Ask}); err != nil { + if err := h.Warranted(oc.Option{ID: "yes"}, oc.Warrant{Ask: b.Ask}); err != nil { t.Fatal(err) } asker.answer(b.Ask, "yes") // the router's record of the answer, which the catch-up reads (ADR 0589 §6) diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/issue.go b/modules/mesh-enrolment/cmd/mesh-enrolment/issue.go index 4cd5ea4c..d37a2155 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/issue.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/issue.go @@ -8,9 +8,9 @@ import ( "strings" "time" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/asker" - "git.novox.be/novox/mesh-sdk/go/asks" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -162,26 +162,26 @@ func (h *Holder) release(e Enrolment) error { // AskOf is the ask of an enrolment, in the words the operator reads: that a machine of this name may join until the // expiry at this address. Its yes binds the act (name, address, expiry). -func (h *Holder) AskOf(e Enrolment) (asks.Ask, error) { - act := asks.Act{"verb": Seat + ".issue", "module": Module, "node": e.Node, "address": e.Address, +func (h *Holder) AskOf(e Enrolment) (oc.Ask, error) { + act := occ.Act{"verb": Seat + ".issue", "module": Module, "node": e.Node, "address": e.Address, "expires": e.Expires.UTC().Format(time.RFC3339)} - bind, err := asks.ActDigest(act) + bind, err := occ.ActDigest(act) if err != nil { - return asks.Ask{}, err + return oc.Ask{}, err } until := e.Expires.UTC().Format("2006-01-02 15:04 MST") closes := h.now().Add(h.AskLasts) if closes.After(e.Expires) { closes = e.Expires } - return asks.Ask{ID: e.Ask, Headline: "Let the machine " + e.Node + " join the mesh?", + return oc.Ask{ID: e.Ask, Headline: "Let the machine " + e.Node + " join the mesh?", Explanation: "A machine named " + e.Node + " may join the mesh until " + until + ", at the private address " + e.Address + ".\n" + "Your yes lets the vault make a one-time token for it; you open the token at your own desk and type it on the new machine.\n" + "What the machine runs is placed later, as for any machine.", - Who: asks.Operator, - Options: []asks.Option{{ID: "yes", Label: "Yes, let it join", Does: "The vault makes a one-time token for " + e.Node + ", valid until " + until + ".", Level: asks.Approve, Binds: bind}, - {ID: "no", Label: "No", Does: "Nothing is made; the address is free again.", Level: asks.Acknowledge}}, - Expires: closes, OnExpiry: "No token is made and the address is free again.", About: "mesh-enrolment-" + e.Node, + Who: occ.Operator, + Options: []oc.Option{{ID: "yes", Label: "Yes, let it join", Does: "The vault makes a one-time token for " + e.Node + ", valid until " + until + ".", Level: occ.Approve, Binds: bind}, + {ID: "no", Label: "No", Does: "Nothing is made; the address is free again.", Level: occ.Acknowledge}}, + Expires: occ.FormatTime(closes), OnExpiry: "No token is made and the address is free again.", About: "mesh-enrolment-" + e.Node, Details: "Asked by " + e.Caller + ": " + e.Why}, nil } @@ -220,10 +220,10 @@ func (h *Holder) byAsk(ask string) (Enrolment, Row, bool, error) { return Enrolment{}, Row{}, false, nil } -// Warranted is the operator's answer, handed once by the asker (asker.Client.Listen): a no ends the enrolment; a yes records +// Warranted is the operator's answer, handed once by the asker (occ.Client.Listen): a no ends the enrolment; a yes records // the warrant, adds the node to placement with its tunnel key, asks the vault for the token, moves the key to live with the bus's expiry at the token's, and says // enrolment-issued (ADR 0587 §4, §5). -func (h *Holder) Warranted(o asks.Option, w asks.Warrant) error { +func (h *Holder) Warranted(o oc.Option, w oc.Warrant) error { e, _, found, err := h.byAsk(w.Ask) if err != nil { return err @@ -315,11 +315,11 @@ func (h *Holder) refuse(e Enrolment, reason string) error { } // Said is the asker's note of an ask that ended with no yes or no (expired, or refused by the router): the enrolment ends. -func (h *Holder) Said(hd asker.Handled, w asks.Warrant, err error) { +func (h *Holder) Said(hd occ.Handled, w oc.Warrant, err error) { if err != nil { h.logf("the answer to %s (%s) ended in an error: %v", w.Ask, hd, err) } - if hd != asker.Recorded { + if hd != occ.Recorded { return } e, _, found, ferr := h.byAsk(w.Ask) @@ -371,9 +371,9 @@ func (h *Holder) Again(node string) (IssueAnswer, error) { } // whereIs is the router's state of an ask, when the asker can read it. -func (h *Holder) whereIs(id string) (string, *asks.Warrant, error) { +func (h *Holder) whereIs(id string) (string, *oc.Warrant, error) { if w, ok := h.Asker.(interface { - Where(string) (string, *asks.Warrant, error) + Where(string) (string, *oc.Warrant, error) }); ok { return w.Where(id) } diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/issue_test.go b/modules/mesh-enrolment/cmd/mesh-enrolment/issue_test.go index 78142593..8abcec46 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/issue_test.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/issue_test.go @@ -7,12 +7,12 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-sdk/go/asker" - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/delivery" ) -var yes = asks.Option{ID: "yes"} +var yes = oc.Option{ID: "yes"} // 774/S55/AC1: issue refuses, naming every finding, what it must: a bad name, a name a node record, a placement key or an // open enrolment holds, and an expiry past a day; never the controller's lease, which no rig writes (issue 1126). @@ -90,10 +90,10 @@ func twoIssuesAtOnceHoldTwoAddressesAndAYesMakesTheToken(t *testing.T) { t.Fatalf("%+v, asked %v", e, r.asker.asked) } ask, _ := r.h.AskOf(e) - if ask.Options[0].Level != asks.Approve || ask.Options[0].Binds == "" || ask.Check(r.now) != nil { - t.Errorf("the ask does not authorise the act: %+v, %v", ask.Options[0], ask.Check(r.now)) + if ask.Options[0].Level != occ.Approve || ask.Options[0].Binds == "" || occ.Check(ask, r.now) != nil { + t.Errorf("the ask does not authorise the act: %+v, %v", ask.Options[0], occ.Check(ask, r.now)) } - if err := r.h.Warranted(yes, asks.Warrant{Ask: e.Ask}); err != nil { + if err := r.h.Warranted(yes, oc.Warrant{Ask: e.Ask}); err != nil { t.Fatal(err) } live, _ := r.enrolment("ace") @@ -123,7 +123,7 @@ func twoIssuesAtOnceHoldTwoAddressesAndAYesMakesTheToken(t *testing.T) { if strings.Contains(string(r.said[0].Body), "secret") || r.said[0].ID != eventID(EventIssued, live) { t.Errorf("the event %s carries a secret-like word or no stable id", r.said[0].Body) } - if err := r.h.Warranted(yes, asks.Warrant{Ask: e.Ask}); err != nil || len(r.said) != 1 { + if err := r.h.Warranted(yes, oc.Warrant{Ask: e.Ask}); err != nil || len(r.said) != 1 { t.Errorf("a second answer said %v: %v", r.events(), err) } } @@ -133,7 +133,7 @@ func twoIssuesAtOnceHoldTwoAddressesAndAYesMakesTheToken(t *testing.T) { func aDeclinedOrExpiredAskTakesBothKeysAndAVaultFailureKeepsTheEnrolmentForAgain(t *testing.T) { r := newRig(t) a, _ := r.h.Issue("person.operator", "ace", "a new machine", tunnelKey, 0) - if err := r.h.Warranted(asks.Option{ID: "no"}, asks.Warrant{Ask: a.Ask}); err != nil { + if err := r.h.Warranted(oc.Option{ID: "no"}, oc.Warrant{Ask: a.Ask}); err != nil { t.Fatal(err) } if len(r.keys()) != 0 || r.events()[0] != EventRefused || len(r.added) != 0 { @@ -141,13 +141,13 @@ func aDeclinedOrExpiredAskTakesBothKeysAndAVaultFailureKeepsTheEnrolmentForAgain } mustContain(t, string(r.said[0].Body), `"address":"`+a.Address+`"`) b, _ := r.h.Issue("person.operator", "g14", "a new machine", tunnelKey, 0) - r.h.Said(asker.Recorded, asks.Warrant{Ask: b.Ask, Outcome: asks.OutcomeExpired}, nil) + r.h.Said(occ.Recorded, oc.Warrant{Ask: b.Ask, Outcome: occ.OutcomeExpired}, nil) if len(r.keys()) != 0 || r.events()[1] != EventRefused { t.Fatalf("keys %v events %v", r.keys(), r.events()) } c, _ := r.h.Issue("person.operator", "novox", "a new machine", tunnelKey, 0) r.vault.fail["token novox"] = true - if err := r.h.Warranted(yes, asks.Warrant{Ask: c.Ask}); err == nil { + if err := r.h.Warranted(yes, oc.Warrant{Ask: c.Ask}); err == nil { t.Fatal("a vault that failed was taken for a token") } if e, _ := r.enrolment("novox"); e.Stage != StageAsked { @@ -162,7 +162,7 @@ func aDeclinedOrExpiredAskTakesBothKeysAndAVaultFailureKeepsTheEnrolmentForAgain if err != nil || again.Ask == c.Ask || len(r.asker.asked) != 4 { t.Fatalf("again %+v, %v, asked %v", again, err, r.asker.asked) } - if err := r.h.Warranted(yes, asks.Warrant{Ask: again.Ask}); err != nil { + if err := r.h.Warranted(yes, oc.Warrant{Ask: again.Ask}); err != nil { t.Fatal(err) } if e, _ := r.enrolment("novox"); e.Stage != StageLive { diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/kit_test.go b/modules/mesh-enrolment/cmd/mesh-enrolment/kit_test.go index 1c04bff1..7805125e 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/kit_test.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/kit_test.go @@ -11,7 +11,8 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-sdk/go/asks" + mv "git.novox.be/novox/mesh-contracts/go/seats/meshvault" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -129,28 +130,28 @@ func (v *fakeVault) do(what string) error { return nil } -func (v *fakeVault) Token(node string, expires time.Time, tunnel delivery.TokenTunnel) (delivery.EnrolmentTokenAnswer, error) { +func (v *fakeVault) Token(node string, expires time.Time, tunnel delivery.TokenTunnel) (mv.EnrolmentTokenAnswer, error) { if v.tunnels == nil { v.tunnels = map[string]delivery.TokenTunnel{} } v.tunnels[node] = tunnel - return delivery.EnrolmentTokenAnswer{Node: node, Broker: "b", Fingerprint: "f", Signer: "s"}, v.do("token " + node) + return mv.EnrolmentTokenAnswer{Node: node, Broker: "b", Fingerprint: "f", Signer: "s"}, v.do("token " + node) } func (v *fakeVault) Sealed(node string) (string, error) { return "sealed-for-" + node, v.do("sealed " + node) } func (v *fakeVault) Drop(node string) error { return v.do("drop " + node) } -func (v *fakeVault) Account(node, sealing, digest string) (delivery.NodeAccountAnswer, error) { - return delivery.NodeAccountAnswer{Node: node, Login: "node." + node, Account: "account-" + node}, v.do("account " + node) +func (v *fakeVault) Account(node, sealing, digest string) (mv.NodeAccountAnswer, error) { + return mv.NodeAccountAnswer{Node: node, Login: "node." + node, Account: "account-" + node}, v.do("account " + node) } func (v *fakeVault) Retire(node string) error { return v.do("retire " + node) } -func (v *fakeVault) SignCertificate(node string, k delivery.JoiningKeys) (delivery.SignAnswer, error) { +func (v *fakeVault) SignCertificate(node string, k delivery.JoiningKeys) (mv.SignAnswer, error) { serving := k.Serving v.cert = k - return delivery.SignAnswer{Kind: delivery.KindNodeCertificate, Node: node, Certificate: "PEM-" + serving}, v.do("certificate " + node) + return mv.SignAnswer{Kind: delivery.KindNodeCertificate, Node: node, Certificate: "PEM-" + serving}, v.do("certificate " + node) } -func (v *fakeVault) SignReply(node string, reply []byte) (delivery.SignAnswer, error) { - return delivery.SignAnswer{Kind: delivery.KindEnrolment, Node: node, Signature: "sig-" + delivery.DigestOf(reply)[7:15], Signer: "fp"}, v.do("sign " + node) +func (v *fakeVault) SignReply(node string, reply []byte) (mv.SignAnswer, error) { + return mv.SignAnswer{Kind: delivery.KindEnrolment, Node: node, Signature: "sig-" + delivery.DigestOf(reply)[7:15], Signer: "fp"}, v.do("sign " + node) } func (v *fakeVault) Signer() (string, error) { return "vault-signer", nil } @@ -171,16 +172,16 @@ func (a *fakeAsker) answer(ask, option string) { a.answers[ask] = option } -func (a *fakeAsker) Where(id string) (string, *asks.Warrant, error) { +func (a *fakeAsker) Where(id string) (string, *oc.Warrant, error) { a.mu.Lock() defer a.mu.Unlock() if o, ok := a.answers[id]; ok { - return "answered", &asks.Warrant{Ask: id, Option: o}, nil + return "answered", &oc.Warrant{Ask: id, Option: o}, nil } return "", nil, nil // no record read: as a router whose record of the ask is gone } -func (a *fakeAsker) Ask(k asks.Ask) error { +func (a *fakeAsker) Ask(k oc.Ask) error { a.mu.Lock() defer a.mu.Unlock() if a.err != nil { @@ -301,4 +302,4 @@ func errText(err error) string { return fmt.Sprint(err) } -func askWarrant(ask string) asks.Warrant { return asks.Warrant{Ask: ask} } +func askWarrant(ask string) oc.Warrant { return oc.Warrant{Ask: ask} } diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/main.go b/modules/mesh-enrolment/cmd/mesh-enrolment/main.go index 778c48d3..88a2269d 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/main.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/main.go @@ -12,8 +12,8 @@ import ( "os" "time" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/asker" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/handacts" "git.novox.be/novox/mesh-sdk/go/views" @@ -139,7 +139,7 @@ func newHolder(v *views.Reader) *Holder { call := Call(stdio.Ask) rec := handacts.Recorder{Ask: stdio.Ask} rd := followedReader{pics: v, rest: runtimeReader{}} - client := asker.New("asked") + client := occ.New("asked") return &Holder{ Store: runtimeStore{}, Read: rd, Vault: vaultOver{call: call, read: rd}, Asker: client, Say: func(event string, body any, id string) error { @@ -160,7 +160,7 @@ func newHolder(v *views.Reader) *Holder { } // warrantOf is the warrant id the teller writes, `@`, read from the router's record of the ask. -func warrantOf(c *asker.Client) func(string) (string, error) { +func warrantOf(c *occ.Client) func(string) (string, error) { return func(ask string) (string, error) { state, w, err := c.Where(ask) if err != nil { @@ -169,7 +169,11 @@ func warrantOf(c *asker.Client) func(string) (string, error) { if w == nil { return "", fmt.Errorf("the router records the ask %s as %q, with no warrant", ask, state) } - return ask + "@" + w.At.UTC().Format(time.RFC3339Nano), nil + at, err := occ.Time(w.At) + if err != nil { + return "", fmt.Errorf("the router's warrant of the ask %s has no readable time: %w", ask, err) + } + return ask + "@" + at.UTC().Format(time.RFC3339Nano), nil } } @@ -218,7 +222,7 @@ func loadedVia(rd Reader) func(string) (bool, error) { func main() { v := followed() h := newHolder(v) - client := h.Asker.(*asker.Client) + client := h.Asker.(*occ.Client) h.Run(func() error { return client.Listen(h.Warranted, h.Said) }) if err := stdio.Serve("", append(Tools(h), v.Tools("enrolment")...)); err != nil { logf("%v", err) diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/vault.go b/modules/mesh-enrolment/cmd/mesh-enrolment/vault.go index 0ebbebbe..50e8fbf5 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/vault.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/vault.go @@ -7,15 +7,16 @@ import ( "fmt" "time" + mv "git.novox.be/novox/mesh-contracts/go/seats/meshvault" "git.novox.be/novox/mesh-sdk/go/delivery" ) // Seats the holder calls (ADR 0587 §3, §5, §7). The vault's two verbs are the ones its manifest names this seat the only // caller of; its `sign` names this seat for a node's certificate and a redeem answer. const ( - vaultToken = "seat:mesh-vault." + delivery.EnrolmentTokenVerb - vaultAccount = "seat:mesh-vault." + delivery.NodeAccountVerb - vaultSign = "seat:mesh-vault." + delivery.SignVerb + vaultToken = "seat:mesh-vault." + mv.VerbEnrolmentToken + vaultAccount = "seat:mesh-vault." + mv.VerbNodeAccount + vaultSign = "seat:mesh-vault." + mv.VerbSign placementAdd = "seat:mesh-placement.propose" ) @@ -39,15 +40,15 @@ func answer[T any](raw json.RawMessage, err error) (T, error) { return out, nil } -func (v vaultOver) Token(node string, expires time.Time, t delivery.TokenTunnel) (delivery.EnrolmentTokenAnswer, error) { +func (v vaultOver) Token(node string, expires time.Time, t delivery.TokenTunnel) (mv.EnrolmentTokenAnswer, error) { // The whole first tunnel the token carries (delivery.TokenTunnel, catalogue's enrolment-token). tunnel := map[string]any{"hub_key": t.HubKey, "hub_endpoint": t.HubEndpoint, "address": t.Address, "key": t.Key, "range": t.Range} - return answer[delivery.EnrolmentTokenAnswer](v.call(vaultToken, map[string]any{"node": node, + return answer[mv.EnrolmentTokenAnswer](v.call(vaultToken, map[string]any{"node": node, "expires": expires.UTC().Format(time.RFC3339), "tunnel": tunnel})) } func (v vaultOver) Sealed(node string) (string, error) { - a, err := answer[delivery.EnrolmentTokenAnswer](v.call(vaultToken, map[string]any{"node": node, "sealed": "true"})) + a, err := answer[mv.EnrolmentTokenAnswer](v.call(vaultToken, map[string]any{"node": node, "sealed": "true"})) if err == nil && a.Sealed == "" { err = errors.New("the vault gave no sealed token") } @@ -55,32 +56,32 @@ func (v vaultOver) Sealed(node string) (string, error) { } func (v vaultOver) Drop(node string) error { - a, err := answer[delivery.EnrolmentTokenAnswer](v.call(vaultToken, map[string]any{"node": node, "drop": "true"})) + a, err := answer[mv.EnrolmentTokenAnswer](v.call(vaultToken, map[string]any{"node": node, "drop": "true"})) if err == nil && !a.Dropped { err = errors.New("the vault did not say the token and its hash are gone") } return err } -func (v vaultOver) Account(node, sealingKey, keysDigest string) (delivery.NodeAccountAnswer, error) { - return answer[delivery.NodeAccountAnswer](v.call(vaultAccount, map[string]any{"node": node, "sealing_key": sealingKey, "keys_digest": keysDigest})) +func (v vaultOver) Account(node, sealingKey, keysDigest string) (mv.NodeAccountAnswer, error) { + return answer[mv.NodeAccountAnswer](v.call(vaultAccount, map[string]any{"node": node, "sealing_key": sealingKey, "keys_digest": keysDigest})) } func (v vaultOver) Retire(node string) error { - a, err := answer[delivery.NodeAccountAnswer](v.call(vaultAccount, map[string]any{"node": node, "retire": "true"})) + a, err := answer[mv.NodeAccountAnswer](v.call(vaultAccount, map[string]any{"node": node, "retire": "true"})) if err == nil && !a.Retired { err = errors.New("the vault did not say the account is retired") } return err } -func (v vaultOver) SignCertificate(node string, k delivery.JoiningKeys) (delivery.SignAnswer, error) { - return answer[delivery.SignAnswer](v.call(vaultSign, map[string]any{"kind": delivery.KindNodeCertificate, "node": node, +func (v vaultOver) SignCertificate(node string, k delivery.JoiningKeys) (mv.SignAnswer, error) { + return answer[mv.SignAnswer](v.call(vaultSign, map[string]any{"kind": delivery.KindNodeCertificate, "node": node, "public_key": k.Identity, "overlay_key": k.Overlay, "sealing_key": k.Sealing, "serving_key": k.Serving})) } -func (v vaultOver) SignReply(node string, reply []byte) (delivery.SignAnswer, error) { - return answer[delivery.SignAnswer](v.call(vaultSign, map[string]any{"kind": delivery.KindEnrolment, "node": node, +func (v vaultOver) SignReply(node string, reply []byte) (mv.SignAnswer, error) { + return answer[mv.SignAnswer](v.call(vaultSign, map[string]any{"kind": delivery.KindEnrolment, "node": node, "reply": base64.StdEncoding.EncodeToString(reply)})) } diff --git a/modules/mesh-enrolment/go.mod b/modules/mesh-enrolment/go.mod index d094f71e..cc3d5c9b 100644 --- a/modules/mesh-enrolment/go.mod +++ b/modules/mesh-enrolment/go.mod @@ -8,6 +8,10 @@ require ( ) require ( + git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/go/seats/meshvault v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-00010101000000-000000000000 github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect github.com/google/go-tpm v0.9.8 // indirect github.com/klauspost/compress v1.20.0 // indirect diff --git a/modules/mesh-grants/cmd/mesh-grants/asking.go b/modules/mesh-grants/cmd/mesh-grants/asking.go index e37a0fbf..59b392c3 100644 --- a/modules/mesh-grants/cmd/mesh-grants/asking.go +++ b/modules/mesh-grants/cmd/mesh-grants/asking.go @@ -5,18 +5,19 @@ import ( "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" ) // sayVia says a condition of the part on its own subject, as a module that uses the seat operator-channel may: the // account consumers that differ, a grant missing, a user list held, a principal refused. -func sayVia(ctx context.Context, js jetstream.JetStream, module string) func(c asks.Condition) error { - return func(c asks.Condition) error { - subject, err := asks.ConditionSubject(module) +func sayVia(ctx context.Context, js jetstream.JetStream, module string) func(c oc.Condition) error { + return func(c oc.Condition) error { + subject, err := occ.ConditionSubject(module) if err != nil { return err } - data, err := asks.EncodeCondition(c) + data, err := occ.EncodeCondition(c) if err != nil { return err } diff --git a/modules/mesh-grants/go.mod b/modules/mesh-grants/go.mod index 3cf0be53..3467cc7f 100644 --- a/modules/mesh-grants/go.mod +++ b/modules/mesh-grants/go.mod @@ -3,6 +3,10 @@ module mesh-grants go 1.26.0 require ( + git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/go/seats/meshvault v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 7b0aa40d..754d1ba5 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -21,7 +21,8 @@ import ( "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/membership" @@ -711,8 +712,8 @@ func TestGrants(t *testing.T) { b.manifestOf("mesh-knowledge", knowledge) // accepted again for the rest of the row // ADR 0595 §6, design 25 § 12.3 (DLV-G3): fill is the bus module's; the part raises no history-full, however full a // bucket is, and clears the old key once after its start. - var said []asks.Condition - p.Say = func(c asks.Condition) error { + var said []oc.Condition + p.Say = func(c oc.Condition) error { if c.Key == grants.FullCondition { said = append(said, c) } @@ -725,7 +726,7 @@ func TestGrants(t *testing.T) { } p.Compose() p.Compose() - if len(said) != 1 || said[0].State != asks.StateCleared { + if len(said) != 1 || said[0].State != occ.StateCleared { t.Errorf("history-full: said %+v, want one clear and no raise", said) } }) @@ -1455,8 +1456,8 @@ func TestGrants(t *testing.T) { t.Errorf("grants = %v", gr) } - var said []asks.Condition - p.Say = func(c asks.Condition) error { said = append(said, c); return nil } + var said []oc.Condition + p.Say = func(c oc.Condition) error { said = append(said, c); return nil } // settled is the placement of the lists agreeing, every hash held, and the loaded list as edit leaves it. settled := func(edit func(modX *grants.User)) func() { return func() { @@ -1512,7 +1513,7 @@ func TestGrants(t *testing.T) { p.Compose() nHeld := 0 for _, c := range said { - if c.Key == grants.HeldCondition && c.State == asks.StateRaised && c.Severity == asks.SeverityWarning && strings.Contains(c.Summary, "a read of this pass failed") { + if c.Key == grants.HeldCondition && c.State == occ.StateRaised && c.Severity == occ.SeverityWarning && strings.Contains(c.Summary, "a read of this pass failed") { nHeld++ } } @@ -1548,21 +1549,21 @@ func TestGrants(t *testing.T) { if p.Compose(); func() bool { now, _, _ := p.Sealed(); return now.Digest != g.Digest }() { t.Errorf("the list in force moved with every read and hash back") } - if last := said[len(said)-1]; last.Key != grants.HeldCondition || last.State != asks.StateCleared { + if last := said[len(said)-1]; last.Key != grants.HeldCondition || last.State != occ.StateCleared { t.Errorf("the held warning is not cleared once nothing is held: %+v", last) } // A part started while the warning stood (the said state is in memory only) clears it on its first pass. - var fresh []asks.Condition + var fresh []oc.Condition fp := partOn(b) fp.Objects = p.Objects - fp.Say = func(c asks.Condition) error { + fp.Say = func(c oc.Condition) error { if c.Key != grants.GrantMissingCondition && c.Key != grants.RefusedCondition && c.Key != grants.HeldModuleCondition { // their own rows fresh = append(fresh, c) } return nil } fp.Compose() - if len(fresh) == 0 || fresh[0].Key != grants.HeldCondition || fresh[0].State != asks.StateCleared { + if len(fresh) == 0 || fresh[0].Key != grants.HeldCondition || fresh[0].State != occ.StateCleared { t.Errorf("a fresh part's first pass does not say the held warning is cleared: %+v", fresh) } // Issue 1945, 2026-10-27 09:21: a list that would leave a seat the list in force holds with no holder, @@ -1587,7 +1588,7 @@ func TestGrants(t *testing.T) { if now, _, _ := p.Sealed(); now.Digest != g.Digest { t.Errorf("a list losing a seat grant was written: %s, was %s", now.Digest, g.Digest) } - if len(said) != 1 || said[0].Key != grants.HeldCondition || said[0].State != asks.StateRaised || !strings.Contains(said[0].Summary, "the last holder of "+seat) { + if len(said) != 1 || said[0].Key != grants.HeldCondition || said[0].State != occ.StateRaised || !strings.Contains(said[0].Summary, "the last holder of "+seat) { t.Errorf("the held list did not raise the warning naming the lost grant: %+v", said) } b.loaded = inForce @@ -2108,8 +2109,8 @@ func TestGrants(t *testing.T) { b := fixture() b.loaded = shadowOf(t, b) p := partOn(b) - var said []asks.Condition - p.Say = func(c asks.Condition) error { + var said []oc.Condition + p.Say = func(c oc.Condition) error { if c.Key == grants.GrantMissingCondition { said = append(said, c) } @@ -2135,7 +2136,7 @@ func TestGrants(t *testing.T) { "mod-y: no fingerprint of the new form: its builder's version is not read yet") { t.Fatalf("missing = %+v", res.Grants) } - if len(said) != 1 || said[0].State != asks.StateRaised || !strings.Contains(said[0].Explanation, "mod-y") { + if len(said) != 1 || said[0].State != occ.StateRaised || !strings.Contains(said[0].Explanation, "mod-y") { t.Fatalf("said %+v", said) } if p.Compose(); len(said) != 1 { @@ -2143,7 +2144,7 @@ func TestGrants(t *testing.T) { } b.put(grants.ReadModules, "mod-y", delivery.Module{V: delivery.Version, FP2: fp, Manifest: delivery.ManifestDigest([]byte(yManifest)), Repository: "novox/y", Commit: "c"}) - if res := p.Compose(); len(res.Grants.Missing) != 0 || len(said) != 2 || said[1].State != asks.StateCleared { + if res := p.Compose(); len(res.Grants.Missing) != 0 || len(said) != 2 || said[1].State != occ.StateCleared { t.Errorf("once mod-y has a grant: missing %v, said %+v", res.Grants.Missing, said) } }) @@ -2348,9 +2349,9 @@ func TestGrants(t *testing.T) { return views.Answer{}, errors.New("no such tool " + tool) } } - var said []asks.Condition + var said []oc.Condition var sayMu sync.Mutex - say := func(c asks.Condition) error { sayMu.Lock(); said = append(said, c); sayMu.Unlock(); return nil } + say := func(c oc.Condition) error { sayMu.Lock(); said = append(said, c); sayMu.Unlock(); return nil } fv := grants.NewViews(owner, say, func() {}, nil) fv.Rest = b var minutes atomic.Int64 @@ -2379,7 +2380,7 @@ func TestGrants(t *testing.T) { return last == state } waitFor(t, func() bool { - return raised(grants.FirstViewUnanswered+".mesh-delivery-indexer.verdicts", asks.StateRaised) + return raised(grants.FirstViewUnanswered+".mesh-delivery-indexer.verdicts", occ.StateRaised) }) p := partOn(b) p.Read, p.Views, p.Say = fv, fv, say @@ -2387,7 +2388,7 @@ func TestGrants(t *testing.T) { if !slices.ContainsFunc(before.UserList.Gaps, func(g grants.Gap) bool { return g.Input == grants.ReadPlacement && g.Failed }) { t.Fatalf("placement answered empty at revision 0 was not a failed read: %+v", before.UserList.Gaps) } - if !raised(grants.FirstViewEmpty+".mesh-delivery-assigner.placement", asks.StateRaised) { + if !raised(grants.FirstViewEmpty+".mesh-delivery-assigner.placement", occ.StateRaised) { t.Fatalf("no first-view-empty: %+v", said) } empty.Store(false) @@ -2397,11 +2398,11 @@ func TestGrants(t *testing.T) { } waitFor(t, func() bool { return fv.Followers[grants.ReadPlacement].Status().Revision == 7 }) waitFor(t, func() bool { - return raised(grants.FirstViewUnanswered+".mesh-delivery-indexer.verdicts", asks.StateCleared) + return raised(grants.FirstViewUnanswered+".mesh-delivery-indexer.verdicts", occ.StateCleared) }) after := p.Compose() if slices.ContainsFunc(after.UserList.Gaps, func(g grants.Gap) bool { return g.Input == grants.ReadPlacement }) || - !raised(grants.FirstViewEmpty+".mesh-delivery-assigner.placement", asks.StateCleared) { + !raised(grants.FirstViewEmpty+".mesh-delivery-assigner.placement", occ.StateCleared) { t.Fatalf("after an answer above 0: gaps %+v, said %+v", after.UserList.Gaps, said) } if after.UserList.Users != before.UserList.Users && before.UserList.Users > after.UserList.Users { @@ -2728,8 +2729,8 @@ func TestGrants(t *testing.T) { } // 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), with no lease key too. - var said, behind []asks.Condition - p.Say = func(c asks.Condition) error { + var said, behind []oc.Condition + p.Say = func(c oc.Condition) error { switch c.Key { case grants.GrantMissingCondition, grants.RefusedCondition, grants.HeldModuleCondition, grants.FullCondition: // their own rows case grants.ConsumerBehindCondition: @@ -2742,7 +2743,7 @@ func TestGrants(t *testing.T) { 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 || + len(said) != 1 || said[0].Key != grants.ConsumerDiffersCondition || said[0].State != occ.StateRaised || !strings.Contains(said[0].Explanation, "EVENTS.b_mod-c") { t.Fatalf("a differing consumer: %+v, said %+v", res.Consumers, said) } @@ -2757,7 +2758,7 @@ func TestGrants(t *testing.T) { 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 { + if len(res.Consumers.Differ) != 0 || said[len(said)-1].State != occ.StateCleared { t.Errorf("once matched: %+v, said %+v", res.Consumers, said) } // 1126 (the self-check's bus-consumer probe ended): a consumer the part owns past the bound behind its stream's @@ -2768,7 +2769,7 @@ func TestGrants(t *testing.T) { } res = p.Compose() if len(res.Consumers.Behind) != 1 || !strings.Contains(res.Consumers.Behind[0], "EVENTS.b_mod-c") || - len(behind) == 0 || behind[len(behind)-1].State != asks.StateRaised || !strings.Contains(behind[len(behind)-1].Explanation, "EVENTS.b_mod-c") { + len(behind) == 0 || behind[len(behind)-1].State != occ.StateRaised || !strings.Contains(behind[len(behind)-1].Explanation, "EVENTS.b_mod-c") { t.Fatalf("a consumer far behind: %+v, said %+v", res.Consumers, behind) } // Still behind at the next pass, by another count: the condition names the consumers alone, so it is not said again. @@ -2779,7 +2780,7 @@ func TestGrants(t *testing.T) { } made["pending"] = grants.BehindBound res = p.Compose() - if len(res.Consumers.Behind) != 0 || behind[len(behind)-1].State != asks.StateCleared { + if len(res.Consumers.Behind) != 0 || behind[len(behind)-1].State != occ.StateCleared { t.Errorf("a consumer back within the bound: %+v, said %+v", res.Consumers, behind) } }) @@ -2808,8 +2809,8 @@ func TestGrants(t *testing.T) { UserList: &delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Digest: "sha256:aa", Users: 5, 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 } + var said []oc.Condition + p.Say = func(c oc.Condition) error { said = append(said, c); return nil } p.Compose() inForce, _, _ := p.Sealed() if inForce.Digest == "" { @@ -2830,7 +2831,7 @@ func TestGrants(t *testing.T) { 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") { + if len(said) != 1 || said[0].Key != grants.HeldCondition || said[0].State != occ.StateRaised || !strings.Contains(said[0].Summary, "a.mod-x, a.nats") { t.Errorf("the hold was said as %+v", said) } // A person accepts exactly the named users' changes of the held list, with why: refused for another set, with no @@ -2855,7 +2856,7 @@ func TestGrants(t *testing.T) { } b.events = nil p.Compose() - if now, _, _ := p.Sealed(); now.Digest == inForce.Digest || said[len(said)-1].State != asks.StateCleared || + if now, _, _ := p.Sealed(); now.Digest == inForce.Digest || said[len(said)-1].State != occ.StateCleared || !slices.Contains(b.events, grants.EventHashesAccepted) { t.Fatalf("an accepted hold was not released once and said: %+v, events %v", said, b.events) } @@ -2864,7 +2865,7 @@ func TestGrants(t *testing.T) { 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 { + if now, _, _ := p.Sealed(); now.Digest == inForce.Digest || said[len(said)-1].State != occ.StateCleared { t.Errorf("changes the vault's rotation names were held: %+v", said) } }) @@ -2973,8 +2974,8 @@ func TestGrants(t *testing.T) { rest := string(list)[at:] return rest[:strings.Index(rest, "} }\n")+4] } - var said []asks.Condition - p.Say = func(c asks.Condition) error { + var said []oc.Condition + p.Say = func(c oc.Condition) error { if c.Key == grants.RefusedCondition { said = append(said, c) } @@ -3015,7 +3016,7 @@ func TestGrants(t *testing.T) { if _, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(now.Digest, "a.mod-r.mod-r-tools")); ok { t.Error("a refused principal the list in force did not carry is in the list") } - if len(res.PrincipalsRefused) != 2 || len(said) == 0 || said[len(said)-1].State != asks.StateRaised || + if len(res.PrincipalsRefused) != 2 || len(said) == 0 || said[len(said)-1].State != occ.StateRaised || !strings.Contains(said[len(said)-1].Summary, "b.mod-r.mod-r-tools (keeps its grants of the list in force)") || !strings.Contains(said[len(said)-1].Summary, "a.mod-r.mod-r-tools (left out of the list") { t.Fatalf("the refusals were not said by name: %v, said %+v", res.PrincipalsRefused, said) @@ -3026,7 +3027,7 @@ func TestGrants(t *testing.T) { t.Errorf("an unchanged refusal was said again: %+v", said[n:]) } b.manifestOf("mod-r", ctl) - if res := p.Compose(); len(res.PrincipalsRefused) != 0 || said[len(said)-1].State != asks.StateCleared { + if res := p.Compose(); len(res.PrincipalsRefused) != 0 || said[len(said)-1].State != occ.StateCleared { t.Errorf("the refusal was not cleared once it composes: %v, said %+v", res.PrincipalsRefused, said) } }) @@ -3058,8 +3059,8 @@ func TestGrants(t *testing.T) { b.put(grants.ReadBuilds, runKey, map[string]any{"v": delivery.Version, "state": "done", "manifest": delivery.ManifestDigest([]byte(ctl))}) p := partOn(b) p.Objects = &fakeObjects{} - var said []asks.Condition - p.Say = func(c asks.Condition) error { + var said []oc.Condition + p.Say = func(c oc.Condition) error { if c.Key == grants.HeldModuleCondition { said = append(said, c) } @@ -3068,7 +3069,7 @@ func TestGrants(t *testing.T) { p.Compose() inForce, _, _ := p.Sealed() before, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(inForce.Digest, "b.mod-r.mod-r-tools")) - if !ok || len(said) != 1 || said[0].State != asks.StateCleared { + if !ok || len(said) != 1 || said[0].State != occ.StateCleared { t.Fatalf("the list in force keeps no grants of b.mod-r.mod-r-tools (%v), or a held module was said before: %+v", ok, said) } for gen, held := range []struct{ name, unknown string }{{"waits", "core-credentials"}, {"refused", ""}} { @@ -3093,7 +3094,7 @@ func TestGrants(t *testing.T) { if !ok || string(after) != string(before) || len(res.PrincipalsRefused) != 0 { t.Errorf("%s: b.mod-r.mod-r-tools did not keep its grants of the list in force: %s, was %s", held.name, after, before) } - if last := said[len(said)-1]; last.State != asks.StateRaised || !strings.Contains(last.Summary, "b.mod-r.mod-r-tools (stays in the list") { + if last := said[len(said)-1]; last.State != occ.StateRaised || !strings.Contains(last.Summary, "b.mod-r.mod-r-tools (stays in the list") { t.Errorf("%s: the kept user was not said: %+v", held.name, last) } } @@ -3113,7 +3114,7 @@ func TestGrants(t *testing.T) { if kept, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(nowList.Digest, "b.mod-r.mod-r-tools")); !ok || string(kept) != string(before) { t.Errorf("b.mod-r.mod-r-tools did not keep its grants of the list in force: %s", kept) } - if last := said[len(said)-1]; last.State != asks.StateRaised || !strings.Contains(last.Summary, "the manifest of mod-r is unreadable") { + if last := said[len(said)-1]; last.State != occ.StateRaised || !strings.Contains(last.Summary, "the manifest of mod-r is unreadable") { t.Errorf("the unreadable manifest was not said: %+v", last) } // The window before the judge: the indexer wrote a changed manifest's value and no verdict on its digest yet. Only that @@ -3127,12 +3128,12 @@ func TestGrants(t *testing.T) { if nowList.Digest == wasList.Digest || strings.Contains(fmt.Sprint(res.Sealed), "kept while a read") { t.Errorf("a manifest with no verdict yet held the whole list: %+v", res.Sealed) } - if last := said[len(said)-1]; last.State != asks.StateRaised || !strings.Contains(last.Summary, "no verdict on this manifest yet") { + if last := said[len(said)-1]; last.State != occ.StateRaised || !strings.Contains(last.Summary, "no verdict on this manifest yet") { t.Errorf("a manifest with no verdict yet was not said: %+v", last) } // Accepted again: the users compose from the manifest and the condition clears. b.manifestOf("mod-r", ctl) - if p.Compose(); said[len(said)-1].State != asks.StateCleared { + if p.Compose(); said[len(said)-1].State != occ.StateCleared { t.Errorf("the condition was not cleared once the manifest is accepted: %+v", said[len(said)-1]) } // ADR 0601 §4: a tool runner whose manifest does not say so yet (an older build's) keeps its user as the list in force @@ -3156,7 +3157,7 @@ func TestGrants(t *testing.T) { if !ok || string(kept) != string(ran) { t.Errorf("the tool runner of a was composed without what it runs: %s, was %s", kept, ran) } - if last := said[len(said)-1]; last.State != asks.StateRaised || !strings.Contains(last.Summary, "a.node-tools (stays in the list") { + if last := said[len(said)-1]; last.State != occ.StateRaised || !strings.Contains(last.Summary, "a.node-tools (stays in the list") { t.Errorf("the kept tool runner was not said: %+v", last) } }) diff --git a/modules/mesh-grants/internal/buscompose/engine.go b/modules/mesh-grants/internal/buscompose/engine.go index 70853571..ef1454ce 100644 --- a/modules/mesh-grants/internal/buscompose/engine.go +++ b/modules/mesh-grants/internal/buscompose/engine.go @@ -1,7 +1,7 @@ package buscompose import ( - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/provisions" @@ -41,7 +41,7 @@ func EnginePackagesGrants(node string) []string { // engineGrants is what a node's engine is granted to hold its node seat and to ask the operator (ADR 0563 §1, §3): it // answers its own node's verbs (the caller-named subjects are added beside them with every principal's), says the // seat's events about its node, answers discovery for the seat, asks who answers what (the login shell's execute, ADR -// 0563 §6), and asks the operator under asks.EngineAsker(node) on the router's seat as its holder declares it, as any +// 0563 §6), and asks the operator under occ.EngineAsker(node) on the router's seat as its holder declares it, as any // user of that seat is granted. // // Its verbs and events are granted as any verb and any event of the seat on its own node, not as the contract's list: @@ -56,7 +56,7 @@ func engineGrants(p Principal) (pub, sub []string) { pub = append(pub, seatSubject(Seat{Name: EngineSeat}, "event", onMachine(every, p.Node))) sub = append(sub, announcing(EngineSeat)...) pub = append(pub, discovering()...) - tp, ts := seatTrafficGrants(SeatTrafficOf(asks.EngineAsker(p.Node), nil, p.Uses, nil)) + tp, ts := seatTrafficGrants(SeatTrafficOf(occ.EngineAsker(p.Node), nil, p.Uses, nil)) // And its acts and asks, under its own node's keys alone; never the bucket's creation, which is the grants part's. if own, err := NodeKeysGrants(EngineActsBucket, p.Node); err == nil { pub = append(pub, own...) diff --git a/modules/mesh-grants/internal/buscompose/nats.go b/modules/mesh-grants/internal/buscompose/nats.go index 421ff2ae..6c25e558 100644 --- a/modules/mesh-grants/internal/buscompose/nats.go +++ b/modules/mesh-grants/internal/buscompose/nats.go @@ -21,6 +21,7 @@ import ( "strings" "time" + mv "git.novox.be/novox/mesh-contracts/go/seats/meshvault" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -331,8 +332,8 @@ func DeskOnly() []string { // VerbsTheEnrolmentAsksTheVault are the vault's verbs the seat mesh-enrolment's holder alone calls (novox/hq ADR 0587 // §1, §3; ADR 0561 §5): a token and its hash made and dropped, a joining node's account minted and retired. -var VerbsTheEnrolmentAsksTheVault = []SeatVerb{{Seat: VaultSeat, Verb: delivery.EnrolmentTokenVerb}, - {Seat: VaultSeat, Verb: delivery.NodeAccountVerb}} +var VerbsTheEnrolmentAsksTheVault = []SeatVerb{{Seat: VaultSeat, Verb: mv.VerbEnrolmentToken}, + {Seat: VaultSeat, Verb: mv.VerbNodeAccount}} // EnrolmentOnly are the subjects the holder of mesh-enrolment alone may publish, however wide another's grant: the // vault's two enrolment verbs, by the tool's and the caller-named shape (Principal.VaultEnrols denies them). @@ -384,7 +385,7 @@ var VaultOwn = SeatVerb{Seat: "mesh-vault", Verb: "own"} // VaultSign is the vault's verb a machine's node-engine asks in its own name for its own node's certificate (novox/hq // ADR 0550 §2); granted as Principal.AsksTheVaultToSign says. -var VaultSign = SeatVerb{Seat: "mesh-vault", Verb: delivery.SignVerb} +var VaultSign = SeatVerb{Seat: "mesh-vault", Verb: mv.VerbSign} // VaultRotateNode is the vault's verb that begins a node's account rotation; while its holder serves it, every node // principal reads its own node's key of node-logins (novox/hq ADR 0583 §1, §2). VaultNodeAccountConfirm is the verb a diff --git a/modules/mesh-grants/internal/buscompose/testdata/user-lists.json b/modules/mesh-grants/internal/buscompose/testdata/user-lists.json index a9a498d2..68876a15 100644 --- a/modules/mesh-grants/internal/buscompose/testdata/user-lists.json +++ b/modules/mesh-grants/internal/buscompose/testdata/user-lists.json @@ -288,7 +288,7 @@ }, { "Name": "a node seat's events, by machine and flat", - "List": "# The mesh's users, composed by the grants part. Do not edit: the next\n# composition overwrites it. Permissions are derived from what each module\n# declares and nothing else (novox/hq ADR 0043, design 29 §2).\n\naccounts {\n MESH {\n jetstream: enabled\n imports = [\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.CONNECT\" }, to: \"mesh.bus.connect\" }\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.DISCONNECT\" }, to: \"mesh.bus.disconnect\" }\n { service: { account: \"$SYS\", subject: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\" }, to: \"mesh.bus.connz\" }\n ]\n users = [\n { user: \"ace.pause\", password: \"$2a$11$ace.pauseace.pauseace.fixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.ace_pause.\u003e\", \"$JS.ACK.SEAT_NODE_POWER.SEAT_NODE_POWER_worker.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.ace.pause\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.ace_pause\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_POWER.SEAT_NODE_POWER_worker\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.ace_pause\", \"$JS.API.CONSUMER.MSG.NEXT.SEAT_NODE_POWER.SEAT_NODE_POWER_worker\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.ace.pause\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"mesh.seat.node-power.event.paused.ace\", \"mesh.seat.node-power.event.slept\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.pause\", \"$SRV.INFO.pause.\u003e\", \"$SRV.PING\", \"$SRV.PING.pause\", \"$SRV.PING.pause.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.pause\", \"$SRV.STATS.pause.\u003e\", \"_INBOX.ace.pause.\u003e\", \"mesh.mod.pause.call.\u003e\", \"mesh.mod.pause.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.ace\", password: \"$2a$11$node.acenode.acenode.afixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.ace\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.ace\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-ace.*.$O.declaration-files-ace.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-ace.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-ace.*\", \"$JS.API.CONSUMER.INFO.NODES.ace\", \"$JS.API.DIRECT.GET.KV_composed-ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.ace\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.ace\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.ace.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-ace.$O.declaration-files-ace.M.*\", \"$JS.API.STREAM.INFO.KV_composed-ace\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_manifests\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-ace.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-ace.\u003e\", \"$KV.composed-ace.\u003e\", \"$KV.mesh-host_acts.ace.\u003e\", \"$KV.mesh-host_data.ace.\u003e\", \"$KV.mesh-host_health.ace\", \"$KV.mesh-host_packages.ace\", \"$KV.node-engine_have.ace.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.*.ace\", \"$SRV.PING.\u003e\", \"mesh.control.ace.\u003e\", \"mesh.seat.node-backup.call.backed-up.ace.node~ace\", \"mesh.seat.node-backup.call.now.ace.node~ace\", \"mesh.seat.node-backup.tool.backed-up.ace\", \"mesh.seat.node-backup.tool.now.ace\", \"mesh.seat.node-engine.event.*.ace\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.ace\", \"_DELIVER.ace.\u003e\", \"_INBOX.node.ace.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.ace.ask.access-act\", \"mesh.node.ace.ask.hand-over\", \"mesh.node.ace.ask.judge-directories\", \"mesh.node.ace.ask.move-secret\", \"mesh.node.ace.ask.report\", \"mesh.node.ace.ask.setuid-search\", \"mesh.node.ace.declare\", \"mesh.seat.node-engine.call.*.ace.*\", \"mesh.seat.node-engine.tool.*.ace\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.novox\", password: \"$2a$11$node.novoxnode.novoxnofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.novox\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.novox\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-novox.*.$O.declaration-files-novox.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-novox.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-novox.*\", \"$JS.API.CONSUMER.INFO.NODES.novox\", \"$JS.API.DIRECT.GET.KV_composed-novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.novox\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.novox\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.novox.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-novox.$O.declaration-files-novox.M.*\", \"$JS.API.STREAM.INFO.KV_composed-novox\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_manifests\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-novox.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-novox.\u003e\", \"$KV.composed-novox.\u003e\", \"$KV.mesh-host_acts.novox.\u003e\", \"$KV.mesh-host_data.novox.\u003e\", \"$KV.mesh-host_health.novox\", \"$KV.mesh-host_packages.novox\", \"$KV.node-engine_have.novox.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.*.novox\", \"$SRV.PING.\u003e\", \"mesh.control.novox.\u003e\", \"mesh.seat.node-backup.call.backed-up.novox.node~novox\", \"mesh.seat.node-backup.call.now.novox.node~novox\", \"mesh.seat.node-backup.tool.backed-up.novox\", \"mesh.seat.node-backup.tool.now.novox\", \"mesh.seat.node-engine.event.*.novox\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.novox\", \"_DELIVER.novox.\u003e\", \"_INBOX.node.novox.\u003e\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.novox.ask.access-act\", \"mesh.node.novox.ask.hand-over\", \"mesh.node.novox.ask.judge-directories\", \"mesh.node.novox.ask.move-secret\", \"mesh.node.novox.ask.report\", \"mesh.node.novox.ask.setuid-search\", \"mesh.node.novox.declare\", \"mesh.seat.node-engine.call.*.novox.*\", \"mesh.seat.node-engine.tool.*.novox\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"novox.mesh-delivery-assigner\", password: \"$2a$11$novox.mesh-delivery-asfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.novox_mesh-delivery-assigner.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.novox.mesh-delivery-assigner\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.novox_mesh-delivery-assigner\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.novox_mesh-delivery-assigner\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.novox.mesh-delivery-assigner\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.mesh-delivery-assigner\", \"$SRV.INFO.mesh-delivery-assigner.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-delivery-assigner\", \"$SRV.PING.mesh-delivery-assigner.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-delivery-assigner\", \"$SRV.STATS.mesh-delivery-assigner.\u003e\", \"_INBOX.novox.mesh-delivery-assigner.\u003e\", \"mesh.mod.mesh-delivery-assigner.call.\u003e\", \"mesh.mod.mesh-delivery-assigner.tool.\u003e\", \"mesh.seat.node-engine.event.manifest-reads-changed.*\", \"mesh.seat.node-power.event.slept\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n ]\n }\n \"$SYS\" {\n exports = [\n { stream: \"$SYS.ACCOUNT.MESH.CONNECT\", accounts: [\"MESH\"] }\n { stream: \"$SYS.ACCOUNT.MESH.DISCONNECT\", accounts: [\"MESH\"] }\n { service: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\", accounts: [\"MESH\"] }\n ]\n }\n}\n", + "List": "# The mesh's users, composed by the grants part. Do not edit: the next\n# composition overwrites it. Permissions are derived from what each module\n# declares and nothing else (novox/hq ADR 0043, design 29 §2).\n\naccounts {\n MESH {\n jetstream: enabled\n imports = [\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.CONNECT\" }, to: \"mesh.bus.connect\" }\n { stream: { account: \"$SYS\", subject: \"$SYS.ACCOUNT.MESH.DISCONNECT\" }, to: \"mesh.bus.disconnect\" }\n { service: { account: \"$SYS\", subject: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\" }, to: \"mesh.bus.connz\" }\n ]\n users = [\n { user: \"ace.pause\", password: \"$2a$11$ace.pauseace.pauseace.fixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.ace_pause.\u003e\", \"$JS.ACK.SEAT_NODE_POWER.SEAT_NODE_POWER_worker.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.ace.pause\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.ace_pause\", \"$JS.API.CONSUMER.INFO.SEAT_NODE_POWER.SEAT_NODE_POWER_worker\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.ace_pause\", \"$JS.API.CONSUMER.MSG.NEXT.SEAT_NODE_POWER.SEAT_NODE_POWER_worker\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.ace.pause\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\", \"mesh.seat.node-power.event.paused.ace\", \"mesh.seat.node-power.event.slept\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.pause\", \"$SRV.INFO.pause.\u003e\", \"$SRV.PING\", \"$SRV.PING.pause\", \"$SRV.PING.pause.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.pause\", \"$SRV.STATS.pause.\u003e\", \"_INBOX.ace.pause.\u003e\", \"mesh.mod.pause.call.\u003e\", \"mesh.mod.pause.tool.\u003e\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.ace\", password: \"$2a$11$node.acenode.acenode.afixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.ace\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.ace\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.ace.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-ace.*.$O.declaration-files-ace.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-ace.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-ace.*\", \"$JS.API.CONSUMER.INFO.NODES.ace\", \"$JS.API.DIRECT.GET.KV_composed-ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.ace\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.ace\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.ace.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.ace.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-ace.$O.declaration-files-ace.M.*\", \"$JS.API.STREAM.INFO.KV_composed-ace\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_manifests\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-ace.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-ace.\u003e\", \"$KV.composed-ace.\u003e\", \"$KV.mesh-host_acts.ace.\u003e\", \"$KV.mesh-host_data.ace.\u003e\", \"$KV.mesh-host_health.ace\", \"$KV.mesh-host_packages.ace\", \"$KV.node-engine_have.ace.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.*.ace\", \"$SRV.PING.\u003e\", \"mesh.control.ace.\u003e\", \"mesh.seat.node-backup.call.backed-up.ace.node~ace\", \"mesh.seat.node-backup.call.now.ace.node~ace\", \"mesh.seat.node-backup.tool.backed-up.ace\", \"mesh.seat.node-backup.tool.now.ace\", \"mesh.seat.node-engine.event.*.ace\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.ace\", \"_DELIVER.ace.\u003e\", \"_INBOX.node.ace.\u003e\", \"mesh.mod.mesh-delivery-view.event.settled\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.ace.ask.access-act\", \"mesh.node.ace.ask.hand-over\", \"mesh.node.ace.ask.judge-directories\", \"mesh.node.ace.ask.move-secret\", \"mesh.node.ace.ask.report\", \"mesh.node.ace.ask.setuid-search\", \"mesh.node.ace.declare\", \"mesh.seat.node-engine.call.*.ace.*\", \"mesh.seat.node-engine.tool.*.ace\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"node.novox\", password: \"$2a$11$node.novoxnode.novoxnofixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.NODES.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_composed-novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_acts.*.$KV.mesh-host_acts.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_data.*.$KV.mesh-host_data.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_health.*.$KV.mesh-host_health.novox\", \"$JS.API.CONSUMER.CREATE.KV_mesh-host_packages.*.$KV.mesh-host_packages.novox\", \"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_provisions.*.$KV.mesh-provisioner_provisions.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.*.node\", \"$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.novox.\u003e\", \"$JS.API.CONSUMER.CREATE.OBJ_declaration-files-novox.*.$O.declaration-files-novox.C.*\", \"$JS.API.CONSUMER.DELETE.KV_composed-novox.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_acts.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_data.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_health.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-host_packages.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_provisions.\u003e\", \"$JS.API.CONSUMER.DELETE.KV_node-engine_have.\u003e\", \"$JS.API.CONSUMER.DELETE.OBJ_declaration-files-novox.*\", \"$JS.API.CONSUMER.INFO.NODES.novox\", \"$JS.API.DIRECT.GET.KV_composed-novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_data.$KV.mesh-host_data.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_mesh-host_health.$KV.mesh-host_health.novox\", \"$JS.API.DIRECT.GET.KV_mesh-host_packages.$KV.mesh-host_packages.novox\", \"$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.novox.\u003e\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.*.node\", \"$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.novox.\u003e\", \"$JS.API.DIRECT.GET.OBJ_declaration-files-novox.$O.declaration-files-novox.M.*\", \"$JS.API.STREAM.INFO.KV_composed-novox\", \"$JS.API.STREAM.INFO.KV_mesh-config_resolved\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_manifests\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_modules\", \"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_verdicts\", \"$JS.API.STREAM.INFO.KV_mesh-host_acts\", \"$JS.API.STREAM.INFO.KV_mesh-host_data\", \"$JS.API.STREAM.INFO.KV_mesh-host_health\", \"$JS.API.STREAM.INFO.KV_mesh-host_packages\", \"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions\", \"$JS.API.STREAM.INFO.KV_node-engine_have\", \"$JS.FC.KV_composed-novox.\u003e\", \"$JS.FC.KV_mesh-config_resolved.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_placement.\u003e\", \"$JS.FC.KV_mesh-delivery-assigner_wants.\u003e\", \"$JS.FC.KV_mesh-delivery-builder_builds.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_candidates.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_manifests.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_modules.\u003e\", \"$JS.FC.KV_mesh-delivery-indexer_verdicts.\u003e\", \"$JS.FC.KV_mesh-host_acts.\u003e\", \"$JS.FC.KV_mesh-host_data.\u003e\", \"$JS.FC.KV_mesh-host_health.\u003e\", \"$JS.FC.KV_mesh-host_packages.\u003e\", \"$JS.FC.KV_mesh-provisioner_provisions.\u003e\", \"$JS.FC.KV_node-engine_have.\u003e\", \"$JS.FC.OBJ_declaration-files-novox.\u003e\", \"$KV.composed-novox.\u003e\", \"$KV.mesh-host_acts.novox.\u003e\", \"$KV.mesh-host_data.novox.\u003e\", \"$KV.mesh-host_health.novox\", \"$KV.mesh-host_packages.novox\", \"$KV.node-engine_have.novox.\u003e\", \"$SRV.INFO\", \"$SRV.INFO.\u003e\", \"$SRV.PING\", \"$SRV.PING.*.novox\", \"$SRV.PING.\u003e\", \"mesh.control.novox.\u003e\", \"mesh.seat.node-backup.call.backed-up.novox.node~novox\", \"mesh.seat.node-backup.call.now.novox.node~novox\", \"mesh.seat.node-backup.tool.backed-up.novox\", \"mesh.seat.node-backup.tool.now.novox\", \"mesh.seat.node-engine.event.*.novox\"] }\n subscribe: { allow: [\"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates\", \"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement\", \"$SRV.INFO\", \"$SRV.INFO.node-engine\", \"$SRV.INFO.node-engine.\u003e\", \"$SRV.PING\", \"$SRV.PING.node-engine\", \"$SRV.PING.node-engine.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.node-engine\", \"$SRV.STATS.node-engine.\u003e\", \"_DELIVER.novox\", \"_DELIVER.novox.\u003e\", \"_INBOX.node.novox.\u003e\", \"mesh.mod.mesh-delivery-view.event.settled\", \"mesh.mod.restic.event.backed-up\", \"mesh.node.novox.ask.access-act\", \"mesh.node.novox.ask.hand-over\", \"mesh.node.novox.ask.judge-directories\", \"mesh.node.novox.ask.move-secret\", \"mesh.node.novox.ask.report\", \"mesh.node.novox.ask.setuid-search\", \"mesh.node.novox.declare\", \"mesh.seat.node-engine.call.*.novox.*\", \"mesh.seat.node-engine.tool.*.novox\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n { user: \"novox.mesh-delivery-assigner\", password: \"$2a$11$novox.mesh-delivery-asfixturefixturefixturefixture00\", permissions: {\n publish: { allow: [\"$JS.ACK.EVENTS.novox_mesh-delivery-assigner.\u003e\", \"$JS.API.CONSUMER.CREATE.KV_mesh-grants_memberships.*.$KV.mesh-grants_memberships.novox.mesh-delivery-assigner\", \"$JS.API.CONSUMER.DELETE.KV_mesh-grants_memberships.\u003e\", \"$JS.API.CONSUMER.INFO.EVENTS.novox_mesh-delivery-assigner\", \"$JS.API.CONSUMER.MSG.NEXT.EVENTS.novox_mesh-delivery-assigner\", \"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.novox.mesh-delivery-assigner\", \"$JS.API.STREAM.INFO.KV_mesh-grants_memberships\", \"$JS.FC.KV_mesh-grants_memberships.\u003e\"] }\n subscribe: { allow: [\"$SRV.INFO\", \"$SRV.INFO.mesh-delivery-assigner\", \"$SRV.INFO.mesh-delivery-assigner.\u003e\", \"$SRV.PING\", \"$SRV.PING.mesh-delivery-assigner\", \"$SRV.PING.mesh-delivery-assigner.\u003e\", \"$SRV.STATS\", \"$SRV.STATS.mesh-delivery-assigner\", \"$SRV.STATS.mesh-delivery-assigner.\u003e\", \"_INBOX.novox.mesh-delivery-assigner.\u003e\", \"mesh.mod.mesh-delivery-assigner.call.\u003e\", \"mesh.mod.mesh-delivery-assigner.tool.\u003e\", \"mesh.seat.node-engine.event.manifest-reads-changed.*\", \"mesh.seat.node-power.event.slept\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n ]\n }\n \"$SYS\" {\n exports = [\n { stream: \"$SYS.ACCOUNT.MESH.CONNECT\", accounts: [\"MESH\"] }\n { stream: \"$SYS.ACCOUNT.MESH.DISCONNECT\", accounts: [\"MESH\"] }\n { service: \"$SYS.REQ.ACCOUNT.MESH.CONNZ\", accounts: [\"MESH\"] }\n ]\n }\n}\n", "Missing": null, "Err": "", "Memberships": { diff --git a/modules/mesh-grants/internal/grants/consumers.go b/modules/mesh-grants/internal/grants/consumers.go index c64344e5..4b1a3d8c 100644 --- a/modules/mesh-grants/internal/grants/consumers.go +++ b/modules/mesh-grants/internal/grants/consumers.go @@ -10,7 +10,8 @@ import ( "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "mesh-grants/internal/buscompose" ) @@ -173,14 +174,14 @@ func (p *Part) sayDiffer(st *ConsumersState) { if p.differKnown && why == p.differSaid { return } - c := asks.Condition{Key: ConsumerDiffersCondition, Subject: asks.Scope{Scope: "module", ID: "mesh-grants"}} + c := oc.Condition{Key: ConsumerDiffersCondition, Subject: oc.ConditionSubject{Scope: "module", ID: "mesh-grants"}} if why == "" { - c.State = asks.StateCleared + c.State = occ.StateCleared } else { - c.Severity, c.State = asks.SeverityWarning, asks.StateRaised + c.Severity, c.State = occ.SeverityWarning, occ.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, " + + c.Explanation = occ.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" } @@ -270,14 +271,14 @@ func (p *Part) sayBehind(st *ConsumersState) { if p.behindKnown && why == p.behindSaid { return } - c := asks.Condition{Key: ConsumerBehindCondition, Subject: asks.Scope{Scope: "module", ID: "mesh-grants"}} + c := oc.Condition{Key: ConsumerBehindCondition, Subject: oc.ConditionSubject{Scope: "module", ID: "mesh-grants"}} if why == "" { - c.State = asks.StateCleared + c.State = occ.StateCleared } else { - c.Severity, c.State = asks.SeverityWarning, asks.StateRaised + c.Severity, c.State = occ.SeverityWarning, occ.StateRaised c.Summary = fmt.Sprintf("%d durable consumers are more than %d messages behind their stream's head", len(st.Behind), BehindBound) c.Headline, c.Needs = "Messages pile up for a listener", "a look at the module that reads them" - c.Explanation = asks.NeedsYou + "a look. These consumers stayed far behind their stream's head at two passes " + + c.Explanation = occ.NeedsYou + "a look. These consumers stayed far behind their stream's head at two passes " + "running, so what is sent to their modules waits unread (how far each is, in the grants part's last pass): " + why c.Resolved = "every account consumer is back within the bound" } diff --git a/modules/mesh-grants/internal/grants/firstviews.go b/modules/mesh-grants/internal/grants/firstviews.go index e7240e04..a9b31d75 100644 --- a/modules/mesh-grants/internal/grants/firstviews.go +++ b/modules/mesh-grants/internal/grants/firstviews.go @@ -11,7 +11,8 @@ import ( "sync" "time" - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/views" ) @@ -79,7 +80,7 @@ type Views struct { // NewViews makes the five followers: ask calls an owner's tool (through the part's own account), say says a condition, // changed wakes a pass. Started by Start. -func NewViews(ask func(tool string) func(context.Context, string) (views.Answer, error), say func(asks.Condition) error, +func NewViews(ask func(tool string) func(context.Context, string) (views.Answer, error), say func(oc.Condition) error, changed func(), logf func(string, ...any)) *Views { v := &Views{Followers: map[string]*views.Follower{}} for _, fv := range FirstViews { @@ -289,14 +290,14 @@ func sameVerdict(a, b delivery.ManifestVerdict) bool { } // unansweredCondition is FirstViewUnanswered for one first view, raised or cleared. -func unansweredCondition(fv FirstView, s views.Status, raised bool) asks.Condition { - c := asks.Condition{Key: FirstViewUnanswered + "." + fv.Owner + "." + fv.State, Subject: asks.Scope{Scope: "module", ID: Module}} +func unansweredCondition(fv FirstView, s views.Status, raised bool) oc.Condition { + c := oc.Condition{Key: FirstViewUnanswered + "." + fv.Owner + "." + fv.State, Subject: oc.ConditionSubject{Scope: "module", ID: Module}} if !raised { - c.State = asks.StateCleared + c.State = occ.StateCleared c.Resolved = fv.Tool + " answered at revision " + fmt.Sprint(s.Revision) return c } - c.State, c.Severity = asks.StateRaised, asks.SeverityWarning + c.State, c.Severity = occ.StateRaised, occ.SeverityWarning c.Summary = fmt.Sprintf("the grants part has no first view of %s's %s: %s has not answered since %s (%s)", fv.Owner, fv.State, fv.Tool, s.Asked.Format(time.RFC3339), strings.TrimSpace(s.Error)) c.Headline = "The grants part waits for " + fv.Owner @@ -308,10 +309,10 @@ func unansweredCondition(fv FirstView, s views.Status, raised bool) asks.Conditi // emptyCondition is FirstViewEmpty for one first view: raised while it answered empty at revision 0, with the // operator's step, cleared by an answer above 0. -func emptyCondition(fv FirstView, raised bool) asks.Condition { - c := asks.Condition{Key: FirstViewEmpty + "." + fv.Owner + "." + fv.State, Subject: asks.Scope{Scope: "module", ID: Module}} +func emptyCondition(fv FirstView, raised bool) oc.Condition { + c := oc.Condition{Key: FirstViewEmpty + "." + fv.Owner + "." + fv.State, Subject: oc.ConditionSubject{Scope: "module", ID: Module}} if !raised { - c.State = asks.StateCleared + c.State = occ.StateCleared c.Resolved = fv.Tool + " answered above revision 0" return c } @@ -319,10 +320,10 @@ func emptyCondition(fv FirstView, raised bool) asks.Condition { if fv.Owner == AssignerModule { step = "the assigner composes the wants again at its next composition" } - c.State, c.Severity = asks.StateRaised, asks.SeverityWarning + c.State, c.Severity = occ.StateRaised, occ.SeverityWarning c.Summary = fmt.Sprintf("%s answered %s's %s empty at revision 0, so the grants part keeps the user list in force", fv.Tool, fv.Owner, fv.State) c.Headline = fv.Owner + "'s " + fv.State + " is empty" - c.Explanation = asks.NeedsYou + "a step. The owner holds nothing of " + fv.State + " (its state rebuilt by a recreate that could " + + c.Explanation = occ.NeedsYou + "a step. The owner holds nothing of " + fv.State + " (its state rebuilt by a recreate that could " + "not keep the data, or a fresh mesh), so the grants part drops no login and keeps what it holds; a login revoked meanwhile " + "stays until the owner's first change (novox/hq design 25 § 12.2)." c.Needs = "let the owner write its state again from its sources (" + step + "), then grants_first_view_again" diff --git a/modules/mesh-grants/internal/grants/full.go b/modules/mesh-grants/internal/grants/full.go index f563887a..18e7528a 100644 --- a/modules/mesh-grants/internal/grants/full.go +++ b/modules/mesh-grants/internal/grants/full.go @@ -1,7 +1,8 @@ package grants import ( - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" ) // **The fill of a module's kept data is the bus module's** (novox/hq ADR 0595 §6, design 25 § 12.3): its one periodic @@ -24,7 +25,7 @@ func (p *Part) clearFull() { if p.fullCleared { return } - c := asks.Condition{Key: FullCondition, Subject: asks.Scope{Scope: "module", ID: Module}, State: asks.StateCleared, + c := oc.Condition{Key: FullCondition, Subject: oc.ConditionSubject{Scope: "module", ID: Module}, State: occ.StateCleared, Resolved: "the fill of a module's kept data is the bus module's own condition now (novox/hq ADR 0595 §6)"} if p.Say(c) == nil { p.fullCleared = true diff --git a/modules/mesh-grants/internal/grants/grants.go b/modules/mesh-grants/internal/grants/grants.go index 8011cf6f..baed2bb1 100644 --- a/modules/mesh-grants/internal/grants/grants.go +++ b/modules/mesh-grants/internal/grants/grants.go @@ -7,7 +7,8 @@ import ( "sort" "strings" - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/delivery" "mesh-grants/internal/buscompose" ) @@ -329,14 +330,14 @@ func (p *Part) sayMissing(missing []string) { if p.missingKnown && why == p.missingSaid { return } - c := asks.Condition{Key: GrantMissingCondition, Subject: asks.Scope{Scope: "module", ID: "mesh-grants"}} + c := oc.Condition{Key: GrantMissingCondition, Subject: oc.ConditionSubject{Scope: "module", ID: "mesh-grants"}} if why == "" { - c.State = asks.StateCleared + c.State = occ.StateCleared } else { - c.Severity, c.State = asks.SeverityWarning, asks.StateRaised + c.Severity, c.State = occ.SeverityWarning, occ.StateRaised c.Summary = fmt.Sprintf("%d placed module(s) have no grant, so no want can name them", len(missing)) c.Headline, c.Needs = "A placed module has no bus grant", "a look at why each has none" - c.Explanation = asks.NeedsYou + "a look. The grants part wrote no grant for these modules, so the assigner's " + + c.Explanation = occ.NeedsYou + "a look. The grants part wrote no grant for these modules, so the assigner's " + "credential rule gives them no want: " + why c.Resolved = "every placed module has a grant at the fingerprint the indexer names" } diff --git a/modules/mesh-grants/internal/grants/held.go b/modules/mesh-grants/internal/grants/held.go index d5177e59..593f66da 100644 --- a/modules/mesh-grants/internal/grants/held.go +++ b/modules/mesh-grants/internal/grants/held.go @@ -8,7 +8,8 @@ import ( "mesh-grants/internal/buscompose" - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" ) // **A module whose manifest waits or is refused keeps its bus users** (clause 3 of the record on a release's Approve; issue @@ -134,14 +135,14 @@ func (p *Part) sayHeldModules(held map[string]string, kept map[string]User) { if p.heldModulesKnown && why == p.heldModulesSaid { return } - c := asks.Condition{Key: HeldModuleCondition, Subject: asks.Scope{Scope: "module", ID: "mesh-grants"}} + c := oc.Condition{Key: HeldModuleCondition, Subject: oc.ConditionSubject{Scope: "module", ID: "mesh-grants"}} if why == "" { - c.State = asks.StateCleared + c.State = occ.StateCleared } else { - c.Severity, c.State = asks.SeverityWarning, asks.StateRaised + c.Severity, c.State = occ.SeverityWarning, occ.StateRaised c.Summary = fmt.Sprintf("%d bus user(s) of a module whose manifest is held are kept in the list: %s", len(users), why) c.Headline, c.Needs = "A module's manifest is held, its bus users are kept", "a manifest the indexer accepts, or a judge that knows its fields" - c.Explanation = asks.NeedsYou + "a manifest the indexer accepts (a refused one mended, or a waiting one judged by an indexer that knows its fields). " + + c.Explanation = occ.NeedsYou + "a manifest the indexer accepts (a refused one mended, or a waiting one judged by an indexer that knows its fields). " + "The rest of the user list is written as ever; a held module's users stay as the list in force holds them: " + why c.Resolved = "every module's manifest is accepted" } diff --git a/modules/mesh-grants/internal/grants/part.go b/modules/mesh-grants/internal/grants/part.go index c23c9498..ba5ab769 100644 --- a/modules/mesh-grants/internal/grants/part.go +++ b/modules/mesh-grants/internal/grants/part.go @@ -12,7 +12,7 @@ import ( "sync" "time" - "git.novox.be/novox/mesh-sdk/go/asks" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/membership" @@ -160,7 +160,7 @@ type Part struct { // 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 func(asks.Condition) error + Say func(oc.Condition) error Now func() time.Time cache Manifests diff --git a/modules/mesh-grants/internal/grants/refused.go b/modules/mesh-grants/internal/grants/refused.go index b05f4949..2c024c29 100644 --- a/modules/mesh-grants/internal/grants/refused.go +++ b/modules/mesh-grants/internal/grants/refused.go @@ -6,7 +6,8 @@ import ( "sort" "strings" - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "mesh-grants/internal/buscompose" ) @@ -120,14 +121,14 @@ func (p *Part) sayPrincipalsRefused(refused map[string]string, kept map[string]U if p.refusedKnown && why == p.refusedSaid { return } - c := asks.Condition{Key: RefusedCondition, Subject: asks.Scope{Scope: "module", ID: "mesh-grants"}} + c := oc.Condition{Key: RefusedCondition, Subject: oc.ConditionSubject{Scope: "module", ID: "mesh-grants"}} if why == "" { - c.State = asks.StateCleared + c.State = occ.StateCleared } else { - c.Severity, c.State = asks.SeverityWarning, asks.StateRaised + c.Severity, c.State = occ.SeverityWarning, occ.StateRaised c.Summary = fmt.Sprintf("%d principal(s) refused their composed grants: %s", len(refused), why) c.Headline, c.Needs = "A principal's bus grants are refused", "a fix of the manifest or the writers table" - c.Explanation = asks.NeedsYou + "a fix of what makes it a second writer. The rest of the user list is written as ever; " + + c.Explanation = occ.NeedsYou + "a fix of what makes it a second writer. The rest of the user list is written as ever; " + "each refused principal keeps the grants the list in force gives it, or stays out where it holds none: " + why c.Resolved = "every principal's grants compose again" } diff --git a/modules/mesh-grants/internal/grants/sealed.go b/modules/mesh-grants/internal/grants/sealed.go index 376900e1..33845137 100644 --- a/modules/mesh-grants/internal/grants/sealed.go +++ b/modules/mesh-grants/internal/grants/sealed.go @@ -9,7 +9,8 @@ import ( "strings" "time" - "git.novox.be/novox/mesh-sdk/go/asks" + occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient" + oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" "git.novox.be/novox/mesh-sdk/go/delivery" "mesh-grants/internal/buscompose" ) @@ -173,14 +174,14 @@ func (p *Part) sayHeld(why string) { if p.heldKnown && why == p.heldSaid { return } - c := asks.Condition{Key: HeldCondition, Subject: asks.Scope{Scope: "module", ID: "mesh-grants"}} + c := oc.Condition{Key: HeldCondition, Subject: oc.ConditionSubject{Scope: "module", ID: "mesh-grants"}} if why == "" { - c.State = asks.StateCleared + c.State = occ.StateCleared } else { reason := strings.TrimPrefix(why, "held: ") - c.Severity, c.State = asks.SeverityWarning, asks.StateRaised + c.Severity, c.State = occ.SeverityWarning, occ.StateRaised c.Summary, c.Headline, c.Needs = "the user list is held: "+reason, "The user list in force is held back", "a look at why" - c.Explanation = asks.NeedsYou + "a look at why. The bus node keeps following the list written before, not the one composed now: " + reason + c.Explanation = occ.NeedsYou + "a look at why. The bus node keeps following the list written before, not the one composed now: " + reason c.Resolved = "a pass finds nothing to hold back and the list advances" } if p.Say(c) == nil {