AM-D3 path: the assignment half carries each node's readings with the runner's fingerprint

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
This commit is contained in:
2026-11-12 06:08:16 +01:00
parent e163aab3d1
commit 5125feabe6
8 changed files with 66 additions and 16 deletions
@@ -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
}
@@ -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}}}
@@ -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
})
}
@@ -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.
@@ -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},
@@ -31,7 +31,12 @@
"capabilities": [
{
"node": "n1",
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"readings": [
"approval-mark/1",
"bus-state-calls"
],
"fingerprint": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
}
],
"cause": "catalog-revision",
@@ -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": [
@@ -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 */