carlos / platform Public

zdd plan 2 task 7 fix: generation units keep the trampoline's signal exits successful

The generation template runs the same instance-exec trampoline as
carlos-instance@.service, which exits 128+n for a death by signal n.
SuccessExitStatus=129 130 141 143 ensures a SIGTERM stop leaves the
generation unit inactive, not failed. Added the directive with its
#126 comment (copied from carlos-instance@.service:41-45). Updated
the test to verify it is present.

Also fixed stale comment in units_hardening_test.go: now four templates
plus the probe, not three templates plus the probe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 4a966739eb0159411b83d278604b2e5cb37eea7a parent 591cbc2
3 files changed, +10 −3
  • internal/host/units/carlos-instance-gen@.service +6 −0
  • internal/host/units_hardening_test.go +2 −2
  • internal/host/units_isolation_test.go +2 −1
diff --git a/internal/host/units/carlos-instance-gen@.service b/internal/host/units/carlos-instance-gen@.service
index 4c98493a3..9a40e4f92 100644
--- a/internal/host/units/carlos-instance-gen@.service
+++ b/internal/host/units/carlos-instance-gen@.service
@@ -26,6 +26,12 @@ Group=carlos
Restart=no
KillMode=mixed
TimeoutStopSec=20
+# The main process is the trampoline, which stays as the namespace's init
+# and runs the app as its child (#126): it forwards the SIGTERM and exits
+# 128+n when the app dies by signal n. These are SIGHUP, SIGINT, SIGPIPE
+# and SIGTERM, the deaths systemd counts as clean for a main process, so a
+# stop still leaves the unit inactive rather than failed.
+SuccessExitStatus=129 130 141 143
StandardError=inherit
# --- the namespace (titogo@.service, 2026-07-31 design, on systemd 257) ---
diff --git a/internal/host/units_hardening_test.go b/internal/host/units_hardening_test.go
index fecb8ee6b..e07da5c74 100644
--- a/internal/host/units_hardening_test.go
+++ b/internal/host/units_hardening_test.go
@@ -39,8 +39,8 @@ const instanceDeny = "IPAddressDeny=localhost multicast " +
// Every directive the hardening adds to the shared namespace block (#214
// items 2 and 3, #178), pinned exactly: the block is byte-identical across
-// the three templates and the probe (TestInstanceTemplatesShareOneNamespace),
-// so pinning it on carlos-instance@ pins it on all four.
+// the four templates and the probe (TestInstanceTemplatesShareOneNamespace),
+// so pinning it on carlos-instance@ pins it on all five.
func TestNamespaceBlockCarriesTheHardening(t *testing.T) {
for _, name := range []string{"carlos-instance@.service", "carlos-instance-gen@.service", "carlos-instance-seed@.service", "carlos-instance-sidecar@.service", "carlos-instance-probe.service"} {
u := readUnit(t, name)
diff --git a/internal/host/units_isolation_test.go b/internal/host/units_isolation_test.go
index 0ddb2c801..c56c46235 100644
--- a/internal/host/units_isolation_test.go
+++ b/internal/host/units_isolation_test.go
@@ -277,7 +277,8 @@ func TestInstanceTemplatesShareOneNamespace(t *testing.T) {
"SuccessExitStatus=129 130 141 143"},
"carlos-instance-gen@.service": {"ExecStart=/opt/carlos/carlos instance-exec run %I", "ExecStopPost=/opt/carlos/carlos instance-exit run %I",
"Restart=no", "KillMode=mixed", "TimeoutStopSec=20", "BindsTo=carlos-edge.service",
- "Requires=data-instances.mount run-carlos-instances.mount run-carlos-logs.mount", "OOMScoreAdjust=500", "OOMPolicy=kill", "TasksMax=256", "Type=simple"},
+ "Requires=data-instances.mount run-carlos-instances.mount run-carlos-logs.mount", "OOMScoreAdjust=500", "OOMPolicy=kill", "TasksMax=256", "Type=simple",
+ "SuccessExitStatus=129 130 141 143"},
"carlos-instance-seed@.service": {"Type=oneshot", "RemainAfterExit=yes", "TimeoutStartSec=120", "ExecStart=/opt/carlos/carlos instance-exec seed %I",
"ExecStopPost=/opt/carlos/carlos instance-exit seed %I", "StandardOutput=file:/run/carlos/logs/%I.seed.sock",
"KillMode=mixed", "TimeoutStopSec=20", "WorkingDirectory=/data/instances/%I"},