carlos / platform Public

docs: nearest-region spec rev 6, R9: placed instances need the secrets key

Production finding (Paul approved the amendment 2026-10-02): only the
Ireland box holds CARLOS_SECRETS_KEY, so instances placed on the UK, US
and AU boxes ran with no config, secrets or store binding (part of the
kpmg outage). Boxes publish secrets_key_fp, the move-any-app spec's
field, in a format fixed here for both (sha256 of the public key's SEC1
bytes, one appconfig helper). The console expects the fingerprint of its
own CARLOS_SECRETS_PUBKEY. Automatic placement needs a match; explicit
socket-backed and replica creates are refused (C31) once a current
writer proves the mismatch, and keep today's behaviour while unknown.
Rollout gains a key-staging step. Changes marked "(R9)".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 9ccfaca6ac15df51d676091c4f703d1bd471ab94 parent db59f32
1 file changed, +139 −28
  • docs/superpowers/specs/2026-10-01-nearest-region-default-design.md +139 −28
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 3ba95f956..30ed55727 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,7 +1,7 @@
# Nearest region by default: each app picks its region at creation
**Date:** 2026-10-01
-**Revision:** 5.
+**Revision:** 6.
- Rev 1 (7fbc9479) went to Astra (GPT-6, via codex), and the findings are in
`CODEX-FINDINGS-NEAREST-REGION-R1.md`.
@@ -18,11 +18,19 @@
invariants to meet, and placement ships first.
- Rev 4.1 records Paul's ruling on that split as R8 and removes the question.
- Rev 5 answers Astra's round 4 (`CODEX-FINDINGS-NEAREST-REGION-R4.md`): three
- P2s, marked "(codex R4-N)".
-
-**Status:** DRAFT for review. The rulings in §3 are Paul's and are not
-reopened here. Everything marked *(decision)* is this spec's and open to
-review.
+ P2s, marked "(codex R4-N)". Astra's round 5 found nothing new; Paul
+ approved rev 5 on 2026-10-02.
+- **Rev 6 is an amendment Paul approved on 2026-10-02, ruling R9**, after a
+ production finding: only the Ireland box holds `CARLOS_SECRETS_KEY`. On the
+ UK, US and AU boxes the config loops log "config sync loop disabled:
+ CARLOS_SECRETS_KEY not set" (`internal/host/agent.go:881-886`), and a wake
+ treats a missing `<host>.config.env` as empty
+ (`internal/activator/backend_exec.go:1146-1149`). An instance placed there
+ runs with no config, no secrets and no store binding. That was part of the
+ kpmg outage. Changes are marked "(R9)".
+
+**Status:** Approved by Paul on 2026-10-02: rev 5, plus the R9 amendment in
+this revision. The rulings in §3 are Paul's and are not reopened here.
**Discussion:** not yet filed on amadan. Latency routing depends on amadan
#169.
**Builds on:**
@@ -138,6 +146,7 @@ Non-goals, each with a follow-up in §20:
| R6 | **Latency routing is in scope** for non-home instances, not placement alone. | §13: its invariants and gates, delivered by the follow-on spec |
| R7 | **The rollback floor is a runbook rule.** It is documented for release notes and the roll recipe, with no outside guard. | §5.7, §17 |
| R8 | **Split** (Paul, 2026-10-01). Placement ships on this spec. Latency routing (R6) is sequenced to its own follow-on spec, built to the §13 invariants. | §13, §17 |
+| R9 | **A box must hold the deployment's secrets key to receive placed instances** (Paul, 2026-10-02). | §6.1, §7, §8.1, §17 |
| D1 | The app region is stored console-side and set once at creation. The create door fills an empty `req.Region` from it **before** normalization, the allow-list and the armed checks. Existing apps are backfilled to the home, so behaviour does not change. | §5, §7 |
| D2 | The guess works in coordinates: timezone to a point, IP to a point through an operator-supplied MaxMind-format `.mmdb` (`CONSOLE_GEOIP_DB`), then the great-circle nearest eligible region. Home when everything fails. | §6 |
| D3 | Surfaces: a new-app picker; names and the guess for external pickers; the CLI as R4. | §9, §10, §11 |
@@ -381,6 +390,46 @@ an automatic one.
Until boxes roll with the fields, no coded region is eligible. The guess then
picks only the home, so the feature cannot outrun the box roll.
+**The secrets key (R9).** An exec instance's config, secrets and store
+binding reach it only through `<host>.config.env`, which the agent renders
+only when the box holds `CARLOS_SECRETS_KEY` (`agent.go:881-886`, `:1812`).
+A wake treats a missing file as empty (`backend_exec.go:1146-1149`), so a box
+without the key runs a placed instance with nothing, silently. Eligibility
+therefore gains a fourth fact.
+
+- **What a box publishes.** One more edges-file field, `secrets_key_fp`: the
+ fingerprint of the public half of the box's `CARLOS_SECRETS_KEY`, never the
+ key. The name, its place beside `placement` and `data_bucket`, and its
+ shape (additive, `omitempty`, absent when the box holds no key or a key
+ that does not parse) are the move-any-app spec's
+ (`2026-10-02-move-any-app-design.md` §6, on `design-moves-any-region`
+ at e41c4fef). Both features read one field.
+- **The format**, which that spec names but does not fix, is fixed here for
+ both: `"sha256:"` followed by the lowercase hex SHA-256 of the public key's
+ uncompressed SEC1 bytes (`(*ecdh.PublicKey).Bytes()`, 65 bytes for P-256).
+ Those are exactly the bytes `CARLOS_SECRETS_PUBKEY` base64-decodes to, so a
+ box (from its private key) and the console (from the public key) compute the
+ same string. One helper, `appconfig.KeyFingerprint`, is the only code that
+ renders it. The move-any-app spec should cite it rather than define a
+ second one.
+- **What the console expects.** The console learns the deployment's
+ fingerprint from its own `CARLOS_SECRETS_PUBKEY` (`Config.SecretsPub`,
+ `cmd/console/main.go:467-490`). That is the key the CLI seals config to,
+ so it is by definition the one a box must be able to open. The move-any-app
+ spec's console makes the same choice. The console never learns the
+ fingerprint from a box: a "majority of boxes" or "the home box" rule would
+ certify whatever key happened to be widespread, including a wrong one.
+ With `CARLOS_SECRETS_PUBKEY` unset, the console cannot judge. It logs that
+ once at startup, and no coded region is eligible for automatic placement.
+- **The rule.** A code is eligible only if at least one box that meets the
+ other three facts also publishes a `secrets_key_fp` equal to the
+ console's. The home stays exempt, as it is from every eligibility fact:
+ it is where every legacy create already lands, and on the flagship it is
+ the one box that holds the key.
+- **The reason word.** A code that fails only this fact reports
+ `no-secrets-key`, between `not-hosting` and `not-eligible` in the order of
+ checks (§9.3, C16).
+
Candidate rules:
- **Coded regions**: boxes whose `Placement` passes `provision.ValidRegion`.
@@ -585,8 +634,8 @@ strictly (§6.1) and refuses in three cases:
- **The read failed**: a 503 with C28. The region could not be confirmed,
which is different from "absent".
-- **No eligible box publishes the code**: C16, naming `absent`, `not-hosting`
- or `not-eligible`. This covers the last box in a region being retired, and
+- **No eligible box publishes the code**: C16, naming `absent`, `not-hosting`,
+ `no-secrets-key` (R9) or `not-eligible`. This covers the last box in a region being retired, and
every box losing its placement code.
- **The allow-list excludes it**: C17.
@@ -600,6 +649,28 @@ refusal before anyone tries (codex 7).
*(decision)* **A defaulted home whose region is in its own `replicas` set is
refused** (C26). Explicit requests keep today's behaviour.
+*(decision, R9)* **An explicit request into a region whose boxes provably
+lack the deployment's secrets key is refused** (C31, 400), for a
+socket-backed instance (`DerivesPaths`) and for a replica. Static and
+addr-backed records need no config on the box and are unaffected.
+
+- **What "provably" means.** The console's `CARLOS_SECRETS_PUBKEY` is set,
+ and every reachable, hosting box publishing the code is a **current
+ writer** (it publishes `accepts_inbound`, which the same box release adds
+ and which is never omitted). Among those boxes, none publishes a matching
+ `secrets_key_fp`. A current writer that publishes no fingerprint holds no
+ key, because the field is omitted exactly then.
+- **Unknown keeps today's behaviour.** An old writer, or a console with no
+ `CARLOS_SECRETS_PUBKEY`, cannot prove a mismatch, so the request proceeds
+ as it would today. Roll order therefore never refuses a working region.
+- **Why refuse, and not keep today's behaviour.** The record this would
+ write is an instance that starts, answers health checks, and runs with no
+ config, no secrets and no store, which is the kpmg outage. It belongs with
+ the existing "reachable but cannot host" refusal (`apiinstances.go:827-852`,
+ #110). That one also refuses an explicit request the box would accept but
+ could never serve. A person who named the region learns why at the door,
+ not from a broken app.
+
**Response.** `apiRecordView` (`apiinstances.go:535`) gains two fields that
sit outside the record, like `PrivateStoreBucket`:
@@ -626,9 +697,11 @@ instance door validates one:
1. `ValidRegion`.
2. Armed and hosting, unless the gate is dormant.
-3. The home's name is accepted through `homeRegionIn`.
+3. Not provably missing the secrets key (R9, the same rule as §7), refused
+ with C31.
+4. The home's name is accepted through `homeRegionIn`.
-Refusals list what would pass (C18, C19). There is no allow-list yet, because
+Refusals list what would pass (C18, C19, C31). 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 (§12).
@@ -699,6 +772,7 @@ The response gains one block:
- `absent`: the code is reported even though `regions` does not list it
(codex 7);
- `not-hosting`;
+ - `no-secrets-key` (R9, §6.1);
- `not-eligible` (§6.1);
- `not-allowed`;
- `pooled`;
@@ -1002,6 +1076,8 @@ than the picker, but it must agree geographically, and the README says so.
| mmdb lookup error | A miss; rate-limited log. |
| timezone UTC, `Etc/*`, unknown or malformed | IP step, then home. |
| boxes not yet publishing the eligibility fields | No coded region is eligible. The guess is the home, and app-derived defaults are refused as `not-eligible`. |
+| a region's boxes hold no secrets key, or a different one (R9) | Not eligible for any automatic choice. App-derived defaults are refused with C16 `no-secrets-key`. Explicit socket-backed or replica creates are refused with C31 once a current writer proves the mismatch. |
+| console has no `CARLOS_SECRETS_PUBKEY` (R9) | Logged once. No coded region is eligible for automatic placement. Explicit creates keep today's behaviour. |
| edges listing fails | The guess is the home. App-derived creates get 503 C28. Explicit creates are unchanged. |
| last box of the app's region retired | App-derived creates are refused (C16 `absent`), and `app_default` and C8 say why. |
| home publishes no AWS region | Every guess is `default`. Fix it with the box's `CARLOS_AWS_REGION`. |
@@ -1039,6 +1115,23 @@ than the picker, but it must agree geographically, and the README says so.
- City and Country-only records;
- private, loopback and CGNAT addresses never reach the reader.
+**Secrets key (R9):**
+
+- `appconfig.KeyFingerprint` of a key's public half equals the fingerprint
+ computed from the base64 `CARLOS_SECRETS_PUBKEY` of the same key.
+- A box with the key publishes `secrets_key_fp`. With no key, or a key that
+ does not parse, it omits the field. The body never contains the private key
+ or the public key itself.
+- Eligibility:
+ - a box with a matching fingerprint is eligible;
+ - a box with a different fingerprint, or none, is `no-secrets-key`;
+ - a console with no `CARLOS_SECRETS_PUBKEY` makes no coded region
+ eligible.
+- An explicit socket-backed create into a region where only current writers
+ publish a mismatch is refused with C31. Under an old writer, or with no
+ console key, it is accepted as today. Static and addr-backed creates are
+ never refused for it.
+
**Edges file and eligibility** (codex R2-1):
- The writer publishes `serves_shared_pool` and `accepts_inbound` from
@@ -1199,18 +1292,25 @@ and `ip` appear in none of:
flagship's Ireland box publishes `region: eu-west-1` and no `placement`.
Use `CARLOS_AWS_REGION` where it does not.
- Set `CONSOLE_HOME_REGION` on every console that will turn the switch on.
-2. **Box release.** It carries the two edges-file eligibility fields. Roll it
- to every box, then check every region's files report all three fields.
- Until then, no coded region is eligible (§6.1).
-3. **Console release, switch off.** It carries the migration, `internal/geo`,
+2. **Stage the secrets key (R9).** Every box in a region that should take
+ placed instances gets the deployment's `CARLOS_SECRETS_KEY` in
+ `/etc/carlos/host.env`, from the deployment's secret store. On the flagship
+ today that means the UK, US and AU boxes. This is infrastructure work in
+ carloku-infrastructure, under the scoped-approval rule (AGENTS.md). Set
+ `CARLOS_SECRETS_PUBKEY` on the console if it is not already set.
+3. **Box release.** It carries the edges-file eligibility fields and
+ `secrets_key_fp`. Roll it to every box. Then check that every region's
+ files carry all of them, and that each `secrets_key_fp` equals the
+ console's. Until then, no coded region is eligible (§6.1).
+4. **Console release, switch off.** It carries the migration, `internal/geo`,
the fill, the endpoints and the UI. Behaviour is unchanged, and it can be
rolled back freely.
-4. **Verify on dev.** Turn the switch on with two eligible regions. Then:
+5. **Verify on dev.** Turn the switch on with two eligible regions. Then:
- 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 pre-roll answer;
- run the service gate below.
-5. **Service gate on the flagship, with the switch still off** (codex R4-3).
+6. **Service gate on the flagship, with the switch still off** (codex R4-3).
Eligibility proves configuration, not a working request path.
Reachability is only a non-empty EIP (`instancesplacement.go:185`), and
@@ -1240,12 +1340,12 @@ and `ip` appear in none of:
The probe instances are deleted afterwards. The same gate is re-run before
the switch is turned on again after any period off, and whenever a new
region becomes eligible.
-6. **Flagship.** Roll the console, then turn the switch on. **From here the
+7. **Flagship.** Roll the console, then turn the switch on. **From here the
binary rollback floor applies (R7).** That release's notes and the roll
recipe in carloku-infrastructure name the floor version. Optionally, set
`CONSOLE_GEOIP_DB` to a DB-IP Lite file.
-7. **CLI release.** It is safe in either order (§9.6).
-8. **titogo**, in its own repo, adopts `/regions/guess` and the place names.
+8. **CLI release.** It is safe in either order (§9.6).
+9. **titogo**, in its own repo, adopts `/regions/guess` and the place names.
No step of Phase P depends on Phase L. Phase P writes nothing to DNS, stamps
no `Steering` field, and needs neither `CARLOS_STEERING_ZONE_ID` nor any
@@ -1255,10 +1355,10 @@ change to the zone.
(§13.3). Its gate is that spec landing with its simulation passing, plus
scoped approval for the infrastructure. Until then, **no deployment arms
`CARLOS_STEERING_ZONE_ID`**, including for the static-site steering that
-already exists. The latency baseline is taken once Phase P step 6 has run.
+already exists. The latency baseline is taken once Phase P step 7 has run.
That makes Phase L depend on Phase P, never the other way round.
-Copy review (§19) happens before Phase P step 3 ships any string.
+Copy review (§19) happens before Phase P step 4 ships any string.
## 18. Questions for Paul
@@ -1294,16 +1394,18 @@ review. There are no em dashes. `{…}` is filled at runtime.
| 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, 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.) |
| C30 | CLI, canary --region on a fleet-placed app | {app} runs on your fleet {fleet}, which decides where every instance runs, so --region does not apply. Run the command without --region. |
+| C31 | API, explicit create into a region without the secrets key (R9) | region {code} can't run instances right now: its boxes don't hold this deployment's secrets key, so an instance there would start without its config or secrets. Choose another region. |
| 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. |
| C28 | API, app region unconfirmed | could not confirm that this app's region {code} can take new instances. Try again, or pass a region. |
| C29 | roll-forward log, read by operators | possible placement discrepancy: {host} of {app} has no region, but {app}'s region is {code}. It may be a deliberate home override; review it. |
-The `{reason}` words in C16 are `absent`, `not-hosting` and `not-eligible`.
-Shown to a person, they read: "no box runs there now", "boxes there can't
-start instances", and "boxes there don't take ordinary instances". Those are
-copy too.
+The `{reason}` words in C16 are `absent`, `not-hosting`, `no-secrets-key`
+(R9) and `not-eligible`. Shown to a person, they read: "no box runs there
+now", "boxes there can't start instances", "boxes there can't open this
+deployment's secrets" (R9), and "boxes there don't take ordinary instances".
+Those are copy too.
C23, place names:
@@ -1414,9 +1516,9 @@ The `docs/site/cli.md`, README and release-note additions (§5.7, §11, §12,
| former question | disposition |
| --- | --- |
-| Q1: apply R5, titogo separate | adopted (§2, §17 step 8) |
+| Q1: apply R5, titogo separate | adopted (§2, §17 step 9) |
| Q2: implement R6 with the DNS corrections, keep the separate approved Phase L rollout and measurements | adopted in substance. The corrections are larger than a patch, so they move to the follow-on spec with invariants (§13); the sequencing is ruled by R8. |
-| Q3: name the exact floor version before activation; keep region-aware reads when disabling | adopted (§5.6, §5.7, §17 step 6) |
+| Q3: name the exact floor version before activation; keep region-aware reads when disabling | adopted (§5.6, §5.7, §17 step 7) |
### Round 4 (rev 4.1, 4ad64f7c)
@@ -1424,6 +1526,15 @@ The `docs/site/cli.md`, README and release-note additions (§5.7, §11, §12,
| --- | --- | --- | --- |
| R4-1 | P2 | Fleet-placed apps still get instance guesses the door will override | **accepted.** Verified at `fleets.go:479` and `apiinstances.go:798`. The guess endpoint resolves fleet placement first and returns `null`; app-creation inference is unchanged; end-to-end test (§6.1, §9.4, §12, §16). |
| R4-2 | P2 | `canary --region` on a fleet behaves differently before and after creation | **accepted.** Verified at `canary.go:405` and `apiinstances.go:798`. Fleet precedence is resolved before either branch, and `--region` on a fleet-placed app refuses identically every time with C30, never suggesting a remove (§11, §12, §16). |
-| R4-3 | P2 | Phase P verification can pass while non-home instances are unreachable | **accepted.** Verified at `instancesplacement.go:185`, `edge.go:2699` and `:1428`. A service gate before the production switch: a fresh probe per eligible region, reached from outside through the platform host and through every other edge with `--resolve`, checking 2xx and `X-Carlos-Version`. All regions must pass. Plus an integration test for the missing-mesh-key case (§16, §17 step 5). |
+| R4-3 | P2 | Phase P verification can pass while non-home instances are unreachable | **accepted.** Verified at `instancesplacement.go:185`, `edge.go:2699` and `:1428`. A service gate before the production switch: a fresh probe per eligible region, reached from outside through the platform host and through every other edge with `--resolve`, checking 2xx and `X-Carlos-Version`. All regions must pass. Plus an integration test for the missing-mesh-key case (§16, §17 step 6). |
Astra raised no open questions in round 4. §18 stands at none.
+
+### Amendment R9 (rev 6, Paul 2026-10-02)
+
+Not a review finding: a production finding, ruled by Paul. Recorded here so
+the dispositions list every change since rev 5. The changes are §3 (R9),
+§6.1 (`secrets_key_fp`, its format, the console's expected fingerprint, the
+fourth eligibility fact), §7 and §8.1 (the explicit refusal, C31), §9.3 and
+C16 (the `no-secrets-key` reason), §15, §16, §17 (staging the key), and §19
+(C31).