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
+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.