carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1# Moves Phase 1, Plan 7: Rehearsal Runbook Implementation Plan
2
3> **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. **Nothing in this plan runs against production without Paul's go**, and every rehearsal gates the next (§15 step 7).
4
5**Goal:** Prove Phase 1 on a probe app that exercises every carried state, then on a disposable real instance, then on kpmg, in M12's order, with a written runbook an operator follows and a pass criterion the run must meet.
6
7**Architecture:** `moveprobe` is a small Go app in `cmd/moveprobe` shaped like ips (§14.4): `instance.key` written at first boot and verified every boot; a required store binding and a sealed secret; a declared schedule and a once-timer; a background loop writing rows; a forked worker that outlives the main process; a switch to fail its readiness path in a named region before or after writing. The runbook is `docs/superpowers/plans/2026-10-06-moves-phase1-rehearsal.md`, a checklist per run with the commands, the expected log events, and the checks. Cross-version tests pin the door and the floor against pre-floor binaries.
8
9**Tech Stack:** Go, `net/http`, `database/sql` with modernc sqlite, the platform's `carlos` CLI, `tofu` for nothing (no infra change in this plan beyond §15 step 5, which is applied as scoped work by Paul).
10
11**Spec:** rev 7.23 §14.3, §14.4, §15. Index: `2026-10-06-moves-phase1-0-index.md`. Plans 1 to 6 are merged, not rolled, and not complete (preflight N1): no run in this plan executes on main until claim Plan 12 and its moves dependants are merged, released and rolled.
12
13## Global Constraints
14
15- Gate, cross-compile, the default Go build cache; tests serial and niced.
16- Rollout order (§15): the dormant release first (consoles, then every box), M17's `sibling_staging_arns` and the versioned-object grants (§15 step 5) applied by Paul, then `control/moves-protocol.json` written create-only **before** any protected production rehearsal (§11, joint N12), then the rehearsals in order: dev IE to uk done; dev forced clean rollback; dev forced dirty rollback; dev declare-lost; the same on the flagship with scoped approval; a disposable ips instance in `bdn`; kpmg with Paul's approval.
17- A run passes only if: the key verifies, the object reads back, the secret is present, every timer ran exactly once around the cut, no background row is missing, and no copy remains in a live namespace of the region that lost (§14.4).
18- `moveprobe`'s readiness-failure switch is an environment variable (`MOVEPROBE_FAIL_READY_IN=<region>[,after-write]`) delivered through the config bundle, so a rehearsal flips it with `carlos env set`, never a rebuild.
19
20## Review Focus
21
221. The forked worker must survive the main process's exit and still be killed by `ReconstructStopEvidence`; a run where the worker survives the cut must fail the run with the stray-process page, never pass silently. Task 1 (`TestMoveprobeWorkerOutlivesMain`) and runbook check R-6.
232. A once-timer registered seconds before the cut must fire exactly once, on the target, with its `due_at` preserved; firing twice or never fails the run. Task 1's timer registration and runbook check R-4.
243. The disposable `bdn` instance must be created with a fresh host name no previous rehearsal used, or a stale `instances/<host>/` or `moves/<host>/` lane makes the sweep's work part of the run. Runbook step S-1.
254. The dirty-rollback run must prove the target's writes came back: the probe app writes a row with the target's `CARLOS_REGION` while it runs on the target; after the rollback the source must hold that row. Runbook check R-7.
265. The declare-lost run must use a box the operator actually stopped through the cloud API (dev only, never the flagship's shared boxes without Paul's go), and the loss record must name the declared sessions. Runbook step S-9.
27
28---
29
30## File structure
31
32| path | responsibility |
33|---|---|
34| `cmd/moveprobe/main.go`, `cmd/moveprobe/main_test.go` | the probe app |
35| `cmd/moveprobe/README.md` | how to deploy it as a carlos app (`carlos ship`) |
36| `internal/console/apiinstancemove_test.go`, `internal/host/systemapply_test.go` | cross-version tests |
37| `docs/superpowers/plans/2026-10-06-moves-phase1-rehearsal.md` | the runbook |
38
39---
40
41### Task 1: `moveprobe`
42
43**Files:** create `cmd/moveprobe/main.go`, `cmd/moveprobe/main_test.go`, `cmd/moveprobe/README.md`.
44
45**Interfaces (the app's behaviour):**
46- The exec backend's launch contract is **flags, not variables** (`internal/activator/backend_exec.go:1541`, in `spawnChildFrom`: `exec.Command(bin, "--socket", sock, "--db", dbPath)`): `moveprobe --socket <unix path> --db <sqlite path>` listens on the socket and opens the database; the files directory is `strings.TrimSuffix(dbPath, ".db")` (F4, the flat layout). The agent-owned environment is composed by `agentOwnedVars` (`backend_exec.go:1603-1626`): `CARLOS_CONTROL_SOCKET` only when the control listener bound, `CARLOS_PURGE`, `CARLOS_REGION` only when the box has a region, and `CARLOS_HOST`. `CARLOS_ADMIN_TOKEN` and `CARLOS_INSTANCE_KEY` come from `<host>.env`, not the composer. The app reads those by name and treats a missing `CARLOS_REGION` as region `home` (preflight N7). `TestMoveprobeLaunchContract` starts the binary exactly as the backend does (`--socket`, `--db`, the agent variables) and asserts `/ready` answers on the socket.
47- First boot: writes `files/instance.key` (32 random bytes, hex) beside the database; every boot: verifies the file exists and matches the row `meta(key='instance_key')` in the database; mismatch → the readiness path answers 500 with `key mismatch`.
48- Requires `MOVEPROBE_BUCKET`, `MOVEPROBE_REGION`, `MOVEPROBE_ENDPOINT`, `MOVEPROBE_ACCESS_KEY`, `MOVEPROBE_SECRET_KEY` (an `objstore` binding with prefix `MOVEPROBE`) and writes `probe/<boot id>.txt` at boot, reading it back on `/ready`; requires `MOVEPROBE_SECRET` (a sealed secret) and answers 500 on `/ready` when absent.
49- Registers, through the instance control socket (`CARLOS_CONTROL_SOCKET`), a declared schedule `every-minute` → `/tick` and, on `POST /arm-once?in=<seconds>`, a once-timer `once-<n>` → `/once`; `/tick` and `/once` insert rows `(kind, name, due_at, fired_at, region)`.
50- A background loop inserts a row `(kind='bg', seq, region)` every 500 ms; a forked worker (`exec.Command(os.Args[0], "worker")` with `Setsid`) inserts `(kind='worker', seq, region)` every second and ignores SIGTERM for 30 s (Review Focus 1).
51- `MOVEPROBE_FAIL_READY_IN=<region>[,after-write]`: in the named region, the **before-write** mode (no suffix) suppresses every mutation on this box from the first instruction: the background loop, the forked worker, the `/tick` and `/once` handlers and the control-socket registrations all return without writing, and `/ready` answers 500, so the target's database, files and timers digests stay exactly what was installed and the rollback is clean (the readiness checks after a spawn, the wake's `waitHealthyCtx` at `backend_exec.go:1335` (GET `/`, any status below 500, `waitHealthy` at `:2344`) and the claim probe's `claimProbeSocketReady` at `internal/host/claimready_linux.go:23-41` (2xx only), would otherwise see rows written before the refusal); the **after-write** mode writes one `bg` row (and lets the worker write) and then fails `/ready`, so the rollback is dirty with a known row to find on the source.
52- `/ready`: 200 when the key verifies, the object reads back and the secret is present, and the failure switch does not apply.
53- `/report`: JSON with the row counts per kind and region, the once-timers fired (name, count, due_at), the key fingerprint, the object body, `has_secret`.
54
55Tests: `TestMoveprobeWorkerOutlivesMain` (start the binary in a temp cgroup-less dir, kill the main process, the worker's pid is alive and still writing; the test then kills it); `TestReadyFailsWhereTold` (both modes: before-write leaves the database's row count and the timers table unchanged after two seconds of running; after-write adds exactly the rows the mode promises); `TestReportCountsRowsByRegion`; `TestMoveprobeLaunchContract`.
56
57- [ ] **Steps 1 to 5**. Commit: `git commit -m "cmd/moveprobe: the rehearsal probe app (§14.4)" ...`
58
59---
60
61### Task 2: Cross-version tests
62
63**Files:** modify `internal/console/apiinstancemove_test.go`, `internal/host/systemapply_test.go`, `internal/provision/move_test.go`.
64
65- `TestDoorRefusesBelowTheFloor`: an edges file with `moves_protocol: 0` (a pre-floor box) in either region → 409 naming the floor; with the field absent → the same.
66- `TestSystemApplyRefusesAPreFloorBinaryOnAParticipant`: a box whose registry holds a held row (`hold_op` set) refuses any `carlos system apply` or rollback (§11 "any downgrade of a box that is a participant in a held operation is refused"); the applier reads the registry at `/data/registry.db` through the same path the agent uses.
67- `TestOldConsoleAbortAllowlist`: `MoveAbort` from a v0.22 console (the current `provision.go:1027-1035` allowlist, reproduced as a test double) refuses every new state (`landing`, `committed`, `rolling_back`, `reverting`, `stuck`), per P0's rule.
68
69- [ ] Commit: `git commit -m "moves: cross-version tests: the floor at the door and at activation, the old abort allowlist (§14.3)" ...`
70
71---
72
73### Task 3: The runbook
74
75**Files:** create `docs/superpowers/plans/2026-10-06-moves-phase1-rehearsal.md`.
76
77The runbook has one section per run in §14.4's order; each section is a checklist an operator ticks:
78
79**Preconditions (once):**
80- [ ] P-1 The Phase 1 release is rolled dormant: consoles first, then every box (`carlos status` shows the build; `control/edges/*.json` all carry `"moves_protocol":1`).
81- [ ] P-2 §15 step 5 applied: `sibling_staging_arns` and the versioned-object grants (Paul, scoped `tofu apply -target`); verify with T10's probe through `carlos move status --host <h>` after the first request (T10 passes).
82- [ ] P-3 `control/moves-protocol.json` written create-only: `{"min":1,"min_release":"<the rolled version>"}` (an `aws s3api put-object --if-none-match '*'` from an operator shell with the deployment profile); `carlos system apply` of an older build on dev now refuses.
83- [ ] P-4 `moveprobe` shipped as app `moveprobe` under the ops account with a store binding `MOVEPROBE`, a sealed secret `MOVEPROBE_SECRET`, a declared schedule, and `moves.ready_path` unset (`/` serves `/ready`'s answer).
84- [ ] P-5 Every rehearsal host is adopted (claim Plan 5's adoption has run: `control/hosts/<host>.json` exists with `op.kind: serving`). This needs C5 merged and the #232 ownership backfill run for every rehearsal host: adoption happens only inside that backfill, never from the move door or the recovery pass, and merging the claim branches alone does not adopt anything.
85- [ ] P-6 The switch is on for this deployment: `CARLOS_MOVES_PHASE1=on` on the console and every box (index D13), with the copy record approved and the default flipped in one commit before any run on the flagship. Run 4 (declare-lost) needs claim Plan 8b merged and rolled (the roster, census, release identity and guard, the termination fence, the real declare-lost and readmit) and `CARLOS_MOVES_DECLARE_LOST` is never set outside tests; until 8b is rolled, Run 4 is skipped and the series continues with Run 5, and the loss run is repeated on dev once 8b lands.
86
87**Run 1, dev IE → uk, done:**
88- [ ] S-1 `carlos instances create --app moveprobe --host mp1-<date>.<domain> --region ie` (a fresh name; Review Focus 3); wake it (`curl https://<host>/`), `POST /arm-once?in=600`, note `/report`.
89- [ ] S-2 `carlos move --host <host> --to uk --manual-cut`; wait for M-C12; `carlos move status --host <host>` shows every T check passing and `attempts_admitted ≥ 1`.
90- [ ] S-3 `carlos move cut --host <host>`; expect M-C5, M-C6, M-C7; exit 0.
91- [ ] R-1 `/report` on uk: the key fingerprint equals S-1's; the object reads back; `has_secret` true.
92- [ ] R-2 the `bg` and `worker` row counts are at least S-1's and every `seq` is contiguous (no missing row).
93- [ ] R-3 the `every-minute` rows continue on uk with region `uk` from the first tick after the open.
94- [ ] R-4 the once-timer fired exactly once, on uk, with S-1's `due_at` (Review Focus 2).
95- [ ] R-5 `aws s3api list-object-versions --bucket <ie data bucket> --prefix instances/<host>/` is empty after the source park acknowledged (`carlos move status` detail shows no obligation; the log has `source_parked` and `done`); `parked/<inc>/<host>/<ret>/` holds the archive.
96- [ ] R-6 the move log has no `stray` page; the ie box's journal shows `ReconstructStopEvidence` killed the worker (the `tree` step OK) (Review Focus 1).
97- [ ] R-7 (not for this run).
98
99**Run 2, dev forced clean rollback:** `carlos env set --app moveprobe MOVEPROBE_FAIL_READY_IN=ie` (the before-write mode on the target, moving uk → ie: nothing is written there, so the digests equal the install's); request and cut; expect M-C8 then M-C9, exit 3; checks R-1 to R-6 on uk (the source), plus: the uk row is `ready` again, the ie bucket's `instances/<host>/` and `moves/<host>/<mv>/` are empty after `cleaned`, `rollback.dirty == false` in the log's `released` event, and the `released` event's three digests equal `landing.installed_db_sha256`, `source.files.plain` and `source.timers_sha256`.
100
101**Run 3, dev forced dirty rollback:** `MOVEPROBE_FAIL_READY_IN=ie,after-write`; expect exit 3; R-7: the source's `/report` holds a `bg` row with region `ie` written on the target during its probe (Review Focus 4); the log's `released` event has `dirty: true`, `returned` names the lane snapshot; `moves/<host>/<rb>/` in the uk bucket is parked after the horizon.
102
103**Run 4, dev declare-lost (needs P-6's Plan 8b clause):** request a move to a dev box Paul designates as disposable; after the flip, stop that box through the AWS console (dev only); `carlos move resolve --host <host> --declare-lost <box> --instance-id <id>`; type the host name at M-C18; expect the clean rollback to complete on the source, exit 3; the loss record names the box, `fenced_at`, the declared timers; restart the box and watch it quarantine the host's local state before anything else (S-9, Review Focus 5).
104
105**Run 5, flagship, scoped approval:** runs 1 to 3 again on the flagship with a `moveprobe` instance, each with Paul's explicit go in the discussion thread.
106
107**Run 6, a disposable ips instance in `bdn`:** `carlos instances create` of an ips instance Tony designates, run 1 only, with Tony's check that the instance serves after the open.
108
109**Run 7, kpmg:** run 1 only, with Paul's approval, in a window agreed with the customer; the cut budget stays 120 s; the runbook's `offline_s` from the `done` event is the number reported back.
110
111**After every run:** attach the `done`/`rolled_back` log event's timings, `/report`, and the bucket listings to the discussion; a failed check stops the series until its cause is fixed and the run repeated.
112
113- [ ] Commit: `git commit -m "docs: the Phase 1 rehearsal runbook in M12's order (§14.4, §15)" ...`
114
115---
116
117### Task 4: Gate
118
119- [ ] Full gate, cross-compile (`cmd/moveprobe` builds on darwin and windows; the `Setsid` attribute is behind `//go:build !windows`).
120
121## Self-review notes
122
123Coverage: §14.3 (Task 2), §14.4 (Tasks 1, 3), §15 steps 2 to 7 (the runbook's preconditions and order; step 1 and step 8 are done per the spec). The infra change of step 5 is Paul's scoped work and is a precondition, not a task.
124