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
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:
@@ -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.
|
||||
|
||||
@@ -23,12 +23,12 @@ type CredentialKey struct {
|
||||
// in neither list: the vault mints nothing for it (issue 1126).
|
||||
func CredentialsNeeded(in Input, node string) (asConsumer, asProvider []CredentialKey, err error) {
|
||||
c := newComposer(in)
|
||||
resolved, _, err := c.resolutionHolding(node)
|
||||
resolved, held, err := c.resolutionHolding(node)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, n := range resolved.Needs {
|
||||
if n.ByRecord || n.From == "" || credentialless(n) {
|
||||
if _, isHeld := held[n.For]; isHeld || n.ByRecord || n.From == "" || credentialless(n) {
|
||||
continue
|
||||
}
|
||||
asConsumer = append(asConsumer, CredentialKey{Provision: n.Name, Consumer: node, ConsumerModule: n.For,
|
||||
|
||||
@@ -337,7 +337,8 @@ func composeEach(in compose.Input, node string, fps map[string]string, applied b
|
||||
hold(m, failed[m], "")
|
||||
}
|
||||
for {
|
||||
out, skipped, stood, err = compose.CompositionsIn(in, node, fps, mode)
|
||||
var leftOut map[string]string
|
||||
out, skipped, stood, leftOut, err = compose.CompositionsHolding(in, node, fps, mode)
|
||||
if err != nil {
|
||||
module, leave := culprit(in, node, fps, mode, failed, err)
|
||||
if module == "" {
|
||||
@@ -346,6 +347,18 @@ func composeEach(in compose.Input, node string, fps map[string]string, applied b
|
||||
hold(module, err.Error(), leave)
|
||||
continue
|
||||
}
|
||||
// A module the node leaves out because its claim cannot be held or its credential was refused (issue 1966) is held with
|
||||
// those words: its handed composition stays and the check names it refused; the rest composes.
|
||||
heldNow := false
|
||||
for _, m := range sortedKeys(leftOut) {
|
||||
if _, done := failed[m]; !done {
|
||||
hold(m, leftOut[m], "")
|
||||
heldNow = true
|
||||
}
|
||||
}
|
||||
if heldNow {
|
||||
continue
|
||||
}
|
||||
again := false
|
||||
if applied && len(stood) > 0 {
|
||||
placed := map[string]bool{}
|
||||
|
||||
@@ -300,3 +300,35 @@ func aHeldModuleOfItsOwnAccountNeverRefusesTheNode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 1966: two modules claiming one seat held once cost the second claimant, not the node: Compose holds it with the claim's
|
||||
// words, composes the first, and (the node-engine keeping what it handed of a held module) leaves its composition out, whether or
|
||||
// not it runs.
|
||||
func TestASecondClaimantOfASeatIsHeldAndTheNodeComposesTheRest(t *testing.T) {
|
||||
first := `{"module":"alpha","version":"1","claims":[{"name":"the-seat","scope":"node"}]}`
|
||||
second := `{"module":"beta","version":"1","claims":[{"name":"the-seat","scope":"node"}]}`
|
||||
for _, running := range []bool{false, true} {
|
||||
b, fps := aNode(t, first, second)
|
||||
scope := Scope{Node: "n1"}
|
||||
if running {
|
||||
scope.Running = []engcomp.Composed{{Node: "n1", Module: "beta", FP: fps["beta"], Resources: nil}}
|
||||
}
|
||||
got, _, held, err := Compose(context.Background(), b, Mesh{}, scope, fps)
|
||||
if err != nil || len(held) != 1 || !strings.Contains(held["beta"], "alpha and beta both claim") {
|
||||
t.Fatalf("running %v: err %v, held %v; want beta held with the claim's words", running, err, held)
|
||||
}
|
||||
for _, c := range got {
|
||||
if c.Module == "beta" {
|
||||
t.Errorf("running %v: the held claimant is composed: %+v", running, c)
|
||||
}
|
||||
}
|
||||
// A check that adds the second claimant is refused for it, in the claim's words, and not judged to compose.
|
||||
p := b.assignments["n1"]
|
||||
ans := Answer(context.Background(), b, Mesh{}, Scope{Node: "n1", Assignments: map[string]*meshmoduleassigner.Assignments{"n1": &p}},
|
||||
fps, scope.Running, "p-h1", 1)
|
||||
if ans.Verdict != nodeengine.CandidateAnswerVerdictRefused || len(ans.Refusals) != 1 || ans.Refusals[0].Module != "beta" ||
|
||||
!strings.Contains(ans.Refusals[0].Why, "both claim") {
|
||||
t.Errorf("running %v: the answer is %+v, want beta refused with the claim's words", running, ans)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@ type Declared struct {
|
||||
// ADR 0163, rule 6), sorted; LeftOutWhy is why each was.
|
||||
LeftOut []string
|
||||
LeftOutWhy map[string]string
|
||||
// Held is, of those, each module left out because its claim cannot be held or its credential was refused, and why (issue
|
||||
// 1966): the node keeps what runs of it and the composer says it held. Not on the wire.
|
||||
Held map[string]string
|
||||
|
||||
// Not on the wire:
|
||||
Received map[string]map[string][]catalogue.Contribution
|
||||
@@ -199,7 +202,7 @@ func (c *composer) declaration(nodeName string, mode Mode) (Declared, error) {
|
||||
if err != nil {
|
||||
return Declared{}, err
|
||||
}
|
||||
out := Declared{Resources: composed.Resources, Adoption: adoptionFor(plan, node, composed),
|
||||
out := Declared{Held: resolved.Held, Resources: composed.Resources, Adoption: adoptionFor(plan, node, composed),
|
||||
Received: composed.Received, Mesh: with.Mesh, BusUsers: with.BusUsers,
|
||||
LeftOut: sortedKeysOf(composed.LeftOut), LeftOutWhy: composed.LeftOut,
|
||||
Withheld: rendered.Withheld, Unbound: rendered.Unbound,
|
||||
|
||||
@@ -26,26 +26,17 @@ func (r *Refusal) Error() string {
|
||||
return "these assignments cannot be applied:\n - " + strings.Join(r.Problems, "\n - ")
|
||||
}
|
||||
|
||||
// Resolve works out everything a node runs, from what was assigned to it.
|
||||
//
|
||||
// The catalogue is every module the mesh knows about; assigned is what a person put on this node.
|
||||
// What comes back is the closure — assigned modules plus everything they require — or a refusal
|
||||
// naming every reason it could not be closed.
|
||||
func Resolve(catalogue map[string]mcat.Manifest, assigned []string, node mcat.Node, world mcat.World) (mcat.Resolution, error) {
|
||||
resolution, _, err := resolve(catalogue, assigned, node, world, true)
|
||||
return resolution, err
|
||||
}
|
||||
|
||||
// ResolveHolding is Resolve where a claim problem costs the module that has it, not the machine (issue 1966, ADR 0636 §3: a
|
||||
// refused claim blocks that claimant only). The modules that cannot hold what they claim come back with why, each still in
|
||||
// ResolveHolding works out everything a node runs, from what was assigned to it. The catalogue is every module the mesh knows
|
||||
// about; assigned is what a person put on this node. What comes back is the closure -- assigned modules plus everything they
|
||||
// require -- or a refusal naming every reason it could not be closed. A claim problem costs the module that has it, not the
|
||||
// machine (issue 1966, ADR 0636 §3: a refused claim blocks that claimant only). The modules that cannot hold what they claim come back with why, each still in
|
||||
// the resolution's modules and none of them in its claims; the caller leaves them out of what it declares (LeftOut), so the
|
||||
// machine composes everything else. Every other refusal is Resolve's.
|
||||
func ResolveHolding(catalogue map[string]mcat.Manifest, assigned []string, node mcat.Node, world mcat.World) (mcat.Resolution, map[string]string, error) {
|
||||
return resolve(catalogue, assigned, node, world, false)
|
||||
return resolve(catalogue, assigned, node, world)
|
||||
}
|
||||
|
||||
// resolve is Resolve; with strict, a claim problem is a refusal beside every other, and without it a module held.
|
||||
func resolve(catalogue map[string]mcat.Manifest, assigned []string, node mcat.Node, world mcat.World, strict bool) (mcat.Resolution, map[string]string, error) {
|
||||
func resolve(catalogue map[string]mcat.Manifest, assigned []string, node mcat.Node, world mcat.World) (mcat.Resolution, map[string]string, error) {
|
||||
elsewhere := world.Held
|
||||
var problems []string
|
||||
|
||||
@@ -608,12 +599,6 @@ func resolve(catalogue map[string]mcat.Manifest, assigned []string, node mcat.No
|
||||
|
||||
problems = append(problems, checkCapabilities(resolution.Modules, node)...)
|
||||
claims, held := holdClaims(resolution.Modules, node, elsewhere, world.Holdings)
|
||||
if strict {
|
||||
for _, why := range held {
|
||||
problems = append(problems, why)
|
||||
}
|
||||
held = nil
|
||||
}
|
||||
problems = append(problems, checkResources(resolution.Modules)...)
|
||||
resolution.Claims = claims
|
||||
|
||||
@@ -634,6 +619,10 @@ func resolve(catalogue map[string]mcat.Manifest, assigned []string, node mcat.No
|
||||
}
|
||||
|
||||
if len(problems) > 0 {
|
||||
// A pass refused anyway says every reason at once, a held module's among them.
|
||||
for _, why := range held {
|
||||
problems = append(problems, why)
|
||||
}
|
||||
sort.Strings(problems)
|
||||
return mcat.Resolution{}, nil, &Refusal{Problems: problems}
|
||||
}
|
||||
@@ -691,21 +680,8 @@ func checkCapabilities(modules []mcat.Manifest, node mcat.Node) []string {
|
||||
return problems
|
||||
}
|
||||
|
||||
// checkClaims refuses two modules holding one singular thing.
|
||||
//
|
||||
// Within this node's own set, and against what is already held elsewhere for the wider scopes. A
|
||||
// claim at mesh scope is the same idea as the mesh's one hub, said once instead of hard-coded.
|
||||
func checkClaims(modules []mcat.Manifest, node mcat.Node, elsewhere []mcat.Held, holdings []mcat.Held) ([]mcat.Held, []string) {
|
||||
held, problems := claimsOf(modules, node, elsewhere, holdings)
|
||||
var out []string
|
||||
for _, p := range problems {
|
||||
out = append(out, p.why)
|
||||
}
|
||||
return held, out
|
||||
}
|
||||
|
||||
// holdClaims is checkClaims with each problem put on the module that has it: the claims that hold, and for each module that
|
||||
// cannot hold what it claims, why (every reason, joined).
|
||||
// holdClaims refuses two modules holding one singular thing, within this node's own set and against what is held elsewhere for
|
||||
// the wider scopes: the claims that hold, and for each module that cannot hold what it claims, why (every reason, joined).
|
||||
func holdClaims(modules []mcat.Manifest, node mcat.Node, elsewhere []mcat.Held, holdings []mcat.Held) ([]mcat.Held, map[string]string) {
|
||||
held, problems := claimsOf(modules, node, elsewhere, holdings)
|
||||
var out map[string]string
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package resolution
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// Resolve is ResolveHolding for a test that wants a claim problem as a refusal of the whole set, as before issue 1966.
|
||||
func Resolve(catalogue map[string]mcat.Manifest, assigned []string, node mcat.Node, world mcat.World) (mcat.Resolution, error) {
|
||||
got, held, err := ResolveHolding(catalogue, assigned, node, world)
|
||||
if err != nil {
|
||||
return mcat.Resolution{}, err
|
||||
}
|
||||
if len(held) > 0 {
|
||||
var problems []string
|
||||
for _, why := range held {
|
||||
problems = append(problems, why)
|
||||
}
|
||||
sort.Strings(problems)
|
||||
return mcat.Resolution{}, &Refusal{Problems: problems}
|
||||
}
|
||||
return got, nil
|
||||
}
|
||||
|
||||
// checkClaims is holdClaims with the problems as a list.
|
||||
func checkClaims(modules []mcat.Manifest, node mcat.Node, elsewhere []mcat.Held, holdings []mcat.Held) ([]mcat.Held, []string) {
|
||||
held, why := holdClaims(modules, node, elsewhere, holdings)
|
||||
var problems []string
|
||||
for _, w := range why {
|
||||
problems = append(problems, w)
|
||||
}
|
||||
sort.Strings(problems)
|
||||
return held, problems
|
||||
}
|
||||
Reference in New Issue
Block a user