Stamp a ledger operation with the caller the runner proved for the call in flight it names, refusing another module's call or one not in flight; serve the ledger calls on the pipe (issue 1966 T5)

This commit is contained in:
2026-11-03 13:49:15 +01:00
parent 8fe7ef3b82
commit 38a634723b
9 changed files with 290 additions and 11 deletions
@@ -52,6 +52,7 @@ func (b *subscribing) BusState(string, json.RawMessage) (json.RawMessage, error)
func (b *subscribing) BusStateWatch(string, json.RawMessage, func(json.RawMessage) error) (func(), error) {
return func() {}, nil
}
func (b *subscribing) Begin(string) (string, func()) { return "", func() {} }
func (b *subscribing) Subscribe(d func(json.RawMessage) error) error {
b.mu.Lock()
defer b.mu.Unlock()
+19 -5
View File
@@ -52,12 +52,21 @@ type Tool struct {
// stdio.CallerOf, and says Unproven when it is absent.
const MetaCaller = "mesh/caller"
// callParams is a `tools/call`'s params: the tool, its arguments as the caller sent them, and — only when the
// subject named one — the caller, in `_meta`, which the runtime writes and no caller can.
func callParams(name, caller string, args json.RawMessage) map[string]any {
// callParams is a `tools/call`'s params: the tool, its arguments as the caller sent them, and in `_meta`, which the
// runtime writes and no caller can, the caller — only when the subject named one — and the id of the call in flight
// (mesh-sdk buscalls.MetaCall), which a ledger's apply names so the runtime stamps the caller it proved (novox/hq ADR
// 0597 §7).
func callParams(name, caller, call string, args json.RawMessage) map[string]any {
params := map[string]any{"name": name, "arguments": args}
meta := map[string]any{}
if caller != "" {
params["_meta"] = map[string]any{MetaCaller: caller}
meta[MetaCaller] = caller
}
if call != "" {
meta[buscalls.MetaCall] = call
}
if len(meta) > 0 {
params["_meta"] = meta
}
return params
}
@@ -101,6 +110,9 @@ type Bus interface {
BusState(method string, params json.RawMessage) (json.RawMessage, error)
// BusStateWatch hands deliver each change of an objects or a ledger watch, the current ones first.
BusStateWatch(method string, params json.RawMessage, deliver func(change json.RawMessage) error) (stop func(), err error)
// Begin says a tool call of the module is in flight for the caller the runtime proved, and answers its id, which the
// bundle is handed in the call's `_meta`, and the end to call once the call is answered (novox/hq ADR 0597 §7).
Begin(caller string) (id string, end func())
}
// busStateCalls are the methods of mesh-sdk buscalls a bundle asks and BusState answers.
@@ -659,7 +671,9 @@ func Start(module, entry string, env []string, mesh Bus, logf func(string, ...an
if len(args) == 0 || string(args) == "null" {
args = json.RawMessage("{}")
}
res, err := asking("tools/call", callParams(full, caller, args), CallTimeout)
call, end := mesh.Begin(caller)
defer end()
res, err := asking("tools/call", callParams(full, caller, call, args), CallTimeout)
if err != nil {
return nil, err
}
+4 -2
View File
@@ -81,8 +81,10 @@ type Snapshots interface {
// Ledger is one ledger of one module: its types and where it is kept.
type Ledger struct {
Module string
Name string
Module string
Name string
// Account is the owning module's authenticated account, which every operation it applies is stamped with.
Account string
Types []catalogue.LedgerType
History History
State State
+5
View File
@@ -65,6 +65,9 @@ func (b *moduleBus) BusState(method string, params json.RawMessage) (json.RawMes
if err = decode(method, params, &r); err == nil {
answer, err = conn.WorkQueueDead(b.module, r)
}
case buscalls.LedgerApply, buscalls.LedgerGet, buscalls.LedgerHistory, buscalls.LedgerQuery, buscalls.LedgerRebuild,
buscalls.LedgerReindex, buscalls.LedgerSnapshot, buscalls.LedgerLag:
answer, err = b.ledgerCall(method, params)
default:
return nil, fmt.Errorf("the runtime answers no %s", method)
}
@@ -89,6 +92,8 @@ func (b *moduleBus) BusStateWatch(method string, params json.RawMessage, deliver
}
return deliver(raw)
})
case buscalls.LedgerWatch:
return b.ledgerWatch(params, deliver)
}
return nil, fmt.Errorf("the runtime answers no %s", method)
}
@@ -13,6 +13,7 @@ import (
type saidCaller struct {
Caller string `json:"caller"`
Call string `json:"call"`
Arguments map[string]any `json:"arguments"`
}
@@ -36,6 +37,7 @@ func TestACallOnTheCallerNamedSubjectReachesTheModuleWithItsCaller(t *testing.T)
asker.Logf = func(string, ...any) {}
defer asker.Close()
calls := map[string]bool{}
for _, key := range []string{"ledger.who", "ledger.who@anchor", "seat:node-desk.who@anchor"} {
var got saidCaller
mt.Until(t, func() error {
@@ -49,6 +51,11 @@ func TestACallOnTheCallerNamedSubjectReachesTheModuleWithItsCaller(t *testing.T)
if got.Caller != "shanks.node-tools" {
t.Errorf("%s reached the module with caller %q, want the asker's bus user", key, got.Caller)
}
// And the id of the call in flight, one per call, which a ledger's apply names (novox/hq ADR 0597 §7).
if got.Call == "" || calls[got.Call] {
t.Errorf("%s reached the module with call id %q, want one of its own", key, got.Call)
}
calls[got.Call] = true
if got.Arguments["caller"] != "novox.node-tools" {
t.Errorf("%s: the arguments did not reach the module as sent: %v", key, got.Arguments)
}
+146 -2
View File
@@ -1,9 +1,12 @@
package runtime
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"sync"
"git.novox.be/novox/mesh-sdk/go/buscalls"
"git.novox.be/novox/mesh-sdk/go/catalogue"
@@ -35,6 +38,59 @@ type ledgerSource struct {
writer func(module string) (*bus.Conn, error)
}
// callsInFlight are the tool calls the runtime is serving, each with the module whose bundle serves it and the caller
// the runtime proved from the subject the call arrived on (bus.CalledSubject, novox/hq issue 365).
type callsInFlight struct {
mu sync.Mutex
of map[string]callInFlight
}
type callInFlight struct{ module, caller string }
// begin keeps a call of module in flight for caller, under an id no bundle can guess, until end.
func (f *callsInFlight) begin(module, caller string) (string, func()) {
var raw [16]byte
_, _ = rand.Read(raw[:])
id := hex.EncodeToString(raw[:])
f.mu.Lock()
if f.of == nil {
f.of = map[string]callInFlight{}
}
f.of[id] = callInFlight{module, caller}
f.mu.Unlock()
return id, func() {
f.mu.Lock()
delete(f.of, id)
f.mu.Unlock()
}
}
// authorOf is the author an apply of module naming call is stamped with (ADR 0597 §7): the module's account, and the
// caller the runtime proved for that call; naming no call, the account alone. A call not in flight, or another
// module's, is refused, naming it. Nothing the bundle writes is taken as the author.
func (f *callsInFlight) authorOf(module, account, call string) (buscalls.Author, error) {
author := buscalls.Author{Account: account}
if call == "" {
return author, nil
}
f.mu.Lock()
inFlight, ok := f.of[call]
f.mu.Unlock()
switch {
case !ok:
return author, fmt.Errorf("the apply names the call %s, which is not a tool call in flight here; an apply names the "+
"call it serves as its tools/call's _meta %s gave it, or none (novox/hq ADR 0597 §7)", call, buscalls.MetaCall)
case inFlight.module != module:
return author, fmt.Errorf("the apply names the call %s, which is another module's tool call; %s names only its own "+
"(novox/hq ADR 0597 §7)", call, module)
}
author.Caller = inFlight.caller
return author, nil
}
// Begin keeps one of the module's tool calls in flight for the caller the runtime proved (launch.Bus).
func (b *moduleBus) Begin(caller string) (string, func()) { return b.all.calls.begin(b.module, caller) }
// ledger opens one of a module's ledgers by its local name.
func (c *consumers) ledger(module, local string) (*ledger.Ledger, error) {
b, err := c.conn.Binding(module, local, catalogue.StorageLedger)
@@ -56,8 +112,96 @@ func (c *consumers) ledger(module, local string) (*ledger.Ledger, error) {
}
}
history, state, snapshots := c.conn.LedgerStores(b, writer)
return &ledger.Ledger{Module: module, Name: local, Types: ts, History: history, State: state, Snapshots: snapshots,
Indexes: c.conn.LedgerIndexes(b), Logf: c.logf}, nil
// The owning module's authenticated account: the user of the connection its history is published on.
account := module
if writer != nil && writer.Credential().User != "" {
account = writer.Credential().User
}
return &ledger.Ledger{Module: module, Name: local, Account: account, Types: ts, History: history, State: state,
Snapshots: snapshots, Indexes: c.conn.LedgerIndexes(b), Logf: c.logf}, nil
}
// ledgerCall answers a bundle's ledger call, as the module whose bundle asks.
func (b *moduleBus) ledgerCall(method string, params json.RawMessage) (any, error) {
var named struct {
Ledger string `json:"ledger"`
}
if err := decode(method, params, &named); err != nil {
return nil, err
}
if method == buscalls.LedgerApply {
var r buscalls.ApplyRequest
if err := decode(method, params, &r); err != nil {
return nil, err
}
// The call is judged first: a forged one is refused before anything of the ledger is touched.
if _, err := b.all.calls.authorOf(b.module, "", r.Call); err != nil {
return nil, err
}
l, err := b.all.ledger(b.module, r.Ledger)
if err != nil {
return nil, err
}
author, err := b.all.calls.authorOf(b.module, l.Account, r.Call)
if err != nil {
return nil, err
}
return l.Apply(r, author)
}
l, err := b.all.ledger(b.module, named.Ledger)
if err != nil {
return nil, err
}
switch method {
case buscalls.LedgerGet:
var r buscalls.InstanceRequest
_ = json.Unmarshal(params, &r)
return l.Get(r)
case buscalls.LedgerHistory:
var r buscalls.HistoryRequest
_ = json.Unmarshal(params, &r)
return l.ReadHistory(r)
case buscalls.LedgerQuery:
var r buscalls.QueryRequest
_ = json.Unmarshal(params, &r)
return l.Query(r)
case buscalls.LedgerRebuild:
var r buscalls.RebuildRequest
_ = json.Unmarshal(params, &r)
return l.Rebuild(r)
case buscalls.LedgerReindex:
var r buscalls.ReindexRequest
_ = json.Unmarshal(params, &r)
return l.Reindex(r)
case buscalls.LedgerSnapshot:
var r buscalls.InstanceRequest
_ = json.Unmarshal(params, &r)
return l.Snapshot(r)
case buscalls.LedgerLag:
var r buscalls.LedgerRequest
_ = json.Unmarshal(params, &r)
return l.Lag(r)
}
return nil, fmt.Errorf("the runtime answers no %s", method)
}
// ledgerWatch hands a bundle each change of its ledger's states.
func (b *moduleBus) ledgerWatch(params json.RawMessage, deliver func(json.RawMessage) error) (func(), error) {
var r buscalls.LedgerWatchRequest
if err := decode(buscalls.LedgerWatch, params, &r); err != nil {
return nil, err
}
l, err := b.all.ledger(b.module, r.Ledger)
if err != nil {
return nil, err
}
return l.Watch(r, func(change buscalls.InstanceChange) error {
raw, err := json.Marshal(change)
if err != nil {
return err
}
return deliver(raw)
})
}
// hasLedger is whether a module's membership binds a ledger.
+103
View File
@@ -1,15 +1,20 @@
package runtime
import (
"context"
"encoding/json"
"errors"
"sort"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/buscalls"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/membership"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"github.com/novox/mesh-tools/node-tools/internal/bus"
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
)
@@ -72,3 +77,101 @@ func TestTheLedgerRepairsAreToolsOfEveryLedgerOwner(t *testing.T) {
})
})
}
// aLedger raises one ledger of module m on the bus as the bus module would create it, binds it in m's membership, and
// answers the runtime's buses for m and for other, its types given as a test gives them.
func aLedger(t *testing.T) (m, other *moduleBus, mesh *mt.Mesh) {
t.Helper()
mesh = mt.New(t)
nc, err := nats.Connect(mt.URL(t))
if err != nil {
t.Fatal(err)
}
t.Cleanup(nc.Close)
js, _ := jetstream.New(nc)
ctx := context.Background()
for _, cfg := range []jetstream.StreamConfig{
{Name: "LEDGER_m_records", Subjects: []string{"mesh.ledger.m.records.>"}},
{Name: "SNAPSHOT_m_records", Subjects: []string{"mesh.ledger-snapshot.m.records.>"}, MaxMsgsPerSubject: 1},
} {
if _, err := js.CreateStream(ctx, cfg); err != nil {
t.Fatal(err)
}
}
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: "m_records"}); err != nil {
t.Fatal(err)
}
mem := mt.MembershipOf("m", "anchor", false, nil)
mem.Bindings = []membership.BusBinding{{Local: "records", Storage: catalogue.StorageLedger, Template: catalogue.TemplateLedger,
History: "LEDGER_m_records", Subject: "mesh.ledger.m.records", State: "m_records", Snapshot: "mesh.ledger-snapshot.m.records"}}
mesh.Issue(t, mem)
mesh.Issue(t, mt.MembershipOf("other", "anchor", false, nil))
conn := connect(t, "node-tools", "anchor")
conn.Follow("m")
conn.Follow("other")
mt.Until(t, func() error {
if conn.Membership("m") == nil || conn.Membership("other") == nil {
return errors.New("not issued yet")
}
return nil
})
all := &consumers{conn: conn, logf: t.Logf, of: map[string]*moduleEvents{}, ledgers: ledgerSource{
types: func(string, string) ([]catalogue.LedgerType, error) {
return []catalogue.LedgerType{{Name: "decision", Schema: map[string]catalogue.LedgerField{"title": {Type: "string"}},
Operations: []catalogue.LedgerOperation{{Name: "title", Effect: catalogue.EffectSetFields, Fields: []string{"title"}, Author: true}}}}, nil
},
writer: func(string) (*bus.Conn, error) { return conn, nil },
}}
return all.forModule("m").(*moduleBus), all.forModule("other").(*moduleBus), mesh
}
// 1966/AC17, 1966/AC18: the author an apply is stamped with is the caller the runtime proved for the call in flight it
// names, whatever author the bundle wrote; another module's call, or one not in flight, is refused naming it (ADR 0597 §7).
func TestAnApplyIsStampedWithTheCallerTheRuntimeProved(t *testing.T) {
m, other, _ := aLedger(t)
call, end := m.Begin("op@shanks")
defer end()
theirs, endTheirs := other.Begin("op@shanks")
defer endTheirs()
forged := `"body":{"title":"T","author":{"account":"x","caller":"forged"}}`
for _, c := range []struct{ name, params, caller, refusal string }{
{"1966/AC17 a call in flight stamps its proven caller", `{"ledger":"records","instance":"decision/1","operation":"title",` + forged + `,"call":"` + call + `","author":"forged"}`, "op@shanks", ""},
{"naming no call stamps the account alone", `{"ledger":"records","instance":"decision/2","operation":"title",` + forged + `}`, "", ""},
{"1966/AC18 another module's call is refused, naming it", `{"ledger":"records","instance":"decision/3","operation":"title",` + forged + `,"call":"` + theirs + `"}`, "", "the call " + theirs + ", which is another module's"},
{"a call not in flight is refused, naming it", `{"ledger":"records","instance":"decision/3","operation":"title",` + forged + `,"call":"c0ffee"}`, "", "the call c0ffee, which is not a tool call in flight"},
} {
t.Run(c.name, func(t *testing.T) {
var req struct {
Instance string `json:"instance"`
}
_ = json.Unmarshal([]byte(c.params), &req)
_, err := m.BusState(buscalls.LedgerApply, json.RawMessage(c.params))
if c.refusal != "" {
if err == nil || !strings.Contains(err.Error(), c.refusal) {
t.Fatalf("applied with %v, want refused with %q", err, c.refusal)
}
return
}
if err != nil {
t.Fatal(err)
}
raw, err := m.BusState(buscalls.LedgerHistory, json.RawMessage(`{"ledger":"records","instance":"`+req.Instance+`"}`))
if err != nil {
t.Fatal(err)
}
var page buscalls.HistoryPage
_ = json.Unmarshal(raw, &page)
if len(page.Operations) != 1 || page.Operations[0].Author != (buscalls.Author{Account: "m", Caller: c.caller}) {
t.Fatalf("the history holds %+v, want the author {m %s}", page.Operations, c.caller)
}
state, _ := m.BusState(buscalls.LedgerGet, json.RawMessage(`{"ledger":"records","instance":"`+req.Instance+`"}`))
if !strings.Contains(string(state), `"author":{"account":"m"`) || strings.Contains(string(state), "forged") {
t.Fatalf("the state took the bundle's author: %s", state)
}
})
}
end()
if _, err := m.BusState(buscalls.LedgerApply, json.RawMessage(`{"ledger":"records","instance":"decision/4","operation":"title","call":"`+call+`"}`)); err == nil {
t.Fatal("a call that ended was still taken as in flight")
}
}
+2
View File
@@ -744,6 +744,8 @@ type consumers struct {
provisioned provisionWatches
// ledgers is how a module's ledger is opened (ledgers.go).
ledgers ledgerSource
// calls are the tool calls in flight, by the id each bundle was handed (ledgers.go, novox/hq ADR 0597 §7).
calls callsInFlight
}
type moduleEvents struct {