A candidate answer shows every differing resource, masked; a module folded in the node's own is judged there; the node's own carries its bus membership as it runs it (issue 1126, seed #531)

novox's answer to seed #531 named 15 resources by id alone, so why each differed
was guessed. ModuleDiffers.Bodies keeps every differing resource with both
bodies, each value under a key a secret could be in masked (the same, another
seal of the same value, another value). The five old-form modules read
'removed' because the agent also lists them by module from the declaration kept
before the switch; they are judged inside the node's own. And the node's own
lacked bus-membership, which only the controller composes from its store: the
membership the node runs is carried, until the grants part hands it sealed.
This commit is contained in:
2026-10-28 01:08:49 +01:00
parent 47eb100992
commit b31c4580aa
6 changed files with 303 additions and 6 deletions
+12 -1
View File
@@ -107,7 +107,18 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st
a.Verdict = delivery.VerdictOf(a)
return a
}
cmp := delivery.CompareComposed(composed, running)
// A module the composition folds inside the node's own is judged there: the agent also says it runs by module, from
// the declaration it kept before the switch (agent.Everything), which is no removal (issue 1126, seed #531).
if len(s.InOwn) > 0 {
var judged []delivery.Composed
for _, c := range running {
if c.Module == "" || !slices.Contains(s.InOwn, c.Module) {
judged = append(judged, c)
}
}
running = judged
}
cmp := delivery.CompareComposedWith(composed, running, s.SameValue)
// A refused module was composed as a stub: what it composed is no difference; its refusal is said.
notFailed := func(m string) bool { _, ok := failed[m]; return !ok }
a.Same = slices.DeleteFunc(cmp.Same, func(m string) bool { return !notFailed(m) })
+19
View File
@@ -135,6 +135,25 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
if a.Verdict != delivery.CandidateSame {
t.Errorf("the fold's answer: %+v, want the same", a)
}
// (A) The agent also says spiegel runs by module, from the declaration kept before the switch (agent.Everything): a
// module the composition folds is judged inside the node's own, not as removed (issue 1126, seed #531's answer).
keptToo := append(append([]delivery.Composed{}, running...), delivery.Composed{Node: "n1", Module: "spiegel", Resources: carriedRes})
if a = Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), InOwn: []string{"spiegel"}}, fps, keptToo, "h1", 3); a.Verdict != delivery.CandidateSame || len(a.Removed) != 0 {
t.Errorf("a folded module running from the kept declaration is answered %+v, want the same", a)
}
// (B) The node's own composition carries its bus membership as it runs it (the controller's, from its store): composed
// the same, not 'in what runs, not composed' (seed #531, novox).
membership := json.RawMessage(`{"id":"bus-membership","type":"file","path":"` + catalogue.BusMembershipPath + `","sealed":"{\"box\":\"m\"}","mode":"0600"}`)
withMembership := make([]delivery.Composed, 0, len(running))
for _, c := range running {
if c.Module == "" {
c.Resources = append(append([]json.RawMessage{}, c.Resources...), membership)
}
withMembership = append(withMembership, c)
}
if a = Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), InOwn: []string{"spiegel"}}, fps, withMembership, "h1", 3); a.Verdict != delivery.CandidateSame {
t.Errorf("the node's own with its bus membership is answered %+v, want the same", a)
}
// Without what runs, the fold composes spiegel from its manifest, as before, and the composition refuses it.
if _, _, err := Compose(ctx, b, Mesh{}, Scope{Node: "n1", InOwn: []string{"spiegel"}}, fps); err == nil ||
!strings.Contains(err.Error(), "spiegel has not been built for this mesh") {
+29
View File
@@ -124,6 +124,10 @@ type Scope struct {
Placements map[string]*delivery.Placement
InOwn []string
Running []delivery.Composed
// SameValue, in an answer, says whether two sealed values open to the same value (the node opens them with its own
// key), so a differing resource shows a value sealed again as such (delivery.CompareComposedWith). Nil shows them
// as differing.
SameValue func(a, b string) (same, known bool)
}
// InOwn is, of the modules p places on its node, those whose resources the node's own composition it runs
@@ -296,6 +300,16 @@ func input(ctx context.Context, r Reader, mesh Mesh, s Scope, refuse func(module
if s.Running[i].Module == "" {
own := s.Running[i]
n.RunsOwn = &own
// Its bus membership as it runs it: the controller composes it from its store, which the node does
// not read; until the grants part hands memberships sealed to the node, the node's own composition
// carries the one it runs (issue 1126, seed #531 on novox: "bus-membership in what runs, not
// composed", which applied would have taken the node's bus credential away).
if sealed := membershipOf(own); sealed != "" {
if in.BusMemberships == nil {
in.BusMemberships = map[string]string{}
}
in.BusMemberships[s.Node] = sealed
}
}
}
}
@@ -693,3 +707,18 @@ func storeSought(in compose.Input) string {
return said + "; " + catalogue.ArtifactStoreProvision + " is served by " + strings.Join(where, ", ") +
", none of it on the private network with a port"
}
// membershipOf is the sealed value of the bus membership resource (catalogue.BusMembershipID) of a node's own
// composition, or "".
func membershipOf(own delivery.Composed) string {
for _, raw := range own.Resources {
var r struct {
ID string `json:"id"`
Sealed string `json:"sealed"`
}
if json.Unmarshal(raw, &r) == nil && r.ID == catalogue.BusMembershipID() {
return r.Sealed
}
}
return ""
}
+189 -5
View File
@@ -28,6 +28,13 @@ type Comparison struct {
// resource is equal it says what else differs (their order, the files filled, the data, the node's own parts, a file
// carried by digest).
func CompareComposed(composed map[string]Composed, running []Composed) Comparison {
return CompareComposedWith(composed, running, nil)
}
// CompareComposedWith is CompareComposed showing, for each module that differs, every differing resource with both
// bodies masked (MaskedPair); sameValue, when given, says whether two sealed values open to the same value (the node
// opens them with its own key), so a value sealed again reads as such.
func CompareComposedWith(composed map[string]Composed, running []Composed, sameValue func(a, b string) (same, known bool)) Comparison {
name := func(c Composed) string {
if c.Module == "" {
return HaveNodeWord
@@ -60,7 +67,7 @@ func CompareComposed(composed map[string]Composed, running []Composed) Compariso
case !composedHere:
out.Removed = append(out.Removed, m)
default:
if d, ok := compareOne(m, a, b); ok {
if d, ok := compareOne(m, a, b, sameValue); ok {
out.Same = append(out.Same, m)
} else {
out.Differs = append(out.Differs, d)
@@ -71,7 +78,7 @@ func CompareComposed(composed map[string]Composed, running []Composed) Compariso
}
// compareOne is one module's comparison: true when the digests agree, else what differs first.
func compareOne(module string, composed, running Composed) (ModuleDiffers, bool) {
func compareOne(module string, composed, running Composed, sameValue func(a, b string) (bool, bool)) (ModuleDiffers, bool) {
composed, running = withoutBusUsers(composed), withoutBusUsers(running)
dc, errC := ComposedDigestOf(composed)
dr, errR := ComposedDigestOf(running)
@@ -99,17 +106,35 @@ func compareOne(module string, composed, running Composed) (ModuleDiffers, bool)
for id := range rb {
ids[id] = true
}
var first ModuleDiffers
found := false
var bodies []ResourceBodies
for _, id := range sortedKeys(ids) {
x, inA := ra[id]
y, inB := rb[id]
why := ""
switch {
case !inB:
return differs(id, "composed, and not in what runs")
why = "composed, and not in what runs"
case !inA:
return differs(id, "in what runs, and not composed")
why = "in what runs, and not composed"
case !bytes.Equal(x, y):
return differs(id, "the resource differs")
why = "the resource differs"
default:
continue
}
if !found {
first, _ = differs(id, why)
found = true
}
if len(bodies) < MaxBodies {
c, r := MaskedPair(x, y, sameValue)
bodies = append(bodies, ResourceBodies{ID: id, Why: why, Composed: cut(c), Running: cut(r)})
}
}
if found {
first.Bodies = bodies
return first, false
}
switch {
case !jsonEqual(a.Filled, b.Filled):
@@ -165,3 +190,162 @@ func resourcesByID(resources []json.RawMessage) map[string][]byte {
}
return out
}
// cut is s cut at MaxBody bytes, saying so.
func cut(s string) string {
if len(s) <= MaxBody {
return s
}
return s[:MaxBody] + "… (cut at " + strconv.Itoa(MaxBody) + " bytes)"
}
// secretWords are the words of a key whose value may hold a secret or a sealed one: masked in a shown body.
var secretWords = []string{"sealed", "secret", "password", "passwd", "token", "private", "key", "credential", "jwt", "salt", "hash"}
func secretKey(k string) bool {
k = strings.ToLower(k)
for _, w := range secretWords {
if strings.Contains(k, w) {
return true
}
}
return false
}
// MaskedPair is two bodies of one resource (either may be nil) as JSON text with every value under a key a secret could
// be in (secretWords, at any depth) masked: "<masked: the same on both sides>", "<masked: another seal of the same
// value>" or "<masked: another value>" when sameValue says so, "<masked: differs>" otherwise, "<masked>" on one side
// alone. A body that is not JSON is shown as its length only.
func MaskedPair(composed, running json.RawMessage, sameValue func(a, b string) (same, known bool)) (string, string) {
var a, b any
okA := composed != nil && json.Unmarshal(composed, &a) == nil
okB := running != nil && json.Unmarshal(running, &b) == nil
if okA && okB {
a, b = maskBoth(a, b, sameValue)
} else if okA {
a, _ = maskBoth(a, nil, nil)
} else if okB {
_, b = maskBoth(nil, b, nil)
}
show := func(v any, raw json.RawMessage, ok bool) string {
if raw == nil {
return ""
}
if !ok {
return "<not JSON: " + strconv.Itoa(len(raw)) + " bytes>"
}
var out bytes.Buffer
enc := json.NewEncoder(&out)
enc.SetEscapeHTML(false)
_ = enc.Encode(v)
return strings.TrimSuffix(out.String(), "\n")
}
return show(a, composed, okA), show(b, running, okB)
}
func maskBoth(a, b any, sameValue func(a, b string) (bool, bool)) (any, any) {
ma, isA := a.(map[string]any)
mb, isB := b.(map[string]any)
if isA || isB {
outA, outB := map[string]any{}, map[string]any{}
keys := map[string]bool{}
for k := range ma {
keys[k] = true
}
for k := range mb {
keys[k] = true
}
for k := range keys {
va, inA := ma[k]
vb, inB := mb[k]
if secretKey(k) {
word := maskWord(va, vb, inA && inB, sameValue)
if inA {
outA[k] = word
}
if inB {
outB[k] = word
}
continue
}
x, y := maskBoth(va, vb, sameValue)
if inA {
outA[k] = x
}
if inB {
outB[k] = y
}
}
var ra, rb any
if isA {
ra = outA
} else {
ra = a
}
if isB {
rb = outB
} else {
rb = b
}
return ra, rb
}
la, isLA := a.([]any)
lb, isLB := b.([]any)
if isLA || isLB {
outA := make([]any, len(la))
outB := make([]any, len(lb))
for i := range max(len(la), len(lb)) {
var x, y any
if i < len(la) {
x = la[i]
}
if i < len(lb) {
y = lb[i]
}
x, y = maskBoth(x, y, sameValue)
if i < len(la) {
outA[i] = x
}
if i < len(lb) {
outB[i] = y
}
}
var ra, rb any = a, b
if isLA {
ra = outA
}
if isLB {
rb = outB
}
return ra, rb
}
return a, b
}
func maskWord(a, b any, both bool, sameValue func(a, b string) (bool, bool)) string {
if !both {
return "<masked>"
}
x, _ := json.Marshal(a)
y, _ := json.Marshal(b)
if bytes.Equal(x, y) {
return "<masked: the same on both sides>"
}
if sameValue != nil {
sa, okA := a.(string)
sb, okB := b.(string)
if !okA {
sa = string(x)
}
if !okB {
sb = string(y)
}
if same, known := sameValue(sa, sb); known {
if same {
return "<masked: another seal of the same value>"
}
return "<masked: another value>"
}
}
return "<masked: differs>"
}
+21
View File
@@ -138,6 +138,9 @@ type CandidateAnswer struct {
// the node's wants with what it runs, and the verdict is theirs; a want the controller holds no composition for is
// refused (Refused, without Returns).
Returns string `json:"returns,omitempty"`
// Note is what the node says of how it answered, in words, beside the verdict: a pass that ran over a minute, inputs
// it waited for (issue 1126: the shadow shows its work). It changes no verdict.
Note string `json:"note,omitempty"`
}
// ReturnsToController is what a node answers a head that deletes its placement file.
@@ -152,8 +155,26 @@ type ModuleDiffers struct {
Running string `json:"running,omitempty"`
Resource string `json:"resource,omitempty"`
Why string `json:"why"`
// Bodies is every resource that differs, both sides, each value a secret could be in masked (issue 1126: the shadow
// shows its work, so why a module differs is read, not guessed). At most MaxBodies, each side cut at MaxBody bytes.
Bodies []ResourceBodies `json:"bodies,omitempty"`
}
// ResourceBodies is one differing resource of a module: its id, why, and its body composed and as it runs, masked
// (MaskedPair); a side that does not hold it is empty.
type ResourceBodies struct {
ID string `json:"id"`
Why string `json:"why"`
Composed string `json:"composed,omitempty"`
Running string `json:"running,omitempty"`
}
// The bounds of a module's shown bodies.
const (
MaxBodies = 20
MaxBody = 8192
)
// ModuleAdded is one module the candidate places on the node and the node does not run: its composition's digest when
// it composed, and the values not yet issued for it (its own secrets, its grants, a host port), each in the words the
// composition stood in for it with.
+33
View File
@@ -283,6 +283,39 @@ func TestPlacement(t *testing.T) {
t.Errorf("%s: differs %+v, want %s: %s with both digests", name, c.Differs, tc.resource, tc.why)
}
}
// Issue 1126, the shadow shows its work: every differing resource with both bodies, each value a secret could be
// in masked (never a secret in an answer), saying whether the two sides hold the same value.
sealedFile := func(id, sealed, mode string) json.RawMessage {
return json.RawMessage(`{"id":"` + id + `","type":"file","module":"nats","path":"/etc/` + id + `","mode":"` + mode + `","sealed":"` + sealed + `"}`)
}
shown := CompareComposedWith(map[string]Composed{"k": comp(sealedFile("a", "box-1", "0600"), sealedFile("b", "box-2", "0600"), file("c", "x"))},
[]Composed{comp(sealedFile("a", "box-1", "0644"), sealedFile("b", "box-3", "0600"), file("c", "y"))},
func(a, b string) (same, known bool) { return a == "box-2" && b == "box-3", true })
if len(shown.Differs) != 1 || len(shown.Differs[0].Bodies) != 3 {
t.Fatalf("every differing resource is not shown: %+v", shown.Differs)
}
for _, row := range []struct{ id, has, hasNot string }{
{"a", `"mode":"0600"`, "box-1"},
{"b", "another seal of the same value", "box-2"},
{"c", `"content":"x"`, ""},
} {
var body ResourceBodies
for _, b := range shown.Differs[0].Bodies {
if b.ID == row.id {
body = b
}
}
both := body.Composed + body.Running
if !strings.Contains(both, row.has) || (row.hasNot != "" && strings.Contains(both, row.hasNot)) {
t.Errorf("%s shown as %+v", row.id, body)
}
}
if c, r := MaskedPair(json.RawMessage(`{"env":{"DB_PASSWORD":"hunter2","PORT":"80"},"list":[{"token":"t1"}]}`),
json.RawMessage(`{"env":{"DB_PASSWORD":"hunter3","PORT":"81"},"list":[{"token":"t1"}]}`), nil); strings.Contains(c+r, "hunter") ||
strings.Contains(c+r, `"t1"`) || !strings.Contains(r, `"PORT":"81"`) || !strings.Contains(c, "<masked: differs>") ||
!strings.Contains(c, "<masked: the same on both sides>") {
t.Errorf("masked as %s and %s", c, r)
}
if c := CompareComposed(nil, []Composed{{Node: "novox", Resources: []json.RawMessage{}}}); len(c.Removed) != 1 || c.Removed[0] != "node" {
t.Errorf("the node's own composition run and not composed is removed: %+v", c)
}