What runs is the agent's compositions plus the kept declaration's rest: health, filling, take act, directories and the others held out of an apply read it; a restart trusts the node-engine's own haves; the bed tests run off Arch (review round 1)

This commit is contained in:
2026-10-24 18:13:40 +02:00
parent 67db003813
commit be5f31ce66
9 changed files with 298 additions and 59 deletions
+1 -5
View File
@@ -27,11 +27,7 @@ func serveFilling(ctx context.Context, opts options, signer ed25519.PublicKey, s
func fillingEngine(opts options, signer ed25519.PublicKey, say func(string)) *filling.Engine {
return &filling.Engine{
Declared: func() (*declaration.Declaration, error) {
raw, err := store.LoadDeclared(store.DeclaredPath(opts.state), signer)
if err != nil {
return nil, err
}
return declaration.Parse(raw)
return declaredNow(opts.state, signer)
},
UIDOf: filling.UIDOf,
Lock: func() (func(), error) {
+2 -7
View File
@@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"git.novox.be/novox/mesh-sdk/go/declaration"
"github.com/novox/mesh-host/internal/apply"
"github.com/novox/mesh-host/internal/link"
"github.com/novox/mesh-host/internal/store"
@@ -13,16 +12,12 @@ import (
// judgeDirectories judges every directory of the declaration this node keeps, read-only (novox/hq issue 358): which
// the walk that follows no link another account could make would refuse. Nothing is made, chmodded or chowned.
func judgeDirectories(statePath string) (string, error) {
kept, err := store.ReadDeclared(store.DeclaredPath(statePath))
d, err := declaredNow(statePath, nil)
if errors.Is(err, store.ErrNothingDeclared) {
return "", errors.New("this node keeps no declaration, so it is declared no directory")
}
if err != nil {
return "", err
}
d, err := declaration.Parse(kept.Declaration)
if err != nil {
return "", fmt.Errorf("the declaration this node keeps does not read: %w", err)
return "", fmt.Errorf("what runs on this node does not read: %w", err)
}
return apply.JudgeDirectories(d), nil
}
+2 -6
View File
@@ -251,16 +251,12 @@ func startKeeping(ctx context.Context, say func(string)) {
// declaration this node keeps, and run beside the applies.
func accessActFrom(statePath string, timeout time.Duration) link.AccessActFunc {
return func(act link.AccessAct) (string, error) {
kept, err := store.ReadDeclared(store.DeclaredPath(statePath))
d, err := declaredNow(statePath, nil)
if errors.Is(err, store.ErrNothingDeclared) {
return "", errors.New("this node keeps no declaration, so it is declared no library")
}
if err != nil {
return "", err
}
d, err := declaration.Parse(kept.Declaration)
if err != nil {
return "", fmt.Errorf("the declaration this node keeps does not read: %w", err)
return "", fmt.Errorf("what runs on this node does not read: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
+3 -16
View File
@@ -30,7 +30,6 @@ import (
"time"
"git.novox.be/novox/mesh-sdk/go/declaration"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/accounts"
"github.com/novox/mesh-host/internal/agent"
"github.com/novox/mesh-host/internal/apply"
@@ -1137,6 +1136,7 @@ func runLink(ctx context.Context, opts options) error {
Timeout: opts.timeout,
}
current := &agentNow{}
setRunsHere(runsFromAgent(current))
factsOfNow := func(ctx context.Context, ag *agent.Agent) {
sayFacts(ctx, opts, mine.Membership.Signer, queue, say, ag)
}
@@ -1879,14 +1879,8 @@ func sayFacts(ctx context.Context, opts options, signer ed25519.PublicKey, sessi
return
}
declared := &declaration.Declaration{}
if running := runningOf(ag); len(running) > 0 {
if d, err := declarationOf(running...); err == nil {
declared = d
}
} else if raw, err := store.LoadDeclared(store.DeclaredPath(opts.state), signer); err == nil {
if d, err := declaration.Parse(raw); err == nil {
declared = d
}
if d, err := declaredNow(opts.state, signer); err == nil {
declared = d
}
held := map[string]bool{}
for _, h := range known.Held {
@@ -1896,13 +1890,6 @@ func sayFacts(ctx context.Context, opts options, signer ed25519.PublicKey, sessi
session.SayFacts(ctx, report)
}
func runningOf(ag *agent.Agent) []delivery.Composed {
if ag == nil {
return nil
}
return ag.Running()
}
// waitForEnrolment returns this node's identity, waiting for one if it has none.
//
// It applies the carried bundle first, if there is one, because that is what a first node does
+83 -4
View File
@@ -228,10 +228,10 @@ func trustedKeys(opts options, controller ed25519.PublicKey) func() []ed25519.Pu
// keptByModule is the resources of the declaration the node kept before the switch, by the module they say; the node's
// own are those that say none (ADR 0539 §9).
func keptByModule(opts options, signer ed25519.PublicKey) func(module string) ([]json.RawMessage, bool) {
func keptByModule(opts options, signer ed25519.PublicKey) func() map[string][]json.RawMessage {
var once sync.Once
var by map[string][]json.RawMessage
return func(module string) ([]json.RawMessage, bool) {
return func() map[string][]json.RawMessage {
once.Do(func() {
raw, err := store.LoadDeclared(store.DeclaredPath(opts.state), signer)
if err != nil {
@@ -256,8 +256,7 @@ func keptByModule(opts options, signer ed25519.PublicKey) func(module string) ([
by[name] = append(by[name], r)
}
})
r, ok := by[module]
return r, ok
return by
}
}
@@ -399,3 +398,83 @@ func busyModules() []string {
sort.Strings(out)
return out
}
// keptDeclaration is the declaration the node kept before the switch, verified with the mesh's key when it is given.
func keptDeclaration(statePath string, signer ed25519.PublicKey) (*declaration.Declaration, error) {
var raw []byte
if len(signer) > 0 {
var err error
if raw, err = store.LoadDeclared(store.DeclaredPath(statePath), signer); err != nil {
return nil, err
}
} else {
kept, err := store.ReadDeclared(store.DeclaredPath(statePath))
if err != nil {
return nil, err
}
raw = kept.Declaration
}
return declaration.Parse(raw)
}
// whatRuns is what runs on this node as the declaration of it (novox/hq ADR 0539 §9): the compositions the agent runs
// and, for every module the agent does not hold yet, what the kept declaration has for it, with the node's own parts
// (adoption, identities, groups, libraries) from the node's composition once the agent holds it and from the kept
// declaration until then. What the node judges its health by, and what the filling, the take act and the judgement of
// directories read.
func whatRuns(everything []delivery.Composed, holdsNode bool, kept func() (*declaration.Declaration, error)) (*declaration.Declaration, error) {
d, err := declarationOf(everything...)
if err != nil {
return nil, err
}
if holdsNode {
return d, nil
}
old, err := kept()
if err != nil {
if len(everything) > 0 {
return d, nil
}
return nil, err
}
d.Adoption, d.Identities, d.Groups, d.Libraries = old.Adoption, old.Identities, old.Groups, old.Libraries
d.Provisioner, d.Builds = old.Provisioner, old.Builds
d.Filled = append(d.Filled, old.Filled...)
return d, nil
}
// runsHere says what runs on this node; the running serve sets it to the agent's view, and a command run by hand reads
// the kept declaration.
var runsHere struct {
mu sync.Mutex
fn func(statePath string, signer ed25519.PublicKey) (*declaration.Declaration, error)
}
func setRunsHere(fn func(statePath string, signer ed25519.PublicKey) (*declaration.Declaration, error)) {
runsHere.mu.Lock()
runsHere.fn = fn
runsHere.mu.Unlock()
}
// declaredNow is the declaration of what runs on this node now.
func declaredNow(statePath string, signer ed25519.PublicKey) (*declaration.Declaration, error) {
runsHere.mu.Lock()
fn := runsHere.fn
runsHere.mu.Unlock()
if fn != nil {
return fn(statePath, signer)
}
return keptDeclaration(statePath, signer)
}
// runsFromAgent is what runs now as the agent in hand sees it.
func runsFromAgent(current *agentNow) func(string, ed25519.PublicKey) (*declaration.Declaration, error) {
return func(statePath string, signer ed25519.PublicKey) (*declaration.Declaration, error) {
kept := func() (*declaration.Declaration, error) { return keptDeclaration(statePath, signer) }
ag := current.get()
if ag == nil {
return kept()
}
return whatRuns(ag.Everything(), ag.HoldsNode(), kept)
}
}
+6 -5
View File
@@ -18,9 +18,7 @@ import (
"github.com/nats-io/nats-server/v2/server"
"github.com/nats-io/nats.go"
"github.com/novox/mesh-host/internal/agent"
"github.com/novox/mesh-host/internal/apply"
"github.com/novox/mesh-host/internal/link"
"github.com/novox/mesh-host/internal/system"
)
// The node agent whole, against a real bus with JetStream started in the test: the wants bucket, the node's composition
@@ -66,15 +64,18 @@ func aServer(t *testing.T) *nats.Conn {
func newItBed(t *testing.T) *itBed {
t.Helper()
built, err := system.For("arch")
if err != nil || built.Confirm(context.Background(), apply.ExecRunner) != nil {
t.Skip("this machine is not one these tests can apply on")
// Not skipped off Arch: a package manager that answers is put on PATH, so the apply of files runs on any build node.
bin := t.TempDir()
if err := os.WriteFile(filepath.Join(bin, "pacman"), []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
was := builtFor
builtFor = "arch"
t.Cleanup(func() { builtFor = was })
b := &itBed{t: t, conn: aServer(t), dir: t.TempDir()}
var err error
if b.js, err = b.conn.JetStream(); err != nil {
t.Fatal(err)
}
+59
View File
@@ -0,0 +1,59 @@
package main
import (
"crypto/ed25519"
"encoding/json"
"errors"
"testing"
"git.novox.be/novox/mesh-sdk/go/declaration"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
func aFileOf(module, id string) json.RawMessage {
raw, _ := json.Marshal(map[string]any{"id": module + "." + id, "type": "file", "path": "/tmp/r6/" + module + "/" + id,
"content": "x", "module": module})
return raw
}
// What runs is the agent's compositions and the kept declaration's resources for each module the agent does not hold;
// the node's own parts are the kept declaration's until the agent holds the node's composition (ADR 0539 §9). One module
// under the agent must not make the rest invisible to the liveness, units, accounts, libraries, filling and the take act.
func TestWhatRunsIsTheAgentsCompositionsAndTheKeptRest(t *testing.T) {
kept := &declaration.Declaration{Identities: []declaration.Identity{{Module: "db", Name: "mesh-db"}},
Libraries: []declaration.Library{{Library: "media"}}}
everything := []delivery.Composed{
{Node: "n1", Module: "web", Resources: []json.RawMessage{aFileOf("web", "a")}},
{Node: "n1", Module: "db", Resources: []json.RawMessage{aFileOf("db", "a")}}, // still the kept declaration's
}
d, err := whatRuns(everything, false, func() (*declaration.Declaration, error) { return kept, nil })
if err != nil {
t.Fatal(err)
}
if len(d.Resources) != 2 || len(d.Identities) != 1 || len(d.Libraries) != 1 {
t.Fatalf("%d resources, %d identities, %d libraries: the kept modules and the node's kept parts are missing",
len(d.Resources), len(d.Identities), len(d.Libraries))
}
// Once the agent holds the node's composition, its parts are the node's.
node := delivery.Composed{Node: "n1", Own: delivery.NodeOwn{Identities: []declaration.Identity{{Module: "web", Name: "mesh-web"}}}}
d, err = whatRuns(append(everything, node), true, func() (*declaration.Declaration, error) { return nil, errors.New("not read") })
if err != nil {
t.Fatal(err)
}
if len(d.Identities) != 1 || d.Identities[0].Name != "mesh-web" || len(d.Libraries) != 0 {
t.Fatalf("the node's parts: %+v %+v", d.Identities, d.Libraries)
}
}
// What the take act, the judgement of directories and the filling read is what runs, not the frozen kept declaration.
func TestDeclaredNowReadsWhatRunsWhenTheAgentIsRunning(t *testing.T) {
setRunsHere(func(string, ed25519.PublicKey) (*declaration.Declaration, error) {
return &declaration.Declaration{Version: 1, Resources: nil, Provisioner: "from-agent"}, nil
})
defer setRunsHere(nil)
d, err := declaredNow("/nowhere", nil)
if err != nil || d.Provisioner != "from-agent" {
t.Fatalf("declaredNow read %+v (%v), not what runs", d, err)
}
}