From 5125feabe6d5762f421f356fb01b13d6def4a5cf Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 06:08:16 +0100 Subject: [PATCH] AM-D3 path: the assignment half carries each node's readings with the runner's fingerprint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The grants part reads a node's capability readings and the runner that announced them from the assignment half; the assigner reads both from the node record it already holds, the digest over all of it. Builds: design 46 7 §6 (the assignment half carries, for each node, its capability readings, each with the fingerprint of the runner that announced it), design 47 G §194, lead answer A-203 (readings, fingerprint) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- .../cmd/mesh-delivery-assigner/graphkept.go | 31 +++++++++++++------ .../mesh-delivery-assigner/graphkept_test.go | 10 ++++++ .../internal/assigngraph/compute.go | 5 ++- .../go/seats/meshwants/contract.go | 8 +++-- .../go/seats/meshwants/contract_test.go | 2 +- .../seats/mesh-wants/fixtures/verbs.json | 7 ++++- .../seats/mesh-wants/seat.json | 13 +++++++- .../ts/seats/meshwants/index.d.ts | 6 +++- 8 files changed, 66 insertions(+), 16 deletions(-) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept.go index c013f018..d05f9bcd 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept.go @@ -79,23 +79,36 @@ func (p *passer) keepGraph(ctx context.Context) { } } -// capabilityDigests is the digest of each node's capability reading, the statement its engine made, by node. +// capabilityDigests is each node's capability reading, by node: the statement its engine made of what it reads, and the +// capabilities its tool runner states with the fingerprint of the runner that announced them, as the node's record carries +// them (design 46 approval mark §6, lead answer A-203), the digest over all of it. func (p *passer) capabilityDigests() []meshwants.AssignmentRevisionCapabilitiesItem { - if p.reads == nil { - return nil - } + records := p.w.records() known := p.w.knownNodes() slices.Sort(known) var out []meshwants.AssignmentRevisionCapabilitiesItem for _, node := range known { - if !p.reads.Known(node) { + read := p.reads != nil && p.reads.Known(node) + rec, recorded := records[node] + if !read && !recorded { continue } - reads := slices.Clone(p.reads.Reads([]string{node})[node]) - slices.Sort(reads) - raw, _ := json.Marshal(reads) + var reads []string + if read { + reads = slices.Clone(p.reads.Reads([]string{node})[node]) + slices.Sort(reads) + } + item := meshwants.AssignmentRevisionCapabilitiesItem{Node: node, Readings: slices.Clone(rec.RunnerCapabilities), + Fingerprint: rec.RunnerFingerprint} + slices.Sort(item.Readings) + raw, _ := json.Marshal(struct { + Reads []string `json:"reads"` + Readings []string `json:"readings"` + Fingerprint string `json:"fingerprint"` + }{reads, item.Readings, item.Fingerprint}) sum := sha256.Sum256(raw) - out = append(out, meshwants.AssignmentRevisionCapabilitiesItem{Node: node, Digest: "sha256:" + hex.EncodeToString(sum[:])}) + item.Digest = "sha256:" + hex.EncodeToString(sum[:]) + out = append(out, item) } return out } diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept_test.go index ad7637be..e6c69ecf 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept_test.go @@ -3,10 +3,12 @@ package main import ( "context" "encoding/json" + "slices" "strings" "testing" "time" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" mcc "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient" mwc "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshwantsclient" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" @@ -37,6 +39,14 @@ func TestTheWantsAreSaidWantedFromTheKeptAssignmentGraphRevision(t *testing.T) { return nil } placeOn(t, p.w, "ace", "web") + // Design 46 approval mark §6: a node's reading keeps the fingerprint of the runner that announced it, as its record says. + rec, _ := json.Marshal(nodeengine.HaveByNodeNodeValue{V: 1, Record: &nodeengine.NodeRecord{V: 1, Node: "ace", + RunnerCapabilities: []string{"bus-state-calls", "approval-mark/1"}, RunnerFingerprint: "sha256:runner"}}) + p.w.put(buckets.Have, "ace.node", rec) + if c := p.capabilityDigests(); len(c) != 1 || c[0].Node != "ace" || c[0].Fingerprint != "sha256:runner" || + !slices.Equal(c[0].Readings, []string{"approval-mark/1", "bus-state-calls"}) || !strings.HasPrefix(c[0].Digest, "sha256:") { + t.Errorf("the capability readings are %+v", c) + } half := mcc.Half{Revision: meshcatalog.CatalogRevision{Branch: trunkBranch, Kind: "trunk", Revision: 4, Source: 9, Entries: []meshcatalog.EntryRef{{Module: "web", Digest: "sha256:" + strings.Repeat("a", 64)}}}, Entries: map[string]meshcatalog.CatalogEntry{"web": {Module: "web", Fp: fpTest}}} diff --git a/modules/mesh-delivery-assigner/internal/assigngraph/compute.go b/modules/mesh-delivery-assigner/internal/assigngraph/compute.go index 2825d6d5..2884df92 100644 --- a/modules/mesh-delivery-assigner/internal/assigngraph/compute.go +++ b/modules/mesh-delivery-assigner/internal/assigngraph/compute.go @@ -202,5 +202,8 @@ func EntryDigest(e meshwants.AssignmentEntry) string { // Same says two revisions list the same entries computed from the same inputs, so the second is no new revision. func Same(a, b meshwants.AssignmentRevision) bool { return slices.Equal(a.Entries, b.Entries) && slices.Equal(a.Catalogs, b.Catalogs) && a.Placements == b.Placements && - slices.Equal(a.Capabilities, b.Capabilities) + slices.EqualFunc(a.Capabilities, b.Capabilities, func(x, y meshwants.AssignmentRevisionCapabilitiesItem) bool { + // A reading's digest is over all of it: its readings and its runner's fingerprint among it. + return x.Node == y.Node && x.Digest == y.Digest + }) } diff --git a/modules/mesh-delivery-contracts/go/seats/meshwants/contract.go b/modules/mesh-delivery-contracts/go/seats/meshwants/contract.go index 23b263fc..21a2276b 100644 --- a/modules/mesh-delivery-contracts/go/seats/meshwants/contract.go +++ b/modules/mesh-delivery-contracts/go/seats/meshwants/contract.go @@ -481,9 +481,13 @@ type AssignmentEntry struct { // AssignmentRevisionCapabilitiesItem is a type of the contract. type AssignmentRevisionCapabilitiesItem struct { - // the digest of the node's capability reading + // the digest of the node's capability reading: the engine's manifest reads, its tool runner's readings and that runner's fingerprint Digest string `json:"digest"` - Node string `json:"node"` + // the fingerprint of the tool runner that announced the readings; empty when none is known + Fingerprint string `json:"fingerprint,omitempty"` + Node string `json:"node"` + // the capabilities the node's tool runner states of the calls it answers, as the node's record carries them (design 46 approval mark 6) + Readings []string `json:"readings,omitempty"` } // AssignmentRevision is an assignment graph revision: the list of its entries' digests and the inputs it was computed from, so it can be computed again and compared. diff --git a/modules/mesh-delivery-contracts/go/seats/meshwants/contract_test.go b/modules/mesh-delivery-contracts/go/seats/meshwants/contract_test.go index 32f26b23..21bafdcd 100644 --- a/modules/mesh-delivery-contracts/go/seats/meshwants/contract_test.go +++ b/modules/mesh-delivery-contracts/go/seats/meshwants/contract_test.go @@ -117,7 +117,7 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v RevisionInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "revision asks a candidate's", subject: "", given: "{\"group\":\"feat/47-G\",\"heads\":\"0123456789ab\"}", wire: "{\"group\":\"feat/47-G\",\"heads\":\"0123456789ab\"}", decode: func(b []byte) (any, error) { var v RevisionInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "revision answers one revision naming its inputs", subject: "", given: "{\"revision\":{\"at\":\"2026-11-09T19:00:00Z\",\"capabilities\":[{\"digest\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"node\":\"n1\"}],\"catalogs\":[{\"branch\":\"main\",\"revision\":12,\"source\":3589}],\"cause\":\"catalog-revision\",\"entries\":[{\"digest\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"module\":\"xclip\",\"node\":\"n1\"}],\"kind\":\"trunk\",\"placements\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"revision\":40,\"v\":1}}", wire: "{\"revision\":{\"at\":\"2026-11-09T19:00:00Z\",\"capabilities\":[{\"digest\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"node\":\"n1\"}],\"catalogs\":[{\"branch\":\"main\",\"revision\":12,\"source\":3589}],\"cause\":\"catalog-revision\",\"entries\":[{\"digest\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"module\":\"xclip\",\"node\":\"n1\"}],\"kind\":\"trunk\",\"placements\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"revision\":40,\"v\":1}}", + {name: "revision answers one revision naming its inputs", subject: "", given: "{\"revision\":{\"at\":\"2026-11-09T19:00:00Z\",\"capabilities\":[{\"digest\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"fingerprint\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"node\":\"n1\",\"readings\":[\"approval-mark/1\",\"bus-state-calls\"]}],\"catalogs\":[{\"branch\":\"main\",\"revision\":12,\"source\":3589}],\"cause\":\"catalog-revision\",\"entries\":[{\"digest\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"module\":\"xclip\",\"node\":\"n1\"}],\"kind\":\"trunk\",\"placements\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"revision\":40,\"v\":1}}", wire: "{\"revision\":{\"at\":\"2026-11-09T19:00:00Z\",\"capabilities\":[{\"digest\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"fingerprint\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"node\":\"n1\",\"readings\":[\"approval-mark/1\",\"bus-state-calls\"]}],\"catalogs\":[{\"branch\":\"main\",\"revision\":12,\"source\":3589}],\"cause\":\"catalog-revision\",\"entries\":[{\"digest\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"module\":\"xclip\",\"node\":\"n1\"}],\"kind\":\"trunk\",\"placements\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"revision\":40,\"v\":1}}", decode: func(b []byte) (any, error) { var v RevisionAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "entry asks by node and module in a revision", subject: "", given: "{\"module\":\"xclip\",\"node\":\"n1\",\"revision\":40}", wire: "{\"module\":\"xclip\",\"node\":\"n1\",\"revision\":40}", decode: func(b []byte) (any, error) { var v EntryInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, diff --git a/modules/mesh-delivery-contracts/seats/mesh-wants/fixtures/verbs.json b/modules/mesh-delivery-contracts/seats/mesh-wants/fixtures/verbs.json index 3a50a8c5..becd27ca 100644 --- a/modules/mesh-delivery-contracts/seats/mesh-wants/fixtures/verbs.json +++ b/modules/mesh-delivery-contracts/seats/mesh-wants/fixtures/verbs.json @@ -31,7 +31,12 @@ "capabilities": [ { "node": "n1", - "digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + "digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "readings": [ + "approval-mark/1", + "bus-state-calls" + ], + "fingerprint": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ], "cause": "catalog-revision", diff --git a/modules/mesh-delivery-contracts/seats/mesh-wants/seat.json b/modules/mesh-delivery-contracts/seats/mesh-wants/seat.json index e0564718..827ae6a6 100644 --- a/modules/mesh-delivery-contracts/seats/mesh-wants/seat.json +++ b/modules/mesh-delivery-contracts/seats/mesh-wants/seat.json @@ -899,7 +899,18 @@ }, "digest": { "type": "string", - "description": "the digest of the node's capability reading" + "description": "the digest of the node's capability reading: the engine's manifest reads, its tool runner's readings and that runner's fingerprint" + }, + "readings": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the capabilities the node's tool runner states of the calls it answers, as the node's record carries them (design 46 approval mark 6)" + }, + "fingerprint": { + "type": "string", + "description": "the fingerprint of the tool runner that announced the readings; empty when none is known" } }, "required": [ diff --git a/modules/mesh-delivery-contracts/ts/seats/meshwants/index.d.ts b/modules/mesh-delivery-contracts/ts/seats/meshwants/index.d.ts index ee5a58f3..f5de211d 100644 --- a/modules/mesh-delivery-contracts/ts/seats/meshwants/index.d.ts +++ b/modules/mesh-delivery-contracts/ts/seats/meshwants/index.d.ts @@ -150,9 +150,13 @@ export interface AssignmentEntry { } export interface AssignmentRevisionCapabilitiesItem { - /** the digest of the node's capability reading */ + /** the digest of the node's capability reading: the engine's manifest reads, its tool runner's readings and that runner's fingerprint */ "digest": string; + /** the fingerprint of the tool runner that announced the readings; empty when none is known */ + "fingerprint"?: string; "node": string; + /** the capabilities the node's tool runner states of the calls it answers, as the node's record carries them (design 46 approval mark 6) */ + "readings"?: string[]; } /** an assignment graph revision: the list of its entries' digests and the inputs it was computed from, so it can be computed again and compared */