From 9d0b3cafc5a6d3ec46efcb12e96e62e2ad8187be Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 23:43:26 +0100 Subject: [PATCH] =?UTF-8?q?A=20modules=20value=20with=20no=20verdict=20yet?= =?UTF-8?q?=20holds=20only=20its=20module's=20users,=20and=20one=20module'?= =?UTF-8?q?s=20value=20that=20does=20not=20read=20is=20a=20gap=20of=20that?= =?UTF-8?q?=20module,=20never=20of=20the=20whole=20list=20(ADR=200614=20?= =?UTF-8?q?=C2=A78,=20issue=20774)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The indexer writes a changed manifest's value before it judges it; the part now reads the indexer's verdicts. Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn --- .../cmd/mesh-grants/integration_test.go | 2 + modules/mesh-grants/grants_test.go | 20 ++++++++ modules/mesh-grants/internal/grants/follow.go | 2 +- modules/mesh-grants/internal/grants/inputs.go | 46 ++++++++++++++++--- modules/mesh-grants/module.json | 1 + 5 files changed, 64 insertions(+), 7 deletions(-) diff --git a/modules/mesh-grants/cmd/mesh-grants/integration_test.go b/modules/mesh-grants/cmd/mesh-grants/integration_test.go index c546269e..25a157c0 100644 --- a/modules/mesh-grants/cmd/mesh-grants/integration_test.go +++ b/modules/mesh-grants/cmd/mesh-grants/integration_test.go @@ -93,6 +93,8 @@ func TestTheGrantsPartWhole(t *testing.T) { put(grants.ReadManifests, digest, delivery.Manifest{V: delivery.Version, Module: module, JSON: raw}) put(grants.ReadModules, module, delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32), Manifest: digest, Repository: "novox/x", Commit: "c"}) + verdict, _ := delivery.EncodeManifestVerdict(delivery.ManifestVerdict{Module: module, State: delivery.VerdictAccepted, Judge: "j", At: time.Now()}) + put(grants.ReadVerdicts, digest, json.RawMessage(verdict)) } manifest("nats", `{"module":"nats","version":"1","claims":[{"name":"mesh-broker"}],"own-secrets":{"broker":"/var/lib/nats/broker"}}`) manifest("mod-c", `{"module":"mod-c","version":"1","own-secrets":{"broker":"/var/lib/c/broker"},"consumes":["nats.done"]}`) diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 162596c9..5967fbc2 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -154,6 +154,12 @@ func (b *fakeBus) put(state, key string, v any) { b.states[state] = map[string][]byte{} } b.states[state][key] = raw + // A modules value that carries no verdict is accepted by the indexer's verdict on its digest, as the indexer writes it; + // a row that wants the window before the judge deletes that key. + if m, ok := v.(delivery.Module); ok && state == grants.ReadModules && m.Verdict == "" && m.Manifest != "" { + verdict, _ := delivery.EncodeManifestVerdict(delivery.ManifestVerdict{Module: key, State: delivery.VerdictAccepted, Judge: "j", At: time.Now()}) + b.put(grants.ReadVerdicts, m.Manifest, json.RawMessage(verdict)) + } } func (b *fakeBus) Keys(state string) ([]string, error) { @@ -2841,6 +2847,20 @@ func TestGrants(t *testing.T) { if last := said[len(said)-1]; last.State != asks.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 + // module's users stay (named unreadable); the others move. + b.manifestOf("mod-r", ctl) + delete(b.states[grants.ReadVerdicts], delivery.ManifestDigest([]byte(ctl))) + b.put(grants.ReadAccounts, "a.mod-x", map[string]any{"user": "a.mod-x", "hash": "$2a$10$gen11", "generation": 11, "fingerprint": "sha256:11"}) + wasList, _, _ = p.Sealed() + res = p.Compose() + nowList, _, _ = p.Sealed() + 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") { + 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 { diff --git a/modules/mesh-grants/internal/grants/follow.go b/modules/mesh-grants/internal/grants/follow.go index dfaef644..9e01a74d 100644 --- a/modules/mesh-grants/internal/grants/follow.go +++ b/modules/mesh-grants/internal/grants/follow.go @@ -21,7 +21,7 @@ type Watcher interface { // // And the seat mesh-enrolment's keys (ADR 0587 §4, §5): a key at live composes enrol., and its end, by the holder or by // the bus's expiry, is a change of the key that takes the user out; no event is needed beside the watch. -var Followed = []string{ReadModules, ReadResolved, ReadHave, ReadWants, ReadAccounts, ReadPlacement, ReadEnrolments, +var Followed = []string{ReadModules, ReadVerdicts, ReadResolved, ReadHave, ReadWants, ReadAccounts, ReadPlacement, ReadEnrolments, ReadNodeLogins} // Follower composes again on every change it hears, never on a timer. It never stops for a refusal: a watch it cannot diff --git a/modules/mesh-grants/internal/grants/inputs.go b/modules/mesh-grants/internal/grants/inputs.go index 839a8f86..3fd6310c 100644 --- a/modules/mesh-grants/internal/grants/inputs.go +++ b/modules/mesh-grants/internal/grants/inputs.go @@ -25,10 +25,13 @@ import ( const ( ReadModules = "mesh-delivery-indexer.modules" ReadManifests = "mesh-delivery-indexer.manifests" - ReadHave = "node-engine.have" - ReadResolved = "mesh-config.resolved" - ReadWants = "mesh-delivery-assigner.wants" - ReadBuilds = "mesh-delivery-builder.builds" + // ReadVerdicts is the indexer's verdicts, one key per manifest digest: a manifest it has not judged yet has none, and is + // held like a refused one (ADR 0614 §8). + ReadVerdicts = "mesh-delivery-indexer.verdicts" + ReadHave = "node-engine.have" + ReadResolved = "mesh-config.resolved" + ReadWants = "mesh-delivery-assigner.wants" + ReadBuilds = "mesh-delivery-builder.builds" // ReadAccounts is the vault's bcrypt hashes of the bus accounts it holds, by bus user (ADR 0547 clause 9). ReadAccounts = "mesh-vault.bus-accounts" // ReadPlacement is the seat mesh-placement's placement (ADR 0567 §1, §9): each node's modules and the seats @@ -44,7 +47,7 @@ const ( // Reads is every state the part reads, in the manifest's order. Not the controller's assignments: the seat's placement // alone places modules (novox/hq ADR 0549 §5, ADR 0571 §10; issue 774). -var Reads = []string{ReadModules, ReadManifests, ReadResolved, ReadHave, ReadWants, ReadBuilds, +var Reads = []string{ReadModules, ReadManifests, ReadVerdicts, ReadResolved, ReadHave, ReadWants, ReadBuilds, ReadAccounts, ReadPlacement, ReadEnrolments, ReadNodeLogins} // The bus module's tools the part calls, as its manifest's invokes name them: read-only, each. @@ -336,7 +339,10 @@ func ReadInputs(r Reader, cache *Manifests) Inputs { for _, module := range indexed { raw, found, err := r.Get(ReadModules, module) if err != nil { - in.Gaps = append(in.Gaps, Gap{Input: ReadModules + " " + module, Why: err.Error(), Failed: true}) + // One module's value that does not read holds that module alone, as a refused manifest does (ADR 0614 §8). + g := Gap{Input: "manifest of " + module, Why: "its value in " + ReadModules + " does not read: " + err.Error()} + in.Refused = append(in.Refused, g) + in.Gaps = append(in.Gaps, g) continue } if !found { @@ -360,6 +366,10 @@ func ReadInputs(r Reader, cache *Manifests) Inputs { // The indexer's refusal, or its wait for a judge that knows a field, carried in its value (ADR 0578 §3): held and named as a refused manifest. if verdict := v.VerdictOf(module); verdict != nil && verdict.Held() { err = errors.New(verdict.Why()) + } else if verdict == nil { + // The indexer writes a changed manifest's value before it judges it, and a new digest has no verdict: until + // the verdict is written the manifest is held (ADR 0614 §8). + err = noVerdictYet(r, v.Manifest) } } if err == nil { @@ -651,3 +661,27 @@ func (in Inputs) declared() map[string]catalogue.Manifest { } return out } + +// noVerdictYet is nil when the indexer's verdicts state holds an accepted verdict on the manifest, and the reason it is held +// when it holds none yet, cannot be read, or refuses it. +func noVerdictYet(r Reader, digest string) error { + key, err := delivery.VerdictsKey(digest) + if err != nil { + return nil + } + raw, found, err := r.Get(ReadVerdicts, key) + if err != nil { + return fmt.Errorf("its verdict cannot be read: %w", err) + } + if !found { + return errors.New("the indexer has written no verdict on this manifest yet") + } + v, err := delivery.DecodeManifestVerdict(raw) + if err != nil { + return fmt.Errorf("its verdict does not read: %w", err) + } + if v.Held() { + return errors.New(v.Why()) + } + return nil +} diff --git a/modules/mesh-grants/module.json b/modules/mesh-grants/module.json index 11e2149f..f7fae6e5 100644 --- a/modules/mesh-grants/module.json +++ b/modules/mesh-grants/module.json @@ -8,6 +8,7 @@ "mesh-delivery-builder.builds", "mesh-delivery-indexer.manifests", "mesh-delivery-indexer.modules", + "mesh-delivery-indexer.verdicts", "mesh-delivery-assigner.placement", "node-engine.have", "mesh-vault.bus-accounts",