host: the agent claims its delegated subtree; the unit file delegates it
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 files changed,
+22
−2
internal/host/agent.go+10 −1internal/host/units/carlos-edge.service+12 −1
diff --git a/internal/host/agent.go b/internal/host/agent.go| index bc5429a14..2dc19b674 100644 |
| --- a/internal/host/agent.go |
| +++ b/internal/host/agent.go |
| @@ -92,6 +92,7 @@ import ( |
| "github.com/carlosframework/platform/internal/activator" |
| "github.com/carlosframework/platform/internal/appconfig" |
| "github.com/carlosframework/platform/internal/caps" |
| + "github.com/carlosframework/platform/internal/cgroup" |
| "github.com/carlosframework/platform/internal/domains" |
| "github.com/carlosframework/platform/internal/edge" |
| "github.com/carlosframework/platform/internal/lease" |
| @@ -131,6 +132,13 @@ func RunAgent(args []string) { |
| // hibernation branch. |
| cache := edge.NewCache(edge.DefaultCacheMaxEntries, edge.DefaultCacheMaxBodyBytes) |
| + // Exec-child accounting: claim the cgroup subtree the unit file |
| + // delegates to us (Delegate= in carlos-edge.service). Nil when there is |
| + // no delegation — an old unit file, a dev run outside systemd, darwin — |
| + // in which case children spawn exactly as before and simply have no |
| + // per-instance readings. |
| + cgm := cgroup.Setup(log.Printf) |
| + |
| cfg, err := activator.ConfigFromEnv() |
| if err != nil { |
| log.Fatalf("agent: %v", err) |
| @@ -148,6 +156,7 @@ func RunAgent(args []string) { |
| OptDir: release.OptDir(), |
| DataDir: dd, |
| Cache: cache, |
| + Cgroups: cgm, |
| }, |
| Unit: &activator.UnitBackend{OptDir: release.OptDir()}, |
| }, reg) // the registry satisfies activator.DueStore directly |
| @@ -161,7 +170,7 @@ func RunAgent(args []string) { |
| go statusLoop(reg) |
| go domainsLoop(reg, peekAcmeCache(args)) |
| go instancesLoop(reg) |
| - go metricsLoop("") |
| + go metricsLoop(cgm.Root()) |
| go configSyncLoop(reg, svc, dd) |
| go configGenLoop(reg, svc, dd) |
| go socketAdvisoryLoop(reg, svc) |
diff --git a/internal/host/units/carlos-edge.service b/internal/host/units/carlos-edge.service| index aa41e536a..ee57b7f40 100644 |
| --- a/internal/host/units/carlos-edge.service |
| +++ b/internal/host/units/carlos-edge.service |
| @@ -38,7 +38,11 @@ ProtectHostname=yes |
| ProtectKernelTunables=yes |
| ProtectKernelModules=yes |
| ProtectKernelLogs=yes |
| -ProtectControlGroups=yes |
| +# ProtectControlGroups is deliberately ABSENT: it would mount /sys/fs/cgroup |
| +# read-only, and Delegate= (below) needs this unit's own subtree writable. |
| +# The hardening that line bought is retained by ownership instead — plain |
| +# DAC keeps everything OUTSIDE the chowned subtree read-only to this |
| +# unprivileged user. |
| ProtectProc=invisible |
| RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX |
| RestrictNamespaces=yes |
| @@ -53,6 +57,13 @@ SystemCallArchitectures=native |
| # No MemoryMax: hibernating instances are this unit's children, so a cap here |
| # would cap the whole set of awake instances at once. |
| TasksMax=1024 |
| +# Exec-backed instances run as this unit's children. Delegation hands the |
| +# unit its own cgroup subtree — chowned to User=carlos, unmanaged by systemd |
| +# below this point — so the agent can give each child its own cgroup for |
| +# per-instance accounting (docs/superpowers/specs/ |
| +# 2026-08-07-exec-child-accounting-design.md). The subtree still dies with |
| +# the service: restart semantics are unchanged. |
| +Delegate=cpu memory io pids |
| [Install] |
| WantedBy=multi-user.target |