carlos / platform Public

zdd plan 2 on main: adapt to instancectl c.do's header return (0bb57a31) and #256 D's WorkingDirectory (b8941639) for the generation template

The generation template's %I is <id>:<n>, so the instance's data directory
comes from the per-generation runtime drop-in (keyed by host id, like the
slice and data mounts). The template's one WorkingDirectory=/data/instances
applies only if the drop-in did not, and the control service refuses the
start before that (drop-in slice check); /data is an empty tmpfs there, so
the chdir fails closed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev b9a5411e0d2879b34595df8187f13d20d36e9615 parent 8088633
4 files changed, +13 −4
  • internal/host/units/carlos-instance-gen@.service +7 −0
  • internal/instancectl/client.go +4 −4
  • internal/instancectl/gen.go +1 −0
  • internal/instancectl/server_gen_test.go +1 −0

CI passed — run details

diff --git a/internal/host/units/carlos-instance-gen@.service b/internal/host/units/carlos-instance-gen@.service
index 9a40e4f92..cf30f3330 100644
--- a/internal/host/units/carlos-instance-gen@.service
+++ b/internal/host/units/carlos-instance-gen@.service
@@ -19,6 +19,13 @@ After=carlos-edge.service data-instances.mount run-carlos-instances.mount run-ca
[Service]
Type=simple
ExecStart=/opt/carlos/carlos instance-exec run %I
+# The tenant runs in its own data directory (#256 D). %I here is
+# <id>:<n>, not the host's id, so the real directory comes from the
+# generation's runtime drop-in, which overrides this line. This one only
+# applies if the drop-in did not, and then the start is already refused
+# (the control service checks the drop-in's slice first), and /data is an
+# empty tmpfs, so the chdir fails closed.
+WorkingDirectory=/data/instances
ExecStopPost=/opt/carlos/carlos instance-exit run %I
User=carlos
Group=carlos
diff --git a/internal/instancectl/client.go b/internal/instancectl/client.go
index b954b7acd..9d495efd5 100644
--- a/internal/instancectl/client.go
+++ b/internal/instancectl/client.go
@@ -184,7 +184,7 @@ func (c *Client) StartGen(ctx context.Context, host string, n uint64, memoryMax
q := genValues(host, n)
q.Set("memory_max", strconv.FormatInt(memoryMax, 10))
var out StartResponse
- if status, err := c.do(ctx, DeadlineControl, http.MethodPost, "/start", q, &out); err != nil {
+ if status, _, err := c.do(ctx, DeadlineControl, http.MethodPost, "/start", q, &out); err != nil {
if status == statusGenUnproven {
return "", fmt.Errorf("%w: %w", ErrGenUnproven, err)
}
@@ -200,12 +200,12 @@ func (c *Client) StopGen(ctx context.Context, host string, n uint64, kill bool)
if kill {
q.Set("kill", "1")
}
- _, err := c.do(ctx, DeadlineControl, http.MethodPost, "/stop", q, nil)
+ _, _, err := c.do(ctx, DeadlineControl, http.MethodPost, "/stop", q, nil)
return err
}
func (c *Client) ResetGen(ctx context.Context, host string, n uint64) error {
- _, err := c.do(ctx, DeadlineControl, http.MethodPost, "/reset", genValues(host, n), nil)
+ _, _, err := c.do(ctx, DeadlineControl, http.MethodPost, "/reset", genValues(host, n), nil)
return err
}
@@ -213,7 +213,7 @@ func (c *Client) ResetGen(ctx context.Context, host string, n uint64) error {
// §7.3): one tier, or two summed during an overlap.
func (c *Client) Parent(ctx context.Context, host string, memoryMax int64) error {
q := url.Values{"host": {host}, "memory_max": {strconv.FormatInt(memoryMax, 10)}}
- _, err := c.do(ctx, DeadlineControl, http.MethodPost, "/parent", q, nil)
+ _, _, err := c.do(ctx, DeadlineControl, http.MethodPost, "/parent", q, nil)
return err
}
diff --git a/internal/instancectl/gen.go b/internal/instancectl/gen.go
index f06f188c3..0bdcd5315 100644
--- a/internal/instancectl/gen.go
+++ b/internal/instancectl/gen.go
@@ -47,6 +47,7 @@ func GenDropIn(host string, n uint64, tier cgroup.Limit) string {
"Slice=" + SliceName(host) + "\n" +
"BindPaths=/data/instances/" + iid + " /run/carlos/instances/" + iid + "\n" +
"ReadWritePaths=/data/instances/" + iid + " /run/carlos/instances/" + iid + "\n" +
+ "WorkingDirectory=/data/instances/" + iid + "\n" +
"BindReadOnlyPaths=/run/carlos-control/exe/" + key + "/app:/opt/carlos/app /run/carlos-control/exe/" + key + "/live:/opt/carlos/live\n" +
"StandardOutput=file:" + GenLogSocket(host, n) + "\n" +
"MemoryMax=" + max + "\n" +
diff --git a/internal/instancectl/server_gen_test.go b/internal/instancectl/server_gen_test.go
index 88317469f..b0fd390cb 100644
--- a/internal/instancectl/server_gen_test.go
+++ b/internal/instancectl/server_gen_test.go
@@ -40,6 +40,7 @@ func TestGenDropInText(t *testing.T) {
Slice=carlos-instances-bill\x2d2.example.com.slice
BindPaths=/data/instances/bill-2.example.com /run/carlos/instances/bill-2.example.com
ReadWritePaths=/data/instances/bill-2.example.com /run/carlos/instances/bill-2.example.com
+WorkingDirectory=/data/instances/bill-2.example.com
BindReadOnlyPaths=/run/carlos-control/exe/bill-2.example.com@7/app:/opt/carlos/app /run/carlos-control/exe/bill-2.example.com@7/live:/opt/carlos/live
StandardOutput=file:/run/carlos/logs/bill-2.example.com@7.run.sock
MemoryMax=536870912