carlos / platform Public

spec: fleet-internal engines rev 11 (Astra R10: I1 on the fenced mark)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 24c79f8a08060daf0f94514483c7bb70b12cf3d2 parent 267c0ee
2 files changed, +83 −8
  • docs/superpowers/specs/2026-10-21-fleet-internal-engines-design.md +32 −8
  • docs/superpowers/specs/CODEX-FINDINGS-ENGINES-R10.md +51 −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.371s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.004s
ok  	github.com/carlosframework/platform/cmd/moveprobe	93.338s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.719s
ok  	github.com/carlosframework/platform/internal/activator	251.216s
ok  	github.com/carlosframework/platform/internal/agentsock	0.032s
ok  	github.com/carlosframework/platform/internal/alerting	0.004s
ok  	github.com/carlosframework/platform/internal/analytics	0.045s
ok  	github.com/carlosframework/platform/internal/appconfig	2.570s
ok  	github.com/carlosframework/platform/internal/apperr	0.095s
ok  	github.com/carlosframework/platform/internal/applog	0.162s
ok  	github.com/carlosframework/platform/internal/awsinv	0.005s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.005s
ok  	github.com/carlosframework/platform/internal/boxid	0.580s
?   	github.com/carlosframework/platform/internal/boxid/boxidtest	[no test files]
?   	github.com/carlosframework/platform/internal/boxid/internal/signtest	[no test files]
ok  	github.com/carlosframework/platform/internal/boxvolumes	0.010s
ok  	github.com/carlosframework/platform/internal/caps	0.015s
ok  	github.com/carlosframework/platform/internal/certwarm	0.005s
ok  	github.com/carlosframework/platform/internal/cgroup	0.738s
ok  	github.com/carlosframework/platform/internal/changelog	0.367s
ok  	github.com/carlosframework/platform/internal/claimactivation	0.901s
ok  	github.com/carlosframework/platform/internal/clamd	0.060s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]

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 a906cb5fe..ab44d5940 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,16 @@
# Fleet-internal engines: a doc engine nobody on the internet can call
**Date:** 2026-10-21
-**Revision:** 10.
+**Revision:** 11.
**Discussion:** amadan carlos/platform #335. Related: #328 (per-app egress allow).
+- **Rev 11** answers Astra round 10 (`CODEX-FINDINGS-ENGINES-R10.md`,
+ reject, one P1: a mark written but never fenced, after a failed reload
+ and a timed-out adoption, let a recovery launch start a declaring build
+ while a public request was admitted). I1 now speaks of the **fenced**
+ mark, not the registry's, and every declaring launch requires the fence
+ whether or not it finds `1` already written (R10-1). Marked
+ **(codex R10-1)**.
- **Rev 10** answers Astra round 9 (`CODEX-FINDINGS-ENGINES-R9.md`, reject,
one P1: after `--public`, a failed first wake falls back to a declaring
`GoodVersion`). Instead of a fourth path-specific rule, §5.1 now states
@@ -247,8 +254,15 @@ copying the source's kind and backend but no version, and adoption skips it
**The invariant (codex R9-1).**
-> **I1.** No process of a build whose manifest declares `doc_engine` runs
-> while its group's mark is `0`.
+> **I1.** A process of a build whose manifest declares `doc_engine` starts
+> only after the edge has **fenced** its group as internal: the edge's
+> fenced counter (below) has reached a registry change that set the
+> group's mark to `1`, and no later change has cleared it.
+
+What protects the public is the edge's serving map, not the registry row
+(codex R10-1): a `1` written while the edge's `Reload` fails
+(`edge.go:925`, `:993`) refuses nobody. So the registry mark is the
+intent, and the fence is the fact I1 is about.
Rounds 2 to 9 each found one more path by which a declaring build could
run under a public mark: adoption's order, requests admitted early, a
@@ -260,11 +274,16 @@ activator launch path (an exec child, a nested run or generation unit
through the control service, a unit-backed `systemctl start`, a seed, a
`carlos run` command, a swap's incoming generation, a wake plan's
fallback or recovery build) reads the manifest of the build it is about to
-start. If that build declares `doc_engine` and the group's mark is `0`,
-the launch first writes `1` and waits for the edge's fence (below), which
-cancels every public request admitted under `0`, including the one whose
-wake triggered the launch; only then does it start the process. A launch
-whose build manifest it cannot read is treated as declaring. Adoption's
+start. If that build declares `doc_engine`, the launch writes `1` if it is
+not already written, and then **always** waits for the edge's fence to
+cover the group's current mark (below), which cancels every public request
+admitted before it, including the one whose wake triggered the launch.
+Only then does it start the process. A launch that finds `1` written but
+not yet fenced (a timed-out adoption, a reload that keeps failing) waits
+the same way; if the fence does not arrive within the bound, the launch is
+refused, the wake fails, and the request gets the activator's ordinary
+wake-failure answer. A launch whose build manifest it cannot read is
+treated as declaring. Adoption's
set-before is the same gate, reached earlier.
`--public` (below) is the only writer of `0`, and it checks I1 directly:
@@ -776,6 +795,10 @@ process, real unix sockets and a real cgroup check where the kernel allows
fallback to a declaring `GoodVersion` after a failed first wake, a launch
of a declaring build under `0` sets `1`, completes the fence, and cancels
the public request that caused it before the process starts.
+- With the edge's reload held failing: an adoption of a declaring build
+ times out with `1` written; a later recovery wake that selects a
+ declaring `GoodVersion` is refused, no declaring process starts, and the
+ public request gets the wake-failure answer.
- Upgrade classification marks a route internal when its live version is
non-declaring but another staged version of the app declares.
- A TCP-backed instance's minted hostname keeps serving across the upgrade;
@@ -885,3 +908,4 @@ it, not only over a private network.
| R8-1 upgrade classifies a running declaring build public | Accepted: staged declaring versions classify `1`; residual stated (§5.1) |
| R8-2 a sleeping unit-backed route cannot clear | Accepted, with R7-1: no automatic clear; `--public` stops the group and checks cgroups (§5.1) |
| R9-1 `--public` then a fallback to a declaring `GoodVersion` | Accepted: invariant I1 enforced at every launch (§5.1) |
+| R10-1 a written but unfenced `1` lets a declaring launch through | Accepted: I1 is about the fenced mark; every declaring launch waits for the fence (§5.1) |
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-ENGINES-R10.md b/docs/superpowers/specs/CODEX-FINDINGS-ENGINES-R10.md
new file mode 100644
index 000000000..f734ae53a
--- /dev/null
+++ b/docs/superpowers/specs/CODEX-FINDINGS-ENGINES-R10.md
@@ -0,0 +1,51 @@
+| Previous finding | Round 10 assessment |
+|---|---|
+| R1-1: flat tenant borrows a nested socket | Fixed by peer-cgroup authentication (§5.4). |
+| R1-2: aliases and path routes remain public | Fixed by checking the final upstream (§5.1). |
+| R1-3: new box starts public | Reported case fixed by classification before serving (§5.1). |
+| R1-4: revoked grants return after restart | Fixed with retained state; fresh-box replay is an explicit residual (§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 ownership (§5.4). |
+| R1-7: transport and body limits | Resolved at spec level (§5.5). |
+| R2-1: stamp follows serving | Reported ordering fixed by the admission barrier (§5.1). |
+| R2-2: upgrade serves unclassified engines | Reported case fixed by startup classification 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 when the barrier completes: 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 is 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 arming prerequisites 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 (§5.8). |
+| R5-1: platform-minted hostnames cannot classify | Fixed by execution-owner inheritance (§5.1). |
+| R5-2: arming rejects isolated unit-backed routes | Fixed by distinguishing backing from layout (§6.2). |
+| R5-3: home edge lacks the probe path | Fixed by signing the probe path (§5.8). |
+| R6-1: minted row’s own grant is ignored | Fixed by aggregating group grants (§5.1). |
+| R6-2: static platform domains are refused | Fixed by restricting inheritance to minted instance rows (§5.1). |
+| R7-1: clearing exposes a still-serving generation | Fixed by stopping the group and proving its cgroups empty (§5.1). |
+| R7-2: TCP-backed minted rows have no owner | Fixed by explicit ownership and unique non-empty backend matching (§5.1). |
+| R8-1: upgrade classifies a running declaring build public | Reported case fixed by scanning staged manifests; missing-manifest exposure remains an explicit residual (§5.1). |
+| R8-2: sleeping unit-backed route cannot clear | Resolved by operator-controlled stop-and-clear (§5.1). |
+| R9-1: recovery launches a declaring `GoodVersion` after clearing | First launch under `0` fixed by the launch gate (§5.1). An already-`1`, unfenced state remains uncovered below. |
+
+Reviewed `267c0ee6` against locally available amadan `origin/main` at `f550afa2`. The implementation files cited below match that revision. No files changed.
+
+**1. P1 — A pending internal mark lets a later recovery launch skip the fence while I1 holds (§§5.1, 6.3).**
+
+The [launch rule](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-fleet-internal-engines/docs/superpowers/specs/2026-10-21-fleet-internal-engines-design.md:263) requires writing `1` and waiting for the fence **when the group’s mark is `0`**. But `1` does not establish that the fence completed: the [timeout rule](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-fleet-internal-engines/docs/superpowers/specs/2026-10-21-fleet-internal-engines-design.md:298) deliberately leaves it set after an unsuccessful wait.
+
+A concrete sequence:
+
+1. A sleeping nested instance has declaring build A as `GoodVersion`, non-declaring B installed, and an overlap-mode failure record that selects A for recovery. With no grant remaining, `--public` stops the group and clears its mark.
+2. Adoption attempts declaring build C. It writes `1`, but the edge’s reload fails. After 30 seconds adoption returns without repointing. The registry says `1`; the serving map still says `0`; B remains installed.
+3. While reload remains unsuccessful, a public request passes the old map’s admission check and wakes the instance. Recovery selects A.
+4. A’s launch reads its declaring manifest and the already-set mark `1`. The specified conditional does not require this launch to finish the outstanding fence. A starts, and the admitted public request reaches it.
+
+These are existing execution paths. A failed alias read aborts reload before replacing the serving map ([edge.go:925](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-fleet-internal-engines/internal/edge/edge.go:925), [edge.go:993](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-fleet-internal-engines/internal/edge/edge.go:993)). A blocked installed build selects `GoodVersion` directly ([rollout/wakeplan.go:97](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-fleet-internal-engines/internal/rollout/wakeplan.go:97)), and the activator launches that selected build ([activator/wakeplan.go:388](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-fleet-internal-engines/internal/activator/wakeplan.go:388)). Once wake succeeds, the waiting request invokes its proxy ([activator.go:5404](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-fleet-internal-engines/internal/activator/activator.go:5404)), which selects the actual serving generation ([genproxy.go:255](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-fleet-internal-engines/internal/edge/genproxy.go:255)).
+
+**I1 holds throughout:** A runs only while the registry mark is `1`. Public access nevertheless remains possible because the edge has not fenced that mark.
+
+**Suggested fix:** Every declaring launch must require acknowledgement of the group’s current internal admission epoch, including when it finds `1` already set. A pending or timed-out transition must remain a launch refusal until its fence completes. Add a test that holds reload unsuccessful, times out adoption, and then requests a recovery wake; no declaring process may start.
+
+**Overall verdict: reject.** I1 alone permits a declaring process behind an internal registry mark that the serving edge has not yet enforced.
\ No newline at end of file