From e703e9b0e2e57464a3cab84542a4f7881ca04a75 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 30 Oct 2026 18:08:02 +0100 Subject: [PATCH] Judge every read for its reader where the bus's users are composed (issue 774) Since 13:25 the vault, the assigner, the watchman, the grants part and the artifact store's tools were refused mesh-controller.lease, and the vault mesh-grants.accounts, though the composer names them those states' readers (ADR 0547 clause 9): the controller splits reads with its own copy of the reader-less ReadsAnnounced, which knows no named readers, and Users only moved reads from granted to refused, never back. Quick fix: the controller calls ReadsAnnouncedBy with the module. Thorough fix, chosen because the kind was met on four owners and the judgement had two copies: Users judges every read again for its reader against the announcements in Records (withAnnouncedReads), whatever split the caller made, and the reader-less ReadsAnnounced is removed so no caller can judge without the reader. A rule over manifests was not added: ADR 0495 clauses 1 and 5 keep readability out of the manifest and replaced the merge gate's reads check by each module's announcement test. gitea.bound and disk-load.readings stay refused: their owners announce them not readable, which their own code fixes. --- go/buscompose/announced.go | 16 ++++++---------- go/buscompose/buscompose_test.go | 21 +++++++++++++++++++++ go/buscompose/userlist_switch.go | 18 ++++++++++++++++++ go/buscompose/users.go | 8 ++++---- 4 files changed, 49 insertions(+), 14 deletions(-) diff --git a/go/buscompose/announced.go b/go/buscompose/announced.go index 09637ba..90c57d8 100644 --- a/go/buscompose/announced.go +++ b/go/buscompose/announced.go @@ -42,16 +42,12 @@ func ControllerAnnouncement(node string) announce.States { return s } -// ReadsAnnounced splits a reader's `reads` (`.`) by what each owner announced: granted when the owner -// announced the state readable, or announced nothing yet (the transition); refused when the owner announced and did -// not announce that state readable. -func ReadsAnnounced(reads []string, announced map[string]announce.States) (granted, refused []string) { - return ReadsAnnouncedBy("", reads, announced) -} - -// ReadsAnnouncedBy is ReadsAnnounced for one reader module, which also honours the readers an owner named for a state -// (announce.State.Readers) and the composer's named readers (NamedReaders, ADR 0547 clause 9): a read named for other -// modules is refused. An empty reader is judged by the readable mark alone. +// ReadsAnnouncedBy splits one reader module's `reads` (`.`) by what each owner announced: granted when the +// owner announced the state readable (by this reader, where it named its readers: announce.State.Readers), when the +// composer names the reader among the state's readers (NamedReaders, ADR 0547 clause 9), or when the owner announced +// nothing yet (the transition); refused otherwise, and a read named for other modules is refused. There is no judgement +// without the reader (issue 774): one made without it refused every named reader, and Users judges again for the +// reader whatever split it is given (withAnnouncedReads). An empty reader is judged by the readable mark alone. func ReadsAnnouncedBy(reader string, reads []string, announced map[string]announce.States) (granted, refused []string) { for _, r := range reads { owner, name, ok := strings.Cut(r, ".") diff --git a/go/buscompose/buscompose_test.go b/go/buscompose/buscompose_test.go index 5eb1bef..3d02084 100644 --- a/go/buscompose/buscompose_test.go +++ b/go/buscompose/buscompose_test.go @@ -552,6 +552,27 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) { t.Errorf("%s reads the lease: %v, want %v", reader, granted, want) } } + // Issue 774 (sweep item 2): Users judges every read for its reader again, whatever split the caller made: the + // controller's own reader-less copy refused the vault the lease and the grants part's accounts it is named for. + announced["mesh-grants"] = announce.States{Module: "mesh-grants", States: []announce.State{{Name: "accounts", Readers: []string{VaultSeat}}}} + announced["disk-load"] = announce.States{Module: "disk-load", States: []announce.State{{Name: "readings"}}} + judged, err := Users(Records{Nodes: []string{"a"}, Announced: announced, Assigned: map[string][]Declared{"a": { + {Module: VaultSeat, ReadsRefused: []string{ControllerLease, GrantsAccounts}}, + {Module: "mesh-config", ReadsRefused: []string{ControllerLease}}, + {Module: "restic", ReadsRefused: []string{"disk-load.readings"}}, + {Module: "messenger", Reads: []string{ControllerLease, GrantsAccounts}}}}}) + if err != nil { + t.Fatal(err) + } + reads := map[string]string{} + for _, u := range judged { + if u.Kind == KindModule { + reads[u.Module] = strings.Join(u.Reads, ",") + } + } + if reads[VaultSeat] != ControllerLease+","+GrantsAccounts || reads["mesh-config"] != "" || reads["restic"] != "" || reads["messenger"] != "" { + t.Errorf("the reads granted for their readers: %v", reads) + } c, _ := ConsumerFor(Principal{Kind: KindModule, Node: "a", Module: "m", Consumes: []string{"x.happened"}}) cfg := ConsumerConfig(c) if cfg.Durable != "a_m" || cfg.AckPolicy != jetstream.AckExplicitPolicy || cfg.AckWait != 30*time.Second || diff --git a/go/buscompose/userlist_switch.go b/go/buscompose/userlist_switch.go index 9f57881..cbd7f98 100644 --- a/go/buscompose/userlist_switch.go +++ b/go/buscompose/userlist_switch.go @@ -5,6 +5,7 @@ import ( "strings" "time" + "git.novox.be/novox/mesh-sdk/go/announce" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/handacts" "git.novox.be/novox/mesh-sdk/go/health" @@ -116,6 +117,23 @@ func readsNamed(module string, reads []string) (granted, refused []string) { return granted, refused } +// withAnnouncedReads is d with its reads judged for d's module (ReadsAnnouncedBy) against what the owners announced: +// every read its manifest names, granted or refused by the caller, judged again here, so a caller that split them +// without the reader (the controller's own copy of a reader-less judgement, issue 774: the vault, the assigner, the +// grants part and the artifact store's tools refused the lease they are named for) cannot refuse a read the reader is +// named for, nor grant one named for another. With no announcements read it is withNamedReaders. +func withAnnouncedReads(d Declared, announced map[string]announce.States) Declared { + if announced == nil { + return withNamedReaders(d) + } + all := append(append([]string(nil), d.Reads...), d.ReadsRefused...) + if len(all) == 0 { + return d + } + d.Reads, d.ReadsRefused = ReadsAnnouncedBy(d.Module, all, announced) + return d +} + // withNamedReaders is d with the reads named for other modules moved to its refused reads. func withNamedReaders(d Declared) Declared { granted, refused := readsNamed(d.Module, d.Reads) diff --git a/go/buscompose/users.go b/go/buscompose/users.go index 662d38b..aaf966e 100644 --- a/go/buscompose/users.go +++ b/go/buscompose/users.go @@ -205,9 +205,9 @@ func Users(r Records) ([]Principal, error) { if !Composed(d) { continue } - // A state with named readers is read by them alone (ADR 0547 clause 9): a read named for another module is - // granted nothing, here and in the runtime carrying the module. - d = withReadLogs(withNamedReaders(d), logs) + // A state with named readers is read by them alone (ADR 0547 clause 9), and a read is granted what its owner + // announced (ADR 0495 clause 3): judged here, for this reader, whatever split the caller made (issue 774). + d = withReadLogs(withAnnouncedReads(d, r.Announced), logs) // **A process account is its own, delivered in its own secret** (broker-, novox/hq // ADR 0436 §1, §2), never the module's `broker` — so it is composed whether or not the module // itself has an account, and is never carried by the machine's runtime, which runs as the @@ -259,7 +259,7 @@ func Users(r Records) ([]Principal, error) { var carried []Declared for _, d := range r.Assigned[node] { if CarriedByRunner(d) { - d = withReadLogs(withNamedReaders(d), logs) + d = withReadLogs(withAnnouncedReads(d, r.Announced), logs) // Never a restricted stream for what the tool runner carries (novox/hq ADR 0376 §1). d.Hears = ComposedHears(d) carried = append(carried, d)