Files
mesh-sdk/go/compose/resolve.go
T
jschoubben 362a3f1378
mesh/delivery merged: source.main moved to novox/mesh-sdk@be77f6656326
mesh/delivery-group group fix/1126-shadow-bodies merged: every member is merged
mesh/check pass 362a3f1378aa on 03da552cb331 | unit 2860 run, 1141 kept, 36 skipped | integration 2887 run, 1241 kept, 29 skipped | over bound: unit (…
A refusal for want of a provider names the modules another node cannot run in this composition, and a refusal naming no placed module is answered at once (issue 1126, probe #532)
ace and g14 answered 'nothing in this mesh provides artifact-store … assign
distribution to a node' while novox runs it: a module another node cannot run
in this composition (catalogue Resolution.Unhostable) offers nothing, silently.
The refusal now names each such module and what it lacks, and each node left
out. And the culprit search composed the whole mesh once per placed module for a
refusal no stub changes (ace's answer took 7m46s): only the modules the refusal
names are tried, at most 8.
2026-10-28 01:39:07 +01:00

295 lines
12 KiB
Go

package compose
import (
"fmt"
"slices"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// LicenceProvision is what a module requires in order to be given a licence (the controller's licences.Provision).
const LicenceProvision = "model-access"
// Resolved is what one node should run, from what was assigned to it, with the settings of everything that resolved.
type Resolved struct {
Plan catalogue.Resolution
Settings catalogue.SettingsBy
// Foreseen are the needs composed with a stand-in for a credential the next send makes (or holds with no value given
// here), as "<module> on <node> needs <provision> from <provider>". Nothing was made.
Foreseen []string
// NotGiven are the words of each credential only a person gives that nobody has: the consumer still resolves without it
// (novox/hq ADR 0337 clause 4).
NotGiven []string
}
// Resolve works out everything a node should run, from what was assigned to it (the controller's planFor, plan.go): the
// resolution over the rest of the mesh, the credential of each thing the node takes from elsewhere, and the settings of
// every module that resolved. It makes and writes nothing: a credential not held is composed with a stand-in and named
// foreseen, one only a person gives and nobody has is left out and said, one the send would refuse is refused with the
// controller's words. A failure to compose the node's own modules is an error that satisfies IsUnresolvable, told apart
// from an unreadable Input (novox/hq 04-ISSUES/152).
func Resolve(in Input, node string) (Resolved, error) {
return newComposer(in).planFor(node)
}
// IsUnresolvable reports whether err is a node's own set failing to compose, rather than the mesh being unable to answer.
func IsUnresolvable(err error) bool { return unresolvable(err) }
// planFor is the controller's planFor.
func (c *composer) planFor(nodeName string) (Resolved, error) {
resolved, err := c.resolution(nodeName)
if err != nil {
return Resolved{}, err
}
node, err := c.in.node(nodeName)
if err != nil {
return Resolved{}, err
}
shelf := c.shelfFor(node)
var out Resolved
// The credential for each thing this node takes from elsewhere.
for i, n := range resolved.Needs {
// Issued for this holder alone and kept by the vault (issue 757).
resolved.Needs[i].IssuedToHolder = issuedToHolder(shelf, n)
resolved.Needs[i].ProviderMoved = providerMoved(shelf, n)
if n.ByRecord {
// Answered by something the mesh holds, so there is no pair-wise secret between two machines.
licence, _ := c.licence(n.From)
if licence.Keys == nil {
// The keys are not given: composed with a stand-in and named, not failed for a value only a send reads.
resolved.Needs[i].Sealed = catalogue.ForeseenSealed
out.Foreseen = append(out.Foreseen, fmt.Sprintf("%s on %s needs %s from %s", n.For, nodeName, n.Name, n.From))
} else {
// Given, and none for this holder: the mesh holds none.
resolved.Needs[i].Sealed = licence.Keys[Holder{Node: nodeName, Module: n.For}]
}
// If this holder is the licence's manager, hand it the manager node's PUBLIC sealing key in its bound facts
// (novox/hq ADR 0050).
if pub := c.managerPublicKeyFor(licence, nodeName, n.For); pub != "" {
serves := map[string]any{}
for k, v := range resolved.Needs[i].Serves {
serves[k] = v
}
serves["manager_public_key"] = pub
resolved.Needs[i].Serves = serves
// The manager holder: its empty pre-adoption refresh token is a waiting state, not a missing consumer key.
resolved.Needs[i].Manager = true
}
continue
}
cred, held := c.credential(n, nodeName)
_, outside := issuedOutsideBy(shelf, n)
switch {
case cred.State == CredentialRefused:
// Said rather than skipped. A machine that resolves cleanly and receives no credential is one that will fail to
// authenticate at some later, less obvious moment.
return Resolved{}, fmt.Errorf("%s on %s needs %s from %s and no credential could be made for it: %s",
n.For, nodeName, n.Name, n.From, cred.Why)
case cred.State == CredentialNotGiven, outside && !(held && cred.State == CredentialHeld):
// Not given, and the rest of the consumer runs (ADR 0337 clause 4): no credential file, so the module can tell
// it was not given; nothing the mesh made stands in for it.
why := cred.Why
if why == "" {
why = "nobody gave it"
}
out.NotGiven = append(out.NotGiven, fmt.Sprintf("%s on %s holds no %s from %s: %s", n.For, nodeName, n.Name, n.From, why))
continue
case cred.State == CredentialHeld && cred.Sealed != "":
resolved.Needs[i].Sealed = cred.Sealed
default:
// Held with no value here, or none held: the next send makes it (or reads it), and a stand-in is composed.
resolved.Needs[i].Sealed = catalogue.ForeseenSealed
out.Foreseen = append(out.Foreseen, fmt.Sprintf("%s on %s needs %s from %s", n.For, nodeName, n.Name, n.From))
}
}
// Settings for everything that resolved, including modules nobody assigned directly: a requirement pulled in by
// something else is still configurable.
settings := catalogue.SettingsBy{}
for _, m := range resolved.Modules {
layers := node.Settings[m.Module]
if len(layers) == 0 {
continue
}
settings[m.Module] = append([]catalogue.Layer(nil), layers...)
}
out.Plan, out.Settings = resolved, settings
return out, nil
}
// credential is how the pair credential of one need stands in the Input, and whether the Input names it at all.
func (c *composer) credential(n catalogue.Needed, consumer string) (Credential, bool) {
for _, cr := range c.in.Credentials {
if cr.Provision == n.Name && cr.Consumer == consumer && cr.ConsumerModule == n.For && cr.Provider == n.From && cr.Local == n.Local {
return cr, true
}
}
return Credential{State: CredentialWouldMake}, false
}
// licence is the licence of that name.
func (c *composer) licence(name string) (Licence, bool) {
for _, l := range c.in.Licences {
if l.Name == name {
return l, true
}
}
return Licence{}, false
}
// managerPublicKeyFor is the manager node's public sealing key, but only when (node, module) is the licence's manager holder
// and empty otherwise (plan.go managerPublicKeyFor): delivered to the manager module in its bound facts so it can re-seal a
// rotated refresh token to this node (novox/hq ADR 0050), and to no consumer.
func (c *composer) managerPublicKeyFor(l Licence, node, module string) string {
if l.Manager == (Holder{}) || node != l.Manager.Node || module != l.Manager.Module {
return ""
}
n, err := c.in.node(node)
if err != nil {
return ""
}
return n.SealingKey
}
// issuedOutsideBy is whether a need's provision is offered by its provider with a credential issued outside the mesh
// (novox/hq ADR 0337), and that provider module: the one answering it when known, else any that offers it so.
func issuedOutsideBy(shelf map[string]catalogue.Manifest, n catalogue.Needed) (string, bool) {
if n.Module != "" {
if m, known := shelf[n.Module]; known {
return n.Module, m.IssuedOutsideOf(n.Name)
}
}
return catalogue.OffersIssuedOutside(shelf, n.Name)
}
// issuedToHolder is whether the vault keeps a need's credential for its holder alone (issues 749, 757 and 779).
func issuedToHolder(shelf map[string]catalogue.Manifest, n catalogue.Needed) bool {
if n.ByRecord || n.Module == "" {
return false
}
provider, known := shelf[n.Module]
if !known || !movedProvider(provider) {
return false
}
if _, issued := provider.IssuedCredentialOf(n.Name); issued {
return true
}
return provider.CredentialFormOf(n.Name) == catalogue.CredentialFormPair
}
// providerMoved is whether a need is answered by a module marked moved (issue 788), or by one whose offer of the provision
// says the provider helper answers it (issue 792).
func providerMoved(shelf map[string]catalogue.Manifest, n catalogue.Needed) bool {
if n.ByRecord || n.Module == "" {
return false
}
provider, known := shelf[n.Module]
return known && (movedProvider(provider) || provider.AnsweredByHelperFor(n.Name))
}
// movedProvider is whether a provider's credentials are the vault's: the module is marked moved, or it holds the
// mesh-vault seat (issue 793).
func movedProvider(m catalogue.Manifest) bool {
return m.Moved || slices.ContainsFunc(m.Claims, func(c catalogue.Claim) bool { return c.Name == "mesh-vault" })
}
// resolution works out what one node should run, from what was assigned to it: the controller's planFor up to and
// including the resolution (plan.go planFor), and no further. A failure to compose the node's own modules is wrapped as
// notResolvable; every other failure is returned as it is.
func (c *composer) resolution(nodeName string) (catalogue.Resolution, error) {
node, err := c.in.node(nodeName)
if err != nil {
return catalogue.Resolution{}, err
}
shelf := c.shelfFor(node)
world, _, left, err := c.theRestOfTheMesh(nodeName)
if err != nil {
return catalogue.Resolution{}, err
}
world.Pinned = node.Pins
// Where each of its consumers of a provision that keeps data was last sent (novox/hq ADR 0232).
world.Bound = node.Bound
onNetwork, err := c.whereEveryoneIs()
if err != nil {
return catalogue.Resolution{}, err
}
// What this mesh can answer with a record rather than a machine, and which record each of this node's modules was
// put on.
world.Licences, world.Using = c.licencesFor(nodeName)
resolved, err := catalogue.Resolve(shelf, node.Assigned,
catalogue.Node{Name: nodeName, Site: node.Place.Site, Capabilities: node.Capabilities,
At: onNetwork[nodeName], PublicDomain: node.PublicDomain,
Account: node.Account, AccountHome: node.AccountHome,
AgentAccount: node.AgentAccount, AgentAccountHome: node.AgentAccountHome}, world)
if err != nil {
// What another node's set would have offered is missing when that set does not resolve here: said with the
// refusal, so "nothing provides" is read beside why (issue 1126, probe #532).
if len(left) > 0 {
var why []string
for _, l := range left {
why = append(why, l.String())
}
err = fmt.Errorf("%w\n\nand %d node(s) offer nothing here, as they could not be resolved: %s", err, len(left), strings.Join(why, "; "))
}
if len(c.unhostable) > 0 {
err = fmt.Errorf("%w\n\nand these modules offer nothing here, as their node cannot run them in this composition: %s",
err, strings.Join(c.unhostable, "; "))
}
// The node's own set does not compose: the only failure a mesh-wide gatherer may pass over.
return catalogue.Resolution{}, notResolvable{err}
}
return resolved, nil
}
// shelfFor is the catalogue as one node composes its own set: the Input's, with each recorded or core module the node keeps at the
// build it runs put in place of the current one (plan.go keepRecorded). A copy: the Input's catalogue is untouched. Only the node's own planFor and
// resolution use it; the rest of the mesh and who is on the network are read with the current manifests, as the controller reads them
// before it keeps a recorded module.
func (c *composer) shelfFor(node Node) map[string]catalogue.Manifest {
if len(node.Kept) == 0 {
return c.in.Catalogue
}
shelf := make(map[string]catalogue.Manifest, len(c.in.Catalogue)+len(node.Kept))
for name, m := range c.in.Catalogue {
shelf[name] = m
}
for name, m := range node.Kept {
shelf[name] = m
}
return shelf
}
// licencesFor is what this node can be answered with by record, and what it was put on. A mesh with no licences at all
// is the ordinary case and not an error.
func (c *composer) licencesFor(node string) (map[string][]catalogue.Record, map[string]map[string]catalogue.Record) {
all := c.in.Licences
if len(all) == 0 {
return nil, nil
}
offered := map[string][]catalogue.Record{}
byName := map[string]catalogue.Record{}
for _, one := range all {
record := catalogue.Record{Name: one.Name, Serves: one.Serves}
offered[LicenceProvision] = append(offered[LicenceProvision], record)
byName[one.Name] = record
}
using := map[string]map[string]catalogue.Record{}
for _, one := range all {
for _, h := range one.Holders {
if h.Node != node {
continue
}
if using[h.Module] == nil {
using[h.Module] = map[string]catalogue.Record{}
}
using[h.Module][LicenceProvision] = byName[one.Name]
}
}
return offered, using
}