From 3c6fb7fc9e91fca1c72dcdec39bd547c150121a6 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 27 Oct 2026 22:25:31 +0100 Subject: [PATCH] =?UTF-8?q?mesh-grants:=20compose=20the=20user=20list=20ag?= =?UTF-8?q?ain=20when=20the=20vault=20says=20account-issued,=20so=20a=20mi?= =?UTF-8?q?nted=20account=20reaches=20the=20bus=20within=20one=20pass=20(i?= =?UTF-8?q?ssue=20774,=20ADR=200561=20=C2=A71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/mesh-grants/cmd/mesh-grants/main.go | 5 +-- modules/mesh-grants/grants_test.go | 35 +++++++++++++++++++ modules/mesh-grants/internal/grants/follow.go | 8 ++--- .../mesh-grants/internal/grants/rootfree.go | 11 +++++- modules/mesh-grants/module.json | 3 +- 5 files changed, 54 insertions(+), 8 deletions(-) diff --git a/modules/mesh-grants/cmd/mesh-grants/main.go b/modules/mesh-grants/cmd/mesh-grants/main.go index b8595cd..b6ff6a5 100644 --- a/modules/mesh-grants/cmd/mesh-grants/main.go +++ b/modules/mesh-grants/cmd/mesh-grants/main.go @@ -134,8 +134,9 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro }) } // The controller makes this module's durable consumer on EVENTS from the manifest's consumes (filtered on the two - // condition subjects); it is pulled and every message acknowledged, so its pending never grows. Where it is not there - // yet the pass says so and hears again at the next change. + // condition subjects and the vault's account-issued; ADR 0561 §6 gives that to the part from the lease write); it is + // pulled and every message acknowledged, so its pending never grows. Where it is not there yet the pass says so and + // hears again at the next change. The vault's bus-accounts is watched beside it, so a hash is composed either way. f.Hear = func(handle func(string, []byte)) error { cons, err := js.Consumer(ctx, "EVENTS", account.Node+"_"+account.Module) if err != nil { diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 7c31abc..fd7159c 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -1909,6 +1909,41 @@ func TestGrants(t *testing.T) { t.Errorf("a part with no bus address wrote %+v: %v", res.Accounts, res.Errors) } }) + + // 774/AC5 (novox/hq ADR 0561 §1): the vault's account-issued wakes a pass, so the next list carries the new user's hash + // within one pass; an event of another kind does not. + t.Run("the vault's account-issued composes the list again", func(t *testing.T) { + b := fixture() + var handle func(string, []byte) + p := partOn(b) + f := &grants.Follower{Part: p, Watch: &fakeWatcher{opened: make(chan string, 16)}, Logf: func(string, ...any) {}, + Hear: func(h func(string, []byte)) error { handle = h; return nil }} + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan struct{}) + go func() { f.Run(ctx); close(done) }() + defer func() { cancel(); <-done }() + waitFor(t, func() bool { return b.count(grants.CallUsers) > 0 && handle != nil }) + held := func() bool { + for _, c := range p.Last().UserList.Credentials.Users { + if c.User == "a.mod-x" { + return c.HashHeld + } + } + return false + } + if held() { + t.Fatal("a.mod-x has a hash before the vault made one") + } + b.mu.Lock() + b.states[grants.ReadAccounts] = map[string][]byte{"a.mod-x": []byte(`{"user":"a.mod-x","hash":"$2a$10$x","generation":1}`)} + b.mu.Unlock() + handle(buscompose.AccountIssuedSubject, []byte(`{"user":"a.mod-x","kind":"module","node":"a","module":"mod-x","generation":1}`)) + waitFor(t, held) + if !slices.Contains(grants.Consumes, grants.EventAccountIssued) || !grants.IsAccountIssued(grants.EventAccountIssued) || + grants.IsAccountIssued("mesh.mod.mesh-vault.event.secret-changed") { + t.Errorf("the part hears %v", grants.Consumes) + } + }) } type fakeAsker struct { diff --git a/modules/mesh-grants/internal/grants/follow.go b/modules/mesh-grants/internal/grants/follow.go index 3d19217..79ff31d 100644 --- a/modules/mesh-grants/internal/grants/follow.go +++ b/modules/mesh-grants/internal/grants/follow.go @@ -28,9 +28,9 @@ type Follower struct { Logf func(string, ...any) // Asked watches the part's own ask to compose now (a tool's compose-now), waking at each; nil watches none. Asked func(wake func()) error - // Hear hears the controller seat's condition events (Consumes) and hands each, by event name, with its body, to - // handle; nil hears none. A condition of the kind root-not-free raised or cleared wakes a pass: a machine's - // root-free word changed. + // Hear hears the controller seat's condition events and the vault's account-issued (Consumes) and hands each, by event + // name, with its body, to handle; nil hears none. A condition of the kind root-not-free raised or cleared wakes a pass: + // a machine's root-free word changed; and so does an account the vault minted (ADR 0561 §1), whose hash the list carries. Hear func(handle func(event string, body []byte)) error wake chan struct{} @@ -124,7 +124,7 @@ func (f *Follower) open() { } if f.Hear != nil && !f.heard_ { err := f.Hear(func(event string, body []byte) { - if IsRootNotFree(body) { + if IsAccountIssued(event) || IsRootNotFree(body) { f.Wake() } }) diff --git a/modules/mesh-grants/internal/grants/rootfree.go b/modules/mesh-grants/internal/grants/rootfree.go index b4e1223..614bc4b 100644 --- a/modules/mesh-grants/internal/grants/rootfree.go +++ b/modules/mesh-grants/internal/grants/rootfree.go @@ -23,8 +23,17 @@ const ( KindRootNotFree = "root-not-free" ) +// EventAccountIssued is the vault's word that it minted an account (novox/hq ADR 0561 §1): its hash is in the vault's +// bus-accounts, so a pass composes the list with it. +const EventAccountIssued = "mesh-vault." + buscompose.AccountIssuedEvent + // Consumes is every event the part hears, as its manifest's consumes name them. -var Consumes = []string{EventConditionRaised, EventConditionCleared} +var Consumes = []string{EventConditionRaised, EventConditionCleared, EventAccountIssued} + +// IsAccountIssued says an event heard is the vault's account-issued, by its name or its bus subject. +func IsAccountIssued(event string) bool { + return event == EventAccountIssued || event == buscompose.AccountIssuedSubject +} // EventSubject is the bus subject of a controller seat event named `mesh-controller.`. func EventSubject(event string) string { diff --git a/modules/mesh-grants/module.json b/modules/mesh-grants/module.json index ebfbb7d..af1fa4c 100644 --- a/modules/mesh-grants/module.json +++ b/modules/mesh-grants/module.json @@ -16,7 +16,8 @@ ], "consumes": [ "mesh-controller.condition-raised", - "mesh-controller.condition-cleared" + "mesh-controller.condition-cleared", + "mesh-vault.account-issued" ], "uses": [ "operator-channel"