Files
mesh-host/internal/link/record.go
T
jschoubben 8734d5ee1a AM-D3 path: the node-engine takes the runner's reading and fingerprint into the node's record
The grants part decides a node's withheld subjects from the runner's own
reading tied to the runner that stated it; the merge into capabilities stays
for placement.

Builds: design 46 7 §6 (its node's node-engine takes both into the node's record), lead answer A-203 (runner-capabilities, runner-fingerprint)

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 06:06:16 +01:00

163 lines
6.3 KiB
Go

package link
import (
"encoding/json"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
sdkkeys "git.novox.be/novox/mesh-sdk/go/keys"
"slices"
"sort"
"strings"
"sync"
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// The node's record (novox/hq ADR 0555 §3, issue 1126): what the machine observes of itself, written in its own have key
// `<node>.node` (nodeengine.HaveByNodeNodeValue.Record) with its facts, so every node and the parts that compose read it from the bus
// and hear its change as the key's. Each value is the one the controller took from this node's enrolment and reports
// until now (its profile's present capabilities and networks, its outward links, its health statement's contract, and
// the public halves of the keys it enrolled with), read here from the same sources, never from the controller.
// RecordKeys are the public halves the node holds and the certificate the mesh issued for its internal name, as read
// from its state directory at the time of a pass: the certificate arrives after enrolment, a rekey changes the tunnel key.
type RecordKeys struct {
TunnelKey string
SealingKey string
ServingKey string
Certificate string
// Authority is the mesh's own certificate as this machine last applied it (public).
Authority string
// Trusts is what this machine trusts a mesh signature from now (delivery.Trusts, ADR 0550 §2).
Trusts []string
// Stated is the capabilities its tool runner states of the calls it answers (CapabilitiesKey), which the record says
// beside the machine's own: a module whose manifest asks for one is held off a machine whose runner does not state it.
Stated []string
// StatedBy is the fingerprint of the tool runner that stated Stated, as its PING carries it (FingerprintKey): the record
// takes both, so a reading is tied to the runner that made it (design 46 approval mark §6, lead answer A-203).
StatedBy string
}
// buildReads are the words and forms this engine reads of a manifest (novox/hq ADR 0614 §4): the reader capabilities of
// the mesh-sdk it is built with, its seats left out. Computed once: they are the build's, and never change while it runs.
// CapabilityWantParts is the capability a node-engine states in its node record when it reads the want's version, parts and
// kept-on (design 47 G §525); the assigner writes them into a node's want only while the record states it.
const CapabilityWantParts = "want-parts"
var buildReads = sync.OnceValue(func() []string {
return slices.DeleteFunc(catalogue.ManifestReads(), func(r string) bool { return strings.HasPrefix(r, catalogue.ReadSeat) })
})
// readsNow is what this engine reads of a manifest now: its build's words and forms, and the seats of the seat list it
// holds now (catalogue.Seats, which the engine's reader of the list gives catalogue: bootstrapped or read; ADR 0636).
func readsNow() []string {
set := map[string]bool{}
for _, r := range buildReads() {
set[r] = true
}
for _, seat := range catalogue.Seats() {
set[catalogue.ReadSeat+seat.Name] = true
}
out := make([]string, 0, len(set))
for r := range set {
out = append(out, r)
}
sort.Strings(out)
return out
}
// readsStatement is what this engine states it reads and since when (the verb manifest-reads, ADR 0618 §3): made at its
// first reading, and made again by Restate whenever the seat list it holds changes what it reads (ADR 0618 §4). The seat
// list is the process's, so the statement is too.
type readsStatement struct {
mu sync.Mutex
reads []string
at time.Time
}
var stated readsStatement
// now is the statement, made at the first reading.
func (s *readsStatement) now() ([]string, time.Time) {
s.mu.Lock()
defer s.mu.Unlock()
if s.reads == nil {
s.reads, s.at = readsNow(), time.Now().UTC()
}
return slices.Clone(s.reads), s.at
}
// restate makes the statement again from what the engine reads now, and says whether it changed.
func (s *readsStatement) restate() ([]string, bool) {
cur := readsNow()
s.mu.Lock()
defer s.mu.Unlock()
if s.reads != nil && slices.Equal(cur, s.reads) {
return slices.Clone(cur), false
}
s.reads, s.at = cur, time.Now().UTC()
return slices.Clone(cur), true
}
// manifestReads is what this engine states it reads of a manifest now, which its record carries.
func manifestReads() []string {
reads, _ := stated.now()
return reads
}
// recordOf is the node's record from one facts report and its keys; nil for a report that carries no profile (one that
// says nothing of the machine, such as a rekey), so the record last written stands.
func recordOf(node string, r Report, keys RecordKeys) *nodeengine.NodeRecord {
if r.Profile == nil {
return nil
}
var p struct {
Capabilities []struct {
Name string `json:"name"`
Present bool `json:"present"`
} `json:"capabilities"`
Machine *struct {
Networks []catalogue.AttachedNetwork `json:"networks"`
} `json:"machine"`
}
raw, err := json.Marshal(r.Profile)
if err != nil || json.Unmarshal(raw, &p) != nil {
return nil
}
rec := &nodeengine.NodeRecord{V: sdkkeys.Version, Node: node, TunnelKey: keys.TunnelKey, SealingKey: keys.SealingKey,
ServingKey: keys.ServingKey, Certificate: keys.Certificate, Authority: keys.Authority, Trusts: keys.Trusts,
ManifestReads: manifestReads()}
for _, c := range p.Capabilities {
if c.Present && c.Name != "" {
rec.Capabilities = append(rec.Capabilities, c.Name)
}
}
// A node-engine of G states that it reads the want's new fields: version, parts and where a kept build is kept (design 47 G §525).
rec.Capabilities = append(rec.Capabilities, CapabilityWantParts)
for _, c := range keys.Stated {
if c != "" && !slices.Contains(rec.Capabilities, c) {
rec.Capabilities = append(rec.Capabilities, c)
}
if c != "" {
rec.RunnerCapabilities = append(rec.RunnerCapabilities, c)
}
}
// The runner's own reading beside the merge, which stays for placement (design 46 approval mark §6, lead answer A-203).
sort.Strings(rec.RunnerCapabilities)
rec.RunnerFingerprint = keys.StatedBy
sort.Strings(rec.Capabilities)
if p.Machine != nil {
rec.NetworksSaid = true
for _, n := range p.Machine.Networks {
rec.Networks = append(rec.Networks, nodeengine.AttachedNetwork{Link: n.Link, Network: n.Network})
}
}
if len(r.Outward) > 0 {
rec.OutwardLinks = append([]string(nil), r.Outward...)
}
if r.Health != nil {
rec.Contract = int64(r.Health.Contract)
}
return rec
}