diff --git a/cmd/mesh-host/nodeagent.go b/cmd/mesh-host/nodeagent.go index 960df22..689a6be 100644 --- a/cmd/mesh-host/nodeagent.go +++ b/cmd/mesh-host/nodeagent.go @@ -814,5 +814,7 @@ func selfComposer(ctx context.Context, session *link.Session, node string, say f // Its modules' own secrets, sealed by the vault to this machine's key (ADR 0550 §5). Own: func(ctx context.Context, args map[string]any) (json.RawMessage, error) { return session.AskVault(ctx, "own", args) - }}) + }, + // And the vault's word that a secret changed, on which it composes again (ADR 0547 §2). + Hear: session.Hear}) } diff --git a/go.mod b/go.mod index b4f5a66..f6ee935 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/novox/mesh-host go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026182306-944a35d200c9 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026182519-53c2def1a520 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 golang.org/x/crypto v0.57.0 diff --git a/go.sum b/go.sum index 3628a34..9c8a6f7 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026181846-91413de33516 h1:xbbss6kQ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026181846-91413de33516/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026182306-944a35d200c9 h1:ls5Jwxw4CZ9aPVhjQm9/qM88erjUU0ENRWajhD7lfb0= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026182306-944a35d200c9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026182519-53c2def1a520 h1:twiziR88X6ttKmw5JTd3aW6xb5bMZZ3Bl7WS2QOP3PE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026182519-53c2def1a520/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/internal/selfcompose/selfcompose.go b/internal/selfcompose/selfcompose.go index 843ad70..133ceb0 100644 --- a/internal/selfcompose/selfcompose.go +++ b/internal/selfcompose/selfcompose.go @@ -42,8 +42,14 @@ type Env struct { // Own asks the vault's `own` in this node's engine's name, with the arguments given: its modules' own secrets and the // pair credentials asked, sealed to its key (ADR 0550 §5). Nil asks nothing, and a module with either does not compose. Own func(ctx context.Context, args map[string]any) (json.RawMessage, error) + // Hear subscribes a subject: here the vault's word that a secret changed (VaultSecretChanged), on which the node + // composes again, so a rotated secret or credential reaches it at once (ADR 0547 §2). Nil hears nothing. + Hear func(subject string) (<-chan []byte, func(), error) } +// VaultSecretChanged is the vault's event that a secret changed: a name and its generation, never a value. +const VaultSecretChanged = "mesh.mod.mesh-vault.event.secret-changed" + // Composer composes the node and is the agent's watcher of its compositions (agent.Env.Composed). type Composer struct { env Env @@ -128,6 +134,30 @@ func (c *Composer) Watch(ctx context.Context, _ string) (<-chan agent.Update, er } }(i, ch) } + if c.env.Hear != nil { + heard, stop, err := c.env.Hear(VaultSecretChanged) + if err != nil { + return nil, fmt.Errorf("the node composes again when a secret changes, and cannot hear the vault say so: %w", err) + } + go func() { + defer stop() + for { + select { + case <-ctx.Done(): + return + case _, ok := <-heard: + if !ok { + return + } + select { + case merged <- tagged{in: -1, ok: true}: + case <-ctx.Done(): + return + } + } + } + }() + } out := make(chan agent.Update, 64) go func() { defer close(out) @@ -144,9 +174,12 @@ func (c *Composer) Watch(ctx context.Context, _ string) (<-chan agent.Update, er c.env.Say(fmt.Sprintf("the watch of %s ended; the node composes no more until its agent starts again", ins[t.in].bucket)) return } - if t.u.Caught { + switch { + case t.in < 0: + dirty = true // a secret changed: its new generation is asked of the vault at the compose + case t.u.Caught: caught[t.in] = true - } else { + default: ins[t.in].take(c, t.u) dirty = true } diff --git a/internal/selfcompose/selfcompose_test.go b/internal/selfcompose/selfcompose_test.go index 74a6d3a..c792cb2 100644 --- a/internal/selfcompose/selfcompose_test.go +++ b/internal/selfcompose/selfcompose_test.go @@ -97,6 +97,13 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { 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 } + secretChanged := make(chan []byte, 1) + env.Hear = func(subject string) (<-chan []byte, func(), error) { + if subject != VaultSecretChanged { + t.Errorf("heard %s", subject) + } + return secretChanged, func() {}, nil + } c := New(env) out, err := c.Watch(ctx, "n1.>") if err != nil { @@ -205,7 +212,8 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { b.mu.Lock() generation++ b.mu.Unlock() - b.put(t, ResolvedBucket, "n1.other.k", []byte(`{"value":3}`)) + // 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) }