A node's record rides in its own have key, <node>.node: no bucket, writer or grant of its own, and its key's change is what readers hear (issue 1126, ADR 0555 §3)

This commit is contained in:
2026-10-26 18:29:45 +01:00
parent 5b1e990eb4
commit 2b0689f654
4 changed files with 31 additions and 55 deletions
+3 -2
View File
@@ -12,7 +12,7 @@
// - what a person decided of each node (its endpoint, site, hub, address on the private network, public domain and
// accounts): the same placement key, Placement.Decided;
// - what each node observes of itself (its tunnel key, capabilities, keys, certificate, links and networks): the
// node-engine's state `records`; the tunnel found at a node's adoption and the names of the hub's peers are decided,
// node's own have key, `<node>.node` (HaveNode.Record); the tunnel found at a node's adoption and the names of the hub's peers are decided,
// in its placement, and whether the hub took the tunnel over and who enrolled under a peer's key are read from both;
// - every module's manifest: the indexer's `modules.<module>` and its manifests by digest, or the digest the
// composing node's want names where it runs another build (Scope.Kept);
@@ -44,7 +44,8 @@ import (
type Reader interface {
// Placements is every node's placement, by node (delivery.PlacementBucket).
Placements(ctx context.Context) (map[string]delivery.Placement, error)
// Records is every node's record, by node (delivery.NodeRecordsBucket).
// Records is every node's record, by node: HaveNode.Record of each node's own have key, `<node>.node` of
// delivery.HaveBucket (ADR 0555 §3).
Records(ctx context.Context) (map[string]delivery.NodeRecord, error)
// Modules is every module the indexer holds, by module (delivery.ModulesBucket).
Modules(ctx context.Context) (map[string]delivery.Module, error)
+9 -2
View File
@@ -213,12 +213,15 @@ func parseModuleKeyPart(part string) (string, error) {
return module, nil
}
// HaveNodeWord is the last token of a node's own facts' key in HaveBucket.
const HaveNodeWord = nodeWord
// HaveNodeKey is the key, in HaveBucket, of a node's own facts: `<node>.node`.
func HaveNodeKey(node string) (string, error) {
if err := checkName("node", node); err != nil {
return "", err
}
return node + ".node", nil
return node + "." + HaveNodeWord, nil
}
// ParseHaveKey is the inverse of HaveKey and HaveNodeKey: isNode is true for `<node>.node`, with module empty.
@@ -381,6 +384,9 @@ type HaveNode struct {
Strays json.RawMessage `json:"strays,omitempty"`
Filters json.RawMessage `json:"filters,omitempty"`
Profile json.RawMessage `json:"profile,omitempty"`
// Record is what the machine observes of itself, typed, for every node and composing part to read (issue 1126,
// ADR 0555 §3): the node's record lives in its own have key, so it needs no bucket, writer or grant of its own.
Record *NodeRecord `json:"record,omitempty"`
}
// Presence is whether a node or a part's instance is online; the entry's own time says since when. The watchman
@@ -524,7 +530,8 @@ func SameHave(a, b Have) bool {
// SameHaveNode says whether two node facts are the same value, comparing each carried JSON without its spacing.
func SameHaveNode(a, b HaveNode) bool {
return a.V == b.V && a.Composed == b.Composed && sameJSON(a.Windows, b.Windows) && sameJSON(a.Busy, b.Busy) &&
sameJSON(a.Strays, b.Strays) && sameJSON(a.Filters, b.Filters) && sameJSON(a.Profile, b.Profile)
sameJSON(a.Strays, b.Strays) && sameJSON(a.Filters, b.Filters) && sameJSON(a.Profile, b.Profile) &&
(a.Record == nil) == (b.Record == nil) && (a.Record == nil || SameNodeRecord(*a.Record, *b.Record))
}
// SamePresence says whether two presences are the same value.
+6 -37
View File
@@ -3,47 +3,16 @@ package delivery
import (
"encoding/json"
"fmt"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// A node's record is what the node knows of itself, written by its own agent into the node-engine's state `records`,
// one key `record.<node>` per node, and read by every node and by the parts that compose (novox/hq ADR 0547 §2: "of
// other nodes it reads only what is public: their records"; issue 1126's prepare, L1 as narrowed by ADR 0549). Where a
// module runs, its host ports, pins, bindings and seat holdings are not in it: they are the catalogue's placement
// (placement.go). Nothing in it is secret: its keys are public halves, its accounts are login names.
//
// The agent writes it at start and whenever a fact it holds changes, only when the value differs, and says
// `node-record-changed`. Seeded from what the node runs and reports, never from the controller's store.
const (
// NodeRecordsState is the node-engine's state of node records, and NodeRecordsBucket its bucket.
NodeRecordsState = "records"
NodeRecordsBucket = "node-engine_" + NodeRecordsState
// NodeRecordsHistory is the revisions the records bucket keeps per key.
NodeRecordsHistory = 5
// NodeRecordChangedEvent is what a node's agent says when it wrote its record.
NodeRecordChangedEvent = "node-record-changed"
recordWord = "record"
)
// NodeRecordKey is the key, in NodeRecordsBucket, of one node's record: `record.<node>`.
func NodeRecordKey(node string) (string, error) {
if err := checkName("node", node); err != nil {
return "", err
}
return recordWord + "." + node, nil
}
// ParseNodeRecordKey is the inverse of NodeRecordKey; it refuses any other key, naming it.
func ParseNodeRecordKey(key string) (node string, err error) {
node, ok := strings.CutPrefix(key, recordWord+".")
if !ok || checkName("node", node) != nil {
return "", fmt.Errorf("delivery: %q is not a node record key, record.<node>", key)
}
return node, nil
}
// A node's record is what the machine observes of itself (issue 1126, ADR 0555 §3), written by its own agent in its
// own have key, `<node>.node` of the node-engine's have bucket (HaveNode.Record), whenever an observed fact changes and
// only when the value differs. The key's change is the event its readers hear: every node and the parts that compose
// watch it. Where a module runs, and every fact a person decides of the node (its endpoint, site, hub, address on the
// private network, public domain, accounts, the tunnel found at adoption), are the catalogue's placement
// (placement.go). Nothing in it is secret: its keys are public halves.
// RecordTunnel is a tunnel found on a node when it was adopted, as compose.Tunnel holds it: public keys and addresses
// only. After the node took it over the tunnel is no longer there to observe, so it is kept as what was decided at
+13 -14
View File
@@ -13,22 +13,9 @@ import (
// check (ADR 0201 §13) refuses such a field in a state every node reads.
var secretNamed = regexp.MustCompile(`(?i)(secret|passw|token|api[-_]?key|private[-_]?key|credential|bearer|cookie|salt|signing)`)
// Issue 1126 (ADR 0555): a node's record keys, reads back, refuses a node that is not a name and an empty capability, and
// Issue 1126 (ADR 0555): a node's record reads back, rides in its have key, refuses a node that is not a name and an empty capability, and
// neither it nor the placement file names a field like a secret.
func TestNodeRecord(t *testing.T) {
key, err := NodeRecordKey("ace")
if err != nil || key != "record.ace" || NodeRecordsBucket != "node-engine_records" {
t.Fatalf("NodeRecordKey = %q, %v; bucket %q", key, err, NodeRecordsBucket)
}
if n, err := ParseNodeRecordKey(key); err != nil || n != "ace" {
t.Fatalf("ParseNodeRecordKey = %q, %v", n, err)
}
for _, bad := range []string{"placement.ace", "record.", "record.a.b"} {
if _, err := ParseNodeRecordKey(bad); err == nil {
t.Errorf("ParseNodeRecordKey(%q) took it", bad)
}
}
hub := NodeRecord{Node: "novox", TunnelKey: "k", Capabilities: []string{"docker"}, SealingKey: "age1x", ServingKey: "pub",
Certificate: "cert", OutwardLinks: []string{"eth0"}, Networks: []catalogue.AttachedNetwork{{Link: "eth0", Network: "192.0.2.0/24"}},
NetworksSaid: true, Contract: 3}
@@ -40,6 +27,18 @@ func TestNodeRecord(t *testing.T) {
if err != nil || !SameNodeRecord(back, hub) || back.V != Version {
t.Fatalf("round trip: %+v, %v", back, err)
}
// It rides in the node's own have key: a have that differs only in its record is another value.
with, without := HaveNode{V: Version, Record: &hub}, HaveNode{V: Version}
if raw, err := EncodeHaveNode(with); err != nil {
t.Fatal(err)
} else if got, err := DecodeHaveNode(raw); err != nil || got.Record == nil || !SameHaveNode(got, with) || SameHaveNode(got, without) {
t.Fatalf("the have's record: %+v, %v", got, err)
}
changed := hub
changed.Contract = 4
if SameHaveNode(with, HaveNode{V: Version, Record: &changed}) {
t.Error("a changed record reads as the same have")
}
for name, r := range map[string]NodeRecord{
"empty capability": {Node: "ace", Capabilities: []string{""}},