carlos / platform Public

Isolated exec spec: revision 6 after codex round 5

Round 5 findings committed unedited. All accepted: InstanceID bounded
to 40 bytes keys every directory, socket and unit name so no unix
address exceeds 107 bytes; seed unit RemainAfterExit=yes retains its
result; only the control endpoint is refused by path; system apply and
the console rollback door refuse a pre-layout release while any box is
nested; recovery endpoints follow the journal's own direction.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZ1ZvWj7naf73GjTX8MBv5
Paul Campbell pushed by paul@keymail.dev 58276fc385ff04087f0692f6476616a4c7c68d15 parent 0236be9
2 files changed, +200 −67
  • docs/superpowers/specs/2026-09-11-isolated-exec-design.md +144 −67
  • docs/superpowers/specs/CODEX-FINDINGS-ISOLATED-EXEC-R5.md +56 −0

CI passed — run details

diff --git a/docs/superpowers/specs/2026-09-11-isolated-exec-design.md b/docs/superpowers/specs/2026-09-11-isolated-exec-design.md
index 9a6d2066e..f864e2afd 100644
--- a/docs/superpowers/specs/2026-09-11-isolated-exec-design.md
+++ b/docs/superpowers/specs/2026-09-11-isolated-exec-design.md
@@ -1,7 +1,7 @@
# Isolated exec — every exec-backed instance in its own namespace
-**Date:** 2026-09-11 · **Status:** revision 5, after codex rounds 1 to 4
-(19 + 14 + 7 + 4 findings, all accepted; §13) · **Requested by:** discussion
+**Date:** 2026-09-11 · **Status:** revision 6, after codex rounds 1 to 5
+(19 + 14 + 7 + 4 + 3 findings, all accepted; §13) · **Requested by:** discussion
#126 (Tito, with box evidence), approved in direction by Paul on
2026-09-11
@@ -264,7 +264,7 @@ scoped to the three instance templates and does not extend to
```
/data/.instances/ the RAW directory: 0700 carlos:carlos; opened by the layout pass only (§3.2, §8.2)
/data/instances/ nosymfollow bind mount OF /data/.instances (§3.2); the view everything else uses
-/data/instances/<host>/ 0700 carlos:carlos, created by the agent
+/data/instances/<iid>/ 0700 carlos:carlos, created by the agent
<host>.db the database (+ -wal, -shm)
.activator-<host>.db-litestream/ the in-process replicator's shadow
<host>.env admin token (EnsureAdminToken, unchanged)
@@ -277,18 +277,35 @@ scoped to the three instance templates and does not extend to
/data/layout/ 0700 carlos:carlos
<host>.json the migration journal for a host in flight (§8.2)
/run/carlos/instances/ bind mount of itself, nosymfollow; 2750 carlos:carlos-sockets
-/run/carlos/instances/<host>/ 2750 carlos:carlos-sockets, created by the agent
+/run/carlos/instances/<iid>/ 2750 carlos:carlos-sockets, created by the agent
app.sock the instance's listener (rt.Socket); 0660 after health, as today
app.sock.control the agent's control listener (rt.Socket + ".control"); 0600
exit.<role>.<invocation>.json the unit's advisory exit record (§6.5); tenant-writable, and treated so
/run/carlos/logs/ bind mount of itself, nosymfollow; 0700 carlos:carlos; NOT bound into any namespace
- <host>.run.sock the agent's log listener for the instance (§6.4)
- <host>.seed.sock ... for the seed
- <host>.sidecar.sock ... for the sidecar
+ <iid>.run.sock the agent's log listener for the instance (§6.4)
+ <iid>.seed.sock ... for the seed
+ <iid>.sidecar.sock ... for the sidecar
/run/carlos-control/ 0750 root:carlos; NOT under /run/carlos, NOT in any namespace
control.sock the root channel (§5); SocketUser=carlos, 0600
```
+**`<iid>` is the instance identifier, bounded to 40 bytes** (codex
+R5-3). A host may be 253 bytes (`controlname.ValidHostShape`), and a
+unix socket address holds 107 bytes of path, so a directory named after
+the host cannot carry `app.sock.control` for every valid host.
+`provision.InstanceID(host)` is the host itself when `len(host) <= 40`,
+and otherwise the first 31 bytes of the host, a `-`, and the first 8 hex
+digits of `sha256(host)`; it is the directory name under `/data/instances`
+and `/run/carlos/instances`, the log socket prefix, and, escaped, the
+unit instance and slice suffix (§4). File names *inside* the directory
+keep the full host (a file name may be 255 bytes). The longest address
+this produces, `/run/carlos/instances/<40>/app.sock.control`, is 79
+bytes. Two rows on one box whose hosts collide in `InstanceID` (a
+truncation collision, astronomically unlikely and checked anyway) refuse
+to classify as anything but `foreign` with both hosts named, and the
+layout pass preflights every socket address it is about to commit
+against 107 bytes before it moves a file.
+
The socket tree keeps the #103 contract: `/run/carlos/instances` and
each host's run directory are `2750 carlos:carlos-sockets` (setgid, so
the socket an instance binds inherits the group, exactly as
@@ -308,20 +325,25 @@ flat paths, and the layout pass is the only thing that ever writes a
nested path into a row.
```go
+func InstanceID(host string) string { // ≤ 40 bytes; see §3.1
+ if len(host) <= 40 { return host }
+ sum := sha256.Sum256([]byte(host))
+ return host[:31] + "-" + hex.EncodeToString(sum[:4])
+}
func InstanceDir(dataDir, host string) string {
- return filepath.Join(dataDir, "instances", host)
+ return filepath.Join(dataDir, "instances", InstanceID(host))
}
func NestedDBPath(dataDir, host string) string {
return filepath.Join(InstanceDir(dataDir, host), host+".db")
}
func InstanceRunDir(host string) string {
- return filepath.Join(SocketDir, "instances", host)
+ return filepath.Join(SocketDir, "instances", InstanceID(host))
}
func NestedSocket(host string) string {
return filepath.Join(InstanceRunDir(host), "app.sock")
}
func LogSocket(host, role string) string {
- return filepath.Join(SocketDir, "logs", host+"."+role+".sock")
+ return filepath.Join(SocketDir, "logs", InstanceID(host)+"."+role+".sock")
}
// DefaultDBPath and DefaultSocket keep their flat meaning for this
// release; the next release, which drops the flat spawn path, points
@@ -483,11 +505,14 @@ file list or a phase from there. In particular:
- the migration journal lives in `/data/layout/<host>.json`, outside
every tenant mount, and every source and destination path in it is
*derived from the host name and the registry row* by the §3.1 helpers
- at plan time. On recovery the pass **recomputes** the plan from the row
- and honours the journal only if its paths equal the recomputation and
- its direction equals the fleet's recorded intent (`control/layout.json`,
- a bucket object no tenant can write, §8.2). During the transition a
- flat tenant can write `/data/layout` (same uid); a journal it forges can
+ at plan time. On recovery the pass **recomputes** both endpoints for
+ the journal's own direction and honours the journal only if its paths
+ equal the recomputation and the row stands at one of them (§8.2); a
+ journal that passes is completed to its own end and the host is then
+ converged to the fleet's recorded intent (`control/layout.json`, a
+ bucket object no tenant can write) before it is released. During the
+ transition a flat tenant can write `/data/layout` (same uid); a journal
+ it forges can
therefore cause the agent to do exactly what the next layout pass would
do for that host anyway, and nothing else (R3-1);
- the launch file is agent-written and then read by the trampoline
@@ -515,14 +540,17 @@ the existing nineteen are (`bootstrap.go:33-34`, `systemapply.go:339-398`):
§8.5, and the two control units of §5.2. Nothing enables the templates;
an instance unit exists only while the agent has started it.
-**Instance names are escaped hosts.** The unit instance is
-`systemd-escape(host)`: for a valid host (`[a-z0-9.-]`) that only turns
-`-` into `\x2d`. `%I` (unescaped) is used wherever the host is a path
-component; `%i` (escaped) wherever it is part of a unit name, which is
-what lets a per-host slice be spelled in the template (R1-8). The agent
-and the control service share one Go escape function (a copy of
-systemd's rules for this character set, pinned by a test against
-`systemd-escape` output for the hosts in the fixture set).
+**Instance names are escaped instance identifiers.** The unit instance
+is `systemd-escape(InstanceID(host))` (§3.1): for a valid identifier
+(`[a-z0-9.-]`) that only turns `-` into `\x2d`. `%I` (unescaped) is used
+wherever the identifier is a path component; `%i` (escaped) wherever it
+is part of a unit name, which is what lets a per-host slice be spelled
+in the template (R1-8). The agent and the control service share one Go
+escape function (a copy of systemd's rules for this character set,
+pinned by a test against `systemd-escape` output for the hosts in the
+fixture set). The templates never need the full host: every path they
+name is keyed by `<iid>`, and the trampoline learns the host from the
+launch file (§6.2).
### 4.1 `carlos-instance@.service`
@@ -659,7 +687,7 @@ Differences:
| | seed | sidecar |
|---|---|---|
-| `Type=` | `oneshot` | `simple` |
+| `Type=` | `oneshot` with `RemainAfterExit=yes`: after `ExecStart` exits the unit stays `active (exited)` or `failed`, loaded, with its `InvocationID`, `Result` and `ExecMainStatus` retained until the control service stops it, so the seed's result can never be garbage-collected before it is read (codex R2-4, R5 disposition) | `simple` |
| `ExecStart=` | `/opt/carlos/carlos instance-exec seed %I` | `/opt/carlos/carlos instance-exec sidecar %I` |
| `ExecStopPost=` | `/opt/carlos/carlos instance-exit seed %I` | `/opt/carlos/carlos instance-exit sidecar %I` |
| `StandardOutput=` | `file:/run/carlos/logs/%I.seed.sock` | `file:/run/carlos/logs/%I.sidecar.sock` |
@@ -806,7 +834,7 @@ itself:
| Verb | What it runs | Returns | Agent deadline |
|---|---|---|---|
| `POST /start?host=H&role=run\|sidecar&memory_max=N` | `systemctl set-property --runtime carlos-instances-<esc>.slice MemoryMax=N MemoryHigh=0.9N MemorySwapMax=N`; `systemctl start <unit>` (blocking: the start job completes) | 200 with `{invocation_id}` from `systemctl show -p InvocationID <unit>`, read immediately after the start returns; or 502 with systemctl's stderr | 30s |
-| `POST /seed?host=H&memory_max=N` | the same `set-property`; `systemctl start --no-block` on `carlos-instance-seed@<esc>.service`; **immediately** `systemctl show -p InvocationID` (the unit is `activating` and has one); then wait until the unit is terminal and its cgroup empty (§6.5's rule, applied by the service as root), then `systemctl show -p Result,ExecMainStatus,ExecMainCode` | 200 with `{invocation_id, result, status, code}`; `result` is `unknown` if the unit unloaded before the final `show` | 150s: 120s seed budget plus `TimeoutStopSec` and slack (R3-5) |
+| `POST /seed?host=H&memory_max=N` | the same `set-property`; `systemctl start` on `carlos-instance-seed@<esc>.service` (blocking: a oneshot's start job completes when `ExecStart` exits, or fails at `TimeoutStartSec`); the unit is now `active (exited)` or `failed` and **stays loaded** (`RemainAfterExit=yes`, §4.2); `systemctl show -p InvocationID,Result,ExecMainStatus,ExecMainCode`; wait for the cgroup to be empty (§6.5's rule, applied by the service as root); `systemctl stop` (instant for an exited oneshot; clears it) | 200 with `{invocation_id, result, status, code}`, always complete | 150s: 120s seed budget plus `TimeoutStopSec` and slack (R3-5) |
| `POST /stop?host=H&role=run\|seed\|sidecar` | `systemctl stop <unit>` (blocking; also cancels a pending start job) | 204 | 30s |
| `POST /reset?host=H&role=…` | `systemctl reset-failed <unit>`, tolerating "not loaded" | 204 | 30s |
| `GET /ready` | nothing | 204 once the probe verdict (§8.5) is `ready`; 503 with the verdict otherwise | 5s |
@@ -877,10 +905,12 @@ can is **systemd's own record of which process is the agent**:
pass that check. It does not get that far, because of two checks on
**every unix dial the edge and the agent make** (`edge.go:2000-2014`'s
`unixProxy`, the activator's health and control dials, §6.1):
- 1. **the path is allowed before the dial**: it must be under
- `/run/carlos/` (flat sockets, `instances/`, `apps/`), so a registry
- row pointing at `/run/carlos-control/control.sock` is refused
- before `connect()`;
+ 1. **the reserved endpoint is refused before the dial**: a path under
+ `/run/carlos-control/` is never dialled as an upstream, so a
+ registry row pointing at `control.sock` is refused before
+ `connect()`. Nothing else is refused by path: a route whose
+ operator stamped `--socket /run/titogo/example.sock` keeps working
+ exactly as today (codex R5-1);
2. **the listener answers to the path that was dialled**: after
`connect()` and before a byte is sent, `getpeername()` on the
connected socket returns the address the *listener* bound, which is
@@ -948,16 +978,20 @@ child; return a `unitProc` carrying the invocation id.
```json
{
"schema": 1,
- "argv": ["/opt/carlos/live/<host>", "--socket", "/run/carlos/instances/<host>/app.sock", "--db", "/data/instances/<host>/<host>.db"],
+ "host": "<host>",
+ "argv": ["/opt/carlos/live/<host>", "--socket", "/run/carlos/instances/<iid>/app.sock", "--db", "/data/instances/<iid>/<host>.db"],
"env": ["PATH=...", "CARLOS_ADMIN_TOKEN=...", "CARLOS_CONTROL_SOCKET=...", "..."]
}
```
-`carlos instance-exec <role> <host>` runs as `ExecStart`, inside the
-namespace, as `carlos`. It reads `/data/instances/<host>/launch.<role>.json`
+`carlos instance-exec <role> <iid>` runs as `ExecStart`, inside the
+namespace, as `carlos`. It reads `/data/instances/<iid>/launch.<role>.json`
through `instancefs`, refuses a schema it does not know, refuses an
-`argv[0]` that is not `/opt/carlos/live/<host>` (defence in depth: the
-file is agent-written; the check is one line), and calls
+`argv[0]` that is not directly under `/opt/carlos/live/` or that does
+not equal `/opt/carlos/live/` plus the file's own `host` (defence in
+depth: the file is agent-written; the prefix check is the one that
+holds even against a rewritten file, since only the platform's live
+links live there), and calls
`syscall.Exec(argv[0], argv, env)`. It never parses env syntax: the
agent's `childEnv` (`backend_exec.go:802-814`) composes the list exactly
as it does today, including the agent-owned removal of tenant-supplied
@@ -1131,16 +1165,11 @@ agent owns these units and any other invocation is an anomaly to end,
not to adopt; the invocation id decides the event line's wording and
nothing about whether a writer is gone.
-**The seed's status is the one place a record is more than wording.**
-`/seed` returns systemd's `result`/`status` when the unit was still
-loaded at the final `show`, and `unknown` otherwise (§5.3). In the
-`unknown` case the agent reads the seed's exit record and takes its
-status. That record is tenant-writable, but a seed is the tenant's own
-code seeding the tenant's own database: a forged "success" leaves the
-tenant with whatever database its seed produced, and a forged "failure"
-deletes the tenant's own fresh database and restores nothing. The only
-party a seed record can deceive is its author, so it is allowed to
-decide the seed's outcome and nothing else.
+**The seed's outcome comes from systemd, always.** The seed unit
+carries `RemainAfterExit=yes` (§4.2), so it stays loaded, with its
+`Result` and `ExecMainStatus`, until the control service reads them and
+stops it (§5.3). `/seed` therefore always returns a complete result and
+the seed's exit record is, like every other record, wording only.
`sweepOnce`'s existing dead-child check (`activator.go:1105-1122`) sees a
flag, never a blocking call. `Restart=no` in the template is what makes
@@ -1405,10 +1434,12 @@ needed) and, per host in scope, in this order:
**Recovery is the journal, and it is exact, and it is checked** (R2-7,
R3-1, R4 disposition). On agent start and at the top of every layout
pass, for each journal, the check is against **both endpoints**, because
-the row is at `from` before step 8 and at `to` after it: the journal's
-`to` must equal the destination the §3.1 helpers derive for that host
-in the journal's own `mode`, its `from` must be the host-named flat
-paths of step 3, and the row must currently equal either `from` or `to`.
+the row is at `from` before step 8 and at `to` after it, and it is
+derived from **the journal's own direction** (codex R5 disposition): for
+`mode: nested`, `from` must be the host-named flat paths of step 3 and
+`to` the nested paths the §3.1 helpers derive; for `mode: flat` the two
+are swapped, artifact sources included. The row must currently equal
+either `from` or `to`.
A journal that fails any of those is refused (`layout: <host>: journal
does not match row; not acting`), the host stays `fenced`, and status
says so. **A journal that passes is completed to its own endpoint even
@@ -1435,16 +1466,42 @@ same fence, the same preflight in reverse. It exists so that a fleet
can be taken back to a layout the previous release serves **before**
that release is installed: the operator's rollback order is `layout
revert` (this release, verified `flat` in status) then `system rollback`.
-A `system rollback` or a failed-gate rollback that happens while hosts
-are `nested` lands the old binary on rows it honours (`rt.DBPath`,
-`rt.Socket`; `backend_exec.go:220-225, 557-562`). On the same boot the
-nested run directories still exist, so the old binary can bind the
-nested socket and serve the nested database while minting flat env
-files; after a reboot the run directories are gone and those hosts fail
-to bind, loudly and losslessly. Either way the database has one history,
-the nested one, and the revert's step 5 handles the stray env files when
-the new binary is back. The layout is never two databases: the fence
-and the destination check see to that.
+**That order is enforced, not advised** (codex R5-2). A downgrade
+removes this release's unit files: `installUnits` runs `systemctl
+disable --now` on every unit the candidate lacks (`systemapply.go:522`),
+so the bind mounts go with the binary, `/data/instances/<iid>/` becomes
+an empty mountpoint directory, and the old backend, honouring the nested
+`rt.DBPath` (`backend_exec.go:220-225`), would find no database and
+restore a second copy into the uncovered directory, which the next
+mount would hide. So:
+
+- **This release's `carlos system apply` refuses to install a candidate
+ that predates the layout** (its version is below the release that
+ carries the mount units) while the box's `layout_mode` is `nested` or
+ any journal exists, with `layout revert first` as the reason; the
+ installer is this binary for every downgrade from here, because a
+ downgrade is an install performed by the installed binary (§8.1).
+- **The console's rollback door** (`system.go:466`) refuses a channel
+ rollback to a pre-layout release while any box in the fleet reports
+ `layout_mode: nested`, `fenced` rows, or no layout field at all, and
+ names the boxes.
+- A failed-gate rollback of the upgrade *to* this release runs the old
+ binary, but at that moment nothing is nested (§8.1). A failed-gate
+ rollback of a later upgrade runs this binary, which knows the layout.
+- **Recovery for a layout that was unmounted anyway** (an operator
+ removing units by hand): at start the control service (root)
+ inspects the underlying mountpoint directories in a private mount
+ namespace (`unshare -m`, `umount` the bind there, list) and, if
+ `/data/instances` or `/run/carlos/instances` holds anything under the
+ mount, records the verdict `mountpoint held files while unmounted`
+ and `GET /ready` refuses nested wakes until an operator has moved or
+ removed them; the layout pass's destination check (step 5) then sees
+ whatever was moved into the raw directory. Nothing is chosen
+ silently; the databases are never two.
+
+The database therefore has one history in every path that is allowed
+to happen, and the two paths that are not allowed are refused with the
+revert named as the fix.
### 8.3 `foreign` rows
@@ -1636,26 +1693,39 @@ sentinel `.path` units name fixed files under `/data`, never a host.
Unit level, all runnable in CI (no systemd needed):
-- `internal/provision`: `InstanceDir`, the nested helpers, the flat
- defaults unchanged, `LogSocket`, the escape function against
- `systemd-escape` fixtures.
+- `internal/provision`: `InstanceID` (identity below 41 bytes,
+ truncation plus hash above, never longer than 40, stable), the nested
+ helpers keyed by it, the flat defaults unchanged, `LogSocket`, the
+ escape function against `systemd-escape` fixtures, and the socket
+ address preflight: a host that fits today's flat paths but whose
+ identifier-keyed paths would exceed 107 bytes cannot exist, pinned by
+ computing the longest address for a 253-byte host; two hosts colliding
+ in `InstanceID` classify as `foreign`.
- `instancefs`: refuses a symlink at every component, refuses `..`,
`O_EXCL` temp plus rename, the mountinfo check for all three mounts.
- `internal/edge` and `internal/activator` dials, with real socket
- pairs: a path outside `/run/carlos/` is refused before `connect()`; a
- symlink under `/run/carlos` to another listener yields a `getpeername`
+ pairs: a path under `/run/carlos-control/` is refused before
+ `connect()`; a custom path elsewhere (`/run/titogo/example.sock`) is
+ dialled as today; a symlink to another listener yields a `getpeername`
mismatch and is closed before a byte is sent; a listener created by
another process and passed over (the `carlos-app@.socket` shape) is
accepted when its bound path is the one dialled.
+- `internal/host/systemapply`: a pre-layout candidate is refused while
+ `layout_mode` is `nested` or a journal exists, and installed when the
+ box is `flat`; the control service's start-time inspection of an
+ underlying mountpoint that holds files (with a fake namespace runner).
+- `internal/console`: the rollback door refuses a pre-layout release
+ while any box reports `nested`, `fenced` or no layout field, naming
+ them.
- `instance-control`: a connection whose peer pid is not the edge's
`MainPID`, or whose uid is not `carlos`, is closed without a response;
every 400 (bad host, bad role, bad limit); the unit and slice names it
builds for a host with a dash; `set-property` (three properties) →
`start` ordering with no `reset-failed`; `/reset` tolerating "not
- loaded"; `/seed` starting `--no-block`, reading the invocation id
- while `activating`, waiting for terminal state plus empty cgroup, and
- returning `unknown` when the unit unloaded first; `/ready` from the
- probe verdict; with a fake `systemctl` runner and a fake peer.
+ loaded"; `/seed` reading a retained result from an `active (exited)`
+ and from a `failed` oneshot, waiting for the empty cgroup, then
+ stopping it; `/ready` from the probe verdict; with a fake `systemctl`
+ runner and a fake peer.
- `internal/activator`: classification of the four row shapes;
`prepare` mints the token for seed and run, writes `launch.<role>.json`
with the deduplicated env and the exact argv (the existing
@@ -1874,3 +1944,10 @@ rollback runs the old binary).
R2-7 journals validated against both endpoints and completed to their own end before convergence (§8.2);
R2-14 `v6: unverified` bounded by the five-minute re-run and reported in status (§8.5);
R1-18 the `foreign` residual accepted with its reason (§8.3).
+- **Round 5** (codex, `CODEX-FINDINGS-ISOLATED-EXEC-R5.md`, 3 new
+ findings plus 2 partials, verdict request changes). All accepted:
+ R5-1 only the control endpoint is refused by path; custom sockets elsewhere dial as today (§5.4);
+ R5-2 `system apply` and the console's rollback door refuse a pre-layout release while any box is nested; underlying mountpoints inspected at start (§8.2);
+ R5-3 `InstanceID`, bounded to 40 bytes, keys every directory, socket and unit name; addresses preflighted against 107 bytes (§3.1, §4, §6.2);
+ R2-4 the seed unit carries `RemainAfterExit=yes`, so its result is retained until read; the record is wording again (§4.2, §5.3, §6.5);
+ R2-7 recovery endpoints derived from the journal's own direction; §3.3 reconciled with §8.2 (§3.3, §8.2).
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-ISOLATED-EXEC-R5.md b/docs/superpowers/specs/CODEX-FINDINGS-ISOLATED-EXEC-R5.md
new file mode 100644
index 000000000..7aeede7cf
--- /dev/null
+++ b/docs/superpowers/specs/CODEX-FINDINGS-ISOLATED-EXEC-R5.md
@@ -0,0 +1,56 @@
+| Prior finding | Disposition at `0236be94` |
+|---|---|
+| R4-1 | **Resolved for the reported failure.** Removing the root-peer rejection preserves systemd-created application listeners. The replacement path restriction introduces R5-1 below. |
+| R4-2 | **Resolved.** The raw directory and confined directory-fd renames provide endpoints on one mount. The changed storage location introduces the downgrade defect in R5-2. |
+| R4-3 | **Resolved.** Custom paths are excluded from migration; the remaining forward sources follow their existing file contracts. |
+| R4-4 | **Resolved.** The probe no longer requires a forbidden netlink route query. |
+| R2-4 | **Partially resolved.** The seed-record outcome rule is now explicit. However, [`start --no-block` followed by `show`](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/isolated-exec-spec/docs/superpowers/specs/2026-09-11-isolated-exec-design.md:809) does not retain invocation identity. `--no-block` only enqueues the job: the subsequent query can precede activation or follow completion and unloading. “Immediately” supplies no synchronization. Retain the unit through completion and result capture, or provide equivalent request-to-invocation correlation. [systemctl semantics](https://raw.githubusercontent.com/systemd/systemd/v257/man/systemctl.xml), [unit garbage collection](https://raw.githubusercontent.com/systemd/systemd/v257/man/systemd.unit.xml). |
+| R2-5 | **Resolved.** Foreign invocations are stopped, and completion still requires terminal state and an empty cgroup. Unproven stops retain replication and the lease. |
+| R2-7 | **Partially resolved.** Forward recovery now recognizes both row endpoints and changed fleet intent. But the [new validation](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/isolated-exec-spec/docs/superpowers/specs/2026-09-11-isolated-exec-design.md:1405) requires `from` to be flat even for a reverse journal. A legitimate nested→flat transaction therefore fails validation. Derive **both** endpoints from the journal’s direction, including artifact sources. Also reconcile §3.3’s remaining requirement that journal direction equal current fleet intent with §8.2’s explicit opposite rule. |
+| R2-9 | **Resolved for the original first-upgrade defect.** Flat minting preserves compatibility before migration. The newly introduced mount-removal defect is R5-2. |
+| R2-14 | **Resolved at spec level.** IPv6 evidence remains explicitly unverified when unavailable; the exception has a five-minute bound, rechecks and status reporting. |
+| R1-4 | **Resolved.** Completion is independent of log EOF, blocking observations stay outside the mutex, and the foreign-invocation completion exception is removed. |
+| R1-5 | **Partially resolved.** Preparation, execution budget, cancellation and termination-before-cleanup are specified. Seed invocation correlation remains defective under R2-4. |
+| R1-8 | **Resolved.** Shared tier limits and the deliberate per-service OOM-family difference are specified. |
+| R1-12 | **Partially resolved.** Consumer fences are specified; reverse-journal recovery remains defective under R2-7. |
+| R1-13 | **Partially resolved.** Explicit reversal and collision refusal replace the original history-selection defect, but reverse validation remains inconsistent under R2-7. |
+| R1-15 | **Resolved at spec level.** There is no unsafe PID-namespace fallback; the probe and bounded IPv6 exception are specified. |
+| R1-18 | **Resolved under the recorded ruling.** Remote reporting and layout operations are specified. The box-local `foreign` repair residual is explicitly accepted. |
+
+**R5-1 — P2 — §§5.4, 6.1, 8.3: Retain the authenticated control service, but preserve supported custom socket paths.**
+
+The new rule rejects every application dial outside `/run/carlos/`. An existing route with `Socket=/run/titogo/example.sock` therefore loses public traffic and health checks on upgrade, even though §8.3 promises that `foreign` routes keep running unchanged.
+
+The existing CLI accepts an explicit socket pathname, and the edge uses it without this directory restriction. The reviewed Tito precedent itself binds beneath `/run/titogo`.
+
+Evidence: [new dial restriction](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/isolated-exec-spec/docs/superpowers/specs/2026-09-11-isolated-exec-design.md:878), [compatibility promise](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/isolated-exec-spec/docs/superpowers/specs/2026-09-11-isolated-exec-design.md:1451), [CLI socket input](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/isolated-exec-spec/cli.go:76), [edge dispatch](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/isolated-exec-spec/internal/edge/edge.go:536).
+
+**Correction:** retain process authentication and the actual-peer-address check. Reserve and reject the broker endpoint explicitly while permitting existing ordinary upstream paths. Verify a legitimate custom socket outside `/run/carlos/` alongside direct and symlinked attempts to proxy to the broker.
+
+**R5-2 — P2 — §§3.2, 8.2, 9: Removing the new bind mounts invalidates the downgrade recovery claim.**
+
+This revision stores the database physically under `/data/.instances` and exposes it through `/data/instances`. An ordinary downgrade to a pre-layout release removes the new mount units: `installUnits` executes `systemctl disable --now` for every removed unit before deleting its file.
+
+Consequently, the old backend’s nested `DBPath` no longer reaches the database. The same change removes the nested runtime-directory view immediately, rather than only after reboot as §8.2 claims.
+
+There is also a second-copy path: the old backend sees the database as absent and invokes restore. Litestream creates missing parent directories, so it can restore into the uncovered `/data/instances/<host>/` while the original remains under `/data/.instances/<host>/`. Reinstalling the bind mount hides that restored copy from the specified collision check.
+
+Evidence: [new mount source](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/isolated-exec-spec/docs/superpowers/specs/2026-09-11-isolated-exec-design.md:380), [downgrade claim](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/isolated-exec-spec/docs/superpowers/specs/2026-09-11-isolated-exec-design.md:1438), [removed-unit handling](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/isolated-exec-spec/internal/host/systemapply.go:522), [missing-database handling](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/isolated-exec-spec/internal/activator/backend_exec.go:220), [restore directory creation](/home/paulca/go/pkg/mod/github.com/benbjohnson/litestream@v0.5.14/replica.go:654).
+
+**Correction:** make completed reversal a checked prerequisite for removing these mounts and starting a pre-layout binary. Define recovery for an already-unmounted layout, including checking the uncovered mountpoint before remounting. Verify through the actual updater’s unit-removal path.
+
+**R5-3 — P1 — §§3.1, 6.1, 8.2: The longer socket paths make previously working hosts unwakeable after migration.**
+
+For a valid 80-byte hostname, the current application socket is 97 bytes and its control socket is 105 bytes. The proposed paths become:
+
+- Application: `/run/carlos/instances/<host>/app.sock` — **111 bytes**.
+- Control: the same path plus `.control` — **119 bytes**.
+- Sidecar log: `/run/carlos/logs/<host>.sidecar.sock` — **110 bytes**.
+
+Linux’s Unix socket address has a 108-byte pathname field; Go requires room for the terminating NUL. These new addresses cannot bind. Migration nevertheless accepts the flat row and commits the nested paths, after which the application cannot wake.
+
+Evidence: [runtime layout](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/isolated-exec-spec/docs/superpowers/specs/2026-09-11-isolated-exec-design.md:279), [existing hostname validation](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/isolated-exec-spec/internal/controlname/names.go:18), [Go’s address-length rejection](/usr/local/go/src/syscall/syscall_linux.go:547), [Linux Unix socket limits](https://man7.org/linux/man-pages/man7/unix.7.html).
+
+**Correction:** specify a bounded per-host runtime identifier consistently across socket paths, unit bind paths and log endpoints. Preflight every resulting Unix socket address before moving files. Include a hostname that fits the current application and control sockets but exceeds the proposed limits.
+
+**Verdict: request changes.**
\ No newline at end of file