carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1**R1-1 — P1 — §5.4: Recommend the control service, with an authenticated control protocol and protected socket resolution.**
2
3The proposed service is reachable through the agent. Instance A can replace its writable `app.sock` with a symlink to `/run/carlos/instance-control.sock`, then send `POST /stop?host=B&role=run` through A’s public route. The edge preserves the request and resolves the socket path in **its own** namespace as `carlos` (`internal/edge/edge.go:2000–2012`). The control service receives an authorized-uid connection carrying an attacker’s request. The same technique reaches sibling application and control sockets. No registry mutation or malformed hostname is needed.
4
5Polkit can express the requested unit-name and verb restrictions: systemd supplies `unit` and `verb` under `org.freedesktop.systemd1.manage-units`, including `set-property` and `reset-failed`. §5.4’s claim that setting properties requires a different action is wrong. However, those details do not constrain property names or values, and a uid-only grant also authorizes every tenant through the still-visible system bus. The wildcard additionally permits names beyond the three installed templates; transient-unit creation uses the same `start` authorization. [systemd authorization implementation](https://raw.githubusercontent.com/systemd/systemd/v257/src/core/dbus-util.c), [unit methods](https://raw.githubusercontent.com/systemd/systemd/v257/src/core/dbus-unit.c), [manager methods](https://raw.githubusercontent.com/systemd/systemd/v257/src/core/dbus-manager.c).
6
7Installing polkit means adding `polkitd`, its Duktape rules engine and missing dependencies, plus rules installation and lifecycle management. Debian’s arm64 daemon package itself occupies about 701 KiB; actual incremental cost depends on the image. [Debian package metadata](https://packages.debian.org/trixie/polkitd).
8
9`set-property --runtime` **does** support a never-started instance of an installed template: `SetUnitProperties` loads and validates the unit first. Retain that mechanism.
10
11**Correction:** retain the narrow service, authenticate requests independently of socket uid using credentials unavailable to tenants, and make every agent socket dial resistant to tenant-controlled symlinks and replacement races. A peer-uid check alone does not fix the proxy attack.
12
13**R1-2 — P1 — §§4.1, 4.2, 6.4: The writable log pathname grants root-opened file descriptors to tenants.**
14
15The running main process can unlink `log.sock` and replace it with a symlink before its sidecar starts. `StandardOutput=file:` follows that pathname outside the sandbox. In systemd 257, `acquire_path()` uses `open()` without `O_NOFOLLOW`; stdout setup precedes mount isolation and dropping uid. A symlink to a root-owned file therefore hands the sidecar a writable descriptor to that file. `ProtectSystem=strict` cannot revoke an already-open descriptor. [systemd execution implementation](https://raw.githubusercontent.com/systemd/systemd/v257/src/core/exec-invoke.c).
16
17**Correction:** place log listeners in agent-controlled directories absent from every tenant’s writable mounts. Give each role a separate protected endpoint. Test pathname replacement before sidecar startup against a root-owned canary file.
18
19**R1-3 — P1 — §§3, 6.1, 6.2, 6.8, 7: Agent filesystem operations bypass the namespace boundary.**
20
21A tenant can replace its own `<host>.env` with a symlink to a sibling’s env file or `/data/litestream.env`. On the next wake, `readEnvFile` follows it in the agent’s namespace and passes the contents into the tenant’s environment (`backend_exec.go:785–794,966–971`). `EnsureAdminToken` notices a symlink only when considering chmod; it still returns success for an existing token (`admintoken.go:116–165`).
22
23There is also a write route: config delivery writes a predictable `.config.env.tmp` using `os.WriteFile`, which follows a planted symlink and truncates its target (`syncexec.go:921–926`). The launch files are likewise tenant-writable despite their “agent-written, 0600” description: the tenant owns their directory and shares the uid.
24
25**Correction:** audit every agent read, write, database open and cleanup against tenant-controlled paths. Use race-safe confined resolution, safe temporary-file creation and replacement, and protected launch metadata. Reject escaping links rather than merely logging them. Test sibling-secret reads and sibling-file overwrites from inside an instance.
26
27**R1-4 — P1 — §§6.3–6.5: Log EOF does not prove process exit.**
28
29A live app can close or redirect stdout and stderr while continuing to serve and write its database. §6.5 nevertheless marks it exited and closes `done`. The sweeper then recycles it (`activator.go:1119–1122`), and `done` no longer proves the termination required by the stop contract. Today `cmd.Wait()` waits for process death independently of output EOF (`backend_exec.go:722–736`).
30
31The proposed polling also contradicts the claim that `activator.go` remains unchanged: `sweepOnce` contains no unit polling, and calls `Exited()` while holding the activator mutex (`activator.go:1105–1119`).
32
33**Correction:** EOF should finish log collection only. Observe terminal systemd state independently, associate observations with the correct invocation, and publish process completion only after termination is proven. Keep blocking systemctl calls outside the activator mutex.
34
35**R1-5 — P1 — §§4.2, 5.3, 6.2: Seeds lose bounded execution and cancellation.**
36
37The seed unit is `Type=oneshot` without `TimeoutStartSec`; systemd disables the startup timeout by default for that type. `/seed` blocks, while `/stop` excludes the seed role. Cancelling an HTTP request or its `systemctl` client does not terminate PID 1’s service. [systemd service documentation](https://raw.githubusercontent.com/systemd/systemd/v257/man/systemd.service.xml).
38
39Today seeds have a two-minute execution budget and are killed on cancellation (`backend_exec.go:368–408,461–467`). A seed error releases the lease (`activator.go:824–828`); leaving the unit running permits database writes after release.
40
41**Correction:** set an explicit seed timeout, support stopping seeds, and wait for their termination before cleanup or lease release. Define seed directory, launch-file and log-listener preparation before `Start`, because seeds run before it.
42
43**R1-6 — P2 — §§4.2, 6.4: Main and sidecar cannot share a single-connection log listener.**
44
45Both units use `log.sock`, but the listener accepts exactly one connection and closes the next. Main and sidecar explicitly run concurrently. The sidecar therefore loses its output and may receive broken-pipe errors. An app connecting to the listener is also not necessarily harmless: it can consume the connection intended for another role.
46
47**Correction:** use separate per-role listeners, with ownership and cleanup defined for seed completion, sidecar retries and main shutdown. Verify that simultaneous main and sidecar output reaches `carlos logs`.
48
49**R1-7 — P2 — §§4.1, 6.7: `PartOf=` authorizes restarts outside the activator’s lifecycle.**
50
51`PartOf=carlos-edge.service` propagates **restart**, not just stop. PID 1 can consequently queue instance starts using old launch descriptions while the new agent is preparing its state. `After=carlos-edge.service` does not wait for the Go startup sweep: the edge is `Type=simple`. `Restart=no` does not prevent dependency-propagated restart jobs. [systemd unit documentation](https://raw.githubusercontent.com/systemd/systemd/v257/man/systemd.unit.xml).
52
53The sweep’s instruction to stop “active” units also omits activating seeds and pending start jobs.
54
55**Correction:** specify stop-only coupling or an explicit restart protocol that cancels and drains all role jobs before migration and activation. Verify edge restart while an instance or seed start is pending.
56
57**R1-8 — P2 — §§4.2, 6.6: The roles no longer share one instance memory tier.**
58
59The only proposed slice is box-wide. Independent main and sidecar `MemoryMax` values do not enforce one combined host limit, and the seed receives no tier override. Today all three shapes enter the same capped host cgroup (`backend_exec.go:149–155,373–375,890–895`).
60
61`OOMPolicy=kill` correctly implements `memory.oom.group=1` for the service’s cgroup; it does not include a sibling sidecar unit. [systemd service documentation](https://raw.githubusercontent.com/systemd/systemd/v257/man/systemd.service.xml).
62
63**Correction:** give each host a resource parent containing all three roles, enforce the tier there, and define the family’s OOM behavior. Preserve the children tier for seeds.
64
65**R1-9 — P2 — §§6.6, 9: The sampler path is wrong, and the console is a missing consumer.**
66
67`carlos-instances.slice` lives under `carlos.slice`, so the proposed scan root lacks a hierarchy component. The actual path is `/sys/fs/cgroup/carlos.slice/carlos-instances.slice/`. [systemd slice documentation](https://raw.githubusercontent.com/systemd/systemd/v257/man/systemd.slice.xml).
68
69Discovering the new services still does not preserve console metrics. `internal/console/metricsview.go:728–729` recognizes existing `carlos-app@` and `carlos-exec@` keys; unknown keys are discarded at `760–762`. The discovery implementation is in `internal/host/metrics_probes.go`, also missing from §9. Counting only run and sidecar omits seed work previously charged to the host.
70
71**Correction:** specify the real hierarchy, aggregation across roles, and a compatible published metric key. Include both consumers and verify the member-visible numbers, not merely filesystem discovery.
72
73**R1-10 — P3 — §4.3: The 60/20/20 split needs cgroup measurements, not an RSS check.**
74
75The preceding memory design measured the agent’s leaf at 1.2 GB, including 1.12 GB of reclaimable page cache; 110 MB was process RSS (`2026-08-28-memory-pressure-containment-design.md`, §1). The new edge-only ceiling also removes tenant processes from its OOM candidate set. Their `OOMScoreAdjust=500` cannot protect the agent when its own 20% limit is exceeded.
76
77**Correction:** gate the split on peak cgroup charge, reclaim behavior and replication/release workloads on micro-sized hosts. Consider a common 80% parent ceiling with subordinate limits to retain borrowing. The proposed numbers are not established by the cited measurement.
78
79**R1-11 — P1 — §8.1: Migration includes processes the activator does not own.**
80
81The migration predicate checks kind, empty backing and socket, but omits `Hibernate`. The runtime explicitly treats unflagged instances as externally served: only hibernate-flagged instances enter the activator (`activator.go:1379–1384`; `edge.go:630–638`).
82
83Those external processes need not die with the agent. Moving their database artifacts and changing their socket rows can therefore happen underneath a live writer.
84
85**Correction:** derive eligibility from the actual activator-management predicate, including routing exclusions. Refuse automatic migration of externally supervised rows until their supervisor and process shutdown are explicitly handled.
86
87**R1-12 — P1 — §§8.1, 6.8: Partial migration is fenced against wakes but not against destructive consumers.**
88
89After a rename failure, the spec leaves the registry row flat and continues. Suppose the database rename succeeded before a later artifact failed. A residence move in `MoveCutting` then sees the old DB path absent and skips its final checkpoint (`movespass.go:285–291`), mirrors the previous replica, purges the old path and completes the region flip (`293–305`). Changing that consumer to `rt.DBPath` does not help: the row deliberately still names the old path.
90
91A row CAS failure after successful renames similarly has no recovery protocol.
92
93**Correction:** persist migration state and fence every consumer until completion or reversal, especially residence moves. Specify recovery from CAS conflicts and destination collisions, and recognize already-migrated rows explicitly. Test a partial rename followed by a cutting move.
94
95**R1-13 — P1 — §8.3: The rollback mechanism contradicts the old code and can discard newer data.**
96
97The old backend does not rebuild the flat DB path when the row supplies one: both `Restore` and `Start` honor `rt.DBPath` (`backend_exec.go:220–225,557–562`). With the migrated row retained, rollback uses the nested database but reads or creates **flat** admin/config files (`579,607,785–794`). It does not perform the claimed fresh restore.
98
99If rollback does produce a newer flat database, the rule that the nested copy wins on re-upgrade discards rollback-period writes from the serving history. Quarantining the newer file does not make that a lossless rollback.
100
101**Correction:** define a stopped, reversible migration of rows and all associated artifacts, or explicit compatibility behavior in the rollback binary. Never select the nested database unconditionally when two histories exist.
102
103**R1-14 — P1 — §9: Adding the socket to `enabledUnits` does not start it.**
104
105The claim about `systemctlPlan` is false. Its start predicate includes `.timer`, `.path` and the relay service, but excludes `.socket` (`internal/host/bootstrap.go:165–179`). The socket can therefore be enabled but inactive until reboot. `ensureSystemUnits` also uses this planner (`systemunits.go:102`).
106
107**Correction:** add socket activation to the planner and its tests. Extend repair checks to the control socket’s active state, and explicitly enable/start it in `update-platform-binary.sh` before restarting the edge. Verify readiness before allowing wakes.
108
109**R1-15 — P1 — §8.4: The permitted `PrivatePIDs=` fallback abandons the isolation property.**
110
111The rollout explicitly permits removing `PrivatePIDs=` and proceeding with same-uid `/proc/<pid>/root` traversal restored. That defeats the database, credential and socket isolation the change is intended to provide. `ProtectProc=invisible` does not substitute for the missing PID namespace. The systemd documentation also says `PrivatePIDs=` is ignored when kernel support is unavailable. [systemd execution documentation](https://raw.githubusercontent.com/systemd/systemd/v257/man/systemd.exec.xml).
112
113**Correction:** refuse the rollout or refuse tenant starts when the required isolation cannot be established. Gate on observed namespace behavior and effective IMDS denial, not solely the systemd version or directive presence.
114
115**R1-16 — P2 — §6.2: `syscall.Exec` changes environment override behavior.**
116
117`childEnv` appends tenant values after inherited non-`CARLOS_*` values (`backend_exec.go:802–813`). Today `exec.Cmd` removes duplicates in favor of the last value. `syscall.Exec` passes duplicates through, and Go’s environment initialization keeps the first occurrence. A tenant override such as `PATH` can consequently lose to the inherited value.
118
119Evidence: local Go sources `os/exec/exec.go:1231,1246–1247`, `syscall/exec_unix.go:280`, and `syscall/env_unix.go:31–44`.
120
121**Correction:** normalize the launch environment to one entry per key using the existing last-value-wins behavior. Test effective values inside the executed process; comparing the JSON array with `cmd.Env` misses this regression.
122
123**R1-17 — P2 — §9: The litestream registry claim does not replace discovery.**
124
125`FleetDBsWithSkipped` builds a claimed-path filter, then applies it to paths the glob found (`internal/ops/litestream.go:354–388`). It does not append every claimed DB path. Only unit-backed paths and the console database are added separately (`390–408`).
126
127Thus extending the glob is required for discovery of nested non-hibernating exec databases, not merely for reporting unrouted leftovers. Hibernating databases remain deliberately excluded at `420–421`.
128
129**Correction:** state the actual discovery/filter/exclusion behavior and make nested discovery mandatory wherever nested non-hibernating rows are supported. Test both replication coverage and skipped reporting.
130
131**R1-18 — P2 — §§8.2, 8.4, 9, 10: The operational workflow remains box-dependent.**
132
133The proposed precheck reads a box registry copy or falls back to SSM. The repository instructions explicitly identify that shape as a product gap. The refusal points users to an operator-local `carlos route` repair, and the spec provides no member-visible proof that isolation is active. New CLI surfaces also omit `docs/site/cli.md` from the change and verification lists.
134
135**Correction:** define an API-backed precheck and repair workflow, a member-visible isolation/refusal status, and CLI documentation. Verify these through member credentials without filesystem or SSM access.
136
137**R1-19 — P2 — §11: `UnitBackend` cannot use this root channel unchanged.**
138
139The follow-up claims `UnitBackend` gets working starts “for free,” but it addresses `carlos-app@<host>.service`; the proposed service constructs only `carlos-instance`, `carlos-instance-seed` and `carlos-instance-sidecar` names (`backend_unit.go:94–103`; spec §5.3).
140
141**Correction:** remove that claim. Supporting `UnitBackend` requires a separately specified authorization extension; do not widen the instance-control allowlist implicitly.
142
143**Verdict: request changes.**