carlos / platform Public

layout: preflight refuses a sidecar env another route owns as its instance env (plan 4 final review)

A route named <h>-sidecar owns <h>-sidecar.env as its instance env, which
h's plan would move as h's sidecar env. Refused in both directions, as the
.config collision is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev f0668be8672388829796b42cd051621c403e16df parent 4146775
2 files changed, +38 −1
  • internal/layout/preflight.go +13 −1
  • internal/layout/preflight_test.go +25 −0
diff --git a/internal/layout/preflight.go b/internal/layout/preflight.go
index 38593b5d2..e38983eac 100644
--- a/internal/layout/preflight.go
+++ b/internal/layout/preflight.go
@@ -18,7 +18,8 @@ import (
// rendered bundle UNLESS a route named <host>.config exists, in which case
// it is that route's instance env (SyncExec's own collision rule). Moving
// it with host would take another route's file; reverting onto it would
-// overwrite one. Either is a refusal naming both owners.
+// overwrite one. Either is a refusal naming both owners. <host>-sidecar.env
+// is shared the same way with a route named <host>-sidecar.
func Preflight(p *Plan, routes []registry.Route) error {
hosts := map[string]bool{}
for _, rt := range routes {
@@ -32,6 +33,17 @@ func Preflight(p *Plan, routes []registry.Route) error {
return fmt.Errorf("layout: %s: its env file %s.env is owned by both %s (its instance env) and %s (its bundle); refusing to move it", p.Host, p.Host, p.Host, owner)
}
}
+ // The sidecar env collides the same way: <host>-sidecar.env is host's
+ // sidecar env, and also the instance env of a route named
+ // <host>-sidecar.
+ if other := p.Host + "-sidecar"; hosts[other] {
+ return fmt.Errorf("layout: %s: %s-sidecar.env is owned by both %s (its sidecar env) and %s (its instance env); refusing to move it", p.Host, p.Host, p.Host, other)
+ }
+ if strings.HasSuffix(p.Host, "-sidecar") {
+ if owner := strings.TrimSuffix(p.Host, "-sidecar"); hosts[owner] {
+ return fmt.Errorf("layout: %s: its env file %s.env is owned by both %s (its instance env) and %s (its sidecar env); refusing to move it", p.Host, p.Host, p.Host, owner)
+ }
+ }
// The activator binds a control socket beside every exec listener at
// the listener's path plus ".control". A nested address that leaves no
// room for it would make the host unwakeable after the move, so it is
diff --git a/internal/layout/preflight_test.go b/internal/layout/preflight_test.go
index b066db997..bcbcd2b7b 100644
--- a/internal/layout/preflight_test.go
+++ b/internal/layout/preflight_test.go
@@ -39,6 +39,31 @@ func TestPreflightRefusesAConfigEnvOwnedByAnotherRoute(t *testing.T) {
}
}
+// The same collision through the sidecar env: a route named <h>-sidecar
+// owns <h>-sidecar.env as its instance env, and h's plan would move it as
+// h's sidecar env.
+func TestPreflightRefusesASidecarEnvOwnedByAnotherRoute(t *testing.T) {
+ routes := []registry.Route{{Host: "x.example.com"}, {Host: "x.example.com-sidecar"}}
+ p, _ := PlanFor("/data", "/run/carlos", "x.example.com", ModeNested)
+ err := Preflight(p, routes)
+ if err == nil || !strings.Contains(err.Error(), "x.example.com-sidecar.env") || !strings.Contains(err.Error(), "x.example.com-sidecar (its instance env)") {
+ t.Fatalf("ambiguous sidecar env accepted: %v", err)
+ }
+ if err := Preflight(p, []registry.Route{{Host: "x.example.com"}}); err != nil {
+ t.Fatalf("unambiguous plan refused: %v", err)
+ }
+ r, _ := PlanFor("/data", "/run/carlos", "x.example.com", ModeFlat)
+ if err := Preflight(r, routes); err == nil {
+ t.Fatal("revert onto another route's sidecar env accepted")
+ }
+ // The other direction: the host named <owner>-sidecar has an env file
+ // that is also <owner>'s sidecar env.
+ c, _ := PlanFor("/data", "/run/carlos", "x.example.com-sidecar", ModeNested)
+ if err := Preflight(c, routes); err == nil || !strings.Contains(err.Error(), "x.example.com (its sidecar env)") {
+ t.Fatalf("an env file that is another route's sidecar env accepted: %v", err)
+ }
+}
+
// A nested socket's control twin must fit a unix socket address. The
// socket dir here is long enough that the nested address overflows it.
func TestPreflightRefusesANestedSocketTooLongForItsControlTwin(t *testing.T) {