carlos / platform Public

docs: nearest-region plan rev 3, after Astra's plan round 2, proved in scratch

All ten new findings (6 P1, 4 P2) and the four partly-resolved round-1
findings verified and accepted. The code blocks they touched were
extracted verbatim into a throwaway worktree at e8406ce6: whole-tree
build and vet clean, every prescribed test FAIL before its
implementation and PASS after, mutation checks confirmed for the
snapshot-completeness, eligible-only location, R9 scope, canary ordering
and guess invariants, and the five cross-compile targets built. The
proof surfaced five more fixes (an import, a test package, keyed
literals, a role-pin entry, the e2e harness), all folded in. The scratch
worktree and branch were deleted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev b7c5b60d44bcf6ce77ee05c754cc6c1d7d8f322b parent 160b392
1 file changed, +632 −237
  • docs/superpowers/plans/2026-10-02-nearest-region-default.md +632 −237
diff --git a/docs/superpowers/plans/2026-10-02-nearest-region-default.md b/docs/superpowers/plans/2026-10-02-nearest-region-default.md
index b2936fe30..ecbd495e4 100644
--- a/docs/superpowers/plans/2026-10-02-nearest-region-default.md
+++ b/docs/superpowers/plans/2026-10-02-nearest-region-default.md
@@ -2,14 +2,18 @@
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-**Revision:** 2.
+**Revision:** 3.
- Rev 1 (9513d52c) was reviewed by Astra
(`CODEX-PLAN-NEAREST-REGION-R1.md`, beside this file): not ready, with
8 P1 and 10 P2. Every finding was checked against the code and accepted.
-- Rev 2 also folds in spec rev 6's ruling R9: placed instances need the
- deployment's secrets key.
-- The dispositions table is at the end of this plan.
+- Rev 2 (fcf15ae9) folded in spec rev 6's ruling R9: placed instances need
+ the deployment's secrets key.
+- Rev 3 answers Astra's round 2 (`CODEX-PLAN-NEAREST-REGION-R2.md`).
+- The code blocks the findings touched were **proved**: extracted verbatim
+ into a throwaway worktree at the branch base, built and vetted, and each
+ prescribed test was run before and after its implementation step.
+- Both dispositions tables, with the proof results, are at the end.
**Goal:** Give every new app a region at creation, preselected from a timezone-then-IP guess, and have new instances default to that region. Never place an instance automatically on a box that cannot open the deployment's secrets. Existing apps, older CLIs and DNS stay unchanged.
@@ -89,7 +93,10 @@ beside this plan. Section numbers below (§N) are the spec's.
- **Cross-compile gate.** The five CI targets, exactly as
`.amadan/ci.d/20-cross@5` runs them:
`CGO_ENABLED=0 GOOS=<os> GOARCH=<arch> go build ./...` for darwin/arm64,
- darwin/amd64, linux/arm64, windows/amd64 and windows/arm64.
+ darwin/amd64, linux/arm64, windows/amd64 and windows/arm64. Every loop
+ below records each failure and ends with `[ "$fail" = 0 ] || exit 1`. Run
+ each such block as one script (`bash -c` or a tool call), so that a failed
+ target stops it before the commit lines that follow (codex plan-R2 10).
- **Test environment.**
- Use the default Go build cache (`~/.cache/go-build`, writable inside the
sandbox). Do not set `GOCACHE`, `GOMODCACHE` or `GOTMPDIR`.
@@ -233,6 +240,14 @@ plan and marked "plan".
| O8 (plan) | `internal/console/server.go` | `console: app region %s/%s: %s (%s)` |
| O9 (plan) | `internal/console/regiondrift.go` | `console: placement discrepancy check %s/%s: only the first %d records were checked` |
| O10 (plan) | `internal/host/edgesfile.go` | `agent: CARLOS_SECRETS_KEY does not parse, so no secrets_key_fp is published: %v` |
+| O11 (plan) | `internal/console/apiinstances.go` | `console: api instances create %s/%s: read app region: %v` |
+| O12 (plan) | same | `console: api instances create %s/%s: read edges: %v` |
+| O13 (plan) | `internal/console/regioncandidates.go` | `console: region policy %s/%s: read manifest: %v` |
+| O14 (plan) | same | `console: region policy %s/%s: open registry: %v` |
+| O15 (plan) | same | `console: region policy %s/%s: placement: %v` |
+| O16 (plan) | `internal/console/apicli.go` | `console: claim %s/%s: read back region: %v` |
+| O17 (plan) | `apimode.go` | `regions: unreadable response: %w` |
+| O18 (plan) | `internal/console/regiondrift.go` | `console: placement discrepancy check %s/%s: %v` |
| C9 | `region_copy.go` | `Region: %s (from your timezone). Use --region to choose another.` |
| C10 | same | `Region: %s (from your network location). Use --region to choose another.` |
| C11 | same | `Region: %s (the platform default). Use --region to choose another.` |
@@ -242,6 +257,7 @@ plan and marked "plan".
| C14 | same | `channel %s, %s` |
| C15 | same | `%s already runs in %s. --region only applies when deploy creates an app. To place one instance elsewhere, use carlos instances create --region.` |
| C15b (plan) | same | `could not read %s's region (%v), so deploy stopped before shipping anything. Try again, or run without --region.` |
+| C15c (plan) | same | `the console could not read %s's region, so deploy stopped before shipping anything. Try again, or run without --region.` |
| C25 | same | `canary %s already runs in %s. --region applies only when canary creates the host. Run carlos canary remove first to start it somewhere else.` **Note for Paul:** the verb is `carlos canary rm` (or `delete`); there is no `remove`. |
| C30 | same | `%s runs on your fleet %s, which decides where every instance runs, so --region does not apply. Run the command without --region.` |
| C30b (plan) | same | `this console can't say where %s's instances run, so --region can't be checked. Update the console, or run the command without --region.` |
@@ -514,9 +530,11 @@ because the schema really did grow.
Run:
```bash
+fail=0
for t in darwin/arm64 darwin/amd64 linux/arm64 windows/amd64 windows/arm64; do
- CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; break; }
+ CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; fail=1; }
done
+[ "$fail" = 0 ] || exit 1 # stops this block before any commit below it
```
Expected: no `FAILED` line. `internal/host` is linked into the CLI.
@@ -745,6 +763,9 @@ func edgesFileSecretsFP(getenv func(string) string) string {
The log text is O10 from the copy record. `ParsePrivateKey`'s errors name
the variable and the parse failure, never the value.
+Add `"github.com/carlosframework/platform/internal/appconfig"` to
+`edgesfile.go`'s imports.
+
In `edgesFileLoop`'s ident literal, add:
```go
@@ -757,9 +778,11 @@ Run:
```bash
mkdir -p /tmp/g && TMPDIR=/tmp/g go test ./internal/host ./internal/appconfig -count=1
+fail=0
for t in darwin/arm64 darwin/amd64 linux/arm64 windows/amd64 windows/arm64; do
- CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; break; }
+ CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; fail=1; }
done
+[ "$fail" = 0 ] || exit 1 # stops this block before any commit below it
```
Expected: PASS, and no `FAILED` line.
@@ -1805,9 +1828,11 @@ and its `golang.org/x/sys` requirement only.
- [ ] **Step 6: Cross-compile, then commit**
```bash
+fail=0
for t in darwin/arm64 darwin/amd64 linux/arm64 windows/amd64 windows/arm64; do
- CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; break; }
+ CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; fail=1; }
done
+[ "$fail" = 0 ] || exit 1 # stops this block before any commit below it
git add go.mod go.sum internal/geo/mmdb.go internal/geo/mmdb_test.go internal/geo/testdata
git commit -m "geo: optional GeoIP locator; lookup errors logged as a fixed category (nearest-region §6.2, §6.5)
@@ -2202,6 +2227,12 @@ INSERT INTO apps (app, created_by, created_at, region, region_source) VALUES ('l
t.Fatal(err)
}
defer s.Close()
+ // Open migrates but does not backfill accounts (NewServer does), and
+ // rename and move both resolve the app's account row: seed the ops
+ // account the legacy row belongs to, and the move's destination.
+ if _, err := s.db.Exec(`INSERT OR IGNORE INTO accounts (id, sqid, name, created_at) VALUES (?, 'bab', 'Ops', 0)`, OpsAccountID); err != nil {
+ t.Fatal(err)
+ }
if err := s.RenameAppIn(OpsAccountID, "legacy", "renamed"); err != nil {
t.Fatal(err)
}
@@ -2353,7 +2384,7 @@ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
- the `Server` struct;
- `NewServer`, whose last line before `return` calls `logRegionConfig`.
- Modify: `cmd/console/main.go:278`
-- Test: `cmd/console/main_test.go` (append)
+- Test: `cmd/console/region_test.go` (create, `package main`)
**Interfaces:**
- Consumes: `geo.Locator` and `geo.OpenMMDB` (Task 5),
@@ -2441,6 +2472,13 @@ func TestLogRegionConfig(t *testing.T) {
if !strings.Contains(out, "CONSOLE_HOME_REGION is unset") || !strings.Contains(out, "CARLOS_SECRETS_PUBKEY is unset") {
t.Fatalf("missing startup lines: %s", out)
}
+ // Switch off: the guess endpoint still answers, so a missing secrets
+ // key is still worth saying (codex plan-R2 8); an unnamed home is not.
+ buf.Reset()
+ logRegionConfig(Config{})
+ if out := buf.String(); !strings.Contains(out, "CARLOS_SECRETS_PUBKEY is unset") || strings.Contains(out, "CONSOLE_HOME_REGION") {
+ t.Fatalf("switch off logged %q, want only the secrets-key line", out)
+ }
buf.Reset()
pub, _ := mustTestSecretsKey()
logRegionConfig(Config{AppRegions: true, HomeRegion: "ie", SecretsPub: pub})
@@ -2453,10 +2491,18 @@ func TestLogRegionConfig(t *testing.T) {
The two `strings.Contains` fragments are substrings of O1 and O2. Use
fragments of the approved text if review changed them.
-Append to `cmd/console/main_test.go`, adding `"path/filepath"` to its imports
-if absent:
+Create `cmd/console/region_test.go`. It must be `package main`:
+`main_test.go` is the external `package main_test` and cannot reach the
+unexported helpers.
```go
+package main
+
+import (
+ "path/filepath"
+ "testing"
+)
+
func TestOpenGeoIPIsOptionalAndNeverFatal(t *testing.T) {
if openGeoIP("") != nil {
t.Fatal("unset CONSOLE_GEOIP_DB must mean no locator")
@@ -2545,12 +2591,10 @@ func (s *Server) appRegion(accountID int64, app string) (string, string, error)
}
// logRegionConfig names, once at startup, each configuration that leaves
-// part of the feature off.
+// part of the feature off. The secrets-key line does not depend on the
+// switch: the guess endpoint answers with the switch off too (R9).
func logRegionConfig(cfg Config) {
- if !cfg.AppRegions {
- return
- }
- if cfg.HomeRegion == "" {
+ if cfg.AppRegions && cfg.HomeRegion == "" {
log.Print("console: CONSOLE_APP_REGIONS=on but CONSOLE_HOME_REGION is unset, so app regions stay off until the home is named")
}
if cfg.SecretsPub == "" {
@@ -2606,7 +2650,7 @@ Expected: PASS.
- [ ] **Step 7: Commit**
```bash
-git add internal/console/server.go internal/console/regionconfig.go internal/console/regionconfig_test.go cmd/console/main.go cmd/console/main_test.go
+git add internal/console/server.go internal/console/regionconfig.go internal/console/regionconfig_test.go cmd/console/main.go cmd/console/region_test.go
git commit -m "console: app-regions switch, expected secrets fingerprint, GeoIP, test seams (nearest-region §5.6, §6.2, R9)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
@@ -2634,7 +2678,11 @@ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
- Produces:
```go
-func readEdgesFilesStrict(ctx context.Context, s release.Store) ([]edgesFilePlacementView, error)
+type edgesSnapshot struct {
+ Files []edgesFilePlacementView
+ Complete bool // false when any listed file could not be read or parsed
+}
+func readEdgesSnapshot(ctx context.Context, s release.Store) (edgesSnapshot, error) // error only when the LISTING fails
type regionCandidate struct {
Code string // record vocabulary; "" is the home
Home bool
@@ -2642,8 +2690,9 @@ type regionCandidate struct {
Reachable, Hosting, KeyOK, Eligible bool
NoKeyProven bool // R9: current writers only, and none holds the key
}
-func regionCandidates(files []edgesFilePlacementView, homeName, expectedFP string) []regionCandidate
-func (s *Server) regionCands(files []edgesFilePlacementView) []regionCandidate
+func regionCandidates(snap edgesSnapshot, homeName, expectedFP string) []regionCandidate
+func (s *Server) regionCands(snap edgesSnapshot) []regionCandidate
+func (s *Server) candsOrHome(ctx context.Context) []regionCandidate // a fresh snapshot, or the home alone
func homeOnlyCandidates() []regionCandidate
func (c regionCandidate) unavailableReason() string // "", absent, not-hosting, no-secrets-key, not-eligible
func findCandidate(cands []regionCandidate, code string) (regionCandidate, bool)
@@ -2765,17 +2814,18 @@ func captureLog(w io.Writer) (restore func()) {
return func() { log.SetOutput(prev); log.SetFlags(flags) }
}
-func views(t *testing.T, bodies ...string) []edgesFilePlacementView {
+// views is a COMPLETE snapshot of the given edges-file bodies.
+func views(t *testing.T, bodies ...string) edgesSnapshot {
t.Helper()
- var out []edgesFilePlacementView
+ snap := edgesSnapshot{Complete: true}
for _, b := range bodies {
var f edgesFilePlacementView
if err := json.Unmarshal([]byte(b), &f); err != nil {
t.Fatal(err)
}
- out = append(out, f)
+ snap.Files = append(snap.Files, f)
}
- return out
+ return snap
}
func TestRegionCandidatesNeedPositiveEligibility(t *testing.T) {
@@ -2831,6 +2881,72 @@ func TestRegionCandidatesSecretsKey(t *testing.T) {
}
}
+// codex plan-R2 1: a negative proof needs EVERY box. With one file
+// unreadable or malformed, a readable keyless box proves nothing.
+func TestNoKeyProvenNeedsACompleteRead(t *testing.T) {
+ incomplete := views(t, homeBox, ukNoKey)
+ incomplete.Complete = false
+ if c, _ := findCandidate(regionCandidates(incomplete, "ie", testKeyFP), "uk"); c.NoKeyProven {
+ t.Fatal("an incomplete read proved a region keyless")
+ }
+ ctx := context.Background()
+ for name, extra := range map[string]string{"malformed": "{not json", "unversioned": `{"box":"uk-box-2"}`} {
+ s := release.NewDirStore(t.TempDir())
+ for key, body := range map[string]string{"ie-box": homeBox, "uk-box": ukNoKey, "uk-box-2": extra} {
+ if _, err := s.Put(ctx, "control/edges/"+key+".json", []byte(body), ""); err != nil {
+ t.Fatal(err)
+ }
+ }
+ snap, err := readEdgesSnapshot(ctx, s)
+ if err != nil || snap.Complete {
+ t.Fatalf("%s: snapshot complete=%v err=%v, want incomplete", name, snap.Complete, err)
+ }
+ if c, _ := findCandidate(regionCandidates(snap, "ie", testKeyFP), "uk"); c.NoKeyProven {
+ t.Fatalf("%s: proved keyless with a box unassessed", name)
+ }
+ }
+ s := release.NewDirStore(t.TempDir())
+ for key, body := range map[string]string{"ie-box": homeBox, "uk-box": ukNoKey, "uk-box-2": ukEligible} {
+ if _, err := s.Put(ctx, "control/edges/"+key+".json", []byte(body), ""); err != nil {
+ t.Fatal(err)
+ }
+ }
+ snap, err := readEdgesSnapshot(ctx, failingGet{Store: s, key: "control/edges/uk-box-2.json"})
+ if err != nil || snap.Complete {
+ t.Fatalf("failed GET: complete=%v err=%v, want incomplete", snap.Complete, err)
+ }
+ if c, _ := findCandidate(regionCandidates(snap, "ie", testKeyFP), "uk"); c.NoKeyProven {
+ t.Fatal("a failed GET let the readable keyless box prove the region keyless")
+ }
+}
+
+type failingGet struct {
+ release.Store
+ key string
+}
+
+func (f failingGet) Get(ctx context.Context, key string) ([]byte, string, error) {
+ if key == f.key {
+ return nil, "", errors.New("read timeout")
+ }
+ return f.Store.Get(ctx, key)
+}
+
+// codex plan-R2 2: a candidate's coordinates come from its ELIGIBLE boxes
+// only; ties among them go to the lexically smallest AWS region.
+func TestCandidateLocationComesFromEligibleBoxes(t *testing.T) {
+ syd1 := edgeJSON("uk-syd-1", "2.2.2.1", "ap-southeast-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)})
+ syd2 := edgeJSON("uk-syd-2", "2.2.2.2", "ap-southeast-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(false), fp: testKeyFP})
+ c, _ := findCandidate(regionCandidates(views(t, homeBox, ukEligible, syd1, syd2), "ie", testKeyFP), "uk")
+ if !c.Eligible || c.AWSRegion != "eu-west-2" {
+ t.Fatalf("mixed eligibility located uk at %q, want the eligible box's eu-west-2", c.AWSRegion)
+ }
+ dub := edgeJSON("uk-dub", "2.2.2.3", "eu-west-1", "uk", eligibleBox)
+ if c, _ := findCandidate(regionCandidates(views(t, homeBox, ukEligible, dub), "ie", testKeyFP), "uk"); c.AWSRegion != "eu-west-1" {
+ t.Fatalf("eligible tie = %q, want the lexically smallest eu-west-1", c.AWSRegion)
+ }
+}
+
func TestRegionCandidatesFoldTheHomesName(t *testing.T) {
coded := edgeJSON("ie2", "5.6.7.9", "eu-west-1", "ie", eligibleBox)
cands := regionCandidates(views(t, homeBox, coded), "ie", testKeyFP)
@@ -2853,13 +2969,13 @@ func TestRegionCandidatesLogAMissingPointOnce(t *testing.T) {
}
}
-func TestReadEdgesFilesStrictReportsListFailure(t *testing.T) {
- if _, err := readEdgesFilesStrict(context.Background(), failingList{}); err == nil {
+func TestReadEdgesSnapshotReportsListFailure(t *testing.T) {
+ if _, err := readEdgesSnapshot(context.Background(), failingList{}); err == nil {
t.Fatal("a failed listing must be an error, not an empty answer (codex R2-2)")
}
- files, err := readEdgesFilesStrict(context.Background(), release.NewDirStore(t.TempDir()))
- if err != nil || len(files) != 0 {
- t.Fatalf("an empty prefix = %v %v", files, err)
+ snap, err := readEdgesSnapshot(context.Background(), release.NewDirStore(t.TempDir()))
+ if err != nil || len(snap.Files) != 0 || !snap.Complete {
+ t.Fatalf("an empty prefix = %+v %v, want complete and empty", snap, err)
}
}
@@ -2905,7 +3021,7 @@ the current etag (`provision.go:775`), so calling it twice is safe.
- [ ] **Step 2: Run them to verify they fail**
-Run: `go test ./internal/console -run 'TestRegionCandidates|TestReadEdgesFilesStrict|TestAppPolicyStates' -count=1`
+Run: `go test ./internal/console -run 'TestRegionCandidates|TestReadEdgesSnapshot|TestAppPolicyStates|TestNoKeyProven|TestCandidateLocation' -count=1`
Expected: FAIL, undefined.
- [ ] **Step 3: Extend the view and add the strict read**
@@ -2929,23 +3045,33 @@ func readEdgesFiles(ctx context.Context, s release.Store) []edgesFilePlacementVi
if s == nil {
return nil
}
- out, err := readEdgesFilesStrict(ctx, s)
+ snap, err := readEdgesSnapshot(ctx, s)
if err != nil {
log.Printf("console: api instances: could not list %s for the placement join: %v", edgesFilePrefix, err)
return nil
}
- return out
+ return snap.Files
}
-// readEdgesFilesStrict REPORTS a failed listing instead of answering
-// empty: the door must tell "could not read" (503, C28) from "no box
-// publishes this code" (codex R2-2). Single bad files are still skipped.
-func readEdgesFilesStrict(ctx context.Context, s release.Store) ([]edgesFilePlacementView, error) {
+// edgesSnapshot is one read of control/edges/. Complete is false when any
+// listed file could not be read or parsed: positive facts about the boxes
+// that WERE read still hold, but nothing may be concluded about "every
+// box" (R9's NoKeyProven, codex plan-R2 1).
+type edgesSnapshot struct {
+ Files []edgesFilePlacementView
+ Complete bool
+}
+
+// readEdgesSnapshot REPORTS a failed listing instead of answering empty:
+// the door must tell "could not read" (503, C28) from "no box publishes
+// this code" (codex R2-2). A single unreadable or malformed file is
+// skipped, as before, and marks the snapshot incomplete.
+func readEdgesSnapshot(ctx context.Context, s release.Store) (edgesSnapshot, error) {
keys, err := s.List(ctx, edgesFilePrefix)
if err != nil {
- return nil, err
+ return edgesSnapshot{}, err
}
- var out []edgesFilePlacementView
+ snap := edgesSnapshot{Complete: true}
for _, key := range keys {
name, ok := strings.CutPrefix(key, edgesFilePrefix)
if !ok || !strings.HasSuffix(name, ".json") || strings.Contains(name, "/") {
@@ -2954,16 +3080,18 @@ func readEdgesFilesStrict(ctx context.Context, s release.Store) ([]edgesFilePlac
body, _, err := s.Get(ctx, key)
if err != nil {
log.Printf("console: api instances: could not read %s for the placement join: %v", key, err)
+ snap.Complete = false
continue
}
var f edgesFilePlacementView
if err := json.Unmarshal(body, &f); err != nil || f.V < 1 || f.Box == "" {
log.Printf("console: api instances: skipping %s in the placement join: not a versioned edges file", key)
+ snap.Complete = false
continue
}
- out = append(out, f)
+ snap.Files = append(snap.Files, f)
}
- return out, nil
+ return snap, nil
}
```
@@ -3018,10 +3146,12 @@ func (c regionCandidate) unavailableReason() string {
func isTrue(b *bool) bool { return b != nil && *b }
-// regionCandidates folds the edges files into candidates. expectedFP is
+// regionCandidates folds an edges snapshot into candidates. expectedFP is
// the console's own secrets fingerprint; "" means it cannot judge, and then
// no coded region is KeyOK. The home is exempt from every eligibility fact.
-func regionCandidates(files []edgesFilePlacementView, homeName, expectedFP string) []regionCandidate {
+// A coded candidate's AWS region comes only from its ELIGIBLE boxes (codex
+// plan-R2 2); NoKeyProven needs a complete snapshot (codex plan-R2 1).
+func regionCandidates(snap edgesSnapshot, homeName, expectedFP string) []regionCandidate {
type acc struct {
c regionCandidate
regions map[string]int
@@ -3039,7 +3169,7 @@ func regionCandidates(files []edgesFilePlacementView, homeName, expectedFP strin
}
h := get("", true)
h.c.Reachable, h.c.Hosting, h.c.KeyOK, h.c.Eligible = true, true, true, true
- for _, f := range files {
+ for _, f := range snap.Files {
home := f.Placement == "" || (homeName != "" && f.Placement == homeName)
if !home && !provision.ValidRegion(f.Placement) {
continue // fleet/<name> and malformed codes are never candidates
@@ -3052,10 +3182,10 @@ func regionCandidates(files []edgesFilePlacementView, homeName, expectedFP strin
if f.EIP == "" {
continue
}
- if f.Region != "" {
- a.regions[f.Region]++
- }
if home {
+ if f.Region != "" {
+ a.regions[f.Region]++
+ }
continue
}
hosting := isTrue(f.HibernationConfigured) || f.HibernationConfigured == nil
@@ -3070,12 +3200,15 @@ func regionCandidates(files []edgesFilePlacementView, homeName, expectedFP strin
a.unknownWriter = a.unknownWriter || f.AcceptsInbound == nil
}
a.c.Eligible = a.c.Eligible || eligible
+ if eligible && f.Region != "" {
+ a.regions[f.Region]++
+ }
}
out := make([]regionCandidate, 0, len(by))
for _, a := range by {
a.c.AWSRegion = majority(a.c.Code, a.regions)
- a.c.NoKeyProven = !a.c.Home && expectedFP != "" && a.hostingBoxes > 0 && !a.unknownWriter && !a.keyFound
- if a.c.Reachable {
+ a.c.NoKeyProven = !a.c.Home && snap.Complete && expectedFP != "" && a.hostingBoxes > 0 && !a.unknownWriter && !a.keyFound
+ if a.c.Eligible {
if _, ok := geo.LookupAWSRegion(a.c.AWSRegion); !ok {
if _, said := noPointSaid.LoadOrStore(a.c.Code, true); !said {
log.Printf("console: region %q has no point in the shipped AWS region table (AWS region %q), so region guesses leave it out", a.c.Code, a.c.AWSRegion)
@@ -3090,8 +3223,18 @@ func regionCandidates(files []edgesFilePlacementView, homeName, expectedFP strin
// regionCands is regionCandidates with this console's home and expected
// secrets fingerprint.
-func (s *Server) regionCands(files []edgesFilePlacementView) []regionCandidate {
- return regionCandidates(files, s.cfg.HomeRegion, s.expectedSecretsFP())
+func (s *Server) regionCands(snap edgesSnapshot) []regionCandidate {
+ return regionCandidates(snap, s.cfg.HomeRegion, s.expectedSecretsFP())
+}
+
+// candsOrHome is the candidates from a fresh snapshot, or the home alone
+// when the listing fails: for displays and guesses that must not fail.
+func (s *Server) candsOrHome(ctx context.Context) []regionCandidate {
+ snap, err := readEdgesSnapshot(ctx, s.cfg.Releases)
+ if err != nil {
+ return homeOnlyCandidates()
+ }
+ return s.regionCands(snap)
}
// majority picks the most common AWS region, ties to the lexically
@@ -3269,7 +3412,7 @@ var (
)
func TestGuessRegionOrder(t *testing.T) {
- loc := fakeLocator{"81.2.69.142": {-33.9, 151.2}}
+ loc := fakeLocator{"81.2.69.142": {Lat: -33.9, Lon: 151.2}}
cands := []regionCandidate{cHome, cUK, cAU}
cases := []struct {
name string
@@ -3450,13 +3593,20 @@ func guessRegion(cands []regionCandidate, allows func(code string) bool, in gues
Run: `go test ./internal/console -run TestGuessRegion -count=1`
Expected: PASS.
-- [ ] **Step 5: Mutation check**
+- [ ] **Step 5: Mutation checks, one at a time (codex plan-R2 9)**
+
+Each mutation must fail the property test on its own, against a passing
+baseline. So restore and re-run between them:
-Temporarily change `guessRegion`'s first line to `return guessResult{}, false`.
-Run `go test ./internal/console -run TestGuessRegionInvariantProperty -count=1`:
-it must FAIL. Then change the final `if homeIn {` to `if true {` and run
-again: it must FAIL. Revert both changes. Run the step 4 command again, and
-confirm with `git diff --stat` that only the intended files changed.
+1. Run `go test ./internal/console -run TestGuessRegionInvariantProperty -count=1`.
+ It must PASS; this is the baseline.
+2. **Mutation A:** make `guessRegion`'s first line `return guessResult{}, false`.
+ Run the same command. It must FAIL with "guess existence false, want
+ true". Revert, and re-run: PASS.
+3. **Mutation B:** change the final `if homeIn {` to `if true {`. This is a
+ home fallback the policy does not allow. Run the same command. It must
+ FAIL with "guess existence true, want false". Revert, and re-run: PASS.
+4. `git diff --stat` shows only this task's two files.
- [ ] **Step 6: Commit**
@@ -3519,7 +3669,6 @@ import (
"bytes"
"context"
"encoding/json"
- "net"
"net/http"
"strings"
"testing"
@@ -3584,7 +3733,7 @@ func TestAPICreateAppResolvesRegion(t *testing.T) {
// is located; a service principal's never is (spec §6.2).
func TestAPICreateAppLocatesOnlyAPerson(t *testing.T) {
fx, token := regionFixture(t)
- fx.srv.cfg.GeoIP = fakeLocator{"81.2.69.142": {51.5, -0.12}}
+ fx.srv.cfg.GeoIP = fakeLocator{"81.2.69.142": {Lat: 51.5, Lon: -0.12}}
if status, res := postJSONFrom(t, newAppURL(fx), map[string]string{"name": "person"}, token, "81.2.69.142"); status != http.StatusOK ||
res["region"] != "uk" || res["region_source"] != "ip" {
t.Fatalf("person = %d %v", status, res)
@@ -3748,7 +3897,7 @@ func (s *Server) validateExplicitRegion(ctx context.Context, raw string) (string
if published && !(caps[code].Reachable && caps[code].Hosting) {
return "", &doorRefusal{http.StatusBadRequest, msgRegionNotArmed(code, s.namedCodes(hostingCaps(caps)))}
}
- if files, err := readEdgesFilesStrict(ctx, s.cfg.Releases); err == nil {
+ if files, err := readEdgesSnapshot(ctx, s.cfg.Releases); err == nil {
if c, ok := findCandidate(s.regionCands(files), code); ok && c.NoKeyProven {
return "", &doorRefusal{http.StatusBadRequest, msgNoSecretsKey(code)}
}
@@ -3796,7 +3945,7 @@ func (s *Server) resolveNewAppRegion(ctx context.Context, rq regionReq, adopting
if adopting {
return "", "default"
}
- files, err := readEdgesFilesStrict(ctx, s.cfg.Releases)
+ files, err := readEdgesSnapshot(ctx, s.cfg.Releases)
if err != nil {
return "", "default"
}
@@ -3888,7 +4037,7 @@ Pass `rq` to `claimApp`. In `claimOK`:
if err != nil {
log.Printf("console: claim %s/%s: read back region: %v", acct.Sqid, name, err)
}
- files, ferr := readEdgesFilesStrict(r.Context(), s.cfg.Releases)
+ files, ferr := readEdgesSnapshot(r.Context(), s.cfg.Releases)
cands := homeOnlyCandidates()
if ferr == nil {
cands = s.regionCands(files)
@@ -4093,7 +4242,7 @@ func (s *Server) newAppPicker(ctx context.Context, ip net.IP) *regionPickerView
if !s.appRegionsOn() {
return nil
}
- files, err := readEdgesFilesStrict(ctx, s.cfg.Releases)
+ files, err := readEdgesSnapshot(ctx, s.cfg.Releases)
if err != nil {
return nil
}
@@ -4122,7 +4271,7 @@ func (s *Server) pickedRegion(ctx context.Context, r *http.Request) (regionReq,
if v == "" || v == autoRegion {
return rq, true
}
- files, err := readEdgesFilesStrict(ctx, s.cfg.Releases)
+ files, err := readEdgesSnapshot(ctx, s.cfg.Releases)
if err != nil {
return rq, false
}
@@ -4146,7 +4295,7 @@ func (s *Server) handleNewAppRegion(w http.ResponseWriter, r *http.Request) {
jsonError(w, http.StatusForbidden, "Cross-site request blocked.")
return
}
- files, err := readEdgesFilesStrict(r.Context(), s.cfg.Releases)
+ files, err := readEdgesSnapshot(r.Context(), s.cfg.Releases)
cands := homeOnlyCandidates()
if err == nil {
cands = s.regionCands(files)
@@ -4196,7 +4345,7 @@ In `claimOK`, replace the audit line:
detail := "created app " + name
if s.appRegionsOn() {
region, _, _ := s.store.AppRegion(acct.ID, name)
- files, _ := readEdgesFilesStrict(r.Context(), s.cfg.Releases)
+ files, _ := readEdgesSnapshot(r.Context(), s.cfg.Releases)
detail = fmt.Sprintf(regionCopy.AuditCreated, name, s.placeOf(region, s.regionCands(files)))
}
s.audit(acct, name, addr, "create", detail)
@@ -4449,6 +4598,9 @@ func TestExplicitCreateIntoAKeylessRegion(t *testing.T) {
t.Fatalf("keyless replica = %d %s, want C31", status, out)
}
mustCreate(t, u, token, `{"host":"s.go.tito.io","region":"uk","kind":"static"}`)
+ // An addr-backed service proxies to a process the platform never starts,
+ // so it needs no config on the box either (codex plan-R2 7).
+ mustCreate(t, u, token, `{"host":"v.go.tito.io","region":"uk","kind":"service","addrs":["10.0.0.4:8080"]}`)
setEdges(t, fx, map[string]string{"uk-box": ukOldWriter})
mustCreate(t, u, token, `{"host":"o.go.tito.io","region":"uk"}`) // unknown keeps today's behaviour
setEdges(t, fx, map[string]string{"uk-box": ukNoKey})
@@ -4501,7 +4653,7 @@ Expected: FAIL.
// An APP-DERIVED region needs positive eligibility — the secrets key
// included (R9) — and never rides the dormant gate (codex R2-2).
if regionFrom == "app" {
- files, err := readEdgesFilesStrict(ctx, s.cfg.Releases)
+ files, err := readEdgesSnapshot(ctx, s.cfg.Releases)
if err != nil {
log.Printf("console: api instances create %s/%s: read edges: %v", acct.Sqid, app, err)
jsonError(w, http.StatusServiceUnavailable, msgAppRegionUnconfirmed(s.homeRegionOut(region)))
@@ -4547,7 +4699,7 @@ of its final `switch` (`:852`):
// CURRENT writer proves the region keyless; unknown keeps today's
// behaviour, so roll order never refuses a working region.
if probe.DerivesPaths() || probe.Role == "replica" {
- if files, err := readEdgesFilesStrict(ctx, s.cfg.Releases); err == nil {
+ if files, err := readEdgesSnapshot(ctx, s.cfg.Releases); err == nil {
if c, ok := findCandidate(s.regionCands(files), region); ok && c.NoKeyProven {
jsonError(w, http.StatusBadRequest, msgNoSecretsKey(s.homeRegionOut(region)))
return
@@ -4573,7 +4725,7 @@ At `:1066`, after `out := recordView(rec)`:
```go
out.RegionFrom = regionFrom
if rec.Role == "" && placed == "" {
- out.RegionName = s.placeOf(rec.Region, s.regionCands(readEdgesFiles(ctx, s.cfg.Releases)))
+ out.RegionName = s.placeOf(rec.Region, s.candsOrHome(ctx))
}
```
@@ -4583,6 +4735,11 @@ Run: `go test ./internal/console -count=1`
Expected: PASS, including `TestAPIInstanceCreate*` and the
placement-door tests.
+Then a mutation check on the R9 gate's scope. Change
+`if probe.DerivesPaths() || probe.Role == "replica" {` to `if true {`, and
+run `go test ./internal/console -run TestExplicitCreateIntoAKeylessRegion -count=1`.
+It must FAIL on the static or addr-backed create. Revert, and re-run: PASS.
+
- [ ] **Step 7: Commit**
```bash
@@ -4608,12 +4765,14 @@ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```go
type appDefaultView struct {
- Region string `json:"region"` // the STORED region, under its name
- Name string `json:"name"`
- Source string `json:"source"`
- Effective string `json:"effective"` // where a no-region create lands
- Available *bool `json:"available,omitempty"`
- Reason string `json:"reason,omitempty"`
+ Region string `json:"region"` // the STORED region, under its name; "" when unknown
+ Name string `json:"name"`
+ Source string `json:"source"`
+ Effective string `json:"effective"` // where a no-region create lands
+ Available *bool `json:"available,omitempty"`
+ Reason string `json:"reason,omitempty"`
+ RegionKnown bool `json:"region_known"` // the stored region was read
+ PlacementKnown bool `json:"placement_known"` // the manifest and fleet placement were read
}
func (s *Server) appDefault(ctx context.Context, acct *Account, app string) appDefaultView
func (s *Server) handleAPIRegionGuess(w http.ResponseWriter, r *http.Request)
@@ -4622,6 +4781,12 @@ func (s *Server) handleAPIRegionGuess(w http.ResponseWriter, r *http.Request)
`GET …/regions` entries gain `aws_region` and `name`. The response gains
`app_default`.
+`region_known` and `placement_known` are additive fields this plan adds to
+spec §9.3's block (codex plan-R2 3). A response is always HTTP 200, so the
+CLI needs them to tell a confirmed fact from one the console could not read.
+`deploy --region` needs the stored region, and `canary --region` needs the
+fleet placement. Each refuses when its fact is unknown (Tasks 21, 22).
+
- [ ] **Step 1: Write the failing tests**
```go
@@ -4631,6 +4796,7 @@ package console
import (
"context"
"encoding/json"
+ "errors"
"io"
"net/http"
"strings"
@@ -4718,6 +4884,25 @@ func TestAppDefaultReasons(t *testing.T) {
}
}
+// codex plan-R2 3: a fact the console could not read is never presented as
+// known: no home-valued region, no non-fleet effective.
+func TestAppDefaultKnownFlags(t *testing.T) {
+ acct := &Account{ID: OpsAccountID, Sqid: "bab"}
+ fx, _, _ := fillFixture(t, "uk")
+ if d := fx.srv.appDefault(t.Context(), acct, "titogo"); !d.RegionKnown || !d.PlacementKnown || d.Region != "uk" {
+ t.Fatalf("readable = %+v", d)
+ }
+ fx.srv.appRegionOf = func(int64, string) (string, string, error) { return "", "", errors.New("db gone") }
+ if d := fx.srv.appDefault(t.Context(), acct, "titogo"); d.RegionKnown || !d.PlacementKnown || d.Region != "" || d.Effective != "" || d.Available != nil {
+ t.Fatalf("unreadable region = %+v, want region unknown and nothing presented as fact", d)
+ }
+ fx2, _, _ := fillFixture(t, "uk")
+ corruptManifest(t, fx2)
+ if d := fx2.srv.appDefault(t.Context(), acct, "titogo"); !d.RegionKnown || d.PlacementKnown || d.Reason != "policy-unreadable" {
+ t.Fatalf("unreadable policy = %+v, want placement unknown", d)
+ }
+}
+
func TestRegionGuessEndpoint(t *testing.T) {
fx, token, _ := fillFixture(t, "")
status, _, out := rawReq(t, http.MethodPost, guessURL(fx), token, `{"tz":"Europe/London"}`)
@@ -4818,31 +5003,34 @@ import (
// appDefaultView is §9.3's app_default. region is the STORED region (what
// deploy --region compares against); effective is where a no-region create
// lands — the fleet itself for a placed app, never the platform home
-// (codex R2-4).
+// (codex R2-4). RegionKnown and PlacementKnown say which facts were READ:
+// an unread fact is left empty, never filled with the home (codex
+// plan-R2 3).
type appDefaultView struct {
- Region string `json:"region"`
- Name string `json:"name"`
- Source string `json:"source"`
- Effective string `json:"effective"`
- Available *bool `json:"available,omitempty"`
- Reason string `json:"reason,omitempty"`
+ Region string `json:"region"`
+ Name string `json:"name"`
+ Source string `json:"source"`
+ Effective string `json:"effective"`
+ Available *bool `json:"available,omitempty"`
+ Reason string `json:"reason,omitempty"`
+ RegionKnown bool `json:"region_known"`
+ PlacementKnown bool `json:"placement_known"`
}
func boolp(b bool) *bool { return &b }
func (s *Server) appDefault(ctx context.Context, acct *Account, app string) appDefaultView {
- stored, source, err := s.appRegion(acct.ID, app)
- files, ferr := readEdgesFilesStrict(ctx, s.cfg.Releases)
+ stored, source, rerr := s.appRegion(acct.ID, app)
+ snap, ferr := readEdgesSnapshot(ctx, s.cfg.Releases)
cands := homeOnlyCandidates()
if ferr == nil {
- cands = s.regionCands(files)
- }
- v := appDefaultView{Region: s.homeRegionOut(stored), Name: s.placeOf(stored, cands), Source: source, Effective: s.homeRegionOut(stored)}
- if err != nil {
- v.Reason = "policy-unreadable"
- return v
+ cands = s.regionCands(snap)
}
pol := s.appPolicy(ctx, acct, app)
+ v := appDefaultView{RegionKnown: rerr == nil, PlacementKnown: pol.State != policyUnreadable}
+ if rerr == nil {
+ v.Region, v.Name, v.Source, v.Effective = s.homeRegionOut(stored), s.placeOf(stored, cands), source, s.homeRegionOut(stored)
+ }
switch {
case pol.State == policyUnreadable:
v.Reason = "policy-unreadable"
@@ -4853,6 +5041,10 @@ func (s *Server) appDefault(ctx context.Context, acct *Account, app string) appD
case pol.pooled():
v.Effective, v.Reason, v.Available = s.homeRegionOut(""), "pooled", boolp(true)
return v
+ case rerr != nil:
+ // The stored region is unknown, so where a create lands is too.
+ v.Reason = "policy-unreadable"
+ return v
case ferr != nil:
v.Reason = "edges-unreadable"
return v
@@ -4895,7 +5087,7 @@ func (s *Server) handleAPIRegionGuess(w http.ResponseWriter, r *http.Request) {
if ip == nil {
ip = s.personIP(r)
}
- files, err := readEdgesFilesStrict(r.Context(), s.cfg.Releases)
+ files, err := readEdgesSnapshot(r.Context(), s.cfg.Releases)
cands := homeOnlyCandidates()
if err == nil {
cands = s.regionCands(files)
@@ -4924,7 +5116,7 @@ In `apiregions.go`, change the gate line to
Name string `json:"name,omitempty"`
}
set, _ := armedPlacements(r.Context(), s.cfg.Releases)
- cands := s.regionCands(readEdgesFiles(r.Context(), s.cfg.Releases))
+ cands := s.candsOrHome(r.Context())
named := func(code string) region {
c, _ := findCandidate(cands, s.homeRegionIn(code))
return region{Code: code, AWSRegion: c.AWSRegion, Name: s.placeOf(s.homeRegionIn(code), cands)}
@@ -4956,16 +5148,30 @@ In `authcli.go`:
s.cliRoute("POST /api/cli/apps/{account}/{app}/regions/guess", svcrole.Allow(svcrole.Instance), s.handleAPIRegionGuess)
```
+Then add the new route to `exemptFromLegacyPin` in
+`internal/console/svcroutes_test.go`, beside `GET …/regions`.
+`TestInstanceRoleMatchesTodaysAppTokenReach` pins every `instance`-role route
+to what an app token reached before service principals, and the guess route
+did not exist then:
+
+```go
+ // The region guess (nearest-region spec §9.4) stands on GET .../regions'
+ // footing: the same deployment topology, filtered by the app's own
+ // policy, for a hint the caller supplies. It did not exist before
+ // service principals, and titogo's per-instance picker is its consumer.
+ "POST /api/cli/apps/{account}/{app}/regions/guess": true,
+```
+
- [ ] **Step 5: Run the tests to verify they pass**
Run: `go test ./internal/console -count=1`
Expected: PASS, including the existing `/regions` tests, whose `regions` and
-`default` must be unchanged.
+`default` must be unchanged, and `TestInstanceRoleMatchesTodaysAppTokenReach`.
- [ ] **Step 6: Commit**
```bash
-git add internal/console/regiondefault.go internal/console/regiondefault_test.go internal/console/apiregions.go internal/console/authcli.go
+git add internal/console/regiondefault.go internal/console/regiondefault_test.go internal/console/apiregions.go internal/console/authcli.go internal/console/svcroutes_test.go
git commit -m "console: app_default with every reason, place names, POST regions/guess (nearest-region §9.3, §9.4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
@@ -5061,7 +5267,7 @@ In `handleAppInstances`, after the placement section:
case d.Region != s.cfg.HomeRegion:
p.RegionLine = fmt.Sprintf(regionCopy.StartsIn, d.Name)
default:
- if files, err := readEdgesFilesStrict(r.Context(), s.cfg.Releases); err == nil && eligibleCount(s.regionCands(files)) >= 2 {
+ if files, err := readEdgesSnapshot(r.Context(), s.cfg.Releases); err == nil && eligibleCount(s.regionCands(files)) >= 2 {
p.RegionLine = fmt.Sprintf(regionCopy.StartsIn, d.Name)
}
}
@@ -5242,14 +5448,14 @@ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
---
-### Task 18: CLI copy (C9 to C15, C13a, C13b, C15b, C22, C25, C30, C30b, from the record)
+### Task 18: CLI copy (C9 to C15, C13a, C13b, C15b, C15c, C22, C25, C30, C30b, from the record)
**Files:**
- Create: `region_copy.go` (root, package `main`)
- Test: `region_copy_test.go`
**Interfaces:**
-- Consumes: the record's C9 to C15, C13a, C13b, C15b, C22a to C22d, C25, C30
+- Consumes: the record's C9 to C15, C13a, C13b, C15b, C15c, C22a to C22d, C25, C30
and C30b (Task 0).
- Produces:
@@ -5260,6 +5466,7 @@ func regionNotAppliedSwitchOff(place string) string // C13b
func instanceCreatedSuffix(channel, place string) string // C14
func deployRegionMismatch(app, place string) string // C15
func deployRegionUnreadable(app string, err error) string // C15b
+func deployRegionUnknown(app string) string // C15c
func canaryRegionMismatch(host, place string) string // C25
func canaryRegionOnFleet(app, fleet string) string // C30
func canaryFleetUnknown(app string) string // C30b
@@ -5292,7 +5499,7 @@ func TestRegionCLICopy(t *testing.T) {
all := []string{
regionNotRecordedOldConsole, regionNotAppliedSwitchOff("P"),
instanceCreatedSuffix("stable", "P"), deployRegionMismatch("blog", "P"),
- deployRegionUnreadable("blog", errors.New("timeout")),
+ deployRegionUnreadable("blog", errors.New("timeout")), deployRegionUnknown("blog"),
canaryRegionMismatch("c.go.tito.io", "P"), canaryRegionOnFleet("blog", "acme"),
canaryFleetUnknown("blog"),
flagRegionAppsCreate, flagRegionDeploy, flagRegionCanary, flagRegionInstancesCreate,
@@ -5359,6 +5566,11 @@ func deployRegionUnreadable(app string, err error) string {
return fmt.Sprintf("could not read %s's region (%v), so deploy stopped before shipping anything. Try again, or run without --region.", app, err)
}
+// C15c: the console answered, but could not read the app's region.
+func deployRegionUnknown(app string) string {
+ return fmt.Sprintf("the console could not read %s's region, so deploy stopped before shipping anything. Try again, or run without --region.", app)
+}
+
// C25
func canaryRegionMismatch(host, place string) string {
return fmt.Sprintf("canary %s already runs in %s. --region applies only when canary creates the host. Run carlos canary remove first to start it somewhere else.", host, place)
@@ -5525,9 +5737,11 @@ Run:
```bash
go test . -run TestLocalTimezone -count=1
+fail=0
for t in darwin/arm64 darwin/amd64 linux/arm64 windows/amd64 windows/arm64; do
- CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; break; }
+ CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; fail=1; }
done
+[ "$fail" = 0 ] || exit 1 # stops this block before any commit below it
```
Expected: PASS, and no `FAILED` line.
@@ -5653,8 +5867,24 @@ func (s *apiSession) createApp(ctx context.Context, a apiAcct, app, place string
}{app, place, controlPlane, buildProbe, region, timezone}
```
-Leave the rest of the function unchanged. Update every other call site with
-`, "", ""` (tests) or the real values (steps 4 and 5).
+Leave the rest of the function unchanged. Update every other call site
+(codex plan-R2 5). The callers are:
+
+- the tests: `apimode_test.go:3041`, `:3052`, `:3060`, `:3066`, `:3099`,
+ `:3106` and `apps_test.go:311`, each gaining `, "", ""`;
+- `apps.go:497` and `deploy.go:536`, which get the real values in steps 4
+ and 5.
+
+`offerCreateApp` also changes signature in step 5, so update its one test
+caller, `deploy_wizard_test.go:104`, to pass `""` as the new last argument:
+
+```go
+ app := offerCreateApp(context.Background(), s, apiAcct{Seg: "bdk", Exact: true},
+ &noAppsError{Account: "tom@keymail.dev"}, "")
+```
+
+Run `grep -n 'createApp(\|offerCreateApp(' *.go` afterwards. Every match
+must carry the new arity.
- [ ] **Step 4: Add `printAppRegion` and wire `apps create`**
@@ -5737,7 +5967,7 @@ reaches a real console.
- [ ] **Step 7: Commit**
```bash
-git add apimode.go apps.go apps_region.go apps_region_test.go deploy.go apps_test.go
+git add apimode.go apps.go apps_region.go apps_region_test.go deploy.go apps_test.go apimode_test.go deploy_wizard_test.go
git commit -m "cli: apps create and deploy onboarding send a timezone, take --region, print the region line (nearest-region §11)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
@@ -5753,11 +5983,16 @@ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
- Test: `deploy_region_test.go`, `deploy_region_e2e_test.go`
**Interfaces:**
-- Consumes: `deployRegionMismatch` and `deployRegionUnreadable` (Task 18).
+- Consumes: `deployRegionMismatch`, `deployRegionUnreadable` and
+ `deployRegionUnknown` (Task 18).
- Produces:
```go
-type apiAppDefault struct{ Region, Name, Source, Effective, Reason string; Available *bool }
+type apiAppDefault struct {
+ Region, Name, Source, Effective, Reason string
+ Available *bool
+ RegionKnown, PlacementKnown bool
+}
type apiRegionEntry struct{ Code, Name string }
type apiRegionsResult struct {
Regions []apiRegionEntry `json:"regions"`
@@ -5785,8 +6020,8 @@ import (
// codex plan-R1 2: a READ failure stops deploy; only a successful response
// without app_default (an older console) skips the check.
func TestDeployRegionPreflight(t *testing.T) {
- uk := &apiAppDefault{Region: "uk", Name: "P-uk", Effective: "uk"}
- fleet := &apiAppDefault{Region: "uk", Name: "P-uk", Effective: "fleet/acme", Reason: "fleet"}
+ uk := &apiAppDefault{Region: "uk", Name: "P-uk", Effective: "uk", RegionKnown: true, PlacementKnown: true}
+ fleet := &apiAppDefault{Region: "uk", Name: "P-uk", Effective: "fleet/acme", Reason: "fleet", RegionKnown: true, PlacementKnown: true}
boom := errors.New("500 from console")
cases := []struct {
name string
@@ -5802,6 +6037,8 @@ func TestDeployRegionPreflight(t *testing.T) {
{"read failed", apiRegionsResult{}, boom, "ie", deployRegionUnreadable("blog", boom)},
{"fleet, stored equal", apiRegionsResult{AppDefault: fleet}, nil, "uk", ""},
{"fleet, stored different", apiRegionsResult{AppDefault: fleet}, nil, "ie", deployRegionMismatch("blog", "P-uk")},
+ // codex plan-R2 3: a 200 whose stored region could not be read.
+ {"region unknown", apiRegionsResult{AppDefault: &apiAppDefault{RegionKnown: false, PlacementKnown: true}}, nil, "ie", deployRegionUnknown("blog")},
}
for _, c := range cases {
err := deployRegionPreflight(c.res, c.err, "blog", c.asked)
@@ -5830,12 +6067,14 @@ In `apimode.go`:
// GET …/regions (nearest-region spec §9.3). AppDefault is nil from a
// console that predates it.
type apiAppDefault struct {
- Region string `json:"region"`
- Name string `json:"name"`
- Source string `json:"source"`
- Effective string `json:"effective"`
- Reason string `json:"reason"`
- Available *bool `json:"available"`
+ Region string `json:"region"`
+ Name string `json:"name"`
+ Source string `json:"source"`
+ Effective string `json:"effective"`
+ Reason string `json:"reason"`
+ Available *bool `json:"available"`
+ RegionKnown bool `json:"region_known"`
+ PlacementKnown bool `json:"placement_known"`
}
type apiRegionEntry struct {
@@ -5880,9 +6119,11 @@ In `deploy.go`:
```go
// deployRegionPreflight is spec §11: --region on an EXISTING app is
// compared with the app's STORED region before anything ships. A failed
-// read STOPS the deploy (C15b): skipping it would ship to whatever the app
-// already is. Only a successful response without app_default — an older
-// console — skips the check.
+// read STOPS the deploy (C15b), and so does a successful answer whose
+// stored region the console could not read (C15c, codex plan-R2 3):
+// either way the region is unknown, and shipping would go to whatever the
+// app already is. Only a successful response without app_default — an
+// older console — skips the check.
func deployRegionPreflight(res apiRegionsResult, readErr error, app, asked string) error {
if asked == "" {
return nil
@@ -5890,7 +6131,13 @@ func deployRegionPreflight(res apiRegionsResult, readErr error, app, asked strin
if readErr != nil {
return errors.New(deployRegionUnreadable(app, readErr))
}
- if res.AppDefault == nil || res.AppDefault.Region == asked {
+ if res.AppDefault == nil {
+ return nil
+ }
+ if !res.AppDefault.RegionKnown {
+ return errors.New(deployRegionUnknown(app))
+ }
+ if res.AppDefault.Region == asked {
return nil
}
return errors.New(deployRegionMismatch(app, res.AppDefault.Name))
@@ -5934,7 +6181,6 @@ console, and no release object lands in the store.
package main
import (
- "bytes"
"context"
"encoding/json"
"net/http"
@@ -5955,20 +6201,23 @@ import (
// signed-in CLI credentials saved, and a front that can fail or strip the
// /regions answer and counts every write reaching the console.
type regionE2E struct {
+ bin string // built before useTempCredentials changes directory
+ creds string
url string
store release.Store
- mode atomic.Value // "", "fail", "legacy"
+ mode atomic.Value // "", "fail", "legacy", "region-unknown", "placement-unknown"
writes atomic.Int64
cst *console.Store
}
func regionE2EConsole(t *testing.T, appRegion string) *regionE2E {
t.Helper()
- useTempCredentials(t)
+ bin := buildCarlos(t) // first: useTempCredentials chdirs out of the module
+ creds := useTempCredentials(t)
clearDeploymentEnv(t)
const addr, app = "paul@keymail.dev", "titogo"
tmp := t.TempDir()
- e := &regionE2E{store: release.NewDirStore(filepath.Join(tmp, "store"))}
+ e := &regionE2E{bin: bin, creds: creds, store: release.NewDirStore(filepath.Join(tmp, "store"))}
e.mode.Store("")
seed, pubB64, err := release.GenerateKey()
if err != nil {
@@ -5997,12 +6246,22 @@ func regionE2EConsole(t *testing.T, appRegion string) *regionE2E {
case "fail":
http.Error(w, `{"error":"boom"}`, http.StatusInternalServerError)
return
- case "legacy":
+ case "legacy", "region-unknown", "placement-unknown":
+ // A successful 200 whose body an older console, or a
+ // console that could not read one fact, would send.
rec := httptest.NewRecorder()
srv.ServeHTTP(rec, r)
var m map[string]any
json.Unmarshal(rec.Body.Bytes(), &m)
- delete(m, "app_default")
+ d, _ := m["app_default"].(map[string]any)
+ switch e.mode.Load().(string) {
+ case "legacy":
+ delete(m, "app_default")
+ case "region-unknown":
+ d["region_known"], d["region"], d["effective"] = false, "", ""
+ case "placement-unknown":
+ d["placement_known"], d["reason"] = false, "policy-unreadable"
+ }
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(m)
return
@@ -6044,8 +6303,8 @@ func regionE2EConsole(t *testing.T, appRegion string) *regionE2E {
// config in reach.
func (e *regionE2E) run(t *testing.T, args ...string) (string, error) {
t.Helper()
- cmd := exec.Command(buildCarlos(t), args...)
- cmd.Env = carlosTestEnv(t, "CARLOS_CREDENTIALS="+os.Getenv("CARLOS_CREDENTIALS"))
+ cmd := exec.Command(e.bin, args...)
+ cmd.Env = carlosTestEnv(t, "CARLOS_CREDENTIALS="+e.creds)
cmd.Dir = t.TempDir()
out, err := cmd.CombinedOutput()
return string(out), err
@@ -6077,22 +6336,27 @@ func artifact(t *testing.T) string {
func TestDeployRegionRefusalsShipNothing(t *testing.T) {
for _, c := range []struct{ mode, want string }{
- {"", "already runs in"}, // the mismatch (C15); the stored region is uk
- {"fail", "deploy stopped before shipping anything"}, // C15b
+ {"", deployRegionMismatch("titogo", "")[:20]}, // C15: the stored region is uk
+ {"fail", "deploy stopped before shipping anything"}, // C15b
+ {"region-unknown", deployRegionUnknown("titogo")[:30]}, // C15c: a 200 that does not know
} {
- e := regionE2EConsole(t, "uk")
- e.mode.Store(c.mode)
- out, err := e.run(t, "deploy", "--app", "titogo", "--region", "ie", "--console", e.url,
- "--version", "abc1234", "--timeout", "1s", artifact(t))
- if err == nil || !bytes.Contains([]byte(out), []byte(c.want)) {
- t.Fatalf("mode %q: err=%v out=%s, want a refusal containing %q", c.mode, err, out, c.want)
- }
- if n := e.writes.Load(); n != 0 {
- t.Fatalf("mode %q: %d write(s) reached the console before the refusal", c.mode, n)
- }
- if n := e.releases(t); n != 0 {
- t.Fatalf("mode %q: %d release object(s) were shipped", c.mode, n)
- }
+ // A subtest each: useTempCredentials chdirs, and only a subtest
+ // restores the directory before the next case builds the binary.
+ t.Run("mode="+c.mode, func(t *testing.T) {
+ e := regionE2EConsole(t, "uk")
+ e.mode.Store(c.mode)
+ out, err := e.run(t, "deploy", "--app", "titogo", "--region", "ie", "--console", e.url,
+ "--channel", "canary/e2e", "--version", "abc1234", "--timeout", "1s", artifact(t))
+ if err == nil || !strings.Contains(out, c.want) {
+ t.Fatalf("err=%v out=%s, want a refusal containing %q", err, out, c.want)
+ }
+ if n := e.writes.Load(); n != 0 {
+ t.Fatalf("%d write(s) reached the console before the refusal", n)
+ }
+ if n := e.releases(t); n != 0 {
+ t.Fatalf("%d release object(s) were shipped", n)
+ }
+ })
}
}
@@ -6102,22 +6366,24 @@ func TestDeployRegionSkipsAgainstAnOlderConsole(t *testing.T) {
e := regionE2EConsole(t, "uk")
e.mode.Store("legacy")
out, _ := e.run(t, "deploy", "--app", "titogo", "--region", "ie", "--console", e.url,
- "--version", "abc1234", "--timeout", "1s", artifact(t))
- if strings.Contains(out, "already runs in") || strings.Contains(out, "deploy stopped before shipping") {
+ "--channel", "canary/e2e", "--version", "abc1234", "--timeout", "1s", artifact(t))
+ if strings.Contains(out, deployRegionMismatch("titogo", "")[:20]) || strings.Contains(out, "deploy stopped before shipping") {
t.Fatalf("an older console refused: %s", out)
}
- if e.writes.Load() == 0 {
- t.Fatalf("deploy never reached the ship: %s", out)
+ if e.writes.Load() == 0 || e.releases(t) == 0 {
+ t.Fatalf("deploy never shipped: %s", out)
}
}
```
-The two `want` fragments are substrings of the draft C15 and C15b. If the
-approved text differs, use substrings of the approved text.
+The `want` fragments come from the copy functions themselves, so they follow
+the approved text. The C15b fragment is a literal substring of its draft; if
+review changed it, use a substring of the approved text.
`useTempCredentials`, `clearDeploymentEnv`, `e2eDeviceLogin`,
`loadCredentials`, `credential` and `buildCarlos` are the existing root test
helpers used by `TestE2EDeployThroughConsole`
-(`deploy_e2e_test.go:266-330`).
+(`deploy_e2e_test.go:266-330`). `useTempCredentials` calls `t.Chdir`
+(`auth_test.go:26`), which is why the binary is built first.
- [ ] **Step 6: Run the command-level tests**
@@ -6149,11 +6415,19 @@ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
- Consumes:
- Task 18: `canaryRegionOnFleet`, `canaryFleetUnknown`,
`canaryRegionMismatch` and `instanceCreatedSuffix`;
- - Task 21: `appRegions`, `apiRegionsResult.nameOf` and `regionE2EConsole`.
+ - Task 21: `appRegions`, `apiRegionsResult.nameOf`, `apiAppDefault`
+ with `PlacementKnown`, and `regionE2EConsole`.
- Produces:
- - `func canaryRegionCheck(res apiRegionsResult, readErr error, recs []apiInstanceRecord, app, host, asked string) error`;
+ - `func canaryRegionCheck(res apiRegionsResult, readErr error, recs []apiInstanceRecord, app, hostPrefix, asked string) error`;
- `func instanceCreatedLine(rec apiInstanceRecord, envSuffix string) string`.
+The check runs once, before `instancesEnable` (`canary.go:381`), which is a
+PUT. It uses only reads: `GET …/regions`, and `GET …/instances`, which lists
+records with or without a manifest (`apiinstances.go:409-420`). An existing
+canary is found by its host prefix, `<name>.<app>.` (`canaryHost`,
+`canary.go:210`), so the domain the PUT would return is not needed (codex
+plan-R2 4).
+
- [ ] **Step 1: Write the failing unit tests**
```go
@@ -6167,12 +6441,16 @@ import (
func TestCanaryRegionCheck(t *testing.T) {
names := []apiRegionEntry{{Code: "ie", Name: "P-ie"}, {Code: "uk", Name: "P-uk"}}
- plain := apiRegionsResult{Regions: names, Default: "ie", AppDefault: &apiAppDefault{Region: "uk", Effective: "uk"}}
- fleet := apiRegionsResult{Regions: names, Default: "ie", AppDefault: &apiAppDefault{Region: "uk", Effective: "fleet/acme", Reason: "fleet"}}
+ known := func(d apiAppDefault) *apiAppDefault { d.RegionKnown, d.PlacementKnown = true, true; return &d }
+ plain := apiRegionsResult{Regions: names, Default: "ie", AppDefault: known(apiAppDefault{Region: "uk", Effective: "uk"})}
+ fleet := apiRegionsResult{Regions: names, Default: "ie", AppDefault: known(apiAppDefault{Region: "uk", Effective: "fleet/acme", Reason: "fleet"})}
older := apiRegionsResult{Regions: names, Default: "ie"} // no app_default
+ unread := apiRegionsResult{Regions: names, Default: "ie", AppDefault: &apiAppDefault{Region: "uk", Effective: "uk", RegionKnown: true, Reason: "policy-unreadable"}}
boom := errors.New("timeout")
- existingUK := []apiInstanceRecord{{Host: "c.go.tito.io", Region: "uk"}}
- existingHome := []apiInstanceRecord{{Host: "c.go.tito.io", Region: ""}}
+ const prefix = "c.blog."
+ existingUK := []apiInstanceRecord{{Host: "c.blog.go.tito.io", Region: "uk"}}
+ existingHome := []apiInstanceRecord{{Host: "c.blog.go.tito.io", Region: ""}}
+ other := []apiInstanceRecord{{Host: "d.blog.go.tito.io", Region: "uk"}}
cases := []struct {
name string
res apiRegionsResult
@@ -6183,21 +6461,20 @@ func TestCanaryRegionCheck(t *testing.T) {
}{
{"no flag", fleet, boom, existingUK, "", ""},
{"new canary", plain, nil, nil, "uk", ""},
+ {"another canary is not this one", plain, nil, other, "ie", ""},
{"existing, same", plain, nil, existingUK, "uk", ""},
{"existing home read as its name", plain, nil, existingHome, "ie", ""},
- // codex plan-R1 18: the place name, not the bare code.
- {"existing, different", plain, nil, existingUK, "ie", canaryRegionMismatch("c.go.tito.io", "P-uk")},
- // codex R4-2: refused identically before and after the canary exists.
+ {"existing, different, named by place", plain, nil, existingUK, "ie", canaryRegionMismatch("c.blog.go.tito.io", "P-uk")},
{"fleet, first run", fleet, nil, nil, "uk", canaryRegionOnFleet("blog", "acme")},
{"fleet, repeat", fleet, nil, existingUK, "uk", canaryRegionOnFleet("blog", "acme")},
- // codex plan-R1 3: an older console cannot rule a fleet out, so the
- // flag is refused on every run, never honoured on the first.
{"older console, first run", older, nil, nil, "uk", canaryFleetUnknown("blog")},
{"older console, repeat", older, nil, existingUK, "uk", canaryFleetUnknown("blog")},
{"read failed", plain, boom, nil, "uk", canaryFleetUnknown("blog")},
+ // codex plan-R2 3: a 200 whose placement the console could not read.
+ {"placement unknown", unread, nil, nil, "uk", canaryFleetUnknown("blog")},
}
for _, c := range cases {
- err := canaryRegionCheck(c.res, c.err, c.recs, "blog", "c.go.tito.io", c.asked)
+ err := canaryRegionCheck(c.res, c.err, c.recs, "blog", prefix, c.asked)
got := ""
if err != nil {
got = err.Error()
@@ -6224,9 +6501,6 @@ func TestInstanceCreatedLine(t *testing.T) {
}
```
-`apiInstanceRecord`'s `Host`, `Channel` and `Region` fields are the ones at
-`apimode.go:2840-2870`.
-
- [ ] **Step 2: Run them to verify they fail**
Run: `go test . -run 'TestCanaryRegionCheck|TestInstanceCreatedLine' -count=1`
@@ -6244,27 +6518,28 @@ Add to `apiInstanceRecord`:
In `canary.go`:
```go
-// canaryRegionCheck is spec §11's canary --region rule, resolved BEFORE
-// the create/rechannel/ready branch, so the answer never depends on
-// whether the canary exists yet (codex R4-2):
+// canaryRegionCheck is spec §11's canary --region rule, run before ANY
+// write, so the answer never depends on whether the canary exists yet
+// (codex R4-2, plan-R2 4):
// - the flag needs a console that can say whether a fleet governs; a
-// failed read, or an older console with no app_default, refuses it on
-// every run (C30b), because the create door would silently replace it
-// with the fleet;
+// failed read, an older console with no app_default, or one that
+// could not read the placement, refuses it on every run (C30b),
+// because the create door would silently replace it with the fleet;
// - a fleet-placed app refuses the flag every time (C30);
-// - an existing canary in another region refuses (C25), named by place.
-func canaryRegionCheck(res apiRegionsResult, readErr error, recs []apiInstanceRecord, app, host, asked string) error {
+// - an existing canary (a record under hostPrefix, "<name>.<app>.") in
+// another region refuses (C25), named by place.
+func canaryRegionCheck(res apiRegionsResult, readErr error, recs []apiInstanceRecord, app, hostPrefix, asked string) error {
if asked == "" {
return nil
}
- if readErr != nil || res.AppDefault == nil {
+ if readErr != nil || res.AppDefault == nil || !res.AppDefault.PlacementKnown {
return errors.New(canaryFleetUnknown(app))
}
if strings.HasPrefix(res.AppDefault.Effective, "fleet/") {
return errors.New(canaryRegionOnFleet(app, strings.TrimPrefix(res.AppDefault.Effective, "fleet/")))
}
for _, r := range recs {
- if r.Host != host {
+ if !strings.HasPrefix(r.Host, hostPrefix) {
continue
}
have := r.Region
@@ -6272,7 +6547,7 @@ func canaryRegionCheck(res apiRegionsResult, readErr error, recs []apiInstanceRe
have = res.Default
}
if have != asked {
- return errors.New(canaryRegionMismatch(host, res.nameOf(have)))
+ return errors.New(canaryRegionMismatch(r.Host, res.nameOf(have)))
}
}
return nil
@@ -6293,43 +6568,34 @@ func instanceCreatedLine(rec apiInstanceRecord, envSuffix string) string {
}
```
-- [ ] **Step 4: Wire the flags and the output**
+- [ ] **Step 4: Wire the flags, the check and the output**
-In `runCanaryDeploy`:
+In `runCanaryDeploy`'s flags:
```go
regionFlag := fs.String("region", "", flagRegionCanary)
```
-The check runs twice, so that every refusal precedes every write.
-`instancesEnable` (`canary.go:381`) is a PUT, so the fleet and console rules
-must run before it.
-
-Directly after `app2 := resolvedApp(...)`:
+After `ctx := context.Background()` and `channel := canaryChannel(canary)`
+(`canary.go:375-377`), and **before** the `instancesEnable` call:
```go
- asked := strings.TrimSpace(*regionFlag)
- var regionRes apiRegionsResult
- if asked != "" {
- res, err := sess.appRegions(ctx, acct, app2)
- // No records yet: this pass applies the console and fleet rules only.
- if err := canaryRegionCheck(res, err, nil, app2, "", asked); err != nil {
+ // --region is checked with reads only, before the first write (the
+ // instancesEnable PUT below): nothing changes when it is refused.
+ if asked := strings.TrimSpace(*regionFlag); asked != "" {
+ res, rerr := sess.appRegions(ctx, acct, app2)
+ existing, _, _, lerr := sess.instanceList(ctx, acct, app2, "", "")
+ if lerr != nil {
+ log.Fatalf("canary: list instances for %s: %v", app2, lerr)
+ }
+ if err := canaryRegionCheck(res, rerr, existing, app2, canary+"."+app2+".", asked); err != nil {
log.Fatalf("canary: %v", err)
}
- regionRes = res
}
```
-After `recs` is listed and before `canaryRecordPlan`, add the existing-canary
-rule:
-
-```go
- if asked != "" {
- if err := canaryRegionCheck(regionRes, nil, recs, app2, host, asked); err != nil {
- log.Fatalf("canary: %v", err)
- }
- }
-```
+`ctx` is declared at `canary.go:375`, so the block must come after it (codex
+plan-R2 5).
In the create branch, pass the flag:
`sess.instanceCreate(ctx, acct, app2, host, strings.TrimSpace(*regionFlag), channel, recKind, env, nil, nil, nil)`.
@@ -6348,7 +6614,7 @@ the output to:
instanceCreatedLine(rec, environmentSuffix(rec.Environment)))
```
-- [ ] **Step 5: Write the command-level test (codex plan-R1 3, 12)**
+- [ ] **Step 5: Write the command-level test (codex plan-R1 3, 12; plan-R2 3, 4)**
```go
// canary_region_e2e_test.go
@@ -6356,50 +6622,80 @@ package main
import (
"context"
+ "errors"
+ "os"
"strings"
"testing"
+ "time"
"github.com/carlosframework/platform/internal/console"
"github.com/carlosframework/platform/internal/provision"
"github.com/carlosframework/platform/internal/release"
)
-// Two identical runs, against a fleet-placed app on the current console and
-// against an older console's /regions: the same refusal both times, and no
-// write, record or upload in between.
-func TestCanaryRegionRefusesIdenticallyAndWritesNothing(t *testing.T) {
- for _, c := range []struct{ name, mode, want string }{
- {"fleet-placed app", "", canaryRegionOnFleet("titogo", "acme")},
- {"older console", "legacy", canaryFleetUnknown("titogo")},
- } {
- e := regionE2EConsole(t, "")
- if err := e.cst.SetAppPlacement(console.OpsAccountID, "titogo", "fleet/acme"); err != nil {
- t.Fatal(err)
- }
- e.mode.Store(c.mode)
- var outs []string
- for run := 0; run < 2; run++ {
- out, err := e.run(t, "canary", "deploy", "--app", "titogo", "--name", "probe", "--region", "uk",
- "--console", e.url, "--version", "abc1234", "--timeout", "1s", artifact(t))
- if err == nil || !strings.Contains(out, c.want) {
- t.Fatalf("%s run %d: err=%v out=%s, want %q", c.name, run, err, out, c.want)
+// Two identical runs per case: the same refusal both times, and no write,
+// no new record, no manifest and no upload, before or after.
+func TestCanaryRegionRefusesBeforeAnyWrite(t *testing.T) {
+ const existing = "probe.titogo.bab.oncarlos.test"
+ cases := []struct {
+ name, mode, region string
+ fleet, canary bool
+ want string
+ }{
+ {"fleet-placed app", "", "uk", true, false, canaryRegionOnFleet("titogo", "acme")},
+ {"older console", "legacy", "uk", true, false, canaryFleetUnknown("titogo")},
+ {"placement unknown", "placement-unknown", "uk", false, false, canaryFleetUnknown("titogo")},
+ {"existing canary elsewhere", "", "ie", false, true, canaryRegionMismatch(existing, "uk")},
+ }
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ e := regionE2EConsole(t, "")
+ ctx := context.Background()
+ if c.fleet {
+ if err := e.cst.SetAppPlacement(console.OpsAccountID, "titogo", "fleet/acme"); err != nil {
+ t.Fatal(err)
+ }
}
- outs = append(outs, out)
- }
- recs, _, err := provision.ListRecords(context.Background(), e.store, release.OpsAccount, "titogo", 1000)
- if err != nil {
- t.Fatal(err)
- }
- if len(recs) != 0 || e.writes.Load() != 0 || e.releases(t) != 0 {
- t.Fatalf("%s: records %d, writes %d, releases %d; want none", c.name, len(recs), e.writes.Load(), e.releases(t))
- }
+ if c.canary {
+ rec := provision.Record{Schema: 1, Host: existing, App: "titogo", Account: release.OpsAccount,
+ Region: "uk", Channel: "canary/probe", CreatedAt: time.Now().UTC()}
+ if err := provision.CreateRecord(ctx, e.store, rec); err != nil {
+ t.Fatal(err)
+ }
+ }
+ e.mode.Store(c.mode)
+ for run := 0; run < 2; run++ {
+ out, err := e.run(t, "canary", "deploy", "--app", "titogo", "--name", "probe", "--region", c.region,
+ "--console", e.url, "--version", "abc1234", "--timeout", "1s", artifact(t))
+ if err == nil || !strings.Contains(out, c.want) {
+ t.Fatalf("run %d: err=%v out=%s, want %q", run, err, out, c.want)
+ }
+ }
+ recs, _, err := provision.ListRecords(ctx, e.store, release.OpsAccount, "titogo", 1000)
+ if err != nil {
+ t.Fatal(err)
+ }
+ wantRecs := 0
+ if c.canary {
+ wantRecs = 1
+ }
+ if len(recs) != wantRecs || e.writes.Load() != 0 || e.releases(t) != 0 {
+ t.Fatalf("records %d (want %d), writes %d, releases %d; want nothing written", len(recs), wantRecs, e.writes.Load(), e.releases(t))
+ }
+ if c.canary && recs[0].Region != "uk" {
+ t.Fatalf("the existing canary moved to %q", recs[0].Region)
+ }
+ if _, err := provision.GetManifest(ctx, e.store, release.OpsAccount, "titogo"); !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("an instances manifest was written: %v", err)
+ }
+ })
}
}
```
-Both refusals in this test come from the first check, which runs before
-`instancesEnable`'s PUT. The zero-writes assertion fails if the check is
-moved after it.
+For the existing-canary case, C25 names the region `uk` by its code: the
+test console has no box publishing `uk`, so `/regions` gives it no place
+name, and `nameOf` falls back to the code.
- [ ] **Step 6: Run the root tests**
@@ -6407,20 +6703,28 @@ Run: `go test . -count=1 -timeout 30m`
Expected: PASS, including the byte-for-byte output assertions in
`canary_test.go` and `instances_environment_test.go`.
+Then the ordering check: move the `--region` block from step 4 to after the
+`instancesEnable` call, and run
+`go test . -run TestCanaryRegionRefusesBeforeAnyWrite -count=1`. It must
+FAIL on writes. Move it back, and re-run: PASS.
+
- [ ] **Step 7: Cross-compile, then commit**
```bash
+fail=0
for t in darwin/arm64 darwin/amd64 linux/arm64 windows/amd64 windows/arm64; do
- CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; break; }
+ CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; fail=1; }
done
+[ "$fail" = 0 ] || exit 1 # stops this block before any commit below it
git add canary.go canary_region_test.go canary_region_e2e_test.go instances_cli.go apimode.go
-git commit -m "cli: canary --region refuses identically on fleets and older consoles; place in output (nearest-region §11)
+git commit -m "cli: canary --region checks with reads before any write; refuses identically on fleets and older consoles (nearest-region §11)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
---
+
### Task 23: The region service-gate script, proved against a real backhaul
**Files:**
@@ -6839,21 +7143,54 @@ func TestApprovedCopyIsApplied(t *testing.T) {
- [ ] **Step 2: Run the full gate**
-Run, from the worktree root:
+Run, from the worktree root, as one script:
```bash
+set -e
go build ./...
+go vet ./...
+test -z "$(gofmt -l .)"
+fail=0
for t in darwin/arm64 darwin/amd64 linux/arm64 windows/amd64 windows/arm64; do
- CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; break; }
+ CGO_ENABLED=0 GOOS=${t%/*} GOARCH=${t#*/} go build ./... || { echo "FAILED $t"; fail=1; }
done
+[ "$fail" = 0 ] || exit 1
+(cd launchctx && go vet ./... && go test ./... -count=1)
+# Unix-socket packages need a short path; /tmp/g is the one place this plan
+# uses /tmp, and only for that (it is RAM-backed tmpfs, not disk).
mkdir -p /tmp/g
-TMPDIR=/tmp/g make test
+TMPDIR=/tmp/g go test ./internal/host/... ./internal/edge/... -count=1
+# Everything else on the on-disk TMPDIR the environment provides (inside
+# the sandbox, $TMPDIR is on disk): console tests need it.
+go test $(go list ./... | grep -v -e /internal/host -e /internal/edge) -count=1 -timeout 30m
```
-Expected: no `FAILED` line, every package passes, `gofmt -l .: clean`, and
-`TestApprovedCopyIsApplied` passes. `TMPDIR=/tmp/g` is for the Unix-socket
-tests in `internal/host` and `internal/edge`. `/tmp/g` is on disk, so the
-console tests' on-disk requirement holds too.
+Expected: the script exits 0, with no `FAILED` line, and
+`TestApprovedCopyIsApplied` among the passes. This is `make test`'s content,
+split so each package gets the `TMPDIR` it needs.
+
+- [ ] **Step 2b: Check for strings nobody reviewed (codex plan-R1 13, plan-R2)**
+
+Presence of the approved strings does not prove the absence of others. List
+every prose literal this branch adds to non-test Go code:
+
+```bash
+BASE=e8406ce6 # the commit this branch was cut from
+git diff -U0 "$BASE" -- '*.go' ':!*_test.go' ':!internal/geo/awsregions.go' \
+ | grep '^+' | grep -v '^+++' \
+ | grep -oE '"([^"\\]|\\.)*[a-z][^"]* [^"]*"' | sort -u
+```
+
+Every line printed must be one of three things:
+
+- a record entry's text;
+- a message that already existed before this branch and was only moved or
+ reused, such as `canary: list instances for %s: %v`;
+- code rather than prose, such as a fragment of a Go expression the pattern
+ happened to catch.
+
+Anything else is unreviewed copy. Take it to a copy review and add it to the
+record before landing.
- [ ] **Step 3: Confirm nothing touches DNS steering (R8)**
@@ -7006,3 +7343,61 @@ Also folded in: **spec rev 6, ruling R9.**
A copy note for Paul is in Task 0: C25 says `carlos canary remove`, but the
verb is `carlos canary rm`.
+
+## Review dispositions (Astra, plan round 2, on fcf15ae9 with spec 9ccfaca6)
+
+Every code block these findings touched was proved, not just read. A
+throwaway worktree was cut at the branch base (e8406ce6). The plan's Go
+blocks for Tasks 1, 1b, 2 to 12, 14, 15, 18 to 22 were extracted from this
+file verbatim and applied. The prescribed tests were run before each
+implementation step (all failed, mostly as build failures) and after it (all
+passed). The scratch worktree and branch were deleted afterwards. The
+results are in the "Proof" column.
+
+| # | sev | finding | disposition | proof |
+| --- | --- | --- | --- | --- |
+| R1-9 | P2 (partly) | Migration preservation | **accepted**, see R2-6 | `TestRegionValuesSurviveRebuildRenameAndMove` FAIL before Task 8's code, PASS after |
+| R1-12 | P2 (partly) | Existing-canary zero-write proof missing | **accepted**, see R2-4 | `TestCanaryRegionRefusesBeforeAnyWrite/existing_canary_elsewhere` PASS |
+| R1-13 | P2 (partly) | Diagnostics outside the copy inventory | **accepted.** O11 to O18 added to Task 0, after a scan of every prose literal the scratch diff added. Task 25 step 2b repeats that scan before landing. | the scan found no other unlisted prose |
+| R1-17 | P2 (partly) | `/tmp/g` called disk-backed | **accepted.** Task 25 runs socket packages on `/tmp/g` (stated as tmpfs) and everything else on the on-disk `TMPDIR`. | gate script restructured |
+| R2-1 | P1 | Incomplete edge reads could prove a region keyless | **accepted.** `readEdgesSnapshot` returns `Complete=false` on any failed GET or malformed file, and `NoKeyProven` requires a complete snapshot. Positive eligibility is unaffected. | `TestNoKeyProvenNeedsACompleteRead` PASS; with `snap.Complete` removed it FAILs ("an incomplete read proved a region keyless") |
+| R2-2 | P1 | Ineligible boxes located a candidate | **accepted.** A coded candidate's AWS region is tallied only from eligible boxes; the home keeps its own tally. | `TestCandidateLocationComesFromEligibleBoxes` PASS; tallying every box FAILs ("located uk at ap-southeast-2") |
+| R2-3 | P1 | A 200 marked `policy-unreadable` passed the CLI checks | **accepted.** `app_default` gains `region_known` and `placement_known` (additive), and an unread region is left empty, never filled with the home. `deploy --region` refuses on an unknown region (new C15c); `canary --region` refuses on an unknown placement (C30b). | `TestAppDefaultKnownFlags` PASS; e2e `mode=region-unknown` and `placement_unknown` refuse with 0 writes |
+| R2-4 | P1 | Existing-canary mismatch checked after a write | **accepted.** One check, with reads only (`GET …/regions`, `GET …/instances`, matched by host prefix `<name>.<app>.`), before `instancesEnable`'s PUT. | `TestCanaryRegionRefusesBeforeAnyWrite` 4/4 PASS with 0 writes, no manifest, record unchanged; with the check moved after the PUT, 4/4 FAIL with "writes 2" |
+| R2-5 | P1 | Three compile failures | **accepted.** Task 12's unused `net` import removed. Task 20 names every caller, including `apimode_test.go`'s six, `apps_test.go:311` and `deploy_wizard_test.go:104`. Task 22's block sits after `ctx` is declared. | `go build ./...` and `go vet ./...` clean on the whole scratch tree |
+| R2-6 | P1 | The migration test's rename could not pass | **accepted.** The test seeds the ops account row before `RenameAppIn` and `MoveApp`. | PASS (see R1-9) |
+| R2-7 | P2 | No test for R9's addr-backed exemption | **accepted.** An explicit addr-backed create into a keyless region must return 201. A mutation step widens the gate. | PASS; with the gate widened to every kind it FAILs ("want 201") |
+| R2-8 | P2 | Missing-key diagnostic gated on the switch | **accepted.** O2 logs whenever `CARLOS_SECRETS_PUBKEY` is unset; O1 stays switch-gated. A switch-off case was added. | `TestLogRegionConfig` PASS |
+| R2-9 | P2 | Second mutation check proved nothing | **accepted.** The steps restore and re-run the baseline between mutations. | baseline PASS; mutation A FAILs ("existence false, want true"); restore PASS; mutation B FAILs ("existence true, want false"); restore PASS |
+| R2-10 | P2 | Cross-build loop returned 0 on failure | **accepted.** Every loop records failures and ends with `[ "$fail" = 0 ] \|\| exit 1`. | all five targets built with exit 0; a loop with a bad target (`plan9/bogus`) exits 1 before the commit line |
+
+Fixes the proof surfaced, beyond Astra's findings, all now in the tasks:
+
+- **Task 1b:** `edgesfile.go` needs the `appconfig` import.
+- **Task 9:** `cmd/console/main_test.go` is `package main_test`, so the new
+ tests go in `cmd/console/region_test.go` (`package main`).
+- **Tasks 11 and 12:** `go vet` rejects unkeyed `geo.Point` literals across
+ packages, so they are keyed.
+- **Task 15:** the guess route joins `exemptFromLegacyPin`
+ (`svcroutes_test.go`), or `TestInstanceRoleMatchesTodaysAppTokenReach`
+ fails.
+- **Task 21's e2e:**
+ - builds the binary before `useTempCredentials` changes directory;
+ - runs each refusal case as a subtest;
+ - passes `--channel canary/e2e`, so the older-console case really ships.
+
+Whole-package results after all of the above:
+
+- `go test ./internal/console`: `ok` (356.9s).
+- `internal/geo`, `internal/appconfig`, `internal/provision` and
+ `cmd/console`: `ok`.
+- The root package could not be run as a whole on this machine. The CLI
+ walks up from the working directory and finds the operator's real
+ `/home/paulca/.carlos/config`, so a pre-existing test exits with "not
+ logged in". The unmodified base fails identically. Every new root test was
+ run by name and passed.
+- The 211 existing tests in the changed CLI files (`canary_test.go`,
+ `instances_*_test.go`, `apps_test.go`, `deploy_*_test.go`,
+ `apimode_test.go`) were run one at a time on both trees. Outcomes were
+ identical: 173 PASS on each, and the same 38 blocked on each by that
+ config. Task 25 step 2 runs them all in CI, where no such config exists.