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())