mesh-config keeps held values in the seat's typed state kept, through the config client's key and codec (B5g, B2f; ADR 0565 §5, ADR 0634 §4; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 16:08:11 +01:00
parent d8b4bb5487
commit 1e974ce9e8
4 changed files with 37 additions and 37 deletions
@@ -580,7 +580,7 @@ func (c *Config) propose(call Call, action string) (Proposal, oc.Ask, []string,
}
trusted = false
k, kept := s.Kept[keptKey(call.Pull, level.Node, level.Module, call.Key)]
was, had = Value{Value: k.Value}, kept
was, had = Value{Value: heldValue(k)}, kept
} else {
var derr error
r, trusted, derr = c.declared(s, level, call.Key)
+28 -34
View File
@@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"git.novox.be/novox/mesh-contracts/go/clients/meshconfigclient"
"regexp"
"strconv"
"strings"
@@ -61,32 +62,25 @@ type PullDecl struct {
At time.Time `json:"at"`
}
// KeptValue is an approved held value, kept apart from the values mesh-config resolves, in this module's own layout
// (the owner's: its state `kept`), which the checker and the assigner's composition check read (novox/hq ADR 0565 §5).
type KeptValue struct {
Pull string `json:"pull"`
Head string `json:"head"`
Digest string `json:"declaration"`
Node string `json:"node"`
Module string `json:"module"`
Key string `json:"key"`
// Value is the value held, as the operator approved it.
Value json.RawMessage `json:"value"`
By string `json:"by"`
Via string `json:"via"`
Why string `json:"why"`
// Approved is who approved it, as the proposal kept it.
Approved string `json:"approved"`
Proposal string `json:"proposal"`
At time.Time `json:"at"`
}
// KeptValue is an approved held value, kept apart from the values mesh-config resolves, in the layout of the seat's state
// `kept`, which the checker and the assigner's composition check read (novox/hq ADR 0565 §5, ADR 0634 §4).
type KeptValue = meshconfigclient.Kept
// keptKey is a held value's key in `kept`: the pull request's key form (pullKey), the node, the module and the config
// key, joined by dots.
func keptKey(pull, node, module, key string) string {
return pullKey(pull) + "." + node + "." + module + "." + key
k, _ := meshconfigclient.HeldValueKey(pull, node, module, key) // a held value of no pull request has no key, and is found by none
return k
}
// heldValue is the value held, as the operator approved it, in its JSON form.
func heldValue(k KeptValue) json.RawMessage {
raw, _ := meshconfigclient.ValueOf(k) // a value that was read from JSON writes as JSON
return raw
}
func heldAt(t time.Time) *time.Time { return &t }
func keptStateKey(k KeptValue) string { return keptKey(k.Pull, k.Node, k.Module, k.Key) }
func keptLevel(k KeptValue) Level { return Level{Kind: LevelModule, Node: k.Node, Module: k.Module} }
@@ -123,9 +117,9 @@ func (c *Config) readPulls(s *Sources) error {
return fmt.Errorf("the pull requests' declarations cannot be read: %w", err)
}
if err := c.each(KeptState, func(key string, raw []byte) error {
var k KeptValue
if err := json.Unmarshal(raw, &k); err != nil || k.Pull == "" {
return nil
k, err := meshconfigclient.DecodeKept(key, raw)
if err != nil {
return nil // not a held value of the seat's layout under its own key
}
s.Kept[key] = k
return nil
@@ -176,11 +170,11 @@ func (c *Config) declarePull(pull, head string, declares map[string]map[string]D
switch {
case !declared:
why = "the head no longer adds or changes its declaration"
case declDigest(d) != kv.Digest:
case declDigest(d) != kv.Declaration:
why = "the head declares the key otherwise than the value was approved against"
default:
var v any
_ = json.Unmarshal(kv.Value, &v)
_ = json.Unmarshal(heldValue(kv), &v)
if p := d.rule().check(v); len(p) > 0 {
why = "the value no longer passes: " + strings.Join(p, "; ")
}
@@ -208,13 +202,13 @@ func (c *Config) declarePull(pull, head string, declares map[string]map[string]D
// config-changed; what could not be done is answered, not dropped.
func (c *Config) heldChange(kv KeptValue, action, why, approved string) []string {
level := keptLevel(kv)
ch := Change{Action: action, Level: &level, Key: kv.Key, Value: kv.Value, By: kv.By, Via: kv.Via, Why: why,
ch := Change{Action: action, Level: &level, Key: kv.Key, Value: heldValue(kv), By: kv.By, Via: kv.Via, Why: why,
Approved: approved, Pull: kv.Pull, Head: kv.Head, At: c.now(), Moved: []Moved{}}
unsaid := c.record(level.ValueKey(kv.Key), ch)
cause := "a value held for " + kv.Pull
what := fmt.Sprintf("config %s %s of %s for %s, held for %s", action, kv.Key, kv.Module, kv.Node, kv.Pull)
if action != ActionDrop {
what += " (" + string(kv.Value) + ")"
what += " (" + string(heldValue(kv)) + ")"
}
if err := c.Store.HandAct(what, why, cause); err != nil {
unsaid = append(unsaid, "the hand-act log does not hold it: "+err.Error())
@@ -260,8 +254,8 @@ type HeldSkipped struct {
}
func heldValueOf(k KeptValue) HeldValue {
return HeldValue{Pull: k.Pull, Head: k.Head, Node: k.Node, Module: k.Module, Key: k.Key, Value: compact(k.Value),
Approved: k.Approved, By: k.By, Why: k.Why, At: k.At}
return HeldValue{Pull: k.Pull, Head: k.Head, Node: k.Node, Module: k.Module, Key: k.Key, Value: compact(heldValue(k)),
Approved: k.Approved, By: k.By, Why: k.Why, At: timeOf(k.At)}
}
// Held answers the approved held values for the pull requests asked, at the heads asked, to the controller alone: a
@@ -373,13 +367,13 @@ func (c *Config) keep(p Proposal) (string, error) {
"propose it again", p.Pull, p.Key)
}
was, had := s.Kept[keptKey(p.Pull, p.Level.Node, p.Level.Module, p.Key)]
if had != p.HadValue || valueDigest(was.Value, had) != p.WasDigest {
if had != p.HadValue || valueDigest(heldValue(was), had) != p.WasDigest {
return "", fmt.Errorf("the value held for %s changed since the operator was shown the change: propose it "+
"again", p.Pull)
}
kv := KeptValue{Pull: p.Pull, Head: pd.Head, Digest: p.DeclDigest, Node: p.Level.Node, Module: p.Level.Module,
kv := KeptValue{Pull: p.Pull, Head: pd.Head, Declaration: p.DeclDigest, Node: p.Level.Node, Module: p.Level.Module,
Key: p.Key, Value: p.Value, By: "the operator", Via: p.From, Why: p.Why, Approved: p.Approved, Proposal: p.ID,
At: c.now()}
At: heldAt(c.now())}
if err := c.Store.Put(KeptState, keptStateKey(kv), kv); err != nil {
return "", fmt.Errorf("the value was not kept: %v", err)
}
@@ -434,7 +428,7 @@ func (c *Config) settlePull(pull string, merged bool, head string) []string {
why = "the merged head is not known, so the value cannot be held to it"
case !strings.EqualFold(head, kv.Head):
why = fmt.Sprintf("it is held at head %s and the pull request merged at %s", short(kv.Head), short(head))
case !known || !declared || declDigest(d) != kv.Digest:
case !known || !declared || declDigest(d) != kv.Declaration:
why = "the merged head does not declare the key as the operator was shown"
}
if why != "" {
@@ -444,7 +438,7 @@ func (c *Config) settlePull(pull string, merged bool, head string) []string {
continue
}
vk := keptLevel(kv).ValueKey(kv.Key)
v := Value{Value: compact(kv.Value), By: kv.By, Via: kv.Via, Why: kv.Why + " (held for " + kv.Pull +
v := Value{Value: compact(heldValue(kv)), By: kv.By, Via: kv.Via, Why: kv.Why + " (held for " + kv.Pull +
", merged)", Approved: kv.Approved, At: c.now()}
if err := c.Store.Put(ValuesState, vk, v); err != nil {
said = append(said, fmt.Sprintf("%s of %s on %s held for %s is not written yet: %v; it is tried "+
@@ -3,6 +3,7 @@ package main
import (
"encoding/json"
"errors"
"git.novox.be/novox/mesh-contracts/go/clients/meshconfigclient"
"strings"
"testing"
"time"
@@ -317,8 +318,11 @@ func TestTheMergeAppliesAHeldValueAndItResolvesOnceTheTrunkDeclaresTheKey(t *tes
holdIt(t, c, m, r)
approved := func() string {
raw, _ := m.Get(KeptState, "novox.mesh-catalog.434.ace.n8n.media-gid")
var k KeptValue
_ = json.Unmarshal(raw.Value, &k)
// the state kept is the seat's layout, under the seat's key: the checker and the assigner read it with this
k, err := meshconfigclient.DecodeKept("novox.mesh-catalog.434.ace.n8n.media-gid", raw.Value)
if err != nil || string(heldValue(k)) != "2000" || k.Declaration == "" || k.At == nil {
t.Fatalf("the held value is not the seat's: %+v, %v", k, err)
}
return k.Approved
}()
m.events, m.handActs = nil, nil
+2
View File
@@ -3,6 +3,7 @@ module mesh-config
go 1.26.0
require (
git.novox.be/novox/mesh-contracts/go/clients/meshconfigclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000
@@ -21,6 +22,7 @@ require (
)
require (
git.novox.be/novox/mesh-contracts/go/clients/meshconfigclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op // indirect
github.com/google/go-tpm v0.9.8 // indirect