mesh-config and mesh-conditions without the controller's lease
mesh/delivery merged: source.main moved to novox/mesh-catalog@d2de18fd6bdd
mesh/check checking 326897d8772f on d2de18fd6bdd
mesh/delivery merged: source.main moved to novox/mesh-catalog@d2de18fd6bdd
mesh/check checking 326897d8772f on d2de18fd6bdd
The controller ended by its unassignment, so its lease is never handed and both stayed in their controller-era branch (issue 1126, A5). mesh-config no longer trusts the controller's relay: a trusted value needs the operator's Approve. The condition keeper carries nothing from the controller, clears once at start what it carried, and refuses take-over as the one source. Both drop their manifest read of mesh-controller.lease. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -3,8 +3,8 @@ package main
|
||||
// **Built dark, compared, then switched by the operator** (novox/hq ADR 0564 clause 9; issue 1952 C5, C8): `compare`
|
||||
// lists the open keys of the keeper and of the controller, each difference with its reason; the kinds that end with the
|
||||
// controller, the kinds only the keeper raises and the keys held while their module is delivered are listed apart and
|
||||
// not counted. `take-over` raises an approve ask under the keeper's own name (ADR 0259), refused while compare shows a
|
||||
// difference, and on the warrant writes the take-over key naming the keeper. `hand-back` is gone (issue 774): nothing
|
||||
// not counted. `take-over` is refused (issue 1126): the keeper is the one source since the controller ended by its
|
||||
// unassignment; a yes to an ask raised before still writes the take-over key naming the keeper. `hand-back` is gone (issue 774): nothing
|
||||
// reads the take-over key any more, so it is answered as gone and changes nothing.
|
||||
|
||||
import (
|
||||
@@ -91,7 +91,7 @@ func (k *Keeper) Compare() (Comparison, error) {
|
||||
}
|
||||
switch {
|
||||
case homed && (home.Carried || home.Replaced):
|
||||
why := "ends with the controller: carried from its bucket until the lease write"
|
||||
why := "ends with the controller: no longer carried (issue 1126)"
|
||||
if home.Replaced {
|
||||
why = "ends with the controller: its fault is observed otherwise"
|
||||
}
|
||||
@@ -125,41 +125,12 @@ type Asks interface {
|
||||
Ask(a asks.Ask) error
|
||||
}
|
||||
|
||||
// TakeOver raises the approve ask of a switch to the keeper, refused while compare differs or after the lease write.
|
||||
// TakeOver is refused: the keeper is the one source of conditions since the controller ended (issue 1126).
|
||||
func (k *Keeper) TakeOver(a Asks, by, why string) (map[string]any, error) {
|
||||
if strings.TrimSpace(why) == "" {
|
||||
return nil, errors.New("a take-over says why")
|
||||
}
|
||||
if k.LeaseAway() {
|
||||
return nil, errors.New("the controller's lease is handed away: the keeper is the one source already")
|
||||
}
|
||||
cmp, err := k.Compare()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !cmp.Empty() {
|
||||
return nil, fmt.Errorf("refused: compare shows %d difference(s), the first %s (%s); the take-over waits for none",
|
||||
len(cmp.Differences), cmp.Differences[0].Key, cmp.Differences[0].Reason)
|
||||
}
|
||||
now := k.Now().UTC()
|
||||
id := fmt.Sprintf("take-over-%d", now.UnixNano())
|
||||
act := asks.Act{"verb": Seat + ".take-over", "module": Module, "ask": id}
|
||||
bind, err := asks.ActDigest(act)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ask := asks.Ask{ID: id, Headline: "Switch the mesh's alarms to the new keeper?", Who: asks.Operator,
|
||||
Explanation: fmt.Sprintf("The new condition keeper holds the same %d open alarms as the controller. Your yes makes "+
|
||||
"the alarm messages come from the keeper, with the same words. It can be handed back until the controller "+
|
||||
"gives up its lease.", cmp.Same),
|
||||
Options: []asks.Option{{ID: "yes", Label: "Yes, switch", Does: "Alarms come from the keeper.", Level: asks.Approve, Binds: bind},
|
||||
{ID: "no", Label: "No, leave it", Does: "Alarms keep coming from the controller.", Level: asks.Acknowledge}},
|
||||
Expires: now.Add(asks.ApproveLasts - time.Minute), OnExpiry: "Alarms keep coming from the controller.",
|
||||
About: Module + "-take-over", Details: "Asked by " + by + ": " + why}
|
||||
if err := a.Ask(ask); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{"asked": id, "same": cmp.Same, "apart": len(cmp.Apart)}, nil
|
||||
return nil, errors.New("the keeper is the one source already: the controller ended by its unassignment (issue 1126)")
|
||||
}
|
||||
|
||||
// Warranted is the act on the operator's answer: a yes, compare still empty, writes the take-over key naming the keeper.
|
||||
|
||||
@@ -3,7 +3,7 @@ package main
|
||||
// **A module being delivered raises no condition until its delivery ends** (novox/hq ADR 0564 clause 13 and Limits 12;
|
||||
// issue 1952 C7, C8). The keeper reads the assigner's wants and the nodes' haves, and judges a module's delivery on a
|
||||
// node with mesh-sdk's delivery.MotionOf, as the view does. While it is delivered there, a finding about it there, or
|
||||
// about what it serves, and a condition carried from the controller's bucket on those subjects, is kept in the state
|
||||
// about what it serves, is kept in the state
|
||||
// `held` and raises nothing. When the delivery ends, healthy at the want or failed, what still stands is raised once,
|
||||
// saying it happened during the delivery; what went is dropped with a log line. The hold lasts at most the delivery's
|
||||
// bound, a key of the state `bounds` whose time to live is the bound: its expiry raises what is held, saying the delivery
|
||||
@@ -98,17 +98,11 @@ func (k *Keeper) motion(node, module string) (delivery.Motion, delivery.Want) {
|
||||
}
|
||||
|
||||
// holdFor says whether key is held now, and as what. Called with mu held.
|
||||
func (k *Keeper) holdFor(key string, f *health.Finding, carried *health.Condition, open bool) (held, bool) {
|
||||
if open || (f == nil && carried == nil) {
|
||||
func (k *Keeper) holdFor(key string, f *health.Finding, open bool) (held, bool) {
|
||||
if open || f == nil {
|
||||
return held{}, false
|
||||
}
|
||||
var kind, source string
|
||||
var subject health.Subject
|
||||
if f != nil {
|
||||
kind, source, subject = f.Kind, f.Source, f.Subject
|
||||
} else {
|
||||
kind, source, subject = carried.Kind, carried.Source, carried.Subject
|
||||
}
|
||||
kind, source, subject := f.Kind, f.Source, f.Subject
|
||||
if _, raisedOut := k.outOfHold[key]; raisedOut {
|
||||
return held{}, false
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ const (
|
||||
)
|
||||
|
||||
// ADR 0564 clause 13 and Limits 12 (issue 1952 C7, C8): a delivery of mesh-controller on novox replayed want by have.
|
||||
// Its own findings and the carried controller-deaf raise nothing during it and none after when they went; one still
|
||||
// Its own findings raise nothing during it and none after when they went; one still
|
||||
// standing at the end is raised once with its during-the-delivery line; one past the bound with its did-not-end line; a
|
||||
// failed delivery raises what stands at once; another module's and the machine's own are raised at once; compare lists a
|
||||
// held key as held.
|
||||
@@ -34,8 +34,6 @@ func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) {
|
||||
raw, _ := json.Marshal(delivery.Have{V: delivery.Version, FP: fp, Composed: composed, Phase: phase, Health: h})
|
||||
return raw
|
||||
}
|
||||
deaf, _ := json.Marshal(health.Condition{Key: "core.controller.deaf", Kind: "controller-deaf", Source: "S4",
|
||||
Subject: health.Subject{Scope: health.ScopeCore, ID: "controller"}, Severity: health.Urgent, Summary: "deaf", Resolver: "self"})
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
// end is the have the delivery ends with, or nil for a delivery past its bound.
|
||||
@@ -68,9 +66,6 @@ func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := r.k.Controller("core.controller.deaf", deaf, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The delivery's bound is its own key's time to live, the module's bound (the default where it names none).
|
||||
if r.st.ttls["bounds/novox.mesh-controller"] != delivery.DefaultDeliveryBound {
|
||||
t.Errorf("%s: the bound key lives %s", c.name, r.st.ttls["bounds/novox.mesh-controller"])
|
||||
@@ -86,11 +81,10 @@ func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) {
|
||||
held++
|
||||
}
|
||||
}
|
||||
if held != 2 {
|
||||
if held != 1 {
|
||||
t.Errorf("%s: compare lists %d held, %+v", c.name, held, cmp)
|
||||
}
|
||||
// The carried condition about the controller goes within the delivery; its own finding goes unless it stands.
|
||||
_ = r.k.Controller("core.controller.deaf", nil, true)
|
||||
// Its own finding goes unless it stands.
|
||||
if !c.standing {
|
||||
_ = r.k.Finding(watchman, own, nil, true)
|
||||
}
|
||||
@@ -106,8 +100,8 @@ func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) {
|
||||
if co, open := r.open(own); c.standing != open || (open && !strings.Contains(co.Explanation, c.line)) {
|
||||
t.Errorf("%s: its own condition is %v: %q", c.name, open, co.Explanation)
|
||||
}
|
||||
if _, open := r.open("core.controller.deaf"); open || len(r.k.Held()) != 0 {
|
||||
t.Errorf("%s: the carried deaf condition was raised, or something is still held: %+v", c.name, r.k.Held())
|
||||
if len(r.k.Held()) != 0 {
|
||||
t.Errorf("%s: something is still held: %+v", c.name, r.k.Held())
|
||||
}
|
||||
}
|
||||
// A seat's finding is about the module the placement says holds the seat on that node (the lead, 2026-10-29): held
|
||||
|
||||
@@ -2,8 +2,8 @@ package main
|
||||
|
||||
// **Where each condition kind lives after the take-over** (novox/hq ADR 0564 clauses 4, 6, 7 and 9, the Context's probe
|
||||
// table; issue 1952 C3, C5): data, so a kind's home is a row and never code. `compare` reads it to tell a difference from
|
||||
// a kind that ends with the controller or that the controller never raises, and the keeper reads it to carry, until the
|
||||
// lease write, the controller's conditions of the kinds that end with it.
|
||||
// a kind that ends with the controller or that the controller never raises, and the keeper reads it to clear, once, what
|
||||
// it carried of the kinds that ended with the controller (EndCarried, issue 1126).
|
||||
//
|
||||
// The rows are ADR 0564's and the lead's table of 2026-10-29, by the controller's source of each kind. A kind with no
|
||||
// row is named by `compare` as "no home decided".
|
||||
@@ -17,8 +17,8 @@ type Home struct {
|
||||
Controller bool
|
||||
// Observer is the module whose findings raise it after the take-over; empty for a kind that ends.
|
||||
Observer string
|
||||
// Carried says it ends with the controller's lease and is carried from the controller's bucket until then
|
||||
// (clause 9: S4, S7, S12, S13, S14, S25, D5, D9, H-controller).
|
||||
// Carried says it ended with the controller: carried from its bucket while it ran (clause 9: S4, S7, S12, S13, S14,
|
||||
// S25, D5, D9, H-controller), cleared once by EndCarried and never carried again (issue 1126).
|
||||
Carried bool
|
||||
// Replaced says the kind itself ends and another observes its fault (D1, D10, the stuck probe): listed apart in
|
||||
// compare and not carried.
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package main
|
||||
|
||||
// The condition keeper (novox/hq ADR 0564 clauses 2, 3, 7, 8, 9 and 13; issue 1952 C2, C3, C7): the one writer of the
|
||||
// mesh's open conditions. It watches every observer's `findings`, the healers' `attempts`, the controller's lease
|
||||
// alone (its `conditions` are no longer read, issue 774), and the assigner's wants and the nodes' haves for the hold. From
|
||||
// all of them it derives, per key, the condition that should be open, and writes the difference by compare-and-set,
|
||||
// mesh's open conditions. It watches every observer's `findings`, the healers' `attempts`, and the assigner's wants and
|
||||
// the nodes' haves for the hold; nothing of the controller (its `conditions` are no longer read, issue 774, and its lease
|
||||
// turns nothing, issue 1126: the controller ended by its unassignment). From all of them it derives, per key, the
|
||||
// condition that should be open, and writes the difference by compare-and-set,
|
||||
// saying each transition as the controller says it (go/health) and keeping it in `history`. Nothing clears a condition
|
||||
// but its finding going (ADR 0231 §14).
|
||||
|
||||
@@ -46,10 +47,8 @@ var Observers = []string{health.WatchmanModule, health.SelfCheckModule, health.H
|
||||
|
||||
// The other states it reads, as `<module>.<name>`.
|
||||
const (
|
||||
AttemptsRead = health.HealersModule + "." + health.AttemptsState
|
||||
ControllerLease = "mesh-controller.lease"
|
||||
ControllerLeaseKey = "holder"
|
||||
WantsRead = delivery.AssignerModule + ".wants"
|
||||
AttemptsRead = health.HealersModule + "." + health.AttemptsState
|
||||
WantsRead = delivery.AssignerModule + ".wants"
|
||||
// PlacementRead is the mesh-placement seat's placement, read for which module holds a seat on a node (hold.go; the
|
||||
// lead, 2026-10-29, for ADR 0564 clause 2's inputs).
|
||||
PlacementRead = delivery.PlacementRead
|
||||
@@ -68,7 +67,7 @@ type Store interface {
|
||||
CreateTTL(state, key string, value any, ttl time.Duration) error
|
||||
}
|
||||
|
||||
// held is a finding or a carried condition kept while its module is delivered (clause 13 (b)).
|
||||
// held is a finding kept while its module is delivered (clause 13 (b)).
|
||||
type held struct {
|
||||
Key string `json:"key"`
|
||||
Node string `json:"node"`
|
||||
@@ -104,8 +103,9 @@ type Keeper struct {
|
||||
findings map[string]map[string]health.Finding
|
||||
blind map[string]string
|
||||
attempts map[string]health.Attempt
|
||||
// ctrl is the controller's conditions as Controller hands them: compare's other side alone, never a condition's
|
||||
// input (issue 1126). Nothing feeds it since issue 774; it goes with compare.
|
||||
ctrl map[string]health.Condition
|
||||
leaseOut bool
|
||||
takeover health.Takeover
|
||||
wants map[string]delivery.Want
|
||||
haves map[string]delivery.Have
|
||||
@@ -209,7 +209,8 @@ func (k *Keeper) Attempt(key string, value []byte, deleted bool) error {
|
||||
return k.Settle(condition)
|
||||
}
|
||||
|
||||
// Controller takes one change of the controller's `conditions` bucket (clauses 8, 9): read until the lease write.
|
||||
// Controller takes one change of the controller's `conditions` bucket, for compare alone: no condition is carried from
|
||||
// it (issue 1126).
|
||||
func (k *Keeper) Controller(key string, value []byte, deleted bool) error {
|
||||
k.mu.Lock()
|
||||
k.init()
|
||||
@@ -219,38 +220,57 @@ func (k *Keeper) Controller(key string, value []byte, deleted bool) error {
|
||||
var c health.Condition
|
||||
if err := json.Unmarshal(value, &c); err != nil {
|
||||
k.mu.Unlock()
|
||||
k.Logf("the controller's condition %s is not readable, so nothing is carried from it: %v", key, err)
|
||||
k.Logf("the controller's condition %s is not readable, so compare does not list it: %v", key, err)
|
||||
return nil
|
||||
}
|
||||
k.ctrl[key] = c
|
||||
}
|
||||
k.mu.Unlock()
|
||||
return k.Settle(key)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Lease takes the controller's lease key as read (clause 8, Limits 8): once it is handed to a holder that is no
|
||||
// controller, nothing more is carried from the controller's bucket, and the kinds that ended with it clear.
|
||||
func (k *Keeper) Lease(away bool) error {
|
||||
k.mu.Lock()
|
||||
k.init()
|
||||
was := k.leaseOut
|
||||
k.leaseOut = away
|
||||
keys := make([]string, 0, len(k.ctrl))
|
||||
for key := range k.ctrl {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
k.mu.Unlock()
|
||||
if was == away {
|
||||
return nil
|
||||
}
|
||||
return k.settleAll(keys)
|
||||
}
|
||||
// endedWhy is why a condition of a kind that ended with the controller is cleared.
|
||||
const endedWhy = "its kind ended with the controller, which was unassigned (issue 1126): no finding raises it again"
|
||||
|
||||
// LeaseAway says whether the controller's lease is handed away.
|
||||
func (k *Keeper) LeaseAway() bool {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
return k.leaseOut
|
||||
// EndCarried clears, once, every open condition of a kind that ended with the controller (Homes' Carried) that no
|
||||
// observer's finding names: carried from the controller's bucket while it ran, nothing raises it again (issue 1126).
|
||||
// It answers how many it cleared.
|
||||
func (k *Keeper) EndCarried() (int, error) {
|
||||
open, err := k.Open()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n := 0
|
||||
var problems []string
|
||||
for _, c := range open {
|
||||
if h, ok := k.homeOf(c.Kind); !ok || !h.Carried {
|
||||
continue
|
||||
}
|
||||
k.mu.Lock()
|
||||
k.init()
|
||||
f := k.wanted(c.Key)
|
||||
k.mu.Unlock()
|
||||
if f != nil {
|
||||
continue
|
||||
}
|
||||
raw, rev, found, err := k.Store.Get(ConditionsState, c.Key)
|
||||
if err != nil || !found {
|
||||
continue
|
||||
}
|
||||
var was health.Condition
|
||||
if json.Unmarshal(raw, &was) != nil {
|
||||
continue
|
||||
}
|
||||
if err := k.clear(c.Key, was, rev, k.Now().UTC(), endedWhy); err != nil {
|
||||
problems = append(problems, fmt.Sprintf("%s: %v", c.Key, err))
|
||||
continue
|
||||
}
|
||||
n++
|
||||
}
|
||||
if len(problems) > 0 {
|
||||
return n, errors.New(strings.Join(problems, "; "))
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Takeover takes the keeper's own take-over key as read.
|
||||
@@ -287,9 +307,9 @@ func (k *Keeper) settleAll(keys []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// wanted is what the inputs say key should be: a finding of an observer, or a condition carried from the controller's
|
||||
// bucket of a kind that ends with the controller, until the lease write (clause 9). Nil when nothing says it.
|
||||
func (k *Keeper) wanted(key string) (*health.Finding, *health.Condition) {
|
||||
// wanted is what the inputs say key should be: a finding of an observer. Nil when nothing says it; nothing is carried
|
||||
// from the controller's bucket (issue 1126).
|
||||
func (k *Keeper) wanted(key string) *health.Finding {
|
||||
observers := make([]string, 0, len(k.findings))
|
||||
for o := range k.findings {
|
||||
observers = append(observers, o)
|
||||
@@ -297,18 +317,10 @@ func (k *Keeper) wanted(key string) (*health.Finding, *health.Condition) {
|
||||
sort.Strings(observers)
|
||||
for _, o := range observers {
|
||||
if f, ok := k.findings[o][key]; ok {
|
||||
return &f, nil
|
||||
return &f
|
||||
}
|
||||
}
|
||||
if c, ok := k.ctrl[key]; ok && !k.leaseOut && k.endsWithController(c) {
|
||||
return nil, &c
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (k *Keeper) endsWithController(c health.Condition) bool {
|
||||
h, ok := k.homeOf(c.Kind)
|
||||
return ok && h.Carried
|
||||
return nil
|
||||
}
|
||||
|
||||
// settleOnce is one try of Settle.
|
||||
@@ -326,11 +338,11 @@ func (k *Keeper) settleOnce(key string) error {
|
||||
}
|
||||
k.mu.Lock()
|
||||
k.init()
|
||||
f, carried := k.wanted(key)
|
||||
hold, holding := k.holdFor(key, f, carried, open)
|
||||
f := k.wanted(key)
|
||||
hold, holding := k.holdFor(key, f, open)
|
||||
k.mu.Unlock()
|
||||
|
||||
if f == nil && carried == nil {
|
||||
if f == nil {
|
||||
k.dropHeld(key, "its finding went while its module was delivered")
|
||||
if !open {
|
||||
return nil
|
||||
@@ -341,7 +353,7 @@ func (k *Keeper) settleOnce(key string) error {
|
||||
return k.keepHeld(hold)
|
||||
}
|
||||
k.dropHeld(key, "")
|
||||
next := k.derive(key, was, open, f, carried, now)
|
||||
next := k.derive(key, was, open, f, now)
|
||||
events := transitions(was, next, open)
|
||||
if len(events) == 0 && open && sameBody(was, next) {
|
||||
return nil
|
||||
@@ -359,28 +371,16 @@ func (k *Keeper) settleOnce(key string) error {
|
||||
k.mu.Unlock()
|
||||
for _, e := range events {
|
||||
e.Condition, e.At, e.Event = next, now, health.EventFor(e.Change)
|
||||
e.Show = k.show(next, carried != nil)
|
||||
e.Show = health.ShowKeeper(next.Key)
|
||||
k.tell(e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *Keeper) show(c health.Condition, carried bool) string {
|
||||
if carried {
|
||||
return health.ShowController(c.Key)
|
||||
}
|
||||
return health.ShowKeeper(c.Key)
|
||||
}
|
||||
|
||||
// derive is the condition key should be now, from what was open and what the inputs say.
|
||||
func (k *Keeper) derive(key string, was health.Condition, open bool, f *health.Finding, carried *health.Condition, now time.Time) health.Condition {
|
||||
func (k *Keeper) derive(key string, was health.Condition, open bool, f *health.Finding, now time.Time) health.Condition {
|
||||
var c health.Condition
|
||||
switch {
|
||||
case carried != nil:
|
||||
// Raised and cleared as the controller's bucket says, with its show and actions (Limits 9).
|
||||
c = *carried
|
||||
c.Epoch = 0
|
||||
c.LooksUnread = ""
|
||||
case open:
|
||||
c = was
|
||||
k.fromFinding(&c, *f, now)
|
||||
@@ -402,9 +402,7 @@ func (k *Keeper) derive(key string, was health.Condition, open bool, f *health.F
|
||||
}
|
||||
k.mu.Unlock()
|
||||
}
|
||||
if carried == nil {
|
||||
k.overlay(&c, now)
|
||||
}
|
||||
k.overlay(&c, now)
|
||||
if !open {
|
||||
k.mu.Lock()
|
||||
if line := k.outOfHold[key]; line != "" {
|
||||
@@ -447,9 +445,7 @@ func (k *Keeper) fromFinding(c *health.Condition, f health.Finding, now time.Tim
|
||||
_ = now
|
||||
}
|
||||
|
||||
// overlay writes the healers' attempts into the condition (clause 7) and, until the lease write, what the controller's
|
||||
// bucket holds for the same key (clause 8): its tried, its resolver and escalation, and its silence where the keeper has
|
||||
// none of its own.
|
||||
// overlay writes the healers' attempts into the condition (clause 7); nothing of the controller's bucket (issue 1126).
|
||||
func (k *Keeper) overlay(c *health.Condition, now time.Time) {
|
||||
k.mu.Lock()
|
||||
var mine []health.Attempt
|
||||
@@ -458,18 +454,8 @@ func (k *Keeper) overlay(c *health.Condition, now time.Time) {
|
||||
mine = append(mine, a)
|
||||
}
|
||||
}
|
||||
ctrl, carry := k.ctrl[c.Key]
|
||||
carry = carry && !k.leaseOut
|
||||
k.mu.Unlock()
|
||||
sort.Slice(mine, func(i, j int) bool { return mine[i].Begun.Before(mine[j].Begun) })
|
||||
if carry && len(ctrl.Tried) > 0 {
|
||||
c.Tried = append([]health.Tried(nil), ctrl.Tried...)
|
||||
if ctrl.Escalated() {
|
||||
c.Resolver, c.Severity = health.ResolverOperator, health.Urgent
|
||||
} else if ctrl.Resolver != "" && ctrl.Resolver != health.ResolverSelf {
|
||||
c.Resolver = ctrl.Resolver
|
||||
}
|
||||
}
|
||||
for _, a := range mine {
|
||||
t := a.Tried()
|
||||
if !containsTried(c.Tried, t) {
|
||||
@@ -487,9 +473,6 @@ func (k *Keeper) overlay(c *health.Condition, now time.Time) {
|
||||
if len(c.Tried) > health.KeptTried {
|
||||
c.Tried = c.Tried[len(c.Tried)-health.KeptTried:]
|
||||
}
|
||||
if carry && c.Silenced == nil && ctrl.Silenced != nil && now.Before(ctrl.Silenced.Until) {
|
||||
c.Silenced = ctrl.Silenced
|
||||
}
|
||||
}
|
||||
|
||||
func containsTried(list []health.Tried, t health.Tried) bool {
|
||||
@@ -556,7 +539,7 @@ func (k *Keeper) clear(key string, was health.Condition, rev uint64, now time.Ti
|
||||
k.mu.Unlock()
|
||||
cleared := now
|
||||
k.tell(health.Event{Condition: was, Event: health.EventCleared, At: now, Change: health.ChangeCleared, Why: why,
|
||||
Cleared: &cleared, Show: k.show(was, false)})
|
||||
Cleared: &cleared, Show: health.ShowKeeper(was.Key)})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -623,9 +606,6 @@ func (k *Keeper) Resync() (int, error) {
|
||||
seen[key] = true
|
||||
}
|
||||
}
|
||||
for key := range k.ctrl {
|
||||
seen[key] = true
|
||||
}
|
||||
k.mu.Unlock()
|
||||
keys := make([]string, 0, len(seen))
|
||||
for key := range seen {
|
||||
|
||||
@@ -141,7 +141,7 @@ func finding(t *testing.T, observer, scope, id, kind, source string, sev health.
|
||||
}
|
||||
|
||||
// ADR 0564 clauses 2, 3, 7, 8 (issue 1952 C2, C3): a finding raises, changes and clears its condition; a blind observer
|
||||
// clears nothing; the healers' attempts and the controller's escalation reach the condition; a reopening within ten
|
||||
// clears nothing; the healers' attempts reach the condition; a reopening within ten
|
||||
// minutes is the same fault; a silence is a transition in history.
|
||||
func TestTheKeeperRaisesChangesAndClearsFromFindings(t *testing.T) {
|
||||
r := newRig()
|
||||
@@ -206,25 +206,12 @@ func TestTheKeeperRaisesChangesAndClearsFromFindings(t *testing.T) {
|
||||
}
|
||||
return ""
|
||||
}},
|
||||
// Until the lease write the controller's escalation for the same key reaches the condition (clause 8).
|
||||
{name: "the controller's escalation is carried", do: func() error {
|
||||
ctrl, _ := json.Marshal(health.Condition{Key: key, Kind: "silent", Severity: health.Urgent, Resolver: health.ResolverOperator,
|
||||
Tried: []health.Tried{{At: r.now, What: "x", Outcome: "escalated", By: "healer H1"}}})
|
||||
_ = r.k.Attempt("h3-1", nil, true)
|
||||
_ = r.k.Attempt("h3-2", nil, true)
|
||||
return r.k.Controller(key, ctrl, false)
|
||||
}, check: func(c health.Condition, _ bool) string {
|
||||
if c.Resolver != health.ResolverOperator || c.Severity != health.Urgent {
|
||||
return fmt.Sprintf("%+v", c)
|
||||
}
|
||||
return ""
|
||||
}},
|
||||
}
|
||||
for _, s := range steps {
|
||||
if err := s.do(); err != nil {
|
||||
t.Fatalf("%s: %v", s.name, err)
|
||||
}
|
||||
if got := r.changes(); s.said != "" && got != s.said || s.said == "" && got != "" && s.name != "the controller's escalation is carried" {
|
||||
if got := r.changes(); s.said != "" && got != s.said || s.said == "" && got != "" {
|
||||
t.Errorf("%s: said %q, want %q", s.name, got, s.said)
|
||||
}
|
||||
if s.check != nil {
|
||||
@@ -243,28 +230,64 @@ func TestTheKeeperRaisesChangesAndClearsFromFindings(t *testing.T) {
|
||||
if !strings.Contains(string(raw0), `"show":"mesh-conditions.conditions key=machine.g14.silent"`) {
|
||||
t.Errorf("the first transition is %s", raw0)
|
||||
}
|
||||
// A carried kind (it ends with the controller) is raised and cleared as the controller's bucket says, until the
|
||||
// lease is handed away, with the controller's show (clause 9, Limits 9).
|
||||
deaf, _ := json.Marshal(health.Condition{Key: "core.controller.deaf", Kind: "controller-deaf", Source: "S4",
|
||||
Subject: health.Subject{Scope: health.ScopeCore, ID: "controller"}, Severity: health.Urgent, Summary: "deaf", Resolver: "self", Epoch: 9})
|
||||
if err := r.k.Controller("core.controller.deaf", deaf, false); err != nil {
|
||||
}
|
||||
|
||||
// 1126/A5/AC2: the keeper carries no controller condition: the controller's bucket, as Controller hands it, opens no
|
||||
// condition and changes none (no escalation, no silence). What it carried while the controller ran, still open, is cleared
|
||||
// once by EndCarried, said as cleared; a condition an observer's finding holds stays; a second EndCarried clears nothing.
|
||||
func TestTheKeeperCarriesNoControllerConditionAndClearsWhatItCarriedOnce(t *testing.T) {
|
||||
r := newRig()
|
||||
key, raw := finding(t, watchman, health.ScopeMachine, "g14", "silent", "S1", health.Warning, r.now)
|
||||
if err := r.k.Finding(watchman, key, raw, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c, open := r.open("core.controller.deaf"); !open || c.Epoch != 0 || r.said[0].Show != "mesh-controller.conditions key=core.controller.deaf" {
|
||||
t.Errorf("a carried kind is %+v, said %+v", c, r.said)
|
||||
}
|
||||
r.said = nil
|
||||
if err := r.k.Lease(true); err != nil {
|
||||
t.Fatal(err)
|
||||
until := r.now.Add(time.Hour)
|
||||
for _, c := range []health.Condition{
|
||||
{Key: "core.controller.deaf", Kind: "controller-deaf", Source: "S4", Subject: health.Subject{Scope: health.ScopeCore, ID: "controller"},
|
||||
Severity: health.Urgent, Summary: "deaf", Resolver: "self"},
|
||||
{Key: key, Kind: "silent", Severity: health.Urgent, Resolver: health.ResolverOperator,
|
||||
Tried: []health.Tried{{At: r.now, What: "x", Outcome: "escalated", By: "healer H1"}},
|
||||
Silenced: &health.Silence{Until: until}},
|
||||
} {
|
||||
raw, _ := json.Marshal(c)
|
||||
if err := r.k.Controller(c.Key, raw, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := r.k.Resync(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if _, open := r.open("core.controller.deaf"); open || r.changes() != "cleared core.controller.deaf" {
|
||||
t.Error("a carried kind outlived the lease write")
|
||||
if _, open := r.open("core.controller.deaf"); open || r.changes() != "" {
|
||||
t.Error("the controller's condition was carried")
|
||||
}
|
||||
if c, _ := r.open(key); c.Severity != health.Warning || c.Resolver != health.ResolverSelf || len(c.Tried) != 0 || c.Silenced != nil {
|
||||
t.Errorf("the controller's escalation reached the keeper's condition: %+v", c)
|
||||
}
|
||||
// Carried while the controller ran: two of kinds that ended with it, still open in conditions.
|
||||
for _, c := range []health.Condition{
|
||||
{Key: "core.controller.deaf", Kind: "controller-deaf", Severity: health.Urgent, Resolver: "self", Count: 1},
|
||||
{Key: "seat.vault.ace.holder-silent", Kind: "holder-silent", Severity: health.Warning, Resolver: "self", Count: 1},
|
||||
} {
|
||||
if _, err := r.st.Create(ConditionsState, c.Key, c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
n, err := r.k.EndCarried()
|
||||
if err != nil || n != 2 || r.changes() != "cleared core.controller.deaf, cleared seat.vault.ace.holder-silent" {
|
||||
t.Fatalf("EndCarried cleared %d (%v)", n, err)
|
||||
}
|
||||
if _, open := r.open(key); !open {
|
||||
t.Error("a condition a finding holds was cleared")
|
||||
}
|
||||
if n, err := r.k.EndCarried(); err != nil || n != 0 || r.changes() != "" {
|
||||
t.Errorf("a second EndCarried cleared %d (%v)", n, err)
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0564 clause 9 (issue 1952 C5, C8; the lead 2026-10-29): compare counts only what both sides can raise. A key held
|
||||
// while its module is delivered, a kind that ends with the controller and a kind the controller never raises are listed
|
||||
// apart; a kind with no home decided is a difference naming the gap; take-over is refused while compare differs, and
|
||||
// apart; a kind with no home decided is a difference naming the gap; take-over is refused (issue 1126), and
|
||||
// hand-back is gone (issue 774, review g1): answered as gone, naming its record, and writing nothing.
|
||||
func TestCompareCountsOnlyWhatBothSidesRaise(t *testing.T) {
|
||||
r := newRig()
|
||||
@@ -296,7 +319,7 @@ func TestCompareCountsOnlyWhatBothSidesRaise(t *testing.T) {
|
||||
"delivery.x.mystery": "no home is decided for the kind mystery",
|
||||
"bus.x.consumer-wrong": "mesh-self-check has not written its finding",
|
||||
// A kind of a probe the operator dropped (2026-10-30) is carried, never missing.
|
||||
"seat.vault.ace.holder-silent": "carried from its bucket until the lease write (dropped 2026-10-30 by the operator",
|
||||
"seat.vault.ace.holder-silent": "no longer carried (issue 1126) (dropped 2026-10-30 by the operator",
|
||||
// The controller's own bus upgrade ends with its lease (the operator, 2026-10-31; issue 1952).
|
||||
"bus.nats.upgrade-failed": "ends with the controller",
|
||||
} {
|
||||
@@ -332,17 +355,14 @@ func TestCompareCountsOnlyWhatBothSidesRaise(t *testing.T) {
|
||||
if len(Homes) != 84 || carried != 34 { // 19 of ADR 0564, the 63 of the table, plan-failed and stalled
|
||||
t.Errorf("the table holds %d kinds, %d carried", len(Homes), carried)
|
||||
}
|
||||
if _, err := r.k.TakeOver(nil, "jochen", "now"); err == nil || !strings.Contains(err.Error(), "2 difference") {
|
||||
t.Errorf("take-over while compare differs: %v", err)
|
||||
if _, err := r.k.TakeOver(nil, "jochen", "now"); err == nil || !strings.Contains(err.Error(), "one source already") {
|
||||
t.Errorf("take-over: %v", err)
|
||||
}
|
||||
for _, away := range []bool{false, true} {
|
||||
_ = r.k.Lease(away)
|
||||
if _, err := r.k.HandBack("jochen", "back"); err == nil || !strings.Contains(err.Error(), "hand-back is gone") ||
|
||||
!strings.Contains(err.Error(), "ADR 0564 clause 9") {
|
||||
t.Errorf("hand-back with the lease away %v: %v, want it answered as gone naming its record", away, err)
|
||||
}
|
||||
if _, _, found, _ := r.k.Store.Get(TakeoverState, health.TakeoverKey); found {
|
||||
t.Errorf("hand-back with the lease away %v wrote the take-over key", away)
|
||||
}
|
||||
if _, err := r.k.HandBack("jochen", "back"); err == nil || !strings.Contains(err.Error(), "hand-back is gone") ||
|
||||
!strings.Contains(err.Error(), "ADR 0564 clause 9") {
|
||||
t.Errorf("hand-back: %v, want it answered as gone naming its record", err)
|
||||
}
|
||||
if _, _, found, _ := r.k.Store.Get(TakeoverState, health.TakeoverKey); found {
|
||||
t.Error("hand-back or take-over wrote the take-over key")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/asker"
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
)
|
||||
|
||||
@@ -106,14 +105,11 @@ func Keep(k *Keeper) {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
watch(k, ControllerLease, "", func(c stdio.StateChange) error {
|
||||
if c.Key != ControllerLeaseKey {
|
||||
return nil
|
||||
}
|
||||
away, why := buscompose.LeaseHandedAway(c.Value, c.Op == "put", nil)
|
||||
logf("the controller's lease: %s", why)
|
||||
return said(k.Lease(away))
|
||||
})
|
||||
// Once at start: what was carried from the controller's bucket while it ran is cleared (issue 1126); nothing raises
|
||||
// those kinds again.
|
||||
if n, err := k.EndCarried(); n > 0 || err != nil {
|
||||
logf("cleared %d condition(s) of kinds that ended with the controller: %v", n, err)
|
||||
}
|
||||
watch(k, WantsRead, "", func(c stdio.StateChange) error { return said(k.Want(c.Key, c.Value, c.Op != "put")) })
|
||||
watch(k, HavesRead, "", func(c stdio.StateChange) error { return said(k.Have(c.Key, c.Value, c.Op != "put")) })
|
||||
watch(k, AttemptsRead, health.HealersModule+" attempts", func(c stdio.StateChange) error {
|
||||
|
||||
@@ -29,14 +29,14 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
|
||||
!slices.Equal(Verbs, catalogue.VerbNames(seat.Serves)) {
|
||||
t.Errorf("module %s runs as %s and claims %+v; the seat serves %v", m.Module, m.RunsAs, m.Claims, catalogue.VerbNames(seat.Serves))
|
||||
}
|
||||
reads := []string{ControllerLease, WantsRead, HavesRead, AttemptsRead, PlacementRead}
|
||||
reads := []string{WantsRead, HavesRead, AttemptsRead, PlacementRead}
|
||||
for _, o := range Observers {
|
||||
reads = append(reads, o+"."+health.FindingsState)
|
||||
}
|
||||
slices.Sort(reads)
|
||||
got := slices.Clone(m.Reads)
|
||||
slices.Sort(got)
|
||||
if slices.Contains(m.Reads, "mesh-controller.conditions") || !slices.Equal(got, reads) || slices.Contains(m.Reads, health.HealersModule+"."+health.WouldState) {
|
||||
if slices.Contains(m.Reads, "mesh-controller.conditions") || slices.Contains(m.Reads, "mesh-controller.lease") || !slices.Equal(got, reads) || slices.Contains(m.Reads, health.HealersModule+"."+health.WouldState) {
|
||||
t.Errorf("reads %v, want %v and never the healers' would", got, reads)
|
||||
}
|
||||
ttl, keyTTL := map[string]int{}, map[string]bool{}
|
||||
|
||||
@@ -73,8 +73,8 @@ func Tools(k *Keeper, a Asks) []stdio.Tool {
|
||||
{Name: Seat + ".compare", Description: "The keeper's open conditions against the controller's, each difference with " +
|
||||
"its reason; what ends with the controller, what only the keeper raises and what is held listed apart.",
|
||||
Run: func(map[string]any) (any, error) { return k.Compare() }},
|
||||
{Name: Seat + ".take-over", Description: "Ask the operator's Approve to switch the readers of conditions to the " +
|
||||
"keeper; refused while compare differs.",
|
||||
{Name: Seat + ".take-over", Description: "Refused: the keeper is the one source of conditions since the " +
|
||||
"controller ended by its unassignment (issue 1126); kept as the seat's verb until the seat drops it.",
|
||||
Input: obj(map[string]any{"why": str("why now")}, "why"),
|
||||
Run: func(args map[string]any) (any, error) {
|
||||
return k.TakeOver(a, stdio.CallerOf(args), strArg(args, "why"))
|
||||
@@ -212,7 +212,7 @@ func (k *Keeper) Inputs() map[string]any {
|
||||
findings[o] = len(fs)
|
||||
}
|
||||
return map[string]any{"findings": findings, "blind": k.blind, "attempts": len(k.attempts),
|
||||
"controller-conditions": len(k.ctrl), "lease-handed-away": k.leaseOut, "follow": k.takeover.Follow,
|
||||
"controller-conditions": len(k.ctrl), "follow": k.takeover.Follow,
|
||||
"wants": len(k.wants), "haves": len(k.haves)}
|
||||
}
|
||||
|
||||
|
||||
@@ -261,7 +261,7 @@ func TestTheKeeperWholeRaisesAndClearsOnTheBus(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
buckets := []string{"mesh-controller_conditions", "mesh-controller_lease", "mesh-delivery-assigner_wants", "node-engine_have", "mesh-delivery-assigner_placement",
|
||||
buckets := []string{"mesh-controller_conditions", "mesh-delivery-assigner_wants", "node-engine_have", "mesh-delivery-assigner_placement",
|
||||
"mesh-healers_attempts"}
|
||||
for _, o := range Observers {
|
||||
buckets = append(buckets, o+"_findings")
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"mesh-provisioner.findings",
|
||||
"artifact-store-tools.findings",
|
||||
"mesh-healers.attempts",
|
||||
"mesh-controller.lease",
|
||||
"mesh-delivery-assigner.wants",
|
||||
"node-engine.have",
|
||||
"mesh-delivery-assigner.placement"
|
||||
|
||||
@@ -75,6 +75,5 @@ Each directory or archive this module declares says how it treats a file it did
|
||||
controller told by `pull-declares`; the controller's mirrors are no longer read (ADR 0565 §4, issue 774).
|
||||
`config_take_over` asks the operator's Approve, refused while any difference stands and switched on the Approve only
|
||||
if none stands then. `config_hand_back` is gone: it answers so and changes nothing. `config_declared` and `config_resync` read and write again what it keeps.
|
||||
- From the controller's lease write a trusted value is set only through propose and the operator's Approve.
|
||||
- Its sets, clears and warrants are recorded in the hand-act log of the seat `mesh-hand-acts`, falling back to the
|
||||
controller's while that seat has no holder and the controller holds its lease.
|
||||
- A trusted value is set only through propose and the operator's Approve: no controller relay is trusted (issue 1126).
|
||||
- Its sets, clears and warrants are recorded in the hand-act log of the seat `mesh-hand-acts` alone.
|
||||
|
||||
@@ -16,9 +16,10 @@ import (
|
||||
// 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.
|
||||
// **Trusted values on the operator's Approve alone.** A trusted value (ADR 0348 §8) is set only 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. No controller relay is trusted (ADR 0565 §13, issue 1126): the controller ended by its unassignment, so no lease
|
||||
// turns its terminal on.
|
||||
|
||||
// ControllerCaller is the controller's bus user: the one caller that carries a value given at its terminal.
|
||||
const ControllerCaller = "controller"
|
||||
@@ -275,9 +276,9 @@ func (c *Config) change(call Call) (*SetAnswer, error) {
|
||||
return nil, fmt.Errorf("%s is refused for %s at %s: %s", show(value), call.Key, level,
|
||||
strings.Join(problems, "; "))
|
||||
}
|
||||
if trusted && call.approval == nil && !c.atTheTerminal(call) {
|
||||
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: "+
|
||||
if trusted && call.approval == nil {
|
||||
return nil, fmt.Errorf("%s is trusted (it decides what runs or who can reach what), so it is set only on "+
|
||||
"the operator's Approve (novox/hq ADR 0348 §8; no controller relay is trusted, ADR 0565 §13), not by %s: "+
|
||||
"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}
|
||||
@@ -293,9 +294,9 @@ func (c *Config) change(call Call) (*SetAnswer, error) {
|
||||
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.approval == nil && !c.atTheTerminal(call) {
|
||||
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 "+
|
||||
if trusted && call.approval == nil {
|
||||
return nil, fmt.Errorf("%s is trusted, so it is cleared only on the operator's Approve (novox/hq ADR 0348 "+
|
||||
"§8; no controller relay is trusted, ADR 0565 §13), not by %s: propose the clear instead, with "+
|
||||
"config's action propose and clear", call.Key, call.Caller)
|
||||
}
|
||||
if err := c.Store.Delete(ValuesState, vk); err != nil {
|
||||
@@ -326,13 +327,6 @@ func (c *Config) change(call Call) (*SetAnswer, error) {
|
||||
return answer, nil
|
||||
}
|
||||
|
||||
// atTheTerminal says a call is the controller's relay of its terminal, which holds only until the controller's lease is
|
||||
// handed away: from the lease write a trusted value is set only through propose and the operator's Approve, whoever
|
||||
// calls (novox/hq ADR 0565 §13).
|
||||
func (c *Config) atTheTerminal(call Call) bool {
|
||||
return call.Caller == ControllerCaller && !c.leaseAway()
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
@@ -404,10 +404,10 @@ func TestTheVerbRefusesWhatADeclarationDoesNotAllow(t *testing.T) {
|
||||
{"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 trusted value at the mesh, not on an Approve", Call{Key: "agent-managed-settings",
|
||||
Value: map[string]any{"a": 1.0}}, "set only on the operator's Approve"},
|
||||
{"a trusted module value, not on an Approve", Call{Key: "managed", Value: map[string]any{},
|
||||
Node: "g14", Module: "claude-code"}, "set only on the operator's Approve"},
|
||||
{"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"},
|
||||
@@ -438,23 +438,27 @@ func TestTheVerbRefusesWhatADeclarationDoesNotAllow(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// **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)
|
||||
// 1126/A5/AC1: **no controller relay is trusted.** With no lease key (the controller ended by its unassignment), a trusted
|
||||
// value set or cleared by the controller's account is refused, naming propose; nothing is written (ADR 0565 §13).
|
||||
func TestNoControllerRelayIsTrusted(t *testing.T) {
|
||||
for _, call := range []Call{
|
||||
{Action: "set", Key: "agent-managed-settings", Value: map[string]any{"a": 1.0}},
|
||||
{Action: "set", Key: "managed", Node: "g14", Module: "claude-code", Value: map[string]any{"a": 1.0}},
|
||||
{Action: "clear", Key: "agent-managed-settings"},
|
||||
} {
|
||||
c, m := aMesh(t)
|
||||
m.mirror(t, ValuesState, "mesh.agent-managed-settings", `{"value":{"b":2},"by":"person.jochen","via":"person.jochen"}`)
|
||||
call.Caller, call.By, call.Why = ControllerCaller, "jochen", "at the terminal"
|
||||
_, err := c.Change(call)
|
||||
if err == nil || !strings.Contains(err.Error(), "propose") {
|
||||
t.Errorf("%s %s from the controller: %v", call.Action, call.Key, err)
|
||||
}
|
||||
if v, _ := storedValue(t, m, "mesh.agent-managed-settings"); string(v.Value) != `{"b":2}` {
|
||||
t.Errorf("%s %s from the controller left %s", call.Action, call.Key, v.Value)
|
||||
}
|
||||
if _, held := storedValue(t, m, "module.g14.claude-code.managed"); held {
|
||||
t.Errorf("%s %s from the controller wrote claude-code's own value", call.Action, call.Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/handacts"
|
||||
)
|
||||
|
||||
@@ -77,15 +76,7 @@ func (runtimeStore) Emit(event string, body any) error { return stdio.Emit(event
|
||||
|
||||
// recorder records into the hand-act log of the seat mesh-hand-acts alone (novox/hq ADR 0565 §11, issue 1953): the manifest
|
||||
// invokes no verb of the controller and the SDK recorder has no fall back to its log.
|
||||
var recorder = handacts.Recorder{Ask: stdio.Ask, LeaseAway: func() bool {
|
||||
e, err := stdio.State(LeaseRead).Get(buscompose.ControllerLeaseKey)
|
||||
var raw []byte
|
||||
if e != nil {
|
||||
raw = e.Value
|
||||
}
|
||||
away, _ := buscompose.LeaseHandedAway(raw, e != nil, err)
|
||||
return away
|
||||
}}
|
||||
var recorder = handacts.Recorder{Ask: stdio.Ask}
|
||||
|
||||
// HandAct records a change in the hand-act log (to-be 45 §7).
|
||||
func (runtimeStore) HandAct(what, why, cause string) error {
|
||||
@@ -238,7 +229,7 @@ func Tools(c *Config) []stdio.Tool {
|
||||
"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: " +
|
||||
"who and why. A trusted value is set only 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. A proposal naming the open pull request that introduces its key (pull) is judged against what " +
|
||||
|
||||
@@ -33,7 +33,7 @@ func TestEveryStateAndActHasItsTool(t *testing.T) {
|
||||
ProposalsState: "config proposals", PullsState: "config list", KeptState: "config list", HistoryLog: "config history",
|
||||
DeclaredState: DeclaredTool, TakeoverState: DeclaredTool,
|
||||
PlacementRead: CompareTool, WantsRead: CompareTool, BuildsRead: CompareTool, ModulesRead: DeclaredTool,
|
||||
ManifestsRead: DeclaredTool, IndexerPredefinedRead: CompareTool, CandidatesRead: CompareTool, LeaseRead: HandBackTool,
|
||||
ManifestsRead: DeclaredTool, IndexerPredefinedRead: CompareTool, CandidatesRead: CompareTool,
|
||||
"a resolution": ResyncTool, "the indexer's candidates taken": ResyncTool, "a take-over decided": TakeOverTool,
|
||||
}
|
||||
served := map[string]bool{}
|
||||
|
||||
@@ -266,8 +266,8 @@ func TestATrustedValueIsProposedAskedUnderItsOwnNameAndSetOnTheOperatorsApprove(
|
||||
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"})
|
||||
// Changed meanwhile.
|
||||
m.mirror(t, ValuesState, "mesh.agent-managed-settings", `{"value":{"b":2},"by":"person.jochen","via":"person.jochen"}`)
|
||||
m.events = nil
|
||||
r.choose(t, a.Proposal, OptionApprove, nil)
|
||||
said, _ := c.Decide()
|
||||
@@ -501,8 +501,7 @@ func TestAClearIsProposedAndATrustedSetNamesTheProposal(t *testing.T) {
|
||||
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"})
|
||||
m.mirror(t, ValuesState, "module.g14.claude-code.managed", `{"value":{"a":1},"by":"person.jochen","via":"person.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") ||
|
||||
|
||||
@@ -26,7 +26,6 @@ import (
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
@@ -41,8 +40,6 @@ const (
|
||||
// IndexerPredefinedRead and CandidatesRead are the indexer's states (ADR 0565 §2).
|
||||
IndexerPredefinedRead = delivery.IndexerModule + "." + delivery.IndexerPredefinedState
|
||||
CandidatesRead = delivery.IndexerModule + "." + delivery.IndexerConfigCandidatesState
|
||||
// LeaseRead is the controller's lease: after its write hand-back is gone and the terminal sets nothing (§13).
|
||||
LeaseRead = buscompose.ControllerLease
|
||||
)
|
||||
|
||||
// The sources the switch names.
|
||||
@@ -95,17 +92,6 @@ func (c *Config) source() (Switch, error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// leaseAway says the controller's lease is handed away (ADR 0561 §3's reading).
|
||||
func (c *Config) leaseAway() bool {
|
||||
e, err := c.Store.Get(LeaseRead, buscompose.ControllerLeaseKey)
|
||||
var raw []byte
|
||||
if e != nil {
|
||||
raw = e.Value
|
||||
}
|
||||
away, _ := buscompose.LeaseHandedAway(raw, e != nil, err)
|
||||
return away
|
||||
}
|
||||
|
||||
// ---- declared --------------------------------------------------------------------------------------------
|
||||
|
||||
// DeclaredKept is one key of `declared`: a module's declarations as the manifest the indexer's `modules.<module>`
|
||||
|
||||
@@ -118,8 +118,7 @@ func TestCompareListsEveryDifferenceAndTheTakeOverWaitsForNone(t *testing.T) {
|
||||
}
|
||||
|
||||
// The operator's Approve switches config to the indexer: pull-declares is refused, the list and the pull requests'
|
||||
// config come from the indexer; hand-back is gone, and a switch naming the controller is read as the indexer; from the
|
||||
// lease write a trusted value is set only on an Approve; declared is written once per manifest (ADR 0565 §3, §4, §13).
|
||||
// config come from the indexer; hand-back is gone, and a switch naming the controller is read as the indexer; declared is written once per manifest (ADR 0565 §3, §4, §13).
|
||||
func TestTheTakeOverAndTheHandBack(t *testing.T) {
|
||||
c, m, r := anAgreeingMesh(t)
|
||||
steps := []struct {
|
||||
@@ -174,15 +173,6 @@ func TestTheTakeOverAndTheHandBack(t *testing.T) {
|
||||
res, err := c.Reconcile()
|
||||
return res.SourceGone + "|" + string(m.resolved(t, "g14", "dunst", "width").Value), err
|
||||
}, "is gone: config no longer reads its records"},
|
||||
{"the lease is written", func() (string, error) {
|
||||
m.mirror(t, LeaseRead, "holder", `{"handed-to":"the operator's hand-over"}`)
|
||||
return "lease", nil
|
||||
}, "lease"},
|
||||
{"the terminal sets no trusted value after the lease write", func() (string, error) {
|
||||
_, err := c.Change(Call{Action: "set", Key: "managed", Module: "claude-code", Node: "g14", Value: map[string]any{},
|
||||
Why: "x", Caller: ControllerCaller, By: "person.jochen"})
|
||||
return "", err
|
||||
}, "propose"},
|
||||
}
|
||||
for _, s := range steps {
|
||||
got, err := s.do()
|
||||
|
||||
@@ -37,8 +37,7 @@
|
||||
"mesh-delivery-indexer.modules",
|
||||
"mesh-delivery-indexer.manifests",
|
||||
"mesh-delivery-indexer.predefined",
|
||||
"mesh-delivery-indexer.config-candidates",
|
||||
"mesh-controller.lease"
|
||||
"mesh-delivery-indexer.config-candidates"
|
||||
],
|
||||
"tools": [
|
||||
"config_compare",
|
||||
|
||||
Reference in New Issue
Block a user