mesh-grants: read manifests for their inputs, as every delivery part does, not against a list from the controller
mesh/delivery merged: source.main moved to novox/mesh-delivery@7ddbf40715ff
mesh/check pass aea22bf793b4 on 7ddbf40715ff | unit 168 run, 479 kept | integration 178 run, 545 kept | composition passed on novox

The part reads seats, buckets, logs and streams, never a config value, and
the merge gate already judged the fallbacks; reading the controller's
predefined mirror would tie the part to the leaving controller (ADR 0547
clause 1). ParseManifestForInputs takes a fallback as it declares itself.

Issue: 1945
This commit is contained in:
2026-10-26 09:36:18 +01:00
parent 72889be87f
commit aea22bf793
5 changed files with 63 additions and 120 deletions
+48 -58
View File
@@ -414,68 +414,58 @@ func TestGrants(t *testing.T) {
}
})
t.Run("manifests judged against the published list of predefined keys; every refusal listed", func(t *testing.T) {
// mod-p's value falls back to a predefined key, as claude-code's managed_settings does (issue 1945); mod-z's manifest
// carries a key mesh-sdk's reader refuses, and mod-z is assigned nowhere.
t.Run("a predefined-key fallback is read with no list; every refused manifest listed", func(t *testing.T) {
// mod-p's value falls back to a predefined key, as claude-code's managed_settings does, and no list is on the bus
// (issue 1945); mod-z's manifest carries a key mesh-sdk's reader refuses, and mod-z is assigned nowhere.
const pManifest = `{"module":"mod-p","version":"1","own-secrets":{"broker":"/var/lib/p/broker"},` +
`"config":{"size":{"type":"number","description":"The size text shows in, in points.","predefined":"font-size","default":12,"runtime":true}}}`
const zManifest = `{"module":"mod-z","version":"1","no-such-key":true}`
published := map[string]any{"keys": map[string]any{"font-size": map[string]any{"type": "number",
"description": "The size a desktop program shows its text in, in points.", "minimum": 6, "maximum": 72}},
"repository": "mesh-catalog", "commit": "c1", "generation": 7, "build": "b1"}
for _, row := range []struct {
name string
list bool
placed bool
refused []string
}{
{"a fallback to a predefined key is read against the list on the bus, and its module placed", true, true,
[]string{"manifest of mod-z"}},
{"with no list on the bus the fallback is refused, said, and its module placed nowhere", false, false,
[]string{"manifest of mod-p", "manifest of mod-z"}},
} {
t.Run(row.name, func(t *testing.T) {
b := fixture()
b.manifestOf("mod-p", pManifest)
b.manifestOf("mod-z", zManifest)
b.assign("b", "mod-p")
if row.list {
b.put(grants.ReadPredefinedKeys, grants.PredefinedListKey, published)
}
in := grants.ReadInputs(b, &grants.Manifests{})
var refused []string
for _, g := range in.Refused {
refused = append(refused, g.Input)
}
if !slices.Equal(refused, row.refused) {
t.Errorf("refused = %v, want %v", in.Refused, row.refused)
}
if !row.list && !gapNamed(in.Gaps, grants.ReadPredefinedKeys) {
t.Errorf("gaps %v do not say the list is missing", in.Gaps)
}
recs, _ := grants.Records(in)
users, err := grants.ComposeUsers(recs)
if err != nil {
t.Fatal(err)
}
if got := slices.Contains(names(users), "b.mod-p"); got != row.placed {
t.Errorf("b.mod-p composed %v, want %v: %v", got, row.placed, names(users))
}
// The pass and the tool listing the held memberships both say every refusal.
part := partOn(b)
res := part.Pass()
if len(res.Refused) != len(row.refused) || !strings.HasPrefix(res.Refused[len(res.Refused)-1], "manifest of mod-z: ") {
t.Errorf("the pass says refused %v, want %v", res.Refused, row.refused)
}
held, err := part.Held()
if err != nil {
t.Fatal(err)
}
if got, _ := held.(map[string]any)["refused"].([]string); len(got) != len(row.refused) {
t.Errorf("grants_membership lists refused %v, want %v", got, row.refused)
}
})
b := fixture()
b.manifestOf("mod-p", pManifest)
b.manifestOf("mod-z", zManifest)
b.assign("b", "mod-p")
in := grants.ReadInputs(b, &grants.Manifests{})
if len(in.Refused) != 1 || in.Refused[0].Input != "manifest of mod-z" || !gapNamed(in.Gaps, "manifest of mod-z") {
t.Errorf("refused = %v: only mod-z's, and named in the gaps", in.Refused)
}
recs, _ := grants.Records(in)
users, err := grants.ComposeUsers(recs)
if err != nil {
t.Fatal(err)
}
if !slices.Contains(names(users), "b.mod-p") {
t.Errorf("b.mod-p is not composed, so its module counts as placed nowhere: %v", names(users))
}
// The pass, the tool listing held memberships, and the log each say the refusal.
part := partOn(b)
res := part.Pass()
if len(res.Refused) != 1 || !strings.HasPrefix(res.Refused[0], "manifest of mod-z: ") {
t.Errorf("the pass says refused %v", res.Refused)
}
held, err := part.Held()
if err != nil {
t.Fatal(err)
}
if got, _ := held.(map[string]any)["refused"].([]string); len(got) != 1 {
t.Errorf("grants_membership lists refused %v", got)
}
var logged []string
var lmu sync.Mutex
f := &grants.Follower{Part: partOn(b), Watch: &fakeWatcher{opened: make(chan string, 16)}, Logf: func(format string, args ...any) {
lmu.Lock()
logged = append(logged, fmt.Sprintf(format, args...))
lmu.Unlock()
}}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() { f.Run(ctx); close(done) }()
waitFor(t, func() bool {
lmu.Lock()
defer lmu.Unlock()
return strings.Contains(strings.Join(logged, "\n"), "refused manifest of mod-z: ")
})
cancel()
<-done
})
t.Run("credentials", func(t *testing.T) {
@@ -17,9 +17,8 @@ type Watcher interface {
// Followed are the states whose change composes again: placement, the indexer's manifests by module, the resolved
// config values, the node agents' haves (a machine's first have, and the bus node's loaded list) and the assigner's
// wants (the manifest each machine is wanted to run), the vault's hashes of the bus accounts, and the list of predefined
// keys the manifests are judged against.
var Followed = []string{ReadAssignments, ReadModules, ReadResolved, ReadHave, ReadWants, ReadAccounts, ReadPredefinedKeys}
// wants (the manifest each machine is wanted to run), and the vault's hashes of the bus accounts.
var Followed = []string{ReadAssignments, ReadModules, ReadResolved, ReadHave, ReadWants, ReadAccounts}
// Follower composes again on every change it hears, never on a timer. It never stops for a refusal: a watch it cannot
// open is said, composed without, and opened again at the next wake (any change it does hear, or a reconnect).
+11 -54
View File
@@ -32,19 +32,11 @@ const (
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"
// ReadPredefinedKeys is the list of predefined keys the mesh published (the newest trunk build's of the catalogue,
// ADR 0348 §7), which the controller mirrors under one key, PredefinedListKey (mesh-sdk buscompose.PredefinedBucket):
// a manifest whose config value falls back to a predefined key is judged against it, as the controller judges it
// (issue 1945).
ReadPredefinedKeys = "mesh-controller.predefined"
)
// PredefinedListKey is the one key of the controller's predefined mirror.
const PredefinedListKey = "list"
// Reads is every state the part reads, in the manifest's order.
var Reads = []string{ReadAssignments, ReadControllerGrant, ReadModules, ReadManifests, ReadResolved, ReadHave, ReadWants, ReadBuilds,
ReadAccounts, ReadPredefinedKeys}
ReadAccounts}
// The bus module's tools the part calls, as its manifest's invokes name them: read-only, each.
const (
@@ -122,45 +114,12 @@ type Inputs struct {
Loaded *delivery.UserListWritten
// Hashes are the vault's hashes of the bus accounts, by bus user (ReadAccounts); kept in memory, never written.
Hashes map[string]HeldHash
// Predefined is the list of predefined keys the mesh published, nil when it cannot be read; PredefinedFrom says
// which build of the catalogue it came from.
Predefined catalogue.PredefinedList
PredefinedFrom string
// Refused is every manifest the indexer holds that the part cannot read or mesh-sdk's reader refuses, assigned or
// not, each with why: its module counts as placed nowhere, so it is said loudly, never passed over (issue 1945).
Refused []Gap
Gaps []Gap
}
// predefinedMirrored is the controller's predefined mirror's value (mesh-controller internal/inventory PredefinedMirrored).
type predefinedMirrored struct {
Keys catalogue.PredefinedList `json:"keys"`
Repository string `json:"repository"`
Commit string `json:"commit"`
Generation int `json:"generation"`
Build string `json:"build"`
}
// readPredefined reads the list of predefined keys the mesh published; what cannot be read is a gap and no list, so a
// manifest falling back to a key is refused, and said, as with no list anywhere.
func readPredefined(r Reader, gaps *[]Gap) (catalogue.PredefinedList, string) {
raw, found, err := r.Get(ReadPredefinedKeys, PredefinedListKey)
var v predefinedMirrored
switch {
case err != nil:
*gaps = append(*gaps, Gap{Input: ReadPredefinedKeys, Why: "refused or absent: " + err.Error() +
": a manifest falling back to a predefined key is refused"})
return nil, ""
case !found:
*gaps = append(*gaps, Gap{Input: ReadPredefinedKeys, Why: "the controller keeps no list yet: a manifest falling back to a predefined key is refused"})
return nil, ""
case json.Unmarshal(raw, &v) != nil || v.Keys == nil:
*gaps = append(*gaps, Gap{Input: ReadPredefinedKeys, Why: "holds no list of keys: a manifest falling back to a predefined key is refused"})
return nil, ""
}
return v.Keys, fmt.Sprintf("%s@%s#%d", v.Repository, v.Commit, v.Generation)
}
// HeldHash is one key of the vault's bus-accounts: the hash, and what it is of. The hash is never said or written by
// the part in shadow.
type HeldHash struct {
@@ -187,18 +146,16 @@ func readHashes(r Reader, gaps *[]Gap) map[string]HeldHash {
// BusModule is the bus's module: it holds mesh-broker.
const BusModule = "nats"
// Manifests caches manifests by digest and the list of predefined keys they were judged against: a digest names one
// manifest for ever, and a newer list judges it again.
// Manifests caches manifests by digest: a digest names one manifest for ever.
type Manifests struct {
mu sync.Mutex
parsed map[string]catalogue.Manifest
}
func (c *Manifests) get(r Reader, digest string, predefined catalogue.PredefinedList, from string) (catalogue.Manifest, error) {
key := digest + " " + from
func (c *Manifests) get(r Reader, digest string) (catalogue.Manifest, error) {
// The lock guards the map alone, never a read on the bus: a tool never waits on a pass's round trip.
c.mu.Lock()
m, ok := c.parsed[key]
m, ok := c.parsed[digest]
c.mu.Unlock()
if ok {
return m, nil
@@ -214,9 +171,10 @@ func (c *Manifests) get(r Reader, digest string, predefined catalogue.Predefined
if err != nil {
return catalogue.Manifest{}, err
}
// Judged against the list the mesh published, read from the bus, as the controller judges it (ADR 0348 §7): never
// against this process's own, which holds none (issue 1945).
m, err = catalogue.ParseManifestWith([]byte(v.JSON), predefined)
// Read for its inputs, as every delivery part reads a manifest (issue 1751): the merge gate judged its config values,
// and the part reads its seats, buckets, logs and streams, never a value; so a value falling back to a predefined key
// is taken as it declares itself, where ParseManifest refused it with no list and placed its module nowhere (issue 1945).
m, err = catalogue.ParseManifestForInputs([]byte(v.JSON))
if err != nil {
return catalogue.Manifest{}, fmt.Errorf("the manifest %s is refused by mesh-sdk's reader: %w", digest, err)
}
@@ -225,7 +183,7 @@ func (c *Manifests) get(r Reader, digest string, predefined catalogue.Predefined
if c.parsed == nil {
c.parsed = map[string]catalogue.Manifest{}
}
c.parsed[key] = m
c.parsed[digest] = m
return m, nil
}
@@ -255,7 +213,6 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
in := Inputs{Assigned: map[string][]string{}, Manifests: map[string]catalogue.Manifest{}, Digests: map[string]string{},
Running: map[string]catalogue.Manifest{},
Values: map[string]any{}, Gaps: append([]Gap(nil), standingGaps...)}
in.Predefined, in.PredefinedFrom = readPredefined(r, &in.Gaps)
nodes := map[string]bool{}
for _, raw := range each(r, ReadAssignments, &in.Gaps) {
var a assignment
@@ -320,7 +277,7 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
}
var m catalogue.Manifest
if err == nil {
m, err = cache.get(r, v.Manifest, in.Predefined, in.PredefinedFrom)
m, err = cache.get(r, v.Manifest)
}
if err != nil {
// Every manifest, assigned or not: one refused declares no seat, so its holders compose as placed nowhere.
@@ -367,7 +324,7 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
fromTrunk = append(fromTrunk, node+"."+module)
continue
}
if m, err := cache.get(r, b.Manifest, in.Predefined, in.PredefinedFrom); err == nil {
if m, err := cache.get(r, b.Manifest); err == nil {
in.Running[node+"."+module] = m
} else {
fromTrunk = append(fromTrunk, node+"."+module)
+1 -3
View File
@@ -202,9 +202,7 @@ func (p *Part) AssertOne(module string) (any, error) {
if err != nil || v.Manifest == "" {
return nil, fmt.Errorf("the indexer names no manifest of %s", module)
}
var gaps []Gap
list, from := readPredefined(p.Read, &gaps)
m, err := p.cache.get(p.Read, v.Manifest, list, from)
m, err := p.cache.get(p.Read, v.Manifest)
if err != nil {
return nil, err
}
+1 -2
View File
@@ -11,8 +11,7 @@
"mesh-delivery-indexer.manifests",
"mesh-delivery-indexer.modules",
"node-engine.have",
"mesh-vault.bus-accounts",
"mesh-controller.predefined"
"mesh-vault.bus-accounts"
],
"invokes": [
"nats.nats_streams",