Provision and provider users take the provisioner's client; mesh-vault writes its findings through the conditions client (K-4..K-20, K-21 part; ADR 0634 §4; issue 1966)

This commit is contained in:
2026-11-05 14:26:48 +01:00
parent 7c617bb085
commit e8f93d5db0
45 changed files with 264 additions and 244 deletions
+3 -3
View File
@@ -1,7 +1,7 @@
package main
import (
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
"encoding/json"
"fmt"
@@ -78,14 +78,14 @@ const ProvisionerPrefix = "provisioner."
// seat mesh-provisioner's state `plans`, `<node>.<module>` (novox/hq ADR 0570 clause 2), no longer the controller's
// assignments. A plan is read as the assignment's value carried it (asAssignment), so the rules that read a plan are
// unchanged.
const AssignmentsRead = provisions.PlansRead
const AssignmentsRead = meshprovisionerclient.PlansRead
// asAssignment is a plan's key in the form the vault's rules read: `{node, module, provisioning: plan}`, nil for none.
func asAssignment(key string, raw []byte, err error) ([]byte, error) {
if err != nil || raw == nil {
return raw, err
}
p, err := provisions.DecodePlan(raw)
p, err := meshprovisionerclient.DecodePlan(raw)
if err != nil {
return nil, fmt.Errorf("the plan %s: %w", key, err)
}
+6 -8
View File
@@ -8,7 +8,8 @@
package main
import (
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
"encoding/json"
@@ -79,7 +80,7 @@ func build() *Env {
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(provisions.ProvisionsRead).Get(key)
e, err := stdio.State(meshprovisionerclient.ProvisionsRead).Get(key)
if err != nil || e == nil {
return nil, err
}
@@ -122,11 +123,8 @@ func watchLeavers(env *Env) bool {
if node, ok := leaverKey(key); ok {
env.Vault.NodeLeft(node)
}
}, env.Vault.PlacementChanged, func(key string, v json.RawMessage) error {
_, err := stdio.State(FindingsState).Put(key, v)
return err
},
func(key string) error { return stdio.State(FindingsState).Delete(key) })
}, env.Vault.PlacementChanged,
meshconditionsclient.FindingWriter{State: stdio.State(FindingsState)})
beginPlacementView(placementView)
}
return true
@@ -222,7 +220,7 @@ 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 {
err := stdio.State(meshprovisionerclient.ProvisionsRead).Watch("", func(c stdio.StateChange) error {
if !c.Current && credentialAsked(c.Value) {
select {
case kick <- struct{}{}:
+20 -20
View File
@@ -3,7 +3,7 @@ package main
// The vault's outcome for each credential it provides (novox/hq ADR 0570 clauses 3, 6, 7): an entry in its own state
// `bound`, one per credential provision `<node>.<consumer>.secret-<provision>[.<local>]`, which the holder of
// mesh-provisioner reads to set that key's state. The entry names the value by the vault's own name for it
// (provisions.VaultName, as its verb `own` names it) and its generation, never the value. A value the vault holds is
// (meshprovisionerclient.VaultName, as its verb `own` names it) and its generation, never the value. A value the vault holds is
// provided; one only the provider's software issues, or only a person gives, waits for them; one the vault has not
// made yet has no entry, and the key stays asked. An offer whose form is none has no credential and no entry. An entry
// is let go only once its key is retiring with the operator's warrant, or gone.
@@ -14,13 +14,13 @@ import (
"fmt"
"sort"
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
)
// BoundState is the vault's state of outcome entries.
const BoundState = "bound"
// Bound is the vault's own state `bound`, and what it reads of the seat's provisions.
// Bound is the vault's own state `bound`, and what it reads of the seat's meshprovisionerclient.
type Bound struct {
Get func(key string) ([]byte, error)
Put func(key string, value []byte) error
@@ -96,8 +96,8 @@ func (v *Vault) letGo(key string) bool {
if raw == nil {
return true
}
p, err := provisions.Decode(raw)
return err == nil && p.State == provisions.Retiring && p.Warrant != nil && p.Warrant.Option == "approve"
p, err := meshprovisionerclient.Decode(raw)
return err == nil && p.State == meshprovisionerclient.Retiring && p.Warrant != nil && p.Warrant.Option == "approve"
}
// outcomes is every credential's outcome the plans call for, by its key.
@@ -119,42 +119,42 @@ func (v *Vault) outcomes() (map[string]BoundOutcome, error) {
if err := json.Unmarshal(raw, &a); err != nil || a.Provisioning == nil { // binds, not handing
continue
}
consumer := provisions.End{Module: a.Module, Node: a.Node}
consumer := meshprovisionerclient.End{Module: a.Module, Node: a.Node}
for _, p := range a.Provisioning.Provisions {
provider := provisions.End{Module: p.ProviderModule, Node: p.Provider}
if p.ProviderModule == provisions.VaultModule && p.Provision == "secret" {
provider := meshprovisionerclient.End{Module: p.ProviderModule, Node: p.Provider}
if p.ProviderModule == meshprovisionerclient.VaultModule && p.Provision == "secret" {
// The vault's own offer: a value it hands the consumer's client on request (`get`), which it serves as
// soon as it runs.
key, err := provisions.Key(a.Node, a.Module, p.Provision, p.Local)
key, err := meshprovisionerclient.Key(a.Node, a.Module, p.Provision, p.Local)
if err == nil {
out[key] = BoundOutcome{Provider: p.Provider, ProviderModule: provisions.VaultModule, Provision: p.Provision,
Consumer: a.Node, Module: a.Module, Local: p.Local, State: provisions.Provided,
out[key] = BoundOutcome{Provider: p.Provider, ProviderModule: meshprovisionerclient.VaultModule, Provision: p.Provision,
Consumer: a.Node, Module: a.Module, Local: p.Local, State: meshprovisionerclient.Provided,
Why: "the vault hands it to the consumer's client on request"}
}
continue
}
ck, ok := provisions.CredentialKey(p.CredentialForm, consumer, provider, p.Provision, p.Local)
ck, ok := meshprovisionerclient.CredentialKey(p.CredentialForm, consumer, provider, p.Provision, p.Local)
if !ok {
continue // form none: no credential, nothing minted
}
parent := provisions.Provision{Provision: p.Provision, Local: p.Local, Consumer: consumer, Provider: provider}
name := provisions.VaultName(p.CredentialForm, parent)
cn, cm, cp, cl, err := provisions.ParseKey(ck)
parent := meshprovisionerclient.Provision{Provision: p.Provision, Local: p.Local, Consumer: consumer, Provider: provider}
name := meshprovisionerclient.VaultName(p.CredentialForm, parent)
cn, cm, cp, cl, err := meshprovisionerclient.ParseKey(ck)
if err != nil || name == "" {
continue
}
o := BoundOutcome{Provider: p.Provider, ProviderModule: provisions.VaultModule, Provision: cp, Consumer: cn,
o := BoundOutcome{Provider: p.Provider, ProviderModule: meshprovisionerclient.VaultModule, Provision: cp, Consumer: cn,
Module: cm, Local: cl, VaultName: name}
rec, held, err := v.Store.Read(name)
switch {
case err != nil:
o.State, o.Why = provisions.Failed, "the vault cannot read "+name+": "+err.Error()
o.State, o.Why = meshprovisionerclient.Failed, "the vault cannot read "+name+": "+err.Error()
case held:
o.State, o.Generation = provisions.Provided, rec.Generation
o.State, o.Generation = meshprovisionerclient.Provided, rec.Generation
case p.CredentialForm == FormIssued:
o.State, o.Why = provisions.Waits, "the provider's software issues it (ADR 0305): `issued` brings it in"
o.State, o.Why = meshprovisionerclient.Waits, "the provider's software issues it (ADR 0305): `issued` brings it in"
case p.CredentialForm == FormGiven:
o.State, o.Why = provisions.Waits, "a person gives it (ADR 0337): `accept` brings it in"
o.State, o.Why = meshprovisionerclient.Waits, "a person gives it (ADR 0337): `accept` brings it in"
default:
continue // not made yet: the key stays asked until the pair pass makes it
}
+3 -3
View File
@@ -59,8 +59,8 @@ import (
"strings"
"time"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-sdk/go/seal"
)
@@ -865,7 +865,7 @@ func (e *Env) credentialsState() []PairOutcome {
// 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 &&
p, err := meshprovisionerclient.Decode(raw)
return err == nil && p.State == meshprovisionerclient.Asked && p.Provider.Module == meshprovisionerclient.VaultModule &&
strings.HasPrefix(p.Provision, catalogue.CredentialProvisionPrefix)
}
@@ -37,7 +37,7 @@ var placementView *views.Follower
// newPlacementView makes the follower: ask calls the tool, gone is told of a deleted key, changed of every change after
// it is held (a first view's keys too).
func newPlacementView(ask func(key string, body any) (json.RawMessage, error), gone func(key string), changed func(),
put func(key string, value json.RawMessage) error, drop func(key string) error) *views.Follower {
findings views.FindingWriter) *views.Follower {
return &views.Follower{Owner: delivery.AssignerModule, Tool: PlacementTool, State: "placement", Logf: say,
Ask: func(_ context.Context, after string) (views.Answer, error) {
body := map[string]any{"values": true}
@@ -55,16 +55,13 @@ func newPlacementView(ask func(key string, body any) (json.RawMessage, error), g
Raise: func(s views.Status) {
say("the vault has no first view of the assigner's placement: %s has not answered since %s (%s); it keeps asking", s.Tool,
s.Asked.Format(time.RFC3339), s.Error)
if k, v, err := views.UnansweredFinding(Seat, s, time.Now()); err == nil {
if err := put(k, v); err != nil {
say("the finding %s is not written: %v", k, err)
}
f := views.UnansweredFinding(Seat, s, time.Now())
if err := findings.Put(f); err != nil {
say("the finding %s is not written: %v", f.ID, err)
}
},
Clear: func(s views.Status) {
if k, _, err := views.UnansweredFinding(Seat, s, time.Now()); err == nil {
_ = drop(k)
}
_ = findings.Delete(views.UnansweredFinding(Seat, s, time.Now()))
},
Changed: func(key string, _ json.RawMessage, deleted bool, _ time.Time) {
if deleted {
@@ -28,9 +28,7 @@ func TestAnUnansweredFirstViewOfThePlacementIsAFindingTheAnswerClears(t *testing
Values: map[string]json.RawMessage{"g14.node": json.RawMessage(`{}`)}})
return raw, nil
}
put := func(k string, v json.RawMessage) error { mu.Lock(); held[k] = v; mu.Unlock(); return nil }
drop := func(k string) error { mu.Lock(); delete(held, k); mu.Unlock(); return nil }
f := newPlacementView(ask, func(string) {}, nil, put, drop)
f := newPlacementView(ask, func(string) {}, nil, heldFindings{&mu, held})
f.Bound, f.Wait = 30*time.Millisecond, 10*time.Millisecond
reader := viewPlacement(func() *views.Follower { return f })
if _, err := reader.Keys(); err == nil {
@@ -57,3 +55,22 @@ func TestAnUnansweredFirstViewOfThePlacementIsAFindingTheAnswerClears(t *testing
t.Errorf("keys %v, %v", keys, err)
}
}
// heldFindings keeps the findings written, by the finding's kind and subject.
type heldFindings struct {
mu *sync.Mutex
held map[string]json.RawMessage
}
func (h heldFindings) Put(f views.Finding) error {
h.mu.Lock()
defer h.mu.Unlock()
h.held[f.ID+"."+f.Kind] = json.RawMessage(f.Summary)
return nil
}
func (h heldFindings) Delete(f views.Finding) error {
h.mu.Lock()
defer h.mu.Unlock()
delete(h.held, f.ID+"."+f.Kind)
return nil
}
@@ -196,16 +196,15 @@ func TestNoBucketHoldsASecret(t *testing.T) {
code = []byte(strings.Replace(string(code), "stdio.State(WantedRead).Watch(", "", 1))
code = []byte(strings.Replace(string(code), "stdio.State(WantedRead).Get(", "", 1))
// And its findings (ADR 0615 §6): the finding of a first view unanswered, written and deleted, never a secret.
code = []byte(strings.Replace(string(code), "stdio.State(FindingsState).Put(", "", 1))
code = []byte(strings.Replace(string(code), "stdio.State(FindingsState).Delete(", "", 1))
code = []byte(strings.Replace(string(code), "meshconditionsclient.FindingWriter{State: stdio.State(FindingsState)}", "", 1))
// And its outcome entries (novox/hq ADR 0570 clauses 3, 6): a value's name and generation, never a value; and
// one read of the seat's provisions, which hold none.
for _, call := range []string{"Get(", "Put(", "Delete(", "Keys("} {
code = []byte(strings.Replace(string(code), "stdio.State(BoundState)."+call, "", 1))
}
code = []byte(strings.Replace(string(code), "stdio.State(provisions.ProvisionsRead).Get(", "", 1))
code = []byte(strings.Replace(string(code), "stdio.State(meshprovisionerclient.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))
code = []byte(strings.Replace(string(code), "stdio.State(meshprovisionerclient.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)
+1
View File
@@ -3,6 +3,7 @@ module mesh-vault
go 1.26.0
require (
git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/seats/meshvault v0.0.0-20261104174344-ae5266e77639
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
github.com/nats-io/nats-server/v2 v2.11.17