From 76224b028db12baf7ab4b25f1fc99b00012a2832 Mon Sep 17 00:00:00 2001 From: Admin <1+admin@noreply.git.dcntrlized.org> Date: Sun, 23 Aug 2026 11:48:23 +0100 Subject: [PATCH] fix(instance): never serve a container-id hostname as the instance name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit os.Hostname() inside the Maestro container is the bare container id (e.g. 6079761d6e4d) — a hex-only hostname now falls back to 'maestro' so the public profile stays meaningful out of the box. DCNTRLIZED_INSTANCE_NAME and instance.json still win. --- src/lib/instance/instance.go | 29 ++++++++-- src/lib/instance/instance_test.go | 91 +++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 src/lib/instance/instance_test.go diff --git a/src/lib/instance/instance.go b/src/lib/instance/instance.go index a3bbe83..6fb9282 100644 --- a/src/lib/instance/instance.go +++ b/src/lib/instance/instance.go @@ -83,12 +83,31 @@ func Save(dataRoot string, id Identity) error { return nil } +// osHostname is swapped in tests. +var osHostname = os.Hostname + func defaultIdentity() Identity { - name := os.Getenv("DCNTRLIZED_INSTANCE_NAME") - if name == "" { - if host, err := os.Hostname(); err == nil { - name = host + if name := os.Getenv("DCNTRLIZED_INSTANCE_NAME"); name != "" { + return Identity{Name: name} + } + // Maestro normally runs containerized, where os.Hostname() is the bare + // container id — useless (and mildly leaking) as a public display name. + if host, err := osHostname(); err == nil && !looksLikeContainerID(host) { + return Identity{Name: host} + } + return Identity{Name: "maestro"} +} + +// looksLikeContainerID reports whether s is a hex-only Docker-style container +// id or short id. +func looksLikeContainerID(s string) bool { + if len(s) < 6 { + return false + } + for _, r := range s { + if !(r >= '0' && r <= '9' || r >= 'a' && r <= 'f' || r >= 'A' && r <= 'F') { + return false } } - return Identity{Name: name} + return true } diff --git a/src/lib/instance/instance_test.go b/src/lib/instance/instance_test.go new file mode 100644 index 0000000..feb1ae3 --- /dev/null +++ b/src/lib/instance/instance_test.go @@ -0,0 +1,91 @@ +// Copyright (C) 2026 Dcntrlized +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package instance + +import ( + "os" + "path/filepath" + "testing" +) + +func TestLoadPrefersFileOverEnv(t *testing.T) { + dir := t.TempDir() + if err := Save(dir, Identity{Name: "home-node", Title: "Home"}); err != nil { + t.Fatal(err) + } + t.Setenv("DCNTRLIZED_INSTANCE_NAME", "from-env") + + id := Load(dir) + if id.Name != "home-node" || id.Title != "Home" { + t.Fatalf("got %+v", id) + } +} + +func TestDefaultIdentityFallbacks(t *testing.T) { + orig := osHostname + defer func() { osHostname = orig }() + + t.Run("env wins", func(t *testing.T) { + t.Setenv("DCNTRLIZED_INSTANCE_NAME", "edge-1") + if id := defaultIdentity(); id.Name != "edge-1" { + t.Fatalf("got %q", id.Name) + } + }) + + t.Run("hostname used when human-readable", func(t *testing.T) { + os.Unsetenv("DCNTRLIZED_INSTANCE_NAME") + osHostname = func() (string, error) { return "archlinux", nil } + if id := defaultIdentity(); id.Name != "archlinux" { + t.Fatalf("got %q", id.Name) + } + }) + + t.Run("container-id hostname replaced", func(t *testing.T) { + os.Unsetenv("DCNTRLIZED_INSTANCE_NAME") + for _, host := range []string{"6079761d6e4d", "6079761d6e4dabcdef1234567890abcdef1234567890abcdef1234567890abcd"} { + osHostname = func() (string, error) { return host, nil } + if id := defaultIdentity(); id.Name != "maestro" { + t.Fatalf("host %q: got %q", host, id.Name) + } + } + }) +} + +func TestLooksLikeContainerID(t *testing.T) { + cases := map[string]bool{ + "6079761d6e4d": true, + "ABCDEF0123456789abcdef0123456789": true, + "archlinux": false, + "maestro-1": false, + "abc": false, + "": false, + } + for s, want := range cases { + if got := looksLikeContainerID(s); got != want { + t.Errorf("looksLikeContainerID(%q) = %v, want %v", s, got, want) + } + } +} + +func TestSaveRejectsEmptyName(t *testing.T) { + dir := t.TempDir() + if err := Save(dir, Identity{}); err == nil { + t.Fatal("expected error for empty name") + } + if _, err := os.Stat(filepath.Join(dir, "instance.json")); !os.IsNotExist(err) { + t.Fatal("no file should be written for an invalid identity") + } +}