mesh-vault: make a new consumer's first credential whether or not it is handed
mesh/delivery merged: source.main moved to novox/mesh-catalog@c52afdf88b84
mesh/delivery-group group fix/1126-provision-credentials merged: every member is merged
mesh/check pass 019614aedc14 on fe7c5ccb64bf | unit 6028 run, 142 kept, 16 skipped | integration 6091 run, 174 kept, 25 skipped | over bound: unit, in…

With the controller gone nothing made the pair row of a new consumer: the vault minted only for a provider
handed to the provisioner (ADR 0349 clause 17's premise, that a holder filled the old way gets the controller's
value). A node composing its own now asks the vault for its credentials (own, ADR 0547 §2, ADR 0550 §5), so the
vault makes the first value of a shared or per-login provision for every holder, create-only (issue 1126; ADR
0570 §6). A remake or a rotation still waits while a holder is filled the old way. A credential asked of the
vault in mesh-provisioner's provisions starts a pass at once, one pass at a time, and vault_status answers each
provider and provision's last outcome as `credentials`.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-02 15:37:23 +01:00
parent fe7c5ccb64
commit 019614aedc
6 changed files with 158 additions and 38 deletions
+7 -10
View File
@@ -123,15 +123,15 @@ func TestAProviderWithRowsIsUnchangedByTheFormItsPlansName(t *testing.T) {
}
// A form the vault does not make (issued by the provider's software, or given by a person), a form it does not know,
// or plans that disagree: nothing is made, and the pass says why. The rule that no holder is left behind still holds.
// or plans that disagree: nothing is made, and the pass says why. (A first value for a provider not handed is made since
// issue 1126: TestAFirstValueIsMadeForEveryHolderAndARemakeWaitsForOneFilledTheOldWay.)
func TestTheVaultMakesNothingForAFormItDoesNotMakeOrCannotTrust(t *testing.T) {
for form, said := range map[string]string{
"issued": "issued by the provider's software",
"given": "given by a person",
"one-for-all": "which is not a credential form",
"shared|pair": "disagree",
"shared|": "no plan says whether",
"shared@unmoved": "stand-in on anchor",
"issued": "issued by the provider's software",
"given": "given by a person",
"one-for-all": "which is not a credential form",
"shared|pair": "disagree",
"shared|": "no plan says whether",
} {
t.Run(form, func(t *testing.T) {
provider := handedWith("anchor", "stand-in")
@@ -139,9 +139,6 @@ func TestTheVaultMakesNothingForAFormItDoesNotMakeOrCannotTrust(t *testing.T) {
if a, b, ok := strings.Cut(form, "|"); ok {
first, second = a, b
}
if a, _, ok := strings.Cut(form, "@"); ok {
first, second, provider = a, a, unmoved("anchor", "stand-in")
}
entry := func(f string) string {
if f == "" {
return "stand-in-api@anchor/stand-in"
+34
View File
@@ -208,6 +208,30 @@ func watchWants(env *Env) bool {
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(provisions.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
}
// placement reads the seat mesh-placement'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() },
@@ -293,7 +317,17 @@ func main() {
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)
}
+47 -14
View File
@@ -10,14 +10,18 @@ package main
// client records its needs from. A consumer binds when such an entry appears and unbinds when it goes, or when its
// assignment goes. The vault reads every plan on each pass (once a minute, and when it starts).
//
// **Every holder can get it, or nothing is made** (the lead's rule on issue 677, note 10). The holders are the
// provider on its machine and every consumer bound to it. Only a module handed to the provisioner gets a value from
// the vault; one still filled the old way gets the controller's (ADR 0349 clause 17). So the vault mints or remakes
// a provider's value only when the provider's assignment is handed to the provisioner, and no consumer the vault
// knows to hold it is still filled the old way: a consumer whose pair row the move brought in (V3) and whose
// assignment is still there with no plan. Otherwise it makes nothing, and says why on each pass that changes what
// it says. A consumer filled the old way that bound after the move names no provider on the bus at all; it is the
// provider's move (design 57 B2, B3) that ends that case.
// **A first value is made for every holder; a remake waits for one filled the old way** (the lead's rule on issue
// 677, note 10, as issue 1126 left it). The holders are the provider on its machine and every consumer bound to it. A
// module handed to the provisioner gets its value through its client (`get`); one that is not gets it through its
// machine's node-engine, which composes its own and asks the vault for exactly the credentials its composition reads
// (`own`, ADR 0547 §2, ADR 0550 §5), now that no controller makes them (ADR 0570 §6). So a provider or consumer the
// vault holds no value for gets its first one made whether or not it is handed: a login of its own for each new
// consumer of a per-login provider, or the provider's one shared value. That is create-only: a value made is never
// made again by a bind. A remake (a bind or unbind changing who holds the shared value) and a rotation still wait
// while the provider, or a consumer whose pair row the move brought in, is not handed: a node composing its own would
// carry the old value until it composes again. The vault says why on each pass that changes what it says. A
// credential asked of the vault in the seat mesh-provisioner's provisions (ADR 0570 §1, §6) starts a pass at once
// (main.go watchCredentialAsks), not only at the minute's.
//
// **What the move brought in is adopted, and nothing is rotated early.** The move (V3) copied each pair row as
// `pair/<provider>/<provision>/<consumer>/<module>[/<local>]`. Rows a provider shares (ADR 0158: its own secret and
@@ -56,6 +60,7 @@ import (
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-sdk/go/seal"
)
@@ -333,12 +338,21 @@ func sharedHolders(k pairKey, b *binds) (string, []string) {
return strings.Join(modules, ","), consumers
}
// heldBack says why the vault may not make a value for k now, or "": a holder that would never get it.
func heldBack(k pairKey, b *binds, rows []pairRow, seen bindings) string {
// unnamedProvider says why the vault may not make a first value for k, or "": no plan names the provider's module.
func unnamedProvider(k pairKey, b *binds) string {
if b == nil || len(b.providerModules) == 0 {
return fmt.Sprintf("no plan names the module serving %s on %s, so the vault cannot tell the provider gets "+
"a value it makes", k.provision, k.provider)
}
return ""
}
// heldBack says why the vault may not remake or rotate a value for k now, or "": a holder filled the old way, which
// would carry the old value until its node composes again.
func heldBack(k pairKey, b *binds, rows []pairRow, seen bindings) string {
if why := unnamedProvider(k, b); why != "" {
return why
}
for m := range b.providerModules {
if !seen.handed[k.provider+"."+m] {
return fmt.Sprintf("the provider %s on %s is not handed to the provisioner, so it would never get a "+
@@ -482,9 +496,7 @@ func (v *Vault) pairOne(k pairKey, b *binds, rows []pairRow, seen bindings) (Pai
}
}
if len(missing) > 0 {
if held := heldBack(k, b, rows, seen); held != "" {
return PairOutcome{Name: name, Outcome: PairHeld, Why: held}, nil
}
// Each one's first login, create-only: whoever composes it, its node asks the vault for it.
return v.firstLogins(k, missing)
}
}
@@ -512,7 +524,8 @@ func (v *Vault) pairOne(k pairKey, b *binds, rows []pairRow, seen bindings) (Pai
if len(consumers) == 0 {
return PairOutcome{Name: name, Outcome: PairUnchanged}, nil
}
if why := heldBack(k, b, rows, seen); why != "" {
// A first value, create-only: every holder gets it through its client or its node's own composition.
if why := unnamedProvider(k, b); why != "" {
return PairOutcome{Name: name, Outcome: PairHeld, Why: why}, nil
}
form, why := b.form(k)
@@ -748,6 +761,9 @@ func (e *Env) pairPass() {
if e.Vault == nil {
return
}
// One pass at a time: the minute's and one an ask of the vault started (watchCredentialAsks) never race a mint.
e.pairing.Lock()
defer e.pairing.Unlock()
outcomes, err := e.Vault.PairPass()
if said, oerr := e.Vault.OutcomePass(); oerr != nil {
say("the credentials' outcomes are not all written: %v", oerr)
@@ -765,6 +781,7 @@ func (e *Env) pairPass() {
if err != nil {
e.lastPairFailure = err.Error()
}
e.pairOutcomes = outcomes
if e.pairsSaid == nil {
e.pairsSaid = map[string]string{}
}
@@ -836,3 +853,19 @@ func pairRowRefusal(read AssignmentReader, node string, parts []string) string {
}
return fmt.Sprintf("%s's plan on %s does not name %s from %s under that name", module, consumer, provision, provider)
}
// credentialsState is what the last pass did, or did not do, for each provider and provision, and why: what
// `vault_status` answers as `credentials`, so a provision whose credential is held back is read, not guessed.
func (e *Env) credentialsState() []PairOutcome {
e.mu.Lock()
defer e.mu.Unlock()
return slices.Clone(e.pairOutcomes)
}
// credentialAsked is whether a key of the seat mesh-provisioner's provisions asks the vault for a credential (ADR 0570
// §1, §6): a provision `secret-<provision>` whose provider is the vault, in state asked.
func credentialAsked(raw []byte) bool {
p, err := provisions.Decode(raw)
return err == nil && p.State == provisions.Asked && p.Provider.Module == provisions.VaultModule &&
strings.HasPrefix(p.Provision, catalogue.CredentialProvisionPrefix)
}
+60 -12
View File
@@ -398,24 +398,72 @@ func TestAProviderTheVaultHoldsNoRowOfIsNotGuessed(t *testing.T) {
}
}
// The vault makes no value a holder would never get (the lead's rule on issue 677): a provider not handed to the
// provisioner, or a consumer the move brought in that is still filled the old way, holds every make and remake
// back, and a rotation too.
func TestNoValueIsMadeThatAHolderFilledTheOldWayWouldNeverGet(t *testing.T) {
t.Run("an unmoved provider", func(t *testing.T) {
// keyedWith is a plan naming provisions alone: its module is not handed to the provisioner, and its node composes its
// own and asks the vault for its credentials (`own`, ADR 0547 §2, ADR 0550 §5).
func keyedWith(node, module string, provisions ...string) string {
return strings.Replace(handedWith(node, module, provisions...), `"templates":{"source":"x","digest":"y"},`, "", 1)
}
// After the controller, a node composing its own gets its credentials from the vault (`own`), so a first value is made
// for a new consumer whether or not its ends are handed to the provisioner, and never remade (issue 1126, 2026-11-02:
// nothing made the row of a new consumer once the controller's perform was gone; ADR 0570 §6). A remake, and a
// rotation, still wait while a holder is filled the old way (the lead's rule on issue 677).
func TestAFirstValueIsMadeForEveryHolderAndARemakeWaitsForOneFilledTheOldWay(t *testing.T) {
t.Run("a first value for holders that compose their own", func(t *testing.T) {
mirror := assignments{
"anchor.stand-in": unmoved("anchor", "stand-in"),
"g14.app": handedWith("g14", "app", "stand-in-api@anchor/stand-in"),
"anchor.stand-in": keyedWith("anchor", "stand-in"),
"ace.cli": keyedWith("ace", "cli", "stand-in-api@anchor/stand-in+pair"),
}
v := pairVault(t, mirror)
if o := pass(t, v, standIn); o.Outcome != PairHeld || !strings.Contains(o.Why, "stand-in on anchor") {
t.Fatalf("an unmoved provider: %+v", o)
if o := pass(t, v, standIn); o.Outcome != PairMade {
t.Fatalf("a new consumer's login: %+v", o)
}
if _, held, _ := v.Store.Read(standIn); held {
t.Fatal("a value was made that the provider would never get")
login := "pair/anchor/stand-in-api/ace/cli"
first, held, _ := v.Store.Read(login)
if !held {
t.Fatal("no login was made for the new consumer")
}
if o := pass(t, v, standIn); o.Outcome != PairPerPair {
t.Fatalf("the pass after: %+v", o)
}
if again, _, _ := v.Store.Read(login); again.Generation != first.Generation || again.AtRest != first.AtRest {
t.Fatal("a login made once was made again")
}
env := &Env{Vault: v.Vault}
env.pairPass()
if got := env.credentialsState(); len(got) != 1 || got[0].Name != standIn || got[0].Outcome != PairPerPair {
t.Fatalf("the credentials' state reads %+v", got)
}
mirror = assignments{
"anchor.stand-in": keyedWith("anchor", "stand-in"),
"ace.cli": keyedWith("ace", "cli", "stand-in-api@anchor/stand-in+shared"),
}
v = pairVault(t, mirror)
if o := pass(t, v, standIn); o.Outcome != PairMade {
t.Fatalf("a new consumer's shared value: %+v", o)
}
mirror["g14.app"] = keyedWith("g14", "app", "stand-in-api@anchor/stand-in+shared")
if o := pass(t, v, standIn); o.Outcome != PairHeld || !strings.Contains(o.Why, "stand-in on anchor") {
t.Fatalf("a second consumer remakes the value its provider composes from: %+v", o)
}
if rec, _, _ := v.Store.Read(standIn); rec.Generation != 1 {
t.Fatalf("the shared value was remade: generation %d", rec.Generation)
}
if _, err := v.Rotate(ControllerCaller, standIn, "person.jochen", "why not"); err == nil {
t.Fatal("a rotation made a value the provider would never get")
t.Fatal("a rotation remade a value a holder filled the old way holds")
}
})
t.Run("an ask of the vault for a credential", func(t *testing.T) {
for raw, want := range map[string]bool{
`{"v":1,"provision":"secret-stand-in-api","consumer":{"module":"cli","node":"ace"},"provider":{"module":"mesh-vault","node":"novox"},"state":"asked"}`: true,
`{"v":1,"provision":"secret-stand-in-api","consumer":{"module":"cli","node":"ace"},"provider":{"module":"mesh-vault","node":"novox"},"state":"provided"}`: false,
`{"v":1,"provision":"stand-in-api","consumer":{"module":"cli","node":"ace"},"provider":{"module":"stand-in","node":"anchor"},"state":"asked"}`: false,
`not a provision`: false,
} {
if credentialAsked([]byte(raw)) != want {
t.Errorf("%s is an ask of the vault: %v", raw, !want)
}
}
})
t.Run("an unmoved consumer", func(t *testing.T) {
+8 -2
View File
@@ -43,6 +43,10 @@ type Env struct {
// did not finish (pair.go): each said once, and again when it changes.
pairsSaid map[string]string
lastPairFailure string
// pairOutcomes is the last pass's outcome for each provider and provision (vault_status `credentials`); pairing
// keeps one pass at a time.
pairOutcomes []PairOutcome
pairing sync.Mutex
}
func (e *Env) keyRefused() string {
@@ -622,7 +626,9 @@ func definitions(env *Env) []toolDef {
Description: "What needs a person's eye in the vault: operator copies still sealed to an earlier key, the operator " +
"key the mesh's export holds if the vault refused it, how often the record of hand-outs was rolled, and " +
"minting (novox/hq ADR 0561 §9, issue 774): the vault mints every wanted bus account it holds no value for, " +
"every wanted account it holds back with why (never one whose value it holds), and those it minted.",
"every wanted account it holds back with why (never one whose value it holds), and those it minted; and " +
"`credentials`: for each provider and provision, what the last pass did for its credential (made, held " +
"and why, per pair) (issue 1126).",
run: func(string, map[string]any) (any, error) {
if env.Vault == nil {
return map[string]any{"ready": false, "error": fmt.Sprint(env.Broken)}, nil
@@ -634,7 +640,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(), "signing": env.Vault.SigningState(), "ok": left == 0 && env.keyRefused() == "" && key != "", "rotation": env.Vault.RotationState()}, nil
"minting": env.Vault.MintState(), "credentials": env.credentialsState(), "retiring": env.Vault.RetireState(), "signing": env.Vault.SigningState(), "ok": left == 0 && env.keyRefused() == "" && key != "", "rotation": env.Vault.RotationState()}, nil
}},
}...)
}
@@ -206,6 +206,8 @@ func TestNoBucketHoldsASecret(t *testing.T) {
code = []byte(strings.Replace(string(code), "stdio.State(BoundState)."+call, "", 1))
}
code = []byte(strings.Replace(string(code), "stdio.State(provisions.ProvisionsRead).Get(", "", 1))
// And one watch of them, for a credential asked of the vault (issue 1126).
code = []byte(strings.Replace(string(code), "stdio.State(provisions.ProvisionsRead).Watch(", "", 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)