carlos / platform Public

spec: fleet-internal engines rev 12 (Astra R11: doc_engine excludes ports)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev a2f71f5f74f7ae8181452148f58a6f5fb9810d72 parent 24c79f8
2 files changed, +89 −1
  • docs/superpowers/specs/2026-10-21-fleet-internal-engines-design.md +37 −1
  • docs/superpowers/specs/CODEX-FINDINGS-ENGINES-R11.md +52 −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.383s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.007s
ok  	github.com/carlosframework/platform/cmd/moveprobe	93.289s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.720s
ok  	github.com/carlosframework/platform/internal/activator	250.483s
ok  	github.com/carlosframework/platform/internal/agentsock	0.017s
ok  	github.com/carlosframework/platform/internal/alerting	0.008s
ok  	github.com/carlosframework/platform/internal/analytics	0.020s
ok  	github.com/carlosframework/platform/internal/appconfig	2.553s
ok  	github.com/carlosframework/platform/internal/apperr	0.124s
ok  	github.com/carlosframework/platform/internal/applog	0.184s
ok  	github.com/carlosframework/platform/internal/awsinv	0.005s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.006s
ok  	github.com/carlosframework/platform/internal/boxid	0.624s
?   	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.014s
ok  	github.com/carlosframework/platform/internal/certwarm	0.005s
ok  	github.com/carlosframework/platform/internal/cgroup	0.720s
ok  	github.com/carlosframework/platform/internal/changelog	0.362s
ok  	github.com/carlosframework/platform/internal/claimactivation	0.894s
ok  	github.com/carlosframework/platform/internal/clamd	0.059s
?   	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 ab44d5940..e3de15036 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:** 11.
+**Revision:** 12.
**Discussion:** amadan carlos/platform #335. Related: #328 (per-app egress allow).
+- **Rev 12** answers Astra round 11 (`CODEX-FINDINGS-ENGINES-R11.md`,
+ reject, one P1: a declaring release that also declares `ports` keeps a
+ granted TCP listener the edge never sees). `doc_engine` and `ports` are
+ refused together at ship, as `doc_engine` and `host_role` already are, and
+ a box renders no port listener for an internal group (R11-1). §5.1 now
+ also says why systemd's own starts (socket activation, `Restart=always`)
+ keep I1. Marked **(codex R11-1)**.
- **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
@@ -291,6 +298,30 @@ the group is stopped and its cgroups empty. Any later declaring launch
re-marks the group through the gate. The one place I1 is not enforced is
stated as a residual under "Classification before serving".
+**Starts systemd makes on its own.** A unit-backed route's unit is also
+started by socket activation (`carlos-app@.socket`) and restarted by
+`Restart=always` (`carlos-app@.service:50`), without the activator. Both
+execute `/opt/carlos/live/<host>` (`:42`), the live version. Adoption
+passes the launch gate before it repoints the live link to a declaring
+build, and `--public` clears only when the live build does not declare, so
+whatever systemd starts is either fenced or non-declaring. The builder
+keeps that true by construction: nothing but adoption (through the gate)
+and `--public` (through its checks) may move a live link.
+
+**The edge is the only ingress (codex R11-1).** I1 protects the public only
+if every way in goes through the edge. A granted `ports` capability is a
+`ListenStream=<port>` socket the edge never sees (`caps/render.go:286`,
+`:319`), and a hibernating route's port starts the unit without the
+activator (`caps/converge.go:241`). So:
+
+- **At ship**, `doc_engine` and `ports` are refused together, beside the
+ existing `host_role` refusal (`caps/caps.go:382`): a conversion engine is
+ reached over HTTP through the platform, never on a port of its own.
+- **On the box**, the capability pass renders no `ListenStream` for any
+ route of an internal group, whatever its grants, and removes an existing
+ one (stopping the socket unit) before the launch gate's fence completes
+ for that group; it logs the grant it is ignoring with the revoke command.
+
**Setting the mark: the serving barrier (codex R2-1).** Adoption
repoints the live symlink (`release/adopt.go:1751`), restarts
(`adopt.go:1831`) and records the version afterwards (`adopt.go:2051`),
@@ -799,6 +830,10 @@ process, real unix sockets and a real cgroup check where the kernel allows
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.
+- `carlos ship` refuses a capability block with both `doc_engine` and
+ `ports`; a box with an existing port grant on a route that becomes
+ internal removes the listener before the fence completes, and a direct
+ connection to the port is refused afterwards.
- 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;
@@ -909,3 +944,4 @@ it, not only over a private network.
| 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) |
+| R11-1 a granted port bypasses the edge | Accepted: `doc_engine` + `ports` refused at ship; no listener for internal groups (§5.1) |
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-ENGINES-R11.md b/docs/superpowers/specs/CODEX-FINDINGS-ENGINES-R11.md
new file mode 100644
index 000000000..27c31f931
--- /dev/null
+++ b/docs/superpowers/specs/CODEX-FINDINGS-ENGINES-R11.md
@@ -0,0 +1,52 @@
+| Previous finding | Round 11 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 | Fixed by classification before serving (§5.1). |
+| R1-4: revoked grants return after restart | Fixed for retained state; fresh-box replay remains 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 | Fixed by the admission barrier (§5.1). |
+| R2-2: upgrade serves unclassified engines | 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 by waiting for cancellation and completion before acknowledging the fence (§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 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 | Fixed by gating the actual selected build (§5.1). |
+| R10-1: a written but unfenced `1` permits recovery launch | Fixed: every declaring launch waits for the current mark’s fence, including an already-written `1` (§5.1). |
+
+Reviewed `24c79f8a` against locally available amadan `origin/main` at `f550afa2`. The implementation files cited below match that revision. No files changed.
+
+**1. P1 — Granted TCP sockets expose a declaring build while I1 holds (§§5.1, 6.3).**
+
+A concrete sequence:
+
+1. A unit-backed HTTP service declares port `844`, holds `port:844`, and serves HTTP on its inherited listener. The deployment permits public ingress to that port.
+2. Deploy a release that retains the port declaration and adds `doc_engine`; supply the `doc-engine` grant for its resource requirements.
+3. Complete the specified launch gate: write `1`, load the internal serving map, drain earlier public edge requests, acknowledge the fence, and start the declaring build.
+4. A public client sends HTTP directly to the box’s port `844`. The inherited TCP listener delivers it to the declaring build without traversing the edge.
+
+**I1 holds throughout step 4:** the declaring process started after its group was fenced, and nothing cleared the mark. Nevertheless, the public reaches it.
+
+This combination is supported by existing code. Capability validation checks ports independently and rejects `doc_engine` together with `host_role`, but permits it together with ports ([caps.go:359](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-fleet-internal-engines/internal/caps/caps.go:359), [caps.go:382](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-fleet-internal-engines/internal/caps/caps.go:382)). `RenderSocket` renders each declared-and-granted port as a bare `ListenStream=<port>`, without consulting `DocEngine` ([render.go:286](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-fleet-internal-engines/internal/caps/render.go:286), [render.go:319](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-fleet-internal-engines/internal/caps/render.go:319)). The service receives the bound sockets directly ([carlos-app@.service:21](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-fleet-internal-engines/internal/host/units/carlos-app@.service:21)).
+
+Hibernation does not close this path. Capability convergence explicitly permits granted ports on hibernating routes and records that a connection starts the unit **without the activator** ([converge.go:241](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-fleet-internal-engines/internal/caps/converge.go:241)). That autonomous start executes the live binary directly; crash restarts also belong to systemd ([carlos-app@.service:42](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/design-fleet-internal-engines/internal/host/units/carlos-app@.service:42)). Thus the claimed launch-path coverage also misses systemd-initiated starts.
+
+**Suggested fix:** Make internal classification incompatible with public TCP listeners for that execution group. Close existing generated listeners before completing the internal transition and prevent capability convergence from restoring them. Enforce the launch gate at the unit’s actual execution entrypoint so socket activation and automatic restarts cannot bypass it. Test a declaring build with an existing port grant: after fencing, a direct public connection must never reach it.
+
+**Overall verdict: reject.** R10 is fixed, but an edge-only fence leaves a supported public ingress path open while I1 holds.
\ No newline at end of file