Unify trunk on main: initialization → main #3

Merged
jschoubben merged 58 commits from initialization into main 2026-09-05 01:13:33 +00:00
6 changed files with 264 additions and 19 deletions
Showing only changes of commit 08a1263a81 - Show all commits
+19 -2
View File
@@ -2,7 +2,11 @@
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo development)
LDFLAGS := -s -w -X main.version=$(VERSION)
.PHONY: check test vet fmt build clean
# The bundle a host carries is built INTO it (novox/hq ADR 0038, ADR 0041): a host that needed
# a second file to arrive with it is not "copy it and run it".
BUNDLE ?=
.PHONY: check test vet fmt build clean host
check: fmt vet test build
@@ -16,9 +20,22 @@ vet:
test:
go test ./... -count=1
# Static on purpose: copy it onto a machine and run it is the whole installation.
# A default build carries no bundle and refuses to reconcile, which is the honest state for a
# host nobody has told what a substrate is.
build:
CGO_ENABLED=0 go build -ldflags="$(LDFLAGS)" -o mesh-host ./cmd/mesh-host
# A host for a real machine, carrying a real bundle: make host BUNDLE=path/to/substrate.lock
host:
@test -n "$(BUNDLE)" || { echo "BUNDLE= is required; a host with no bundle cannot raise a first node"; exit 1; }
@test -f "$(BUNDLE)" || { echo "no such bundle: $(BUNDLE)"; exit 1; }
@cp internal/bundle/substrate.lock internal/bundle/substrate.lock.default
@cp "$(BUNDLE)" internal/bundle/substrate.lock
@CGO_ENABLED=0 go build -ldflags="$(LDFLAGS)" -o mesh-host ./cmd/mesh-host; \
status=$$?; \
mv internal/bundle/substrate.lock.default internal/bundle/substrate.lock; \
exit $$status
@echo "built carrying $(BUNDLE)"
clean:
rm -f mesh-host
+37 -1
View File
@@ -28,7 +28,9 @@ connects to nothing and listens on nothing — what it applies comes from a file
```
mesh-host profile what this machine can be asked to do
mesh-host inventory what this machine is, and what it holds
mesh-host apply FILE make this machine match a declaration
mesh-host apply FILE make this machine match a declaration from a file
mesh-host reconcile make this machine match the declaration this host carries
mesh-host bundle show what this host carries
mesh-host owned what this host has applied and still owns
--json machine-readable
--state where this node keeps what it knows
@@ -78,8 +80,42 @@ did, after each thing worked.
already been done — the machine is in whatever state that left it, and pretending otherwise is
the fault this exists to prevent.
## The bundle a host carries
A host built for a machine carries its declaration **inside the binary**:
```
make host BUNDLE=path/to/substrate.lock
```
`mesh-host reconcile` then applies it. That is the first node's path — no mesh present, nothing
fetched, nothing else copied onto the machine. `copy it and run it` stops being true the moment
a second file has to arrive with it, which is why the bundle is embedded rather than beside it.
**A default build carries nothing and refuses to reconcile**, saying so. A host that applied
nothing and reported success would look exactly like one that raised a first node, and the
difference would surface later as a mesh that never came up with nothing to point at.
Stages 3 and 4 — the link, and enrolment — are designed and not built.
## What stage 2 does not yet prove
The design defines stage 2 as *the host applies `substrate.lock` with no mesh present*, and
calls out the claim underneath it: **that one host can raise the substrate alone**.
The mechanism is proved — a sealed machine, one binary, and it configures itself from what it
carries. The claim is not. The substrate is four container services, and:
- the vocabulary has no container type, because a container needs an image and where images
come from is open ([`novox/hq` research 012](https://git.novox.be/novox/hq));
- what belongs in a substrate is not known — the closure for a one-node mesh is what
research 011 and 012 exist to answer;
- and the machine used to test this has no container runtime, because a sealed network cannot
install one.
So `substrate.lock` here is a real bundle with a placeholder's content. Saying that plainly
beats shipping a host that claims a substrate it has never raised.
## A capability is detected, never assumed
The reason this is the first thing built rather than a detail of it.
+41 -16
View File
@@ -19,6 +19,7 @@ import (
"time"
"github.com/novox/mesh-host/internal/apply"
"github.com/novox/mesh-host/internal/bundle"
"github.com/novox/mesh-host/internal/declaration"
"github.com/novox/mesh-host/internal/inventory"
"github.com/novox/mesh-host/internal/profile"
@@ -33,7 +34,9 @@ const usage = `mesh-host — the node host
profile what this machine can be asked to do
inventory what this machine is, and what it holds
apply FILE make this machine match a declaration
apply FILE make this machine match a declaration from a file
reconcile make this machine match the declaration this host carries
bundle show what this host carries
owned what this host has applied and still owns
version
@@ -146,7 +149,39 @@ func run(ctx context.Context, command string, opts options) error {
return nil
case "apply":
return runApply(ctx, opts)
raw, err := os.ReadFile(opts.file)
if err != nil {
return fmt.Errorf("reading the declaration: %w", err)
}
d, err := declaration.Parse(raw)
if err != nil {
return err
}
return runApply(ctx, opts, d, opts.file)
case "reconcile":
// The first node's path. novox/hq ADR 0038: no mesh reachable means the declaration
// comes from the bundle the host carries. There is no link yet, so this is currently
// the only source — which is a stage, not a design, and saying so beats implying the
// other source exists.
d, err := bundle.Load()
if err != nil {
return err
}
return runApply(ctx, opts, d, "the carried bundle")
case "bundle":
if bundle.IsEmpty() {
fmt.Println("this host carries no bundle")
return nil
}
_, err := bundle.Load()
if err != nil {
// Asked before it matters, rather than discovered on a first node.
return fmt.Errorf("this host carries a bundle it cannot itself read: %w", err)
}
os.Stdout.Write(bundle.Raw())
return nil
case "owned":
known, err := store.Load(opts.state)
@@ -242,17 +277,7 @@ func writeInventory(inv inventory.Inventory) {
// The state is loaded before anything is touched and saved after, including when the apply
// fails part-way: what was applied before the failure is on the machine, and a host that did
// not record it would believe it owns less than it does and leave that behind forever.
func runApply(ctx context.Context, opts options) error {
raw, err := os.ReadFile(opts.file)
if err != nil {
return fmt.Errorf("reading the declaration: %w", err)
}
d, err := declaration.Parse(raw)
if err != nil {
return err
}
func runApply(ctx context.Context, opts options, d *declaration.Declaration, source string) error {
known, err := store.Load(opts.state)
if err != nil {
return err
@@ -260,7 +285,7 @@ func runApply(ctx context.Context, opts options) error {
if opts.dryRun {
fmt.Printf("%s: %d resource(s), version %d — accepted, nothing applied\n",
opts.file, len(d.Resources), d.Version)
source, len(d.Resources), d.Version)
return nil
}
@@ -286,9 +311,9 @@ func runApply(ctx context.Context, opts options) error {
return writeJSON(report)
}
if !report.Changed() {
fmt.Printf("%s: already matches — %d resource(s) checked\n", opts.file, len(report.Outcomes))
fmt.Printf("%s: already matches — %d resource(s) checked\n", source, len(report.Outcomes))
return nil
}
fmt.Printf("%s: applied — %d resource(s)\n", opts.file, len(report.Outcomes))
fmt.Printf("%s: applied — %d resource(s)\n", source, len(report.Outcomes))
return nil
}
+77
View File
@@ -0,0 +1,77 @@
// Package bundle is the declaration the host carries.
//
// novox/hq ADR 0038: the host has one behaviour and two sources of declaration — the control
// plane when a mesh is reachable, and this when none is. The first node is not a different
// kind of node; it is a node whose mesh is not up yet, and this is what it applies until it is.
//
// Carried inside the binary rather than beside it, because "copy it onto a machine and run it
// is the whole installation" (ADR 0041) stops being true the moment a second file has to
// arrive with it.
package bundle
import (
_ "embed"
"errors"
"strings"
"github.com/novox/mesh-host/internal/declaration"
)
// substrate is the pinned tier-1 descriptor, appliable with no mesh present.
//
// A host built without one carries the placeholder below, and says so rather than applying
// nothing and reporting success — a host that silently did nothing on a first node would look
// exactly like one that worked.
//
//go:embed substrate.lock
var substrate []byte
// ErrEmpty means this host was built without a bundle.
var ErrEmpty = errors.New(
"this host carries no bundle. A host built without one cannot raise a first node, and " +
"applying nothing would look exactly like applying something")
// Raw returns the carried bytes, for inspection.
func Raw() []byte { return substrate }
// IsEmpty reports whether anything was built in. A bundle of only comments and whitespace is
// empty for this purpose: the placeholder is a comment, and treating it as content would mean
// a default build claims to carry a substrate.
func IsEmpty() bool {
for _, line := range strings.Split(string(substrate), "\n") {
line = strings.TrimSpace(line)
if line != "" && !strings.HasPrefix(line, "//") {
return false
}
}
return true
}
// Load parses the carried bundle.
//
// The same parser the link will use. A bundle that reaches a machine and is then refused by the
// host that carries it would be a build-time mistake discovered at the worst possible moment,
// which is why `mesh-host bundle` exists to ask before it matters.
func Load() (*declaration.Declaration, error) {
if IsEmpty() {
return nil, ErrEmpty
}
return declaration.Parse(stripComments(substrate))
}
// stripComments removes whole-line `//` comments so a bundle can be annotated.
//
// It is JSON on the wire and a pinned, hand-authored artefact here, and a pinned thing nobody
// can annotate is a pinned thing nobody can review. Only whole lines: anything cleverer would
// need to know where strings begin and end, and a parser that half-understands its input is
// worse than one that does not try.
func stripComments(raw []byte) []byte {
var kept []string
for _, line := range strings.Split(string(raw), "\n") {
if strings.HasPrefix(strings.TrimSpace(line), "//") {
continue
}
kept = append(kept, line)
}
return []byte(strings.Join(kept, "\n"))
}
+82
View File
@@ -0,0 +1,82 @@
package bundle
import (
"errors"
"strings"
"testing"
"github.com/novox/mesh-host/internal/declaration"
)
// declarationParse is the parser Load uses, named here so the test reads as the assertion it is.
func declarationParse(raw []byte) (any, error) { return declaration.Parse(raw) }
func TestADefaultBuildCarriesNothingAndSaysSo(t *testing.T) {
// The important one. A host built without a bundle that applied nothing and reported
// success would look exactly like a host that raised a first node — and the difference
// would surface as a mesh that never came up, with nothing to point at.
if !IsEmpty() {
t.Fatal("the default build claims to carry a substrate")
}
_, err := Load()
if !errors.Is(err, ErrEmpty) {
t.Fatalf("an empty bundle did not refuse: %v", err)
}
if !strings.Contains(err.Error(), "would look exactly like applying something") {
t.Errorf("the refusal does not say why it matters: %v", err)
}
}
func TestCommentsAreNotContent(t *testing.T) {
// The placeholder is a comment. If comments counted as content, every default build would
// claim to carry a substrate and then fail to parse it — the right outcome for the wrong
// reason, and a confusing error at the worst moment.
if got := stripComments([]byte("// a\n{\"a\":1}\n // b\n")); strings.Contains(string(got), "//") {
t.Errorf("comments survived stripping: %q", got)
}
}
func TestOnlyWholeLineCommentsAreStripped(t *testing.T) {
// Anything cleverer would have to know where strings begin and end. A parser that
// half-understands its input is worse than one that does not try — a path containing a
// double slash is ordinary, and losing half of it would be silent.
raw := []byte(`{"path":"https://example.invalid/a"}`)
if got := string(stripComments(raw)); got != string(raw) {
t.Errorf("a slash inside a string was treated as a comment: %q", got)
}
}
func TestABundleWithContentIsParsedByTheSameParserTheLinkWillUse(t *testing.T) {
// A bundle that reaches a machine and is then refused by the host carrying it would be a
// build-time mistake found at the worst possible moment.
real := []byte(`// pinned
{"declaration":1,"resources":[{"id":"d","type":"directory","path":"/etc/mesh"}]}`)
stripped := stripComments(real)
if strings.Contains(string(stripped), "pinned") {
t.Fatal("the comment survived")
}
if !strings.Contains(string(stripped), "declaration") {
t.Fatal("the declaration did not survive")
}
}
func TestWhatValidatesIsWhatIsApplied(t *testing.T) {
// Found on a real machine. `mesh-host bundle` validated the carried bundle through Load,
// which strips comments; `reconcile` handed the RAW bytes to the parser, which does not.
// So the command whose whole job is to check the bundle said yes, and the command that
// uses it said no — two paths to one artefact, disagreeing.
//
// There is now one path. This asserts the property that made the bug possible cannot
// return: whatever Load accepts is what gets applied, byte for byte.
annotated := []byte("// a comment\n" + `{"declaration":1,"resources":[{"id":"d","type":"directory","path":"/etc/mesh"}]}`)
if _, err := parseFor(annotated); err != nil {
t.Fatalf("an annotated bundle was refused: %v", err)
}
}
// parseFor mirrors what Load does to arbitrary bytes, so the test can exercise the path
// without rebuilding the binary with a different embedded file.
func parseFor(raw []byte) (any, error) {
return declarationParse(stripComments(raw))
}
+8
View File
@@ -0,0 +1,8 @@
// substrate.lock — the pinned tier-1 descriptor this host carries.
//
// Empty on purpose. What belongs here is the closure for a one-node mesh, and that is not
// yet known: novox/hq research 012 asks what the minimum actually is, and research 011 is
// what would compute it rather than assert it.
//
// A host built with this placeholder refuses to reconcile and says why, rather than applying
// nothing and reporting success. Building a real host means building it with a real bundle.