carlos / platform Public

caps: deterministic drop-in renderer, golden-pinned for console and keymail

RenderService/RenderSocket turn a manifest's declared Set, filtered through
an operator's Grants, into fixed-order, sorted-list systemd drop-in bytes so
an unchanged converge pass writes nothing, reloads nothing, and restarts
nothing. Self-service capabilities (command, runtime_dir, children)
materialize from the manifest alone; operator-consented ones (ports,
host_role) require the matching grant. daemon and data emit no directives
here by design (Task 6 and the template's own StateDirectory own them).

A nil effective render means "remove the generated file", never an empty
stanza. Three golden files (console's host_role-only drop-in, keymail's
full set, and the socket unit's ListenStream) are hand-written from the
spec's §4a worked examples, not generated from this code.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 141d7f523eee73df55d1b87151246b6dc06deee3 parent 350dae8
5 files changed, +358 −0
  • internal/caps/render.go +128 −0
  • internal/caps/render_test.go +209 −0
  • internal/caps/testdata/console.service.conf +5 −0
  • internal/caps/testdata/keymail.service.conf +13 −0
  • internal/caps/testdata/keymail.socket.conf +3 −0
diff --git a/internal/caps/render.go b/internal/caps/render.go
new file mode 100644
index 000000000..fa480324b
--- /dev/null
+++ b/internal/caps/render.go
@@ -0,0 +1,128 @@
+// Package caps: the deterministic drop-in renderer (spec §4a).
+//
+// RenderService and RenderSocket turn a manifest's Set, filtered through an
+// operator's Grants, into the exact bytes of a systemd drop-in — nothing
+// else touches disk here (that is Task 6's converge pass; this file is pure
+// bytes-in-bytes-out). Directive order is FIXED and every list is sorted, so
+// re-rendering an unchanged Set/Grants pair is byte-identical: the converge
+// pass's "write only if it differs" check is what keeps an unchanged pass
+// from triggering a daemon-reload or a restart.
+package caps
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+)
+
+// execStartPath is the literal ExecStart target, byte-identical to
+// internal/host/units/carlos-app@.service's own `ExecStart=%s serve` line.
+// The template is not parameterised by CARLOS_OPT_DIR either — a drop-in
+// that disagreed with the template about the path would be a silent split
+// brain (spec §4a).
+const execStartPath = "/opt/carlos/live/%i"
+
+// RenderService returns the [Service] drop-in body for the capabilities
+// that are BOTH declared by s and (for the operator-consented ones)
+// granted by g, or nil when the effective set writes no [Service]
+// directive at all. Nil means "remove the generated file", not "write an
+// empty stanza" — one generated file per unit, and removal semantics are
+// the reason (spec §3c).
+//
+// Directive order is FIXED: command, then runtime_dir, then children, then
+// host_role. `daemon` and `data` are self-service capabilities that emit no
+// [Service] directive at all here — `daemon` is enable/start/restart
+// behaviour (Task 6 owns it), and `data`'s path is already the template's
+// own StateDirectory=carlos-app/%i. Ports are the socket unit's alone; see
+// RenderSocket.
+func RenderService(app string, s *Set, g Grants) []byte {
+ if s == nil {
+ return nil
+ }
+
+ var lines []string
+
+ if s.Command != "" {
+ // The empty ExecStart= reset FIRST — systemd's documented way to
+ // reset a list-valued directive from a drop-in. Without it the
+ // unit has two ExecStart lines and refuses to start.
+ lines = append(lines,
+ "ExecStart=",
+ "ExecStart="+execStartPath+" "+s.Command,
+ )
+ }
+
+ if s.RuntimeDir != "" {
+ lines = append(lines,
+ "RuntimeDirectory="+s.RuntimeDir,
+ "RuntimeDirectoryMode=0700",
+ "RuntimeDirectoryPreserve=restart",
+ )
+ }
+
+ if s.Children {
+ // A platform-chosen tier, not a manifest-chosen number: `infinity`
+ // is unsayable and no number comes from a manifest.
+ lines = append(lines,
+ "KillMode=control-group",
+ "TasksMax=1024",
+ "MemoryMax=1G",
+ )
+ }
+
+ if s.HostRole && g.HostRole {
+ user := AppUser(app)
+ lines = append(lines,
+ "DynamicUser=no",
+ "User="+user,
+ "Group="+user,
+ )
+ }
+
+ if len(lines) == 0 {
+ return nil
+ }
+ return dropIn("[Service]", lines)
+}
+
+// RenderSocket returns the [Socket] drop-in body, or nil when no declared
+// port is also granted. keymail with no `port:25` grant serves HTTP and
+// does not bind the MX — it does not refuse to start (spec §2).
+func RenderSocket(s *Set, g Grants) []byte {
+ if s == nil {
+ return nil
+ }
+
+ var ports []int
+ for _, p := range s.Ports {
+ if g.Ports[p.Port] {
+ ports = append(ports, p.Port)
+ }
+ }
+ if len(ports) == 0 {
+ return nil
+ }
+ sort.Ints(ports)
+
+ lines := make([]string, len(ports))
+ for i, p := range ports {
+ lines[i] = fmt.Sprintf("ListenStream=%d", p)
+ }
+ return dropIn("[Socket]", lines)
+}
+
+// dropIn assembles the marker line, the section header, and the directive
+// lines, each newline-terminated — so the body ends with exactly one
+// trailing newline and no blank line separates the marker from the header.
+func dropIn(section string, lines []string) []byte {
+ var b strings.Builder
+ b.WriteString(Marker)
+ b.WriteByte('\n')
+ b.WriteString(section)
+ b.WriteByte('\n')
+ for _, l := range lines {
+ b.WriteString(l)
+ b.WriteByte('\n')
+ }
+ return []byte(b.String())
+}
diff --git a/internal/caps/render_test.go b/internal/caps/render_test.go
new file mode 100644
index 000000000..3b8fec5dc
--- /dev/null
+++ b/internal/caps/render_test.go
@@ -0,0 +1,209 @@
+package caps
+
+import (
+ "bytes"
+ "encoding/json"
+ "os"
+ "testing"
+)
+
+// keymailSet is the full declared set the worked example in spec §4a
+// renders from: command, runtime_dir, children, host_role and a single
+// privileged port, plus a `data` capability that (being self-service and
+// directive-free) must never show up in the rendered bytes.
+func keymailSet(t *testing.T) *Set {
+ t.Helper()
+ var s Set
+ raw := `{
+ "v": 1,
+ "command": "relay",
+ "daemon": true,
+ "children": true,
+ "runtime_dir": "keymail",
+ "ports": [{"port": 25, "name": "mx"}],
+ "data": {"replicate": "platform", "dbs": ["registry.db"]},
+ "host_role": true
+ }`
+ if err := json.Unmarshal([]byte(raw), &s); err != nil {
+ t.Fatalf("decode keymail set: %v", err)
+ }
+ return &s
+}
+
+func keymailGrants() Grants {
+ return Grants{Ports: map[int]bool{25: true}, HostRole: true}
+}
+
+func consoleSet(t *testing.T) *Set {
+ t.Helper()
+ var s Set
+ raw := `{"v": 1, "daemon": true, "host_role": true}`
+ if err := json.Unmarshal([]byte(raw), &s); err != nil {
+ t.Fatalf("decode console set: %v", err)
+ }
+ return &s
+}
+
+func consoleGrants() Grants {
+ return Grants{HostRole: true}
+}
+
+func readGolden(t *testing.T, name string) []byte {
+ t.Helper()
+ b, err := os.ReadFile("testdata/" + name)
+ if err != nil {
+ t.Fatalf("read golden %s: %v", name, err)
+ }
+ return b
+}
+
+func TestRenderServiceMatchesKeymailGolden(t *testing.T) {
+ got := RenderService("keymail", keymailSet(t), keymailGrants())
+ want := readGolden(t, "keymail.service.conf")
+ if !bytes.Equal(got, want) {
+ t.Fatalf("RenderService mismatch:\n got: %q\n want: %q", got, want)
+ }
+}
+
+func TestRenderSocketMatchesKeymailGolden(t *testing.T) {
+ got := RenderSocket(keymailSet(t), keymailGrants())
+ want := readGolden(t, "keymail.socket.conf")
+ if !bytes.Equal(got, want) {
+ t.Fatalf("RenderSocket mismatch:\n got: %q\n want: %q", got, want)
+ }
+}
+
+func TestRenderServiceMatchesConsoleGolden(t *testing.T) {
+ got := RenderService("console", consoleSet(t), consoleGrants())
+ want := readGolden(t, "console.service.conf")
+ if !bytes.Equal(got, want) {
+ t.Fatalf("RenderService mismatch:\n got: %q\n want: %q", got, want)
+ }
+}
+
+// TestRenderIsDeterministic renders the same effective set from two
+// independently-decoded Sets whose "ports" arrays are given in different
+// orders, and asserts byte-identical output with ports in ascending order
+// regardless of declaration order.
+func TestRenderIsDeterministic(t *testing.T) {
+ const a = `{"v":1,"command":"relay","ports":[{"port":587,"name":"submission"},{"port":25,"name":"mx"}]}`
+ const b = `{"v":1,"command":"relay","ports":[{"port":25,"name":"mx"},{"port":587,"name":"submission"}]}`
+
+ var sa, sb Set
+ if err := json.Unmarshal([]byte(a), &sa); err != nil {
+ t.Fatalf("decode a: %v", err)
+ }
+ if err := json.Unmarshal([]byte(b), &sb); err != nil {
+ t.Fatalf("decode b: %v", err)
+ }
+
+ g := Grants{Ports: map[int]bool{25: true, 587: true}}
+
+ gotA := RenderSocket(&sa, g)
+ gotB := RenderSocket(&sb, g)
+ if !bytes.Equal(gotA, gotB) {
+ t.Fatalf("render not deterministic across declaration order:\n a: %q\n b: %q", gotA, gotB)
+ }
+
+ want := []byte(Marker + "\n[Socket]\nListenStream=25\nListenStream=587\n")
+ if !bytes.Equal(gotA, want) {
+ t.Fatalf("ports not sorted ascending: %q", gotA)
+ }
+
+ // Rendering again from the same Set must reproduce the identical bytes.
+ again := RenderSocket(&sa, g)
+ if !bytes.Equal(gotA, again) {
+ t.Fatalf("render not repeatable: %q vs %q", gotA, again)
+ }
+
+ svcA := RenderService("keymail", &sa, g)
+ svcAgain := RenderService("keymail", &sa, g)
+ if !bytes.Equal(svcA, svcAgain) {
+ t.Fatalf("service render not repeatable: %q vs %q", svcA, svcAgain)
+ }
+}
+
+// TestUngrantedCapabilitiesMaterializeNothing: keymail's full set with no
+// grants at all still renders the self-service lines (command, runtime_dir,
+// children) but none of the operator-consented ones — no DynamicUser/User/
+// Group, and a nil socket body. keymail with no port:25 grant serves HTTP
+// and does not bind the MX; it does not refuse to start.
+func TestUngrantedCapabilitiesMaterializeNothing(t *testing.T) {
+ got := RenderService("keymail", keymailSet(t), Grants{})
+ if got == nil {
+ t.Fatal("service body is nil, want the self-service lines")
+ }
+ for _, want := range []string{
+ "ExecStart=\n",
+ "ExecStart=/opt/carlos/live/%i relay\n",
+ "RuntimeDirectory=keymail\n",
+ "KillMode=control-group\n",
+ } {
+ if !bytes.Contains(got, []byte(want)) {
+ t.Errorf("service body missing self-service line %q:\n%s", want, got)
+ }
+ }
+ for _, unwanted := range []string{"DynamicUser", "User=", "Group="} {
+ if bytes.Contains(got, []byte(unwanted)) {
+ t.Errorf("service body has ungranted host_role directive %q:\n%s", unwanted, got)
+ }
+ }
+
+ if sock := RenderSocket(keymailSet(t), Grants{}); sock != nil {
+ t.Fatalf("socket body = %q, want nil when port:25 is not granted", sock)
+ }
+}
+
+// TestEmptyEffectiveSetRendersNil: daemon and data emit no directives, and
+// nothing else is declared, so both bodies are nil.
+func TestEmptyEffectiveSetRendersNil(t *testing.T) {
+ s := &Set{V: 1, Daemon: true, Data: &Data{Replicate: DataReplicatePlatform, DBs: []string{"console.db"}}}
+ if got := RenderService("console", s, Grants{}); got != nil {
+ t.Fatalf("RenderService = %q, want nil", got)
+ }
+ if got := RenderSocket(s, Grants{}); got != nil {
+ t.Fatalf("RenderSocket = %q, want nil", got)
+ }
+}
+
+// TestNilSetRendersNil: a nil *Set (every manifest before this feature, and
+// every route that never declared capabilities) must render nil, not panic.
+func TestNilSetRendersNil(t *testing.T) {
+ if got := RenderService("console", nil, Grants{HostRole: true}); got != nil {
+ t.Fatalf("RenderService(nil) = %q, want nil", got)
+ }
+ if got := RenderSocket(nil, Grants{Ports: map[int]bool{25: true}}); got != nil {
+ t.Fatalf("RenderSocket(nil) = %q, want nil", got)
+ }
+}
+
+// TestRenderStartsWithTheMarker checks every non-nil body this file
+// produces opens with the marker line.
+func TestRenderStartsWithTheMarker(t *testing.T) {
+ bodies := [][]byte{
+ RenderService("keymail", keymailSet(t), keymailGrants()),
+ RenderSocket(keymailSet(t), keymailGrants()),
+ RenderService("console", consoleSet(t), consoleGrants()),
+ }
+ for i, b := range bodies {
+ if b == nil {
+ t.Fatalf("body %d is nil, want non-nil to check its marker", i)
+ }
+ if !bytes.HasPrefix(b, []byte(Marker+"\n")) {
+ t.Errorf("body %d does not start with the marker:\n%s", i, b)
+ }
+ }
+}
+
+// A command identical to the template's default ("serve") is still an
+// explicit declaration: the reset+rewrite pair is still correct and still
+// deterministic, even though the rewritten value matches what the template
+// already says.
+func TestCommandIdenticalToTemplateDefaultStillEmitsReset(t *testing.T) {
+ s := &Set{V: 1, Command: "serve"}
+ got := RenderService("console", s, Grants{})
+ want := []byte(Marker + "\n[Service]\nExecStart=\nExecStart=/opt/carlos/live/%i serve\n")
+ if !bytes.Equal(got, want) {
+ t.Fatalf("got %q, want %q", got, want)
+ }
+}
diff --git a/internal/caps/testdata/console.service.conf b/internal/caps/testdata/console.service.conf
new file mode 100644
index 000000000..6e6c6f4bb
--- /dev/null
+++ b/internal/caps/testdata/console.service.conf
@@ -0,0 +1,5 @@
+# GENERATED by carlos adopt — do not hand-edit
+[Service]
+DynamicUser=no
+User=carlos-console
+Group=carlos-console
diff --git a/internal/caps/testdata/keymail.service.conf b/internal/caps/testdata/keymail.service.conf
new file mode 100644
index 000000000..79c15098c
--- /dev/null
+++ b/internal/caps/testdata/keymail.service.conf
@@ -0,0 +1,13 @@
+# GENERATED by carlos adopt — do not hand-edit
+[Service]
+ExecStart=
+ExecStart=/opt/carlos/live/%i relay
+RuntimeDirectory=keymail
+RuntimeDirectoryMode=0700
+RuntimeDirectoryPreserve=restart
+KillMode=control-group
+TasksMax=1024
+MemoryMax=1G
+DynamicUser=no
+User=carlos-keymail
+Group=carlos-keymail
diff --git a/internal/caps/testdata/keymail.socket.conf b/internal/caps/testdata/keymail.socket.conf
new file mode 100644
index 000000000..8739f52e4
--- /dev/null
+++ b/internal/caps/testdata/keymail.socket.conf
@@ -0,0 +1,3 @@
+# GENERATED by carlos adopt — do not hand-edit
+[Socket]
+ListenStream=25