Merge pull request 'Without the controller: the vault retires and rotates, the healers act, the artifact store is swept, config and conditions stand alone (issue 1126)' (#592) from fix/1126-gates-catalog into main

This commit was merged in pull request #592.
This commit is contained in:
2026-11-02 10:36:34 +00:00
50 changed files with 384 additions and 759 deletions
@@ -88,7 +88,7 @@ func (runtimeBus) Publish(subject string, body any) error {
return stdio.SeatPublish(subject, body, "") return stdio.SeatPublish(subject, body, "")
} }
// follow watches the builder's done keys and the wants: a build's end sweeps (once the lease names no controller) and // follow watches the builder's done keys and the wants: a build's end sweeps and
// observes again; a want changed observes again. Event-driven: nothing reads them on a timer. // observes again; a want changed observes again. Event-driven: nothing reads them on a timer.
func follow(h *Holder) { func follow(h *Holder) {
<-stdio.Served() <-stdio.Served()
@@ -9,8 +9,8 @@ package main
// At each build's end (a done key written) it sweeps as the controller's take-in did: every kept archive held first // At each build's end (a done key written) it sweeps as the controller's take-in did: every kept archive held first
// (ADR 0251 §8), then what is eligible let go of, oldest first, within the same bound per sweep, an index left for a // (ADR 0251 §8), then what is eligible let go of, oldest first, within the same bound per sweep, an index left for a
// `collect` a person confirms (ADR 0251 §7), each deletion recorded in the hand-act log (seat mesh-hand-acts) and said // `collect` a person confirms (ADR 0251 §7), each deletion recorded in the hand-act log (seat mesh-hand-acts) and said
// as artifacts-gone. **Nothing is let go until the controller's lease names a holder that is no controller**: until // as artifacts-gone. **No lease turns it** (issue 1126): the controller ended by its unassignment, so its take-in sweeps
// then the controller's take-in sweeps, and two sweeps never run at once. It also observes S25, image-missing: a want's // nothing and this seat is the one sweep. It also observes S25, image-missing: a want's
// image the store does not hold, a finding in its own state `findings` the condition keeper reads. // image the store does not hold, a finding in its own state `findings` the condition keeper reads.
import ( import (
@@ -24,7 +24,6 @@ import (
"time" "time"
stdio "git.novox.be/novox/mesh-sdk/go" stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/handacts" "git.novox.be/novox/mesh-sdk/go/handacts"
@@ -40,7 +39,6 @@ const (
HavesRead = "node-engine.have" HavesRead = "node-engine.have"
// ManifestsRead is the indexer's manifests: the images a manifest pins by itself. // ManifestsRead is the indexer's manifests: the images a manifest pins by itself.
ManifestsRead = delivery.IndexerModule + ".manifests" ManifestsRead = delivery.IndexerModule + ".manifests"
LeaseRead = buscompose.ControllerLease
// FindingsState is the holder's own findings (health.FindingsState): image-missing. // FindingsState is the holder's own findings (health.FindingsState): image-missing.
FindingsState = health.FindingsState FindingsState = health.FindingsState
// Module is this module, the observer its findings name. // Module is this module, the observer its findings name.
@@ -58,7 +56,7 @@ const (
ImageMissing = "image-missing" ImageMissing = "image-missing"
) )
// Bus is what the holder reads and writes on the bus: its read states (wants, builds, haves, the lease), its own // Bus is what the holder reads and writes on the bus: its read states (wants, builds, haves), its own
// findings, and its seat's event. The runtime in main; a test's in memory. // findings, and its seat's event. The runtime in main; a test's in memory.
type Bus interface { type Bus interface {
Keys(state string) ([]string, error) Keys(state string) ([]string, error)
@@ -106,13 +104,6 @@ type Build struct {
Manifest string Manifest string
} }
// LeaseAway says the controller's lease names a holder that is no controller: then the seat sweeps.
func (h *Holder) LeaseAway() bool {
raw, found, err := h.Bus.Get(LeaseRead, buscompose.ControllerLeaseKey)
away, _ := buscompose.LeaseHandedAway(raw, found, err)
return away
}
// Builds is every done build the builder's keys hold, each with its end. // Builds is every done build the builder's keys hold, each with its end.
func (h *Holder) Builds() ([]Build, error) { func (h *Holder) Builds() ([]Build, error) {
keys, err := h.Bus.Keys(BuildsRead) keys, err := h.Bus.Keys(BuildsRead)
@@ -495,10 +486,6 @@ func (h *Holder) Collect(why string, confirm bool, most int) (*CollectAnswer, er
if most > MostAtAll { if most > MostAtAll {
return nil, fmt.Errorf("collect lets go of at most %d at once, not %d", MostAtAll, most) return nil, fmt.Errorf("collect lets go of at most %d at once, not %d", MostAtAll, most)
} }
if confirm && !h.LeaseAway() {
return nil, errors.New("the controller holds its lease, and its take-in sweeps the store until it hands it away: " +
"this seat lets nothing go before then (novox/hq ADR 0571 clause 2); ask the controller's collect")
}
return h.Sweep(context.Background(), Sweeping{Confirmed: confirm, DryRun: !confirm, Most: most, Why: why, By: "collect"}) return h.Sweep(context.Background(), Sweeping{Confirmed: confirm, DryRun: !confirm, Most: most, Why: why, By: "collect"})
} }
@@ -665,11 +652,8 @@ func (h *Holder) sayGone(j *judged, refs []string, how string) {
} }
} }
// AfterBuild is the sweep at a build's end: only once the lease names no controller. // AfterBuild is the sweep at a build's end.
func (h *Holder) AfterBuild(key string) { func (h *Holder) AfterBuild(key string) {
if !h.LeaseAway() {
return
}
a, err := h.Sweep(context.Background(), Sweeping{Most: MostPerSweep, By: "after the build " + key}) a, err := h.Sweep(context.Background(), Sweeping{Most: MostPerSweep, By: "after the build " + key})
if err != nil { if err != nil {
h.logf("the sweep after %s: %v", key, err) h.logf("the sweep after %s: %v", key, err)
@@ -3,7 +3,7 @@
package main package main
// The seat mesh-artifact-sweep's holder whole against a real bus (novox/hq ADR 0571 clause 2, issue 1955): the builder's done // The seat mesh-artifact-sweep's holder whole against a real bus (novox/hq ADR 0571 clause 2, issue 1955): the builder's done
// keys, the wants and the controller's lease in a real nats-server's key-value buckets, a build's end heard by a watch // keys and the wants in a real nats-server's key-value buckets, no lease key anywhere (issue 1126), a build's end heard by a watch
// of the builds as the runtime hands it over, each deletion recorded by a request to the seat mesh-hand-acts on the bus, // of the builds as the runtime hands it over, each deletion recorded by a request to the seat mesh-hand-acts on the bus,
// artifacts-gone said on the seat's subject, and S25's finding written to the holder's own bucket. Only the store's // artifacts-gone said on the seat's subject, and S25's finding written to the holder's own bucket. Only the store's
// door is in memory: its HTTP is the copied registry package's, tested there. // door is in memory: its HTTP is the copied registry package's, tested there.
@@ -20,7 +20,6 @@ import (
"github.com/nats-io/nats.go/jetstream" "github.com/nats-io/nats.go/jetstream"
stdio "git.novox.be/novox/mesh-sdk/go" stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/handacts" "git.novox.be/novox/mesh-sdk/go/handacts"
"git.novox.be/novox/mesh-sdk/go/testbus" "git.novox.be/novox/mesh-sdk/go/testbus"
) )
@@ -117,7 +116,7 @@ func (b realBus) Publish(subject string, body any) error {
return b.nc.Publish(subject, raw) return b.nc.Publish(subject, raw)
} }
func TestTheSeatSweepsAtABuildsEndOnARealBusOnceTheLeaseIsAway(t *testing.T) { func TestTheSeatSweepsAtABuildsEndOnARealBusWithNoLease(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()
nc, err := nats.Connect(testbus.URL(t)) nc, err := nats.Connect(testbus.URL(t))
@@ -129,7 +128,7 @@ func TestTheSeatSweepsAtABuildsEndOnARealBusOnceTheLeaseIsAway(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
for _, s := range []string{BuildsRead, WantsRead, HavesRead, LeaseRead, ManifestsRead, "artifact-store-tools." + FindingsState} { for _, s := range []string{BuildsRead, WantsRead, HavesRead, ManifestsRead, "artifact-store-tools." + FindingsState} {
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: bucketOf(s), History: 5}); err != nil { if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: bucketOf(s), History: 5}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -137,7 +136,7 @@ func TestTheSeatSweepsAtABuildsEndOnARealBusOnceTheLeaseIsAway(t *testing.T) {
bus := realBus{ctx: ctx, nc: nc, js: js} bus := realBus{ctx: ctx, nc: nc, js: js}
// The mesh as aMesh lays it, copied onto the real buckets: letta's seven builds, a want, the store's listing. // The mesh as aMesh lays it, copied onto the real buckets: letta's seven builds, a want, the store's listing.
mem, memb, door, _ := aMesh(t, &buscompose.LeaseHolder{Instance: "c1", Host: "novox", Epoch: 3}) mem, memb, door, _ := aMesh(t)
// The builder's log as written today: aMesh dates it from 2026-10-01 for its fixed clock, and this holder runs on the // The builder's log as written today: aMesh dates it from 2026-10-01 for its fixed clock, and this holder runs on the
// real clock, so read as dated the entries fall out of the log's 30 days and every copy is kept as unrecorded. // real clock, so read as dated the entries fall out of the log's 30 days and every copy is kept as unrecorded.
shift := time.Since(memb.log[len(memb.log)-1].At) shift := time.Since(memb.log[len(memb.log)-1].At)
@@ -145,7 +144,7 @@ func TestTheSeatSweepsAtABuildsEndOnARealBusOnceTheLeaseIsAway(t *testing.T) {
e.At = e.At.Add(shift) e.At = e.At.Add(shift)
bus.log = append(bus.log, e) bus.log = append(bus.log, e)
} }
for _, s := range []string{BuildsRead, WantsRead, LeaseRead, ManifestsRead} { for _, s := range []string{BuildsRead, WantsRead, ManifestsRead} {
keys, _ := memb.Keys(s) keys, _ := memb.Keys(s)
for _, k := range keys[:min(len(keys), 6)] { // the seventh build ends later, on the bus for _, k := range keys[:min(len(keys), 6)] { // the seventh build ends later, on the bus
raw, _, _ := memb.Get(s, k) raw, _, _ := memb.Get(s, k)
@@ -175,7 +174,7 @@ func TestTheSeatSweepsAtABuildsEndOnARealBusOnceTheLeaseIsAway(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
h := &Holder{Bus: findingsAt{realBus: bus}, Door: mem.Door, Listed: mem.Listed, Now: time.Now, Logf: t.Logf} h := &Holder{Bus: findingsAt{realBus: bus}, Door: mem.Door, Listed: mem.Listed, Now: time.Now, Logf: t.Logf}
h.Acts = handacts.Recorder{LeaseAway: h.LeaseAway, Ask: func(key string, body any) (json.RawMessage, error) { h.Acts = handacts.Recorder{Ask: func(key string, body any) (json.RawMessage, error) {
raw, _ := json.Marshal(body) raw, _ := json.Marshal(body)
m, err := nc.Request("mesh.seat."+strings.TrimPrefix(strings.Replace(key, ".", ".tool.", 1), "seat:"), raw, 5*time.Second) m, err := nc.Request("mesh.seat."+strings.TrimPrefix(strings.Replace(key, ".", ".tool.", 1), "seat:"), raw, 5*time.Second)
if err != nil { if err != nil {
@@ -204,16 +203,8 @@ func TestTheSeatSweepsAtABuildsEndOnARealBusOnceTheLeaseIsAway(t *testing.T) {
} }
} }
// While the lease names the controller, a build's end lets nothing go. // No lease key: the build's end holds the kept archives and lets go of letta's 2nd, which the 7th moved past the five
ended() // newest and no want names (the want keeps the 1st), recording each deletion.
if len(door.asked) != 0 {
t.Fatalf("before the lease write the store was asked %v", door.asked)
}
// The lease handed away: the next build's end holds the kept archives and lets go of letta's 2nd, which the 7th moved
// past the five newest and no want names (the want keeps the 1st), recording each deletion.
if err := bus.Put(LeaseRead, buscompose.ControllerLeaseKey, buscompose.LeaseHolder{HandedTo: "mesh-grants", Why: "the lease write"}); err != nil {
t.Fatal(err)
}
ended() ended()
deadline := time.Now().Add(30 * time.Second) deadline := time.Now().Add(30 * time.Second)
for { for {
@@ -11,7 +11,6 @@ import (
"time" "time"
stdio "git.novox.be/novox/mesh-sdk/go" stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/handacts" "git.novox.be/novox/mesh-sdk/go/handacts"
"git.novox.be/novox/mesh-sdk/go/health" "git.novox.be/novox/mesh-sdk/go/health"
@@ -139,8 +138,8 @@ func archiveRef(module string, n int) string {
} }
// A mesh with seven done builds of letta, the n-th ending n hours after a day began, each with an image and an archive; // A mesh with seven done builds of letta, the n-th ending n hours after a day began, each with an image and an archive;
// a want names the first on ace; the store lists them all and one image no build names; the lease as given. // a want names the first on ace; the store lists them all and one image no build names; no lease key (issue 1126).
func aMesh(t *testing.T, lease *buscompose.LeaseHolder) (*Holder, *memBus, *memDoor, *[]string) { func aMesh(t *testing.T) (*Holder, *memBus, *memDoor, *[]string) {
t.Helper() t.Helper()
day := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC) day := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)
b := &memBus{states: map[string]map[string][]byte{}, history: map[string][]stdio.StateRevision{}} b := &memBus{states: map[string]map[string][]byte{}, history: map[string][]stdio.StateRevision{}}
@@ -194,9 +193,6 @@ func aMesh(t *testing.T, lease *buscompose.LeaseHolder) (*Holder, *memBus, *memD
_ = b.Put(ManifestsRead, mk, json.RawMessage(manifest)) _ = b.Put(ManifestsRead, mk, json.RawMessage(manifest))
want, _ := json.Marshal(delivery.Want{V: delivery.Version, FP: fp(1), Self: true}) want, _ := json.Marshal(delivery.Want{V: delivery.Version, FP: fp(1), Self: true})
_ = b.Put(WantsRead, "want.ace.letta", json.RawMessage(want)) _ = b.Put(WantsRead, "want.ace.letta", json.RawMessage(want))
if lease != nil {
_ = b.Put(LeaseRead, buscompose.ControllerLeaseKey, lease)
}
var acts []string var acts []string
h := &Holder{Bus: b, Door: func() (Door, error) { return door, nil }, h := &Holder{Bus: b, Door: func() (Door, error) { return door, nil },
Listed: func(context.Context) (*View, error) { return &View{L: listing}, nil }, Listed: func(context.Context) (*View, error) { return &View{L: listing}, nil },
@@ -206,18 +202,14 @@ func aMesh(t *testing.T, lease *buscompose.LeaseHolder) (*Holder, *memBus, *memD
acts = append(acts, key+" "+string(raw)) acts = append(acts, key+" "+string(raw))
return json.RawMessage(`{}`), nil return json.RawMessage(`{}`), nil
}}} }}}
h.Acts.LeaseAway = h.LeaseAway
return h, b, door, &acts return h, b, door, &acts
} }
// ADR 0571 clause 2: the keep set from the store's listing, the wants and the five newest builds; nothing let go of while // ADR 0571 clause 2, 1126/A4/AC2: the keep set from the store's listing, the wants and the five newest builds; with no
// the lease names a controller; once it names another holder, a build's end lets go of the eligible within the bound, // lease key, a build's end and a confirmed collect let go of the eligible within the bound, leave an index for a
// leaves an index for a confirmed collect, records each deletion and says artifacts-gone. // confirmed collect, record each deletion as a hand act and say artifacts-gone.
func TestTheSeatKeepsTheFiveNewestAndTheWantedAndSweepsOnlyOnceTheLeaseIsAway(t *testing.T) { func TestTheSeatKeepsTheFiveNewestAndTheWantedAndSweepsWithNoLease(t *testing.T) {
controller := &buscompose.LeaseHolder{Instance: "c1", Host: "novox", Epoch: 3} h, _, _, _ := aMesh(t)
handed := &buscompose.LeaseHolder{HandedTo: "mesh-grants", HandedBy: "person.jochen", Why: "the lease write"}
h, _, _, _ := aMesh(t, controller)
r, err := h.Artifacts("") r, err := h.Artifacts("")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -244,9 +236,10 @@ func TestTheSeatKeepsTheFiveNewestAndTheWantedAndSweepsOnlyOnceTheLeaseIsAway(t
t.Errorf("the references are %d, counted %v, at %q", len(r.References), r.Counts, r.Store) t.Errorf("the references are %d, counted %v, at %q", len(r.References), r.Counts, r.Store)
} }
all := []string{archiveRef("letta", 2), imageRef("letta", 2), mirror(1)}
for _, c := range []struct { for _, c := range []struct {
name string name string
lease *buscompose.LeaseHolder how string // "build" (a build's end), "collect" (a confirmed collect), or a sweep asked with most
index bool index bool
most int most int
letGo []string letGo []string
@@ -254,45 +247,58 @@ func TestTheSeatKeepsTheFiveNewestAndTheWantedAndSweepsOnlyOnceTheLeaseIsAway(t
skipped int skipped int
unlog int unlog int
}{ }{
{name: "the lease names a controller", lease: controller}, {name: "a build's end", how: "build", letGo: all},
{name: "no lease key", lease: nil}, {name: "a confirmed collect", how: "collect", letGo: all},
{name: "handed away", lease: handed, most: MostPerSweep, letGo: []string{archiveRef("letta", 2), imageRef("letta", 2), mirror(1)}}, {name: "a sweep", most: MostPerSweep, letGo: all},
{name: "an index waits", lease: handed, index: true, most: MostPerSweep, letGo: []string{archiveRef("letta", 2), mirror(1)}, skipped: 1}, {name: "an index waits", index: true, most: MostPerSweep, letGo: []string{archiveRef("letta", 2), mirror(1)}, skipped: 1},
{name: "bounded", lease: handed, most: 1, letGo: []string{archiveRef("letta", 2)}, left: 2}, {name: "bounded", most: 1, letGo: []string{archiveRef("letta", 2)}, left: 2},
// A kept build the log no longer holds: what it stood on is unknown, so no copy goes. // A kept build the log no longer holds: what it stood on is unknown, so no copy goes.
{name: "a kept build's bases unknown", lease: handed, most: MostPerSweep, letGo: []string{archiveRef("letta", 2), imageRef("letta", 2)}, unlog: 3}, {name: "a kept build's bases unknown", most: MostPerSweep, letGo: []string{archiveRef("letta", 2), imageRef("letta", 2)}, unlog: 3},
} { } {
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {
h, b, door, acts := aMesh(t, c.lease) h, b, door, acts := aMesh(t)
door.indexes[imageRef("letta", 2)] = c.index door.indexes[imageRef("letta", 2)] = c.index
if c.unlog > 0 { if c.unlog > 0 {
b.log = append(b.log[:c.unlog-1:c.unlog-1], b.log[c.unlog:]...) b.log = append(b.log[:c.unlog-1:c.unlog-1], b.log[c.unlog:]...)
} }
if c.most == 0 { switch c.how {
case "build":
h.AfterBuild("letta.x") h.AfterBuild("letta.x")
if _, err := h.Collect("tidy", true, 0); err == nil { var gone []string
t.Error("a confirmed collect was let through while the controller sweeps") for _, x := range door.asked {
if r, ok := strings.CutPrefix(x, "let go "); ok {
gone = append(gone, r)
} }
if len(door.asked) != 0 || len(*acts) != 0 || len(b.published) != 0 {
t.Fatalf("before the lease write the store was asked %v, acts %v, said %v", door.asked, *acts, b.published)
} }
return if fmt.Sprint(gone) != fmt.Sprint(c.letGo) {
t.Fatalf("a build's end let go of %v, want %v", gone, c.letGo)
}
default:
var a *CollectAnswer
var err error
if c.how == "collect" {
a, err = h.Collect("tidy", true, 0)
} else {
a, err = h.Sweep(context.Background(), Sweeping{Most: c.most, By: "after the build letta.x"})
} }
a, err := h.Sweep(context.Background(), Sweeping{Most: c.most, By: "after the build letta.x"})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if fmt.Sprint(a.LetGo) != fmt.Sprint(c.letGo) || a.Left != c.left || a.Skipped != c.skipped || a.Held != 6 || a.Missing != 0 { if fmt.Sprint(a.LetGo) != fmt.Sprint(c.letGo) || a.Left != c.left || a.Skipped != c.skipped || a.Held != 6 || a.Missing != 0 {
t.Fatalf("let go of %v (left %d, skipped %d, held %d, missing %d), want %v", a.LetGo, a.Left, a.Skipped, a.Held, a.Missing, c.letGo) t.Fatalf("let go of %v (left %d, skipped %d, held %d, missing %d), want %v", a.LetGo, a.Left, a.Skipped, a.Held, a.Missing, c.letGo)
} }
}
// Every kept archive held before the first deletion. // Every kept archive held before the first deletion.
first := -1 held := 0
for i, x := range door.asked { for _, x := range door.asked {
if strings.HasPrefix(x, "let go ") && first < 0 { if strings.HasPrefix(x, "let go ") {
first = i break
}
if strings.HasPrefix(x, "hold ") {
held++
} }
} }
if first != 6 { if held != 6 {
t.Errorf("the store was asked %v: the six kept archives are held first", door.asked) t.Errorf("the store was asked %v: the six kept archives are held first", door.asked)
} }
if len(*acts) != len(c.letGo) || !strings.HasPrefix((*acts)[0], "seat:mesh-hand-acts.hand-act ") || if len(*acts) != len(c.letGo) || !strings.HasPrefix((*acts)[0], "seat:mesh-hand-acts.hand-act ") ||
@@ -310,7 +316,7 @@ func TestTheSeatKeepsTheFiveNewestAndTheWantedAndSweepsOnlyOnceTheLeaseIsAway(t
// S25 (image-missing): a want whose build names an image the store does not hold is a finding in the seat's own state, // S25 (image-missing): a want whose build names an image the store does not hold is a finding in the seat's own state,
// written once and deleted once the store holds it again; images names what a machine's wants and haves name. // written once and deleted once the store holds it again; images names what a machine's wants and haves name.
func TestAWantedImageTheStoreLacksIsAFindingAndImagesNamesWantsAndHaves(t *testing.T) { func TestAWantedImageTheStoreLacksIsAFindingAndImagesNamesWantsAndHaves(t *testing.T) {
h, b, _, _ := aMesh(t, nil) h, b, _, _ := aMesh(t)
view, _ := h.Listed(context.Background()) view, _ := h.Listed(context.Background())
repo, digest, _ := splitReference(imageRef("letta", 1)) repo, digest, _ := splitReference(imageRef("letta", 1))
delete(view.L.Repos[repo].Revisions, digest) delete(view.L.Repos[repo].Revisions, digest)
@@ -1,8 +1,7 @@
// The artifact-store-tools module's Go bundle (novox/hq ADR 0251 §1–3, ADR 0571 clause 2, to-be 51): a process the // The artifact-store-tools module's Go bundle (novox/hq ADR 0251 §1–3, ADR 0571 clause 2, to-be 51): a process the
// node's runtime launches and speaks MCP over stdio to. It says what the artifact store holds — its repositories, its // node's runtime launches and speaks MCP over stdio to. It says what the artifact store holds — its repositories, its
// size, and what the mesh keeps of each manifest — read from the store's own files through the store's own container, // size, and what the mesh keeps of each manifest — read from the store's own files through the store's own container,
// and it holds the seat mesh-artifact-sweep: the keep set, the sweep at each build's end once the controller's lease names // and it holds the seat mesh-artifact-sweep: the keep set, the sweep at each build's end, and S25's image-missing.
// no controller, and S25's image-missing.
// //
// **A module beside the store, not the store's own.** A module that provides the artifact store cannot also build an // **A module beside the store, not the store's own.** A module that provides the artifact store cannot also build an
// artifact: building publishes to the store, so the store would be needed to create itself. So these tools are a // artifact: building publishes to the store, so the store would be needed to create itself. So these tools are a
@@ -29,7 +28,7 @@ func main() {
} }
h := &Holder{Bus: runtimeBus{}, Door: DoorAt(os.Getenv("MESH_ARTIFACT_STORE_ADDRESS_FILE")), Listed: s.View, h := &Holder{Bus: runtimeBus{}, Door: DoorAt(os.Getenv("MESH_ARTIFACT_STORE_ADDRESS_FILE")), Listed: s.View,
Now: time.Now, Logf: logf} Now: time.Now, Logf: logf}
h.Acts = handacts.Recorder{Ask: stdio.Ask, LeaseAway: h.LeaseAway} h.Acts = handacts.Recorder{Ask: stdio.Ask}
go follow(h) go follow(h)
// An empty name serves as the module the runtime names (MESH_SERVED_MODULE): artifact-store-tools. // An empty name serves as the module the runtime names (MESH_SERVED_MODULE): artifact-store-tools.
if err := stdio.Serve("", append(Tools(s, h, &Catalogue{Ask: stdio.Ask}), SeatTools(h, s)...)); err != nil { if err := stdio.Serve("", append(Tools(s, h, &Catalogue{Ask: stdio.Ask}), SeatTools(h, s)...)); err != nil {
@@ -436,7 +436,7 @@ func TestTheToolsServedAreTheToolsTheManifestNames(t *testing.T) {
t.Fatalf("served %v, manifest %v", served, listed) t.Fatalf("served %v, manifest %v", served, listed)
} }
// ADR 0571 clause 2: it holds the seat mesh-artifact-sweep and serves its verbs, and calls no verb of the controller's; // ADR 0571 clause 2: it holds the seat mesh-artifact-sweep and serves its verbs, and calls no verb of the controller's;
// the lease is the one controller state it reads (clause 10's exemption). // it reads no controller state, the lease neither (issue 1126).
want := []string{"seat:mesh-hand-acts.hand-act", CatalogueModules, CatalogueModule} want := []string{"seat:mesh-hand-acts.hand-act", CatalogueModules, CatalogueModule}
if !reflect.DeepEqual(m.Invokes, want) { if !reflect.DeepEqual(m.Invokes, want) {
t.Errorf("invokes %v", m.Invokes) t.Errorf("invokes %v", m.Invokes)
@@ -455,7 +455,7 @@ func TestTheToolsServedAreTheToolsTheManifestNames(t *testing.T) {
t.Errorf("claims %+v, serves %v, the seat's verbs %v", m.Claims, verbs, contract) t.Errorf("claims %+v, serves %v, the seat's verbs %v", m.Claims, verbs, contract)
} }
for _, r := range m.Reads { for _, r := range m.Reads {
if strings.HasPrefix(r, "mesh-controller.") && r != LeaseRead { if strings.HasPrefix(r, "mesh-controller.") {
t.Errorf("reads the controller's %s", r) t.Errorf("reads the controller's %s", r)
} }
} }
+1 -2
View File
@@ -27,8 +27,7 @@
"mesh-delivery-builder.builds", "mesh-delivery-builder.builds",
"mesh-delivery-builder.build-log", "mesh-delivery-builder.build-log",
"mesh-delivery-indexer.manifests", "mesh-delivery-indexer.manifests",
"node-engine.have", "node-engine.have"
"mesh-controller.lease"
], ],
"state": [ "state": [
"findings" "findings"
@@ -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` // **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 // 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 // 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 // not counted. `take-over` is refused (issue 1126): the keeper is the one source since the controller ended by its
// difference, and on the warrant writes the take-over key naming the keeper. `hand-back` is gone (issue 774): nothing // 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. // reads the take-over key any more, so it is answered as gone and changes nothing.
import ( import (
@@ -91,7 +91,7 @@ func (k *Keeper) Compare() (Comparison, error) {
} }
switch { switch {
case homed && (home.Carried || home.Replaced): 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 { if home.Replaced {
why = "ends with the controller: its fault is observed otherwise" why = "ends with the controller: its fault is observed otherwise"
} }
@@ -125,41 +125,12 @@ type Asks interface {
Ask(a asks.Ask) error 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) { func (k *Keeper) TakeOver(a Asks, by, why string) (map[string]any, error) {
if strings.TrimSpace(why) == "" { if strings.TrimSpace(why) == "" {
return nil, errors.New("a take-over says why") return nil, errors.New("a take-over says why")
} }
if k.LeaseAway() { return nil, errors.New("the keeper is the one source already: the controller ended by its unassignment (issue 1126)")
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
} }
// Warranted is the act on the operator's answer: a yes, compare still empty, writes the take-over key naming the keeper. // 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; // **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 // 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 // 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, // `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 // 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 // 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. // 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) { func (k *Keeper) holdFor(key string, f *health.Finding, open bool) (held, bool) {
if open || (f == nil && carried == nil) { if open || f == nil {
return held{}, false return held{}, false
} }
var kind, source string kind, source, subject := f.Kind, f.Source, f.Subject
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
}
if _, raisedOut := k.outOfHold[key]; raisedOut { if _, raisedOut := k.outOfHold[key]; raisedOut {
return held{}, false 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. // 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 // 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 // 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. // 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}) raw, _ := json.Marshal(delivery.Have{V: delivery.Version, FP: fp, Composed: composed, Phase: phase, Health: h})
return raw 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 { for _, c := range []struct {
name string name string
// end is the have the delivery ends with, or nil for a delivery past its bound. // 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) 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). // 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 { 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"]) 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++ held++
} }
} }
if held != 2 { if held != 1 {
t.Errorf("%s: compare lists %d held, %+v", c.name, held, cmp) 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. // Its own finding goes unless it stands.
_ = r.k.Controller("core.controller.deaf", nil, true)
if !c.standing { if !c.standing {
_ = r.k.Finding(watchman, own, nil, true) _ = 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)) { 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) 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 { if 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()) 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 // 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 // **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 // 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 // a kind that ends with the controller or that the controller never raises, and the keeper reads it to clear, once, what
// lease write, the controller's conditions of the kinds that end with it. // 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 // 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". // row is named by `compare` as "no home decided".
@@ -17,8 +17,8 @@ type Home struct {
Controller bool Controller bool
// Observer is the module whose findings raise it after the take-over; empty for a kind that ends. // Observer is the module whose findings raise it after the take-over; empty for a kind that ends.
Observer string Observer string
// Carried says it ends with the controller's lease and is carried from the controller's bucket until then // Carried says it ended with the controller: carried from its bucket while it ran (clause 9: S4, S7, S12, S13, S14,
// (clause 9: S4, S7, S12, S13, S14, S25, D5, D9, H-controller). // S25, D5, D9, H-controller), cleared once by EndCarried and never carried again (issue 1126).
Carried bool Carried bool
// Replaced says the kind itself ends and another observes its fault (D1, D10, the stuck probe): listed apart in // Replaced says the kind itself ends and another observes its fault (D1, D10, the stuck probe): listed apart in
// compare and not carried. // compare and not carried.
@@ -1,9 +1,10 @@
package main 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 // 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 // mesh's open conditions. It watches every observer's `findings`, the healers' `attempts`, and the assigner's wants and
// alone (its `conditions` are no longer read, issue 774), and the assigner's wants and the nodes' haves for the hold. From // the nodes' haves for the hold; nothing of the controller (its `conditions` are no longer read, issue 774, and its lease
// all of them it derives, per key, the condition that should be open, and writes the difference by compare-and-set, // 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 // 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). // but its finding going (ADR 0231 §14).
@@ -47,8 +48,6 @@ var Observers = []string{health.WatchmanModule, health.SelfCheckModule, health.H
// The other states it reads, as `<module>.<name>`. // The other states it reads, as `<module>.<name>`.
const ( const (
AttemptsRead = health.HealersModule + "." + health.AttemptsState AttemptsRead = health.HealersModule + "." + health.AttemptsState
ControllerLease = "mesh-controller.lease"
ControllerLeaseKey = "holder"
WantsRead = delivery.AssignerModule + ".wants" 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 // 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). // lead, 2026-10-29, for ADR 0564 clause 2's inputs).
@@ -68,7 +67,7 @@ type Store interface {
CreateTTL(state, key string, value any, ttl time.Duration) error 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 { type held struct {
Key string `json:"key"` Key string `json:"key"`
Node string `json:"node"` Node string `json:"node"`
@@ -104,8 +103,9 @@ type Keeper struct {
findings map[string]map[string]health.Finding findings map[string]map[string]health.Finding
blind map[string]string blind map[string]string
attempts map[string]health.Attempt 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 ctrl map[string]health.Condition
leaseOut bool
takeover health.Takeover takeover health.Takeover
wants map[string]delivery.Want wants map[string]delivery.Want
haves map[string]delivery.Have haves map[string]delivery.Have
@@ -209,7 +209,8 @@ func (k *Keeper) Attempt(key string, value []byte, deleted bool) error {
return k.Settle(condition) 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 { func (k *Keeper) Controller(key string, value []byte, deleted bool) error {
k.mu.Lock() k.mu.Lock()
k.init() k.init()
@@ -219,38 +220,57 @@ func (k *Keeper) Controller(key string, value []byte, deleted bool) error {
var c health.Condition var c health.Condition
if err := json.Unmarshal(value, &c); err != nil { if err := json.Unmarshal(value, &c); err != nil {
k.mu.Unlock() 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 return nil
} }
k.ctrl[key] = c k.ctrl[key] = c
} }
k.mu.Unlock() k.mu.Unlock()
return k.Settle(key)
}
// 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 nil
} }
return k.settleAll(keys)
}
// LeaseAway says whether the controller's lease is handed away. // endedWhy is why a condition of a kind that ended with the controller is cleared.
func (k *Keeper) LeaseAway() bool { const endedWhy = "its kind ended with the controller, which was unassigned (issue 1126): no finding raises it again"
// 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.mu.Lock()
defer k.mu.Unlock() k.init()
return k.leaseOut 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. // Takeover takes the keeper's own take-over key as read.
@@ -287,9 +307,9 @@ func (k *Keeper) settleAll(keys []string) error {
return nil return nil
} }
// wanted is what the inputs say key should be: a finding of an observer, or a condition carried from the controller's // wanted is what the inputs say key should be: a finding of an observer. Nil when nothing says it; nothing is carried
// bucket of a kind that ends with the controller, until the lease write (clause 9). Nil when nothing says it. // from the controller's bucket (issue 1126).
func (k *Keeper) wanted(key string) (*health.Finding, *health.Condition) { func (k *Keeper) wanted(key string) *health.Finding {
observers := make([]string, 0, len(k.findings)) observers := make([]string, 0, len(k.findings))
for o := range k.findings { for o := range k.findings {
observers = append(observers, o) observers = append(observers, o)
@@ -297,18 +317,10 @@ func (k *Keeper) wanted(key string) (*health.Finding, *health.Condition) {
sort.Strings(observers) sort.Strings(observers)
for _, o := range observers { for _, o := range observers {
if f, ok := k.findings[o][key]; ok { 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
return nil, &c
}
return nil, nil
}
func (k *Keeper) endsWithController(c health.Condition) bool {
h, ok := k.homeOf(c.Kind)
return ok && h.Carried
} }
// settleOnce is one try of Settle. // settleOnce is one try of Settle.
@@ -326,11 +338,11 @@ func (k *Keeper) settleOnce(key string) error {
} }
k.mu.Lock() k.mu.Lock()
k.init() k.init()
f, carried := k.wanted(key) f := k.wanted(key)
hold, holding := k.holdFor(key, f, carried, open) hold, holding := k.holdFor(key, f, open)
k.mu.Unlock() k.mu.Unlock()
if f == nil && carried == nil { if f == nil {
k.dropHeld(key, "its finding went while its module was delivered") k.dropHeld(key, "its finding went while its module was delivered")
if !open { if !open {
return nil return nil
@@ -341,7 +353,7 @@ func (k *Keeper) settleOnce(key string) error {
return k.keepHeld(hold) return k.keepHeld(hold)
} }
k.dropHeld(key, "") 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) events := transitions(was, next, open)
if len(events) == 0 && open && sameBody(was, next) { if len(events) == 0 && open && sameBody(was, next) {
return nil return nil
@@ -359,28 +371,16 @@ func (k *Keeper) settleOnce(key string) error {
k.mu.Unlock() k.mu.Unlock()
for _, e := range events { for _, e := range events {
e.Condition, e.At, e.Event = next, now, health.EventFor(e.Change) 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) k.tell(e)
} }
return nil 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. // 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 var c health.Condition
switch { 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: case open:
c = was c = was
k.fromFinding(&c, *f, now) 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() k.mu.Unlock()
} }
if carried == nil {
k.overlay(&c, now) k.overlay(&c, now)
}
if !open { if !open {
k.mu.Lock() k.mu.Lock()
if line := k.outOfHold[key]; line != "" { if line := k.outOfHold[key]; line != "" {
@@ -447,9 +445,7 @@ func (k *Keeper) fromFinding(c *health.Condition, f health.Finding, now time.Tim
_ = now _ = now
} }
// overlay writes the healers' attempts into the condition (clause 7) and, until the lease write, what the controller's // overlay writes the healers' attempts into the condition (clause 7); nothing of the controller's bucket (issue 1126).
// 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.
func (k *Keeper) overlay(c *health.Condition, now time.Time) { func (k *Keeper) overlay(c *health.Condition, now time.Time) {
k.mu.Lock() k.mu.Lock()
var mine []health.Attempt var mine []health.Attempt
@@ -458,18 +454,8 @@ func (k *Keeper) overlay(c *health.Condition, now time.Time) {
mine = append(mine, a) mine = append(mine, a)
} }
} }
ctrl, carry := k.ctrl[c.Key]
carry = carry && !k.leaseOut
k.mu.Unlock() k.mu.Unlock()
sort.Slice(mine, func(i, j int) bool { return mine[i].Begun.Before(mine[j].Begun) }) 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 { for _, a := range mine {
t := a.Tried() t := a.Tried()
if !containsTried(c.Tried, t) { 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 { if len(c.Tried) > health.KeptTried {
c.Tried = c.Tried[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 { 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() k.mu.Unlock()
cleared := now cleared := now
k.tell(health.Event{Condition: was, Event: health.EventCleared, At: now, Change: health.ChangeCleared, Why: why, 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 return nil
} }
@@ -623,9 +606,6 @@ func (k *Keeper) Resync() (int, error) {
seen[key] = true seen[key] = true
} }
} }
for key := range k.ctrl {
seen[key] = true
}
k.mu.Unlock() k.mu.Unlock()
keys := make([]string, 0, len(seen)) keys := make([]string, 0, len(seen))
for key := range 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 // 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. // minutes is the same fault; a silence is a transition in history.
func TestTheKeeperRaisesChangesAndClearsFromFindings(t *testing.T) { func TestTheKeeperRaisesChangesAndClearsFromFindings(t *testing.T) {
r := newRig() r := newRig()
@@ -206,25 +206,12 @@ func TestTheKeeperRaisesChangesAndClearsFromFindings(t *testing.T) {
} }
return "" 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 { for _, s := range steps {
if err := s.do(); err != nil { if err := s.do(); err != nil {
t.Fatalf("%s: %v", s.name, err) 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) t.Errorf("%s: said %q, want %q", s.name, got, s.said)
} }
if s.check != nil { 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"`) { if !strings.Contains(string(raw0), `"show":"mesh-conditions.conditions key=machine.g14.silent"`) {
t.Errorf("the first transition is %s", raw0) 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 {
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) // 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)
} }
r.said = nil r.said = nil
if err := r.k.Lease(true); err != nil { 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) t.Fatal(err)
} }
if _, open := r.open("core.controller.deaf"); open || r.changes() != "cleared core.controller.deaf" { if _, err := r.k.Resync(); err != nil {
t.Error("a carried kind outlived the lease write") t.Fatal(err)
}
}
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 // 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 // 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. // hand-back is gone (issue 774, review g1): answered as gone, naming its record, and writing nothing.
func TestCompareCountsOnlyWhatBothSidesRaise(t *testing.T) { func TestCompareCountsOnlyWhatBothSidesRaise(t *testing.T) {
r := newRig() r := newRig()
@@ -296,7 +319,7 @@ func TestCompareCountsOnlyWhatBothSidesRaise(t *testing.T) {
"delivery.x.mystery": "no home is decided for the kind mystery", "delivery.x.mystery": "no home is decided for the kind mystery",
"bus.x.consumer-wrong": "mesh-self-check has not written its finding", "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. // 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). // 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", "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 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) 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") { if _, err := r.k.TakeOver(nil, "jochen", "now"); err == nil || !strings.Contains(err.Error(), "one source already") {
t.Errorf("take-over while compare differs: %v", err) 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") || 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") { !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) t.Errorf("hand-back: %v, want it answered as gone naming its record", err)
} }
if _, _, found, _ := r.k.Store.Get(TakeoverState, health.TakeoverKey); found { 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) 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" stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asker" "git.novox.be/novox/mesh-sdk/go/asker"
"git.novox.be/novox/mesh-sdk/go/asks" "git.novox.be/novox/mesh-sdk/go/asks"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/health" "git.novox.be/novox/mesh-sdk/go/health"
) )
@@ -106,14 +105,11 @@ func Keep(k *Keeper) {
} }
return nil return nil
} }
watch(k, ControllerLease, "", func(c stdio.StateChange) error { // Once at start: what was carried from the controller's bucket while it ran is cleared (issue 1126); nothing raises
if c.Key != ControllerLeaseKey { // those kinds again.
return nil if n, err := k.EndCarried(); n > 0 || err != nil {
logf("cleared %d condition(s) of kinds that ended with the controller: %v", n, err)
} }
away, why := buscompose.LeaseHandedAway(c.Value, c.Op == "put", nil)
logf("the controller's lease: %s", why)
return said(k.Lease(away))
})
watch(k, WantsRead, "", func(c stdio.StateChange) error { return said(k.Want(c.Key, c.Value, c.Op != "put")) }) 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, 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 { 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)) { !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)) 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 { for _, o := range Observers {
reads = append(reads, o+"."+health.FindingsState) reads = append(reads, o+"."+health.FindingsState)
} }
slices.Sort(reads) slices.Sort(reads)
got := slices.Clone(m.Reads) got := slices.Clone(m.Reads)
slices.Sort(got) 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) t.Errorf("reads %v, want %v and never the healers' would", got, reads)
} }
ttl, keyTTL := map[string]int{}, map[string]bool{} 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 " + {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.", "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() }}, 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 " + {Name: Seat + ".take-over", Description: "Refused: the keeper is the one source of conditions since the " +
"keeper; refused while compare differs.", "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"), Input: obj(map[string]any{"why": str("why now")}, "why"),
Run: func(args map[string]any) (any, error) { Run: func(args map[string]any) (any, error) {
return k.TakeOver(a, stdio.CallerOf(args), strArg(args, "why")) 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) findings[o] = len(fs)
} }
return map[string]any{"findings": findings, "blind": k.blind, "attempts": len(k.attempts), 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)} "wants": len(k.wants), "haves": len(k.haves)}
} }
@@ -261,7 +261,7 @@ func TestTheKeeperWholeRaisesAndClearsOnTheBus(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) 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"} "mesh-healers_attempts"}
for _, o := range Observers { for _, o := range Observers {
buckets = append(buckets, o+"_findings") buckets = append(buckets, o+"_findings")
-1
View File
@@ -28,7 +28,6 @@
"mesh-provisioner.findings", "mesh-provisioner.findings",
"artifact-store-tools.findings", "artifact-store-tools.findings",
"mesh-healers.attempts", "mesh-healers.attempts",
"mesh-controller.lease",
"mesh-delivery-assigner.wants", "mesh-delivery-assigner.wants",
"node-engine.have", "node-engine.have",
"mesh-delivery-assigner.placement" "mesh-delivery-assigner.placement"
+2 -3
View File
@@ -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). 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 `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. 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. - 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`, falling back to the - Its sets, clears and warrants are recorded in the hand-act log of the seat `mesh-hand-acts` alone.
controller's while that seat has no holder and the controller holds its lease.
+10 -16
View File
@@ -16,9 +16,10 @@ import (
// values it moves; records the change in the history and the hand-act log; raises `config-changed`; and answers // 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. // 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 // **Trusted values on the operator's Approve alone.** A trusted value (ADR 0348 §8) is set only on the operator's
// call the controller carries from its terminal, or on the operator's warrant for a proposal (proposals.go, design // warrant for a proposal (proposals.go, design 57 K4); a set through the verb is refused and names the proposal as the
// 57 K4); a set through the verb is refused and names the proposal as the way. // 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. // ControllerCaller is the controller's bus user: the one caller that carries a value given at its terminal.
const ControllerCaller = "controller" 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, return nil, fmt.Errorf("%s is refused for %s at %s: %s", show(value), call.Key, level,
strings.Join(problems, "; ")) strings.Join(problems, "; "))
} }
if trusted && call.approval == nil && !c.atTheTerminal(call) { 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 at "+ return nil, fmt.Errorf("%s is trusted (it decides what runs or who can reach what), so it is set only on "+
"the controller's terminal or on the operator's Approve (novox/hq ADR 0348 §8); %s is not the terminal: "+ "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) "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} 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 { if !had {
return nil, fmt.Errorf("nothing is set for %s at %s, so there is nothing to clear", call.Key, level) 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) { if trusted && call.approval == nil {
return nil, fmt.Errorf("%s is trusted, so it is cleared only at the controller's terminal or on the "+ return nil, fmt.Errorf("%s is trusted, so it is cleared only on the operator's Approve (novox/hq ADR 0348 "+
"operator's Approve (novox/hq ADR 0348 §8); %s is not the terminal: propose the clear instead, with "+ "§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) "config's action propose and clear", call.Key, call.Caller)
} }
if err := c.Store.Delete(ValuesState, vk); err != nil { if err := c.Store.Delete(ValuesState, vk); err != nil {
@@ -326,13 +327,6 @@ func (c *Config) change(call Call) (*SetAnswer, error) {
return answer, nil 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 // 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). // 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) { 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 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"}, {"a value not matching its pattern", Call{Key: "origin", Value: "Home 1", Node: "g14", Module: "dunst"},
"does not match the pattern"}, "does not match the pattern"},
{"a trusted value at the mesh, not from the terminal", Call{Key: "agent-managed-settings", {"a trusted value at the mesh, not on an Approve", Call{Key: "agent-managed-settings",
Value: map[string]any{"a": 1.0}}, "set only at the controller's terminal"}, Value: map[string]any{"a": 1.0}}, "set only on the operator's Approve"},
{"a trusted module value, not from the terminal", Call{Key: "managed", Value: map[string]any{}, {"a trusted module value, not on an Approve", Call{Key: "managed", Value: map[string]any{},
Node: "g14", Module: "claude-code"}, "set only at the controller's terminal"}, 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"}, {"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"}, {"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"}, {"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). // 1126/A5/AC1: **no controller relay is trusted.** With no lease key (the controller ended by its unassignment), a trusted
func TestATrustedValueFromTheTerminalIsSetWithWhoGaveIt(t *testing.T) { // 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) c, m := aMesh(t)
a := set(t, c, Call{Key: "agent-managed-settings", Value: map[string]any{"b": 2.0, "a": 1.0}, m.mirror(t, ValuesState, "mesh.agent-managed-settings", `{"value":{"b":2},"by":"person.jochen","via":"person.jochen"}`)
Caller: ControllerCaller, By: "jochen"}) call.Caller, call.By, call.Why = ControllerCaller, "jochen", "at the terminal"
if string(a.Value) != `{"a":1,"b":2}` { _, err := c.Change(call)
t.Errorf("the value is written as %s", a.Value) if err == nil || !strings.Contains(err.Error(), "propose") {
t.Errorf("%s %s from the controller: %v", call.Action, call.Key, err)
} }
r := m.resolved(t, "g14", "claude-code", "managed") if v, _ := storedValue(t, m, "mesh.agent-managed-settings"); string(v.Value) != `{"b":2}` {
if r.Source != SourceMesh || !r.Trusted || string(r.Value) != `{"a":1,"b":2}` { t.Errorf("%s %s from the controller left %s", call.Action, call.Key, v.Value)
t.Errorf("claude-code's managed settings resolve as %+v", r) }
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)
} }
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)
} }
} }
+2 -11
View File
@@ -19,7 +19,6 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go" stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asks" "git.novox.be/novox/mesh-sdk/go/asks"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/handacts" "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 // 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. // 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 { var recorder = handacts.Recorder{Ask: stdio.Ask}
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
}}
// HandAct records a change in the hand-act log (to-be 45 §7). // HandAct records a change in the hand-act log (to-be 45 §7).
func (runtimeStore) HandAct(what, why, cause string) error { 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 " + "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 " + "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 " + "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 " + "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 " + "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 " + "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", ProposalsState: "config proposals", PullsState: "config list", KeptState: "config list", HistoryLog: "config history",
DeclaredState: DeclaredTool, TakeoverState: DeclaredTool, DeclaredState: DeclaredTool, TakeoverState: DeclaredTool,
PlacementRead: CompareTool, WantsRead: CompareTool, BuildsRead: CompareTool, ModulesRead: 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, "a resolution": ResyncTool, "the indexer's candidates taken": ResyncTool, "a take-over decided": TakeOverTool,
} }
served := map[string]bool{} served := map[string]bool{}
@@ -266,8 +266,8 @@ func TestATrustedValueIsProposedAskedUnderItsOwnNameAndSetOnTheOperatorsApprove(
func TestAnApproveIsRefusedWhenTheStoredValueChangedSince(t *testing.T) { func TestAnApproveIsRefusedWhenTheStoredValueChangedSince(t *testing.T) {
c, m, r := aProposingMesh(t) c, m, r := aProposingMesh(t)
a := propose(t, c, Call{Key: "agent-managed-settings", Value: map[string]any{"a": 1.0}}) a := propose(t, c, Call{Key: "agent-managed-settings", Value: map[string]any{"a": 1.0}})
// Set at the terminal meanwhile. // Changed meanwhile.
set(t, c, Call{Key: "agent-managed-settings", Value: map[string]any{"b": 2.0}, Caller: ControllerCaller, By: "jochen"}) m.mirror(t, ValuesState, "mesh.agent-managed-settings", `{"value":{"b":2},"by":"person.jochen","via":"person.jochen"}`)
m.events = nil m.events = nil
r.choose(t, a.Proposal, OptionApprove, nil) r.choose(t, a.Proposal, OptionApprove, nil)
said, _ := c.Decide() said, _ := c.Decide()
@@ -501,8 +501,7 @@ func TestAClearIsProposedAndATrustedSetNamesTheProposal(t *testing.T) {
if err == nil || !strings.Contains(err.Error(), "propose it instead") { if err == nil || !strings.Contains(err.Error(), "propose it instead") {
t.Errorf("a trusted set through the verb: %v", err) 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", m.mirror(t, ValuesState, "module.g14.claude-code.managed", `{"value":{"a":1},"by":"person.jochen","via":"person.jochen"}`)
Caller: ControllerCaller, By: "jochen"})
a := propose(t, c, Call{Key: "managed", Clear: true, Node: "g14", Module: "claude-code"}) a := propose(t, c, Call{Key: "managed", Clear: true, Node: "g14", Module: "claude-code"})
ask := r.asks[a.Proposal] ask := r.asks[a.Proposal]
if !strings.HasPrefix(ask.Headline, "Clear managed for claude-code on g14") || if !strings.HasPrefix(ask.Headline, "Clear managed for claude-code on g14") ||
@@ -26,7 +26,6 @@ import (
"time" "time"
"git.novox.be/novox/mesh-sdk/go/asks" "git.novox.be/novox/mesh-sdk/go/asks"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/delivery" "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 and CandidatesRead are the indexer's states (ADR 0565 §2).
IndexerPredefinedRead = delivery.IndexerModule + "." + delivery.IndexerPredefinedState IndexerPredefinedRead = delivery.IndexerModule + "." + delivery.IndexerPredefinedState
CandidatesRead = delivery.IndexerModule + "." + delivery.IndexerConfigCandidatesState 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. // The sources the switch names.
@@ -95,17 +92,6 @@ func (c *Config) source() (Switch, error) {
return s, nil 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 -------------------------------------------------------------------------------------------- // ---- declared --------------------------------------------------------------------------------------------
// DeclaredKept is one key of `declared`: a module's declarations as the manifest the indexer's `modules.<module>` // 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' // 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 // 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).
// lease write a trusted value is set only on an Approve; declared is written once per manifest (ADR 0565 §3, §4, §13).
func TestTheTakeOverAndTheHandBack(t *testing.T) { func TestTheTakeOverAndTheHandBack(t *testing.T) {
c, m, r := anAgreeingMesh(t) c, m, r := anAgreeingMesh(t)
steps := []struct { steps := []struct {
@@ -174,15 +173,6 @@ func TestTheTakeOverAndTheHandBack(t *testing.T) {
res, err := c.Reconcile() res, err := c.Reconcile()
return res.SourceGone + "|" + string(m.resolved(t, "g14", "dunst", "width").Value), err return res.SourceGone + "|" + string(m.resolved(t, "g14", "dunst", "width").Value), err
}, "is gone: config no longer reads its records"}, }, "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 { for _, s := range steps {
got, err := s.do() got, err := s.do()
+1 -2
View File
@@ -37,8 +37,7 @@
"mesh-delivery-indexer.modules", "mesh-delivery-indexer.modules",
"mesh-delivery-indexer.manifests", "mesh-delivery-indexer.manifests",
"mesh-delivery-indexer.predefined", "mesh-delivery-indexer.predefined",
"mesh-delivery-indexer.config-candidates", "mesh-delivery-indexer.config-candidates"
"mesh-controller.lease"
], ],
"tools": [ "tools": [
"config_compare", "config_compare",
+8 -8
View File
@@ -15,15 +15,15 @@ Each act is begun by a create of its key in `attempts` before it is made and fin
mid-act has spent it and a second instance cannot begin it again. The condition keeper writes the condition's mid-act has spent it and a second instance cannot begin it again. The condition keeper writes the condition's
`tried`, resolver and escalation from `attempts`; a healer writes no condition. `tried`, resolver and escalation from `attempts`; a healer writes no condition.
**Only once the controller's lease names no controller.** Until the write that hands the lease away, each act it would **They act, and no lease turns it** (issue 1126): the controller ended by its unassignment. While it ran, each act a
take is written to `would`, never to `attempts`: it counts for no budget, settle or brake and reaches no condition. healer would take was written to `would`, never to `attempts`; nothing writes `would` any more.
Each key of `attempts` and `would` lives a week from its last write, its own time to live (the buckets are declared Each key of `attempts` and `would` lives a week from its last write, its own time to live (the buckets are declared
`key-ttl`): nothing else deletes `would`, and once the lease is handed away nothing is written there. The healers' waits `key-ttl`): nothing else deletes `would`. The healers' waits
are keys of `wakes` whose time to live is the wait: `settle.<condition key>` ends with a settle and judges the condition are keys of `wakes` whose time to live is the wait: `settle.<condition key>` ends with a settle and judges the condition
again, `fortnight` ends when S15's oldest counted act leaves the fortnight and counts again. The watch of `wakes` hears again, `fortnight` ends when S15's oldest counted act leaves the fortnight and counts again. The watch of `wakes` hears
each expiry; no timer runs in the process. A wait still running is on the bus across a restart; one that expired while each expiry; no timer runs in the process. A wait still running is on the bus across a restart; one that expired while
no instance watched is judged at the start, once the lease is first read. no instance watched is judged at the start.
S15: it hears the seat mesh-hand-acts' `act-recorded` and watches the controller's hand-act log and writes `healer-wanted` for a cause repaired by hand twice within a S15: it hears the seat mesh-hand-acts' `act-recorded` and watches the controller's hand-act log and writes `healer-wanted` for a cause repaired by hand twice within a
fortnight; a drill and a person's decision by design never count. fortnight; a drill and a person's decision by design never count.
@@ -31,8 +31,8 @@ fortnight; a drill and a person's decision by design never count.
## What it keeps ## What it keeps
- `mesh-healers_attempts`: every act and escalation, each key kept a week. - `mesh-healers_attempts`: every act and escalation, each key kept a week.
- `mesh-healers_would`: the newest act it would take per healer and condition while the controller holds the lease, - `mesh-healers_would`: the newest act it would take per healer and condition while the controller ran, each key
each key kept a week. kept a week; nothing writes it any more.
- `mesh-healers_findings`: the brake, its own blindness, and S15. - `mesh-healers_findings`: the brake, its own blindness, and S15.
- `mesh-healers_wakes`: the waits, each key living as long as it waits. - `mesh-healers_wakes`: the waits, each key living as long as it waits.
@@ -41,6 +41,6 @@ fortnight; a drill and a person's decision by design never count.
- `mesh-healers.healers`: the registry, the acts of the last days, the would-acts, the brake, whether it acts and why; - `mesh-healers.healers`: the registry, the acts of the last days, the would-acts, the brake, whether it acts and why;
`recount=true` counts the hand-act log again. `recount=true` counts the hand-act log again.
- `mesh-healers.heal`: one act now on an open condition, by key and why. - `mesh-healers.heal`: one act now on an open condition, by key and why.
- `mesh-healers.drill`: records a drill in the hand-act log of the seat `mesh-hand-acts` as `hand-act drill` (the - `mesh-healers.drill`: records a drill in the hand-act log of the seat `mesh-hand-acts` as `hand-act drill`; with
controller's `drill` while that seat has no holder and the controller holds its lease). S15 never counts a drill. no holder of that seat it is refused. S15 never counts a drill.
- `healers_findings`, `healers_would`, `healers_wakes`: the reads of its buckets. - `healers_findings`, `healers_would`, `healers_wakes`: the reads of its buckets.
@@ -9,10 +9,9 @@ package main
// healer's to say: the condition clears when its observer no longer sees it, and the condition keeper writes `tried`, // healer's to say: the condition clears when its observer no longer sees it, and the condition keeper writes `tried`,
// the resolver and the escalation into the condition from `attempts`. // the resolver and the escalation into the condition from `attempts`.
// //
// **Only once the controller's lease names no controller** (ADR 0564 §8): until then each act it would take is // **They act, and no lease turns it** (issue 1126): the controller ended by its unassignment, so the gate on its lease
// written to the bucket `would`, never to `attempts`, so a would-act counts for no budget, settle or brake, reaches // (ADR 0564 §8) is gone. The bucket `would` holds what they would have done while it ran, each key kept a week (its
// no `tried` and escalates nothing. `would` keeps each key a week (its bucket's age): nothing else deletes it, and once // bucket's age); nothing writes there any more.
// the lease is handed away nothing is written there again.
import ( import (
"encoding/json" "encoding/json"
@@ -42,8 +41,6 @@ const (
AttemptsState = health.AttemptsState AttemptsState = health.AttemptsState
WouldState = health.WouldState WouldState = health.WouldState
FindingsState = health.FindingsState FindingsState = health.FindingsState
LeaseRead = buscompose.ControllerLease
LeaseKey = buscompose.ControllerLeaseKey
ConditionsRead = "mesh-conditions.conditions" ConditionsRead = "mesh-conditions.conditions"
ActedEvent = health.EventHealerActed ActedEvent = health.EventHealerActed
// ActedSubject is where the seat says it, which its consumers hear (catalogue.SeatSays). // ActedSubject is where the seat says it, which its consumers hear (catalogue.SeatSays).
@@ -115,8 +112,8 @@ type Condition struct {
func (c Condition) Escalated() bool { return c.Resolver == "operator" && len(c.Tried) > 0 } func (c Condition) Escalated() bool { return c.Resolver == "operator" && len(c.Tried) > 0 }
// Attempt, Would and Acted are mesh-sdk's health layouts: an act begun in `attempts` (its key // Attempt, Would and Acted are mesh-sdk's health layouts: an act begun in `attempts` (its key
// `<healer>.<condition key>.<begun ns>` is the healers' own), an act written to `would` while the lease names a // `<healer>.<condition key>.<begun ns>` is the healers' own), an act written to `would` while the controller ran
// controller (one key per healer and condition, `<healer>.<condition key>`, its newest), and the body of healer-acted, // (one key per healer and condition, `<healer>.<condition key>`, its newest), and the body of healer-acted,
// epoch left out (ADR 0564 Limits 10). // epoch left out (ADR 0564 Limits 10).
type ( type (
Attempt = health.Attempt Attempt = health.Attempt
@@ -182,48 +179,14 @@ type Healers struct {
Registry []Healer Registry []Healer
mu sync.Mutex mu sync.Mutex
// lease is the controller's lease as last read: acting is on only while it is handed away.
acting bool
leaseWhy string
// declined is why each condition was last passed over, said once. // declined is why each condition was last passed over, said once.
declined map[string]string declined map[string]string
// acts are the hand-act log as watched, for S15. // acts are the hand-act log as watched, for S15.
acts map[string]HandAct acts map[string]HandAct
} }
// LeaseHeard records what the lease says: acting is on only while it names a holder that is no controller (ADR 0564 // actingWhy is why the healers act: what the tool `healers` says.
// §8, Limits 8, as ADR 0561 §3's reader reads it). const actingWhy = "the healers act: the controller ended by its unassignment, so no lease turns them (issue 1126)"
func (h *Healers) LeaseHeard(raw []byte, found bool, readErr error) {
on, why := buscompose.LeaseHandedAway(raw, found, readErr)
h.mu.Lock()
flipped := on != h.acting || h.leaseWhy == ""
h.acting, h.leaseWhy = on, leaseWords(on, why)
h.mu.Unlock()
if flipped {
h.Logf("[%s] the healers %s: %s", Module, map[bool]string{true: "ACT", false: "act in shadow"}[on], h.leaseWhy)
}
}
// leaseWords says the lease's state as the healers read it; the SDK's words speak of minting.
func leaseWords(on bool, why string) string {
if on {
return "the controller's lease is handed away: the healers act"
}
if strings.Contains(why, "cannot be read") || strings.Contains(why, "not readable") {
return "the controller's lease cannot be read, so it is taken to name a controller: every act is written as would"
}
return "the controller's lease names a controller (or none is written, so one may take it): every act is written as would"
}
// Acting says whether the healers act, and why.
func (h *Healers) Acting() (bool, string) {
h.mu.Lock()
defer h.mu.Unlock()
if h.leaseWhy == "" {
return false, "the controller's lease has not been read yet: every act is written as would"
}
return h.acting, h.leaseWhy
}
// healerFor is the healer that acts here on a kind. // healerFor is the healer that acts here on a kind.
func (h *Healers) healerFor(kind string) (Healer, bool) { func (h *Healers) healerFor(kind string) (Healer, bool) {
@@ -235,7 +198,7 @@ func (h *Healers) healerFor(kind string) (Healer, bool) {
return Healer{}, false return Healer{}, false
} }
// Heard is a condition the keeper raised or changed: judged, and acted on, written as would, or passed over. // Heard is a condition the keeper raised or changed: judged, and acted on or passed over.
func (h *Healers) Heard(body []byte) error { func (h *Healers) Heard(body []byte) error {
var c Condition var c Condition
if err := json.Unmarshal(body, &c); err != nil { if err := json.Unmarshal(body, &c); err != nil {
@@ -265,13 +228,6 @@ func (h *Healers) Judge(c Condition, by string) (string, error) {
} }
delete(h.declined, c.Key) delete(h.declined, c.Key)
now := h.Now() now := h.Now()
if !h.acting {
w := Would{Healer: row.ID, Condition: c.Key, Kind: c.Kind, Act: row.Repair, At: now.UTC(), Why: h.whyNot()}
if err := h.Store.PutTTL(WouldState, row.ID+"."+c.Key, w, KeptFor); err != nil {
return "", fmt.Errorf("the would-act of %s on %s is not written: %w", row.ID, c.Key, err)
}
return fmt.Sprintf("healer %s would act on %s: %s", row.ID, c.Key, w.Why), nil
}
all, err := h.attempts() all, err := h.attempts()
if err != nil { if err != nil {
h.blind(err) h.blind(err)
@@ -299,13 +255,6 @@ func (h *Healers) Judge(c Condition, by string) (string, error) {
return h.act(row, c, budget, len(spent), by) return h.act(row, c, budget, len(spent), by)
} }
func (h *Healers) whyNot() string {
if h.leaseWhy == "" {
return "the controller's lease has not been read yet"
}
return h.leaseWhy
}
// act is one healer's act: begun by a create in attempts, made, finished, and said. // act is one healer's act: begun by a create in attempts, made, finished, and said.
func (h *Healers) act(row Healer, c Condition, budget string, spent int, by string) (string, error) { func (h *Healers) act(row Healer, c Condition, budget string, spent int, by string) (string, error) {
begun := h.Now().UTC() begun := h.Now().UTC()
@@ -379,7 +328,7 @@ func (h *Healers) tell(e Acted) {
} }
} }
// Rearm is the healers' start, once the lease is first read: a settle whose key expired while no instance watched has // Rearm is the healers' start: a settle whose key expired while no instance watched has
// no expiry left to hear, so each newest act whose settle ended within its window is judged at once. A settle still // no expiry left to hear, so each newest act whose settle ended within its window is judged at once. A settle still
// running keeps its key on the bus and needs nothing. // running keeps its key on the bus and needs nothing.
func (h *Healers) Rearm() { func (h *Healers) Rearm() {
@@ -611,14 +560,14 @@ func (h *Healers) see(source string, found []Observation) {
// --- H3 -------------------------------------------------------------------------------------------------------- // --- H3 --------------------------------------------------------------------------------------------------------
// moduleOf is the module whose objects a condition names: a consumer of a module's own (`<stream>.<module>`). The // moduleOf is the module whose objects a condition names: a consumer of a module's own (`<stream>.<module>`). The
// controller's own consumers are not a module's: the controller asserts them itself until its lease. // controller's own consumers are not a module's: they go with the controller's leftovers (issue 1126).
func moduleOf(c Condition) (string, string) { func moduleOf(c Condition) (string, string) {
stream, consumer, ok := strings.Cut(c.Subject.ID, ".") stream, consumer, ok := strings.Cut(c.Subject.ID, ".")
if c.Kind != "consumer-lost" || !ok || stream == "" || consumer == "" { if c.Kind != "consumer-lost" || !ok || stream == "" || consumer == "" {
return "", "it names no consumer of a module whose objects the grants part asserts" return "", "it names no consumer of a module whose objects the grants part asserts"
} }
if consumer == buscompose.ControllerName || strings.HasPrefix(consumer, buscompose.ControllerName+"-") { if consumer == buscompose.ControllerName || strings.HasPrefix(consumer, buscompose.ControllerName+"-") {
return "", "the consumer is the controller's own, which the controller asserts itself while it holds its lease" return "", "the consumer is the controller's own, which the grants part does not assert"
} }
return consumer, "" return consumer, ""
} }
@@ -154,10 +154,6 @@ func (w *world) fresh() {
} }
} }
func (w *world) handAway() {
w.h.LeaseHeard([]byte(`{"handed-to":"the operator's hand-over"}`), true, nil)
}
// lost is a module's consumer the bus does not hold, as D6 raises it. // lost is a module's consumer the bus does not hold, as D6 raises it.
func lost(module string) Condition { func lost(module string) Condition {
return Condition{Key: "bus.EVENTS." + module + ".missing", Kind: "consumer-lost", Severity: "warning", Resolver: "self", return Condition{Key: "bus.EVENTS." + module + ".missing", Kind: "consumer-lost", Severity: "warning", Resolver: "self",
@@ -170,47 +166,21 @@ func body(c Condition) []byte {
return raw return raw
} }
// No act is made while the lease names a controller: each is written to would, never to attempts, so it counts for no // 1126/A4/AC1: with no lease key read at all (the controller ended by its unassignment), a condition a healer covers is
// budget or brake and nothing is asked or said; one is made from the write that hands the lease away (ADR 0564 §8). // acted on: one attempt, finished, asked and said, and nothing written to would.
func TestTheHealersActOnlyOnceTheLeaseNamesNoController(t *testing.T) { func TestTheHealersActWithNoLease(t *testing.T) {
cases := []struct {
name string
lease func(h *Healers)
acts bool
}{
{"never read", func(*Healers) {}, false},
{"absent", func(h *Healers) { h.LeaseHeard(nil, false, nil) }, false},
{"unreadable", func(h *Healers) { h.LeaseHeard(nil, false, errors.New("timeout")) }, false},
{"a controller holds it", func(h *Healers) {
h.LeaseHeard([]byte(`{"instance":"novox/1/2026-10-27","epoch":57}`), true, nil)
}, false},
{"handed away", func(h *Healers) { h.LeaseHeard([]byte(`{"handed-to":"the operator"}`), true, nil) }, true},
}
for _, tc := range cases {
w := newWorld() w := newWorld()
tc.lease(w.h)
if err := w.h.Heard(body(lost("records"))); err != nil { if err := w.h.Heard(body(lost("records"))); err != nil {
t.Fatalf("%s: %v", tc.name, err) t.Fatal(err)
} }
attempts, would := w.store.keys(AttemptsState), w.store.keys(WouldState) attempts, would := w.store.keys(AttemptsState), w.store.keys(WouldState)
if tc.acts {
if len(attempts) != 1 || len(would) != 0 || len(w.asked) != 1 || !strings.Contains(w.asked[0], `"module":"records"`) || if len(attempts) != 1 || len(would) != 0 || len(w.asked) != 1 || !strings.Contains(w.asked[0], `"module":"records"`) ||
len(w.said) != 1 || w.said[0].Outcome != OutcomeActed { len(w.said) != 1 || w.said[0].Outcome != OutcomeActed {
t.Errorf("%s: attempts %v, would %v, asked %v, said %+v", tc.name, attempts, would, w.asked, w.said) t.Fatalf("attempts %v, would %v, asked %v, said %+v", attempts, would, w.asked, w.said)
continue
} }
a := w.store.attempt(t, attempts[0]) a := w.store.attempt(t, attempts[0])
if a.Outcome != OutcomeActed || a.Finished == nil || a.Healer != "H3" || a.Condition != "bus.EVENTS.records.missing" { if a.Outcome != OutcomeActed || a.Finished == nil || a.Healer != "H3" || a.Condition != "bus.EVENTS.records.missing" {
t.Errorf("%s: the attempt is %+v", tc.name, a) t.Errorf("the attempt is %+v", a)
}
continue
}
if len(attempts) != 0 || len(w.asked) != 0 || len(w.said) != 0 || !slices.Equal(would, []string{"H3.bus.EVENTS.records.missing"}) {
t.Errorf("%s: attempts %v, would %v, asked %v, said %v", tc.name, attempts, would, w.asked, w.said)
}
if _, braked, _ := w.store.Get(FindingsState, brakeKey); braked {
t.Errorf("%s: a would-act moved the brake", tc.name)
}
} }
} }
@@ -219,7 +189,6 @@ func TestTheHealersActOnlyOnceTheLeaseNamesNoController(t *testing.T) {
// in an hour and lets go an hour after the last (ADR 0231 §21, ADR 0564 §7). // in an hour and lets go an hour after the last (ADR 0231 §21, ADR 0564 §7).
func TestBudgetSettleEscalationAndBrake(t *testing.T) { func TestBudgetSettleEscalationAndBrake(t *testing.T) {
w := newWorld() w := newWorld()
w.handAway()
c := lost("records") c := lost("records")
_ = w.store.Put(ConditionsRead, c.Key, c) _ = w.store.Put(ConditionsRead, c.Key, c)
settle := settlePrefix + c.Key settle := settlePrefix + c.Key
@@ -247,7 +216,6 @@ func TestBudgetSettleEscalationAndBrake(t *testing.T) {
switch s.how { switch s.how {
case "restart": case "restart":
w.fresh() w.fresh()
w.handAway()
w.h.Rearm() w.h.Rearm()
case "expire": case "expire":
w.h.WakeHeard(settle, OpExpire) w.h.WakeHeard(settle, OpExpire)
@@ -277,28 +245,24 @@ func TestBudgetSettleEscalationAndBrake(t *testing.T) {
} }
// A settle whose key expired while no instance watched is judged at the start. // A settle whose key expired while no instance watched is judged at the start.
r := newWorld() r := newWorld()
r.handAway()
_ = r.store.Put(ConditionsRead, c.Key, c) _ = r.store.Put(ConditionsRead, c.Key, c)
if _, err := r.h.Judge(c, ""); err != nil { if _, err := r.h.Judge(c, ""); err != nil {
t.Fatal(err) t.Fatal(err)
} }
r.now = r.now.Add(7 * time.Minute) r.now = r.now.Add(7 * time.Minute)
r.fresh() r.fresh()
r.handAway()
r.h.Rearm() r.h.Rearm()
if len(r.said) != 2 || r.said[1].Outcome != OutcomeEscalated { if len(r.said) != 2 || r.said[1].Outcome != OutcomeEscalated {
t.Errorf("a settle that ended unheard is not judged at the start: %+v", r.said) t.Errorf("a settle that ended unheard is not judged at the start: %+v", r.said)
} }
// A second instance cannot begin the act an attempt's key already holds. // A second instance cannot begin the act an attempt's key already holds.
x := newWorld() x := newWorld()
x.handAway()
_ = x.store.CreateTTL(AttemptsState, attemptKey("H3", lost("mail").Key, x.now.UTC()), Attempt{}, KeptFor) _ = x.store.CreateTTL(AttemptsState, attemptKey("H3", lost("mail").Key, x.now.UTC()), Attempt{}, KeptFor)
if said, _ := x.h.Judge(lost("mail"), ""); !strings.Contains(said, "another instance") || len(x.asked) != 0 { if said, _ := x.h.Judge(lost("mail"), ""); !strings.Contains(said, "another instance") || len(x.asked) != 0 {
t.Errorf("a second instance acted: %q, asked %v", said, x.asked) t.Errorf("a second instance acted: %q, asked %v", said, x.asked)
} }
// The brake: twelve acts in the hour stop every healer, said as an urgent finding. // The brake: twelve acts in the hour stop every healer, said as an urgent finding.
b := newWorld() b := newWorld()
b.handAway()
for i := 0; i < BrakeLimit; i++ { for i := 0; i < BrakeLimit; i++ {
b.now = b.now.Add(time.Minute) b.now = b.now.Add(time.Minute)
_ = b.store.CreateTTL(AttemptsState, attemptKey("H3", "x", b.now), Attempt{Healer: "H3", Condition: "x", BudgetKey: "x", _ = b.store.CreateTTL(AttemptsState, attemptKey("H3", "x", b.now), Attempt{Healer: "H3", Condition: "x", BudgetKey: "x",
@@ -325,7 +289,6 @@ func TestBudgetSettleEscalationAndBrake(t *testing.T) {
} }
// Blind to what they did, they act on nothing and say so. // Blind to what they did, they act on nothing and say so.
z := newWorld() z := newWorld()
z.handAway()
z.store.keysErr[AttemptsState] = errors.New("the bus is away") z.store.keysErr[AttemptsState] = errors.New("the bus is away")
if _, err := z.h.Judge(lost("mail"), ""); err == nil || len(z.asked) != 0 { if _, err := z.h.Judge(lost("mail"), ""); err == nil || len(z.asked) != 0 {
t.Errorf("blind, the healers acted: %v", err) t.Errorf("blind, the healers acted: %v", err)
@@ -350,7 +313,6 @@ func TestTheRegistryAndWhatH3Answers(t *testing.T) {
} }
for _, tc := range cases { for _, tc := range cases {
w := newWorld() w := newWorld()
w.handAway()
said, _ := w.h.Judge(tc.c, "") said, _ := w.h.Judge(tc.c, "")
if acted := len(w.asked) == 1; acted != tc.holds { if acted := len(w.asked) == 1; acted != tc.holds {
t.Errorf("%s: acted %v, want %v (%s)", tc.c.Key, acted, tc.holds, said) t.Errorf("%s: acted %v, want %v (%s)", tc.c.Key, acted, tc.holds, said)
@@ -420,7 +382,6 @@ func TestACauseRepairedByHandTwiceIsAHealerWanted(t *testing.T) {
// answer every bucket. // answer every bucket.
func TestTheVerbsAndReads(t *testing.T) { func TestTheVerbsAndReads(t *testing.T) {
w := newWorld() w := newWorld()
w.handAway()
c := lost("records") c := lost("records")
_ = w.store.Put(ConditionsRead, c.Key, c) _ = w.store.Put(ConditionsRead, c.Key, c)
if _, err := w.h.Heal(c.Key, "", "person.jochen"); err == nil { if _, err := w.h.Heal(c.Key, "", "person.jochen"); err == nil {
@@ -444,28 +405,19 @@ func TestTheVerbsAndReads(t *testing.T) {
if a["acting"] != true || len(a["acts"].([]Attempt)) != 1 || len(a["healers"].([]map[string]any)) != 3 { if a["acting"] != true || len(a["acts"].([]Attempt)) != 1 || len(a["healers"].([]map[string]any)) != 3 {
t.Errorf("healers answered %v", a) t.Errorf("healers answered %v", a)
} }
// drill records into mesh-hand-acts as a drill, and never into the controller's log, whoever holds the lease // drill records into mesh-hand-acts as a drill, and never into the controller's log (ADR 0565 §11, issue 1955).
// (ADR 0565 §11, issue 1955).
drills := []struct { drills := []struct {
name string name string
seatGone bool seatGone bool
leaseAway bool
args map[string]any args map[string]any
log, err string log, err string
}{ }{
{"into the seat", false, true, map[string]any{"what": "stopped records", "why": "does it raise"}, handacts.LogSeat, ""}, {"into the seat", false, map[string]any{"what": "stopped records", "why": "does it raise"}, handacts.LogSeat, ""},
{"into the seat under a controller's lease", false, false, map[string]any{"what": "stopped records", "why": "x"}, handacts.LogSeat, ""}, {"no holder: refused, the controller's not asked", true, map[string]any{"what": "stopped records", "why": "x"}, "", "nothing was recorded"},
{"no holder: refused, the controller's not asked", true, false, map[string]any{"what": "stopped records", "why": "x"}, "", "nothing was recorded"}, {"says nothing", false, map[string]any{}, "", "says what"},
{"no holder, lease away", true, true, map[string]any{"what": "stopped records", "why": "x"}, "", "nothing was recorded"},
{"says nothing", false, true, map[string]any{}, "", "says what"},
} }
for _, d := range drills { for _, d := range drills {
x := newWorld() x := newWorld()
if d.leaseAway {
x.handAway()
} else {
x.h.LeaseHeard([]byte(`{"instance":"novox/1","epoch":5}`), true, nil)
}
var asked []string var asked []string
x.h.Ask = func(key string, body any) (json.RawMessage, error) { x.h.Ask = func(key string, body any) (json.RawMessage, error) {
raw, _ := json.Marshal(body) raw, _ := json.Marshal(body)
+6 -12
View File
@@ -1,8 +1,8 @@
// mesh-healers: the holder of the seat mesh-healers (novox/hq ADR 0564 §7, §8). A Go bundle served by a runtime of its // mesh-healers: the holder of the seat mesh-healers (novox/hq ADR 0564 §7, §8). A Go bundle served by a runtime of its
// own, on its own bus account (`runs-as`), so that its account alone writes its buckets: `attempts` (every act begun, // own, on its own bus account (`runs-as`), so that its account alone writes its buckets: `attempts` (every act begun,
// made and finished, and every escalation, kept a week), `would` (what it would have done while the controller holds // made and finished, and every escalation, kept a week), `would` (what it would have done while the controller ran,
// its lease, kept a week) and `findings` (the brake, its blindness, and S15). It hears the condition keeper's // kept a week; nothing writes it any more) and `findings` (the brake, its blindness, and S15). It hears the condition keeper's
// condition-raised and condition-changed on a durable consumer, watches the controller's lease, and counts the seat's hand acts // condition-raised and condition-changed on a durable consumer, and counts the seat's hand acts
// for S15. stdout is the MCP channel; what this module says, it says on stderr. // for S15. stdout is the MCP channel; what this module says, it says on stderr.
package main package main
@@ -11,7 +11,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"sync"
"time" "time"
stdio "git.novox.be/novox/mesh-sdk/go" stdio "git.novox.be/novox/mesh-sdk/go"
@@ -56,14 +55,9 @@ func main() {
h := &Healers{Store: runtimeStore{}, Registry: Registry(), Now: time.Now, Logf: logf, h := &Healers{Store: runtimeStore{}, Registry: Registry(), Now: time.Now, Logf: logf,
Ask: func(key string, body any) (json.RawMessage, error) { return stdio.Ask(key, body) }, Ask: func(key string, body any) (json.RawMessage, error) { return stdio.Ask(key, body) },
Publish: stdio.SeatPublish} Publish: stdio.SeatPublish}
// The lease first: until it is read the healers write would-acts only. Once it is first read, a settle that ended // At start, a settle that ended while no instance watched is judged (Rearm); the waits still running are keys on
// while no instance watched is judged (Rearm); the waits still running are keys on the bus, heard on their expiry. // the bus, heard on their expiry.
var started sync.Once go h.Rearm()
go stdio.State(LeaseRead).Follow(LeaseKey, func(c stdio.StateChange) error {
h.LeaseHeard(c.Value, c.Op == "put", nil)
started.Do(func() { go h.Rearm() })
return nil
})
go stdio.State(WakesState).Follow("", func(c stdio.StateChange) error { go stdio.State(WakesState).Follow("", func(c stdio.StateChange) error {
h.WakeHeard(c.Key, c.Op) h.WakeHeard(c.Key, c.Op)
return nil return nil
@@ -26,7 +26,7 @@ type manifest struct {
// The manifest says what the code does: it claims the seat mesh-healers once for the mesh with its verbs (ADR 0564 §1, // The manifest says what the code does: it claims the seat mesh-healers once for the mesh with its verbs (ADR 0564 §1,
// §7), runs on its own account, says healer-acted, hears the keeper's raised and changed, asks only the grants part's // §7), runs on its own account, says healer-acted, hears the keeper's raised and changed, asks only the grants part's
// assert, reads the lease, the keeper's conditions and the hand-act log, and keeps attempts and would a week each. // assert, reads the keeper's conditions (never the controller's lease, issue 1126) and the hand-act log, and keeps attempts and would a week each.
// Every state it keeps has a tool that reads it, and every act it takes by itself a tool that starts it (ADR 0564 §12). // Every state it keeps has a tool that reads it, and every act it takes by itself a tool that starts it (ADR 0564 §12).
func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) { func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
raw, err := os.ReadFile(filepath.Join("..", "..", "module.json")) raw, err := os.ReadFile(filepath.Join("..", "..", "module.json"))
@@ -50,7 +50,7 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
{"emits (healer-acted is the seat's, said on its subject)", m.Emits, nil}, {"emits (healer-acted is the seat's, said on its subject)", m.Emits, nil},
{"consumes", m.Consumes, []string{RaisedEvent, ChangedEvent, RecordedEvent}}, {"consumes", m.Consumes, []string{RaisedEvent, ChangedEvent, RecordedEvent}},
{"invokes", m.Invokes, []string{AssertTool, "seat:mesh-hand-acts.hand-act", "seat:mesh-hand-acts.hand-acts"}}, {"invokes", m.Invokes, []string{AssertTool, "seat:mesh-hand-acts.hand-act", "seat:mesh-hand-acts.hand-acts"}},
{"reads", m.Reads, []string{LeaseRead, ConditionsRead}}, {"reads", m.Reads, []string{ConditionsRead}},
} { } {
if !slices.Equal(c.got, c.want) { if !slices.Equal(c.got, c.want) {
t.Errorf("%s %v, want %v", c.name, c.got, c.want) t.Errorf("%s %v, want %v", c.name, c.got, c.want)
@@ -74,7 +74,7 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
} }
readBy := map[string]string{AttemptsState: HealersTool, WouldState: WouldTool, FindingsState: FindingsTool, readBy := map[string]string{AttemptsState: HealersTool, WouldState: WouldTool, FindingsState: FindingsTool,
WakesState: WakesTool, "a settle's end": HealTool, WakesState: WakesTool, "a settle's end": HealTool,
"an act on an event": HealTool, "S15's count": HealersTool + " recount=true", "the lease as read": HealersTool} "an act on an event": HealTool, "S15's count": HealersTool + " recount=true"}
tools := map[string]bool{} tools := map[string]bool{}
for _, tool := range Tools(&Healers{}) { for _, tool := range Tools(&Healers{}) {
tools[tool.Name] = true tools[tool.Name] = true
@@ -38,8 +38,8 @@ func Tools(h *Healers) []stdio.Tool {
return []stdio.Tool{ return []stdio.Tool{
{Name: HealersTool, {Name: HealersTool,
Description: "The mesh's healers (novox/hq ADR 0564 §7, §8): the registry, every act and escalation of the last " + Description: "The mesh's healers (novox/hq ADR 0564 §7, §8): the registry, every act and escalation of the last " +
"days (the bucket attempts), whether the mesh-wide brake holds, whether they act or only write what they " + "days (the bucket attempts), whether the mesh-wide brake holds, that they act (no lease turns them, " +
"would do while the controller holds its lease, and the would-acts. recount=true counts the hand-act log " + "issue 1126), and the would-acts kept from while the controller ran. recount=true counts the hand-act log " +
"again for S15 (a cause repaired by hand twice in a fortnight).", "again for S15 (a cause repaired by hand twice in a fortnight).",
Input: obj(map[string]any{ Input: obj(map[string]any{
"days": map[string]any{"type": "number", "description": "how many days of acts back (default 7)"}, "days": map[string]any{"type": "number", "description": "how many days of acts back (default 7)"},
@@ -47,8 +47,7 @@ func Tools(h *Healers) []stdio.Tool {
Run: func(a map[string]any) (any, error) { return h.Answer(intArg(a, "days", 7), boolArg(a, "recount")) }}, Run: func(a map[string]any) (any, error) { return h.Answer(intArg(a, "days", 7), boolArg(a, "recount")) }},
{Name: HealTool, {Name: HealTool,
Description: "One healer's act now on an open condition, by its key, within its budget and the brake, as an " + Description: "One healer's act now on an open condition, by its key, within its budget and the brake, as an " +
"event would have it — or, while the controller holds its lease, the act it would take written as would. " + "event would have it. Says what was done.",
"Says what was done.",
Input: obj(map[string]any{ Input: obj(map[string]any{
"key": map[string]any{"type": "string", "description": "the condition's key"}, "key": map[string]any{"type": "string", "description": "the condition's key"},
"why": map[string]any{"type": "string", "description": "why, in a sentence"}}), "why": map[string]any{"type": "string", "description": "why, in a sentence"}}),
@@ -76,8 +75,8 @@ func Tools(h *Healers) []stdio.Tool {
Input: obj(map[string]any{"key": map[string]any{"type": "string"}}), Input: obj(map[string]any{"key": map[string]any{"type": "string"}}),
Run: func(a map[string]any) (any, error) { return h.read(WakesState, strArg(a, "key")) }}, Run: func(a map[string]any) (any, error) { return h.read(WakesState, strArg(a, "key")) }},
{Name: WouldTool, {Name: WouldTool,
Description: "What the healers would have done while the controller holds its lease (their bucket would, each " + Description: "What the healers would have done while the controller ran (their bucket would, each " +
"key kept a week): one per healer and condition, its newest; or one whole by key.", "key kept a week; nothing writes it any more): one per healer and condition, its newest; or one whole by key.",
Input: obj(map[string]any{"key": map[string]any{"type": "string"}}), Input: obj(map[string]any{"key": map[string]any{"type": "string"}}),
Run: func(a map[string]any) (any, error) { return h.read(WouldState, strArg(a, "key")) }}, Run: func(a map[string]any) (any, error) { return h.read(WouldState, strArg(a, "key")) }},
} }
@@ -147,7 +146,6 @@ func (h *Healers) Answer(days int, recount bool) (any, error) {
"budget": fmt.Sprintf("%d act(s) within %s, %s apart at least", r.Budget, r.Window, r.Settle), "budget": fmt.Sprintf("%d act(s) within %s, %s apart at least", r.Budget, r.Window, r.Settle),
"then": r.Then, "acts-in": r.ActsIn, "from": r.From}) "then": r.Then, "acts-in": r.ActsIn, "from": r.From})
} }
acting, why := h.Acting()
would, err := h.read(WouldState, "") would, err := h.read(WouldState, "")
if err != nil { if err != nil {
would = map[string]any{"unread": err.Error()} would = map[string]any{"unread": err.Error()}
@@ -161,7 +159,7 @@ func (h *Healers) Answer(days int, recount bool) (any, error) {
if recent == nil { if recent == nil {
recent = []Attempt{} recent = []Attempt{}
} }
return map[string]any{"healers": rows, "acts": recent, "days": days, "acting": acting, "why": why, "would": would, return map[string]any{"healers": rows, "acts": recent, "days": days, "acting": true, "why": actingWhy, "would": would,
"brake": map[string]any{"holds": braked, "said": said, "limit": BrakeLimit, "window": BrakeWindow.String()}, "brake": map[string]any{"holds": braked, "said": said, "limit": BrakeLimit, "window": BrakeWindow.String()},
"declined": declined, "declined": declined,
"note": "a heal is never a hand act; whether it repaired anything is the condition's clearing to say"}, nil "note": "a heal is never a hand act; whether it repaired anything is the condition's clearing to say"}, nil
@@ -5,9 +5,9 @@ package main
// mesh-healers whole against a real bus (novox/hq ADR 0564 §7, §8; issue 1952 H3): the built binary, played over its // mesh-healers whole against a real bus (novox/hq ADR 0564 §7, §8; issue 1952 H3): the built binary, played over its
// real stdin and stdout as the node's tool runtime plays it (mesh/state.* and watches answered from a real nats-server's // real stdin and stdout as the node's tool runtime plays it (mesh/state.* and watches answered from a real nats-server's
// key-value buckets, the keeper's events handed over as the runtime hands them, the grants part's assert answered). An // key-value buckets, the keeper's events handed over as the runtime hands them, the grants part's assert answered). An
// induced failure, a module's consumer gone, is only written as would while the controller holds its lease; from the // induced failure, a module's consumer gone, is acted on with no lease key anywhere (issue 1126), its attempt on the bus
// write that hands the lease away a healer acts, its attempt on the bus and healer-acted said; past twelve acts in the // and healer-acted said, nothing written as would; past twelve acts in the hour the brake stops every healer, said as an
// hour the brake stops every healer, said as an urgent finding on the bus. // urgent finding on the bus.
import ( import (
"bufio" "bufio"
@@ -320,7 +320,7 @@ func raised(key, module string) []byte {
return raw return raw
} }
// 1952/H3: an induced failure; the healer acts only from the lease write, and brakes. // 1952/H3, 1126/A4: an induced failure with no lease key; the healer acts, and brakes.
func TestTheHealersWholeActOnTheBusAndBrake(t *testing.T) { func TestTheHealersWholeActOnTheBusAndBrake(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel) t.Cleanup(cancel)
@@ -341,7 +341,7 @@ func TestTheHealersWholeActOnTheBusAndBrake(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
for _, b := range []string{Module + "_" + AttemptsState, Module + "_" + WouldState, Module + "_" + FindingsState, Module + "_" + WakesState, for _, b := range []string{Module + "_" + AttemptsState, Module + "_" + WouldState, Module + "_" + FindingsState, Module + "_" + WakesState,
"mesh-controller_lease", "mesh-conditions_conditions", "mesh-controller_hand-acts"} { "mesh-conditions_conditions", "mesh-controller_hand-acts"} {
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b}); err != nil { if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -357,10 +357,6 @@ func TestTheHealersWholeActOnTheBusAndBrake(t *testing.T) {
k, _ := bucket(name).Keys(ctx) k, _ := bucket(name).Keys(ctx)
return k return k
} }
lease := bucket("mesh-controller_lease")
if _, err := lease.Put(ctx, LeaseKey, []byte(`{"instance":"novox/1/2026-10-27","epoch":57}`)); err != nil {
t.Fatal(err)
}
acted, err := nc.SubscribeSync("mesh.seat." + Seat + ".event." + ActedEvent) acted, err := nc.SubscribeSync("mesh.seat." + Seat + ".event." + ActedEvent)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -385,24 +381,11 @@ func TestTheHealersWholeActOnTheBusAndBrake(t *testing.T) {
_ = json.Unmarshal(raw, &a) _ = json.Unmarshal(raw, &a)
return a return a
} }
eventually(t, "the lease read as a controller's", func() bool { eventually(t, "the healers acting", func() bool { return healers()["acting"] == true })
a := healers()
return a != nil && strings.Contains(fmt.Sprint(a["why"]), "names a controller")
})
// The runtime's consumer holds an event until the bundle subscribes: delivered once it hears them. // The runtime's consumer holds an event until the bundle subscribes: delivered once it hears them.
eventually(t, "the healers hearing the keeper's conditions", func() bool { return p.hears(RaisedEvent) && p.hears(ChangedEvent) }) eventually(t, "the healers hearing the keeper's conditions", func() bool { return p.hears(RaisedEvent) && p.hears(ChangedEvent) })
// The controller holds the lease: the induced failure is written as would, never acted on. // No lease key: the induced failure is acted on.
p.deliver(RaisedEvent, raised("bus.EVENTS.records.missing", "records"))
eventually(t, "the would-act written", func() bool { return len(keys(Module+"_"+WouldState)) == 1 })
if k := keys(Module + "_" + AttemptsState); len(k) != 0 {
t.Fatalf("acted under the controller's lease: %v", k)
}
// The write that hands the lease away: the same failure is acted on.
if _, err := lease.Put(ctx, LeaseKey, []byte(`{"handed-to":"the operator's hand-over"}`)); err != nil {
t.Fatal(err)
}
eventually(t, "the healers acting", func() bool { return healers()["acting"] == true })
p.deliver(RaisedEvent, raised("bus.EVENTS.records.missing", "records")) p.deliver(RaisedEvent, raised("bus.EVENTS.records.missing", "records"))
eventually(t, "the act begun and finished on the bus", func() bool { eventually(t, "the act begun and finished on the bus", func() bool {
k := keys(Module + "_" + AttemptsState) k := keys(Module + "_" + AttemptsState)
@@ -412,6 +395,9 @@ func TestTheHealersWholeActOnTheBusAndBrake(t *testing.T) {
e, err := bucket(Module+"_"+AttemptsState).Get(ctx, k[0]) e, err := bucket(Module+"_"+AttemptsState).Get(ctx, k[0])
return err == nil && strings.Contains(string(e.Value()), `"outcome":"acted"`) return err == nil && strings.Contains(string(e.Value()), `"outcome":"acted"`)
}) })
if k := keys(Module + "_" + WouldState); len(k) != 0 {
t.Errorf("written as would: %v", k)
}
msg, err := acted.NextMsg(10 * time.Second) msg, err := acted.NextMsg(10 * time.Second)
if err != nil || !strings.Contains(string(msg.Data), `"healer":"H3"`) { if err != nil || !strings.Contains(string(msg.Data), `"healer":"H3"`) {
t.Fatalf("healer-acted was not said: %v", err) t.Fatalf("healer-acted was not said: %v", err)
-1
View File
@@ -24,7 +24,6 @@
"seat:mesh-hand-acts.hand-acts" "seat:mesh-hand-acts.hand-acts"
], ],
"reads": [ "reads": [
"mesh-controller.lease",
"mesh-conditions.conditions" "mesh-conditions.conditions"
], ],
"state": [ "state": [
@@ -72,7 +72,7 @@ func (l *loginsState) Keys() ([]string, error) {
return out, nil return out, nil
} }
// enrolling is a vault that mints, with the lease handed away, a loaded user list at revision 570, the bus on novox, the // enrolling is a vault that mints, with no lease heard (issue 1126), a loaded user list at revision 570, the bus on novox, the
// nodes g14, ace and novox placed, their existing logins adopted, and hand acts of person.jochen's. // nodes g14, ace and novox placed, their existing logins adopted, and hand acts of person.jochen's.
type enrolling struct { type enrolling struct {
testVault testVault
@@ -87,7 +87,6 @@ func newEnrolling(t *testing.T) *enrolling {
e := &enrolling{testVault: v, accounts: &fakeAccounts{keys: map[string]AccountHash{}}, logins: &loginsState{keys: map[string]NodeLogin{}}, acts: map[string]HandActSeen{}} e := &enrolling{testVault: v, accounts: &fakeAccounts{keys: map[string]AccountHash{}}, logins: &loginsState{keys: map[string]NodeLogin{}}, acts: map[string]HandActSeen{}}
v.Accounts, v.Logins = e.accounts, e.logins v.Accounts, v.Logins = e.accounts, e.logins
v.Placement = placementsOf(t, map[string][]string{"novox": {"nats"}, "g14": {"web"}, "ace": {"shop"}}) v.Placement = placementsOf(t, map[string][]string{"novox": {"nats"}, "g14": {"web"}, "ace": {"shop"}})
v.LeaseHeard([]byte(`{"handed-to":"the operator's hand-over"}`), true, nil)
v.LoadedListHeard(570) v.LoadedListHeard(570)
v.TokenBase = func() (TokenBase, error) { v.TokenBase = func() (TokenBase, error) {
return TokenBase{Broker: "tls://10.77.0.1:4222", Fingerprint: "sha256:bus"}, nil return TokenBase{Broker: "tls://10.77.0.1:4222", Fingerprint: "sha256:bus"}, nil
@@ -1,41 +0,0 @@
package main
// The controller's lease as the vault reads it (novox/hq ADR 0561 §3): handed away only to a holder that is no
// controller, and not for a controller's value, a key deleted or expired, or one that cannot be read. It turns retirement
// (retire.go) and rotation (rotation.go), never minting: the vault mints every wanted account it holds no value for,
// whoever holds the lease (mint.go, issue 774). No clock and no setting turns it. A key's age passing is heard by no
// watch, so the lease is read afresh before a retirement or a rotation (LeaseRead).
import "git.novox.be/novox/mesh-sdk/go/buscompose"
// LeaseHeard is the watch of the controller's lease: it records whether it is handed away, and says when that turned.
func (v *Vault) LeaseHeard(raw []byte, found bool, readErr error) {
if !v.setLease(raw, found, readErr) {
return
}
on, why := v.leaseHanded()
v.log("the controller's lease is %s: %s", map[bool]string{true: "HANDED AWAY", false: "not handed away"}[on], why)
}
// setLease records what the lease says, and answers whether minting turned on or off by it.
func (v *Vault) setLease(raw []byte, found bool, readErr error) bool {
on, why := buscompose.LeaseHandedAway(raw, found, readErr)
m := &v.minting
m.mu.Lock()
defer m.mu.Unlock()
flipped := on != m.on || m.why == ""
if flipped {
m.since = v.Now().UTC()
}
m.on, m.why = on, why
return flipped
}
// leaseAfresh reads the lease again before a retirement or a rotation, where the vault can: a key that expired since is heard by no watch.
func (v *Vault) leaseAfresh() {
if v.LeaseRead == nil {
return
}
raw, found, err := v.LeaseRead()
v.setLease(raw, found, err)
}
+1 -33
View File
@@ -58,7 +58,7 @@ func build() *Env {
watchedPlan := func(key string) ([]byte, error) { raw, err := assigned.Read(key); return asAssignment(key, raw, err) } watchedPlan := func(key string) ([]byte, error) { raw, err := assigned.Read(key); return asAssignment(key, raw, err) }
env.Vault = &Vault{Store: store, Own: own, Askers: Declared{Read: watchedPlan, Confirm: readAssignment, Store: store}, env.Vault = &Vault{Store: store, Own: own, Askers: Declared{Read: watchedPlan, Confirm: readAssignment, Store: store},
Assignments: Mirror{Keys: assignmentKeys, Read: readAssignment}, Emit: emit, Ask: stdio.Ask, Assignments: Mirror{Keys: assignmentKeys, Read: readAssignment}, Emit: emit, Ask: stdio.Ask,
Now: time.Now, Log: say, Accounts: busAccounts{}, Haves: readHave, LeaseRead: readLease, WantRead: readWant, Now: time.Now, Log: say, Accounts: busAccounts{}, Haves: readHave, WantRead: readWant,
HaveKeys: func() ([]string, error) { return stdio.State(HaveRead).Keys() }, Placement: placement, Signers: busSigners{}, HaveKeys: func() ([]string, error) { return stdio.State(HaveRead).Keys() }, Placement: placement, Signers: busSigners{},
AskCallers: askCallers(os.Getenv("MESH_VAULT_SECRET_ASK_CALLERS")), EnrolCallers: askCallers(os.Getenv("MESH_VAULT_ENROLMENT_CALLERS")), AskCallers: askCallers(os.Getenv("MESH_VAULT_SECRET_ASK_CALLERS")), EnrolCallers: askCallers(os.Getenv("MESH_VAULT_ENROLMENT_CALLERS")),
Logins: busNodeLogins{}, ReadAct: readHandAct, TokenBase: TokenBaseFromFile(os.Getenv("MESH_VAULT_STATE")), Logins: busNodeLogins{}, ReadAct: readHandAct, TokenBase: TokenBaseFromFile(os.Getenv("MESH_VAULT_STATE")),
@@ -229,34 +229,6 @@ func readWant(user string) ([]byte, bool, error) {
return e.Value, true, nil return e.Value, true, nil
} }
// readLease is the controller's lease key, read through the runtime under the vault's own account (its manifest reads it).
func readLease() ([]byte, bool, error) {
e, err := stdio.State(LeaseRead).Get(LeaseKey)
if err != nil || e == nil {
return nil, false, err
}
return e.Value, true, nil
}
// watchLease turns minting on and off as the controller's lease is written (lease.go, ADR 0561 §3): a watch, never a
// poll; and the lease is read afresh before every mint. It answers whether the watch began.
func watchLease(env *Env) bool {
if env.Vault == nil {
return true
}
env.Vault.LeaseHeard(readLease())
err := stdio.State(LeaseRead).Watch(LeaseKey, func(c stdio.StateChange) error {
deleted := c.Op == "delete" || c.Op == "purge" || len(c.Value) == 0
env.Vault.LeaseHeard(c.Value, !deleted, nil)
return nil
})
if err != nil {
say("the controller's lease is not watched, so minting stays as last read; begun again at the next pass: %v", err)
return false
}
return true
}
// adoptOperatorKey takes the operator key from the export the mesh writes to the vault's root (what // adoptOperatorKey takes the operator key from the export the mesh writes to the vault's root (what
// `operator key set` recorded; the controller keeps only a public half there), and seals again any // `operator key set` recorded; the controller keeps only a public half there), and seals again any
// operator copy not yet sealed to the current key. Run every minute, so a failure partway is finished. It // operator copy not yet sealed to the current key. Run every minute, so a failure partway is finished. It
@@ -319,7 +291,6 @@ func main() {
// The operator key first: nothing is kept without it, so a want heard before it would not be minted. // The operator key first: nothing is kept without it, so a want heard before it would not be minted.
env.pass() env.pass()
watched, watching := watchHandings(env), watchAssignments() watched, watching := watchHandings(env), watchAssignments()
lease := watchLease(env)
wants := watchWants(env) wants := watchWants(env)
leavers := watchLeavers(env) leavers := watchLeavers(env)
for { for {
@@ -332,9 +303,6 @@ func main() {
if !watching { if !watching {
watching = watchAssignments() watching = watchAssignments()
} }
if !lease {
lease = watchLease(env)
}
if !wants { if !wants {
wants = watchWants(env) wants = watchWants(env)
} }
+10 -42
View File
@@ -11,10 +11,10 @@ package main
// on `secret-changed` and composes. // on `secret-changed` and composes.
// //
// **The vault is the one issuer** (issue 774, research 072's gap "vault mints after the lease"): it mints every wanted // **The vault is the one issuer** (issue 774, research 072's gap "vault mints after the lease"): it mints every wanted
// account it holds no value for, whoever holds the controller's lease, and the controller mints none once a vault is // account it holds no value for, and the controller mints none once a vault is installed (mesh-controller forwards
// installed (mesh-controller forwards `module issue` and its assignment's issue to issue-account). No account depends // `module issue` and its assignment's issue to issue-account). No account depends on a node reporting to the
// on a node reporting to the controller. The lease still turns retirement and rotation (retire.go, rotation.go), and // controller. No lease turns minting, retirement or rotation: the controller ended by its unassignment (issue 1126).
// vault_status names it. **Never two issuers of one value** (§3, §4): an account whose value the vault holds, or whose // **Never two issuers of one value** (§3, §4): an account whose value the vault holds, or whose
// hash it adopted, is never minted; the node accounts and the controller's own are never wanted; a value the controller // hash it adopted, is never minted; the node accounts and the controller's own are never wanted; a value the controller
// moves in never replaces one held (move.go), and one it hands with origin issued never replaces an account the vault // moves in never replaces one held (move.go), and one it hands with origin issued never replaces an account the vault
// minted (AcceptTake): during the delivery an older controller may still mint, and the vault's value wins. // minted (AcceptTake): during the delivery an older controller may still mint, and the vault's value wins.
@@ -38,23 +38,13 @@ import (
// WantedRead is the grants part's state of wanted accounts, as the vault's manifest reads it. // WantedRead is the grants part's state of wanted accounts, as the vault's manifest reads it.
const WantedRead = buscompose.GrantsAccounts const WantedRead = buscompose.GrantsAccounts
// LeaseRead is the controller's lease, as the vault's manifest reads it, and LeaseKey its one key.
const (
LeaseRead = buscompose.ControllerLease
LeaseKey = buscompose.ControllerLeaseKey
)
// AccountIssuedEvent is what the vault says when it minted an account (mesh-sdk buscompose.AccountIssued). // AccountIssuedEvent is what the vault says when it minted an account (mesh-sdk buscompose.AccountIssued).
const AccountIssuedEvent = buscompose.AccountIssuedEvent const AccountIssuedEvent = buscompose.AccountIssuedEvent
// minting is what the vault knows of the wanted accounts and of the controller's lease, kept in memory and read again // minting is what the vault knows of the wanted accounts, kept in memory and read again from the bus at every start:
// from the bus at every start: the grants part's state and the lease are the record, never this. on, why and since are // the grants part's state is the record, never this.
// the lease (handed away or not), which turns retirement and rotation, never minting.
type minting struct { type minting struct {
mu sync.Mutex mu sync.Mutex
on bool
why string
since time.Time
// wants are the wanted accounts, by user, as last heard. // wants are the wanted accounts, by user, as last heard.
wants map[string]buscompose.WantedAccount wants map[string]buscompose.WantedAccount
// heldBack are the wanted users the vault did not mint, with why (vault_status). // heldBack are the wanted users the vault did not mint, with why (vault_status).
@@ -81,34 +71,25 @@ type Minted struct {
Hashed bool `json:"hashed"` Hashed bool `json:"hashed"`
} }
// MintState is what vault_status says of minting: always on (the vault is the one issuer, issue 774), and the lease, // MintState is what vault_status says of minting: always on (the vault is the one issuer, issue 774).
// which turns retirement and rotation.
type MintState struct { type MintState struct {
On bool `json:"on"` On bool `json:"on"`
Why string `json:"why"` Why string `json:"why"`
// Lease is whether the controller's lease is handed away, LeaseWhy why, and Since when that last turned.
Lease bool `json:"lease_handed_away"`
LeaseWhy string `json:"lease_why"`
Since time.Time `json:"since,omitzero"`
Wanted int `json:"wanted"` Wanted int `json:"wanted"`
HeldBack map[string]string `json:"held_back"` HeldBack map[string]string `json:"held_back"`
Minted []Minted `json:"minted"` Minted []Minted `json:"minted"`
} }
// mintingWhy is why the vault mints, whoever holds the lease. // mintingWhy is why the vault mints.
const mintingWhy = "the vault is the one issuer of bus accounts: it mints every wanted account it holds no value for " + const mintingWhy = "the vault is the one issuer of bus accounts: it mints every wanted account it holds no value for " +
"and adopted no hash of, whoever holds the controller's lease (issue 774)" "and adopted no hash of (issue 774)"
// MintState answers how minting stands. A user held back whose account the vault holds a value for meanwhile (moved in, // MintState answers how minting stands. A user held back whose account the vault holds a value for meanwhile (moved in,
// handed, or minted by another path) is not said held back: it is not (issue 774, 2026-10-31). // handed, or minted by another path) is not said held back: it is not (issue 774, 2026-10-31).
func (v *Vault) MintState() MintState { func (v *Vault) MintState() MintState {
m := &v.minting m := &v.minting
m.mu.Lock() m.mu.Lock()
out := MintState{On: true, Why: mintingWhy, Lease: m.on, LeaseWhy: m.why, Since: m.since, Wanted: len(m.wants), out := MintState{On: true, Why: mintingWhy, Wanted: len(m.wants), HeldBack: map[string]string{}, Minted: []Minted{}}
HeldBack: map[string]string{}, Minted: []Minted{}}
if out.LeaseWhy == "" {
out.LeaseWhy = leaseUnread
}
wants := map[string]buscompose.WantedAccount{} wants := map[string]buscompose.WantedAccount{}
for u, why := range m.heldBack { for u, why := range m.heldBack {
out.HeldBack[u] = why out.HeldBack[u] = why
@@ -143,19 +124,6 @@ func (v *Vault) holdsAccount(w buscompose.WantedAccount) bool {
return err == nil && BusAccountValue(secret, value) return err == nil && BusAccountValue(secret, value)
} }
// leaseUnread is why nothing is retired or rotated before the lease is read.
const leaseUnread = "the controller's lease has not been read yet, so nothing is retired or rotated"
// leaseHanded says whether the controller's lease is handed away, and why: what retirement and rotation wait for.
func (v *Vault) leaseHanded() (bool, string) {
v.minting.mu.Lock()
defer v.minting.mu.Unlock()
if v.minting.why == "" {
return false, leaseUnread
}
return v.minting.on, v.minting.why
}
func (v *Vault) holdBack(user, why string) { func (v *Vault) holdBack(user, why string) {
m := &v.minting m := &v.minting
m.mu.Lock() m.mu.Lock()
@@ -5,7 +5,7 @@ package main
// The vault whole against a real bus (novox/hq ADR 0561 §1 to §4, issue 774's acceptance 12): the built binary, played // The vault whole against a real bus (novox/hq ADR 0561 §1 to §4, issue 774's acceptance 12): the built binary, played
// over its real stdin and stdout as the node's tool runtime plays it (tools/call with the caller in _meta; mesh/state.* // over its real stdin and stdout as the node's tool runtime plays it (tools/call with the caller in _meta; mesh/state.*
// and mesh/publish answered from a real nats-server's key-value buckets and subjects), its store in a directory of its // and mesh/publish answered from a real nats-server's key-value buckets and subjects), its store in a directory of its
// own. A want written to the grants part's state is minted whoever holds the controller's lease (issue 774: the vault is // own. A want written to the grants part's state is minted with no lease asked (issue 774: the vault is
// the one issuer): the hash is in bus-accounts, own answers the value sealed to the node, the value is the controller's // the one issuer): the hash is in bus-accounts, own answers the value sealed to the node, the value is the controller's
// shape and its hash's password, account-issued is said; an adopted user is never minted. // shape and its hash's password, account-issued is said; an adopted user is never minted.
@@ -330,7 +330,7 @@ func TestTheVaultWholeMintsOnTheBusWhoeverHoldsTheLease(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
for _, b := range []string{"mesh-vault_bus-accounts", "mesh-grants_accounts", "mesh-controller_lease", for _, b := range []string{"mesh-vault_bus-accounts", "mesh-grants_accounts",
"mesh-provisioner_plans", "node-engine_have", "mesh-delivery-assigner_placement"} { "mesh-provisioner_plans", "node-engine_have", "mesh-delivery-assigner_placement"} {
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 2}); err != nil { if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 2}); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -372,14 +372,13 @@ func TestTheVaultWholeMintsOnTheBusWhoeverHoldsTheLease(t *testing.T) {
_ = json.Unmarshal(raw, &s) _ = json.Unmarshal(raw, &s)
return s.Minting return s.Minting
} }
// The controller holds the lease, and the want is minted all the same. // No lease key: the want is minted all the same (issue 1126).
p.put(t, "mesh-controller.lease", LeaseKey, map[string]any{"instance": "novox/1/2026-10-27", "epoch": 57})
x, y := aWant("mod-x"), aWant("mod-y") x, y := aWant("mod-x"), aWant("mod-y")
p.put(t, "mesh-vault.bus-accounts", y.User, AccountHash{User: y.User, Hash: "$2a$04$adoptedadoptedadoptedadoptedadoptedadoptedadopte", Adopted: true}) p.put(t, "mesh-vault.bus-accounts", y.User, AccountHash{User: y.User, Hash: "$2a$04$adoptedadoptedadoptedadoptedadoptedadoptedadopte", Adopted: true})
p.put(t, "mesh-grants.accounts", x.User, x) p.put(t, "mesh-grants.accounts", x.User, x)
p.put(t, "mesh-grants.accounts", y.User, y) p.put(t, "mesh-grants.accounts", y.User, y)
eventually(t, "the account minted under the controller's lease", func() bool { return p.hashOf(t, x.User) != nil }) eventually(t, "the account minted", func() bool { return p.hashOf(t, x.User) != nil })
if st := status(); !st.On || st.Lease || len(st.HeldBack) != 0 { if st := status(); !st.On || len(st.HeldBack) != 0 {
t.Errorf("vault_status says %+v", st) t.Errorf("vault_status says %+v", st)
} }
nodePub, nodePriv := aSealingKey(t) nodePub, nodePriv := aSealingKey(t)
+14 -19
View File
@@ -24,9 +24,9 @@ func wantRaw(t *testing.T, w buscompose.WantedAccount) []byte {
return raw return raw
} }
// mintingVault is a vault with a state of bus accounts, ace's sealing key stated, ace's modules assigned, and minting // mintingVault is a vault with a state of bus accounts, ace's sealing key stated and ace's modules assigned. No lease is
// turned on or off as on says. // heard: none turns minting, retirement or rotation (issue 1126).
func mintingVault(t *testing.T, on bool) (testVault, *fakeAccounts, string) { func mintingVault(t *testing.T) (testVault, *fakeAccounts, string) {
t.Helper() t.Helper()
plans := assignments{"ace.mod-x": `{"node":"ace","module":"mod-x"}`, "ace.mod-y": `{"node":"ace","module":"mod-y"}`} plans := assignments{"ace.mod-x": `{"node":"ace","module":"mod-x"}`, "ace.mod-y": `{"node":"ace","module":"mod-y"}`}
v := newVault(t, nil) v := newVault(t, nil)
@@ -37,11 +37,6 @@ func mintingVault(t *testing.T, on bool) (testVault, *fakeAccounts, string) {
if err := v.recordNodeKey("ace", pub, "node.ace"); err != nil { if err := v.recordNodeKey("ace", pub, "node.ace"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if on {
v.LeaseHeard([]byte(`{"handed-to":"the operator's hand-over"}`), true, nil)
} else {
v.LeaseHeard([]byte(`{"instance":"novox/1/2026-10-27","epoch":57}`), true, nil)
}
return v, accounts, priv return v, accounts, priv
} }
@@ -49,7 +44,7 @@ func mintingVault(t *testing.T, on bool) (testVault, *fakeAccounts, string) {
// shape as the module's broker, its hash in bus-accounts, said account-issued before secret-changed, and answered by own // shape as the module's broker, its hash in bus-accounts, said account-issued before secret-changed, and answered by own
// sealed to the node; a value held or a hash adopted is never minted. // sealed to the node; a value held or a hash adopted is never minted.
func TestTheVaultMintsAWantedAccountOnceAndNothingItHolds(t *testing.T) { func TestTheVaultMintsAWantedAccountOnceAndNothingItHolds(t *testing.T) {
v, accounts, nodePriv := mintingVault(t, true) v, accounts, nodePriv := mintingVault(t)
w := aWant("mod-x") w := aWant("mod-x")
before := len(v.bus.events) before := len(v.bus.events)
v.WantHeard(w.User, wantRaw(t, w), false) v.WantHeard(w.User, wantRaw(t, w), false)
@@ -107,11 +102,11 @@ func TestTheVaultMintsAWantedAccountOnceAndNothingItHolds(t *testing.T) {
} }
// 774/AC8, after research 072's gap "vault mints after the lease": the vault is the one issuer, so a wanted account is // 774/AC8, after research 072's gap "vault mints after the lease": the vault is the one issuer, so a wanted account is
// minted while the controller still holds the lease (never an adopted one), vault_status says minting is on and the // minted with no lease asked (never an adopted one), vault_status says minting is on and names no lease (issue 1126),
// lease apart, and a want held back is not said held back once the vault holds its value. During the delivery an older // and a want held back is not said held back once the vault holds its value. During the delivery an older
// controller may still mint: its value handed with origin issued never replaces the account the vault minted. // controller may still mint: its value handed with origin issued never replaces the account the vault minted.
func TestTheVaultMintsWhoeverHoldsTheLease(t *testing.T) { func TestTheVaultMintsWhoeverHoldsTheLease(t *testing.T) {
v, accounts, _ := mintingVault(t, false) v, accounts, _ := mintingVault(t)
accounts.keys = map[string]AccountHash{"ace.mod-y": {User: "ace.mod-y", Hash: "$2a$04$adopted", Adopted: true}} accounts.keys = map[string]AccountHash{"ace.mod-y": {User: "ace.mod-y", Hash: "$2a$04$adopted", Adopted: true}}
for _, m := range []string{"mod-x", "mod-y"} { for _, m := range []string{"mod-x", "mod-y"} {
w := aWant(m) w := aWant(m)
@@ -119,14 +114,14 @@ func TestTheVaultMintsWhoeverHoldsTheLease(t *testing.T) {
} }
minted, held, _ := v.Store.Read("own/ace/mod-x/broker") minted, held, _ := v.Store.Read("own/ace/mod-x/broker")
if !held { if !held {
t.Fatalf("not minted under the controller's lease: %+v", v.MintState()) t.Fatalf("not minted: %+v", v.MintState())
} }
if _, held, _ := v.Store.Read("own/ace/mod-y/broker"); held { if _, held, _ := v.Store.Read("own/ace/mod-y/broker"); held {
t.Error("an adopted account was minted") t.Error("an adopted account was minted")
} }
st := v.MintState() st := v.MintState()
if !st.On || st.Lease || !strings.Contains(st.LeaseWhy, "controller holds the lease") || len(st.HeldBack) != 0 { if !st.On || strings.Contains(st.Why, "lease") || len(st.HeldBack) != 0 {
t.Errorf("under the controller's lease vault_status says %+v", st) t.Errorf("vault_status says %+v", st)
} }
// An older controller's covered account, handed with origin issued: refused, the vault's value kept. // An older controller's covered account, handed with origin issued: refused, the vault's value kept.
x := aWant("mod-x") x := aWant("mod-x")
@@ -156,7 +151,7 @@ func TestTheVaultMintsWhoeverHoldsTheLease(t *testing.T) {
// 774/AC10 issue-account (novox/hq ADR 0561 §9): the verb mints one wanted user now, read afresh, whoever holds the lease; // 774/AC10 issue-account (novox/hq ADR 0561 §9): the verb mints one wanted user now, read afresh, whoever holds the lease;
// it is refused for a node's account and the controller's, and for a user nobody wants; a user held already is said so. // it is refused for a node's account and the controller's, and for a user nobody wants; a user held already is said so.
func TestIssueAccountMintsOneWantedUserNowAndRefusesTheRest(t *testing.T) { func TestIssueAccountMintsOneWantedUserNowAndRefusesTheRest(t *testing.T) {
v, _, _ := mintingVault(t, false) v, _, _ := mintingVault(t)
w := aWant("mod-x") w := aWant("mod-x")
raw := wantRaw(t, w) raw := wantRaw(t, w)
v.WantRead = func(user string) ([]byte, bool, error) { v.WantRead = func(user string) ([]byte, bool, error) {
@@ -191,7 +186,7 @@ func TestIssueAccountMintsOneWantedUserNowAndRefusesTheRest(t *testing.T) {
// over the second login `<user>--b`, never of a node's account, the controller's or an adopted hash, and `rotate` of a // over the second login `<user>--b`, never of a node's account, the controller's or an adopted hash, and `rotate` of a
// covered account stays refused, so nothing rotates in place. // covered account stays refused, so nothing rotates in place.
func TestOneRotationBeginsPerRevisionOfTheLoadedList(t *testing.T) { func TestOneRotationBeginsPerRevisionOfTheLoadedList(t *testing.T) {
v, accounts, _ := mintingVault(t, true) v, accounts, _ := mintingVault(t)
for _, m := range []string{"mod-x", "mod-y"} { for _, m := range []string{"mod-x", "mod-y"} {
w := aWant(m) w := aWant(m)
v.WantHeard(w.User, wantRaw(t, w), false) v.WantHeard(w.User, wantRaw(t, w), false)
@@ -227,7 +222,7 @@ func TestOneRotationBeginsPerRevisionOfTheLoadedList(t *testing.T) {
// A mint that failed (here: no operator key yet, so nothing can be kept) is tried again at the pass, never lost. // A mint that failed (here: no operator key yet, so nothing can be kept) is tried again at the pass, never lost.
func TestAFailedMintIsTriedAgainAtThePass(t *testing.T) { func TestAFailedMintIsTriedAgainAtThePass(t *testing.T) {
v, accounts, _ := mintingVault(t, true) v, accounts, _ := mintingVault(t)
key, _ := v.Store.OperatorKey() key, _ := v.Store.OperatorKey()
if err := v.Store.setOperatorKey("", "the test", v.Now()); err != nil { if err := v.Store.setOperatorKey("", "the test", v.Now()); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -250,7 +245,7 @@ func TestAFailedMintIsTriedAgainAtThePass(t *testing.T) {
// broker-management on 2026-10-30, issue 774) is replaced by a minted account when the grants part wants its user; // broker-management on 2026-10-30, issue 774) is replaced by a minted account when the grants part wants its user;
// a held account still never is. // a held account still never is.
func TestAHeldValueThatIsNoAccountIsReplacedByAMintedOne(t *testing.T) { func TestAHeldValueThatIsNoAccountIsReplacedByAMintedOne(t *testing.T) {
v, accounts, _ := mintingVault(t, true) v, accounts, _ := mintingVault(t)
w := aWant("mod-x") w := aWant("mod-x")
if _, err := v.Put("own/ace/mod-x/broker", `{"password":"pw"}`, "the test"); err != nil { if _, err := v.Put("own/ace/mod-x/broker", `{"password":"pw"}`, "the test"); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -268,10 +268,6 @@ func (v *Vault) RotateNode(caller string, a RotateNodeArgs) (RotateNodeAnswer, e
if v.Accounts == nil { if v.Accounts == nil {
return RotateNodeAnswer{}, errors.New("the vault has no state of bus accounts to write to") return RotateNodeAnswer{}, errors.New("the vault has no state of bus accounts to write to")
} }
v.leaseAfresh()
if on, why := v.leaseHanded(); !on {
return RotateNodeAnswer{}, fmt.Errorf("no rotation begins: %s", why)
}
v.minting.mu.Lock() v.minting.mu.Lock()
revision := v.minting.loadedRevision revision := v.minting.loadedRevision
v.minting.mu.Unlock() v.minting.mu.Unlock()
@@ -596,10 +592,6 @@ func (v *Vault) RotationPass() {
if err != nil || len(running) > 0 { if err != nil || len(running) > 0 {
return return
} }
v.leaseAfresh()
if on, _ := v.leaseHanded(); !on {
return
}
bus, err := v.busNode() bus, err := v.busNode()
if err != nil { if err != nil {
return return
+3 -13
View File
@@ -7,8 +7,8 @@ package main
// retired in its store, keeping it (a value is deleted only by a person, ADR 0230). As ADR 0230 §15 and §18 have it, the // retired in its store, keeping it (a value is deleted only by a person, ADR 0230). As ADR 0230 §15 and §18 have it, the
// same set of accounts gone must stand for five passes and ten minutes first, and a set of more than three, or more than // same set of accounts gone must stand for five passes and ten minutes first, and a set of more than three, or more than
// half of the accounts held, waits for a person, who retires them with `account_retire`. Wanted again, an account is // half of the accounts held, waits for a person, who retires them with `account_retire`. Wanted again, an account is
// re-enabled at once: the mark taken off and its hash written again. Nothing is retired while the controller holds the // re-enabled at once: the mark taken off and its hash written again. Nothing is retired before the wanted accounts are read
// lease (it forgets the accounts it mints), or before the wanted accounts are read whole. // whole. No lease turns it: the controller ended by its unassignment (issue 1126).
import ( import (
"errors" "errors"
@@ -135,8 +135,6 @@ func (v *Vault) RetirePass() {
if v.Accounts == nil { if v.Accounts == nil {
return return
} }
v.leaseAfresh()
on, leaseWhy := v.leaseHanded()
m := &v.minting m := &v.minting
m.mu.Lock() m.mu.Lock()
read := m.retiring.wantsRead read := m.retiring.wantsRead
@@ -150,9 +148,6 @@ func (v *Vault) RetirePass() {
case !read: case !read:
reset("the wanted accounts are not read whole yet, so none is known to be gone") reset("the wanted accounts are not read whole yet, so none is known to be gone")
return return
case !on:
reset("nothing is retired: " + leaseWhy)
return
} }
gone, held, err := v.goneAccounts() gone, held, err := v.goneAccounts()
if err != nil { if err != nil {
@@ -239,12 +234,7 @@ func (v *Vault) RetireAccounts(caller string, users []string, why string) (any,
if len(users) == 0 { if len(users) == 0 {
return nil, errors.New("account_retire names the accounts (`users`), as vault_status lists them waiting") return nil, errors.New("account_retire names the accounts (`users`), as vault_status lists them waiting")
} }
// As the passes: never while the controller holds the lease (it forgets the accounts it mints), nor before the wanted // As the passes: never before the wanted accounts are read whole (an account not read yet is not known to be gone).
// accounts are read whole (an account not read yet is not known to be gone).
v.leaseAfresh()
if on, why := v.leaseHanded(); !on {
return nil, fmt.Errorf("nothing was retired: %s", why)
}
v.minting.mu.Lock() v.minting.mu.Lock()
read := v.minting.retiring.wantsRead read := v.minting.retiring.wantsRead
v.minting.mu.Unlock() v.minting.mu.Unlock()
@@ -11,7 +11,7 @@ import (
// more than three, or more than half of those held, at once: past that it waits for a person (account_retire). Wanted // more than three, or more than half of those held, at once: past that it waits for a person (account_retire). Wanted
// again, it is re-enabled at once; and a retired account is not hashed again at a start. // again, it is re-enabled at once; and a retired account is not hashed again at a start.
func TestAWantGoneRetiresItsAccountOnlyWithinTheBounds(t *testing.T) { func TestAWantGoneRetiresItsAccountOnlyWithinTheBounds(t *testing.T) {
v, accounts, _ := mintingVault(t, true) v, accounts, _ := mintingVault(t)
clock := time.Date(2026, 10, 28, 9, 0, 0, 0, time.UTC) clock := time.Date(2026, 10, 28, 9, 0, 0, 0, time.UTC)
v.Now = func() time.Time { return clock } v.Now = func() time.Time { return clock }
modules := []string{"m1", "m2", "m3", "m4", "m5", "m6", "m7", "m8"} modules := []string{"m1", "m2", "m3", "m4", "m5", "m6", "m7", "m8"}
@@ -65,14 +65,7 @@ func TestAWantGoneRetiresItsAccountOnlyWithinTheBounds(t *testing.T) {
if _, err := v.RetireAccounts("person.jochen", []string{"ace.m2"}, "test"); err == nil { if _, err := v.RetireAccounts("person.jochen", []string{"ace.m2"}, "test"); err == nil {
t.Error("retired by hand an account still wanted") t.Error("retired by hand an account still wanted")
} }
// Never while the controller holds the lease, nor before the wants are read whole. // Never before the wants are read whole; no lease is asked (issue 1126).
held := v.LeaseRead
v.LeaseRead = func() ([]byte, bool, error) { return []byte(`{"instance":"novox/1","epoch":57}`), true, nil }
if _, err := v.RetireAccounts("person.jochen", []string{"ace.m5"}, "the modules left"); err == nil || !strings.Contains(err.Error(), "controller holds the lease") {
t.Errorf("retired by hand under the controller's lease: %v", err)
}
v.LeaseRead = held
v.LeaseHeard([]byte(`{"handed-to":"the operator's hand-over"}`), true, nil)
v.minting.mu.Lock() v.minting.mu.Lock()
v.minting.retiring.wantsRead = false v.minting.retiring.wantsRead = false
v.minting.mu.Unlock() v.minting.mu.Unlock()
@@ -4,9 +4,8 @@ package main
// 2026-10-27). A module's, a process's or a provisioner client's account rotates over two credentials (ADR 0114 §10): // 2026-10-27). A module's, a process's or a provisioner client's account rotates over two credentials (ADR 0114 §10):
// a second login, `<user>--b`, carrying the first login's permissions verbatim, which the bus composer composes only while // a second login, `<user>--b`, carrying the first login's permissions verbatim, which the bus composer composes only while
// the account's key in `bus-accounts` names it in `rotating`. The vault starts at most one rotation per revision of the // the account's key in `bus-accounts` names it in `rotating`. The vault starts at most one rotation per revision of the
// user list the bus loaded. What composes and delivers the second login is a subtask of its own, before the lease write; // user list the bus loaded, and no lease turns it (issue 1126). `rotate` of a covered account stays refused, so nothing
// until it is built nothing begins a rotation, and `rotate` of a covered account stays refused, so nothing rotates in // rotates in place.
// place.
import ( import (
"fmt" "fmt"
@@ -42,9 +41,6 @@ func (v *Vault) BeginRotation(user string) (Rotating, error) {
if nodeOrController(user) { if nodeOrController(user) {
return Rotating{}, fmt.Errorf("%s does not rotate here: a node's account and the controller's keep the hash they have (ADR 0561 §4)", user) return Rotating{}, fmt.Errorf("%s does not rotate here: a node's account and the controller's keep the hash they have (ADR 0561 §4)", user)
} }
if on, why := v.leaseHanded(); !on {
return Rotating{}, fmt.Errorf("no rotation begins: %s", why)
}
v.minting.mu.Lock() v.minting.mu.Lock()
revision := v.minting.loadedRevision revision := v.minting.loadedRevision
v.minting.mu.Unlock() v.minting.mu.Unlock()
@@ -53,7 +53,7 @@ func TestTheVaultWholeRotatesANodesLoginWithAStandInEngine(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
for _, b := range []string{"mesh-vault_bus-accounts", "mesh-vault_node-logins", "mesh-grants_accounts", "mesh-controller_lease", for _, b := range []string{"mesh-vault_bus-accounts", "mesh-vault_node-logins", "mesh-grants_accounts",
"mesh-provisioner_plans", "node-engine_have", "mesh-delivery-assigner_placement"} { "mesh-provisioner_plans", "node-engine_have", "mesh-delivery-assigner_placement"} {
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 2}); err != nil { if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 2}); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -90,7 +90,8 @@ func TestTheVaultWholeRotatesANodesLoginWithAStandInEngine(t *testing.T) {
} }
go p.serve(stdout) go p.serve(stdout)
// The bus node (novox, running nats) and the node that rotates (g14); the lease handed away; g14's login adopted from // The bus node (novox, running nats) and the node that rotates (g14); no lease key anywhere (issue 1126: the controller
// ended by its unassignment); g14's login adopted from
// the loaded list, its password known to the engine. // the loaded list, its password known to the engine.
for node, modules := range map[string][]string{"novox": {"nats"}, "g14": {"web"}} { for node, modules := range map[string][]string{"novox": {"nats"}, "g14": {"web"}} {
pl := delivery.Placement{V: delivery.Version, Node: node} pl := delivery.Placement{V: delivery.Version, Node: node}
@@ -108,7 +109,6 @@ func TestTheVaultWholeRotatesANodesLoginWithAStandInEngine(t *testing.T) {
} }
} }
} }
p.put(t, "mesh-controller.lease", LeaseKey, map[string]any{"handed-to": "the operator's hand-over"})
oldPassword := "the-old-password-of-g14" oldPassword := "the-old-password-of-g14"
oldHash, _ := bcrypt.GenerateFromPassword([]byte(oldPassword), bcrypt.MinCost) oldHash, _ := bcrypt.GenerateFromPassword([]byte(oldPassword), bcrypt.MinCost)
p.put(t, "mesh-vault.bus-accounts", "node.g14", AccountHash{User: "node.g14", Hash: string(oldHash), Adopted: true}) p.put(t, "mesh-vault.bus-accounts", "node.g14", AccountHash{User: "node.g14", Hash: string(oldHash), Adopted: true})
+1 -2
View File
@@ -307,7 +307,7 @@ func definitions(env *Env) []toolDef {
{Name: Seat + ".issue-account", {Name: Seat + ".issue-account",
Description: "Mint one bus account the grants part wants, now (novox/hq ADR 0561 §9): the recovery when the vault did not " + Description: "Mint one bus account the grants part wants, now (novox/hq ADR 0561 §9): the recovery when the vault did not " +
"hear a want, and what the controller's `module issue` forwards to (issue 774). Reads the wanted key afresh from " + "hear a want, and what the controller's `module issue` forwards to (issue 774). Reads the wanted key afresh from " +
"mesh-grants.accounts and mints it as a want would, whoever holds the controller's lease: never for a node's " + "mesh-grants.accounts and mints it as a want would: never for a node's " +
"account or the controller's, never a user nobody wants, and never anew for one whose value the vault holds or whose hash it adopted (it says so). Answers " + "account or the controller's, never a user nobody wants, and never anew for one whose value the vault holds or whose hash it adopted (it says so). Answers " +
"whether it minted, the secret and generation, never the value or the hash.", "whether it minted, the secret and generation, never the value or the hash.",
Input: map[string]any{"user": str("the bus user, e.g. ace.mesh-build-go")}, Input: map[string]any{"user": str("the bus user, e.g. ace.mesh-build-go")},
@@ -645,7 +645,6 @@ func definitions(env *Env) []toolDef {
Description: "What needs a person's eye in the vault: operator copies still sealed to an earlier key, the operator " + Description: "What needs a person's eye in the vault: operator copies still sealed to an earlier key, the operator " +
"key the mesh's export holds if the vault refused it, how often the record of hand-outs was rolled, and " + "key the mesh's export holds if the vault refused it, how often the record of hand-outs was rolled, and " +
"minting (novox/hq ADR 0561 §9, issue 774): the vault mints every wanted bus account it holds no value for, " + "minting (novox/hq ADR 0561 §9, issue 774): the vault mints every wanted bus account it holds no value for, " +
"whoever holds the controller's lease; whether that lease is handed away (it turns retirement and rotation), " +
"every wanted account it holds back with why (never one whose value it holds), and those it minted.", "every wanted account it holds back with why (never one whose value it holds), and those it minted.",
run: func(string, map[string]any) (any, error) { run: func(string, map[string]any) (any, error) {
if env.Vault == nil { if env.Vault == nil {
+1 -3
View File
@@ -69,9 +69,7 @@ type Vault struct {
WantRead func(user string) ([]byte, bool, error) WantRead func(user string) ([]byte, bool, error)
// Placement reads the seat's placement (placed.go): where modules stand once the assignments are gone. // Placement reads the seat's placement (placed.go): where modules stand once the assignments are gone.
Placement PlacementReader Placement PlacementReader
// LeaseRead reads the controller's lease afresh before a mint (lease.go): nil reads it only as the watch hands it. // minting is what the vault knows of the wanted bus accounts (mint.go).
LeaseRead func() ([]byte, bool, error)
// minting is the switch on the lease and what the vault knows of the wanted bus accounts (mint.go).
minting minting minting minting
// Signers is the vault's state `signers`, where it publishes its signing halves (signing.go, ADR 0550 §2): nil writes // Signers is the vault's state `signers`, where it publishes its signing halves (signing.go, ADR 0550 §2): nil writes
// none. // none.
@@ -135,7 +135,7 @@ func filesUnder(t *testing.T, dir string) map[string]string {
// (ADR 0547 clause 9), opened in accounts.go alone. It reads five buckets of others', and only reads them: the // (ADR 0547 clause 9), opened in accounts.go alone. It reads five buckets of others', and only reads them: the
// seat mesh-provisioner's plans, for what each node's plan names (ADR 0570 clause 2), (askers.go, design 57 V4), the node-engines' haves, where // seat mesh-provisioner's plans, for what each node's plan names (ADR 0570 clause 2), (askers.go, design 57 V4), the node-engines' haves, where
// the bus node hands the loaded list's hashes sealed to the vault (adopt.go, ADR 0547 clause 9), the grants part's wanted // the bus node hands the loaded list's hashes sealed to the vault (adopt.go, ADR 0547 clause 9), the grants part's wanted
// accounts and the controller's lease (mint.go, lease.go, ADR 0561 §2, §3), and the seat's placement (placed.go), in // accounts (mint.go, ADR 0561 §2), and the seat's placement (placed.go), in
// main.go alone. // main.go alone.
func TestNoBucketHoldsASecret(t *testing.T) { func TestNoBucketHoldsASecret(t *testing.T) {
raw, err := os.ReadFile("../../module.json") raw, err := os.ReadFile("../../module.json")
@@ -151,9 +151,9 @@ func TestNoBucketHoldsASecret(t *testing.T) {
"buckets are the accounts' hashes (ADR 0547 clause 9), the signing keys' public halves (ADR 0550 §2) and the "+ "buckets are the accounts' hashes (ADR 0547 clause 9), the signing keys' public halves (ADR 0550 §2) and the "+
"credentials' outcomes, names and generations alone (ADR 0570 clauses 3, 6) and each node's login in use (ADR 0583)", s) "credentials' outcomes, names and generations alone (ADR 0570 clauses 3, 6) and each node's login in use (ADR 0583)", s)
} }
if r, _ := json.Marshal(manifest["reads"]); string(r) != `["node-engine.have","mesh-grants.accounts","mesh-controller.lease","mesh-delivery-assigner.placement","mesh-provisioner.plans","mesh-provisioner.provisions"]` { if r, _ := json.Marshal(manifest["reads"]); string(r) != `["node-engine.have","mesh-grants.accounts","mesh-delivery-assigner.placement","mesh-provisioner.plans","mesh-provisioner.provisions"]` {
t.Errorf("the manifest reads %s: the vault reads the seat mesh-provisioner's plans and provisions, the haves, the "+ t.Errorf("the manifest reads %s: the vault reads the seat mesh-provisioner's plans and provisions, the haves, the "+
"grants part's wanted accounts, the lease and the placement alone", r) "grants part's wanted accounts and the placement alone, never the controller's lease (issue 1126)", r)
} }
sources, _ := filepath.Glob("*.go") sources, _ := filepath.Glob("*.go")
bucket := regexp.MustCompile(`stdio\.State\(|nats-io|jetstream|KeyValue`) bucket := regexp.MustCompile(`stdio\.State\(|nats-io|jetstream|KeyValue`)
@@ -195,9 +195,6 @@ func TestNoBucketHoldsASecret(t *testing.T) {
code = []byte(strings.Replace(string(code), "stdio.State(HaveRead).Keys(", "", 1)) code = []byte(strings.Replace(string(code), "stdio.State(HaveRead).Keys(", "", 1))
code = []byte(strings.Replace(string(code), "stdio.State(WantedRead).Watch(", "", 1)) code = []byte(strings.Replace(string(code), "stdio.State(WantedRead).Watch(", "", 1))
code = []byte(strings.Replace(string(code), "stdio.State(WantedRead).Get(", "", 1)) code = []byte(strings.Replace(string(code), "stdio.State(WantedRead).Get(", "", 1))
// And the controller's lease, one read and one watch (ADR 0561 §3).
code = []byte(strings.Replace(string(code), "stdio.State(LeaseRead).Get(", "", 1))
code = []byte(strings.Replace(string(code), "stdio.State(LeaseRead).Watch(", "", 1))
// And the seat's placement, where modules stand (placed.go): its keys and one key, read. // And the seat's placement, where modules stand (placed.go): its keys and one key, read.
code = []byte(strings.Replace(string(code), "stdio.State(PlacementRead).Keys(", "", 1)) code = []byte(strings.Replace(string(code), "stdio.State(PlacementRead).Keys(", "", 1))
code = []byte(strings.Replace(string(code), "stdio.State(PlacementRead).Get(", "", 1)) code = []byte(strings.Replace(string(code), "stdio.State(PlacementRead).Get(", "", 1))
@@ -922,7 +919,7 @@ func TestTheRecordOfHandOutsIsBounded(t *testing.T) {
// broker-nats-tools, wanted as a process account, has the random value the controller moved in replaced by a minted // broker-nats-tools, wanted as a process account, has the random value the controller moved in replaced by a minted
// account, and a value that is no account is refused for it from then on. // account, and a value that is no account is refused for it from then on.
func TestABusAccountIsKeptOnlyAsAnAccount(t *testing.T) { func TestABusAccountIsKeptOnlyAsAnAccount(t *testing.T) {
v, accounts, _ := mintingVault(t, true) v, accounts, _ := mintingVault(t)
const tools, address = "own/novox/nats/broker-nats-tools", "own/novox/mesh-controller/broker-address" const tools, address = "own/novox/nats/broker-nats-tools", "own/novox/mesh-controller/broker-address"
for _, row := range []struct { for _, row := range []struct {
name, value string name, value string
-1
View File
@@ -32,7 +32,6 @@
"reads": [ "reads": [
"node-engine.have", "node-engine.have",
"mesh-grants.accounts", "mesh-grants.accounts",
"mesh-controller.lease",
"mesh-delivery-assigner.placement", "mesh-delivery-assigner.placement",
"mesh-provisioner.plans", "mesh-provisioner.plans",
"mesh-provisioner.provisions" "mesh-provisioner.provisions"