carlos / platform Public

provision: instance identifier and nested path helpers (isolated exec plan 1, task 1)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZ1ZvWj7naf73GjTX8MBv5
Paul Campbell pushed by paul@keymail.dev e41f808b5cc5eb4463d7b415d19a777ebd0c03dc parent 96622c7
2 files changed, +203 −0
  • internal/provision/provision.go +100 −0
  • internal/provision/provision_test.go +103 −0
diff --git a/internal/provision/provision.go b/internal/provision/provision.go
index c5da151c6..f8cb63063 100644
--- a/internal/provision/provision.go
+++ b/internal/provision/provision.go
@@ -8,6 +8,8 @@ package provision
import (
"context"
+ "crypto/sha256"
+ "encoding/hex"
"encoding/json"
"errors"
"fmt"
@@ -1010,3 +1012,101 @@ const SocketDir = "/run/carlos"
// used before this helper existed were Join'd; this keeps that.)
func DefaultSocket(host string) string { return filepath.Join(SocketDir, host+".sock") }
func DefaultDBPath(dataDir, host string) string { return filepath.Join(dataDir, host+".db") }
+
+// MaxInstanceID bounds InstanceID. A unix socket address holds MaxUnixPath
+// bytes of path and a host may be 253 bytes, so the directories a socket
+// lives under are keyed by a bounded identifier, never by the host itself
+// (isolated-exec design §3.1, codex R5-3).
+const MaxInstanceID = 40
+
+// MaxUnixPath is sockaddr_un's sun_path (108 bytes) minus the NUL Go
+// insists on room for.
+const MaxUnixPath = 107
+
+// ControlSocketDir and ControlSocketPath are the root instance-control
+// channel (design §5). Deliberately NOT under SocketDir: an instance's
+// namespace replaces /run/carlos with an empty tmpfs, and nothing an
+// instance can reach must resolve to this path.
+const (
+ ControlSocketDir = "/run/carlos-control"
+ ControlSocketPath = ControlSocketDir + "/control.sock"
+)
+
+// InstanceID is the bounded per-host identifier every isolated-exec
+// directory, socket, unit instance and slice is keyed by. A host of at
+// most MaxInstanceID bytes is its own identifier. A longer host becomes
+// "_" + its first 22 bytes + "-" + 16 hex digits of its sha256, also
+// MaxInstanceID bytes.
+//
+// The underscore is the whole reason the two forms can share one
+// namespace: no valid host contains one (controlname labels are
+// [a-z0-9-]), so a hashed identifier can never equal any short host, and
+// two hashed identifiers collide only on a 64-bit hash collision between
+// two long hosts with the same 22-byte prefix (codex R6-1). Anything that
+// finds two rows with one identifier treats them as a conflict, never as
+// the same host.
+func InstanceID(host string) string {
+ if len(host) <= MaxInstanceID {
+ return host
+ }
+ sum := sha256.Sum256([]byte(host))
+ return "_" + host[:22] + "-" + hex.EncodeToString(sum[:8])
+}
+
+// InstanceDir is the per-host data directory of the nested layout:
+// <dataDir>/instances/<InstanceID(host)>. File names INSIDE it keep the
+// full host (a file name may be 255 bytes; a socket path may not).
+func InstanceDir(dataDir, host string) string {
+ return filepath.Join(dataDir, "instances", InstanceID(host))
+}
+
+// NestedDBPath is the database under InstanceDir. It is what the layout
+// pass writes into a migrated row's DBPath; DefaultDBPath stays flat in
+// this release (design §8.1).
+func NestedDBPath(dataDir, host string) string {
+ return filepath.Join(InstanceDir(dataDir, host), host+".db")
+}
+
+// InstanceRunDir is the per-host socket directory of the nested layout.
+func InstanceRunDir(host string) string {
+ return filepath.Join(SocketDir, "instances", InstanceID(host))
+}
+
+// NestedSocket is the instance's listener under InstanceRunDir; its
+// control socket is NestedSocket + ".control", as for every exec route.
+func NestedSocket(host string) string {
+ return filepath.Join(InstanceRunDir(host), "app.sock")
+}
+
+// LogSocket is the agent's per-role log listener for a host (design
+// §6.4): outside every tenant mount, keyed by the identifier.
+func LogSocket(host, role string) string {
+ return filepath.Join(SocketDir, "logs", InstanceID(host)+"."+role+".sock")
+}
+
+// IsControlEndpoint reports whether path is the control channel or
+// anything else under its directory. The edge and the agent refuse to
+// dial such a path as an upstream (design §5.4): a registry row pointing
+// at it is refused before connect().
+func IsControlEndpoint(path string) bool {
+ clean := filepath.Clean(path)
+ return clean == ControlSocketDir || strings.HasPrefix(clean, ControlSocketDir+"/")
+}
+
+// LongestSocketAddress is the longest unix socket path any helper here
+// can produce, over the longest valid host, so a test can pin it under
+// MaxUnixPath rather than trusting arithmetic in a comment.
+func LongestSocketAddress() int {
+ host := strings.Repeat("x", 253)
+ longest := 0
+ for _, p := range []string{
+ NestedSocket(host),
+ NestedSocket(host) + ".control",
+ LogSocket(host, "sidecar"), // the longest role name
+ } {
+ if len(p) > longest {
+ longest = len(p)
+ }
+ }
+ return longest
+}
diff --git a/internal/provision/provision_test.go b/internal/provision/provision_test.go
index 407dbb631..595b52611 100644
--- a/internal/provision/provision_test.go
+++ b/internal/provision/provision_test.go
@@ -1006,3 +1006,106 @@ func TestValidateShapeGate(t *testing.T) {
t.Fatalf("plaintext gate accepted: %v", err)
}
}
+
+func TestInstanceIDShortHostsAreThemselves(t *testing.T) {
+ for _, h := range []string{"h", "bill.go.tito.io", strings.Repeat("a", 40)} {
+ if got := InstanceID(h); got != h {
+ t.Errorf("InstanceID(%q) = %q, want the host itself", h, got)
+ }
+ }
+}
+
+func TestInstanceIDLongHostsAreBoundedAndPrefixed(t *testing.T) {
+ long := strings.Repeat("a", 60) + ".example.com"
+ got := InstanceID(long)
+ if len(got) != MaxInstanceID {
+ t.Fatalf("InstanceID(long) = %q (%d bytes), want %d", got, len(got), MaxInstanceID)
+ }
+ if got[0] != '_' {
+ t.Errorf("InstanceID(long) = %q, want a leading underscore: no valid host has one", got)
+ }
+ if got[1:23] != long[:22] {
+ t.Errorf("InstanceID(long) = %q, want the host's first 22 bytes after the underscore", got)
+ }
+ if got[23] != '-' {
+ t.Errorf("InstanceID(long) = %q, want a dash before the hash", got)
+ }
+ if got != InstanceID(long) {
+ t.Error("InstanceID is not stable")
+ }
+ // Two long hosts sharing a 22-byte prefix get different identifiers.
+ other := strings.Repeat("a", 60) + ".example.org"
+ if InstanceID(other) == got {
+ t.Errorf("InstanceID(%q) == InstanceID(%q); the hash must separate them", long, other)
+ }
+}
+
+func TestInstanceIDDomainsNeverMeet(t *testing.T) {
+ // codex R6-1: the hashed form of one host must never equal any valid
+ // short host. A leading underscore is not a valid host label.
+ long := strings.Repeat("a", 31) + ".example.com"
+ if ValidHostShape(InstanceID(long)) {
+ t.Errorf("InstanceID(%q) = %q is a valid host shape; the domains overlap", long, InstanceID(long))
+ }
+}
+
+func TestNestedHelpersAreKeyedByInstanceID(t *testing.T) {
+ long := strings.Repeat("b", 50) + ".example.com"
+ iid := InstanceID(long)
+ if got, want := InstanceDir("/data", long), "/data/instances/"+iid; got != want {
+ t.Errorf("InstanceDir = %q, want %q", got, want)
+ }
+ if got, want := NestedDBPath("/data/", long), "/data/instances/"+iid+"/"+long+".db"; got != want {
+ t.Errorf("NestedDBPath = %q, want %q (file names keep the full host)", got, want)
+ }
+ if got, want := InstanceRunDir(long), "/run/carlos/instances/"+iid; got != want {
+ t.Errorf("InstanceRunDir = %q, want %q", got, want)
+ }
+ if got, want := NestedSocket(long), "/run/carlos/instances/"+iid+"/app.sock"; got != want {
+ t.Errorf("NestedSocket = %q, want %q", got, want)
+ }
+ if got, want := LogSocket(long, "sidecar"), "/run/carlos/logs/"+iid+".sidecar.sock"; got != want {
+ t.Errorf("LogSocket = %q, want %q", got, want)
+ }
+}
+
+func TestFlatDefaultsUnchangedThisRelease(t *testing.T) {
+ // Spec §8.1: nothing mints nested in this release. The flat helpers
+ // are what routeFor and `carlos add` still use.
+ if got := DefaultSocket("h"); got != "/run/carlos/h.sock" {
+ t.Errorf("DefaultSocket = %q", got)
+ }
+ if got := DefaultDBPath("/data", "h"); got != "/data/h.db" {
+ t.Errorf("DefaultDBPath = %q", got)
+ }
+}
+
+func TestLongestSocketAddressFitsSockaddrUn(t *testing.T) {
+ // codex R5-3: an 80-byte host overflowed the first draft's paths. The
+ // bound is computed, not assumed, over a 253-byte host.
+ if got := LongestSocketAddress(); got > MaxUnixPath {
+ t.Fatalf("longest socket address is %d bytes, over the %d-byte limit", got, MaxUnixPath)
+ }
+ host := strings.Repeat("c", 253)
+ for _, p := range []string{NestedSocket(host), NestedSocket(host) + ".control", LogSocket(host, "sidecar")} {
+ if len(p) > MaxUnixPath {
+ t.Errorf("%q is %d bytes", p, len(p))
+ }
+ }
+}
+
+func TestIsControlEndpoint(t *testing.T) {
+ for path, want := range map[string]bool{
+ ControlSocketPath: true,
+ "/run/carlos-control/anything": true,
+ "/run/carlos-control": true,
+ "/run/carlos/h.sock": false,
+ "/run/carlos/instances/h/app.sock": false,
+ "/run/carlos-controlx/control.sock": false,
+ "/run/titogo/example.sock": false,
+ } {
+ if got := IsControlEndpoint(path); got != want {
+ t.Errorf("IsControlEndpoint(%q) = %v, want %v", path, got, want)
+ }
+ }
+}