carlos / platform Public
Isolated Exec, Plan 1 of 4: Foundation Implementation Plan
> For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: Land every building block of the isolated-exec design that changes nothing on a flat fleet: the path helpers and instance identifier, the symlink-refusing file helper, the unix-dial checks, the slice and mount units, and the bootstrap wiring for them.
Architecture: Four self-contained pieces that later plans consume by name: provision.InstanceID and the nested path helpers (§3.1), the instancefs package built on openat2 (§3.2), unixdial.Checked used by every unix dial the edge and agent make (§5.4, §6.1), and the carlos.slice / carlos-instances.slice / three nosymfollow mount units with their tmpfiles lines and bootstrap arming (§3.2, §4.3). The edge unit moves into carlos.slice and loses its own memory lines. No spawn path changes in this plan.
Tech Stack: Go 1.26, golang.org/x/sys/unix (already in go.mod), systemd 257 unit files embedded via embed.FS, the repo's existing test style (testing, no assertion library).
Spec: docs/superpowers/specs/2026-09-11-isolated-exec-design.md (revision 12). Read §2, §3, §4.3, §5.4, §6.1 and §9 before starting. §13 records every codex ruling; none may be reopened.
Global Constraints
go build ./...,go vet ./...,go test ./... -count=1,gofmt -l .clean before every commit (AGENTS.md). UseGOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off; socket-binding tests needTMPDIR=/tmp/g(create it) and may need the sandbox off.- CI cross-compiles
darwin,windowsandlinux-arm64: every Linux-only syscall lives in a_linux.gofile with a//go:build linuxtag and has a!linuxstub that returnserrors.ErrUnsupported. InstanceIDis at most 40 bytes; a hashed identifier starts with_; the longest socket address the helpers produce is under 108 bytes (spec §3.1).- Unit files are the source of truth in
internal/host/units/and are embedded; nothing is applied by drop-in. carlos-edge.servicekeeps every hardening line it has today; onlySlice=,Requires=/After=for the mounts, and the three memory lines change (spec §4.3).- Copy rules: no em dashes in any user-facing string; log lines use the existing
verb: subject: detailshape. - Commit after every task with the attribution trailer the session gives you. Never push to
main. The branch isworktree-isolated-exec-spec(worktree.claude/worktrees/isolated-exec-spec).
---
File Structure
| File | Responsibility | |---|---| | internal/provision/provision.go (modify, after line 1012) | InstanceID, InstanceDir, NestedDBPath, InstanceRunDir, NestedSocket, LogSocket, ControlSocketDir, ControlSocketPath, IsControlEndpoint, MaxUnixPath, LongestSocketAddress | | internal/provision/provision_test.go (modify) | tests for the above | | internal/instancefs/instancefs.go (create) | OpenBeneath, ReadBeneath, WriteAtomic, MountNoSymfollow; documentation of the contract | | internal/instancefs/instancefs_linux.go (create) | the openat2 and renameat implementations | | internal/instancefs/instancefs_other.go (create) | !linux stubs | | internal/instancefs/instancefs_linux_test.go (create) | tests | | internal/unixdial/unixdial.go (create) | Checked(ctx, dialer, path): refuse the control endpoint, dial, verify getpeername | | internal/unixdial/unixdial_test.go (create) | real socket-pair tests | | internal/edge/edge.go:2647 (modify) | dialUnixWithRetry uses unixdial.Checked | | internal/activator/backend_exec.go:916 (modify) | socketClient uses unixdial.Checked | | internal/release/versions.go:364 (modify) | unixHTTPClient uses unixdial.Checked | | internal/host/socket_advice.go:48 (modify) | socketServing uses unixdial.Checked | | internal/host/units/carlos.slice (create) | outer memory wall | | internal/host/units/carlos-instances.slice (create) | parent of per-host slices | | internal/host/units/data-instances.mount (create) | /data/.instances → /data/instances, nosymfollow | | internal/host/units/run-carlos-instances.mount (create) | /run/carlos/.instances → /run/carlos/instances | | internal/host/units/run-carlos-logs.mount (create) | /run/carlos/.logs → /run/carlos/logs | | internal/host/units/carlos-edge.service (modify) | Slice=carlos.slice; Requires=/After= the mounts; drop MemoryHigh=/MemoryMax= | | internal/host/units/carlos-litestream.service (modify) | After=data-instances.mount | | internal/host/bootstrap.go (modify) | embed glob, tmpfiles lines, dirs, enabledUnits, systemctlPlan | | internal/host/units_isolation_test.go (create) | unit-file assertions for this plan | | internal/host/host_test.go:1195 (modify) | the plan test learns about sockets and mounts |
---
Task 1: Instance identifier and nested path helpers
Files:
- Modify:
internal/provision/provision.go(append afterDefaultDBPath, line 1012) - Test:
internal/provision/provision_test.go(append)
Interfaces:
- Consumes:
SocketDir("/run/carlos"),DefaultSocket,DefaultDBPath(unchanged: they keep their flat meaning in this release, spec §3.1). - Produces: ``
go const MaxInstanceID = 40 const MaxUnixPath = 107 // sockaddr_un sun_path minus the NUL const ControlSocketDir = "/run/carlos-control" const ControlSocketPath = "/run/carlos-control/control.sock" func InstanceID(host string) string func InstanceDir(dataDir, host string) string func NestedDBPath(dataDir, host string) string func InstanceRunDir(host string) string func NestedSocket(host string) string func LogSocket(host, role string) string func IsControlEndpoint(path string) bool // true for anything under ControlSocketDir func LongestSocketAddress() int // the worst case over every helper, for the pinned test``
- [ ] Step 1: Write the failing tests
Append to internal/provision/provision_test.go:
func TestInstanceIDShortHostsAreThemselves(t *testing.T) {
for _, h := range []string{"h", "bill.go.tito.io", strings.Repeat("a", 40)} {
if got := InstanceID(h); got != h {
t.Errorf("InstanceID(%q) = %q, want the host itself", h, got)
}
}
}
func TestInstanceIDLongHostsAreBoundedAndPrefixed(t *testing.T) {
long := strings.Repeat("a", 60) + ".example.com"
got := InstanceID(long)
if len(got) != MaxInstanceID {
t.Fatalf("InstanceID(long) = %q (%d bytes), want %d", got, len(got), MaxInstanceID)
}
if got[0] != '_' {
t.Errorf("InstanceID(long) = %q, want a leading underscore: no valid host has one", got)
}
if got[1:23] != long[:22] {
t.Errorf("InstanceID(long) = %q, want the host's first 22 bytes after the underscore", got)
}
if got[23] != '-' {
t.Errorf("InstanceID(long) = %q, want a dash before the hash", got)
}
if got != InstanceID(long) {
t.Error("InstanceID is not stable")
}
// Two long hosts sharing a 22-byte prefix get different identifiers.
other := strings.Repeat("a", 60) + ".example.org"
if InstanceID(other) == got {
t.Errorf("InstanceID(%q) == InstanceID(%q); the hash must separate them", long, other)
}
}
func TestInstanceIDDomainsNeverMeet(t *testing.T) {
// codex R6-1: the hashed form of one host must never equal any valid
// short host. A leading underscore is not a valid host label.
long := strings.Repeat("a", 31) + ".example.com"
if ValidHostShape(InstanceID(long)) {
t.Errorf("InstanceID(%q) = %q is a valid host shape; the domains overlap", long, InstanceID(long))
}
}
func TestNestedHelpersAreKeyedByInstanceID(t *testing.T) {
long := strings.Repeat("b", 50) + ".example.com"
iid := InstanceID(long)
if got, want := InstanceDir("/data", long), "/data/instances/"+iid; got != want {
t.Errorf("InstanceDir = %q, want %q", got, want)
}
if got, want := NestedDBPath("/data/", long), "/data/instances/"+iid+"/"+long+".db"; got != want {
t.Errorf("NestedDBPath = %q, want %q (file names keep the full host)", got, want)
}
if got, want := InstanceRunDir(long), "/run/carlos/instances/"+iid; got != want {
t.Errorf("InstanceRunDir = %q, want %q", got, want)
}
if got, want := NestedSocket(long), "/run/carlos/instances/"+iid+"/app.sock"; got != want {
t.Errorf("NestedSocket = %q, want %q", got, want)
}
if got, want := LogSocket(long, "sidecar"), "/run/carlos/logs/"+iid+".sidecar.sock"; got != want {
t.Errorf("LogSocket = %q, want %q", got, want)
}
}
func TestFlatDefaultsUnchangedThisRelease(t *testing.T) {
// Spec §8.1: nothing mints nested in this release. The flat helpers
// are what routeFor and `carlos add` still use.
if got := DefaultSocket("h"); got != "/run/carlos/h.sock" {
t.Errorf("DefaultSocket = %q", got)
}
if got := DefaultDBPath("/data", "h"); got != "/data/h.db" {
t.Errorf("DefaultDBPath = %q", got)
}
}
func TestLongestSocketAddressFitsSockaddrUn(t *testing.T) {
// codex R5-3: an 80-byte host overflowed the first draft's paths. The
// bound is computed, not assumed, over a 253-byte host.
if got := LongestSocketAddress(); got > MaxUnixPath {
t.Fatalf("longest socket address is %d bytes, over the %d-byte limit", got, MaxUnixPath)
}
host := strings.Repeat("c", 253)
for _, p := range []string{NestedSocket(host), NestedSocket(host) + ".control", LogSocket(host, "sidecar")} {
if len(p) > MaxUnixPath {
t.Errorf("%q is %d bytes", p, len(p))
}
}
}
func TestIsControlEndpoint(t *testing.T) {
for path, want := range map[string]bool{
ControlSocketPath: true,
"/run/carlos-control/anything": true,
"/run/carlos-control": true,
"/run/carlos/h.sock": false,
"/run/carlos/instances/h/app.sock": false,
"/run/carlos-controlx/control.sock": false,
"/run/titogo/example.sock": false,
} {
if got := IsControlEndpoint(path); got != want {
t.Errorf("IsControlEndpoint(%q) = %v, want %v", path, got, want)
}
}
}Add "strings" to the test file's imports if it is not already there (it is used by other tests in that file; check with grep -n '"strings"' internal/provision/provision_test.go).
- [ ] Step 2: Run the tests to verify they fail
Run: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/provision -run 'TestInstanceID|TestNestedHelpers|TestFlatDefaults|TestLongestSocket|TestIsControlEndpoint' -count=1 Expected: FAIL with undefined: InstanceID (and the others).
- [ ] Step 3: Implement the helpers
Append to internal/provision/provision.go after DefaultDBPath (line 1012). Add "crypto/sha256" and "encoding/hex" to the import block.
// MaxInstanceID bounds InstanceID. A unix socket address holds MaxUnixPath
// bytes of path and a host may be 253 bytes, so the directories a socket
// lives under are keyed by a bounded identifier, never by the host itself
// (isolated-exec design §3.1, codex R5-3).
const MaxInstanceID = 40
// MaxUnixPath is sockaddr_un's sun_path (108 bytes) minus the NUL Go
// insists on room for.
const MaxUnixPath = 107
// ControlSocketDir and ControlSocketPath are the root instance-control
// channel (design §5). Deliberately NOT under SocketDir: an instance's
// namespace replaces /run/carlos with an empty tmpfs, and nothing an
// instance can reach must resolve to this path.
const (
ControlSocketDir = "/run/carlos-control"
ControlSocketPath = ControlSocketDir + "/control.sock"
)
// InstanceID is the bounded per-host identifier every isolated-exec
// directory, socket, unit instance and slice is keyed by. A host of at
// most MaxInstanceID bytes is its own identifier. A longer host becomes
// "_" + its first 22 bytes + "-" + 16 hex digits of its sha256, also
// MaxInstanceID bytes.
//
// The underscore is the whole reason the two forms can share one
// namespace: no valid host contains one (controlname labels are
// [a-z0-9-]), so a hashed identifier can never equal any short host, and
// two hashed identifiers collide only on a 64-bit hash collision between
// two long hosts with the same 22-byte prefix (codex R6-1). Anything that
// finds two rows with one identifier treats them as a conflict, never as
// the same host.
func InstanceID(host string) string {
if len(host) <= MaxInstanceID {
return host
}
sum := sha256.Sum256([]byte(host))
return "_" + host[:22] + "-" + hex.EncodeToString(sum[:8])
}
// InstanceDir is the per-host data directory of the nested layout:
// <dataDir>/instances/<InstanceID(host)>. File names INSIDE it keep the
// full host (a file name may be 255 bytes; a socket path may not).
func InstanceDir(dataDir, host string) string {
return filepath.Join(dataDir, "instances", InstanceID(host))
}
// NestedDBPath is the database under InstanceDir. It is what the layout
// pass writes into a migrated row's DBPath; DefaultDBPath stays flat in
// this release (design §8.1).
func NestedDBPath(dataDir, host string) string {
return filepath.Join(InstanceDir(dataDir, host), host+".db")
}
// InstanceRunDir is the per-host socket directory of the nested layout.
func InstanceRunDir(host string) string {
return filepath.Join(SocketDir, "instances", InstanceID(host))
}
// NestedSocket is the instance's listener under InstanceRunDir; its
// control socket is NestedSocket + ".control", as for every exec route.
func NestedSocket(host string) string {
return filepath.Join(InstanceRunDir(host), "app.sock")
}
// LogSocket is the agent's per-role log listener for a host (design
// §6.4): outside every tenant mount, keyed by the identifier.
func LogSocket(host, role string) string {
return filepath.Join(SocketDir, "logs", InstanceID(host)+"."+role+".sock")
}
// IsControlEndpoint reports whether path is the control channel or
// anything else under its directory. The edge and the agent refuse to
// dial such a path as an upstream (design §5.4): a registry row pointing
// at it is refused before connect().
func IsControlEndpoint(path string) bool {
clean := filepath.Clean(path)
return clean == ControlSocketDir || strings.HasPrefix(clean, ControlSocketDir+"/")
}
// LongestSocketAddress is the longest unix socket path any helper here
// can produce, over the longest valid host, so a test can pin it under
// MaxUnixPath rather than trusting arithmetic in a comment.
func LongestSocketAddress() int {
host := strings.Repeat("x", 253)
longest := 0
for _, p := range []string{
NestedSocket(host),
NestedSocket(host) + ".control",
LogSocket(host, "sidecar"), // the longest role name
} {
if len(p) > longest {
longest = len(p)
}
}
return longest
}- [ ] Step 4: Run the tests to verify they pass
Run: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/provision -count=1 Expected: PASS (the whole package, including TestDefaults).
- [ ] Step 5: gofmt and commit
gofmt -l internal/provision
git add internal/provision/provision.go internal/provision/provision_test.go
git commit -m "provision: instance identifier and nested path helpers (isolated exec plan 1, task 1)"---
Task 2: The instancefs package
Files:
- Create:
internal/instancefs/instancefs.go - Create:
internal/instancefs/instancefs_linux.go - Create:
internal/instancefs/instancefs_other.go - Create:
internal/instancefs/instancefs_linux_test.go
Interfaces:
- Consumes:
golang.org/x/sys/unix(Openat2,OpenHow,RESOLVE_BENEATH,RESOLVE_NO_SYMLINKS,RESOLVE_NO_MAGICLINKS,Renameat),crypto/rand. - Produces: ``
go package instancefs var ErrSymlink = errors.New("instancefs: path contains a symlink") var ErrEscapes = errors.New("instancefs: path escapes its directory") // OpenBeneath opens name (a relative path with no "..") under dir, following no symlink at any component. func OpenBeneath(dir, name string, flag int, perm os.FileMode) (*os.File, error) // ReadBeneath is OpenBeneath + io.ReadAll. func ReadBeneath(dir, name string) ([]byte, error) // WriteAtomic writes data to a random O_EXCL temp file in dir and renames it over name. Never follows a symlink at the destination. func WriteAtomic(dir, name string, data []byte, perm os.FileMode) error // MountNoSymfollow reports whether mountpoint is a mount whose options include nosymfollow, per /proc/self/mountinfo. func MountNoSymfollow(mountpoint string) (bool, error)`Every function returnserrors.ErrUnsupported` on non-Linux.
- [ ] Step 1: Write the failing tests
Create internal/instancefs/instancefs_linux_test.go:
//go:build linux
package instancefs
import (
"errors"
"os"
"path/filepath"
"strings"
"testing"
)
func TestOpenBeneathReadsAPlainFile(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "h.env"), []byte("A=1\n"), 0o600); err != nil {
t.Fatal(err)
}
got, err := ReadBeneath(dir, "h.env")
if err != nil {
t.Fatalf("ReadBeneath: %v", err)
}
if string(got) != "A=1\n" {
t.Errorf("got %q", got)
}
}
func TestOpenBeneathRefusesASymlinkAtTheLeaf(t *testing.T) {
dir := t.TempDir()
secret := filepath.Join(t.TempDir(), "secret")
if err := os.WriteFile(secret, []byte("token"), 0o600); err != nil {
t.Fatal(err)
}
if err := os.Symlink(secret, filepath.Join(dir, "h.env")); err != nil {
t.Fatal(err)
}
_, err := ReadBeneath(dir, "h.env")
if !errors.Is(err, ErrSymlink) {
t.Fatalf("ReadBeneath through a symlink: err = %v, want ErrSymlink", err)
}
}
func TestOpenBeneathRefusesASymlinkedDirectory(t *testing.T) {
dir := t.TempDir()
other := t.TempDir()
if err := os.WriteFile(filepath.Join(other, "x"), []byte("x"), 0o600); err != nil {
t.Fatal(err)
}
if err := os.Symlink(other, filepath.Join(dir, "sub")); err != nil {
t.Fatal(err)
}
_, err := ReadBeneath(dir, "sub/x")
if !errors.Is(err, ErrSymlink) {
t.Fatalf("err = %v, want ErrSymlink", err)
}
}
func TestOpenBeneathRefusesDotDotAndAbsolute(t *testing.T) {
dir := t.TempDir()
for _, name := range []string{"../x", "a/../../x", "/etc/passwd"} {
_, err := ReadBeneath(dir, name)
if !errors.Is(err, ErrEscapes) {
t.Errorf("ReadBeneath(%q): err = %v, want ErrEscapes", name, err)
}
}
}
func TestWriteAtomicReplacesAndNeverFollows(t *testing.T) {
dir := t.TempDir()
if err := WriteAtomic(dir, "h.config.env", []byte("one\n"), 0o600); err != nil {
t.Fatalf("WriteAtomic: %v", err)
}
if err := WriteAtomic(dir, "h.config.env", []byte("two\n"), 0o600); err != nil {
t.Fatalf("WriteAtomic (replace): %v", err)
}
got, err := os.ReadFile(filepath.Join(dir, "h.config.env"))
if err != nil || string(got) != "two\n" {
t.Fatalf("got %q, %v", got, err)
}
fi, err := os.Stat(filepath.Join(dir, "h.config.env"))
if err != nil || fi.Mode().Perm() != 0o600 {
t.Errorf("mode = %v, %v", fi.Mode(), err)
}
// A symlink planted at the destination is REPLACED as a directory
// entry; its target is untouched.
victim := filepath.Join(t.TempDir(), "victim")
if err := os.WriteFile(victim, []byte("keep"), 0o600); err != nil {
t.Fatal(err)
}
if err := os.Remove(filepath.Join(dir, "h.config.env")); err != nil {
t.Fatal(err)
}
if err := os.Symlink(victim, filepath.Join(dir, "h.config.env")); err != nil {
t.Fatal(err)
}
if err := WriteAtomic(dir, "h.config.env", []byte("three\n"), 0o600); err != nil {
t.Fatalf("WriteAtomic over a symlink: %v", err)
}
if b, _ := os.ReadFile(victim); string(b) != "keep" {
t.Errorf("symlink target was written through: %q", b)
}
if fi, err := os.Lstat(filepath.Join(dir, "h.config.env")); err != nil || fi.Mode()&os.ModeSymlink != 0 {
t.Errorf("destination is still a symlink: %v %v", fi, err)
}
// No temp file is left behind.
entries, _ := os.ReadDir(dir)
for _, e := range entries {
if strings.Contains(e.Name(), ".tmp-") {
t.Errorf("temp file left behind: %s", e.Name())
}
}
}
func TestMountNoSymfollowReadsMountinfo(t *testing.T) {
// / is a mount and does not carry nosymfollow on any ordinary box; a
// path that is not a mountpoint is reported as not a nosymfollow mount
// with a nil error (the caller's message names the path).
got, err := MountNoSymfollow("/")
if err != nil {
t.Fatalf("MountNoSymfollow(/): %v", err)
}
if got {
t.Error("/ reported as nosymfollow")
}
got, err = MountNoSymfollow(t.TempDir())
if err != nil || got {
t.Errorf("temp dir: %v %v", got, err)
}
// The parser, against a fixture line, since a real nosymfollow mount
// needs root to create.
line := "36 35 98:0 /.instances /data/instances rw,nosuid,nodev,noexec,nosymfollow,relatime - ext4 /dev/root rw"
if !mountinfoLineHas(line, "/data/instances", "nosymfollow") {
t.Error("parser missed nosymfollow on a fixture line")
}
if mountinfoLineHas(line, "/data", "nosymfollow") {
t.Error("parser matched the wrong mountpoint")
}
}- [ ] Step 2: Run the tests to verify they fail
Run: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/instancefs -count=1 Expected: FAIL to build (undefined: ReadBeneath and so on; or "no Go files").
- [ ] Step 3: Write the package
Create internal/instancefs/instancefs.go:
// Package instancefs is how the agent opens anything inside an instance's
// own directory. Every exec-backed instance runs as the agent's uid, and
// its directory is writable by it, so any path the agent opens there is a
// request the instance makes on the agent's behalf: a symlink at
// <host>.env pointing at /data/litestream.env would hand the instance the
// bucket credentials as its own environment (isolated-exec design §1.3).
//
// The kernel closes that for every reader at once with the nosymfollow
// mount option on the instance trees (design §3.2). This package is the
// belt to that brace: it opens with openat2(RESOLVE_BENEATH |
// RESOLVE_NO_SYMLINKS | RESOLVE_NO_MAGICLINKS), so a symlink is refused
// with a named error even on a box whose mount is missing the option, and
// it writes through O_EXCL temp files and rename, so a planted symlink at
// a destination is replaced as a directory entry rather than written
// through.
//
// Nothing here is Linux-specific in its contract; the implementations
// are, and the !linux build returns errors.ErrUnsupported so the CLI still
// builds on a Mac (AGENTS.md's cross-compile gate).
package instancefs
import (
"errors"
"os"
"path/filepath"
"strings"
)
// ErrSymlink is returned when any component of the path is a symlink.
var ErrSymlink = errors.New("instancefs: path contains a symlink")
// ErrEscapes is returned for a name that is absolute or reaches above dir.
var ErrEscapes = errors.New("instancefs: path escapes its directory")
// cleanRel validates name as a relative path that stays inside dir.
func cleanRel(name string) (string, error) {
if name == "" || filepath.IsAbs(name) {
return "", ErrEscapes
}
c := filepath.Clean(name)
if c == ".." || strings.HasPrefix(c, "../") {
return "", ErrEscapes
}
return c, nil
}
// ReadBeneath is OpenBeneath followed by reading the whole file.
func ReadBeneath(dir, name string) ([]byte, error) {
f, err := OpenBeneath(dir, name, os.O_RDONLY, 0)
if err != nil {
return nil, err
}
defer f.Close()
return readAll(f)
}
func readAll(f *os.File) ([]byte, error) {
var out []byte
buf := make([]byte, 32*1024)
for {
n, err := f.Read(buf)
out = append(out, buf[:n]...)
if err != nil {
if err.Error() == "EOF" {
return out, nil
}
return out, err
}
if n == 0 {
return out, nil
}
}
}Replace the hand-rolled readAll with io.ReadAll (add "io" to imports and delete the helper). It is shown expanded only so the file compiles if io is forgotten; the committed version must use io.ReadAll.
Create internal/instancefs/instancefs_linux.go:
//go:build linux
package instancefs
import (
"bufio"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"golang.org/x/sys/unix"
)
const resolveFlags = unix.RESOLVE_BENEATH | unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_NO_MAGICLINKS
// openDir opens dir itself as a directory fd, also refusing symlinks in
// dir's own components: the instance directory's parent is the agent's,
// but the instance directory itself could have been swapped for a
// symlink by a same-uid process before the mount option landed.
func openDir(dir string) (int, error) {
fd, err := unix.Open(dir, unix.O_PATH|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
if err != nil {
if errors.Is(err, unix.ELOOP) {
return -1, fmt.Errorf("%w: %s", ErrSymlink, dir)
}
return -1, &os.PathError{Op: "open", Path: dir, Err: err}
}
return fd, nil
}
// OpenBeneath opens name under dir with openat2, following no symlink at
// any component and never leaving dir.
func OpenBeneath(dir, name string, flag int, perm os.FileMode) (*os.File, error) {
rel, err := cleanRel(name)
if err != nil {
return nil, err
}
dfd, err := openDir(dir)
if err != nil {
return nil, err
}
defer unix.Close(dfd)
how := unix.OpenHow{
Flags: uint64(flag) | unix.O_CLOEXEC,
Mode: uint64(perm.Perm()),
Resolve: resolveFlags,
}
fd, err := unix.Openat2(dfd, rel, &how)
if err != nil {
switch {
case errors.Is(err, unix.ELOOP):
return nil, fmt.Errorf("%w: %s", ErrSymlink, filepath.Join(dir, rel))
case errors.Is(err, unix.EXDEV):
return nil, fmt.Errorf("%w: %s", ErrEscapes, filepath.Join(dir, rel))
}
return nil, &os.PathError{Op: "openat2", Path: filepath.Join(dir, rel), Err: err}
}
return os.NewFile(uintptr(fd), filepath.Join(dir, rel)), nil
}
// WriteAtomic writes data to a fresh O_EXCL temp file beside name and
// renames it over name. rename(2) replaces the directory entry and never
// follows a symlink at the destination, so a planted link is displaced,
// not written through. The temp name is random, so no same-uid process
// can pre-plant it (the O_EXCL refuses it if one does).
func WriteAtomic(dir, name string, data []byte, perm os.FileMode) error {
rel, err := cleanRel(name)
if err != nil {
return err
}
if strings.Contains(rel, "/") {
return fmt.Errorf("%w: WriteAtomic takes a bare file name, got %q", ErrEscapes, name)
}
dfd, err := openDir(dir)
if err != nil {
return err
}
defer unix.Close(dfd)
var rnd [8]byte
if _, err := rand.Read(rnd[:]); err != nil {
return err
}
tmp := "." + rel + ".tmp-" + hex.EncodeToString(rnd[:])
how := unix.OpenHow{
Flags: unix.O_WRONLY | unix.O_CREAT | unix.O_EXCL | unix.O_CLOEXEC,
Mode: uint64(perm.Perm()),
Resolve: resolveFlags,
}
fd, err := unix.Openat2(dfd, tmp, &how)
if err != nil {
return &os.PathError{Op: "openat2", Path: filepath.Join(dir, tmp), Err: err}
}
f := os.NewFile(uintptr(fd), filepath.Join(dir, tmp))
cleanup := func() { f.Close(); _ = unix.Unlinkat(dfd, tmp, 0) }
if _, err := f.Write(data); err != nil {
cleanup()
return err
}
if err := f.Sync(); err != nil {
cleanup()
return err
}
if err := f.Close(); err != nil {
_ = unix.Unlinkat(dfd, tmp, 0)
return err
}
if err := unix.Renameat(dfd, tmp, dfd, rel); err != nil {
_ = unix.Unlinkat(dfd, tmp, 0)
return &os.PathError{Op: "renameat", Path: filepath.Join(dir, rel), Err: err}
}
return nil
}
// MountNoSymfollow reports whether mountpoint is a mountpoint whose
// options include nosymfollow, by reading /proc/self/mountinfo. A path
// that is not a mountpoint is (false, nil).
func MountNoSymfollow(mountpoint string) (bool, error) {
f, err := os.Open("/proc/self/mountinfo")
if err != nil {
return false, err
}
defer f.Close()
want := filepath.Clean(mountpoint)
sc := bufio.NewScanner(f)
for sc.Scan() {
if mountinfoLineHas(sc.Text(), want, "nosymfollow") {
return true, nil
}
}
return false, sc.Err()
}
// mountinfoLineHas parses one mountinfo line: field 5 is the mountpoint
// (octal-escaped), field 6 the per-mount options.
func mountinfoLineHas(line, mountpoint, option string) bool {
fields := strings.Fields(line)
if len(fields) < 6 {
return false
}
if unescapeMountinfo(fields[4]) != mountpoint {
return false
}
for _, o := range strings.Split(fields[5], ",") {
if o == option {
return true
}
}
return false
}
// unescapeMountinfo undoes the \040-style escapes mountinfo applies to
// spaces, tabs, newlines and backslashes in paths.
func unescapeMountinfo(s string) string {
if !strings.Contains(s, `\`) {
return s
}
var b strings.Builder
for i := 0; i < len(s); i++ {
if s[i] == '\\' && i+3 < len(s) {
var v int
if _, err := fmt.Sscanf(s[i+1:i+4], "%o", &v); err == nil {
b.WriteByte(byte(v))
i += 3
continue
}
}
b.WriteByte(s[i])
}
return b.String()
}Create internal/instancefs/instancefs_other.go:
//go:build !linux
package instancefs
import (
"errors"
"os"
)
func OpenBeneath(dir, name string, flag int, perm os.FileMode) (*os.File, error) {
if _, err := cleanRel(name); err != nil {
return nil, err
}
return nil, errors.ErrUnsupported
}
func WriteAtomic(dir, name string, data []byte, perm os.FileMode) error {
if _, err := cleanRel(name); err != nil {
return err
}
return errors.ErrUnsupported
}
func MountNoSymfollow(mountpoint string) (bool, error) { return false, errors.ErrUnsupported }
func mountinfoLineHas(line, mountpoint, option string) bool { return false }- [ ] Step 4: Run the tests to verify they pass, and cross-compile
Run: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/instancefs -count=1 Expected: PASS.
Run: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off GOOS=darwin go build ./internal/instancefs && GOOS=windows go build ./internal/instancefs Expected: both build. (RESOLVE_* and OpenHow exist in x/sys v0.46.0 for linux; the other file never references them.)
- [ ] Step 5: gofmt and commit
gofmt -l internal/instancefs
git add internal/instancefs
git commit -m "instancefs: symlink-refusing opens and atomic writes for instance directories (plan 1, task 2)"---
Task 3: Checked unix dials in the edge and the agent
Files:
- Create:
internal/unixdial/unixdial.go - Create:
internal/unixdial/unixdial_test.go - Modify:
internal/edge/edge.go:2647-2664(dialUnixWithRetry) - Modify:
internal/activator/backend_exec.go:916-925(socketClient) - Modify:
internal/release/versions.go:364-374(unixHTTPClient) - Modify:
internal/host/socket_advice.go:48-55(socketServing)
Interfaces:
- Consumes:
provision.IsControlEndpoint. - Produces: ``
go package unixdial var ErrControlEndpoint = errors.New("unixdial: refusing to dial the instance control channel as an upstream") var ErrPeerMismatch = errors.New("unixdial: listener is bound to a different path than the one dialled") // Checked dials path as a unix stream socket and returns the connection only if // (1) path is not under provision.ControlSocketDir and (2) getpeername on the // connected socket equals filepath.Clean(path). Otherwise the connection is // closed and the error names which check failed. func Checked(ctx context.Context, d *net.Dialer, path string) (net.Conn, error)``
- [ ] Step 1: Write the failing tests
Create internal/unixdial/unixdial_test.go:
package unixdial
import (
"context"
"errors"
"net"
"os"
"path/filepath"
"testing"
)
// short returns a socket path short enough for sockaddr_un under the
// session's long TMPDIR (SDD lessons: run with TMPDIR=/tmp/g).
func short(t *testing.T, name string) string {
t.Helper()
dir, err := os.MkdirTemp("", "ud")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { os.RemoveAll(dir) })
return filepath.Join(dir, name)
}
func listen(t *testing.T, path string) net.Listener {
t.Helper()
ln, err := net.Listen("unix", path)
if err != nil {
t.Skipf("cannot bind a unix socket here (%v); run with TMPDIR=/tmp/g outside the sandbox", err)
}
t.Cleanup(func() { ln.Close() })
go func() {
for {
c, err := ln.Accept()
if err != nil {
return
}
c.Close()
}
}()
return ln
}
func TestCheckedAcceptsTheListenerItDialled(t *testing.T) {
path := short(t, "app.sock")
listen(t, path)
c, err := Checked(context.Background(), &net.Dialer{}, path)
if err != nil {
t.Fatalf("Checked: %v", err)
}
c.Close()
}
func TestCheckedRefusesASymlinkToAnotherListener(t *testing.T) {
real := short(t, "other.sock")
listen(t, real)
link := short(t, "app.sock")
if err := os.Symlink(real, link); err != nil {
t.Fatal(err)
}
c, err := Checked(context.Background(), &net.Dialer{}, link)
if c != nil {
c.Close()
}
if !errors.Is(err, ErrPeerMismatch) {
t.Fatalf("err = %v, want ErrPeerMismatch", err)
}
}
func TestCheckedRefusesTheControlEndpointBeforeDialling(t *testing.T) {
// No listener exists; the refusal must come from the path check, not
// from a connect error.
_, err := Checked(context.Background(), &net.Dialer{}, "/run/carlos-control/control.sock")
if !errors.Is(err, ErrControlEndpoint) {
t.Fatalf("err = %v, want ErrControlEndpoint", err)
}
}
func TestCheckedAcceptsAListenerCreatedByAnotherProcessShape(t *testing.T) {
// The carlos-app@.socket shape: the listener's creator is not the
// process that serves it. Credentials are never consulted, only the
// bound path, so this passes as long as the path matches.
path := short(t, "apps.sock")
listen(t, path)
c, err := Checked(context.Background(), &net.Dialer{}, path)
if err != nil {
t.Fatalf("Checked: %v", err)
}
c.Close()
}
func TestCheckedAcceptsACustomPathOutsideRunCarlos(t *testing.T) {
// codex R5-1: an operator's --socket /run/titogo/example.sock keeps
// working; only the control endpoint is refused by path.
path := short(t, "example.sock")
listen(t, path)
c, err := Checked(context.Background(), &net.Dialer{}, path)
if err != nil {
t.Fatalf("Checked: %v", err)
}
c.Close()
}
func TestCheckedPassesConnectErrorsThrough(t *testing.T) {
path := short(t, "absent.sock")
_, err := Checked(context.Background(), &net.Dialer{}, path)
if err == nil || errors.Is(err, ErrPeerMismatch) || errors.Is(err, ErrControlEndpoint) {
t.Fatalf("err = %v, want the raw connect error (ENOENT) so retry logic still sees it", err)
}
}- [ ] Step 2: Run the tests to verify they fail
Run: mkdir -p /tmp/g; TMPDIR=/tmp/g GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/unixdial -count=1 Expected: build failure, undefined: Checked.
- [ ] Step 3: Write the package
Create internal/unixdial/unixdial.go:
// Package unixdial is the one way the edge and the agent dial a unix
// socket. Two checks ride on every dial (isolated-exec design §5.4):
//
// - the reserved control endpoint is refused BEFORE connect(): a registry
// row pointing rt.Socket at the root control channel must never be
// proxied to, however it got there;
// - the listener must answer to the path that was dialled: after
// connect() and before a byte is sent, getpeername() names the path
// the LISTENER bound, which is its real path however it was reached.
// A symlink at /run/carlos/<host>.sock pointing at a sibling's socket
// or at the control socket therefore yields a mismatch, and the
// connection is closed unused.
//
// Credentials are deliberately not consulted: a listener systemd created
// and passed to a DynamicUser service reports root in SO_PEERCRED, and
// that shape (carlos-app@.socket) must keep working (codex R4-1).
package unixdial
import (
"context"
"errors"
"fmt"
"net"
"path/filepath"
"github.com/carlosframework/platform/internal/provision"
)
// ErrControlEndpoint is returned, before any connect, for a path under
// provision.ControlSocketDir.
var ErrControlEndpoint = errors.New("unixdial: refusing to dial the instance control channel as an upstream")
// ErrPeerMismatch is returned, after connect and before any byte is sent,
// when the listener's bound path is not the path that was dialled.
var ErrPeerMismatch = errors.New("unixdial: listener is bound to a different path than the one dialled")
// Checked dials path as a unix stream socket with d and applies both
// checks. Connect errors (ENOENT, ECONNREFUSED) are returned unwrapped so
// callers' retry logic keeps seeing them.
func Checked(ctx context.Context, d *net.Dialer, path string) (net.Conn, error) {
if provision.IsControlEndpoint(path) {
return nil, fmt.Errorf("%w: %s", ErrControlEndpoint, path)
}
if d == nil {
d = &net.Dialer{}
}
conn, err := d.DialContext(ctx, "unix", path)
if err != nil {
return nil, err
}
if err := verifyPeer(conn, path); err != nil {
conn.Close()
return nil, err
}
return conn, nil
}
// verifyPeer compares the peer address of a connected unix socket with the
// dialled path. net's UnixConn fills RemoteAddr from getpeername(2) at
// connect time, which is the listener's bound address.
func verifyPeer(conn net.Conn, dialled string) error {
ra := conn.RemoteAddr()
ua, ok := ra.(*net.UnixAddr)
if !ok || ua == nil {
return fmt.Errorf("%w: peer address %v is not a unix address", ErrPeerMismatch, ra)
}
want := filepath.Clean(dialled)
got := ua.Name
if got == "" || got[0] == '@' || filepath.Clean(got) != want {
return fmt.Errorf("%w: dialled %s, listener is bound to %q", ErrPeerMismatch, want, got)
}
return nil
}Note for the implementer: Go's net package sets a dialed UnixConn's remote address from getpeername(2) after connect(2) succeeds (netFD.connect returns the peer sockaddr and dial installs it). If a test shows RemoteAddr() echoing the dialled symlink path instead of the bound path, replace verifyPeer's body with an explicit unix.Getpeername through conn.(*net.UnixConn).SyscallConn().Control(...) in a _linux.go file with a !linux stub that returns nil, and keep the tests as they are. The tests are the contract.
- [ ] Step 4: Run the tests to verify they pass
Run: TMPDIR=/tmp/g GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/unixdial -count=1 -v Expected: PASS for all six (they skip, not fail, where the sandbox refuses socket(); run outside the sandbox to see them pass).
- [ ] Step 5: Wire the four dial sites
internal/edge/edge.go:2647: replace the body of dialUnixWithRetry's dial line.
func dialUnixWithRetry(ctx context.Context, d *net.Dialer, socketPath string) (net.Conn, error) {
const window = 13 * time.Second
deadline := time.Now().Add(window)
for {
conn, err := unixdial.Checked(ctx, d, socketPath)
if err == nil {
return conn, nil
}
if ctx.Err() != nil || !isRetryableDialErr(err) || time.Now().After(deadline) {
return nil, err
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(100 * time.Millisecond):
}
}
}isRetryableDialErr is unchanged: ErrControlEndpoint and ErrPeerMismatch are not ECONNREFUSED/ENOENT, so they are not retried. Add the import "github.com/carlosframework/platform/internal/unixdial".
internal/activator/backend_exec.go:916:
func socketClient(socket string) *http.Client {
return &http.Client{
Timeout: 3 * time.Second,
Transport: &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return unixdial.Checked(ctx, &net.Dialer{}, socket)
},
},
}
}internal/release/versions.go:364:
func unixHTTPClient(socket string) *http.Client {
return &http.Client{
Timeout: 3 * time.Second,
Transport: &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return unixdial.Checked(ctx, &net.Dialer{}, socket)
},
},
}
}internal/host/socket_advice.go:48:
func socketServing(socket string) bool {
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
c, err := unixdial.Checked(ctx, &net.Dialer{}, socket)
if err != nil {
return false
}
c.Close()
return true
}Add "context" and the unixdial import there if missing. Check for an import cycle: unixdial imports provision; provision must not import edge, activator, release or host (it does not today: grep -n '"github.com/carlosframework/platform/internal/' internal/provision/*.go). If release is imported by provision (it is, for the store), that is fine: release importing unixdial importing provision importing release WOULD be a cycle. Check go build ./... after wiring release/versions.go; if it reports an import cycle, move IsControlEndpoint, ControlSocketDir and ControlSocketPath out of provision into a new leaf package internal/controlsock with no imports, have provision re-export them as constants (const ControlSocketDir = controlsock.Dir), and have unixdial import controlsock only. Update Task 1's tests only if the names move; the values do not.
- [ ] Step 6: Build, vet, run the touched packages
Run: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go build ./... && go vet ./internal/edge ./internal/activator ./internal/release ./internal/host ./internal/unixdial Expected: clean.
Run: TMPDIR=/tmp/g GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/edge ./internal/activator ./internal/release ./internal/host -count=1 Expected: PASS (these bind sockets; run outside the sandbox with TMPDIR=/tmp/g).
- [ ] Step 7: gofmt and commit
gofmt -l internal
git add internal/unixdial internal/edge/edge.go internal/activator/backend_exec.go internal/release/versions.go internal/host/socket_advice.go
git commit -m "unixdial: refuse the control endpoint and verify the listener's bound path on every unix dial (plan 1, task 3)"---
Task 4: Slice and mount units, and the edge unit's move into carlos.slice
Files:
- Create:
internal/host/units/carlos.slice - Create:
internal/host/units/carlos-instances.slice - Create:
internal/host/units/data-instances.mount - Create:
internal/host/units/run-carlos-instances.mount - Create:
internal/host/units/run-carlos-logs.mount - Modify:
internal/host/units/carlos-edge.service([Unit]lines 6-9;[Service]lines 71-79) - Modify:
internal/host/units/carlos-litestream.service([Unit], line 12) - Modify:
internal/host/bootstrap.go:33(embed glob) - Test:
internal/host/units_isolation_test.go(create)
Interfaces:
- Consumes: nothing from earlier tasks.
- Produces: the unit names
carlos.slice,carlos-instances.slice,data-instances.mount,run-carlos-instances.mount,run-carlos-logs.mount, which Task 5 enables, and which plan 2's templatesRequires=.
- [ ] Step 1: Write the failing tests
Create internal/host/units_isolation_test.go:
package host
import (
"strings"
"testing"
)
func readUnit(t *testing.T, name string) string {
t.Helper()
b, err := unitsFS.ReadFile("units/" + name)
if err != nil {
t.Fatalf("%s: %v", name, err)
}
return string(b)
}
// The outer memory wall moves from the edge unit to carlos.slice
// (isolated-exec design §4.3): it bounds the agent plus every awake
// instance as before, but instances are now sibling units, not children.
func TestCarlosSliceCarriesTheOuterWall(t *testing.T) {
s := readUnit(t, "carlos.slice")
for _, want := range []string{"[Slice]", "MemoryHigh=70%", "MemoryMax=80%"} {
if !strings.Contains(s, want) {
t.Errorf("carlos.slice lacks %q", want)
}
}
e := readUnit(t, "carlos-edge.service")
if !strings.Contains(e, "Slice=carlos.slice") {
t.Error("carlos-edge.service is not in carlos.slice")
}
for _, never := range []string{"MemoryHigh=", "MemoryMax="} {
if strings.Contains(e, never) {
t.Errorf("carlos-edge.service still carries %q; the wall is the slice's now", never)
}
}
// What must NOT change on the edge unit.
for _, keep := range []string{"OOMPolicy=continue", "Delegate=cpu memory io pids", "User=carlos",
"AmbientCapabilities=CAP_NET_BIND_SERVICE", "ReadWritePaths=/data /run/carlos", "RestrictNamespaces=yes"} {
if !strings.Contains(e, keep) {
t.Errorf("carlos-edge.service lost %q", keep)
}
}
i := readUnit(t, "carlos-instances.slice")
if !strings.Contains(i, "[Slice]") {
t.Error("carlos-instances.slice has no [Slice] section")
}
for _, never := range []string{"MemoryHigh=", "MemoryMax="} {
if strings.Contains(i, never) {
t.Errorf("carlos-instances.slice carries %q; the walls are carlos.slice and the per-host slices", never)
}
}
}
// The three mounts: a bind of a raw directory onto its protected view,
// nosymfollow, with DefaultDependencies=no and explicit ordering, because
// the default ordering (before local-fs.target) and tmpfiles (after it)
// would form a cycle systemd breaks by dropping the mount (codex R2-2).
func TestInstanceMountsAreShapedForNosymfollow(t *testing.T) {
cases := map[string][2]string{
"data-instances.mount": {"/data/.instances", "/data/instances"},
"run-carlos-instances.mount": {"/run/carlos/.instances", "/run/carlos/instances"},
"run-carlos-logs.mount": {"/run/carlos/.logs", "/run/carlos/logs"},
}
for name, ww := range cases {
m := readUnit(t, name)
for _, want := range []string{
"What=" + ww[0], "Where=" + ww[1], "Type=none",
"Options=bind,nosymfollow,nosuid,nodev,noexec",
"DefaultDependencies=no",
"After=", "systemd-tmpfiles-setup.service",
"Before=", "carlos-edge.service",
"Conflicts=umount.target", "Before=umount.target",
"WantedBy=multi-user.target",
} {
if !strings.Contains(m, want) {
t.Errorf("%s lacks %q", name, want)
}
}
if ww[0] == ww[1] {
t.Errorf("%s binds a directory onto itself; the migration rename would fail with EXDEV (codex R4-2)", name)
}
}
d := readUnit(t, "data-instances.mount")
for _, want := range []string{"RequiresMountsFor=/data", "local-fs.target", "carlos-litestream.service"} {
if !strings.Contains(d, want) {
t.Errorf("data-instances.mount lacks %q", want)
}
}
e := readUnit(t, "carlos-edge.service")
for _, want := range []string{"Requires=data-instances.mount run-carlos-instances.mount run-carlos-logs.mount",
"After=network-online.target data-instances.mount run-carlos-instances.mount run-carlos-logs.mount"} {
if !strings.Contains(e, want) {
t.Errorf("carlos-edge.service lacks %q: an edge that starts without the mounts must not start", want)
}
}
l := readUnit(t, "carlos-litestream.service")
if !strings.Contains(l, "After=network-online.target data-instances.mount") {
t.Error("carlos-litestream.service is not ordered after data-instances.mount")
}
}
// Every unit file, whatever its suffix, is embedded: a .slice or .mount
// left out of the glob would be silently absent from every box.
func TestEmbeddedUnitsIncludeSlicesAndMounts(t *testing.T) {
names, err := unitNames()
if err != nil {
t.Fatal(err)
}
have := map[string]bool{}
for _, n := range names {
have[n] = true
}
for _, want := range []string{"carlos.slice", "carlos-instances.slice", "data-instances.mount", "run-carlos-instances.mount", "run-carlos-logs.mount"} {
if !have[want] {
t.Errorf("%s is not embedded (bootstrap.go's go:embed glob)", want)
}
}
}- [ ] Step 2: Run the tests to verify they fail
Run: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/host -run 'TestCarlosSlice|TestInstanceMounts|TestEmbeddedUnits' -count=1 Expected: FAIL (carlos.slice: open units/carlos.slice: file does not exist, and so on).
- [ ] Step 3: Write the unit files
internal/host/units/carlos.slice:
# The outer memory wall of the memory-pressure containment design
# (2026-08-28 §2.2), moved here from carlos-edge.service by the isolated-exec
# design (§4.3): exec-backed instances used to be the edge's children, so a
# ceiling on the edge unit bounded the agent plus every awake instance at
# once. They are sibling units now, so the ceiling lives one level up, on
# a slice that holds the edge AND carlos-instances.slice, and bounds exactly
# what it bounded before. Borrowing between agent and tenants and the OOM
# candidate set (a tenant at OOMScoreAdjust=500 is picked before the agent)
# are unchanged.
#
# Percentages so one file fits a t4g.micro and a t4g.medium; 80% leaves what
# the box's other units (keymail, console, ssm, litestream, journald) use.
[Unit]
Description=carlos (edge, activator and every exec-backed instance)
[Slice]
MemoryHigh=70%
MemoryMax=80%internal/host/units/carlos-instances.slice:
# The parent of every per-host slice (carlos-instances-<iid>.slice, created
# on demand by the first carlos-instance*@ unit that names it; isolated-exec
# design §4.3). No cap of its own: the outer wall is carlos.slice, the inner
# walls are the per-host slices the control service sets the tier on. This
# level exists so the sampler has one root to scan and so an operator can
# `systemctl status carlos-instances.slice`.
[Unit]
Description=carlos exec-backed instances
[Slice]internal/host/units/data-instances.mount:
# The protected view of the instance data tree (isolated-exec design §3.2).
#
# /data/.instances is the RAW directory, opened by the layout pass only;
# this bind of it onto /data/instances is the view everything else uses.
# nosymfollow is the isolation's belt: path resolution under this mount
# returns ELOOP at any symlink, for every process on the box, in every
# namespace that sees the mount, with no check-then-use race. An instance
# runs as the agent's uid and can write its own directory, so any symlink
# it plants would otherwise be followed by the agent (reading a sibling's
# env file as this instance's), by the edge (dialling a sibling's socket
# as this host) and by PID 1. Here none of them follow it.
#
# It is a bind of a DIFFERENT directory, not of itself, because rename(2)
# refuses to cross a mount boundary (EXDEV) even on one filesystem, and
# the migration moves /data/<host>.db into this tree by rename (codex R4-2).
#
# DefaultDependencies=no, with the ordering written out: a mount unit's
# default place is before local-fs.target, and systemd-tmpfiles-setup,
# which creates the two directories, runs after it, so the defaults form a
# cycle systemd breaks by dropping this mount (codex R2-2). Shutdown
# ordering is written out for the same reason.
[Unit]
Description=carlos instance data (bind, nosymfollow)
DefaultDependencies=no
RequiresMountsFor=/data
After=local-fs.target systemd-tmpfiles-setup.service
Before=carlos-edge.service carlos-instance-control.service carlos-litestream.service
Conflicts=umount.target
Before=umount.target
[Mount]
What=/data/.instances
Where=/data/instances
Type=none
Options=bind,nosymfollow,nosuid,nodev,noexec
[Install]
WantedBy=multi-user.targetinternal/host/units/run-carlos-instances.mount:
# The protected view of the instance socket tree (isolated-exec design
# §3.2): /run/carlos/.instances bound onto /run/carlos/instances with
# nosymfollow. See data-instances.mount for why a bind, why of a different
# directory, and why DefaultDependencies=no. Nothing is ever renamed into
# this tree, so the raw directory has no caller at all. /run is tmpfs, so
# the two directories come from the tmpfiles line every boot, which is why
# this is ordered after systemd-tmpfiles-setup.service.
[Unit]
Description=carlos instance sockets (bind, nosymfollow)
DefaultDependencies=no
After=systemd-tmpfiles-setup.service
Before=carlos-edge.service carlos-instance-control.service
Conflicts=umount.target
Before=umount.target
[Mount]
What=/run/carlos/.instances
Where=/run/carlos/instances
Type=none
Options=bind,nosymfollow,nosuid,nodev,noexec
[Install]
WantedBy=multi-user.targetinternal/host/units/run-carlos-logs.mount:
# The protected view of the agent's per-role log sockets (isolated-exec
# design §3.2, §6.4): /run/carlos/.logs bound onto /run/carlos/logs with
# nosymfollow. This directory is bound into NO instance namespace; the
# mount option is for the transition release, in which unisolated flat
# tenants still share the agent's uid and could otherwise turn a log
# socket path into a symlink that PID 1 opens as root for some unit's
# stdout (codex R3-1). See data-instances.mount for the ordering.
[Unit]
Description=carlos instance log sockets (bind, nosymfollow)
DefaultDependencies=no
After=systemd-tmpfiles-setup.service
Before=carlos-edge.service carlos-instance-control.service
Conflicts=umount.target
Before=umount.target
[Mount]
What=/run/carlos/.logs
Where=/run/carlos/logs
Type=none
Options=bind,nosymfollow,nosuid,nodev,noexec
[Install]
WantedBy=multi-user.target- [ ] Step 4: Edit the edge and litestream units
internal/host/units/carlos-edge.service, [Unit] section (lines 6-9) becomes:
[Unit]
Description=carlos edge (TLS terminator, host router, activator, release adoption)
# The three nosymfollow mounts of the isolated-exec design (§3.2) are hard
# requirements: an edge that starts without them would start unprotected,
# so it does not start at all.
Requires=data-instances.mount run-carlos-instances.mount run-carlos-logs.mount
After=network-online.target data-instances.mount run-carlos-instances.mount run-carlos-logs.mount
Wants=network-online.targetIn [Service], replace lines 71-72 (MemoryHigh=70% and MemoryMax=80%) and the comment block above them (the block that begins # The outer wall (memory-pressure spec 2026-08-28 §2.2)) with:
# The outer wall (memory-pressure spec 2026-08-28 §2.2) used to be two
# MemoryHigh/MemoryMax lines here, bounding the agent plus its exec
# children. Instances are sibling units now (isolated-exec design §4.3), so
# the wall is carlos.slice's, which holds this unit and every instance and
# bounds exactly what it bounded before. The oom_score_adj story is
# unchanged: tenants carry OOMScoreAdjust=500 in their template.
Slice=carlos.sliceKeep OOMPolicy=continue and its comment, TasksMax=1024, and Delegate= exactly as they are.
internal/host/units/carlos-litestream.service, line 12: After=network-online.target becomes After=network-online.target data-instances.mount (the replicator reads nested databases through the protected view; it must see the mount).
- [ ] Step 5: Widen the embed glob
internal/host/bootstrap.go:33:
//go:embed units/*.service units/*.timer units/*.socket units/*.path units/*.slice units/*.mount
var unitsFS embed.FS- [ ] Step 6: Run the tests to verify they pass
Run: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/host -run 'TestCarlosSlice|TestInstanceMounts|TestEmbeddedUnits|TestBootstrap|TestSystemUnits|TestAppTemplate|TestVerifyDrills' -count=1 Expected: PASS. If an existing test asserts the edge unit's MemoryMax=80% (search: grep -n 'MemoryMax=80' internal/host/*_test.go internal/caps/*_test.go), move that assertion to carlos.slice in that test and keep the rest of it.
- [ ] Step 7: Commit
git add internal/host/units/carlos.slice internal/host/units/carlos-instances.slice internal/host/units/data-instances.mount internal/host/units/run-carlos-instances.mount internal/host/units/run-carlos-logs.mount internal/host/units/carlos-edge.service internal/host/units/carlos-litestream.service internal/host/bootstrap.go internal/host/units_isolation_test.go
git commit -m "units: carlos.slice outer wall and the three nosymfollow instance mounts (plan 1, task 4)"---
Task 5: Bootstrap wiring: tmpfiles lines, directories, arming
Files:
- Modify:
internal/host/bootstrap.go(tmpfilesConfat line 61;enabledUnitsat line 116;dirsat line 296;systemctlPlanat line 165) - Modify:
internal/host/host_test.go:1195(TestBootstrapPlanJSONNamesWhatALiveRunArms) - Test:
internal/host/units_isolation_test.go(append)
Interfaces:
- Consumes: the unit names from Task 4.
- Produces:
tmpfilesConflines for/data/.instances,/data/instances,/data/layout,/run/carlos/.instances,/run/carlos/instances,/run/carlos/.logs,/run/carlos/logs,/run/carlos-control, and the probe fixture;systemctlPlanstarting.socketand.mountunits; the mounts inenabledUnits.
- [ ] Step 1: Write the failing tests
Append to internal/host/units_isolation_test.go:
// tmpfiles creates the raw directories and the mountpoints every boot
// (/run is tmpfs), with the modes the design gives them (§3.1, §3.2):
// the socket trees keep the carlos-sockets group and setgid (issue #103),
// the log tree and data trees are the agent's alone, and the control
// directory is root's so no carlos process can create anything in it.
func TestTmpfilesCreatesTheInstanceTrees(t *testing.T) {
for _, want := range []string{
"d /data/.instances 0700 carlos carlos -",
"d /data/instances 0755 carlos carlos -",
"d /data/layout 0700 carlos carlos -",
"d /run/carlos/.instances 2750 carlos carlos-sockets -",
"d /run/carlos/instances 2750 carlos carlos-sockets -",
"d /run/carlos/.logs 0700 carlos carlos -",
"d /run/carlos/logs 0700 carlos carlos -",
"d /run/carlos-control 0750 root carlos -",
"f /etc/carlos/probe-secret 0640 root carlos - probe",
} {
if !strings.Contains(tmpfilesConf, want+"\n") {
t.Errorf("tmpfilesConf lacks %q", want)
}
}
// The existing lines stay: the flat socket dir and the apps subtree.
for _, keep := range []string{"d /run/carlos 2750 carlos carlos-sockets -", "d /run/carlos/apps 0750 carlos carlos -"} {
if !strings.Contains(tmpfilesConf, keep+"\n") {
t.Errorf("tmpfilesConf lost %q", keep)
}
}
}
// bootstrap -root <dir> creates the same directories in the staging tree
// (a live run's chown -R covers them), so a fresh box has them before the
// mounts try to bind them.
func TestBootstrapCreatesTheInstanceDirectories(t *testing.T) {
root := t.TempDir()
if err := bootstrap(root, nil); err != nil {
t.Fatalf("bootstrap: %v", err)
}
for _, d := range []string{"data/.instances", "data/instances", "data/layout", "run/carlos/.instances", "run/carlos/instances", "run/carlos/.logs", "run/carlos/logs", "run/carlos-control"} {
if fi, err := os.Stat(filepath.Join(root, d)); err != nil || !fi.IsDir() {
t.Errorf("%s: %v", d, err)
}
}
}
// The mounts are enabled AND started by the plan: `systemctl enable` on a
// mount unit writes a symlink for the next boot and nothing else, the same
// trap timers and path units fell into (codex R1-14). A socket unit the
// same. The relay is still the only service the plan starts.
func TestSystemctlPlanStartsMountsAndSockets(t *testing.T) {
plan := systemctlPlan([]string{"carlos-edge.service", "data-instances.mount", "run-carlos-instances.mount", "run-carlos-logs.mount", "carlos-adopt.timer", "carlos-instance-control.socket"})
var started []string
for _, args := range plan {
if args[0] == "start" {
started = append(started, args[1:]...)
}
}
got := strings.Join(started, " ")
for _, want := range []string{"data-instances.mount", "run-carlos-instances.mount", "run-carlos-logs.mount", "carlos-adopt.timer", "carlos-instance-control.socket"} {
if !strings.Contains(got, want) {
t.Errorf("plan does not start %s: %v", want, plan)
}
}
if strings.Contains(got, "carlos-edge.service") {
t.Errorf("plan starts the edge: %v", plan)
}
}
func TestEnabledUnitsIncludeTheMounts(t *testing.T) {
have := map[string]bool{}
for _, u := range enabledUnits {
have[u] = true
}
for _, want := range []string{"data-instances.mount", "run-carlos-instances.mount", "run-carlos-logs.mount"} {
if !have[want] {
t.Errorf("enabledUnits lacks %s", want)
}
}
}Add "os" and "path/filepath" to that test file's imports.
- [ ] Step 2: Run the tests to verify they fail
Run: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/host -run 'TestTmpfiles|TestBootstrapCreatesTheInstance|TestSystemctlPlanStartsMounts|TestEnabledUnitsInclude' -count=1 Expected: FAIL on every one.
- [ ] Step 3: Extend
tmpfilesConf, the directory list,enabledUnitsandsystemctlPlan
internal/host/bootstrap.go:61, replace the constant:
const tmpfilesConf = "d /run/carlos 2750 " + serviceUser + " " + socketsGroup + " -\n" +
"d /run/carlos/apps 0750 " + serviceUser + " " + serviceUser + " -\n" +
// The isolated-exec trees (design §3.1, §3.2). Each protected view is
// a nosymfollow bind of the dotted raw directory beside it (the mount
// units); tmpfiles creates both every boot because /run is tmpfs and
// because the mounts are ordered after it. The socket trees keep the
// consumers group and setgid, exactly like /run/carlos above, so an
// enrolled router can still dial a migrated instance (issue #103).
"d /data/.instances 0700 " + serviceUser + " " + serviceUser + " -\n" +
"d /data/instances 0755 " + serviceUser + " " + serviceUser + " -\n" +
"d /data/layout 0700 " + serviceUser + " " + serviceUser + " -\n" +
"d /run/carlos/.instances 2750 " + serviceUser + " " + socketsGroup + " -\n" +
"d /run/carlos/instances 2750 " + serviceUser + " " + socketsGroup + " -\n" +
"d /run/carlos/.logs 0700 " + serviceUser + " " + serviceUser + " -\n" +
"d /run/carlos/logs 0700 " + serviceUser + " " + serviceUser + " -\n" +
// The root instance-control channel's directory (design §5.2): root's,
// so no carlos process can create, rename or replace anything in it;
// group carlos with search so the agent can connect.
"d /run/carlos-control 0750 root " + serviceUser + " -\n" +
// The readiness probe's fixture (design §8.5, codex R11-1): a file
// root:carlos 0640 that an isolated instance must NOT be able to read.
// Written here, not by the control service, whose ProtectSystem=strict
// makes /etc read-only to it.
"f /etc/carlos/probe-secret 0640 root " + serviceUser + " - probe\n"In bootstrapWith's dirs slice (line 296 onward), add after the {filepath.Join(runDir, "apps"), 0o750}, entry:
// The isolated-exec trees (design §3.1). The dotted names are the
// raw directories the mount units bind onto the undotted views;
// see tmpfilesConf for the modes and why both exist.
{filepath.Join(dataDir, ".instances"), 0o700},
{filepath.Join(dataDir, "instances"), 0o755},
{filepath.Join(dataDir, "layout"), 0o700},
{filepath.Join(runDir, ".instances"), 0o750 | os.ModeSetgid},
{filepath.Join(runDir, "instances"), 0o750 | os.ModeSetgid},
{filepath.Join(runDir, ".logs"), 0o700},
{filepath.Join(runDir, "logs"), 0o700},
// Root-owned on a live box (privilegedPlan chowns /run/carlos
// recursively to the service user; this one sits BESIDE it, not
// under it, precisely so that chown never reaches it).
{filepath.Join(root, "run", "carlos-control"), 0o750},enabledUnits (line 116):
var enabledUnits = []string{"carlos-edge.service", relayUnitName, "carlos-adopt.timer", "carlos-adopt.path", "carlos-stage.path", "carlos-restart.path",
"carlos-system-update.path", "carlos-system-recover.service",
// The isolated-exec mounts (design §3.2): the edge Requires= them.
"data-instances.mount", "run-carlos-instances.mount", "run-carlos-logs.mount"}systemctlPlan (line 165): the armed predicate becomes
for _, u := range units {
if strings.HasSuffix(u, ".timer") || strings.HasSuffix(u, ".path") ||
strings.HasSuffix(u, ".socket") || strings.HasSuffix(u, ".mount") || u == relayUnitName {
armed = append(armed, u)
}
}and extend the function's doc comment with one paragraph:
// Socket and mount units are armed the same way (isolated-exec design
// §8.6, codex R1-14): an enabled-but-unstarted socket is a control channel
// nobody is listening on, and an enabled-but-unmounted bind is an instance
// tree with no protection, until the next reboot. Starting either is
// idempotent.Update TestBootstrapPlanJSONNamesWhatALiveRunArms (host_test.go:1195): add the three mounts to its want map. Its "starts no service but the relay" loop is unchanged and still passes.
- [ ] Step 4: Run the tests to verify they pass
Run: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/host -count=1 Expected: PASS for the whole package. (TestBootstrapWritesJournaldCap and the other bootstrap tests run bootstrap(root, nil) against a temp root and are unaffected by the new directories.)
- [ ] Step 5: Full gate
Run, outside the sandbox with TMPDIR=/tmp/g:
export GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off TMPDIR=/tmp/g
go build ./... && go vet ./... && gofmt -l . && go test ./... -count=1
GOOS=darwin go build ./... && GOOS=windows go build ./... && GOARCH=arm64 go build ./...Expected: every package passes, gofmt -l . prints nothing, the three cross-compiles build.
- [ ] Step 6: Commit
git add internal/host/bootstrap.go internal/host/host_test.go internal/host/units_isolation_test.go
git commit -m "bootstrap: create and arm the isolated-exec trees, mounts and control directory (plan 1, task 5)"---
Self-review
Spec coverage for this plan's scope. §3.1 helpers: Task 1. §3.1 InstanceID bound and the _ domain rule: Task 1. §3.2 mounts, tmpfiles, instancefs, O_EXCL writes: Tasks 2, 4, 5. §4.3 carlos.slice, carlos-instances.slice, edge unit changes: Task 4. §5.4 dial checks on every dial the edge and agent make: Task 3 (edge unixProxy via dialUnixWithRetry, activator socketClient, release unixHTTPClient, host socketServing; schedule_wiring.proxyFor uses edge.UnixProxy and is covered; internal/vet/vet.go is a local harness dialling a socket it created itself and is deliberately left alone). §8.6 planner starts sockets and mounts: Task 5. §9 units and installers, tmpfiles: Tasks 4, 5. Deferred to later plans, by design: the three templates, the probe, the control units and verbs (plan 2); the activator unit path and every consumer of the nested paths (plan 3); the layout verb, journal, status field, downgrade guards and docs/site/cli.md (plan 4). update-platform-binary.sh in pinfra (enable the new units by name) is a pinfra change noted for the roll, outside this repo.
Placeholder scan. None; every step carries its code. The one conditional in Task 3 step 5 (an import cycle through release) names the exact fallback.
Type consistency. InstanceID, InstanceDir, NestedDBPath, InstanceRunDir, NestedSocket, LogSocket, IsControlEndpoint, ControlSocketDir, ControlSocketPath, MaxInstanceID, MaxUnixPath, LongestSocketAddress are spelled identically in Tasks 1 and 3. unixdial.Checked(ctx, d, path) is spelled identically at all four call sites. unitNames() exists (bootstrap.go:482) and is used unchanged in Task 4's test.