carlos / platform Public

spec: fleet-internal engines rev 8 (Astra R7: 1 P1, 1 P2, both accepted)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 9306d7a502bbaf530c5e660fa7fbf5b41a3b01e7 parent d59eec0
2 files changed, +95 −9
  • docs/superpowers/specs/2026-10-21-fleet-internal-engines-design.md +46 −9
  • docs/superpowers/specs/CODEX-FINDINGS-ENGINES-R7.md +49 −0

CI cancelled — superseded by a newer push — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.011s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok  	github.com/carlosframework/platform	104.004s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	2.865s
ok  	github.com/carlosframework/platform/cmd/moveprobe	92.342s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.707s

amadan: job cancelled — a newer push superseded it
diff --git a/docs/superpowers/specs/2026-10-21-fleet-internal-engines-design.md b/docs/superpowers/specs/2026-10-21-fleet-internal-engines-design.md
index de8d42a7e..86d2fe1eb 100644
--- a/docs/superpowers/specs/2026-10-21-fleet-internal-engines-design.md
+++ b/docs/superpowers/specs/2026-10-21-fleet-internal-engines-design.md
@@ -1,9 +1,15 @@
# Fleet-internal engines: a doc engine nobody on the internet can call
**Date:** 2026-10-21
-**Revision:** 7.
+**Revision:** 8.
**Discussion:** amadan carlos/platform #335. Related: #328 (per-app egress allow).
+- **Rev 8** answers Astra round 7 (`CODEX-FINDINGS-ENGINES-R7.md`, reject,
+ a P1 and a P2, both accepted). Clearing the mark waits for proof that no
+ process of a declaring build is left, not for adoption's return, which
+ under generation rollouts only schedules the swap (R7-1). A minted row
+ records its owner explicitly; the fallback match needs a unique non-empty
+ backend, TCP included (R7-2). Marked **(codex R7-n)**.
- **Rev 7** answers Astra round 6 (`CODEX-FINDINGS-ENGINES-R6.md`, reject,
a P1 and a P2 in rev 6's minted-row rule, both accepted). Inheritance is
scoped to minted `instance` rows, static and service platform rows keep
@@ -204,9 +210,15 @@ copying the source's kind and backend but no version, and adoption skips it
- A minted `static` or `service` row (including a static canary on its own
channel, `reconcile.go:368`) is `0` by construction, like any row of
those kinds. The rest of this paragraph does not apply to it.
-- A minted `instance` row's **execution owner** is the non-platform
- `instance` row on the box with the same account, app and backend
- (`Socket`, or for a fleet-placed source the same `Region`). The edge
+- A minted `instance` row's **execution owner** is recorded on the row: the
+ domains reconciler writes the source's host into a new `routes.owner`
+ column when it mints (`mintedRoute` has the source in hand) and backfills
+ it on its next pass for rows minted earlier **(codex R7-2)**. Until a row
+ has an owner recorded, the edge falls back to matching the non-platform
+ `instance` row with the same account and app and the same **non-empty**
+ backend: `Socket`, `Addr` (TCP-backed rows, `cli.go:172`,
+ `domains/reconcile.go:835`), or a fleet-placed `Region`. Empty values never
+ match, and a match that is not unique counts as no match. The edge
resolves owners at `Reload`.
- The owner and every minted row that shares it form one **group** with
one mark and one admission epoch. The group is internal when the owner's
@@ -215,8 +227,9 @@ copying the source's kind and backend but no version, and adoption skips it
minted row (`cli.go:1432`); such a grant marks the whole group, and
`--grant` and `--revoke` on any row of the group run §5.1's barrier for
the group.
-- A minted `instance` row whose owner the edge cannot find is treated as
- `NULL` and refused, and logged by name.
+- A minted `instance` row whose owner the edge cannot find, or finds
+ ambiguously, is treated as `NULL` and refused, and logged by name, until
+ the reconciler records its owner.
**Set before, clear after: the serving barrier (codex R2-1).** Adoption
repoints the live symlink (`release/adopt.go:1751`), restarts
@@ -247,9 +260,25 @@ and the edge serves an in-memory map refreshed on a 2 s registry poll
refused publicly for that interval, which costs availability, never
exposure.
- **Becoming public** (the incoming release does not declare `doc_engine`,
- the route is `1`, and it holds no grant): adoption repoints and restarts
- first, records the version, and only then writes `0`. A failed or
- rolled-back adoption leaves `1`.
+ the route is `1`, and no row of its group holds the grant): the mark is
+ cleared only once the execution layer **confirms that no process of a
+ declaring build remains** for the group **(codex R7-1)**. Adoption's
+ return proves nothing: with generation rollouts armed, the restart
+ callback only schedules the swap (`host/agent.go:4309`,
+ `rollout/manager.go:483`) while the declaring generation keeps serving
+ (`edge/genproxy.go:255`). The proof per backing:
+ - a nested or exec instance: the activator reports the serving
+ generation's build as the non-declaring version and no retiring
+ generation of a declaring build still has a process (after the swap,
+ `activator/swap.go:638`, and the retiring member's exit), or the
+ instance is asleep with its live version non-declaring;
+ - a unit-backed route: the unit's current main process was started from
+ the non-declaring version after the repoint.
+
+ Until that proof, the mark stays `1`, through a queued, failed or
+ abandoned replacement and through a rollback. The capability pass checks
+ for the proof each pass, and the activator signals it at the swap's end
+ so the clear is prompt.
- **Rollback** is an adoption like any other: to a declaring release, set
before; away from one, clear after.
- The same barrier applies to a grant: `carlos route --grant doc-engine`
@@ -674,6 +703,12 @@ process, real unix sockets and a real cgroup check where the kernel allows
admits engine calls exactly as the owner does; a minted hostname of a
public route keeps working across the upgrade; a static platform domain
and a static canary keep serving.
+- With generation rollouts armed, deploying a non-declaring release over a
+ declaring one keeps the route internal until the retiring generation has
+ exited; a failed swap leaves it internal.
+- A TCP-backed instance's minted hostname keeps serving across the upgrade;
+ two TCP instances of one app with an unrecorded owner are refused until
+ the reconciler records it.
- A `doc-engine` grant on a minted row of a non-declaring release makes the
owner and every minted row internal, through the barrier.
- `carlos engines arm` succeeds on a box with nested callers and a
@@ -771,3 +806,5 @@ it, not only over a private network.
| R5-3 no source for the probe path | Accepted: the ticket signs the watcher's probe path (§5.8) |
| R6-1 grant on a minted row ignored | Accepted: grants anywhere in the group mark the group (§5.1) |
| R6-2 static platform domains refused | Accepted: inheritance only for minted instance rows (§5.1) |
+| R7-1 clear-after reopens a still-serving engine under rollouts | Accepted: clear only on proof no declaring process remains (§5.1) |
+| R7-2 TCP-backed minted rows have no owner | Accepted: owner recorded on the row; unique non-empty fallback, TCP included (§5.1) |
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-ENGINES-R7.md b/docs/superpowers/specs/CODEX-FINDINGS-ENGINES-R7.md
new file mode 100644
index 000000000..4a24a3f01
--- /dev/null
+++ b/docs/superpowers/specs/CODEX-FINDINGS-ENGINES-R7.md
@@ -0,0 +1,49 @@
+| Previous finding | Round 7 assessment |
+|---|---|
+| R1-1: flat tenant borrows a nested socket | Fixed by the peer-cgroup check (§5.4). |
+| R1-2: aliases and path routes remain public | Fixed by checking the final upstream (§5.1). |
+| R1-3: new box starts public | Fixed by classification before serving and refusal of `NULL` (§5.1). |
+| R1-4: revoked grants return after restart | Fixed for retained local state; fresh-box replay remains explicitly accepted (§5.3). |
+| R1-5: door exceptions fail across routing | Resolved by removing the owner exception and defining the identity transition (§5.4). |
+| R1-6: socket lifecycle conflicts | Fixed by instance-level listener ownership (§5.4). |
+| R1-7: transport and body limits | Resolved at spec level (§5.5). |
+| R2-1: stamp follows serving | Set-before barrier fixed (§5.1). A separate clear-after failure follows below. |
+| R2-2: upgrade serves unclassified engines | Fixed by classification before listeners and refusal of `NULL` (§5.1). |
+| R2-3: flat callers borrow door credentials | Fixed by the door’s UID restriction (§§5.4, 6.2). |
+| R2-4: deployment verification fails | Fixed by watch tickets (§5.8). |
+| R2-5: port-80 refusal cannot work remotely | Resolved by the narrowed contract (§5.1). |
+| R3-1: previously admitted requests cross the mark | Fixed: acknowledgement waits for cancellation and completion (§5.1). |
+| R3-2: flat tenant injects into a nested caller | Fixed by excluding unisolated execution while armed; historical compromise remains explicitly excluded (§6.2). |
+| R3-3: grant-only routes lack watch tickets | Fixed: every deploy target receives a ticket (§5.8). |
+| R4-1: foreign instances and workers remain unisolated | Fixed by the arming prerequisite and launch refusals (§6.2). |
+| R4-2: path routing widens an engine grant | Fixed by exact-host engine dispatch (§5.4). |
+| R4-3: watch tickets expire mid-watch | Fixed by renewal every four minutes (§5.8). |
+| R5-1: platform-minted hostnames cannot classify | Socket-backed case fixed (§5.1); the TCP backend case remains uncovered, detailed below. |
+| R5-2: arming rejects isolated unit-backed routes | Fixed by distinguishing backing from exec layout (§6.2). |
+| R5-3: home edge lacks the probe path | Fixed by signing the watcher’s probe path (§5.8). |
+| R6-1: minted row’s own grant is ignored | Fixed by aggregating grants across the execution-owner group (§5.1). |
+| R6-2: static platform domains are refused | Fixed by restricting inheritance to minted instance rows (§5.1). |
+
+Reviewed spec `d59eec0c` against locally available amadan `origin/main` at `aaa6a949`. The implementation files cited below match that revision. No files changed.
+
+1. **P1 — The clear-after sequence can reopen a still-serving engine during a generation rollout (§§5.1, 6.3).**
+
+ Run an awake nested instance with generation rollouts enabled. Its serving release declares `doc_engine`, so its mark is `1`, but it holds no `doc-engine` grant. Deploy a non-declaring release.
+
+ Section 5.1 clears the mark after adoption repoints, restarts and records the version. On this execution path, however, the restart callback only schedules the replacement. Adoption can finish while the declaring generation still serves. Clearing the mark then exposes that engine publicly; if the successor fails, the exposure can persist.
+
+ The adoption callback selects `Manager.Adopted` when rollout is armed ([agent.go:4309](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-fleet-internal-engines/internal/host/agent.go:4309)). That method nudges the scheduler and returns success without waiting for a replacement ([manager.go:483](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-fleet-internal-engines/internal/rollout/manager.go:483)). The restart wrapper returns `nil` ([agent.go:4057](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-fleet-internal-engines/internal/host/agent.go:4057)), allowing adoption to record the new version ([adopt.go:2051](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-fleet-internal-engines/internal/release/adopt.go:2051)). Requests still select the actual serving generation ([genproxy.go:255](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-fleet-internal-engines/internal/edge/genproxy.go:255)), which changes separately during the successful swap ([swap.go:638](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-fleet-internal-engines/internal/activator/swap.go:638)).
+
+ **Suggested fix:** Assign clearing to confirmed activation of the non-declaring serving generation, with no remaining group grant. Neither the live symlink, recorded version nor successful scheduling callback proves that activation. Keep `1` through queued, failed and abandoned replacements.
+
+2. **P2 — Execution-owner matching omits supported TCP-backed instance rows (§5.1).**
+
+ Start with a public, classified instance using `Addr: 127.0.0.1:9000`, an empty `Socket`, and its automatically minted platform hostname. Upgrade the edge.
+
+ The new ownership rule identifies the source through `Socket` or fleet `Region`; neither identifies this TCP backend. Applying missing-owner refusal disables the platform hostname. Treating two empty socket values as a match instead makes ownership ambiguous when the app has multiple TCP instances.
+
+ This is an established route shape. `carlos add` explicitly supports TCP-only unit-backed instances without assigning a socket ([cli.go:172](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-fleet-internal-engines/cli.go:172)). Domain reconciliation selects sources carrying `Addr` ([reconcile.go:835](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-fleet-internal-engines/internal/domains/reconcile.go:835)) and copies that address into the minted row ([reconcile.go:881](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-fleet-internal-engines/internal/domains/reconcile.go:881)). An existing reconciliation test covers exactly this instance-and-platform-domain shape ([reconcile_test.go:113](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-fleet-internal-engines/internal/domains/reconcile_test.go:113)).
+
+ **Suggested fix:** Include TCP backend identity in execution-owner resolution, or record the source route explicitly. Empty socket values must not establish ownership; unresolved or ambiguous ownership should remain fail-closed.
+
+**Overall verdict: reject.** The generation rollout path can reopen a still-running engine. TCP-backed platform domains also need an ownership rule.
\ No newline at end of file