not a service A shell, a terminal, a chat client, a desktop are a package plus configuration in somebody's home. A mesh with no notion of a user can own /etc and nothing anybody looks at, which is most of the reason to manage a machine at all. Three shapes, and the vocabulary test asserts the count precisely because widening it widens what a compromised control plane can express: user a login, its shell and its groups archive a set of files, fetched by digest and unpacked (file) gains `bytes` for what is not text, and `owner` `user` also makes "zsh is my login shell" declared state. chsh is a command, the link may not carry one, and a shell settable only by hand is a shell the mesh cannot manage. Groups are additive and never pruned — usermod without --append REPLACES them, which would silently remove every group that makes a login able to use the machine. A machine's own groups are not the mesh's to know about. The archive is the one place this host reaches out on its own; everywhere else it holds one outbound connection and fetches nothing. So it carries the discipline the bootstrap already uses for images: pinned by digest, and the digest checked before a single file is written. Two decisions in the unpacker worth naming: - an entry naming a path outside the archive is REFUSED, not sanitised. Rewriting it to land inside would put a file somewhere nobody asked for and report success. Found by the test: the first version quietly relocated it. - symlinks and device nodes are refused rather than skipped, or an archive that needed one arrives silently incomplete. A partial host does archives and refuses users: an archive needs a filesystem and a way to fetch; a user needs a user database it is allowed to write.
101 lines
4.6 KiB
Go
101 lines
4.6 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 0005).
|
|
//
|
|
// 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 0005). 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 0004 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)
|
|
}
|
|
|
|
// Users are one of the shapes this host refuses.
|
|
//
|
|
// Android's user database belongs to the framework and is not something an ordinary app may
|
|
// write. Refused with a reason rather than attempted, the same as package, service and container
|
|
// above — and the profile says so, so the control plane never sends one.
|
|
func (android) CreateUser(context.Context, Runner, string, string, string) error {
|
|
return fmt.Errorf("this host implements no users: Android's user database belongs to the " +
|
|
"framework and is not writable by an ordinary process")
|
|
}
|
|
|
|
func (android) SetUserShell(context.Context, Runner, string, string) error {
|
|
return fmt.Errorf("this host implements no users")
|
|
}
|
|
|
|
func (android) AddUserToGroup(context.Context, Runner, string, string) error {
|
|
return fmt.Errorf("this host implements no users")
|
|
}
|