Design 47's last milestones as one release: the contracts for health, control records, provisioning and history leaving the controller (issues 774, 1126, 1952, 1953, 1954, 1955, 1289) #133

Merged
mesh-tools merged 53 commits from feat/774-l3-reland into main 2026-10-30 11:01:40 +00:00
6 changed files with 74 additions and 33 deletions
Showing only changes of commit 7bd0f6c2bd - Show all commits
+19 -6
View File
@@ -614,7 +614,9 @@ func healthChecks(t *testing.T) {
users, err := Users(Records{Nodes: []string{"a"}, Assigned: map[string][]Declared{"a": {
{Module: "mesh-conditions", State: []Bucket{bucket("mesh-conditions", "conditions"), bucket("mesh-conditions", "takeover")},
Reads: []string{"mesh-self-check.findings", "mesh-healers.attempts", ControllerConditions}},
{Module: "mesh-self-check", State: []Bucket{bucket("mesh-self-check", "findings")}, Reads: []string{"restic.good"}},
{Module: "mesh-self-check", State: []Bucket{bucket("mesh-self-check", "findings")}, Reads: []string{"restic.good", GrantsWithheldRead}},
// Issue 1955 L7: the grants part keeps the hearings it withholds; the self-check reads them.
{Module: GrantsModule, State: []Bucket{bucket(GrantsModule, GrantsWithheldState)}},
{Module: "mesh-healers", State: []Bucket{bucket("mesh-healers", "findings"), bucket("mesh-healers", "attempts"), bucket("mesh-healers", "would")}},
{Module: WatchmanModule, State: []Bucket{bucket(WatchmanModule, "findings")}, Reads: []string{delivery.NodeHealthRead}},
{Module: "mesh-hand-acts", State: []Bucket{bucket("mesh-hand-acts", "warrants")}, Logs: []Log{{Module: "mesh-hand-acts", Name: "acts"}}},
@@ -624,8 +626,10 @@ func healthChecks(t *testing.T) {
bucket("mesh-provisioner", "registry")}},
{Module: "mesh-vault", Reads: []string{"mesh-provisioner.provisions", "mesh-provisioner.plans"}},
{Module: "postgres", Reads: []string{"mesh-provisioner.provisions", "mesh-provisioner.plans"}},
// ADR 0571 clauses 2, 4 (issue 1955): the artifact store's seat says artifacts-gone; the assigner keeps seat-holders.
{Module: "artifact-store-tools", Holds: []Seat{{Name: "artifact-store", Emits: []string{"artifacts-gone"}}},
// The node's provisioner client reads its machine's plans and records no needs (ADR 0570 clause 2; its V3).
{Module: "mesh-provisioner-client", ProvisionsNode: true},
// ADR 0571 clauses 2, 4 (issue 1955): the seat mesh-artifact-sweep says artifacts-gone; the assigner keeps seat-holders.
{Module: "artifact-store-tools", Holds: []Seat{{Name: "mesh-artifact-sweep", Emits: []string{"artifacts-gone"}}},
Reads: []string{delivery.SeatHoldersRead}},
{Module: delivery.AssignerModule, State: []Bucket{bucket(delivery.AssignerModule, delivery.SeatHoldersState)}},
// ADR 0571 clause 3: disk-load keeps the readings; the backup holder reads them.
@@ -652,8 +656,12 @@ func healthChecks(t *testing.T) {
"$JS.API.DIRECT.GET.KV_mesh-provisioner_plans.$KV.mesh-provisioner_plans.a.keycloak"},
"a.mesh-hand-acts": {"$KV.mesh-hand-acts_warrants.mesh-config.ask-1", "mesh.log.mesh-hand-acts.acts.act-1"},
"a.disk-load": {"$KV.disk-load_readings.a.pg.data"},
"provisioner.a": {"$JS.API.DIRECT.GET.KV_mesh-provisioner_plans.$KV.mesh-provisioner_plans.a.keycloak",
"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_plans.c1.$KV.mesh-provisioner_plans.a.keycloak"},
// ADR 0571 clause 3: restic's bundle writes each item's last good night as its own state; the self-check reads it.
"a.mesh-self-check": {"$JS.API.DIRECT.GET.KV_restic_good.$KV.restic_good.a.pg.data"},
"a.mesh-self-check": {"$JS.API.DIRECT.GET.KV_restic_good.$KV.restic_good.a.pg.data",
"$JS.API.DIRECT.GET.KV_mesh-grants_withheld.$KV.mesh-grants_withheld." + WithheldKey(Withheld{Node: "a", Module: "reader", Stream: "MAIL"})},
"a.mesh-grants": {"$KV.mesh-grants_withheld.reader.mail.a"},
"a.restic": {"$JS.API.DIRECT.GET.KV_disk-load_readings.$KV.disk-load_readings.a.pg.data", "$KV.restic_good.a.pg.data",
// ADR 0571 clause 7: its own membership from the grants part's state.
"$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.a.restic"},
@@ -672,15 +680,20 @@ func healthChecks(t *testing.T) {
mayNot := map[string][]string{
// The keeper writes no findings and no attempts; no observer writes the keeper's buckets (clause 2).
"a.mesh-conditions": {"$KV.mesh-self-check_findings.x", "$KV.mesh-healers_attempts.x", "$KV.mesh-controller_conditions.x"},
"a.mesh-self-check": {"$KV.mesh-conditions_conditions.x", "$KV.mesh-conditions_takeover.switch", "$KV.restic_good.a.pg.data"},
"a.mesh-self-check": {"$KV.mesh-conditions_conditions.x", "$KV.mesh-conditions_takeover.switch", "$KV.restic_good.a.pg.data",
"$KV.mesh-grants_withheld.reader.mail.a"},
// Only mesh-provisioner writes provisions and plans, and it writes none of the grants part's states (ADR 0570
// clauses 1, 8).
"a.mesh-provisioner": {"$KV.mesh-grants_accounts.a.m", "$KV.mesh-grants_user-list.sealed"},
"a.mesh-vault": {"$KV.mesh-provisioner_provisions.a.keycloak.postgres-database", "$KV.mesh-provisioner_plans.a.keycloak",
"mesh.seat.artifact-store.event.artifacts-gone"},
"mesh.seat.mesh-artifact-sweep.event.artifacts-gone"},
// Each writes its own state alone, and these users are composed.
"a.disk-load": {"$KV.restic_good.a.pg.data"},
"a.mesh-delivery-builder": {"$KV.disk-load_readings.a.pg.data"},
// Another machine's plans are not its to read; no client writes or reads the needs any more.
"provisioner.a": {"$JS.API.DIRECT.GET.KV_mesh-provisioner_plans.$KV.mesh-provisioner_plans.b.keycloak",
"$KV.mesh-provisioner_plans.a.keycloak", "$KV." + ProvisionerNeedsBucket + ".a.keycloak.db.x",
"$JS.API.DIRECT.GET.KV_" + ProvisionerNeedsBucket + ".$KV." + ProvisionerNeedsBucket + ".a.keycloak.db.x"},
// Only the assigner writes seat-holders.
"a.artifact-store-tools": {"$KV.mesh-delivery-assigner_seat-holders.mesh-broker"},
// Only disk-load writes its readings.
+10 -6
View File
@@ -3,9 +3,11 @@ package buscompose
import (
"errors"
"fmt"
"git.novox.be/novox/mesh-sdk/go/delivery"
"strings"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/provisions"
"github.com/nats-io/nats.go"
)
@@ -34,9 +36,10 @@ var ConfigResolvedBucket = BucketName("mesh-config", "resolved")
// ProvisionerReads are the domains' buckets a provisioner client reads its own machine's keys of, each keyed
// `<node>.…` (research 051): placement, the controller's assignments mirror (`<node>.<module>`), whose value carries
// the provisioner's plan of a module handed to it (inventory.Provisioning, design 57 P5); and config,
// mesh-config's resolved values. **Named one by one**, as the controller's asks are: a domain that moves to the
// mesh-config's resolved values; and the seat mesh-provisioner's plans of its machine, `<node>.<module>` (novox/hq ADR
// 0570 clause 2). **Named one by one**, as the controller's asks are: a domain that moves to the
// provisioner adds its bucket here, and a bucket not named is one the client cannot read.
var ProvisionerReads = []string{ConfigResolvedBucket}
var ProvisionerReads = []string{ConfigResolvedBucket, provisions.PlansBucket}
// ProvisionerReadsWhole are the buckets a provisioner client reads every key of: the controller's assignments mirror,
// where a provider's consumers are bound to it in their own machines' plans, which the provider machine's client
@@ -68,9 +71,10 @@ var (
)
// ProvisionerWrites are the server's buckets a client writes, and reads, its own machine's keys of (`<node>.>`)
// and no other's: the needs it records, what it applied, and the items of the collections on its machine. A
// client cannot write another machine's needs, or say another machine applied a file.
var ProvisionerWrites = []string{ProvisionerNeedsBucket, ProvisionerAppliedBucket, ProvisionerCollectBucket}
// and no other's: what it applied, and the items of the collections on its machine. A client cannot say another
// machine applied a file. It records no needs: since the client reads its machine's plans from the seat
// mesh-provisioner's `plans` (novox/hq ADR 0570 clause 2; the client's V3), the needs bucket is granted to none.
var ProvisionerWrites = []string{ProvisionerAppliedBucket, ProvisionerCollectBucket}
// VerbsTheProvisionerAsksTheVault are the vault's verbs a provisioner client asks in its own name (ADR 0349
// clause 11): `get`, a secret its machine's declaration names, answered sealed to the asker's key. On the subject
+28 -1
View File
@@ -3,6 +3,7 @@ package buscompose
import (
"slices"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/handacts"
@@ -79,7 +80,7 @@ func grantsOwnObjects(p Principal, pattern string) bool {
// readers too (announce.State.Readers); both are honoured.
const VaultBusAccounts = "mesh-vault.bus-accounts"
// ArtifactStoreTools is the module holding the seat artifact-store (novox/hq ADR 0571 clause 2).
// ArtifactStoreTools is the module holding the seat mesh-artifact-sweep (novox/hq ADR 0571 clause 2).
const ArtifactStoreTools = "artifact-store-tools"
// NamedReaders are the states only the modules named may read, by `<owner>.<name>`: the grants part's wanted accounts
@@ -125,3 +126,29 @@ func withNamedReaders(d Declared) Declared {
d.ReadsRefused = append(append([]string(nil), d.ReadsRefused...), refused...)
return d
}
// **The hearings the grants part withholds, on the bus** (issue 1955 L7, mesh-delivery e564eaf): a hearing of a restricted
// stream a module asks in the tool runner, withheld because only an account of its own may hear it (ADR 0376 §2), kept
// in the grants part's state `withheld`, one key `<module>.<stream, lower case>.<machine>` (the controller's condition id
// of it), written on change only and deleted once heard. The self-check's probe D-heard reads it there.
const (
GrantsWithheldState = "withheld"
GrantsWithheldBucket = GrantsModule + "_" + GrantsWithheldState
GrantsWithheldRead = GrantsModule + "." + GrantsWithheldState
WithheldVersion = 1
)
// WithheldHearing is one withheld hearing: the machine, the module, the restricted stream and its emitter, the plain
// words of it and the remedy (Withheld.Said), and when the grants part first wrote it as it stands.
type WithheldHearing struct {
V int `json:"v"`
Node string `json:"node"`
Module string `json:"module"`
Stream string `json:"stream"`
Emitter string `json:"emitter"`
Said string `json:"said"`
At time.Time `json:"at"`
}
// WithheldKey is a withheld hearing's key: `<module>.<stream, lower case>.<machine>`.
func WithheldKey(w Withheld) string { return w.Module + "." + strings.ToLower(w.Stream) + "." + w.Node }
@@ -1,16 +1,17 @@
package catalogue
// **The artifact store's sweep, out of the controller** (novox/hq ADR 0571 clause 2, issue 1955): the seat
// `artifact-store`, held once for the mesh by artifact-store-tools, serves what the controller served of its store
// (artifacts, held, mirrors, images, collect) and says `artifacts-gone`. Its keep set is the store's own listing joined
// **The artifact sweep, out of the controller** (novox/hq ADR 0571 clause 2, issue 1955): what the artifact store keeps
// and lets go of, the seat `mesh-artifact-sweep` (named for its scope, ADR 0121 §1; the operator, 2026-10-29), held
// once for the mesh by artifact-store-tools, serves what the controller served of its store (artifacts, held, mirrors, images, collect) and says `artifacts-gone`. Its keep set is the store's own listing joined
// with every want's fingerprint and each module's five newest builds from the builder's log (ADR 0526 §1); it lets go
// of nothing until the controller's lease names a holder that is no controller.
// ArtifactStoreSeat is the seat artifact-store-tools holds.
const ArtifactStoreSeat = "artifact-store"
// ArtifactSweepSeat is the seat artifact-store-tools holds. Not the seat mesh-artifact-store, the store itself held by
// distribution, and not the provision artifact-store it delivers.
const ArtifactSweepSeat = "mesh-artifact-sweep"
// artifactStoreVerbs are the seat's verbs, with the controller's arguments so a caller changes only the seat it asks.
func artifactStoreVerbs() []Verb {
// artifactSweepVerbs are the seat's verbs, with the controller's arguments so a caller changes only the seat it asks.
func artifactSweepVerbs() []Verb {
return []Verb{
{Name: "artifacts", Description: "Every artifact the mesh made that lives in its artifact store, each with its " +
"repository, digest, kind (image or archive) and state: kept, and why (a want names it, or one of the five " +
@@ -37,14 +38,10 @@ func artifactStoreVerbs() []Verb {
"why": "with confirm: why, required and recorded",
"most": "let go of at most this many (500 by default, at most 5000)",
}, nil, "confirm")},
{Name: "mirrors", Description: "The bases the mesh copied into its artifact store for builds to stand on, each " +
"with its state: kept because a kept build stood on it, or eligible. With record and confirm and why, copies " +
"no record names are recorded as the mesh's. Deletes nothing.",
Input: schema(map[string]string{
"record": "references to record, artifact-store://<repository>@sha256:<hex>, separated by spaces or commas",
"confirm": "\"true\": record them (needs why); without it nothing is recorded",
"why": "with confirm: why, required and recorded",
}, nil, "confirm")},
{Name: "mirrors", Description: "The bases the mesh copied into its artifact store, each kept (a kept build stood on " +
"it), eligible (only builds no longer kept stood on it) or unrecorded (no build the builder's log holds stood on " +
"it: never removed), by what the builder's log says each build stood on. Records nothing by hand.",
Input: schema(map[string]string{}, nil)},
{Name: "images", Description: "Every container image one machine's want and have name, each with the resources " +
"naming it: what the machine keeps when it prunes its images. Reads only.",
Input: schema(map[string]string{"node": "the machine's name"}, []string{"node"})},
+3 -3
View File
@@ -303,9 +303,9 @@ var defaultSeats = append([]Seat{
// policy and the bus step at the assigner, and the hand-act log.
{Name: WantsSeat, Scope: ScopeMesh, Decision: controlRecordsDecision, Emits: []string{"policy-changed"}, Serves: wantsVerbs()},
{Name: HandActsSeat, Scope: ScopeMesh, Decision: controlRecordsDecision, Emits: []string{"act-recorded"}, Serves: handActsVerbs()},
// The artifact store's sweep (novox/hq ADR 0571 clause 2, artifact_store_seat.go).
{Name: ArtifactStoreSeat, Scope: ScopeMesh, Decision: "novox/hq ADR 0571", Emits: []string{"artifacts-gone"},
Serves: artifactStoreVerbs()},
// The artifact sweep (novox/hq ADR 0571 clause 2, artifact_sweep_seat.go).
{Name: ArtifactSweepSeat, Scope: ScopeMesh, Decision: "novox/hq ADR 0571", Emits: []string{"artifacts-gone"},
Serves: artifactSweepVerbs()},
// Deferred renames (novox/hq ADR 0121): these deliver a provision, so renaming them is a
// delivering-seat migration with a mesh-wide cascade if a holder stops resolving mid-flight.
// They keep their names until that migration is done deliberately, apart from the node-* pass.
+2 -2
View File
@@ -77,7 +77,7 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
{WantsSeat, "upgrade,bus", "policy-changed"},
{HandActsSeat, "hand-act,hand-acts,warranted", "act-recorded"},
// ADR 0571 clause 2: the artifact store's sweep.
{ArtifactStoreSeat, "artifacts,held,collect,mirrors,images", "artifacts-gone"},
{ArtifactSweepSeat, "artifacts,held,collect,mirrors,images", "artifacts-gone"},
} {
s, ok := SeatNamed(c.seat)
if !ok || s.Scope != ScopeMesh || s.Replicated || s.Delivers != "" || !strings.Contains(s.Decision, "ADR 05") ||
@@ -122,7 +122,7 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
// mesh-build-machine row goes, when no registered manifest claims it.
// Forty-eight with mesh-delivery-checker (novox/hq ADR 0547 §10, issue 774); forty-nine with node-engine (novox/hq ADR 0563 §1);
// fifty-three with the four health seats (novox/hq ADR 0564 clause 1); fifty-five with mesh-wants and mesh-hand-acts
// (novox/hq ADR 0565 clause 1); fifty-six with artifact-store (novox/hq ADR 0571 clause 2).
// (novox/hq ADR 0565 clause 1); fifty-six with mesh-artifact-sweep (novox/hq ADR 0571 clause 2).
if len(Seats()) != 56 {
t.Errorf("the mesh defines %d seats rather than 56; the set is closed, so a change here is "+
"a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames())