Stage 1 — the host reports what a machine is and can do

Tier 0's first slice, per novox/hq 03-DESIGN/01-to-be/05-the-node-host.md. It
applies nothing, connects to nothing, listens on nothing. 2.9 MB, static, no
dynamic dependencies: copy it onto a machine and run it is the whole install,
which is the property ADR 0041 rests on.

A capability is detected, never assumed. Every detector runs something that only
succeeds if the thing FUNCTIONS — the daemon is asked for its version, the
package database is queried, the firewall is asked to list a ruleset, which
needs the privilege as well as the tool. 04-ISSUES/007 is the fault this
prevents: a client on disk with its daemon down looks exactly like a working
runtime, and a node assigned work on that basis fails when the work arrives.

Every verdict carries the reason and the method. A capability reported absent
with no reason is the same fault in a new place: something nobody can act on.

Two bugs found by running rather than reasoning, both silent:

systemctl is-system-running exits non-zero for every state except `running` —
including `degraded`, which means units failed and the init is emphatically
there. Reading the exit code reported NO service manager on a machine whose init
it was. That is 007 in the mirror, and both directions place work wrongly. A
verdict now reads what a tool says about itself, not only how it exited.

And `mesh-host inventory --json` printed text: the standard library stops
parsing at the first non-flag argument, so the flag sat unread and the command
exited 0 having ignored what was asked. The parser now takes the subcommand off
the front, and a stray or mistyped argument is refused rather than dropped.

Detection deliberately does NOT follow ADR 0008. That rule governs applying
state, where a failed step means the machine is not what was asked for. A failed
probe is a finding — "absent, because the probe failed" — and aborting would
replace one legible absence with total ignorance of the rest.

