rootfs apps: rev 3, release process, carlos run in v1, interactive sessions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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. |