The node-engine's live path holds a module whose claim or credential fails (issue 1966, review round 1)
mesh/delivery merged: source.main moved to novox/mesh-host@6e461bd24aa5
mesh/check being carried to ef41fa2e2796 on 6e461bd24aa5 | from 7c483da93a03

compose.CompositionsHolding returns the modules the declaration leaves out for a claim that cannot be held or a refused
credential; frombus composeEach holds each with those words, so the node-engine keeps what it handed of the module (no
stop of a running module), the candidate answer and the check's sandbox name it refused, and the rest composes.
CredentialsNeeded asks nothing for a held module. Resolve and checkClaims move to a test helper (production uses
ResolveHolding and holdClaims); a pass refused anyway names a held module's reason among the others.

Quick fix: Declared.Held read in the composer only. Thorough, chosen: the same held words reach every reader of the
composition (selfcompose, ComposeSent, the candidate answer) through composeEach's one hold path.

Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5
This commit is contained in:
2026-11-09 13:04:55 +01:00
parent 868ee6beb0
commit ef41fa2e27
7 changed files with 125 additions and 57 deletions
+26 -17
View File
@@ -28,7 +28,7 @@ import (
// writer folds them (issue 1126). Each module's Data says whether its manifest declares data. The compositions are unsigned
// (ADR 0593 §6) and not yet by digest (Composed.ByDigest).
func Compositions(in Input, node string, fps map[string]string, mode Mode) (out map[string]engcomp.Composed, skipped []string, err error) {
out, skipped, _, err = compositions(in, node, fps, mode, false)
out, skipped, _, _, err = compositions(in, node, fps, mode, false)
return out, skipped, err
}
@@ -45,7 +45,7 @@ const BusUsersStandIn = "the bus's user list"
// ErrStoodIn, naming each, so a stand-in is never applied as if it were the value; but for the bus's user list
// (BusUsersStandIn), which the bus node holds out of every apply of a composition it made itself.
func CompositionsApplied(in Input, node string, fps map[string]string) (map[string]engcomp.Composed, []string, error) {
out, skipped, _, err := compositions(in, node, fps, Reading, true)
out, skipped, _, _, err := compositions(in, node, fps, Reading, true)
return out, skipped, err
}
@@ -56,13 +56,22 @@ func CompositionsApplied(in Input, node string, fps map[string]string) (map[stri
// module the candidate adds, or one missing for a module the node runs. A module whose every value is held composes
// as CompositionsApplied composes it: the modes differ only in an own secret never issued.
func CompositionsShadow(in Input, node string, fps map[string]string) (out map[string]engcomp.Composed, skipped, stood []string, err error) {
return compositions(in, node, fps, Foreseeing, false)
out, skipped, stood, _, err = compositions(in, node, fps, Foreseeing, false)
return out, skipped, stood, err
}
// CompositionsIn is the node's compositions in mode with what they stand in for answered (stood, sorted, the bus's user
// list left out) rather than refused: CompositionsShadow in Foreseeing, and in Reading what CompositionsApplied refuses
// before it is refused, so a caller that applies refuses each stand-in to the module it is for alone (issue 1126).
func CompositionsIn(in Input, node string, fps map[string]string, mode Mode) (out map[string]engcomp.Composed, skipped, stood []string, err error) {
out, skipped, stood, _, err = compositions(in, node, fps, mode, false)
return out, skipped, stood, err
}
// CompositionsHolding is CompositionsIn with the modules the node leaves out as they run, each with why: one whose claim cannot be
// held or whose credential was refused (issue 1966). Their compositions are not in out, so a caller that applies keeps what it last
// applied of each and says it held, as for any module refused alone.
func CompositionsHolding(in Input, node string, fps map[string]string, mode Mode) (out map[string]engcomp.Composed, skipped, stood []string, held map[string]string, err error) {
return compositions(in, node, fps, mode, false)
}
@@ -75,7 +84,7 @@ func Folds(inOwn bool, fp string) bool {
return inOwn && !strings.HasPrefix(fp, meshcatalogclient.SourcePrefix2)
}
func compositions(in Input, node string, fps map[string]string, mode Mode, applied bool) (out map[string]engcomp.Composed, skipped, stood []string, err error) {
func compositions(in Input, node string, fps map[string]string, mode Mode, applied bool) (out map[string]engcomp.Composed, skipped, stood []string, heldBy map[string]string, err error) {
reading := in
reading.Nodes = append([]Node(nil), in.Nodes...)
found := false
@@ -99,7 +108,7 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli
return module != "" && Folds(inOwn[module], fps[module])
}
if !found {
return nil, nil, nil, fmt.Errorf("compose: the mesh has no node %q", node)
return nil, nil, nil, nil, fmt.Errorf("compose: the mesh has no node %q", node)
}
// A folded module built by a build of the old form (Node.RunsOwn) is left to the node: composed from what runs of it
// (Carried), so what it provides and requires still shapes the rest and what the others read of it (its published
@@ -167,10 +176,10 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli
}
d, problems, err := ComposeIn(reading, node, mode)
if err != nil {
return nil, nil, nil, err
return nil, nil, nil, nil, err
}
if len(problems) > 0 {
return nil, nil, nil, fmt.Errorf("compose: %s's declaration is refused by the node-engine's validator: %v", node, problems)
return nil, nil, nil, nil, fmt.Errorf("compose: %s's declaration is refused by the node-engine's validator: %v", node, problems)
}
for _, x := range d.StoodIn {
// The bus's user list is the bus node's own to follow, written alone and never from a composition the node
@@ -186,11 +195,11 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli
sort.Strings(stood)
stood = slices.Compact(stood) // an own secret is both stood in for and foreseen
if applied && len(stood) > 0 {
return nil, nil, nil, fmt.Errorf("%w on %s: %s", ErrStoodIn, node, strings.Join(stood, ", "))
return nil, nil, nil, nil, fmt.Errorf("%w on %s: %s", ErrStoodIn, node, strings.Join(stood, ", "))
}
body, err := d.Body()
if err != nil {
return nil, nil, nil, err
return nil, nil, nil, nil, err
}
var envelope struct {
Resources []json.RawMessage `json:"resources"`
@@ -199,7 +208,7 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli
Provisioner string `json:"provisioner_account"`
}
if err := json.Unmarshal(body, &envelope); err != nil {
return nil, nil, nil, err
return nil, nil, nil, nil, err
}
var kept []json.RawMessage
keptPaths := map[string]bool{}
@@ -213,7 +222,7 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli
Path string `json:"path"`
}
if err := json.Unmarshal(raw, &head); err != nil {
return nil, nil, nil, err
return nil, nil, nil, nil, err
}
if isCarried(head.Module, head.ID) {
continue // what the stub composed of it: the node's own carries what runs of it instead
@@ -223,11 +232,11 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli
// compositionsOf): the node's own composition is then the controller's, byte for byte.
var fields map[string]json.RawMessage
if err := json.Unmarshal(raw, &fields); err != nil {
return nil, nil, nil, err
return nil, nil, nil, nil, err
}
delete(fields, "module")
if raw, err = json.Marshal(fields); err != nil {
return nil, nil, nil, err
return nil, nil, nil, nil, err
}
if head.Type == string(declaration.TypeFile) {
foldedPaths[head.Path] = true
@@ -248,7 +257,7 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli
if len(carried) > 0 {
var err error
if kept, foldedPaths, err = carry(kept, foldedPaths, runsOwn, isCarried); err != nil {
return nil, nil, nil, err
return nil, nil, nil, nil, err
}
for _, p := range runsOwn.Filled {
if foldedPaths[p] && !slices.Contains(envelope.Filled, p) {
@@ -270,12 +279,12 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli
out, err = engcomp.Split(node, kept, fps, filled,
engcomp.NodeOwn{Adoption: envelope.Adoption, Provisioner: envelope.Provisioner})
if err != nil {
return nil, nil, nil, err
return nil, nil, nil, nil, err
}
if len(ownFilled) > 0 {
own, err := engcomp.NodeKey(node)
if err != nil {
return nil, nil, nil, err
return nil, nil, nil, nil, err
}
c := out[own]
c.Filled = append(c.Filled, ownFilled...)
@@ -295,7 +304,7 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli
skipped = append(skipped, m)
}
sort.Strings(skipped)
return out, skipped, stood, nil
return out, skipped, stood, d.Held, nil
}
// RunsAnyOf says runs (a composition as it runs) holds a resource of module, by its module or the module its id names.