From fe7d4504bf29230b116452c6dbb6f8be6524d25c Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 2 Nov 2026 10:35:59 +0100 Subject: [PATCH] mesh-vault: retire and rotate without the controller's lease The controller ended by its unassignment and its lease key expired, so the gate on a handed lease kept retirement, rotation and node-login rotation off for good (issue 1126, A3). Drop the gate, the lease watch and the manifest's read of mesh-controller.lease; vault_status no longer reports a lease. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- .../cmd/mesh-vault/enrolment_test.go | 3 +- modules/mesh-vault/cmd/mesh-vault/lease.go | 41 ------------- modules/mesh-vault/cmd/mesh-vault/main.go | 34 +---------- modules/mesh-vault/cmd/mesh-vault/mint.go | 58 +++++-------------- .../cmd/mesh-vault/mint_integration_test.go | 11 ++-- .../mesh-vault/cmd/mesh-vault/mint_test.go | 33 +++++------ .../mesh-vault/cmd/mesh-vault/noderotation.go | 8 --- modules/mesh-vault/cmd/mesh-vault/retire.go | 16 +---- .../mesh-vault/cmd/mesh-vault/retire_test.go | 11 +--- modules/mesh-vault/cmd/mesh-vault/rotation.go | 8 +-- .../mesh-vault/rotation_integration_test.go | 6 +- modules/mesh-vault/cmd/mesh-vault/tools.go | 3 +- modules/mesh-vault/cmd/mesh-vault/vault.go | 4 +- .../mesh-vault/cmd/mesh-vault/vault_test.go | 11 ++-- modules/mesh-vault/module.json | 1 - 15 files changed, 50 insertions(+), 198 deletions(-) delete mode 100644 modules/mesh-vault/cmd/mesh-vault/lease.go diff --git a/modules/mesh-vault/cmd/mesh-vault/enrolment_test.go b/modules/mesh-vault/cmd/mesh-vault/enrolment_test.go index d8e04307..420a4ac7 100644 --- a/modules/mesh-vault/cmd/mesh-vault/enrolment_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/enrolment_test.go @@ -72,7 +72,7 @@ func (l *loginsState) Keys() ([]string, error) { return out, nil } -// enrolling is a vault that mints, with the lease handed away, a loaded user list at revision 570, the bus on novox, the +// enrolling is a vault that mints, with no lease heard (issue 1126), a loaded user list at revision 570, the bus on novox, the // nodes g14, ace and novox placed, their existing logins adopted, and hand acts of person.jochen's. type enrolling struct { testVault @@ -87,7 +87,6 @@ func newEnrolling(t *testing.T) *enrolling { e := &enrolling{testVault: v, accounts: &fakeAccounts{keys: map[string]AccountHash{}}, logins: &loginsState{keys: map[string]NodeLogin{}}, acts: map[string]HandActSeen{}} v.Accounts, v.Logins = e.accounts, e.logins v.Placement = placementsOf(t, map[string][]string{"novox": {"nats"}, "g14": {"web"}, "ace": {"shop"}}) - v.LeaseHeard([]byte(`{"handed-to":"the operator's hand-over"}`), true, nil) v.LoadedListHeard(570) v.TokenBase = func() (TokenBase, error) { return TokenBase{Broker: "tls://10.77.0.1:4222", Fingerprint: "sha256:bus"}, nil diff --git a/modules/mesh-vault/cmd/mesh-vault/lease.go b/modules/mesh-vault/cmd/mesh-vault/lease.go deleted file mode 100644 index 85264172..00000000 --- a/modules/mesh-vault/cmd/mesh-vault/lease.go +++ /dev/null @@ -1,41 +0,0 @@ -package main - -// The controller's lease as the vault reads it (novox/hq ADR 0561 §3): handed away only to a holder that is no -// controller, and not for a controller's value, a key deleted or expired, or one that cannot be read. It turns retirement -// (retire.go) and rotation (rotation.go), never minting: the vault mints every wanted account it holds no value for, -// whoever holds the lease (mint.go, issue 774). No clock and no setting turns it. A key's age passing is heard by no -// watch, so the lease is read afresh before a retirement or a rotation (LeaseRead). - -import "git.novox.be/novox/mesh-sdk/go/buscompose" - -// LeaseHeard is the watch of the controller's lease: it records whether it is handed away, and says when that turned. -func (v *Vault) LeaseHeard(raw []byte, found bool, readErr error) { - if !v.setLease(raw, found, readErr) { - return - } - on, why := v.leaseHanded() - v.log("the controller's lease is %s: %s", map[bool]string{true: "HANDED AWAY", false: "not handed away"}[on], why) -} - -// setLease records what the lease says, and answers whether minting turned on or off by it. -func (v *Vault) setLease(raw []byte, found bool, readErr error) bool { - on, why := buscompose.LeaseHandedAway(raw, found, readErr) - m := &v.minting - m.mu.Lock() - defer m.mu.Unlock() - flipped := on != m.on || m.why == "" - if flipped { - m.since = v.Now().UTC() - } - m.on, m.why = on, why - return flipped -} - -// leaseAfresh reads the lease again before a retirement or a rotation, where the vault can: a key that expired since is heard by no watch. -func (v *Vault) leaseAfresh() { - if v.LeaseRead == nil { - return - } - raw, found, err := v.LeaseRead() - v.setLease(raw, found, err) -} diff --git a/modules/mesh-vault/cmd/mesh-vault/main.go b/modules/mesh-vault/cmd/mesh-vault/main.go index a76ac978..253dd233 100644 --- a/modules/mesh-vault/cmd/mesh-vault/main.go +++ b/modules/mesh-vault/cmd/mesh-vault/main.go @@ -58,7 +58,7 @@ func build() *Env { watchedPlan := func(key string) ([]byte, error) { raw, err := assigned.Read(key); return asAssignment(key, raw, err) } env.Vault = &Vault{Store: store, Own: own, Askers: Declared{Read: watchedPlan, Confirm: readAssignment, Store: store}, Assignments: Mirror{Keys: assignmentKeys, Read: readAssignment}, Emit: emit, Ask: stdio.Ask, - Now: time.Now, Log: say, Accounts: busAccounts{}, Haves: readHave, LeaseRead: readLease, WantRead: readWant, + Now: time.Now, Log: say, Accounts: busAccounts{}, Haves: readHave, WantRead: readWant, HaveKeys: func() ([]string, error) { return stdio.State(HaveRead).Keys() }, Placement: placement, Signers: busSigners{}, AskCallers: askCallers(os.Getenv("MESH_VAULT_SECRET_ASK_CALLERS")), EnrolCallers: askCallers(os.Getenv("MESH_VAULT_ENROLMENT_CALLERS")), Logins: busNodeLogins{}, ReadAct: readHandAct, TokenBase: TokenBaseFromFile(os.Getenv("MESH_VAULT_STATE")), @@ -229,34 +229,6 @@ func readWant(user string) ([]byte, bool, error) { return e.Value, true, nil } -// readLease is the controller's lease key, read through the runtime under the vault's own account (its manifest reads it). -func readLease() ([]byte, bool, error) { - e, err := stdio.State(LeaseRead).Get(LeaseKey) - if err != nil || e == nil { - return nil, false, err - } - return e.Value, true, nil -} - -// watchLease turns minting on and off as the controller's lease is written (lease.go, ADR 0561 §3): a watch, never a -// poll; and the lease is read afresh before every mint. It answers whether the watch began. -func watchLease(env *Env) bool { - if env.Vault == nil { - return true - } - env.Vault.LeaseHeard(readLease()) - err := stdio.State(LeaseRead).Watch(LeaseKey, func(c stdio.StateChange) error { - deleted := c.Op == "delete" || c.Op == "purge" || len(c.Value) == 0 - env.Vault.LeaseHeard(c.Value, !deleted, nil) - return nil - }) - if err != nil { - say("the controller's lease is not watched, so minting stays as last read; begun again at the next pass: %v", err) - return false - } - return true -} - // adoptOperatorKey takes the operator key from the export the mesh writes to the vault's root (what // `operator key set` recorded; the controller keeps only a public half there), and seals again any // operator copy not yet sealed to the current key. Run every minute, so a failure partway is finished. It @@ -319,7 +291,6 @@ func main() { // The operator key first: nothing is kept without it, so a want heard before it would not be minted. env.pass() watched, watching := watchHandings(env), watchAssignments() - lease := watchLease(env) wants := watchWants(env) leavers := watchLeavers(env) for { @@ -332,9 +303,6 @@ func main() { if !watching { watching = watchAssignments() } - if !lease { - lease = watchLease(env) - } if !wants { wants = watchWants(env) } diff --git a/modules/mesh-vault/cmd/mesh-vault/mint.go b/modules/mesh-vault/cmd/mesh-vault/mint.go index 86f73cfd..414cf5bd 100644 --- a/modules/mesh-vault/cmd/mesh-vault/mint.go +++ b/modules/mesh-vault/cmd/mesh-vault/mint.go @@ -11,10 +11,10 @@ package main // on `secret-changed` and composes. // // **The vault is the one issuer** (issue 774, research 072's gap "vault mints after the lease"): it mints every wanted -// account it holds no value for, whoever holds the controller's lease, and the controller mints none once a vault is -// installed (mesh-controller forwards `module issue` and its assignment's issue to issue-account). No account depends -// on a node reporting to the controller. The lease still turns retirement and rotation (retire.go, rotation.go), and -// vault_status names it. **Never two issuers of one value** (§3, §4): an account whose value the vault holds, or whose +// account it holds no value for, and the controller mints none once a vault is installed (mesh-controller forwards +// `module issue` and its assignment's issue to issue-account). No account depends on a node reporting to the +// controller. No lease turns minting, retirement or rotation: the controller ended by its unassignment (issue 1126). +// **Never two issuers of one value** (§3, §4): an account whose value the vault holds, or whose // hash it adopted, is never minted; the node accounts and the controller's own are never wanted; a value the controller // moves in never replaces one held (move.go), and one it hands with origin issued never replaces an account the vault // minted (AcceptTake): during the delivery an older controller may still mint, and the vault's value wins. @@ -38,23 +38,13 @@ import ( // WantedRead is the grants part's state of wanted accounts, as the vault's manifest reads it. const WantedRead = buscompose.GrantsAccounts -// LeaseRead is the controller's lease, as the vault's manifest reads it, and LeaseKey its one key. -const ( - LeaseRead = buscompose.ControllerLease - LeaseKey = buscompose.ControllerLeaseKey -) - // AccountIssuedEvent is what the vault says when it minted an account (mesh-sdk buscompose.AccountIssued). const AccountIssuedEvent = buscompose.AccountIssuedEvent -// minting is what the vault knows of the wanted accounts and of the controller's lease, kept in memory and read again -// from the bus at every start: the grants part's state and the lease are the record, never this. on, why and since are -// the lease (handed away or not), which turns retirement and rotation, never minting. +// minting is what the vault knows of the wanted accounts, kept in memory and read again from the bus at every start: +// the grants part's state is the record, never this. type minting struct { - mu sync.Mutex - on bool - why string - since time.Time + mu sync.Mutex // wants are the wanted accounts, by user, as last heard. wants map[string]buscompose.WantedAccount // heldBack are the wanted users the vault did not mint, with why (vault_status). @@ -81,34 +71,25 @@ type Minted struct { Hashed bool `json:"hashed"` } -// MintState is what vault_status says of minting: always on (the vault is the one issuer, issue 774), and the lease, -// which turns retirement and rotation. +// MintState is what vault_status says of minting: always on (the vault is the one issuer, issue 774). type MintState struct { - On bool `json:"on"` - Why string `json:"why"` - // Lease is whether the controller's lease is handed away, LeaseWhy why, and Since when that last turned. - Lease bool `json:"lease_handed_away"` - LeaseWhy string `json:"lease_why"` - Since time.Time `json:"since,omitzero"` + On bool `json:"on"` + Why string `json:"why"` Wanted int `json:"wanted"` HeldBack map[string]string `json:"held_back"` Minted []Minted `json:"minted"` } -// mintingWhy is why the vault mints, whoever holds the lease. +// mintingWhy is why the vault mints. const mintingWhy = "the vault is the one issuer of bus accounts: it mints every wanted account it holds no value for " + - "and adopted no hash of, whoever holds the controller's lease (issue 774)" + "and adopted no hash of (issue 774)" // MintState answers how minting stands. A user held back whose account the vault holds a value for meanwhile (moved in, // handed, or minted by another path) is not said held back: it is not (issue 774, 2026-10-31). func (v *Vault) MintState() MintState { m := &v.minting m.mu.Lock() - out := MintState{On: true, Why: mintingWhy, Lease: m.on, LeaseWhy: m.why, Since: m.since, Wanted: len(m.wants), - HeldBack: map[string]string{}, Minted: []Minted{}} - if out.LeaseWhy == "" { - out.LeaseWhy = leaseUnread - } + out := MintState{On: true, Why: mintingWhy, Wanted: len(m.wants), HeldBack: map[string]string{}, Minted: []Minted{}} wants := map[string]buscompose.WantedAccount{} for u, why := range m.heldBack { out.HeldBack[u] = why @@ -143,19 +124,6 @@ func (v *Vault) holdsAccount(w buscompose.WantedAccount) bool { return err == nil && BusAccountValue(secret, value) } -// leaseUnread is why nothing is retired or rotated before the lease is read. -const leaseUnread = "the controller's lease has not been read yet, so nothing is retired or rotated" - -// leaseHanded says whether the controller's lease is handed away, and why: what retirement and rotation wait for. -func (v *Vault) leaseHanded() (bool, string) { - v.minting.mu.Lock() - defer v.minting.mu.Unlock() - if v.minting.why == "" { - return false, leaseUnread - } - return v.minting.on, v.minting.why -} - func (v *Vault) holdBack(user, why string) { m := &v.minting m.mu.Lock() diff --git a/modules/mesh-vault/cmd/mesh-vault/mint_integration_test.go b/modules/mesh-vault/cmd/mesh-vault/mint_integration_test.go index 62b39a82..e631d5d0 100644 --- a/modules/mesh-vault/cmd/mesh-vault/mint_integration_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/mint_integration_test.go @@ -5,7 +5,7 @@ package main // The vault whole against a real bus (novox/hq ADR 0561 §1 to §4, issue 774's acceptance 12): the built binary, played // over its real stdin and stdout as the node's tool runtime plays it (tools/call with the caller in _meta; mesh/state.* // and mesh/publish answered from a real nats-server's key-value buckets and subjects), its store in a directory of its -// own. A want written to the grants part's state is minted whoever holds the controller's lease (issue 774: the vault is +// own. A want written to the grants part's state is minted with no lease asked (issue 774: the vault is // the one issuer): the hash is in bus-accounts, own answers the value sealed to the node, the value is the controller's // shape and its hash's password, account-issued is said; an adopted user is never minted. @@ -330,7 +330,7 @@ func TestTheVaultWholeMintsOnTheBusWhoeverHoldsTheLease(t *testing.T) { if err != nil { t.Fatal(err) } - for _, b := range []string{"mesh-vault_bus-accounts", "mesh-grants_accounts", "mesh-controller_lease", + for _, b := range []string{"mesh-vault_bus-accounts", "mesh-grants_accounts", "mesh-provisioner_plans", "node-engine_have", "mesh-delivery-assigner_placement"} { if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 2}); err != nil { t.Fatal(err) @@ -372,14 +372,13 @@ func TestTheVaultWholeMintsOnTheBusWhoeverHoldsTheLease(t *testing.T) { _ = json.Unmarshal(raw, &s) return s.Minting } - // The controller holds the lease, and the want is minted all the same. - p.put(t, "mesh-controller.lease", LeaseKey, map[string]any{"instance": "novox/1/2026-10-27", "epoch": 57}) + // No lease key: the want is minted all the same (issue 1126). x, y := aWant("mod-x"), aWant("mod-y") p.put(t, "mesh-vault.bus-accounts", y.User, AccountHash{User: y.User, Hash: "$2a$04$adoptedadoptedadoptedadoptedadoptedadoptedadopte", Adopted: true}) p.put(t, "mesh-grants.accounts", x.User, x) p.put(t, "mesh-grants.accounts", y.User, y) - eventually(t, "the account minted under the controller's lease", func() bool { return p.hashOf(t, x.User) != nil }) - if st := status(); !st.On || st.Lease || len(st.HeldBack) != 0 { + eventually(t, "the account minted", func() bool { return p.hashOf(t, x.User) != nil }) + if st := status(); !st.On || len(st.HeldBack) != 0 { t.Errorf("vault_status says %+v", st) } nodePub, nodePriv := aSealingKey(t) diff --git a/modules/mesh-vault/cmd/mesh-vault/mint_test.go b/modules/mesh-vault/cmd/mesh-vault/mint_test.go index 7ec6151c..d1fc78b3 100644 --- a/modules/mesh-vault/cmd/mesh-vault/mint_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/mint_test.go @@ -24,9 +24,9 @@ func wantRaw(t *testing.T, w buscompose.WantedAccount) []byte { return raw } -// mintingVault is a vault with a state of bus accounts, ace's sealing key stated, ace's modules assigned, and minting -// turned on or off as on says. -func mintingVault(t *testing.T, on bool) (testVault, *fakeAccounts, string) { +// mintingVault is a vault with a state of bus accounts, ace's sealing key stated and ace's modules assigned. No lease is +// heard: none turns minting, retirement or rotation (issue 1126). +func mintingVault(t *testing.T) (testVault, *fakeAccounts, string) { t.Helper() plans := assignments{"ace.mod-x": `{"node":"ace","module":"mod-x"}`, "ace.mod-y": `{"node":"ace","module":"mod-y"}`} v := newVault(t, nil) @@ -37,11 +37,6 @@ func mintingVault(t *testing.T, on bool) (testVault, *fakeAccounts, string) { if err := v.recordNodeKey("ace", pub, "node.ace"); err != nil { t.Fatal(err) } - if on { - v.LeaseHeard([]byte(`{"handed-to":"the operator's hand-over"}`), true, nil) - } else { - v.LeaseHeard([]byte(`{"instance":"novox/1/2026-10-27","epoch":57}`), true, nil) - } return v, accounts, priv } @@ -49,7 +44,7 @@ func mintingVault(t *testing.T, on bool) (testVault, *fakeAccounts, string) { // shape as the module's broker, its hash in bus-accounts, said account-issued before secret-changed, and answered by own // sealed to the node; a value held or a hash adopted is never minted. func TestTheVaultMintsAWantedAccountOnceAndNothingItHolds(t *testing.T) { - v, accounts, nodePriv := mintingVault(t, true) + v, accounts, nodePriv := mintingVault(t) w := aWant("mod-x") before := len(v.bus.events) v.WantHeard(w.User, wantRaw(t, w), false) @@ -107,11 +102,11 @@ func TestTheVaultMintsAWantedAccountOnceAndNothingItHolds(t *testing.T) { } // 774/AC8, after research 072's gap "vault mints after the lease": the vault is the one issuer, so a wanted account is -// minted while the controller still holds the lease (never an adopted one), vault_status says minting is on and the -// lease apart, and a want held back is not said held back once the vault holds its value. During the delivery an older +// minted with no lease asked (never an adopted one), vault_status says minting is on and names no lease (issue 1126), +// and a want held back is not said held back once the vault holds its value. During the delivery an older // controller may still mint: its value handed with origin issued never replaces the account the vault minted. func TestTheVaultMintsWhoeverHoldsTheLease(t *testing.T) { - v, accounts, _ := mintingVault(t, false) + v, accounts, _ := mintingVault(t) accounts.keys = map[string]AccountHash{"ace.mod-y": {User: "ace.mod-y", Hash: "$2a$04$adopted", Adopted: true}} for _, m := range []string{"mod-x", "mod-y"} { w := aWant(m) @@ -119,14 +114,14 @@ func TestTheVaultMintsWhoeverHoldsTheLease(t *testing.T) { } minted, held, _ := v.Store.Read("own/ace/mod-x/broker") if !held { - t.Fatalf("not minted under the controller's lease: %+v", v.MintState()) + t.Fatalf("not minted: %+v", v.MintState()) } if _, held, _ := v.Store.Read("own/ace/mod-y/broker"); held { t.Error("an adopted account was minted") } st := v.MintState() - if !st.On || st.Lease || !strings.Contains(st.LeaseWhy, "controller holds the lease") || len(st.HeldBack) != 0 { - t.Errorf("under the controller's lease vault_status says %+v", st) + if !st.On || strings.Contains(st.Why, "lease") || len(st.HeldBack) != 0 { + t.Errorf("vault_status says %+v", st) } // An older controller's covered account, handed with origin issued: refused, the vault's value kept. x := aWant("mod-x") @@ -156,7 +151,7 @@ func TestTheVaultMintsWhoeverHoldsTheLease(t *testing.T) { // 774/AC10 issue-account (novox/hq ADR 0561 §9): the verb mints one wanted user now, read afresh, whoever holds the lease; // it is refused for a node's account and the controller's, and for a user nobody wants; a user held already is said so. func TestIssueAccountMintsOneWantedUserNowAndRefusesTheRest(t *testing.T) { - v, _, _ := mintingVault(t, false) + v, _, _ := mintingVault(t) w := aWant("mod-x") raw := wantRaw(t, w) v.WantRead = func(user string) ([]byte, bool, error) { @@ -191,7 +186,7 @@ func TestIssueAccountMintsOneWantedUserNowAndRefusesTheRest(t *testing.T) { // over the second login `--b`, never of a node's account, the controller's or an adopted hash, and `rotate` of a // covered account stays refused, so nothing rotates in place. func TestOneRotationBeginsPerRevisionOfTheLoadedList(t *testing.T) { - v, accounts, _ := mintingVault(t, true) + v, accounts, _ := mintingVault(t) for _, m := range []string{"mod-x", "mod-y"} { w := aWant(m) v.WantHeard(w.User, wantRaw(t, w), false) @@ -227,7 +222,7 @@ func TestOneRotationBeginsPerRevisionOfTheLoadedList(t *testing.T) { // A mint that failed (here: no operator key yet, so nothing can be kept) is tried again at the pass, never lost. func TestAFailedMintIsTriedAgainAtThePass(t *testing.T) { - v, accounts, _ := mintingVault(t, true) + v, accounts, _ := mintingVault(t) key, _ := v.Store.OperatorKey() if err := v.Store.setOperatorKey("", "the test", v.Now()); err != nil { t.Fatal(err) @@ -250,7 +245,7 @@ func TestAFailedMintIsTriedAgainAtThePass(t *testing.T) { // broker-management on 2026-10-30, issue 774) is replaced by a minted account when the grants part wants its user; // a held account still never is. func TestAHeldValueThatIsNoAccountIsReplacedByAMintedOne(t *testing.T) { - v, accounts, _ := mintingVault(t, true) + v, accounts, _ := mintingVault(t) w := aWant("mod-x") if _, err := v.Put("own/ace/mod-x/broker", `{"password":"pw"}`, "the test"); err != nil { t.Fatal(err) diff --git a/modules/mesh-vault/cmd/mesh-vault/noderotation.go b/modules/mesh-vault/cmd/mesh-vault/noderotation.go index 9c074f3f..8409482d 100644 --- a/modules/mesh-vault/cmd/mesh-vault/noderotation.go +++ b/modules/mesh-vault/cmd/mesh-vault/noderotation.go @@ -268,10 +268,6 @@ func (v *Vault) RotateNode(caller string, a RotateNodeArgs) (RotateNodeAnswer, e if v.Accounts == nil { return RotateNodeAnswer{}, errors.New("the vault has no state of bus accounts to write to") } - v.leaseAfresh() - if on, why := v.leaseHanded(); !on { - return RotateNodeAnswer{}, fmt.Errorf("no rotation begins: %s", why) - } v.minting.mu.Lock() revision := v.minting.loadedRevision v.minting.mu.Unlock() @@ -596,10 +592,6 @@ func (v *Vault) RotationPass() { if err != nil || len(running) > 0 { return } - v.leaseAfresh() - if on, _ := v.leaseHanded(); !on { - return - } bus, err := v.busNode() if err != nil { return diff --git a/modules/mesh-vault/cmd/mesh-vault/retire.go b/modules/mesh-vault/cmd/mesh-vault/retire.go index 5862adf7..6cdb489a 100644 --- a/modules/mesh-vault/cmd/mesh-vault/retire.go +++ b/modules/mesh-vault/cmd/mesh-vault/retire.go @@ -7,8 +7,8 @@ package main // retired in its store, keeping it (a value is deleted only by a person, ADR 0230). As ADR 0230 §15 and §18 have it, the // same set of accounts gone must stand for five passes and ten minutes first, and a set of more than three, or more than // half of the accounts held, waits for a person, who retires them with `account_retire`. Wanted again, an account is -// re-enabled at once: the mark taken off and its hash written again. Nothing is retired while the controller holds the -// lease (it forgets the accounts it mints), or before the wanted accounts are read whole. +// re-enabled at once: the mark taken off and its hash written again. Nothing is retired before the wanted accounts are read +// whole. No lease turns it: the controller ended by its unassignment (issue 1126). import ( "errors" @@ -135,8 +135,6 @@ func (v *Vault) RetirePass() { if v.Accounts == nil { return } - v.leaseAfresh() - on, leaseWhy := v.leaseHanded() m := &v.minting m.mu.Lock() read := m.retiring.wantsRead @@ -150,9 +148,6 @@ func (v *Vault) RetirePass() { case !read: reset("the wanted accounts are not read whole yet, so none is known to be gone") return - case !on: - reset("nothing is retired: " + leaseWhy) - return } gone, held, err := v.goneAccounts() if err != nil { @@ -239,12 +234,7 @@ func (v *Vault) RetireAccounts(caller string, users []string, why string) (any, if len(users) == 0 { return nil, errors.New("account_retire names the accounts (`users`), as vault_status lists them waiting") } - // As the passes: never while the controller holds the lease (it forgets the accounts it mints), nor before the wanted - // accounts are read whole (an account not read yet is not known to be gone). - v.leaseAfresh() - if on, why := v.leaseHanded(); !on { - return nil, fmt.Errorf("nothing was retired: %s", why) - } + // As the passes: never before the wanted accounts are read whole (an account not read yet is not known to be gone). v.minting.mu.Lock() read := v.minting.retiring.wantsRead v.minting.mu.Unlock() diff --git a/modules/mesh-vault/cmd/mesh-vault/retire_test.go b/modules/mesh-vault/cmd/mesh-vault/retire_test.go index 2c4fe6ae..e4e4c55e 100644 --- a/modules/mesh-vault/cmd/mesh-vault/retire_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/retire_test.go @@ -11,7 +11,7 @@ import ( // more than three, or more than half of those held, at once: past that it waits for a person (account_retire). Wanted // again, it is re-enabled at once; and a retired account is not hashed again at a start. func TestAWantGoneRetiresItsAccountOnlyWithinTheBounds(t *testing.T) { - v, accounts, _ := mintingVault(t, true) + v, accounts, _ := mintingVault(t) clock := time.Date(2026, 10, 28, 9, 0, 0, 0, time.UTC) v.Now = func() time.Time { return clock } modules := []string{"m1", "m2", "m3", "m4", "m5", "m6", "m7", "m8"} @@ -65,14 +65,7 @@ func TestAWantGoneRetiresItsAccountOnlyWithinTheBounds(t *testing.T) { if _, err := v.RetireAccounts("person.jochen", []string{"ace.m2"}, "test"); err == nil { t.Error("retired by hand an account still wanted") } - // Never while the controller holds the lease, nor before the wants are read whole. - held := v.LeaseRead - v.LeaseRead = func() ([]byte, bool, error) { return []byte(`{"instance":"novox/1","epoch":57}`), true, nil } - if _, err := v.RetireAccounts("person.jochen", []string{"ace.m5"}, "the modules left"); err == nil || !strings.Contains(err.Error(), "controller holds the lease") { - t.Errorf("retired by hand under the controller's lease: %v", err) - } - v.LeaseRead = held - v.LeaseHeard([]byte(`{"handed-to":"the operator's hand-over"}`), true, nil) + // Never before the wants are read whole; no lease is asked (issue 1126). v.minting.mu.Lock() v.minting.retiring.wantsRead = false v.minting.mu.Unlock() diff --git a/modules/mesh-vault/cmd/mesh-vault/rotation.go b/modules/mesh-vault/cmd/mesh-vault/rotation.go index 9f1d6312..4322e626 100644 --- a/modules/mesh-vault/cmd/mesh-vault/rotation.go +++ b/modules/mesh-vault/cmd/mesh-vault/rotation.go @@ -4,9 +4,8 @@ package main // 2026-10-27). A module's, a process's or a provisioner client's account rotates over two credentials (ADR 0114 §10): // a second login, `--b`, carrying the first login's permissions verbatim, which the bus composer composes only while // the account's key in `bus-accounts` names it in `rotating`. The vault starts at most one rotation per revision of the -// user list the bus loaded. What composes and delivers the second login is a subtask of its own, before the lease write; -// until it is built nothing begins a rotation, and `rotate` of a covered account stays refused, so nothing rotates in -// place. +// user list the bus loaded, and no lease turns it (issue 1126). `rotate` of a covered account stays refused, so nothing +// rotates in place. import ( "fmt" @@ -42,9 +41,6 @@ func (v *Vault) BeginRotation(user string) (Rotating, error) { if nodeOrController(user) { return Rotating{}, fmt.Errorf("%s does not rotate here: a node's account and the controller's keep the hash they have (ADR 0561 §4)", user) } - if on, why := v.leaseHanded(); !on { - return Rotating{}, fmt.Errorf("no rotation begins: %s", why) - } v.minting.mu.Lock() revision := v.minting.loadedRevision v.minting.mu.Unlock() diff --git a/modules/mesh-vault/cmd/mesh-vault/rotation_integration_test.go b/modules/mesh-vault/cmd/mesh-vault/rotation_integration_test.go index 3a80e078..2db1d205 100644 --- a/modules/mesh-vault/cmd/mesh-vault/rotation_integration_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/rotation_integration_test.go @@ -53,7 +53,7 @@ func TestTheVaultWholeRotatesANodesLoginWithAStandInEngine(t *testing.T) { if err != nil { t.Fatal(err) } - for _, b := range []string{"mesh-vault_bus-accounts", "mesh-vault_node-logins", "mesh-grants_accounts", "mesh-controller_lease", + for _, b := range []string{"mesh-vault_bus-accounts", "mesh-vault_node-logins", "mesh-grants_accounts", "mesh-provisioner_plans", "node-engine_have", "mesh-delivery-assigner_placement"} { if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 2}); err != nil { t.Fatal(err) @@ -90,7 +90,8 @@ func TestTheVaultWholeRotatesANodesLoginWithAStandInEngine(t *testing.T) { } go p.serve(stdout) - // The bus node (novox, running nats) and the node that rotates (g14); the lease handed away; g14's login adopted from + // The bus node (novox, running nats) and the node that rotates (g14); no lease key anywhere (issue 1126: the controller + // ended by its unassignment); g14's login adopted from // the loaded list, its password known to the engine. for node, modules := range map[string][]string{"novox": {"nats"}, "g14": {"web"}} { pl := delivery.Placement{V: delivery.Version, Node: node} @@ -108,7 +109,6 @@ func TestTheVaultWholeRotatesANodesLoginWithAStandInEngine(t *testing.T) { } } } - p.put(t, "mesh-controller.lease", LeaseKey, map[string]any{"handed-to": "the operator's hand-over"}) oldPassword := "the-old-password-of-g14" oldHash, _ := bcrypt.GenerateFromPassword([]byte(oldPassword), bcrypt.MinCost) p.put(t, "mesh-vault.bus-accounts", "node.g14", AccountHash{User: "node.g14", Hash: string(oldHash), Adopted: true}) diff --git a/modules/mesh-vault/cmd/mesh-vault/tools.go b/modules/mesh-vault/cmd/mesh-vault/tools.go index ce181856..bdf87dde 100644 --- a/modules/mesh-vault/cmd/mesh-vault/tools.go +++ b/modules/mesh-vault/cmd/mesh-vault/tools.go @@ -307,7 +307,7 @@ func definitions(env *Env) []toolDef { {Name: Seat + ".issue-account", Description: "Mint one bus account the grants part wants, now (novox/hq ADR 0561 §9): the recovery when the vault did not " + "hear a want, and what the controller's `module issue` forwards to (issue 774). Reads the wanted key afresh from " + - "mesh-grants.accounts and mints it as a want would, whoever holds the controller's lease: never for a node's " + + "mesh-grants.accounts and mints it as a want would: never for a node's " + "account or the controller's, never a user nobody wants, and never anew for one whose value the vault holds or whose hash it adopted (it says so). Answers " + "whether it minted, the secret and generation, never the value or the hash.", Input: map[string]any{"user": str("the bus user, e.g. ace.mesh-build-go")}, @@ -645,7 +645,6 @@ func definitions(env *Env) []toolDef { Description: "What needs a person's eye in the vault: operator copies still sealed to an earlier key, the operator " + "key the mesh's export holds if the vault refused it, how often the record of hand-outs was rolled, and " + "minting (novox/hq ADR 0561 §9, issue 774): the vault mints every wanted bus account it holds no value for, " + - "whoever holds the controller's lease; whether that lease is handed away (it turns retirement and rotation), " + "every wanted account it holds back with why (never one whose value it holds), and those it minted.", run: func(string, map[string]any) (any, error) { if env.Vault == nil { diff --git a/modules/mesh-vault/cmd/mesh-vault/vault.go b/modules/mesh-vault/cmd/mesh-vault/vault.go index 010e2808..531e8349 100644 --- a/modules/mesh-vault/cmd/mesh-vault/vault.go +++ b/modules/mesh-vault/cmd/mesh-vault/vault.go @@ -69,9 +69,7 @@ type Vault struct { WantRead func(user string) ([]byte, bool, error) // Placement reads the seat's placement (placed.go): where modules stand once the assignments are gone. Placement PlacementReader - // LeaseRead reads the controller's lease afresh before a mint (lease.go): nil reads it only as the watch hands it. - LeaseRead func() ([]byte, bool, error) - // minting is the switch on the lease and what the vault knows of the wanted bus accounts (mint.go). + // minting is what the vault knows of the wanted bus accounts (mint.go). minting minting // Signers is the vault's state `signers`, where it publishes its signing halves (signing.go, ADR 0550 §2): nil writes // none. diff --git a/modules/mesh-vault/cmd/mesh-vault/vault_test.go b/modules/mesh-vault/cmd/mesh-vault/vault_test.go index 5bda6d41..e380ed56 100644 --- a/modules/mesh-vault/cmd/mesh-vault/vault_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/vault_test.go @@ -135,7 +135,7 @@ func filesUnder(t *testing.T, dir string) map[string]string { // (ADR 0547 clause 9), opened in accounts.go alone. It reads five buckets of others', and only reads them: the // seat mesh-provisioner's plans, for what each node's plan names (ADR 0570 clause 2), (askers.go, design 57 V4), the node-engines' haves, where // the bus node hands the loaded list's hashes sealed to the vault (adopt.go, ADR 0547 clause 9), the grants part's wanted -// accounts and the controller's lease (mint.go, lease.go, ADR 0561 §2, §3), and the seat's placement (placed.go), in +// accounts (mint.go, ADR 0561 §2), and the seat's placement (placed.go), in // main.go alone. func TestNoBucketHoldsASecret(t *testing.T) { raw, err := os.ReadFile("../../module.json") @@ -151,9 +151,9 @@ func TestNoBucketHoldsASecret(t *testing.T) { "buckets are the accounts' hashes (ADR 0547 clause 9), the signing keys' public halves (ADR 0550 §2) and the "+ "credentials' outcomes, names and generations alone (ADR 0570 clauses 3, 6) and each node's login in use (ADR 0583)", s) } - if r, _ := json.Marshal(manifest["reads"]); string(r) != `["node-engine.have","mesh-grants.accounts","mesh-controller.lease","mesh-delivery-assigner.placement","mesh-provisioner.plans","mesh-provisioner.provisions"]` { + if r, _ := json.Marshal(manifest["reads"]); string(r) != `["node-engine.have","mesh-grants.accounts","mesh-delivery-assigner.placement","mesh-provisioner.plans","mesh-provisioner.provisions"]` { t.Errorf("the manifest reads %s: the vault reads the seat mesh-provisioner's plans and provisions, the haves, the "+ - "grants part's wanted accounts, the lease and the placement alone", r) + "grants part's wanted accounts and the placement alone, never the controller's lease (issue 1126)", r) } sources, _ := filepath.Glob("*.go") bucket := regexp.MustCompile(`stdio\.State\(|nats-io|jetstream|KeyValue`) @@ -195,9 +195,6 @@ func TestNoBucketHoldsASecret(t *testing.T) { code = []byte(strings.Replace(string(code), "stdio.State(HaveRead).Keys(", "", 1)) code = []byte(strings.Replace(string(code), "stdio.State(WantedRead).Watch(", "", 1)) code = []byte(strings.Replace(string(code), "stdio.State(WantedRead).Get(", "", 1)) - // And the controller's lease, one read and one watch (ADR 0561 §3). - code = []byte(strings.Replace(string(code), "stdio.State(LeaseRead).Get(", "", 1)) - code = []byte(strings.Replace(string(code), "stdio.State(LeaseRead).Watch(", "", 1)) // And the seat's placement, where modules stand (placed.go): its keys and one key, read. code = []byte(strings.Replace(string(code), "stdio.State(PlacementRead).Keys(", "", 1)) code = []byte(strings.Replace(string(code), "stdio.State(PlacementRead).Get(", "", 1)) @@ -922,7 +919,7 @@ func TestTheRecordOfHandOutsIsBounded(t *testing.T) { // broker-nats-tools, wanted as a process account, has the random value the controller moved in replaced by a minted // account, and a value that is no account is refused for it from then on. func TestABusAccountIsKeptOnlyAsAnAccount(t *testing.T) { - v, accounts, _ := mintingVault(t, true) + v, accounts, _ := mintingVault(t) const tools, address = "own/novox/nats/broker-nats-tools", "own/novox/mesh-controller/broker-address" for _, row := range []struct { name, value string diff --git a/modules/mesh-vault/module.json b/modules/mesh-vault/module.json index 7e3d2cc3..dcfa8eb1 100644 --- a/modules/mesh-vault/module.json +++ b/modules/mesh-vault/module.json @@ -32,7 +32,6 @@ "reads": [ "node-engine.have", "mesh-grants.accounts", - "mesh-controller.lease", "mesh-delivery-assigner.placement", "mesh-provisioner.plans", "mesh-provisioner.provisions"