docs: nearest-region spec rev 2, after Astra's round-1 review
All eight findings verified against the code and accepted, marked "(codex N)" in the text: pooled apps get no guess; adoption resolves the guess after it knows it is an adoption and discards it; non-home app regions need a named home and a CONSOLE_APP_REGIONS switch; a read-side- first rollout with a binary rollback floor; canary --region on an existing canary; three-state manifest policy for /regions; app_default availability for absent regions; location hints moved to POST bodies so the canonical-host redirect cannot echo them. Twelve of the sixteen open questions are resolved in the body; three choices remain for Paul. §19 records every disposition.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 file changed,
+628
−345
docs/superpowers/specs/2026-10-01-nearest-region-default-design.md+628 −345
diff --git a/docs/superpowers/specs/2026-10-01-nearest-region-default-design.md b/docs/superpowers/specs/2026-10-01-nearest-region-default-design.md| index df4f30cc4..d213f17fb 100644 |
| --- a/docs/superpowers/specs/2026-10-01-nearest-region-default-design.md |
| +++ b/docs/superpowers/specs/2026-10-01-nearest-region-default-design.md |
| @@ -1,13 +1,17 @@ |
| # Nearest region by default: each app picks its region at creation |
| **Date:** 2026-10-01 |
| +**Revision:** 2. Rev 1 (7fbc9479) was reviewed by Astra (GPT-6, via codex): |
| +`CODEX-FINDINGS-NEAREST-REGION-R1.md`, 4 P1 and 4 P2, verdict not ready for |
| +planning. Every finding was checked against the code and accepted. Changes |
| +are marked "(codex N)" in the text, and §19 records each disposition. |
| **Status:** DRAFT for review. The rulings in §3 are Paul's (2026-10-01) and |
| are not reopened here. Everything marked *(decision)* is this spec's and open |
| -to review. §17 lists what the code could not settle. |
| +to review. §17 holds only the questions that need Paul. |
| **Discussion:** not yet filed on amadan. |
| **Builds on:** #329 (the armed-placement gate), #346 (the home region's name, |
| `CONSOLE_HOME_REGION`), the customer-fleets placement column, #162 (replica |
| -placements), #175/#331 (instance moves). None of them changes here. |
| +placements), #175/#331 (instance moves). |
| ## 1. The problem, as found |
| @@ -41,12 +45,13 @@ What exists to build on: placement codes are bare (`ie`, `uk`, `au`), |
| operator free text for the public status page (`internal/host/agent.go:88-92`). |
| Every box publishes its AWS region and its placement code in its edges file |
| (`internal/host/edgesfile.go:155-176`; the region comes from `edgesFileRegion`, |
| -`:537`), and the console already reads both (`edgesFilePlacementView`, |
| -`internal/console/instancesplacement.go:35-55`; `servedBy.Region`, `:25-28`). |
| -`placements` and `armedPlacements` (`instancesplacement.go:173`, `:207`) know |
| -which codes can take an instance. `GET /api/cli/apps/{account}/{app}/regions` |
| -(`internal/console/apiregions.go:31-55`) returns codes, `armed` and |
| -`"default": HomeRegion`. There is no geolocation of any kind. |
| +`:537`, which honours a `CARLOS_AWS_REGION` override), and the console already |
| +reads both (`edgesFilePlacementView`, `internal/console/instancesplacement.go:35-55`; |
| +`servedBy.Region`, `:25-28`). `placements` and `armedPlacements` |
| +(`instancesplacement.go:173`, `:207`) know which codes can take an instance. |
| +`GET /api/cli/apps/{account}/{app}/regions` (`internal/console/apiregions.go:31-55`) |
| +returns codes, `armed` and `"default": HomeRegion`, and reads no manifest. |
| +There is no geolocation of any kind. |
| ## 2. Goals and non-goals |
| @@ -55,14 +60,25 @@ Goals: |
| - A new app gets a region at creation, preselected from a guess of where its |
| creator is, and its new instances start there unless told otherwise. |
| - The guess is generic: any deployment, any set of AWS regions. |
| -- The guess can never name a region the create door would refuse. |
| +- The guess can never name a region the create door would refuse, or one the |
| + reconciler would never mint (codex 1). |
| - Existing apps, older CLIs and external callers that send nothing behave |
| exactly as today. |
| -Non-goals: moving existing instances (#175/#331); box sizing and disk alerts; |
| -an account-level default (R1); changing an app's region after creation |
| -(§17 Q4); a browser instance-create form, which does not exist today (§10); |
| -any box-side change. |
| +Non-goals: |
| + |
| +- Moving existing instances (#175/#331). |
| +- Box sizing and disk alerts. |
| +- An account-level default (R1). |
| +- Changing an app's region after creation. This release is creation-only. A |
| + later owner-only verb would change future creates only and leave existing |
| + instances alone. |
| +- A browser instance-create form. None exists today (§10), and the CLI and |
| + API already reach the shared door. |
| +- Sidecars following the app's region. Their door has no armed gate, so that |
| + needs its own design (§12). |
| +- Pool-aware candidates. Pooled apps get no guess and no default (§6.1, §7). |
| +- Any change to DNS steering (§17 Q2), and any box-side change. |
| ## 3. Rulings (Paul, 2026-10-01) |
| @@ -74,7 +90,7 @@ any box-side change. |
| | R4 | The CLI guesses too. `carlos deploy` creating an app sends its timezone, the console resolves it, the CLI prints one line ("Region: UK · London (from your timezone). Use --region to choose another."). No prompt. `--region` overrides everywhere. `carlos instances create` defaults to the app's region. | §11 | |
| | D1 | App region stored console-side, set once at creation. The create door fills an empty `req.Region` from it **before** normalization, allow-list and armed checks. Boxes unchanged. Existing apps backfilled to the home: zero behaviour change. | §5, §7 | |
| | D2 | Guess in coordinates: timezone to a point, IP to a point via an operator-supplied MaxMind-format `.mmdb` (`CONSOLE_GEOIP_DB`), then great-circle nearest **armed** region. Home when everything fails. | §6 | |
| -| D3 | Surfaces: new-app picker; external pickers get names and the guess from `GET …/regions`; CLI as R4. | §9, §10, §11 | |
| +| D3 | Surfaces: new-app picker; external pickers get names and the guess; CLI as R4. | §9, §10, §11 | |
| | D4 | Place names from one platform-shipped AWS-region table, used everywhere, reconciled with `STATUS_BOX_REGION`. | §13 | |
| ## 4. The shape, in one paragraph |
| @@ -83,11 +99,13 @@ The console's `apps` row gains `region`, in the record's own vocabulary (`""` |
| is the home), and `region_source`, one word saying how it was picked. Both |
| app-create doors resolve a region before the row is written: an explicit |
| choice, else the creator's timezone, else the request's IP through the |
| -optional GeoIP database, else the home, where "resolve" means the great-circle |
| -nearest region that can host an instance right now. From then on an instance |
| +optional GeoIP database, else the home. "Resolve" means the great-circle |
| +nearest region that can host an instance right now. From then on, an instance |
| create that names no region gets the app's region, and that value meets every |
| check an explicit one meets. Existing rows read `""`, which is what they have |
| -always meant. |
| +always meant. Nothing writes a non-home app region until the operator turns |
| +the feature on, and the console refuses to turn it on without a named home |
| +(§5.6). |
| ## 5. Data model |
| @@ -127,12 +145,12 @@ the rule the comment at `:499-503` already records for `placement`, |
| `CreateAppInRegion(accountID, app, createdBy, region, source, now)` carries |
| both columns; `CreateAppIn` (`store.go:1349`) becomes a call to it with |
| `("", "")`, so `CreateApp` (`:1342`) is unchanged. `claimApp` |
| -(`internal/console/server.go:3466`) gains `region, source` parameters. |
| +(`internal/console/server.go:3466`) gains a `regionReq` parameter (§5.5). |
| This departs from the placement precedent on purpose. `SetAppPlacement` after |
| `CreateAppIn` (`server.go:3550-3567`) is best-effort because `carlos apps |
| -place` repairs it. An app region has no repair verb (§17 Q4), so a failed |
| -second write would leave the app at the home for good, silently. |
| +place` repairs it. An app region has no repair verb, so a failed second write |
| +would leave the app at the home for good, silently. |
| Rename (`store.go:1791`) and transfer (`:1709`) update the same row, so the |
| region survives both. The legacy owners backfill (`:665`) inserts defaults, |
| @@ -145,25 +163,85 @@ which mean the home. |
| `""` into `""` and nothing changes, byte for byte. Writing the home's code |
| instead would gain nothing (`homeRegionIn` normalizes it back to `""`), would |
| be impossible where `CONSOLE_HOME_REGION` is unset, and would freeze the home |
| -as of migration day where `""` follows it as records already do. It is |
| -idempotent by construction: the ALTERs swallow "duplicate column" and there is |
| -no one-shot `PRAGMA user_version` step. |
| - |
| -### 5.5 Adoption (#79) |
| - |
| -An owner claiming a name whose objects already exist (`adoptingExistingApp`, |
| -`server.go:3502`, `:3545`) is restoring an app that probably has instances at |
| -the home. **(decision) Adoption stores `""`/`""` unless the request names a |
| -region.** Restoring a console row must not send a live app's next instance |
| -somewhere new. |
| +as of migration day, where `""` follows it as records already do. It is |
| +idempotent by construction: the ALTERs swallow "duplicate column", and there |
| +is no one-shot `PRAGMA user_version` step. |
| + |
| +### 5.5 Explicit choice versus guess, and adoption (codex 2) |
| + |
| +The request to `claimApp` is a `regionReq{explicit string; tz string; |
| +person bool}`, not a resolved region. **Only `explicit` is a person's |
| +choice.** The guess is resolved inside `claimApp`, after it knows whether |
| +this is an adoption, so no caller can hand it a guess dressed as a choice. |
| + |
| +**(decision) Adoption never changes an existing app's region by guessing.** |
| +When an owner claims a name whose objects already exist (`adoptingExistingApp`, |
| +`server.go:3502`, `:3545`), the app probably has instances at the home. An |
| +adoption stores `""` with source `default` unless `explicit` is set. A guess, |
| +however it was computed and whatever the form displayed, is discarded. |
| + |
| +The browser form keeps the two apart without trusting any label: the |
| +preselected option carries the value `auto`, and only a region code is |
| +`explicit` (§10). The CLI sends `region` only for `--region`, and `timezone` |
| +separately (§9.1). |
| + |
| +### 5.6 The switch and the named home (codex 3, codex 4) |
| + |
| +**(decision) Non-home app regions are written only when both hold:** |
| + |
| +- `CONSOLE_APP_REGIONS=on` (new, default off), and |
| +- `CONSOLE_HOME_REGION` is set. |
| + |
| +If the switch is on and the home is unnamed, the console logs this once at |
| +startup and behaves as if the switch were off. A warning alone would not do. |
| +With an unnamed home, `""` is the only way to say "home", and `""` also means |
| +"no preference". A request could then never choose the home over a non-home |
| +app default, and an app whose region stopped hosting would be stuck (codex 3). |
| +A named home makes `"region": "<home name>"` an explicit choice at every door. |
| + |
| +While the switch is effectively off: |
| + |
| +- Every new app gets `""`/`default`. An explicit non-home `region` on app |
| + create is not stored, and the response shows what was (§9.1). |
| +- The new-app picker is hidden. |
| +- The guess endpoint (§9.4) still answers, because a per-instance guess for |
| + titogo writes nothing to the app row. |
| +- **The door's fill still runs.** Turning the switch off stops new promises. |
| + It never breaks the ones already made. |
| + |
| +### 5.7 Mixed versions and rollback (codex 4) |
| + |
| +The console owns its SQLite database, so one database is served by one |
| +console at a time. "Mixed versions" means a rollback, or a roll that has not |
| +reached a deployment's console yet. A console older than this feature ignores |
| +the columns. After a rollback it would place a no-region create for a `uk` |
| +app at the home, succeeding outside the app's promised region. |
| + |
| +**(decision) The rule:** |
| + |
| +1. **Roll the read side first.** The first release carries the migration, the |
| + fill, the endpoints and the UI with the switch off. With the switch off, |
| + every app row is `""`, so this release can be rolled back freely. |
| +2. **The switch is the feature's rollback lever.** Turning it on happens only |
| + after that release has run cleanly. Turning it off stops new non-home |
| + values and keeps the fill. |
| +3. **Once any app holds a non-home region, the binary rollback floor is the |
| + first region-aware release.** README's console section names that floor |
| + version. Rolling the binary below it is out of policy. If it happens |
| + anyway, the failure is today's behaviour, not data loss: no-region creates |
| + land at the home. On roll-forward, the console logs each app with a |
| + non-home region that has records with an empty region created after the |
| + app. That is one log line per app, so the gap is found rather than assumed. |
| + |
| +CLIs mix freely in both directions (§9.6). |
| ## 6. Resolving a region |
| ### 6.1 Candidates |
| -**(decision) One function, `regionCandidates(ctx)`, in `instancesplacement.go`, |
| -from the same `readEdgesFiles` read `placements` uses.** Each candidate is |
| -`{code, awsRegion, armed}`: |
| +**(decision) One function, `regionCandidates(ctx, policy)`, in |
| +`instancesplacement.go`, from the same `readEdgesFiles` read `placements` |
| +uses.** Each candidate is `{code, awsRegion, armed}`: |
| - **Coded regions**: boxes whose `Placement` passes `provision.ValidRegion`. |
| Fleet codes (`fleet/<name>`) fail on the `/`, so customer fleets are never |
| @@ -174,10 +252,20 @@ from the same `readEdgesFiles` read `placements` uses.** Each candidate is |
| (`apiregions.go:42-47`). |
| - **AWS region**: the `Region` of the armed boxes behind the code. If they |
| disagree, the most common wins, ties to the lexically smallest, logged once. |
| -- **Allow-list**: when the app's manifest is known (§9.3), candidates pass |
| - through `instanceRegionAllowed` (`apiinstances.go:1797`) on the |
| - **normalized** code, the door's exact predicate. At app creation there is no |
| - manifest yet, so nothing is filtered. |
| +- **Policy** is one of three states, read with `provision.GetManifest` |
| + directly and never through `instanceManifest`, which answers 409 or 502 |
| + for the caller (`apiinstances.go:117`) (codex 6): |
| + - *absent*: no allow-list, no pool. This is app creation, and any app that |
| + has not run `instances enable`. |
| + - *known*: candidates pass through `instanceRegionAllowed` |
| + (`apiinstances.go:1797`) on the **normalized** code, the door's exact |
| + predicate. **A pooled manifest yields no candidates and no guess** |
| + (codex 1). The armed set knows nothing of pools, and the reconciler |
| + mints a record only on a box serving both its pool and its region |
| + (`internal/provision/reconcile.go:335`). Pool-aware candidates would |
| + need eligibility proven by the target box itself, and are a non-goal. |
| + - *unreadable*: no guess, and an explicit error at the guess endpoint |
| + (§9.4). An unrestricted policy is never invented. |
| - **Dormant gate**: when no box publishes a placement, the home is the only |
| candidate and every guess is `default`. A single-box deployment sees no |
| picker and no change. |
| @@ -189,32 +277,55 @@ A point is a latitude and longitude held in memory for one request. |
| **Timezone.** *(decision)* A new package, `internal/geo`, embeds from one IANA |
| tzdata release: `zone.tab` (one row per country and zone, ISO 6709 |
| coordinates), a link table generated from tzdata's `Link` lines, and a |
| -`VERSION` stamp. `zone.tab`, not `zone1970.tab`, because browsers report names |
| -`zone1970.tab` has merged away: `Europe/Amsterdam` has its own row in |
| -`zone.tab` and none in `zone1970.tab`. Links matter because Chrome reports |
| -`Asia/Calcutta` where Firefox reports `Asia/Kolkata`. Go's `time/tzdata` |
| -carries no coordinates, so it cannot be the source. **Kept current** by |
| -`scripts/update-tzgeo.sh <version>`, which fetches the release from |
| -`data.iana.org`, checks its signature and rewrites the three files; it is a |
| -release-checklist step beside the Go toolchain bump. A stale table only misses |
| -zone names newer than it, and those fall through to IP. |
| - |
| -Lookup: a shape check (at most 64 bytes, one to three `/`-separated segments |
| -of `[A-Za-z0-9_+-]`); then unmappable by rule `""`, `Local`, `UTC`, `UCT`, |
| -`GMT`, `Universal`, `Zulu`, `Factory` and all of `Etc/`, which name an offset |
| -or a machine, not a place (R3); then links, at most 4 hops; then `zone.tab`, |
| -case-insensitive. A miss at any step is unmappable. |
| - |
| -**IP.** *(decision)* `CONSOLE_GEOIP_DB` names an operator-supplied `.mmdb` |
| -(GeoLite2/GeoIP2 City or Country, DB-IP City or Country Lite), read in |
| -`cmd/console/main.go` beside `CONSOLE_HOME_REGION` (`main.go:278`) into a new |
| -`Config.GeoIP`. Unset means IP fallback off, the default. Unopenable or not an |
| -mmdb: logged once, fallback off, **the console still starts**. Opened once, |
| -memory-mapped; replacing it is an atomic rename plus a console restart. A |
| -lookup takes the record's `location` (City databases), else |
| -`country.iso_code`, else `registered_country.iso_code` through country points. |
| -A lookup error is a miss, logged at most once a minute. Loopback, private, |
| -link-local, CGNAT and unspecified addresses are never looked up. |
| +`VERSION` stamp. |
| + |
| +- `zone.tab`, not `zone1970.tab`, because browsers report names that |
| + `zone1970.tab` has merged away. `Europe/Amsterdam` has its own row in |
| + `zone.tab` and none in `zone1970.tab`. |
| +- Links matter because Chrome reports `Asia/Calcutta` where Firefox reports |
| + `Asia/Kolkata`. |
| +- Go's `time/tzdata` carries no coordinates, so it cannot be the source. |
| + |
| +**Kept current** by `scripts/update-tzgeo.sh <version>`, which fetches the |
| +release from `data.iana.org`, checks its signature and rewrites the three |
| +files. It is a release-checklist step beside the Go toolchain bump. A stale |
| +table only misses zone names newer than it, and those fall through to IP. |
| + |
| +The lookup: |
| + |
| +1. A shape check: at most 64 bytes, one to three `/`-separated segments of |
| + `[A-Za-z0-9_+-]`. |
| +2. Unmappable by rule: `""`, `Local`, `UTC`, `UCT`, `GMT`, `Universal`, |
| + `Zulu`, `Factory` and all of `Etc/`. These name an offset or a machine, |
| + not a place (R3). |
| +3. Links, at most 4 hops. |
| +4. `zone.tab`, case-insensitive. |
| + |
| +A miss at any step is unmappable. |
| + |
| +**IP.** *(decision)* `CONSOLE_GEOIP_DB` names an operator-supplied `.mmdb`, |
| +read in `cmd/console/main.go` beside `CONSOLE_HOME_REGION` (`main.go:278`) |
| +into a new `Config.GeoIP`. |
| + |
| +- **The recommended source is DB-IP Lite (CC BY 4.0).** Its required |
| + attribution appears on the page that uses it (C24) and in the README. |
| +- MaxMind GeoLite2 also works. Its EULA allows limited sharing, on |
| + conditions that include attribution and keeping the data updated. The |
| + operator owns that compliance. |
| +- Unset means IP fallback off, the default. |
| +- Unopenable or not an mmdb: logged once, fallback off, and **the console |
| + still starts**. |
| +- The file is opened once and memory-mapped. Replacing it is an atomic |
| + rename plus a console restart. |
| +- A lookup takes the record's `location` (City databases), else |
| + `country.iso_code`, else `registered_country.iso_code`, through country |
| + points. A lookup error is a miss, logged at most once a minute. |
| +- Loopback, private, link-local, CGNAT and unspecified addresses are never |
| + looked up. |
| + |
| +The reader is `github.com/oschwald/maxminddb-golang` (ISC), pinned to a |
| +reviewed version. Test fixtures are pinned files from MaxMind's `MaxMind-DB` |
| +test data (MIT or Apache-2.0). This avoids writing a database parser. |
| **Country points.** *(decision)* From the same `zone.tab`, not a second |
| dataset. A one-row country uses its row. A multi-row country uses a shipped |
| @@ -228,8 +339,8 @@ averages a continent silently. |
| (`internal/console/authcli.go:876`), which trusts `X-Forwarded-For` only from |
| the edge. *(decision)* **Only a person's request is located by its own |
| address.** A service or app principal is a server (amadan Pages, titogo), and |
| -its address says where the server is. Those callers get the IP step only from |
| -an explicit `ip` parameter (§9.3). |
| +its address says where the server is. Those callers get the IP step only by |
| +sending an address in the guess body (§9.4). |
| ### 6.3 Nearest |
| @@ -255,18 +366,22 @@ else IP gives a point → nearest → "ip" |
| else home → "default" |
| ``` |
| -If the home has no point, the result is the home with `default` whatever the |
| -inputs. If the allow-list excludes the home (regions endpoint only) and no |
| -nearest exists, there is no guess (`"guess": null`) and pickers preselect |
| -nothing. |
| +If the home has no point, the result is the home with `default`, whatever the |
| +inputs. There is no guess (`"guess": null`) when the policy is pooled or |
| +unreadable, or when the allow-list excludes the home and no nearest exists. |
| + |
| +### 6.5 Logged, and not logged (codex 8) |
| -### 6.5 Logged, and not logged |
| +One line per app creation, `console: app region <sqid>/<app>: <code> (<source>)`. |
| +Once per process: an unusable `CONSOLE_GEOIP_DB`, a code with no point, or |
| +boxes disagreeing on a code's AWS region. |
| -One line per app creation, `console: app region <sqid>/<app>: <code> (<source>)`, |
| -and once per process an unusable `CONSOLE_GEOIP_DB`, a code with no point, or |
| -boxes disagreeing on a code's AWS region. **The timezone, the IP and any |
| -coordinate never reach a log line, an audit row, a response body or the |
| -database.** |
| +**The timezone, the IP and any coordinate never reach a URL, a log line, an |
| +audit row, a response body or the database.** They travel only in POST bodies |
| +(§9.4, §10). The canonical-host redirect copies the request's whole query |
| +string into its `Location` header and HTML (`server.go:1306`, `:1326`), so a |
| +query parameter would be echoed back. A 308 on a POST keeps the body out of |
| +both. |
| ## 7. The instance create door |
| @@ -295,27 +410,39 @@ region := s.homeRegionIn(raw) |
| It sits after `appPlacement` (`apiinstances.go:763`) and before |
| `checkReplicaCreate` (`:781`), the allow-list (`:799`) and the armed gate |
| (`:827`). **Every check an explicit region meets, a defaulted one meets too** |
| -(R2). Each condition earns its place: |
| +(R2). An explicit home name is non-empty, so it never triggers the fill, and |
| +`homeRegionIn` then turns it into `""`. That is the override that always |
| +exists once §5.6 holds (codex 3). Each condition earns its place: |
| | condition | why the default stays out | |
| | --- | --- | |
| | request names a region | an explicit choice wins (R2) | |
| | `placed != ""` | a fleet placement already governs every instance's region (`:785`) | |
| -| `m.Pool != ""` | the armed set knows nothing of pools; a defaulted region with no box serving the pool mints nowhere (§12) | |
| -| `req.Role != ""` | a replica must name its placement; filling it would turn "role replica needs a placement" (`:578-581`) into a silent acceptance | |
| -| not `DerivesPaths()` | static and addr-backed records serve everywhere or proxy to processes the platform never starts; only a socket-backed instance has a process and data that region places | |
| -| store read fails | refused, as the placement read above it is (`:764-768`); falling back to the home is the bug this spec fixes | |
| - |
| -**Refusals name their origin.** When `regionFrom == "app"` and the allow-list |
| -or armed gate fails, the message says the region came from the app and how to |
| -override it (C16, C17); "region uk is not armed" would puzzle a caller who |
| -sent no region. *(decision)* **A defaulted region that fails is refused, not |
| -quietly replaced by the home.** The app's region is a placement promise (R1), |
| -and silent fallback is how 116 instances ended up in one place (§17 Q3). |
| +| `m.Pool != ""` | a defaulted region with no box serving the pool mints nowhere (§6.1) | |
| +| `req.Role != ""` | a replica must name its placement. Filling it would turn "role replica needs a placement" (`:578-581`) into a silent acceptance. | |
| +| not `DerivesPaths()` | static and addr-backed records serve everywhere, or proxy to processes the platform never starts. Only a socket-backed instance has a process and data that region places. | |
| +| store read fails | refused, as the placement read above it is (`:764-768`). Falling back to the home is the bug this spec fixes. | |
| + |
| +**Refusals name their origin.** If `regionFrom == "app"` and the allow-list or |
| +the armed gate fails, the message says the region came from the app and how |
| +to override it (C16, C17). "region uk is not armed" would puzzle a caller who |
| +sent no region. |
| + |
| +*(decision)* **A defaulted region that fails is refused, not quietly replaced |
| +by the home.** The app's region is a placement promise (R1), and silent |
| +relocation is how 116 instances ended up in one place. The way out is always |
| +an explicit region, the home's name included (§5.6). The refusal is visible |
| +before anyone tries, in `app_default` (§9.3) and on the Instances tab (§10) |
| +(codex 7). |
| + |
| +*(decision)* **A defaulted home whose region is in its own `replicas` set is |
| +refused** (C26). A home record and its mirror in the same placement is not a |
| +replica. Explicit requests keep today's behaviour, because tightening #162's |
| +door for them is a separate change. |
| **Response.** `apiRecordView` (`apiinstances.go:535`) gains `region_from` |
| -(`request`, `app`, `fleet`, or omitted for the home default) and |
| -`region_name` (§13), outside the record like `PrivateStoreBucket`. |
| +(`request`, `app`, `fleet`, or omitted for the home default) and `region_name` |
| +(§13). Both sit outside the record, like `PrivateStoreBucket`. |
| ## 8. Every create path |
| @@ -323,21 +450,22 @@ and silent fallback is how 116 instances ended up in one place (§17 Q3). |
| | path | file:line | change | |
| | --- | --- | --- | |
| -| New-app form, GET | `handleNewAppForm`, `server.go:3586`; `app_new.html` | candidates plus an IP guess for a person; picker when two or more are armed (§10) | |
| -| New-app form, POST | `handleNewApp`, `server.go:3607`; claim at `:3626` | reads `region`, `tz`, `guess`, `guess_source`; resolves; passes to `claimApp` | |
| +| New-app form, GET | `handleNewAppForm`, `server.go:3586`; `app_new.html` | picker with an `auto` option (§10), when the switch is on and two or more regions are armed | |
| +| New-app form, POST | `handleNewApp`, `server.go:3607`; claim at `:3626` | `region` is `auto` or a code; `tz` hidden field; builds `regionReq` (§5.5) | |
| | CLI API create | `handleAPICreateApp`, `internal/console/apicli.go:330`; body `:359`; claim `:415`; response `:461` | `region`, `timezone` in; `region`, `region_name`, `region_source`, `regions_available` out | |
| -| The claim | `claimApp`, `server.go:3466`; insert `:3550` | `region, source` parameters; `CreateAppInRegion` (§5.3); `""` on adoption (§5.5) | |
| +| The claim | `claimApp`, `server.go:3466`; insert `:3550` | takes `regionReq`; resolves after adoption is known; `CreateAppInRegion` (§5.3) | |
| | `carlos apps create` | `runAppsCreate`, `apps.go:442`; call `:497`; print `:502` | `--region`; sends timezone; prints the region line (§11) | |
| | `carlos deploy` onboarding | `offerCreateApp`, `deploy.go:513`, reached from `runDeploy` `:113`; call `:536`; print `:540` | `--region`; sends timezone; prints the region line | |
| | CLI client | `apiSession.createApp`, `apimode.go:994`; `apiCreateAppResult`, `:958` | two `omitempty` fields in; four tolerant fields out | |
| | Legacy owners backfill | `BackfillOpsMemberships`, `store.go:623`, insert `:665` | none: defaults mean the home | |
| -*(decision)* An explicit region at app create is validated like the instance |
| -door validates one: `ValidRegion`, then armed and hosting unless the gate is |
| -dormant, with the home's name accepted through `homeRegionIn`. Refusals list |
| -what would pass (C18, C19). There is no allow-list yet (no manifest yet). A |
| -fleet `place` and a `region` may both be sent and both are stored; the fleet |
| -governs while placed, the region if the placement is ever cleared. |
| +*(decision)* An explicit region at app create is validated the way the |
| +instance door validates one: `ValidRegion`, then armed and hosting unless the |
| +gate is dormant, with the home's name accepted through `homeRegionIn`. |
| +Refusals list what would pass (C18, C19). There is no allow-list yet, because |
| +there is no manifest yet. A fleet `place` and a `region` may both be sent, and |
| +both are stored. The fleet governs while the app is placed, and the region |
| +applies if the placement is ever cleared. |
| ### 8.2 Where an instance is created |
| @@ -345,21 +473,26 @@ governs while placed, the region if the placement is ever cleared. |
| | --- | --- | --- | --- | |
| | The door | `handleAPIInstanceCreate`, `apiinstances.go:608`, region `:772` | `""` means home | the fill (§7) | |
| | `carlos instances create` | `instances_cli.go:322` | `--region` or `""` | help text; prints `region_name` (§11) | |
| -| `carlos canary` | `canary.go:407` | `""` | the door fills the app's region; new `--region` | |
| +| `carlos canary` | `canary.go:407` | `""` | the door fills the app's region; new `--region`, with the existing-canary rule (§11) | |
| | `carlos deploy` static host | `mintPlatformHost`, `deploy.go:858` | static, `""` | none: static is not filled | |
| | First-route domain attach | `internal/console/domainattach.go:178` | static, no region | none | |
| -| External callers (titogo, jam) | `POST …/instances`, `internal/console/authcli.go:613` | usually `""` | follow the app's region; can read the guess (§9.3) | |
| -| Sidecar create | `handleAPISidecarCreate`, `internal/console/apisidecars.go:257` | `""` or explicit | **none in this spec** (§17 Q10) | |
| +| External callers (titogo, jam) | `POST …/instances`, `internal/console/authcli.go:613` | usually `""` | follow the app's region; can ask for a guess (§9.4) | |
| +| Sidecar create | `handleAPISidecarCreate`, `internal/console/apisidecars.go:257` | `""` or explicit | none (§12) | |
| ## 9. API changes |
| -All additive: new request fields optional, new response fields ignorable. |
| -`readJSON` is not strict (`authcli.go:932-947`), so an old console ignores new |
| -request fields instead of refusing them. |
| +All of it is additive: new request fields are optional and new response |
| +fields are ignorable. `readJSON` is not strict (`authcli.go:932-947`), so an |
| +old console ignores the new request fields instead of refusing them. |
| + |
| +### 9.1 `POST /api/cli/apps/{account}/new` |
| + |
| +The request gains two fields: |
| -**9.1 `POST /api/cli/apps/{account}/new`.** Request gains `region` (a code or |
| -the home's name) and `timezone` (a hint, used only without `region`, never |
| -stored). Response gains: |
| +- `region`: an explicit choice, either a code or the home's name. |
| +- `timezone`: a hint, used only without `region`, and never stored. |
| + |
| +The response gains these, echoing what was **stored**: |
| ```json |
| {"region": "uk", "region_name": "UK · London", "region_source": "timezone", |
| @@ -367,104 +500,157 @@ stored). Response gains: |
| ``` |
| `region` is the stored value under its name (`homeRegionOut`). |
| -`regions_available` counts armed candidates; the CLI stays silent below two. |
| +`regions_available` counts armed candidates, and the CLI stays silent below |
| +two. With the switch off (§5.6), an explicit non-home `region` is answered |
| +with the home and `default`. The CLI compares the two and says so (C13b). |
| -**9.2 `POST /api/cli/apps/{account}/{app}/instances`.** Request unchanged; |
| -behaviour per §7; response gains `region_from` and `region_name`. |
| +### 9.2 `POST /api/cli/apps/{account}/{app}/instances` |
| -**9.3 `GET /api/cli/apps/{account}/{app}/regions`.** Gate and role unchanged |
| -(`authcli.go:586`). New optional query parameters `tz` and `ip` replace the |
| -request's own inputs, which is how titogo asks on behalf of its visitor |
| -rather than its server. |
| +The request is unchanged, and the behaviour follows §7. The response gains |
| +`region_from` and `region_name`. |
| -```json |
| -{"regions": [ |
| - {"code": "ie", "armed": true, "aws_region": "eu-west-1", "name": "Ireland · Dublin"}, |
| - {"code": "uk", "armed": true, "aws_region": "eu-west-2", "name": "UK · London"}], |
| - "default": "uk", |
| - "app_region": "uk", "app_region_source": "timezone", |
| - "guess": {"code": "ie", "name": "Ireland · Dublin", "source": "ip"}} |
| -``` |
| +### 9.3 `GET /api/cli/apps/{account}/{app}/regions` |
| -**(decision) `default` keeps its meaning, "where a create naming no region |
| -lands", and is now computed.** For every existing app, and for fleet-placed |
| -and pooled apps, that is still `HomeRegion`, today's value. `guess` is per |
| -caller, `default` per app: a per-instance picker (titogo) preselects `guess`; |
| -an app-level display shows `default`. `guess` honours the allow-list (§6.1). |
| -`name` falls back to the bare code (§13). |
| +The gate and role are unchanged (`authcli.go:586`), and so are the legacy |
| +fields (codex 6). `regions` and `default` keep today's meaning and values. |
| +`default` is still `HomeRegion`, and the list is still the observed topology, |
| +unfiltered. Each entry gains `aws_region` and `name`, and the response gains |
| +one block: |
| -**9.4 Browser: `GET /a/{sqid}/apps/new/region?tz=…`.** Session-gated through |
| -`s.account` like the form's GET; returns `{"code", "name", "source"}` for the |
| -new-app page's script; writes nothing. `new` is a reserved app name |
| -(`server.go:1150`), so it cannot collide with an app route. |
| +```json |
| +"app_default": {"code": "uk", "name": "UK · London", "source": "timezone", |
| + "available": false, "reason": "absent"} |
| +``` |
| -**9.5 Compatibility.** |
| +`app_default` is where a create that names no region lands for **this** app, |
| +and whether it can land there now. That makes it the answer to "why are my |
| +creates refused" (codex 7). |
| + |
| +- `code` is the app's region under its name, or the home for an app with |
| + no region of its own, or for a pooled or fleet-placed one. |
| +- `reason` is one of: |
| + - `absent`: no box publishes the code. The code is reported even though |
| + `regions` does not list it. |
| + - `not-hosting`: the region is reachable but cannot host. |
| + - `not-allowed`: the manifest's allow-list excludes it. |
| + - `pooled`: the app's manifest names a pool, so the default is the home |
| + (§7). |
| + - `fleet`: a fleet placement governs. |
| + - `policy-unreadable`: the manifest read failed, and `available` is |
| + omitted. |
| +- An absent manifest is not an error. It reads as no pool and no allow-list, |
| + so this endpoint stays as available as it is today. |
| +- No query parameters are added (codex 8). |
| + |
| +### 9.4 `POST /api/cli/apps/{account}/{app}/regions/guess` |
| + |
| +This is a new route, under the same gate and role as §9.3. It is how titogo |
| +asks on behalf of its own visitor (codex 8). The request body is |
| +`{"tz": "...", "ip": "..."}`, and both fields are optional. A person's request |
| +with no `ip` uses its own address, and a service or app principal's never |
| +does (§6.2). |
| + |
| +The answer is `{"guess": {"code", "name", "source"}}`, or `{"guess": null}` |
| +when the policy is pooled or nothing qualifies. It is a 502 with C27 when the |
| +manifest cannot be read. The guess is per caller and writes nothing. A |
| +per-instance picker preselects it and sends it back as an explicit region, |
| +which the door checks like any other. |
| + |
| +### 9.5 Browser: `POST /a/{sqid}/apps/new/region` |
| + |
| +This route takes a form body with `csrf` and `tz`, is session-gated through |
| +`s.account`, and returns `{"code", "name", "source"}` for the new-app page's |
| +script (§10). It writes nothing. `new` is a reserved app name |
| +(`server.go:1150`), so the route cannot collide with an app's. |
| + |
| +### 9.6 Compatibility and mixed versions |
| | client | console | result | |
| | --- | --- | --- | |
| | old CLI | new | no `timezone`: IP for a person, then home. Never an error. Extra fields ignored. | |
| -| new CLI | old | new fields ignored; no `region` in the response; CLI prints nothing, or C13 if `--region` was given | |
| -| titogo, unchanged | new | creates follow the app's region, which for every existing app is the home | |
| -| new CLI | new | R4 | |
| +| new CLI | old | new fields ignored, and the response has no `region`. The CLI prints nothing, or C13a if `--region` was given. | |
| +| new CLI | new, switch off | the app is created at the home. With `--region`, C13b. | |
| +| titogo, unchanged | new | creates follow the app's region, which for every existing app is the home. `default` is unchanged. | |
| +| any | rolled back below the floor | the §5.7 rule: no-region creates land at the home and are logged on roll-forward | |
| ## 10. Console UI |
| -**New-app page** (`app_new.html`): |
| - |
| -- A `<select name="region">` under the name, listing armed candidates by place |
| - name (C1, C2, C23), preselected with the server's guess. A server render |
| - cannot see the browser's timezone, so that is IP, then home. The guessed |
| - option carries C3. Hidden inputs: `tz`, `guess`, `guess_source`. |
| -- An inline script, progressive enhancement like the account switcher's |
| - (`templates/layout.html:583`), fills `tz` from |
| - `Intl.DateTimeFormat().resolvedOptions().timeZone` and, **if the select is |
| - untouched**, fetches §9.4 and moves the selection and the hidden guess |
| - fields to the answer. With JavaScript off, the IP-or-home preselection |
| - stands. |
| -- Fewer than two armed candidates, or a failed candidate read: no picker, and |
| - the form is byte-identical to today's. POST then resolves server-side. |
| - |
| -**POST** (`handleNewApp`): a `region` in the candidate set is used. Its source |
| -is `guess_source` when `region == guess` and the word is `timezone`, `ip` or |
| -`default`, else `chosen`. The word is a label, so forging it changes a log |
| -line and nothing else. A region outside the armed set re-renders with C5. No |
| -`region` field (one region, or a form cached from before the roll) means the |
| -server resolves from `tz`, then IP, then home. The success flash |
| -(`server.go:3644`) is existing copy and untouched. The audit detail (`:3643`) |
| -gains the place (C21). |
| - |
| -**Instances tab** (Placement section, `app_instances.html:60-78`): when the |
| -app is not fleet-placed and two or more regions are armed, one line says where |
| -new instances start (C7) or that they cannot start there now (C8). |
| - |
| -**There is no browser instance-create form.** The brief assumed one. The |
| -app's region reaches instance creation through the door's default (§7), and |
| -CLI and external pickers read it from §9.3. Building a form is §17 Q5. |
| +### New-app page (`app_new.html`), shown when the switch is on and two or more regions are armed |
| + |
| +- A `<select name="region">` sits under the name. Its first option has value |
| + `auto` and the label C3, naming the server's guess. Server-side, that guess |
| + is IP then home, since a render cannot see the browser's timezone. Every |
| + armed region follows as an option with its code as the value and its place |
| + name as the label (C1, C2, C23). A hidden `tz` field rides along. |
| +- **`auto` is not a region** (codex 2). Selecting a code is the person's |
| + choice. Leaving `auto` asks the server to guess, and on an adoption the |
| + guess is discarded (§5.5). No hidden guess or source field is trusted. |
| +- An inline script, progressive enhancement in the style of the account |
| + switcher (`templates/layout.html:583`), fills `tz` from |
| + `Intl.DateTimeFormat().resolvedOptions().timeZone` and POSTs it to §9.5. It |
| + only relabels the `auto` option with the answer. The value stays `auto`. |
| + With JavaScript off, the IP-or-home label stands. |
| +- With the switch off, fewer than two armed regions, or a failed candidate |
| + read, there is no picker, and the form is byte-identical to today's. |
| +- DB-IP attribution (C24) is shown under the picker when the IP step is |
| + configured. |
| + |
| +### POST (`handleNewApp`) |
| + |
| +- A code in the armed set is `explicit`. |
| +- `auto`, or no `region` field (one region, or a form cached from before the |
| + roll), means the server resolves from `tz`, then IP, then home. On an |
| + adoption it resolves to `""`. |
| +- A code outside the armed set re-renders with C5. |
| +- The success flash (`server.go:3644`) is existing copy and is untouched. |
| +- The audit detail (`:3643`) gains the place (C21). |
| + |
| +### Instances tab (Placement section, `app_instances.html:60-78`), from `app_default` (§9.3) |
| + |
| +- C8 shows **whenever the default cannot accept a create**, however many |
| + regions are armed, and names the reason (codex 7). |
| +- Otherwise C7 shows when the app has a non-home region, or when two or more |
| + regions are armed. |
| +- Pooled and fleet-placed apps show neither line. |
| ## 11. CLI |
| -Timezone detection is a new helper in the root package, with injected |
| -`getenv`/`readlink`/`readFile`, using only portable calls so the `20-cross` |
| -gate builds it for darwin, windows and linux-arm64 with no build tags: |
| -`$TZ` (leading `:` stripped; a path keeps what follows `zoneinfo/`), else |
| -`readlink /etc/localtime` after `zoneinfo/` (Linux, and macOS's |
| -`/var/db/timezone/zoneinfo/…`), else the first line of `/etc/timezone`, else |
| -nothing. Windows gets nothing and the console falls back to IP (§17 Q15). |
| +### Timezone detection |
| + |
| +The helper lives in the root package. Its `getenv`, `readlink` and `readFile` |
| +are injected, and it uses only portable calls, so the `20-cross` gate builds |
| +it for darwin, windows and linux-arm64 with no build tags. It tries, in order: |
| + |
| +1. `$TZ`, with a leading `:` stripped. A path keeps what follows `zoneinfo/`. |
| +2. `readlink /etc/localtime`, keeping what follows `zoneinfo/`. This covers |
| + Linux and macOS's `/var/db/timezone/zoneinfo/…`. |
| +3. The first line of `/etc/timezone`. |
| +4. Nothing. |
| + |
| +On Windows only a valid `$TZ` is sent. Otherwise the console falls back to IP, |
| +then the home, and `--region` is always available. This is accepted for this |
| +release, since Windows zone names would need a CLDR mapping. |
| + |
| +### Flags |
| | command | flag | meaning | |
| | --- | --- | --- | |
| -| `carlos apps create` | `--region <code>` | the app's region; default resolved by the console | |
| -| `carlos deploy` | `--region <code>` | the region of the app this deploy creates. On an existing app: silent if equal to its region, **refused before anything ships** if different (C15), since the person expected a placement that will not happen. Skipped against an old console (no `app_region` in §9.3). | |
| -| `carlos canary` | `--region <code>` | the canary's region; default the app's | |
| -| `carlos instances create` | `--region <code>` | unchanged; help says the default is now the app's region | |
| - |
| -Output: after "created app …" (`apps.go:502`, `deploy.go:540`), one line by |
| -source, C9 to C12, only when `regions_available >= 2`. `carlos instances |
| -create` and `carlos canary` add the place to their existing line when the |
| -response carries `region_name` (C14). No prompts (R4). `docs/site/cli.md` |
| -gains the flags under `### carlos apps`, `### carlos deploy` and |
| -`### carlos canary`, and the new default under `### carlos instances`; no new |
| -command, so the docsite gate is unaffected. |
| +| `carlos apps create` | `--region <code>` | the app's region. The default is resolved by the console. | |
| +| `carlos deploy` | `--region <code>` | the region of the app this deploy creates. On an existing app it reads `app_default.code` (§9.3): silent if equal, and **refused before anything ships** if different (C15). Skipped against a console with no `app_default`. | |
| +| `carlos canary` | `--region <code>` | the canary's region; the default is the app's. If the canary host already exists, the record's region is compared with the flag. A record region of `""` reads as the `regions` response's home. Equal proceeds. **Different is refused before any record write, rechannel or upload** (C25) (codex 5). Moving a canary is a move, not this verb. | |
| +| `carlos instances create` | `--region <code>` | unchanged; the help says the default is now the app's region | |
| + |
| +### Output and docs |
| + |
| +- After "created app …" (`apps.go:502`, `deploy.go:540`), one line by source, |
| + C9 to C12, only when `regions_available >= 2`. |
| +- `carlos instances create` and `carlos canary` add the place to their |
| + existing line when the response carries `region_name` (C14). |
| +- No prompts (R4). |
| +- `docs/site/cli.md` gains the flags under `### carlos apps`, |
| + `### carlos deploy` and `### carlos canary`, and the new default under |
| + `### carlos instances`. There is no new command, so the docsite gate is |
| + unaffected. |
| ## 12. Interactions |
| @@ -472,45 +658,55 @@ command, so the docsite gate is unaffected. |
| overrides the request's region today (`apiinstances.go:785-797`). The fill |
| runs only when `placed == ""`, so the precedence is explicit: **fleet |
| placement, then the request's region, then the app's region, then the |
| - home.** A placed app's region is stored and dormant until the placement is |
| - cleared. |
| -- **Pools** (`Manifest.Pool`). Boxes are chosen by `poolServed` |
| - (`internal/provision/reconcile.go:1339`) and the armed set does not know which |
| - regions serve which pool, so the default is skipped for pooled manifests |
| - (§7). An explicit `--region` behaves as today. |
| + home.** A placed app's region is stored and stays dormant until the |
| + placement is cleared. |
| +- **Pools** (`Manifest.Pool`). There is no default fill (§7) and no guess |
| + (§6.1), and `app_default` reports `pooled`. An explicit `--region` behaves |
| + as it does today, and the hazard it carries today is named and not fixed |
| + here: a region with no box serving the pool mints nothing. A test pins that |
| + the guess cannot lead a pooled app there (codex 1). |
| - **Replicas (#162).** A `role: replica` create is never filled, so its |
| - refusal stands. A home record with a `replicas` set is filled like any |
| - other. Nothing stops a home's region equalling one of its own replica |
| - placements, and defaulting makes that likelier (§17 Q12). |
| + refusal stands. A defaulted home inside its own `replicas` set is refused |
| + (§7). |
| - **Moves (#175/#331).** A move rewrites one record's `region` at its cut. |
| **It never touches the app's region.** An app whose instances all moved to |
| - `au` still starts new ones in its own region: where this instance is, and |
| - where the next one starts, are different questions. |
| -- **Static.** Not filled; everywhere-class (`reconcile.go:228`). |
| -- **Private storage (#412).** Unaffected; stores are keyed on |
| + `au` still starts new ones in its own region. |
| +- **Static.** Not filled, because static is everywhere-class |
| + (`reconcile.go:228`). |
| +- **Private storage (#412).** Unaffected, since stores are keyed on |
| `(account, app, environment)`. |
| -- **Sidecars.** Unchanged (§17 Q10). |
| +- **Sidecars.** Unchanged. `handleAPISidecarCreate` has no armed gate, so |
| + filling a region there could strand a sidecar where nothing checks. Following |
| + the app's region needs its own design. |
| +- **Allow-lists and the home's name.** `instanceRegionAllowed` compares the |
| + normalized `""`, so a manifest listing the home's code refuses creates at |
| + the home today. This change keeps that normalization and reinterprets no |
| + existing manifest. The README documents that the named home maps to `""`, |
| + and rollout audits production manifests for `regions` first (§16). |
| ## 13. Place names |
| -**(decision) One table, `internal/geo/awsregions.go`: AWS region code to |
| -`{latitude, longitude, place}`**, covering the AWS commercial regions as of |
| -2026-10-01 (C23). Coordinates are the metro area's: AWS publishes no facility |
| -locations, and tens of kilometres never change a choice between regions. A |
| -region missing from the table shows its bare code and is left out of the |
| -guess, with a log line. Adding a row when AWS opens a region is the whole |
| -maintenance cost. **Used everywhere**: the picker, the Instances tab, every |
| -`name` in §9, and the CLI lines, which print the console's `region_name` and |
| -need no table of their own. |
| +**(decision) One table, `internal/geo/awsregions.go`, maps an AWS region code |
| +to `{latitude, longitude, place}`.** |
| + |
| +- It covers the AWS commercial regions on AWS's official list as of |
| + 2026-10-01 (C23). The table records that source and date in a comment. |
| +- Coordinates are the metro area's, each checked by hand when the table is |
| + built. AWS publishes no facility locations, and tens of kilometres never |
| + change a choice between regions. |
| +- A region missing from the table shows its bare code and is left out of the |
| + guess, with a log line. |
| +- Adding a row when AWS opens a region is the whole maintenance cost. |
| +- **Used everywhere**: the picker, the Instances tab, every `name` in §9, and |
| + the CLI lines. The CLI prints the console's `region_name` and needs no |
| + table of its own. |
| **`STATUS_BOX_REGION` stays, with a different job.** It is the public status |
| page's commitment: operator free text, deliberately coarse, "never derived |
| from cloud metadata" (`agent.go:88-92`, README `:578`). This spec neither |
| -reads nor derives it. The table names every armed region consistently, |
| -including boxes whose operator set nothing, which free text cannot. The two |
| -should not contradict each other about one box, so the README's |
| -`STATUS_BOX_REGION` entry gains a sentence recommending the table's name. |
| -Whether the flagship's values change is §17 Q8. |
| +reads it nor derives it. It may stay coarser than the picker ("EU · Ireland" |
| +beside "Ireland · Dublin"), but it must agree geographically. The README's |
| +`STATUS_BOX_REGION` entry says so. |
| ## 14. Failure modes |
| @@ -520,160 +716,209 @@ Whether the flagship's values change is §17 Q8. |
| | mmdb missing, unreadable or corrupt at startup | logged once; IP step off; console starts | |
| | mmdb lookup error | a miss; rate-limited log | |
| | timezone UTC, `Etc/*`, unknown or malformed | IP step, then home | |
| -| edges files unreadable | home only; picker hidden; app gets `""`/`default` | |
| -| home publishes no AWS region | every guess `default` | |
| +| edges files unreadable | home only; picker hidden; app gets `""`/`default`; `app_default` of a non-home app reads `absent` | |
| +| home publishes no AWS region | every guess `default`; fix with the box's `CARLOS_AWS_REGION` | |
| | an armed region's AWS region not in the table | out of the guess; still selectable; shown by code | |
| -| app's region later unarmed | defaulted creates refused (C16); explicit `--region` works; §9.3 shows `armed: false` | |
| -| app's region excluded by a later allow-list | refused (C17) | |
| +| app's region later unarmed or absent | defaulted creates refused (C16); explicit regions, the home's name included, still work; `app_default` and C8 say why | |
| +| app's region excluded by a later allow-list | refused (C17); `app_default.reason` `not-allowed` | |
| | app-region read fails at the door | 502 (C20); no record written | |
| +| manifest unreadable | guess endpoint 502 (C27); `/regions` still answers with `policy-unreadable` | |
| +| switch on, home unnamed | switch treated as off; logged once | |
| +| console binary rolled back below the floor | no-region creates land at the home; logged per app on roll-forward (§5.7) | |
| | CLI on a VPN or remote shell | timezone usually still right; the line names the region and `--region` | |
| | service principal creates an app | no IP step; `default` unless it sends `timezone` or `region` | |
| -| old console, new CLI with `--region` | C13; app at the home | |
| -| two consoles disagree | cannot: candidates sort, ties are deterministic, data is embedded | |
| +| owner adopts an existing app | `""`/`default` unless `region` is explicit | |
| ## 15. Test plan |
| -**`internal/geo`.** Haversine on known pairs (Dublin to London about 464 km, |
| -within 5). Nearest: one candidate; a clear winner; an exact tie goes to the |
| -home; a non-home tie goes to the smaller code; two codes in one AWS region; an |
| -unarmed candidate nearer than every armed one is never picked; a pointless |
| -candidate is excluded; a pointless home gives `default`; none gives |
| -`default`. Timezones: `Europe/London`; `Asia/Calcutta` and `Asia/Kolkata` |
| -give one point; `Europe/Amsterdam` (in `zone.tab`, not `zone1970.tab`); |
| -`America/Argentina/Buenos_Aires`; case-insensitivity; and `UTC`, `Etc/UTC`, |
| -`Etc/GMT+5`, `GMT`, `Factory`, `Local`, `""`, 65 bytes, `../etc/passwd` and a |
| -link loop are all unmappable. Country points: every multi-row country has an |
| -override (the test a tzdata refresh trips), and every override names a real |
| -zone. mmdb: unset, missing path, truncated file, non-mmdb file; a City record |
| -gives its location, a Country-only record its country point; private, |
| -loopback and CGNAT addresses never reach the reader (a counting fake proves |
| -it). |
| - |
| -**Store.** Fresh database; one from before the columns; one from before |
| -`migrateAppsKey` carrying the new ALTERs (the columns survive the rebuild); |
| -`migrate` run twice. Pre-existing rows read `""`/`""`. `CreateAppInRegion` |
| -writes both columns in one statement; `CreateAppIn` and `CreateApp` write |
| -`""`/`""`; rename and transfer keep the region. |
| - |
| -**The door** (`apiinstances_test.go`). No region with app region `uk`, armed: |
| -record `uk`, `region_from` `app`. App region `""`: record byte-identical to |
| -the pre-change golden. Explicit wins over app; fleet wins over both. Pooled, |
| -static, addr-backed and replica creates are not filled, and a replica with no |
| -region gets the existing refusal word for word. Unarmed app region gives C16, |
| -allow-list exclusion C17, store error 502. A dormant gate accepts app region |
| -`uk`, as it accepts an explicit `uk` today. |
| - |
| -**App create.** API: an old body from a person resolves by IP, then home; a |
| -`timezone` body; explicit valid, invalid (C18) and unarmed (C19) regions; |
| -`place` plus `region`; adoption stores `""`; a service principal gets no IP |
| -step; response fields echo what was stored. Browser: one region renders |
| -today's form byte for byte; the IP guess is preselected; §9.4 needs a session |
| -and is 404 to a non-member; `region == guess` keeps the source; a different |
| -region or a forged `guess_source` records `chosen`; an unarmed region |
| -re-renders with C5; CSRF unchanged. |
| - |
| -**Regions endpoint.** For an existing app, `regions` and `default` match |
| -today's golden. `name` and `aws_region` present; `tz` and `ip` honoured; |
| -`guess` honours the allow-list, and is `null` when it excludes the home and |
| -nothing has a point. |
| - |
| -**CLI.** Detection with injected functions: `TZ=":Europe/London"`, a `TZ` |
| -path, a plain `TZ`, the `/etc/localtime` link on Linux and macOS, |
| -`/etc/timezone`, nothing. One line per source; silence below two regions; |
| -silence or C13 against an old console. `deploy --region` on an existing app: |
| -different is refused before anything ships (prove nothing uploaded), equal is |
| -silent. Shell-out tests use `carlosTestEnv` and a scratch `cmd.Dir` |
| -(AGENTS.md). `20-cross` builds the detector everywhere. |
| +### `internal/geo` |
| + |
| +- Haversine on known pairs: Dublin to London is about 464 km, within 5. |
| +- Nearest: |
| + - one candidate, and a clear winner; |
| + - an exact tie goes to the home, and a non-home tie to the smaller code; |
| + - two codes in one AWS region; |
| + - an unarmed candidate nearer than every armed one is never picked; |
| + - a candidate with no point is excluded, a home with no point gives |
| + `default`, and no candidates gives `default`. |
| +- Timezones: |
| + - `Europe/London` maps, and `Asia/Calcutta` and `Asia/Kolkata` give one |
| + point; |
| + - `Europe/Amsterdam` maps (it is in `zone.tab`, not `zone1970.tab`); |
| + - `America/Argentina/Buenos_Aires` maps, and matching is case-insensitive; |
| + - `UTC`, `Etc/UTC`, `Etc/GMT+5`, `GMT`, `Factory`, `Local`, `""`, a 65-byte |
| + name, `../etc/passwd` and a link loop are all unmappable. |
| +- Country points: every multi-row country has an override (this is the test a |
| + tzdata refresh trips), and every override names a real zone. |
| +- mmdb, using pinned `MaxMind-DB` fixtures: |
| + - unset, a missing path, a truncated file and a non-mmdb file; |
| + - a City record gives its location, and a Country-only record its country |
| + point; |
| + - private, loopback and CGNAT addresses never reach the reader, as a |
| + counting fake proves. |
| + |
| +### Store |
| + |
| +- Four database states: fresh; from before the columns; from before |
| + `migrateAppsKey` but carrying the new ALTERs (the columns must survive the |
| + rebuild); and `migrate` run twice. |
| +- Pre-existing rows read `""`/`""`. |
| +- `CreateAppInRegion` writes both columns in one statement, and `CreateAppIn` |
| + and `CreateApp` write `""`/`""`. |
| +- Rename and transfer keep the region. |
| + |
| +### The door (`apiinstances_test.go`) |
| + |
| +- No region with app region `uk`, armed: the record says `uk`, and |
| + `region_from` says `app`. |
| +- App region `""`: the record is byte-identical to the pre-change golden. |
| +- An explicit region wins over the app's, and a fleet wins over both. |
| +- An explicit home name with app region `uk` gives `""` (codex 3). |
| +- Pooled, static, addr-backed and replica creates are not filled. A replica |
| + with no region gets the existing refusal word for word. |
| +- An unarmed app region gives C16, an allow-list exclusion C17, a store error |
| + 502, and a defaulted home inside its own `replicas` C26. |
| +- A dormant gate accepts app region `uk`, as it accepts an explicit `uk` |
| + today. |
| +- **Switch off with an existing `uk` row: the fill still applies** (codex 4). |
| + |
| +### Pools, end to end (codex 1) |
| + |
| +- A pooled manifest gives `guess: null` and `app_default.reason: pooled`. |
| +- A record for the pool in a region whose boxes do not serve that pool is out |
| + of `inScope` on those boxes. This pins the hazard the guess now cannot |
| + reach. |
| + |
| +### App create |
| + |
| +- An old body from a person resolves by IP, then home. |
| +- A `timezone` body. |
| +- Explicit regions: valid, invalid (C18) and unarmed (C19). |
| +- `place` plus `region`. |
| +- Response fields echo what was stored. |
| +- The switch off, or on with an unnamed home: the app is stored at home. |
| +- A service principal gets no IP step. |
| +- **Adoption (codex 2):** |
| + - with `auto`, or with a `timezone` and no `region`, it stores `""`; |
| + - with an explicit code, it stores that code; |
| + - a browser POST that never touched the select stores `""` on adoption. |
| + |
| +### Browser |
| + |
| +- The switch off, or a single region, renders today's form byte for byte. |
| +- The `auto` option is labelled with the IP guess. |
| +- §9.5 needs a session and CSRF, and is 404 to a non-member. |
| +- A code records `chosen`, and an unarmed code re-renders with C5. |
| + |
| +### Endpoints |
| + |
| +- `/regions` for an existing app matches today's golden in `regions` and |
| + `default`. |
| +- `app_default` covers each `reason`, including `absent` for a code no box |
| + publishes. |
| +- An absent manifest still answers. An unreadable manifest gives |
| + `policy-unreadable`, and a guess 502. |
| + |
| +### Privacy (codex 8) |
| + |
| +With a canonical-host redirect configured: |
| + |
| +- a guess POST with `tz` and `ip` leaves neither in the `Location` header, |
| + the redirect body, any error body, the log output captured during the |
| + test, or any audit row; |
| +- §9.3 accepts no hint parameters. |
| + |
| +### CLI |
| + |
| +- Detection, with injected functions: `TZ=":Europe/London"`, a `TZ` path, a |
| + plain `TZ`, the `/etc/localtime` link on Linux and macOS, `/etc/timezone`, |
| + and nothing. |
| +- One output line per source, and silence below two regions. |
| +- C13a against an old console, and C13b against a console with the switch |
| + off. |
| +- `deploy --region` on an existing app: different is refused before anything |
| + ships (prove nothing was uploaded), and equal is silent. |
| +- `canary --region` against an existing canary in another region is refused |
| + before any record write or upload; in the same region it proceeds |
| + (codex 5). |
| +- Shell-out tests use `carlosTestEnv` and a scratch `cmd.Dir` (AGENTS.md), and |
| + `20-cross` builds the detector on every target. |
| ## 16. Rollout |
| -1. **Console first.** One release carries the migration, `internal/geo`, the |
| - fill, the endpoints and the UI. Old CLIs and titogo see no change, because |
| - every existing app reads `""`. Until the new CLI ships, apps created from |
| - the CLI resolve by IP when a database is configured and otherwise land at |
| - the home, as today. No box roll, no edges-file change. |
| -2. **Verify on dev** with two armed regions: create an app in the browser and |
| - read its row; create an instance with no region and read its record; diff |
| - `GET …/regions` for an old app against the pre-roll answer. |
| -3. **Flagship console**, through the usual console roll. Optionally place the |
| - mmdb and set `CONSOLE_GEOIP_DB` in the deployment repo (§17 Q6). |
| -4. **CLI release.** Safe in either order (§9.5); the console must go first for |
| - the lines to appear. |
| -5. **titogo**, in its own repo, adopts `guess` and the names from §9.3. |
| - |
| -Copy review (§18) happens before step 1 ships any string. |
| - |
| -## 17. Open questions |
| - |
| -1. **This does not unload Ireland by itself.** Existing apps keep the home (by |
| - ruling). If the 116 instances belong to a few long-lived apps that create |
| - instances through the API (titogo, Managed Jelly), their next instances |
| - still land in Ireland. Relief needs new apps, titogo sending `guess` per |
| - instance, or moves. Is titogo's adoption part of "done"? |
| -2. **Latency depends on DNS.** Non-static instance hosts are not steered by |
| - default, and the per-instance DNS seam is recorded, not built |
| +1. **Before any roll:** |
| + - audit production manifests for `regions` (§12); |
| + - confirm each box publishes the AWS region it is in, especially that the |
| + flagship's Ireland box publishes `region: eu-west-1` with no |
| + `placement`; use the box's existing `CARLOS_AWS_REGION` override where |
| + it does not; |
| + - set `CONSOLE_HOME_REGION` on every console that will turn the switch on. |
| +2. **Console release, switch off.** It carries the migration, `internal/geo`, |
| + the fill, the endpoints and the UI. Behaviour is unchanged, and this |
| + release can be rolled back freely (§5.7). No box roll and no edges-file |
| + change. |
| +3. **Verify on dev.** Turn the switch on, with two armed regions. Create an |
| + app in the browser and read its row. Create an instance with no region and |
| + read its record. Diff `/regions` for an old app against the answer from |
| + before the roll. |
| +4. **Flagship.** Roll the console, then turn the switch on. From here the |
| + binary rollback floor applies, and the README names it. Optionally place a |
| + DB-IP Lite file and set `CONSOLE_GEOIP_DB`. |
| +5. **CLI release.** It is safe in either order (§9.6), but the lines appear |
| + only against a console with the switch on. |
| +6. **titogo**, in its own repo, adopts `/regions/guess` and the names (§17 Q1). |
| + |
| +Copy review (§18) happens before step 2 ships any string. |
| + |
| +## 17. Questions for Paul |
| + |
| +1. **Is titogo's adoption part of "done"?** Existing apps keep the home, by |
| + ruling. If the 116 Ireland instances belong to a few long-lived apps that |
| + create instances through the API (titogo, Managed Jelly), the platform |
| + change alone relieves Ireland only for new apps. |
| + - Option (a): done when the platform ships. |
| + - Option (b): done when titogo sends `guess` per instance. |
| + - **Recommendation: (b)**, since relieving Ireland is the point. |
| +2. **Is latency verified or assumed?** Non-static instance hosts are not |
| + steered, and the per-instance DNS seam is recorded, not built |
| (`apiinstances.go:1052-1057`). If the apps-domain wildcard resolves to the |
| home's edge, a UK visitor to a UK-placed instance goes through Ireland and |
| - back: placement improves, latency may get worse. Is |
| - `CARLOS_STEERING_ZONE_ID` set on the flagship? Should a non-home instance |
| - get `Steering: latency` by default? |
| -3. **Refuse or fall back when the app's region goes dark?** §7 refuses, so a |
| - signup path fails until someone passes a region. The alternative places the |
| - instance at the home and says so in `region_from`. |
| -4. **Can an app's region change?** D1 says set once, so a wrong guess (a VPN, |
| - a laptop abroad) means `--region` on every instance. A later owner-only |
| - `carlos apps region` would fix that. |
| -5. **A browser instance-create form.** Build one, or leave instance creation to |
| - the CLI and the API? |
| -6. **Which GeoIP database, under which licence?** GeoLite2 needs a MaxMind |
| - account and forbids redistribution. DB-IP Lite is CC BY 4.0 and requires |
| - attribution where used, which would be new copy on the new-app page. |
| -7. **A new Go dependency.** `github.com/oschwald/maxminddb-golang` (ISC), or a |
| - few hundred lines in `internal/geo`. Test fixtures: MaxMind's test-data |
| - repository (licence to check) or a test-only writer. |
| -8. **Place-name style.** The draft says "UK · London", "Ireland · Dublin"; the |
| - status page's example is "EU · Ireland". Do the flagship's |
| - `STATUS_BOX_REGION` values (in carloku-infrastructure) change to match? |
| -9. **The home's coordinates.** §6.1 derives them from boxes with no placement |
| - code. Confirm the flagship's Ireland box publishes `region: eu-west-1` and |
| - no `placement`. Is a `CONSOLE_HOME_AWS_REGION` override wanted? |
| -10. **Sidecars.** `handleAPISidecarCreate` has no armed gate, so filling a |
| - region there could strand a sidecar where nothing checks. Should sidecars |
| - follow the app's region, with a gate added? |
| -11. **Pools.** Skipping pooled apps is safe, but they never benefit. Should |
| - the armed set learn pools? |
| -12. **A home in one of its own replica placements.** No rail refuses it |
| - (`checkReplicaCreate`, `apiinstances.go:570`). Legal? |
| -13. **Allow-lists and the home's name.** `instanceRegionAllowed` compares the |
| - normalized `""`, so a manifest listing `ie` refuses creates at an `ie` |
| - home. Does any production manifest carry `regions`? |
| -14. **Unnamed home, several regions.** Without `CONSOLE_HOME_REGION` an API |
| - caller cannot pick the home explicitly (`""` means "no preference"). |
| - Should the console warn at startup in that state? |
| -15. **Windows.** No timezone is sent (Windows zone names need a CLDR mapping), |
| - so Windows users get the IP step. Acceptable? |
| -16. **The AWS region list.** C23 is from memory. The newest rows |
| - (`ap-east-2`, `ap-southeast-6`, `ap-southeast-7`, `mx-central-1`) need |
| - checking against AWS's list when the table is built. |
| + back. |
| + - Option (a): ship as a placement change, and measure the request path |
| + before claiming any latency gain. |
| + - Option (b): also default non-home instances to `Steering: latency`. |
| + - **Recommendation: (a).** Steering is a separate change with its own |
| + costs, and whether `CARLOS_STEERING_ZONE_ID` is set on the flagship |
| + needs checking first. |
| +3. **The rollback floor (§5.7).** Once the switch is on, rolling the console |
| + binary below the first region-aware release sends no-region creates for |
| + non-home apps to the home until roll-forward. That is today's behaviour, |
| + logged afterwards. |
| + - Option (a): accept the floor as a runbook rule. |
| + - Option (b): require a mechanism that refuses such creates. An old binary |
| + cannot carry one, so it would have to live outside the console, for |
| + example a box-side check. |
| + - **Recommendation: (a).** |
| ## 18. Copy (pending copy review) |
| -Drafts. None ships until Paul has reviewed it through copy review. No em |
| -dashes. `{…}` is filled at runtime. |
| +These are drafts. None ships until Paul has reviewed it through copy review. |
| +There are no em dashes, and `{…}` is filled at runtime. |
| | id | where | draft | |
| | --- | --- | --- | |
| | C1 | new-app page, picker label | Region | |
| | C2 | new-app page, help | Where this app's instances run. We picked the region nearest to you. Choose another if most of your users are somewhere else. | |
| -| C3 | new-app page, guessed option suffix | (nearest to you) | |
| +| C3 | new-app page, `auto` option | {place} (nearest to you) | |
| | C5 | new-app page, flash | That region can't take new apps right now. Choose another. | |
| | C7 | Instances tab | New instances start in {place}, this app's region. | |
| -| C8 | Instances tab, region not armed | This app's region, {place}, can't take new instances right now. | |
| +| C8 | Instances tab, default unavailable | New instances can't start in {place}, this app's region, right now. Pass a region to place one elsewhere. | |
| | C9 | CLI, from timezone | Region: {place} (from your timezone). Use --region to choose another. | |
| | C10 | CLI, from IP | Region: {place} (from your network location). Use --region to choose another. | |
| | C11 | CLI, fallback | Region: {place} (the platform default). Use --region to choose another. | |
| | C12 | CLI, chosen | Region: {place}. | |
| -| C13 | CLI, old console ignored --region | This console does not record an app region yet, so --region was not applied. New instances will start in the platform's default region. | |
| +| C13a | CLI, old console ignored --region | This console does not record an app region yet, so --region was not applied. | |
| +| C13b | CLI, console did not apply --region | This console is not assigning app regions yet, so --region was not applied. This app's instances start in {place}. | |
| | C14 | CLI, instances create and canary | created instance {host} (channel {channel}, {place}) | |
| | C15 | CLI, deploy --region on an existing app | {app} already runs in {place}. --region only applies when deploy creates an app. To place one instance elsewhere, use carlos instances create --region. | |
| | C16 | API, defaulted region not armed | this app's region {code} can't take new instances right now. Armed regions: {list}. Pass a region to place this instance elsewhere. | |
| @@ -683,6 +928,10 @@ dashes. `{…}` is filled at runtime. |
| | C20 | API, app region unreadable | could not read this app's region. Try again. | |
| | C21 | activity feed | created app {name} in {place} | |
| | C22 | flag help | apps create: "region this app's instances start in (default: the region nearest you)". deploy: "region for the app this deploy creates (default: the region nearest you)". canary: "region for the canary instance (default: the app's region)". instances create: "region for this instance (default: the app's region, subject to the manifest's allow-list)". | |
| +| C24 | new-app page, under the picker, IP step configured | IP location by DB-IP. (Linked to db-ip.com; DB-IP's required wording to be confirmed against its licence page.) | |
| +| C25 | CLI, canary --region on an existing canary | canary {host} already runs in {place}. --region applies only when canary creates the host. Run carlos canary remove first to start it somewhere else. | |
| +| C26 | API, defaulted home inside its replica set | this app's region {code} is also one of this instance's replica placements. Pass a region for the instance, or drop {code} from replicas. | |
| +| C27 | API, guess with unreadable policy | could not read this app's instance policy, so there is no region suggestion. Try again. | |
| C23, place names: |
| @@ -706,5 +955,39 @@ C23, place names: |
| | ap-northeast-1 | Japan · Tokyo | ap-southeast-7 | Thailand · Bangkok | |
| | ap-northeast-2 | South Korea · Seoul | ap-northeast-3 | Japan · Osaka | |
| -The `docs/site/cli.md` and README additions (§11, §13) are prose and go |
| -through the same review. |
| +The `docs/site/cli.md` and README additions (§5.7, §11, §12, §13) are prose |
| +and go through the same review. |
| + |
| +## 19. Review dispositions (Astra, round 1) |
| + |
| +| # | sev | finding | disposition | |
| +| --- | --- | --- | --- | |
| +| 1 | P1 | A picker submitting the guess bypasses the pool exclusion | **accepted.** Pooled policy yields no candidates and `guess: null` (§6.1); `app_default` reports `pooled`; end-to-end test (§15). | |
| +| 2 | P1 | Browser adoption silently changes an existing app's default | **accepted.** `regionReq` separates the explicit choice from the hint; the guess resolves after adoption is known and is discarded on adoption; the form's `auto` value replaces the forgeable source label (§5.5, §10). | |
| +| 3 | P1 | An unnamed home makes override and recovery impossible | **accepted.** Non-home app regions are written only when the home is named; an explicit home name is always an override (§5.6, §7). | |
| +| 4 | P1 | No mixed-version or rollback rule | **accepted.** Read side ships first with the switch off; the switch is the rollback lever; a binary rollback floor with a roll-forward log (§5.6, §5.7); acceptance of the floor is §17 Q3. | |
| +| 5 | P2 | `canary --region` undefined for an existing canary | **accepted.** Equal proceeds; different is refused before any write or upload (§11, C25). | |
| +| 6 | P2 | Manifest absence and read failure unspecified for `/regions` | **accepted.** Read directly, three states; legacy fields unfiltered; unreadable policy gives no guess and an explicit error (§6.1, §9.3, §9.4). | |
| +| 7 | P2 | The unavailable-default explanation disappears when the region is absent | **accepted.** `app_default` reports availability and reason for codes no box publishes; C8 shows regardless of region count (§9.3, §10). | |
| +| 8 | P2 | Query-string hints can be reflected by the canonical-host redirect | **accepted.** Hints move to POST bodies (§9.4, §9.5); `/regions` takes no hint parameters; privacy assertions over redirects, errors, logs and audit (§15). | |
| + |
| +Astra's recommendations on rev 1's open questions: |
| + |
| +| rev 1 Q | recommendation | disposition | |
| +| --- | --- | --- | |
| +| Q1 Ireland relief | titogo adoption in the completion criteria | kept for Paul (§17 Q1), recommended | |
| +| Q2 DNS and latency | verify and measure; no silent steering | kept for Paul (§17 Q2), recommended; steering listed as a non-goal | |
| +| Q3 unarmed default | keep refusal, usable override, visible status | adopted (§7, §9.3, §10) | |
| +| Q4 mutable region | creation-only now; a later verb affects future creates only | adopted (§2) | |
| +| Q5 browser instance form | defer | adopted (§2) | |
| +| Q6 GeoIP database | DB-IP Lite with attribution; correct the GeoLite2 statement | adopted (§6.2, C24) | |
| +| Q7 dependency and fixtures | pinned `maxminddb-golang`; pinned MaxMind-DB fixtures | adopted (§6.2) | |
| +| Q8 place names | keep picker style; require geographic consistency | adopted (§13) | |
| +| Q9 home coordinates | verify before rollout; use `CARLOS_AWS_REGION`, no new console override | adopted (§16 step 1) | |
| +| Q10 sidecars | leave unchanged; separate design | adopted (§2, §12) | |
| +| Q11 pools | suppress pooled guesses now; defer pool-aware candidates | adopted (§6.1) | |
| +| Q12 home/replica overlap | refuse new home records whose region is in their replica set | adopted for defaulted regions (§7, C26); explicit requests unchanged, because tightening #162's door is a separate change | |
| +| Q13 allow-list spelling | preserve normalization; audit manifests; document | adopted (§12, §16) | |
| +| Q14 unnamed home | a warning is not enough | adopted, as finding 3 | |
| +| Q15 Windows | accept the fallback; honour `$TZ` | adopted (§11) | |
| +| Q16 AWS list | codes confirmed; record the source; verify coordinates | adopted (§13) | |