carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

R1-1 — P1 — §5.4: Recommend the control service, with an authenticated control protocol and protected socket resolution.

The 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.

Polkit 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, unit methods, manager methods.

Installing 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.

set-property --runtime does support a never-started instance of an installed template: SetUnitProperties loads and validates the unit first. Retain that mechanism.

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.

R1-2 — P1 — §§4.1, 4.2, 6.4: The writable log pathname grants root-opened file descriptors to tenants.

The 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.

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.

R1-3 — P1 — §§3, 6.1, 6.2, 6.8, 7: Agent filesystem operations bypass the namespace boundary.

A 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).

There 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.

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.

R1-4 — P1 — §§6.3–6.5: Log EOF does not prove process exit.

A 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).

The 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).

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.

R1-5 — P1 — §§4.2, 5.3, 6.2: Seeds lose bounded execution and cancellation.

The 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.

Today 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.

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.

R1-6 — P2 — §§4.2, 6.4: Main and sidecar cannot share a single-connection log listener.

Both 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.

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.

R1-7 — P2 — §§4.1, 6.7: PartOf= authorizes restarts outside the activator’s lifecycle.

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.

The sweep’s instruction to stop “active” units also omits activating seeds and pending start jobs.

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.

R1-8 — P2 — §§4.2, 6.6: The roles no longer share one instance memory tier.

The 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).

OOMPolicy=kill correctly implements memory.oom.group=1 for the service’s cgroup; it does not include a sibling sidecar unit. systemd service documentation.

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.

R1-9 — P2 — §§6.6, 9: The sampler path is wrong, and the console is a missing consumer.

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.

Discovering 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.

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.

R1-10 — P3 — §4.3: The 60/20/20 split needs cgroup measurements, not an RSS check.

The 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.

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.

R1-11 — P1 — §8.1: Migration includes processes the activator does not own.

The 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).

Those external processes need not die with the agent. Moving their database artifacts and changing their socket rows can therefore happen underneath a live writer.

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.

R1-12 — P1 — §§8.1, 6.8: Partial migration is fenced against wakes but not against destructive consumers.

After 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.

A row CAS failure after successful renames similarly has no recovery protocol.

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.

R1-13 — P1 — §8.3: The rollback mechanism contradicts the old code and can discard newer data.

The 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.

If 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.

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.

R1-14 — P1 — §9: Adding the socket to enabledUnits does not start it.

The 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).

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.

R1-15 — P1 — §8.4: The permitted PrivatePIDs= fallback abandons the isolation property.

The 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.

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.

R1-16 — P2 — §6.2: syscall.Exec changes environment override behavior.

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.

Evidence: local Go sources os/exec/exec.go:1231,1246–1247, syscall/exec_unix.go:280, and syscall/env_unix.go:31–44.

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.

R1-17 — P2 — §9: The litestream registry claim does not replace discovery.

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).

Thus 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.

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.

R1-18 — P2 — §§8.2, 8.4, 9, 10: The operational workflow remains box-dependent.

The 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.

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.

R1-19 — P2 — §11: UnitBackend cannot use this root channel unchanged.

The 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).

Correction: remove that claim. Supporting UnitBackend requires a separately specified authorization extension; do not widen the instance-control allowlist implicitly.

Verdict: request changes.