From 68dd69f4ef6c8ae79cb7251bffaa57f18435e33d Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 11:59:35 +0100 Subject: [PATCH] CAT-N9: the old remove tools refuse an entry of the new form, naming nats_drop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mesh deletes a module's kept data only through drop and recreate (ADR 0595 §4 as its insight has it; design 25 § 12.3): nats_stream_remove, nats_bucket_remove and nats_consumer_remove read the keeper's own state asked and refuse a stream or bucket the keeper made, before the bus is reached, naming nats_drop and nats_recreate; one that cannot be told apart is not removed. The old sections' entries are removed as before. Rows of the existing refusal test. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- modules/nats/cmd/nats-tools/main.go | 2 +- modules/nats/cmd/nats-tools/removal.go | 39 +++++++++++++++++++++-- modules/nats/cmd/nats-tools/tools_test.go | 20 +++++++++++- 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/modules/nats/cmd/nats-tools/main.go b/modules/nats/cmd/nats-tools/main.go index f5b56028..8f79d0ff 100644 --- a/modules/nats/cmd/nats-tools/main.go +++ b/modules/nats/cmd/nats-tools/main.go @@ -179,7 +179,7 @@ func allTools() []stdio.Tool { defer cancel() return monitor(ctx, "/jsz?accounts=1&streams=1&config=1") } - out := append(tools(monitor, ReadUsers, UsersWritten), removalTools(LiveBus(), SaidByTheRuntime)...) + out := append(tools(monitor, ReadUsers, UsersWritten), removalTools(LiveBus(), SaidByTheRuntime, HeldInState)...) out = append(out, expiryTools(LiveBus(), SaidByTheRuntime, RecordedBySeat)...) out = append(out, brokerTools(HeldInState, jsz, StreamHistory(LiveBus()))...) return append(out, requestTools(RequestsInState())...) diff --git a/modules/nats/cmd/nats-tools/removal.go b/modules/nats/cmd/nats-tools/removal.go index 21888b0f..2d4b804a 100644 --- a/modules/nats/cmd/nats-tools/removal.go +++ b/modules/nats/cmd/nats-tools/removal.go @@ -333,9 +333,23 @@ func RemoveStream(ctx context.Context, bus Bus, said Said, a map[string]any) (an } // removalTools are the module's three tools that change the bus; every other tool here reads. -func removalTools(bus Bus, said Said) []stdio.Tool { +// removalTools are the three removal tools. **They refuse an entry of the new form** (design 25 § 12.3; ADR 0595 §4 as +// its insight of 2026-11-03 has it): the mesh deletes a module's kept data only through the bus module's drop and +// recreate, so a stream, a bucket or a consumer of a stream the keeper made (its state asked, read through newForm) is +// refused, naming nats_drop, before the bus is reached; the old sections' entries are removed as before. A newForm +// that cannot be read refuses too: what cannot be told apart is not removed. +func removalTools(bus Bus, said Said, newForm heldEntries) []stdio.Tool { run := func(f func(context.Context, Bus, Said, map[string]any) (any, error)) func(map[string]any) (any, error) { - return func(a map[string]any) (any, error) { return f(context.Background(), bus, said, a) } + return func(a map[string]any) (any, error) { + stream := arg(a, "stream") + if b := arg(a, "bucket"); b != "" { + stream = "KV_" + b + } + if err := refuseNewForm(newForm, stream); err != nil { + return nil, err + } + return f(context.Background(), bus, said, a) + } } return []stdio.Tool{ {Name: "nats_consumer_remove", @@ -353,3 +367,24 @@ func removalTools(bus Bus, said Said) []stdio.Tool { Run: run(RemoveKeys)}, } } + +// refuseNewForm refuses a stream the keeper made for a module's bus state, naming the tool that removes it. +func refuseNewForm(newForm heldEntries, stream string) error { + if newForm == nil || stream == "" { + return nil + } + held, err := newForm() + if err != nil { + return fmt.Errorf("whether %s is a module's kept data of the new form cannot be read (%v), so nothing was removed", stream, err) + } + for module, a := range held { + for local, m := range a.Made { + for _, o := range m.Plan.Objects { + if o.Stream() == stream { + return fmt.Errorf("%s is %s's kept data %q, which the bus module made: it is removed only by nats_drop (an orphan, on the operator's warrant) or rebuilt by nats_recreate, never by this tool; nothing was removed", stream, module, local) + } + } + } + } + return nil +} diff --git a/modules/nats/cmd/nats-tools/tools_test.go b/modules/nats/cmd/nats-tools/tools_test.go index 7d1a8cdc..e2493a15 100644 --- a/modules/nats/cmd/nats-tools/tools_test.go +++ b/modules/nats/cmd/nats-tools/tools_test.go @@ -10,6 +10,9 @@ import ( "time" "github.com/nats-io/nats.go/jetstream" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "nats-tools/internal/keeper" ) const connz = `{"connections":[ @@ -220,6 +223,17 @@ func TestTheRemovalToolsRefuseBeforeTheyReachTheBus(t *testing.T) { return nil, nil, nil } said := func(string, any) error { t.Fatal("an audit event was said for a refused call"); return nil } + newForm := func() (map[string]keeper.Asked, error) { + made := map[string]keeper.Made{} + for _, e := range []catalogue.BusStateEntry{{Name: "log", Storage: "stream", Template: "log"}, {Name: "drafts", Storage: "key-value", Template: "state"}} { + p, err := keeper.PlanOf("m", e) + if err != nil { + t.Fatal(err) + } + made[e.Name] = keeper.Made{Plan: p, Created: true} + } + return map[string]keeper.Asked{"m": {Module: "m", Made: made}}, nil + } for _, c := range []struct { tool string args map[string]any @@ -238,9 +252,13 @@ func TestTheRemovalToolsRefuseBeforeTheyReachTheBus(t *testing.T) { {"nats_stream_remove", map[string]any{"stream": "ASSIGNMENTS", "why": " "}, "why is required"}, {"nats_stream_remove", map[string]any{"stream": "A.>", "why": "x"}, "stream is required"}, {"nats_stream_remove", map[string]any{"stream": "", "why": "x"}, "stream is required"}, + // CAT-N9: an entry of the new form goes only by drop or recreate; an old section's goes as before. + {"nats_stream_remove", map[string]any{"stream": "m_log", "why": "x"}, "removed only by nats_drop"}, + {"nats_bucket_remove", map[string]any{"bucket": "m_drafts", "node": "g14", "why": "x"}, "m's kept data \"drafts\""}, + {"nats_consumer_remove", map[string]any{"stream": "m_log", "consumer": "c", "why": "x"}, "removed only by nats_drop"}, } { var run func(map[string]any) (any, error) - for _, tl := range removalTools(bus, said) { + for _, tl := range removalTools(bus, said, newForm) { if tl.Name == c.tool { run = tl.Run }