spec rev 9: claim-mode egress deferred, one policy writer per bucket, request budget (Paul, 2026-10-25)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 file changed,
+57
−6
docs/superpowers/specs/2026-10-21-instance-sizes-and-egress-design.md+57 −6
diff --git a/docs/superpowers/specs/2026-10-21-instance-sizes-and-egress-design.md b/docs/superpowers/specs/2026-10-21-instance-sizes-and-egress-design.md| index 05bf1da84..1ac945fe8 100644 |
| --- a/docs/superpowers/specs/2026-10-21-instance-sizes-and-egress-design.md |
| +++ b/docs/superpowers/specs/2026-10-21-instance-sizes-and-egress-design.md |
| @@ -1,8 +1,10 @@ |
| # Instance sizes and egress allows |
| Amadan #328 (an isolated instance reaching a private service its operator |
| -runs) and #329 (operator-set CPU and memory per instance). Revision 8, |
| -2026-10-21. Revision 8 folds in what the three plan writers found |
| +runs) and #329 (operator-set CPU and memory per instance). Revision 9, |
| +2026-10-25. Revision 9 adds Paul's rulings 9 to 11 (claim-mode |
| +egress deferred, one policy writer per bucket, a request budget, §3a). |
| +Revision 8 folds in what the three plan writers found |
| against the code (§9). Revisions 2 to 7 answer the builder check, Astra rounds |
| 1 to 6, and Paul's ruling on fleet addresses (`CODEX-FINDINGS-instance-sizes-egress-R<n>.md`); the |
| dispositions are §9. **Approved by Paul, 2026-10-21.** |
| @@ -36,6 +38,23 @@ written by root onto each instance's own slice** |
| 8. **Fleet box addresses are checked when an allow is granted and |
| reported afterwards, not enforced continuously by root** (2026-10-21, |
| §4.2). |
| +9. **Egress under host claims is deferred** (2026-10-25). On a box with |
| + claims on, root writes no allow and reports `egress: not yet |
| + supported under claims`. The claim-mode source (root reading and |
| + verifying the policy itself, a revision floor, an authenticated app |
| + incarnation, durable verified state across restarts) is built with |
| + claim activation, as its own thread. Everything in this spec marked |
| + "under claims" for egress is that follow-up, not this build. Sizes |
| + under claims are unchanged: a new size waits for the next start. |
| +10. **One policy writer per bucket** (2026-10-25). A console takes a |
| + writer lease in the bucket before it signs any policy; a second |
| + console refuses policy writes and names the lease holder. Multi- |
| + console coordination is follow-up work if a deployment ever needs it. |
| +11. **S3 requests cost money** (2026-10-25). No reader polls the policy on |
| + its own timer. Policy objects map to the #336 change index's app scope |
| + (§3a); readers re-read an app's policy only when its generation moves, |
| + plus the 30-minute backstop. The steady-state budget is in §3a and is |
| + tested. |
| ## 1. Why |
| @@ -150,7 +169,8 @@ that way. |
| unverifiable`) and as no override for sizes (the build's tier, from the |
| next start; never a live shrink). A console down for longer than the |
| lifetime therefore closes granted addresses, the door keys' trade. |
| -- **Under claims root reads one policy per app**, not per host, at most |
| +- **[Deferred by ruling 9; kept as the follow-up's starting point.]** |
| + **Under claims root reads one policy per app**, not per host, at most |
| every 30 seconds, as a conditional read on the ETag, so the loop costs |
| one small request per app with allows per 30 seconds and nothing when |
| unchanged. An app with no policy object is re-read every 5 minutes (and on |
| @@ -172,7 +192,7 @@ that way. |
| policy exists (its purge, `ownership.go:514`, never touches |
| `control/app-policy/`; its create, `server.go:4130`, never clears it). |
| Two rails: |
| - - **Rollout rule:** no policy is granted until every console that |
| + - **Rollout rule** (now enforced by ruling 10's writer lease): no policy is granted until every console that |
| writes the deployment's bucket runs this version. A deployment runs |
| one console per bucket today (temporary static sites spec, "one |
| console per deployment bucket, which is how every deployment runs"), |
| @@ -181,7 +201,7 @@ that way. |
| A console downgraded below this version after policies exist must |
| delete them first; the release notes and the downgrade runbook say |
| so. |
| - - **With host claims on, the policy is bound to the app's incarnation:** |
| + - **[Deferred by ruling 9.]** **With host claims on, the policy is bound to the app's incarnation:** |
| it carries the app's `app_id` (`ClaimAppID`, the identity the claim |
| purge journal and admissions already use, `hostops/admission.go`), |
| and root ignores a policy whose `app_id` is not the current app's, |
| @@ -196,6 +216,36 @@ that way. |
| whole, the plan adds the prefix to the boundary before anything reads |
| it. |
| +## 3a. Request budget (ruling 11) |
| + |
| +The deployment bucket is the store of record, and every request costs |
| +money (#336: the October bill was polling that scaled apps × boxes × |
| +passes). This feature must add **no request a box makes per pass when |
| +nothing changed**. |
| + |
| +- **Change index scope.** `release.ChangeScope` maps |
| + `control/app-policy/<sqid>/<app>.json` to the scope `apps/<sqid>/<app>` |
| + (a new row in its table, pinned by its table test), so every policy |
| + write bumps the app's generation through the concrete stores' existing |
| + hook (#336 spec §3.3). The agent re-reads an app's policy only when |
| + that app's generation moved, or on the 30-minute backstop walk, which |
| + reads `control/app-policy/` with one LIST and GETs only keys whose ETag |
| + differs from the last read. |
| +- **Root reads no store at all** for egress (pre-claim source is the |
| + registry; claim mode is deferred, ruling 9). |
| +- **Console:** the grant check reads `control/edges/` only on a grant; |
| + the report re-runs on a `control/edges` generation change and on the |
| + backstop, never on a 15-minute timer. The re-sign pass reads each live |
| + policy once per 12 hours, as door keys do. |
| +- **Sizes** add no store reads: `mem_total_mib` rides the edges file the |
| + box already writes, on change only. |
| +- **Budget, stated and tested:** with no policy changes, the added |
| + steady-state cost per box per day is at most 48 LISTs (the backstop's |
| + one per 30 minutes) and zero GETs; per console per day, 2 GETs per live |
| + policy (re-sign) plus 48 LISTs. Tests count store calls through a |
| + counting store wrapper on the agent's reconcile and the console's loops |
| + and fail on any increase over that. |
| + |
| ## 4. Egress allow (#328) |
| ### 4.1 What is accepted |
| @@ -271,7 +321,8 @@ request (`server.go:33`). Its source follows the trust root already has |
| for `HostApp`, the lookup that decides which build a nested generation |
| may run (`validateStagedExe`): |
| -- **With host claims on**, `HostApp` is store-verified |
| +- **[Deferred by ruling 9: with claims on, root writes no allow.]** |
| + **With host claims on**, `HostApp` is store-verified |
| (`rootInstallationHostApp`: claim holder, installation, and the record |
| root reads from the store). Root then reads |
| `control/app-policy/<sqid>/<app>.json` from the store itself, for that |