25 tests: structure and logic with a fake runner, and the same detectors against
this machine, because a test that fakes the system under detection asserts only
that the fake behaves as expected.
This commit is contained in:
2026-08-26 00:25:08 +02:00
commit 73c010e7ef
12 changed files with 1257 additions and 0 deletions
+180
View File
@@ -0,0 +1,180 @@
// Command mesh-host is tier 0 of the Novox Mesh: the one thing installed by hand, and the
// only thing that changes a machine.
//
// Stage 1 (novox/hq 03-DESIGN/01-to-be/05-the-node-host.md) is profile and inventory only —
// the host reads what this machine can do and what it is, and reports it. It applies nothing,
// connects to nothing, and listens on nothing.
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"os/signal"
"syscall"
"text/tabwriter"
"time"
"github.com/novox/mesh-host/internal/inventory"
"github.com/novox/mesh-host/internal/profile"
)
// version is stamped at build time. Unset in a development build, and said so rather than
// defaulted to something that looks like a release.
var version = "development build"
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
version
--json machine-readable output
--timeout how long any single probe may take (default 10s)
Stage 1: reports only. It applies nothing, connects to nothing, listens on nothing.
`
func main() {
// A probe runs a command on a real machine. Ctrl-C must stop the host, not be swallowed by
// whatever it is waiting for.
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
command, opts, err := parseArgs(os.Args[1:])
if err == nil {
err = run(ctx, command, opts.json, opts.timeout)
}
if err != nil {
fmt.Fprintf(os.Stderr, "mesh-host: %v\n", err)
os.Exit(1)
}
}
type options struct {
json bool
timeout time.Duration
}
// parseArgs takes the subcommand first, then its flags.
//
// The standard library stops parsing at the first non-flag argument, so `mesh-host inventory
// --json` left `--json` sitting in the positional arguments and printed text — a flag the user
// passed, silently ignored, with a successful exit. That is the fault this whole project keeps
// naming, so the parser takes the subcommand off the front and parses what follows.
func parseArgs(args []string) (string, options, error) {
opts := options{timeout: 10 * time.Second}
command := ""
if len(args) > 0 {
command = args[0]
args = args[1:]
}
set := flag.NewFlagSet("mesh-host", flag.ContinueOnError)
set.SetOutput(os.Stderr)
set.Usage = func() { fmt.Fprint(os.Stderr, usage) }
set.BoolVar(&opts.json, "json", false, "machine-readable output")
set.DurationVar(&opts.timeout, "timeout", opts.timeout, "how long any single probe may take")
if err := set.Parse(args); err != nil {
return "", opts, err
}
// Anything left over was neither the command nor a flag. Refused rather than ignored: a
// mistyped argument that changes nothing and reports success is worse than an error.
if rest := set.Args(); len(rest) > 0 {
return "", opts, fmt.Errorf("unexpected argument %q — try `mesh-host help`", rest[0])
}
return command, opts, nil
}
func run(ctx context.Context, command string, jsonOut bool, timeout time.Duration) error {
switch command {
case "profile":
p := profile.Detect(ctx, profile.Default(nil), timeout)
if jsonOut {
return writeJSON(p)
}
writeProfile(p)
return nil
case "inventory":
inv := inventory.Collect(ctx, nil, profile.Default(nil), timeout)
if jsonOut {
return writeJSON(inv)
}
writeInventory(inv)
return nil
case "version":
fmt.Println(version)
return nil
case "", "help", "-h", "--help":
fmt.Fprint(os.Stderr, usage)
return nil
default:
return fmt.Errorf("unknown command %q — try `mesh-host help`", command)
}
}
func writeJSON(v any) error {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(v)
}
// writeProfile prints every verdict WITH its reason.
//
// The reason is not decoration: a capability reported absent with no reason is something
// nobody can act on, and this is the surface where a person meets that.
func writeProfile(p profile.Profile) {
fmt.Printf("%s/%s\n\n", p.Kernel, p.Architecture)
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
for _, v := range p.Capabilities {
mark := "no "
if v.Present {
mark = "yes"
}
fmt.Fprintf(w, " %s\t%s\t%s\n", mark, v.Name, v.Detail)
}
w.Flush()
if missing := p.Missing(); len(missing) > 0 {
fmt.Printf("\ncannot be asked to: %v\n", missing)
}
}
func writeInventory(inv inventory.Inventory) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "machine\t%s\n", inv.Machine)
if inv.Distribution != "" {
fmt.Fprintf(w, "distribution\t%s\n", inv.Distribution)
}
if inv.Kernel != "" {
fmt.Fprintf(w, "kernel\t%s\n", inv.Kernel)
}
fmt.Fprintf(w, "architecture\t%s/%s\n", inv.OS, inv.Architecture)
fmt.Fprintf(w, "cpus\t%d\n", inv.CPUs)
if inv.MemoryKB > 0 {
fmt.Fprintf(w, "memory\t%d MB\n", inv.MemoryKB/1024)
}
fmt.Fprintf(w, "observed\t%s\n", inv.ObservedAt.Format(time.RFC3339))
w.Flush()
fmt.Println()
writeProfile(inv.Profile)
// Printed last and never hidden. An inventory that quietly omits what it could not read
// is the same fault as a report assembled from intent (novox/hq ADR 0035).
if len(inv.Unreadable) > 0 {
fmt.Println("\ncould not read:")
for _, u := range inv.Unreadable {
fmt.Printf(" %s\n", u)
}
}
}
+83
View File
@@ -0,0 +1,83 @@
package main
import (
"testing"
"time"
)
// Argument handling gets tests because it already failed silently once: `mesh-host inventory
// --json` printed text. The standard library stops parsing at the first non-flag argument, so
// the flag sat unread in the positional arguments and the command exited 0 having ignored what
// the user asked for.
//
// Silently doing something other than what was asked, and reporting success, is the fault this
// project exists to name — so it gets defended here rather than remembered.
func TestAFlagAfterTheCommandIsRead(t *testing.T) {
command, opts, err := parseArgs([]string{"inventory", "--json"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if command != "inventory" {
t.Errorf("command = %q, want inventory", command)
}
if !opts.json {
t.Error("--json after the subcommand was ignored")
}
}
func TestFlagsAreReadInEitherPosition(t *testing.T) {
for _, args := range [][]string{
{"profile", "--json", "--timeout", "3s"},
{"profile", "--timeout=3s", "--json"},
} {
_, opts, err := parseArgs(args)
if err != nil {
t.Fatalf("%v: unexpected error: %v", args, err)
}
if !opts.json || opts.timeout != 3*time.Second {
t.Errorf("%v parsed as json=%v timeout=%s", args, opts.json, opts.timeout)
}
}
}
func TestAMistypedFlagIsRefusedNotIgnored(t *testing.T) {
// The cost of getting this wrong is asymmetric: an error is a moment's annoyance, and a
// silently dropped flag is a report that answers a question nobody asked.
if _, _, err := parseArgs([]string{"profile", "--jsom"}); err == nil {
t.Fatal("a mistyped flag was accepted")
}
}
func TestAnUnexpectedArgumentIsRefused(t *testing.T) {
if _, _, err := parseArgs([]string{"profile", "extra"}); err == nil {
t.Fatal("a stray argument was ignored rather than refused")
}
}
func TestTheDefaultsAreTheDocumentedOnes(t *testing.T) {
// The usage text promises 10s. A default that drifts from what is printed is a small lie
// that costs someone an afternoon.
_, opts, err := parseArgs([]string{"profile"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if opts.timeout != 10*time.Second {
t.Errorf("default timeout is %s; the usage text says 10s", opts.timeout)
}
if opts.json {
t.Error("json output is on by default; the usage text says it is a flag")
}
}
func TestNoCommandIsNotAnError(t *testing.T) {
// Running the binary with no arguments prints usage and exits 0. A host that returns
// failure for "tell me what you do" is noise in every script that probes it.
command, _, err := parseArgs(nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if command != "" {
t.Errorf("command = %q, want empty", command)
}
}