From 8d4c13427ad1099dbfc3fbaccb2022e9e8d98d78 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 2 Nov 2026 11:46:12 +0100 Subject: [PATCH] Build against mesh-sdk without the controller (issue 1126) mesh-sdk's group B member renames the grant to Grant, drops the controller principal, the signer and the lease fields from the user list key, and the keeper's and config's take-over verbs. The grants part, the assigner and the view use the new names; the judge's copies of the health and config seats follow; the tests' fixtures lose the controller's user, and the refused principal is one removing what the closed set does not name, the controller's mirror being gone. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- .../cmd/mesh-delivery-assigner/pass.go | 6 +- .../cmd/mesh-delivery-assigner/pass_test.go | 4 +- .../cmd/mesh-delivery-assigner/puts.go | 4 +- .../cmd/mesh-delivery-assigner/world_test.go | 19 ++-- .../internal/assign/wants.go | 4 +- .../internal/assign/wants_test.go | 4 +- modules/mesh-delivery-indexer/judge/rules.go | 34 +------ .../mesh-delivery-indexer/judge/rules_test.go | 6 +- .../cmd/mesh-delivery-view-tools/main.go | 3 +- .../mesh-delivery-view/internal/view/read.go | 12 +-- .../cmd/mesh-grants/integration_test.go | 2 +- modules/mesh-grants/grants_test.go | 89 ++++++++++--------- modules/mesh-grants/internal/grants/grants.go | 14 +-- .../mesh-grants/internal/grants/records.go | 2 +- modules/mesh-grants/internal/grants/sealed.go | 2 +- modules/mesh-grants/internal/grants/tools.go | 12 +-- 16 files changed, 93 insertions(+), 124 deletions(-) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go index 382d1f4..2764e8f 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go @@ -167,7 +167,7 @@ func (p *passer) wants(ctx context.Context) { return p.w.composed(node, name, fp) // a placed node's own: the digest its own have names (ADR 0571 §6) }, GrantedAt: func(fp string) bool { - key, err := delivery.ControllerGrantKey(name, fp) + key, err := delivery.PartGrantKey(name, fp) return err == nil && p.w.has(grants, key) }, Restored: func(node string) bool { @@ -180,13 +180,13 @@ func (p *passer) wants(ctx context.Context) { }} if fp != "" { held, _ := delivery.HeldKey(name, fp) - grant, _ := delivery.ControllerGrantKey(name, fp) + grant, _ := delivery.PartGrantKey(name, fp) hold, _ := delivery.HoldAskKey(name, fp) m.Released = p.answeredWith(hold, "release") m.Kept = p.answeredWith(hold, "keep") m.Built, m.Held, m.Granted = p.w.built(name, fp), p.w.has(delivery.AssignerWantsBucket, held) && !m.Released, p.w.has(grants, grant) if raw := p.w.value(grants, grant); raw != nil { - if g, err := delivery.DecodeControllerGrant(raw); err == nil { + if g, err := delivery.DecodeGrant(raw); err == nil { m.GrantedOn = func(node string) bool { return assign.GrantNamesNode(g, node, name) } } } diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go index 00e59e7..ee60a4e 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go @@ -293,8 +293,8 @@ func putOfTheBus(t *testing.T) { default: } composes(t, p.w, "novox", digest) - gkey, _ := delivery.ControllerGrantKey("nats", fp) - g, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{}) + gkey, _ := delivery.PartGrantKey("nats", fp) + g, _ := delivery.EncodeGrant(delivery.Grant{}) world(delivery.PartGrantsBucket, gkey, g) got := step() akey, _ := delivery.RecordAskKey("novox", "nats", digest) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/puts.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/puts.go index 0729c41..fbf2032 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/puts.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/puts.go @@ -226,9 +226,9 @@ func (p *passer) composedFor(node, module, fp string) (*delivery.ComposedDigest, out = &d } granted := false - if gkey, err := delivery.ControllerGrantKey(module, fp); err == nil { + if gkey, err := delivery.PartGrantKey(module, fp); err == nil { if raw := p.w.value(p.w.grantsBucket(), gkey); raw != nil { - if g, err := delivery.DecodeControllerGrant(raw); err == nil { + if g, err := delivery.DecodeGrant(raw); err == nil { granted = g.NamesNode(node, module) } } diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world_test.go index e21e828..eb14547 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world_test.go @@ -127,25 +127,22 @@ func theWorldCountsTheValuesAKeyTakes(t *testing.T) { if got := w.changed(delivery.HaveBucket, "gone"); got != 0 { t.Errorf("changed = %d for a key never seen", got) } - // ADR 0571 clause 9: grants are read from the grants part's bucket, and the controller's own grants count for nothing. + // ADR 0571 clause 9: grants are read from the grants part's bucket, the one bucket of grants (ADR 0590). fp := delivery.SourcePrefix2 + strings.Repeat("d", 64) - gkey, _ := delivery.ControllerGrantKey("web", fp) - grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{Accounts: []string{"ace.web"}}) + gkey, _ := delivery.PartGrantKey("web", fp) + grant, _ := delivery.EncodeGrant(delivery.Grant{Accounts: []string{"ace.web"}}) for _, row := range []struct { - name string - inPart, inController bool - granted bool + name string + inPart bool + granted bool }{ - {"the part's grant", true, false, true}, - {"only the controller's grant", false, true, false}, + {"the part's grant", true, true}, + {"no grant", false, false}, } { p := newPasser(buckets{}, time.Now()) if row.inPart { p.w.put(delivery.PartGrantsBucket, gkey, grant) } - if row.inController { - p.w.put(delivery.ControllerGrantsBucket, gkey, grant) - } if got := p.w.grantsBucket(); got != delivery.PartGrantsBucket { t.Errorf("%s: grants are read from %s, want the part's", row.name, got) } diff --git a/modules/mesh-delivery-assigner/internal/assign/wants.go b/modules/mesh-delivery-assigner/internal/assign/wants.go index 460808b..3859e68 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants.go @@ -733,8 +733,8 @@ func (m Module) userListRevision(node string, before delivery.Want, had bool, n return w, true } -// GrantNamesNode says whether a grant of module names its accounts on node (mesh-sdk's ControllerGrant.NamesNode, which a +// GrantNamesNode says whether a grant of module names its accounts on node (mesh-sdk's Grant.NamesNode, which a // put reads by too). -func GrantNamesNode(g delivery.ControllerGrant, node, module string) bool { +func GrantNamesNode(g delivery.Grant, node, module string) bool { return g.NamesNode(node, module) } diff --git a/modules/mesh-delivery-assigner/internal/assign/wants_test.go b/modules/mesh-delivery-assigner/internal/assign/wants_test.go index 470ec5c..35af5f8 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants_test.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants_test.go @@ -293,7 +293,7 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { // Rule 6 per node: a grant written for the module's other nodes does not grant a node whose account it does not name // (issue 1945, live on 2026-10-25 at 20:04: ace's mesh-build-go was wanted with no account on the bus). m, n := ready() - grant := delivery.ControllerGrant{Accounts: []string{"ace.mail", "ace.mail.send"}} + grant := delivery.Grant{Accounts: []string{"ace.mail", "ace.mail.send"}} m.GrantedOn = func(node string) bool { return GrantNamesNode(grant, node, "mail") } out := Wants(m, n, wantedOld(), nil) if w := out.Wants["ace"]; w.FP != fNew { @@ -312,7 +312,7 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { {[]string{"g14.mailer"}, "g14", "mail", false}, {[]string{"ace.mail"}, "g14", "mail", false}, } { - if got := GrantNamesNode(delivery.ControllerGrant{Accounts: c.accounts}, c.node, c.module); got != c.named { + if got := GrantNamesNode(delivery.Grant{Accounts: c.accounts}, c.node, c.module); got != c.named { t.Errorf("%v names %s of %s: %v, want %v", c.accounts, c.module, c.node, got, c.named) } } diff --git a/modules/mesh-delivery-indexer/judge/rules.go b/modules/mesh-delivery-indexer/judge/rules.go index fc74f5b..99841cb 100644 --- a/modules/mesh-delivery-indexer/judge/rules.go +++ b/modules/mesh-delivery-indexer/judge/rules.go @@ -7370,21 +7370,8 @@ var defaultSeats = append([]catalogue.Seat{ Serves: []catalogue.Verb{ {Name: "config", Optional: true, Description: "Set, clear, list or read the history of config values: " + "mesh config or node config for a predefined key, or a module's own value on one node, each set " + - "with why, validated, and naming each module on each node whose value moves. A trusted value only " + - "from the controller's terminal.", + "with why, validated, and naming each module on each node whose value moves.", Input: configInput()}, - // The switch of config to its own sources (novox/hq ADR 0565 §4): optional until every mesh-config serves it. - {Name: "compare", Optional: true, Description: "Every difference between config's own sources and the " + - "controller's records, each with its reason: a module's declared config against the controller's mirror, " + - "the indexer's predefined list against the mirror's, and each open pull request's config candidate against " + - "what the controller told. The take-over waits for none.", - Input: schema(map[string]string{}, nil)}, - {Name: "take-over", Optional: true, Description: "Ask the operator's Approve to switch config to its own " + - "sources (the indexer); refused while compare shows a difference.", - Input: schema(map[string]string{"why": "why, in a sentence"}, []string{"why"})}, - {Name: "hand-back", Optional: true, Description: "Switch config back to the controller's mirrors, until the " + - "controller hands its lease away; refused after it.", - Input: schema(map[string]string{"why": "why, in a sentence"}, []string{"why"})}, }}, // The provisioner's server (novox/hq ADR 0349 clause 3, design 57 P4): its holder keeps the registry of which // domain answers which kind of placeholder, and the buckets mesh-provisioner_needs, _applied and _collect, @@ -7967,7 +7954,7 @@ func conditionsVerbs() []catalogue.Verb { {Name: "conditions", Description: "What is wrong with the mesh now: every open condition, urgent first, then oldest, " + "raised from the observers' findings (the watchman, the self-check, the healers, the assigner) and cleared when " + "the finding goes, never by hand. Given a key, that one whole with its evidence; with history, every transition " + - "lately, the controller's before the take-over included.", + "lately.", Input: schema(map[string]string{ "key": "a condition's key: that one whole; with history, only its transitions", "scope": "only this scope: machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module", @@ -7984,17 +7971,6 @@ func conditionsVerbs() []catalogue.Verb { "why": "why, required and recorded", "cause": "the cause in a word (the condition's kind when absent)", }, []string{"key", "for", "why"})}, - {Name: "compare", Description: "The keeper's open conditions against the controller's: every key open in one and not " + - "the other, or open in both with another severity, each with its reason; the kinds that end with the controller " + - "and the keys held while their module is delivered listed apart and not counted. The take-over waits for it empty.", - Input: schema(map[string]string{}, nil)}, - {Name: "take-over", Description: "Ask the operator's Approve to switch who the router, the grants part and " + - "mesh-watcher hear from the controller to the keeper; refused while compare shows a difference. On the warrant " + - "the keeper writes its take-over key.", - Input: schema(map[string]string{"why": "why now, shown to the operator"}, []string{"why"})}, - {Name: "hand-back", Description: "Switch the readers back to the controller's conditions, until the controller's " + - "lease is handed away; refused after it.", - Input: schema(map[string]string{"why": "why, kept with the switch"}, []string{"why"})}, {Name: "resync", Description: "Read every observer's findings again and raise and clear what differs from the open " + "conditions: the recovery of a keeper that missed a change.", Input: schema(map[string]string{}, nil)}, @@ -8016,8 +7992,7 @@ func watchdogsVerbs() []catalogue.Verb { Input: schema(map[string]string{"machine": "one machine"}, nil)}, {Name: "dead-letters", Description: "Every message a consumer on the bus gave up on, kept in DEAD_LETTERS: whose " + "consumer, the subject, how often it was handed over and when it was given up, newest first. With id: that one " + - "whole. With deliver or drop, a hand act with why, taken only once the controller's lease is handed away; " + - "before, refused naming the controller's verb.", + "whole. With deliver or drop, a hand act with why.", Input: schema(map[string]string{ "id": "a dead letter's id, as the list gives it: that one whole, with its message", "consumer": "a consumer's name, or .: only what that one gave up on", @@ -8052,8 +8027,7 @@ func selfCheckVerbs() []catalogue.Verb { func healersVerbs() []catalogue.Verb { return []catalogue.Verb{ {Name: "healers", Description: "The healers: each registered response to one kind of condition, its repair, its " + - "budget and what happens when it is spent; every act taken lately with its outcome, the acts it would have " + - "taken while the controller's lease names a controller, and the mesh-wide brake.", + "budget and what happens when it is spent; every act taken lately with its outcome, and the mesh-wide brake.", Input: schema(map[string]string{"days": "how many days of acts back (default 7)"}, nil)}, {Name: "heal", Description: "Take one healer's act for one open condition now, within its budget and the brake.", Input: schema(map[string]string{ diff --git a/modules/mesh-delivery-indexer/judge/rules_test.go b/modules/mesh-delivery-indexer/judge/rules_test.go index 9a7288b..a85dc00 100644 --- a/modules/mesh-delivery-indexer/judge/rules_test.go +++ b/modules/mesh-delivery-indexer/judge/rules_test.go @@ -4994,12 +4994,12 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) { // The four health seats (novox/hq ADR 0564 clause 1): at mesh scope, each held once, each naming ADR 0564, with the // verbs and events the record names; the events keep the controller's names (clause 3). for _, c := range []struct{ seat, verbs, emits string }{ - {catalogue.ConditionsSeat, "conditions,silence,compare,take-over,hand-back,resync", "condition-raised,condition-changed,condition-cleared"}, + {catalogue.ConditionsSeat, "conditions,silence,resync", "condition-raised,condition-changed,condition-cleared"}, {catalogue.WatchdogsSeat, "signals,stuck,presence,dead-letters,resync", ""}, {catalogue.SelfCheckSeat, "doctor,probes", "checked"}, {catalogue.HealersSeat, "healers,heal,drill", "healer-acted"}, - // And mesh-config's switch (ADR 0565 §4), beside its config. - {"mesh-config", "config,compare,take-over,hand-back", ""}, + // And mesh-config's config; its switch went with the controller (ADR 0590 clause 3). + {"mesh-config", "config", ""}, // And ADR 0565 clause 1's two. {catalogue.WantsSeat, "upgrade,bus", "policy-changed"}, {catalogue.HandActsSeat, "hand-act,hand-acts,warranted", "act-recorded"}, diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go index adc6d5d..258dc66 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go @@ -219,8 +219,7 @@ func Tools(r view.Reader, u view.UsageReader) []stdio.Tool { {Name: view.TrustsReadTool, Description: "What every node trusts a mesh signature from, in one call (novox/hq ADR 0550 §2): each node's record's trusts " + "(from node-engine_have .node), whether it names the vault's signing key as mesh-vault publishes it in its " + - "state signers and whether it still names the controller's, the nodes that do not name the vault's yet (not_yet), " + - "and every: true once all do, the gate before the controller's lease is handed away. Read-only.", + "state signers, the nodes that do not name the vault's yet (not_yet), and every: true once all do. Read-only.", Input: map[string]any{"type": "object", "properties": map[string]any{}}, Run: func(map[string]any) (any, error) { return r.Trusts() diff --git a/modules/mesh-delivery-view/internal/view/read.go b/modules/mesh-delivery-view/internal/view/read.go index ca3956e..fe28ea0 100644 --- a/modules/mesh-delivery-view/internal/view/read.go +++ b/modules/mesh-delivery-view/internal/view/read.go @@ -757,15 +757,14 @@ func (r Reader) Haves(node, module string) (KeptAnswer, error) { } // TrustsReadTool reads what every node trusts a mesh signature from, in one call (novox/hq ADR 0550 §2; design 47's L3 -// s6b): the gate before the lease write is that every node's record names the vault's key. +// s6b): whether every node's record names the vault's key. const TrustsReadTool = "node_trusts" // NodeTrusts is what one node's record says it trusts: its lines (delivery.Trusts) and whether they name the vault's key -// the vault publishes and the controller's key. +// the vault publishes. type NodeTrusts struct { - Trusts []string `json:"trusts"` - TrustsVault bool `json:"trusts_vault"` - TrustsController bool `json:"trusts_controller"` + Trusts []string `json:"trusts"` + TrustsVault bool `json:"trusts_vault"` } // TrustsAnswer is node_trusts' answer: the vault's key as it publishes it, each node's trusts, the nodes whose record does @@ -824,9 +823,6 @@ func (r Reader) Trusts() (TrustsAnswer, error) { if h.Record != nil { t.Trusts = h.Record.Trusts } - for _, line := range t.Trusts { - t.TrustsController = t.TrustsController || strings.HasPrefix(line, delivery.SignerController+" key ") - } t.TrustsVault = out.VaultKey != "" && delivery.TrustsKey(t.Trusts, delivery.SignerVault, out.VaultKey) out.Nodes[node] = t if !t.TrustsVault { diff --git a/modules/mesh-grants/cmd/mesh-grants/integration_test.go b/modules/mesh-grants/cmd/mesh-grants/integration_test.go index d1703b8..fb0836c 100644 --- a/modules/mesh-grants/cmd/mesh-grants/integration_test.go +++ b/modules/mesh-grants/cmd/mesh-grants/integration_test.go @@ -182,7 +182,7 @@ func TestTheGrantsPartWhole(t *testing.T) { return false } g, err := delivery.DecodeGrantsUserList(e.Value()) - return err == nil && g.Follow == delivery.FollowGrants && g.LeaseHanded + return err == nil && g.Follow == delivery.FollowGrants }) if n := said.Load(); n != 0 { t.Errorf("%d memberships were said on their subject", n) diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index bc3d903..7590d55 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -390,7 +390,7 @@ func fixture() *fakeBus { b.put(grants.ReadHave, "a.node", map[string]any{"v": 1}) b.put(grants.ReadHave, "b.node", map[string]any{"v": 1}) b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, - UserList: &delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Wanted: "sha256:aa", Digest: "sha256:aa", Users: 6}}) + UserList: &delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Wanted: "sha256:aa", Digest: "sha256:aa", Users: 5}}) return b } @@ -523,9 +523,9 @@ func TestGrants(t *testing.T) { gap string }{ {"every record on the bus", func(*fakeBus) {}, - []string{"a.mod-x", "a.nats", "b.mod-y", "controller", "node.a", "node.b"}, "people"}, + []string{"a.mod-x", "a.nats", "b.mod-y", "node.a", "node.b"}, "people"}, {"a manifest missing is a gap and its user is left out", func(b *fakeBus) { delete(b.states[grants.ReadModules], "mod-y") }, - []string{"a.mod-x", "a.nats", "controller", "node.a", "node.b"}, "manifest of mod-y"}, + []string{"a.mod-x", "a.nats", "node.a", "node.b"}, "manifest of mod-y"}, {"a want's built manifest is composed, not the trunk's; the list's records carry both (issue 774)", func(b *fakeBus) { built := strings.Replace(xManifest, `{"name":"kept","history":3}`, `{"name":"newer"}`, 1) digest := delivery.ManifestDigest([]byte(built)) @@ -534,9 +534,9 @@ func TestGrants(t *testing.T) { b.put(grants.ReadWants, "want.a.mod-x", delivery.Want{V: delivery.Version, FP: fp, Composed: "sha256:" + strings.Repeat("ef", 32)}) key, _ := delivery.BuilderBuildKey("mod-x", fp) b.put(grants.ReadBuilds, key, map[string]any{"v": delivery.Version, "state": "done", "manifest": digest}) - }, []string{"a.mod-x", "a.nats", "b.mod-y", "controller", "node.a", "node.b"}, "people"}, + }, []string{"a.mod-x", "a.nats", "b.mod-y", "node.a", "node.b"}, "people"}, {"placement refused is a gap; the machines come from the haves", func(b *fakeBus) { b.refused[grants.ReadPlacement] = true }, - []string{"controller", "node.a", "node.b"}, grants.ReadPlacement}, + []string{"node.a", "node.b"}, grants.ReadPlacement}, } { t.Run(row.name, func(t *testing.T) { b := fixture() @@ -1099,7 +1099,7 @@ func TestGrants(t *testing.T) { // 1952/S5 (ADR 0564 Limits 4): the bus module's tools bundle declares its own account and what it may remove. n.manifestOf("nats", strings.Replace(strings.TrimSuffix(natsManifest, "}"), `"broker":"/var/lib/nats/broker"`, `"broker":"/var/lib/nats/broker","broker-nats-tools":"${dir:tools-secret}/broker-nats-tools"`, 1)+ `,"resources":[{"id":"tools-secret","type":"directory","path":"/var/lib/mesh/nats-tools","mode":"0711"}],"build":{"artifacts":[{"name":"tools","kind":"bundle","language":"go",`+ - `"system":"arch","from":"cmd/nats-tools","binary":"nats-tools","loads":["nats-tools"],"bus":{"removes":["DEAD_LETTER_NOTICES.controller","mesh-host_acts"]}}]}}`) + `"system":"arch","from":"cmd/nats-tools","binary":"nats-tools","loads":["nats-tools"],"bus":{"removes":["controller-leftovers","mesh-host_acts"]}}]}}`) n.manifestOf("claude-licence-manager", string(licence)) n.assign("novox", "nats") n.assign("novox", "claude-licence-manager") @@ -1118,7 +1118,7 @@ func TestGrants(t *testing.T) { tools = d.Processes } } - if len(tools) != 1 || tools[0].Process != "nats-tools" || !slices.Equal(tools[0].Removes, []string{"DEAD_LETTER_NOTICES.controller", "mesh-host_acts"}) { + if len(tools) != 1 || tools[0].Process != "nats-tools" || !slices.Equal(tools[0].Removes, []string{"controller-leftovers", "mesh-host_acts"}) { t.Errorf("the bus module's principals = %+v, want nats-tools carrying both removals", tools) } if m := grants.Memberships(recs)["novox.claude-licence-manager"]; len(m.Membership.Seats) == 0 { @@ -1208,7 +1208,7 @@ func TestGrants(t *testing.T) { w.Key = busKey.PublicKey b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, UserList: &w}) } - loadedBefore := delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Wanted: "sha256:aa", Digest: "sha256:aa", Users: 6} + loadedBefore := delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Wanted: "sha256:aa", Digest: "sha256:aa", Users: 5} withKey(loadedBefore) objs := &fakeObjects{} p.Objects = objs @@ -1229,13 +1229,13 @@ func TestGrants(t *testing.T) { t.Errorf("user-list before any composition = %v", got) } first := p.Compose() - if !first.Written || first.Revision != 1 || !first.UserList.Compare.Equal() || first.UserList.Users != 6 { + if !first.Written || first.Revision != 1 || !first.UserList.Compare.Equal() || first.UserList.Users != 5 { t.Fatalf("first composition = %+v", first) } // The bus node follows the part's list alone (issue 1126): a first list that would leave out every loaded user // while the vault holds no hash is held, nothing sealed, the key saying follow grants; a tool's part (no object // bucket) writes none. - if g, _, had := p.Sealed(); !had || g.Follow != delivery.FollowGrants || !g.LeaseHanded || g.Digest != "" || + if g, _, had := p.Sealed(); !had || g.Follow != delivery.FollowGrants || g.Digest != "" || len(objs.objects) != 0 || first.Sealed == nil || !strings.Contains(first.Sealed.Why, "would be left out") { t.Errorf("the first sealed list = %+v, %d objects, %+v", g, len(objs.objects), first.Sealed) } @@ -1322,8 +1322,8 @@ func TestGrants(t *testing.T) { settled(lacks)() p.Compose() g, _, _ := p.Sealed() - if g.Follow != delivery.FollowGrants || !g.LeaseHanded { - t.Errorf("the key says follow %s, lease handed %v; want grants with no lease key (1126/A1/AC2)", g.Follow, g.LeaseHanded) + if g.Follow != delivery.FollowGrants { + t.Errorf("the key says follow %s, want grants (1126/A1/AC2)", g.Follow) } if users := strings.Count(open(g), "{ user: "); users != g.Users || g.Users == 0 || len(g.LeftOut) != 0 { t.Errorf("the sealed list holds %d users, the key says %d, left out %v", users, g.Users, g.LeftOut) @@ -1823,7 +1823,7 @@ func TestGrants(t *testing.T) { if res.Grants == nil || !slices.Equal(held(), []string{key("mod-x"), key("nats")}) || len(res.Grants.Written) != 2 { t.Fatalf("grants of the hashed modules: held %v, %+v", held(), res.Grants) } - if g, err := delivery.DecodeControllerGrant(b.own[grants.StateGrants][key("mod-x")]); err != nil || !slices.Equal(g.Accounts, []string{"a.mod-x"}) { + if g, err := delivery.DecodeGrant(b.own[grants.StateGrants][key("mod-x")]); err != nil || !slices.Equal(g.Accounts, []string{"a.mod-x"}) { t.Errorf("the grant of mod-x is %+v, %v", g, err) } said := func() int { @@ -1871,7 +1871,7 @@ func TestGrants(t *testing.T) { } loads := func(source, digest string) { b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, - UserList: &delivery.UserListWritten{Source: source, Revision: 8, Wanted: digest, Digest: digest, Users: 6, Key: busKey.PublicKey, + UserList: &delivery.UserListWritten{Source: source, Revision: 8, Wanted: digest, Digest: digest, Users: 5, Key: busKey.PublicKey, Follows: delivery.FollowGrants}}) } loads("a.nats.src2", "sha256:aa") @@ -1976,10 +1976,9 @@ func TestGrants(t *testing.T) { } }) - // ADR 0571 §13's test for this repository (issue 774, 774/S24/AC1), replacing the guard of ADR 0549 §6 and its list - // ../assignments-readers: at phase write no module.json here depends on mesh-controller beyond the lease and the three - // kept to the write (mesh-sdk catalogue.ManifestRefusals, the judge grants_lease_check runs). - t.Run("no module of this repository depends on the controller beyond what the lease write keeps", func(t *testing.T) { + // ADR 0590 clause 5's test for this repository (issue 1126): no module.json here names mesh-controller, the lease + // read and the builder's consume of artifacts-gone included (mesh-sdk catalogue.ManifestRefusals). + t.Run("no module of this repository names the controller", func(t *testing.T) { paths, _ := filepath.Glob("../*/module.json") manifests := map[string]catalogue.Manifest{} for _, path := range paths { @@ -1996,7 +1995,7 @@ func TestGrants(t *testing.T) { if len(manifests) < 8 { t.Fatalf("read %d manifests, want every module of the repository", len(manifests)) } - for _, why := range catalogue.ManifestRefusals(manifests, catalogue.PhaseWrite) { + for _, why := range catalogue.ManifestRefusals(manifests) { t.Error(why) } }) @@ -2318,7 +2317,7 @@ func TestGrants(t *testing.T) { t.Fatal(err) } b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, - UserList: &delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Digest: "sha256:aa", Users: 6, Key: busKey.PublicKey}}) + UserList: &delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Digest: "sha256:aa", Users: 5, Key: busKey.PublicKey}}) p := partOn(b) p.Objects = &fakeObjects{} var said []asks.Condition @@ -2400,7 +2399,7 @@ func TestGrants(t *testing.T) { t.Fatal(err) } b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, - UserList: &delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Digest: "sha256:aa", Users: 6, Key: busKey.PublicKey}}) + UserList: &delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Digest: "sha256:aa", Users: 5, Key: busKey.PublicKey}}) p := partOn(b) p.Objects = &fakeObjects{} var digests []string @@ -2434,7 +2433,7 @@ func TestGrants(t *testing.T) { // The loaded list's permissions are read back from the keys when the bus module's tools cannot say them. b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, UserList: &delivery.UserListWritten{Source: delivery.GrantsUserListSource, Follows: delivery.FollowGrants, Revision: 8, - Digest: digests[2], Users: 6, Key: busKey.PublicKey}}) + Digest: digests[2], Users: 5, Key: busKey.PublicKey}}) b.refuseUsers = true if res := p.Compose(); slices.ContainsFunc(res.Grants.Waits, func(w string) bool { return strings.Contains(w, "unknown") }) { t.Errorf("the kept permissions of the loaded list were not read: %v", res.Grants.Waits) @@ -2446,23 +2445,27 @@ func TestGrants(t *testing.T) { // written, and the refusal is raised by name; one the list in force does not carry stays out, said too. t.Run("a refused principal keeps its grants of the list in force; the rest of the list is written", func(t *testing.T) { b := fixture() - const ctl = `{"module":"mesh-controller","version":"1","own-secrets":{"broker":"/var/lib/c/broker"}}` - refusedCtl := strings.Replace(ctl, `}}`, `},"state":[{"name":"declared-config"}]}`, 1) - b.manifestOf("mesh-controller", ctl) - b.assign("b", "mesh-controller") + // A tools bundle's account whose bus removes what the closed set does not name is refused (issue 1126: the + // controller's mirror as a second writer, which this row used, went with the controller). + const ctl = `{"module":"mod-r","version":"1","own-secrets":{"broker":"/var/lib/r/broker","broker-mod-r-tools":"${dir:tools-secret}/broker-mod-r-tools"},` + + `"resources":[{"id":"tools-secret","type":"directory","path":"/var/lib/mesh/mod-r-tools","mode":"0711"}],"build":{"artifacts":[{"name":"tools",` + + `"kind":"bundle","language":"go","system":"arch","from":"cmd/mod-r-tools","binary":"mod-r-tools","loads":["mod-r-tools"],"bus":{}}]}}` + refusedCtl := strings.Replace(ctl, `"bus":{}`, `"bus":{"removes":["EVENTS.x"]}`, 1) + b.manifestOf("mod-r", ctl) + b.assign("b", "mod-r") hash := func(user, h string, gen int) { b.put(grants.ReadAccounts, user, map[string]any{"user": user, "hash": h, "generation": gen, "fingerprint": fmt.Sprintf("sha256:%d", gen)}) } for _, u := range shadowOf(t, b) { hash(u.User, "$2a$10$first", 1) } - hash("a.mesh-controller", "$2a$10$first", 1) + hash("a.mod-r.mod-r-tools", "$2a$10$first", 1) busKey, err := seal.GenerateKeyPair() if err != nil { t.Fatal(err) } b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, - UserList: &delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Digest: "sha256:aa", Users: 6, Key: busKey.PublicKey}}) + UserList: &delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Digest: "sha256:aa", Users: 5, Key: busKey.PublicKey}}) p := partOn(b) objs := &fakeObjects{} p.Objects = objs @@ -2494,39 +2497,39 @@ func TestGrants(t *testing.T) { if inForce.Digest == "" { t.Fatal("no list written after take-over") } - entryBefore := entryOf(inForce, "b.mesh-controller") - ctlKey := grants.SealedUserKey(inForce.Digest, "b.mesh-controller") + entryBefore := entryOf(inForce, "b.mod-r.mod-r-tools") + ctlKey := grants.SealedUserKey(inForce.Digest, "b.mod-r.mod-r-tools") before, _, ok, _ := b.ownGet(grants.StateUserList, ctlKey) if !ok { - t.Fatal("the list in force keeps no grants of b.mesh-controller") + t.Fatal("the list in force keeps no grants of b.mod-r.mod-r-tools") } - // b.mesh-controller now declares the controller's mirror as its own state: refused as a second writer. a.mod-x's hash - // changes in the same pass, and a.mesh-controller, refused the same way, is placed new. - b.manifestOf("mesh-controller", refusedCtl) - b.assign("a", "mesh-controller") + // b.mod-r.mod-r-tools now removes what the set does not name: refused. a.mod-x's hash + // changes in the same pass, and a.mod-r.mod-r-tools, refused the same way, is placed new. + b.manifestOf("mod-r", refusedCtl) + b.assign("a", "mod-r") hash("a.mod-x", "$2a$10$second", 2) res := p.Compose() now, _, _ := p.Sealed() if now.Digest == inForce.Digest || slices.ContainsFunc(res.Errors, func(e string) bool { return strings.Contains(e, "composed whole") }) { t.Fatalf("a refused principal froze the list: sealed %+v, errors %v", res.Sealed, res.Errors) } - after, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(now.Digest, "b.mesh-controller")) + after, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(now.Digest, "b.mod-r.mod-r-tools")) if !ok || string(after) != string(before) { - t.Errorf("b.mesh-controller did not keep its grants of the list in force: %s, was %s", after, before) + t.Errorf("b.mod-r.mod-r-tools did not keep its grants of the list in force: %s, was %s", after, before) } // The kept user is written as the composer writes one: the bus reads the same entry it loaded. - if got := entryOf(now, "b.mesh-controller"); entryBefore == "" || got != entryBefore { + if got := entryOf(now, "b.mod-r.mod-r-tools"); entryBefore == "" || got != entryBefore { t.Errorf("the kept entry is written otherwise than the list in force holds it:\n%s\nwas\n%s", got, entryBefore) } - if entryOf(now, "a.mesh-controller") != "" { + if entryOf(now, "a.mod-r.mod-r-tools") != "" { t.Error("a refused principal the list in force did not carry is in the sealed list") } - if _, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(now.Digest, "a.mesh-controller")); ok { + if _, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(now.Digest, "a.mod-r.mod-r-tools")); ok { t.Error("a refused principal the list in force did not carry is in the list") } if len(res.PrincipalsRefused) != 2 || len(said) == 0 || said[len(said)-1].State != asks.StateRaised || - !strings.Contains(said[len(said)-1].Summary, "b.mesh-controller (keeps its grants of the list in force)") || - !strings.Contains(said[len(said)-1].Summary, "a.mesh-controller (left out of the list") { + !strings.Contains(said[len(said)-1].Summary, "b.mod-r.mod-r-tools (keeps its grants of the list in force)") || + !strings.Contains(said[len(said)-1].Summary, "a.mod-r.mod-r-tools (left out of the list") { t.Fatalf("the refusals were not said by name: %v, said %+v", res.PrincipalsRefused, said) } // Composing again changes nothing and says nothing new; once the manifest composes, the warning is cleared. @@ -2534,7 +2537,7 @@ func TestGrants(t *testing.T) { if p.Compose(); len(said) != n { t.Errorf("an unchanged refusal was said again: %+v", said[n:]) } - b.manifestOf("mesh-controller", ctl) + b.manifestOf("mod-r", ctl) if res := p.Compose(); len(res.PrincipalsRefused) != 0 || said[len(said)-1].State != asks.StateCleared { t.Errorf("the refusal was not cleared once it composes: %v, said %+v", res.PrincipalsRefused, said) } diff --git a/modules/mesh-grants/internal/grants/grants.go b/modules/mesh-grants/internal/grants/grants.go index d5ae433..50eeeaa 100644 --- a/modules/mesh-grants/internal/grants/grants.go +++ b/modules/mesh-grants/internal/grants/grants.go @@ -266,7 +266,7 @@ const GrantMissingCondition = "mesh-grants.grant-missing" // in the vault (drop). **The fingerprint is the indexer's, never a commit's** (issue 1126: link2pay, snake, f1-circuits // and txt-game, whose source commit has not moved since a build of the old form, have a done key of the new form at it, // and that is the fingerprint granted). Pure. -func MissingGrants(in Inputs, want map[string]delivery.ControllerGrant, drop []string, skip map[string]bool) []string { +func MissingGrants(in Inputs, want map[string]delivery.Grant, drop []string, skip map[string]bool) []string { placed := map[string]bool{} for _, ms := range in.Assigned { for _, m := range ms { @@ -347,7 +347,7 @@ func (p *Part) sayMissing(missing []string) { // GrantsOf is the grant of every module at the fingerprint the indexer's trunk names for it, by key, and the keys that // have none (drop): see the comment above. A module in skip (its manifest was refused) is neither. Pure. -func GrantsOf(in Inputs, recs buscompose.Records, skip map[string]bool) (map[string]delivery.ControllerGrant, []string, error) { +func GrantsOf(in Inputs, recs buscompose.Records, skip map[string]bool) (map[string]delivery.Grant, []string, error) { principals, err := buscompose.Users(recs) if err != nil { return nil, nil, err @@ -363,7 +363,7 @@ func GrantsOf(in Inputs, recs buscompose.Records, skip map[string]bool) (map[str } accounts[p.Module] = append(accounts[p.Module], p.Username()) } - out := map[string]delivery.ControllerGrant{} + out := map[string]delivery.Grant{} var drop []string for module, fp := range in.Fingerprints { if skip[module] || !strings.HasPrefix(fp, delivery.SourcePrefix2) { @@ -379,7 +379,7 @@ func GrantsOf(in Inputs, recs buscompose.Records, skip map[string]bool) (map[str continue } sort.Strings(names) - out[key] = delivery.ControllerGrant{Accounts: names} + out[key] = delivery.Grant{Accounts: names} } sort.Strings(drop) return out, drop, nil @@ -426,7 +426,7 @@ func (p *Part) writeGrants(res *Result, in Inputs, recs buscompose.Records, comp } for _, key := range sortedKeys(want) { g := want[key] - data, err := delivery.EncodeControllerGrant(g) + data, err := delivery.EncodeGrant(g) if err != nil { res.Errors = append(res.Errors, fmt.Sprintf("the grant %s cannot be encoded: %v", key, err)) continue @@ -435,7 +435,7 @@ func (p *Part) writeGrants(res *Result, in Inputs, recs buscompose.Records, comp res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be read: %v", StateGrants, key, err)) continue } else if found { - if was, err := delivery.DecodeControllerGrant(raw); err == nil && delivery.SameControllerGrant(was, withVersion(g)) { + if was, err := delivery.DecodeGrant(raw); err == nil && delivery.SameGrant(was, withVersion(g)) { continue } } @@ -463,7 +463,7 @@ func (p *Part) writeGrants(res *Result, in Inputs, recs buscompose.Records, comp } } -func withVersion(g delivery.ControllerGrant) delivery.ControllerGrant { +func withVersion(g delivery.Grant) delivery.Grant { g.V = delivery.Version return g } diff --git a/modules/mesh-grants/internal/grants/records.go b/modules/mesh-grants/internal/grants/records.go index 08be575..183f2da 100644 --- a/modules/mesh-grants/internal/grants/records.go +++ b/modules/mesh-grants/internal/grants/records.go @@ -38,7 +38,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) { } // The enrolment users, from the seat mesh-enrolment's keys, enrolling through its redeem alone (ADR 0587 §4, §6). out := buscompose.Records{Assigned: map[string][]buscompose.Declared{}, People: map[string][]string{}, - Interchangeable: map[string]bool{}, Enrolling: slices.Clone(in.Enrolling), LeaseHandedAway: true, + Interchangeable: map[string]bool{}, Enrolling: slices.Clone(in.Enrolling), // And each node's logins (ADR 0583 §1): both while it rotates. NodeLogins: in.NodeLogins} values := func(provider, setting string) []any { diff --git a/modules/mesh-grants/internal/grants/sealed.go b/modules/mesh-grants/internal/grants/sealed.go index e20c315..17bbf7a 100644 --- a/modules/mesh-grants/internal/grants/sealed.go +++ b/modules/mesh-grants/internal/grants/sealed.go @@ -87,7 +87,7 @@ func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut, dropped next := cur // The bus node follows this list alone (ADR 0571 clause 8): always since the controller ended by its unassignment // (novox/hq ADR 0590, issue 1126). - next.Follow, next.LeaseHanded = delivery.FollowGrants, true + next.Follow = delivery.FollowGrants key := "" if in.Loaded != nil { key = in.Loaded.Key diff --git a/modules/mesh-grants/internal/grants/tools.go b/modules/mesh-grants/internal/grants/tools.go index d6c8d64..64acfac 100644 --- a/modules/mesh-grants/internal/grants/tools.go +++ b/modules/mesh-grants/internal/grants/tools.go @@ -35,14 +35,14 @@ const WritesGrants = "the grants part writes grants.. into its state // Grants answers the tool grants: the grants the part wrote, one module's or all. func (p *Part) Grants(module string) (any, error) { out := map[string]any{"writes": WritesGrants} - read := func(get func() ([]string, func(string) ([]byte, bool, error), error)) (map[string]delivery.ControllerGrant, error) { + read := func(get func() ([]string, func(string) ([]byte, bool, error), error)) (map[string]delivery.Grant, error) { keys, one, err := get() if err != nil { return nil, err } - m := map[string]delivery.ControllerGrant{} + m := map[string]delivery.Grant{} for _, k := range keys { - mod, _, err := delivery.ParseControllerGrantKey(k) + mod, _, err := delivery.ParsePartGrantKey(k) if err != nil || (module != "" && mod != module) { continue } @@ -53,7 +53,7 @@ func (p *Part) Grants(module string) (any, error) { if !found { continue } - g, err := delivery.DecodeControllerGrant(raw) + g, err := delivery.DecodeGrant(raw) if err != nil { return nil, fmt.Errorf("%s: %w", k, err) } @@ -73,10 +73,10 @@ func (p *Part) Grants(module string) (any, error) { Module string `json:"module"` Accounts []string `json:"accounts,omitempty"` } - list := func(m map[string]delivery.ControllerGrant) []grant { + list := func(m map[string]delivery.Grant) []grant { out := []grant{} for _, k := range sortedKeys(m) { - mod, _, _ := delivery.ParseControllerGrantKey(k) + mod, _, _ := delivery.ParsePartGrantKey(k) out = append(out, grant{Key: k, Module: mod, Accounts: m[k].Accounts}) } return out