Read a provider's outcome per offer, and read route-proxy's and node-tools' entries, so provisions a helper does not answer stop failing and routed keys stop standing asked (issue 774)
mesh/delivery-group group fix/held-module-and-assigner-contract rejected: composed together: the checker's verdict at the members' heads (c82f18b5e6bb): integr…
mesh/delivery closed: the pull request closed unmerged
mesh/check failed: integration: mesh-healers/cmd/mesh-healers.TestTheHealersWholeActOnTheBusAndBrake: never: the would-act written | b8ce4f0c0ac5 on 3…
mesh/delivery-group group fix/held-module-and-assigner-contract rejected: composed together: the checker's verdict at the members' heads (c82f18b5e6bb): integr…
mesh/delivery closed: the pull request closed unmerged
mesh/check failed: integration: mesh-healers/cmd/mesh-healers.TestTheHealersWholeActOnTheBusAndBrake: never: the would-act written | b8ce4f0c0ac5 on 3…
The sweep of 2026-10-30 20:10: <node>.claude-code.gitea-read and <node>.mesh-build-node.npm-package-registry failed on all four nodes with "gitea does not serve": gitea's provider helper answers only forge-account and refuses every other bind into gitea.bound, and the provisioner took any entry of a module declaring `bound` as the outcome. And novox.gitea.route, novox.keycloak.route and <node>.claude-code.mcp-endpoint stood asked: route-proxy and node-tools keep entries the manifest did not read. Quick fix: add route-proxy.bound and node-tools.bound to the reads. Thorough fix: judge each offer by who answers it (answered-by: helper reads the entries, any other offer of a module that names a helper is judged by its have), and say on gitea's forge-account that the helper answers it. Chose both: the reads alone leave gitea's two provisions failed.
This commit is contained in:
@@ -423,7 +423,8 @@
|
||||
{
|
||||
"name": "forge-account",
|
||||
"credential": {"form": "none"},
|
||||
"scope": "mesh"
|
||||
"scope": "mesh",
|
||||
"answered-by": "helper"
|
||||
}
|
||||
],
|
||||
"build": {
|
||||
|
||||
@@ -66,8 +66,10 @@ const (
|
||||
|
||||
// BoundReads are the providers' entries this module's manifest reads, `<module>.bound`, whose changes it hears. Which
|
||||
// path judges a provider is its manifest's word (keepsEntries), not this list: a provider that declares `bound` and is
|
||||
// not read here leaves its keys asked, saying its entries are not read.
|
||||
var BoundReads = []string{"gitea", "keycloak", "postgres", "route-adapter", "mesh-vault"}
|
||||
// not read here leaves its keys asked, saying its entries are not read. route-proxy (mesh-control) and node-tools
|
||||
// (mesh-tools) answer route and mcp-endpoint by the provider helper from outside this catalogue (issue 774, sweep of
|
||||
// 2026-10-30 20:10: their keys stood asked on every node).
|
||||
var BoundReads = []string{"gitea", "keycloak", "postgres", "route-adapter", "route-proxy", "node-tools", "mesh-vault"}
|
||||
|
||||
// boundRead is a provider's entries as this module reads them.
|
||||
func boundRead(module string) string { return module + ".bound" }
|
||||
@@ -546,7 +548,7 @@ type entry struct {
|
||||
// asked, said so.
|
||||
func (h *Holder) outcome(key string, p *provisions.Provision) {
|
||||
p.Ask, p.Warrant = "", nil
|
||||
keeps, err := h.keepsEntries(p.Provider.Module)
|
||||
keeps, err := h.keepsEntries(p.Provider.Module, p.Provision)
|
||||
if err != nil {
|
||||
p.State, p.Why = provisions.Asked, "the manifest of "+p.Provider.Module+" cannot be read: "+err.Error()
|
||||
return
|
||||
@@ -644,39 +646,30 @@ func (h *Holder) haveOutcome(p *provisions.Provision) {
|
||||
}
|
||||
}
|
||||
|
||||
// keepsEntries says a provider's manifest at the trunk declares the state `bound`: its outcome per consumer is its
|
||||
// entries there; any other provider's is its own have.
|
||||
func (h *Holder) keepsEntries(module string) (bool, error) {
|
||||
mk, err := delivery.ModulesKey(module)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
e, err := h.Store.Get(ModulesRead, mk)
|
||||
if err != nil || e == nil {
|
||||
return false, fmt.Errorf("the indexer holds no modules.%s (%v)", module, err)
|
||||
}
|
||||
m, err := delivery.DecodeModule(e.Value)
|
||||
if err != nil || m.Manifest == "" {
|
||||
return false, fmt.Errorf("the indexer's modules.%s names no manifest", module)
|
||||
}
|
||||
key, err := delivery.ManifestsKey(m.Manifest)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
me, err := h.Store.Get(ManifestsRead, key)
|
||||
if err != nil || me == nil {
|
||||
return false, fmt.Errorf("the indexer holds no manifest of %s (%v)", module, err)
|
||||
}
|
||||
man, err := delivery.DecodeManifest(me.Value)
|
||||
// keepsEntries says whether a provider's outcome for one provision is its entries (its manifest at the trunk declares the
|
||||
// state `bound`) rather than its own have. When any offer of the provider says `answered-by: helper`, only those offers
|
||||
// are answered by its entries: a module running the helper for one provision and making another some other way (gitea:
|
||||
// the helper answers forge-account, the vault issues gitea-read, the package harness makes npm-package-registry) is
|
||||
// judged per offer, so the helper's "does not serve" for an offer it does not run never becomes the provision's outcome
|
||||
// (issue 774, sweep of 2026-10-30 20:10). A provider whose offers say nothing keeps every outcome in its entries.
|
||||
func (h *Holder) keepsEntries(module, provision string) (bool, error) {
|
||||
man, err := h.providerManifest(module)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return keepsEntriesFor(man, provision)
|
||||
}
|
||||
|
||||
// keepsEntriesFor is keepsEntries over a provider's manifest JSON.
|
||||
func keepsEntriesFor(manifest, provision string) (bool, error) {
|
||||
var decl struct {
|
||||
State []json.RawMessage `json:"state"`
|
||||
State []json.RawMessage `json:"state"`
|
||||
Provides []json.RawMessage `json:"provides"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(man.JSON), &decl); err != nil {
|
||||
if err := json.Unmarshal([]byte(manifest), &decl); err != nil {
|
||||
return false, err
|
||||
}
|
||||
bound := false
|
||||
for _, raw := range decl.State {
|
||||
var name string
|
||||
if json.Unmarshal(raw, &name) != nil {
|
||||
@@ -687,10 +680,56 @@ func (h *Holder) keepsEntries(module string) (bool, error) {
|
||||
name = full.Name
|
||||
}
|
||||
if name == "bound" {
|
||||
return true, nil
|
||||
bound = true
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
if !bound {
|
||||
return false, nil
|
||||
}
|
||||
helped, says := false, false
|
||||
for _, raw := range decl.Provides {
|
||||
var full struct {
|
||||
Name string `json:"name"`
|
||||
AnsweredBy string `json:"answered-by"`
|
||||
}
|
||||
if json.Unmarshal(raw, &full) != nil || full.AnsweredBy != catalogue.AnsweredByHelper {
|
||||
continue
|
||||
}
|
||||
says = true
|
||||
if full.Name == provision {
|
||||
helped = true
|
||||
}
|
||||
}
|
||||
return helped || !says, nil
|
||||
}
|
||||
|
||||
// providerManifest is a provider's manifest JSON at the trunk, as the indexer holds it.
|
||||
func (h *Holder) providerManifest(module string) (string, error) {
|
||||
mk, err := delivery.ModulesKey(module)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
e, err := h.Store.Get(ModulesRead, mk)
|
||||
if err != nil || e == nil {
|
||||
return "", fmt.Errorf("the indexer holds no modules.%s (%v)", module, err)
|
||||
}
|
||||
m, err := delivery.DecodeModule(e.Value)
|
||||
if err != nil || m.Manifest == "" {
|
||||
return "", fmt.Errorf("the indexer's modules.%s names no manifest", module)
|
||||
}
|
||||
key, err := delivery.ManifestsKey(m.Manifest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
me, err := h.Store.Get(ManifestsRead, key)
|
||||
if err != nil || me == nil {
|
||||
return "", fmt.Errorf("the indexer holds no manifest of %s (%v)", module, err)
|
||||
}
|
||||
man, err := delivery.DecodeManifest(me.Value)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return man.JSON, nil
|
||||
}
|
||||
|
||||
// retired says a retiring key the operator approved is retired: its provider's entry is gone.
|
||||
@@ -698,7 +737,7 @@ func (h *Holder) retired(key string, p provisions.Provision) (bool, string) {
|
||||
if p.Warrant == nil || p.Warrant.Option != OptionApprove {
|
||||
return false, ""
|
||||
}
|
||||
if keeps, err := h.keepsEntries(p.Provider.Module); err != nil || !keeps {
|
||||
if keeps, err := h.keepsEntries(p.Provider.Module, p.Provision); err != nil || !keeps {
|
||||
return false, ""
|
||||
}
|
||||
e, err := h.Store.Get(boundRead(p.Provider.Module), key)
|
||||
|
||||
@@ -412,3 +412,63 @@ func TestTheHolderHoldsNoTimer(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// manifestOf puts a provider's manifest into the indexer's reads as the holder finds it.
|
||||
func (m *mesh) manifestOf(module, raw string) {
|
||||
digest := delivery.ManifestDigest([]byte(raw))
|
||||
value, _ := delivery.EncodeManifest(delivery.Manifest{Module: module, JSON: raw})
|
||||
_ = m.s.Put(ManifestsRead, mustKey(delivery.ManifestsKey(digest)), json.RawMessage(value))
|
||||
_ = m.s.Put(ModulesRead, mustKey(delivery.ModulesKey(module)),
|
||||
json.RawMessage(`{"v":1,"none2":"a test","manifest":"`+digest+`","repository":"novox/mesh-catalog","commit":"c1"}`))
|
||||
}
|
||||
|
||||
// A provider's outcome is read per offer: from its entries where the provider helper answers that offer, from its have
|
||||
// where it does not, and the entries of route-proxy and node-tools are read (issue 774, the sweep of 2026-10-30 20:10:
|
||||
// <node>.claude-code.gitea-read failed "gitea does not serve" on all four nodes while gitea's helper answers only
|
||||
// forge-account, and novox.gitea.route and <node>.claude-code.mcp-endpoint stood asked, their providers' entries unread).
|
||||
func TestAProvidersOutcomeIsReadPerOffer(t *testing.T) {
|
||||
const (
|
||||
gitea = `{"module":"gitea","version":"1","state":["bound","announced"],"provides":[{"name":"npm-package-registry","credential":{"form":"pair"}},{"name":"gitea-read","credential":{"issued-by":"software","tool":"gitea_agent_token"}},{"name":"forge-account","credential":{"form":"none"},"answered-by":"helper"}]}`
|
||||
routes = `{"module":"route-proxy","version":"1","state":["bound"],"provides":[{"name":"route","scope":"mesh","identity":false,"answered-by":"helper"}]}`
|
||||
nodeTools = `{"module":"node-tools","version":"1","state":["bound"],"provides":[{"name":"mcp-endpoint","scope":"node","answered-by":"helper"}]}`
|
||||
running = `{"v":1,"fp":"src2:` + "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + `","phase":"running","health":"ok"}`
|
||||
refused = `{"state":"failed","refused":"claude-code on ace is bound to gitea on novox for gitea-read, which gitea does not serve"}`
|
||||
)
|
||||
for _, row := range []struct {
|
||||
name string
|
||||
node string
|
||||
plan map[string]catalogue.Plan
|
||||
key string
|
||||
bucket string // the provider's entries, written as entry
|
||||
entry string
|
||||
have string // the provider's have key, written running
|
||||
state string
|
||||
}{
|
||||
{"gitea-read: the helper's refusal is not its outcome; gitea runs, so provided", "ace",
|
||||
map[string]catalogue.Plan{"claude-code": planOf(catalogue.PlanProvision{Provision: "gitea-read", Provider: "novox", ProviderModule: "gitea"})},
|
||||
"ace.claude-code.gitea-read", "gitea.bound", refused, "novox.gitea", provisions.Provided},
|
||||
{"forge-account: still the helper's entry", "ace",
|
||||
map[string]catalogue.Plan{"mesh-build-go": planOf(catalogue.PlanProvision{Provision: "forge-account", Provider: "novox", ProviderModule: "gitea"})},
|
||||
"ace.mesh-build-go.forge-account", "gitea.bound", `{"state":"provided"}`, "", provisions.Provided},
|
||||
{"route: route-proxy's entry is read", "novox",
|
||||
map[string]catalogue.Plan{"gitea": planOf(catalogue.PlanProvision{Provision: "route", Provider: "novox", ProviderModule: "route-proxy"})},
|
||||
"novox.gitea.route", "route-proxy.bound", `{"state":"provided"}`, "", provisions.Provided},
|
||||
{"mcp-endpoint: node-tools' entry is read", "ace",
|
||||
map[string]catalogue.Plan{"claude-code": planOf(catalogue.PlanProvision{Provision: "mcp-endpoint", Provider: "ace", ProviderModule: "node-tools"})},
|
||||
"ace.claude-code.mcp-endpoint", "node-tools.bound", `{"state":"provided"}`, "", provisions.Provided},
|
||||
} {
|
||||
m := newMesh()
|
||||
m.manifestOf("gitea", gitea)
|
||||
m.manifestOf("route-proxy", routes)
|
||||
m.manifestOf("node-tools", nodeTools)
|
||||
m.atWant[row.node], m.atNewest[row.node] = row.plan, row.plan
|
||||
m.s.write(t, row.bucket, row.key, row.entry)
|
||||
if row.have != "" {
|
||||
m.s.write(t, HaveRead, row.have, running)
|
||||
}
|
||||
m.compose(t, row.node)
|
||||
if p := m.state(t, row.key); p.State != row.state {
|
||||
t.Errorf("%s: %s is %s (%s)", row.name, row.key, p.State, p.Why)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
"keycloak.bound",
|
||||
"postgres.bound",
|
||||
"route-adapter.bound",
|
||||
"route-proxy.bound",
|
||||
"node-tools.bound",
|
||||
"mesh-vault.bound"
|
||||
],
|
||||
"tools": [
|
||||
|
||||
Reference in New Issue
Block a user