Feature 57 (config and provisioning): modules declare config and provisions, their files rendered from templates; synced with main (issues of feature 57, 874)

This commit is contained in:
2026-10-16 07:37:43 +00:00
parent 1a2c917abf
commit 9652b2d5ce
771 changed files with 52063 additions and 1774 deletions
+52
View File
@@ -0,0 +1,52 @@
# mesh-config
Holds the `mesh-config` seat: the owner of every config value (novox/hq ADR 0348 §9, design 57 K3). Go, one
binary (`cmd/mesh-config`), served by a runtime of its own on its own bus account (`runs-as`), so that its
account alone writes its buckets.
## What it keeps
- `mesh-config_values`: each value set, keyed by its level: `mesh.<key>` (mesh config), `node.<node>.<key>`
(node config) and `module.<node>.<module>.<key>` (a module's own value on one node), each with who set it,
through which account, why and when.
- `mesh-config_resolved`: each assigned module's value for each key it declares on its node, keyed
`<node>.<module>.<key>`, with its source: `default`, `mesh`, `node`, `module`, or `unset` for a required value
nothing sets.
- `mesh-config_predefined`: the list of predefined keys, published from the list the controller kept (its mirror
`mesh-controller.predefined`), the newest trunk build winning. A newer list dropping a key that still holds a
value at the mesh or a node is withheld, and every listing says why, until that value is cleared.
- `mesh-config_proposals`: each value proposed for the operator's Approve, whole, with the value stored at its level
when it was proposed (what the operator is shown the change against), who proposed it, why, and what came of it.
- the log `history`: every set, clear and move of resolved values, with the proven caller and why.
## How a value resolves
From the controller's mirrors of the assignments and of each module's declarations (`mesh-controller.assignments`,
`mesh-controller.declared-config`): the manifest's default; mesh config for the predefined key the value falls back
to; node config for that key on the node; the module's own value on that node, the most specific winning. A value
naming no predefined key resolves from its default and the module's own value only (ADR 0348 §2). It resolves at
start, whenever a mirror moves, and every five minutes, writing only the values that moved.
## The verb
`mesh-config.config` with `action` `list` (the default), `set`, `clear` or `history`, narrowed by `module`, `node`
and `key`. A set or a clear names one level (no module and no node for mesh config, a node for node config, both
for a module's own value), needs `why`, refuses a key the module or the predefined list does not declare (naming
the declared keys), validates the value, writes it and the resolved values it moves, keeps it in the history and
the controller's hand-act log, raises `config-changed`, and answers with each module on each node whose value
moved. It opens no walk. A trusted value is taken only from the controller, which carries it from its terminal and
names who gave it in `by`, or on the operator's Approve of a proposal.
## Proposing a value (ADR 0348 §8, design 57 K4)
`action` `propose` (with `value`, or `clear`) judges the value as a set would, keeps it in `mesh-config_proposals`,
and asks the operator under mesh-config's own name on the `operator-channel` seat (its own grant, from `uses`, and
its own cap of three open asks; the same change twice is refused, and a newer proposal for the same level and key
replaces the older). The ask says the level, the key and the value, exact where who answers is proven, and that,
approved, it takes effect at once on every node it reaches; Approve and Decline are both at the level approve and
each binds the proposal's level, key, value and the value it was shown against. On the answer mesh-config reads
the router's own record of its ask and holds the warrant to the ask composed again from the proposal; it refuses,
and sets nothing, when the proposal or the value stored at the level changed since. An Approve that holds sets the
value as a set does, keeps who approved it beside the value (`approved`), and asks the controller to record the
warrant in its hand-act log. A Decline, an expiry or the router's refusal sets nothing. `action` `proposals` lists
them, newest first; with `proposal`, one read whole, with whether the router's ask is the one composed from it.
@@ -0,0 +1,438 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"sync"
"time"
)
// The verb `config` (novox/hq ADR 0348 §1, §8, §9, §10, design 57 K3): set or clear a value at one level with why,
// list the values, and read their history, narrowed by module and node. A set refuses a key the module or the list
// of predefined keys does not declare, naming the declared keys; validates the value; writes it and the resolved
// values it moves; records the change in the history and the hand-act log; raises `config-changed`; and answers
// with each module on each node whose value moved. A config change opens no walk and waits at no first-node gate.
//
// **Trusted values at the terminal or on the operator's Approve.** A trusted value (ADR 0348 §8) is set only by a
// call the controller carries from its terminal, or on the operator's warrant for a proposal (proposals.go, design
// 57 K4); a set through the verb is refused and names the proposal as the way.
// ControllerCaller is the controller's bus user: the one caller that carries a value given at its terminal.
const ControllerCaller = "controller"
// Unproven is the caller of a call the runtime named none for (the SDK's word).
const Unproven = "unproven"
// Entry is one key's current value in a state.
type Entry struct {
Key string
Value json.RawMessage
Revision uint64
}
// Store is the bus as this module reaches it: the state it keeps and reads, its history log, its events, and the
// controller's hand-act log.
type Store interface {
Get(state, key string) (*Entry, error)
Put(state, key string, value any) error
Delete(state, key string) error
Keys(state string) ([]string, error)
Append(log, key string, value any) error
Read(log string) ([]LogEntry, error)
Emit(event string, body any) error
HandAct(what, why, cause string) error
}
// LogEntry is one entry of a log.
type LogEntry struct {
Key string
Value json.RawMessage
At time.Time
}
// ConfigChanged is the event every change raises: what was set or cleared at which level, by whom and why, and
// each module on each node whose resolved value moved. Never a secret: config holds none.
const ConfigChanged = "config-changed"
// Change is a set, a clear, or a move of resolved values with no set behind it (a declaration or an assignment
// changed), as the history and the event say it.
type Change struct {
Action string `json:"action"`
Level *Level `json:"level,omitempty"`
Key string `json:"key,omitempty"`
Value json.RawMessage `json:"value,omitempty"`
Was json.RawMessage `json:"was,omitempty"`
By string `json:"by,omitempty"`
Via string `json:"via,omitempty"`
Why string `json:"why,omitempty"`
// Approved is who approved it on a channel that proves who answered, for a value set on the operator's warrant.
Approved string `json:"approved,omitempty"`
At time.Time `json:"at"`
Moved []Moved `json:"moved"`
}
// Config is the module: its store, its clock, and one lock, so a set and a resolution never interleave.
type Config struct {
Store Store
Now func() time.Time
Logf func(format string, a ...any)
mu sync.Mutex
// withheld is why the newest list of predefined keys was not published, empty when it was (publish).
withheld string
// Asks is the operator channel as this module asks on it (proposals.go); nil, nothing can be proposed.
Asks AskBus
// RouterWithin and RouterEvery bound how long a proposal waits for the router's word on its ask, and how often
// it looks; zero is RouterAnswersWithin and RouterAnswersEvery.
RouterWithin, RouterEvery time.Duration
endedMu sync.Mutex
ended map[string]endedWord
}
// Call is one call of the verb, its arguments read.
type Call struct {
Action string
Key string
Value any
Module string
Node string
Why string
// Caller is the bus user the runtime proved; By is who gave the value at the controller's terminal, said by the
// controller alone.
Caller string
By string
Limit int
// Clear asks a proposal to clear the value instead of setting it; Proposal names one proposal to read.
Clear bool
Proposal string
// approval is the operator's warrant a set or a clear is made on: given by a decided proposal alone, never by
// a call's arguments.
approval *approval
}
// SetAnswer is what a set or a clear answers.
type SetAnswer struct {
Said string `json:"said"`
Level Level `json:"level"`
Key string `json:"key"`
Value json.RawMessage `json:"value,omitempty"`
Was json.RawMessage `json:"was,omitempty"`
Moved []Moved `json:"moved"`
Result []NodeResult `json:"result"`
// Unsaid is what could not be recorded or announced after the value was written: said, never dropped.
Unsaid []string `json:"unsaid,omitempty"`
}
// NodeResult is one affected node's result. Until the provisioner client fills files from mesh-config_resolved
// (design 57 P5, P6), a module on a node takes its new value at its next composition; that is said, not hidden.
type NodeResult struct {
Node string `json:"node"`
Modules []string `json:"modules"`
Result string `json:"result"`
}
// Do runs one call of the verb.
func (c *Config) Do(call Call) (any, error) {
switch call.Action {
case "", "list":
return c.List(call)
case "set", "clear":
return c.Change(call)
case "history":
return c.History(call)
case "propose":
return c.Propose(call)
case "proposals":
return c.Proposals(call)
}
return nil, fmt.Errorf("config does set, clear, list, history, propose or proposals; %q is none of them", call.Action)
}
// level is the level a call names, refused as ADR 0348 §3 says: a module's own value is for one node.
func (call Call) level() (Level, error) {
for _, name := range []string{call.Node, call.Module} {
if name != "" && !nodeOrModule.MatchString(name) {
return Level{}, fmt.Errorf("%q is not a node's or a module's name", name)
}
}
switch {
case call.Module != "" && call.Node != "":
return Level{Kind: LevelModule, Node: call.Node, Module: call.Module}, nil
case call.Module != "":
return Level{}, fmt.Errorf("a module's own value is set for one node, and no node is named: there is no "+
"module value for the whole mesh (novox/hq ADR 0348 §3); name the node, or set mesh config for the "+
"predefined key %s's value falls back to, or change its default in the catalogue", call.Module)
case call.Node != "":
return Level{Kind: LevelNode, Node: call.Node}, nil
}
return Level{Kind: LevelMesh}, nil
}
// Change sets or clears one value.
func (c *Config) Change(call Call) (*SetAnswer, error) {
if call.Clear || call.Proposal != "" {
return nil, fmt.Errorf("clear and proposal are a proposal's: %s takes neither", call.Action)
}
call.approval = nil // only a decided proposal gives one
c.mu.Lock()
defer c.mu.Unlock()
return c.change(call)
}
// change sets or clears one value; the caller holds the lock.
func (c *Config) change(call Call) (*SetAnswer, error) {
level, err := call.level()
if err != nil {
return nil, err
}
if !keyName.MatchString(call.Key) {
return nil, fmt.Errorf("name the key to %s: lower-case letters, digits, dots, dashes and underscores", call.Action)
}
if strings.TrimSpace(call.Why) == "" {
return nil, fmt.Errorf("say why the value is %s: every change keeps its why (novox/hq ADR 0348 §9)",
map[string]string{"set": "set", "clear": "cleared"}[call.Action])
}
if call.Caller == "" || call.Caller == Unproven {
return nil, errors.New("the call names no proven caller, and every change keeps who made it " +
"(novox/hq ADR 0348 §9): call config through the mesh, which names you")
}
by := call.Caller
if call.By != "" {
if call.Caller != ControllerCaller {
return nil, fmt.Errorf("by names who gave a value at the controller's terminal, which only the controller "+
"says; %s is not the controller", call.Caller)
}
by = call.By
}
s, err := c.sources()
if err != nil {
return nil, err
}
r, trusted, err := c.declared(s, level, call.Key)
if err != nil {
return nil, err
}
vk := level.ValueKey(call.Key)
was, had := s.Values[vk]
if call.approval != nil {
// **On Approve, the value stored must still be the one the operator was shown the change against** (ADR
// 0348 §8, ADR 0277 §13): a value set or cleared since, at the terminal or on another warrant, refuses it.
if had != call.approval.HadValue || valueDigest(was.Value, had) != call.approval.WasDigest {
return nil, fmt.Errorf("%s at %s is %s now, and the operator was shown the change against %s: it changed "+
"since, so nothing is set; propose it again", call.Key, level, shownOrNone(was.Value, had),
shownOrNone(call.approval.Was, call.approval.HadValue))
}
by = call.approval.By
}
answer := &SetAnswer{Level: level, Key: call.Key}
if had {
answer.Was = compact(was.Value)
}
now := c.now()
switch call.Action {
case "set":
if call.Value == nil {
return nil, fmt.Errorf("give the value to set %s to", call.Key)
}
raw, err := json.Marshal(call.Value)
if err != nil {
return nil, fmt.Errorf("the value cannot be read: %v", err)
}
var value any
_ = json.Unmarshal(raw, &value)
if problems := r.check(value); len(problems) > 0 {
return nil, fmt.Errorf("%s is refused for %s at %s: %s", show(value), call.Key, level,
strings.Join(problems, "; "))
}
if trusted && call.Caller != ControllerCaller && call.approval == nil {
return nil, fmt.Errorf("%s is trusted (it decides what runs or who can reach what), so it is set only at "+
"the controller's terminal or on the operator's Approve (novox/hq ADR 0348 §8); %s is not the terminal: "+
"propose it instead, with config's action propose", call.Key, call.Caller)
}
v := Value{Value: compact(raw), By: by, Via: call.Caller, Why: strings.TrimSpace(call.Why), At: now}
if call.approval != nil {
v.Approved = call.approval.Says
}
if err := c.Store.Put(ValuesState, vk, v); err != nil {
return nil, fmt.Errorf("the value was not written: %v", err)
}
s.Values[vk] = v
answer.Value = v.Value
case "clear":
if !had {
return nil, fmt.Errorf("nothing is set for %s at %s, so there is nothing to clear", call.Key, level)
}
if trusted && call.Caller != ControllerCaller && call.approval == nil {
return nil, fmt.Errorf("%s is trusted, so it is cleared only at the controller's terminal or on the "+
"operator's Approve (novox/hq ADR 0348 §8); %s is not the terminal: propose the clear instead, with "+
"config's action propose and clear", call.Key, call.Caller)
}
if err := c.Store.Delete(ValuesState, vk); err != nil {
return nil, fmt.Errorf("the value was not cleared: %v", err)
}
delete(s.Values, vk)
}
moved, err := c.writeResolved(s)
answer.Moved = moved
if err != nil {
// The value is written and the resolved values are not all: said, and put right on the next resolution.
answer.Unsaid = append(answer.Unsaid, "the resolved values are not all written yet ("+err.Error()+"); "+
"the next resolution writes them")
}
change := Change{Action: call.Action, Level: &level, Key: call.Key, Value: answer.Value, Was: answer.Was, By: by,
Via: call.Caller, Why: strings.TrimSpace(call.Why), At: now, Moved: moved}
cause := "the config verb, called by " + call.Caller
if call.approval != nil {
change.Approved = call.approval.Says
cause = "the operator's Approve of proposal " + call.approval.Proposal + ", proposed by " + call.Caller
}
answer.Unsaid = append(answer.Unsaid, c.record(vk, change)...)
if err := c.Store.HandAct(handActWhat(change), change.Why, cause); err != nil {
answer.Unsaid = append(answer.Unsaid, "the hand-act log does not hold it: "+err.Error())
}
answer.Result = nodeResults(moved)
answer.Said = said(change)
return answer, nil
}
// declared is the rule a value at a level is held to, and whether it is trusted; refused, naming the declared
// keys, for a key the module or the list of predefined keys does not declare (ADR 0348 §10).
func (c *Config) declared(s Sources, level Level, key string) (rule, bool, error) {
if level.Kind == LevelModule {
decl, ok := s.Declared[level.Module]
if !ok {
return rule{}, false, fmt.Errorf("%s is not a registered module, so it declares no config", level.Module)
}
d, ok := decl.Config[key]
if !ok {
return rule{}, false, fmt.Errorf("%s declares no value %q; it declares: %s", level.Module, key,
joinKeys(decl.Config))
}
if !assigned(s, level.Node, level.Module) {
return rule{}, false, fmt.Errorf("%s is not assigned to %s, and a module's own value is set for a node it "+
"is assigned to (novox/hq ADR 0348 §3); it is assigned to: %s", level.Module, level.Node,
strings.Join(nodesOf(s, level.Module), ", "))
}
trusted := d.Trusted
if p, ok := s.Predefined[d.Predefined]; ok && p.Trusted {
trusted = true
}
return d.rule(), trusted, nil
}
if len(s.Predefined) == 0 {
return rule{}, false, fmt.Errorf("%s holds values only for the keys of the list of predefined keys, and no "+
"list is published yet (novox/hq ADR 0348 §7)", level)
}
p, ok := s.Predefined[key]
if !ok {
return rule{}, false, fmt.Errorf("%s holds values only for the predefined keys, and %q is not one; the "+
"predefined keys are: %s (novox/hq ADR 0348 §7)", level, key, joinKeys(s.Predefined))
}
return p.rule(), p.Trusted, nil
}
func assigned(s Sources, node, module string) bool {
for _, a := range s.Assignments {
if a.Node == node && a.Module == module {
return true
}
}
return false
}
func nodesOf(s Sources, module string) []string {
var out []string
for _, a := range s.Assignments {
if a.Module == module {
out = append(out, a.Node)
}
}
sort.Strings(out)
if len(out) == 0 {
return []string{"no node"}
}
return out
}
// record keeps a change in the history and raises its event; what could not be done is answered, not dropped.
func (c *Config) record(key string, change Change) []string {
var unsaid []string
if err := c.Store.Append(HistoryLog, key, change); err != nil {
unsaid = append(unsaid, "the history does not hold it: "+err.Error())
}
if err := c.Store.Emit(ConfigChanged, change); err != nil {
unsaid = append(unsaid, "config-changed was not raised: "+err.Error())
}
for _, u := range unsaid {
c.logf("[mesh-config] %s %s: %s", change.Action, key, u)
}
return unsaid
}
func handActWhat(ch Change) string {
what := fmt.Sprintf("config %s %s at %s", ch.Action, ch.Key, ch.Level)
if ch.Action == "set" {
what += " to " + string(ch.Value)
}
return what + ", by " + ch.By
}
func said(ch Change) string {
var b strings.Builder
if ch.Action == "set" {
fmt.Fprintf(&b, "%s is %s at %s", ch.Key, ch.Value, ch.Level)
} else {
fmt.Fprintf(&b, "%s is cleared at %s", ch.Key, ch.Level)
}
switch len(ch.Moved) {
case 0:
b.WriteString("; no module's value moved")
case 1:
m := ch.Moved[0]
fmt.Fprintf(&b, "; %s on %s takes %s (from %s)", m.Module, m.Node, orUnset(m.To), m.Source)
default:
fmt.Fprintf(&b, "; %d values moved on %d node(s)", len(ch.Moved), len(nodeResults(ch.Moved)))
}
return b.String()
}
func orUnset(raw json.RawMessage) string {
if len(raw) == 0 {
return "no value (unset)"
}
return string(raw)
}
func nodeResults(moved []Moved) []NodeResult {
byNode := map[string]map[string]bool{}
for _, m := range moved {
if byNode[m.Node] == nil {
byNode[m.Node] = map[string]bool{}
}
byNode[m.Node][m.Module] = true
}
out := []NodeResult{}
for _, node := range sortedKeys(byNode) {
out = append(out, NodeResult{Node: node, Modules: sortedKeys(byNode[node]),
Result: "resolved and written to mesh-config_resolved; the module takes it when its files are filled again"})
}
return out
}
func (c *Config) now() time.Time {
if c.Now == nil {
return time.Now().UTC()
}
return c.Now().UTC()
}
func (c *Config) logf(format string, a ...any) {
if c.Logf != nil {
c.Logf(format, a...)
}
}
@@ -0,0 +1,553 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"sync"
"testing"
"time"
)
// memStore is the bus as mesh-config reaches it, in memory: its states and the mirrors it reads, its history log,
// the events it raised and the hand-acts it recorded.
type memStore struct {
mu sync.Mutex
states map[string]map[string][]byte
logs map[string][]LogEntry
events []Change
handActs []string
// failHandAct makes the hand-act log refuse.
failHandAct bool
}
func newStore() *memStore {
return &memStore{states: map[string]map[string][]byte{}, logs: map[string][]LogEntry{}}
}
func (m *memStore) Get(state, key string) (*Entry, error) {
m.mu.Lock()
defer m.mu.Unlock()
v, ok := m.states[state][key]
if !ok {
return nil, nil
}
return &Entry{Key: key, Value: append([]byte(nil), v...)}, nil
}
func (m *memStore) Put(state, key string, value any) error {
raw, err := json.Marshal(value)
if err != nil {
return err
}
m.mu.Lock()
defer m.mu.Unlock()
if m.states[state] == nil {
m.states[state] = map[string][]byte{}
}
m.states[state][key] = raw
return nil
}
func (m *memStore) Delete(state, key string) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.states[state], key)
return nil
}
func (m *memStore) Keys(state string) ([]string, error) {
m.mu.Lock()
defer m.mu.Unlock()
var keys []string
for k := range m.states[state] {
keys = append(keys, k)
}
sort.Strings(keys)
return keys, nil
}
func (m *memStore) Append(log, key string, value any) error {
raw, err := json.Marshal(value)
if err != nil {
return err
}
m.mu.Lock()
defer m.mu.Unlock()
m.logs[log] = append(m.logs[log], LogEntry{Key: key, Value: raw, At: time.Now()})
return nil
}
func (m *memStore) Read(log string) ([]LogEntry, error) {
m.mu.Lock()
defer m.mu.Unlock()
return append([]LogEntry(nil), m.logs[log]...), nil
}
func (m *memStore) Emit(event string, body any) error {
if event != ConfigChanged {
return fmt.Errorf("mesh-config emits no %s", event)
}
m.mu.Lock()
defer m.mu.Unlock()
m.events = append(m.events, body.(Change))
return nil
}
func (m *memStore) HandAct(what, why, cause string) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.failHandAct {
return errors.New("the controller did not answer")
}
m.handActs = append(m.handActs, what+" | "+why+" | "+cause)
return nil
}
// mirror writes what the controller mirrors, in the shapes of its mirrors.
func (m *memStore) mirror(t *testing.T, read, key, raw string) {
t.Helper()
if !json.Valid([]byte(raw)) {
t.Fatalf("not JSON: %s", raw)
}
m.mu.Lock()
defer m.mu.Unlock()
if m.states[read] == nil {
m.states[read] = map[string][]byte{}
}
m.states[read][key] = []byte(raw)
}
func (m *memStore) assign(t *testing.T, node, module string) {
m.mirror(t, AssignmentsRead, node+"."+module,
fmt.Sprintf(`{"node":%q,"module":%q,"assigned":"2026-10-14T00:00:00Z"}`, node, module))
}
func (m *memStore) resolved(t *testing.T, node, module, key string) Resolved {
t.Helper()
e, _ := m.Get(ResolvedState, ResolvedKey(node, module, key))
if e == nil {
t.Fatalf("%s holds no resolved value for %s on %s", key, module, node)
}
var r Resolved
if err := json.Unmarshal(e.Value, &r); err != nil {
t.Fatal(err)
}
return r
}
// dunstDeclared is dunst's declarations as the declared-config mirror on the feature branch writes them: kind, why
// and default always written, so a config-form key without a default comes as kind "", why "" and default null.
const dunstDeclared = `{"module":"dunst","settings":{
"font-size":{"kind":"","default":10,"why":"","type":"number","description":"text size, in points","minimum":6,"maximum":72,"predefined":"font-size"},
"width":{"kind":"","default":300,"why":"","type":"number","description":"the width of a notification, in pixels","minimum":100},
"position":{"kind":"","default":"top-right","why":"","type":"string","description":"where notifications show","enum":["top-right","top-left"]},
"origin":{"kind":"","default":null,"why":"","type":"string","description":"the operator's own origin","required":true,"pattern":"[a-z]+"}}}`
// theList is the controller's predefined mirror, as it writes it.
const theList = `{"keys":{"agent-managed-settings":{"type":"object","description":"managed settings","trusted":true},
"font-size":{"type":"number","description":"text size, in points","minimum":6,"maximum":72}},
"repository":"mesh-catalog","commit":"aaaaaaaaaaaa","generation":5,"build":"b5"}`
func aMesh(t *testing.T) (*Config, *memStore) {
t.Helper()
m := newStore()
m.mirror(t, DeclaredConfigRead, "dunst", dunstDeclared)
m.mirror(t, DeclaredConfigRead, "claude-code", `{"module":"claude-code","settings":{
"managed":{"kind":"","default":{},"why":"","type":"object","description":"managed settings","trusted":true,"predefined":"agent-managed-settings"}}}`)
m.mirror(t, DeclaredConfigRead, "old", `{"module":"old","settings":{"workers":{"kind":"preference","default":4,"why":"two per core"}}}`)
m.mirror(t, PredefinedRead, PredefinedMirrorKey, theList)
for _, node := range []string{"g14", "novox"} {
m.assign(t, node, "dunst")
}
m.assign(t, "g14", "claude-code")
m.assign(t, "g14", "old")
clock := time.Date(2026, 10, 14, 19, 0, 0, 0, time.UTC)
c := &Config{Store: m, Now: func() time.Time { clock = clock.Add(time.Second); return clock }, Logf: t.Logf}
if _, err := c.Reconcile(); err != nil {
t.Fatal(err)
}
m.events = nil // the first resolution's
return c, m
}
func set(t *testing.T, c *Config, call Call) *SetAnswer {
t.Helper()
if call.Action == "" {
call.Action = "set"
}
if call.Caller == "" {
call.Caller = "g14.node-tools"
}
if call.Why == "" {
call.Why = "the operator asked for it"
}
a, err := c.Change(call)
if err != nil {
t.Fatalf("%s %s: %v", call.Action, call.Key, err)
}
return a
}
func expect(t *testing.T, m *memStore, node string, value, source string) {
t.Helper()
r := m.resolved(t, node, "dunst", "font-size")
if string(r.Value) != value || r.Source != source {
t.Errorf("dunst's font-size on %s is %s from %s, want %s from %s", node, r.Value, r.Source, value, source)
}
}
// **The four sources, each in turn** (ADR 0348 §2, How it is checked): the manifest's default; mesh config for the
// predefined key it falls back to; node config for that key on the node; the module's own value on that node. Each
// cleared falls back to the one below.
func TestEachOfTheFourSourcesResolvesInTurnAndEachClearedFallsBack(t *testing.T) {
c, m := aMesh(t)
expect(t, m, "g14", "10", SourceDefault)
expect(t, m, "novox", "10", SourceDefault)
a := set(t, c, Call{Key: "font-size", Value: 12.0})
expect(t, m, "g14", "12", SourceMesh)
expect(t, m, "novox", "12", SourceMesh)
if len(a.Moved) != 2 || a.Level.Kind != LevelMesh {
t.Errorf("a mesh value moved %+v", a.Moved)
}
a = set(t, c, Call{Key: "font-size", Value: 14.0, Node: "g14"})
expect(t, m, "g14", "14", SourceNode)
expect(t, m, "novox", "12", SourceMesh)
if len(a.Moved) != 1 || a.Moved[0].Node != "g14" || string(a.Moved[0].From) != "12" || string(a.Moved[0].To) != "14" {
t.Errorf("g14's node value moved %+v", a.Moved)
}
a = set(t, c, Call{Key: "font-size", Value: 16.0, Node: "g14", Module: "dunst"})
expect(t, m, "g14", "16", SourceModule)
expect(t, m, "novox", "12", SourceMesh)
if len(a.Moved) != 1 || a.Moved[0].Source != SourceModule {
t.Errorf("dunst's own value moved %+v", a.Moved)
}
// A value set below the module's own moves nothing it overrides.
if a = set(t, c, Call{Key: "font-size", Value: 18.0, Node: "g14"}); len(a.Moved) != 0 {
t.Errorf("a node value under a module's own moved %+v", a.Moved)
}
set(t, c, Call{Action: "clear", Key: "font-size", Node: "g14", Module: "dunst"})
expect(t, m, "g14", "18", SourceNode)
set(t, c, Call{Action: "clear", Key: "font-size", Node: "g14"})
expect(t, m, "g14", "12", SourceMesh)
set(t, c, Call{Action: "clear", Key: "font-size"})
expect(t, m, "g14", "10", SourceDefault)
expect(t, m, "novox", "10", SourceDefault)
}
// **A value naming no predefined key ignores mesh and node config** (ADR 0348 §2): it resolves from its default and
// the module's own value only.
func TestAValueNamingNoPredefinedKeyResolvesFromItsDefaultAndItsOwnValueOnly(t *testing.T) {
c, m := aMesh(t)
set(t, c, Call{Key: "font-size", Value: 12.0})
set(t, c, Call{Key: "font-size", Value: 14.0, Node: "g14"})
if r := m.resolved(t, "g14", "dunst", "width"); string(r.Value) != "300" || r.Source != SourceDefault {
t.Errorf("width is %s from %s, want its default", r.Value, r.Source)
}
set(t, c, Call{Key: "width", Value: 400.0, Node: "g14", Module: "dunst"})
if r := m.resolved(t, "g14", "dunst", "width"); string(r.Value) != "400" || r.Source != SourceModule {
t.Errorf("width is %s from %s, want dunst's own value", r.Value, r.Source)
}
// A module not yet moved, in the settings form: its default and its own value.
if r := m.resolved(t, "g14", "old", "workers"); string(r.Value) != "4" || r.Source != SourceDefault {
t.Errorf("old's workers is %s from %s", r.Value, r.Source)
}
set(t, c, Call{Key: "workers", Value: 8.0, Node: "g14", Module: "old"})
if r := m.resolved(t, "g14", "old", "workers"); string(r.Value) != "8" || r.Source != SourceModule {
t.Errorf("old's workers is %s from %s", r.Value, r.Source)
}
}
// **A required value nothing sets is answered unset** (ADR 0348 §4), and once set, it is the value.
func TestARequiredValueNothingSetsIsAnsweredUnset(t *testing.T) {
c, m := aMesh(t)
r := m.resolved(t, "g14", "dunst", "origin")
if r.Source != SourceUnset || !r.Required || len(r.Value) != 0 {
t.Errorf("an unset required value resolves as %+v", r)
}
set(t, c, Call{Key: "origin", Value: "home", Node: "g14", Module: "dunst"})
if r = m.resolved(t, "g14", "dunst", "origin"); r.Source != SourceModule || string(r.Value) != `"home"` {
t.Errorf("a set required value resolves as %+v", r)
}
if r = m.resolved(t, "novox", "dunst", "origin"); r.Source != SourceUnset {
t.Errorf("another node's required value resolves as %+v", r)
}
}
// **The verb refuses** (ADR 0348 §3, §8, §10, How it is checked): a key not declared, naming the declared keys; a
// module value without a node; a value failing validation; a trusted value without the terminal; a change without
// why or a proven caller.
func TestTheVerbRefusesWhatADeclarationDoesNotAllow(t *testing.T) {
c, m := aMesh(t)
for _, tc := range []struct {
name string
call Call
want string
}{
{"a key the module does not declare", Call{Key: "colour", Value: "red", Node: "g14", Module: "dunst"},
"it declares: font-size, origin, position, width"},
{"a key not on the predefined list, at the mesh", Call{Key: "width", Value: 400.0},
"the predefined keys are: agent-managed-settings, font-size"},
{"a key not on the predefined list, at a node", Call{Key: "width", Value: 400.0, Node: "g14"}, "is not one"},
{"a module value without a node", Call{Key: "width", Value: 400.0, Module: "dunst"},
"no module value for the whole mesh"},
{"a module on a node it is not assigned to", Call{Key: "width", Value: 400.0, Node: "ace", Module: "dunst"},
"it is assigned to: g14, novox"},
{"a module not registered", Call{Key: "width", Value: 400.0, Node: "g14", Module: "nobody"}, "not a registered module"},
{"a value of another type", Call{Key: "font-size", Value: "12"}, `"12" is not a number`},
{"a value below its minimum", Call{Key: "font-size", Value: 2.0, Node: "g14"}, "below the minimum 6"},
{"a value above its maximum", Call{Key: "font-size", Value: 99.0, Node: "g14", Module: "dunst"}, "above the maximum 72"},
{"a value not in its enum", Call{Key: "position", Value: "middle", Node: "g14", Module: "dunst"}, "is not one of"},
{"a value not matching its pattern", Call{Key: "origin", Value: "Home 1", Node: "g14", Module: "dunst"},
"does not match the pattern"},
{"a trusted value at the mesh, not from the terminal", Call{Key: "agent-managed-settings",
Value: map[string]any{"a": 1.0}}, "set only at the controller's terminal"},
{"a trusted module value, not from the terminal", Call{Key: "managed", Value: map[string]any{},
Node: "g14", Module: "claude-code"}, "set only at the controller's terminal"},
{"a change without why", Call{Key: "font-size", Value: 12.0, Why: " "}, "say why"},
{"an unproven caller", Call{Key: "font-size", Value: 12.0, Caller: Unproven}, "no proven caller"},
{"by from anyone but the controller", Call{Key: "font-size", Value: 12.0, By: "jochen"}, "only the controller says"},
{"a clear of nothing set", Call{Action: "clear", Key: "font-size", Node: "g14"}, "nothing is set"},
} {
t.Run(tc.name, func(t *testing.T) {
call := tc.call
if call.Action == "" {
call.Action = "set"
}
if call.Caller == "" {
call.Caller = "g14.node-tools"
}
if call.Why == "" {
call.Why = "a test"
}
_, err := c.Change(call)
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("refused with %v, want %q", err, tc.want)
}
})
}
if keys, _ := m.Keys(ValuesState); len(keys) != 0 {
t.Errorf("a refused call wrote %v", keys)
}
if len(m.events) != 0 || len(m.handActs) != 0 {
t.Errorf("a refused call raised %d events and recorded %d hand-acts", len(m.events), len(m.handActs))
}
}
// **A trusted value from the controller's terminal is set**, with who gave it there (ADR 0348 §8).
func TestATrustedValueFromTheTerminalIsSetWithWhoGaveIt(t *testing.T) {
c, m := aMesh(t)
a := set(t, c, Call{Key: "agent-managed-settings", Value: map[string]any{"b": 2.0, "a": 1.0},
Caller: ControllerCaller, By: "jochen"})
if string(a.Value) != `{"a":1,"b":2}` {
t.Errorf("the value is written as %s", a.Value)
}
r := m.resolved(t, "g14", "claude-code", "managed")
if r.Source != SourceMesh || !r.Trusted || string(r.Value) != `{"a":1,"b":2}` {
t.Errorf("claude-code's managed settings resolve as %+v", r)
}
e, _ := m.Get(ValuesState, "mesh.agent-managed-settings")
var v Value
_ = json.Unmarshal(e.Value, &v)
if v.By != "jochen" || v.Via != ControllerCaller {
t.Errorf("the value says it was set by %s via %s", v.By, v.Via)
}
}
// **Every set and clear is in the history and the hand-act log with the proven caller and why, and raises
// config-changed naming what moved** (ADR 0348 §9, §10).
func TestEveryChangeIsKeptWithWhoAndWhyAndRaisesConfigChanged(t *testing.T) {
c, m := aMesh(t)
m.events = nil
set(t, c, Call{Key: "font-size", Value: 12.0, Caller: "novox.node-tools", Why: "bigger on every desk"})
set(t, c, Call{Action: "clear", Key: "font-size", Caller: "g14.node-tools", Why: "back to the default"})
history, err := c.History(Call{})
if err != nil {
t.Fatal(err)
}
var changes []HistoryEntry
for _, h := range history {
if h.Action == "set" || h.Action == "clear" {
changes = append(changes, h)
}
}
if len(changes) != 2 || changes[0].By != "novox.node-tools" || changes[0].Why != "bigger on every desk" ||
changes[1].Action != "clear" || changes[1].By != "g14.node-tools" || string(changes[1].Was) != "12" {
t.Fatalf("the history holds %+v", changes)
}
if len(m.handActs) != 2 || !strings.Contains(m.handActs[0], "config set font-size at mesh config to 12, by novox.node-tools | bigger on every desk") {
t.Errorf("the hand-act log holds %q", m.handActs)
}
if len(m.events) != 2 || len(m.events[0].Moved) != 2 || m.events[0].Key != "font-size" || m.events[1].Action != "clear" {
t.Errorf("config-changed was raised as %+v", m.events)
}
// Narrowed: g14's dunst sees both, through what they moved; a node that holds no dunst sees neither.
if h, _ := c.History(Call{Module: "dunst", Node: "g14"}); len(h) < 2 {
t.Errorf("dunst on g14's history holds %d changes", len(h))
}
if h, _ := c.History(Call{Node: "ace"}); len(h) != 0 {
t.Errorf("ace's history holds %+v", h)
}
}
// **A hand-act the log refuses is said in the answer, never dropped**, and the value is set.
func TestAChangeTheHandActLogRefusesIsSaid(t *testing.T) {
c, m := aMesh(t)
m.failHandAct = true
a := set(t, c, Call{Key: "font-size", Value: 12.0})
if len(a.Unsaid) != 1 || !strings.Contains(a.Unsaid[0], "hand-act log does not hold it") {
t.Errorf("the answer says %q", a.Unsaid)
}
expect(t, m, "g14", "12", SourceMesh)
}
// **The newest list of predefined keys is published, and an older one heard later is not** (ADR 0348 §7).
func TestTheNewestListOfPredefinedKeysIsPublished(t *testing.T) {
c, m := aMesh(t)
published := func() map[string]Published {
out := map[string]Published{}
keys, _ := m.Keys(PredefinedState)
for _, k := range keys {
e, _ := m.Get(PredefinedState, k)
var p Published
_ = json.Unmarshal(e.Value, &p)
out[k] = p
}
return out
}
if p := published(); len(p) != 2 || p["font-size"].Generation != 5 || !p["agent-managed-settings"].Trusted {
t.Fatalf("published %+v", p)
}
m.mirror(t, PredefinedRead, PredefinedMirrorKey, `{"keys":{"font-size":{"type":"number","description":"x"},
"theme":{"type":"string","description":"the theme"}},"commit":"bbbb","generation":7}`)
if _, err := c.Reconcile(); err != nil {
t.Fatal(err)
}
if p := published(); len(p) != 2 || p["theme"].Generation != 7 || p["agent-managed-settings"].Type != "" {
t.Fatalf("a newer list published %+v", p)
}
m.mirror(t, PredefinedRead, PredefinedMirrorKey, theList) // generation 5, heard later
if _, err := c.Reconcile(); err != nil {
t.Fatal(err)
}
if p := published(); p["theme"].Generation != 7 {
t.Fatalf("an older list replaced the newer one: %+v", p)
}
}
// **A list dropping a key that still holds a value is withheld** (ADR 0348 §7): the list published stands, and the
// listing says why until the value is cleared.
func TestAListDroppingAKeyThatHoldsAValueIsWithheldUntilItIsCleared(t *testing.T) {
c, m := aMesh(t)
set(t, c, Call{Key: "font-size", Value: 12.0, Node: "novox"})
m.mirror(t, PredefinedRead, PredefinedMirrorKey, `{"keys":{"agent-managed-settings":{"type":"object",
"description":"managed settings","trusted":true}},"commit":"cccccccccc","generation":9}`)
if _, err := c.Reconcile(); err != nil {
t.Fatal(err)
}
listed, err := c.List(Call{})
if err != nil {
t.Fatal(err)
}
if _, kept := listed.Predefined["font-size"]; !kept ||
!strings.Contains(listed.Withheld, "drops font-size (still set as node config on novox)") {
t.Fatalf("the list was published over a value it drops: withheld %q, published %v", listed.Withheld,
joinKeys(listed.Predefined))
}
set(t, c, Call{Action: "clear", Key: "font-size", Node: "novox"})
if _, err := c.Reconcile(); err != nil {
t.Fatal(err)
}
listed, _ = c.List(Call{})
if _, kept := listed.Predefined["font-size"]; kept || listed.Withheld != "" {
t.Errorf("once cleared, the list is still withheld: %q, %v", listed.Withheld, joinKeys(listed.Predefined))
}
}
// **An assignment, a declaration or its removal moves the resolved values**, written only where they moved, and
// said as config-changed; a pass over unchanged sources writes nothing.
func TestAnAssignmentMovesTheResolvedValuesAndAQuietPassWritesNothing(t *testing.T) {
c, m := aMesh(t)
r, err := c.Reconcile()
if err != nil || len(r.Moved) != 0 {
t.Fatalf("a pass over unchanged sources moved %+v (%v)", r.Moved, err)
}
m.events = nil
m.assign(t, "ace", "dunst")
if r, _ = c.Reconcile(); len(r.Moved) != 4 {
t.Errorf("an assignment moved %+v", r.Moved)
}
expect(t, m, "ace", "10", SourceDefault)
if len(m.events) != 1 || m.events[0].Action != "resolve" {
t.Errorf("an assignment raised %+v", m.events)
}
_ = m.Delete(AssignmentsRead, "ace.dunst")
if r, _ = c.Reconcile(); len(r.Moved) != 4 || !r.Moved[0].Gone {
t.Errorf("an unassignment moved %+v", r.Moved)
}
if e, _ := m.Get(ResolvedState, ResolvedKey("ace", "dunst", "font-size")); e != nil {
t.Error("an unassigned module keeps its resolved values")
}
}
// **The declared-config mirror's two shapes read the same** (the release lead's note on issue 652): kind, why and a
// null default written for a config-form key are not given, and a mirror that leaves them out reads alike.
func TestTheDeclaredConfigMirrorsTwoShapesReadTheSame(t *testing.T) {
with, err := ParseModuleDeclarations([]byte(`{"module":"x","settings":{"k":{"kind":"","default":null,"why":"","type":"string","description":"d","required":true}}}`))
if err != nil {
t.Fatal(err)
}
without, err := ParseModuleDeclarations([]byte(`{"module":"x","settings":{"k":{"type":"string","description":"d","required":true}}}`))
if err != nil {
t.Fatal(err)
}
a, _ := json.Marshal(with)
b, _ := json.Marshal(without)
if string(a) != string(b) || with.Config["k"].HasDefault() {
t.Errorf("the two shapes read as %s and %s", a, b)
}
if r := resolveOne(Sources{}, "n", "x", "k", with.Config["k"]); r.Source != SourceUnset {
t.Errorf("a null default resolved as %+v", r)
}
}
// **A listing narrows by module and node**, and says the mesh and node values its modules fall back to.
func TestAListingNarrowsByModuleAndNode(t *testing.T) {
c, _ := aMesh(t)
set(t, c, Call{Key: "font-size", Value: 12.0})
set(t, c, Call{Key: "font-size", Value: 14.0, Node: "novox"})
l, err := c.List(Call{Module: "dunst", Node: "g14"})
if err != nil {
t.Fatal(err)
}
if len(l.Resolved) != 4 || len(l.Set) != 1 || l.Set[0].Level.Kind != LevelMesh || l.Predefined != nil {
t.Errorf("dunst on g14 lists %d resolved and %+v", len(l.Resolved), l.Set)
}
l, _ = c.List(Call{Node: "novox"})
if len(l.Resolved) != 4 || len(l.Set) != 2 {
t.Errorf("novox lists %d resolved and %+v", len(l.Resolved), l.Set)
}
}
// **The verb's arguments are read strictly**: one it does not know is refused, naming what it takes.
func TestTheVerbsArgumentsAreReadStrictly(t *testing.T) {
if _, err := callOf(map[string]any{"action": "set", "level": "mesh"}); err == nil ||
!strings.Contains(err.Error(), "config takes no level") {
t.Errorf("an unknown argument: %v", err)
}
if _, err := callOf(map[string]any{"key": 3.0}); err == nil {
t.Error("a key that is not a string was read")
}
call, err := callOf(map[string]any{"action": "set", "key": "font-size", "value": 12.0, "why": " w "})
if err != nil || call.Value != 12.0 || call.Why != "w" {
t.Errorf("read %+v (%v)", call, err)
}
}
@@ -0,0 +1,232 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"math"
"regexp"
"slices"
"sort"
"strings"
)
// What mesh-config resolves from (novox/hq ADR 0348 §2, §4, §7): each module's declarations, the list of predefined
// keys, and the assignments, as the controller mirrors them on the bus (design 57 F3, K3).
// The types a config value may have (ADR 0348 §4).
const (
TypeNumber = "number"
TypeString = "string"
TypeBoolean = "boolean"
TypeObject = "object"
TypeList = "list"
)
// Declaration is one value a module declares, as the declared-config mirror holds it: the config form's fields,
// or the settings form's kind, default and why for a module not yet moved (ADR 0348 §1). The mirror writes kind,
// default and why for every key, so a config-form key comes as kind "", why "" and default null: an empty kind or
// why and a null default are read as not given, never as an empty value or a null default.
type Declaration struct {
Kind string `json:"kind,omitempty"`
Default json.RawMessage `json:"default,omitempty"`
Why string `json:"why,omitempty"`
Type string `json:"type,omitempty"`
Description string `json:"description,omitempty"`
Enum []any `json:"enum,omitempty"`
Minimum *float64 `json:"minimum,omitempty"`
Maximum *float64 `json:"maximum,omitempty"`
Pattern string `json:"pattern,omitempty"`
Trusted bool `json:"trusted,omitempty"`
Predefined string `json:"predefined,omitempty"`
Required bool `json:"required,omitempty"`
Runtime bool `json:"runtime,omitempty"`
}
// HasDefault is whether the declaration gives a default: present and not null.
func (d Declaration) HasDefault() bool {
return len(bytes.TrimSpace(d.Default)) > 0 && string(bytes.TrimSpace(d.Default)) != "null"
}
// TypeOf is the value's type: the config form's, or for the settings form the type of its default.
func (d Declaration) TypeOf() string {
if d.Type != "" {
return d.Type
}
if !d.HasDefault() {
return ""
}
var v any
if json.Unmarshal(d.Default, &v) != nil {
return ""
}
return typeOfValue(v)
}
// ModuleDeclarations is one module's value of the declared-config mirror.
type ModuleDeclarations struct {
Module string `json:"module"`
Config map[string]Declaration `json:"settings"`
}
// ParseModuleDeclarations reads one key of the declared-config mirror.
func ParseModuleDeclarations(raw []byte) (ModuleDeclarations, error) {
var m ModuleDeclarations
if err := json.Unmarshal(raw, &m); err != nil {
return m, fmt.Errorf("not a module's declarations: %w", err)
}
if m.Module == "" {
return m, fmt.Errorf("a module's declarations that name no module")
}
if m.Config == nil {
m.Config = map[string]Declaration{}
}
for key, d := range m.Config {
if !d.HasDefault() {
d.Default = nil
}
m.Config[key] = d
}
return m, nil
}
// PredefinedKey is one key of the list of predefined keys (ADR 0348 §7): its type, description, validation and
// whether it is trusted. It has no default.
type PredefinedKey struct {
Type string `json:"type"`
Description string `json:"description"`
Enum []any `json:"enum,omitempty"`
Minimum *float64 `json:"minimum,omitempty"`
Maximum *float64 `json:"maximum,omitempty"`
Pattern string `json:"pattern,omitempty"`
Trusted bool `json:"trusted,omitempty"`
}
// PredefinedList is the list the controller kept, as its predefined mirror holds it under PredefinedMirrorKey.
type PredefinedList struct {
Keys map[string]PredefinedKey `json:"keys"`
Repository string `json:"repository"`
Commit string `json:"commit"`
Generation int `json:"generation"`
Build string `json:"build"`
}
// PredefinedMirrorKey is the controller's predefined mirror's one key.
const PredefinedMirrorKey = "list"
// Published is one key of mesh-config_predefined: the key's declaration, and the build of the list it came from.
type Published struct {
PredefinedKey
Generation int `json:"generation"`
Commit string `json:"commit"`
}
// Assignment is one key of the assignments mirror: a module on a node.
type Assignment struct {
Node string `json:"node"`
Module string `json:"module"`
}
// rule is the validation a value is held to, from a module's declaration or a predefined key.
type rule struct {
Type string
Enum []any
Minimum *float64
Maximum *float64
Pattern string
}
func (d Declaration) rule() rule {
return rule{Type: d.TypeOf(), Enum: d.Enum, Minimum: d.Minimum, Maximum: d.Maximum, Pattern: d.Pattern}
}
func (k PredefinedKey) rule() rule {
return rule{Type: k.Type, Enum: k.Enum, Minimum: k.Minimum, Maximum: k.Maximum, Pattern: k.Pattern}
}
// check is every way value breaks the rule, said in a sentence each; none for a value that holds.
func (r rule) check(value any) []string {
var problems []string
if r.Type != "" && typeOfValue(value) != r.Type {
return []string{fmt.Sprintf("%s is not a %s", show(value), r.Type)}
}
if len(r.Enum) > 0 && !slices.ContainsFunc(r.Enum, func(e any) bool { return sameValue(e, value) }) {
problems = append(problems, fmt.Sprintf("%s is not one of %s", show(value), show(r.Enum)))
}
if n, ok := value.(float64); ok {
if r.Minimum != nil && n < *r.Minimum {
problems = append(problems, fmt.Sprintf("%s is below the minimum %s", show(n), show(*r.Minimum)))
}
if r.Maximum != nil && n > *r.Maximum {
problems = append(problems, fmt.Sprintf("%s is above the maximum %s", show(n), show(*r.Maximum)))
}
if math.IsNaN(n) || math.IsInf(n, 0) {
problems = append(problems, "a number is finite")
}
}
if s, ok := value.(string); ok && r.Pattern != "" {
pattern, err := regexp.Compile(`^(?:` + r.Pattern + `)$`)
switch {
case err != nil:
problems = append(problems, fmt.Sprintf("its pattern %q cannot be read: %v", r.Pattern, err))
case !pattern.MatchString(s):
problems = append(problems, fmt.Sprintf("%q does not match the pattern %q", s, r.Pattern))
}
}
return problems
}
// typeOfValue is the config type of a value read from JSON.
func typeOfValue(v any) string {
switch v.(type) {
case float64:
return TypeNumber
case string:
return TypeString
case bool:
return TypeBoolean
case map[string]any:
return TypeObject
case []any:
return TypeList
}
return ""
}
func sameValue(a, b any) bool {
ra, _ := json.Marshal(a)
rb, _ := json.Marshal(b)
return bytes.Equal(ra, rb)
}
func show(v any) string {
raw, err := json.Marshal(v)
if err != nil {
return fmt.Sprint(v)
}
return string(raw)
}
// keyName is what a config key may be (the catalogue's rule): lower-case letters, digits, dots, dashes and
// underscores, starting with a letter or digit.
var keyName = regexp.MustCompile(`^[a-z0-9][a-z0-9_.-]*$`)
// nodeOrModule is what a node or a module name may be: one token of a bucket's key.
var nodeOrModule = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
func sortedKeys[V any](m map[string]V) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
sort.Strings(out)
return out
}
func joinKeys[V any](m map[string]V) string {
keys := sortedKeys(m)
if len(keys) == 0 {
return "none"
}
return strings.Join(keys, ", ")
}
+144
View File
@@ -0,0 +1,144 @@
package main
import (
"encoding/json"
"fmt"
)
// Listing the values and reading their history, narrowed by module and node (ADR 0348 §1).
// Listed is what a listing answers.
type Listed struct {
// Resolved is each module's value on each node the call narrows to, with where it came from.
Resolved []Resolved `json:"resolved"`
// Set is every value set at a level the call reaches, with who set it and why.
Set []SetValue `json:"set"`
// Predefined is the published list of predefined keys, for a call that names no module.
Predefined map[string]Published `json:"predefined,omitempty"`
// Withheld is why the newest list of predefined keys is not published, when it is not.
Withheld string `json:"withheld,omitempty"`
}
// SetValue is one value set, at its level.
type SetValue struct {
Level Level `json:"level"`
Key string `json:"key"`
Value
}
// List answers the resolved values and the values set, narrowed by module, node and key.
func (c *Config) List(call Call) (*Listed, error) {
for _, name := range []string{call.Node, call.Module} {
if name != "" && !nodeOrModule.MatchString(name) {
return nil, fmt.Errorf("%q is not a node's or a module's name", name)
}
}
c.mu.Lock()
withheld := c.withheld
c.mu.Unlock()
s, err := c.sources()
if err != nil {
return nil, err
}
if call.Module != "" {
if _, ok := s.Declared[call.Module]; !ok {
return nil, fmt.Errorf("%s is not a registered module, so it declares no config", call.Module)
}
}
out := &Listed{Resolved: []Resolved{}, Set: []SetValue{}, Withheld: withheld}
resolved := Resolve(s)
for _, k := range sortedKeys(resolved) {
r := resolved[k]
if (call.Module == "" || r.Module == call.Module) && (call.Node == "" || r.Node == call.Node) &&
(call.Key == "" || r.Key == call.Key || r.Predefined == call.Key) {
out.Resolved = append(out.Resolved, r)
}
}
// The predefined keys the narrowed modules fall back to: their mesh and node values are listed too.
fallsBack := map[string]bool{}
for _, r := range out.Resolved {
if r.Predefined != "" {
fallsBack[r.Predefined] = true
}
}
for _, k := range sortedKeys(s.Values) {
level, key, ok := parseValueKey(k)
if !ok {
continue
}
if call.Key != "" && key != call.Key {
continue
}
reached := false
switch level.Kind {
case LevelMesh:
reached = call.Module == "" || fallsBack[key]
case LevelNode:
reached = (call.Node == "" || level.Node == call.Node) && (call.Module == "" || fallsBack[key])
case LevelModule:
reached = (call.Node == "" || level.Node == call.Node) && (call.Module == "" || level.Module == call.Module)
}
if reached {
out.Set = append(out.Set, SetValue{Level: level, Key: key, Value: s.Values[k]})
}
}
if call.Module == "" {
out.Predefined = s.Predefined
}
return out, nil
}
// HistoryEntry is one change as the history keeps it.
type HistoryEntry struct {
Key string `json:"key"`
Change
}
// History is the changes the call narrows to, newest last, at most its limit (100 when it gives none).
func (c *Config) History(call Call) ([]HistoryEntry, error) {
entries, err := c.Store.Read(HistoryLog)
if err != nil {
return nil, fmt.Errorf("the history cannot be read: %w", err)
}
limit := call.Limit
if limit <= 0 {
limit = 100
}
out := []HistoryEntry{}
for _, e := range entries {
var ch Change
if err := json.Unmarshal(e.Value, &ch); err != nil {
continue
}
if !reaches(call, e.Key, ch) {
continue
}
out = append(out, HistoryEntry{Key: e.Key, Change: ch})
}
if len(out) > limit {
out = out[len(out)-limit:]
}
return out, nil
}
// reaches is whether a change concerns what the call narrows to: its level, or a value it moved.
func reaches(call Call, key string, ch Change) bool {
if call.Module == "" && call.Node == "" && call.Key == "" {
return true
}
if level, k, ok := parseValueKey(key); ok {
if (call.Key == "" || k == call.Key) && (call.Node == "" || level.Node == call.Node) &&
(call.Module == "" || level.Module == call.Module) {
return true
}
}
// A change at another level reaches the call through what it moved there: mesh config moving a module's
// value on the node named.
for _, m := range ch.Moved {
if (call.Module == "" || m.Module == call.Module) && (call.Node == "" || m.Node == call.Node) &&
(call.Key == "" || m.Key == call.Key || ch.Key == call.Key) {
return true
}
}
return false
}
+283
View File
@@ -0,0 +1,283 @@
// mesh-config: the owner of every config value, holder of the mesh-config seat (novox/hq ADR 0348 §9, design 57
// K3). A Go bundle served by a runtime of its own, on its own bus account (`runs-as`, ADR 0259 §8), so that its
// account alone writes its buckets: mesh-config_values (each value set, at the mesh, a node, or a module on a
// node), mesh-config_resolved (each module's value on each node, with its source), mesh-config_predefined (the
// published list of predefined keys) and mesh-config_proposals (each value proposed for the operator's Approve,
// asked under this module's own name, design 57 K4).
// It resolves from the controller's mirrors of the assignments, the modules' declarations and the list it kept
// (design 57 F3, K3). stdout is the MCP channel; what this module says, it says on stderr.
package main
import (
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asks"
)
// Seat is the role this module holds, once for the mesh, and Verb its one verb.
const (
Seat = "mesh-config"
Verb = "config"
)
func logf(format string, a ...any) { fmt.Fprintf(os.Stderr, format+"\n", a...) }
// runtimeStore is the bus, through the runtime that launched this bundle.
type runtimeStore struct{}
func (runtimeStore) Get(state, key string) (*Entry, error) {
e, err := stdio.State(state).Get(key)
if err != nil || e == nil {
return nil, err
}
return &Entry{Key: e.Key, Value: e.Value, Revision: e.Revision}, nil
}
func (runtimeStore) Put(state, key string, value any) error {
_, err := stdio.State(state).Put(key, value)
return err
}
func (runtimeStore) Delete(state, key string) error { return stdio.State(state).Delete(key) }
func (runtimeStore) Keys(state string) ([]string, error) { return stdio.State(state).Keys() }
func (runtimeStore) Append(log, key string, value any) error {
_, err := stdio.Log(log).Append(key, value)
return err
}
func (runtimeStore) Read(log string) ([]LogEntry, error) {
var out []LogEntry
for from := uint64(0); ; {
page, err := stdio.Log(log).Read(from, "", 1000)
if err != nil {
return nil, err
}
for _, e := range page.Entries {
out = append(out, LogEntry{Key: e.Key, Value: e.Value, At: e.At})
}
if page.Next == 0 {
return out, nil
}
from = page.Next
}
}
func (runtimeStore) Emit(event string, body any) error { return stdio.Emit(event, body) }
// HandAct records a change in the controller's hand-act log (to-be 45 §7) through its seat's verb.
func (runtimeStore) HandAct(what, why, cause string) error {
_, err := stdio.Ask("seat:mesh-controller.hand-act", map[string]any{"what": what, "why": why, "cause": cause})
return err
}
// runtimeAsks is the operator channel through the runtime: asks published under this module's own name, the
// router's records of them read under that name alone (its `uses` of the seat, ADR 0259 §3), and the controller's
// hand-act log for a warrant acted on.
type runtimeAsks struct{}
func (runtimeAsks) Publish(subject string, body any, id string) error {
return stdio.SeatPublish(subject, body, id)
}
func (runtimeAsks) Record(id string) (*stdio.StateEntry, error) { return stdio.SeatRecord("", id) }
func (runtimeAsks) Kinds() ([]stdio.KindHeld, error) { return stdio.SeatKinds() }
func (runtimeAsks) Warranted(ask string) error {
_, err := stdio.Ask("seat:mesh-controller.warranted", map[string]any{"asker": Module, "ask": ask})
return err
}
func main() {
c := &Config{Store: runtimeStore{}, Logf: logf, Asks: runtimeAsks{}}
go resolving(c)
go hearing(c)
if err := stdio.Serve("", Tools(c)); err != nil {
logf("%v", err)
os.Exit(1)
}
}
// resolving resolves at start and again whenever a source moves, and every five minutes besides, so a change
// heard nowhere is still resolved. The runtime answers only once Serve runs, so the watches are asked again
// until they are taken.
func resolving(c *Config) {
moved := make(chan struct{}, 1)
poke := func(stdio.StateChange) error {
select {
case moved <- struct{}{}:
default:
}
return nil
}
for _, read := range []string{AssignmentsRead, DeclaredConfigRead, PredefinedRead} {
go func(read string) {
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
time.Sleep(wait)
if err := stdio.State(read).Watch("", poke); err != nil {
logf("[mesh-config] %s is not watched yet (%v); asking again in %s", read, err, wait)
continue
}
return
}
}(read)
}
tick := time.NewTicker(5 * time.Minute)
defer tick.Stop()
time.Sleep(time.Second)
for {
r, err := c.Reconcile()
switch {
case err != nil:
logf("[mesh-config] the values are not all resolved: %v", err)
case len(r.Moved) > 0:
logf("[mesh-config] resolved: %d value(s) moved", len(r.Moved))
}
if r.Published != "" {
logf("[mesh-config] published %s", r.Published)
}
select {
case <-moved:
time.Sleep(500 * time.Millisecond) // a burst of writes is one resolution
select {
case <-moved:
default:
}
case <-tick.C:
}
}
}
// hearing takes the router's word on this module's asks, asked again until the runtime takes the subscription, and
// reads the router's record of every waiting ask once a minute besides, so an answer heard nowhere is still taken.
func hearing(c *Config) {
go func() {
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
time.Sleep(wait)
err := stdio.Subscribe(asks.Seat+".decided."+Module, func(e stdio.Envelope) error {
var w asks.Warrant
if json.Unmarshal(e.Body, &w) == nil {
for _, s := range c.HeardEnd(w) {
logf("[mesh-config] %s", s)
}
}
return nil
})
if err == nil {
logf("[mesh-config] hears the operator's answers")
return
}
logf("[mesh-config] does not hear the operator's answers yet (%v); asking again in %s", err, wait)
}
}()
tick := time.NewTicker(time.Minute)
defer tick.Stop()
for range tick.C {
said, err := c.Decide()
if err != nil {
logf("[mesh-config] reading the operator's answers: %v", err)
}
for _, s := range said {
logf("[mesh-config] %s", s)
}
}
}
// Tools is the seat's verb as the runtime serves it.
func Tools(c *Config) []stdio.Tool {
return []stdio.Tool{{
Name: Seat + "." + Verb,
Description: "Set, clear, list or read the history of config values (novox/hq ADR 0348). A value is set at " +
"one level: mesh config (no module, no node) or node config (a node) for a predefined key, or a " +
"module's own value on one node (module and node). Each set or clear needs why, is validated against " +
"its declaration, names each module on each node whose value moves, and is kept in the history with " +
"who and why. A trusted value is set only at the controller's terminal or on the operator's Approve: " +
"propose asks the operator, under mesh-config's own name on a channel that proves who answered, and sets " +
"the value on Approve if what it was shown against still stands; proposals lists them, or one with " +
"proposal. list and history are narrowed by module, node and key.",
Input: map[string]any{"type": "object", "properties": map[string]any{
"action": map[string]any{"type": "string",
"enum": []string{"list", "set", "clear", "history", "propose", "proposals"},
"description": "list when not given"},
"key": map[string]any{"type": "string", "description": "the config key"},
"value": map[string]any{"description": "for set: the value, of the key's declared type"},
"module": map[string]any{"type": "string", "description": "a module: its own value, with node"},
"node": map[string]any{"type": "string", "description": "a node"},
"why": map[string]any{"type": "string", "description": "for set and clear: why, in a sentence"},
"by": map[string]any{"type": "string",
"description": "who gave the value at the controller's terminal: said by the controller alone"},
"limit": map[string]any{"type": "number", "description": "for history: at most this many, newest"},
"clear": map[string]any{"type": "boolean",
"description": "for propose: propose clearing the value at its level instead of setting one"},
"proposal": map[string]any{"type": "string", "description": "for proposals: one proposal's id, read whole"},
}},
Run: func(args map[string]any) (any, error) {
call, err := callOf(args)
if err != nil {
return nil, err
}
call.Caller = stdio.CallerOf(args)
return c.Do(call)
},
}}
}
// callOf reads a call's arguments strictly: an argument it does not know is refused, not ignored.
func callOf(args map[string]any) (Call, error) {
known := map[string]bool{"action": true, "key": true, "value": true, "module": true, "node": true, "why": true,
"by": true, "limit": true, "clear": true, "proposal": true}
var unknown []string
for k := range args {
if !known[k] {
unknown = append(unknown, k)
}
}
if len(unknown) > 0 {
return Call{}, fmt.Errorf("config takes no %s; it takes action, key, value, module, node, why, by, limit, "+
"clear and proposal",
strings.Join(unknown, ", "))
}
str := func(k string) (string, error) {
v, ok := args[k]
if !ok || v == nil {
return "", nil
}
s, ok := v.(string)
if !ok {
return "", fmt.Errorf("%s is a string", k)
}
return strings.TrimSpace(s), nil
}
var call Call
var problems []error
for k, into := range map[string]*string{"action": &call.Action, "key": &call.Key, "module": &call.Module,
"node": &call.Node, "why": &call.Why, "by": &call.By, "proposal": &call.Proposal} {
s, err := str(k)
problems = append(problems, err)
*into = s
}
if v, ok := args["limit"]; ok && v != nil {
n, ok := v.(float64)
if !ok || n < 0 || n != float64(int(n)) {
problems = append(problems, errors.New("limit is a whole number"))
}
call.Limit = int(n)
}
if v, ok := args["clear"]; ok && v != nil {
b, ok := v.(bool)
if !ok {
problems = append(problems, errors.New("clear is true or false"))
}
call.Clear = b
}
if v, ok := args["value"]; ok {
// The value as given, a JSON value: a number for a number, never its text; the validation judges it.
call.Value = v
}
return call, errors.Join(problems...)
}
@@ -0,0 +1,928 @@
package main
// A config value proposed for the operator's Approve (novox/hq ADR 0348 §8, ADR 0277's rules with mesh-config as
// the asker; design 57 K4).
//
// Whoever the bus admits may PROPOSE a value at one level — mesh config, node config or a module's own value on a
// node — a trusted one among them, which the verb sets only from the controller's terminal. A proposal changes
// nothing. It is judged as a set judges it, then kept in mesh-config_proposals (which this module's account alone
// writes) with the value whole, the value stored at that level when it was proposed (what the operator is shown
// the change against), who proposed it, why and when. It is asked of the operator **under mesh-config's own name**
// on the operator-channel seat — its own grant, from its manifest's `uses`, and its own cap of open asks — as an
// ask whose two answers, Approve and Decline, are both at the level approve, so only a channel that proves who
// answered carries either. The ask says the level, the key and the value, and that, approved, it takes effect at
// once on every node it reaches; each option binds the proposal's level, key, the digest of the exact value and of
// the value it was shown against.
//
// **On the answer** mesh-config reads the router's own record of its ask (which only the router writes, and this
// module reads under its own name alone), composes the ask again from the proposal and holds the warrant to it:
// the same ask digest, an option it offered, at its level, in time (asks.Warrant.For), and the act the option
// bound is this proposal's (asks.Option.Performs). A record whose value or shown value changed after it was asked
// is refused, and so is an Approve when the value stored at the level is no longer the one the operator was shown
// the change against. Then the value is set exactly as a set at the terminal is, with the same judgement and
// history, and who approved it is kept beside the value; the warrant is recorded in the controller's hand-act log.
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"sort"
"strconv"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asks"
"git.novox.be/novox/mesh-sdk/go/outward"
)
// Module is this module's name: the one the bus grants its asks under.
const Module = "mesh-config"
// ProposalVerb is the act a proposal's answers bind.
const ProposalVerb = Seat + "." + Verb
// ProposalLasts is how long the operator has to answer: a day less ten minutes (ADR 0277 §9), under the router's
// bound for an approval.
const ProposalLasts = asks.ApproveLasts - 10*time.Minute
// How long a proposal waits for the router's word on its new ask, and how often it looks (ADR 0277 §20).
const (
RouterAnswersWithin = 8 * time.Second
RouterAnswersEvery = 250 * time.Millisecond
)
// The two answers, both at the level approve.
const (
OptionApprove = "approve"
OptionDecline = "decline"
)
// Where a proposal stands.
const (
ProposalWaiting = "waiting"
ProposalApproved = "approved"
ProposalDeclined = "declined"
ProposalEnded = "ended"
ProposalRefused = "refused"
ProposalUnasked = "not asked"
)
// AskBus is the operator channel as this module reaches it: publish an ask under its own name, read the router's
// record of one of its asks, the kinds of the channel bench with what each promises, and the controller's
// hand-act log for a warrant acted on.
type AskBus interface {
Publish(subject string, body any, id string) error
Record(id string) (*stdio.StateEntry, error)
Kinds() ([]stdio.KindHeld, error)
Warranted(ask string) error
}
// Proposal is one value proposed, as mesh-config_proposals keeps it. Every field the ask is composed from is here,
// so the ask is composed again from it alone to hold the warrant to.
type Proposal struct {
ID string `json:"id"`
Action string `json:"action"`
Level Level `json:"level"`
Key string `json:"key"`
// Value is the value proposed, whole, and Digest its digest (valueDigest); a clear carries none.
Value json.RawMessage `json:"value,omitempty"`
Digest string `json:"digest"`
// Was is the value stored at the level when it was proposed, HadValue whether there was one, and WasDigest its
// digest: what the operator is shown the change against, and what must still stand on Approve.
Was json.RawMessage `json:"was,omitempty"`
HadValue bool `json:"had-value"`
WasDigest string `json:"was-digest"`
Trusted bool `json:"trusted,omitempty"`
// Reaches is each module on each node whose value it moves, as it was judged when proposed.
Reaches []string `json:"reaches"`
// From is who proposed it, as the bus proved the caller; Why is theirs; At is when.
From string `json:"from"`
Why string `json:"why"`
At time.Time `json:"at"`
// State is where it stands, Outcome what came of it in words, Decided when; Answered is the warrant a person's
// choice held on (Approve or Decline), and Approved who approved it.
State string `json:"state"`
Outcome string `json:"outcome,omitempty"`
Decided time.Time `json:"decided,omitempty"`
Answered string `json:"answered,omitempty"`
Approved string `json:"approved,omitempty"`
// Recorded says the controller's hand-act log holds the warrant; Record is why not, while it does not.
Recorded bool `json:"recorded,omitempty"`
Record string `json:"record,omitempty"`
}
// approval is what a decided proposal gives the set it makes: the warrant in words, and what the operator was
// shown the change against.
type approval struct {
Proposal string
By string
Says string
Was json.RawMessage
HadValue bool
WasDigest string
}
// valueDigest is the digest a proposal binds of a value: SHA-256 over its one encoding (compact), "none" when
// there is no value.
func valueDigest(raw json.RawMessage, had bool) string {
text := "none"
if had {
text = "value:" + string(compact(raw))
}
sum := sha256.Sum256([]byte(text))
return "sha256:" + hex.EncodeToString(sum[:])
}
func shownOrNone(raw json.RawMessage, had bool) string {
if !had {
return "no value set"
}
return string(compact(raw))
}
// fingerprint is a digest as the operator is shown it: its first 24 hexadecimal digits in groups of four.
func fingerprint(digest string) string {
h := strings.TrimPrefix(digest, "sha256:")
if len(h) < 24 {
return h
}
var g []string
for i := 0; i < 24; i += 4 {
g = append(g, h[i:i+4])
}
return strings.Join(g, " ")
}
// Fingerprint is the proposal as the operator is shown it: the value proposed, or for a clear the value it clears.
func (p Proposal) Fingerprint() string {
if p.Action == "clear" {
return fingerprint(p.WasDigest)
}
return fingerprint(p.Digest)
}
// about is what its ask is about: the value's place, so a newer proposal for it replaces an older one still open
// (ADR 0277 §10, at a level, a key).
func (p Proposal) about() string { return "config." + p.Level.ValueKey(p.Key) }
// Act is what an answer binds: this proposal's level, key, the digest of its exact value and of the value it was
// shown against, who proposed it and when, and the answer.
func (p Proposal) Act(option string) asks.Act {
return asks.Act{"verb": ProposalVerb, "option": option, "proposal": p.ID, "action": p.Action,
"level": p.Level.Kind, "node": p.Level.Node, "module": p.Level.Module, "key": p.Key, "value": p.Digest,
"was": p.WasDigest, "had-value": strconv.FormatBool(p.HadValue), "from": p.From,
"at": p.At.UTC().Format(time.RFC3339Nano)}
}
// ---- what the operator reads --------------------------------------------------------------------------
// Markers for what a phone is not shown.
const (
markAddress = "‹address›"
markPath = "‹path›"
markWithheld = "‹withheld›"
)
// sayable is a text as a channel that proves nothing is shown it, under the content rule (outward.Check): as it
// is, or the marker of what it carries.
func sayable(text string) (string, bool) {
refusal, ok := outward.Check(text)
if ok {
return text, true
}
switch refusal.Class {
case "address":
return markAddress, false
case "path":
return markPath, false
}
return markWithheld, false
}
// change is the change in one line, "+ key: value", "~ key: value (was: old)" or "- key (was: old)": each value
// exact (for a channel that proves who answers, ADR 0277 §6 and issue 383) or under the content rule.
func (p Proposal) change(exact bool) string {
say := func(raw json.RawMessage) string {
text := string(compact(raw))
if exact {
return text
}
s, _ := sayable(text)
return s
}
switch {
case p.Action == "clear":
return fmt.Sprintf("- %s (was: %s)", p.Key, say(p.Was))
case p.HadValue:
return fmt.Sprintf("~ %s: %s (was: %s)", p.Key, say(p.Value), say(p.Was))
}
return fmt.Sprintf("+ %s: %s", p.Key, say(p.Value))
}
// whole is whether every value is shown exactly under the content rule.
func (p Proposal) whole() bool { return p.change(false) == p.change(true) }
// reaches is every node it reaches, in words.
func (p Proposal) reaches() string {
nodes := map[string]bool{}
for _, r := range p.Reaches {
if i := strings.LastIndex(r, " on "); i > 0 {
nodes[r[i+4:]] = true
}
}
switch len(nodes) {
case 0:
return "none today, as it moves no module's value"
case 1:
return sortedKeys(nodes)[0] + " (" + strings.Join(p.Reaches, ", ") + ")"
}
list := sortedKeys(nodes)
return fmt.Sprintf("%d nodes, %s", len(list), strings.Join(list, ", "))
}
// summary is the ask's first line: the level, the key and the value, and that it takes effect at once.
func (p Proposal) summary() string {
verb := "Sets " + p.Key
if p.Action == "clear" {
verb = "Clears " + p.Key
}
trusted := ""
if p.Trusted {
trusted = ", a trusted value,"
}
reach := p.reaches()
s := fmt.Sprintf("%s%s at %s; approved, it takes effect at once on every node it reaches: %s.", verb, trusted,
p.Level, reach)
if len([]rune(s)) > asks.SummaryLength {
s = fmt.Sprintf("%s%s at %s; approved, it takes effect at once on every node it reaches (%d modules).", verb,
trusted, p.Level, len(p.Reaches))
}
return s
}
// point is a line cut to what a phone shows at a glance, saying so.
func point(line string) string {
const cut = "… (cut: read it in the note)"
if r := []rune(line); len(r) > asks.PointLength {
return string(r[:asks.PointLength-len([]rune(cut))]) + cut
}
return line
}
// headline is the ask's headline, shortened when the names make it longer than the router allows (ADR 0277 §5).
func (p Proposal) headline() string {
verb := "Set"
if p.Action == "clear" {
verb = "Clear"
}
where := "for the whole mesh"
switch p.Level.Kind {
case LevelNode:
where = "on " + p.Level.Node
case LevelModule:
where = "for " + p.Level.Module + " on " + p.Level.Node
}
for _, h := range []string{fmt.Sprintf("%s %s %s?", verb, p.Key, where), fmt.Sprintf("%s a config value %s?", verb,
where), verb + " a config value?"} {
if len([]rune(h)) <= asks.HeadlineLength {
if s, ok := sayable(h); ok {
return s
}
}
}
return verb + " a config value?"
}
// Ask is the proposal's ask, composed from it alone: its words, its two options each binding its act, who
// answers, its expiry and what it is about.
func (p Proposal) Ask() asks.Ask {
why, _ := sayable("Why: " + p.Why)
if why != "Why: "+p.Why {
why = "Why: given, and read in full with the proposal"
}
compose := func(exact bool) string {
return strings.Join([]string{p.summary(), point(p.change(exact)), point(why)}, "\n")
}
details := func(exact bool) string {
var d strings.Builder
d.WriteString(p.change(exact) + "\n")
from, _ := sayable(p.From)
fmt.Fprintf(&d, "Proposed by %s at %s UTC. Fingerprint %s.\n", from, p.At.UTC().Format("15:04 on 2 Jan"),
p.Fingerprint())
if !exact && !p.whole() {
d.WriteString("Where who answers is not proven, its values are shown as ‹address›, ‹path› or ‹withheld›.\n")
}
fmt.Fprintf(&d, "Read it whole, with this fingerprint: mesh-config's config verb with action proposals and "+
"proposal %s, through the mesh MCP server.", p.ID)
return d.String()
}
a := asks.Ask{ID: p.ID, Headline: p.headline(), Explanation: compose(false), Who: asks.Operator,
Expires: p.At.Add(ProposalLasts), OnExpiry: "the proposal is discarded; nothing changes", About: p.about(),
Details: details(false)}
if w := compose(true); w != a.Explanation {
a.Whole = w
}
if w := details(true); w != a.Details {
a.WholeDetails = w
}
does := "the value is set at once, and every node it reaches takes it"
if p.Action == "clear" {
does = "the value is cleared at once, and every node it reaches takes what is below it"
}
for _, o := range []struct{ id, label, does string }{
{OptionApprove, "Approve", does},
{OptionDecline, "Decline", "nothing changes; the proposal is discarded"},
} {
binds, _ := asks.ActDigest(p.Act(o.id))
a.Options = append(a.Options, asks.Option{ID: o.id, Label: o.label, Does: o.does, Level: asks.Approve,
Binds: binds})
}
return a
}
// problem says why a proposal as kept is not one this module made, or "": judged again before an Approve is
// acted on, so a record whose value or shown value changed after it was asked is refused (ADR 0277 §12).
func (p Proposal) problem() string {
switch {
case !asks.UsableID(p.ID):
return "its id is not one"
case p.Action != "set" && p.Action != "clear":
return "its action is neither set nor clear"
case !keyName.MatchString(p.Key):
return "its key is not one"
case p.At.IsZero():
return "it says no time"
case p.Action == "set" && valueDigest(p.Value, true) != p.Digest:
return "its value is not the value its digest names: it changed after it was asked"
case p.Action == "clear" && (len(p.Value) != 0 || p.Digest != valueDigest(nil, false)):
return "a clear carries no value"
case valueDigest(p.Was, p.HadValue) != p.WasDigest:
return "the value it was shown against is not the one its digest names: it changed after it was asked"
}
return ""
}
// ---- proposing -----------------------------------------------------------------------------------------
// ProposeAnswer is what a proposal answers.
type ProposeAnswer struct {
Said string `json:"said"`
Proposal string `json:"proposal"`
Asked string `json:"asked"`
Fingerprint string `json:"fingerprint"`
Expires time.Time `json:"expires"`
Router string `json:"router"`
Reaches []string `json:"reaches"`
Replaced []string `json:"replaced,omitempty"`
}
// AtTheTerminalInstead names the other way, said whenever a proposal cannot be asked (ADR 0277 §19).
const AtTheTerminalInstead = "the operator may set it at the controller's terminal instead, through the controller's " +
"config line, which carries the value to mesh-config"
// approverReady says why no channel that proves who answered can carry an approval, when this module can tell
// from the channel bench's kinds; an empty list says nothing, and the router is the judge (ADR 0259 §4).
func approverReady(bus AskBus) string {
kinds, err := bus.Kinds()
if err != nil || len(kinds) == 0 {
return ""
}
channels := 0
for _, k := range kinds {
if k.Seat != asks.ChannelSeat {
continue
}
channels++
if k.Promises("choice") && k.Promises("exact-render") && k.Promises("verified-sender") {
return ""
}
}
if channels == 0 {
return ""
}
return "no channel that proves who answered (one promising a verified sender, such as Telegram) is on the mesh"
}
func newProposalID() string {
var b [8]byte
_, _ = rand.Read(b[:])
return "cfg-" + hex.EncodeToString(b[:])
}
// endedWord is the router's word that an ask ended without a choice, heard while its proposer waits.
type endedWord struct {
Outcome asks.Outcome
Words string
}
func (c *Config) heardEnded(id string) (endedWord, bool) {
c.endedMu.Lock()
defer c.endedMu.Unlock()
w, ok := c.ended[id]
return w, ok
}
func (c *Config) tellEnded(id string, w endedWord) {
c.endedMu.Lock()
defer c.endedMu.Unlock()
if c.ended == nil {
c.ended = map[string]endedWord{}
}
c.ended[id] = w
}
// Propose judges a value as a set would, keeps it, and asks the operator under mesh-config's own name. Nothing is
// set here: only the operator's Approve sets it (Decide).
func (c *Config) Propose(call Call) (*ProposeAnswer, error) {
refuse := func(format string, a ...any) (*ProposeAnswer, error) {
return nil, errors.New(fmt.Sprintf(format, a...) + ". Nothing was proposed")
}
if c.Asks == nil {
return refuse("the operator cannot be asked from here; %s", AtTheTerminalInstead)
}
if call.By != "" {
return refuse("by names who gave a value at the controller's terminal; a proposal is asked of the operator")
}
if call.Proposal != "" {
return refuse("proposal names a proposal to read, with action proposals")
}
action := "set"
if call.Clear {
action = "clear"
if call.Value != nil {
return refuse("a clear carries no value")
}
}
c.mu.Lock()
prop, ask, replaced, err := c.propose(call, action)
c.mu.Unlock()
if err != nil {
return nil, err
}
answer := &ProposeAnswer{Proposal: prop.ID, Asked: ask.Headline, Fingerprint: prop.Fingerprint(),
Expires: ask.Expires, Reaches: prop.Reaches, Replaced: replaced,
Router: "the router has not said yet whether it took the ask; config's proposals shows it"}
// The router's word, before answering (ADR 0277 §20): it refuses at once an ask no channel can carry.
within, every := c.RouterWithin, c.RouterEvery
if within <= 0 {
within = RouterAnswersWithin
}
if every <= 0 {
every = RouterAnswersEvery
}
for deadline := time.Now().Add(within); time.Now().Before(deadline); time.Sleep(every) {
if w, ended := c.heardEnded(prop.ID); ended {
return nil, fmt.Errorf("the router did not ask the operator: the ask %s (%s). The proposal is ended and "+
"nothing is set; %s", w.Outcome, w.Words, AtTheTerminalInstead)
}
if e, err := c.Asks.Record(prop.ID); err == nil && e != nil {
answer.Router = "the router took the ask and shows it on the channels that can carry it"
break
}
}
answer.Said = fmt.Sprintf("proposed %s of %s at %s (fingerprint %s): the operator is asked %q on a channel that "+
"proves who answered. Nothing changes until they approve; on Approve it takes effect at once, and on Decline "+
"or at %s UTC it is discarded", action, prop.Key, prop.Level, prop.Fingerprint(), ask.Headline,
ask.Expires.UTC().Format("15:04 on 2 Jan"))
return answer, nil
}
// propose is Propose's judgement, its record and its ask; the caller holds the lock.
func (c *Config) propose(call Call, action string) (Proposal, asks.Ask, []string, error) {
fail := func(format string, a ...any) (Proposal, asks.Ask, []string, error) {
return Proposal{}, asks.Ask{}, nil, errors.New(fmt.Sprintf(format, a...) + ". Nothing was proposed")
}
level, err := call.level()
if err != nil {
return fail("%v", err)
}
if !keyName.MatchString(call.Key) {
return fail("name the key to propose: lower-case letters, digits, dots, dashes and underscores")
}
why := strings.TrimSpace(call.Why)
if why == "" || strings.Contains(why, "\n") {
return fail("say why, in one line: every change keeps its why (novox/hq ADR 0348 §9)")
}
if call.Caller == "" || call.Caller == Unproven {
return fail("the call names no proven caller, and a proposal keeps who made it: call config through the mesh, " +
"which names you")
}
s, err := c.sources()
if err != nil {
return Proposal{}, asks.Ask{}, nil, err
}
// **Judged as a set judges it, before anyone is asked** (ADR 0277 §4): no ask for a value the verb would refuse.
r, trusted, err := c.declared(s, level, call.Key)
if err != nil {
return fail("%v", err)
}
vk := level.ValueKey(call.Key)
was, had := s.Values[vk]
now := c.now()
prop := Proposal{ID: newProposalID(), Action: action, Level: level, Key: call.Key, HadValue: had,
WasDigest: valueDigest(was.Value, had), Trusted: trusted, From: call.Caller, Why: why, At: now,
State: ProposalWaiting, Digest: valueDigest(nil, false)}
if had {
prop.Was = compact(was.Value)
}
after := Sources{Assignments: s.Assignments, Declared: s.Declared, Predefined: s.Predefined,
Values: map[string]Value{}}
for k, v := range s.Values {
after.Values[k] = v
}
switch action {
case "set":
if call.Value == nil {
return fail("give the value to propose for %s", call.Key)
}
raw, err := json.Marshal(call.Value)
if err != nil {
return fail("the value cannot be read: %v", err)
}
var value any
_ = json.Unmarshal(raw, &value)
if problems := r.check(value); len(problems) > 0 {
return fail("%s is refused for %s at %s: %s", show(value), call.Key, level, strings.Join(problems, "; "))
}
prop.Value, prop.Digest = compact(raw), valueDigest(raw, true)
if had && string(compact(was.Value)) == string(prop.Value) {
return fail("%s at %s already holds exactly %s", call.Key, level, prop.Value)
}
after.Values[vk] = Value{Value: prop.Value}
case "clear":
if !had {
return fail("nothing is set for %s at %s, so there is nothing to clear", call.Key, level)
}
delete(after.Values, vk)
}
for _, m := range diff(Resolve(s), Resolve(after)) {
prop.Reaches = append(prop.Reaches, m.Module+" on "+m.Node)
}
if prop.Reaches == nil {
prop.Reaches = []string{}
}
// **The operator is shown exactly what they approve** where who answered is proven (ADR 0277 §6, §19): a value
// shaped like a secret could be shown nowhere, so it is not proposed; config holds no secret.
if refusal, ok := outward.CheckSecret(prop.change(true)); !ok {
return fail("the change carries %s, which leaves the mesh nowhere, so the operator could not be shown what "+
"they approve; config holds no secret, and %s", refusal, AtTheTerminalInstead)
}
// **Its own cap of open asks, and the same change once** (ADR 0277 §21); a newer proposal for the same place
// replaces an older one still open (§10).
all, err := c.proposals()
if err != nil {
return Proposal{}, asks.Ask{}, nil, err
}
var replace []Proposal
open := 0
for _, o := range all {
if o.State != ProposalWaiting || now.After(o.At.Add(ProposalLasts)) {
continue
}
if o.about() == prop.about() {
if o.Action == prop.Action && o.Digest == prop.Digest && o.WasDigest == prop.WasDigest {
return fail("the same change is proposed already, as %s, and waits for the operator's answer", o.ID)
}
replace = append(replace, o)
continue
}
open++
}
if open >= asks.MostOpen {
return fail("%d proposals already wait for the operator's answer, and mesh-config asks at most %d at once; "+
"config's proposals lists them", open, asks.MostOpen)
}
if why := approverReady(c.Asks); why != "" {
return fail("%s; %s", why, AtTheTerminalInstead)
}
ask := prop.Ask()
if err := ask.Check(now); err != nil {
return fail("%v", err)
}
if err := c.Store.Put(ProposalsState, prop.ID, prop); err != nil {
return fail("the proposal could not be kept, so the operator was not asked: %v", err)
}
var replaced []string
for _, o := range replace {
o.State, o.Outcome, o.Decided = ProposalEnded, "replaced by the newer proposal "+prop.ID+"; nothing was set", now
if err := c.Store.Put(ProposalsState, o.ID, o); err == nil {
replaced = append(replaced, o.ID)
}
}
if err := c.Asks.Publish(asks.AskSubject(Module), ask, ask.ID); err != nil {
prop.State, prop.Outcome = ProposalUnasked, "the ask could not be published: "+err.Error()
_ = c.Store.Put(ProposalsState, prop.ID, prop)
c.logf("[mesh-config] proposal %s could not be asked: %v", prop.ID, err)
return Proposal{}, asks.Ask{}, nil, fmt.Errorf("the operator could not be asked (%v); the proposal is kept "+
"and will never be set; %s", err, AtTheTerminalInstead)
}
c.logf("[mesh-config] proposal %s (%s %s at %s, fingerprint %s) asked of the operator", prop.ID, action, prop.Key,
prop.Level, prop.Fingerprint())
return prop, ask, replaced, nil
}
// proposals is every proposal kept, oldest first.
func (c *Config) proposals() ([]Proposal, error) {
var out []Proposal
err := c.each(ProposalsState, func(key string, raw []byte) error {
var p Proposal
if json.Unmarshal(raw, &p) != nil || p.ID != key {
return nil // not one this module wrote: never asked, never acted on
}
out = append(out, p)
return nil
})
if err != nil {
return nil, fmt.Errorf("the proposals cannot be read: %w", err)
}
sort.SliceStable(out, func(i, j int) bool { return out[i].At.Before(out[j].At) })
return out, nil
}
// ---- deciding --------------------------------------------------------------------------------------------
// routerRecord is the router's record of one ask, as far as this module reads it.
type routerRecord struct {
State string `json:"state"`
Ask *asks.Ask `json:"ask"`
Warrant *asks.Warrant `json:"warrant"`
}
// Decide decides every waiting proposal whose ask the router has closed, oldest warrant first, each held to the
// warrant as described at the top of this file; and asks the controller to record every warrant acted on that its
// hand-act log does not hold yet. It answers a line for each decision.
func (c *Config) Decide() ([]string, error) {
if c.Asks == nil {
return nil, nil
}
c.mu.Lock()
defer c.mu.Unlock()
all, err := c.proposals()
if err != nil {
return nil, err
}
now := c.now()
type closed struct {
p Proposal
w asks.Warrant
}
var ready []closed
var said []string
for _, p := range all {
if p.State != ProposalWaiting {
continue
}
e, err := c.Asks.Record(p.ID)
if err == nil && e == nil && now.After(p.At.Add(ProposalLasts+time.Hour)) {
// Ended, and the router keeps no record of it: no answer can be proven, so nothing is set.
said = append(said, c.end(p, "the router keeps no record of its ask (refused before it was taken, or "+
"forgotten); nothing was set"))
continue
}
if err != nil || e == nil {
continue // open, not yet recorded, or not readable now: read again on the next look
}
var rec routerRecord
if json.Unmarshal(e.Value, &rec) != nil || rec.State == "open" || rec.Warrant == nil {
continue
}
ready = append(ready, closed{p, *rec.Warrant})
}
sort.SliceStable(ready, func(i, j int) bool { return ready[i].w.At.Before(ready[j].w.At) })
for _, r := range ready {
said = append(said, c.decide(r.p, r.w))
}
c.recordWarrants(now)
return said, nil
}
// end records a proposal ended without the operator's choice.
func (c *Config) end(p Proposal, why string) string {
p.State, p.Outcome, p.Decided = ProposalEnded, why, c.now()
if err := c.Store.Put(ProposalsState, p.ID, p); err != nil {
return fmt.Sprintf("proposal %s: %s, and that could not be kept (%v)", p.ID, why, err)
}
return "proposal " + p.ID + ": " + why
}
// decide holds one closed ask's warrant to its proposal and, on an Approve that holds, sets the value. The caller
// holds the lock.
func (c *Config) decide(p Proposal, w asks.Warrant) string {
now := c.now()
keep := func(state, outcome string) string {
p.State, p.Outcome, p.Decided = state, outcome, now
if err := c.Store.Put(ProposalsState, p.ID, p); err != nil {
return fmt.Sprintf("proposal %s: %s, and that could not be kept (%v)", p.ID, outcome, err)
}
return "proposal " + p.ID + ": " + outcome
}
refused := func(why string) string {
c.logf("[mesh-config] REFUSED the answer to proposal %s: %s", p.ID, why)
return keep(ProposalRefused, "the answer is refused and nothing is set: "+why)
}
if w.Asker != Module || w.Ask != p.ID {
return refused(fmt.Sprintf("the router's record is for %s's ask %s", w.Asker, w.Ask))
}
if w.Outcome != asks.OutcomeChosen {
why := "the ask " + string(w.Outcome)
if w.Words != "" {
why += " (" + w.Words + ")"
}
return keep(ProposalEnded, why+"; nothing was set")
}
// **The record is held to what was asked** (ADR 0277 §11, §12): its value and the value it was shown against
// still digest to what the ask bound, the warrant answers the ask composed again from it, and the act about to be
// performed is the one the chosen option bound.
if problem := p.problem(); problem != "" {
return refused("the proposal is not the one asked: " + problem)
}
option, err := w.For(Module, p.Ask())
if err == nil {
err = option.Performs(p.Act(option.ID))
}
if err != nil {
return refused(err.Error())
}
switch option.ID {
case OptionDecline:
p.Answered = w.Says()
return keep(ProposalDeclined, "declined by the operator ("+w.Says()+"); nothing was set")
case OptionApprove:
p.Answered, p.Approved = w.Says(), approvedWords(p, w)
default:
return refused("the option " + option.ID + " is not one a proposal offers")
}
// Claimed before it is acted on: a warrant heard twice, or read again, sets nothing twice.
p.State, p.Outcome, p.Decided = ProposalApproved, "approved by the operator; being set", now
if err := c.Store.Put(ProposalsState, p.ID, p); err != nil {
return fmt.Sprintf("proposal %s: approved by the operator, and not set: the decision could not be kept (%v); "+
"it is read again on the next look", p.ID, err)
}
call := Call{Action: p.Action, Key: p.Key, Module: p.Level.Module, Node: p.Level.Node, Why: p.Why, Caller: p.From,
approval: &approval{Proposal: p.ID, By: "the operator", Says: p.Approved, Was: p.Was, HadValue: p.HadValue,
WasDigest: p.WasDigest}}
if p.Action == "set" {
var v any
_ = json.Unmarshal(p.Value, &v)
call.Value = v
if v == nil {
call.Value = json.RawMessage("null")
}
}
a, err := c.change(call)
if err != nil {
return keep(ProposalRefused, "approved by the operator, and refused on Approve, so nothing is set: "+err.Error())
}
return keep(ProposalApproved, "approved by the operator and set: "+a.Said)
}
// approvedWords is who approved it, as kept beside the value (ADR 0277 §15).
func approvedWords(p Proposal, w asks.Warrant) string {
who := "the operator"
if w.By != nil {
who = "the " + w.By.Who
if w.By.Identity != "" {
who += ", as " + w.By.Kind + " identity " + w.By.Identity
}
via := w.By.Kind
if w.By.Verified != "" {
via += " (" + w.By.Verified + ")"
}
who += ", via " + via
}
return fmt.Sprintf("approved by %s at %s, ask %s, proposed by %s", who, w.At.UTC().Format(time.RFC3339), p.ID, p.From)
}
// recordWarrants asks the controller to record each warrant acted on (Approve or Decline) that its hand-act log
// does not hold yet (ADR 0277 §17): the controller reads it from the router's own record. The caller holds the lock.
func (c *Config) recordWarrants(now time.Time) {
all, err := c.proposals()
if err != nil {
return
}
for _, p := range all {
if p.Recorded || p.Answered == "" || now.Sub(p.Decided) > 7*24*time.Hour {
continue
}
if err := c.Asks.Warranted(p.ID); err != nil {
p.Record = "not recorded in the hand-act log yet: " + err.Error()
} else {
p.Recorded, p.Record = true, ""
}
_ = c.Store.Put(ProposalsState, p.ID, p)
}
}
// HeardEnd takes the router's word on one of this module's asks. An ask ended without a choice ends its proposal
// at once, and is told to a proposer waiting on it; nothing is set on such a word, so it needs no proof beyond its
// subject. A choice is only ever taken from the router's record (Decide).
func (c *Config) HeardEnd(w asks.Warrant) []string {
if w.Asker != Module || !asks.UsableID(w.Ask) {
return nil
}
if w.Outcome == asks.OutcomeChosen {
said, err := c.Decide()
if err != nil {
said = append(said, "reading the operator's answers: "+err.Error())
}
return said
}
c.mu.Lock()
defer c.mu.Unlock()
defer c.tellEnded(w.Ask, endedWord{Outcome: w.Outcome, Words: w.Words})
e, err := c.Store.Get(ProposalsState, w.Ask)
if err != nil || e == nil {
return nil
}
var p Proposal
if json.Unmarshal(e.Value, &p) != nil || p.ID != w.Ask || p.State != ProposalWaiting {
return nil
}
why := "the ask " + string(w.Outcome)
if w.Words != "" {
why += " (" + w.Words + ")"
}
return []string{c.end(p, why+"; nothing was set")}
}
// ---- the list ----------------------------------------------------------------------------------------------
// ProposalRow is one proposal as a listing says it.
type ProposalRow struct {
ID string `json:"id"`
Action string `json:"action"`
Level Level `json:"level"`
Key string `json:"key"`
State string `json:"state"`
Outcome string `json:"outcome,omitempty"`
From string `json:"from"`
At time.Time `json:"at"`
Expires time.Time `json:"expires"`
Fingerprint string `json:"fingerprint"`
Approved string `json:"approved,omitempty"`
// What one proposal read whole says besides.
Value json.RawMessage `json:"value,omitempty"`
Was json.RawMessage `json:"was,omitempty"`
Why string `json:"why,omitempty"`
Reaches []string `json:"reaches,omitempty"`
Router string `json:"router,omitempty"`
Phone string `json:"the phone,omitempty"`
Warning string `json:"warning,omitempty"`
}
// Proposals is every proposal, newest first; with a proposal's id, that one whole, with whether the router's ask
// is the one composed from it.
func (c *Config) Proposals(call Call) ([]ProposalRow, error) {
all, err := c.proposals()
if err != nil {
return nil, err
}
out := []ProposalRow{}
for i := len(all) - 1; i >= 0; i-- {
p := all[i]
if call.Proposal != "" && p.ID != call.Proposal {
continue
}
if (call.Key != "" && p.Key != call.Key) || (call.Node != "" && p.Level.Node != call.Node) ||
(call.Module != "" && p.Level.Module != call.Module) {
continue
}
row := ProposalRow{ID: p.ID, Action: p.Action, Level: p.Level, Key: p.Key, State: p.State, Outcome: p.Outcome,
From: p.From, At: p.At, Expires: p.At.Add(ProposalLasts), Fingerprint: p.Fingerprint(), Approved: p.Approved}
if p.State == ProposalWaiting && c.now().After(row.Expires) {
row.State = "expired, not yet heard"
}
if call.Proposal != "" {
row.Value, row.Was, row.Why, row.Reaches = p.Value, p.Was, p.Why, p.Reaches
if problem := p.problem(); problem != "" {
row.Warning = "this proposal is never set: " + problem
}
if c.Asks != nil {
if e, err := c.Asks.Record(p.ID); err == nil && e != nil {
var rec routerRecord
if json.Unmarshal(e.Value, &rec) == nil {
row.Router = rec.State
switch {
case rec.Ask == nil:
row.Phone = "cannot be compared: the router's record keeps no ask"
case rec.Ask.Digest() == p.Ask().Digest():
row.Phone = "shows the ask composed from exactly this proposal (fingerprint " + p.Fingerprint() + ")"
default:
row.Phone = "shows ANOTHER ask than this proposal composes: do not approve it"
}
}
}
}
}
out = append(out, row)
}
if call.Proposal != "" && len(out) == 0 {
return nil, errors.New("no proposal " + strconv.Quote(call.Proposal) + " is kept")
}
return out, nil
}
@@ -0,0 +1,568 @@
package main
import (
"encoding/json"
"errors"
"strings"
"sync"
"testing"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asks"
)
// standIn is the operator channel's router and a channel that proves who answered, in memory: it takes the asks
// mesh-config publishes, keeps its own record of each under mesh-config's name, and records the operator's choice
// as the router's warrant. Nothing of mesh-config's is trusted by it: it digests the ask it took.
type standIn struct {
mu sync.Mutex
subjects []string
asks map[string]asks.Ask
records map[string][]byte
kinds []stdio.KindHeld
refuse error
warranted []string
// refuseWith makes the router refuse every ask it is published, as the router does for an ask no channel can
// carry: no record, and its word on the decided subject (heard through hear).
refuseWith string
hear func(asks.Warrant)
// noRecord keeps the router from recording an ask it took (an ask it has not taken yet).
noRecord bool
}
func newStandIn() *standIn {
return &standIn{asks: map[string]asks.Ask{}, records: map[string][]byte{}, kinds: []stdio.KindHeld{
{Seat: asks.ChannelSeat, Kind: "telegram", Module: "telegram", Node: "novox",
Capabilities: []string{"choice", "exact-render", "verified-sender"}}}}
}
func (r *standIn) Publish(subject string, body any, id string) error {
r.mu.Lock()
defer r.mu.Unlock()
if r.refuse != nil {
return r.refuse
}
a, ok := body.(asks.Ask)
if !ok || a.ID != id {
return errors.New("not an ask")
}
r.subjects = append(r.subjects, subject)
r.asks[id] = a
if r.refuseWith != "" {
w := asks.Warrant{Ask: id, Asker: Module, About: a.About, Outcome: asks.OutcomeRefused, Words: r.refuseWith,
At: time.Now()}
go r.hear(w)
return nil
}
if !r.noRecord {
raw, _ := json.Marshal(map[string]any{"state": "open", "ask": a})
r.records[id] = raw
}
return nil
}
func (r *standIn) Record(id string) (*stdio.StateEntry, error) {
r.mu.Lock()
defer r.mu.Unlock()
raw, ok := r.records[id]
if !ok {
return nil, nil
}
return &stdio.StateEntry{Key: Module + "." + id, Value: raw}, nil
}
func (r *standIn) Kinds() ([]stdio.KindHeld, error) { return r.kinds, nil }
func (r *standIn) Warranted(ask string) error {
r.mu.Lock()
defer r.mu.Unlock()
r.warranted = append(r.warranted, ask)
return nil
}
// choose is the operator choosing an option on a channel that proves who answered: the router's record of the ask
// it took, closed with its warrant.
func (r *standIn) choose(t *testing.T, id, option string, tweak func(*asks.Warrant)) asks.Warrant {
t.Helper()
r.mu.Lock()
defer r.mu.Unlock()
a, ok := r.asks[id]
if !ok {
t.Fatalf("the router took no ask %s", id)
}
o, ok := a.Option(option)
if !ok {
t.Fatalf("the ask %s offers no %s", id, option)
}
w := asks.Warrant{Ask: id, Asker: Module, About: a.About, Outcome: asks.OutcomeChosen, Option: o.ID, Label: o.Label,
Level: o.Level, By: &asks.Person{Who: asks.Operator, Kind: "telegram", Identity: "4242", Verified: "user id verified"},
Channel: "telegram", Proofs: []string{"P1"}, At: time.Date(2026, 10, 14, 19, 30, 0, 0, time.UTC),
AskDigest: a.Digest()}
if tweak != nil {
tweak(&w)
}
raw, _ := json.Marshal(map[string]any{"state": "closed", "ask": a, "warrant": w})
r.records[id] = raw
return w
}
// aProposingMesh is aMesh with the stand-in channel, its waits short.
func aProposingMesh(t *testing.T) (*Config, *memStore, *standIn) {
t.Helper()
c, m := aMesh(t)
r := newStandIn()
r.hear = func(w asks.Warrant) { c.HeardEnd(w) }
c.Asks, c.RouterWithin, c.RouterEvery = r, 300*time.Millisecond, 5*time.Millisecond
return c, m, r
}
func propose(t *testing.T, c *Config, call Call) *ProposeAnswer {
t.Helper()
call.Action = "propose"
if call.Caller == "" {
call.Caller = "g14.node-tools"
}
if call.Why == "" {
call.Why = "the agents need the new rule"
}
a, err := c.Propose(call)
if err != nil {
t.Fatalf("propose %s: %v", call.Key, err)
}
return a
}
func storedProposal(t *testing.T, m *memStore, id string) Proposal {
t.Helper()
e, _ := m.Get(ProposalsState, id)
if e == nil {
t.Fatalf("no proposal %s is kept", id)
}
var p Proposal
if err := json.Unmarshal(e.Value, &p); err != nil {
t.Fatal(err)
}
return p
}
func storedValue(t *testing.T, m *memStore, key string) (Value, bool) {
t.Helper()
e, _ := m.Get(ValuesState, key)
if e == nil {
return Value{}, false
}
var v Value
if err := json.Unmarshal(e.Value, &v); err != nil {
t.Fatal(err)
}
return v, true
}
// **A trusted value proposed end to end** (ADR 0348 §8, How it is checked: "A proposal's test"): mesh-config asks
// under its own name; its ask names the level, the key and the value and says the change takes effect at once;
// each option binds them; nothing is set until the operator's Approve; on Approve the value is set as a set is,
// who approved it is kept beside it, and the warrant is recorded in the hand-act log.
func TestATrustedValueIsProposedAskedUnderItsOwnNameAndSetOnTheOperatorsApprove(t *testing.T) {
c, m, r := aProposingMesh(t)
m.events, m.handActs = nil, nil
value := map[string]any{"permissions": map[string]any{"deny": []any{"Bash(ssh:*)"}}}
a := propose(t, c, Call{Key: "agent-managed-settings", Value: value})
// Nothing is set by a proposal.
if _, set := storedValue(t, m, "mesh.agent-managed-settings"); set || len(m.events) != 0 || len(m.handActs) != 0 {
t.Fatalf("a proposal set something: %d events, %d hand-acts", len(m.events), len(m.handActs))
}
if !strings.Contains(a.Router, "took the ask") || a.Proposal == "" || len(a.Reaches) != 1 ||
a.Reaches[0] != "claude-code on g14" {
t.Errorf("the proposal answered %+v", a)
}
// Asked under mesh-config's own name, on the operator channel.
if len(r.subjects) != 1 || r.subjects[0] != asks.AskSubject("mesh-config") {
t.Fatalf("asked on %v", r.subjects)
}
ask := r.asks[a.Proposal]
if err := ask.Check(c.now()); err != nil {
t.Fatalf("the ask: %v", err)
}
if err := asks.Glance(ask.Explanation); err != nil {
t.Errorf("the ask does not read at a glance: %v\n%s", err, ask.Explanation)
}
for _, want := range []string{"agent-managed-settings", "mesh config", "takes effect at once", "claude-code",
"a trusted value"} {
if !strings.Contains(ask.Explanation, want) {
t.Errorf("the ask's explanation does not say %q:\n%s", want, ask.Explanation)
}
}
if !strings.Contains(ask.Headline, "agent-managed-settings") || !strings.Contains(ask.Headline, "whole mesh") {
t.Errorf("the headline is %q", ask.Headline)
}
// The value exact where who answers is proven; under the content rule elsewhere.
exact := `+ agent-managed-settings: {"permissions":{"deny":["Bash(ssh:*)"]}}`
shown := ask.Explanation
if ask.Whole != "" {
shown = ask.Whole
}
if !strings.Contains(shown, exact) || !strings.Contains(ask.Details+ask.WholeDetails, a.Proposal) {
t.Errorf("the ask does not show the value whole:\n%s\n%s", shown, ask.WholeDetails)
}
// Each option binds the level, the key and the value.
p := storedProposal(t, m, a.Proposal)
for _, o := range ask.Options {
if o.Level != asks.Approve {
t.Errorf("the option %s is at %s", o.ID, o.Level)
}
act := p.Act(o.ID)
want, _ := asks.ActDigest(act)
if o.Binds != want || act["level"] != LevelMesh || act["key"] != "agent-managed-settings" ||
act["value"] != valueDigest(json.RawMessage(`{"permissions":{"deny":["Bash(ssh:*)"]}}`), true) ||
act["was"] != valueDigest(nil, false) {
t.Errorf("the option %s binds %s for %v", o.ID, o.Binds, act)
}
}
// Still nothing while the ask is open.
if said, _ := c.Decide(); len(said) != 0 {
t.Errorf("an open ask decided %v", said)
}
r.choose(t, a.Proposal, OptionApprove, nil)
said, err := c.Decide()
if err != nil || len(said) != 1 || !strings.Contains(said[0], "approved by the operator and set") {
t.Fatalf("the Approve decided %v (%v)", said, err)
}
v, set := storedValue(t, m, "mesh.agent-managed-settings")
if !set || string(v.Value) != `{"permissions":{"deny":["Bash(ssh:*)"]}}` || v.By != "the operator" ||
v.Via != "g14.node-tools" || !strings.Contains(v.Approved, "approved by the operator, as telegram identity 4242, "+
"via telegram (user id verified)") || !strings.Contains(v.Approved, "ask "+a.Proposal) ||
!strings.Contains(v.Approved, "proposed by g14.node-tools") {
t.Fatalf("the value is %+v", v)
}
if res := m.resolved(t, "g14", "claude-code", "managed"); res.Source != SourceMesh ||
string(res.Value) != string(v.Value) {
t.Errorf("claude-code's managed settings resolve as %+v", res)
}
if len(m.events) != 1 || m.events[0].Approved != v.Approved || len(m.handActs) != 1 ||
!strings.Contains(m.handActs[0], "the operator's Approve of proposal "+a.Proposal) {
t.Errorf("the set raised %+v and recorded %q", m.events, m.handActs)
}
listed, _ := c.List(Call{Key: "agent-managed-settings"})
if len(listed.Set) != 1 || listed.Set[0].Approved != v.Approved {
t.Errorf("the listing says %+v", listed.Set)
}
if p = storedProposal(t, m, a.Proposal); p.State != ProposalApproved || !p.Recorded ||
len(r.warranted) != 1 || r.warranted[0] != a.Proposal {
t.Errorf("the proposal is %+v; warrants recorded %v", p, r.warranted)
}
// A warrant heard again, or read again, sets nothing twice.
if said, _ = c.Decide(); len(said) != 0 || len(m.events) != 1 {
t.Errorf("the warrant read again decided %v", said)
}
}
// **An Approve against a changed value is refused** (ADR 0348 §8, ADR 0277 §13; design 57 K4's proof): the value
// stored at the level changed after the operator was shown the change against it, so nothing is set.
func TestAnApproveIsRefusedWhenTheStoredValueChangedSince(t *testing.T) {
c, m, r := aProposingMesh(t)
a := propose(t, c, Call{Key: "agent-managed-settings", Value: map[string]any{"a": 1.0}})
// Set at the terminal meanwhile.
set(t, c, Call{Key: "agent-managed-settings", Value: map[string]any{"b": 2.0}, Caller: ControllerCaller, By: "jochen"})
m.events = nil
r.choose(t, a.Proposal, OptionApprove, nil)
said, _ := c.Decide()
if len(said) != 1 || !strings.Contains(said[0], "it changed since, so nothing is set") {
t.Fatalf("the Approve decided %v", said)
}
if v, _ := storedValue(t, m, "mesh.agent-managed-settings"); string(v.Value) != `{"b":2}` || v.Approved != "" {
t.Errorf("the value is %+v", v)
}
if len(m.events) != 0 {
t.Errorf("a refused Approve raised %+v", m.events)
}
if p := storedProposal(t, m, a.Proposal); p.State != ProposalRefused {
t.Errorf("the proposal is %s: %s", p.State, p.Outcome)
}
// And a clear since: the value it was shown against (a value) is no longer stored.
set(t, c, Call{Key: "font-size", Value: 12.0, Node: "g14"})
a = propose(t, c, Call{Key: "font-size", Value: 14.0, Node: "g14"})
set(t, c, Call{Action: "clear", Key: "font-size", Node: "g14"})
r.choose(t, a.Proposal, OptionApprove, nil)
if said, _ = c.Decide(); len(said) != 1 || !strings.Contains(said[0], "no value set") {
t.Fatalf("the Approve after a clear decided %v", said)
}
if _, isSet := storedValue(t, m, "node.g14.font-size"); isSet {
t.Error("an Approve against a cleared value set it")
}
}
// **A record that changed after it was asked is refused on Approve** (ADR 0277 §12): its value, or the value it was
// shown against, no longer what the ask bound.
func TestAnApproveIsRefusedWhenTheProposalChangedAfterItWasAsked(t *testing.T) {
for _, tc := range []struct {
name string
tamper func(p *Proposal)
want string
}{
{"its value", func(p *Proposal) { p.Value = json.RawMessage(`{"evil":true}`) }, "its value is not the value"},
{"its value and digest", func(p *Proposal) {
p.Value = json.RawMessage(`{"evil":true}`)
p.Digest = valueDigest(p.Value, true)
}, "it was not the ask the person was shown"},
{"the value it was shown against", func(p *Proposal) {
p.Was, p.HadValue = json.RawMessage(`{"x":1}`), true
}, "the value it was shown against"},
{"its level", func(p *Proposal) { p.Level = Level{Kind: LevelNode, Node: "g14"} }, "not the ask the person was shown"},
} {
t.Run(tc.name, func(t *testing.T) {
c, m, r := aProposingMesh(t)
a := propose(t, c, Call{Key: "agent-managed-settings", Value: map[string]any{"a": 1.0}})
r.choose(t, a.Proposal, OptionApprove, nil)
p := storedProposal(t, m, a.Proposal)
tc.tamper(&p)
_ = m.Put(ProposalsState, p.ID, p)
said, _ := c.Decide()
if len(said) != 1 || !strings.Contains(said[0], tc.want) {
t.Fatalf("decided %v, want %q", said, tc.want)
}
if keys, _ := m.Keys(ValuesState); len(keys) != 0 {
t.Errorf("a changed proposal set %v", keys)
}
})
}
}
// **A warrant that is not the answer to the ask is refused** (ADR 0277 §11): another ask's digest, another level, an
// answer after expiry, nobody verified.
func TestAWarrantThatDoesNotAnswerTheAskIsRefused(t *testing.T) {
for _, tc := range []struct {
name string
tweak func(w *asks.Warrant)
}{
{"another ask's digest", func(w *asks.Warrant) { w.AskDigest = "sha256:00" }},
{"another level", func(w *asks.Warrant) { w.Level = asks.Acknowledge }},
{"after its expiry", func(w *asks.Warrant) { w.At = time.Date(2026, 10, 16, 0, 0, 0, 0, time.UTC) }},
{"an option it did not offer", func(w *asks.Warrant) { w.Option = "force" }},
{"no person", func(w *asks.Warrant) { w.By = nil }},
} {
t.Run(tc.name, func(t *testing.T) {
c, m, r := aProposingMesh(t)
a := propose(t, c, Call{Key: "agent-managed-settings", Value: map[string]any{"a": 1.0}})
r.choose(t, a.Proposal, OptionApprove, tc.tweak)
said, _ := c.Decide()
if len(said) != 1 || !strings.Contains(said[0], "refused") {
t.Fatalf("decided %v", said)
}
if keys, _ := m.Keys(ValuesState); len(keys) != 0 {
t.Errorf("a warrant that does not hold set %v", keys)
}
if len(r.warranted) != 0 {
t.Errorf("a refused warrant was recorded as acted on: %v", r.warranted)
}
})
}
}
// **On Decline, at expiry, or on the router's refusal nothing is set** (ADR 0277 §9, §16, §20), and a Decline is a
// warrant the hand-act log holds.
func TestDeclineExpiryAndTheRoutersRefusalSetNothing(t *testing.T) {
c, m, r := aProposingMesh(t)
a := propose(t, c, Call{Key: "agent-managed-settings", Value: map[string]any{"a": 1.0}})
r.choose(t, a.Proposal, OptionDecline, nil)
if said, _ := c.Decide(); len(said) != 1 || !strings.Contains(said[0], "declined by the operator") {
t.Fatalf("the Decline decided %v", said)
}
if p := storedProposal(t, m, a.Proposal); p.State != ProposalDeclined || p.Approved != "" || !p.Recorded {
t.Errorf("the declined proposal is %+v", p)
}
// Ended without a choice: the router's word.
a = propose(t, c, Call{Key: "font-size", Value: 12.0, Node: "g14"})
c.HeardEnd(asks.Warrant{Ask: a.Proposal, Asker: Module, Outcome: asks.OutcomeExpired, At: time.Now()})
if p := storedProposal(t, m, a.Proposal); p.State != ProposalEnded || !strings.Contains(p.Outcome, "expired") {
t.Errorf("the expired proposal is %+v", p)
}
// Ended, and the router never recorded it: discarded once its time is past.
r.noRecord = true
a = propose(t, c, Call{Key: "font-size", Value: 13.0, Node: "novox"})
if !strings.Contains(a.Router, "has not said yet") {
t.Errorf("an ask the router has not taken answered %q", a.Router)
}
later := time.Date(2026, 10, 16, 0, 0, 0, 0, time.UTC)
c.Now = func() time.Time { return later }
if said, _ := c.Decide(); len(said) != 1 || !strings.Contains(said[0], "keeps no record") {
t.Errorf("an ask never recorded decided %v", said)
}
if keys, _ := m.Keys(ValuesState); len(keys) != 0 {
t.Errorf("an unanswered proposal set %v", keys)
}
// Refused by the router at once: said to the proposer.
c2, m2, r2 := aProposingMesh(t)
r2.refuseWith = "no channel can carry an approval"
_, err := c2.Propose(Call{Action: "propose", Key: "font-size", Value: 12.0, Caller: "g14.node-tools", Why: "w"})
if err == nil || !strings.Contains(err.Error(), "no channel can carry an approval") {
t.Fatalf("the router's refusal answered %v", err)
}
all, _ := c2.proposals()
if len(all) != 1 || all[0].State != ProposalEnded {
t.Errorf("the refused proposal is %+v", all)
}
if keys, _ := m2.Keys(ValuesState); len(keys) != 0 {
t.Errorf("a refused proposal set %v", keys)
}
}
// **A proposal is judged as a set before anyone is asked, and refused at once when it cannot be asked** (ADR 0277 §4,
// §19, §21): a value the verb would refuse; nothing that would change; a value shaped like a secret; no channel that
// proves who answered; a publish that fails; the same change twice; a fourth open ask.
func TestAProposalIsJudgedAndRefusedBeforeTheOperatorIsAsked(t *testing.T) {
c, m, r := aProposingMesh(t)
set(t, c, Call{Key: "font-size", Value: 12.0})
for _, tc := range []struct {
name string
call Call
want string
}{
{"a key not declared", Call{Key: "colour", Value: "red", Node: "g14", Module: "dunst"}, "it declares:"},
{"a value failing validation", Call{Key: "font-size", Value: 99.0}, "above the maximum 72"},
{"a module value without a node", Call{Key: "width", Value: 400.0, Module: "dunst"}, "no module value for the whole mesh"},
{"nothing that would change", Call{Key: "font-size", Value: 12.0}, "already holds exactly 12"},
{"a clear of nothing", Call{Key: "font-size", Clear: true, Node: "g14"}, "nothing to clear"},
{"a clear with a value", Call{Key: "font-size", Clear: true, Value: 1.0}, "a clear carries no value"},
{"a secret's shape", Call{Key: "agent-managed-settings", Value: map[string]any{"env": map[string]any{
"API_TOKEN": "ghp_abcdefghijklmnopqrstuvwxyz0123456789"}}}, "leaves the mesh nowhere"},
{"by", Call{Key: "font-size", Value: 13.0, By: "jochen"}, "by names who gave"},
{"no why", Call{Key: "font-size", Value: 13.0, Why: " "}, "say why"},
{"an unproven caller", Call{Key: "font-size", Value: 13.0, Caller: Unproven}, "no proven caller"},
} {
t.Run(tc.name, func(t *testing.T) {
call := tc.call
call.Action = "propose"
if call.Caller == "" {
call.Caller = "g14.node-tools"
}
if call.Why == "" {
call.Why = "a test"
}
if _, err := c.Propose(call); err == nil || !strings.Contains(err.Error(), tc.want) ||
!strings.Contains(err.Error(), "Nothing was proposed") {
t.Fatalf("refused with %v, want %q", err, tc.want)
}
})
}
if len(r.subjects) != 0 {
t.Fatalf("a refused proposal was asked: %v", r.subjects)
}
r.kinds = []stdio.KindHeld{{Seat: asks.ChannelSeat, Kind: "desktop", Capabilities: []string{"choice"}}}
if _, err := c.Propose(Call{Action: "propose", Key: "font-size", Value: 13.0, Caller: "g", Why: "w"}); err == nil ||
!strings.Contains(err.Error(), "no channel that proves who answered") {
t.Errorf("no approving channel: %v", err)
}
r.kinds = newStandIn().kinds
r.refuse = errors.New("the bus is away")
if _, err := c.Propose(Call{Action: "propose", Key: "font-size", Value: 13.0, Caller: "g", Why: "w"}); err == nil ||
!strings.Contains(err.Error(), "could not be asked") {
t.Errorf("a failed publish: %v", err)
}
r.refuse = nil
first := propose(t, c, Call{Key: "font-size", Value: 14.0, Node: "g14"})
if _, err := c.Propose(Call{Action: "propose", Key: "font-size", Value: 14.0, Node: "g14", Caller: "g", Why: "w"}); err == nil ||
!strings.Contains(err.Error(), "proposed already, as "+first.Proposal) {
t.Errorf("the same change twice: %v", err)
}
// A newer proposal for the same place replaces the older (ADR 0277 §10), and is not counted twice.
newer := propose(t, c, Call{Key: "font-size", Value: 15.0, Node: "g14"})
if len(newer.Replaced) != 1 || newer.Replaced[0] != first.Proposal ||
storedProposal(t, m, first.Proposal).State != ProposalEnded {
t.Errorf("the newer proposal replaced %v", newer.Replaced)
}
propose(t, c, Call{Key: "font-size", Value: 15.0, Node: "novox"})
propose(t, c, Call{Key: "agent-managed-settings", Value: map[string]any{"a": 1.0}})
if _, err := c.Propose(Call{Action: "propose", Key: "width", Value: 400.0, Node: "g14", Module: "dunst",
Caller: "g", Why: "w"}); err == nil || !strings.Contains(err.Error(), "3 proposals already wait") {
t.Errorf("a fourth open ask: %v", err)
}
if keys, _ := m.Keys(ValuesState); len(keys) != 1 {
t.Errorf("proposals set %v", keys)
}
}
// **A clear is proposed and approved the same way**, and a trusted value set or cleared through the verb is refused,
// naming the proposal as the way (ADR 0277 §2).
func TestAClearIsProposedAndATrustedSetNamesTheProposal(t *testing.T) {
c, m, r := aProposingMesh(t)
_, err := c.Change(Call{Action: "set", Key: "agent-managed-settings", Value: map[string]any{}, Caller: "g14.node-tools",
Why: "w"})
if err == nil || !strings.Contains(err.Error(), "propose it instead") {
t.Errorf("a trusted set through the verb: %v", err)
}
set(t, c, Call{Key: "managed", Value: map[string]any{"a": 1.0}, Node: "g14", Module: "claude-code",
Caller: ControllerCaller, By: "jochen"})
a := propose(t, c, Call{Key: "managed", Clear: true, Node: "g14", Module: "claude-code"})
ask := r.asks[a.Proposal]
if !strings.HasPrefix(ask.Headline, "Clear managed for claude-code on g14") ||
!strings.Contains(ask.Explanation, `- managed (was: {"a":1})`) {
t.Errorf("the clear asks %q:\n%s", ask.Headline, ask.Explanation)
}
r.choose(t, a.Proposal, OptionApprove, nil)
if said, _ := c.Decide(); len(said) != 1 || !strings.Contains(said[0], "is cleared") {
t.Fatalf("the clear decided %v", said)
}
if _, isSet := storedValue(t, m, "module.g14.claude-code.managed"); isSet {
t.Error("an approved clear left the value")
}
h, _ := c.History(Call{Key: "managed"})
if last := h[len(h)-1]; last.Action != "clear" || !strings.Contains(last.Approved, "approved by the operator") {
t.Errorf("the history keeps %+v", last)
}
}
// **The proposals are listed, and one is read whole** with whether the router's ask is the one composed from it.
func TestTheProposalsAreListedAndOneIsReadWhole(t *testing.T) {
c, m, _ := aProposingMesh(t)
a := propose(t, c, Call{Key: "agent-managed-settings", Value: map[string]any{"a": 1.0}})
b := propose(t, c, Call{Key: "font-size", Value: 12.0, Node: "g14"})
rows, err := c.Do(Call{Action: "proposals"})
if err != nil {
t.Fatal(err)
}
if l := rows.([]ProposalRow); len(l) != 2 || l[0].ID != b.Proposal || l[1].State != ProposalWaiting || l[0].Value != nil {
t.Errorf("listed %+v", l)
}
one, err := c.Proposals(Call{Proposal: a.Proposal})
if err != nil || len(one) != 1 || string(one[0].Value) != `{"a":1}` || one[0].Fingerprint != a.Fingerprint ||
!strings.Contains(one[0].Phone, "exactly this proposal") {
t.Fatalf("read whole %+v (%v)", one, err)
}
p := storedProposal(t, m, a.Proposal)
p.Value, p.Digest = json.RawMessage(`{"a":2}`), valueDigest(json.RawMessage(`{"a":2}`), true)
_ = m.Put(ProposalsState, p.ID, p)
if one, _ = c.Proposals(Call{Proposal: a.Proposal}); !strings.Contains(one[0].Phone, "ANOTHER ask") {
t.Errorf("a changed proposal reads %q", one[0].Phone)
}
if _, err := c.Proposals(Call{Proposal: "cfg-none"}); err == nil {
t.Error("an unknown proposal was read")
}
}
// **The verb's new arguments are read strictly.**
func TestTheProposalArgumentsAreReadStrictly(t *testing.T) {
call, err := callOf(map[string]any{"action": "propose", "key": "k", "clear": true, "proposal": " cfg-1 "})
if err != nil || !call.Clear || call.Proposal != "cfg-1" {
t.Errorf("read %+v (%v)", call, err)
}
if _, err := callOf(map[string]any{"clear": "yes"}); err == nil {
t.Error("a clear that is not a boolean was read")
}
c, _ := aMesh(t)
if _, err := c.Do(Call{Action: "set", Key: "font-size", Value: 12.0, Clear: true, Caller: "g", Why: "w"}); err == nil {
t.Error("a set took clear")
}
if _, err := c.Do(Call{Action: "propose", Key: "font-size", Value: 12.0, Caller: "g", Why: "w"}); err == nil ||
!strings.Contains(err.Error(), "cannot be asked from here") {
t.Errorf("a proposal with no channel: %v", err)
}
}
@@ -0,0 +1,24 @@
package main
import "testing"
// A module only pulled in by another on a node, and handed to the provisioner there, has a value of its own in the
// assignments mirror (mesh-controller internal/inventory AssignmentValues, issue 765): what requires it, its plan,
// and no time it was assigned. Its values resolve there like an assigned module's, so the node's client can fill its
// templates; they go when the value goes.
func TestAModulePulledInHasItsValuesResolvedOnItsNode(t *testing.T) {
c, m := aMesh(t)
if _, err := c.Reconcile(); err != nil {
t.Fatal(err)
}
m.mirror(t, AssignmentsRead, "ace.dunst", `{"node":"ace","module":"dunst","required-by":"desk",`+
`"provisioning":{"templates":{"source":"x","digest":"y"},"files":[]}}`)
if r, err := c.Reconcile(); err != nil || len(r.Moved) != 4 {
t.Fatalf("a pulled-in module's value moved %+v (%v)", r.Moved, err)
}
expect(t, m, "ace", "10", SourceDefault)
_ = m.Delete(AssignmentsRead, "ace.dunst")
if r, _ := c.Reconcile(); len(r.Moved) != 4 || !r.Moved[0].Gone {
t.Errorf("a module no longer pulled in moved %+v", r.Moved)
}
}
@@ -0,0 +1,229 @@
package main
import (
"encoding/json"
"fmt"
"sort"
"strings"
"time"
)
// The four sources (novox/hq ADR 0348 §2, §3, §9): a module's value on a node resolves, the most specific winning,
// from the default in its manifest, mesh config for the predefined key it falls back to, node config for that key
// on the node, and the module's own value on that node. A value naming no predefined key resolves from its default
// and the module's own value only.
// The states this module keeps, and those it reads (its manifest's `state` and `reads`).
const (
ValuesState = "values"
ResolvedState = "resolved"
PredefinedState = "predefined"
ProposalsState = "proposals"
AssignmentsRead = "mesh-controller.assignments"
DeclaredConfigRead = "mesh-controller.declared-config"
PredefinedRead = "mesh-controller.predefined"
// HistoryLog is the log of every set and clear, with the proven caller and why (ADR 0348 §9).
HistoryLog = "history"
)
// The sources a resolved value says it came from.
const (
SourceDefault = "default"
SourceMesh = "mesh"
SourceNode = "node"
SourceModule = "module"
// SourceUnset is a required value nothing sets: mesh-config answers it unset (ADR 0348 §4).
SourceUnset = "unset"
)
// The levels a value is set at, and how each is keyed in mesh-config_values (ADR 0348 §9).
const (
LevelMesh = "mesh"
LevelNode = "node"
LevelModule = "module"
)
// Level is where a value is set: the mesh, a node, or a module on a node.
type Level struct {
Kind string `json:"level"`
Node string `json:"node,omitempty"`
Module string `json:"module,omitempty"`
}
// ValueKey is the level's key for a config key in mesh-config_values.
func (l Level) ValueKey(key string) string {
switch l.Kind {
case LevelNode:
return "node." + l.Node + "." + key
case LevelModule:
return "module." + l.Node + "." + l.Module + "." + key
}
return "mesh." + key
}
func (l Level) String() string {
switch l.Kind {
case LevelNode:
return "node config on " + l.Node
case LevelModule:
return l.Module + "'s own value on " + l.Node
}
return "mesh config"
}
// parseValueKey is the level and the config key a key of mesh-config_values names.
func parseValueKey(k string) (Level, string, bool) {
parts := strings.SplitN(k, ".", 4)
switch {
case len(parts) >= 2 && parts[0] == LevelMesh:
return Level{Kind: LevelMesh}, strings.Join(parts[1:], "."), true
case len(parts) >= 3 && parts[0] == LevelNode:
return Level{Kind: LevelNode, Node: parts[1]}, strings.Join(parts[2:], "."), true
case len(parts) == 4 && parts[0] == LevelModule:
return Level{Kind: LevelModule, Node: parts[1], Module: parts[2]}, parts[3], true
}
return Level{}, "", false
}
// Value is one key of mesh-config_values: the value set, who set it and why, and when.
type Value struct {
Value json.RawMessage `json:"value"`
// By is who set it, as proven: the caller, or for a value given at the controller's terminal, who gave it there.
By string `json:"by"`
// Via is the bus account the call came from.
Via string `json:"via"`
Why string `json:"why"`
// Approved is who approved it, for a value set on the operator's warrant (ADR 0348 §8, ADR 0277 §15): told from
// one given at the terminal, which leaves it empty.
Approved string `json:"approved,omitempty"`
At time.Time `json:"at"`
}
// Resolved is one key of mesh-config_resolved, `<node>.<module>.<key>`: the value a module on a node takes for a
// key, and where it came from. Built from the sources alone, so the same sources give the same bytes and a pass
// over unchanged sources writes nothing.
type Resolved struct {
Node string `json:"node"`
Module string `json:"module"`
Key string `json:"key"`
Value json.RawMessage `json:"value,omitempty"`
Source string `json:"source"`
// Predefined is the predefined key the value falls back to, when it names one.
Predefined string `json:"predefined,omitempty"`
// Required says an unset value is the operator's own to set.
Required bool `json:"required,omitempty"`
Trusted bool `json:"trusted,omitempty"`
}
// ResolvedKey is a resolved value's key in mesh-config_resolved.
func ResolvedKey(node, module, key string) string { return node + "." + module + "." + key }
// Sources is everything a resolution reads: the assignments, every module's declarations, the published list of
// predefined keys, and every value set.
type Sources struct {
Assignments []Assignment
Declared map[string]ModuleDeclarations
Predefined map[string]Published
Values map[string]Value
}
// Resolve is every assigned module's every declared value on its node, keyed as mesh-config_resolved is.
func Resolve(s Sources) map[string]Resolved {
out := map[string]Resolved{}
for _, a := range s.Assignments {
decl, ok := s.Declared[a.Module]
if !ok {
continue
}
for _, key := range sortedKeys(decl.Config) {
r := resolveOne(s, a.Node, a.Module, key, decl.Config[key])
out[ResolvedKey(a.Node, a.Module, key)] = r
}
}
return out
}
func resolveOne(s Sources, node, module, key string, d Declaration) Resolved {
r := Resolved{Node: node, Module: module, Key: key, Source: SourceUnset, Predefined: d.Predefined,
Required: d.Required, Trusted: d.Trusted}
if d.HasDefault() {
r.Value, r.Source = compact(d.Default), SourceDefault
}
if d.Predefined != "" {
if p, ok := s.Predefined[d.Predefined]; ok && p.Trusted {
r.Trusted = true
}
if v, ok := s.Values[Level{Kind: LevelMesh}.ValueKey(d.Predefined)]; ok {
r.Value, r.Source = compact(v.Value), SourceMesh
}
if v, ok := s.Values[Level{Kind: LevelNode, Node: node}.ValueKey(d.Predefined)]; ok {
r.Value, r.Source = compact(v.Value), SourceNode
}
}
if v, ok := s.Values[Level{Kind: LevelModule, Node: node, Module: module}.ValueKey(key)]; ok {
r.Value, r.Source = compact(v.Value), SourceModule
}
return r
}
// compact is a JSON value encoded the one way: objects' keys sorted, no spaces.
func compact(raw json.RawMessage) json.RawMessage {
var v any
if err := json.Unmarshal(raw, &v); err != nil {
return raw
}
out, err := json.Marshal(v)
if err != nil {
return raw
}
return out
}
// Moved is one resolved value that changed: a module on a node whose value for a key moved, from what to what.
type Moved struct {
Node string `json:"node"`
Module string `json:"module"`
Key string `json:"key"`
From json.RawMessage `json:"from,omitempty"`
To json.RawMessage `json:"to,omitempty"`
Source string `json:"source"`
// Gone says the module no longer takes the key there: unassigned, or no longer declared.
Gone bool `json:"gone,omitempty"`
}
// diff is what moved between the resolved values held and those wanted, in key order.
func diff(held, want map[string]Resolved) []Moved {
var out []Moved
for _, k := range sortedKeys(want) {
w := want[k]
h, had := held[k]
if had && string(encodeResolved(h)) == string(encodeResolved(w)) {
continue
}
m := Moved{Node: w.Node, Module: w.Module, Key: w.Key, To: w.Value, Source: w.Source}
if had {
m.From = h.Value
}
out = append(out, m)
}
for _, k := range sortedKeys(held) {
if _, ok := want[k]; !ok {
h := held[k]
out = append(out, Moved{Node: h.Node, Module: h.Module, Key: h.Key, From: h.Value, Gone: true})
}
}
sort.SliceStable(out, func(i, j int) bool {
return ResolvedKey(out[i].Node, out[i].Module, out[i].Key) < ResolvedKey(out[j].Node, out[j].Module, out[j].Key)
})
return out
}
func encodeResolved(r Resolved) []byte {
raw, err := json.Marshal(r)
if err != nil {
panic(fmt.Sprintf("a resolved value cannot be encoded: %v", err))
}
return raw
}
@@ -0,0 +1,252 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"strings"
)
// Reading the sources, writing the resolved values, and publishing the list of predefined keys.
// sources reads everything a resolution reads, from the bus as it is now.
func (c *Config) sources() (Sources, error) {
s := Sources{Declared: map[string]ModuleDeclarations{}, Predefined: map[string]Published{},
Values: map[string]Value{}}
var problems []string
if err := c.each(AssignmentsRead, func(key string, raw []byte) error {
var a Assignment
if err := json.Unmarshal(raw, &a); err != nil || a.Node == "" || a.Module == "" {
return fmt.Errorf("the assignment %s is not one: %s", key, raw)
}
s.Assignments = append(s.Assignments, a)
return nil
}); err != nil {
return s, fmt.Errorf("the assignments cannot be read: %w", err)
}
if err := c.each(DeclaredConfigRead, func(key string, raw []byte) error {
m, err := ParseModuleDeclarations(raw)
if err != nil {
// One module's declarations unread leaves that module unresolved, said; the rest resolve.
problems = append(problems, fmt.Sprintf("%s: %v", key, err))
return nil
}
s.Declared[m.Module] = m
return nil
}); err != nil {
return s, fmt.Errorf("the modules' declarations cannot be read: %w", err)
}
if err := c.each(PredefinedState, func(key string, raw []byte) error {
var p Published
if err := json.Unmarshal(raw, &p); err != nil {
return fmt.Errorf("the published predefined key %s is not one: %w", key, err)
}
s.Predefined[key] = p
return nil
}); err != nil {
return s, fmt.Errorf("the published list of predefined keys cannot be read: %w", err)
}
if err := c.each(ValuesState, func(key string, raw []byte) error {
var v Value
if err := json.Unmarshal(raw, &v); err != nil {
return fmt.Errorf("the value %s is not one: %w", key, err)
}
s.Values[key] = v
return nil
}); err != nil {
return s, fmt.Errorf("the values cannot be read: %w", err)
}
for _, p := range problems {
c.logf("[mesh-config] a module's declarations are not read, so its values are not resolved: %s", p)
}
return s, nil
}
// each hands every key of a state to visit.
func (c *Config) each(state string, visit func(key string, raw []byte) error) error {
keys, err := c.Store.Keys(state)
if err != nil {
return err
}
for _, k := range keys {
e, err := c.Store.Get(state, k)
if err != nil {
return err
}
if e == nil {
continue // removed between the listing and the read
}
if err := visit(k, e.Value); err != nil {
return err
}
}
return nil
}
// held is what mesh-config_resolved holds now.
func (c *Config) held() (map[string]Resolved, error) {
out := map[string]Resolved{}
err := c.each(ResolvedState, func(key string, raw []byte) error {
var r Resolved
if err := json.Unmarshal(raw, &r); err != nil {
// A value nobody can read is written again from the sources.
out[key] = Resolved{Key: key, Source: "unreadable"}
return nil
}
out[key] = r
return nil
})
return out, err
}
// writeResolved makes mesh-config_resolved hold what the sources resolve to, writing only what moved, and
// answers what moved.
func (c *Config) writeResolved(s Sources) ([]Moved, error) {
held, err := c.held()
if err != nil {
return nil, fmt.Errorf("the resolved values cannot be read: %w", err)
}
want := Resolve(s)
moved := diff(held, want)
var problems []error
for _, m := range moved {
key := ResolvedKey(m.Node, m.Module, m.Key)
if m.Gone {
if err := c.Store.Delete(ResolvedState, key); err != nil {
problems = append(problems, fmt.Errorf("removing %s: %w", key, err))
}
continue
}
if err := c.Store.Put(ResolvedState, key, want[key]); err != nil {
problems = append(problems, fmt.Errorf("writing %s: %w", key, err))
}
}
// A key that is no resolved value at all (written by an older mesh-config, or unreadable) is removed.
for key, h := range held {
if _, ok := want[key]; !ok && h.Source == "unreadable" {
if err := c.Store.Delete(ResolvedState, key); err != nil {
problems = append(problems, fmt.Errorf("removing %s: %w", key, err))
}
}
}
return moved, errors.Join(problems...)
}
// Resolution is what one pass of Reconcile did.
type Resolution struct {
Moved []Moved `json:"moved"`
Published string `json:"published,omitempty"`
Withheld string `json:"withheld,omitempty"`
}
// Reconcile publishes the newest list of predefined keys and brings mesh-config_resolved to what the sources
// resolve to: run at start, and whenever an assignment, a declaration or the list moves. A move it makes raises
// config-changed like a set, with no level, naming what moved.
func (c *Config) Reconcile() (Resolution, error) {
c.mu.Lock()
defer c.mu.Unlock()
var r Resolution
published, err := c.publish()
if err != nil {
c.logf("[mesh-config] the list of predefined keys is not published: %v", err)
}
r.Published, r.Withheld = published, c.withheld
s, err := c.sources()
if err != nil {
return r, err
}
moved, err := c.writeResolved(s)
r.Moved = moved
if len(moved) > 0 {
c.record("resolved", Change{Action: "resolve", Why: "an assignment, a declaration or the list of predefined " +
"keys changed", At: c.now(), Moved: moved})
}
return r, err
}
// publish makes mesh-config_predefined hold the list the controller kept, when it is newer than the one published
// (ADR 0348 §7: the newest trunk build wins). **A list dropping a key that still holds a value is withheld**: the
// list published stays, and why is said in every listing and in the log, because a value for a key no longer on
// the list could neither be resolved nor cleared by name (§7: removing a key still holding a value is refused).
// It answers what it published, empty when nothing changed.
func (c *Config) publish() (string, error) {
e, err := c.Store.Get(PredefinedRead, PredefinedMirrorKey)
if err != nil {
return "", fmt.Errorf("the controller's list cannot be read: %w", err)
}
if e == nil {
return "", nil // no list kept yet
}
var list PredefinedList
if err := json.Unmarshal(e.Value, &list); err != nil || list.Keys == nil {
return "", fmt.Errorf("the controller's list is not one: %s", e.Value)
}
current := map[string]Published{}
if err := c.each(PredefinedState, func(key string, raw []byte) error {
var p Published
if err := json.Unmarshal(raw, &p); err != nil {
return err
}
current[key] = p
return nil
}); err != nil {
return "", err
}
generation := 0
for _, p := range current {
generation = max(generation, p.Generation)
}
if len(current) > 0 && generation >= list.Generation {
c.withheld = ""
return "", nil
}
held := map[string][]string{}
if err := c.each(ValuesState, func(key string, _ []byte) error {
level, k, ok := parseValueKey(key)
if ok && level.Kind != LevelModule {
if _, kept := list.Keys[k]; !kept {
held[k] = append(held[k], level.String())
}
}
return nil
}); err != nil {
return "", err
}
if len(held) > 0 {
var why []string
for _, k := range sortedKeys(held) {
why = append(why, fmt.Sprintf("%s (still set as %s)", k, strings.Join(held[k], ", ")))
}
c.withheld = fmt.Sprintf("the list at %s (generation %d) is not published: it drops %s; clear those values "+
"first, or keep the key (novox/hq ADR 0348 §7)", short(list.Commit), list.Generation, strings.Join(why, "; "))
c.logf("[mesh-config] %s", c.withheld)
return "", nil
}
c.withheld = ""
var problems []error
for _, k := range sortedKeys(list.Keys) {
p := Published{PredefinedKey: list.Keys[k], Generation: list.Generation, Commit: list.Commit}
if err := c.Store.Put(PredefinedState, k, p); err != nil {
problems = append(problems, err)
}
}
for k := range current {
if _, kept := list.Keys[k]; !kept {
if err := c.Store.Delete(PredefinedState, k); err != nil {
problems = append(problems, err)
}
}
}
if err := errors.Join(problems...); err != nil {
return "", err
}
return fmt.Sprintf("the list at %s (generation %d): %s", short(list.Commit), list.Generation,
joinKeys(list.Keys)), nil
}
func short(commit string) string {
if len(commit) > 8 {
return commit[:8]
}
return commit
}
+5
View File
@@ -0,0 +1,5 @@
module mesh-config
go 1.25.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013235212-c8b07242a009
+2
View File
@@ -0,0 +1,2 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013235212-c8b07242a009 h1:I9rm8HhYRJ0+mzQQz+VAH0kgv2YJWkvctEc67Yd0PX8=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013235212-c8b07242a009/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
+73
View File
@@ -0,0 +1,73 @@
{
"module": "mesh-config",
"version": "1",
"runs-as": "mesh-config",
"claims": [
{
"name": "mesh-config",
"scope": "mesh",
"serves": [
"config"
]
}
],
"emits": [
"config-changed"
],
"uses": [
"operator-channel"
],
"invokes": [
"seat:mesh-controller.hand-act",
"seat:mesh-controller.warranted"
],
"reads": [
"mesh-controller.assignments",
"mesh-controller.declared-config",
"mesh-controller.predefined"
],
"state": [
"values",
"resolved",
"predefined",
"proposals"
],
"logs": [
"history"
],
"own-secrets": {
"broker": "${dir:state}/broker"
},
"secrets-owner": "mesh-config",
"resources": [
{
"id": "account",
"type": "user",
"name": "mesh-config",
"shell": "/usr/bin/nologin",
"home": "/var/lib/mesh-config"
},
{
"id": "state",
"type": "directory",
"mode": "0700",
"owner": "mesh-config",
"place": "."
}
],
"build": {
"artifacts": [
{
"name": "tools",
"kind": "bundle",
"language": "go",
"system": "arch",
"from": "cmd/mesh-config",
"binary": "mesh-config",
"loads": [
"mesh-config"
]
}
]
}
}