From 8f87dbe94d760c00db2c2361f5b402f8832c5cf7 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 2 Nov 2026 09:44:26 +0100 Subject: [PATCH] mesh-delivery-teller: read asks from the router's bucket the grant names The teller read the router's record from KV_asks, a bucket no composed grant names, so every read was denied and its operator answers could not be read back. It now reads messenger_asks, as mesh-grants does. The integration bed grants the real subject and reads a record on the teller's credential, so it no longer agrees with the mistake. (issue 774) Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- .../cmd/mesh-delivery-teller/decided.go | 14 +++++++---- .../mesh-delivery-teller/integration_test.go | 23 +++++++++++++++---- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/decided.go b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/decided.go index f6f4e9b..ee7780b 100644 --- a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/decided.go +++ b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/decided.go @@ -37,14 +37,20 @@ func publishVia(ctx context.Context, js jetstream.JetStream) func(subject string } } -// recordVia reads the router's record of one of this module's asks: the key `.` of the router's bucket asks, -// by a direct get on the subject the controller grants for it, `$JS.API.DIRECT.GET.KV_asks.$KV.asks..>`. The -// record is {state, warrant}. An ask the router holds no record of is nil. +// messengerAsks is the router's records bucket as the seat operator-channel's manifest declares it (its `records`), the +// one the controller composes this account's direct get on (mesh-sdk buscompose SeatTrafficOf): never `asks`, a bucket +// no grant names (issue 774). The SDK names no constant for it. +const messengerAsks = "messenger_asks" + +// recordVia reads the router's record of one of this module's asks: the key `.` of the router's bucket +// messenger_asks, by a direct get on the subject the controller grants for it, +// `$JS.API.DIRECT.GET.KV_messenger_asks.$KV.messenger_asks..>`. The record is {state, warrant}. An ask the +// router holds no record of is nil. func recordVia(ctx context.Context, conn *nats.Conn, module string) func(id string) (*stdio.StateEntry, error) { return func(id string) (*stdio.StateEntry, error) { ctx, cancel := context.WithTimeout(ctx, callTimeout) defer cancel() - msg, err := conn.RequestWithContext(ctx, "$JS.API.DIRECT.GET.KV_asks.$KV.asks."+module+"."+id, nil) + msg, err := conn.RequestWithContext(ctx, "$JS.API.DIRECT.GET.KV_"+messengerAsks+".$KV."+messengerAsks+"."+module+"."+id, nil) if errors.Is(err, nats.ErrNoResponders) { return nil, nil } diff --git a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/integration_test.go b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/integration_test.go index e32088b..9975628 100644 --- a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/integration_test.go +++ b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/integration_test.go @@ -130,9 +130,9 @@ func TestTellerWaitsForTheGate(t *testing.T) { // 1240/AC2 // them (internal/broker): a module's own state is the read grants of its bucket and `$KV..>` to write it // (state.go stateGrants, readGrants), a read of another module's bucket is the read grants alone, and a module that uses // the seat operator-channel submits its own `accept.ask.`, `accept.cancel.` and `accept.condition.`, -// hears its own `event.decided.` and reads `$KV.asks..>` of the router's record (seattraffic.go +// hears its own `event.decided.` and reads `$KV.messenger_asks..>` of the router's record (seattraffic.go // SeatTrafficOf, with the messenger's seat declaring accepts ask, cancel, condition, emits decided, by-caller and records -// asks). The gate is a bundle on the tool runner's account (ADR 0437 §3), so the gate's writes are the account node-tools'. +// messenger_asks). The gate is a bundle on the tool runner's account (ADR 0437 §3), so the gate's writes are the account node-tools'. // The permissions are these lists written out, not the controller's function: the broker tests of issue 1243 prove that. func readOf(bucket string) []string { stream := "KV_" + bucket @@ -176,7 +176,7 @@ const ( func fullConf(dir string) string { teller := append(append(append(keepsOf(tellerAnswers), keepsOf(tellerAsked)...), readOf(gateAsksKV)...), - seatAsk, seatCancel, seatCondition, "$JS.API.DIRECT.GET.KV_asks.$KV.asks.mesh-delivery-teller.>") + seatAsk, seatCancel, seatCondition, "$JS.API.DIRECT.GET.KV_"+messengerAsks+".$KV."+messengerAsks+".mesh-delivery-teller.>") teller = append(append(teller, readOf(assignerAsks)...), readOf(viewAnswers)...) // issue 1297 B10: the manifest's reads view := append(append(append(readOf(tellerAnswers), readOf(gateAsksKV)...), readOf(assignerAsks)...), keepsOf(viewAnswers)...) assigner := keepsOf(assignerAsks) @@ -244,7 +244,7 @@ func startFull(t *testing.T) *fullBed { } ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) t.Cleanup(cancel) - for name, history := range map[string]uint8{gateAsksKV: delivery.AsksHistory, tellerAnswers: delivery.AnswersHistory, tellerAsked: 1, assignerAsks: delivery.AssignerAsksHistory, viewAnswers: delivery.ViewAnswersHistory} { + for name, history := range map[string]uint8{gateAsksKV: delivery.AsksHistory, tellerAnswers: delivery.AnswersHistory, tellerAsked: 1, messengerAsks: 1, assignerAsks: delivery.AssignerAsksHistory, viewAnswers: delivery.ViewAnswersHistory} { if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: name, History: history}); err != nil { t.Fatal(err) } @@ -331,6 +331,21 @@ func TestTellerCarriesAnAskAndWritesTheAnswer(t *testing.T) { // 1343/AC1, AC2 rn := startRunner(b.ctx, func(c context.Context) error { return run(c, func(k string) string { return env[k] }, logs) }) t.Cleanup(func() { rn.finish(t, 10*time.Second) }) + // Issue 774: the router's record is read on the teller's own credential from the bucket the controller grants it. + if mkv, err := b.js.KeyValue(b.ctx, messengerAsks); err != nil { + t.Fatal(err) + } else if _, err := mkv.Put(b.ctx, "mesh-delivery-teller.r1", []byte(`{"state":"open"}`)); err != nil { + t.Fatal(err) + } + tc, err := nats.Connect(b.srv.ClientURL(), nats.UserInfo("n1.mesh-delivery-teller", "pw"), nats.CustomInboxPrefix("_INBOX.n1.mesh-delivery-teller")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(tc.Close) + if rec, err := recordVia(b.ctx, tc, "mesh-delivery-teller")("r1"); err != nil || rec == nil { + t.Fatalf("the router's record read as the teller: %v, %v; want it", rec, err) + } + key, value, ga := gateAsk(t, twoHeads(), twoHeads()) kv, err := tools.KeyValue(b.ctx, gateAsksKV) if err != nil {