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
This commit is contained in:
2026-11-02 10:35:59 +01:00
parent db104bdb08
commit fe7d4504bf
15 changed files with 50 additions and 198 deletions
@@ -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
@@ -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)
}
+1 -33
View File
@@ -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)
}
+13 -45
View File
@@ -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()
@@ -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)
+14 -19
View File
@@ -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 `<user>--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)
@@ -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
+3 -13
View File
@@ -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()
@@ -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()
@@ -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, `<user>--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()
@@ -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})
+1 -2
View File
@@ -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 {
+1 -3
View File
@@ -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.
@@ -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
-1
View File
@@ -32,7 +32,6 @@
"reads": [
"node-engine.have",
"mesh-grants.accounts",
"mesh-controller.lease",
"mesh-delivery-assigner.placement",
"mesh-provisioner.plans",
"mesh-provisioner.provisions"