Per-system bundles, and Android's start problem closed by narrowing it
Two gaps. The bundle's contents are per system even though its mechanism is not, so there are now three: substrate-arch.lock, substrate-alpine.lock and substrate-android.lock. All three are embedded and a host reads only the one it was built for. Arch and Alpine remain placeholders -- the closure for a one-node mesh is still research 011/012's open question, and inventing it here would be worse than an honest placeholder. Android's is not a placeholder. It says a partial host cannot raise a mesh and why: every step of a bootstrap is a package, a container, or an action against one, and those are exactly the shapes it refuses. So a partial host can JOIN a mesh and cannot BE the first node. That belongs where somebody looking for the android bundle will find it. Also separated two things that were being conflated: "this system has no bundle" and "this system was never built". Loading a bundle for debian is not ErrEmpty, and the test asserts they differ. 0062 -- a host may be episodic. There is no way to keep a process running on an ordinary Android device: init needs root, a foreground service can be killed for memory. The answer is not to fight that. It is that being killed IS disconnection, which ADR 0036 already made an ordinary situation -- and everything the design does for a laptop that closes is what an episodic host needs, at a shorter period. An authoritative local store, reconcile on start, last-heard-from reported without an alarm. So the gap closes by requiring less rather than building something. No keep- alive, no Android daemon, no fighting the platform's process management. Two consequences recorded rather than glossed. Last-heard-from is a much weaker signal on an episodic host, so a healthy phone reads as a dead server unless the reader knows which kind it is looking at. And a declaration may take a long time to land, which makes 0058's separation of outstanding from failed load-bearing rather than tidy. Left open deliberately: how an episodic host is actually started, and -- first -- what an Android node is for. Building the start mechanism before deciding that would be building it for nobody.
This commit is contained in:
+34
-15
@@ -12,33 +12,49 @@ package bundle
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/novox/mesh-host/internal/declaration"
|
||||
)
|
||||
|
||||
// substrate is the pinned tier-1 descriptor, appliable with no mesh present.
|
||||
// One bundle per operating system, because its CONTENTS are per system even though its
|
||||
// mechanism is not: package names, unit names and service names all differ
|
||||
// (novox/hq ADR 0060). All three are embedded and the host applies the one it was built for —
|
||||
// an arch host never reads the alpine bundle.
|
||||
//
|
||||
// A host built without one carries the placeholder below, and says so rather than applying
|
||||
// A host whose bundle is only comments carries nothing, 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
|
||||
//go:embed substrate-arch.lock
|
||||
var archLock []byte
|
||||
|
||||
// ErrEmpty means this host was built without a bundle.
|
||||
//go:embed substrate-alpine.lock
|
||||
var alpineLock []byte
|
||||
|
||||
//go:embed substrate-android.lock
|
||||
var androidLock []byte
|
||||
|
||||
var locks = map[string][]byte{
|
||||
"arch": archLock,
|
||||
"alpine": alpineLock,
|
||||
"android": androidLock,
|
||||
}
|
||||
|
||||
// ErrEmpty means this host carries no 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")
|
||||
"this host carries no bundle. A host 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 }
|
||||
func Raw(system string) []byte { return locks[system] }
|
||||
|
||||
// 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") {
|
||||
// empty for this purpose: a placeholder is a comment, and treating it as content would mean a
|
||||
// host claims to carry a substrate it does not.
|
||||
func IsEmpty(system string) bool {
|
||||
for _, line := range strings.Split(string(locks[system]), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" && !strings.HasPrefix(line, "//") {
|
||||
return false
|
||||
@@ -52,14 +68,17 @@ func IsEmpty() bool {
|
||||
// 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() {
|
||||
func Load(system string) (*declaration.Declaration, error) {
|
||||
if _, known := locks[system]; !known {
|
||||
return nil, fmt.Errorf("no bundle is built for %q", system)
|
||||
}
|
||||
if IsEmpty(system) {
|
||||
return nil, ErrEmpty
|
||||
}
|
||||
// ParseTrusted: the bundle arrives with the binary, so it may carry actions the link may
|
||||
// not (novox/hq ADR 0047). The bootstrap needs them — creating the control plane's database
|
||||
// happens before there is any mesh to ask for one.
|
||||
return declaration.ParseTrusted(stripComments(substrate))
|
||||
return declaration.ParseTrusted(stripComments(locks[system]))
|
||||
}
|
||||
|
||||
// stripComments removes whole-line `//` comments so a bundle can be annotated.
|
||||
|
||||
@@ -15,10 +15,10 @@ 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() {
|
||||
if !IsEmpty("arch") {
|
||||
t.Fatal("the default build claims to carry a substrate")
|
||||
}
|
||||
_, err := Load()
|
||||
_, err := Load("arch")
|
||||
if !errors.Is(err, ErrEmpty) {
|
||||
t.Fatalf("an empty bundle did not refuse: %v", err)
|
||||
}
|
||||
@@ -80,3 +80,34 @@ func TestWhatValidatesIsWhatIsApplied(t *testing.T) {
|
||||
func parseFor(raw []byte) (any, error) {
|
||||
return declarationParse(stripComments(raw))
|
||||
}
|
||||
|
||||
func TestEverySystemHasABundleAndAndroidsRefuses(t *testing.T) {
|
||||
// The bundle's contents are per system even though its mechanism is not (novox/hq ADR
|
||||
// 0060), so a host must find one built for it — and a host built for a system with no
|
||||
// bundle at all must say that rather than behave like an empty one.
|
||||
for _, system := range []string{"arch", "alpine", "android"} {
|
||||
if _, err := Load(system); err == nil {
|
||||
t.Errorf("%s: a placeholder bundle loaded as if it had contents", system)
|
||||
} else if !errors.Is(err, ErrEmpty) {
|
||||
t.Errorf("%s: refused for the wrong reason: %v", system, err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := Load("debian"); err == nil {
|
||||
t.Error("a bundle was loaded for a system nobody has built")
|
||||
} else if errors.Is(err, ErrEmpty) {
|
||||
t.Error("an unbuilt system was reported as an empty bundle; those are different things")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAndroidsBundleSaysWhyThereIsNone(t *testing.T) {
|
||||
// Not a placeholder waiting to be filled in. An android host implements neither `package`
|
||||
// nor `container` nor `service`, so every step of the bootstrap is a shape it does not
|
||||
// have — a partial host can JOIN a mesh and cannot BE the first node.
|
||||
text := string(Raw("android"))
|
||||
for _, want := range []string{"cannot raise a mesh", "JOIN", "first node"} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Errorf("the android bundle does not explain itself; missing %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// substrate-alpine.lock — the pinned tier-1 descriptor the ALPINE host carries.
|
||||
//
|
||||
// Per system, because its CONTENTS are: this one names apk packages and OpenRC services where
|
||||
// the arch bundle names pacman packages and systemd units (novox/hq ADR 0060).
|
||||
//
|
||||
// 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.
|
||||
@@ -0,0 +1,12 @@
|
||||
// substrate-android.lock — deliberately not a bundle.
|
||||
//
|
||||
// An android host cannot raise a mesh, and this file says so rather than being an empty
|
||||
// placeholder waiting to be filled in.
|
||||
//
|
||||
// The substrate is a container runtime, a store and the control plane (novox/hq
|
||||
// 07-the-substrate.md). An android host implements `file`, `directory` and `action` and refuses
|
||||
// `package`, `container` and `service` (ADR 0060) — so every step of the bootstrap is a shape it
|
||||
// does not have. No amount of filling this in changes that.
|
||||
//
|
||||
// **A partial host can JOIN a mesh and cannot BE the first node.** That is a real distinction
|
||||
// and it belongs here, where somebody looking for the android bundle will find it.
|
||||
@@ -1,4 +1,7 @@
|
||||
// substrate.lock — the pinned tier-1 descriptor this host carries.
|
||||
// substrate-arch.lock — the pinned tier-1 descriptor the ARCH host carries.
|
||||
//
|
||||
// Per system, because its CONTENTS are: package names, unit names and service names all differ
|
||||
// (novox/hq ADR 0060). The mechanism is shared; what it names is not.
|
||||
//
|
||||
// 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
|
||||
Reference in New Issue
Block a user