Review of L2, round 2: a refusal at the first compose after a start keeps what the module's have said runs (1); each have of a self-composing node carries the values its composition holds sealed, and the composer reuses them after a restart, so a rotated secret moves no composition at a restart (2); pin mesh-sdk dd9d9d5 (issue 1126, ADR 0547 §3)
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 4476e5808047 on ff9386dcccc2 | novox/mesh-host#149 in group feat/1126-nodes-compose

This commit is contained in:
2026-10-26 20:23:16 +01:00
parent 58de6aa10a
commit 4476e58080
9 changed files with 255 additions and 72 deletions
+52 -2
View File
@@ -90,10 +90,12 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
env := b.env()
// The vault's own: keeper's password sealed to n1's key, sealed again (other bytes) at every ask.
var asks, generation int
sealedAt := map[string]int{} // each box handed, by the generation it opens to
env.Own = func(context.Context, map[string]any) (json.RawMessage, error) {
b.mu.Lock()
defer b.mu.Unlock()
asks++
sealedAt[fmt.Sprintf(`{"ask":%d}`, asks)] = generation + 1
return json.RawMessage(fmt.Sprintf(`{"node":"n1","key":"sk","own":[{"module":"keeper","name":"password","origin":"made",`+
`"generation":%d,"sealed":{"ask":%d}}]}`, generation+1, asks)), nil
}
@@ -207,6 +209,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
if !strings.Contains(handed, `{\"ask\":`) {
t.Fatalf("keeper's composition does not carry the sealed value: %s", handed)
}
before := handed
b.put(t, ResolvedBucket, "n1.other.k", []byte(`{"value":2}`))
quiet(t, out)
b.mu.Lock()
@@ -214,8 +217,55 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
b.mu.Unlock()
// The vault says the secret changed: the node composes again on that word, with no other input moving.
secretChanged <- []byte(`{"name":"own/n1/keeper/password","generation":2}`)
if u := next(t, out); u.Key != keeperKey {
t.Fatalf("a new generation handed %s, want keeper again", u.Key)
rotated := next(t, out)
if rotated.Key != keeperKey {
t.Fatalf("a new generation handed %s, want keeper again", rotated.Key)
}
// Review of L2 (round 2): rotated, then the node-engine restarts. The agent has applied nothing yet, so the box the
// node runs comes from keeper's have (Held); the vault seals the value again, other bytes, and the box the have holds
// is kept, since it opens to the same value: the composition does not move. The box from before the rotation opens
// to the old value, so the vault's is taken.
boxOf := func(raw string) string {
c, err := delivery.DecodeComposed([]byte(raw))
if err != nil {
t.Fatal(err)
}
sealed := agent.SealedIn(c)
if len(sealed) != 1 {
t.Fatalf("keeper's composition carries %v", sealed)
}
return sealed[0]
}
for _, row := range []struct {
name, held string
kept bool
}{{"the have holds the rotated value", boxOf(string(rotated.Value)), true}, {"the have holds the value before the rotation", boxOf(before), false}} {
restarted := env
restarted.Running = func() []delivery.Composed { return nil }
restarted.Held = func(module string) []string {
if module == "keeper" {
return []string{row.held}
}
return nil
}
restarted.Unseal = func(box string) ([]byte, error) {
b.mu.Lock()
defer b.mu.Unlock()
if g, ok := sealedAt[box]; ok {
return []byte(fmt.Sprintf("password-%d", g)), nil
}
return nil, fmt.Errorf("not sealed to n1: %s", box)
}
again := New(restarted)
again.wants = map[string]delivery.Want{"keeper": {FP: fp, Self: true}}
rows, err := reader{c: again}.OwnSecrets(ctx, "n1")
if err != nil || len(rows) != 1 {
t.Fatalf("%s: own secrets %+v, %v", row.name, rows, err)
}
if kept := rows[0].Sealed == row.held; kept != row.kept {
t.Errorf("%s: composed with %s (held %s), kept %v, want %v", row.name, rows[0].Sealed, row.held, kept, row.kept)
}
}
// The pair credentials asked (ADR 0550 §5): the keys go to the vault's own with their role, a consumer's comes back as