Feature 57 (config and provisioning): release mesh-tools (the provider's StateBus, the SDK pin, templates), reviewed
mesh/merge-gate head 33d136e70227 merged into trunk 6b3e15042fee — pass, with a note: builds mesh-tools, node-tools (+40 dependent(s)) → ace, g14, novox, s…
mesh/repo-check head 33d136e70227 merged into trunk 6b3e15042fee — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the ch…
mesh/delivery delivered

This commit is contained in:
2026-10-16 09:41:17 +00:00
parent 6b3e15042f
commit 33d136e702
14 changed files with 963 additions and 8 deletions
+98
View File
@@ -0,0 +1,98 @@
package bus
import (
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/nats-io/nats.go"
)
// A module's code asks its machine's provisioner client for its values at runtime (novox/hq ADR 0349 clause 14 as
// ADR 0370 changes it, issue 685).
//
// While a module's code runs here, its asks reach the bus under this runtime's account, and the bus proves only
// that account. So the runtime asks its own machine's client on ProvisionAskSubject — which the controller grants
// this machine's tool runner alone to publish (ADR 0370 §3) — naming the module itself, from the bundle that asks
// and never from the module's code or arguments (§4). The client answers that module's config values, for what its
// manifest declares, and no secret: the vault hands a secret only to a caller the bus proves (§2).
// ProvisionAskSubject is where this machine's tool runner asks its provisioner client (mesh-controller
// internal/broker ProvisionerAskSubject): `prov.ask.<node>`.
func ProvisionAskSubject(node string) string { return "prov.ask." + node }
// ProvisionChangedSubjects is every module's word of what moved on a machine (mesh-controller internal/broker
// ProvisionerChangedSubject): `prov.changed.<node>.<module>`, a module's name dots and all.
func ProvisionChangedSubjects(node string) string { return "prov.changed." + node + ".>" }
// ProvisionTimeout bounds one ask of the client; a variable so a test need not wait.
var ProvisionTimeout = 10 * time.Second
// ProvisionChanged is what the client says moved of one module: the kind, and the key, or every key of the kind when
// empty. Never the value.
type ProvisionChanged struct {
Kind string `json:"kind"`
Key string `json:"key,omitempty"`
}
// ProvisionRefused is the client's refusal, in its own words: a key the module does not declare, a module not placed
// on the machine, a secret, a kind it does not answer yet.
type ProvisionRefused struct{ Reason string }
func (r *ProvisionRefused) Error() string { return r.Reason }
// AskProvisioner asks this machine's provisioner client for one value of module: kind `config` or `bound`, and the
// key. The module is the caller's to name from the bundle it runs; this never takes it from what a bundle sent.
func (c *Conn) AskProvisioner(module, kind, key string) (json.RawMessage, error) {
if c.node == "" {
return nil, errors.New("this runtime names no machine, so it has no provisioner client to ask")
}
body, err := json.Marshal(map[string]string{"module": module, "kind": kind, "key": key})
if err != nil {
return nil, err
}
msg, err := c.nc.Request(ProvisionAskSubject(c.node), body, ProvisionTimeout)
if errors.Is(err, nats.ErrNoResponders) {
return nil, fmt.Errorf("%s's provisioner client does not answer: it is not running there, or the bus does "+
"not let %s ask it", c.node, c.cred.User)
}
if err != nil {
return nil, fmt.Errorf("asking %s's provisioner client for %s's %s %s: %w", c.node, module, kind, key, err)
}
var answered struct {
Value json.RawMessage `json:"value"`
Error string `json:"error"`
}
if err := json.Unmarshal(msg.Data, &answered); err != nil {
return nil, fmt.Errorf("%s's provisioner client answered something that is not its answer: %w", c.node, err)
}
if answered.Error != "" {
return nil, &ProvisionRefused{Reason: answered.Error}
}
if len(answered.Value) == 0 {
return nil, fmt.Errorf("%s's provisioner client answered no value and no reason", c.node)
}
return answered.Value, nil
}
// OnProvisionChanged hands told every word of this machine's provisioner client of what moved, by module, until
// stopped.
func (c *Conn) OnProvisionChanged(told func(module string, changed ProvisionChanged)) (func(), error) {
if c.node == "" {
return nil, errors.New("this runtime names no machine, so it hears no provisioner client")
}
prefix := "prov.changed." + c.node + "."
sub, err := c.nc.Subscribe(ProvisionChangedSubjects(c.node), func(m *nats.Msg) {
var changed ProvisionChanged
if json.Unmarshal(m.Data, &changed) != nil || changed.Kind == "" {
return
}
told(strings.TrimPrefix(m.Subject, prefix), changed)
})
if err != nil {
return nil, err
}
return func() { _ = sub.Unsubscribe() }, c.nc.Flush()
}