From 958018e696e43c234732bf871f3affd31d1d2b14 Mon Sep 17 00:00:00 2001 From: mesh-admin Date: Wed, 14 Oct 2026 01:38:27 +0000 Subject: [PATCH] mesh-vault: the ledger keeps the ledger; retirement moves to retirement.go (review 255) Co-Authored-By: Claude Opus 5.5 --- modules/mesh-vault/cmd/mesh-vault/ledger.go | 281 ------------------ .../mesh-vault/cmd/mesh-vault/ledger_test.go | 224 -------------- 2 files changed, 505 deletions(-) diff --git a/modules/mesh-vault/cmd/mesh-vault/ledger.go b/modules/mesh-vault/cmd/mesh-vault/ledger.go index 0abadb43..17465276 100644 --- a/modules/mesh-vault/cmd/mesh-vault/ledger.go +++ b/modules/mesh-vault/cmd/mesh-vault/ledger.go @@ -25,7 +25,6 @@ import ( "regexp" "sort" "strings" - "sync" "time" ) @@ -250,283 +249,3 @@ func DeliveredFingerprint(c Contribution) (string, int, error) { } return Fingerprint(v), len(v), nil } - -// EventRetirement is the one event a provider says about retirement (novox/hq ADR 0230 §35); the controller -// derives the permission to emit it for every module that receives contributions. -const EventRetirement = "provisioner.retirement" - -// The rules of ADR 0230 §15 and §18. -const ( - StablePasses = 5 - StableFor = 10 * time.Minute - RetireAtOnce = 3 - RetireFraction = 0.5 - SayAgainEvery = time.Hour -) - -// Custody is the pass over the contributions file that keeps the ledger, and what it remembers between -// passes about the holders the mesh no longer asks for. -type Custody struct { - Ledger *Ledger - Receives string - Emit func(event string, body map[string]any) error - Now func() time.Time - Log func(format string, args ...any) - - mu sync.Mutex - key string - count int - firstSeen time.Time - asked map[string]bool - read bool - waiting *heldSet - rejected *heldSet -} - -type heldSet struct { - set []string - since time.Time - saidAt time.Time - by, why string -} - -// Pass records every delivered value, re-enables a retired holder asked for again, and counts the holders -// no longer asked for toward their retirement. -func (c *Custody) Pass() { - c.mu.Lock() - defer c.mu.Unlock() - asked, err := Contributions(c.Receives) - if err != nil { - c.Log("THE CONTRIBUTIONS FILE IS NOT USABLE (%s): %v — nothing is recorded or retired on this pass, and "+ - "the count toward retiring anyone starts again", c.Receives, err) - c.key, c.count = "", 0 - return - } - now := c.Now() - keep := map[string]bool{} - for _, a := range asked { - keep[a.As] = true - value, err := delivered(a) - if err != nil { - c.Log("%s: %v", a.As, err) - continue - } - held, outcome, err := c.Ledger.Record(a.As, a.Node, value, now) - if err != nil { - c.Log("%s: %v", a.As, err) - continue - } - if held.Retired != nil { - if err := c.Ledger.Mark(a.As, nil); err != nil { - c.Log("%s: asked for again, and its retirement mark could not be cleared: %v", a.As, err) - } else { - c.Log("%s: asked for again — re-enabled, its retirement mark cleared", a.As) - c.say(EventRetirement, map[string]any{"change": "reenabled", "consumers": []string{a.As}}) - } - } - if outcome == Unchanged { - continue - } - event := "provisioned" - if outcome == Rotated { - event = "rotated" - } - c.Log("%s: %s (%s…, rotations %d)", held.As, outcome, held.Fingerprint[:19], held.Rotations) - c.say(event, map[string]any{"consumer": held.Consumer, "as": held.As, "fingerprint": held.Fingerprint, "rotations": held.Rotations}) - } - c.asked, c.read = keep, true - c.countUnasked(now) -} - -func (c *Custody) active() []Held { - var out []Held - for _, h := range c.Ledger.List() { - if h.Retired == nil { - out = append(out, h) - } - } - return out -} - -func (c *Custody) countUnasked(now time.Time) { - active := c.active() - var unasked []string - for _, h := range active { - if !c.asked[h.As] { - unasked = append(unasked, h.As) - } - } - key := strings.Join(unasked, ",") - switch { - case len(unasked) == 0: - c.settle("every holder is asked for again") - c.key, c.count = "", 0 - return - case key != c.key: - if c.waiting != nil || c.rejected != nil { - c.settle("the set the mesh no longer asks for changed") - } - c.key, c.count, c.firstSeen = key, 1, now - c.Log("no longer asked for: %s; retired once the same set holds for %d passes and %s (novox/hq ADR 0230)", - key, StablePasses, StableFor) - default: - c.count++ - } - if c.rejected != nil || c.count < StablePasses || now.Sub(c.firstSeen) < StableFor { - return - } - if overTheBound(len(unasked), len(active)) { - c.wait(unasked, len(active), now) - return - } - c.retire(unasked, fmt.Sprintf("the mesh stopped asking for it: the same result in %d consecutive passes over %s", - c.count, now.Sub(c.firstSeen).Round(time.Second)), now) -} - -func overTheBound(n, held int) bool { - return n > RetireAtOnce || (held > 1 && float64(n) > RetireFraction*float64(held)) -} - -func (c *Custody) wait(set []string, held int, now time.Time) { - if c.waiting == nil { - c.waiting = &heldSet{set: set, since: now} - c.Log("RETIREMENT WAITS FOR A PERSON: the mesh no longer asks for %d of the %d holder(s) (%s), more than %d "+ - "at once or more than half. Nothing is retired until provisioner_retire_approve or "+ - "provisioner_retire_reject (novox/hq ADR 0230)", len(set), held, strings.Join(set, ", "), RetireAtOnce) - } else if now.Sub(c.waiting.saidAt) < SayAgainEvery { - return - } - c.waiting.saidAt = now - c.say(EventRetirement, map[string]any{"change": "waiting", "consumers": set, "held": held, - "since": c.waiting.since.UTC().Format(time.RFC3339)}) -} - -func (c *Custody) settle(why string) { - var set []string - switch { - case c.waiting != nil: - set = c.waiting.set - case c.rejected != nil: - set = c.rejected.set - default: - return - } - c.waiting, c.rejected = nil, nil - c.Log("the set waiting on retirement (%s) is settled: %s", strings.Join(set, ", "), why) - c.say(EventRetirement, map[string]any{"change": "settled", "consumers": set, "why": why}) -} - -// retire marks each holder in set retired: the ledger keeps it, and its history, until a person deletes it. -func (c *Custody) retire(set []string, why string, now time.Time) { - at := now.UTC().Format(isoMillis) - var done []string - for _, as := range set { - if err := c.Ledger.Mark(as, &RetiredMark{At: at, Why: why}); err != nil { - c.Log("%s: could not be marked retired: %v", as, err) - continue - } - done = append(done, as) - } - c.key, c.count, c.waiting = "", 0, nil - if len(done) == 0 { - return - } - c.Log("retired, kept in the ledger until a person deletes it: %s — %s", strings.Join(done, ", "), why) - c.say(EventRetirement, map[string]any{"change": "retired", "consumers": done, "why": why}) -} - -// Approve retires exactly the set waiting (or rejected) for a person. -func (c *Custody) Approve(by, why string) ([]string, error) { - c.mu.Lock() - defer c.mu.Unlock() - if strings.TrimSpace(why) == "" { - return nil, errors.New("an approval says why") - } - set := c.waiting - if set == nil { - set = c.rejected - } - if set == nil { - return nil, errors.New("no set waits for retirement") - } - c.say(EventRetirement, map[string]any{"change": "approved", "consumers": set.set, "by": by, "why": why}) - members := set.set - c.rejected = nil - c.retire(members, "approved by "+by+": "+why, c.Now()) - return members, nil -} - -// Reject keeps the waiting set active, and asks no more about it while it stays the same set. -func (c *Custody) Reject(by, why string) ([]string, error) { - c.mu.Lock() - defer c.mu.Unlock() - if strings.TrimSpace(why) == "" { - return nil, errors.New("a rejection says why") - } - if c.waiting == nil { - return nil, errors.New("no set waits for retirement") - } - c.rejected = &heldSet{set: c.waiting.set, since: c.Now(), by: by, why: why} - c.waiting = nil - c.Log("retirement of %s rejected by %s: %s", strings.Join(c.rejected.set, ", "), by, why) - c.say(EventRetirement, map[string]any{"change": "rejected", "consumers": c.rejected.set, "by": by, "why": why}) - return c.rejected.set, nil -} - -// Delete forgets one retired holder, on a person's word. Refused: an active holder, one the mesh asks for, -// and any without a why. -func (c *Custody) Delete(as, by, why string) error { - c.mu.Lock() - defer c.mu.Unlock() - if strings.TrimSpace(why) == "" { - return errors.New("a deletion says why") - } - h, ok := c.Ledger.Get(as) - if !ok { - return fmt.Errorf("the ledger holds nothing for %s", as) - } - if h.Retired == nil { - return fmt.Errorf("%s is active, and only a retired holder is deleted (novox/hq ADR 0230 §31)", as) - } - if c.asked[as] { - return fmt.Errorf("%s is asked for, and is not deleted", as) - } - if !c.Ledger.Delete(as) { - return fmt.Errorf("%s could not be deleted", as) - } - c.Log("%s deleted from the ledger by %s: %s", as, by, why) - c.say(EventRetirement, map[string]any{"change": "deleted", "consumers": []string{as}, "by": by, "why": why}) - return nil -} - -// Retirement is what is held, waiting, rejected and retired. -func (c *Custody) Retirement() map[string]any { - c.mu.Lock() - defer c.mu.Unlock() - active, retired := []string{}, []map[string]any{} - for _, h := range c.Ledger.List() { - if h.Retired == nil { - active = append(active, h.As) - } else { - retired = append(retired, map[string]any{"consumer": h.As, "node": h.Consumer, "retired_at": h.Retired.At, "why": h.Retired.Why}) - } - } - out := map[string]any{"active": active, "retired": retired, "stable_passes": StablePasses, - "stable_for_seconds": int(StableFor.Seconds()), "counting": c.key, "passes": c.count, "waiting": nil, "rejected": nil} - if c.waiting != nil { - out["waiting"] = map[string]any{"consumers": c.waiting.set, "since": c.waiting.since.UTC().Format(time.RFC3339)} - } - if c.rejected != nil { - out["rejected"] = map[string]any{"consumers": c.rejected.set, "by": c.rejected.by, "why": c.rejected.why} - } - return out -} - -func (c *Custody) say(event string, body map[string]any) { - if c.Emit == nil { - return - } - if err := c.Emit(event, body); err != nil { - c.Log("%s could not be said: %v", event, err) - } -} diff --git a/modules/mesh-vault/cmd/mesh-vault/ledger_test.go b/modules/mesh-vault/cmd/mesh-vault/ledger_test.go index 28e56c0d..74aaa70e 100644 --- a/modules/mesh-vault/cmd/mesh-vault/ledger_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/ledger_test.go @@ -120,230 +120,6 @@ func TestWhatTheMeshDeliversIsFingerprintedNeverReturned(t *testing.T) { } } -// custodyAt is a custody pass over a contributions file in dir, on a clock the test moves. -type custodyAt struct { - *Custody - bus *published - receives string - dir string - clock *time.Time -} - -func newCustody(t *testing.T) custodyAt { - dir := t.TempDir() - l, _ := NewLedger(filepath.Join(dir, "ledger")) - bus := &published{} - clock := time.Date(2026, 10, 14, 9, 0, 0, 0, time.UTC) - c := custodyAt{bus: bus, receives: filepath.Join(dir, "mesh.json"), dir: dir, clock: &clock} - c.Custody = &Custody{Ledger: l, Receives: c.receives, Emit: bus.emit, Now: func() time.Time { return *c.clock }, Log: t.Logf} - return c -} - -// ask writes a contributions file asking for these logins, each with its value. -func (c custodyAt) ask(values map[string]string) { - given := []map[string]string{} - for as, v := range values { - f := filepath.Join(c.dir, as+".secret") - _ = os.WriteFile(f, []byte(v), 0o600) - given = append(given, map[string]string{"node": "anchor", "as": as, "secret": f}) - } - doc, _ := json.Marshal(map[string]any{"requirement": "secret", "given": given}) - _ = os.WriteFile(c.receives, doc, 0o600) -} - -// passes runs n passes a minute apart. -func (c custodyAt) passes(n int) { - for i := 0; i < n; i++ { - c.Pass() - *c.clock = c.clock.Add(time.Minute) - } -} - -func (c custodyAt) changes() []string { - var out []string - for _, e := range c.bus.events { - if e["event"] == EventRetirement { - out = append(out, e["body"].(map[string]any)["change"].(string)) - } else { - out = append(out, e["event"].(string)) - } - } - return out -} - -// The custody pass: a delivered value is recorded and announced by fingerprint, a changed one is a rotation. -func TestTheCustodyPassRecordsAndRotates(t *testing.T) { - c := newCustody(t) - c.ask(map[string]string{"anchor-redis": "v1"}) - c.passes(1) - c.ask(map[string]string{"anchor-redis": "v2"}) - c.passes(1) - if got := strings.Join(c.changes(), ","); got != "provisioned,rotated" { - t.Errorf("announced %s", got) - } - for _, raw := range c.bus.raw { - if strings.Contains(raw, `"v1"`) || strings.Contains(raw, `"v2"`) { - t.Errorf("announced a value: %s", raw) - } - } -} - -// A contributions file that does not say who is asked for — unreadable, not JSON, without a given list, a -// null one, or for another requirement — is no answer: nobody is retired, and the count starts again. -func TestAFileThatSaysNothingRetiresNobody(t *testing.T) { - for _, doc := range []string{"{not json", `{"requirement":"secret"}`, `{"requirement":"secret","given":null}`, - `{"requirement":"redis-cache","given":[]}`} { - c := newCustody(t) - c.ask(map[string]string{"anchor-redis": "v1"}) - c.passes(1) - _ = os.WriteFile(c.receives, []byte(doc), 0o600) - c.passes(30) - if h, ok := c.Ledger.Get("anchor-redis"); !ok || h.Retired != nil { - t.Errorf("%s: the holder was retired or lost: %+v %v", doc, h, ok) - } - if _, err := Contributions(c.receives); err == nil { - t.Errorf("%s was read as an answer", doc) - } - } - if _, err := Contributions(filepath.Join(t.TempDir(), "missing.json")); err == nil { - t.Error("a missing file was read as an answer") - } -} - -// A holder the mesh stops asking for is retired only after five passes AND ten minutes, is never deleted -// by a pass, keeps its history, and is re-enabled when asked for again. -func TestAHolderNoLongerAskedForIsRetiredNotDeleted(t *testing.T) { - c := newCustody(t) - c.ask(map[string]string{"a-app": "1", "b-app": "2", "c-app": "3"}) - c.passes(1) - c.ask(map[string]string{"a-app": "1", "b-app": "2"}) - c.Pass() // the first pass that sees c-app unasked - for i := 0; i < 8; i++ { - *c.clock = c.clock.Add(10 * time.Second) // five passes, but in under ten minutes - c.Pass() - } - if h, _ := c.Ledger.Get("c-app"); h.Retired != nil { - t.Fatal("retired before ten minutes had passed") - } - *c.clock = c.clock.Add(10 * time.Minute) - c.Pass() - h, ok := c.Ledger.Get("c-app") - if !ok || h.Retired == nil || h.Retired.Why == "" { - t.Fatalf("not retired, or retired without a why: %+v %v", h, ok) - } - if h.Fingerprint != Fingerprint("3") { - t.Error("retiring lost what the ledger knew of the holder") - } - if _, err := os.Stat(filepath.Join(c.dir, "ledger", "c-app.json")); err != nil { - t.Error("a pass deleted a ledger entry") - } - c.ask(map[string]string{"a-app": "1", "b-app": "2", "c-app": "3"}) - c.Pass() - if h, _ := c.Ledger.Get("c-app"); h.Retired != nil { - t.Error("asked for again, the holder stayed retired") - } - if got := strings.Join(c.changes(), ","); got != "provisioned,provisioned,provisioned,retired,reenabled" { - t.Errorf("announced %s", got) - } -} - -// A different set, or a file that says nothing, starts the count again. -func TestTheCountStartsAgain(t *testing.T) { - c := newCustody(t) - c.ask(map[string]string{"a-app": "1", "b-app": "2", "c-app": "3"}) - c.passes(1) - c.ask(map[string]string{"a-app": "1", "b-app": "2"}) - c.passes(4) - _ = os.WriteFile(c.receives, []byte(`{"given":null}`), 0o600) - c.passes(1) - c.ask(map[string]string{"a-app": "1", "b-app": "2"}) - c.passes(4) - *c.clock = c.clock.Add(time.Hour) - if h, _ := c.Ledger.Get("c-app"); h.Retired != nil { - t.Fatal("retired though the count was broken") - } - c.passes(1) - if h, _ := c.Ledger.Get("c-app"); h.Retired == nil { - t.Fatal("not retired after five unbroken passes and an hour") - } -} - -// Too many at once waits for a person: approve retires exactly that set, reject keeps it. -func TestTooManyAtOnceWaitsForAPerson(t *testing.T) { - all := map[string]string{"a-app": "1", "b-app": "2", "c-app": "3", "d-app": "4", "e-app": "5"} - for _, decide := range []string{"approve", "reject"} { - c := newCustody(t) - c.ask(all) - c.passes(1) - c.ask(map[string]string{"a-app": "1"}) - c.passes(5) - *c.clock = c.clock.Add(time.Hour) - c.passes(1) - r := c.Retirement() - if r["waiting"] == nil { - t.Fatalf("four of five no longer asked for did not wait: %v", r) - } - for _, h := range c.Ledger.List() { - if h.Retired != nil { - t.Fatalf("%s retired while the set waited", h.As) - } - } - if _, err := c.Approve("person.jochen", ""); err == nil { - t.Error("an approval without a why was taken") - } - if decide == "approve" { - set, err := c.Approve("person.jochen", "the four apps are gone") - if err != nil || len(set) != 4 { - t.Fatalf("%v %v", set, err) - } - if h, _ := c.Ledger.Get("b-app"); h.Retired == nil { - t.Error("approved, not retired") - } - if h, _ := c.Ledger.Get("a-app"); h.Retired != nil { - t.Error("an asked-for holder was retired") - } - } else { - if _, err := c.Reject("person.jochen", "they come back"); err != nil { - t.Fatal(err) - } - c.passes(10) - if h, _ := c.Ledger.Get("b-app"); h.Retired != nil { - t.Error("rejected, but retired") - } - if c.Retirement()["waiting"] != nil { - t.Error("a rejected set was asked about again") - } - } - } -} - -// Only a person deletes, with a why, and only a retired holder the mesh no longer asks for. -func TestOnlyAPersonDeletesARetiredHolder(t *testing.T) { - c := newCustody(t) - c.ask(map[string]string{"a-app": "1", "b-app": "2", "c-app": "3"}) - c.passes(1) - if err := c.Delete("c-app", "person.jochen", "gone"); err == nil { - t.Error("an active holder was deleted") - } - c.ask(map[string]string{"a-app": "1", "b-app": "2"}) - c.passes(5) - *c.clock = c.clock.Add(time.Hour) - c.passes(1) - if err := c.Delete("c-app", "person.jochen", ""); err == nil { - t.Error("deleted without a why") - } - if err := c.Delete("c-app", "person.jochen", "gitea's redis is gone for good"); err != nil { - t.Fatal(err) - } - if _, ok := c.Ledger.Get("c-app"); ok { - t.Error("deleted, but still in the ledger") - } - last := c.changes() - if last[len(last)-1] != "deleted" { - t.Errorf("announced %v", last) - } -} - func logins(hs []Held) string { var out []string for _, h := range hs {