mesh-grants: no go/delivery, health or provisions import left: the user list and grants are its own grantsstate, the other parts' states are named in its buscompose names.go (checked against its manifest), their values are the seats' types (ADR 0634 §1, §4; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 16:54:28 +01:00
parent e52d9d7791
commit 5c73853653
29 changed files with 557 additions and 383 deletions
@@ -13,8 +13,8 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/seal"
"github.com/nats-io/nats-server/v2/server"
@@ -34,8 +34,8 @@ func theBusNodeFollowsTheSealedListUnderTheComposedGrants(t *testing.T) {
Nodes: []string{"novox", "ace"},
Assigned: map[string][]buscompose.Declared{
"novox": {{Module: buscompose.GrantsModule, State: []buscompose.Bucket{{Module: buscompose.GrantsModule, Name: "user-list"}}}},
"ace": {{Module: "plex", Reads: []string{delivery.ComposedRead}},
{Module: "mesh-delivery-view", Reads: []string{delivery.ComposedRead}, ReadsObjects: []string{"mesh-knowledge_sections"}},
"ace": {{Module: "plex", Reads: []string{buscompose.ComposedRead}},
{Module: "mesh-delivery-view", Reads: []string{buscompose.ComposedRead}, ReadsObjects: []string{"mesh-knowledge_sections"}},
{Module: "mesh-knowledge", Objects: []buscompose.ObjectBucket{{Module: "mesh-knowledge", Name: "sections", MaxMiB: 1, Restricted: true}}}},
},
}
@@ -117,19 +117,19 @@ func theBusNodeFollowsTheSealedListUnderTheComposedGrants(t *testing.T) {
t.Fatal(err)
}
list := []byte(accounts)
digest := delivery.UserListDigest(list)
object, err := delivery.SealUserList(list, node.PublicKey)
digest := grantsstate.UserListDigest(list)
object, err := grantsstate.SealUserList(list, node.PublicKey)
if err != nil {
t.Fatal(err)
}
name := delivery.UserListObjectName(digest)
name := grantsstate.UserListObjectName(digest)
for range 2 { // put twice: a second put of a name purges its old chunks
if _, err := objects.PutBytes(ctx, name, object); err != nil {
t.Fatalf("the grants part put its object: %v", err)
}
}
value, err := delivery.EncodeGrantsUserList(delivery.GrantsUserList{Follow: delivery.FollowGrants, Digest: digest,
Object: name, SealedTo: delivery.SealingKeyID(node.PublicKey), Node: "novox", Revision: 1, Users: len(users)})
value, err := grantsstate.EncodeGrantsUserList(grantsstate.GrantsUserList{Follow: grantsstate.FollowGrants, Digest: digest,
Object: name, SealedTo: grantsstate.SealingKeyID(node.PublicKey), Node: "novox", Revision: 1, Users: len(users)})
if err != nil {
t.Fatal(err)
}
@@ -155,13 +155,13 @@ func theBusNodeFollowsTheSealedListUnderTheComposedGrants(t *testing.T) {
t.Fatalf("the bus node watches the key: %v", err)
}
defer w.Stop()
var got delivery.GrantsUserList
var got grantsstate.GrantsUserList
select {
case e := <-w.Updates():
if e == nil {
t.Fatal("the watch found no value")
}
if got, err = delivery.DecodeGrantsUserList(e.Value()); err != nil {
if got, err = grantsstate.DecodeGrantsUserList(e.Value()); err != nil {
t.Fatal(err)
}
case <-ctx.Done():
@@ -200,7 +200,7 @@ func theBusNodeFollowsTheSealedListUnderTheComposedGrants(t *testing.T) {
if err != nil {
t.Fatalf("the bus node reads the object by its name: %v", err)
}
opened, err := delivery.OpenUserList(body, got.Digest, func(b seal.Box) ([]byte, error) {
opened, err := grantsstate.OpenUserList(body, got.Digest, func(b seal.Box) ([]byte, error) {
s, err := seal.Open(b, node.PrivateKey)
return []byte(s), err
})
@@ -19,7 +19,14 @@ import (
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/membership"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/seal"
@@ -91,11 +98,11 @@ func TestTheGrantsPartWhole(t *testing.T) {
}
}
manifest := func(module, raw string) {
digest := delivery.ManifestDigest([]byte(raw))
put(grants.ReadManifests, digest, delivery.Manifest{V: delivery.Version, Module: module, JSON: raw})
put(grants.ReadModules, module, delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32), Manifest: digest,
digest := meshcatalogclient.ManifestDigest([]byte(raw))
put(grants.ReadManifests, digest, meshcatalog.ManifestsByDigestValue{V: keys.Version, Module: module, Manifest: raw})
put(grants.ReadModules, module, meshcatalog.ModulesByModuleValue{V: keys.Version, Fp2: "src2:" + strings.Repeat("ab", 32), Manifest: digest,
Repository: "novox/x", Commit: "c"})
verdict, _ := delivery.EncodeManifestVerdict(delivery.ManifestVerdict{Module: module, State: delivery.VerdictAccepted, Judge: "j", At: time.Now()})
verdict, _ := meshcatalogclient.EncodeVerdict(meshcatalog.VerdictsByDigestValue{Module: module, State: meshcatalogclient.VerdictAccepted, Judge: "j", At: new(time.Now())})
put(grants.ReadVerdicts, digest, json.RawMessage(verdict))
}
manifest("nats", `{"module":"nats","version":"1","claims":[{"name":"mesh-broker"}],"own-secrets":{"broker":"/var/lib/nats/broker"},`+
@@ -107,16 +114,16 @@ func TestTheGrantsPartWhole(t *testing.T) {
t.Fatal(err)
}
manifest(grants.Module, string(own))
pl, _ := delivery.EncodePlacement(delivery.Placement{Node: "a", Modules: []delivery.PlacedModule{{Module: "nats"}, {Module: "mod-c"},
pl, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{Node: "a", Modules: []meshmoduleassigner.PlacedModule{{Module: "nats"}, {Module: "mod-c"},
{Module: grants.Module}}})
if _, err := kvs[grants.ReadPlacement].Put(ctx, "placement.a", pl); err != nil {
t.Fatal(err)
}
busKey, _ := seal.GenerateKeyPair()
put(grants.ReadHave, "a.node", map[string]any{"v": 1})
put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK,
UserList: &delivery.UserListWritten{Source: delivery.GrantsUserListSource, Revision: 1, Digest: "sha256:aa", Users: 1,
Key: busKey.PublicKey, Follows: delivery.FollowGrants}})
put(grants.ReadHave, "a.nats", nodeengine.HaveByNodeModuleValue{V: keys.Version, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK,
UserList: &nodeengine.UserListWritten{Source: grantsstate.GrantsUserListSource, Revision: 1, Digest: "sha256:aa", Users: 1,
Key: busKey.PublicKey, Follows: grantsstate.FollowGrants}})
// The bus module's read tools, as nats-tools answers them: the streams with their consumers as the bus holds them now.
answer := func(m *nats.Msg, result any) {
@@ -265,8 +272,8 @@ func TestTheGrantsPartWhole(t *testing.T) {
if err != nil {
return false
}
g, err := delivery.DecodeGrantsUserList(e.Value())
return err == nil && g.Follow == delivery.FollowGrants
g, err := grantsstate.DecodeGrantsUserList(e.Value())
return err == nil && g.Follow == grantsstate.FollowGrants
})
if n := said.Load(); n != 0 {
t.Errorf("%d memberships were said on their subject", n)
@@ -7,7 +7,7 @@ import (
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
)
// sealedObjects is the part's object bucket of user lists sealed to the bus node (novox/hq ADR 0547 clause 7), created on
@@ -18,14 +18,14 @@ type sealedObjects struct {
}
func (o sealedObjects) store(ctx context.Context) (jetstream.ObjectStore, error) {
s, err := o.js.ObjectStore(ctx, delivery.GrantsUserListObjects)
s, err := o.js.ObjectStore(ctx, grantsstate.GrantsUserListObjects)
if errors.Is(err, jetstream.ErrBucketNotFound) {
s, err = o.js.CreateObjectStore(ctx, jetstream.ObjectStoreConfig{Bucket: delivery.GrantsUserListObjects,
s, err = o.js.CreateObjectStore(ctx, jetstream.ObjectStoreConfig{Bucket: grantsstate.GrantsUserListObjects,
Description: "the bus's user list composed by the grants part, each sealed to the bus node's key under the hex of its digest (novox/hq ADR 0547 clause 7): written by mesh-grants alone, read by name by the node-engine of the node holding mesh-broker",
Storage: jetstream.FileStorage, MaxBytes: 64 << 20})
}
if err != nil {
return nil, fmt.Errorf("the object bucket %s: %w", delivery.GrantsUserListObjects, err)
return nil, fmt.Errorf("the object bucket %s: %w", grantsstate.GrantsUserListObjects, err)
}
return s, nil
}
+15
View File
@@ -3,6 +3,21 @@ module mesh-grants
go 1.26.0
require (
git.novox.be/novox/mesh-catalog/modules/disk-load/diskreadings v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhealers v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshselfcheck v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/seats/meshbroker v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-00010101000000-000000000000
+138 -126
View File
@@ -22,10 +22,22 @@ import (
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/membership"
"git.novox.be/novox/mesh-sdk/go/seal"
"git.novox.be/novox/mesh-sdk/go/views"
@@ -160,8 +172,8 @@ func (b *fakeBus) put(state, key string, v any) {
b.states[state][key] = raw
// A modules value that carries no verdict is accepted by the indexer's verdict on its digest, as the indexer writes it;
// a row that wants the window before the judge deletes that key.
if m, ok := v.(delivery.Module); ok && state == grants.ReadModules && m.Verdict == "" && m.Manifest != "" {
verdict, _ := delivery.EncodeManifestVerdict(delivery.ManifestVerdict{Module: key, State: delivery.VerdictAccepted, Judge: "j", At: time.Now()})
if m, ok := v.(meshcatalog.ModulesByModuleValue); ok && state == grants.ReadModules && m.Verdict == "" && m.Manifest != "" {
verdict, _ := meshcatalogclient.EncodeVerdict(meshcatalog.VerdictsByDigestValue{Module: key, State: meshcatalogclient.VerdictAccepted, Judge: "j", At: new(time.Now())})
b.put(grants.ReadVerdicts, m.Manifest, json.RawMessage(verdict))
}
}
@@ -315,23 +327,23 @@ func (b *fakeBus) count(key string) int {
// manifestOf is a fixture manifest of one module, in the indexer's two buckets.
func (b *fakeBus) manifestOf(module, raw string) {
digest := delivery.ManifestDigest([]byte(raw))
b.put(grants.ReadManifests, digest, delivery.Manifest{V: delivery.Version, Module: module, JSON: raw})
b.put(grants.ReadModules, module, delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32), Manifest: digest, Repository: "novox/x", Commit: "c"})
digest := meshcatalogclient.ManifestDigest([]byte(raw))
b.put(grants.ReadManifests, digest, meshcatalog.ManifestsByDigestValue{V: keys.Version, Module: module, Manifest: raw})
b.put(grants.ReadModules, module, meshcatalog.ModulesByModuleValue{V: keys.Version, Fp2: "src2:" + strings.Repeat("ab", 32), Manifest: digest, Repository: "novox/x", Commit: "c"})
}
// refuse marks the indexer's refusal of module's manifest in its value (novox/hq ADR 0578 §1).
func (b *fakeBus) refuse(module, raw, problem string) {
b.put(grants.ReadModules, module, delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32),
Manifest: delivery.ManifestDigest([]byte(raw)), Repository: "novox/x", Commit: "c", Verdict: delivery.VerdictRefused,
b.put(grants.ReadModules, module, meshcatalog.ModulesByModuleValue{V: keys.Version, Fp2: "src2:" + strings.Repeat("ab", 32),
Manifest: meshcatalogclient.ManifestDigest([]byte(raw)), Repository: "novox/x", Commit: "c", Verdict: meshcatalogclient.VerdictRefused,
Problems: []string{problem}})
}
// assign places module on node in the seat's placement, the one place the part reads it (ADR 0549 §5; issue 774).
func (b *fakeBus) assign(node, module string) {
var pl delivery.Placement
var pl meshmoduleassigner.Placement
if raw, ok := b.states[grants.ReadPlacement]["placement."+node]; ok {
pl, _ = delivery.DecodePlacement(raw)
pl, _ = meshmoduleassignerclient.DecodePlacement(raw)
}
pl.Node = node
for _, m := range pl.Modules {
@@ -339,8 +351,8 @@ func (b *fakeBus) assign(node, module string) {
return
}
}
pl.Modules = append(pl.Modules, delivery.PlacedModule{Module: module})
raw, err := delivery.EncodePlacement(pl)
pl.Modules = append(pl.Modules, meshmoduleassigner.PlacedModule{Module: module})
raw, err := meshmoduleassignerclient.EncodePlacement(pl)
if err != nil {
panic(err)
}
@@ -352,12 +364,12 @@ func (b *fakeBus) assign(node, module string) {
// unassign takes module off node in the seat's placement.
func (b *fakeBus) unassign(node, module string) {
pl, err := delivery.DecodePlacement(b.states[grants.ReadPlacement]["placement."+node])
pl, err := meshmoduleassignerclient.DecodePlacement(b.states[grants.ReadPlacement]["placement."+node])
if err != nil {
return
}
pl.Modules = slices.DeleteFunc(pl.Modules, func(m delivery.PlacedModule) bool { return m.Module == module })
raw, _ := delivery.EncodePlacement(pl)
pl.Modules = slices.DeleteFunc(pl.Modules, func(m meshmoduleassigner.PlacedModule) bool { return m.Module == module })
raw, _ := meshmoduleassignerclient.EncodePlacement(pl)
b.states[grants.ReadPlacement]["placement."+node] = raw
}
@@ -404,8 +416,8 @@ func fixture() *fakeBus {
b.assign("b", "mod-y")
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: 5}})
b.put(grants.ReadHave, "a.nats", nodeengine.HaveByNodeModuleValue{V: keys.Version, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK,
UserList: &nodeengine.UserListWritten{Source: "a.nats.src2", Revision: 7, Wanted: "sha256:aa", Digest: "sha256:aa", Users: 5}})
return b
}
@@ -519,7 +531,7 @@ func TestGrants(t *testing.T) {
}
// The one object bucket: the part's own, of the user list sealed to the bus node, in sealed.go (ADR 0547 clause 7).
if path == sealedOnly {
if strings.Count(string(src), "CreateObjectStore(") != 1 || !strings.Contains(string(src), "delivery.GrantsUserListObjects") {
if strings.Count(string(src), "CreateObjectStore(") != 1 || !strings.Contains(string(src), "grantsstate.GrantsUserListObjects") {
t.Errorf("sealed.go creates other than the part's one object bucket")
}
src = []byte(strings.ReplaceAll(string(src), "CreateObjectStore", ""))
@@ -557,12 +569,12 @@ func TestGrants(t *testing.T) {
[]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))
b.put(grants.ReadManifests, digest, delivery.Manifest{V: delivery.Version, Module: "mod-x", JSON: built})
digest := meshcatalogclient.ManifestDigest([]byte(built))
b.put(grants.ReadManifests, digest, meshcatalog.ManifestsByDigestValue{V: keys.Version, Module: "mod-x", Manifest: built})
fp := "src2:" + strings.Repeat("cd", 32)
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})
b.put(grants.ReadWants, "want.a.mod-x", meshwantsclient.Want{V: keys.Version, Fp: fp, Composed: "sha256:" + strings.Repeat("ef", 32)})
key, _ := builds.BuilderBuildKey("mod-x", fp)
b.put(grants.ReadBuilds, key, map[string]any{"v": keys.Version, "state": "done", "manifest": digest})
}, []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{"node.a", "node.b"}, grants.ReadPlacement},
@@ -643,7 +655,7 @@ func TestGrants(t *testing.T) {
}
var readers []string
for _, u := range shadowOf(t, cands) {
if slices.Contains(u.Publish, "$JS.API.DIRECT.GET.KV_"+delivery.PlacementCandidatesBucket+".>") {
if slices.Contains(u.Publish, "$JS.API.DIRECT.GET.KV_"+buscompose.PlacementCandidatesBucket+".>") {
readers = append(readers, u.User)
}
}
@@ -708,12 +720,12 @@ func TestGrants(t *testing.T) {
// ADR 0601 clause 3: the owner's trunk manifest now waits for a field; its running build's manifest still declares the
// bucket, so web keeps its object grant.
fp := "src2:" + strings.Repeat("cd", 32)
b.put(grants.ReadModules, "mesh-knowledge", delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32),
Manifest: delivery.ManifestDigest([]byte(`{"module":"mesh-knowledge","version":"2"}`)), Repository: "novox/x", Commit: "c",
Verdict: delivery.VerdictRefused, Problems: []string{"it waits"}, Unknown: []string{"core-credentials"}})
b.put(grants.ReadHave, "a.mesh-knowledge", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, FP: fp})
heldKey, _ := delivery.BuilderBuildKey("mesh-knowledge", fp)
b.put(grants.ReadBuilds, heldKey, map[string]any{"v": delivery.Version, "state": "done", "manifest": delivery.ManifestDigest([]byte(knowledge))})
b.put(grants.ReadModules, "mesh-knowledge", meshcatalog.ModulesByModuleValue{V: keys.Version, Fp2: "src2:" + strings.Repeat("ab", 32),
Manifest: meshcatalogclient.ManifestDigest([]byte(`{"module":"mesh-knowledge","version":"2"}`)), Repository: "novox/x", Commit: "c",
Verdict: meshcatalogclient.VerdictRefused, Problems: []string{"it waits"}, Unknown: []string{"core-credentials"}})
b.put(grants.ReadHave, "a.mesh-knowledge", nodeengine.HaveByNodeModuleValue{V: keys.Version, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK, Fp: fp})
heldKey, _ := builds.BuilderBuildKey("mesh-knowledge", fp)
b.put(grants.ReadBuilds, heldKey, map[string]any{"v": keys.Version, "state": "done", "manifest": meshcatalogclient.ManifestDigest([]byte(knowledge))})
if heldWeb := (func() grants.User {
us := shadowOf(t, b)
return us[slices.IndexFunc(us, func(u grants.User) bool { return u.User == "a.web" })]
@@ -786,16 +798,16 @@ func TestGrants(t *testing.T) {
b.assign("b", "telegram")
if held {
// The trunk's manifest waits for a field; the build that runs was judged on the manifest before.
b.put(grants.ReadModules, "messenger", delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32),
Manifest: delivery.ManifestDigest([]byte(`{"module":"messenger","version":"2"}`)), Repository: "novox/x", Commit: "c",
Verdict: delivery.VerdictRefused, Problems: []string{"it waits"}, Unknown: []string{"core-credentials"}})
b.put(grants.ReadModules, "messenger", meshcatalog.ModulesByModuleValue{V: keys.Version, Fp2: "src2:" + strings.Repeat("ab", 32),
Manifest: meshcatalogclient.ManifestDigest([]byte(`{"module":"messenger","version":"2"}`)), Repository: "novox/x", Commit: "c",
Verdict: meshcatalogclient.VerdictRefused, Problems: []string{"it waits"}, Unknown: []string{"core-credentials"}})
if runs {
fp := "src2:" + strings.Repeat("cd", 32)
digest := delivery.ManifestDigest([]byte(router))
b.put(grants.ReadManifests, digest, delivery.Manifest{V: delivery.Version, Module: "messenger", JSON: router})
b.put(grants.ReadHave, "a.messenger", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, FP: fp})
key, _ := delivery.BuilderBuildKey("messenger", fp)
b.put(grants.ReadBuilds, key, map[string]any{"v": delivery.Version, "state": "done", "manifest": digest})
digest := meshcatalogclient.ManifestDigest([]byte(router))
b.put(grants.ReadManifests, digest, meshcatalog.ManifestsByDigestValue{V: keys.Version, Module: "messenger", Manifest: router})
b.put(grants.ReadHave, "a.messenger", nodeengine.HaveByNodeModuleValue{V: keys.Version, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK, Fp: fp})
key, _ := builds.BuilderBuildKey("messenger", fp)
b.put(grants.ReadBuilds, key, map[string]any{"v": keys.Version, "state": "done", "manifest": digest})
}
}
return b
@@ -972,15 +984,15 @@ func TestGrants(t *testing.T) {
const oldS = `{"module":"mod-s","version":"1","own-secrets":{"broker":"/var/lib/s/broker"},` +
`"provides":[{"name":"smtp","scope":"mesh"}],"serves":{"smtp":{"port":587}}}`
bt.manifestOf("mod-s", oldS)
oldDigest := delivery.ManifestDigest([]byte(oldS))
oldDigest := meshcatalogclient.ManifestDigest([]byte(oldS))
bt.manifestOf("mod-s", `{"module":"mod-s","version":"1","own-secrets":{"broker":"/var/lib/s/broker"},`+
`"config":{"domain":{"type":"string","description":"The mail domain.","required":true}},`+
`"provides":[{"name":"smtp","scope":"mesh"}],"serves":{"smtp":{"port":587,"domain":"${config:domain}"}}}`)
bt.assign("b", "mod-s")
runsFP := "src2:" + strings.Repeat("cd", 32)
bt.put(grants.ReadWants, "want.b.mod-s", delivery.Want{V: delivery.Version, FP: runsFP, Composed: "sha256:" + strings.Repeat("ef", 32)})
runsKey, _ := delivery.BuilderBuildKey("mod-s", runsFP)
bt.put(grants.ReadBuilds, runsKey, map[string]any{"v": delivery.Version, "state": "done", "manifest": oldDigest})
bt.put(grants.ReadWants, "want.b.mod-s", meshwantsclient.Want{V: keys.Version, Fp: runsFP, Composed: "sha256:" + strings.Repeat("ef", 32)})
runsKey, _ := builds.BuilderBuildKey("mod-s", runsFP)
bt.put(grants.ReadBuilds, runsKey, map[string]any{"v": keys.Version, "state": "done", "manifest": oldDigest})
for _, u := range shadowOf(t, bt) {
if u.User == "node.a" && !slices.Contains(u.Publish, served) {
t.Errorf("node a is not granted the domain the trunk's mod-s serves, while b runs one that serves none")
@@ -1202,25 +1214,25 @@ func TestGrants(t *testing.T) {
if recs, _ := grants.Records(in); holds(recs, "a") != 1 || holds(recs, "b") != 1 {
t.Fatalf("before placement the seat is held on a %d and b %d times, want both", holds(recs, "a"), holds(recs, "b"))
}
in.Placed = map[string]delivery.Placement{
"a": {Node: "a", Modules: []delivery.PlacedModule{{Module: "store"}}, Holds: []delivery.Holding{{Claim: "store-seat", Scope: "mesh", Module: "store"}}},
"b": {Node: "b", Modules: []delivery.PlacedModule{{Module: "store"}}}}
in.Placed = map[string]meshmoduleassigner.Placement{
"a": {Node: "a", Modules: []meshmoduleassigner.PlacedModule{{Module: "store"}}, Holds: []meshmoduleassigner.Holding{{Claim: "store-seat", Scope: "mesh", Module: "store"}}},
"b": {Node: "b", Modules: []meshmoduleassigner.PlacedModule{{Module: "store"}}}}
if recs, _ := grants.Records(in); holds(recs, "a") != 1 || holds(recs, "b") != 0 {
t.Errorf("placed, the seat is held on a %d and b %d times, want a alone", holds(recs, "a"), holds(recs, "b"))
}
b := fixture()
pl, _ := delivery.EncodePlacement(delivery.Placement{Node: "b", Modules: []delivery.PlacedModule{{Module: "mod-y"}, {Module: "mod-x"}}})
pl, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{Node: "b", Modules: []meshmoduleassigner.PlacedModule{{Module: "mod-y"}, {Module: "mod-x"}}})
b.states[grants.ReadPlacement] = map[string][]byte{"placement.b": pl}
read := grants.ReadInputs(b, &grants.Manifests{})
if !slices.Contains(read.Assigned["b"], "mod-x") || read.Placed["b"].Node != "b" || slices.ContainsFunc(read.Gaps, func(g grants.Gap) bool { return g.Input == "seat holdings" }) {
t.Errorf("read: assigned %v, placed %v, gaps %v", read.Assigned, read.Placed, read.Gaps)
}
// Research 072 row 37: a node key and a key per module, the legacy mirror left out, read the same.
perModule, _ := delivery.PlacementValues(delivery.Placement{Node: "b", Modules: []delivery.PlacedModule{{Module: "mod-y"}, {Module: "mod-x"}}})
perModule, _ := meshmoduleassignerclient.PlacementValues(meshmoduleassigner.Placement{Node: "b", Modules: []meshmoduleassigner.PlacedModule{{Module: "mod-y"}, {Module: "mod-x"}}})
delete(perModule, "placement.b")
nb := fixture()
nb.states[grants.ReadPlacement] = perModule
if got := grants.ReadInputs(nb, &grants.Manifests{}); !slices.Equal(got.Placed["b"].Assigned(), []string{"mod-y", "mod-x"}) ||
if got := grants.ReadInputs(nb, &grants.Manifests{}); !slices.Equal(meshmoduleassignerclient.Assigned(got.Placed["b"]), []string{"mod-y", "mod-x"}) ||
slices.ContainsFunc(got.Gaps, func(g grants.Gap) bool { return strings.HasPrefix(g.Input, grants.ReadPlacement) }) {
t.Errorf("read of the keys per module: placed %v, gaps %v", got.Placed, got.Gaps)
}
@@ -1235,12 +1247,12 @@ func TestGrants(t *testing.T) {
in.Manifests["other"] = catalogue.Manifest{Module: "other", Version: "1", Claims: []catalogue.Claim{{Name: "other-seat", Scope: catalogue.ScopeMesh}},
DefinesSeats: []catalogue.SeatDeclaration{{Name: "other-seat", Scope: catalogue.ScopeMesh, Serves: []catalogue.Verb{{Name: "v"}}}}}
in.Assigned["b"] = append(in.Assigned["b"], "other")
in.Placed["b"] = delivery.Placement{Node: "b", Modules: []delivery.PlacedModule{{Module: "store"}, {Module: "other"}}}
in.Placed["b"] = meshmoduleassigner.Placement{Node: "b", Modules: []meshmoduleassigner.PlacedModule{{Module: "store"}, {Module: "other"}}}
if recs, _ := grants.Records(in); holds(recs, "a") != 1 || holds(recs, "b") != 1 {
t.Errorf("a seat no placement holds is held on b %d times, want once (and store-seat on a alone: a %d)", holds(recs, "b"), holds(recs, "a"))
}
in.Placed["b"] = delivery.Placement{Node: "b", Modules: []delivery.PlacedModule{{Module: "store"}, {Module: "other"}},
Holds: []delivery.Holding{{Claim: "other-seat", Scope: "mesh", Module: "someone-else"}}}
in.Placed["b"] = meshmoduleassigner.Placement{Node: "b", Modules: []meshmoduleassigner.PlacedModule{{Module: "store"}, {Module: "other"}},
Holds: []meshmoduleassigner.Holding{{Claim: "other-seat", Scope: "mesh", Module: "someone-else"}}}
if recs, _ := grants.Records(in); holds(recs, "b") != 0 {
t.Errorf("a seat placement holds by another module is still held by other on b")
}
@@ -1256,7 +1268,7 @@ func TestGrants(t *testing.T) {
t.Fatal(err)
}
}
var seed delivery.Placement
var seed meshmoduleassigner.Placement
var live struct{ Users []grants.User }
file("placement-novox-0c61bad.json", &seed)
file("node-tools-novox-licence-subjects.json", &live)
@@ -1279,7 +1291,7 @@ func TestGrants(t *testing.T) {
// alone (ADR 0636 §1), does not bring the refusal back, and the runner is granted none of them.
n.manifestOf(buscompose.EngineModule, `{"module":"mesh-host","version":"1","consumes":["restic.backed-up"],"state":[{"name":"health"},{"name":"findings"},{"name":"data"},{"name":"packages"}]}`)
n.put(grants.ReadHave, "novox.node", map[string]any{"v": 1})
pl, _ = delivery.EncodePlacement(seed)
pl, _ = meshmoduleassignerclient.EncodePlacement(seed)
n.states[grants.ReadPlacement] = map[string][]byte{"placement.novox": pl}
recs, _ := grants.Records(grants.ReadInputs(n, &grants.Manifests{}))
var tools []buscompose.ProcessPrincipal
@@ -1375,17 +1387,17 @@ func TestGrants(t *testing.T) {
if err != nil {
t.Fatal(err)
}
withKey := func(w delivery.UserListWritten) {
withKey := func(w nodeengine.UserListWritten) {
w.Key = busKey.PublicKey
b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, UserList: &w})
b.put(grants.ReadHave, "a.nats", nodeengine.HaveByNodeModuleValue{V: keys.Version, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK, UserList: &w})
}
loadedBefore := delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Wanted: "sha256:aa", Digest: "sha256:aa", Users: 5}
loadedBefore := nodeengine.UserListWritten{Source: "a.nats.src2", Revision: 7, Wanted: "sha256:aa", Digest: "sha256:aa", Users: 5}
withKey(loadedBefore)
objs := &fakeObjects{}
p.Objects = objs
open := func(g delivery.GrantsUserList) string {
open := func(g grantsstate.GrantsUserList) string {
t.Helper()
list, err := delivery.OpenUserList(objs.objects[g.Object], g.Digest, func(bx seal.Box) ([]byte, error) {
list, err := grantsstate.OpenUserList(objs.objects[g.Object], g.Digest, func(bx seal.Box) ([]byte, error) {
out, err := seal.Open(bx, busKey.PrivateKey)
return []byte(out), err
})
@@ -1406,7 +1418,7 @@ func TestGrants(t *testing.T) {
// 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.Digest != "" ||
if g, _, had := p.Sealed(); !had || g.Follow != grantsstate.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)
}
@@ -1493,21 +1505,21 @@ func TestGrants(t *testing.T) {
settled(lacks)()
p.Compose()
g, _, _ := p.Sealed()
if g.Follow != delivery.FollowGrants {
if g.Follow != grantsstate.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)
}
withKey(delivery.UserListWritten{Source: delivery.GrantsUserListSource, Revision: g.Revision, Wanted: g.Digest, Digest: g.Digest, Users: g.Users, Follows: delivery.FollowGrants})
withKey(nodeengine.UserListWritten{Source: grantsstate.GrantsUserListSource, Revision: int64(g.Revision), Wanted: g.Digest, Digest: g.Digest, Users: int64(g.Users), Follows: grantsstate.FollowGrants})
// Steady (issue 1126, 2026-10-28 00:16: the shadow list moved every 3 s, 99270 to 99357, with nothing
// differing): the bus node saying again that it loaded the same list, at a later time and the key's
// later revision, moves neither the shadow list nor the sealed key.
_, sealedRev, _ := p.Sealed()
p.Compose()
for i := 0; i < 3; i++ {
withKey(delivery.UserListWritten{Source: delivery.GrantsUserListSource, Revision: sealedRev + uint64(i+1), Wanted: g.Digest,
Digest: g.Digest, Users: g.Users, Follows: delivery.FollowGrants, At: time.Date(2026, 10, 28, 0, 16, i, 0, time.UTC)})
withKey(nodeengine.UserListWritten{Source: grantsstate.GrantsUserListSource, Revision: int64(sealedRev) + int64(i+1), Wanted: g.Digest,
Digest: g.Digest, Users: int64(g.Users), Follows: grantsstate.FollowGrants, At: time.Date(2026, 10, 28, 0, 16, i, 0, time.UTC)})
if res := p.Compose(); res.Written {
t.Errorf("pass %d: the shadow list is written again for the bus node saying the same list again (revision %d)", i, res.Revision)
}
@@ -1862,13 +1874,13 @@ func TestGrants(t *testing.T) {
t.Fatalf("the records carry the node-engine as %+v", recs.Engine)
}
for _, node := range recs.Nodes {
bucket, _ := delivery.ComposedBucketFor(node)
bucket, _ := buscompose.ComposedBucketFor(node)
if !slices.Contains(b.creates, "KV_"+bucket) {
t.Errorf("the compositions of %s were not created: %v", node, b.creates)
}
engine := catalogue.Manifest{Module: buscompose.EngineModule}
if cfg := grants.Configs(engine, recs.Nodes)["KV_"+bucket]; cfg.MaxMsgsPerSubject != delivery.ComposedHistory {
t.Errorf("the compositions of %s keep %d revisions, want %d", node, cfg.MaxMsgsPerSubject, delivery.ComposedHistory)
if cfg := grants.Configs(engine, recs.Nodes)["KV_"+bucket]; cfg.MaxMsgsPerSubject != buscompose.ComposedHistory {
t.Errorf("the compositions of %s keep %d revisions, want %d", node, cfg.MaxMsgsPerSubject, buscompose.ComposedHistory)
}
}
for node, mods := range recs.Assigned {
@@ -1998,7 +2010,7 @@ func TestGrants(t *testing.T) {
b.loaded = shadowOf(t, b)
p := partOn(b)
fp := "src2:" + strings.Repeat("ab", 32)
key := func(module string) string { k, _ := delivery.PartGrantKey(module, fp); return k }
key := func(module string) string { k, _ := grantsstate.PartGrantKey(module, fp); return k }
hash := func(user string) {
b.put(grants.ReadAccounts, user, map[string]any{"user": user, "hash": "$2a$10$x", "generation": 1})
}
@@ -2009,7 +2021,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.DecodeGrant(b.own[grants.StateGrants][key("mod-x")]); err != nil || !slices.Equal(g.Accounts, []string{"a.mod-x"}) {
if g, err := grantsstate.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 {
@@ -2056,17 +2068,17 @@ func TestGrants(t *testing.T) {
t.Fatal(err)
}
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: 5, Key: busKey.PublicKey,
Follows: delivery.FollowGrants}})
b.put(grants.ReadHave, "a.nats", nodeengine.HaveByNodeModuleValue{V: keys.Version, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK,
UserList: &nodeengine.UserListWritten{Source: source, Revision: 8, Wanted: digest, Digest: digest, Users: 5, Key: busKey.PublicKey,
Follows: grantsstate.FollowGrants}})
}
loads("a.nats.src2", "sha256:aa")
p.Objects = &fakeObjects{}
moves := func(module, manifest, fp string) string {
digest := delivery.ManifestDigest([]byte(manifest))
b.put(grants.ReadManifests, digest, delivery.Manifest{V: delivery.Version, Module: module, JSON: manifest})
b.put(grants.ReadModules, module, delivery.Module{V: delivery.Version, FP2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"})
k, _ := delivery.PartGrantKey(module, fp)
digest := meshcatalogclient.ManifestDigest([]byte(manifest))
b.put(grants.ReadManifests, digest, meshcatalog.ManifestsByDigestValue{V: keys.Version, Module: module, Manifest: manifest})
b.put(grants.ReadModules, module, meshcatalog.ModulesByModuleValue{V: keys.Version, Fp2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"})
k, _ := grantsstate.PartGrantKey(module, fp)
return k
}
written := func(k string) bool { _, ok := b.own[grants.StateGrants][k]; return ok }
@@ -2084,7 +2096,7 @@ func TestGrants(t *testing.T) {
if sealed.Digest == "" {
t.Fatal("no list sealed once every user has a hash")
}
loads(delivery.GrantsUserListSource, sealed.Digest)
loads(grantsstate.GrantsUserListSource, sealed.Digest)
b.refuseUsers = true
// (2) mod-y's trunk adds a state, so its account adds permissions: it waits, judged from the part's own record of the
// loaded list; mod-x at another fingerprint with the same permissions is written.
@@ -2097,7 +2109,7 @@ func TestGrants(t *testing.T) {
t.Fatalf("an added permission against the loaded list: %+v", res.Grants)
}
// A loaded list the part keeps no permissions of: every grant naming an account waits, said.
loads(delivery.GrantsUserListSource, "sha256:bb")
loads(grantsstate.GrantsUserListSource, "sha256:bb")
keyX4 := moves("mod-x", xManifest, "src2:"+strings.Repeat("12", 32))
if res := p.Compose(); written(keyX4) || len(res.Grants.Waits) != 2 || !strings.Contains(res.Grants.Waits[0], "unknown") {
t.Fatalf("an unknown loaded list let a grant through: %+v", res.Grants)
@@ -2133,15 +2145,15 @@ func TestGrants(t *testing.T) {
// The private network the mesh provides is placed with no fingerprint by construction: never a missing grant.
b.assign("b", "mesh-wireguard")
fp := "src2:d4afe7a1abab5ccdbef6d8629b2245006a2aec60290f430f7b4756e2930ed60c"
b.put(grants.ReadModules, "mod-x", delivery.Module{V: delivery.Version, FP2: fp, Manifest: delivery.ManifestDigest([]byte(xManifest)),
b.put(grants.ReadModules, "mod-x", meshcatalog.ModulesByModuleValue{V: keys.Version, Fp2: fp, Manifest: meshcatalogclient.ManifestDigest([]byte(xManifest)),
Repository: "novox/Link2Pay", Commit: "5d20e8bfbc953c51e8ebe3c671fe883df9c36e15"})
b.put(grants.ReadModules, "mod-y", delivery.Module{V: delivery.Version, None2: "its builder's version is not read yet",
Manifest: delivery.ManifestDigest([]byte(yManifest)), Repository: "novox/y", Commit: "c"})
b.put(grants.ReadModules, "mod-y", meshcatalog.ModulesByModuleValue{V: keys.Version, None2: "its builder's version is not read yet",
Manifest: meshcatalogclient.ManifestDigest([]byte(yManifest)), Repository: "novox/y", Commit: "c"})
for _, u := range []string{"a.mod-x", "a.nats", "b.mod-y"} {
b.put(grants.ReadAccounts, u, map[string]any{"user": u, "hash": "$2a$10$x", "generation": 1})
}
res := p.Compose()
key, _ := delivery.PartGrantKey("mod-x", fp)
key, _ := grantsstate.PartGrantKey("mod-x", fp)
if _, held := b.own[grants.StateGrants][key]; !held {
t.Errorf("mod-x has no grant at the indexer's fingerprint %s: %+v", fp, res.Grants)
}
@@ -2155,7 +2167,7 @@ func TestGrants(t *testing.T) {
if p.Compose(); len(said) != 1 {
t.Errorf("the same missing grant was said again: %+v", said)
}
b.put(grants.ReadModules, "mod-y", delivery.Module{V: delivery.Version, FP2: fp, Manifest: delivery.ManifestDigest([]byte(yManifest)),
b.put(grants.ReadModules, "mod-y", meshcatalog.ModulesByModuleValue{V: keys.Version, Fp2: fp, Manifest: meshcatalogclient.ManifestDigest([]byte(yManifest)),
Repository: "novox/y", Commit: "c"})
if res := p.Compose(); len(res.Grants.Missing) != 0 || len(said) != 2 || said[1].State != occ.StateCleared {
t.Errorf("once mod-y has a grant: missing %v, said %+v", res.Grants.Missing, said)
@@ -2517,35 +2529,35 @@ func TestGrants(t *testing.T) {
t.Fatalf("%s: %v", event, err)
}
}
verdictOf := func() delivery.ManifestVerdict {
verdictOf := func() meshcatalog.VerdictsByDigestValue {
raw, _, _ := fv.Get(grants.ReadVerdicts, digest)
v, _ := delivery.DecodeManifestVerdict(raw)
v, _ := meshcatalogclient.DecodeVerdict(raw)
return v
}
asked := verdicts.Status().Asks
heard(delivery.ManifestAcceptedEvent, delivery.ManifestAccepted{V: 1, Module: "mod-z", Digest: digest, Judge: "j", Revision: 8})
if v := verdictOf(); v.State != delivery.VerdictAccepted || v.Module != "mod-z" || verdicts.Status().Revision != 8 {
heard(meshcatalog.EventManifestAccepted, meshcatalog.ManifestAcceptedEvent{V: 1, Module: "mod-z", Digest: digest, Judge: "j", Revision: 8})
if v := verdictOf(); v.State != meshcatalogclient.VerdictAccepted || v.Module != "mod-z" || verdicts.Status().Revision != 8 {
t.Errorf("manifest-accepted at the next revision: %+v at %d", v, verdicts.Status().Revision)
}
heard(delivery.ManifestRefusedEvent, delivery.ManifestRefused{V: 1, Module: "mod-z", Digest: digest, Judge: "j", Problems: []string{"p"}, Revision: 9})
heard(delivery.ManifestAcceptedEvent, delivery.ManifestAccepted{V: 1, Module: "mod-z", Digest: digest, Judge: "j", Revision: 9})
if v := verdictOf(); !v.Refused() || !slices.Equal(v.Problems, []string{"p"}) || verdicts.Status().Asks != asked {
heard(meshcatalog.EventManifestRefused, meshcatalog.ManifestRefusedEvent{V: 1, Module: "mod-z", Digest: digest, Judge: "j", Problems: []string{"p"}, Revision: 9})
heard(meshcatalog.EventManifestAccepted, meshcatalog.ManifestAcceptedEvent{V: 1, Module: "mod-z", Digest: digest, Judge: "j", Revision: 9})
if v := verdictOf(); !meshcatalogclient.VerdictRefuses(v) || !slices.Equal(v.Problems, []string{"p"}) || verdicts.Status().Asks != asked {
t.Errorf("manifest-refused, then a revision already held: %+v, asks %d (was %d)", v, verdicts.Status().Asks, asked)
}
// A revision below the picture's (ADR 0615 §5; the verdicts made again restart low): the same verdict again is a
// redelivery, passed over; another verdict asks the first view once more.
heard(delivery.ManifestRefusedEvent, delivery.ManifestRefused{V: 1, Module: "mod-z", Digest: digest, Judge: "j", Problems: []string{"p"}, Revision: 3})
heard(meshcatalog.EventManifestRefused, meshcatalog.ManifestRefusedEvent{V: 1, Module: "mod-z", Digest: digest, Judge: "j", Problems: []string{"p"}, Revision: 3})
if verdicts.Status().Asks != asked {
t.Errorf("a held verdict redelivered at a lower revision asked: asks %d (was %d)", verdicts.Status().Asks, asked)
}
heard(delivery.ManifestAcceptedEvent, delivery.ManifestAccepted{V: 1, Module: "mod-z", Digest: digest, Judge: "j", Revision: 2})
heard(meshcatalog.EventManifestAccepted, meshcatalog.ManifestAcceptedEvent{V: 1, Module: "mod-z", Digest: digest, Judge: "j", Revision: 2})
waitFor(t, func() bool { return verdicts.Status().Asks > asked })
waitFor(t, func() bool { s := verdicts.Status(); return !s.Answered.Before(s.Asked) })
asked = verdicts.Status().Asks
heard(delivery.ManifestAcceptedEvent, delivery.ManifestAccepted{V: 1, Module: "mod-z", Digest: digest, Judge: "j"})
heard(meshcatalog.EventManifestAccepted, meshcatalog.ManifestAcceptedEvent{V: 1, Module: "mod-z", Digest: digest, Judge: "j"})
waitFor(t, func() bool { return verdicts.Status().Asks > asked })
asked = verdicts.Status().Asks
heard(delivery.ManifestAcceptedEvent, delivery.ManifestAccepted{V: 1, Module: "mod-z", Digest: digest, Judge: "j", Revision: 12})
heard(meshcatalog.EventManifestAccepted, meshcatalog.ManifestAcceptedEvent{V: 1, Module: "mod-z", Digest: digest, Judge: "j", Revision: 12})
waitFor(t, func() bool { return verdicts.Status().Asks > asked })
if err := fv.HearVerdict(ctx, "manifest-judged", []byte("{}"), t.Logf); err == nil {
t.Error("an event that is no verdict was taken")
@@ -2664,14 +2676,14 @@ func TestGrants(t *testing.T) {
b.manifestOf("mesh-enrolment", enrolmentManifest)
b.assign("a", "mesh-enrolment")
issued := time.Date(2026, 11, 2, 1, 0, 0, 0, time.UTC)
for node, stage := range map[string]string{"lab": delivery.EnrolmentLive, "pi": delivery.EnrolmentAsked} {
raw, err := delivery.EncodeEnrolment(delivery.Enrolment{Node: node, Address: "10.0.0.7", Why: "joins",
Caller: "a.mesh-enrolment", Stage: stage, Issued: issued, Expires: issued.Add(time.Hour),
for node, stage := range map[string]string{"lab": buscompose.EnrolmentLive, "pi": "asked"} {
raw, err := json.Marshal(meshnodeenroller.EnrolmentsByNodeNodeValue{Node: node, Address: "10.0.0.7", Why: "joins",
Caller: "a.mesh-enrolment", Stage: stage, Issued: &issued, Expires: new(issued.Add(time.Hour)),
OverlayKey: base64.StdEncoding.EncodeToString(make([]byte, 32))})
if err != nil {
t.Fatal(err)
}
b.put(grants.ReadEnrolments, delivery.EnrolmentNodeKey(node), json.RawMessage(raw))
b.put(grants.ReadEnrolments, meshnodeenroller.EnrolmentsByNodeNodeKey(node), json.RawMessage(raw))
}
b.put(grants.ReadAccounts, "enrol.lab", map[string]any{"user": "enrol.lab", "hash": "$2a$10$x", "generation": 1})
in := grants.ReadInputs(b, &grants.Manifests{})
@@ -2696,7 +2708,7 @@ func TestGrants(t *testing.T) {
t.Errorf("enrol.lab's hash is not carried: %+v", c)
}
b.mu.Lock()
delete(b.states[grants.ReadEnrolments], delivery.EnrolmentNodeKey("lab"))
delete(b.states[grants.ReadEnrolments], meshnodeenroller.EnrolmentsByNodeNodeKey("lab"))
b.mu.Unlock()
if slices.Contains(names(shadowOf(t, b)), "enrol.lab") {
t.Error("enrol.lab stays after its key ended")
@@ -2713,14 +2725,14 @@ func TestGrants(t *testing.T) {
b.manifestOf("mesh-vault", vaultManifest)
b.assign("a", "mesh-vault")
since := time.Date(2026, 11, 2, 1, 0, 0, 0, time.UTC)
login := func(l delivery.NodeLogin) {
raw, err := delivery.EncodeNodeLogin(l)
login := func(l meshvault.NodeLoginsByNodeValue) {
raw, err := meshvaultclient.EncodeNodeLogin(l)
if err != nil {
t.Fatal(err)
}
b.put(grants.ReadNodeLogins, l.Node, json.RawMessage(raw))
}
login(delivery.NodeLogin{Node: "b", Login: "node.b", Rotating: "node.b--b", Stage: delivery.RotationBegun, Since: since})
login(meshvault.NodeLoginsByNodeValue{Node: "b", Login: "node.b", Rotating: "node.b--b", Stage: meshvaultclient.RotationBegun, Since: since})
b.put(grants.ReadAccounts, "node.b--b", map[string]any{"user": "node.b--b", "hash": "$2a$10$x", "generation": 1})
users := shadowOf(t, b)
in := grants.ReadInputs(b, &grants.Manifests{})
@@ -2734,20 +2746,20 @@ func TestGrants(t *testing.T) {
t.Error("the second login is not judged its node's, or not rotating")
}
get := func(key string) string {
return "$JS.API.DIRECT.GET.KV_" + delivery.NodeLoginsBucket + ".$KV." + delivery.NodeLoginsBucket + "." + key
return "$JS.API.DIRECT.GET.KV_" + buscompose.NodeLoginsBucket + ".$KV." + buscompose.NodeLoginsBucket + "." + key
}
for _, u := range users {
if !strings.HasPrefix(u.User, "node.") {
continue
}
node, _ := delivery.LoginNode(u.User)
node, _ := meshvaultclient.LoginNode(u.User)
for _, of := range []string{"a", "b"} {
if slices.Contains(u.Publish, get(of)) != (of == node) {
t.Errorf("%s reads %s's node-logins key: %v", u.User, of, of != node)
}
}
}
login(delivery.NodeLogin{Node: "b", Login: "node.b--b", Since: since.Add(time.Hour)})
login(meshvault.NodeLoginsByNodeValue{Node: "b", Login: "node.b--b", Since: since.Add(time.Hour)})
if got := names(shadowOf(t, b)); slices.Contains(got, "node.b") || !slices.Contains(got, "node.b--b") {
t.Errorf("after rotation-done the node users are %v", got)
}
@@ -2892,8 +2904,8 @@ func TestGrants(t *testing.T) {
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: 5, Key: busKey.PublicKey}})
b.put(grants.ReadHave, "a.nats", nodeengine.HaveByNodeModuleValue{V: keys.Version, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK,
UserList: &nodeengine.UserListWritten{Source: "a.nats.src2", Revision: 7, Digest: "sha256:aa", Users: 5, Key: busKey.PublicKey}})
p := partOn(b)
p.Objects = &fakeObjects{}
var said []oc.Condition
@@ -2974,8 +2986,8 @@ func TestGrants(t *testing.T) {
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: 5, Key: busKey.PublicKey}})
b.put(grants.ReadHave, "a.nats", nodeengine.HaveByNodeModuleValue{V: keys.Version, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK,
UserList: &nodeengine.UserListWritten{Source: "a.nats.src2", Revision: 7, Digest: "sha256:aa", Users: 5, Key: busKey.PublicKey}})
p := partOn(b)
p.Objects = &fakeObjects{}
var digests []string
@@ -3007,8 +3019,8 @@ 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,
b.put(grants.ReadHave, "a.nats", nodeengine.HaveByNodeModuleValue{V: keys.Version, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK,
UserList: &nodeengine.UserListWritten{Source: grantsstate.GrantsUserListSource, Follows: grantsstate.FollowGrants, Revision: 8,
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") }) {
@@ -3040,14 +3052,14 @@ func TestGrants(t *testing.T) {
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: 5, Key: busKey.PublicKey}})
b.put(grants.ReadHave, "a.nats", nodeengine.HaveByNodeModuleValue{V: keys.Version, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK,
UserList: &nodeengine.UserListWritten{Source: "a.nats.src2", Revision: 7, Digest: "sha256:aa", Users: 5, Key: busKey.PublicKey}})
p := partOn(b)
objs := &fakeObjects{}
p.Objects = objs
entryOf := func(g delivery.GrantsUserList, user string) string {
entryOf := func(g grantsstate.GrantsUserList, user string) string {
t.Helper()
list, err := delivery.OpenUserList(objs.objects[g.Object], g.Digest, func(bx seal.Box) ([]byte, error) {
list, err := grantsstate.OpenUserList(objs.objects[g.Object], g.Digest, func(bx seal.Box) ([]byte, error) {
out, err := seal.Open(bx, busKey.PrivateKey)
return []byte(out), err
})
@@ -3137,13 +3149,13 @@ func TestGrants(t *testing.T) {
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: 5, Key: busKey.PublicKey}})
b.put(grants.ReadHave, "a.nats", nodeengine.HaveByNodeModuleValue{V: keys.Version, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK,
UserList: &nodeengine.UserListWritten{Source: "a.nats.src2", Revision: 7, Digest: "sha256:aa", Users: 5, Key: busKey.PublicKey}})
// mod-r runs a build whose manifest is the accepted one: the held module's seats and streams are composed from it.
runFP := "src2:" + strings.Repeat("cd", 32)
b.put(grants.ReadHave, "b.mod-r", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, FP: runFP})
runKey, _ := delivery.BuilderBuildKey("mod-r", runFP)
b.put(grants.ReadBuilds, runKey, map[string]any{"v": delivery.Version, "state": "done", "manifest": delivery.ManifestDigest([]byte(ctl))})
b.put(grants.ReadHave, "b.mod-r", nodeengine.HaveByNodeModuleValue{V: keys.Version, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK, Fp: runFP})
runKey, _ := builds.BuilderBuildKey("mod-r", runFP)
b.put(grants.ReadBuilds, runKey, map[string]any{"v": keys.Version, "state": "done", "manifest": meshcatalogclient.ManifestDigest([]byte(ctl))})
p := partOn(b)
p.Objects = &fakeObjects{}
var said []oc.Condition
@@ -3167,11 +3179,11 @@ func TestGrants(t *testing.T) {
if held.unknown != "" {
unknown = []string{held.unknown}
}
b.put(grants.ReadModules, "mod-r", delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32),
Manifest: delivery.ManifestDigest([]byte(ctl)), Repository: "novox/x", Commit: "c", Verdict: delivery.VerdictRefused,
b.put(grants.ReadModules, "mod-r", meshcatalog.ModulesByModuleValue{V: keys.Version, Fp2: "src2:" + strings.Repeat("ab", 32),
Manifest: meshcatalogclient.ManifestDigest([]byte(ctl)), Repository: "novox/x", Commit: "c", Verdict: meshcatalogclient.VerdictRefused,
Problems: []string{"it is held"}, Unknown: unknown})
b.put(grants.ReadModules, "mod-q", delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32),
Manifest: delivery.ManifestDigest([]byte(ctl)), Repository: "novox/x", Commit: "c"})
b.put(grants.ReadModules, "mod-q", meshcatalog.ModulesByModuleValue{V: keys.Version, Fp2: "src2:" + strings.Repeat("ab", 32),
Manifest: meshcatalogclient.ManifestDigest([]byte(ctl)), Repository: "novox/x", Commit: "c"})
res := p.Compose()
now, _, _ := p.Sealed()
after, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(now.Digest, "b.mod-r.mod-r-tools"))
@@ -3188,8 +3200,8 @@ func TestGrants(t *testing.T) {
// A held module that runs no build the part can read declares nothing to anyone: only its own users stay as the list in
// force holds them, said unreadable; the others move (ADR 0614 §8).
delete(b.states[grants.ReadHave], "b.mod-r")
b.put(grants.ReadModules, "mod-r", delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32),
Manifest: delivery.ManifestDigest([]byte(ctl)), Repository: "novox/x", Commit: "c", Verdict: delivery.VerdictRefused,
b.put(grants.ReadModules, "mod-r", meshcatalog.ModulesByModuleValue{V: keys.Version, Fp2: "src2:" + strings.Repeat("ab", 32),
Manifest: meshcatalogclient.ManifestDigest([]byte(ctl)), Repository: "novox/x", Commit: "c", Verdict: meshcatalogclient.VerdictRefused,
Problems: []string{"it is held"}, Unknown: []string{"core-credentials"}})
b.put(grants.ReadAccounts, "a.mod-x", map[string]any{"user": "a.mod-x", "hash": "$2a$10$gen9", "generation": 9, "fingerprint": "sha256:9"})
wasList, _, _ := p.Sealed()
@@ -3207,7 +3219,7 @@ func TestGrants(t *testing.T) {
// The window before the judge: the indexer wrote a changed manifest's value and no verdict on its digest yet. Only that
// module's users stay (named unreadable); the others move.
b.manifestOf("mod-r", ctl)
delete(b.states[grants.ReadVerdicts], delivery.ManifestDigest([]byte(ctl)))
delete(b.states[grants.ReadVerdicts], meshcatalogclient.ManifestDigest([]byte(ctl)))
b.put(grants.ReadAccounts, "a.mod-x", map[string]any{"user": "a.mod-x", "hash": "$2a$10$gen11", "generation": 11, "fingerprint": "sha256:11"})
wasList, _, _ = p.Sealed()
res = p.Compose()
@@ -1,6 +1,6 @@
package buscompose
import "git.novox.be/novox/mesh-sdk/go/delivery"
import "git.novox.be/novox/mesh-delivery/sdk/buildstep"
// The view alone reads the build seats' queues (novox/hq ADR 0525 §2, issue 1887 A8a): the shared workers' counts and the
// oldest waiting ask, to show the backlog. It may not publish an ask: only the builder and the checker ask, by their seat use.
@@ -18,8 +18,8 @@ const (
// Reads only: no consumer, no pull, no acknowledgement; the builder puts its asks by its seat use, as before.
func BuildAskReads() []string {
var out []string
for _, lang := range delivery.AskLanguages {
if stream, err := delivery.AskStream(lang); err == nil {
for _, lang := range buildstep.AskLanguages {
if stream, err := buildstep.AskStream(lang); err == nil {
out = append(out, "$JS.API.STREAM.INFO."+stream, "$JS.API.STREAM.MSG.GET."+stream)
}
}
@@ -27,18 +27,18 @@ func BuildAskReads() []string {
}
// BuildBacklogReads are the API subjects the view requests: the information of each build seat's stream and of each of its
// shared workers (delivery.AskStream, delivery.AskWorker), and the message of the stream by sequence. Reads only: no
// shared workers (buildstep.AskStream, buildstep.AskWorker), and the message of the stream by sequence. Reads only: no
// message is pulled, none acknowledged, none published.
func BuildBacklogReads() []string {
var out []string
for _, lang := range delivery.AskLanguages {
stream, err := delivery.AskStream(lang)
for _, lang := range buildstep.AskLanguages {
stream, err := buildstep.AskStream(lang)
if err != nil {
continue
}
out = append(out, "$JS.API.STREAM.INFO."+stream, "$JS.API.STREAM.MSG.GET."+stream)
for _, kind := range delivery.AskKinds {
if w, err := delivery.AskWorker(lang, kind); err == nil {
for _, kind := range buildstep.AskKinds {
if w, err := buildstep.AskWorker(lang, kind); err == nil {
out = append(out, "$JS.API.CONSUMER.INFO."+stream+"."+w)
}
}
@@ -4,6 +4,7 @@ import (
"encoding/json"
"git.novox.be/novox/mesh-contracts/go/seats/meshbroker"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"maps"
"os"
"path/filepath"
@@ -13,9 +14,12 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-sdk/go/announce"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/nats-io/nats.go/jetstream"
)
@@ -56,8 +60,8 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) {
// ADR 0567 §3, §9: a node reads every key of the seat's placement and its candidates (it composes with the whole
// proposal laid over), and none of its open proposals; it writes neither.
reads := strings.Join(NodeComposeReads("laptop"), " ")
for bucket, granted := range map[string]bool{delivery.PlacementBucket: true, delivery.PlacementCandidatesBucket: true,
delivery.PlacementProposalsBucket: false} {
for bucket, granted := range map[string]bool{meshmoduleassignerclient.PlacementBucket: true, PlacementCandidatesBucket: true,
PlacementProposalsBucket: false} {
if strings.Contains(reads, "$JS.API.DIRECT.GET.KV_"+bucket+".>") != granted || strings.Contains(reads, "$KV."+bucket+".laptop") {
t.Errorf("a node is granted %s: %v, want %v", bucket, !granted, granted)
}
@@ -281,8 +285,8 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) {
since := time.Date(2026, 11, 2, 1, 0, 0, 0, time.UTC)
users, err := Users(Records{Nodes: []string{"ace", "g14"},
Assigned: map[string][]Declared{"ace": {{Module: "mesh-vault", Holds: []Seat{vault}}}},
NodeLogins: map[string]delivery.NodeLogin{
"g14": {Node: "g14", Login: "node.g14", Rotating: "node.g14--b", Stage: delivery.RotationBegun, Since: since},
NodeLogins: map[string]meshvault.NodeLoginsByNodeValue{
"g14": {Node: "g14", Login: "node.g14", Rotating: "node.g14--b", Stage: meshvaultclient.RotationBegun, Since: since},
// A node that rotated before and rotates no more logs in as its second login alone.
"ace": {Node: "ace", Login: "node.ace--b", Since: since}}})
if err != nil {
@@ -317,19 +321,19 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) {
t.Errorf("the two logins' permissions differ:\n%v\n%v", a, b)
}
for login, p := range perms {
node, _ := delivery.LoginNode(login)
node, _ := meshvaultclient.LoginNode(login)
if !slices.Contains(p.Subscribe, "_INBOX.node."+node+".>") || slices.Contains(p.Subscribe, "_INBOX."+login+".>") && login != "node."+node {
t.Errorf("%s's inbox: %v", login, p.Subscribe)
}
}
// AC3: g14 reads node-logins key g14 and not ace, and writes neither; it confirms in its own login's name.
get := func(key string) string {
return "$JS.API.DIRECT.GET.KV_" + delivery.NodeLoginsBucket + ".$KV." + delivery.NodeLoginsBucket + "." + key
return "$JS.API.DIRECT.GET.KV_" + NodeLoginsBucket + ".$KV." + NodeLoginsBucket + "." + key
}
confirm := func(login string) string {
return "mesh.seat.mesh-vault.call.node-account-confirm." + CallerToken(login)
}
for subject, want := range map[string]bool{get("g14"): true, get("ace"): false, "$KV." + delivery.NodeLoginsBucket + ".g14": false,
for subject, want := range map[string]bool{get("g14"): true, get("ace"): false, "$KV." + NodeLoginsBucket + ".g14": false,
confirm("node.g14--b"): true, confirm("node.g14"): false} {
if MayPublish(perms["node.g14--b"], subject) != want {
t.Errorf("node.g14--b may publish %s: %v, want %v", subject, !want, want)
@@ -339,7 +343,7 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) {
t.Error("a node reads node-logins while the vault does not rotate")
}
// And nothing but the grants part names node-logins among its reads.
if !ReaderMay(GrantsModule, delivery.NodeLoginsRead) || ReaderMay("shop", delivery.NodeLoginsRead) {
if !ReaderMay(GrantsModule, NodeLoginsRead) || ReaderMay("shop", NodeLoginsRead) {
t.Error("node-logins' named readers")
}
})
@@ -408,14 +412,14 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) {
}
}
if name == "ace.mesh-enrolment" && (!MayPublish(perms, call("sign", name)) ||
!MayPublish(perms, "$KV."+delivery.EnrolmentsBucket+".node.lab") || !MayPublish(perms, "$KV."+delivery.EnrolmentsBucket+".address.10.0.0.7")) {
!MayPublish(perms, "$KV."+EnrolmentsBucket+".node.lab") || !MayPublish(perms, "$KV."+EnrolmentsBucket+".address.10.0.0.7")) {
t.Errorf("mesh-enrolment does not sign or write its bucket: %v", perms.Publish)
}
if name != "ace.mesh-enrolment" && MayPublish(perms, "$KV."+delivery.EnrolmentsBucket+".node.lab") {
if name != "ace.mesh-enrolment" && MayPublish(perms, "$KV."+EnrolmentsBucket+".node.lab") {
t.Errorf("%s writes the enrolments", name)
}
}
if !ReaderMay(GrantsModule, delivery.EnrolmentsRead) || ReaderMay("shop", delivery.EnrolmentsRead) {
if !ReaderMay(GrantsModule, EnrolmentsRead) || ReaderMay("shop", EnrolmentsRead) {
t.Error("the enrolments' named readers")
}
})
@@ -624,7 +628,7 @@ func healthChecks(t *testing.T) {
}{
{"mesh-conditions", "mesh-healers.attempts", true}, {"mesh-conditions", HealersWould, false}, {"mesh-delivery-view", HealersWould, false},
// ADR 0577 clause 1: the node-engine's data items are disk-load's alone to read.
{"disk-load", delivery.EngineDataRead, true}, {"restic", delivery.EngineDataRead, false},
{"disk-load", EngineDataRead, true}, {"restic", EngineDataRead, false},
} {
granted, _ := ReadsAnnouncedBy(c.reader, []string{c.read}, announced)
if (len(granted) == 1) != c.may {
@@ -639,7 +643,7 @@ func healthChecks(t *testing.T) {
// 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: WatchmanModule, State: []Bucket{bucket(WatchmanModule, "findings")}, Reads: []string{NodeHealthRead}},
{Module: "mesh-hand-acts", State: []Bucket{bucket("mesh-hand-acts", "warrants")}, Logs: []Log{{Module: "mesh-hand-acts", Name: "acts"}}},
// ADR 0570 clauses 1, 8 (issue 1954 L6-P5): mesh-provisioner keeps provisions and plans; the vault and a provider
// read them.
@@ -651,17 +655,17 @@ func healthChecks(t *testing.T) {
{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)}},
Reads: []string{meshmoduleassignerclient.SeatHoldersRead}},
{Module: AssignerModule, State: []Bucket{bucket(AssignerModule, meshmoduleassigner.StateSeatHolders)}},
// ADR 0571 clause 3: disk-load keeps the readings; the backup holder reads them.
// ADR 0577 clause 1: and it reads its own node's keys of the node-engine's data items, which restic may not.
{Module: "disk-load", State: []Bucket{bucket("disk-load", "readings")}, Reads: []string{delivery.EngineDataRead}},
{Module: "restic", Reads: []string{"disk-load.readings", delivery.EngineDataRead}, State: []Bucket{bucket("restic", "good")}},
{Module: "disk-load", State: []Bucket{bucket("disk-load", "readings")}, Reads: []string{EngineDataRead}},
{Module: "restic", Reads: []string{"disk-load.readings", EngineDataRead}, State: []Bucket{bucket("restic", "good")}},
// ADR 0571 clauses 1, 9: the indexer reads the builder's log through its own consumer; the catalogue module asks
// the builder's tool.
{Module: delivery.BuilderModule, Logs: []Log{{Module: delivery.BuilderModule, Name: delivery.BuilderLog, MaxAgeDays: 30}}},
{Module: delivery.IndexerModule, Reads: []string{delivery.BuildLogRead}},
{Module: "mesh-catalog", Invokes: []string{delivery.BuilderModule + "." + delivery.BuildsSinceTool}},
{Module: builds.BuilderModule, Logs: []Log{{Module: builds.BuilderModule, Name: builds.BuilderLog, MaxAgeDays: 30}}},
{Module: IndexerModule, Reads: []string{builds.BuildLogRead}},
{Module: "mesh-catalog", Invokes: []string{builds.BuilderModule + "." + builds.BuildsSinceTool}},
{Module: "telegram", Holds: []Seat{{Name: "telegram-sender", Accepts: []string{"send"}}},
Processes: []ProcessPrincipal{{Process: "w", Queues: []QueueOwner{{Process: "w", Stream: "M_WORK"}, {Process: "w", Stream: "KV_x"}}}}},
{Module: "nats", SnapshotsTheBus: true, Processes: []ProcessPrincipal{{Process: "nats-tools",
@@ -785,9 +789,9 @@ func healthChecks(t *testing.T) {
"a.shop.nats-tools": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-conditions_conditions.x", "$JS.API.STREAM.DELETE.ASSIGNMENTS"},
}
// ADR 0571 clause 1: a read naming another module's log is issued to the reader as a log it reads, never writes.
if m := MembershipFor("a", Declared{Module: delivery.IndexerModule, Reads: []string{delivery.BuildLogRead, "x.y"}},
Placements{Logs: map[string]bool{delivery.BuildLogRead: true}}); len(m.Logs) != 1 || m.Logs[0].Writes ||
m.Logs[0].Stream != "LOG_mesh-delivery-builder_build-log" || m.Logs[0].Name != delivery.BuildLogRead {
if m := MembershipFor("a", Declared{Module: IndexerModule, Reads: []string{builds.BuildLogRead, "x.y"}},
Placements{Logs: map[string]bool{builds.BuildLogRead: true}}); len(m.Logs) != 1 || m.Logs[0].Writes ||
m.Logs[0].Stream != "LOG_mesh-delivery-builder_build-log" || m.Logs[0].Name != builds.BuildLogRead {
t.Errorf("the indexer's membership issues the logs %+v", m.Logs)
}
// 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
@@ -957,13 +961,13 @@ func healthChecks(t *testing.T) {
}
}
// ADR 0593 §6 (D14): a named reader of delivery.ComposedRead reads every node's composed-<node>, read-only, with the
// ADR 0593 §6 (D14): a named reader of ComposedRead reads every node's composed-<node>, read-only, with the
// listing and the creation advisory that find them, and its membership issues the family by its prefix; a module not
// named is refused it; each node's engine writes its own node's bucket alone.
func everyNodesCompositions(t *testing.T) {
records := Records{Nodes: []string{"one", "two"}, Assigned: map[string][]Declared{
"one": {{Module: "mesh-delivery-view", Reads: []string{delivery.ComposedRead}},
{Module: "plex", Reads: []string{delivery.ComposedRead}}},
"one": {{Module: "mesh-delivery-view", Reads: []string{ComposedRead}},
{Module: "plex", Reads: []string{ComposedRead}}},
}}
users, err := Users(records)
if err != nil {
@@ -1004,7 +1008,7 @@ func everyNodesCompositions(t *testing.T) {
}
// A bundle reading them (the provisioner, carried by the machine's runtime) reads them on the runtime's connection.
carried := Principal{Kind: KindNodeTools, Node: "one", Module: "node-tools", Nodes: []string{"one", "two"},
Carries: []Declared{{Module: "mesh-provisioner", Reads: []string{delivery.ComposedRead}}}}
Carries: []Declared{{Module: "mesh-provisioner", Reads: []string{ComposedRead}}}}
runner, err := PermissionsFor(carried)
if err != nil {
t.Fatal(err)
@@ -1013,9 +1017,9 @@ func everyNodesCompositions(t *testing.T) {
!MayPublish(runner, "$JS.API.DIRECT.GET.KV_composed-two.$KV.composed-two.two.node") {
t.Errorf("the runtime carrying the provisioner cannot read every node's compositions")
}
m := MembershipFor("one", Declared{Module: "mesh-delivery-view", Reads: []string{delivery.ComposedRead}}, Placements{})
m := MembershipFor("one", Declared{Module: "mesh-delivery-view", Reads: []string{ComposedRead}}, Placements{})
if !slices.ContainsFunc(m.State, func(s StateIssued) bool {
return s.Name == delivery.ComposedRead && s.PerNode && s.Bucket == delivery.ComposedBucketPrefix && !s.Writes
return s.Name == ComposedRead && s.PerNode && s.Bucket == ComposedBucketPrefix && !s.Writes
}) {
t.Errorf("the view's membership issues %+v", m.State)
}
@@ -2,14 +2,12 @@ package buscompose
import (
"fmt"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// The compositions' buckets (novox/hq ADR 0539 clauses 2 and 4, as ADR 0593 §6 changes them): one per node,
// `composed-<node>`, which that node's engine alone writes, unsigned, with every composition it makes, and which the
// grants part creates (EngineComposedBuckets). The provisioner, the assigner and the view read every node's
// (delivery.ComposedRead, a named-reader state); no engine applies a value read from it.
// (ComposedRead, a named-reader state); no engine applies a value read from it.
// ComposedReads is what a node's node-engine publishes to keep and follow its compositions and wants (ADR 0593 §6, ADR
// 0539 clause 9): the writes of its own composition bucket's keys (`<node>.<module>.<fp>` and `<node>.node`, all under
@@ -17,7 +15,7 @@ import (
// assigner's bucket, and the indexer's candidates by one key's direct get (ADR 0593 §7: `compose-candidate` reads the
// check's candidate itself; no listing and no watch); nothing of another node's, never the bucket's creation.
func ComposedReads(node string) ([]string, error) {
bucket, err := delivery.ComposedBucketFor(node)
bucket, err := ComposedBucketFor(node)
if err != nil {
return nil, err
}
@@ -25,31 +23,31 @@ func ComposedReads(node string) ([]string, error) {
return nil, fmt.Errorf("%q cannot be part of a subject: a permission is a subject pattern, and this would widen it", node)
}
out := append([]string{"$KV." + bucket + ".>"}, readGrants(bucket, "")...)
out = append(out, readGrants(delivery.AssignerWantsBucket, "want."+node+".>")...)
out = append(out, readGrants(AssignerWantsBucket, "want."+node+".>")...)
return append(out, CandidateReads...), nil
}
// CandidateReads is what a node's engine is granted of the indexer's candidates and manifest verdicts to answer
// `compose-candidate` (ADR 0593 §7, ADR 0578 §3): each bucket read whole as the builder's builds are (a key is read
// through a short consumer), never written.
var CandidateReads = append(readGrants(delivery.CandidatesBucket, ""), readGrants(delivery.VerdictsBucket, "")...)
var CandidateReads = append(readGrants(CandidatesBucket, ""), readGrants(VerdictsBucket, "")...)
// EngineComposedBuckets are the composition buckets of every node, as the node-engine's state (ADR 0593 §6): the grants
// part creates each, create-only, with the history ADR 0539 clause 4 keeps (delivery.ComposedHistory), as it creates a
// part creates each, create-only, with the history ADR 0539 clause 4 keeps (ComposedHistory), as it creates a
// module's declared state. A node whose name a bucket cannot take is left out (its engine is refused its writes too).
func EngineComposedBuckets(nodes []string) []Bucket {
var out []Bucket
for _, node := range sortedCopy(nodes) {
bucket, err := delivery.ComposedBucketFor(node)
bucket, err := ComposedBucketFor(node)
if err != nil || !safeSubject.MatchString(node) {
continue
}
out = append(out, Bucket{Module: EngineModule, Name: delivery.ComposedState, Whole: bucket, History: delivery.ComposedHistory})
out = append(out, Bucket{Module: EngineModule, Name: ComposedState, Whole: bucket, History: ComposedHistory})
}
return out
}
// composedFamily is every node's composition bucket, for a reader of delivery.ComposedRead.
// composedFamily is every node's composition bucket, for a reader of ComposedRead.
func composedFamily(nodes []string) []string {
var out []string
for _, b := range EngineComposedBuckets(nodes) {
@@ -3,8 +3,7 @@ package buscompose
import (
nodeengineseat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
)
// The node seat node-engine (novox/hq ADR 0563 §1): every node's engine holds it on its own bus account, and no module
@@ -26,11 +25,7 @@ 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)
// EngineDataBucket is the engine's state of its node's placed modules' data items (novox/hq ADR 0577 clause 1): one
// key `<node>.<module>` per module, written by that node's engine alone.
var EngineDataBucket = delivery.EngineDataBucket
var EnginePackagesBucket = BucketName(EngineModule, EnginePackagesState)
// EngineFindingsBucket is the engine's state of the findings it raises about its own node (novox/hq ADR 0564 §2, ADR 0636 §1):
// keys `machine.<node>.<kind>`, written by that node's engine alone and cleared by it; the condition keeper raises each as a
@@ -75,13 +70,13 @@ func engineGrants(p Principal) (pub, sub []string) {
// And the provisions its node's modules consume, its own node's keys alone, read (novox/hq ADR 0570 clause 6: the
// consuming engine checks the vault's name and generation of what it is handed).
if safeSubject.MatchString(p.Node) {
pub = append(pub, readGrants(provisions.ProvisionsBucket, p.Node+".>")...)
pub = append(pub, readGrants(meshprovisionerclient.ProvisionsBucket, p.Node+".>")...)
}
// And its node's own health report, its key `<node>` alone (issue 774, the operator's alert table): what the watchman
// reads for the machine's alerts.
if safeSubject.MatchString(p.Node) {
pub = append(pub, append([]string{"$KV." + delivery.NodeHealthBucket + "." + p.Node},
readGrants(delivery.NodeHealthBucket, p.Node)...)...)
pub = append(pub, append([]string{"$KV." + NodeHealthBucket + "." + p.Node},
readGrants(NodeHealthBucket, p.Node)...)...)
}
// And its node's placed modules' data items, its own node's keys alone (ADR 0577 clause 1).
if own, err := NodeKeysGrants(EngineDataBucket, p.Node); err == nil {
@@ -6,8 +6,9 @@ import (
"sort"
"strings"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhealers"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshselfcheck"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/health"
)
// **The health holders' grants** (novox/hq ADR 0564 clauses 2, 5 and 8, Limits 2, 4 and 8; issue 1952 S5; ADR 0590).
@@ -22,7 +23,7 @@ import (
// controller's leftovers (ADR 0590 clause 6), never the backup's account `broker`, which only reads (ADR 0235).
// HealersWould is what mesh-healers would have done while a controller was placed: read by nobody (ADR 0564 clause 8).
var HealersWould = health.HealersModule + "." + health.WouldState
var HealersWould = meshhealers.Seat + "." + meshhealers.StateWould
// ControllerLeftoverConsumers are the ended controller's durable consumers left on the bus, one leftover of the module
// mesh-delivery placed nowhere (issue 1126, milestone section 3), and mesh-catalog's of the controller's built-before,
@@ -160,7 +161,7 @@ func WatchmanHears() []string {
"mesh.mod.*.event.provisioner.failing",
"mesh.mod.*.event.provisioner.recovered",
"mesh.control.*.tools-alive",
"mesh.seat.mesh-self-check.event." + health.EventChecked,
"mesh.seat." + meshselfcheck.Seat + ".event." + meshselfcheck.EventChecked,
"$JS.EVENT.ADVISORY.CONSUMER.DELETED.>",
"mesh.mod.nats.event.bus.slow-consumer",
"mesh.mod.nats.event.bus.connection-closed",
@@ -0,0 +1,75 @@
package buscompose
import (
"fmt"
"regexp"
)
// **The states and holders of other parts that the grants part names** (novox/hq ADR 0634 §1: no contract names a holder; ADR 0635
// §4): each is the holder's own manifest's, as its `reads` and states say it, and the part reads or grants them by these names.
// TestTheNamesAreTheManifests checks those its own manifest lists in `reads`.
const (
AssignerModule = "mesh-delivery-assigner"
IndexerModule = "mesh-delivery-indexer"
VaultModule = "mesh-vault"
// EnrolmentModule is the module holding the seat mesh-node-enroller.
EnrolmentModule = "mesh-enrolment"
AssignerWantsBucket = AssignerModule + "_wants"
PlacementCandidatesBucket = AssignerModule + "_candidates"
PlacementProposalsBucket = AssignerModule + "_placement-proposals"
CandidatesBucket = IndexerModule + "_candidates"
ModulesBucket = IndexerModule + "_modules"
ManifestsBucket = IndexerModule + "_manifests"
VerdictsBucket = IndexerModule + "_verdicts"
EnrolmentsState = "enrolments"
EnrolmentsBucket = EnrolmentModule + "_" + EnrolmentsState
EnrolmentsRead = EnrolmentModule + "." + EnrolmentsState
// EnrolmentLive and EnrolmentRedeeming are the stages of an enrolment the grants part reads (the seat's Stage).
EnrolmentLive = "live"
EnrolmentRedeeming = "redeeming"
NodeLoginsState = "node-logins"
NodeLoginsBucket = VaultModule + "_" + NodeLoginsState
NodeLoginsRead = VaultModule + "." + NodeLoginsState
SignersBucket = VaultModule + "_signers"
// RotateNodeVerb and NodeAccountConfirmVerb are the seat mesh-vault's verbs the grants part grants.
RotateNodeVerb = "rotate-node"
NodeAccountConfirmVerb = "node-account-confirm"
// The node-engine's states (its module is EngineModule).
EngineDataState = "data"
EngineDataBucket = EngineModule + "_" + EngineDataState
EngineDataRead = EngineModule + "." + EngineDataState
// EnginePackagesState is the engine's state of its node's declared packages.
EnginePackagesState = "packages"
NodeHealthBucket = EngineModule + "_health"
NodeHealthRead = EngineModule + ".health"
HaveBucket = "node-engine_have"
// HaveNodeWord is the word a node's own key in `have` carries after its name.
HaveNodeWord = "node"
// ComposedState is the engine's per-node state of compositions, kept one bucket per node, `composed-<node>`.
ComposedState = "composed"
ComposedRead = EngineModule + "." + ComposedState
ComposedBucketPrefix = "composed-"
ComposedHistory = 5
)
// ComposedReaders are the modules the engine's compositions are read by (the engine's `composed` state is named-reader).
var ComposedReaders = []string{AssignerModule, "mesh-delivery-view", "mesh-provisioner"}
var composedNode = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
// ComposedBucketFor is the name of a node's bucket of compositions: `composed-<node>`, the node's name one a bucket and a
// subject token accept (letters, digits, '_' and '-').
func ComposedBucketFor(node string) (string, error) {
if node == "" {
return "", fmt.Errorf("buscompose: the node name is empty; a node's composition bucket is named for its node")
}
if !composedNode.MatchString(node) {
return "", fmt.Errorf("buscompose: the node name %q is not one a bucket accepts: letters, digits, '_' and '-' only", node)
}
return ComposedBucketPrefix + node, nil
}
@@ -0,0 +1,50 @@
package buscompose
import (
"encoding/json"
"os"
"slices"
"strings"
"testing"
)
// TestTheNamesAreTheManifests: the holders and states of other parts that the grants part names are the ones its manifest
// lists in `reads` and `invokes` (no contract names a holder: novox/hq ADR 0634 §1), and a state's bucket is its holder's
// name and the state's.
func TestTheNamesAreTheManifests(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
var m struct {
Module string `json:"module"`
Reads []string `json:"reads"`
Invokes []string `json:"invokes"`
}
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
if m.Module != GrantsModule {
t.Errorf("the manifest's module is %q, the part names itself %q", m.Module, GrantsModule)
}
for _, c := range []struct{ list, name, bucket string }{
{"reads", HaveBucket[:strings.Index(HaveBucket, "_")] + ".have", HaveBucket},
{"reads", NodeLoginsRead, NodeLoginsBucket},
{"reads", EnrolmentsRead, EnrolmentsBucket},
{"invokes", AssignerModule + ".wants", AssignerWantsBucket},
{"invokes", IndexerModule + ".modules", ModulesBucket},
{"invokes", IndexerModule + ".manifests", ManifestsBucket},
{"invokes", IndexerModule + ".verdicts", VerdictsBucket},
} {
list := m.Reads
if c.list == "invokes" {
list = m.Invokes
}
if !slices.Contains(list, c.name) {
t.Errorf("the part names %s, its manifest's %s are %v", c.name, c.list, list)
}
if got := strings.Replace(c.name, ".", "_", 1); got != c.bucket {
t.Errorf("%s is kept in %s, the part names the bucket %s", c.name, got, c.bucket)
}
}
}
@@ -25,7 +25,6 @@ import (
mv "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// A Kind is what a principal is, which decides the shape of its authority rather than its
@@ -131,7 +130,7 @@ type Principal struct {
// Objects are the bus names of its module's own object buckets, and ReadsObjects those of others it reads (novox/hq
// ADR 0592 §5, §9).
Objects, ReadsObjects []string
// Nodes are every node the records know, for a read of a state kept one bucket per node (delivery.ComposedRead, ADR
// Nodes are every node the records know, for a read of a state kept one bucket per node (ComposedRead, ADR
// 0593 §6): set only where the principal or a module it carries reads one.
Nodes []string
// FilesFor are the nodes whose declaration-file buckets (novox/hq ADR 0486 §2) this principal may remove, for the bus
@@ -395,8 +394,8 @@ var VaultSign = SeatVerb{Seat: "mesh-vault", Verb: mv.VerbSign}
// principal reads its own node's key of node-logins (novox/hq ADR 0583 §1, §2). VaultNodeAccountConfirm is the verb a
// node-engine confirms a switch with, in its new login's name (§3).
var (
VaultRotateNode = SeatVerb{Seat: "mesh-vault", Verb: delivery.RotateNodeVerb}
VaultNodeAccountConfirm = SeatVerb{Seat: "mesh-vault", Verb: delivery.NodeAccountConfirmVerb}
VaultRotateNode = SeatVerb{Seat: "mesh-vault", Verb: RotateNodeVerb}
VaultNodeAccountConfirm = SeatVerb{Seat: "mesh-vault", Verb: NodeAccountConfirmVerb}
)
// EnrolmentRedeem is the seat mesh-enrolment's verb a joining node-engine calls as enrol.<node> (novox/hq ADR 0587 §4,
@@ -415,7 +414,7 @@ func NodeLoginsReads(node string) []string {
if !safeSubject.MatchString(node) {
return nil
}
return readGrants(delivery.NodeLoginsBucket, node)
return readGrants(NodeLoginsBucket, node)
}
// perMachineEvents are a node-scoped seat's events about the holder itself, whose last token is the
@@ -730,7 +729,7 @@ func PermissionsFor(p Principal) (Permissions, error) {
pub = append(pub, WitnessSubjects(p)...)
// And the keys of its own node in the have bucket, which it alone writes (novox/hq ADR 0429 §1, issue 1136):
// what the node has of each module, read by the view and the watchman. Never the controller's.
haveKeys, err := NodeKeysGrants(delivery.HaveBucket, p.Node)
haveKeys, err := NodeKeysGrants(HaveBucket, p.Node)
if err != nil {
return Permissions{}, err
}
@@ -738,7 +737,7 @@ func PermissionsFor(p Principal) (Permissions, error) {
// And every node's own facts, `<node>.node`, its record among them (issue 1126, ADR 0555 §3): a node agent composing
// its own modules reads the other nodes' public records, which its firewall and resolver need (ADR 0547 §2). Read
// only: each node still writes its own keys alone.
pub = append(pub, readGrants(delivery.HaveBucket, "*."+delivery.HaveNodeWord)...)
pub = append(pub, readGrants(HaveBucket, "*."+HaveNodeWord)...)
// And what it composes its own modules from (ADR 0547 §2, ADR 0567 §9): the seat mesh-placement's placement and
// candidates, the indexer's modules and manifests, the builder's builds (the manifest of a build its want names), and mesh-config's resolved values of
// its own node alone. Read only.
@@ -797,7 +796,7 @@ func PermissionsFor(p Principal) (Permissions, error) {
// halves, read and watched (ADR 0550 §2). Read only.
if p.AsksTheVaultToSign {
pub = append(pub, VaultSignCall(p))
pub = append(pub, readGrants(delivery.SignersBucket, "")...)
pub = append(pub, readGrants(SignersBucket, "")...)
}
// And, while the vault rotates node accounts, its own node's key of node-logins, read and watched, and no other
// key (novox/hq ADR 0583 §1 insight): the login it dials as; and the confirmation of a switch in its own name (§3).
@@ -1103,7 +1102,7 @@ func PermissionsFor(p Principal) (Permissions, error) {
pub = append(pub, objectGrants(objectNames(d.Objects), d.ReadsObjects)...)
// A bundle reads every node's compositions on the runtime's own connection (ADR 0593 §6), which hears a node's
// bucket made later by any stream's creation.
if slices.Contains(d.Reads, delivery.ComposedRead) && !slices.Contains(sub, familyCreatedAdvisory) {
if slices.Contains(d.Reads, ComposedRead) && !slices.Contains(sub, familyCreatedAdvisory) {
sub = append(sub, familyCreatedAdvisory)
}
}
@@ -1214,7 +1213,7 @@ func PermissionsFor(p Principal) (Permissions, error) {
}
}
// And the vault's enrolment verbs to every principal but mesh-enrolment's (novox/hq ADR 0587 §1).
if p.VaultEnrols && !((p.Kind == KindModule || p.Kind == KindProcess) && p.Module == delivery.EnrolmentModule) {
if p.VaultEnrols && !((p.Kind == KindModule || p.Kind == KindProcess) && p.Module == EnrolmentModule) {
for _, only := range EnrolmentOnly() {
if slices.ContainsFunc(pub, func(a string) bool { return SubjectsOverlap(a, only) }) {
deny = append(deny, only)
@@ -5,8 +5,8 @@ import (
"fmt"
"strings"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/provisions"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient"
"github.com/nats-io/nats.go"
)
@@ -49,7 +49,7 @@ var ProvisionerReads = []string{ConfigResolvedBucket}
// reason** (2026-10-30 recovery, issue 774): the client V3 watches every machine's plans to bind a provider's
// consumers, and granted only its own machine's keys it read none. ADR 0570 clause 3 has providers act on the seat's
// provision keys instead; once the client does, plans go back to its own machine's keys.
var ProvisionerReadsWhole = []string{delivery.PlacementBucket, provisions.PlansBucket}
var ProvisionerReadsWhole = []string{meshmoduleassignerclient.PlacementBucket, meshprovisionerclient.PlansBucket}
// BoundState is a providing module's state of entries, one per consumer (mesh-sdk go/provider, issue 678): a
// consumer's machine's client reads its own keys of it, `<node>.<module>.<provision>[.<local>]`, to fill
@@ -147,7 +147,7 @@ func provisionerGrants(p Principal) (pub, sub []string, err error) {
// Its own node's own have, `<node>.node`, read and never written, and no other key of the bucket: it fills every
// module again when the node-engine moves to a new composition of the node's own (issue 1945), since an apply can
// leave a filled file as the composition had it.
pub = append(pub, readGrants(delivery.HaveBucket, p.Node+".node")...)
pub = append(pub, readGrants(HaveBucket, p.Node+".node")...)
pub = append(pub, "prov.applied."+p.Node+".>")
token := CallerToken(p.Username())
if token == "" {
@@ -28,7 +28,7 @@ import (
// (`$KV.<bucket>.<module>.>`), so an asker reads the state of its own asks and no other asker's; and on a seat pulled by
// kind, **what its workers gave up on**, the copies DEAD_LETTERS keeps of the seat's stream (DeadLetterReads, issue 734).
// BuildAskMaxDeliver bounds the deliveries of a build seat's ask (issue 1907): mesh-sdk delivery.AskMaxDeliver, written
// BuildAskMaxDeliver bounds the deliveries of a build seat's ask (issue 1907): sdk/buildstep AskMaxDeliver, written
// here as the broker mirrors the catalogue rather than importing it.
const BuildAskMaxDeliver = 5
@@ -82,7 +82,7 @@ func SeatTrafficOf(module string, holds, uses, watches []Seat) SeatTraffic {
}
if len(s.Accepts) > 0 && s.PulledByKind {
// One worker per accepted kind, which every holder pulls in the order of Accepts (ADR 0525 §4, ADR 0536 §4):
// `SEAT_<SEAT>_<KIND>_worker`, filtered to `accept.<kind>` (mesh-sdk delivery.AskWorker).
// `SEAT_<SEAT>_<KIND>_worker`, filtered to `accept.<kind>` (sdk/buildstep AskWorker).
for _, a := range s.Accepts {
t.Workers = append(t.Workers, Worker{Stream: seatStreamName(s.Name), Consumer: WorkerName(s.Name, a),
Filter: "mesh.seat." + s.Name + ".accept." + a})
@@ -252,7 +252,7 @@ func SeatTrafficObjects(users []Principal) ([]Stream, []Consumer) {
}
if byKind {
// A build seat's ask is offered a bounded number of times (issue 1907): the holder that takes it past
// mesh-sdk delivery.AskRuns ends it failed saying it is poison, and the bound stops a run that kills
// sdk/buildstep AskRuns ends it failed saying it is poison, and the bound stops a run that kills
// every node it reaches even when that holder dies too.
c.MaxDeliver = BuildAskMaxDeliver
c.Why = module + " holds " + seat + "; each holder pulls an ask when it has a free slot, and an ask that " +
@@ -2,8 +2,9 @@ package buscompose
import (
"fmt"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-sdk/go/announce"
"git.novox.be/novox/mesh-sdk/go/delivery"
"slices"
"strings"
"time"
@@ -168,7 +169,7 @@ func stateGrants(a stateAccess) []string {
out = append(out, announce.Subject(a.Module))
}
for _, r := range a.Reads {
if r == delivery.ComposedRead {
if r == ComposedRead {
// Every node's compositions, read-only (ADR 0593 §6), with the listing that finds their buckets.
for _, bucket := range composedFamily(a.Nodes) {
read(bucket, "")
@@ -197,7 +198,7 @@ func stateGrants(a stateAccess) []string {
// NodeKeyedReads are the states keyed `<node>.>` whose reader reads its own machine's keys alone, whatever its `reads`
// names: the node-engine's data items, each node's disk-load reading its own node's (novox/hq ADR 0577 clause 1). A
// machine with no usable name reaches none of them.
var NodeKeyedReads = map[string]bool{delivery.EngineDataRead: true}
var NodeKeyedReads = map[string]bool{EngineDataRead: true}
// StreamCreatedAdvisory is the subject the bus says a stream was created on (the server's own advisory), here for a
// bucket's stream KV_<bucket>.
@@ -230,7 +231,7 @@ func stateCreatedHeard(a stateAccess) []string {
add(BucketName(a.Module, name))
}
for _, r := range a.Reads {
if r == delivery.ComposedRead {
if r == ComposedRead {
// A node's bucket made after the reader began is heard by any stream's creation (ADR 0593 §6).
if !seen[familyCreatedAdvisory] {
seen[familyCreatedAdvisory] = true
@@ -265,7 +266,7 @@ type stateAccess struct {
ReadOnly []string
Reads []string
KeyedReads []KeyedRead
// Nodes are every node, whose bucket of a per-node state (delivery.ComposedRead) a read of it reaches.
// Nodes are every node, whose bucket of a per-node state (ComposedRead) a read of it reaches.
Nodes []string
}
@@ -302,10 +303,10 @@ func stateIssuedFor(d Declared, node string) []StateIssued {
out = append(out, issued)
}
for _, r := range d.Reads {
if r == delivery.ComposedRead {
if r == ComposedRead {
// One bucket per node, issued as the family's prefix (ADR 0593 §6): the runtime reads every node's bucket as one
// state, a node's bucket made later included.
out = append(out, StateIssued{Name: r, Bucket: delivery.ComposedBucketPrefix, PerNode: true})
out = append(out, StateIssued{Name: r, Bucket: ComposedBucketPrefix, PerNode: true})
continue
}
if bucket, ok := bucketOfRead(r); ok {
@@ -340,8 +341,8 @@ const ResolvedBucket = "mesh-config_resolved"
// secret. Reads only; the node is checked as every subject token is by the caller's principal.
func NodeComposeReads(node string) []string {
var out []string
for _, bucket := range []string{delivery.PlacementBucket, delivery.PlacementCandidatesBucket, delivery.ModulesBucket,
delivery.ManifestsBucket, delivery.BuilderBuildsBucket} {
for _, bucket := range []string{meshmoduleassignerclient.PlacementBucket, PlacementCandidatesBucket, ModulesBucket,
ManifestsBucket, builds.BuilderBuildsBucket} {
out = append(out, readGrants(bucket, "")...)
}
return append(out, readGrants(ResolvedBucket, node+".>")...)
@@ -350,7 +351,7 @@ func NodeComposeReads(node string) []string {
// NodeComposeAdvisories are the stream-created advisories of the seat mesh-placement's placement and candidates
// (ADR 0567 §9): what a node agent hears to bind each once it is made. Those two and no wider.
func NodeComposeAdvisories() []string {
return []string{StreamCreatedAdvisory(delivery.PlacementBucket), StreamCreatedAdvisory(delivery.PlacementCandidatesBucket)}
return []string{StreamCreatedAdvisory(meshmoduleassignerclient.PlacementBucket), StreamCreatedAdvisory(PlacementCandidatesBucket)}
}
// ServedReadGrants is the read of each key of mesh-config's resolved values named, `<node>.<module>.<key>`
@@ -1,13 +1,13 @@
package buscompose
import (
"git.novox.be/novox/mesh-catalog/modules/disk-load/diskreadings"
"slices"
"strings"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
"git.novox.be/novox/mesh-sdk/go/announce"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/health"
)
// **The grants part's user list, sealed to the bus node** (novox/hq ADR 0547 clause 7, issue 1945). The grants part writes
@@ -26,10 +26,10 @@ import (
// GrantsUserListBucket, GrantsUserListKey, GrantsUserListObjects and GrantsUserListObjectStream are delivery's names,
// here where the grants are composed.
const (
GrantsUserListBucket = delivery.GrantsUserListBucket
GrantsUserListKey = delivery.GrantsUserListKey
GrantsUserListObjects = delivery.GrantsUserListObjects
GrantsUserListObjectStream = delivery.GrantsUserListObjectStream
GrantsUserListBucket = grantsstate.GrantsUserListBucket
GrantsUserListKey = grantsstate.GrantsUserListKey
GrantsUserListObjects = grantsstate.GrantsUserListObjects
GrantsUserListObjectStream = grantsstate.GrantsUserListObjectStream
)
// GrantsUserListFollows is what the node-engine of the node holding mesh-broker may publish to follow the grants part's
@@ -92,14 +92,14 @@ const ConfigModule = "mesh-config"
var NamedReaders = map[string][]string{VaultBusAccounts: {GrantsModule}, GrantsAccounts: {VaultSeat}, HealersWould: {},
// ADR 0577 clause 1: the node-engine's data items are read by disk-load alone, each node's its own node's keys
// (NodeKeyedReads); the engine announces nothing of its own, so its readers are named here.
delivery.EngineDataRead: {health.DiskLoadModule},
EngineDataRead: {diskreadings.DiskLoadModule},
// ADR 0583 §1 insight: every node's login is read by the grants part alone, each engine its own node's key through its
// machine principal (NodeLoginsReads), never through `reads`.
delivery.NodeLoginsRead: {GrantsModule},
NodeLoginsRead: {GrantsModule},
// ADR 0587 §2: the open enrolments are read by their holder and, as a named reader, by the grants part.
delivery.EnrolmentsRead: {GrantsModule},
EnrolmentsRead: {GrantsModule},
// ADR 0593 §6: every node's compositions are read by the provisioner, the assigner and the view alone.
delivery.ComposedRead: delivery.ComposedReaders}
ComposedRead: ComposedReaders}
// ReaderMay says whether module may read the state read (`<owner>.<name>`) by the named readers: always, for a state
// that names none.
@@ -6,9 +6,10 @@ import (
"sort"
"strings"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
"git.novox.be/novox/mesh-sdk/go/announce"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// Every user the composed file should contain, derived from what the mesh knows.
@@ -165,7 +166,7 @@ type Records struct {
Engine *Declared
// NodeLogins is each node's key of the vault's node-logins (novox/hq ADR 0583 §1): the login in use and the one
// rotating, each composed as a user of the node's permissions. A node absent logs in as `node.<n>` alone.
NodeLogins map[string]delivery.NodeLogin
NodeLogins map[string]meshvault.NodeLoginsByNodeValue
// Announced is each module's last announcement of the states it keeps (novox/hq ADR 0495, issue 1790), as the
// controller keeps it; a module absent has announced nothing yet. Nil where none was read.
Announced map[string]announce.States
@@ -361,7 +362,7 @@ func Users(r Records) ([]Principal, error) {
for _, d := range out[i].Carries {
reads = append(reads, d.Reads...)
}
if slices.Contains(reads, delivery.ComposedRead) {
if slices.Contains(reads, ComposedRead) {
out[i].Nodes = sortedCopy(r.Nodes)
}
}
@@ -445,17 +446,17 @@ func asksSeatOf(r Records) []Seat {
// not hold (a key naming another node's login is no login of this node's).
func nodeLoginsOf(r Records, node string) []string {
l, ok := r.NodeLogins[node]
if !ok || l.Node != node || (l.Login != delivery.FirstLogin(node) && l.Login != delivery.SecondLogin(node)) {
if !ok || l.Node != node || (l.Login != meshvaultclient.FirstLogin(node) && l.Login != meshvaultclient.SecondLogin(node)) {
return []string{""}
}
login := func(name string) string {
if name == delivery.FirstLogin(node) {
if name == meshvaultclient.FirstLogin(node) {
return ""
}
return name
}
out := []string{login(l.Login)}
if l.Rotates() && l.Rotating == delivery.OtherLogin(node, l.Login) {
if meshvaultclient.Rotates(l) && l.Rotating == meshvaultclient.OtherLogin(node, l.Login) {
out = append(out, login(l.Rotating))
}
return out
@@ -4,8 +4,6 @@ import (
"fmt"
"slices"
"strings"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// The writers table (novox/hq to-be 45 §1, ADR 0227 rule 1), compiled in.
@@ -179,13 +177,13 @@ var WritersTable = []WriterRow{
// The seat mesh-enrolment's open enrolments (novox/hq ADR 0587 §2, issue 774): its holder alone writes them, each a
// compare-and-set at the revision read; the grants part reads them as a named reader.
{State: "the open enrolments", Writer: "the seat mesh-enrolment's holder, compare-and-set",
KeptIn: "key-value " + delivery.EnrolmentsBucket + ", with per-key expiry",
KeptIn: "key-value " + EnrolmentsBucket + ", with per-key expiry",
Others: "the grants part reads them (a named reader) and composes each enrol.<node> user from them",
Subjects: kvOf(delivery.EnrolmentsBucket), Writes: moduleNamed(delivery.EnrolmentModule)},
Subjects: kvOf(EnrolmentsBucket), Writes: moduleNamed(EnrolmentModule)},
// Which login each node uses (novox/hq ADR 0583 §1 insight): the vault alone writes it.
{State: "a node's logins", Writer: "the vault", KeptIn: "key-value " + delivery.NodeLoginsBucket,
{State: "a node's logins", Writer: "the vault", KeptIn: "key-value " + NodeLoginsBucket,
Others: "the grants part reads every key, each node's engine its own node's key alone",
Subjects: kvOf(delivery.NodeLoginsBucket), Writes: moduleNamed(delivery.VaultModule)},
Subjects: kvOf(NodeLoginsBucket), Writes: moduleNamed(VaultModule)},
// What a consumer gave up on (novox/hq issue 330): copied by the watchman from the server's notice (ADR 0564
// clause 5, ADR 0590 clause 2).
{State: "a message a consumer gave up on", Writer: "the watchman", KeptIn: "the bus, the stream " + DeadLettersStream,
@@ -194,14 +192,14 @@ var WritersTable = []WriterRow{
// What each machine has of each module (novox/hq ADR 0429 §1, design 47): written by that machine's node-engine under
// its own node's keys; the view and the watchman read it.
{State: "a machine's have", Writer: "the node-engine of the machine, under its own keys `<node>.>`",
KeptIn: "key-value " + delivery.HaveBucket + ", twenty revisions of each key",
Others: "read through `reads` as node-engine.have", Subjects: kvOf(delivery.HaveBucket), Writes: ownNodeKeys},
KeptIn: "key-value " + HaveBucket + ", twenty revisions of each key",
Others: "read through `reads` as node-engine.have", Subjects: kvOf(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 own health report (issue 774): its engine's own key, read by the watchman.
{State: "a machine's health report", Writer: "the node-engine of the machine, its key `<node>` alone",
KeptIn: "key-value " + delivery.NodeHealthBucket, Others: "read by the watchman through `reads`",
Subjects: kvOf(delivery.NodeHealthBucket), Writes: ownNodeKeys},
KeptIn: "key-value " + NodeHealthBucket, Others: "read by the watchman through `reads`",
Subjects: kvOf(NodeHealthBucket), Writes: ownNodeKeys},
// 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`",
@@ -14,8 +14,9 @@ import (
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/views"
)
@@ -276,24 +277,24 @@ func (v *Views) HearVerdict(ctx context.Context, event string, data []byte, logf
if f == nil {
return nil
}
var verdict delivery.ManifestVerdict
var verdict meshcatalog.VerdictsByDigestValue
var digest string
var revision uint64
switch event {
case delivery.ManifestAcceptedEvent:
var a delivery.ManifestAccepted
case meshcatalog.EventManifestAccepted:
var a meshcatalog.ManifestAcceptedEvent
if err := json.Unmarshal(data, &a); err != nil {
return fmt.Errorf("%s does not read: %w", event, err)
}
verdict = delivery.ManifestVerdict{V: delivery.Version, Module: a.Module, State: delivery.VerdictAccepted, Judge: a.Judge}
digest, revision = a.Digest, a.Revision
case delivery.ManifestRefusedEvent:
var r delivery.ManifestRefused
verdict = meshcatalog.VerdictsByDigestValue{V: keys.Version, Module: a.Module, State: meshcatalogclient.VerdictAccepted, Judge: a.Judge}
digest, revision = a.Digest, uint64(a.Revision)
case meshcatalog.EventManifestRefused:
var r meshcatalog.ManifestRefusedEvent
if err := json.Unmarshal(data, &r); err != nil {
return fmt.Errorf("%s does not read: %w", event, err)
}
verdict = delivery.ManifestVerdict{V: delivery.Version, Module: r.Module, State: delivery.VerdictRefused, Judge: r.Judge, Problems: r.Problems}
digest, revision = r.Digest, r.Revision
verdict = meshcatalog.VerdictsByDigestValue{V: keys.Version, Module: r.Module, State: meshcatalogclient.VerdictRefused, Judge: r.Judge, Problems: r.Problems}
digest, revision = r.Digest, uint64(r.Revision)
default:
return fmt.Errorf("%q is not a verdict event of the indexer", event)
}
@@ -306,7 +307,7 @@ func (v *Views) HearVerdict(ctx context.Context, event string, data []byte, logf
if revision == 0 {
return gap(fmt.Sprintf("the indexer's %s of %s carries no revision", event, verdict.Module))
}
key, err := delivery.VerdictsKey(digest)
key, err := meshcatalogclient.VerdictsKey(digest)
if err != nil {
if revision <= held.Revision {
return nil
@@ -318,7 +319,7 @@ func (v *Views) HearVerdict(ctx context.Context, event string, data []byte, logf
// holds for its manifest is passed over, so a consumer's backlog after a restart asks nothing.
if revision < held.Revision {
if raw, found := f.Get(key); found {
if was, err := delivery.DecodeManifestVerdict(raw); err == nil && sameVerdict(was, verdict) {
if was, err := meshcatalogclient.DecodeVerdict(raw); err == nil && sameVerdict(was, verdict) {
return nil
}
}
@@ -337,7 +338,7 @@ func (v *Views) HearVerdict(ctx context.Context, event string, data []byte, logf
}
// sameVerdict says two verdicts of one manifest say the same: its module, accepted or refused, the judge and the problems.
func sameVerdict(a, b delivery.ManifestVerdict) bool {
func sameVerdict(a, b meshcatalog.VerdictsByDigestValue) bool {
return a.Module == b.Module && a.State == b.State && a.Judge == b.Judge && slices.Equal(a.Problems, b.Problems)
}
@@ -6,7 +6,7 @@ import (
"strings"
"sync"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
)
// Watcher watches another part's state: changed hears each change after what stands, ended hears the watch end. A watch
@@ -92,7 +92,7 @@ func (f *Follower) heard(state, key string, value []byte) bool {
if state != ReadHave {
return true
}
node, module, isNode, err := delivery.ParseHaveKey(key)
node, module, isNode, err := nodeengineclient.ParseHaveKey(key)
if err != nil {
return false
}
+18 -15
View File
@@ -9,7 +9,10 @@ import (
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
"git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay"
"git.novox.be/novox/mesh-sdk/go/keys"
"mesh-grants/internal/buscompose"
)
@@ -250,7 +253,7 @@ func (p *Part) loadedContents(in Inputs, read []User, readErr error) (map[string
switch {
case l == nil:
return nil, "the bus node says no loaded list, and nats_users: " + readErr.Error()
case l.Source != delivery.GrantsUserListSource || l.Follows != delivery.FollowGrants || l.Error != "":
case l.Source != grantsstate.GrantsUserListSource || l.Follows != grantsstate.FollowGrants || l.Error != "":
return nil, "the bus loaded no list of the grants part's (source " + l.Source + ", follows " + l.Follows + "), and nats_users: " + readErr.Error()
}
if kept, ok := p.sealedGrantsOf(l.Digest); ok {
@@ -267,7 +270,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.Grant, drop []string, skip map[string]bool) []string {
func MissingGrants(in Inputs, want map[string]grantsstate.Grant, drop []string, skip map[string]bool) []string {
placed := map[string]bool{}
for _, ms := range in.Assigned {
for _, m := range ms {
@@ -279,11 +282,11 @@ func MissingGrants(in Inputs, want map[string]delivery.Grant, drop []string, ski
// A module the mesh provides itself (the private network) is built by no repository, so the indexer holds no
// fingerprint of it and the assigner wants none: the controller composes it with the node's own. It needs no grant,
// and naming it missing raised a warning nothing could clear (sweep 2026-10-30 item 6).
if delivery.ProvidedByMesh(module) {
if overlay.ProvidedByMesh(module) {
continue
}
fp, has := in.Fingerprints[module]
key, err := delivery.PartGrantKey(module, fp)
key, err := grantsstate.PartGrantKey(module, fp)
if has && err == nil {
if _, granted := want[key]; granted {
continue
@@ -348,7 +351,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.Grant, []string, error) {
func GrantsOf(in Inputs, recs buscompose.Records, skip map[string]bool) (map[string]grantsstate.Grant, []string, error) {
principals, err := buscompose.Users(recs)
if err != nil {
return nil, nil, err
@@ -364,13 +367,13 @@ 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.Grant{}
out := map[string]grantsstate.Grant{}
var drop []string
for module, fp := range in.Fingerprints {
if skip[module] || !strings.HasPrefix(fp, delivery.SourcePrefix2) {
if skip[module] || !strings.HasPrefix(fp, meshcatalogclient.SourcePrefix2) {
continue
}
key, err := delivery.PartGrantKey(module, fp)
key, err := grantsstate.PartGrantKey(module, fp)
if err != nil {
continue
}
@@ -380,7 +383,7 @@ func GrantsOf(in Inputs, recs buscompose.Records, skip map[string]bool) (map[str
continue
}
sort.Strings(names)
out[key] = delivery.Grant{Accounts: names}
out[key] = grantsstate.Grant{Accounts: names}
}
sort.Strings(drop)
return out, drop, nil
@@ -427,7 +430,7 @@ func (p *Part) writeGrants(res *Result, in Inputs, recs buscompose.Records, comp
}
for _, key := range sortedKeys(want) {
g := want[key]
data, err := delivery.EncodeGrant(g)
data, err := grantsstate.EncodeGrant(g)
if err != nil {
res.Errors = append(res.Errors, fmt.Sprintf("the grant %s cannot be encoded: %v", key, err))
continue
@@ -436,7 +439,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.DecodeGrant(raw); err == nil && delivery.SameGrant(was, withVersion(g)) {
if was, err := grantsstate.DecodeGrant(raw); err == nil && grantsstate.SameGrant(was, withVersion(g)) {
continue
}
}
@@ -449,7 +452,7 @@ func (p *Part) writeGrants(res *Result, in Inputs, recs buscompose.Records, comp
continue
}
done.Written = append(done.Written, key)
module, fp, _ := delivery.ParsePartGrantKey(key)
module, fp, _ := grantsstate.ParsePartGrantKey(key)
p.say(res, EventGrantWritten, map[string]any{"module": module, "fingerprint": fp, "accounts": g.Accounts})
}
for _, key := range drop {
@@ -464,7 +467,7 @@ func (p *Part) writeGrants(res *Result, in Inputs, recs buscompose.Records, comp
}
}
func withVersion(g delivery.Grant) delivery.Grant {
g.V = delivery.Version
func withVersion(g grantsstate.Grant) grantsstate.Grant {
g.V = keys.Version
return g
}
+54 -43
View File
@@ -17,9 +17,20 @@ import (
"sync"
"time"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-grants/internal/buscompose"
)
// The states of other parts the grants part reads, as its manifest's reads name them (`<module>.<state>`).
@@ -37,13 +48,13 @@ const (
ReadAccounts = "mesh-vault.bus-accounts"
// ReadPlacement is the seat mesh-placement's placement (ADR 0567 §1, §9): each node's modules and the seats
// they hold.
ReadPlacement = delivery.PlacementRead
ReadPlacement = meshmoduleassignerclient.PlacementRead
// ReadEnrolments is the seat mesh-enrolment's open enrolments, read as a named reader (ADR 0587 §2, §4): a key at
// live or redeeming composes the user enrol.<node>.
ReadEnrolments = delivery.EnrolmentsRead
ReadEnrolments = buscompose.EnrolmentsRead
// ReadNodeLogins is the vault's node-logins, read as its named reader (ADR 0583 §1 insight): each node's login in use
// and the one rotating, both composed as users of the node's permissions.
ReadNodeLogins = delivery.NodeLoginsRead
ReadNodeLogins = buscompose.NodeLoginsRead
)
// Reads is every state the part reads, in the manifest's order. Not the controller's assignments: the seat's placement
@@ -122,7 +133,7 @@ type Inputs struct {
Assigned map[string][]string
// Placed is each node's placement as the seat mesh-placement holds it (ADR 0567 §1); empty before any is read.
// Where it is, a seat held once for the mesh is held where placement says, not wherever a module claiming it runs.
Placed map[string]delivery.Placement
Placed map[string]meshmoduleassigner.Placement
// Manifests is each module's manifest at the indexer's trunk, by module.
Manifests map[string]catalogue.Manifest
// Bound is, by module, the streams of each bus-state entry the bus module said it created and has not dropped, from
@@ -143,7 +154,7 @@ type Inputs struct {
Values map[string]any
// BusNode is the machine running the bus's module, and Loaded what its node agent says it loaded.
BusNode string
Loaded *delivery.UserListWritten
Loaded *nodeengine.UserListWritten
// Hashes are the vault's hashes of the bus accounts, by bus user (ReadAccounts); kept in memory, never written.
Hashes map[string]HeldHash
// Enrolling is every node whose enrolment key is at live or redeeming (ReadEnrolments, ADR 0587 §4, §5), sorted: one
@@ -151,7 +162,7 @@ type Inputs struct {
Enrolling []string
// NodeLogins is each node's key of the vault's node-logins (ReadNodeLogins, ADR 0583 §1); a node absent logs in as
// node.<n> alone.
NodeLogins map[string]delivery.NodeLogin
NodeLogins map[string]meshvault.NodeLoginsByNodeValue
// Refused is every manifest the indexer holds that the part cannot read or mesh-sdk's reader refuses, assigned or
// not, each with why: its module counts as placed nowhere, so it is said loudly, never passed over (issue 1945).
Refused []Gap
@@ -257,13 +268,13 @@ func (c *Manifests) get(r Reader, digest string) (catalogue.Manifest, error) {
if !found {
return catalogue.Manifest{}, fmt.Errorf("the indexer holds no manifest %s", digest)
}
v, err := delivery.DecodeManifest(raw)
v, err := meshcatalogclient.DecodeManifest(raw)
if err != nil {
return catalogue.Manifest{}, err
}
// Decoded, never judged (novox/hq ADR 0578 §3): the indexer's judge judged it, and its verdict is taken where the
// module's value is read (Inputs); a field this part does not know is set aside.
m, err = catalogue.Decode([]byte(v.JSON))
m, err = catalogue.Decode([]byte(v.Manifest))
if err != nil {
return catalogue.Manifest{}, fmt.Errorf("the manifest %s does not decode: %w", digest, err)
}
@@ -312,7 +323,7 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
if keys, err := r.Keys(ReadPlacement); err != nil {
in.Gaps = append(in.Gaps, Gap{Input: ReadPlacement, Why: "refused or absent: nothing is placed (" + err.Error() + ")", Failed: true})
} else {
// Each node's keys in either form, assembled by the seat's one reader (delivery.PlacementsOf; research 072
// Each node's keys in either form, assembled by the seat's one reader (meshmoduleassignerclient.PlacementsOf; research 072
// row 37): its node key and a key per module, or its legacy key `placement.<node>`.
values := map[string][]byte{}
for _, k := range keys {
@@ -320,26 +331,26 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
if err != nil || !found {
continue
}
if _, _, _, err := delivery.ParsePlacementKey(k); err != nil {
if _, _, _, err := meshmoduleassignerclient.ParseKey(k); err != nil {
in.Gaps = append(in.Gaps, Gap{Input: ReadPlacement + " " + k, Why: "not a placement key, so it places nothing"})
continue
}
values[k] = raw
}
placed, err := delivery.PlacementsOf(values)
placed, err := meshmoduleassignerclient.PlacementsOf(values)
if err != nil {
in.Gaps = append(in.Gaps, Gap{Input: ReadPlacement, Why: "a key does not read, so its node is composed from what does: " + err.Error()})
}
for node, p := range placed {
if in.Placed == nil {
in.Placed = map[string]delivery.Placement{}
in.Placed = map[string]meshmoduleassigner.Placement{}
}
in.Placed[node] = p
}
}
for node, p := range in.Placed {
nodes[node] = true
for _, m := range p.Assigned() {
for _, m := range meshmoduleassignerclient.Assigned(p) {
if !slices.Contains(in.Assigned[node], m) {
in.Assigned[node] = append(in.Assigned[node], m)
}
@@ -356,7 +367,7 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
in.Gaps = append(in.Gaps, Gap{Input: ReadHave, Why: "refused or absent: " + err.Error(), Failed: true})
}
for _, k := range haveKeys {
if node, _, isNode, err := delivery.ParseHaveKey(k); err == nil && isNode {
if node, _, isNode, err := nodeengineclient.ParseHaveKey(k); err == nil && isNode {
nodes[node] = true
}
}
@@ -369,7 +380,7 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
if raw, found, err := r.Get(ReadHave, in.BusNode+"."+BusModule); err != nil {
in.Gaps = append(in.Gaps, Gap{Input: "the loaded list's have", Why: err.Error(), Failed: true})
} else if found {
if h, err := delivery.DecodeHave(raw); err == nil {
if h, err := nodeengineclient.DecodeHave(raw); err == nil {
in.Loaded = h.UserList
}
}
@@ -404,12 +415,12 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
if !found {
continue
}
v, err := delivery.DecodeModule(raw)
v, err := meshcatalogclient.DecodeModule(raw)
if err == nil && v.Manifest == "" {
err = fmt.Errorf("the indexer's value names no manifest digest")
}
if err == nil && strings.HasPrefix(v.FP2, delivery.SourcePrefix2) {
in.Fingerprints[module] = v.FP2
if err == nil && strings.HasPrefix(v.Fp2, meshcatalogclient.SourcePrefix2) {
in.Fingerprints[module] = v.Fp2
} else if err == nil {
why := v.None2
if why == "" {
@@ -420,8 +431,8 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
var m catalogue.Manifest
if err == nil {
// The indexer's refusal, or its wait for a judge that knows a field, carried in its value (ADR 0578 §3): held and named as a refused manifest.
if verdict := v.VerdictOf(module); verdict != nil && verdict.Held() {
err = errors.New(verdict.Why())
if verdict := meshcatalogclient.ModuleVerdict(module, v); verdict != nil && meshcatalogclient.VerdictHeld(*verdict) {
err = errors.New(meshcatalogclient.VerdictWhy(*verdict))
} else if verdict == nil {
// The indexer writes a changed manifest's value before it judges it, and a new digest has no verdict: until
// the verdict is written the manifest is held (ADR 0614 §8).
@@ -447,11 +458,11 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
// A module the mesh provides itself has no repository the indexer reads: its manifest comes with mesh-sdk, and the
// controller's Provide overwrites any row of it at every start, so the provided one always wins here too (issue 1945,
// the private network on every machine). An indexer manifest of the same name is said as a gap, never used.
for _, name := range delivery.ProvidedModules() {
for _, name := range overlay.ProvidedModules() {
if !slices.Contains(modules, name) {
continue
}
m, raw, err := delivery.ProvidedManifest(name)
m, raw, err := overlay.ProvidedManifest(name)
in.Gaps = slices.DeleteFunc(in.Gaps, func(g Gap) bool { return g.Input == "manifest of "+name })
in.Refused = slices.DeleteFunc(in.Refused, func(g Gap) bool { return g.Input == "manifest of "+name })
if _, held := in.Digests[name]; held {
@@ -477,19 +488,19 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
// What each machine is wanted to run: the want's fingerprint, the builder's build of it, the manifest it built.
var fromTrunk []string
for key, raw := range each(r, ReadWants, &in.Gaps) {
node, module, err := delivery.ParseWantKey(key)
node, module, err := meshwantsclient.ParseWantKey(key)
if err != nil || !slices.Contains(in.Assigned[node], module) {
continue
}
w, err := delivery.DecodeWant(raw)
w, err := meshwantsclient.DecodeWant(raw)
if err != nil {
fromTrunk = append(fromTrunk, node+"."+module)
continue
}
if w.Removed || w.FP == "" {
if w.Removed || w.Fp == "" {
continue
}
bk, err := delivery.BuilderBuildKey(module, w.FP)
bk, err := builds.BuilderBuildKey(module, w.Fp)
if err != nil {
continue
}
@@ -498,7 +509,7 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
fromTrunk = append(fromTrunk, node+"."+module)
continue
}
b, err := delivery.DecodeBuilderBuild(braw)
b, err := builds.DecodeBuilderBuild(braw)
if err != nil || b.Manifest == "" {
fromTrunk = append(fromTrunk, node+"."+module)
continue
@@ -560,7 +571,7 @@ func readEnrolling(r Reader, gaps *[]Gap) ([]string, bool) {
}
var out []string
for _, k := range keys {
node, isNode := delivery.EnrolmentKeyNode(k)
node, isNode := meshnodeenroller.ParseEnrolmentsByNodeNodeKey(k)
if !isNode {
continue
}
@@ -572,13 +583,13 @@ func readEnrolling(r Reader, gaps *[]Gap) ([]string, bool) {
if !found {
continue
}
e, err := delivery.DecodeEnrolment(raw)
e, err := meshnodeenroller.DecodeEnrolmentsByNodeNodeValue(raw)
if err != nil || e.Node != node {
*gaps = append(*gaps, Gap{Input: ReadEnrolments + " " + k, Why: "does not read as the enrolment of " + node +
", so it composes no user"})
continue
}
if e.Stage == delivery.EnrolmentLive || e.Stage == delivery.EnrolmentRedeeming {
if e.Stage == buscompose.EnrolmentLive || e.Stage == buscompose.EnrolmentRedeeming {
out = append(out, node)
}
}
@@ -590,13 +601,13 @@ func readEnrolling(r Reader, gaps *[]Gap) ([]string, bool) {
// never a failed read: every node then logs in as node.<n> alone, as before any rotation. A key that does not read is
// named and leaves its node at node.<n>; a read that fails is a failed read, since a rotating node's new login would be
// left out.
func readNodeLogins(r Reader, gaps *[]Gap) map[string]delivery.NodeLogin {
func readNodeLogins(r Reader, gaps *[]Gap) map[string]meshvault.NodeLoginsByNodeValue {
keys, err := r.Keys(ReadNodeLogins)
if err != nil {
*gaps = append(*gaps, Gap{Input: ReadNodeLogins, Why: "not read yet: every node logs in as node.<node> (" + err.Error() + ")"})
return nil
}
out := map[string]delivery.NodeLogin{}
out := map[string]meshvault.NodeLoginsByNodeValue{}
for _, node := range keys {
raw, found, err := r.Get(ReadNodeLogins, node)
if err != nil {
@@ -606,7 +617,7 @@ func readNodeLogins(r Reader, gaps *[]Gap) map[string]delivery.NodeLogin {
if !found {
continue
}
l, err := delivery.DecodeNodeLogin(raw)
l, err := meshvaultclient.DecodeNodeLogin(raw)
if err != nil || l.Node != node {
*gaps = append(*gaps, Gap{Input: ReadNodeLogins + " " + node, Why: "does not read as the logins of " + node +
", so it logs in as node." + node})
@@ -622,7 +633,7 @@ func readNodeLogins(r Reader, gaps *[]Gap) map[string]delivery.NodeLogin {
func (in Inputs) Rotating() map[string]bool {
out := map[string]bool{}
for _, l := range in.NodeLogins {
if l.Rotates() {
if meshvaultclient.Rotates(l) {
out[l.Login], out[l.Rotating] = true, true
}
}
@@ -680,11 +691,11 @@ func heldManifests(r Reader, in Inputs, cache *Manifests) map[string]catalogue.M
if err != nil || !found {
continue
}
h, err := delivery.DecodeHave(raw)
if err != nil || h.FP == "" {
h, err := nodeengineclient.DecodeHave(raw)
if err != nil || h.Fp == "" {
continue
}
bk, err := delivery.BuilderBuildKey(module, h.FP)
bk, err := builds.BuilderBuildKey(module, h.Fp)
if err != nil {
continue
}
@@ -692,7 +703,7 @@ func heldManifests(r Reader, in Inputs, cache *Manifests) map[string]catalogue.M
if err != nil || !found {
continue
}
b, err := delivery.DecodeBuilderBuild(braw)
b, err := builds.DecodeBuilderBuild(braw)
if err != nil || b.Manifest == "" {
continue
}
@@ -724,7 +735,7 @@ func (in Inputs) declared() map[string]catalogue.Manifest {
// noVerdictYet is nil when the indexer's verdicts state holds an accepted verdict on the manifest, and the reason it is held
// when it holds none yet, cannot be read, or refuses it.
func noVerdictYet(r Reader, digest string) error {
key, err := delivery.VerdictsKey(digest)
key, err := meshcatalogclient.VerdictsKey(digest)
if err != nil {
return nil
}
@@ -735,12 +746,12 @@ func noVerdictYet(r Reader, digest string) error {
if !found {
return errors.New("the indexer has written no verdict on this manifest yet")
}
v, err := delivery.DecodeManifestVerdict(raw)
v, err := meshcatalogclient.DecodeVerdict(raw)
if err != nil {
return fmt.Errorf("its verdict does not read: %w", err)
}
if v.Held() {
return errors.New(v.Why())
if meshcatalogclient.VerdictHeld(v) {
return errors.New(meshcatalogclient.VerdictWhy(v))
}
return nil
}
+6 -5
View File
@@ -13,8 +13,9 @@ import (
"sync"
"time"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
"git.novox.be/novox/mesh-sdk/go/membership"
"mesh-grants/internal/buscompose"
)
@@ -25,7 +26,7 @@ const (
StateMemberships = "memberships"
StateAssertions = "assertions"
// StateGrants is the part's grants by module and fingerprint, the key layout of the controller's (grants.go).
StateGrants = delivery.PartGrantsState
StateGrants = grantsstate.PartGrantsState
KeyList = "list"
// KeyPass is the last pass's result, and KeyAsked a tool's ask to compose again now, both in StateUserList.
KeyPass = "pass"
@@ -72,9 +73,9 @@ type UserListState struct {
Digest string `json:"digest"`
Users int `json:"users"`
// LeftOut are the assignments the shadow list leaves out, each with why.
LeftOut []Gap `json:"left_out"`
Compare Comparison `json:"compare"`
Loaded *delivery.UserListWritten `json:"loaded,omitempty"`
LeftOut []Gap `json:"left_out"`
Compare Comparison `json:"compare"`
Loaded *nodeengine.UserListWritten `json:"loaded,omitempty"`
// Credentials says per user whether the vault holds its hash and how its line would differ (ADR 0547 clause 9).
Credentials Credentials `json:"credentials"`
BusNode string `json:"bus_node,omitempty"`
@@ -7,8 +7,8 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-grants/internal/buscompose"
)
@@ -258,7 +258,7 @@ func bucketsFor(m catalogue.Manifest, nodes []string) []buscompose.Bucket {
if m.Module != buscompose.EngineModule {
return out
}
out = slices.DeleteFunc(out, func(b buscompose.Bucket) bool { return b.Name == delivery.ComposedState })
out = slices.DeleteFunc(out, func(b buscompose.Bucket) bool { return b.Name == buscompose.ComposedState })
return append(out, buscompose.EngineComposedBuckets(nodes)...)
}
@@ -415,8 +415,8 @@ func hearsOf(m catalogue.Manifest, declared map[string]catalogue.Manifest, node
// module claiming it wherever it is placed, as it was before placement: the seeded placements hold only the seats the
// controller had on record (twelve on the control-node), and treating their holds as every holder left nine seats with
// no holder in the user list of 2026-10-27 09:21 (issue 1945). A seat of every node is held wherever the module runs.
func heldWherePlaced(holds []buscompose.Seat, placed map[string]delivery.Placement, node, module string) []buscompose.Seat {
same := func(x delivery.Holding, h buscompose.Seat) bool {
func heldWherePlaced(holds []buscompose.Seat, placed map[string]meshmoduleassigner.Placement, node, module string) []buscompose.Seat {
same := func(x meshmoduleassigner.Holding, h buscompose.Seat) bool {
return (x.Scope == "" || x.Scope == catalogue.ScopeMesh) && sameSeat(x.Claim, h.Name) && x.Kind == h.Kind
}
var out []buscompose.Seat
@@ -427,12 +427,12 @@ func heldWherePlaced(holds []buscompose.Seat, placed map[string]delivery.Placeme
}
onRecord := false
for _, p := range placed {
if slices.ContainsFunc(p.Holds, func(x delivery.Holding) bool { return same(x, h) }) {
if slices.ContainsFunc(p.Holds, func(x meshmoduleassigner.Holding) bool { return same(x, h) }) {
onRecord = true
break
}
}
if !onRecord || slices.ContainsFunc(placed[node].Holds, func(x delivery.Holding) bool { return same(x, h) && x.Module == module }) {
if !onRecord || slices.ContainsFunc(placed[node].Holds, func(x meshmoduleassigner.Holding) bool { return same(x, h) && x.Module == module }) {
out = append(out, h)
}
}
@@ -9,7 +9,7 @@ import (
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/health"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
"mesh-grants/internal/buscompose"
)
@@ -24,8 +24,8 @@ const CallRootFree = "seat:node-engine.root-free"
// since the controller ended by its unassignment (novox/hq ADR 0590 §3, which ends ADR 0564 §9's selection by the
// take-over key).
const (
EventKeeperRaised = health.KeeperModule + ".condition-raised"
EventKeeperCleared = health.KeeperModule + ".condition-cleared"
EventKeeperRaised = meshconditions.Seat + ".condition-raised"
EventKeeperCleared = meshconditions.Seat + ".condition-cleared"
// KindRootNotFree is the condition kind whose events wake a pass.
KindRootNotFree = "root-not-free"
)
@@ -33,7 +33,7 @@ const (
// FromKeeper says a condition event heard is the keeper's, by its name (`<seat>.<event>`) or its bus subject
// (`mesh.seat.<seat>.event.<event>`).
func FromKeeper(event string) bool {
return strings.HasPrefix(event, health.KeeperModule+".") || strings.HasPrefix(event, "mesh.seat."+health.KeeperModule+".event.")
return strings.HasPrefix(event, meshconditions.Seat+".") || strings.HasPrefix(event, "mesh.seat."+meshconditions.Seat+".event.")
}
// EventAccountIssued is the vault's word that it minted an account (novox/hq ADR 0561 §1): its hash is in the vault's
+18 -18
View File
@@ -11,7 +11,7 @@ import (
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
"mesh-grants/internal/buscompose"
)
@@ -22,8 +22,8 @@ import (
// the users left out for want of a hash. The key always says FollowGrants: the bus node follows this list alone since the
// controller ended by its unassignment (ADR 0590, issue 1126). Only the process writes them (Objects); a tool asks it.
// KeySealed is the key of the state user-list the bus node reads (delivery.GrantsUserListKey).
const KeySealed = delivery.GrantsUserListKey
// KeySealed is the key of the state user-list the bus node reads (grantsstate.GrantsUserListKey).
const KeySealed = grantsstate.GrantsUserListKey
// Objects is the part's object bucket of sealed lists: the process's connection; nil in the tools.
type Objects interface {
@@ -69,14 +69,14 @@ func ComposeList(r buscompose.Records, hashes map[string]HeldHash, refused map[s
}
// Sealed is the part's key `user-list.sealed` as written, and whether there is one.
func (p *Part) Sealed() (delivery.GrantsUserList, uint64, bool) {
func (p *Part) Sealed() (grantsstate.GrantsUserList, uint64, bool) {
raw, rev, found, err := p.Store.Get(StateUserList, KeySealed)
if err != nil || !found {
return delivery.GrantsUserList{}, 0, false
return grantsstate.GrantsUserList{}, 0, false
}
g, err := delivery.DecodeGrantsUserList(raw)
g, err := grantsstate.DecodeGrantsUserList(raw)
if err != nil {
return delivery.GrantsUserList{}, 0, false
return grantsstate.GrantsUserList{}, 0, false
}
return g, rev, true
}
@@ -88,12 +88,12 @@ 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 = delivery.FollowGrants
next.Follow = grantsstate.FollowGrants
key := ""
if in.Loaded != nil {
key = in.Loaded.Key
}
digest := delivery.UserListDigest(list)
digest := grantsstate.UserListDigest(list)
why, held := "", ""
switch {
case p.Objects == nil:
@@ -110,10 +110,10 @@ func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut, dropped
why = p.holdsBack(in, leftOut, dropped, bus, lost, digest)
held = why
p.keepHashHold(res, digest)
case cur.Digest != digest || cur.SealedTo != delivery.SealingKeyID(key) || cur.Node != in.BusNode:
object, err := delivery.SealUserList(list, key)
case cur.Digest != digest || cur.SealedTo != grantsstate.SealingKeyID(key) || cur.Node != in.BusNode:
object, err := grantsstate.SealUserList(list, key)
if err == nil {
err = p.Objects.PutObject(delivery.UserListObjectName(digest), object)
err = p.Objects.PutObject(grantsstate.UserListObjectName(digest), object)
}
if err != nil {
res.Errors = append(res.Errors, "the sealed user list could not be put: "+err.Error())
@@ -123,7 +123,7 @@ func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut, dropped
if cur.Digest != digest {
next.Revision++
}
next.Digest, next.Object, next.SealedTo, next.Node = digest, delivery.UserListObjectName(digest), delivery.SealingKeyID(key), in.BusNode
next.Digest, next.Object, next.SealedTo, next.Node = digest, grantsstate.UserListObjectName(digest), grantsstate.SealingKeyID(key), in.BusNode
}
p.sayHeld(held)
if next.Digest == digest {
@@ -137,7 +137,7 @@ func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut, dropped
return
}
next.At = p.now()
data, err := delivery.EncodeGrantsUserList(next)
data, err := grantsstate.EncodeGrantsUserList(next)
if err == nil {
_, err = p.Store.Put(StateUserList, KeySealed, data)
}
@@ -192,7 +192,7 @@ func (p *Part) sayHeld(why string) {
// HeldCondition is the key of the warning raised while a list in force is held back.
const HeldCondition = "mesh-grants.user-list-held"
func sameSealed(a, b delivery.GrantsUserList) bool {
func sameSealed(a, b grantsstate.GrantsUserList) bool {
// Its time is no change, and no user left out is the same written as [] or null: the key read back says [] for the
// nil a pass composes, which read as a change wrote the key again at every pass, and each write moved the bus node's
// report and woke the next pass (issue 1126: the shadow list's revision 99270 to 99357 in 90 s, 2026-10-28).
@@ -210,11 +210,11 @@ func sameSealed(a, b delivery.GrantsUserList) bool {
// SealedState is what a pass says of the sealed list: the key as written, and why nothing was written when nothing was.
type SealedState struct {
Written *delivery.GrantsUserList `json:"written,omitempty"`
Why string `json:"why,omitempty"`
Written *grantsstate.GrantsUserList `json:"written,omitempty"`
Why string `json:"why,omitempty"`
}
func sealedSays(g delivery.GrantsUserList, had bool, why string) *SealedState {
func sealedSays(g grantsstate.GrantsUserList, had bool, why string) *SealedState {
s := &SealedState{Why: why}
if had {
s.Written = &g
+11 -9
View File
@@ -7,7 +7,9 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
"git.novox.be/novox/mesh-sdk/go/membership"
"mesh-grants/internal/buscompose"
)
@@ -42,14 +44,14 @@ const WritesGrants = "the grants part writes grants.<module>.<fp> 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.Grant, error) {
read := func(get func() ([]string, func(string) ([]byte, bool, error), error)) (map[string]grantsstate.Grant, error) {
keys, one, err := get()
if err != nil {
return nil, err
}
m := map[string]delivery.Grant{}
m := map[string]grantsstate.Grant{}
for _, k := range keys {
mod, _, err := delivery.ParsePartGrantKey(k)
mod, _, err := grantsstate.ParsePartGrantKey(k)
if err != nil || (module != "" && mod != module) {
continue
}
@@ -60,7 +62,7 @@ func (p *Part) Grants(module string) (any, error) {
if !found {
continue
}
g, err := delivery.DecodeGrant(raw)
g, err := grantsstate.DecodeGrant(raw)
if err != nil {
return nil, fmt.Errorf("%s: %w", k, err)
}
@@ -80,10 +82,10 @@ func (p *Part) Grants(module string) (any, error) {
Module string `json:"module"`
Accounts []string `json:"accounts,omitempty"`
}
list := func(m map[string]delivery.Grant) []grant {
list := func(m map[string]grantsstate.Grant) []grant {
out := []grant{}
for _, k := range sortedKeys(m) {
mod, _, _ := delivery.ParsePartGrantKey(k)
mod, _, _ := grantsstate.ParsePartGrantKey(k)
out = append(out, grant{Key: k, Module: mod, Accounts: m[k].Accounts})
}
return out
@@ -245,7 +247,7 @@ func (p *Part) UserList() (any, error) {
}
if st.BusNode != "" {
if raw, found, err := p.Read.Get(ReadHave, st.BusNode+"."+BusModule); err == nil && found {
if h, err := delivery.DecodeHave(raw); err == nil {
if h, err := nodeengineclient.DecodeHave(raw); err == nil {
out["loaded_now"] = h.UserList
}
}
@@ -269,7 +271,7 @@ func (p *Part) AssertOne(module string) (any, error) {
if !found {
return nil, fmt.Errorf("the indexer holds no module %s", module)
}
v, err := delivery.DecodeModule(raw)
v, err := meshcatalogclient.DecodeModule(raw)
if err != nil || v.Manifest == "" {
return nil, fmt.Errorf("the indexer names no manifest of %s", module)
}
@@ -10,8 +10,8 @@ import (
"sort"
"strings"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-grants/internal/buscompose"
)
@@ -397,7 +397,7 @@ func Placed(in Inputs) func(user string) bool {
return true
case parts[0] == "node" || parts[0] == "provisioner":
// A node's second login, node.<n>--b, is its node's (ADR 0583 §1).
return slices.Contains(in.Nodes, strings.TrimSuffix(parts[1], delivery.SecondLoginSuffix))
return slices.Contains(in.Nodes, strings.TrimSuffix(parts[1], meshvaultclient.SecondLoginSuffix))
case parts[0] == "person" || parts[0] == "enrol":
return true
}