docs: instance moves — Astra's ruling recommendations (unedited)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 file changed,
+202
−0
docs/superpowers/specs/CODEX-RULINGS-INSTANCE-MOVES.md+202 −0
CI passed — run details
diff --git a/docs/superpowers/specs/CODEX-RULINGS-INSTANCE-MOVES.md b/docs/superpowers/specs/CODEX-RULINGS-INSTANCE-MOVES.md| new file mode 100644 |
| index 000000000..25d48a161 |
| --- /dev/null |
| +++ b/docs/superpowers/specs/CODEX-RULINGS-INSTANCE-MOVES.md |
| @@ -0,0 +1,202 @@ |
| +Citations refer to checkout `5ca13db9` unless marked `origin/main`. P0 and #331’s cut ordering remain settled. |
| + |
| +### R1: Who owns the freeze? |
| + |
| +**Recommendation:** Accept: the platform invokes declared hooks; undeclared hooks preserve current move behavior. |
| + |
| +**Why:** The current cut endpoint only requests a transition; it cannot wake an instance for a tenant’s preceding freeze (`internal/console/apiinstancemove.go:232`). The platform already owns waking and stopping, so it can reliably sequence the hook. P0 installs the wake fence before trusting quiescence (`origin/main`, `internal/host/movespass.go:362`), making graceful freeze separate from database correctness. |
| + |
| +**If wrong:** Apps with unusual freeze requirements may need a richer hook contract; that is an additive extension. |
| + |
| +**Blocks:** P1; lifecycle integration in P4 and P8. |
| + |
| +**Confidence:** high |
| + |
| +### R2: How should hooks authenticate and wake instances? |
| + |
| +**Recommendation:** Accept: use `ServeHook` over the instance socket with the existing instance admin token. |
| + |
| +**Why:** `scheduleToken` already ensures and reads the token through activator coordination (`internal/host/schedule_wiring.go:161`). Its path resolver handles both flat and isolated layouts (`internal/host/schedule_wiring.go:192`). `ServeHook` should retain P0’s fence checks and activity accounting, with explicit eligibility for each lifecycle phase rather than a general fence bypass (`origin/main`, `internal/activator/activator.go:2793`). Require idempotent calls keyed by move ID, a fixed deadline across retries, and hook-path refusal on both public and local edge doors. |
| + |
| +**If wrong:** Retry ambiguity could repeat application actions; changing authentication after adoption would require coordinated app updates. |
| + |
| +**Blocks:** P1, P4’s verify call, and P6’s adoption verification. |
| + |
| +**Confidence:** high |
| + |
| +### R3: Which stores follow an instance? |
| + |
| +**Recommendation:** Accept: move only explicitly declared, platform-provisioned stores or exclusive instance prefixes. |
| + |
| +**Why:** The platform’s provisioner manages buckets and their credentials, giving it the authority needed to create a destination (`internal/objstore/provisioner.go:12`). Named environment updates provide a way to change one instance’s coordinates without rewriting the app default (`internal/appconfig/generation.go:126`). Arbitrary external buckets lack that ownership contract. Report exactly which data moves and which remains, subject to the writer-fencing ruling below. |
| + |
| +**If wrong:** Apps with external stores cannot achieve complete relocation through this feature; adding a supported external-store adapter later is feasible. |
| + |
| +**Blocks:** P8; store-related completion and retention semantics in P4/P5. |
| + |
| +**Confidence:** high |
| + |
| +### R4: What does rollback mean after the cut? |
| + |
| +**Recommendation:** Accept: rollback is an explicitly requested move back, and home must support both directions. |
| + |
| +**Why:** The API currently rejects home as a destination (`internal/console/apiinstancemove.go:196`). Reconciliation uses exact region equality, so replacing home’s empty record value would change ownership scope (`internal/provision/reconcile.go:339`). `CARLOS_HOME_PLACEMENT` should supply the external placement identity while preserving that internal value. Home children also need the placement code because their environment currently removes `CARLOS_REGION` (`internal/activator/backend_exec.go:989`). |
| + |
| +**If wrong:** Automatic restoration of a retained source copy could discard target writes; reversing that mistake may be impossible. |
| + |
| +**Blocks:** P3, P4’s recovery contract, and P5. |
| + |
| +**Confidence:** high |
| + |
| +### R5: Should app routers stop tracking instance location? |
| + |
| +**Recommendation:** Accept: provide one authenticated local edge door and migrate every enrolled consumer to it. |
| + |
| +**Why:** The edge already backhauls requests when the host is absent locally (`internal/edge/edge.go:1381`). Isolated instances expose their socket through an instance-specific directory, so computed flat paths cannot remain the consumer contract (`internal/host/units/carlos-instance@.service:47`). Enrollment must cover the router, relay, and import-sync process described in G18 before P9 runs. Preserve public-door protections and grant forwarded-address trust only after authenticating the caller. |
| + |
| +**If wrong:** Some consumers may require additional supported operations; maintaining parallel tenant location maps would be expensive to unwind. |
| + |
| +**Blocks:** P7; P9 and then P6 on deployments with direct socket consumers. |
| + |
| +**Confidence:** high |
| + |
| +### Q1: Must measured cut time fit the freeze budget? |
| + |
| +**Recommendation:** Accept expiry while the process is stopped; use measured cut time for estimates and warnings, not a correctness gate. |
| + |
| +**Why:** P0’s cut fence prevents waking the stopped source independently of application freeze state (`origin/main`, `internal/host/movespass.go:366`). A historical duration cannot establish an upper bound on the next checkpoint or transfer. Persist one freeze deadline and never extend it through retries. Treat grace expiry and total service downtime as separate reported facts; external store writers require their own fencing. |
| + |
| +**If wrong:** Customers may misunderstand the budget as a downtime ceiling; a separate preflight downtime policy can be added later. |
| + |
| +**Blocks:** P1’s deadline semantics and P4’s reporting. |
| + |
| +**Confidence:** high |
| + |
| +### Q2: What happens when the serving home has replicas? |
| + |
| +**Recommendation:** Refuse such moves initially until the platform supports a replica-aware transition. |
| + |
| +**Why:** Replica placements are an explicit residence choice, continuously converged from the home record (`internal/provision/provision.go:170`). Replica construction creates a separate record, while the lease/drain protocol remains tenant-owned (`internal/provision/replica.go:16`, `internal/provision/replica.go:44`). Keeping placements unchanged is attractive, but does not establish how outstanding leases or drains follow the new home. This ruling concerns a home with replicas, not P0’s settled refusal to move a replica record. |
| + |
| +**If wrong:** Some otherwise viable moves remain unavailable; widening a refusal later is substantially easier than repairing a broken replica transition. |
| + |
| +**Blocks:** The shared eligibility contract for P1/P3/P4. |
| + |
| +**Confidence:** medium |
| + |
| +### Q3: What identifies callers of the edge-local door? |
| + |
| +**Recommendation:** Use an app-scoped credential, preferably restricted to local-edge access; retain Unix permissions as an additional access check. |
| + |
| +**Why:** Isolated instances still run as `carlos:carlos`, so Unix uid alone does not identify their app (`internal/host/units/carlos-instance@.service:28`). Their protection comes from filesystem namespaces, including the hidden shared runtime directory (`internal/host/units/carlos-instance@.service:47`). Authenticate the credential to account and app, then check the requested host’s ownership. Give every enrolled consumer access through supported credential delivery, and strip the door credential before forwarding while preserving application authorization headers. Before isolation, this credential does not cure the existing shared-filesystem exposure. |
| + |
| +**If wrong:** Credential rotation adds operational work; replacing a group-only authorization contract after deployment would be harder. |
| + |
| +**Blocks:** P7’s authentication and enrollment design; consequently P9/P6. |
| + |
| +**Confidence:** high |
| + |
| +### Q4: What residency promise covers the control plane? |
| + |
| +**Recommendation:** Keep authoritative records, bundles, secrets, and move logs in the deployment’s home region, and state that boundary explicitly. |
| + |
| +**Why:** Config manifests use account-scoped deployment-store keys (`internal/appconfig/appconfig.go:104`). Remote delivery reads named environments from that same store (`internal/appconfig/syncexec.go:182`). Regional bundle copies alone would neither relocate the remaining control plane nor establish independent regional operation. Distinguish authoritative storage from runtime copies delivered to the destination, and disclose source-data retention separately. |
| + |
| +**If wrong:** Customers requiring complete regional residency cannot use this promise; satisfying them later requires a broader control-plane design. |
| + |
| +**Blocks:** P4’s completion contract, P5’s retention promise, and P8’s delivery semantics. |
| + |
| +**Confidence:** high |
| + |
| +### Q5: Who may request moves before #171 is fixed? |
| + |
| +**Recommendation:** Keep move mutations for other accounts operator-only until #171’s fix is deployed and verified. |
| + |
| +**Why:** Today the instance service role reaches request, cut, and abort endpoints (`internal/console/authcli.go:625`). The region-host policy also permits writes across the deployment bucket (`infra/modules/region-host/main.tf:166`). Restricting API callers limits product exposure but does not prevent a compromised box from forging records. An operator-controlled first adopter may proceed under explicit scope; general self-service and store-grant issuance require trustworthy move authorization. |
| + |
| +**If wrong:** Adoption slows, but the gate is easy to remove; unauthorized data relocation is much harder to reverse. |
| + |
| +**Blocks:** Authorization planning in P1/P4, P8’s grant issuance, and general self-service rollout. |
| + |
| +**Confidence:** high |
| + |
| +### Q6: Who copies stores, and with what credential? |
| + |
| +**Recommendation:** Use a platform-run copy with a move-scoped IAM user, but enforce a fixed permission expiry independently of terminal cleanup. |
| + |
| +**Why:** Copying requires source reads, while the standing provisioning role deliberately lacks tenant-object reads (`internal/lease/mirror.go:202`, `infra/modules/region-host/main.tf:609`). Scope access to the exact source and destination prefixes, including destination listing for delta comparison. IAM user credentials are long-lived, so deletion at completion alone does not make them temporary; enforce a bounded authorization window and reap abandoned users. [AWS documents time-bounded policy conditions](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_aws-dates.html). App-run copying would return a core platform responsibility to every tenant. |
| + |
| +**If wrong:** Grant creation and expiry can stall copies; the credential mechanism can change behind the platform contract. |
| + |
| +**Blocks:** P8. |
| + |
| +**Confidence:** high |
| + |
| +### Q7: How should direct-mode `carlos add` establish ownership? |
| + |
| +**Recommendation:** Require explicit account selection in direct mode and reject conflicts with established ownership. |
| + |
| +**Why:** Both `--account` and `--account-id` already exist; the problematic default is `ops` (`cli.go:72`). App names are unique only within an account, so inheriting ownership from the app name alone is ambiguous (`internal/domains/reconcile.go:699`). Wrong ownership has an immediate effect because alias claims reject account mismatches (`internal/domains/aliases.go:106`). P6 should authorize its exact old-to-new ownership transfer explicitly, without turning ordinary adds into ownership reassignment. |
| + |
| +**If wrong:** Existing scripts need an account argument; repairing silently misowned routes is more disruptive. |
| + |
| +**Blocks:** P6’s ownership contract and the direct-add fix, which needs an explicit slice assignment. |
| + |
| +**Confidence:** high |
| + |
| +### Q8: How much adoption downtime is acceptable initially? |
| + |
| +**Recommendation:** Start with a scheduled, per-account stop window and retain replicate-then-cut adoption as a later capability. |
| + |
| +**Why:** Existing reconciliation deliberately refuses to replace a differently shaped route (`internal/provision/reconcile.go:1022`). Adoption therefore needs an explicit import and ownership-transfer protocol regardless of transfer speed. Restore already fails closed on transfer errors and validates restored databases (`internal/activator/backend_exec.go:290`, `internal/activator/backend_exec.go:313`). For the quoted 5 MB first case, a stopped snapshot is a reasonable initial tradeoff, but the proposed minute-or-two estimate must be measured through first successful service. |
| + |
| +**If wrong:** Larger imports cause unacceptable outages; adding replication later is possible if import sessions remain distinct from record creation. |
| + |
| +**Blocks:** P6’s import protocol, progress reporting, and abort boundary. |
| + |
| +**Confidence:** medium |
| + |
| +## Order to rule them in |
| + |
| +1. **Q5:** Establish who may exercise the first customer path. |
| +2. **R5 → Q3:** Unblock P7 and enrollment of every consumer before P9. |
| +3. **Q7 → Q8:** Set ownership and downtime terms for P6. |
| +4. **R1 → R2 → Q1:** Set P1’s hook contract. |
| +5. **R4 → Q2:** Complete the home-return and eligibility decisions for P3. |
| +6. **Q4 → R3 → Q6:** Set the residency and store-copy contract. |
| + |
| +Keep P2 alongside this work. Roll P0 before any production move; it is merged, not rolled. Include P4 in the first-customer plan for durable status and recovery, and require it if registered timers must survive. Execute P3 infrastructure changes and the P9 migration only within scoped production approval. |
| + |
| +## Decisions the record is missing |
| + |
| +### M1: How are all store writers stopped? |
| + |
| +**Recommendation:** Require platform-enforced write fencing for every moved bucket or prefix before its final delta, covering outstanding uploads and other credential holders. |
| + |
| +**Why:** Store credentials authorize object writes independently of the instance process (`internal/objstore/aws.go:179`). Stopping that process therefore does not prove that a browser upload, relay, or another holder cannot write. Prefix-based movement additionally needs exclusive ownership of the declared keys. This extends store correctness without changing #331’s database cut ordering. |
| + |
| +**If wrong:** Late writes remain only in the source and disappear after cleanup; detecting and merging them later is difficult. |
| + |
| +**Blocks:** P8; P5’s store deletion. |
| + |
| +**Confidence:** high |
| + |
| +### M2: How does a user recover when landing cannot finish? |
| + |
| +**Recommendation:** Require API/CLI inspection and retry of forward progress, with an explicit recovery path when an ordinary move back cannot start. |
| + |
| +**Why:** Restore can fail before the destination serves or has a usable database (`internal/activator/backend_exec.go:290`). Consequently, “move back” is not a complete recovery procedure for every failed landing. The current status response becomes `none` when the move clears (`internal/console/apiinstancemove.go:88`), so recovery must retain durable move identity and evidence. Specify safe import abort before destination writes, and never silently restore an older source over newer target data. |
| + |
| +**If wrong:** Customers still require box or AWS access during failures; retrospective recovery tools may lack the evidence needed to act safely. |
| + |
| +**Blocks:** P4, P6, and P8. |
| + |
| +**Confidence:** high |
| + |
| +## Where you disagree with the record’s own proposal |
| + |
| +- **§8/Q3:** Authenticate the local edge door with an app-scoped credential, not group membership alone. |
| +- **§7.1/Q6:** “Short-lived” must mean enforced permission expiry, not eventual IAM-user deletion. |
| +- **G17/Q7:** Account flags already exist; remove the silent ownership default and validate conflicts. |
| +- **§7.1:** A stopped child does not establish that an object store has no remaining writers. |
| +- **§13:** P4 should join the first-customer plan because status and supported recovery are part of completing the job. |
| \ No newline at end of file |