Give the control records their layouts, a person's policy and its one derivation, and the engine's own packages key, so the assigner, the indexer and the nodes keep them without the controller (issue 1953 L5-S2, S3, S4)

This commit is contained in:
2026-10-29 22:14:01 +01:00
parent fbd00c8943
commit 20cac82ab7
7 changed files with 240 additions and 1 deletions
+17 -1
View File
@@ -75,7 +75,9 @@ func engineBeyond(user, g string) bool {
}
return strings.HasPrefix(g, "mesh.seat."+EngineSeat+".") && strings.Contains(g, "."+node) ||
strings.HasPrefix(g, "$SRV.") || strings.Contains(g, ".node-engine-"+node) ||
strings.Contains(g, "KV_"+EngineActsBucket) || strings.HasPrefix(g, "$KV."+EngineActsBucket+"."+node+".")
strings.Contains(g, "KV_"+EngineActsBucket) || strings.HasPrefix(g, "$KV."+EngineActsBucket+"."+node+".") ||
// And its node's declared packages (novox/hq ADR 0565 clause 6).
strings.Contains(g, "KV_"+EnginePackagesBucket) || g == "$KV."+EnginePackagesBucket+"."+node
}
// createsAnAccountsConsumer is the grants part's create and read of a durable consumer an account needs, by its stream
@@ -643,6 +645,20 @@ func healthChecks(t *testing.T) {
"a.nats": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-host_acts.g14.a1"},
"a.shop.nats-tools": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-conditions_conditions.x"},
}
// ADR 0565 clauses 6, 7 (issue 1953 L5-S4): a node's engine writes its own packages key and no other node's; only the
// assigner writes its policies.
for _, u := range users {
perms, err := PermissionsFor(u)
if err != nil {
continue
}
if u.Kind == KindNode && (!MayPublish(perms, "$KV.mesh-host_packages.a") || MayPublish(perms, "$KV.mesh-host_packages.b")) {
t.Errorf("node %s writes the packages keys %v", u.Node, perms.Publish)
}
if u.Kind == KindModule && MayPublish(perms, "$KV.mesh-delivery-assigner_policies.x") {
t.Errorf("%s writes the assigner's policies", u.Username())
}
}
// The tools account's secret is the module's own broker-<binary>, which its manifest's own-secrets names and the
// vault keeps by that name (AccountSecret).
if w := (WantedAccount{User: "a.nats.nats-tools", Kind: KindProcess, Node: "a", Module: "nats", Process: "nats-tools",
+15
View File
@@ -3,6 +3,7 @@ package buscompose
import (
"git.novox.be/novox/mesh-sdk/go/asks"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// The node seat node-engine (novox/hq ADR 0563 §1): every node's engine holds it on its own bus account, and no module
@@ -22,6 +23,16 @@ const (
// EngineActsBucket is that state's bucket.
var EngineActsBucket = BucketName(EngineModule, EngineActsState)
// EnginePackagesBucket is the engine's state of its node's declared packages (novox/hq ADR 0565 clause 6): one key, the
// node's name, written by that node's engine alone.
var EnginePackagesBucket = BucketName(EngineModule, delivery.EnginePackagesState)
// EnginePackagesGrants is what a node's engine publishes to keep its node's key of the packages bucket: the key alone,
// and its read.
func EnginePackagesGrants(node string) []string {
return append([]string{"$KV." + EnginePackagesBucket + "." + node}, readGrants(EnginePackagesBucket, node)...)
}
// engineGrants is what a node's engine is granted to hold its node seat and to ask the operator (ADR 0563 §1, §3): it
// answers its own node's verbs (the caller-named subjects are added beside them with every principal's), says the
// seat's events about its node, answers discovery for the seat, asks who answers what (the login shell's execute, ADR
@@ -41,5 +52,9 @@ func engineGrants(p Principal) (pub, sub []string) {
if own, err := NodeKeysGrants(EngineActsBucket, p.Node); err == nil {
pub = append(pub, own...)
}
// And its node's declared packages, its own key alone (ADR 0565 clause 6).
if safeSubject.MatchString(p.Node) {
pub = append(pub, EnginePackagesGrants(p.Node)...)
}
return append(pub, tp...), append(sub, ts...)
}
+4
View File
@@ -248,6 +248,10 @@ var WritersTable = []WriterRow{
Others: "read through `reads` as node-engine.have", Subjects: kvOf(delivery.HaveBucket), Writes: ownNodeKeys},
// The acts a node's engine was asked through its node seat, and its own asks (novox/hq ADR 0563 §2, §3): the
// node-engine's declared state, created by the grants part, written by each node's engine under its own keys.
// A node's declared packages (novox/hq ADR 0565 clause 6): its engine's own key, read by pacman's verbs.
{State: "a machine's declared packages", Writer: "the node-engine of the machine, its key `<node>` alone",
KeptIn: "key-value " + EnginePackagesBucket, Others: "read by pacman through `reads`",
Subjects: kvOf(EnginePackagesBucket), Writes: ownNodeKeys},
{State: "a machine's hand acts", Writer: "the node-engine of the machine, under its own keys `<node>.>`, compare-and-set",
KeptIn: "key-value " + EngineActsBucket + ", the node-engine's declared state acts",
Others: "read through the node seat's verb acts", Subjects: kvOf(EngineActsBucket),
+44
View File
@@ -3,6 +3,7 @@ package catalogue
import (
"fmt"
"strings"
"time"
)
// What the mesh does when a module's build moves (novox/hq ADR 0236, extending ADR 0162 §3 and ADR
@@ -118,3 +119,46 @@ func (m Manifest) irreplaceable() string {
}
return ""
}
// PersonPolicy is a person's choice of a module's upgrade policy, as the assigner keeps it in its state `policies`, one
// key per module (novox/hq ADR 0565 clause 7, issue 1953 L5-S2): the policy, why, who as the runtime proved the caller,
// and when. Set and taken back through `mesh-wants.upgrade`, each recorded as a hand act.
type PersonPolicy struct {
V int `json:"v"`
Module string `json:"module"`
Policy string `json:"policy"`
Why string `json:"why"`
By string `json:"by"`
At time.Time `json:"at"`
}
// PersonPolicyVersion is the version of a person's policy's layout.
const PersonPolicyVersion = 1
// Check refuses a choice the assigner could not keep: a policy that is not roll, together or record, one other than
// roll without why, or one with no person.
func (p PersonPolicy) Check() error {
switch {
case p.Module == "":
return fmt.Errorf("a person's policy names its module")
case p.Policy != PolicyRoll && p.Policy != PolicyTogether && p.Policy != PolicyRecord:
return fmt.Errorf("%s: a policy is %q, %q or %q, not %q", p.Module, PolicyRoll, PolicyTogether, PolicyRecord, p.Policy)
case strings.TrimSpace(p.Why) == "":
return fmt.Errorf("%s: a person's policy says why", p.Module)
case strings.TrimSpace(p.By) == "":
return fmt.Errorf("%s: a person's policy names who chose it", p.Module)
}
return nil
}
// Upgrade is a module's policy and where it comes from (ADR 0565 clause 7): the one derivation every reader takes,
// `mesh-wants.upgrade` and each node agent's judgement of a move alike. A person's choice over the module's word, its
// irreplaceable data and the default; the bus is never rolled, whoever says so (ADR 0236). By names the person when one
// chose.
func Upgrade(m Manifest, chosen *PersonPolicy) (policy, from, why, by string) {
policy, from, why = DerivedUpgrade(m)
if from == FromBus || chosen == nil {
return policy, from, why, ""
}
return chosen.Policy, FromPerson, chosen.Why, chosen.By
}
+20
View File
@@ -32,6 +32,26 @@ func TestWhereAModulesUpgradePolicyComesFrom(t *testing.T) {
if policy != c.policy || from != c.from {
t.Errorf("%s: %s from %s, want %s from %s", c.name, policy, from, c.policy, c.from)
}
// ADR 0565 clause 7 (issue 1953 L5-S3): a person's choice wins over each source but the bus; none leaves the
// derivation as it was.
person := &PersonPolicy{V: PersonPolicyVersion, Module: c.m.Module, Policy: PolicyTogether, Why: "one zone", By: "person.jochen"}
policy, from, _, by := Upgrade(c.m, person)
wantPolicy, wantFrom, wantBy := PolicyTogether, FromPerson, "person.jochen"
if c.from == FromBus {
wantPolicy, wantFrom, wantBy = c.policy, FromBus, ""
}
if policy != wantPolicy || from != wantFrom || by != wantBy {
t.Errorf("%s with a person's choice: %s from %s by %q", c.name, policy, from, by)
}
if policy, from, _, _ := Upgrade(c.m, nil); policy != c.policy || from != c.from {
t.Errorf("%s with no choice: %s from %s", c.name, policy, from)
}
}
for _, p := range []PersonPolicy{{Module: "m", Policy: "sometimes", Why: "x", By: "p"}, {Module: "m", Policy: PolicyRecord, By: "p"},
{Module: "m", Policy: PolicyRecord, Why: "x"}} {
if p.Check() == nil {
t.Errorf("%+v was accepted", p)
}
}
}
+95
View File
@@ -0,0 +1,95 @@
package delivery
import (
"encoding/json"
"fmt"
"strings"
"time"
)
// **The control records' layouts, out of the controller** (novox/hq ADR 0565 clauses 2, 3 and 6; issue 1953 L5-S2): what
// the indexer keeps of the catalogue's predefined list and of each open pull request's config declarations, which
// mesh-config reads in place of the controller's mirrors and `pull-declares`; and each node's declared packages, which
// its engine writes in place of the controller's mirror.
// The indexer's two states (clause 2).
const (
// IndexerPredefinedState holds one key, PredefinedListKey: the catalogue's predefined.json at the newest merge taken.
IndexerPredefinedState = "predefined"
PredefinedListKey = "list"
// IndexerConfigCandidatesState holds one key per open pull request (ConfigCandidateKey).
IndexerConfigCandidatesState = "config-candidates"
)
// PredefinedKept is the indexer's key `predefined.list`: the list whole, as predefined.json holds it, with the merge it
// was read at. A merge older than the one the key names never replaces it (ADR 0348 §7).
type PredefinedKept struct {
V int `json:"v"`
Commit string `json:"commit"`
At time.Time `json:"at"`
List json.RawMessage `json:"list"`
}
// ConfigDeclaration is one config key a module's manifest declares, in the layout of the controller's declared-config
// mirror (mesh-controller internal/inventory DeclaredMirrored) unchanged, so mesh-config reads the indexer's as it read
// the mirror's.
type ConfigDeclaration struct {
Kind string `json:"kind"`
Default json.RawMessage `json:"default"`
Why string `json:"why"`
Type string `json:"type,omitempty"`
Description string `json:"description,omitempty"`
Enum json.RawMessage `json:"enum,omitempty"`
Minimum *float64 `json:"minimum,omitempty"`
Maximum *float64 `json:"maximum,omitempty"`
Pattern string `json:"pattern,omitempty"`
Trusted bool `json:"trusted,omitempty"`
Predefined string `json:"predefined,omitempty"`
Required bool `json:"required,omitempty"`
Runtime bool `json:"runtime,omitempty"`
}
// ConfigCandidate is one open pull request's key of `config-candidates`: its head, and per module the config
// declarations the head's manifests add or change against the trunk's, an empty set when none. Written at each new
// head and each trunk merge that changes a module it names; deleted when the pull request merges or closes.
type ConfigCandidate struct {
V int `json:"v"`
Pull string `json:"pull"`
Head string `json:"head"`
Declares map[string]map[string]ConfigDeclaration `json:"declares"`
At time.Time `json:"at"`
}
// ConfigCandidateKey is a pull request's key, `owner/repository#number` with `/` and `#` as dots, the form mesh-config
// keeps a pull request's values under.
func ConfigCandidateKey(pull string) (string, error) {
owner, rest, ok := strings.Cut(strings.TrimSpace(pull), "/")
repo, number, ok2 := strings.Cut(rest, "#")
if !ok || !ok2 || owner == "" || repo == "" || number == "" || strings.ContainsAny(owner+repo+number, ".*> ") {
return "", fmt.Errorf("delivery: %q is not a pull request, owner/repository#number", pull)
}
return owner + "." + repo + "." + number, nil
}
// The node-engine's packages (clause 6): its state `packages`, one key, the node's name, written by that node's engine
// alone.
const EnginePackagesState = "packages"
// PackageDeclared is one package a node's modules declare, the controller's declared-packages mirror's entry unchanged
// (mesh-controller internal/inventory PackageDeclared): its name, the modules that declare it that way, sorted, and
// whether they declare it absent.
type PackageDeclared struct {
Package string `json:"package"`
Modules []string `json:"modules"`
Absent bool `json:"absent"`
}
// DeclaredPackages is one node's key of `packages`, the mirror's value unchanged: its packages sorted by name, absent
// after present. Fixed fields and no time, so the same declaration is the same bytes (ADR 0289 §1).
type DeclaredPackages struct {
Node string `json:"node"`
Packages []PackageDeclared `json:"packages"`
}
// The assigner's state of a person's upgrade policy, one key per module (clause 7; catalogue.PersonPolicy).
const AssignerPoliciesState = "policies"
+45
View File
@@ -0,0 +1,45 @@
package delivery
import (
"encoding/json"
"testing"
)
// ADR 0565 clauses 2 and 6 (issue 1953 L5-S2): the control records' layouts read what their writers write. The
// packages value is the controller's declared-packages mirror's, as its writer's own test pins it (mesh-controller
// internal/inventory/declared_packages_test.go at 0fbb5ef3); the config declaration is its declared-config mirror's
// (DeclaredMirrored); a pull request's key is mesh-config's form.
func TestTheControlRecordsReadTheirWritersValues(t *testing.T) {
for _, c := range []struct {
name string
raw string
into any
}{
{"declared packages", `{"node":"one","packages":[{"package":"curl","modules":["backup","web"],"absent":false},` +
`{"package":"nano","modules":["web"],"absent":true}]}`, &DeclaredPackages{}},
{"a config candidate", `{"v":1,"pull":"novox/mesh-catalog#42","head":"abc","declares":{"mail":{"quota":{"kind":"config",` +
`"default":10,"why":"a quota","type":"integer","minimum":1}}},"at":"2026-10-29T12:00:00Z"}`, &ConfigCandidate{}},
{"the predefined list", `{"v":1,"commit":"abc","at":"2026-10-29T12:00:00Z","list":{"domain":{"type":"string"}}}`, &PredefinedKept{}},
} {
if err := json.Unmarshal([]byte(c.raw), c.into); err != nil {
t.Fatalf("%s: %v", c.name, err)
}
again, _ := json.Marshal(c.into)
var want, got any
_ = json.Unmarshal([]byte(c.raw), &want)
_ = json.Unmarshal(again, &got)
if string(jsonOf(want)) != string(jsonOf(got)) || (c.name == "declared packages" && string(again) != c.raw) {
t.Errorf("%s reads back as %s", c.name, again)
}
}
for pull, want := range map[string]string{"novox/mesh-catalog#42": "novox.mesh-catalog.42", "x#1": "", "a/b": "", "a/b.c#1": ""} {
if got, err := ConfigCandidateKey(pull); got != want || (want == "") != (err != nil) {
t.Errorf("%s keyed %q, %v", pull, got, err)
}
}
}
func jsonOf(v any) []byte {
b, _ := json.Marshal(v)
return b
}