carlos / platform Public

rootfs apps: rev 3, release process, carlos run in v1, interactive sessions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 3148d87df33f94b64053a0772d5c04633a3aef88 parent 155e392
1 file changed, +92 −9
  • docs/superpowers/specs/2026-10-24-rootfs-apps-design.md +92 −9

CI cancelled — superseded by a newer push — run details

CI log
=== gate ===
.amadan/test-gate.sh

amadan: job cancelled — a newer push superseded it
diff --git a/docs/superpowers/specs/2026-10-24-rootfs-apps-design.md b/docs/superpowers/specs/2026-10-24-rootfs-apps-design.md
index 54699b27f..dc7a74ca3 100644
--- a/docs/superpowers/specs/2026-10-24-rootfs-apps-design.md
+++ b/docs/superpowers/specs/2026-10-24-rootfs-apps-design.md
@@ -1,10 +1,12 @@
# Rootfs apps: run any Linux app on your own boxes, isolated like a CARLOS instance
**Date:** 2026-10-24
-**Revision:** 2. Draft from a brainstorming session with Paul, 2026-10-24.
+**Revision:** 3. Draft from a brainstorming session with Paul, 2026-10-24.
Rev 2 answers Astra's builder check (`CODEX-FINDINGS-ROOTFS-APPS-R1.md`,
six questions): §5.0 is new, §5.6 and §5.7 are narrowed, §5.2 settles
-deploy overlap. Marked **(B1-n)**.
+deploy overlap. Marked **(B1-n)**. Rev 3 adds the `release` process
+(§5.2.1, Paul: "Perfect"), makes `carlos run` part of v1 (§5.5) and adds
+interactive sessions (§5.10).
**Discussion:** amadan carlos/platform #366.
**Base:** origin/main 64a002c8.
@@ -243,12 +245,14 @@ The manifest names processes:
```json
"processes": {
- "web": {"command": ["bundle", "exec", "puma", "-C", "config/puma.rb"]},
- "worker": {"command": ["bin/jobs"]}
+ "web": {"command": ["bundle", "exec", "puma", "-C", "config/puma.rb"]},
+ "worker": {"command": ["bin/jobs"]},
+ "release": {"command": ["bin/rails", "db:migrate"]}
}
```
-- Exactly one `web`, required. Zero or more others, names matching a
+- Exactly one `web`, required. At most one `release` (§5.2.1). Zero or
+ more others, which are workers, names matching a
short identifier pattern. Commands are argv arrays, absolute or
resolved against the image's `PATH`. Optional `env` (plain values,
no secrets) and `working_dir` per process.
@@ -278,6 +282,43 @@ The manifest names processes:
release still serves, exactly as in a rolling Kubernetes deploy. Making
them backward-compatible is the app's job, and the guide says so.
+### 5.2.1 The `release` process: migrations on deploy
+
+The equivalent of a Helm pre-upgrade hook or Heroku's release phase.
+
+- The activator runs `release` during an instance's swap to a new
+ version, on the instance's own box, from the **new** image, with the
+ instance's directory and environment (`CARLOS_DB`, `DATABASE_URL`,
+ everything `web` gets). It runs in the one-off command unit
+ (`carlos-instance-cmd@`, rootfs variant), so it is inside the
+ instance's slice and namespace.
+- **Exit 0:** the new `web` starts, then workers are handed over (§5.2).
+- **Non-zero exit or timeout:** the swap is abandoned for that instance.
+ The old version keeps serving, the failure shows in `carlos logs` and
+ in status, and the next deploy tries again. The builder sets the
+ timeout (my call: 10 minutes default, declarable up to the `carlos run`
+ maximum).
+- **Breaking migrations use the existing swap modes**
+ (`internal/rollout/policy.go`, manifest `swap`):
+ - `overlap` (default): `release` runs while the old version still
+ serves, so a migration must be backward-compatible (expand, then
+ contract), as in a rolling Kubernetes deploy.
+ - `hold`: stop the old version, run `release`, start the new one. Safe
+ for breaking migrations; costs a few seconds of downtime.
+- **How often it runs.** Once per instance per deploy. With per-instance
+ SQLite that is the only correct scope. With a shared external database
+ and one instance (the v1 Kubernetes case) it matches Kubernetes'
+ run-once Job. A run-once-per-release scope for many copies sharing one
+ database belongs to the stateless-replicas design (§12); Rails'
+ migration lock makes a duplicate run wait and then do nothing, but that
+ design should define the scope rather than rely on it.
+- **A hibernating instance** runs `release` at its next wake, before it
+ serves.
+- **Rollback** of a release does not roll back the schema, as on
+ Kubernetes. The guide says so.
+- Distinct from seeds: a seed runs once at an instance's first boot;
+ `release` runs on every deploy, including the first, after the seed.
+
### 5.3 The rootfs unit variant
A rootfs instance runs from a variant of `carlos-instance@.service` (and
@@ -331,9 +372,14 @@ review of its own.
existing seed mechanism runs a command once before first serve. A
rootfs app may declare a `seed` process. Builder's call whether this
lands in v1; the sample app can run `db:prepare` from its own boot.
-- **`carlos run`** (one-off commands, `carlos-instance-cmd@`): a rootfs
- variant makes `carlos run rails console` and `rails db:migrate` work.
- Desirable, not required for v1.
+- **`carlos run`** (one-off commands, `carlos-instance-cmd@`) is part of
+ v1. A rootfs variant of the cmd unit makes `carlos run -- bin/rails
+ db:migrate:status`, rake tasks and `rails runner` work, from the
+ instance's current image, with today's contract (argv only, no stdin,
+ output about a second behind). The `release` process (§5.2.1) uses the
+ same unit with the new image, so `carlos run --version`, deferred in
+ the run design because the trampoline execs only the live build, falls
+ out of the same work.
- **Moves.** A rootfs app on a fleet box does not move between regions
in v1. Moves refuse a rootfs row. A move would also break an app that
relies on a same-region database (R4).
@@ -430,6 +476,34 @@ not a security boundary:
processes, image size) before uploading. Only `--check` needs a local
runtime.
+### 5.10 Interactive sessions (`rails console`)
+
+`carlos run` has no stdin and polls output through the bucket, so it
+cannot host `rails console`, `php artisan tinker` or a shell. An
+interactive session needs a live, two-way stream with a terminal.
+
+- **Command.** `carlos run --interactive` (`-it`), e.g.
+ `carlos run -it --app shop --instance shop.example.com -- bin/rails console`.
+ My call over a new `carlos console` verb: "the console" already names
+ the control plane in the docs and CLI, and `-it` is what Docker and
+ kubectl users expect.
+- **Transport.** CLI to console over a WebSocket; console to box over the
+ box's existing outbound tunnel (fleet boxes have no inbound path, and
+ need none); on the box, the agent asks the control service for a
+ rootfs cmd unit with a pseudo-terminal, inside the instance's slice and
+ namespace, from the current image.
+- **Authority.** Members with a role that may run commands today, plus an
+ explicit per-app setting that allows interactive sessions, off by
+ default. It is a live shell into production data.
+- **Audit.** Who, which instance, start, end and exit status, as `carlos
+ run` records today. Keystrokes and output are not recorded in v1.
+- **Limits.** One interactive session per instance at a time beside any
+ `carlos run`, an idle timeout, and the run design's maximum duration.
+- Works for binary apps too, if they ship a shell or REPL; nothing here
+ is rootfs-only except the image the session runs from.
+- Not required for step 1 of §6. Required before step 3: a team leaving
+ Kubernetes uses `kubectl exec -it ... rails console` and will miss it.
+
### 5.9 Reaching the app's own services
- **Public endpoints** (S3, a managed database with a public endpoint):
@@ -485,6 +559,11 @@ runtime.
instance's slice and tier. Nothing the image starts escapes the cgroup.
- C6. The egress allow (§5.9) can only add the listed destinations; it
never reopens IMDS, the box's loopback or the box's own addresses.
+- C8. A failed or timed-out `release` never lets the new `web` serve,
+ and never stops the old one under `overlap`.
+- C9. An interactive session runs inside the instance's namespace and
+ slice like every other process of the instance (C5), and is refused
+ unless the app allows it.
- C7. One writer still holds: one box runs a given instance, under the
lease, always-on or not.
@@ -546,7 +625,11 @@ resource IDs. Managed databases are the customer's choice of endpoint.
- Upload cap of 2 GiB for the rootfs part; worker cap of 4 per instance.
- Health bound of at most 120 s for rootfs apps.
- Moves refuse rootfs rows in v1.
-- Seeds and `carlos run` for rootfs are desirable, not required for v1.
+- Seeds for rootfs are desirable, not required for v1; `carlos run` is
+ required.
+- `release` timeout 10 minutes by default (§5.2.1).
+- Interactive sessions as `carlos run -it`, not `carlos console`; off by
+ default per app; needed before step 3, not step 1 (§5.10).
- The egress allow is in this design because step 3 cannot happen
without it, but it is not needed for step 1 and can land after it.