Files
mesh-catalog/modules/mesh-vault/cmd/mesh-vault/main.go
T
jschoubben c9d8950fd0 Every module follows the assigner's new places: the contracts' go/ path, mesh-wants in the delivery contracts, the state and tool assignments (issue 1966)
The legacy key form placement.<node> is gone from the client, so the readers and their fixtures lose it. The check noplacement
now also refuses the assigner's retired names in a manifest.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 22:29:07 +01:00

397 lines
14 KiB
Go

// mesh-vault (novox/hq ADR 0085, ADR 0113, ADR 0349 §7 and §11): one binary, launched by its node's runtime
// and speaking MCP to it over stdio through the Go SDK. It holds the `mesh-vault` seat: it keeps every
// secret in its own store, sealed at rest and with a copy sealed to the operator key; it answers the seat's
// verb `get` with a secret sealed to the asker's key; it raises `secret-changed` without a value. Beside
// that it keeps the custody ledger of the pair credentials the controller still delivers to it.
//
// stdout is the MCP channel; everything this module says, it says on stderr.
package main
import (
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/views"
)
func say(format string, args ...any) {
fmt.Fprintf(os.Stderr, "[mesh-vault] "+format+"\n", args...)
}
func emit(event string, body map[string]any) error { return stdio.Emit(event, body) }
// build makes the vault from the module's words: directories, never values.
func build() *Env {
env := &Env{Receives: os.Getenv("MESH_RECEIVES"), Root: os.Getenv("MESH_VAULT_ROOT")}
if l, err := NewLedger(os.Getenv("MESH_VAULT_LEDGER")); err == nil {
env.Ledger = l
} else {
say("%v", err)
}
if env.Ledger != nil && env.Receives != "" {
env.Custody = &Custody{Ledger: env.Ledger, Receives: env.Receives, Resource: "secret", Node: os.Getenv("MESH_NODE"),
Emit: emit, Now: time.Now, Log: say}
}
store, err := OpenStore(os.Getenv("MESH_VAULT_STORE"))
if err != nil {
env.Broken = fmt.Errorf("the vault could not open its store: %w", err)
say("%v", env.Broken)
return env
}
own, err := OwnKey(os.Getenv("MESH_VAULT_STATE"), store)
if err != nil {
env.Broken = fmt.Errorf("the vault could not open its key: %w", err)
say("%v", env.Broken)
return env
}
// Who may ask (askers.go): a node's provisioner client, for what its module's plan there names.
// Where it sees a consumer bind a provider (pair.go): the same mirror, every key.
// The assignments as the vault's watch keeps them (watchAssignments), read over the bus until it has caught up.
assigned = &WatchedAssignments{Fallback: readPlanRaw}
watchedPlan := func(key string) ([]byte, error) { raw, err := assigned.Read(key); return asAssignment(key, raw, err) }
env.Vault = &Vault{Store: store, Own: own, Askers: Declared{Read: watchedPlan, Confirm: readAssignment, Store: store},
Assignments: Mirror{Keys: assignmentKeys, Read: readAssignment}, Emit: emit, Ask: stdio.Ask,
Now: time.Now, Log: say, Accounts: busAccounts{}, Haves: readHave, WantRead: readWant,
HaveKeys: func() ([]string, error) { return stdio.State(HaveRead).Keys() }, Assigner: seatAssignments, Signers: busSigners{},
AskCallers: askCallers(os.Getenv("MESH_VAULT_SECRET_ASK_CALLERS")), EnrolCallers: askCallers(os.Getenv("MESH_VAULT_ENROLMENT_CALLERS")),
RetireCallers: retireCallers(os.Getenv("MESH_VAULT_RETIRE_CALLERS")), Logins: busNodeLogins{}, ReadAct: readHandAct, TokenBase: TokenBaseFromFile(os.Getenv("MESH_VAULT_STATE")),
Bound: Bound{
Get: func(key string) ([]byte, error) {
e, err := stdio.State(BoundState).Get(key)
if err != nil || e == nil {
return nil, err
}
return e.Value, nil
},
Put: func(key string, value []byte) error {
_, err := stdio.State(BoundState).Put(key, json.RawMessage(value))
return err
},
Delete: func(key string) error { return stdio.State(BoundState).Delete(key) },
Keys: func() ([]string, error) { return stdio.State(BoundState).Keys() },
Provision: func(key string) ([]byte, error) {
e, err := stdio.State(ProvisionsRead).Get(key)
if err != nil || e == nil {
return nil, err
}
return e.Value, nil
},
}}
return env
}
// readHandAct reads a hand act back by its id through the seat mesh-hand-acts (the vault's manifest invokes it): nil when
// the log holds none.
func readHandAct(id string) (*HandActSeen, error) {
raw, err := stdio.Ask("mesh-hand-acts.hand-acts", map[string]any{"days": 2})
if err != nil {
return nil, err
}
var listing struct {
Acts []HandActSeen `json:"acts"`
}
if err := json.Unmarshal(raw, &listing); err != nil {
return nil, fmt.Errorf("mesh-hand-acts answered what is not its listing: %w", err)
}
for _, a := range listing.Acts {
if a.ID == id {
return &a, nil
}
}
return nil, nil
}
// watchLeavers begins the picture of the assignments and deletes a node's key of node-logins when its node key of the
// assignments is deleted (noderotation.go): the owner's event, never a poll. It answers whether it began; the minute pass
// (LoginsPass) catches what it missed.
func watchLeavers(env *Env) bool {
if env.Vault == nil {
return true
}
if assignmentsView == nil {
assignmentsView = newAssignmentsView(stdio.Ask, func(key string) {
if node, ok := leaverKey(key); ok {
env.Vault.NodeLeft(node)
}
}, env.Vault.AssignmentsChanged,
meshconditionsclient.FindingWriter{State: stdio.State(FindingsState)})
beginAssignmentsView(assignmentsView)
}
return true
}
// assigned is the assignments mirror as the vault's watch keeps it.
var assigned *WatchedAssignments
// watchAssignments begins the watch of the assignments mirror the vault's rules read (issue 1126, 2026-10-28): a
// watch, never a poll, so `own` and `get` read no assignment over the bus once it has caught up. It answers whether the
// watch began; one that did not is begun again at the next minute pass, and reads go to the bus meanwhile.
func watchAssignments() bool {
if assigned == nil {
return true
}
// Each attempt begins from nothing held: one that failed partway leaves nothing a later one would not hand again.
attempt := assigned.Begin()
if err := stdio.State(AssignmentsRead).Watch("", attempt.Take); err != nil {
assigned.Begin() // what the failed attempt took counts for nothing, and reads go to the bus
say("the assignments are not watched, so each is read over the bus; begun again at the next pass: %v", err)
return false
}
attempt.Caught()
say("the assignments are watched: the vault's rules read them from memory")
return true
}
// readHave is one key of the node-engines' have bucket, read through the runtime under the vault's own account (its
// manifest `reads` it), or nil when there is none.
func readHave(key string) ([]byte, error) {
e, err := stdio.State(HaveRead).Get(key)
if err != nil || e == nil {
return nil, err
}
return e.Value, nil
}
// watchHandings adopts from the bus module's have whenever it changes (adopt.go): a watch, never a poll. It answers
// whether the watch began; one that did not is begun again at the next minute pass.
func watchHandings(env *Env) bool {
if env.Vault == nil {
return true
}
// The have changes at every look of the bus module's health: why nothing was adopted is said once, and again when
// it changes.
var said string
err := stdio.State(HaveRead).Watch("*."+BusModule, func(c stdio.StateChange) error {
node := strings.TrimSuffix(c.Key, "."+BusModule)
if c.Op == "delete" || len(c.Value) == 0 {
env.Vault.HaveGone(node)
return nil
}
_, err := env.Vault.HaveHeard(node, c.Value, "the watch of "+HaveRead)
if line := fmt.Sprint(err); err != nil && line != said {
say("adopted nothing from %s: %v", c.Key, err)
said = line
} else if err == nil {
said = ""
}
return nil
})
if err != nil {
say("the bus module's have is not watched, so no hash is adopted; begun again at the next pass: %v", err)
return false
}
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
}
// watchCredentialAsks starts a pass over the providers' credentials as soon as a credential is asked of the vault in
// the seat mesh-provisioner's provisions (ADR 0570 §1, §6; issue 1126: a new consumer's login waited for nothing but
// the minute): a watch, never a poll. Asks heard while a pass runs are one pass after it. It answers whether the watch
// began; one that did not is begun again at the next minute pass, which makes the same values meanwhile.
func watchCredentialAsks(env *Env, kick chan<- struct{}) bool {
if env.Vault == nil {
return true
}
err := stdio.State(ProvisionsRead).Watch("", func(c stdio.StateChange) error {
if !c.Current && credentialAsked(c.Value) {
select {
case kick <- struct{}{}:
default: // a pass is already asked: it reads every ask
}
}
return nil
})
if err != nil {
say("the credentials asked of the vault are not watched, so each waits for the minute's pass; begun again then: %v", err)
return false
}
return true
}
// seatAssignments reads the seat mesh-module-assigner's assignments through its owner's tool and events (assignmentsview.go), never the
// owner's bucket.
var seatAssignments = viewAssignments(func() *views.Follower { return assignmentsView })
// 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
}
// 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
// returns why the pass did not re-seal everything, or nil.
func adoptOperatorKey(env *Env) error {
if env.Vault == nil {
if env.Broken != nil {
return env.Broken
}
return errors.New("the vault was not built")
}
if raw := exportedOperatorKey(env.Root); env.Root != "" && raw != "" {
key, err := publicPEM(raw)
if err == nil {
_, err = env.Vault.SetOperatorKey(key, "the mesh's export")
}
env.refuseKey("")
if err != nil {
env.refuseKey(err.Error())
say("the operator key in the mesh's export is not taken, or not every copy is sealed to it yet: %v", err)
}
return err
}
_, err := env.Vault.Reseal()
if err != nil {
say("%v", err)
}
return err
}
// readAssignment is one value of the controller's assignments mirror, read through the runtime under the vault's
// own account (its manifest `reads` it), or nil when there is none.
func readAssignment(key string) ([]byte, error) {
raw, err := readPlanRaw(key)
return asAssignment(key, raw, err)
}
// readPlanRaw is one key of the seat mesh-provisioner's plans as it is kept, nil when there is none.
func readPlanRaw(key string) ([]byte, error) {
e, err := stdio.State(AssignmentsRead).Get(key)
if err != nil || e == nil {
return nil, err
}
return e.Value, nil
}
// assignmentKeys is every key of the controller's assignments mirror, read the same way.
func assignmentKeys() ([]string, error) { return stdio.State(AssignmentsRead).Keys() }
func main() {
env := build()
go func() {
tick := time.NewTicker(time.Minute)
defer tick.Stop()
// At start, every covered account it holds whose hash is not of its current generation (ADR 0547 clause 9).
if env.Vault != nil {
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, watching := watchHandings(env), watchAssignments()
wants := watchWants(env)
leavers := watchLeavers(env)
kick := make(chan struct{}, 1)
go func() {
for range kick {
env.pairPass()
}
}()
asks := watchCredentialAsks(env, kick)
for {
if !asks {
asks = watchCredentialAsks(env, kick)
}
if !leavers {
leavers = watchLeavers(env)
}
if !watched {
watched = watchHandings(env)
}
if !watching {
watching = watchAssignments()
}
if !wants {
wants = watchWants(env)
}
env.pass()
if env.Vault != nil {
// The signing key and authority once the operator key is taken, and `signers` when it differs (ADR 0550 §2).
if err := env.Vault.SigningPass(); err != nil {
env.Vault.signingSaid(err)
} else {
env.Vault.signingSaid(nil)
}
env.Vault.HashOwed()
env.Vault.MintOwed()
env.Vault.LedgersPass()
env.Vault.RetirePass()
env.Vault.RotationPass()
env.Vault.LoginsPass()
}
env.pairPass()
if env.Custody != nil {
env.Custody.Pass()
}
<-tick.C
}
}()
if err := stdio.Serve("", tools(env)); err != nil {
say("%v", err)
os.Exit(1)
}
}
// retireCallers reads the vault's list `retire-callers`, the file the mesh writes from its trusted config value
// (stdio.ParseCallers): a list that cannot be read allows nobody, and says why.
func retireCallers(path string) stdio.Callers {
if path == "" {
say("the vault was started without its retire-callers list (MESH_VAULT_RETIRE_CALLERS): retire answers nobody")
return nil
}
raw, err := os.ReadFile(path)
if err != nil {
say("the retire-callers list cannot be read, so retire answers nobody: %v", err)
return nil
}
callers, err := stdio.ParseCallers(raw)
if err != nil {
say("%v: retire answers nobody", err)
return nil
}
return callers
}
// askCallers are the callers the vault's manifest names for secret-ask besides the controller (novox/hq ADR 0563 §8):
// MESH_VAULT_SECRET_ASK_CALLERS, comma-separated, each a module (on any machine), <machine>.<module> or person.<name>.
func askCallers(list string) stdio.Callers {
var out stdio.Callers
for _, c := range strings.Split(list, ",") {
if c = strings.TrimSpace(c); c != "" {
out = append(out, c)
}
}
return out
}