Compare commits

..
25 changed files with 2315 additions and 1936 deletions
+1 -11
View File
@@ -53,8 +53,6 @@ type AccountHash struct {
Adopted bool `json:"adopted,omitempty"`
From string `json:"from,omitempty"`
Node string `json:"node,omitempty"`
// Rotating is a rotation under way over a second login (rotation.go, ADR 0561 §4): absent when none is.
Rotating *Rotating `json:"rotating,omitempty"`
}
// AccountStore is the vault's state of bus-account hashes: read and written by the vault's instances alone.
@@ -63,8 +61,6 @@ type AccountStore interface {
Put(a AccountHash) error
// Users is every user with a key.
Users() ([]string, error)
// Delete takes a user's key out: an account retired (retire.go).
Delete(user string) error
}
// busAccounts is AccountStore over the vault's own state through its runtime.
@@ -84,8 +80,6 @@ func (busAccounts) Get(user string) (*AccountHash, error) {
func (busAccounts) Users() ([]string, error) { return stdio.State(BusAccountsState).Keys() }
func (busAccounts) Delete(user string) error { return stdio.State(BusAccountsState).Delete(user) }
func (busAccounts) Put(a AccountHash) error {
_, err := stdio.State(BusAccountsState).Put(a.User, a)
return err
@@ -187,10 +181,6 @@ func (v *Vault) HashAccount(secret string, force bool) (Hashed, error) {
if err != nil {
return Hashed{}, err
}
if rec.Retired != nil {
v.hashes.paid(secret)
return Hashed{}, fmt.Errorf("%s is retired (%s): its hash is written again only when it is wanted again", secret, rec.Retired.Why)
}
user, password, err := accountOf(secret, value)
if err != nil {
return Hashed{}, err
@@ -269,7 +259,7 @@ func (v *Vault) HashHeld() (written []Hashed, failed map[string]string) {
return nil, failed
}
for _, r := range records {
if !Covered(r.Name) || r.Retired != nil {
if !Covered(r.Name) {
continue
}
h, err := v.HashAccount(r.Name, false)
+26 -3
View File
@@ -58,6 +58,32 @@ type adoptionKept struct {
failed string // why the last look found nothing to adopt, or ""
}
// busNode is the node the assignments name as holding the bus module: exactly one.
func (v *Vault) busNode() (string, error) {
if v.Assignments.Keys == nil {
return "", errors.New("the vault cannot read the assignments, so it cannot tell which node holds the bus")
}
keys, err := v.Assignments.Keys()
if err != nil {
return "", fmt.Errorf("the assignments' keys could not be read: %w", err)
}
var nodes []string
for _, k := range keys {
if node, module, ok := strings.Cut(k, "."); ok && module == BusModule {
nodes = append(nodes, node)
}
}
sort.Strings(nodes)
switch len(nodes) {
case 1:
return nodes[0], nil
case 0:
return "", fmt.Errorf("no node is assigned the bus module %s", BusModule)
}
return "", fmt.Errorf("%d nodes are assigned the bus module %s (%s): the vault adopts from one alone", len(nodes),
BusModule, strings.Join(nodes, ", "))
}
// AdoptNow is the tool account_adopt: the bus node's have read now and adopted, though it says the same as before.
func (v *Vault) AdoptNow(caller string) (Adoption, error) {
if v.Haves == nil {
@@ -117,9 +143,6 @@ func (v *Vault) adopt(node string, raw []byte, by string, force bool) (Adoption,
if err != nil {
return Adoption{}, fmt.Errorf("the have of %s on %s is not a have: %w", BusModule, node, err)
}
if have.UserList != nil {
v.LoadedListHeard(have.UserList.Revision)
}
if have.UserList == nil || have.UserList.Handed == nil {
return Adoption{}, fmt.Errorf("the have of %s on %s hands no hashes yet (its node-engine hands them once it "+
"heard the vault's key in its answer to node-key, and writes the list)", BusModule, node)
@@ -48,13 +48,6 @@ func (f *fakeAccounts) Put(a AccountHash) error {
return nil
}
func (f *fakeAccounts) Delete(user string) error {
f.mu.Lock()
defer f.mu.Unlock()
delete(f.keys, user)
return nil
}
func (f *fakeAccounts) Users() ([]string, error) {
f.mu.Lock()
defer f.mu.Unlock()
@@ -1,49 +0,0 @@
package main
// The switch on the controller's lease (novox/hq ADR 0561 §3): minting is on only while the lease is handed to a holder
// that is no controller, and off for a controller's value, a key deleted or expired, or one that cannot be read. The
// write that hands it away turns it on and mints every wanted account the vault holds no value for; a lease handed back
// turns it off. No clock and no setting turns it. A key's age passing is heard by no watch, so the lease is read afresh
// before every mint (LeaseRead): a lease that expired since mints nothing.
import "git.novox.be/novox/mesh-sdk/go/buscompose"
// LeaseHeard is the watch of the controller's lease. When minting turns on or off, every wanted account is judged again:
// minted when on, held back with the new why when off.
func (v *Vault) LeaseHeard(raw []byte, found bool, readErr error) {
if !v.setLease(raw, found, readErr) {
return
}
on, why := v.mintingOn()
v.log("minting is %s: %s", map[bool]string{true: "ON", false: "off"}[on], why)
for _, user := range v.wantedUsers() {
if w, ok := v.wanted(user); ok {
if _, err := v.mintWanted(w, "the lease write"); err != nil {
v.log("%s is not minted: %v", user, err)
}
}
}
}
// 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 mint, 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 -82
View File
@@ -51,8 +51,7 @@ func build() *Env {
// Where it sees a consumer bind a provider (pair.go): the same mirror, every key.
env.Vault = &Vault{Store: store, Own: own, Askers: Declared{Read: 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,
Placement: placement}
Now: time.Now, Log: say, Accounts: busAccounts{}, Haves: readHave}
return env
}
@@ -96,74 +95,6 @@ func watchHandings(env *Env) bool {
return true
}
// watchWants mints from the grants part's wanted accounts as they change (mint.go, ADR 0561 §2): a watch, never a poll.
// It answers whether the watch began; one that did not is begun again at the next minute pass.
func watchWants(env *Env) bool {
if env.Vault == nil {
return true
}
err := stdio.State(WantedRead).Watch("", func(c stdio.StateChange) error {
env.Vault.WantHeard(c.Key, c.Value, c.Op == "delete" || c.Op == "purge" || len(c.Value) == 0)
return nil
})
if err != nil {
say("the grants part's wanted accounts are not watched, so nothing is minted; begun again at the next pass: %v", err)
return false
}
// The watch returns once every current key was handed: what has no key now is no longer wanted.
env.Vault.WantsRead()
return true
}
// placement reads the indexer's placement through the runtime under the vault's own account (its manifest reads it).
var placement = PlacementReader{
Keys: func() ([]string, error) { return stdio.State(PlacementRead).Keys() },
Read: func(key string) ([]byte, error) {
e, err := stdio.State(PlacementRead).Get(key)
if err != nil || e == nil {
return nil, err
}
return e.Value, nil
},
}
// readWant is one wanted account of the grants part, read afresh (issue-account).
func readWant(user string) ([]byte, bool, error) {
e, err := stdio.State(WantedRead).Get(user)
if err != nil || e == nil {
return nil, false, err
}
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
@@ -217,26 +148,14 @@ func main() {
written, failed := env.Vault.HashHeld()
say("hashed %d bus accounts at start; %d could not be, tried again every minute: %v", len(written), len(failed), failed)
}
// The operator key first: nothing is kept without it, so a want heard before it would not be minted.
env.pass()
watched := watchHandings(env)
lease := watchLease(env)
wants := watchWants(env)
for {
if !watched {
watched = watchHandings(env)
}
if !lease {
lease = watchLease(env)
}
if !wants {
wants = watchWants(env)
}
env.pass()
if env.Vault != nil {
env.Vault.HashOwed()
env.Vault.MintOwed()
env.Vault.RetirePass()
}
env.pairPass()
if env.Custody != nil {
-376
View File
@@ -1,376 +0,0 @@
package main
// The vault mints every new bus account (novox/hq ADR 0561 §1 to §4, issue 774).
//
// The grants part says which accounts the mesh wants, in its state `accounts` that the vault alone reads (§2): one key
// per bus user, holding what the controller's credential carries beside the password. The vault watches it, and for a
// user it holds no value for and whose hash it did not adopt it makes a password, keeps the account whole (built by
// mesh-sdk's HeldCredential, byte for byte the controller's shape) as the module's own secret on that node
// (`own/<node>/<module>/<broker|broker-<process>|provisioner-broker>`), sealed, with the operator's copy; writes the
// bcrypt hash into `bus-accounts`; and says `account-issued`, then `secret-changed`. The node's engine asks `own` again
// on `secret-changed` and composes.
//
// **One issuer at every moment** (§3): minting is on only while the controller's lease is handed to a holder that is no
// controller (mesh-sdk LeaseHandedAway). Until then the controller mints, the vault keeps hashing what it is handed, and
// vault_status names every wanted account it holds back and why. **Nothing is minted anew** (§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.
// **Never two values for one user** (§7): a user whose hash the vault already wrote from another of its secrets is held
// back, said, and never replaced but by a rotation.
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"sync"
"time"
"git.novox.be/novox/mesh-sdk/go/buscompose"
)
// 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 the switch and 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 and the lease are the record, never this.
type minting struct {
mu sync.Mutex
on bool
why string
since time.Time
// 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).
heldBack map[string]string
// owed are the wanted users whose mint failed, tried again at the pass (MintOwed).
owed map[string]bool
// minted are the accounts this vault minted since it started, by user (vault_status).
minted map[string]Minted
// loadedRevision is the highest revision of the user list the bus node said it loaded (rotation.go).
loadedRevision uint64
// retiring is what the passes keep of the accounts no longer wanted (retire.go).
retiring retiring
// mintMu keeps one mint at a time: a want heard, the switch turning on, and issue-account.
mintMu sync.Mutex
}
// Minted is what one mint did: the account and its secret, never its value or its hash.
type Minted struct {
User string `json:"user"`
Secret string `json:"secret"`
Generation int `json:"generation"`
At time.Time `json:"at"`
// Hashed says its hash is written; false leaves it owed, written at the next pass.
Hashed bool `json:"hashed"`
}
// MintState is what vault_status says of minting.
type MintState struct {
On bool `json:"on"`
Why string `json:"why"`
Since time.Time `json:"since,omitzero"`
Wanted int `json:"wanted"`
HeldBack map[string]string `json:"held_back"`
Minted []Minted `json:"minted"`
}
// MintState answers how minting stands.
func (v *Vault) MintState() MintState {
m := &v.minting
m.mu.Lock()
defer m.mu.Unlock()
out := MintState{On: m.on, Why: m.why, Since: m.since, Wanted: len(m.wants), HeldBack: map[string]string{}, Minted: []Minted{}}
if out.Why == "" {
out.Why = "the controller's lease has not been read yet, so nothing is minted"
}
for u, why := range m.heldBack {
out.HeldBack[u] = why
}
for _, x := range m.minted {
out.Minted = append(out.Minted, x)
}
sort.Slice(out.Minted, func(i, j int) bool { return out.Minted[i].User < out.Minted[j].User })
return out
}
// mintingOn says whether the vault mints now, and why.
func (v *Vault) mintingOn() (bool, string) {
v.minting.mu.Lock()
defer v.minting.mu.Unlock()
if v.minting.why == "" {
return false, "the controller's lease has not been read yet, so nothing is minted"
}
return v.minting.on, v.minting.why
}
func (v *Vault) holdBack(user, why string) {
m := &v.minting
m.mu.Lock()
defer m.mu.Unlock()
if m.heldBack == nil {
m.heldBack = map[string]string{}
}
m.heldBack[user] = why
}
func (v *Vault) noLongerHeld(user string) {
m := &v.minting
m.mu.Lock()
defer m.mu.Unlock()
delete(m.heldBack, user)
}
// AccountSecret is the vault's name of the own secret a wanted account is held in.
func AccountSecret(w buscompose.WantedAccount) (string, error) {
return OwnSecret(w.Node, w.Module, w.OwnSecret())
}
// WantHeard is the watch of the grants part's wanted accounts: a key put is a want, minted when minting is on; a key
// deleted is a want gone (retire.go).
func (v *Vault) WantHeard(user string, raw []byte, deleted bool) {
if deleted {
v.wantGone(user)
return
}
w, err := buscompose.ReadWantedAccount(user, raw)
if err != nil {
v.log("the wanted account %s is not read, so nothing is minted for it: %v", user, err)
v.holdBack(user, "its wanted key is not readable: "+err.Error())
return
}
v.minting.mu.Lock()
if v.minting.wants == nil {
v.minting.wants = map[string]buscompose.WantedAccount{}
}
v.minting.wants[user] = w
v.minting.mu.Unlock()
v.wantBack(user)
if _, err := v.mintWanted(w, "the grants part's want of "+user); err != nil {
v.log("%s is not minted: %v", user, err)
}
}
// errHeldBack is a want the vault does not mint now, with why: not an error of the vault.
type errHeldBack struct{ why string }
func (e errHeldBack) Error() string { return e.why }
// mintWanted mints w when minting is on, the vault holds no value for it and adopted no hash of it; otherwise it says
// why not and, where that is a hold, keeps it for vault_status.
func (v *Vault) mintWanted(w buscompose.WantedAccount, by string) (Minted, error) {
m, err := v.mintOnce(w, by)
var held errHeldBack
v.owe(w.User, err != nil && !errors.As(err, &held) && w.Valid() == nil)
return m, err
}
// mintOnce is one try of mintWanted.
func (v *Vault) mintOnce(w buscompose.WantedAccount, by string) (Minted, error) {
v.minting.mintMu.Lock()
defer v.minting.mintMu.Unlock()
if err := w.Valid(); err != nil {
v.holdBack(w.User, err.Error())
return Minted{}, err
}
secret, err := AccountSecret(w)
if err != nil {
v.holdBack(w.User, err.Error())
return Minted{}, err
}
if _, held, err := v.Store.Read(secret); err != nil {
return Minted{}, err
} else if held {
// Its value is held (the controller's, handed in, or one the vault minted): never minted anew (§4).
v.noLongerHeld(w.User)
return Minted{}, errHeldBack{secret + " is held already: an account the vault holds a value for is never minted anew (ADR 0561 §4)"}
}
if v.Accounts == nil {
v.holdBack(w.User, "the vault has no state of bus accounts to write the hash to")
return Minted{}, errors.New("the vault has no state of bus accounts to write the hash to")
}
was, err := v.Accounts.Get(w.User)
if err != nil {
v.holdBack(w.User, "its key in "+BusAccountsState+" cannot be read: "+err.Error())
return Minted{}, err
}
if was != nil && was.Adopted {
// The hash the bus loaded is kept (§4): not minted, and not held back either.
v.noLongerHeld(w.User)
return Minted{}, errHeldBack{w.User + "'s hash was adopted from the list the bus loaded: it keeps it, and is never minted (ADR 0561 §4)"}
}
if was != nil && was.Of != "" && was.Of != secret {
why := fmt.Sprintf("%s holds the hash of %s for %s, not of %s: a user has one value, so nothing is minted until "+
"a person sees why (ADR 0561 §7)", BusAccountsState, was.Of, w.User, secret)
v.holdBack(w.User, why)
return Minted{}, errHeldBack{why}
}
v.leaseAfresh()
if on, why := v.mintingOn(); !on {
v.holdBack(w.User, "held back: "+why)
return Minted{}, errHeldBack{why}
}
raw := make([]byte, 30)
if _, err := rand.Read(raw); err != nil {
return Minted{}, err
}
value, err := buscompose.HeldCredential(w, base64.RawURLEncoding.EncodeToString(raw))
if err != nil {
v.holdBack(w.User, err.Error())
return Minted{}, err
}
c, err := v.keepLocked(secret, string(value), "the vault", "", OriginMade, keepOptions{neverReplace: true, quiet: true})
if err != nil {
v.holdBack(w.User, "its value could not be kept: "+err.Error())
return Minted{}, err
}
out := Minted{User: w.User, Secret: secret, Generation: c.Generation, At: c.At}
// The hash first, then account-issued, then secret-changed: the grants part composes the user before the node asks.
if _, err := v.HashAccount(secret, false); err != nil {
v.hashes.owe(secret, err.Error())
v.log("%s is minted and kept, and ITS HASH IS NOT WRITTEN, written at the next pass: %v", w.User, err)
} else {
out.Hashed = true
if v.Emit != nil {
e := buscompose.AccountIssued{User: w.User, Kind: w.Kind, Node: w.Node, Module: w.Module, Process: w.Process,
Generation: c.Generation, At: c.At}
body := map[string]any{}
b, _ := json.Marshal(e)
_ = json.Unmarshal(b, &body)
if err := v.Emit(AccountIssuedEvent, body); err != nil {
v.log("%s is minted and hashed, but %s could not be said: %v", w.User, AccountIssuedEvent, err)
}
}
}
v.announce(c)
v.minting.mu.Lock()
if v.minting.minted == nil {
v.minting.minted = map[string]Minted{}
}
v.minting.minted[w.User] = out
v.minting.mu.Unlock()
v.noLongerHeld(w.User)
v.log("minted the bus account %s as %s (generation %d), for %s", w.User, secret, c.Generation, by)
return out, nil
}
// MintOwed tries again, at the vault's minute pass, every wanted account whose mint failed (its value could not be kept,
// its key read, its password made): a want heard is never lost to one refusal. A want held back is not owed: the switch
// or the want changing judges it again.
func (v *Vault) MintOwed() {
v.minting.mu.Lock()
var owed []string
for u := range v.minting.owed {
owed = append(owed, u)
}
v.minting.mu.Unlock()
sort.Strings(owed)
for _, u := range owed {
if w, ok := v.wanted(u); ok {
if _, err := v.mintWanted(w, "the pass, after a mint that failed"); err != nil {
v.log("%s is still not minted: %v", u, err)
}
} else {
v.owe(u, false)
}
}
}
// owe marks user's mint as failed (tried again at the pass) or not.
func (v *Vault) owe(user string, owed bool) {
v.minting.mu.Lock()
defer v.minting.mu.Unlock()
if !owed {
delete(v.minting.owed, user)
return
}
if v.minting.owed == nil {
v.minting.owed = map[string]bool{}
}
v.minting.owed[user] = true
}
// heldBackOrMinted is a mint's answer as a tool gives it.
func heldBackOrMinted(m Minted, err error) (any, error) {
var held errHeldBack
if errors.As(err, &held) {
return map[string]any{"minted": false, "why": held.why}, nil
}
if err != nil {
return nil, err
}
return map[string]any{"minted": true, "account": m}, nil
}
// IssueAccount is the seat's verb issue-account (ADR 0561 §9): one wanted user minted now, its key read afresh from the
// grants part's state. The recovery when a want was not heard: it mints nothing a want would not, and is refused while
// minting is off, for a node's account and the controller's (never minted here, §4), and for a user nobody wants.
func (v *Vault) IssueAccount(caller, user string) (any, error) {
user = strings.TrimSpace(user)
if user == "" {
return nil, errors.New("issue-account names a bus user (`user`), e.g. ace.mesh-build-go")
}
if nodeOrController(user) {
return nil, fmt.Errorf("%s is never minted by the vault: a node's account and the controller's keep the hash they "+
"have (ADR 0561 §4)", user)
}
v.leaseAfresh()
if on, why := v.mintingOn(); !on {
return nil, fmt.Errorf("not minted: %s", why)
}
raw, found, err := []byte(nil), false, error(nil)
if v.WantRead != nil {
raw, found, err = v.WantRead(user)
} else if w, ok := v.wanted(user); ok {
raw, err = json.Marshal(w)
found = err == nil
}
if err != nil {
return nil, fmt.Errorf("the wanted account %s could not be read: %w", user, err)
}
if !found {
return nil, fmt.Errorf("the grants part holds no wanted account %s: the vault mints only what placement composes "+
"(grants_accounts lists them)", user)
}
w, err := buscompose.ReadWantedAccount(user, raw)
if err != nil {
return nil, err
}
v.log("issue-account %s, asked by %s", user, someoneOr("", caller))
return heldBackOrMinted(v.mintWanted(w, "issue-account by "+someoneOr("", caller)))
}
// wantedUsers is every wanted user heard, sorted.
func (v *Vault) wantedUsers() []string {
v.minting.mu.Lock()
defer v.minting.mu.Unlock()
out := make([]string, 0, len(v.minting.wants))
for u := range v.minting.wants {
out = append(out, u)
}
sort.Strings(out)
return out
}
func (v *Vault) wanted(user string) (buscompose.WantedAccount, bool) {
v.minting.mu.Lock()
defer v.minting.mu.Unlock()
w, ok := v.minting.wants[user]
return w, ok
}
// nodeOrController says user is a node's account or the controller's: never minted here (ADR 0561 §4, §9).
func nodeOrController(user string) bool {
return user == ControllerCaller || strings.HasPrefix(user, "node.")
}
@@ -1,422 +0,0 @@
//go:build integration
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 only once the controller's lease is handed away: 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, and a lease handed back mints nothing.
import (
"bufio"
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/nacl/box"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/seal"
"git.novox.be/novox/mesh-sdk/go/testbus"
)
// wire is one line between the runtime and the bundle (the SDK's stdio message).
type wire struct {
ID json.RawMessage `json:"id,omitempty"`
Method string `json:"method,omitempty"`
Params json.RawMessage `json:"params,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
Error *struct {
Code int `json:"code"`
Message string `json:"message"`
} `json:"error,omitempty"`
}
// player is the node's tool runtime as far as the vault needs it, over a real bus.
type player struct {
t *testing.T
ctx context.Context
nc *nats.Conn
js jetstream.JetStream
mu sync.Mutex
in io.Writer
next int
pending map[string]chan wire
said []string
}
// bucketOf is the bucket a state names: the vault's own by its local name, another's as `<owner>.<name>`.
func bucketOf(state string) string {
if owner, name, ok := strings.Cut(state, "."); ok {
return owner + "_" + name
}
return "mesh-vault_" + state
}
func (p *player) send(m map[string]any) {
raw, _ := json.Marshal(m)
p.mu.Lock()
defer p.mu.Unlock()
_, _ = p.in.Write(append(raw, '\n'))
}
func (p *player) answer(id json.RawMessage, result any, err error) {
if err != nil {
p.send(map[string]any{"id": id, "error": map[string]any{"code": -32000, "message": err.Error()}})
return
}
p.send(map[string]any{"id": id, "result": result})
}
func (p *player) kv(state string) (jetstream.KeyValue, error) {
return p.js.KeyValue(p.ctx, bucketOf(state))
}
// serve answers what the bundle asks, and hands the answers to what the test asked it.
func (p *player) serve(out io.Reader) {
scanner := bufio.NewScanner(out)
scanner.Buffer(make([]byte, 1<<20), 1<<24)
for scanner.Scan() {
var m wire
if json.Unmarshal(scanner.Bytes(), &m) != nil {
continue
}
if m.Method == "" {
var key string
_ = json.Unmarshal(m.ID, &key)
p.mu.Lock()
ch := p.pending[key]
delete(p.pending, key)
p.mu.Unlock()
if ch != nil {
ch <- m
}
continue
}
go p.handle(m)
}
}
func (p *player) handle(m wire) {
var a struct {
State string `json:"state"`
Key string `json:"key"`
Value json.RawMessage `json:"value"`
Body json.RawMessage `json:"body"`
}
_ = json.Unmarshal(m.Params, &a)
switch m.Method {
case "mesh/state.get":
kv, err := p.kv(a.State)
if err != nil {
p.answer(m.ID, nil, err)
return
}
e, err := kv.Get(p.ctx, a.Key)
if errors.Is(err, jetstream.ErrKeyNotFound) {
p.answer(m.ID, nil, nil)
return
}
if err != nil {
p.answer(m.ID, nil, err)
return
}
p.answer(m.ID, map[string]any{"key": e.Key(), "value": json.RawMessage(e.Value()), "revision": e.Revision()}, nil)
case "mesh/state.put":
kv, err := p.kv(a.State)
if err == nil {
var rev uint64
rev, err = kv.Put(p.ctx, a.Key, a.Value)
if err == nil {
p.answer(m.ID, map[string]any{"revision": rev}, nil)
return
}
}
p.answer(m.ID, nil, err)
case "mesh/state.keys":
kv, err := p.kv(a.State)
if err != nil {
p.answer(m.ID, nil, err)
return
}
keys, err := kv.Keys(p.ctx)
if errors.Is(err, jetstream.ErrNoKeysFound) {
keys, err = []string{}, nil
}
p.answer(m.ID, keys, err)
case "mesh/state.delete":
kv, err := p.kv(a.State)
if err == nil {
err = kv.Delete(p.ctx, a.Key)
}
p.answer(m.ID, map[string]any{}, err)
case "mesh/state.watch":
kv, err := p.kv(a.State)
if err != nil {
p.answer(m.ID, nil, err)
return
}
key := strings.ReplaceAll(a.Key, "**", ">")
if key == "" {
key = ">"
}
w, err := kv.Watch(p.ctx, key)
if err != nil {
p.answer(m.ID, nil, err)
return
}
var watch string
_ = json.Unmarshal(m.ID, &watch)
current := true
for e := range w.Updates() {
if e == nil {
current = false
p.answer(m.ID, map[string]any{}, nil)
continue
}
op := "put"
if e.Operation() != jetstream.KeyValuePut {
op = "delete"
}
change := map[string]any{"state": a.State, "key": e.Key(), "op": op, "revision": e.Revision(), "current": current}
if op == "put" {
change["value"] = json.RawMessage(e.Value())
}
p.mu.Lock()
p.next++
id := fmt.Sprintf("w-%d", p.next)
p.mu.Unlock()
p.send(map[string]any{"id": id, "method": "mesh/state", "params": map[string]any{"watch": watch, "change": change}})
}
case "mesh/publish":
var e struct {
Key string `json:"key"`
Body json.RawMessage `json:"body"`
}
_ = json.Unmarshal(m.Params, &e)
p.mu.Lock()
p.said = append(p.said, e.Key+" "+string(e.Body))
p.mu.Unlock()
p.answer(m.ID, map[string]any{}, p.nc.Publish("mesh.mod.mesh-vault.event."+e.Key, e.Body))
default:
// mesh/ask and the rest: nothing answers on this bus, as a call with no responder.
p.answer(m.ID, nil, errors.New("nats: no responders available for request"))
}
}
// call is a tools/call of the vault as caller, answered as the tool's JSON.
func (p *player) call(name, caller string, args map[string]any) (json.RawMessage, error) {
p.mu.Lock()
p.next++
id := fmt.Sprintf("c-%d", p.next)
ch := make(chan wire, 1)
p.pending[id] = ch
p.mu.Unlock()
p.send(map[string]any{"id": id, "method": "tools/call", "params": map[string]any{"name": name, "arguments": args,
"_meta": map[string]any{"mesh/caller": caller}}})
select {
case m := <-ch:
var r struct {
Content []struct {
Text string `json:"text"`
} `json:"content"`
IsError bool `json:"isError"`
}
if m.Error != nil {
return nil, errors.New(m.Error.Message)
}
if err := json.Unmarshal(m.Result, &r); err != nil || len(r.Content) != 1 {
return nil, fmt.Errorf("%s answered %s", name, m.Result)
}
if r.IsError {
return nil, errors.New(r.Content[0].Text)
}
return json.RawMessage(r.Content[0].Text), nil
case <-time.After(30 * time.Second):
return nil, fmt.Errorf("%s did not answer in 30s", name)
}
}
func (p *player) put(t *testing.T, state, key string, v any) {
t.Helper()
kv, err := p.kv(state)
if err != nil {
t.Fatal(err)
}
raw, _ := json.Marshal(v)
if _, err := kv.Put(p.ctx, key, raw); err != nil {
t.Fatal(err)
}
}
func (p *player) hashOf(t *testing.T, user string) *AccountHash {
t.Helper()
kv, err := p.kv(BusAccountsState)
if err != nil {
t.Fatal(err)
}
e, err := kv.Get(p.ctx, user)
if errors.Is(err, jetstream.ErrKeyNotFound) {
return nil
}
if err != nil {
t.Fatal(err)
}
var a AccountHash
if err := json.Unmarshal(e.Value(), &a); err != nil {
t.Fatal(err)
}
return &a
}
func eventually(t *testing.T, what string, ok func() bool) {
t.Helper()
for end := time.Now().Add(30 * time.Second); time.Now().Before(end); time.Sleep(50 * time.Millisecond) {
if ok() {
return
}
}
t.Fatalf("never: %s", what)
}
// 774/AC12.
func TestTheVaultWholeMintsOnTheBusOnlyFromTheLeaseWrite(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
dir := t.TempDir()
bin := filepath.Join(dir, "mesh-vault")
build := exec.Command("go", "build", "-o", bin, ".")
build.Stderr = os.Stderr
if err := build.Run(); err != nil {
t.Fatalf("the vault does not build: %v", err)
}
nc, err := nats.Connect(testbus.URL(t))
if err != nil {
t.Fatal(err)
}
t.Cleanup(nc.Close)
js, err := jetstream.New(nc)
if err != nil {
t.Fatal(err)
}
for _, b := range []string{"mesh-vault_bus-accounts", "mesh-grants_accounts", "mesh-controller_lease",
"mesh-controller_assignments", "node-engine_have", "mesh-delivery-indexer_placement"} {
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 2}); err != nil {
t.Fatal(err)
}
}
// The operator key, as the mesh exports it to the vault's root.
opPub, _, err := box.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
root := filepath.Join(dir, "root")
_ = os.MkdirAll(root, 0o700)
export, _ := json.Marshal(map[string]string{"operator-key": base64.StdEncoding.EncodeToString(opPub[:])})
if err := os.WriteFile(filepath.Join(root, "export.json"), export, 0o600); err != nil {
t.Fatal(err)
}
cmd := exec.CommandContext(ctx, bin)
cmd.Env = append(os.Environ(), "MESH_VAULT_STORE="+filepath.Join(dir, "store"), "MESH_VAULT_STATE="+filepath.Join(dir, "state"),
"MESH_VAULT_ROOT="+root, "MESH_VAULT_LEDGER="+filepath.Join(dir, "ledger"), "MESH_RECEIVES="+filepath.Join(dir, "mesh.json"),
"MESH_NODE=novox", "MESH_MODULE=mesh-vault")
stdin, _ := cmd.StdinPipe()
stdout, _ := cmd.StdoutPipe()
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = stdin.Close(); cancel(); _ = cmd.Wait() })
p := &player{t: t, ctx: ctx, nc: nc, js: js, in: stdin, pending: map[string]chan wire{}}
go p.serve(stdout)
status := func() MintState {
raw, err := p.call("vault_status", "person.jochen", nil)
if err != nil {
return MintState{}
}
var s struct {
Minting MintState `json:"minting"`
}
_ = json.Unmarshal(raw, &s)
return s.Minting
}
// The controller holds the lease: a want is held back, nothing is minted.
p.put(t, "mesh-controller.lease", LeaseKey, map[string]any{"instance": "novox/1/2026-10-27", "epoch": 57})
x, y, z := aWant("mod-x"), aWant("mod-y"), aWant("mod-z")
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 want held back under the controller's lease", func() bool {
return strings.Contains(status().HeldBack[x.User], "controller holds the lease")
})
if p.hashOf(t, x.User) != nil {
t.Fatal("minted under the controller's lease")
}
// The write that hands the lease away mints it.
p.put(t, "mesh-controller.lease", LeaseKey, map[string]any{"handed-to": "the operator's hand-over"})
eventually(t, "the account minted at the lease write", func() bool { return p.hashOf(t, x.User) != nil })
nodePub, nodePriv := aSealingKey(t)
if _, err := p.call(Seat+".node-key", "node.ace", map[string]any{"key": nodePub}); err != nil {
t.Fatal(err)
}
raw, err := p.call(Seat+".own", "node.ace", map[string]any{})
if err != nil {
t.Fatal(err)
}
var own OwnAnswer
if err := json.Unmarshal(raw, &own); err != nil {
t.Fatal(err)
}
if len(own.Own) != 1 || own.Own[0].Module != "mod-x" || own.Own[0].Name != "broker" || own.Own[0].Origin != OriginMade {
t.Fatalf("own answers %+v", own.Own)
}
value, err := seal.Open(own.Own[0].Sealed, nodePriv)
if err != nil {
t.Fatal(err)
}
var held struct{ Password string }
_ = json.Unmarshal([]byte(value), &held)
shape, _ := buscompose.HeldCredential(x, held.Password)
if value != string(shape) {
t.Errorf("the minted value %q is not the controller's shape %q", value, shape)
}
if h := p.hashOf(t, x.User); bcrypt.CompareHashAndPassword([]byte(h.Hash), []byte(held.Password)) != nil || h.Adopted || h.Generation != 1 {
t.Errorf("bus-accounts %s holds %+v, not the minted password's hash", x.User, h)
}
if h := p.hashOf(t, y.User); h == nil || !h.Adopted {
t.Errorf("the adopted %s was replaced: %+v", y.User, h)
}
p.mu.Lock()
said := strings.Join(p.said, "\n")
p.mu.Unlock()
if !strings.Contains(said, AccountIssuedEvent+` {"`) || !strings.Contains(said, `"user":"ace.mod-x"`) || strings.Contains(said, held.Password) {
t.Errorf("the vault said: %s", said)
}
// Handed back to a controller: a new want mints nothing.
p.put(t, "mesh-controller.lease", LeaseKey, map[string]any{"instance": "novox/2/2026-10-28", "epoch": 58})
p.put(t, "mesh-grants.accounts", z.User, z)
eventually(t, "the new want held back once the lease is the controller's again", func() bool {
return strings.Contains(status().HeldBack[z.User], "controller holds the lease")
})
if p.hashOf(t, z.User) != nil {
t.Error("minted after the lease was handed back")
}
}
@@ -1,244 +0,0 @@
package main
import (
"encoding/json"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/seal"
)
// aWant is a wanted account of a module on ace, as the grants part writes it.
func aWant(module string) buscompose.WantedAccount {
return buscompose.WantedAccount{User: "ace." + module, Kind: buscompose.KindModule, Node: "ace", Module: module,
URL: "nats://novox.mesh:4222", Fingerprint: "ab:cd", Claims: []buscompose.ClaimedSeat{{Seat: "s", Scope: "node"}}}
}
func wantRaw(t *testing.T, w buscompose.WantedAccount) []byte {
t.Helper()
raw, err := json.Marshal(w)
if err != nil {
t.Fatal(err)
}
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) {
t.Helper()
plans := assignments{"ace.mod-x": `{"node":"ace","module":"mod-x"}`, "ace.mod-y": `{"node":"ace","module":"mod-y"}`}
v := newVault(t, nil)
v.Askers = Declared{Read: plans.read, Store: v.Store}
accounts := &fakeAccounts{}
v.Accounts = accounts
pub, priv := aSealingKey(t)
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
}
// 774/AC7 (novox/hq ADR 0561 §1, §2, §4): a want with no value, minting on, is minted once: kept whole in the controller's
// 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)
w := aWant("mod-x")
before := len(v.bus.events)
v.WantHeard(w.User, wantRaw(t, w), false)
rec, held, err := v.Store.Read("own/ace/mod-x/broker")
if err != nil || !held || rec.Generation != 1 || rec.Origin != OriginMade {
t.Fatalf("the minted account is kept as %+v (held %v, %v)", rec, held, err)
}
value, err := seal.Open(rec.AtRest, v.Own.PrivateKey)
if err != nil {
t.Fatal(err)
}
var got struct{ Password string }
_ = json.Unmarshal([]byte(value), &got)
shape, _ := buscompose.HeldCredential(w, got.Password)
if len(got.Password) != 40 || value != string(shape) {
t.Errorf("the minted value is %q, not the controller's shape %q", value, shape)
}
if !accounts.hashOf(w.User, got.Password, 1) {
t.Errorf("%s's hash is not the minted password's: %+v", w.User, accounts.keys[w.User])
}
var order []string
for _, e := range v.bus.events[before:] {
order = append(order, e["event"].(string))
}
if strings.Join(order, ",") != AccountHashedEvent+","+AccountIssuedEvent+","+ChangedEvent {
t.Errorf("said %v, want the hash, account-issued, then secret-changed", order)
}
for _, raw := range v.bus.raw[before:] {
if strings.Contains(raw, got.Password) || strings.Contains(raw, "$2a$") {
t.Errorf("a value or a hash was said: %s", raw)
}
}
own, err := v.OwnOfNode("node.ace")
if err != nil || len(own.Own) != 1 {
t.Fatalf("own answered %+v, %v", own, err)
}
if opened, err := seal.Open(own.Own[0].Sealed, nodePriv); err != nil || opened != value {
t.Errorf("own's sealed value opens to %q, %v", opened, err)
}
// Heard again, or for a value held, or for a hash adopted: nothing is minted, nothing is held back.
v.WantHeard(w.User, wantRaw(t, w), false)
if rec, _, _ := v.Store.Read("own/ace/mod-x/broker"); rec.Generation != 1 {
t.Errorf("a want heard again minted generation %d", rec.Generation)
}
accounts.keys["ace.mod-y"] = AccountHash{User: "ace.mod-y", Hash: "$2a$04$adopted", Adopted: true}
y := aWant("mod-y")
v.WantHeard(y.User, wantRaw(t, y), false)
if _, held, _ := v.Store.Read("own/ace/mod-y/broker"); held || accounts.keys["ace.mod-y"].Hash != "$2a$04$adopted" {
t.Errorf("an adopted user was minted")
}
if st := v.MintState(); len(st.HeldBack) != 0 || len(st.Minted) != 1 {
t.Errorf("vault_status says %+v: an adopted user is not held back, one account minted", st)
}
}
// 774/AC8 (novox/hq ADR 0561 §3): while the controller holds the lease nothing is minted and vault_status says why and
// names each wanted account held back (never an adopted one); the write that hands the lease away mints them; a lease
// handed back, expired or unreadable turns minting off again, read afresh before every mint.
func TestNothingIsMintedUntilTheLeaseIsHandedAway(t *testing.T) {
v, accounts, _ := mintingVault(t, false)
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)
v.WantHeard(w.User, wantRaw(t, w), false)
}
st := v.MintState()
if st.On || !strings.Contains(st.Why, "controller holds the lease") || len(st.HeldBack) != 1 ||
!strings.Contains(st.HeldBack["ace.mod-x"], "controller holds the lease") {
t.Fatalf("under the controller's lease vault_status says %+v", st)
}
if _, held, _ := v.Store.Read("own/ace/mod-x/broker"); held {
t.Fatal("minted while the controller holds the lease")
}
// Handed away: the wanted account is minted at that write, the adopted one is not.
lease := []byte(`{"handed-to":"the operator's hand-over"}`)
v.LeaseRead = func() ([]byte, bool, error) { return lease, true, nil }
v.LeaseHeard(lease, true, nil)
if _, held, _ := v.Store.Read("own/ace/mod-x/broker"); !held {
t.Fatalf("not minted at the lease write: %+v", v.MintState())
}
if _, held, _ := v.Store.Read("own/ace/mod-y/broker"); held {
t.Error("an adopted account was minted at the lease write")
}
if st := v.MintState(); !st.On || len(st.HeldBack) != 0 {
t.Errorf("after the lease write vault_status says %+v", st)
}
// The key expired (no watch hears a key's age pass): read afresh before the next mint, nothing is minted.
v.LeaseRead = func() ([]byte, bool, error) { return nil, false, nil }
z := aWant("mod-z")
v.WantHeard(z.User, wantRaw(t, z), false)
if _, held, _ := v.Store.Read("own/ace/mod-z/broker"); held {
t.Error("minted after the lease's key expired")
}
if st := v.MintState(); st.On || !strings.Contains(st.HeldBack["ace.mod-z"], "no lease is written") {
t.Errorf("after the key expired vault_status says %+v", st)
}
}
// 774/AC10 issue-account (novox/hq ADR 0561 §9): the verb mints one wanted user now, read afresh; it is refused while
// minting is off, 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)
w := aWant("mod-x")
raw := wantRaw(t, w)
v.WantRead = func(user string) ([]byte, bool, error) {
if user == w.User {
return raw, true, nil
}
return nil, false, nil
}
for user, says := range map[string]string{
"ace.mod-x": "controller holds the lease",
"node.ace": "never minted",
"controller": "never minted",
} {
if _, err := v.IssueAccount("person.jochen", user); err == nil || !strings.Contains(err.Error(), says) {
t.Errorf("%s: %v, want a refusal saying %q", user, err, says)
}
}
v.LeaseHeard([]byte(`{"handed-to":"the operator's hand-over"}`), true, nil)
if _, err := v.IssueAccount("person.jochen", "ace.nobody"); err == nil || !strings.Contains(err.Error(), "no wanted account") {
t.Errorf("a user nobody wants: %v", err)
}
got, err := v.IssueAccount("person.jochen", w.User)
if err != nil || got.(map[string]any)["minted"] != true {
t.Fatalf("issue-account: %v, %v", got, err)
}
if again, err := v.IssueAccount("person.jochen", w.User); err != nil || again.(map[string]any)["minted"] != false ||
!strings.Contains(again.(map[string]any)["why"].(string), "held already") {
t.Errorf("issue-account again: %v, %v", again, err)
}
}
// 774/AC10 the rotation's rule (novox/hq ADR 0561 §4): at most one rotation begins per revision of the loaded user list,
// 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)
for _, m := range []string{"mod-x", "mod-y"} {
w := aWant(m)
v.WantHeard(w.User, wantRaw(t, w), false)
}
if _, err := v.BeginRotation("ace.mod-x"); err == nil || !strings.Contains(err.Error(), "not known yet") {
t.Errorf("began with no loaded revision known: %v", err)
}
v.LoadedListHeard(570)
r, err := v.BeginRotation("ace.mod-x")
if err != nil || r.Login != "ace.mod-x--b" || r.Revision != 570 || r.Generation != 2 || accounts.keys["ace.mod-x"].Rotating == nil {
t.Fatalf("the first rotation: %+v, %v", r, err)
}
if _, err := v.BeginRotation("ace.mod-y"); err == nil || !strings.Contains(err.Error(), "one rotation begins per revision") {
t.Errorf("a second rotation in one revision: %v", err)
}
v.LoadedListHeard(571)
if _, err := v.BeginRotation("ace.mod-y"); err != nil {
t.Errorf("the next revision's rotation: %v", err)
}
if _, err := v.BeginRotation("ace.mod-x"); err == nil || !strings.Contains(err.Error(), "rotating already") {
t.Errorf("an account rotating began again: %v", err)
}
accounts.keys["node.ace"] = AccountHash{User: "node.ace", Hash: "$2a$04$n", Adopted: true}
for _, u := range []string{"node.ace", "controller"} {
if _, err := v.BeginRotation(u); err == nil {
t.Errorf("%s began a rotation", u)
}
}
if _, err := v.RotateFor(ControllerCaller, "own/ace/mod-x/broker", Narrow{}, "the test", "a rotation in place"); err == nil {
t.Error("a covered account rotated in place")
}
}
// 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)
key, _ := v.Store.OperatorKey()
if err := v.Store.setOperatorKey("", "the test", v.Now()); err != nil {
t.Fatal(err)
}
w := aWant("mod-x")
v.WantHeard(w.User, wantRaw(t, w), false)
if _, ok := accounts.keys[w.User]; ok {
t.Fatal("minted with no operator key")
}
if err := v.Store.setOperatorKey(key, "the test", v.Now()); err != nil {
t.Fatal(err)
}
v.MintOwed()
if _, ok := accounts.keys[w.User]; !ok {
t.Errorf("not minted at the pass after the failure: %+v", v.MintState())
}
}
+10 -7
View File
@@ -84,14 +84,17 @@ func (v *Vault) credentialRefusal(node string, c CredentialAsked) string {
if c.Consumer != node {
return fmt.Sprintf("%s asks as a consumer on %s, and is not that machine", node, c.Consumer)
}
// The consuming module stands on the machine: read from the placement and the assignments, never assumed (one
// that cannot be read refuses).
stands, err := v.standsOn(node, c.ConsumerModule)
if err != nil {
return fmt.Sprintf(notYetPrefix+"where %s stands could not be read: %v", c.ConsumerModule, err)
// The consuming module stands on the machine: read, never assumed (an assignment that cannot be read refuses).
read, why := v.assignmentReader(node)
if why != "" {
return why
}
if !stands {
return fmt.Sprintf("%s is not on %s: neither placed nor assigned there, nor required by a module there", c.ConsumerModule, node)
raw, err := read(node + "." + c.ConsumerModule)
if err != nil {
return fmt.Sprintf(notYetPrefix+"the assignment %s.%s could not be read: %v", node, c.ConsumerModule, err)
}
if raw == nil {
return fmt.Sprintf("%s is not on %s: neither assigned there nor required by a module there", c.ConsumerModule, node)
}
return ""
case "provider":
-134
View File
@@ -1,134 +0,0 @@
package main
// Where modules stand, from the catalogue's placement (novox/hq ADR 0549 §3, §5; ADR 0561; issue 774). The controller's
// assignments mirror goes with its lease, so the vault reads which module stands on which node from the indexer's
// placement first, and the assignments beside it while both stand: a module either names stands. The provisioner's plan,
// which only an assignment's value carries, is read from the assignments still: its home after the lease is a subtask of
// its own (issue 774).
import (
"errors"
"fmt"
"slices"
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// PlacementRead is the indexer's placement, as the vault's manifest reads it.
const PlacementRead = delivery.IndexerModule + "." + delivery.PlacementState
// PlacementReader reads the indexer's placement: every key, and one key's value (nil when there is none).
type PlacementReader struct {
Keys func() ([]string, error)
Read func(key string) ([]byte, error)
}
// placements is every node's placement, by node; none when the vault reads no placement.
func (v *Vault) placements() (map[string]delivery.Placement, error) {
out := map[string]delivery.Placement{}
if v.Placement.Keys == nil || v.Placement.Read == nil {
return out, nil
}
keys, err := v.Placement.Keys()
if err != nil {
return nil, fmt.Errorf("the placement's keys could not be read: %w", err)
}
for _, k := range keys {
node, err := delivery.ParsePlacementKey(k)
if err != nil {
continue
}
raw, err := v.Placement.Read(k)
if err != nil {
return nil, fmt.Errorf("the placement of %s could not be read: %w", node, err)
}
if raw == nil {
continue
}
p, err := delivery.DecodePlacement(raw)
if err != nil || p.Node != node {
continue
}
out[node] = p
}
return out, nil
}
// placedOn says module is placed on node by the catalogue.
func (v *Vault) placedOn(node, module string) (bool, error) {
if v.Placement.Read == nil {
return false, nil
}
key, err := delivery.PlacementKey(node)
if err != nil {
return false, err
}
raw, err := v.Placement.Read(key)
if err != nil || raw == nil {
return false, err
}
p, err := delivery.DecodePlacement(raw)
if err != nil {
return false, fmt.Errorf("the placement of %s is not readable: %w", node, err)
}
return slices.Contains(p.Assigned(), module), nil
}
// standsOn says module stands on node: placed there, or assigned there while the assignments stand. A read that fails is
// an error, never "not there".
func (v *Vault) standsOn(node, module string) (bool, error) {
placed, err := v.placedOn(node, module)
if err != nil || placed {
return placed, err
}
d, ok := v.Askers.(Declared)
if !ok || d.Read == nil {
if v.Placement.Read == nil {
return false, errors.New("the vault reads neither the placement nor the assignments")
}
return false, nil
}
raw, err := standing(d.Read)(node + "." + module)
return raw != nil, err
}
// busNode is the node running the bus's module: from the placement, and from the assignments while no placement names
// one. The vault adopts from one node alone.
func (v *Vault) busNode() (string, error) {
placed, err := v.placements()
if err != nil {
return "", err
}
var nodes []string
for node, p := range placed {
if slices.Contains(p.Assigned(), BusModule) {
nodes = append(nodes, node)
}
}
from := "placed"
if len(nodes) == 0 && v.Assignments.Keys != nil {
from = "assigned"
keys, err := v.Assignments.Keys()
if err != nil {
return "", fmt.Errorf("the assignments' keys could not be read: %w", err)
}
for _, k := range keys {
if node, module, ok := strings.Cut(k, "."); ok && module == BusModule {
nodes = append(nodes, node)
}
}
} else if len(nodes) == 0 && len(placed) == 0 {
return "", errors.New("the vault reads neither the placement nor the assignments, so it cannot tell which node holds the bus")
}
sort.Strings(nodes)
switch len(nodes) {
case 1:
return nodes[0], nil
case 0:
return "", fmt.Errorf("no node is %s the bus module %s", from, BusModule)
}
return "", fmt.Errorf("%d nodes are %s the bus module %s (%s): the vault adopts from one alone", len(nodes), from,
BusModule, strings.Join(nodes, ", "))
}
@@ -1,66 +0,0 @@
package main
import (
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// placementsOf is the indexer's placement of each node, as the vault reads it.
func placementsOf(t *testing.T, nodes map[string][]string) PlacementReader {
t.Helper()
values := map[string][]byte{}
for node, modules := range nodes {
p := delivery.Placement{V: delivery.Version, Node: node}
for _, m := range modules {
p.Modules = append(p.Modules, delivery.PlacedModule{Module: m})
}
raw, err := delivery.EncodePlacement(p)
if err != nil {
t.Fatal(err)
}
key, _ := delivery.PlacementKey(node)
values[key] = raw
}
return PlacementReader{
Keys: func() ([]string, error) {
var out []string
for k := range values {
out = append(out, k)
}
return out, nil
},
Read: func(key string) ([]byte, error) { return values[key], nil },
}
}
// 774/AC11 (novox/hq ADR 0561, ADR 0549 §3, §5): with no assignments to read, the vault tells from the catalogue's
// placement which node holds the bus and whether a consumer's module stands on its node; while both stand, a module
// either names stands.
func TestTheVaultReadsWhereModulesStandFromPlacement(t *testing.T) {
v := newVault(t, nil)
v.Placement = placementsOf(t, map[string][]string{"novox": {"nats", "web"}, "ace": {"shop"}})
if node, err := v.busNode(); err != nil || node != "novox" {
t.Errorf("the bus node from placement: %q, %v", node, err)
}
asked := func(node, module string) CredentialAsked {
return CredentialAsked{Role: "consumer", Provision: "postgres", Consumer: node, ConsumerModule: module, Provider: "novox"}
}
if why := v.credentialRefusal("ace", asked("ace", "shop")); why != "" {
t.Errorf("shop stands on ace by placement, refused: %s", why)
}
if why := v.credentialRefusal("ace", asked("ace", "web")); !strings.Contains(why, "is not on ace") {
t.Errorf("web is not placed on ace, and is answered %q", why)
}
// While both stand, an assignment still names a module placement does not yet.
v.Askers = Declared{Read: assignments{"ace.web": `{"node":"ace","module":"web"}`}.read, Store: v.Store}
if why := v.credentialRefusal("ace", asked("ace", "web")); why != "" {
t.Errorf("web assigned on ace, refused: %s", why)
}
// Two nodes placed with the bus: the vault adopts from neither.
v.Placement = placementsOf(t, map[string][]string{"novox": {"nats"}, "ace": {"nats"}})
if _, err := v.busNode(); err == nil || !strings.Contains(err.Error(), "2 nodes") {
t.Errorf("two bus nodes: %v", err)
}
}
-264
View File
@@ -1,264 +0,0 @@
package main
// A wanted account no longer wanted is retired within ADR 0230's bounds (novox/hq ADR 0561 §2, issue 774).
//
// The grants part deletes a wanted account's key when no input names its user any more. The vault then retires the
// account: it takes its hash out of `bus-accounts`, so the grants part's next list drops the user, and marks the value
// 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.
import (
"errors"
"fmt"
"sort"
"strings"
"time"
)
// retiring is what the passes keep of the accounts no longer wanted, under minting.mu.
type retiring struct {
wantsRead bool
key string
passes int
first time.Time
waiting []string
why string
retired []string
}
// RetireState is what vault_status says of retirement.
type RetireState struct {
Gone []string `json:"gone"`
Passes int `json:"passes"`
Since time.Time `json:"since,omitzero"`
Waiting []string `json:"waiting_for_a_person"`
Why string `json:"why"`
Retired []string `json:"retired"`
}
// RetireState answers how retirement stands.
func (v *Vault) RetireState() RetireState {
m := &v.minting
m.mu.Lock()
defer m.mu.Unlock()
r := m.retiring
out := RetireState{Passes: r.passes, Since: r.first, Why: r.why, Waiting: append([]string{}, r.waiting...),
Retired: append([]string{}, r.retired...), Gone: []string{}}
if r.key != "" {
out.Gone = strings.Split(r.key, " ")
}
return out
}
// WantsRead says the wanted accounts were read whole once (the watch handed every current key): only then is an account
// with no key known to be no longer wanted.
func (v *Vault) WantsRead() {
v.minting.mu.Lock()
defer v.minting.mu.Unlock()
v.minting.retiring.wantsRead = true
}
// wantGone is a wanted account's key deleted: forgotten as wanted, its retirement left to the passes.
func (v *Vault) wantGone(user string) {
v.minting.mu.Lock()
defer v.minting.mu.Unlock()
delete(v.minting.wants, user)
delete(v.minting.heldBack, user)
}
// wantBack is a wanted account's key written: a retired account of it is re-enabled at once.
func (v *Vault) wantBack(user string) {
w, ok := v.wanted(user)
if !ok {
return
}
secret, err := AccountSecret(w)
if err != nil {
return
}
rec, held, err := v.Store.Read(secret)
if err != nil || !held || rec.Retired == nil {
return
}
rec.Retired = nil
if err := v.Store.Write(rec); err != nil {
v.log("%s is wanted again and NOT RE-ENABLED: its mark could not be taken off: %v", user, err)
return
}
if _, err := v.HashAccount(secret, true); err != nil {
v.hashes.owe(secret, err.Error())
v.log("%s is wanted again and its hash is not written yet, written at the next pass: %v", user, err)
}
v.minting.mu.Lock()
r := &v.minting.retiring
for i, u := range r.retired {
if u == user {
r.retired = append(r.retired[:i], r.retired[i+1:]...)
break
}
}
v.minting.mu.Unlock()
v.log("%s is wanted again: re-enabled, its hash written again", user)
}
// goneAccounts are the users whose hash the vault wrote from a value it holds and that no wanted key names, sorted, and
// how many such hashes it holds in all.
func (v *Vault) goneAccounts() ([]string, int, error) {
users, err := v.Accounts.Users()
if err != nil {
return nil, 0, err
}
var gone []string
held := 0
for _, u := range users {
a, err := v.Accounts.Get(u)
if err != nil {
return nil, 0, err
}
if a == nil || a.Adopted || a.Of == "" {
continue
}
held++
if _, wanted := v.wanted(u); !wanted {
gone = append(gone, u)
}
}
sort.Strings(gone)
return gone, held, nil
}
// RetirePass is one pass of retirement, at the vault's minute pass.
func (v *Vault) RetirePass() {
if v.Accounts == nil {
return
}
v.leaseAfresh()
on, leaseWhy := v.mintingOn()
m := &v.minting
m.mu.Lock()
read := m.retiring.wantsRead
m.mu.Unlock()
reset := func(why string) {
m.mu.Lock()
defer m.mu.Unlock()
m.retiring.key, m.retiring.passes, m.retiring.first, m.retiring.waiting, m.retiring.why = "", 0, time.Time{}, nil, why
}
switch {
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 {
reset("the accounts held could not be read: " + err.Error())
return
}
if len(gone) == 0 {
reset("")
return
}
now := v.Now().UTC()
key := strings.Join(gone, " ")
m.mu.Lock()
r := &m.retiring
if r.key != key {
r.key, r.passes, r.first, r.waiting = key, 0, now, nil
}
r.passes++
passes, since := r.passes, r.first
m.mu.Unlock()
setWhy := func(why string, waiting []string) {
m.mu.Lock()
defer m.mu.Unlock()
m.retiring.why, m.retiring.waiting = why, waiting
}
if passes < StablePasses || now.Sub(since) < StableFor {
setWhy(fmt.Sprintf("%d no longer wanted (%s): retired once the same set stands for %d passes and %s (ADR 0230)",
len(gone), key, StablePasses, StableFor), nil)
return
}
if len(gone) > RetireAtOnce || float64(len(gone)) > RetireFraction*float64(held) {
why := fmt.Sprintf("%d accounts are no longer wanted, more than %d or more than half of the %d held at once: they "+
"wait for a person (account_retire), ADR 0230 §18", len(gone), RetireAtOnce, held)
setWhy(why, gone)
v.log("%s: %s", why, key)
return
}
for _, u := range gone {
if err := v.retireOne(u, "no longer wanted by the grants part", "the vault's pass"); err != nil {
v.log("%s is not retired: %v", u, err)
}
}
reset("")
}
// retireOne marks the value of user's account retired and takes its hash out of bus-accounts.
func (v *Vault) retireOne(user, why, by string) error {
a, err := v.Accounts.Get(user)
if err != nil {
return err
}
if a == nil || a.Adopted || a.Of == "" {
return fmt.Errorf("the vault holds no account of %s it made or was handed: an adopted hash is never retired here", user)
}
rec, held, err := v.Store.Read(a.Of)
if err != nil {
return err
}
if held {
rec.Retired = &RetiredMark{At: v.Now().UTC().Format(time.RFC3339), Why: why + ", by " + by}
if err := v.Store.Write(rec); err != nil {
return fmt.Errorf("its value could not be marked retired: %w", err)
}
}
if err := v.Accounts.Delete(user); err != nil {
return fmt.Errorf("its hash could not be taken out of %s: %w", BusAccountsState, err)
}
v.minting.mu.Lock()
v.minting.retiring.retired = append(v.minting.retiring.retired, user)
v.minting.mu.Unlock()
v.log("retired the bus account %s (%s): its hash is out of %s and its value kept, marked; %s", user, a.Of,
BusAccountsState, why)
return nil
}
// RetireAccounts is the tool account_retire: a person retires accounts no longer wanted, past ADR 0230's bound, with why.
func (v *Vault) RetireAccounts(caller string, users []string, why string) (any, error) {
if strings.TrimSpace(why) == "" {
return nil, errors.New("account_retire says why (`why`)")
}
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.mintingOn(); !on {
return nil, fmt.Errorf("nothing was retired: %s", why)
}
v.minting.mu.Lock()
read := v.minting.retiring.wantsRead
v.minting.mu.Unlock()
if !read {
return nil, errors.New("nothing was retired: the wanted accounts are not read whole yet, so none is known to be gone")
}
for _, u := range users {
if _, wanted := v.wanted(u); wanted {
return nil, fmt.Errorf("%s is still wanted by the grants part: only an account no longer wanted is retired; nothing was retired", u)
}
}
var done []string
for _, u := range users {
if err := v.retireOne(u, strings.TrimSpace(why), someoneOr("", caller)); err != nil {
return map[string]any{"retired": done}, fmt.Errorf("%s: %w", u, err)
}
done = append(done, u)
}
return map[string]any{"retired": done}, nil
}
@@ -1,89 +0,0 @@
package main
import (
"strings"
"testing"
"time"
)
// 774/AC9 (novox/hq ADR 0561 §2, ADR 0230 §15, §18): a wanted account whose key is deleted is retired (its hash taken
// out of bus-accounts, its value kept, marked) only once the same set has stood for five passes and ten minutes, and never
// 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)
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"}
for _, m := range modules {
w := aWant(m)
v.WantHeard(w.User, wantRaw(t, w), false)
}
v.WantsRead()
if len(accounts.keys) != 8 {
t.Fatalf("minted %d", len(accounts.keys))
}
pass := func(n int, every time.Duration) {
for i := 0; i < n; i++ {
clock = clock.Add(every)
v.RetirePass()
}
}
v.WantHeard("ace.m1", nil, true)
pass(5, time.Second)
if _, ok := accounts.keys["ace.m1"]; !ok {
t.Fatal("retired after five passes in five seconds: ADR 0230 waits ten minutes too")
}
pass(1, 10*time.Minute)
if _, ok := accounts.keys["ace.m1"]; ok {
t.Fatalf("not retired after five passes and ten minutes: %+v", v.RetireState())
}
if rec, held, _ := v.Store.Read("own/ace/m1/broker"); !held || rec.Retired == nil {
t.Fatalf("the retired account's value is %+v (held %v): kept, and marked", rec, held)
}
if written, _ := v.HashHeld(); len(written) != 0 {
t.Errorf("a start hashed a retired account again: %+v", written)
}
// Wanted again: re-enabled at once.
w := aWant("m1")
v.WantHeard(w.User, wantRaw(t, w), false)
if rec, _, _ := v.Store.Read("own/ace/m1/broker"); rec.Retired != nil || accounts.keys["ace.m1"].Hash == "" {
t.Fatalf("wanted again, ace.m1 is %+v with hash %v", rec.Retired, accounts.keys["ace.m1"])
}
// Four gone at once is over the bound: it waits for a person, who retires them by hand.
for _, m := range modules[4:] {
v.WantHeard("ace."+m, nil, true)
}
pass(6, 3*time.Minute)
st := v.RetireState()
if len(accounts.keys) != 8 || len(st.Waiting) != 4 || !strings.Contains(st.Why, "more than 3") {
t.Fatalf("over the bound: %d keys, %+v", len(accounts.keys), st)
}
if _, err := v.RetireAccounts("person.jochen", []string{"ace.m5"}, ""); err == nil {
t.Error("retired by hand with no why")
}
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)
v.minting.mu.Lock()
v.minting.retiring.wantsRead = false
v.minting.mu.Unlock()
if _, err := v.RetireAccounts("person.jochen", []string{"ace.m5"}, "the modules left"); err == nil || !strings.Contains(err.Error(), "not read whole") {
t.Errorf("retired by hand before the wants were read: %v", err)
}
v.WantsRead()
if _, err := v.RetireAccounts("person.jochen", []string{"ace.m5", "ace.m6"}, "the modules left"); err != nil {
t.Fatal(err)
}
if _, ok := accounts.keys["ace.m5"]; ok || len(accounts.keys) != 6 {
t.Errorf("after a person's retirement %d keys remain", len(accounts.keys))
}
}
@@ -1,94 +0,0 @@
package main
// The record of a bus account's rotation, and its one rule (novox/hq ADR 0561 §4; issue 774, the lead's decision of
// 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.
import (
"fmt"
"strings"
"time"
)
// SecondLoginSuffix makes an account's second login: reserved, so no module or process may be named to end in it.
const SecondLoginSuffix = "--b"
// Rotating is a rotation under way, as the account's key in bus-accounts says it: the second login, the generation of
// the value it carries, since when, and the revision of the loaded user list it began at.
type Rotating struct {
Login string `json:"login"`
Generation int `json:"generation"`
Since time.Time `json:"since"`
Revision uint64 `json:"revision"`
}
// LoadedListHeard keeps the revision of the user list the bus node says it loaded (its have of the bus module).
func (v *Vault) LoadedListHeard(revision uint64) {
v.minting.mu.Lock()
defer v.minting.mu.Unlock()
if revision > v.minting.loadedRevision {
v.minting.loadedRevision = revision
}
}
// BeginRotation records a rotation of user's account at the loaded list's revision, under the one rule: none for a
// node's account or the controller's, none while minting is off, none of an account the vault did not make or was not
// handed, none twice for one account, and none when another began at this revision.
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.mintingOn(); !on {
return Rotating{}, fmt.Errorf("no rotation begins: %s", why)
}
v.minting.mu.Lock()
revision := v.minting.loadedRevision
v.minting.mu.Unlock()
if revision == 0 {
return Rotating{}, fmt.Errorf("no rotation begins: the revision of the user list the bus loaded is not known yet")
}
users, err := v.Accounts.Users()
if err != nil {
return Rotating{}, err
}
var mine *AccountHash
began := ""
for _, u := range users {
a, err := v.Accounts.Get(u)
if err != nil {
return Rotating{}, err
}
if a == nil {
continue
}
if a.Rotating != nil && a.Rotating.Revision == revision {
began = u
}
if u == user {
mine = a
}
}
switch {
case mine == nil || mine.Adopted || mine.Of == "":
return Rotating{}, fmt.Errorf("the vault holds no account of %s it made or was handed, so it rotates none", user)
case mine.Rotating != nil:
return Rotating{}, fmt.Errorf("%s is rotating already, since %s (second login %s)", user, mine.Rotating.Since.Format(time.RFC3339), mine.Rotating.Login)
case strings.HasSuffix(user, SecondLoginSuffix):
return Rotating{}, fmt.Errorf("%s is a second login, which never rotates of its own", user)
case began != "":
return Rotating{}, fmt.Errorf("no rotation of %s begins: %s's began at the loaded list's revision %d, and one "+
"rotation begins per revision (ADR 0561 §4)", user, began, revision)
}
r := Rotating{Login: user + SecondLoginSuffix, Generation: mine.Generation + 1, Since: v.Now().UTC(), Revision: revision}
next := *mine
next.Rotating = &r
if err := v.Accounts.Put(next); err != nil {
return Rotating{}, err
}
v.log("a rotation of %s begins at the loaded list's revision %d: second login %s", user, revision, r.Login)
return r, nil
}
@@ -48,8 +48,6 @@ type Record struct {
// MovedFrom is, for a value the move took from the controller's store (design 57's V3), the node-engine that
// handed it in and the controller's generation for it, kept as it was.
MovedFrom *MovedFrom `json:"moved-from,omitempty"`
// Retired marks a bus account no longer wanted (retire.go, ADR 0561 §2): its value kept, its hash taken out.
Retired *RetiredMark `json:"retired,omitempty"`
}
// OperatorCopy is a value sealed to the operator key named in Key.
+2 -40
View File
@@ -251,20 +251,6 @@ func definitions(env *Env) []toolDef {
// clause 9, adopt.go): a public half, never the private one.
return map[string]any{"recorded": true, "vault_key": v.Own.PublicKey}, nil
}},
{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. Reads the wanted key afresh from mesh-grants.accounts and mints it as a want would: only while " +
"the controller's lease is handed away, 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")},
run: func(caller string, a map[string]any) (any, error) {
v, err := env.ready()
if err != nil {
return nil, err
}
return v.IssueAccount(caller, arg(a, "user"))
}},
{Name: Seat + ".own",
Description: "The own secrets of this machine's modules, each sealed to the machine's sealing key (novox/hq ADR 0547 §2, " +
"ADR 0550 §5, issue 1126): from the node-engine of the machine alone, about its own machine (the bus proves which), " +
@@ -438,28 +424,6 @@ func definitions(env *Env) []toolDef {
}
return env.Vault.HashUser(arg(a, "user"), caller)
}},
{Name: "account_retire",
Description: "Retire bus accounts the grants part no longer wants, by a person's act with why (novox/hq ADR 0561 §2, " +
"ADR 0230 §18): the vault takes each one's hash out of its state `bus-accounts`, so the grants part's next user " +
"list drops the user, and keeps its value marked retired. For the set vault_status lists as waiting for a " +
"person (more than three, or more than half, gone at once); refused for an account still wanted, an adopted " +
"hash, and with no why. Wanted again, an account is re-enabled at once.",
Input: map[string]any{"users": map[string]any{"type": "array", "items": str("a bus user")},
"why": str("why they are retired")},
run: func(caller string, a map[string]any) (any, error) {
if env.Vault == nil {
return nil, errors.New("the vault is not ready")
}
var users []string
if list, ok := a["users"].([]any); ok {
for _, u := range list {
if s, ok := u.(string); ok {
users = append(users, strings.TrimSpace(s))
}
}
}
return env.Vault.RetireAccounts(caller, users, arg(a, "why"))
}},
{Name: "secret_vault_export",
Description: "Every secret the vault itself holds, as its copy sealed to the operator key, in the shape of " +
"`secret export`: saved to a file, `nox secret recover vault mesh-vault <name> --key <operator key file> " +
@@ -528,9 +492,7 @@ func definitions(env *Env) []toolDef {
run: func(string, map[string]any) (any, error) { return env.health(), nil }},
{Name: "vault_status",
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 §3, §9): whether the vault mints bus accounts now and why (only while the " +
"controller's lease is handed away), every wanted account it holds back with why, and those it minted.",
"key the mesh's export holds if the vault refused it, and how often the record of hand-outs was rolled.",
run: func(string, map[string]any) (any, error) {
if env.Vault == nil {
return map[string]any{"ready": false, "error": fmt.Sprint(env.Broken)}, nil
@@ -542,7 +504,7 @@ func definitions(env *Env) []toolDef {
key, _ := env.Vault.Store.OperatorKey()
return map[string]any{"ready": true, "operator_key": key, "copies_on_an_earlier_key": left,
"operator_key_refused": env.keyRefused(), "handed_rolled": env.Vault.Store.Rolled(),
"minting": env.Vault.MintState(), "retiring": env.Vault.RetireState(), "ok": left == 0 && env.keyRefused() == "" && key != ""}, nil
"ok": left == 0 && env.keyRefused() == "" && key != ""}, nil
}},
}
}
+3 -15
View File
@@ -56,14 +56,6 @@ type Vault struct {
hashes hashing
// adoption is the last adoption (adopt.go), under mu.
adoption adoptionKept
// WantRead reads one wanted account afresh (issue-account): nil reads the wants as the watch handed them.
WantRead func(user string) ([]byte, bool, error)
// Placement reads the catalogue'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 minting
// callsMu guards pending: the keys of the calls open now (intake.go), never written anywhere.
callsMu sync.Mutex
pending map[string]*callKey
@@ -112,10 +104,8 @@ func (v *Vault) keep(name, value, by, via, origin string) (Changed, error) {
// was first made, and where it was moved from.
type keepOptions struct {
neverReplace bool
// quiet keeps secret-changed for the caller to say once the rest is done (a minted account: mint.go).
quiet bool
madeAt time.Time
movedFrom *MovedFrom
madeAt time.Time
movedFrom *MovedFrom
}
// keepWith is keep with the move's options. A covered bus account kept anew has its hash written once it is kept
@@ -196,9 +186,7 @@ func (v *Vault) keepLocked(name, value, by, via, origin string, opt keepOptions)
return Changed{}, err
}
c := Changed{Name: name, Generation: rec.Generation, At: now, By: by}
if !opt.quiet {
v.announce(c)
}
v.announce(c)
return c, nil
}
@@ -131,11 +131,9 @@ func filesUnder(t *testing.T, dir string) map[string]string {
// The manifest declares one bucket, and no secret is in it: the vault's only place for a secret is its own store on its
// own disk (ADR 0349 §7, ADR 0201 §13). The one bucket is bus-accounts, the bcrypt hashes of the bus accounts it holds
// (ADR 0547 clause 9), opened in accounts.go alone. It reads five buckets of others', and only reads them: the
// controller's assignments, for what each node's plan names (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 catalogue's placement (placed.go), in
// main.go alone.
// (ADR 0547 clause 9), opened in accounts.go alone. It reads two buckets of others', and only reads them: the
// controller's assignments, for what each node's plan names (askers.go, design 57 V4), and the node-engines' haves, where
// the bus node hands the loaded list's hashes sealed to the vault (adopt.go, ADR 0547 clause 9), in main.go alone.
func TestNoBucketHoldsASecret(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
if err != nil {
@@ -149,9 +147,8 @@ func TestNoBucketHoldsASecret(t *testing.T) {
t.Errorf("the manifest declares buckets %s: a secret is in no bucket, sealed or not (ADR 0201 §13), and the one "+
"bucket is the accounts' hashes (ADR 0547 clause 9)", s)
}
if r, _ := json.Marshal(manifest["reads"]); string(r) != `["mesh-controller.assignments","node-engine.have","mesh-grants.accounts","mesh-controller.lease","mesh-delivery-indexer.placement"]` {
t.Errorf("the manifest reads %s: the vault reads the controller's assignments, the haves and the grants part's "+
"wanted accounts alone", r)
if r, _ := json.Marshal(manifest["reads"]); string(r) != `["mesh-controller.assignments","node-engine.have"]` {
t.Errorf("the manifest reads %s: the vault reads the controller's assignments and the haves alone", r)
}
sources, _ := filepath.Glob("*.go")
bucket := regexp.MustCompile(`stdio\.State\(|nats-io|jetstream|KeyValue`)
@@ -169,21 +166,11 @@ func TestNoBucketHoldsASecret(t *testing.T) {
code = []byte(strings.Replace(string(code), "stdio.State(BusAccountsState).Get(", "", 1))
code = []byte(strings.Replace(string(code), "stdio.State(BusAccountsState).Put(", "", 1))
code = []byte(strings.Replace(string(code), "stdio.State(BusAccountsState).Keys(", "", 1))
code = []byte(strings.Replace(string(code), "stdio.State(BusAccountsState).Delete(", "", 1))
}
// The haves, in main.go alone: one read and one watch, never a write; and one watch of the wanted accounts, the
// grants part's, which hold no secret (ADR 0561 §2).
// The haves, in main.go alone: one read and one watch, never a write.
if f == "main.go" {
code = []byte(strings.Replace(string(code), "stdio.State(HaveRead).Get(", "", 1))
code = []byte(strings.Replace(string(code), "stdio.State(HaveRead).Watch(", "", 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 catalogue'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))
}
if m := bucket.Find(code); m != nil {
t.Errorf("%s opens a bucket (%s): the vault keeps every secret in its own store only", f, m)
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-vault
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027220144-3f7957e16972
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026112527-c2a410bb6376
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
golang.org/x/crypto v0.57.0
+6 -2
View File
@@ -1,5 +1,9 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027220144-3f7957e16972 h1:iYDYv4HpV1ic20Ban0MaOErryQfNT8ngjlh0bKW6sCU=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027220144-3f7957e16972/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021100209-cb54756c3865 h1:VLir9NZbyi1LdH1YwWDIVRuL+lDC+AYlm6FZPlvmlm4=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021100209-cb54756c3865/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026102838-174789941936 h1:HGBoWimZXYZp+Qn9VCstYDitOP84t9EUQYd1yM9j2eg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026102838-174789941936/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026112527-c2a410bb6376 h1:by2zBASLb8h5jA1IiYJWykXwLnHYpuZh/zFNa4NsbOU=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026112527-c2a410bb6376/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
+3 -9
View File
@@ -15,15 +15,11 @@
"provisioned",
"rotated",
"secret-changed",
"account-hashed",
"account-issued"
"account-hashed"
],
"reads": [
"mesh-controller.assignments",
"node-engine.have",
"mesh-grants.accounts",
"mesh-controller.lease",
"mesh-delivery-indexer.placement"
"node-engine.have"
],
"state": [
{
@@ -160,7 +156,6 @@
"secret_own",
"account_hash",
"account_adopt",
"account_retire",
"secret_asks",
"secret_vault_export",
"provisioner_retirement",
@@ -183,8 +178,7 @@
"move-in",
"node-key",
"move-asks",
"own",
"issue-account"
"own"
]
}
]
+1113
View File
File diff suppressed because it is too large Load Diff
+269
View File
@@ -0,0 +1,269 @@
{
"v": 1,
"node": "g14",
"decided": {
"address": "10.10.0.4",
"account": "jochen",
"tunnel": {
"interface": "wg0",
"unit": "wg-quick@wg0",
"config": "/etc/wireguard/wg0.conf",
"port": 51820,
"mtu": 1380,
"address": "10.10.0.4/24",
"range": "10.10.0.0/24",
"public-key": "mq76YPW0FrExhAvt/ZG1SB2BK6/cPISKgXO53SEMazU="
}
},
"modules": [
{
"module": "adwaita"
},
{
"module": "asus-zephyrus-g14"
},
{
"module": "avahi"
},
{
"module": "blueman"
},
{
"module": "bluetooth"
},
{
"module": "ca-trust"
},
{
"module": "claude-code",
"has-mesh-wide": true,
"has-layer": true
},
{
"module": "clipmenu"
},
{
"module": "cups"
},
{
"module": "dbus"
},
{
"module": "dev-tools"
},
{
"module": "disk-load"
},
{
"module": "dmenu"
},
{
"module": "docker"
},
{
"module": "docker-compose"
},
{
"module": "dunst",
"has-layer": true
},
{
"module": "fail2ban"
},
{
"module": "feh"
},
{
"module": "flatpak"
},
{
"module": "fonts"
},
{
"module": "forticlient"
},
{
"module": "gnome-keyring"
},
{
"module": "go"
},
{
"module": "hostname",
"has-layer": true
},
{
"module": "i3"
},
{
"module": "i3status-rust"
},
{
"module": "kernel"
},
{
"module": "lemurs"
},
{
"module": "localization"
},
{
"module": "logrotate"
},
{
"module": "memory-pressure"
},
{
"module": "mesh-build-go",
"has-layer": true
},
{
"module": "mesh-build-node",
"bindings": {
"npm-package-registry": {
"node": "novox",
"module": "gitea"
}
}
},
{
"module": "mesh-cli"
},
{
"module": "mesh-files"
},
{
"module": "mesh-host",
"has-layer": true
},
{
"module": "mesh-mail-notifier"
},
{
"module": "mesh-provisioner-client"
},
{
"module": "mesh-reads"
},
{
"module": "mesh-wireguard"
},
{
"module": "netcheck"
},
{
"module": "networkmanager"
},
{
"module": "nextcloud-client"
},
{
"module": "nftables"
},
{
"module": "nm-applet"
},
{
"module": "node-env"
},
{
"module": "node-tools"
},
{
"module": "nodejs"
},
{
"module": "okioki",
"has-layer": true
},
{
"module": "openrazer"
},
{
"module": "pacman",
"has-layer": true
},
{
"module": "picom"
},
{
"module": "polychromatic"
},
{
"module": "power",
"has-mesh-wide": true,
"has-layer": true
},
{
"module": "powerlevel10k"
},
{
"module": "restic"
},
{
"module": "rofi"
},
{
"module": "screen-lock"
},
{
"module": "sensors"
},
{
"module": "slack"
},
{
"module": "snapd"
},
{
"module": "ssh-client"
},
{
"module": "sshd"
},
{
"module": "sudo"
},
{
"module": "systemd"
},
{
"module": "systemd-resolved"
},
{
"module": "time-sync"
},
{
"module": "triggerhappy"
},
{
"module": "xclip"
},
{
"module": "xdg",
"has-mesh-wide": true,
"has-layer": true
},
{
"module": "xorg"
},
{
"module": "xterm"
},
{
"module": "zsh"
},
{
"module": "zsh-autosuggestions"
},
{
"module": "zsh-syntax-highlighting"
}
],
"pins": {
"wildcard-resolution": {
"node": "novox",
"module": "dnsmasq"
}
}
}
+584
View File
@@ -0,0 +1,584 @@
{
"v": 1,
"node": "novox",
"decided": {
"endpoint": "142.132.152.141:51820",
"site": "hosting",
"hub": true,
"address": "10.10.0.1",
"public-domain": "novox.be",
"account": "jochens",
"agent-account": "nox",
"tunnel": {
"interface": "wg0",
"unit": "wg-quick@wg0",
"config": "/etc/wireguard/wg0.conf",
"port": 51820,
"address": "10.10.0.1/24",
"range": "10.10.0.0/24",
"public-key": "xW0ETQwyR9rHi2XSfOJrKkairE+Fp70FPNNcirqMixY="
},
"peer-names": {
"XT1l25Z0/YhRNQ8FQM+9XsAp7chx5MI0eLOOg1SUEXA=": "shanks",
"mq76YPW0FrExhAvt/ZG1SB2BK6/cPISKgXO53SEMazU=": "g14",
"zTKYP6Sz1hDwnwxIHgiGXYAeGcWbqti3tcSvKEA0Ew4=": "ace"
}
},
"modules": [
{
"module": "artifact-store-tools",
"has-mesh-wide": true,
"has-layer": true
},
{
"module": "avahi"
},
{
"module": "ca-trust"
},
{
"module": "claude-code",
"has-mesh-wide": true,
"has-layer": true
},
{
"module": "claude-licence-manager",
"bindings": {
"postgres-database": {
"node": "novox",
"module": "postgres"
},
"secret": {
"node": "novox",
"module": "mesh-vault"
}
}
},
{
"module": "dbus"
},
{
"module": "de-spiegel",
"ports": {
"35621": 20005
},
"bindings": {
"smtp": {
"node": "novox",
"module": "mailu"
}
}
},
{
"module": "desk-channel",
"has-layer": true
},
{
"module": "dev-tools"
},
{
"module": "disk-load"
},
{
"module": "distribution",
"layer": {
"ports": {
"5000": 5100
}
},
"has-layer": true
},
{
"module": "dnsmasq"
},
{
"module": "docker"
},
{
"module": "fail2ban"
},
{
"module": "gitea",
"ports": {
"3000": 20000
},
"bindings": {
"postgres-database": {
"node": "novox",
"module": "postgres"
},
"secret": {
"node": "novox",
"module": "mesh-vault"
}
},
"layer": {
"expose": {
"22": "anywhere"
},
"ports": {
"22": 222
}
},
"has-layer": true
},
{
"module": "go"
},
{
"module": "hostname",
"has-layer": true
},
{
"module": "invoicing",
"ports": {
"80": 20007,
"9000": 20008
},
"bindings": {
"mongodb-database": {
"node": "novox",
"module": "mongodb"
},
"s3-bucket": {
"node": "novox",
"module": "minio"
}
}
},
{
"module": "kernel",
"has-layer": true
},
{
"module": "keycloak",
"ports": {
"8080": 20009
},
"bindings": {
"postgres-database": {
"node": "novox",
"module": "postgres"
}
},
"has-layer": true
},
{
"module": "link2pay"
},
{
"module": "localization"
},
{
"module": "logrotate"
},
{
"module": "mailu",
"ports": {
"110": 110,
"143": 143,
"25": 25,
"4243": 20004,
"465": 465,
"587": 587,
"8080": 20010,
"993": 993,
"995": 995
},
"bindings": {
"postgres-database": {
"node": "novox",
"module": "postgres"
},
"secret": {
"node": "novox",
"module": "mesh-vault"
}
},
"has-mesh-wide": true,
"has-layer": true
},
{
"module": "mesh-build-go",
"has-layer": true
},
{
"module": "mesh-build-node",
"bindings": {
"npm-package-registry": {
"node": "novox",
"module": "gitea"
}
}
},
{
"module": "mesh-catalog",
"bindings": {
"postgres-database": {
"node": "novox",
"module": "postgres"
}
}
},
{
"module": "mesh-cli"
},
{
"module": "mesh-config"
},
{
"module": "mesh-controller"
},
{
"module": "mesh-delivery-checker"
},
{
"module": "mesh-delivery-forge-bridge"
},
{
"module": "mesh-delivery-gate",
"has-layer": true
},
{
"module": "mesh-delivery-indexer"
},
{
"module": "mesh-delivery-teller"
},
{
"module": "mesh-delivery-watchman"
},
{
"module": "mesh-files"
},
{
"module": "mesh-grants"
},
{
"module": "mesh-host",
"has-layer": true
},
{
"module": "mesh-issues"
},
{
"module": "mesh-knowledge",
"bindings": {
"postgres-database": {
"node": "novox",
"module": "postgres"
}
}
},
{
"module": "mesh-mail",
"bindings": {
"postgres-database": {
"node": "novox",
"module": "postgres"
},
"s3-bucket": {
"node": "novox",
"module": "minio"
}
},
"has-mesh-wide": true,
"has-layer": true
},
{
"module": "mesh-mail-reader",
"bindings": {
"postgres-database": {
"node": "novox",
"module": "postgres"
}
}
},
{
"module": "mesh-mail-web",
"bindings": {
"oidc-client": {
"node": "novox",
"module": "keycloak"
}
},
"layer": {
"endpoints": {
"web": {
"reach": "internal"
}
}
},
"has-layer": true
},
{
"module": "mesh-provisioner"
},
{
"module": "mesh-provisioner-client"
},
{
"module": "mesh-reads"
},
{
"module": "mesh-vault"
},
{
"module": "mesh-wireguard"
},
{
"module": "messenger",
"has-mesh-wide": true,
"has-layer": true
},
{
"module": "minio",
"ports": {
"9000": 20001,
"9001": 20002
}
},
{
"module": "mongodb",
"ports": {
"27017": 20006
}
},
{
"module": "mssql",
"layer": {
"ports": {
"1433": 4848
}
},
"has-layer": true
},
{
"module": "nats"
},
{
"module": "netcheck"
},
{
"module": "nextcloud",
"ports": {
"80": 20003
},
"bindings": {
"postgres-database": {
"node": "novox",
"module": "postgres"
},
"s3-bucket": {
"node": "novox",
"module": "minio"
}
}
},
{
"module": "nftables"
},
{
"module": "node-env"
},
{
"module": "node-tools"
},
{
"module": "nodejs"
},
{
"module": "only-office"
},
{
"module": "pacman",
"has-layer": true
},
{
"module": "photos",
"ports": {
"9000": 20011
},
"bindings": {
"mongodb-database": {
"node": "novox",
"module": "mongodb"
},
"s3-bucket": {
"node": "novox",
"module": "minio"
}
}
},
{
"module": "postgres",
"layer": {
"ports": {
"5432": 6852
}
},
"has-layer": true
},
{
"module": "power",
"has-mesh-wide": true,
"has-layer": true
},
{
"module": "powerlevel10k"
},
{
"module": "records",
"has-mesh-wide": true,
"has-layer": true
},
{
"module": "restic",
"layer": {
"places": {
"repository": "/home/restic"
}
},
"has-layer": true
},
{
"module": "route-proxy"
},
{
"module": "ssh-client"
},
{
"module": "sshd"
},
{
"module": "step-ca"
},
{
"module": "sudo"
},
{
"module": "systemd"
},
{
"module": "systemd-networkd"
},
{
"module": "telegram"
},
{
"module": "time-sync"
},
{
"module": "umami",
"bindings": {
"postgres-database": {
"node": "novox",
"module": "postgres"
},
"secret": {
"node": "novox",
"module": "mesh-vault"
}
},
"layer": {
"expose": {
"3000": "mesh"
},
"ports": {
"3000": 1212
}
},
"has-layer": true
},
{
"module": "website",
"ports": {
"8080": 20012
}
},
{
"module": "zsh",
"has-layer": true
},
{
"module": "zsh-autosuggestions"
},
{
"module": "zsh-syntax-highlighting"
}
],
"holds": [
{
"claim": "git",
"scope": "mesh",
"module": "gitea",
"site": "hosting"
},
{
"claim": "mesh-artifact-store",
"scope": "mesh",
"module": "distribution",
"site": "hosting"
},
{
"claim": "mesh-broker",
"scope": "mesh",
"module": "nats",
"site": "hosting"
},
{
"claim": "mesh-catalog",
"scope": "mesh",
"module": "mesh-catalog",
"site": "hosting"
},
{
"claim": "mesh-config",
"scope": "mesh",
"module": "mesh-config",
"site": "hosting"
},
{
"claim": "mesh-controller",
"scope": "mesh",
"module": "mesh-controller",
"site": "hosting"
},
{
"claim": "mesh-delivery",
"scope": "mesh",
"module": "mesh-delivery-gate",
"site": "hosting"
},
{
"claim": "mesh-dns-resolver",
"scope": "mesh",
"module": "dnsmasq",
"site": "hosting"
},
{
"claim": "mesh-provisioner",
"scope": "mesh",
"module": "mesh-provisioner",
"site": "hosting"
},
{
"claim": "mesh-store",
"scope": "mesh",
"module": "postgres",
"site": "hosting"
},
{
"claim": "mesh-vault",
"scope": "mesh",
"module": "mesh-vault",
"site": "hosting"
},
{
"claim": "npm-package-registry",
"scope": "mesh",
"module": "gitea",
"site": "hosting"
}
]
}
+290
View File
@@ -0,0 +1,290 @@
{
"v": 1,
"node": "shanks",
"decided": {
"site": "home",
"address": "10.10.0.3",
"account": "jochen",
"tunnel": {
"interface": "wg0",
"unit": "wg-quick@wg0",
"config": "/etc/wireguard/wg0.conf",
"port": 51820,
"address": "10.10.0.3/24",
"range": "10.10.0.0/24",
"public-key": "XT1l25Z0/YhRNQ8FQM+9XsAp7chx5MI0eLOOg1SUEXA="
}
},
"modules": [
{
"module": "adwaita"
},
{
"module": "avahi"
},
{
"module": "blueman"
},
{
"module": "bluetooth"
},
{
"module": "brother",
"bindings": {
"s3-bucket": {
"node": "novox",
"module": "minio"
}
},
"has-layer": true
},
{
"module": "buildah"
},
{
"module": "ca-trust"
},
{
"module": "claude-code",
"has-mesh-wide": true,
"has-layer": true
},
{
"module": "clipmenu"
},
{
"module": "cups"
},
{
"module": "dbus"
},
{
"module": "dev-tools"
},
{
"module": "disk-load"
},
{
"module": "dmenu"
},
{
"module": "docker"
},
{
"module": "docker-compose"
},
{
"module": "dunst"
},
{
"module": "fail2ban"
},
{
"module": "feh"
},
{
"module": "flatpak"
},
{
"module": "fonts"
},
{
"module": "forticlient"
},
{
"module": "gnome-keyring"
},
{
"module": "go"
},
{
"module": "hostname",
"has-layer": true
},
{
"module": "i3"
},
{
"module": "i3status-rust"
},
{
"module": "kernel",
"has-layer": true
},
{
"module": "lab",
"bindings": {
"npm-package-registry": {
"node": "novox",
"module": "gitea"
}
},
"has-mesh-wide": true,
"has-layer": true
},
{
"module": "lemurs"
},
{
"module": "localization"
},
{
"module": "logrotate"
},
{
"module": "mesh-build-go",
"has-layer": true
},
{
"module": "mesh-build-image"
},
{
"module": "mesh-build-node",
"bindings": {
"npm-package-registry": {
"node": "novox",
"module": "gitea"
}
}
},
{
"module": "mesh-cli"
},
{
"module": "mesh-delivery-builder"
},
{
"module": "mesh-delivery-view"
},
{
"module": "mesh-files"
},
{
"module": "mesh-host",
"has-layer": true
},
{
"module": "mesh-mail-notifier"
},
{
"module": "mesh-provisioner-client"
},
{
"module": "mesh-reads"
},
{
"module": "mesh-wireguard"
},
{
"module": "mounts",
"has-layer": true
},
{
"module": "netcheck"
},
{
"module": "networkmanager"
},
{
"module": "nextcloud-client"
},
{
"module": "nftables"
},
{
"module": "nm-applet"
},
{
"module": "node-env"
},
{
"module": "node-tools"
},
{
"module": "nodejs"
},
{
"module": "openrazer"
},
{
"module": "pacman",
"has-layer": true
},
{
"module": "picom"
},
{
"module": "polychromatic"
},
{
"module": "power",
"has-mesh-wide": true,
"has-layer": true
},
{
"module": "powerlevel10k"
},
{
"module": "restic"
},
{
"module": "rofi"
},
{
"module": "screen-lock"
},
{
"module": "slack"
},
{
"module": "snapd"
},
{
"module": "ssh-client"
},
{
"module": "sshd"
},
{
"module": "sudo"
},
{
"module": "systemd"
},
{
"module": "systemd-resolved"
},
{
"module": "time-sync"
},
{
"module": "xclip"
},
{
"module": "xdg",
"has-mesh-wide": true,
"has-layer": true
},
{
"module": "xorg"
},
{
"module": "xterm"
},
{
"module": "zsh"
},
{
"module": "zsh-autosuggestions"
},
{
"module": "zsh-syntax-highlighting"
}
],
"pins": {
"wildcard-resolution": {
"node": "novox",
"module": "dnsmasq"
}
}
}