carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1# One `carlos run` command against an isolated exec-backed instance (run
2# spec §5.4). It is the seed template outside seven lines, and a test holds
3# it there (TestCmdTemplateEqualsSeedOutsideTheNamedLines): the same uid,
4# namespace, bind mounts, slice and working directory as the instance, so
5# the command has exactly the instance's reach. The agent starts it
6# through the control service's POST /cmd and follows it with GET
7# /cmd/<host>; the agent, not this file, enforces the run's own timeout.
8[Unit]
9Description=carlos exec-backed instance %I (cmd)
10# Stop-only coupling, as for the seed: when the edge stops, the command
11# stops, and only the agent starts one. A run picked before an agent
12# restart is never run again (run spec §5.2).
13BindsTo=carlos-edge.service
14Requires=data-instances.mount run-carlos-instances.mount run-carlos-logs.mount
15After=carlos-edge.service data-instances.mount run-carlos-instances.mount run-carlos-logs.mount
16
17[Service]
18Type=oneshot
19RemainAfterExit=yes
20# The result stays loaded, active (exited) or failed, until the agent has
21# read it and stopped the unit (codex 4).
22# A backstop past the run cap of 6 hours; the agent stops the unit at the
23# run's own timeout.
24TimeoutStartSec=6h5m
25ExecStart=/opt/carlos/carlos instance-exec cmd %I
26# The advisory exit record, as for the other templates. Not "+" (codex R3-2).
27ExecStopPost=/opt/carlos/carlos instance-exit cmd %I
28User=carlos
29Group=carlos
30Slice=carlos-instances-%i.slice
31# SIGTERM to the command, SIGKILL to anything left 20 s later.
32KillMode=mixed
33TimeoutStopSec=20
34# No SuccessExitStatus=: the agent reads the exit status itself.
35# Two listeners, so stdout and stderr stay apart (run spec §5.5). PID 1
36# opens both as root, outside the namespace, on the nosymfollow logs mount.
37StandardOutput=file:/run/carlos/logs/%I.cmd.sock
38StandardError=file:/run/carlos/logs/%I.cmd-err.sock
39# The instance's own two directories, the only writable data paths.
40BindPaths=/data/instances/%I /run/carlos/instances/%I
41ReadWritePaths=/data/instances/%I /run/carlos/instances/%I
42# The working directory is the instance's own data directory, the one its
43# database is in (discussion #256 item D). A flat child ran with cwd /,
44# and an app writing relative paths (./data/blobs) wrote under /data;
45# without this line a unit's cwd is / on a read-only tmpfs and that app
46# exits at start with "read-only file system". systemd applies it after
47# the namespace is set up, so the bind above is what it chdirs into. Same
48# value in all three templates: the seed and the sidecar see the files
49# the instance made from the same place.
50WorkingDirectory=/data/instances/%I
51# Its own build, and no other app's (#214 item 2). The namespace block
52# empties /opt/carlos; these put back /opt/carlos/live/<host>, a link to
53# ../app, and /opt/carlos/app, which PID 1 binds by chasing the source
54# link to the build the run was admitted against at every start. The
55# control service writes both sources before it starts the unit. They are
56# the command's own, cmd-app and cmd-live, not the app and live the
57# instance and sidecar templates bind (merge ruling M4): a sidecar starting
58# on another build around a swap can never repoint the source this unit
59# is about to bind, nor the reverse.
60BindReadOnlyPaths=/run/carlos-control/exe/%I/cmd-app:/opt/carlos/app /run/carlos-control/exe/%I/cmd-live:/opt/carlos/live
61
62# --- the namespace (titogo@.service, 2026-07-31 design, on systemd 257) ---
63ProtectSystem=strict
64TemporaryFileSystem=/data:ro
65TemporaryFileSystem=/run/carlos:ro
66# Every other tree a carlos-uid unit writes on the box is hidden the same
67# way (codex R9-2): the restore drills' cache under /var/cache, and
68# /var/lib as a class. An instance needs nothing from either.
69TemporaryFileSystem=/var/cache:ro
70TemporaryFileSystem=/var/lib:ro
71# The version store holds every app's build on the box, world-readable so
72# a unit can exec its own (#214 item 2). An instance sees none of it: an
73# empty /opt/carlos with the platform binary that ExecStart= and
74# ExecStopPost= run put back, and, outside this block because it is per
75# instance, its own build at /opt/carlos/live/<host>.
76TemporaryFileSystem=/opt/carlos:ro
77BindReadOnlyPaths=/opt/carlos/carlos
78# Root-written credentials a carlos-GROUP process may read live here:
79# /etc/carlos/relay-masters is root:carlos 0640 for carlos-relay.service,
80# and an instance's Group=carlos would read it too (codex R10-1). An
81# instance gets its configuration through its launch file and needs
82# nothing from /etc/carlos. /run/carlos-control is the root control
83# service's socket directory (design §5.2): hiding it here too means a
84# connect from inside any instance's namespace is impossible outright,
85# not merely refused by the peer check on the far end (final review,
86# plan 2, item 3).
87InaccessiblePaths=/etc/carlos /run/carlos-control
88ProtectHome=yes
89PrivateTmp=yes
90PrivateDevices=yes
91# PrivateDevices= bind-mounts the HOST's /dev/shm into the private /dev,
92# and every instance is uid carlos, so a sibling's shared memory would be
93# one open away. Each unit gets its own, charged to its own cgroup.
94TemporaryFileSystem=/dev/shm:mode=1777,nosuid,nodev
95# POSIX message queues and SysV IPC belong to an IPC namespace, and every
96# instance is uid carlos, so a sibling's queue would be one mq_open away
97# (#178). PrivateIPC= gives each unit its own namespace, and mq_open()
98# goes through that namespace's own internal mount. PrivateDevices=
99# carries the host's /dev/mqueue into the private /dev the way it carries
100# /dev/shm, and that mount would still list and open the host namespace's
101# queues, so it is replaced the same way.
102PrivateIPC=yes
103TemporaryFileSystem=/dev/mqueue:ro
104# /sys/fs/cgroup read-only: the edge's delegated subtree is carlos-owned,
105# and its cgroup.kill / cgroup.freeze would otherwise be one write away
106# for a same-uid tenant (codex R9-1).
107ProtectControlGroups=yes
108ProtectProc=invisible
109ProcSubset=pid
110PrivatePIDs=yes
111RestrictNamespaces=yes
112NoNewPrivileges=yes
113CapabilityBoundingSet=
114RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
115# Abstract unix sockets live in the network namespace, which instances
116# share with the box because they need outbound TCP, and the line above
117# cannot tell them from path sockets (#178). The trampoline scopes them
118# with Landlock before it execs the app (instanceexec.ScopeAbstractUnix):
119# an instance can connect only to abstract sockets its own processes
120# bound. A sibling can still bind a name first; that is a refusal of
121# service, never a connection.
122#
123# Egress (#214 item 3): the internet, and nothing on the box or private.
124# Loopback is where the console and other tenants' addr upstreams listen;
125# RFC 1918, ULA, link-local and CGNAT are the VPC, other boxes and the
126# cloud's internal services. IMDS stays named so the property is
127# greppable. A cgroup filter: it drops in both directions for every
128# socket the unit opens. The one thing an instance needs from these
129# ranges is the box's DNS resolver, so the control service re-opens
130# exactly the nameservers the instance sees with IPAddressAllow= on
131# carlos-instances.slice (an allow wins, and a slice's lists reach every
132# unit below it). The probe checks both halves.
133IPAddressDeny=localhost multicast 0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 169.254.0.0/16 169.254.169.254/32 172.16.0.0/12 192.0.0.0/24 192.168.0.0/16 198.18.0.0/15 240.0.0.0/4 64:ff9b::/96 fc00::/7 fd00:ec2::254/128 fe80::/10 fec0::/10
134# An allow opens an address both ways and on every port, so a loopback
135# resolver can never be opened: a tenant could bind systemd-resolved's
136# 127.0.0.53 itself and reach every listener bound to all addresses
137# through it. Where resolved runs, instances resolve through its
138# upstreams instead, and the control service opens those. The "-" makes
139# this a no-op on a box without resolved.
140BindReadOnlyPaths=-/run/systemd/resolve/resolv.conf:/etc/resolv.conf
141ProtectKernelTunables=yes
142ProtectKernelModules=yes
143ProtectKernelLogs=yes
144ProtectClock=yes
145ProtectHostname=yes
146RemoveIPC=yes
147LockPersonality=yes
148RestrictRealtime=yes
149# No RestrictSUIDSGID=: systemd cannot read the mode inside openat2's
150# open_how, so it refuses openat2 outright (ENOSYS), and every instancefs
151# open in here is an openat2 (#126, first roll; systemd 257). What it
152# guarded is covered: with no capabilities an instance can only make a
153# set-id file owned by carlos, which gains nothing for another carlos
154# process, NoNewPrivileges= voids it in here, and no other uid can reach
155# one (the tenant trees are nosuid, PrivateTmp's sit under a root 0700
156# directory).
157MemoryDenyWriteExecute=yes
158SystemCallArchitectures=native
159
160# --- per-service resources; the tier lives on the host slice (§4.3) ---
161TasksMax=256
162# The kernel picks a tenant before it picks anything else in carlos.slice.
163OOMScoreAdjust=500
164# One OOM kill takes every process of THIS service (systemd sets
165# memory.oom.group on the service cgroup for OOMPolicy=kill). The sidecar
166# is a sibling service with its own family rule (§4.3).
167OOMPolicy=kill
168