From 78e5de538b7253287a1b60a3ad1c83b3c3ad1253 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 12:06:40 +0100 Subject: [PATCH] T-6: the tool runner reads a sent object by its reference, under the bus state capability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mesh/objects.sent-get reads a sender's sent store only; refused: a state's objects, a reference that is none, an object over a bundle's channel. Builds: design 47 G §347, §59, §462 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- node-tools/internal/bus/sent.go | 55 ++++++++++++++++++++ node-tools/internal/bus/sent_test.go | 30 +++++++++++ node-tools/internal/runtime/announce_test.go | 14 +++++ node-tools/internal/runtime/busstate.go | 10 +++- 4 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 node-tools/internal/bus/sent.go create mode 100644 node-tools/internal/bus/sent_test.go diff --git a/node-tools/internal/bus/sent.go b/node-tools/internal/bus/sent.go new file mode 100644 index 0000000..7cd2406 --- /dev/null +++ b/node-tools/internal/bus/sent.go @@ -0,0 +1,55 @@ +package bus + +import ( + "context" + "errors" + "fmt" + "strings" + + "git.novox.be/novox/mesh-sdk/go/buscalls" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/stateobjects" + "github.com/nats-io/nats.go/jetstream" +) + +// SentGet reads one object of a sender's store of sent objects by the reference that carried it (ADR 0666 §16): the +// store is the sender's `_sent-objects`, and no other — a state's objects are reached through their owner's +// bindings alone. It reads as this connection's account, which the bus lets read a sender's sent store only where the +// grants part granted it, from the manifests' callers and consumers; a refusal says the store. +func (c *Conn) SentGet(r buscalls.ObjectsSentGetRequest) (*buscalls.ObjectData, error) { + ref := stateobjects.Ref{Store: r.Store, Object: r.Object, Size: r.Size, Digest: r.Digest, Form: stateobjects.FormVersion} + if _, ok := stateobjects.ReadRef(stateobjects.Wrap(ref)); !ok { + return nil, errors.New("a sent object is named by the reference's store, object, size and sha256, as the reply or event carried them") + } + if !strings.HasSuffix(r.Store, catalogue.BusNameSeparator+catalogue.SentObjectsLocal) { + return nil, fmt.Errorf("%s is no sent store (a module's is `%s%s`): only a sent object is read by its reference", + r.Store, catalogue.BusNameSeparator, catalogue.SentObjectsLocal) + } + if r.Size > stateobjects.BundleObjectMost { + return nil, fmt.Errorf("the sent object %s is %d bytes and a bundle's runtime channel carries at most %d: it is not read "+ + "through a bundle", r.Object, r.Size, stateobjects.BundleObjectMost) + } + js, err := c.jetStream() + if err != nil { + return nil, err + } + ctx, cancel := context.WithTimeout(context.Background(), StateTimeout) + defer cancel() + obs, err := js.ObjectStore(ctx, r.Store) + if err != nil { + return nil, fmt.Errorf("the sent store %s cannot be read as %s: it is not on the bus, or the bus grants this account no "+ + "read of it (the manifests' callers and consumers are granted): %w", r.Store, c.cred.Module, err) + } + info, err := obs.GetInfo(ctx, r.Object) + if errors.Is(err, jetstream.ErrObjectNotFound) { + return nil, fmt.Errorf("the sent object %s is not in %s: it is kept 7 days, then expires", r.Object, r.Store) + } + if err != nil { + return nil, fmt.Errorf("the sent object %s of %s: %w", r.Object, r.Store, err) + } + data, err := obs.GetBytes(ctx, r.Object) + if err != nil { + return nil, fmt.Errorf("the sent object %s of %s: %w", r.Object, r.Store, err) + } + return &buscalls.ObjectData{Info: objectInfoOf(info), Data: data}, nil +} diff --git a/node-tools/internal/bus/sent_test.go b/node-tools/internal/bus/sent_test.go new file mode 100644 index 0000000..26ce44d --- /dev/null +++ b/node-tools/internal/bus/sent_test.go @@ -0,0 +1,30 @@ +package bus + +import ( + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/buscalls" + "git.novox.be/novox/mesh-sdk/go/stateobjects" +) + +// Design 47 G §347, §59, §462: a sent object is read by the reference that carried it, from a sender's sent store only; a +// state's objects, a reference that is none and an object too large for a bundle's channel are refused before the bus is asked. +func TestSentGetRefusesWhatIsNoSentObjectsReference(t *testing.T) { + digest := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + big := int64(stateobjects.BundleObjectMost) + 1 + for _, c := range []struct { + name string + req buscalls.ObjectsSentGetRequest + want string + }{ + {"no digest", buscalls.ObjectsSentGetRequest{Store: "m_sent-objects", Object: "o", Size: 3}, "named by the reference"}, + {"a state's objects", buscalls.ObjectsSentGetRequest{Store: "m_asks-objects", Object: "o", Size: 3, Digest: digest}, "no sent store"}, + {"over the channel", buscalls.ObjectsSentGetRequest{Store: "m_sent-objects", Object: "o", Size: big, Digest: digest}, "not read through a bundle"}, + } { + _, err := (&Conn{}).SentGet(c.req) + if err == nil || !strings.Contains(err.Error(), c.want) { + t.Errorf("%s: got %v, want an error containing %q", c.name, err, c.want) + } + } +} diff --git a/node-tools/internal/runtime/announce_test.go b/node-tools/internal/runtime/announce_test.go index 6965fea..441c81e 100644 --- a/node-tools/internal/runtime/announce_test.go +++ b/node-tools/internal/runtime/announce_test.go @@ -2,6 +2,7 @@ package runtime import ( "encoding/json" + "git.novox.be/novox/mesh-sdk/go/buscalls" "sort" "strings" "testing" @@ -143,3 +144,16 @@ func TestTheRunnerStatesTheCapabilitiesOfItsCallsInItsPing(t *testing.T) { t.Errorf("the runner states %q (%v), want its bus-state and ledger calls", ping.Metadata[announce.Capabilities], err) } } + +// Design 47 G §462: the call that reads a sent object is listed under the bus state capability, and the ledger calls keep theirs. +func TestTheSentObjectCallBelongsToTheBusStateCapability(t *testing.T) { + for c, method := range map[string]string{CapBusStateCalls: buscalls.ObjectsSentGet} { + found := false + for _, m := range callFamilies[c] { + found = found || m == method + } + if !found { + t.Errorf("%s does not list %s", c, method) + } + } +} diff --git a/node-tools/internal/runtime/busstate.go b/node-tools/internal/runtime/busstate.go index 2c67519..3d55731 100644 --- a/node-tools/internal/runtime/busstate.go +++ b/node-tools/internal/runtime/busstate.go @@ -21,7 +21,7 @@ const ( // callFamilies are the methods each capability stands for, the ones BusState and BusStateWatch answer. var callFamilies = map[string][]string{ CapBusStateCalls: {buscalls.ObjectsPut, buscalls.ObjectsGet, buscalls.ObjectsList, buscalls.ObjectsDelete, buscalls.ObjectsInfo, - buscalls.ObjectsWatch, buscalls.StreamRead, buscalls.StreamFill, buscalls.WorkQueueSize, buscalls.WorkQueueDead}, + buscalls.ObjectsWatch, buscalls.ObjectsSentGet, buscalls.StreamRead, buscalls.StreamFill, buscalls.WorkQueueSize, buscalls.WorkQueueDead}, CapLedgerCalls: {buscalls.LedgerApply, buscalls.LedgerGet, buscalls.LedgerHistory, buscalls.LedgerWatch, buscalls.LedgerQuery, buscalls.LedgerRebuild, buscalls.LedgerReindex, buscalls.LedgerSnapshot, buscalls.LedgerLag}, } @@ -65,6 +65,14 @@ func (b *moduleBus) BusState(method string, params json.RawMessage) (json.RawMes err = conn.ObjectsDelete(b.module, r) answer = buscalls.Done{} } + case buscalls.ObjectsSentGet: + var r buscalls.ObjectsSentGetRequest + if err = decode(method, params, &r); err == nil { + var got *buscalls.ObjectData + if got, err = conn.SentGet(r); err == nil { + answer = got + } + } case buscalls.ObjectsList: var r buscalls.ObjectsRequest if err = decode(method, params, &r); err == nil {