From b8abf3ed3b7c2b428e43b49d5d8dd38b99eaa12a Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 01:16:54 +0100 Subject: [PATCH] Fix 9.7a: the grants part resolves a read naming a seat to its holder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds: design 47 G A-116 (§636, §616) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- modules/mesh-grants/grants_test.go | 33 +++++++++++++++++++ .../mesh-grants/internal/grants/records.go | 31 +++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index e48802c2..6b8dad52 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -4964,6 +4964,39 @@ func TestTheGrantsPartKeepsWhatEachNodeRuns(t *testing.T) { } } +// A module reading a seat holder's state, `seat:.`, is granted the read of the module holding the seat on record +// (design 47 G A-116). +func TestASeatNamedReadIsResolvedToItsHolder(t *testing.T) { + for _, row := range []struct { + name, holder string + reads, want []string + }{ + {"resolved to the holder on record", "prov-one", []string{"seat:mesh-provisioner.plans", "mesh-vault.node-logins"}, + []string{"prov-one.plans", "mesh-vault.node-logins"}}, + {"a new holder is read with no change of the manifest", "prov-two", []string{"seat:mesh-provisioner.provisions"}, + []string{"prov-two.provisions"}}, + {"no holder on record stays as written", "", []string{"seat:mesh-provisioner.plans"}, []string{"seat:mesh-provisioner.plans"}}, + } { + t.Run(row.name, func(t *testing.T) { + in := grants.Inputs{Nodes: []string{"a"}, Assigned: map[string][]string{"a": {"mod-t"}}, + Manifests: map[string]catalogue.Manifest{"mod-t": {Module: "mod-t", Version: "1", RunsAs: "mod-t", Reads: row.reads, + OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/var/lib/mod-t/broker"}}}}} + if row.holder != "" { + in.Placed = map[string]meshmoduleassigner.Assignments{"a": {Node: "a", + Holds: []meshmoduleassigner.Holding{{Claim: "mesh-provisioner", Scope: "mesh", Module: row.holder}}}} + } + recs, _ := grants.ListRecords(in) + var got []string + for _, d := range recs.Assigned["a"] { + got = d.Reads + } + if !slices.Equal(got, row.want) { + t.Errorf("reads = %v, want %v", got, row.want) + } + }) + } +} + // A module naming a seat's view, `seat:.views.`, is granted the view of the module holding the seat on record, so a // new holder is granted with no change of its manifest (design 47 G §616). func TestASeatNamedViewIsResolvedToItsHolder(t *testing.T) { diff --git a/modules/mesh-grants/internal/grants/records.go b/modules/mesh-grants/internal/grants/records.go index 50e2a5f5..e4252c5b 100644 --- a/modules/mesh-grants/internal/grants/records.go +++ b/modules/mesh-grants/internal/grants/records.go @@ -88,6 +88,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) { } declare := func(m catalogue.Manifest) buscompose.Declared { m.Consumes = resolveSeatViews(m.Consumes, holders) + m.Reads = resolveSeatReads(m.Reads, holders) d := declaredFor(m, seats, declarers) if len(in.Placed) > 0 { d.Holds = heldWherePlaced(d.Holds, holders, node, m.Module) @@ -212,6 +213,36 @@ func resolveSeatViews(consumes []string, holders []catalogue.Held) []string { return out } +// resolveSeatReads resolves each `seat:.` of a module's reads to `.`, the module holding the seat +// on record, as resolveSeatViews does a view: a new holder is read with no change of the manifest (design 47 G A-116). An entry +// whose seat has no holder on record stays as written, and grants nothing. Pure. +func resolveSeatReads(reads []string, holders []catalogue.Held) []string { + var out []string + for i, r := range reads { + rest, isSeat := strings.CutPrefix(r, seatViewConsume) + seatName, state, named := strings.Cut(rest, ".") + if !isSeat || !named || state == "" || strings.Contains(state, ".") { + if out != nil { + out = append(out, r) + } + continue + } + if out == nil { + out = slices.Clone(reads[:i]) + } + holder := slices.IndexFunc(holders, func(h catalogue.Held) bool { return sameSeat(h.Claim, seatName) }) + if holder < 0 { + out = append(out, r) + continue + } + out = append(out, holders[holder].Module+"."+state) + } + if out == nil { + return reads + } + return out +} + // consumedOf splits what a manifest consumes as the composer grants it: a seat's event on the seat's own subject (its // watches), every other by its emitter's (novox/hq ADR 0118). A placed module and the node-engine's own manifest are split // alike.