plan: console release defences, plan 4 of 4 (D4 and rollout)
The release recipe builds and rolls its console dev first. roll-console.sh in carloku-infrastructure gains a dev target, one artifact for every target, a binding check and the dev-proof carry. platform-infrastructure's copy is retired. A rollout runbook, and the operational run, gated on Paul at each production step. The flagship console's binding was confirmed wrong on 2026-10-06.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 file changed,
+439
−0
docs/superpowers/plans/2026-10-06-console-release-defences-plan-4-d4.md+439 −0
CI cancelled — superseded by a newer push — run details
diff --git a/docs/superpowers/plans/2026-10-06-console-release-defences-plan-4-d4.md b/docs/superpowers/plans/2026-10-06-console-release-defences-plan-4-d4.md| new file mode 100644 |
| index 000000000..53cd5be7c |
| --- /dev/null |
| +++ b/docs/superpowers/plans/2026-10-06-console-release-defences-plan-4-d4.md |
| @@ -0,0 +1,439 @@ |
| +# Console release defences, Plan 4 of 4: D4 (the release carries a console; the roll goes dev first) and the rollout |
| + |
| +> **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. |
| + |
| +**Goal:** Each public release publishes a signed console artifact. The flagship |
| +console is rolled with exactly those bytes, through dev first, carrying dev's |
| +proof. The four defences are switched on in the order spec §6 requires. |
| + |
| +**Architecture:** Three repos. The platform README's release recipe gains the |
| +console build and a console roll step. `carloku-infrastructure/scripts/roll-console.sh` |
| +gains: |
| +- a `dev` target; |
| +- `--artifact` and a single `-trimpath` build; |
| +- a binding check; |
| +- the dev-proof carry, with a one-time `--bootstrap`. |
| + |
| +The rollout is a runbook section in carloku-infrastructure's README, run by an |
| +operator over SSM. It is not code. |
| + |
| +**Tech Stack:** bash, the `carlos` CLI (with Plans 1–3 built), AWS CLI with |
| +SSM, `jq`. |
| + |
| +**Spec:** `docs/superpowers/specs/2026-10-06-console-release-defences-design.md` |
| +(rev 5): §5 (D4), §6 (rollout). **Depends on Plans 1–3 being merged and in a |
| +release.** The script calls `carlos proof get|put` (Plan 3) and relies on the |
| +box's settle check (Plan 1). |
| + |
| +## Global Constraints |
| + |
| +- The release builds `console-linux-arm64` and `console-linux-amd64` with `go build -trimpath -ldflags "-s -w -X github.com/carlosframework/platform/internal/version.Build=vX.Y.Z" ./cmd/console` and lists them in the signed `SHA256SUMS.txt` (spec §5.1). |
| +- Dev coordinates: profile `kass`, bucket `carlos-dev-640012953448`, console box `i-05498eada6f0afd23`, origin `platform.dev.carlosframework.com`, region eu-west-1 (spec §5.2). |
| +- Flagship coordinates: profile `keymail`, bucket `carlos-flagship-271376211898`, console box `i-092c0c1eea75723cb`, origin `platform.carlosframework.com`. |
| +- The script refuses a flagship roll without a dev proof, except with `--bootstrap`, which prints that it is skipping (spec §5.2). |
| +- The script keeps its final `systemctl restart` on any box whose binding is wrong (spec §5.2, §2.2). |
| +- The public `carlos-linux-*` binary is never a console artifact, and the recipe says so (spec §5.2). |
| +- README prose in both repos goes through the copy-review skill before it is committed (Paul's standing rule for READMEs). Plans 1 and 2's README notes need the same gate. The copy rule: no em dashes in new prose (`[[copy-voice-no-em-dashes]]`). |
| +- Tito is a third party: the script's `tito` target is left as it is and never run by this plan. |
| +- **Plan decision:** the console routes' readiness uses the TCP-connect fallback (spec §2.4 rule 2) on `127.0.0.1:8090`, not a configured probe. The console's `/healthz` answers as soon as its listener is up, so a probe would check nothing more than the connect does. No box-local CLI sets `registry.Route.Health` on a console row today; adding one is a follow-up, needed only if that equivalence ever stops holding. |
| + |
| +## Review Focus |
| + |
| +1. **The public release's `SHA256SUMS.txt` lacks a console line** (an older release, or a recipe step skipped). Expect: `--artifact` refuses an unverifiable file, never ships it. Covered in Task 2 (`verify_artifact`). |
| +2. **Dev has the build but its proof isn't minted yet** (inside the 15 minutes). Expect: the flagship target exits non-zero naming how long dev has had it, with nothing shipped to flagship. Task 2's dry-run check. |
| +3. **The binding on a box is wrong.** Expect: the script says so, keeps its final restart, and still verifies `/proc/<pid>/exe`. Task 2's dry-run check against the flagship box today (stamped on `console.carloku.com`, while `ExecStart` names `platform.carlosframework.com`; confirm on the box). |
| +4. **An operator rolls a hotfix sha, not a release.** Expect: one `-trimpath` build reused for dev and flagship, so the proof's hash matches. Task 2's build-once rule. |
| +5. **Someone runs the old two-repo habit** (platform-infrastructure's copy of the script for flagship). Expect: that copy says it is retired and points here. Task 3. |
| + |
| +--- |
| + |
| +### Task 1: The release recipe publishes a console and rolls it dev first |
| + |
| +**Files:** |
| +- Modify: `README.md` (platform repo): "Releasing the platform" steps 2, 4 and 6, and a new console step after "Last, roll it" |
| + |
| +- [ ] **Step 1: Draft the recipe changes** |
| + |
| +Step 2, append: |
| + |
| +```markdown |
| + Build the console twice, from the same clean checkout: |
| + `CGO_ENABLED=0 GOOS=linux GOARCH=<arch> go build -trimpath -ldflags "-s -w -X github.com/carlosframework/platform/internal/version.Build=vX.Y.Z" -o console-linux-<arch> ./cmd/console` |
| + for arm64 and amd64. The console is `./cmd/console`. The `carlos` binary is |
| + never a console artifact; shipping it as one took the flagship console down |
| + on 2026-10-06. |
| +``` |
| + |
| +Step 4: "over the eight files" becomes "over the ten files". Step 6: "with all |
| +ten files" becomes "with all twelve files". |
| + |
| +After the "Last, roll it" paragraph, add: |
| + |
| +```markdown |
| +**Roll the console.** After the boxes are current, roll the release's own |
| +console bytes, dev first: |
| + |
| + curl -sSLO $rel/console-linux-arm64 # verified against SHA256SUMS.txt like the CLI above |
| + carloku-infrastructure/scripts/roll-console.sh --artifact console-linux-arm64 --version vX.Y.Z - dev |
| + # wait for dev's proof: 15 minutes of the build running healthily |
| + carloku-infrastructure/scripts/roll-console.sh --artifact console-linux-arm64 --version vX.Y.Z - flagship |
| + |
| +The flagship roll refuses until dev has proven those exact bytes. |
| +``` |
| + |
| +- [ ] **Step 2: Put the new prose through copy review** |
| + |
| +Invoke the `copy-review` skill on the three new passages above. Apply Paul's |
| +edits verbatim. |
| + |
| +- [ ] **Step 3: Commit (platform repo, implementation branch)** |
| + |
| +```bash |
| +git add README.md |
| +git commit -m "README: the release builds and rolls its console, dev first (console defences D4)" |
| +``` |
| + |
| +--- |
| + |
| +### Task 2: `roll-console.sh`: dev target, one artifact, binding check, proof carry |
| + |
| +**Files:** |
| +- Modify: `~/github.com/carlosframework/carloku-infrastructure/scripts/roll-console.sh` (whole file below) |
| +- Create the work on a branch in that repo, `console-release-defences`. It is a GitHub repo: commit on a branch and open a PR for Paul. Do not merge. |
| + |
| +**Interfaces:** |
| +- Consumes: `ship-app.sh <profile> <bucket> <app> <ver> <channel> <kind> <artifact>` (same directory, unchanged); `carlos proof get --app console --sha256 <S>` and `carlos proof put --app console <file>` (Plan 3); `carlos version` (prints `carlos <build>`). |
| +- Produces: `roll-console.sh [--artifact FILE --version V] [--bootstrap] <platform-checkout|-> dev|flagship|tito`. |
| + |
| +- [ ] **Step 1: Replace the script** |
| + |
| +```bash |
| +#!/bin/bash |
| +# 🤖 Roll the console: build (or take) ONE artifact, ship + promote it, |
| +# verify the running binary. Console release defences D4 (platform spec |
| +# 2026-10-06-console-release-defences-design.md §5.2). |
| +# |
| +# roll-console.sh [--artifact FILE --version V] [--bootstrap] <platform-checkout|-> <dev|flagship|tito> |
| +# |
| +# --artifact rolls exactly FILE (a release's public console-linux-arm64, |
| +# checked against that release's signed SHA256SUMS.txt first), so dev and |
| +# flagship run the bytes the public got. Without it the script builds |
| +# ./cmd/console ONCE at the checkout's HEAD with -trimpath and keeps the file |
| +# in $CONSOLE_BUILD_DIR (default ~/.cache/roll-console/<sha>) for the next |
| +# target: a second build would be different bytes, and flagship's dev proof |
| +# names bytes. |
| +# |
| +# flagship refuses unless dev has proven those bytes (`carlos proof get` |
| +# against dev). --bootstrap skips that once, for the roll that installs D3. |
| +# |
| +# The box adopts and restarts the console itself; its settle check rolls back |
| +# a build that does not come up. The old adopt + restart SSM steps run only on |
| +# a box whose console.service ExecStart does not name the unit-stamped route's |
| +# live link, where the settle check cannot judge the restart. |
| +set -euo pipefail |
| + |
| +artifact= version= bootstrap=0 |
| +while [ $# -gt 2 ]; do |
| + case $1 in |
| + --artifact) artifact=$2; shift 2;; |
| + --version) version=$2; shift 2;; |
| + --bootstrap) bootstrap=1; shift;; |
| + *) echo "unknown flag $1" >&2; exit 2;; |
| + esac |
| +done |
| +repo=${1:?usage: roll-console.sh [--artifact FILE --version V] [--bootstrap] <platform-checkout|-> <dev|flagship|tito>} |
| +target=${2:?target: dev, flagship or tito} |
| +scripts=$(cd "$(dirname "$0")" && pwd) |
| + |
| +# name -> "aws-profile bucket instance-id console-origin" |
| +coords() { |
| + case $1 in |
| + dev) echo "kass carlos-dev-640012953448 i-05498eada6f0afd23 platform.dev.carlosframework.com";; |
| + flagship) echo "keymail carlos-flagship-271376211898 i-092c0c1eea75723cb platform.carlosframework.com";; |
| + tito) echo "titogo carlos-tito-platform-665106592142 i-025a0b70f3d93304d carlos.tito.io";; |
| + *) echo "unknown deployment $1: dev, flagship or tito" >&2; exit 2;; |
| + esac |
| +} |
| +read -r profile bucket instance origin <<<"$(coords "$target")" |
| + |
| +# A native carlos to drive ship/promote/proof. From the checkout when one is |
| +# given, else the one on PATH (it must have `proof`, i.e. Plans 1-3). |
| +tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT |
| +if [ "$repo" != - ]; then |
| + if [ -n "$(git -C "$repo" status --porcelain)" ]; then |
| + echo "refusing to build: $repo has uncommitted changes" >&2; exit 1 |
| + fi |
| + (cd "$repo" && CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X github.com/carlosframework/platform/internal/version.Build=$(git -C "$repo" rev-parse --short HEAD)" -o "$tmp/carlos" .) |
| + carlos=$tmp/carlos |
| +else |
| + carlos=$(command -v carlos) |
| +fi |
| +"$carlos" proof -h >/dev/null 2>&1 || { echo "$carlos has no 'proof' verb: build from a checkout with the console release defences" >&2; exit 1; } |
| + |
| +if [ -n "$artifact" ]; then |
| + [ -n "$version" ] || { echo "--artifact needs --version (the release tag)" >&2; exit 2; } |
| + sums=$(dirname "$artifact")/SHA256SUMS.txt |
| + [ -f "$sums" ] || { echo "no $sums next to $artifact: download the release's SHA256SUMS.txt and .sig and verify them first (README, Releasing the platform)" >&2; exit 1; } |
| + want=$(awk -v f="$(basename "$artifact")" '$2==f {print $1}' "$sums") |
| + [ -n "$want" ] || { echo "$sums has no line for $(basename "$artifact"): this release did not publish a console" >&2; exit 1; } |
| + got=$(sha256sum "$artifact" | cut -d' ' -f1) |
| + [ "$got" = "$want" ] || { echo "$artifact sha256 $got does not match $sums ($want)" >&2; exit 1; } |
| + file=$artifact |
| +else |
| + [ "$repo" != - ] || { echo "no --artifact and no checkout to build from" >&2; exit 2; } |
| + version=$(git -C "$repo" rev-parse --short HEAD) |
| + dir=${CONSOLE_BUILD_DIR:-$HOME/.cache/roll-console/$version} |
| + file=$dir/console-linux-arm64 |
| + if [ ! -f "$file" ]; then |
| + mkdir -p "$dir" |
| + (cd "$repo" && CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath \ |
| + -ldflags "-s -w -X github.com/carlosframework/platform/internal/version.Build=$version" -o "$file" ./cmd/console) |
| + echo "built $file (kept for the next target)" |
| + fi |
| +fi |
| +sha=$(sha256sum "$file" | cut -d' ' -f1) |
| +echo "== console $version sha256 $sha -> $target" |
| + |
| +# ssm_run <command>: one command on this target's console box (jq-built |
| +# parameters; see the 2026-08-09 note in git history for why). |
| +ssm_run() { |
| + local params id status |
| + params=$(jq -n --arg c "$1" '{commands: [$c]}') |
| + id=$(aws --profile "$profile" --region eu-west-1 ssm send-command --instance-ids "$instance" \ |
| + --document-name AWS-RunShellScript --parameters "$params" --query Command.CommandId --output text) |
| + aws --profile "$profile" --region eu-west-1 ssm wait command-executed --command-id "$id" --instance-id "$instance" || true |
| + status=$(aws --profile "$profile" --region eu-west-1 ssm list-command-invocations --command-id "$id" --details \ |
| + --query 'CommandInvocations[0].CommandPlugins[0].Status' --output text) |
| + aws --profile "$profile" --region eu-west-1 ssm list-command-invocations --command-id "$id" --details \ |
| + --query 'CommandInvocations[0].CommandPlugins[0].Output' --output text |
| + [ "$status" = Success ] |
| +} |
| + |
| +# The binding: which live link console.service runs, and which route carries |
| +# the unit stamp. Equal means the box's settle check judges the restart. |
| +binding=$(ssm_run 'exec=$(systemctl show -p ExecStart console.service | sed -n "s/.*path=\([^ ;]*\).*/\1/p"); \ |
| + stamped=$(sqlite3 -readonly /data/registry.db "select host from routes where unit=\"console.service\"" | tr "\n" " "); \ |
| + echo "$exec|$stamped"') |
| +exec_link=${binding%%|*}; stamped=${binding#*|} |
| +bound=0 |
| +[ "$exec_link" = "/opt/carlos/live/${stamped% }" ] && bound=1 |
| +if [ $bound = 1 ]; then |
| + echo " binding ok: console.service runs $exec_link, stamped on ${stamped% }" |
| +else |
| + echo " binding WRONG: console.service runs ${exec_link:-?}, unit stamp on '${stamped% }' — keeping the final restart (fix: platform spec §6 step 1)" |
| +fi |
| + |
| +if [ "$target" = flagship ]; then |
| + proof=$tmp/proof.json |
| + if AWS_PROFILE=kass AWS_REGION=eu-west-1 CARLOS_DEPLOYMENT_BUCKET=carlos-dev-640012953448 \ |
| + "$carlos" proof get --app console --sha256 "$sha" >"$proof"; then |
| + echo " dev proof found" |
| + elif [ $bootstrap = 1 ]; then |
| + echo " --bootstrap: NOT checking for a dev proof (only for the roll that installs D3)" |
| + proof= |
| + else |
| + echo "flagship: dev has not proven $sha (above); roll it to dev and wait, or --bootstrap once" >&2 |
| + exit 1 |
| + fi |
| +fi |
| + |
| +CARLOS_BIN=$carlos "$scripts/ship-app.sh" "$profile" "$bucket" console "$version" canary/console binary "$file" |
| +if [ "$target" = flagship ] && [ -n "${proof:-}" ]; then |
| + AWS_PROFILE=keymail AWS_REGION=eu-west-1 CARLOS_DEPLOYMENT_BUCKET=carlos-flagship-271376211898 \ |
| + "$carlos" proof put --app console "$proof" |
| +fi |
| + |
| +if [ $bound = 0 ]; then |
| + ssm_run "bash -c 'set -a; source /etc/carlos/host.env; /opt/carlos/carlos adopt' 2>&1 | tail -8" |
| + ssm_run "systemctl restart console && sleep 2 && systemctl is-active console" |
| +fi |
| + |
| +echo "== $target: waiting for the running binary to be this build" |
| +for i in $(seq 1 24); do |
| + exe=$(ssm_run 'readlink /proc/$(systemctl show -p MainPID --value console)/exe' || true) |
| + case $exe in */versions/$version/console*) break;; esac |
| + sleep 5 |
| +done |
| +case $exe in |
| + */versions/$version/console*) ;; |
| + *) echo "$target: console is running ${exe:-nothing}, not versions/$version/console — check the box's adopt journal for a settle rollback" >&2; exit 1;; |
| +esac |
| +curl -fsS -o /dev/null "https://$origin/" |
| +echo "== $target: console $version live at https://$origin" |
| +``` |
| + |
| +Checked on the flagship console box on 2026-10-06: `/usr/bin/sqlite3` is |
| +present, and `ExecStart` is `/opt/carlos/live/platform.carlosframework.com`, |
| +while `carlos instances` shows the `console.service` stamp on |
| +`console.carloku.com`. That is the wrong binding Review Focus 3 expects. Run |
| +`ssm_run 'command -v sqlite3'` against dev once as well. If dev lacks it, |
| +install it there (`apt-get install -y sqlite3`) rather than branch the |
| +script. |
| + |
| +- [ ] **Step 2: Static checks** |
| + |
| +Run: `bash -n scripts/roll-console.sh && shellcheck -S warning scripts/roll-console.sh` |
| +Expected: no output. Fix every warning except SC2016 on the single-quoted |
| +SSM command strings, which are expanded on the box on purpose. |
| + |
| +- [ ] **Step 3: Read-only check against dev and flagship** |
| + |
| +Temporarily add `[ "${ROLL_DRY:-0}" = 1 ] && exit 0` immediately after the |
| +binding report. Run |
| +`ROLL_DRY=1 scripts/roll-console.sh - dev` and |
| +`ROLL_DRY=1 scripts/roll-console.sh - flagship` with the sandbox off |
| +(assumed-role profiles: `[[aws-from-the-sandbox]]`). |
| + |
| +Expected: |
| +- dev prints its binding line; |
| +- flagship prints `binding WRONG`, unless someone already restamped it, which rollout step 1 does; |
| +- nothing is shipped. |
| + |
| +Then remove the line. |
| + |
| +- [ ] **Step 4: Commit and open the PR (carloku-infrastructure)** |
| + |
| +```bash |
| +cd ~/github.com/carlosframework/carloku-infrastructure |
| +git checkout -b console-release-defences |
| +git add scripts/roll-console.sh |
| +git commit -m "roll-console: dev target, one artifact for every target, binding check, dev proof before flagship" |
| +git push -u origin console-release-defences |
| +gh pr create --title "roll-console: dev first, one artifact, dev proof (console release defences D4)" \ |
| + --body "Implements platform spec 2026-10-06-console-release-defences-design.md §5.2. Not to be run until the platform release carrying Plans 1-3 is on the boxes. |
| + |
| +🤖 Generated with [Claude Code](https://claude.com/claude-code)" |
| +``` |
| + |
| +This repo lives on GitHub, not amadan, so a PR is the right door here. |
| +Leave it for Paul to merge. |
| + |
| +--- |
| + |
| +### Task 3: Retire platform-infrastructure's copy of the script |
| + |
| +**Files:** |
| +- Modify: `~/amadan.net/carlos/platform-infrastructure/scripts/roll-console.sh`: replace the body with a pointer |
| + |
| +- [ ] **Step 1: Replace the body** |
| + |
| +```bash |
| +#!/bin/bash |
| +# Retired 2026-10: the console roll lives in carloku-infrastructure |
| +# (scripts/roll-console.sh), which rolls dev first and carries dev's proof |
| +# to flagship. Platform spec 2026-10-06-console-release-defences-design.md §5.2. |
| +echo "roll-console.sh moved to carloku-infrastructure/scripts/roll-console.sh (dev, flagship, tito)" >&2 |
| +exit 2 |
| +``` |
| + |
| +- [ ] **Step 2: Commit on a branch in that repo** |
| + |
| +Read that repo's own instructions first (its README or AGENTS.md) for where |
| +and how changes land. Then commit: |
| +`git commit -m "roll-console: retired, see carloku-infrastructure"`. |
| + |
| +--- |
| + |
| +### Task 4: The rollout runbook |
| + |
| +**Files:** |
| +- Modify: `~/github.com/carlosframework/carloku-infrastructure/README.md`: add a section "Console release defences: rollout", on the Task 2 branch |
| + |
| +- [ ] **Step 1: Write the section** |
| + |
| +The commands below are the spec §6 steps, made concrete. Each SSM command is |
| +run with `.claude/ssm.sh <profile> <region> <instance> '<cmd>'` or the |
| +script's `ssm_run` pattern. |
| + |
| +````markdown |
| +## Console release defences: rollout |
| + |
| +Run once, after the platform release that carries the defences is published. |
| +In order: |
| + |
| +1. **Fix the bindings.** On each console box (dev `i-05498eada6f0afd23`, |
| + flagship `i-092c0c1eea75723cb`), the one route stamped with |
| + `console.service` must be the host its `ExecStart` names: |
| + |
| + systemctl show -p ExecStart console.service # path=/opt/carlos/live/<host> |
| + /opt/carlos/carlos route --host <that host> --unit console.service |
| + /opt/carlos/carlos route --host <any other stamped host> --unit "" |
| + |
| + On flagship today that means stamping `platform.carlosframework.com` and |
| + clearing `console.carloku.com`. |
| +2. **Roll the boxes** with `carlos system update --console <origin> --version vX.Y.Z` |
| + on dev and flagship, then `carlos system status`. |
| +3. **Dev proving.** On dev's console box, as root: |
| + |
| + /opt/carlos/carlos proof keygen --out /etc/carlos/proving.key # prints the public key |
| + cat > /etc/carlos/proof-policy.json <<'EOF' |
| + {"schema":1,"prove":{"apps":["console"],"prover":"dev","after":"15m","key_file":"/etc/carlos/proving.key"}} |
| + EOF |
| + chmod 0644 /etc/carlos/proof-policy.json |
| + |
| + Record the public key here: `dev proving key: <base64>`. |
| +4. **Roll the release's console to dev:** |
| + `scripts/roll-console.sh --artifact console-linux-arm64 --version vX.Y.Z - dev`. |
| + Watch the box journal for `settled on`. |
| +5. **Rehearse on dev**, then delete each rehearsal release from dev's bucket |
| + (`apps/<acct>/console/releases/<ver>/`) and its staged copy on the box: |
| + - ship the public `carlos-linux-arm64` as console `rehearse-cli` with a |
| + **v0.24.1** `carlos`, direct-bucket mode (`CARLOS_DEPLOYMENT_BUCKET` |
| + set). Promote it, and expect the box journal to show `not pointed live` |
| + with no restart; |
| + - ship a console built from `./cmd/console` with `os.Exit(2)` as the first |
| + line of `main`, as `rehearse-exit`. Expect a rollback within 30 s, a |
| + refusal, and an alert email. A plain re-promote stays refused, and |
| + `carlos promote --retry rehearse-exit canary/console` retries; |
| + - ship a console whose `serve` sleeps forever before listening, as |
| + `rehearse-hang`. Expect a deadline rollback that replaces the process. |
| +6. **Wait for dev's proof** of step 4's build: `carlos proof get --app console --sha256 <sha>` |
| + against dev. |
| +7. **Roll it to flagship:** `scripts/roll-console.sh --artifact console-linux-arm64 --version vX.Y.Z - flagship`. |
| +8. **Switch flagship's requirement on.** On flagship's console box, as root: |
| + |
| + cat > /etc/carlos/proof-policy.json <<'EOF' |
| + {"schema":1,"require":[{"app":"console","prover":"dev","key":"<dev proving key>"}]} |
| + EOF |
| + chmod 0644 /etc/carlos/proof-policy.json |
| + |
| + The live build is unaffected. It enters the box's healthy history after |
| + 15 minutes. |
| + |
| +**Break glass:** on flagship, `rm /etc/carlos/proof-policy.json`, then |
| +`/opt/carlos/carlos adopt --stage-only`. The box logs hourly that the |
| +requirement is off until the file is back. |
| + |
| +The policy files are not in tofu `user_data` on purpose: a whole-root apply |
| +restarts every box. |
| +```` |
| + |
| +The `carlos proof keygen --out` flag name must match what Plan 3 defines. |
| +Check Plan 3's `proof keygen` task and use its exact flags here before |
| +committing. |
| + |
| +- [ ] **Step 2: Put the section through copy review** |
| + |
| +Invoke the `copy-review` skill on the section's prose (not the commands). |
| +Apply Paul's edits verbatim. |
| + |
| +- [ ] **Step 3: Commit on the Task 2 branch and push** |
| + |
| +```bash |
| +git add README.md |
| +git commit -m "README: console release defences rollout runbook" |
| +git push |
| +``` |
| + |
| +--- |
| + |
| +### Task 5: Run the rollout |
| + |
| +This task is operational and outward-facing: it changes production boxes. |
| +**Confirm with Paul before each of steps 1, 7 and 8** of the runbook. |
| +Steps 2–6 touch only dev or are read-only on flagship, so they need his |
| +go-ahead once, at the start. |
| + |
| +- [ ] **Step 1:** Run runbook steps 1–8 in order, pasting each command's output into the branch's amadan discussion (`amadan discuss new carlos/platform -branch <impl branch> -title "Console release defences rollout"`). |
| +- [ ] **Step 2:** After step 8, check `carlos status --live` against flagship shows no refusals and `curl -sI https://console.carloku.com/ | grep -i x-carlos-version` names vX.Y.Z. |
| +- [ ] **Step 3:** Update the `[[console-release-defences]]` memory with what was rolled and when. |