From 7bd0f6c2bdf2f85678d3957764aa8e0f139bbb37 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 29 Oct 2026 23:53:51 +0100 Subject: [PATCH] Name the artifact sweep's seat mesh-artifact-sweep for its scope, grant the provisioner client its machine's plans in place of the needs, and keep the grants part's withheld hearings in one layout, so the seat names follow ADR 0121 and the client and the self-check read the bus (issue 1955, ADR 0571 clause 2, ADR 0570 clause 2) --- go/buscompose/buscompose_test.go | 25 ++++++++++++---- go/buscompose/provisioner.go | 16 ++++++---- go/buscompose/userlist_switch.go | 29 ++++++++++++++++++- ...t_store_seat.go => artifact_sweep_seat.go} | 27 ++++++++--------- go/catalogue/seats.go | 6 ++-- go/catalogue/seats_test.go | 4 +-- 6 files changed, 74 insertions(+), 33 deletions(-) rename go/catalogue/{artifact_store_seat.go => artifact_sweep_seat.go} (70%) diff --git a/go/buscompose/buscompose_test.go b/go/buscompose/buscompose_test.go index 290a569..0138721 100644 --- a/go/buscompose/buscompose_test.go +++ b/go/buscompose/buscompose_test.go @@ -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. diff --git a/go/buscompose/provisioner.go b/go/buscompose/provisioner.go index 5def5fd..19cd361 100644 --- a/go/buscompose/provisioner.go +++ b/go/buscompose/provisioner.go @@ -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 // `.…` (research 051): placement, the controller's assignments mirror (`.`), 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, `.` (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 (`.>`) -// 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 diff --git a/go/buscompose/userlist_switch.go b/go/buscompose/userlist_switch.go index 1bc1303..dc187d8 100644 --- a/go/buscompose/userlist_switch.go +++ b/go/buscompose/userlist_switch.go @@ -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 `.`: 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 `..` (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: `..`. +func WithheldKey(w Withheld) string { return w.Module + "." + strings.ToLower(w.Stream) + "." + w.Node } diff --git a/go/catalogue/artifact_store_seat.go b/go/catalogue/artifact_sweep_seat.go similarity index 70% rename from go/catalogue/artifact_store_seat.go rename to go/catalogue/artifact_sweep_seat.go index 3fa1bf6..cf8cefc 100644 --- a/go/catalogue/artifact_store_seat.go +++ b/go/catalogue/artifact_sweep_seat.go @@ -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://@sha256:, 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"})}, diff --git a/go/catalogue/seats.go b/go/catalogue/seats.go index b45ff1e..e8e68b0 100644 --- a/go/catalogue/seats.go +++ b/go/catalogue/seats.go @@ -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. diff --git a/go/catalogue/seats_test.go b/go/catalogue/seats_test.go index 3182411..e6b206b 100644 --- a/go/catalogue/seats_test.go +++ b/go/catalogue/seats_test.go @@ -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())