mesh-self-check reads the placement, seat holders, presence and health reports through the seats' types and clients; RootContract is the node-engine client's (B5g; ADR 0634 §1, §4; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 16:09:16 +01:00
parent 1e974ce9e8
commit b8642c8b27
6 changed files with 84 additions and 61 deletions
@@ -9,12 +9,13 @@ import (
"context"
"encoding/json"
"fmt"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
placing "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"os"
"time"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
"git.novox.be/novox/mesh-sdk/go/views"
@@ -76,10 +77,10 @@ func NewChecker() *Checker {
// self-check's own (first-view-unanswered), written into its state findings.
func newViews() *views.Reader {
r := &views.Reader{Module: Module, Logf: logf, Findings: meshconditionsclient.FindingWriter{State: stdio.State("findings")}, Acts: meshhandactsclient.Recorder{Ask: stdio.Ask}}
r.Follow(delivery.AssignerModule, delivery.PlacementState, PlacementTool)
r.Follow(delivery.AssignerModule, delivery.SeatHoldersState, SeatHoldersTool)
r.Follow(delivery.IndexerModule, "modules", ModulesTool)
r.Follow(delivery.IndexerModule, "manifests", ManifestsTool)
r.Follow(assignerclient.Module, placing.StatePlacement, PlacementTool)
r.Follow(assignerclient.Module, placing.StateSeatHolders, SeatHoldersTool)
r.Follow(indexerModule, "modules", ModulesTool)
r.Follow(indexerModule, "manifests", ManifestsTool)
return r
}
@@ -22,15 +22,14 @@ import (
"git.novox.be/novox/mesh-catalog/modules/disk-load/diskreadings"
"git.novox.be/novox/mesh-catalog/modules/restic/backupgood"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// The states D13 reads (declared under `reads`).
const (
ReadingsRead = diskreadings.ReadingsRead
GoodRead = backupgood.GoodRead
ModulesRead = delivery.IndexerModule + ".modules"
ManifestsRead = delivery.IndexerModule + ".manifests"
ModulesRead = indexerModule + ".modules"
ManifestsRead = indexerModule + ".manifests"
)
// The kinds D13 raises.
@@ -10,6 +10,9 @@ import (
"context"
"encoding/json"
"fmt"
watchdogs "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
placing "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"net"
"regexp"
"slices"
@@ -18,22 +21,21 @@ import (
"sync"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// The states the machine probes read (declared under `reads`), and the verb D8 asks each node.
const (
PlacementRead = delivery.AssignerModule + "." + "placement"
SeatHoldersRead = delivery.SeatHoldersRead
PresenceRead = delivery.WatchmanModule + ".presence"
PlacementRead = assignerclient.Module + "." + "placement"
SeatHoldersRead = assignerclient.SeatHoldersRead
PresenceRead = watchmanModule + ".presence"
BannedSeat = "node-intrusion-prevention"
BannedVerb = "seat:" + BannedSeat + ".banned"
ScopeMachine = meshconditionsclient.ScopeMachine
)
// placements is every node's placement, by node, from the state's keys in either form: a node key and its module keys,
// or the form before, `placement.<node>` (delivery.PlacementsOf; research 072 row 37).
func (c *Checker) placements() (map[string]delivery.Placement, error) {
// or the form before, `placement.<node>` (assignerclient.PlacementsOf; research 072 row 37).
func (c *Checker) placements() (map[string]placing.Placement, error) {
keys, err := c.Store.Keys(PlacementRead)
if err != nil {
return nil, fmt.Errorf("the placement cannot be read: %w", err)
@@ -48,7 +50,7 @@ func (c *Checker) placements() (map[string]delivery.Placement, error) {
values[k] = raw
}
}
out, err := delivery.PlacementsOf(values)
out, err := assignerclient.PlacementsOf(values)
if err != nil {
return nil, fmt.Errorf("the placement is not readable: %w", err)
}
@@ -63,15 +65,15 @@ func (c *Checker) heard() (map[string]bool, error) {
}
out := map[string]bool{}
for _, k := range keys {
node, _, _, err := delivery.ParsePresenceKey(k)
if err != nil || node == "" {
node, ok := watchdogs.ParsePresenceByNodeKey(k)
if !ok || node == "" {
continue
}
raw, ok, err := c.Store.Get(PresenceRead, k)
if err != nil {
return nil, fmt.Errorf("the presence of %s cannot be read: %w", node, err)
}
var p delivery.Presence
var p watchdogs.PresenceByNodeValue
if ok && json.Unmarshal(raw, &p) == nil && p.Online {
out[node] = true
}
@@ -94,7 +96,7 @@ func (c *Checker) holdersOf(seat string) ([]string, error) {
if err != nil {
return nil, fmt.Errorf("the holders of %s cannot be read: %w", k, err)
}
var sh delivery.SeatHolders
var sh placing.SeatHolders
if !ok || json.Unmarshal(raw, &sh) != nil || sh.Seat != seat || sh.Node == "" || len(sh.Holders) == 0 {
continue
}
@@ -214,3 +216,10 @@ func probeBans(ctx context.Context, c *Checker) ([]Observation, error) {
}
return sorted(out), nil
}
// The holders whose states the probes read, one constant each (a reader names its holder once; ADR 0634 §4).
const (
watchmanModule = "mesh-delivery-watchman"
indexerModule = "mesh-delivery-indexer"
nodeEngineModule = "mesh-host"
)
@@ -9,18 +9,19 @@ package main
import (
"context"
"fmt"
engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
nodeseat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
placing "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"sort"
"strings"
"sync"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// The engine's words and bounds the judgement reads (mesh-host rootsearch).
const (
// HealthRead is the node-engines' health reports.
HealthRead = delivery.NodeHealthRead
HealthRead = (nodeEngineModule + "." + nodeseat.StateHealth)
// rootPending starts a verdict's reason while the engine's setuid search runs and no complete one judges.
rootPending = "not judged yet (search running)"
// rootQuiet is how long nothing is raised while an agent account waits for that search, counted from when this
@@ -32,17 +33,17 @@ const (
)
// report is a node's health report, and whether it has one.
func (c *Checker) report(node string) (delivery.NodeHealth, bool, error) {
func (c *Checker) report(node string) (nodeseat.HealthByNodeValue, bool, error) {
raw, ok, err := c.Store.Get(HealthRead, node)
if err != nil {
return delivery.NodeHealth{}, false, fmt.Errorf("the health report of %s cannot be read: %w", node, err)
return nodeseat.HealthByNodeValue{}, false, fmt.Errorf("the health report of %s cannot be read: %w", node, err)
}
if !ok {
return delivery.NodeHealth{}, false, nil
return nodeseat.HealthByNodeValue{}, false, nil
}
h, err := delivery.DecodeNodeHealth(raw)
h, err := engineclient.DecodeHealth(raw)
if err != nil {
return delivery.NodeHealth{}, false, fmt.Errorf("the health report of %s: %w", node, err)
return nodeseat.HealthByNodeValue{}, false, fmt.Errorf("the health report of %s: %w", node, err)
}
return h, true, nil
}
@@ -83,7 +84,7 @@ const (
// judgedConfined is the controller's judgement over one machine's report: confined only when the machine is heard
// from, its engine judges root, and every verdict on the agent account is healthy. why is said either way.
func judgedConfined(agent string, h delivery.NodeHealth, had, heard bool) (bool, string) {
func judgedConfined(agent string, h nodeseat.HealthByNodeValue, had, heard bool) (bool, string) {
switch {
case !had:
return false, fmt.Sprintf("the agent account %s is not judged: the machine's node-engine has stated "+
@@ -91,10 +92,10 @@ func judgedConfined(agent string, h delivery.NodeHealth, had, heard bool) (bool,
case !heard:
return false, fmt.Sprintf("the agent account %s is not judged: the machine is not heard from, and a verdict "+
"from before is not a verdict on now", agent)
case h.Contract < delivery.RootContract:
case h.Contract < engineclient.RootContract:
return false, fmt.Sprintf("the agent account %s is not judged: the machine's node-engine is older than "+
"the judging of an account's root (its statement's contract is %d, the judging is %d)",
agent, h.Contract, delivery.RootContract)
agent, h.Contract, engineclient.RootContract)
}
verdicts := verdictsOn(agent, h)
if len(verdicts) == 0 {
@@ -118,8 +119,8 @@ func judgedConfined(agent string, h delivery.NodeHealth, had, heard bool) (bool,
}
// verdictsOn is every verdict on the account, sorted by module and resource.
func verdictsOn(agent string, h delivery.NodeHealth) []delivery.RootVerdict {
var out []delivery.RootVerdict
func verdictsOn(agent string, h nodeseat.HealthByNodeValue) []nodeseat.RootAccountVerdict {
var out []nodeseat.RootAccountVerdict
for _, v := range h.Root {
if v.Account == agent {
out = append(out, v)
@@ -131,8 +132,8 @@ func verdictsOn(agent string, h delivery.NodeHealth) []delivery.RootVerdict {
// rootVerdictKind reads a report for the agent account: pending when every verdict on it is healthy or waits for the
// setuid search, at least one of them that; complete when every verdict is healthy or one found a way to root.
func rootVerdictKind(agent string, h delivery.NodeHealth, had, heard bool) int {
if !had || !heard || h.Contract < delivery.RootContract {
func rootVerdictKind(agent string, h nodeseat.HealthByNodeValue, had, heard bool) int {
if !had || !heard || h.Contract < engineclient.RootContract {
return verdictOther
}
pending, any := false, false
@@ -159,7 +160,7 @@ func rootVerdictKind(agent string, h delivery.NodeHealth, had, heard bool) int {
// agentQuiet says the one thing keeping a machine's agent account unjudged is the engine's setuid search, first seen
// waiting less than rootQuiet ago.
func (c *Checker) agentQuiet(node, agent string, h delivery.NodeHealth, had, heard bool) bool {
func (c *Checker) agentQuiet(node, agent string, h nodeseat.HealthByNodeValue, had, heard bool) bool {
switch rootVerdictKind(agent, h, had, heard) {
case verdictComplete:
c.pending.judged(node)
@@ -186,7 +187,7 @@ type confinement struct {
}
// confined judges node's agent account from its placement, its report and its presence.
func (c *Checker) confined(node string, place delivery.Placement, heard bool) (confinement, error) {
func (c *Checker) confined(node string, place placing.Placement, heard bool) (confinement, error) {
agent := place.Decided.AgentAccount
if agent == "" {
return confinement{Why: fmt.Sprintf("%s names no agent account: agents run as the operator account (%s)",
@@ -4,6 +4,13 @@ import (
"context"
"encoding/json"
"errors"
engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
nodeseat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
watchdogs "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
placing "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
catalogclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"slices"
"strings"
"testing"
@@ -13,7 +20,6 @@ import (
"git.novox.be/novox/mesh-catalog/modules/restic/backupgood"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// bus is the bus module's answers as a probe asks them: every mesh stream and consumer as defined, unless changed.
@@ -257,42 +263,42 @@ func machines() *memStore {
node, address, endpoint string
online bool
}{{"novox", "10.10.0.1/16", "203.0.113.5:51820", true}, {"ace", "10.10.0.2/16", "", true}, {"g14", "10.10.0.3/16", "", false}} {
pl := delivery.Placement{V: 1, Node: p.node,
Decided: delivery.NodeDecided{Address: p.address, Endpoint: p.endpoint, Hub: p.endpoint != ""}}
pl := placing.Placement{V: 1, Node: p.node,
Decided: placing.NodeDecided{Address: p.address, Endpoint: p.endpoint, Hub: p.endpoint != ""}}
if p.node == "ace" {
pl.Modules = []delivery.PlacedModule{{Module: "sshd"}} // a module key beside the node key
pl.Modules = []placing.PlacedModule{{Module: "sshd"}} // a module key beside the node key
}
place(s, pl)
_ = s.Put(PresenceRead, p.node, delivery.Presence{V: 1, Online: p.online})
key, _ := delivery.SeatHoldersKey(BannedSeat, p.node)
_ = s.Put(SeatHoldersRead, key, delivery.SeatHolders{V: 1, Seat: BannedSeat, Scope: "node", Node: p.node,
Holders: []delivery.SeatHolder{{Module: "fail2ban", Node: p.node}}})
_ = s.Put(PresenceRead, p.node, watchdogs.PresenceByNodeValue{V: 1, Online: p.online})
key, _ := assignerclient.SeatHoldersKey(BannedSeat, p.node)
_ = s.Put(SeatHoldersRead, key, placing.SeatHolders{V: 1, Seat: BannedSeat, Scope: "node", Node: p.node,
Holders: []placing.SeatHoldersHoldersItem{{Module: "fail2ban", Node: p.node}}})
}
return s
}
// place writes a node's placement as the seat does, its node key and a key per module (research 072 row 37); g14's in
// the form before, `placement.g14`, which the probes still read while the change is delivered.
func place(s *memStore, p delivery.Placement) {
func place(s *memStore, p placing.Placement) {
if p.Node == "g14" {
raw, _ := delivery.EncodePlacement(p)
raw, _ := assignerclient.EncodePlacement(p)
_ = s.Put(PlacementRead, "placement.g14", json.RawMessage(raw))
return
}
values, _ := delivery.PlacementValues(p)
values, _ := assignerclient.PlacementValues(p)
for k, raw := range values {
_ = s.Put(PlacementRead, k, json.RawMessage(raw))
}
}
// placed is every node's placement the store holds.
func placed(s *memStore) map[string]delivery.Placement {
func placed(s *memStore) map[string]placing.Placement {
keys, _ := s.Keys(PlacementRead)
values := map[string][]byte{}
for _, k := range keys {
values[k], _, _ = s.Get(PlacementRead, k)
}
out, _ := delivery.PlacementsOf(values)
out, _ := assignerclient.PlacementsOf(values)
return out
}
@@ -314,20 +320,20 @@ func agents(verdicts map[string]string) *memStore {
p := placed(s)[node]
p.Decided.AgentAccount = "agent"
place(s, p)
h := delivery.NodeHealth{V: 1, Node: node, Contract: 13}
h := nodeseat.HealthByNodeValue{V: 1, Node: node, Contract: 13}
switch v {
case "":
case "old":
h.Contract = 2
h.Root = []delivery.RootVerdict{{Account: "agent", Module: "claude-code", Resource: "claude-code.agent", State: stateHealthy}}
h.Root = []nodeseat.RootAccountVerdict{{Account: "agent", Module: "claude-code", Resource: "claude-code.agent", State: stateHealthy}}
case "pending":
h.Root = []delivery.RootVerdict{{Account: "agent", Module: "claude-code", Resource: "claude-code.agent",
h.Root = []nodeseat.RootAccountVerdict{{Account: "agent", Module: "claude-code", Resource: "claude-code.agent",
State: stateUnknown, Reason: rootPending + ": 3 of 9 trees"}}
default:
h.Root = []delivery.RootVerdict{{Account: "agent", Module: "claude-code", Resource: "claude-code.agent", State: v,
h.Root = []nodeseat.RootAccountVerdict{{Account: "agent", Module: "claude-code", Resource: "claude-code.agent", State: v,
Reason: "can become root without a person: a sudo rule"}}
}
raw, _ := delivery.EncodeNodeHealth(h)
raw, _ := engineclient.EncodeHealth(h)
_ = s.Put(HealthRead, node, json.RawMessage(raw))
}
return s
@@ -429,10 +435,10 @@ func (r *dataRig) firstSeen(node, module, item string, ago time.Duration) *dataR
}
func (r *dataRig) holder(node string) {
key, _ := delivery.SeatHoldersKey("node-backup", node)
_ = r.s.Put(SeatHoldersRead, key, delivery.SeatHolders{V: 1, Seat: "node-backup", Scope: "node", Node: node,
Holders: []delivery.SeatHolder{{Module: "restic", Node: node}}})
_ = r.s.Put(PresenceRead, node, delivery.Presence{V: 1, Online: true})
key, _ := assignerclient.SeatHoldersKey("node-backup", node)
_ = r.s.Put(SeatHoldersRead, key, placing.SeatHolders{V: 1, Seat: "node-backup", Scope: "node", Node: node,
Holders: []placing.SeatHoldersHoldersItem{{Module: "restic", Node: node}}})
_ = r.s.Put(PresenceRead, node, watchdogs.PresenceByNodeValue{V: 1, Online: true})
}
// dataMesh builds the mesh: pg declares data (backed up within 48 h, written all the time within 24 h) and dumps; plex
@@ -450,9 +456,9 @@ func dataMesh(change func(*dataRig)) *memStore {
man.Data.Own = append(man.Data.Own, it)
}
raw, _ := json.Marshal(man)
digest := delivery.ManifestDigest(raw)
_ = r.s.Put(ManifestsRead, digest, delivery.Manifest{V: 1, Module: module, JSON: string(raw)})
_ = r.s.Put(ModulesRead, module, delivery.Module{V: 1, Manifest: digest})
digest := catalogclient.ManifestDigest(raw)
_ = r.s.Put(ManifestsRead, digest, catalog.ManifestsByDigestValue{V: 1, Module: module, Manifest: string(raw)})
_ = r.s.Put(ModulesRead, module, catalog.ModulesByModuleValue{V: 1, Manifest: digest})
}
declare("pg", map[string]catalogue.DataItem{
"data": {Class: catalogue.ClassValuable, Within: "48h", Active: "24h"},
+7
View File
@@ -3,6 +3,13 @@ module mesh-self-check
go 1.26.0
require (
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f