Files
mesh-host/internal/system/android.go
T
jschoubben ebba16ce4a 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.
2026-08-28 01:24:06 +02:00

83 lines
3.8 KiB
Go

package system
import (
"context"
"fmt"
"github.com/novox/mesh-host/internal/declaration"
)
// android is a partial host, and being partial is the point.
//
// It implements `file`, `directory` and `action` — the shapes that need only a filesystem and a
// way to run something — and refuses the other three. That is not a broken host: a declaration
// naming a shape this host does not implement is refused whole, the same treatment an unknown
// type gets, and the profile tells the control plane which shapes exist so it never sends one
// it cannot do (novox/hq ADR 0060).
//
// What it cannot do, and why:
//
// - **package** — there is no package manager an ordinary app may drive. Installing software
// on Android means the framework installing an APK, which is not something a process asks
// for on its own behalf.
// - **service** — Android's init reads .rc files from the system partition, which needs root
// and an unlocked bootloader. On a normal device nothing can register with it.
// - **container** — no container runtime, and no kernel access to give one.
//
// **This host is EPISODIC** (novox/hq ADR 0062). Everywhere else an init runs the launcher at
// boot and the launcher supervises the host. Android grants neither: nothing to register with
// without root, and nothing worth supervising, because a supervisor would be killed alongside
// what it supervises.
//
// So it runs when the platform allows and is killed when the platform wants the memory — and
// that is **disconnection**, which ADR 0036 already made an ordinary situation rather than an
// exception. It needs no keep-alive and no new mechanism: the store is already authoritative
// while disconnected, reconcile already happens on start, and the mesh already reports *last
// heard from* rather than alarming on silence.
//
// It also **cannot be the first node** — every step of raising a substrate is a shape it
// refuses — and its bundle says so rather than being an empty placeholder.
type android struct{}
func (android) Name() string { return "android" }
func (android) Shapes() []declaration.Type { return portableShapes() }
func (android) Confirm(ctx context.Context, run Runner) error {
// Ask the property service, which exists on every Android and nowhere else. A file path
// check would pass inside a chroot; this asks something only Android answers.
if _, err := run(ctx, "getprop", "ro.build.version.sdk"); err != nil {
return fmt.Errorf(
"this is the android host and the property service does not answer here. Either "+
"this is not Android, or it is a container without it: %w", err)
}
return nil
}
// The four below are unreachable through the ordinary path: Check refuses a declaration naming
// these shapes before anything is applied. They are here so that "unreachable" fails loudly if
// it ever stops being true, rather than a nil applier being called.
func (a android) PackageInstalled(context.Context, Runner, string) (bool, error) {
return false, fmt.Errorf("%w: package (there is no package manager an app may drive)", ErrUnsupported)
}
func (a android) InstallPackage(context.Context, Runner, string) error {
return fmt.Errorf("%w: package", ErrUnsupported)
}
func (a android) ServiceState(context.Context, Runner, string) (string, error) {
return "", fmt.Errorf("%w: service (init is not reachable without root)", ErrUnsupported)
}
func (a android) SetServiceState(context.Context, Runner, string, string) error {
return fmt.Errorf("%w: service", ErrUnsupported)
}
func (a android) ServiceBoot(context.Context, Runner, string) (string, error) {
return "", fmt.Errorf("%w: service", ErrUnsupported)
}
func (a android) SetServiceBoot(context.Context, Runner, string, string) error {
return fmt.Errorf("%w: service", ErrUnsupported)
}