From 138d9afd7b9f2231cc8c6019651451aa52f54819 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 5 Oct 2026 23:43:14 +0200 Subject: [PATCH] Rename hosts to hostname, which also writes /etc/hostname (hq ADR 0223) Two files say one fact, the machine's name, and nothing owned /etc/hostname. The name written is the operator's hostname setting, with no default: three of four machines call themselves something other than their mesh name, and renaming one is the operator's call. It takes effect at the next boot. --- modules/hostname/README.md | 34 +++++++++++++ .../cmd/hostname-tools/hostsfile.go} | 4 +- .../cmd/hostname-tools/hostsfile_test.go} | 39 ++++++++++++--- .../cmd/hostname-tools}/main.go | 16 ++++--- modules/{hosts => hostname}/go.mod | 2 +- modules/{hosts => hostname}/go.sum | 0 modules/hostname/module.json | 48 +++++++++++++++++++ modules/hosts/module.json | 41 ---------------- 8 files changed, 127 insertions(+), 57 deletions(-) create mode 100644 modules/hostname/README.md rename modules/{hosts/cmd/hosts-tools/hosts.go => hostname/cmd/hostname-tools/hostsfile.go} (98%) rename modules/{hosts/cmd/hosts-tools/hosts_test.go => hostname/cmd/hostname-tools/hostsfile_test.go} (87%) rename modules/{hosts/cmd/hosts-tools => hostname/cmd/hostname-tools}/main.go (79%) rename modules/{hosts => hostname}/go.mod (78%) rename modules/{hosts => hostname}/go.sum (100%) create mode 100644 modules/hostname/module.json delete mode 100644 modules/hosts/module.json diff --git a/modules/hostname/README.md b/modules/hostname/README.md new file mode 100644 index 0000000..729eab4 --- /dev/null +++ b/modules/hostname/README.md @@ -0,0 +1,34 @@ +# hostname + +The machine's names (novox/hq ADR 0199, ADR 0223): it holds the node seat `node-hostname` and owns +the two files that say what a machine is called — `/etc/hostname` and the machine's own lines in +`/etc/hosts`. It was `hosts`, holding `node-hosts-file`; the seat was renamed, and the old name +resolves to it as an alias. + +## What it writes + +- **`/etc/hostname`, whole**: the module's `hostname` setting, and nothing else. There is no + default. A machine's name is the operator's: the mesh's name for a machine and the name it calls + itself need not be the same, and writing the mesh's name silently would rename a machine. Set it + per machine — `settings set hostname '{"hostname": ""}' --node ` — before the module + is assigned there; without it the module is left out of that machine's declaration, naming the + key. A mesh-wide `{"hostname": "${machine:name}"}` makes every machine call itself by its mesh + name, and a machine's own setting still overrides it. +- **The machine's own lines in `/etc/hosts`**, as the mesh's marked region at the start of the file: + `localhost` and `127.0.1.1` with the machine's mesh name. Every other line is the operator's, kept + byte for byte and given back when the module goes. + +## When a new name takes effect + +At the machine's **next boot**. The kernel's name is set from `/etc/hostname` when the machine +starts; writing the file changes what `hostnamectl` reports as the static name and nothing that is +running. The module declares nothing that would set it live: a graphical session's X authority is +keyed by the name the session started under, so changing it underneath a running session refuses +every new window until the person logs in again. Reboot when that is acceptable, or run +`hostnamectl hostname ` by hand. + +## Its verbs + +`/node-hostname.entries`, `.add` and `.remove`: the hosts file's lines, each marked whose it +is; add one address and its names to the operator's lines; remove a name or an address from them. +They change the machine's file and nothing else. `/etc/hostname` has no verb — it is the setting. diff --git a/modules/hosts/cmd/hosts-tools/hosts.go b/modules/hostname/cmd/hostname-tools/hostsfile.go similarity index 98% rename from modules/hosts/cmd/hosts-tools/hosts.go rename to modules/hostname/cmd/hostname-tools/hostsfile.go index d25507e..1a38131 100644 --- a/modules/hosts/cmd/hosts-tools/hosts.go +++ b/modules/hostname/cmd/hostname-tools/hostsfile.go @@ -72,7 +72,7 @@ func execRunner(ctx context.Context, name string, args ...string) (string, error type Line struct { // Text is the line exactly as it is in the file. Text string `json:"text"` - // Owner is whose it is: the block's id (`mesh hosts.own`, or another tool's) or "operator". + // Owner is whose it is: the block's id (`mesh hostname.own`, or another tool's) or "operator". Owner string `json:"owner"` // Address and Names are an entry's; absent for a comment or a blank line. Address string `json:"address,omitempty"` @@ -319,7 +319,7 @@ func (h HostsFile) write(ctx context.Context, content string) error { if err := os.WriteFile(staged, []byte(content), 0o644); err != nil { return err } - beside := filepath.Join(filepath.Dir(h.Path), "."+filepath.Base(h.Path)+".hosts-tools") + beside := filepath.Join(filepath.Dir(h.Path), "."+filepath.Base(h.Path)+".hostname-tools") if _, err := h.Run(ctx, "install", "-m", "0644", staged, beside); err != nil { return err } diff --git a/modules/hosts/cmd/hosts-tools/hosts_test.go b/modules/hostname/cmd/hostname-tools/hostsfile_test.go similarity index 87% rename from modules/hosts/cmd/hosts-tools/hosts_test.go rename to modules/hostname/cmd/hostname-tools/hostsfile_test.go index 03fea54..b8562b6 100644 --- a/modules/hosts/cmd/hosts-tools/hosts_test.go +++ b/modules/hostname/cmd/hostname-tools/hostsfile_test.go @@ -17,10 +17,10 @@ import ( const file = "# Static table lookup for hostnames.\n" + "127.0.0.1\tlocaldev.example.com\n" + "127.0.0.1 a.example.com b.example.com\n" + - "# BEGIN mesh hosts.own\n" + + "# BEGIN mesh hostname.own\n" + "127.0.0.1\tlocalhost\n" + "::1\tlocalhost\n" + - "# END mesh hosts.own\n" + + "# END mesh hostname.own\n" + "# BEGIN other-tool\n" + "192.0.2.7\tproject.test\n" + "# END other-tool\n" @@ -47,7 +47,7 @@ func TestEveryLineSaysWhoseItIs(t *testing.T) { if lines[0].Address != "" || lines[0].Owner != Operator { t.Errorf("a comment is the operator's and no entry: %+v", lines[0]) } - if lines[3].Owner != "mesh hosts.own" || lines[4].Owner != "mesh hosts.own" || lines[6].Owner != "mesh hosts.own" { + if lines[3].Owner != "mesh hostname.own" || lines[4].Owner != "mesh hostname.own" || lines[6].Owner != "mesh hostname.own" { t.Errorf("the mesh's block, its markers included: %+v", lines[3:7]) } if lines[8].Owner != "other-tool" || !reflect.DeepEqual(lines[8].Names, []string{"project.test"}) { @@ -146,7 +146,7 @@ func TestRemoveTakesOneNameOrOneAddressAndBlocksStayByteForByte(t *testing.T) { if err != nil || n != 2 { t.Fatalf("%d %v", n, err) } - if !strings.Contains(all, "# BEGIN mesh hosts.own\n127.0.0.1\tlocalhost\n") { + if !strings.Contains(all, "# BEGIN mesh hostname.own\n127.0.0.1\tlocalhost\n") { t.Errorf("the mesh's own localhost is not the operator's to remove: %q", all) } if !reflect.DeepEqual(blocks(all), blocks(file)) { @@ -158,7 +158,7 @@ func TestRemoveRefusesANameOnlyABlockWritesNamingWhose(t *testing.T) { if _, _, err := WithRemoved(file, "project.test"); err == nil || !strings.Contains(err.Error(), "written by other-tool") { t.Errorf("%v", err) } - if _, _, err := WithRemoved(file, "::1"); err == nil || !strings.Contains(err.Error(), "written by mesh hosts.own") { + if _, _, err := WithRemoved(file, "::1"); err == nil || !strings.Contains(err.Error(), "written by mesh hostname.own") { t.Errorf("%v", err) } if _, n, err := WithRemoved(file, "nowhere.test"); err != nil || n != 0 { @@ -200,7 +200,7 @@ func TestTheVerbsWriteTheFileWholeBesideItAndRenameItOver(t *testing.T) { if err != nil || !added.Added || added.Line != "192.0.2.9\tlab.test" { t.Fatalf("%+v %v", added, err) } - beside := filepath.Join(dir, ".hosts.hosts-tools") + beside := filepath.Join(dir, ".hosts.hostname-tools") if len(calls) != 2 || calls[0][0] != "install" || calls[0][len(calls[0])-1] != beside || !reflect.DeepEqual(calls[1], []string{"mv", "-f", beside, path}) { t.Errorf("%v", calls) @@ -284,3 +284,30 @@ func TestNamesAreSplitOnSpacesAndCommasOrTakenAsAList(t *testing.T) { t.Errorf("%v", got) } } + +// /etc/hostname is the module's whole file (novox/hq ADR 0223), and what it says is the operator's +// `hostname` setting — never the mesh's name for the machine written silently: on the mesh this was +// built for, three of four machines call themselves something else, and renaming a machine is the +// operator's to decide. +func TestTheMachinesNameIsTheOperatorsSetting(t *testing.T) { + raw, err := os.ReadFile("../../module.json") + if err != nil { + t.Fatal(err) + } + var m struct { + Resources []map[string]any `json:"resources"` + } + if err := json.Unmarshal(raw, &m); err != nil { + t.Fatal(err) + } + for _, r := range m.Resources { + if r["path"] != "/etc/hostname" { + continue + } + if r["id"] != "name" || r["into"] != nil || r["content"] != "${setting:hostname}\n" { + t.Errorf("/etc/hostname is not written whole from the hostname setting: %v", r) + } + return + } + t.Error("the module does not write /etc/hostname") +} diff --git a/modules/hosts/cmd/hosts-tools/main.go b/modules/hostname/cmd/hostname-tools/main.go similarity index 79% rename from modules/hosts/cmd/hosts-tools/main.go rename to modules/hostname/cmd/hostname-tools/main.go index b93f13e..0fd678c 100644 --- a/modules/hosts/cmd/hosts-tools/main.go +++ b/modules/hostname/cmd/hostname-tools/main.go @@ -1,7 +1,9 @@ -// hosts-tools (novox/hq ADR 0199): the hosts file's tools. One binary, launched by the machine's tool -// runtime and speaking MCP to it over stdio through the Go SDK (ADR 0193, ADR 0198): the -// node-hosts-file seat's three verbs — the file's lines with whose each is, add an operator's line, -// remove one. They change the machine's file and nothing else; the controller holds none of it. +// hostname-tools (novox/hq ADR 0199, ADR 0223): the tools of the machine's names. One binary, +// launched by the machine's tool runtime and speaking MCP to it over stdio through the Go SDK (ADR +// 0193, ADR 0198): the node-hostname seat's three verbs — the hosts file's lines with whose each +// is, add an operator's line, remove one. They change the machine's file and nothing else; the +// controller holds none of it. /etc/hostname has no verb: it is the module's resource, set by the +// module's `hostname` setting. // // stdout is the MCP channel; everything this module says, it says on stderr. package main @@ -17,11 +19,11 @@ import ( ) // Seat is the role this module holds. -const Seat = "node-hosts-file" +const Seat = "node-hostname" func main() { if err := stdio.Serve("", tools(HostsFile{Path: HostsPath, Run: execRunner})); err != nil { - fmt.Fprintf(os.Stderr, "[hosts] %v\n", err) + fmt.Fprintf(os.Stderr, "[hostname] %v\n", err) os.Exit(1) } } @@ -61,7 +63,7 @@ func namesArg(a map[string]any) []string { } // verb is one of the seat's verbs: listed as `.`, so the runtime serves it on the seat's -// subject, as /node-hosts-file.. +// subject, as /node-hostname.. func verb(name, description string, input map[string]any, run func(a map[string]any) (any, error)) stdio.Tool { return stdio.Tool{Name: Seat + "." + name, Description: description, Input: input, Run: run} } diff --git a/modules/hosts/go.mod b/modules/hostname/go.mod similarity index 78% rename from modules/hosts/go.mod rename to modules/hostname/go.mod index 0b34892..25f2d02 100644 --- a/modules/hosts/go.mod +++ b/modules/hostname/go.mod @@ -1,4 +1,4 @@ -module hosts +module hostname go 1.25.0 diff --git a/modules/hosts/go.sum b/modules/hostname/go.sum similarity index 100% rename from modules/hosts/go.sum rename to modules/hostname/go.sum diff --git a/modules/hostname/module.json b/modules/hostname/module.json new file mode 100644 index 0000000..bd605df --- /dev/null +++ b/modules/hostname/module.json @@ -0,0 +1,48 @@ +{ + "module": "hostname", + "version": "1", + "claims": [ + { + "name": "node-hostname", + "scope": "node", + "serves": [ + "entries", + "add", + "remove" + ] + } + ], + "resources": [ + { + "id": "own", + "type": "file", + "path": "/etc/hosts", + "mode": "0644", + "into": "block", + "at": "start", + "content": "# The machine's own names (module hostname, novox/hq ADR 0199, ADR 0223). Every line outside this\n# block is the operator's: kept across every push, changed through the node-hostname verbs add and\n# remove, and given back when this module goes. The mesh's names are not here: the mesh's resolver\n# answers them.\n127.0.0.1\tlocalhost\n::1\tlocalhost\n127.0.1.1\t${machine:name}\n" + }, + { + "id": "name", + "type": "file", + "path": "/etc/hostname", + "mode": "0644", + "content": "${setting:hostname}\n" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/hostname-tools", + "binary": "hostname-tools", + "loads": [ + "hostname-tools" + ] + } + ] + } +} diff --git a/modules/hosts/module.json b/modules/hosts/module.json deleted file mode 100644 index 0319dd5..0000000 --- a/modules/hosts/module.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "module": "hosts", - "version": "1", - "claims": [ - { - "name": "node-hosts-file", - "scope": "node", - "serves": [ - "entries", - "add", - "remove" - ] - } - ], - "resources": [ - { - "id": "own", - "type": "file", - "path": "/etc/hosts", - "mode": "0644", - "into": "block", - "at": "start", - "content": "# The machine's own names (module hosts, novox/hq ADR 0199). Every line outside this block is the\n# operator's: kept across every push, changed through the node-hosts-file verbs add and remove, and\n# given back when this module goes. The mesh's names are not here: the mesh's resolver answers them.\n127.0.0.1\tlocalhost\n::1\tlocalhost\n127.0.1.1\t${machine:name}\n" - } - ], - "build": { - "artifacts": [ - { - "name": "tools", - "kind": "bundle", - "language": "go", - "system": "arch", - "from": "cmd/hosts-tools", - "binary": "hosts-tools", - "loads": [ - "hosts-tools" - ] - } - ] - } -}