Files
mesh-host/internal/link/record.go
T
jschoubben 9ab85ef92e
mesh/delivery merged: source.main moved to novox/mesh-host@875b4ee5ce38
mesh/delivery-group group fix/1966-seat-reads-bootstrap merged: every member is merged
mesh/check failed (2): mesh-delivery-checker: no published of the tagged version of go-sdk of mesh-delivery…; and 1 more | 9ab85ef92e6c on 875b4ee5ce38
The node-engine states the seats of the seat list it holds, again at each change (issue 1966)
manifestReads froze the statement at its first call, before the engine gave
catalogue even its bootstrapped seats (ADR 0635 empties mesh-sdk's table
until the list is read), so ace's engine stated no seat and its own next
build, naming mesh-catalog, was held on it.

The words and forms stay the build's, computed once; the seats are those of
the list held now. The reader of the seat list restates after each change:
the node record carries the new statement at once and the node seat's event
manifest-reads-changed says it (ADR 0618 §4); an unchanged statement writes
and says nothing; the verb manifest-reads answers when it was made.

Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5
2026-11-08 02:36:41 +01:00

148 lines
5.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
}
// 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.
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)
}
}
for _, c := range keys.Stated {
if c != "" && !slices.Contains(rec.Capabilities, c) {
rec.Capabilities = append(rec.Capabilities, c)
}
}
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
}