From 5a9e997349b42c3a7bd0a3a11394ef7798e3c63a Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 15:15:58 +0100 Subject: [PATCH] Check a ledger operation against the types its binding carries, refusing a ledger bound with none (issue 1966 T2) --- .../runtime/ledger_credentials_test.go | 9 ++---- .../runtime/ledger_integration_test.go | 14 ++++----- node-tools/internal/runtime/ledgers.go | 29 +++++-------------- node-tools/internal/runtime/ledgers_test.go | 13 +++++---- node-tools/internal/runtime/runtime.go | 5 ---- 5 files changed, 25 insertions(+), 45 deletions(-) diff --git a/node-tools/internal/runtime/ledger_credentials_test.go b/node-tools/internal/runtime/ledger_credentials_test.go index a517f7c..0a801d9 100644 --- a/node-tools/internal/runtime/ledger_credentials_test.go +++ b/node-tools/internal/runtime/ledger_credentials_test.go @@ -67,7 +67,9 @@ func TestALedgerIsWrittenOnlyOnItsModulesLedgerConnection(t *testing.T) { } 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"}} + History: "LEDGER_m_records", Subject: "mesh.ledger.m.records", State: "m_records", Snapshot: "mesh.ledger-snapshot.m.records", + Types: []catalogue.LedgerType{{Name: "decision", Operations: []catalogue.LedgerOperation{ + {Name: "title", Effect: catalogue.EffectSetFields, Fields: []string{"title"}}}}}}} raw, _ := json.Marshal(mem) var sdk membership.Membership _ = json.Unmarshal(raw, &sdk) @@ -96,12 +98,7 @@ func TestALedgerIsWrittenOnlyOnItsModulesLedgerConnection(t *testing.T) { } creds := FollowLedgerCredentials(file, bus.Dial, t.Logf) t.Cleanup(creds.Stop) - types := func(string, string) ([]catalogue.LedgerType, error) { - return []catalogue.LedgerType{{Name: "decision", Operations: []catalogue.LedgerOperation{ - {Name: "title", Effect: catalogue.EffectSetFields, Fields: []string{"title"}}}}}, nil - } all := &consumers{conn: conn, logf: t.Logf, of: map[string]*moduleEvents{}} - WithLedgerTypes(types)(all) WithLedgerWriters(creds.Writer)(all) m := all.forModule("m").(*moduleBus) apply := func(instance string) error { diff --git a/node-tools/internal/runtime/ledger_integration_test.go b/node-tools/internal/runtime/ledger_integration_test.go index 11d8153..c58e304 100644 --- a/node-tools/internal/runtime/ledger_integration_test.go +++ b/node-tools/internal/runtime/ledger_integration_test.go @@ -78,10 +78,17 @@ func TestTheLedgerKeepsItsPromisesOnTheBus(t *testing.T) { t.Fatal(err) } } + types := []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}, + {Name: "note", Effect: catalogue.EffectAppend, Into: "notes", Fields: []string{"says"}}, + }, + Indexes: []catalogue.LedgerIndex{{Name: "by-title", On: []string{"title"}}}}} binding := membership.BusBinding{Local: "records", Storage: catalogue.StorageLedger, Template: catalogue.TemplateLedger, History: "LEDGER_m_records", Subject: "mesh.ledger.m.records", State: "m_records", Indexes: map[string]string{"by-title": "m_records_by_title"}, Snapshot: "mesh.ledger-snapshot.m.records"} mem := mt.MembershipOf("m", "anchor", false, nil) + binding.Types = types mem.Bindings = []membership.BusBinding{binding} mesh.Issue(t, mem) conn := connect(t, "node-tools", "anchor") @@ -92,14 +99,7 @@ func TestTheLedgerKeepsItsPromisesOnTheBus(t *testing.T) { } return nil }) - types := []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}, - {Name: "note", Effect: catalogue.EffectAppend, Into: "notes", Fields: []string{"says"}}, - }, - Indexes: []catalogue.LedgerIndex{{Name: "by-title", On: []string{"title"}}}}} all := &consumers{conn: conn, logf: t.Logf, of: map[string]*moduleEvents{}} - WithLedgerTypes(func(string, string) ([]catalogue.LedgerType, error) { return types, nil })(all) WithLedgerWriters(func(string) (*bus.Conn, error) { return conn, nil })(all) m := all.forModule("m").(*moduleBus) diff --git a/node-tools/internal/runtime/ledgers.go b/node-tools/internal/runtime/ledgers.go index 7c15a1b..b3d62dc 100644 --- a/node-tools/internal/runtime/ledgers.go +++ b/node-tools/internal/runtime/ledgers.go @@ -20,21 +20,9 @@ import ( // for the module whose bundle or whose repair tool asks, from its own bindings alone, with its types and the connection // its history is published on. -// LedgerTypes is where the runner reads a ledger's types, by module and local name. Where it reads them from on the -// live mesh is not decided (issue 1966: the binding carries no types, and no record names a source); until it is, -// the runner refuses to open a ledger and says why, and a test gives the types. -type LedgerTypes func(module, local string) ([]catalogue.LedgerType, error) - -// errTypesUndecided is the refusal of a ledger whose types the runner has no source for. -func errTypesUndecided(module, local string) error { - return fmt.Errorf("the tool runner cannot open %s's ledger %q: it reads a ledger's types from nowhere yet, since its "+ - "binding carries none and no record names their source (issue 1966)", module, local) -} - -// ledgerSource is how the runner opens a module's ledger: its types, and the connection its history and snapshots are -// published on (the module's ledger credential, ADR 0597 §9). +// ledgerSource is how the runner opens a module's ledger beside its binding: the connection its history and snapshots +// are published on (the module's ledger credential, ADR 0597 §9). type ledgerSource struct { - types LedgerTypes writer func(module string) (*bus.Conn, error) } @@ -97,13 +85,12 @@ func (c *consumers) ledger(module, local string) (*ledger.Ledger, error) { if err != nil { return nil, err } - types := c.ledgers.types - if types == nil { - return nil, errTypesUndecided(module, local) - } - ts, err := types(module, local) - if err != nil { - return nil, err + // Its types travel on its binding, copied by the grants part from the entry the manifest-accepted event carries + // (mesh-sdk membership.BusBinding.Types): the runner reads no other part's state for them (ADR 0596). + ts := b.Types + if len(ts) == 0 { + return nil, fmt.Errorf("%s's ledger %q is bound with no types, so no operation can be checked against one: its "+ + "binding carries the types its manifest asks for in `requires` (novox/hq ADR 0597 §1)", module, local) } var writer *bus.Conn if c.ledgers.writer != nil { diff --git a/node-tools/internal/runtime/ledgers_test.go b/node-tools/internal/runtime/ledgers_test.go index 789cece..8b17dbf 100644 --- a/node-tools/internal/runtime/ledgers_test.go +++ b/node-tools/internal/runtime/ledgers_test.go @@ -79,7 +79,7 @@ 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. +// answers the runtime's buses for m and for other; its types travel on its binding, and a second binding carries none. func aLedger(t *testing.T) (m, other *moduleBus, mesh *mt.Mesh) { t.Helper() mesh = mt.New(t) @@ -103,7 +103,11 @@ func aLedger(t *testing.T) (m, other *moduleBus, mesh *mt.Mesh) { } 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"}} + History: "LEDGER_m_records", Subject: "mesh.ledger.m.records", State: "m_records", Snapshot: "mesh.ledger-snapshot.m.records", + Types: []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}}}}}, + {Local: "untyped", 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") @@ -116,10 +120,6 @@ func aLedger(t *testing.T) (m, other *moduleBus, mesh *mt.Mesh) { 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 @@ -138,6 +138,7 @@ func TestAnApplyIsStampedWithTheCallerTheRuntimeProved(t *testing.T) { {"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 ledger bound with no types is refused, naming it", `{"ledger":"untyped","instance":"decision/3","operation":"title",` + forged + `}`, "", `m's ledger "untyped" is bound with no types`}, {"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) { diff --git a/node-tools/internal/runtime/runtime.go b/node-tools/internal/runtime/runtime.go index 7e4756e..cd76059 100644 --- a/node-tools/internal/runtime/runtime.go +++ b/node-tools/internal/runtime/runtime.go @@ -242,11 +242,6 @@ func WithLedgerWriters(writer func(module string) (*bus.Conn, error)) Option { return func(c *consumers) { c.ledgers.writer = writer } } -// WithLedgerTypes gives the runtime where it reads a ledger's types (ledgers.go). -func WithLedgerTypes(types LedgerTypes) Option { - return func(c *consumers) { c.ledgers.types = types } -} - // Run launches, binds and serves, and serves each module's bundle again when it appears, changes or // goes, with no restart. It answers a stop function. func Run(conn *bus.Conn, served []Served, envs map[string]map[string]string, logf func(string, ...any), opts ...Option) (func(), error) {