carlos / platform Public

hostbox: agent inventory paths by allow-list, not a reserved-name deny-list (CI on d52ea9a9)

A nested host may name its own state container or paths inside it. A flat
host may name its database, its SQLite companions and its host-named state
directory, the last only when the host name cannot collide with a root
namespace or another host's database (flatStateNameSafe). The raw
.instances literal and the record-census flag go away with the deny-list.
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 5ba2323264a9067b81f6c9eda6a79861be72a82d parent d52ea9a
3 files changed, +116 −23
  • internal/host/plan9fix2_inventory_linux_test.go +4 −4
  • internal/hostbox/inventoryscope.go +30 −6
  • internal/hostbox/plan9fix2_inventory_test.go +82 −13

CI passed — run details

diff --git a/internal/host/plan9fix2_inventory_linux_test.go b/internal/host/plan9fix2_inventory_linux_test.go
index 1a5e0a7ae..aab0fe7ce 100644
--- a/internal/host/plan9fix2_inventory_linux_test.go
+++ b/internal/host/plan9fix2_inventory_linux_test.go
@@ -106,7 +106,7 @@ func TestR2InventoryNamesForeignArtifact(t *testing.T) {
t.Error("foreign inventory became durable")
}
in.Entries = in.Entries[:1]
- in.Entries = append(in.Entries, hostbox.StopInventoryEntry{Kind: "path", ManagedRoot: "data", RelativePath: filepath.Join("instances", in.Host), ArtifactID: "state", Ownership: row.ClaimOwner, Materialization: "intended"})
+ in.Entries = append(in.Entries, hostbox.StopInventoryEntry{Kind: "path", ManagedRoot: "data", RelativePath: filepath.Join("instances", in.Host, in.Host+".db"), ArtifactID: "state", Ownership: row.ClaimOwner, Materialization: "intended"})
if e = remote.WriteStopInventory(t.Context(), in); e != nil {
t.Fatal(e)
}
@@ -142,14 +142,14 @@ func TestPlan9Fix2RetireOwnStateAndRecoverArchivedRoute(t *testing.T) {
t.Fatal(err)
}
var st syscall.Stat_t
- if err = syscall.Lstat(state, &st); err != nil {
+ if err = syscall.Lstat(row.DBPath, &st); err != nil {
t.Fatal(err)
}
in, err := root.Reconcile.Inventory.ReadExactStopInventory(t.Context(), target)
if err != nil {
t.Fatal(err)
}
- in.Entries = append(in.Entries, hostbox.StopInventoryEntry{Kind: "path", ArtifactID: "state", ManagedRoot: "data", RelativePath: filepath.Join("instances", target.Host), Ownership: row.ClaimOwner, Materialization: "materialized", Device: uint64(st.Dev), Inode: st.Ino, Type: st.Mode & syscall.S_IFMT})
+ in.Entries = append(in.Entries, hostbox.StopInventoryEntry{Kind: "path", ArtifactID: "state", ManagedRoot: "data", RelativePath: filepath.Join("instances", target.Host, target.Host+".db"), Ownership: row.ClaimOwner, Materialization: "materialized", Device: uint64(st.Dev), Inode: st.Ino, Type: st.Mode & syscall.S_IFMT})
if err = root.Reconcile.Inventory.WriteStopInventory(t.Context(), in); err != nil {
t.Fatal(err)
}
@@ -158,7 +158,7 @@ func TestPlan9Fix2RetireOwnStateAndRecoverArchivedRoute(t *testing.T) {
if !errors.Is(first, ErrClaimStopIdentity) {
t.Fatalf("own-state retirement: %v", first)
}
- if _, err = os.Stat(state); !errors.Is(err, os.ErrNotExist) {
+ if _, err = os.Stat(row.DBPath); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("own state not parked: %v", err)
}
if row, err = reg.Get(target.Host); err != nil || row != nil {
diff --git a/internal/hostbox/inventoryscope.go b/internal/hostbox/inventoryscope.go
index 71746c434..2a3c7d956 100644
--- a/internal/hostbox/inventoryscope.go
+++ b/internal/hostbox/inventoryscope.go
@@ -45,8 +45,7 @@ func ValidateAgentInventory(in StopInventory, row *registry.Route, dataDir strin
if row.DBPath != provision.NestedDBPath(dataDir, in.Host) && row.DBPath != provision.DefaultDBPath(dataDir, in.Host) {
return ErrTypedInventory
}
- first := strings.SplitN(e.RelativePath, string(filepath.Separator), 2)[0]
- if first == ".parked" || first == "hostguard" || first == ".hostclaims" || first == ".hostexec" || first == ".instances" || strings.HasPrefix(first, "registry.db") {
+ if filepath.Clean(e.RelativePath) != e.RelativePath || !filepath.IsLocal(e.RelativePath) {
return ErrTypedInventory
}
parent, err := filepath.Rel(dataDir, filepath.Dir(row.DBPath))
@@ -54,16 +53,23 @@ func ValidateAgentInventory(in StopInventory, row *registry.Route, dataDir strin
return ErrTypedInventory
}
if parent == "." {
- if first == "instances" {
+ if row.DBPath == filepath.Join(dataDir, "registry.db") {
return ErrTypedInventory
}
- // Flat legacy layouts use the host-named state directory plus
- // the database and its exact SQLite companion names.
+ // Flat legacy layouts: the database, its exact SQLite companion
+ // names, and the host-named state directory PrepareState creates,
+ // but only for a host name that cannot collide with a root
+ // namespace (see flatStateNameSafe).
db := filepath.Base(row.DBPath)
- if db != in.Host+".db" || (e.RelativePath != in.Host && !strings.HasPrefix(e.RelativePath, in.Host+"/") && e.RelativePath != db && e.RelativePath != db+"-wal" && e.RelativePath != db+"-shm" && e.RelativePath != db+"-journal") {
+ isDB := e.RelativePath == db || e.RelativePath == db+"-wal" || e.RelativePath == db+"-shm" || e.RelativePath == db+"-journal"
+ isState := flatStateNameSafe(in.Host) && (e.RelativePath == in.Host || strings.HasPrefix(e.RelativePath, in.Host+string(filepath.Separator)))
+ if !isDB && !isState {
return ErrTypedInventory
}
} else if e.RelativePath != parent && !strings.HasPrefix(e.RelativePath, parent+string(filepath.Separator)) {
+ // The host's own nested state container (root derives it from
+ // NestedDBPath), or a path inside it. Parking the empty container
+ // is what lets a cold handover find the namespace empty.
return ErrTypedInventory
}
default:
@@ -114,3 +120,21 @@ func (a ReconcileAdapter) retirementRoute(row registry.Route, target hostguard.F
}
return found, nil
}
+
+// flatStateNameSafe reports whether a flat layout's host-named state
+// directory, <data>/<host>, can be named by agent inventory. A dotted host
+// name of valid shape can never equal a root namespace at the data root:
+// those are dotless or start with a dot. The remaining dotted collisions
+// are the registry database and another host's database or SQLite
+// companions, <data>/<other>.db*, which are refused by suffix.
+func flatStateNameSafe(host string) bool {
+ if !strings.Contains(host, ".") || !provision.ValidHostShape(host) || strings.HasPrefix(host, "registry.db") {
+ return false
+ }
+ for _, suffix := range []string{".db", ".db-wal", ".db-shm", ".db-journal"} {
+ if strings.HasSuffix(host, suffix) {
+ return false
+ }
+ }
+ return true
+}
diff --git a/internal/hostbox/plan9fix2_inventory_test.go b/internal/hostbox/plan9fix2_inventory_test.go
index 91323d807..abee1cfb3 100644
--- a/internal/hostbox/plan9fix2_inventory_test.go
+++ b/internal/hostbox/plan9fix2_inventory_test.go
@@ -1,6 +1,7 @@
package hostbox
import (
+ "errors"
"path/filepath"
"testing"
@@ -15,8 +16,24 @@ func TestPlan9Fix2InventoryNamespace(t *testing.T) {
name, kind, root, path, id string
want bool
}{
- {"state", "path", "data", "instances/app.example", "", true},
+ {"state parent itself", "path", "data", "instances/app.example", "", true},
{"child", "path", "data", "instances/app.example/app.example.db", "", true},
+ {"nested child", "path", "data", "instances/app.example/sub/file", "", true},
+ {"sibling host dir", "path", "data", "instances/other.example", "", false},
+ {"sibling host file", "path", "data", "instances/other.example/other.example.db", "", false},
+ {"prefix sibling child", "path", "data", "instances/app.example-other/app.example.db", "", false},
+ {"not clean dot", "path", "data", "instances/app.example/./x", "", false},
+ {"not clean double slash", "path", "data", "instances/app.example//x", "", false},
+ {"not clean trailing slash", "path", "data", "instances/app.example/x/", "", false},
+ {"traversal back in", "path", "data", "instances/app.example/../app.example/x", "", false},
+ {"traversal out", "path", "data", "../x", "", false},
+ {"absolute", "path", "data", "/instances/app.example/x", "", false},
+ {"absolute data path", "path", "data", filepath.ToSlash(filepath.Join(data, "instances", "app.example", "x")), "", false},
+ {"root registry db", "path", "data", "registry.db", "", false},
+ {"root registry db-wal", "path", "data", "registry.db-wal", "", false},
+ {"root registry db-shm", "path", "data", "registry.db-shm", "", false},
+ {"root registry db-journal", "path", "data", "registry.db-journal", "", false},
+ {"flat db name under nested", "path", "data", "app.example.db", "", false},
{"route", "route", "registry", "app.example", "row", true},
{"timer", "timer", "registry", "app.example/daily", "timer", true},
{"foreign state", "path", "data", "instances/other.example", "", false},
@@ -33,9 +50,13 @@ func TestPlan9Fix2InventoryNamespace(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
in := base
in.Entries = []StopInventoryEntry{{Kind: tc.kind, ManagedRoot: tc.root, RelativePath: filepath.FromSlash(tc.path), RowIdentity: tc.id, Ownership: "owner", Materialization: "materialized"}}
- if err := ValidateAgentInventory(in, &row, data); (err == nil) != tc.want {
+ err := ValidateAgentInventory(in, &row, data)
+ if (err == nil) != tc.want {
t.Fatalf("allowed=%v: %v", tc.want, err)
}
+ if !tc.want && !errors.Is(err, ErrTypedInventory) {
+ t.Fatalf("rejection is not ErrTypedInventory: %v", err)
+ }
})
}
// A missing row can record the initial route intent, but grants no path authority.
@@ -48,16 +69,47 @@ func TestPlan9Fix2InventoryNamespace(t *testing.T) {
t.Fatal("missing row granted path authority")
}
row.DBPath = filepath.Join(data, row.Host+".db")
- for _, name := range []string{row.Host, row.Host + ".db", row.Host + ".db-wal", row.Host + ".db-shm"} {
+ for _, name := range []string{row.Host + ".db", row.Host + ".db-wal", row.Host + ".db-shm", row.Host + ".db-journal", row.Host, row.Host + "/" + row.Host + ".db", row.Host + "/child"} {
base.Entries[0].RelativePath = name
if err := ValidateAgentInventory(base, &row, data); err != nil {
t.Fatalf("legacy %s: %v", name, err)
}
}
- for _, name := range []string{".", ".hostclaims", row.Host + "-other", row.Host + ".db-other"} {
- base.Entries[0].RelativePath = name
- if err := ValidateAgentInventory(base, &row, data); err == nil {
- t.Fatalf("legacy accepted foreign %s", name)
+ for _, name := range []string{
+ ".", ".hostclaims",
+ row.Host + "-other", row.Host + ".db-other", row.Host + ".db/", "./" + row.Host + ".db",
+ "../" + row.Host + ".db", "/" + row.Host + ".db", filepath.ToSlash(filepath.Join(data, row.Host+".db")),
+ "other.example.db", "other.example", "instances/other.example", "instances/other.example/other.example.db",
+ "registry.db", "registry.db-wal", "registry.db-shm", "registry.db-journal",
+ } {
+ base.Entries[0].RelativePath = filepath.FromSlash(name)
+ if err := ValidateAgentInventory(base, &row, data); !errors.Is(err, ErrTypedInventory) {
+ t.Fatalf("legacy accepted foreign %s: %v", name, err)
+ }
+ }
+}
+
+func TestPlan9Fix2InventoryRegistryDBAtRootRefused(t *testing.T) {
+ data := t.TempDir()
+ row := registry.Route{Host: "app.example", ClaimOwnership: registry.ClaimOwnership{ClaimIncarnation: "inc", ClaimStamp: 1, ClaimOwner: "owner", ClaimRowID: "row"}}
+ for _, dbPath := range []string{
+ filepath.Join(data, "app.example.db"),
+ filepath.Join(data, "instances", "app.example", "app.example.db"),
+ } {
+ row.DBPath = dbPath
+ for _, name := range []string{"registry.db", "registry.db-wal", "registry.db-shm", "registry.db-journal"} {
+ in := StopInventory{Host: row.Host, Incarnation: "inc", InstallationStamp: 1, Entries: []StopInventoryEntry{{Kind: "path", ManagedRoot: "data", RelativePath: name, Ownership: "owner", Materialization: "materialized"}}}
+ if err := ValidateAgentInventory(in, &row, data); !errors.Is(err, ErrTypedInventory) {
+ t.Fatalf("db %s accepted %s: %v", dbPath, name, err)
+ }
+ }
+ }
+ row.Host = "registry"
+ row.DBPath = filepath.Join(data, "registry.db")
+ for _, name := range []string{"registry.db", "registry.db-wal", "registry.db-shm", "registry.db-journal"} {
+ in := StopInventory{Host: row.Host, Incarnation: "inc", InstallationStamp: 1, Entries: []StopInventoryEntry{{Kind: "path", ManagedRoot: "data", RelativePath: name, Ownership: "owner", Materialization: "materialized"}}}
+ if err := ValidateAgentInventory(in, &row, data); !errors.Is(err, ErrTypedInventory) {
+ t.Fatalf("colliding host accepted %s: %v", name, err)
}
}
}
@@ -69,22 +121,39 @@ func TestPlan9Fix2AgentCannotChooseNamespaceThroughRegistryDBPath(t *testing.T)
t.Run(parent, func(t *testing.T) {
row.DBPath = filepath.Join(data, filepath.FromSlash(parent), "app.example.db")
in := StopInventory{Host: row.Host, Incarnation: "inc", InstallationStamp: 1, Entries: []StopInventoryEntry{{Kind: "path", ManagedRoot: "data", RelativePath: filepath.FromSlash(parent), Ownership: "owner", Materialization: "materialized"}}}
- if err := ValidateAgentInventory(in, &row, data); err == nil {
- t.Fatalf("agent chose root namespace %s through DBPath", parent)
+ if err := ValidateAgentInventory(in, &row, data); !errors.Is(err, ErrTypedInventory) {
+ t.Fatalf("agent chose root namespace %s through DBPath: %v", parent, err)
}
})
}
row.Host = "hostguard"
row.DBPath = filepath.Join(data, "hostguard.db")
in := StopInventory{Host: row.Host, Incarnation: "inc", InstallationStamp: 1, Entries: []StopInventoryEntry{{Kind: "path", ManagedRoot: "data", RelativePath: "hostguard", Ownership: "owner", Materialization: "materialized"}}}
- if err := ValidateAgentInventory(in, &row, data); err == nil {
- t.Fatal("flat hostname selected root hostguard namespace")
+ if err := ValidateAgentInventory(in, &row, data); !errors.Is(err, ErrTypedInventory) {
+ t.Fatalf("flat hostname selected root hostguard namespace: %v", err)
}
row.Host = "instances"
row.DBPath = filepath.Join(data, "instances.db")
in.Host = row.Host
in.Entries[0].RelativePath = "instances/other.example"
- if err := ValidateAgentInventory(in, &row, data); err == nil {
- t.Fatal("flat hostname selected shared instances namespace")
+ if err := ValidateAgentInventory(in, &row, data); !errors.Is(err, ErrTypedInventory) {
+ t.Fatalf("flat hostname selected shared instances namespace: %v", err)
+ }
+}
+
+// A flat host-named state directory is nameable only for a host whose name
+// cannot collide with a root namespace or another host's database file.
+func TestPlan9Fix3FlatStateNameMustNotCollide(t *testing.T) {
+ data := t.TempDir()
+ for host, want := range map[string]bool{
+ "app.example": true, "hostguard": false, "instances": false, "registry.db": false,
+ "registry.db-wal.example": false, "other.db": false, "other.db-wal": false, "shop.example.db-journal": false,
+ } {
+ row := registry.Route{Host: host, DBPath: filepath.Join(data, host+".db"), ClaimOwnership: registry.ClaimOwnership{ClaimIncarnation: "inc", ClaimStamp: 1, ClaimOwner: "owner", ClaimRowID: "row"}}
+ in := StopInventory{Host: host, Incarnation: "inc", InstallationStamp: 1, Entries: []StopInventoryEntry{{Kind: "path", ManagedRoot: "data", RelativePath: host, Ownership: "owner", Materialization: "intended"}}}
+ err := ValidateAgentInventory(in, &row, data)
+ if (err == nil) != want {
+ t.Errorf("host %q: allowed=%v, want %v (%v)", host, err == nil, want, err)
+ }
}
}