diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index fe5cd07..03acd28 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -414,6 +414,60 @@ func TestGrants(t *testing.T) { } }) + 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}` + 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) { shadow := []grants.User{{User: "a.held"}, {User: "a.new"}, {User: "a.bare"}} loaded := []grants.User{{User: "a.held"}, {User: "a.bare"}, {User: "enrol.x"}, {User: "person.op"}} diff --git a/modules/mesh-grants/internal/grants/follow.go b/modules/mesh-grants/internal/grants/follow.go index a3a6400..2fb7824 100644 --- a/modules/mesh-grants/internal/grants/follow.go +++ b/modules/mesh-grants/internal/grants/follow.go @@ -39,6 +39,7 @@ type Follower struct { // held and notAsserted are what the last pass held back and could not create, said again only when they change. held string notAsserted string + refusedMans string } func (f *Follower) init() { @@ -150,6 +151,18 @@ func (f *Follower) open() { // the pass could not create, each with why (issue 1790), when the set changes from the last pass's; and every stream it // created, one line per module. func (f *Follower) said(res Result) { + if refused := strings.Join(res.Refused, "\n"); refused != f.refusedMans { + f.refusedMans = refused + if len(res.Refused) == 0 { + f.logf("every manifest the indexer holds is read") + } else { + f.logf("%d manifests cannot be read, so their modules count as placed nowhere (grants_membership with no node and module lists them):", + len(res.Refused)) + for _, r := range res.Refused { + f.logf("refused %s", r) + } + } + } if held := strings.Join(res.NotPublished, "\n"); held != f.held { f.held = held if len(res.NotPublished) == 0 { diff --git a/modules/mesh-grants/internal/grants/inputs.go b/modules/mesh-grants/internal/grants/inputs.go index 2a480ed..a281b81 100644 --- a/modules/mesh-grants/internal/grants/inputs.go +++ b/modules/mesh-grants/internal/grants/inputs.go @@ -114,7 +114,10 @@ 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 - Gaps []Gap + // 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 } // 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 @@ -168,7 +171,10 @@ func (c *Manifests) get(r Reader, digest string) (catalogue.Manifest, error) { if err != nil { return catalogue.Manifest{}, err } - m, err = catalogue.ParseManifest([]byte(v.JSON)) + // 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) } @@ -269,17 +275,15 @@ func ReadInputs(r Reader, cache *Manifests) Inputs { if err == nil && v.Manifest == "" { err = fmt.Errorf("the indexer's value names no manifest digest") } - if err != nil { - if wanted[module] { - in.Gaps = append(in.Gaps, Gap{Input: "manifest of " + module, Why: err.Error()}) - } - continue + var m catalogue.Manifest + if err == nil { + m, err = cache.get(r, v.Manifest) } - m, err := cache.get(r, v.Manifest) if err != nil { - if wanted[module] { - in.Gaps = append(in.Gaps, Gap{Input: "manifest of " + module, Why: err.Error()}) - } + // Every manifest, assigned or not: one refused declares no seat, so its holders compose as placed nowhere. + g := Gap{Input: "manifest of " + module, Why: err.Error()} + in.Refused = append(in.Refused, g) + in.Gaps = append(in.Gaps, g) continue } in.Manifests[module] = m diff --git a/modules/mesh-grants/internal/grants/part.go b/modules/mesh-grants/internal/grants/part.go index b78b602..21533b9 100644 --- a/modules/mesh-grants/internal/grants/part.go +++ b/modules/mesh-grants/internal/grants/part.go @@ -96,8 +96,11 @@ type Result struct { AssertionsWritten int `json:"assertions_written"` // Asserted is each module whose declared streams this pass created, ` at : created …`, and // NotAsserted each creation refused, ` : ` (issue 1790). - Asserted []string `json:"asserted,omitempty"` - NotAsserted []string `json:"not_asserted,omitempty"` + Asserted []string `json:"asserted,omitempty"` + NotAsserted []string `json:"not_asserted,omitempty"` + // Refused is every manifest the indexer holds that this pass could not read, `manifest of : `: its + // module counts as placed nowhere (issue 1945). + Refused []string `json:"refused,omitempty"` Errors []string `json:"errors,omitempty"` PublishedOrAsserted string `json:"published_or_asserted"` } @@ -165,6 +168,9 @@ func (p *Part) Compose() Result { p.mu.Unlock() }() in := ReadInputs(p.Read, &p.cache) + for _, g := range in.Refused { + res.Refused = append(res.Refused, g.Input+": "+g.Why) + } recs, left := Records(in) st := UserListState{V: 1, Mode: Shadow, LeftOut: left, Loaded: in.Loaded, BusNode: in.BusNode, Gaps: append(append([]Gap(nil), in.Gaps...), MembershipGaps...), At: p.now()} diff --git a/modules/mesh-grants/internal/grants/tools.go b/modules/mesh-grants/internal/grants/tools.go index 7fe78c0..bbc7f2a 100644 --- a/modules/mesh-grants/internal/grants/tools.go +++ b/modules/mesh-grants/internal/grants/tools.go @@ -116,7 +116,11 @@ func (p *Part) Held() (any, error) { key, why, _ := strings.Cut(n, ": ") out = append(out, held{Membership: key, Why: why}) } - return map[string]any{"held": out, "count": len(out), "pass": rev, "published": res.Published}, nil + refused := res.Refused + if refused == nil { + refused = []string{} + } + return map[string]any{"held": out, "count": len(out), "pass": rev, "published": res.Published, "refused": refused}, nil } // Pass composes and keeps the result as the last pass, for the tools to read.