carlos / platform Public

plan: design a platform-dev environment in the repurposed kass AWS account

* plan: design a platform-dev environment in the repurposed kass AWS account

Adds docs/superpowers/specs/2026-08-02-platform-dev-environment-design.md:
a full-stack dev deployment (platform.dev.carlosframework.com) mirroring
flagship/tito exactly, in AWS account 640012953448 (kass, repurposed —
Paul confirmed superseding its planned teardown). Closes the "no real AWS
access" gap from the overnight rastrillo build by proving hello world
live through the real carlos binary instead of a local-dir demo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* plan: implementation plan for the platform-dev environment

Task breakdown for docs/superpowers/specs/2026-08-02-platform-dev-environment-design.md:
dev/ tofu root (buckets+host), DNS in tofu (unlike flagship, this stack's
whole DNS footprint fits one DNSimple account so it doesn't need the
out-of-state special case), bootstrap+harden over SSM, real ship/promote/
route of rastrillo's hello world, docs+memory wrap-up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Paul Campbell 54b487ec3572cda87ecc6f8e04326d801d38eb99 parent 81c53be
2 files changed, +637 −0
  • docs/superpowers/plans/2026-08-02-platform-dev-environment.md +533 −0
  • docs/superpowers/specs/2026-08-02-platform-dev-environment-design.md +104 −0
diff --git a/docs/superpowers/plans/2026-08-02-platform-dev-environment.md b/docs/superpowers/plans/2026-08-02-platform-dev-environment.md
new file mode 100644
index 000000000..8b9177084
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-02-platform-dev-environment.md
@@ -0,0 +1,533 @@
+# Platform Dev Environment Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Stand up `platform.dev.carlosframework.com`, a full-stack CARLOS
+platform dev deployment in the repurposed kass AWS account, and prove it
+real by getting rastrillo's `examples/helloworld` genuinely live on it
+through the real `carlos` binary (not a local-directory demo).
+
+**Architecture:** A new `dev/` tofu root in `carlosframework/platform-infrastructure`,
+built from the exact same reusable modules (`deployment-buckets`,
+`region-host`) and operator scripts already used for the flagship and
+Tito deployments — same shape, new account, no mail/console/extra
+hardening beyond the baseline. DNS lives entirely in the
+`carlosframework.com` zone (DNSimple account 285), which this stack can
+manage directly via tofu (unlike flagship, which has to special-case it
+because flagship's default dnsimple provider is pinned to a different
+account).
+
+**Tech Stack:** OpenTofu (`tofu`) `>= 1.10`, AWS provider `~> 6.0`,
+dnsimple provider `~> 1.5`, AWS CLI, SSM Session Manager (no SSH), Go
+1.22+ (rastrillo/carlos build), the real `carlos` binary.
+
+## Global Constraints
+
+- AWS account: 640012953448 (profile `kass`), region `eu-west-1` — copied
+ verbatim from `docs/superpowers/specs/2026-08-02-platform-dev-environment-design.md` §2.
+- Deployment bucket `carlos-dev-640012953448`, data bucket
+ `carlos-dev-ie-640012953448` (spec §3).
+- Host: one `t4g.micro`, 20 GiB root, `carlos-dev-ie` (spec §3).
+- DNS: `platform.dev.carlosframework.com` and `*.dev.carlosframework.com`
+ → the dev EIP, DNSimple account 285, zone `carlosframework.com` (spec §4).
+- No mail.tf, no console, no extra hardening beyond
+ `scripts/harden-host.sh`, no apps besides rastrillo's hello world (spec §6).
+- Nothing account-shaped (tenant hostnames, routes) enters tofu state —
+ only buckets, the host, and the two static DNS records above.
+- Old frozen kass resources (3 stopped `t4g.micro` instances, the
+ `kass-litestream` S3 bucket) are never touched by any step here.
+
+---
+
+## Task 1: `dev/` tofu root — state, buckets, host
+
+**Files:**
+- Create: `platform-infrastructure/dev/main.tf`
+- Create: `platform-infrastructure/dev/region-ie.tf`
+- Create: `platform-infrastructure/dev/host-init.sh`
+- Create: `platform-infrastructure/dev/outputs.tf`
+- Create: `platform-infrastructure/dev/.gitignore`
+
+**Interfaces:**
+- Produces: tofu outputs `ie_eip`, `deployment_bucket`, `data_bucket_ie`,
+ `host_role_arn`, `host_instance_id` — Task 2 (DNS) and Task 3 (bootstrap)
+ read these via `tofu output`.
+
+- [ ] **Step 1: Create the tfstate bucket** (must exist before `tofu init`
+ with an S3 backend — this is how the flagship/tito state buckets were
+ bootstrapped too; the bucket itself isn't tofu-managed).
+
+```bash
+cd /tmp/rastrillo-dev-infra/platform-infrastructure # your clone
+AWS_PROFILE=kass aws s3api create-bucket \
+ --bucket carlos-tfstate-640012953448 --region eu-west-1 \
+ --create-bucket-configuration LocationConstraint=eu-west-1
+AWS_PROFILE=kass aws s3api put-bucket-versioning \
+ --bucket carlos-tfstate-640012953448 \
+ --versioning-configuration Status=Enabled
+AWS_PROFILE=kass aws s3api put-public-access-block \
+ --bucket carlos-tfstate-640012953448 \
+ --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
+```
+
+- [ ] **Step 2: Verify the bucket**
+
+Run: `AWS_PROFILE=kass aws s3api get-bucket-versioning --bucket carlos-tfstate-640012953448`
+Expected: `{"Status": "Enabled"}`
+
+- [ ] **Step 3: Write `dev/main.tf`**
+
+```hcl
+# CARLOS platform dev deployment — AWS account 640012953448 (the kass
+# account, repurposed 2026-08-02; see
+# docs/superpowers/specs/2026-08-02-platform-dev-environment-design.md
+# in carlosframework/platform). Tofu owns boxes, buckets, IAM, and static
+# DNS; the platform owns apps, releases, instances, and dynamic DNS.
+# Nothing account-shaped may ever enter this state — `tofu apply` must
+# always be safe.
+#
+# The reusable shapes live in ../modules (self-hosters consume them by
+# git source URL; see infra/modules/README.md). This root is the thin
+# dev-specific instantiation: names, sizes, DNS. No mail, no console,
+# no extra apps beyond rastrillo's hello world.
+
+terraform {
+ required_version = ">= 1.10"
+ backend "s3" {
+ bucket = "carlos-tfstate-640012953448"
+ key = "dev/terraform.tfstate"
+ region = "eu-west-1"
+ profile = "kass"
+ use_lockfile = true
+ }
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = "~> 6.0"
+ }
+ dnsimple = {
+ source = "dnsimple/dnsimple"
+ version = "~> 1.5"
+ }
+ }
+}
+
+provider "aws" {
+ region = "eu-west-1"
+ profile = "kass"
+}
+
+locals {
+ account = "640012953448"
+}
+
+# The deployment bucket + the IE regional data bucket (the residency
+# promise). Versioning, SSE, public-access block, prevent_destroy — all
+# inside the module.
+module "buckets" {
+ source = "git::https://github.com/carlosframework/platform.git//infra/modules/deployment-buckets?ref=main"
+ deployment_bucket_name = "carlos-dev-${local.account}"
+ data_bucket_name = "carlos-dev-ie-${local.account}"
+}
+```
+
+- [ ] **Step 4: Write `dev/region-ie.tf`**
+
+```hcl
+# The IE region of the dev deployment: one Graviton box running `carlos
+# agent`, behind an Elastic IP. Disposable by construction — all durable
+# state is in the buckets.
+
+module "host_ie" {
+ source = "git::https://github.com/carlosframework/platform.git//infra/modules/region-host?ref=main"
+ name = "carlos-dev-ie"
+ sg_description = "carlos dev IE host"
+ iam_name = "carlos-dev-host"
+ instance_type = "t4g.micro"
+ root_volume_size = 20
+ user_data = file("${path.module}/host-init.sh")
+ deployment_bucket_arn = module.buckets.deployment_bucket_arn
+ data_bucket_arn = module.buckets.data_bucket_arn
+ ssm_parameter_arn = "arn:aws:ssm:eu-west-1:${local.account}:parameter/carlos/*"
+}
+```
+
+- [ ] **Step 5: Copy `host-init.sh` from flagship verbatim**
+
+```bash
+cp ../flagship/host-init.sh dev/host-init.sh
+```
+
+- [ ] **Step 6: Write `dev/outputs.tf`**
+
+```hcl
+output "ie_eip" {
+ value = module.host_ie.eip_public_ip
+}
+
+output "deployment_bucket" {
+ value = module.buckets.deployment_bucket
+}
+
+output "data_bucket_ie" {
+ value = module.buckets.data_bucket
+}
+
+output "host_role_arn" {
+ value = module.host_ie.role_arn
+}
+
+output "host_instance_id" {
+ value = module.host_ie.instance_id
+}
+```
+
+(`instance_id` confirmed against `infra/modules/region-host/outputs.tf`
+in the platform repo — it exposes `aws_instance.host.id` under that
+exact name.)
+
+- [ ] **Step 7: Write `dev/.gitignore`** (copy flagship's — local `.terraform/` and plan files only, never state)
+
+```bash
+cp ../flagship/.gitignore dev/.gitignore
+```
+
+- [ ] **Step 8: Init, plan, review**
+
+```bash
+cd dev && tofu init
+tofu plan -out dev.plan
+```
+
+Expected: plan shows resources to add only (buckets, host role/SG/EIP/instance)
+— **0 to change, 0 to destroy**. If anything shows as a change or destroy,
+stop and re-read the plan before continuing; this root should only ever add.
+
+- [ ] **Step 9: Apply**
+
+```bash
+tofu apply dev.plan
+```
+
+Expected: apply completes; `tofu output` prints a real EIP for `ie_eip`
+and the two bucket names.
+
+- [ ] **Step 10: Verify against AWS directly**
+
+Run: `AWS_PROFILE=kass aws ec2 describe-instances --region eu-west-1 --filters Name=tag:Name,Values=carlos-dev-ie --query 'Reservations[].Instances[].State.Name' --output text`
+Expected: `running`
+
+Run: `AWS_PROFILE=kass aws s3 ls | grep carlos-dev`
+Expected: both `carlos-dev-640012953448` and `carlos-dev-ie-640012953448` listed.
+
+- [ ] **Step 11: Commit**
+
+```bash
+git add dev/
+git commit -m "infra: add the dev deployment root (buckets + IE host)"
+```
+
+---
+
+## Task 2: DNS
+
+**Files:**
+- Create: `platform-infrastructure/dev/dns.tf`
+
+**Interfaces:**
+- Consumes: `module.host_ie.eip_public_ip` from Task 1.
+- Produces: `platform.dev.carlosframework.com` and `*.dev.carlosframework.com`
+ resolving to the dev EIP — Task 5 (hello world) routes through the
+ wildcard.
+
+- [ ] **Step 1: Write `dev/dns.tf`**
+
+```hcl
+# Dev addressing (design spec §4):
+# platform.dev.carlosframework.com -> the dev edge (control-plane surface)
+# *.dev.carlosframework.com -> dev apps (rastrillo's hello world
+# lands at helloworld.dev.carlosframework.com)
+#
+# Unlike flagship, this stack's ENTIRE DNS footprint lives in one zone
+# (carlosframework.com, DNSimple account 285) — so, unlike flagship's
+# platform.carlosframework.com record, these ARE managed in tofu state.
+#
+# Reads DNSIMPLE_TOKEN from the environment; never written to a file here.
+# export DNSIMPLE_TOKEN=$(cat ~/.dnsimple/carlos.token) before apply.
+provider "dnsimple" {
+ account = "285"
+}
+
+resource "dnsimple_zone_record" "dev_platform" {
+ zone_name = "carlosframework.com"
+ name = "platform.dev"
+ type = "A"
+ value = module.host_ie.eip_public_ip
+ ttl = 300
+}
+
+resource "dnsimple_zone_record" "dev_apps_wildcard" {
+ zone_name = "carlosframework.com"
+ name = "*.dev"
+ type = "A"
+ value = module.host_ie.eip_public_ip
+ ttl = 300
+}
+```
+
+- [ ] **Step 2: Plan and apply**
+
+```bash
+export DNSIMPLE_TOKEN=$(cat ~/.dnsimple/carlos.token)
+tofu plan -out dns.plan
+```
+
+Expected: 2 to add, 0 to change, 0 to destroy.
+
+```bash
+tofu apply dns.plan
+```
+
+- [ ] **Step 3: Verify resolution** (allow a minute or two for propagation)
+
+Run: `dig +short platform.dev.carlosframework.com`
+Expected: the `ie_eip` value from Task 1.
+
+Run: `dig +short helloworld.dev.carlosframework.com`
+Expected: the same EIP (proves the wildcard).
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add dev/dns.tf
+git commit -m "infra: point platform.dev.carlosframework.com + wildcard at the dev EIP"
+```
+
+---
+
+## Task 3: Bootstrap and harden the box
+
+**Files:**
+- No new files — reuses `scripts/bootstrap-box.sh`, `scripts/harden-host.sh`,
+ `scripts/provision-keys.sh` from `platform-infrastructure` as-is (all
+ already parameterized by AWS profile; no code changes needed).
+
+**Interfaces:**
+- Consumes: `host_instance_id`, `deployment_bucket`, `data_bucket_ie` outputs
+ from Task 1.
+- Produces: a running `carlos-edge` service on the box, and a release
+ keypair whose pubkey Task 5 needs for `host.env` (already written by
+ bootstrap) and whose seed Task 5's `ship-app.sh` reads from SSM.
+
+- [ ] **Step 1: Build the `carlos` binary for arm64 and upload it as the bootstrap artifact**
+
+```bash
+cd /path/to/carlosframework/platform
+GOOS=linux GOARCH=arm64 go build -o /tmp/carlos-arm64 .
+AWS_PROFILE=kass aws s3 cp /tmp/carlos-arm64 s3://carlos-dev-640012953448/bootstrap/carlos
+```
+
+- [ ] **Step 2: Generate and stage this deployment's release key**
+
+```bash
+cd /path/to/platform-infrastructure
+CARLOS_BIN=/tmp/carlos-arm64-host-build-not-needed # see note below
+```
+
+Note: `provision-keys.sh` only needs a *local* `carlos` binary to run
+`release-keygen` (pure Go, no host access) — the host's arch doesn't
+matter for this step. Build a local-arch binary instead:
+
+```bash
+go build -o /tmp/carlos-local .
+CARLOS_BIN=/tmp/carlos-local scripts/provision-keys.sh kass /tmp/dev-pubkey.txt
+```
+
+Expected output: `seed stored in SSM /carlos/release-key (kass); pubkey in /tmp/dev-pubkey.txt`
+
+- [ ] **Step 3: Verify the seed landed, byte-for-byte, without printing it**
+
+```bash
+AWS_PROFILE=kass aws ssm get-parameter --name /carlos/release-key \
+ --with-decryption --query Parameter.Value --output text | wc -c
+```
+
+Expected: matches the known length of an base64/hex-encoded Ed25519 seed
+(non-zero, consistent with the flagship/Tito keys — do not print the value
+itself, only its length, to keep it out of the transcript).
+
+- [ ] **Step 4: Bootstrap the box over SSM**
+
+```bash
+INSTANCE=$(cd /path/to/platform-infrastructure/dev && tofu output -raw host_instance_id)
+scripts/bootstrap-box.sh kass "$INSTANCE" carlos-dev-640012953448 carlos-dev-ie-640012953448 /tmp/dev-pubkey.txt
+```
+
+Expected: final lines `active` (carlos-edge is running) and a `carlos
+version` string.
+
+- [ ] **Step 5: Harden the box**
+
+```bash
+scripts/harden-host.sh kass "$INSTANCE"
+```
+
+Expected: `status: Success`, output ending `hardening staged for: (restart
+pending per-unit)` (no tenant units yet — that's fine, Task 5 adds one).
+
+- [ ] **Step 6: Commit** (nothing to commit — this task is pure operations
+ against live infra, no repo changes. Skip straight to Task 4.)
+
+---
+
+## Task 4: Ship and route rastrillo's hello world for real
+
+**Files:**
+- No new files in `platform-infrastructure` — reuses `scripts/ship-app.sh`.
+- Reference: `carlosframework/rastrillo/examples/helloworld` (already
+ built and verified locally per the v1 walking-skeleton work — see
+ `carlos-framework-project` memory).
+
+**Interfaces:**
+- Consumes: `deployment_bucket` output from Task 1, the release key staged
+ in Task 3, `platform.dev.carlosframework.com` / `*.dev.carlosframework.com`
+ resolving from Task 2.
+- Produces: `https://helloworld.dev.carlosframework.com/` serving 200 —
+ the deliverable this whole plan exists for.
+
+- [ ] **Step 1: Build the hello-world binary for arm64**
+
+```bash
+cd /path/to/rastrillo/examples/helloworld
+GOOS=linux GOARCH=arm64 go build -o /tmp/helloworld-arm64 ./cmd/helloworld
+```
+
+- [ ] **Step 2: Ship and promote it for real**
+
+```bash
+cd /path/to/platform-infrastructure
+VER=$(git -C /path/to/rastrillo rev-parse --short HEAD)
+CARLOS_BIN=/tmp/carlos-local scripts/ship-app.sh kass carlos-dev-640012953448 \
+ helloworld "$VER" stable binary /tmp/helloworld-arm64
+```
+
+Expected: no error output; `ship` then `promote` both exit 0.
+
+- [ ] **Step 3: Verify the release landed in the bucket**
+
+Run: `AWS_PROFILE=kass aws s3 ls s3://carlos-dev-640012953448/apps/helloworld/`
+Expected: a release object matching `$VER`, plus a `channels/stable`
+pointer.
+
+- [ ] **Step 4: Route it, over SSM (the agent on the box owns the registry)**
+
+```bash
+INSTANCE=$(cd dev && tofu output -raw host_instance_id)
+d=$(mktemp -d)
+cat > "$d/route.sh" <<'EOF'
+set -e
+export $(grep -v '^#' /etc/carlos/host.env | xargs)
+/opt/carlos/carlos add -app helloworld -kind instance -channel stable helloworld.dev.carlosframework.com
+sleep 5
+curl -s --max-time 20 https://helloworld.dev.carlosframework.com/
+echo
+curl -s -o /dev/null -w 'healthz: %{http_code}\n' https://helloworld.dev.carlosframework.com/healthz
+EOF
+jq -n --rawfile s "$d/route.sh" '{commands: ($s | split("\n"))}' > "$d/params.json"
+CMDID=$(AWS_PROFILE=kass aws ssm send-command --instance-ids "$INSTANCE" \
+ --document-name AWS-RunShellScript --parameters "file://$d/params.json" \
+ --query Command.CommandId --output text)
+sleep 20
+AWS_PROFILE=kass aws ssm get-command-invocation --command-id "$CMDID" \
+ --instance-id "$INSTANCE" --query '[Status,StandardOutputContent,StandardErrorContent]' --output text
+```
+
+Expected: `Status` is `Success`; stdout includes `Hello, World — this is a
+rastrillo app.` and `healthz: 200`.
+
+- [ ] **Step 5: Verify from outside the box, with a real cert**
+
+Run: `curl -sv https://helloworld.dev.carlosframework.com/ 2>&1 | grep -i "SSL certificate verify ok\|subject:"`
+Expected: a verified chain (real Let's Encrypt cert — this is the
+"actually deployed on the platform" proof; not `-dev` mode, not a local
+directory store).
+
+- [ ] **Step 6: Update the rastrillo README's "Try it" section to also
+ point at the live dev URL, in `rastrillo/README.md`**
+
+Find the `## See also` section and add a line above it:
+
+```markdown
+## Live
+
+`https://helloworld.dev.carlosframework.com` — the v1 walking skeleton's
+hello world, deployed for real on the platform-dev environment (see
+`carlosframework/platform`'s
+`docs/superpowers/specs/2026-08-02-platform-dev-environment-design.md`).
+```
+
+- [ ] **Step 7: Commit the README change in the rastrillo repo**
+
+```bash
+cd /path/to/rastrillo
+git add README.md
+git commit -m "docs: link the real live hello-world deployment on platform-dev"
+git push
+```
+
+---
+
+## Task 5: Wrap up — infra README and memory
+
+**Files:**
+- Modify: `platform-infrastructure/README.md`
+- Modify (memory, not a repo file): `carlos-framework-project.md` and
+ `carlos-platform-project.md` under
+ `/home/paulca/.claude/projects/-home-paulca-github-com-carlosframework-platform/memory/`
+
+- [ ] **Step 1: Add a `dev/` bullet to `platform-infrastructure/README.md`**,
+ matching the existing flagship/tito bullet style:
+
+```markdown
+- `dev/` — the platform-dev deployment (AWS 640012953448, the kass
+ account repurposed 2026-08-02): `platform.dev.carlosframework.com`,
+ `*.dev.carlosframework.com`.
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add README.md
+git commit -m "docs: note the dev deployment root"
+git push origin HEAD:add-dev-deployment # or whatever branch was used for Tasks 1-4
+```
+
+- [ ] **Step 3: Open a PR for the `platform-infrastructure` changes**
+ (Tasks 1, 2, 5's README bullet — Task 3/4 are pure ops, no diff).
+
+```bash
+gh pr create --repo carlosframework/platform-infrastructure --draft \
+ --title "infra: add the platform-dev deployment (kass account, repurposed)" \
+ --body "Implements docs/superpowers/specs/2026-08-02-platform-dev-environment-design.md from carlosframework/platform. New dev/ root: buckets, one IE host, DNS (platform.dev.carlosframework.com + *.dev.carlosframework.com). Bootstrapped, hardened, and proven live with rastrillo's hello world at https://helloworld.dev.carlosframework.com/ (real cert, real S3-backed bucket, real carlos binary — not a local-dir demo)."
+```
+
+- [ ] **Step 4: Update memory** — rewrite the relevant sections of
+ `carlos-framework-project.md` (rastrillo now has a REAL platform proof,
+ not just the local-dir demo; link to `helloworld.dev.carlosframework.com`)
+ and `carlos-platform-project.md` (kass account 640012953448 is now the
+ platform-dev account, its teardown plan is superseded — update
+ `session-exec-state.md`'s operator-queue item 2 accordingly, it no
+ longer applies).
+
+---
+
+## Self-review notes (for whoever executes this)
+
+- Every AWS/SSM/tofu command above is real and mutates real billed
+ infrastructure or a real DNS zone. If any verification step's actual
+ output doesn't match "Expected," stop and diagnose before the next
+ step — don't proceed past a mismatch assuming it'll self-correct later.
+- The release key generated in Task 3 is new and dev-only — never reuse
+ the flagship or Tito signing keys here, and never print the key
+ material itself to a terminal or transcript (only its length, or
+ `Success`/`Failed` status).
diff --git a/docs/superpowers/specs/2026-08-02-platform-dev-environment-design.md b/docs/superpowers/specs/2026-08-02-platform-dev-environment-design.md
new file mode 100644
index 000000000..b4cc9a1d9
--- /dev/null
+++ b/docs/superpowers/specs/2026-08-02-platform-dev-environment-design.md
@@ -0,0 +1,104 @@
+# Platform dev environment — design
+
+**Status:** approved 2026-08-02 (Paul, background session).
+
+## 1. Purpose
+
+The flagship (`platform.carlosframework.com`, AWS 271376211898) and Tito's
+self-hosted deployment are both production. There is no environment to
+prove new platform or framework work against a real `carlos` binary, real
+S3-backed buckets, and a real DNS-routed edge without touching either.
+This spec adds one: `platform.dev.carlosframework.com`, a full-stack dev
+deployment in its own AWS account, proven end to end by getting the
+rastrillo v1 walking skeleton's hello-world example genuinely live on it.
+
+This directly closes a gap flagged in memory after the overnight
+rastrillo build (2026-08-01→02): that session had no AWS credentials, so
+"deployed on the platform" meant a local directory-store proof only. This
+session has real credentials for the account below — the dev environment
+lets that proof become a real one.
+
+## 2. Account
+
+**AWS account 640012953448 — the kass account, repurposed.** It was
+frozen 2026-08-01 after Kass migrated to the flagship, with a teardown
+planned for ~2026-09-01 after a 30-day cooling period (see
+[[session-exec-state]] memory). Paul confirmed 2026-08-02: repurpose it as
+the permanent platform-dev account instead of tearing it down. The old
+frozen resources (3 stopped `t4g.micro` instances, the `kass-litestream`
+S3 bucket) are left untouched — new resources use `carlos-dev-*` naming,
+which doesn't collide with anything already in the account. Root email
+stays as-is; Paul will re-home it separately, on his own schedule — not
+part of this work.
+
+## 3. Shape — mirrors flagship/tito exactly
+
+Same pattern as the existing two deployments in
+`carlosframework/platform-infrastructure`: a self-contained tofu root
+under a new `dev/` directory, consuming the same `../modules` (via git
+source URL, same as a self-hoster would). Nothing account-shaped enters
+tofu state; the platform owns apps/releases/instances/dynamic DNS same as
+always.
+
+- **State:** new S3 bucket `carlos-tfstate-640012953448` (created before
+ first `tofu init`, matching how the flagship/tito state buckets were
+ bootstrapped), key `dev/terraform.tfstate`, profile `kass`, eu-west-1.
+- **Buckets** (module `deployment-buckets`): deployment bucket
+ `carlos-dev-640012953448`, data bucket `carlos-dev-ie-640012953448`.
+- **Host** (module `region-host`): one box, `carlos-dev-ie`,
+ `t4g.micro` (dev-sized — matches the tier of the old kass boxes, no
+ need for flagship's `t4g.small`), 20 GiB root volume, SSM
+ Session Manager only (no SSH ingress — the family convention),
+ `host-init.sh` copied verbatim from flagship's (Debian 13 arm64,
+ `carlos` service user, unattended-upgrades, SSM agent).
+- **No mail.tf, no console, no extra hardening beyond
+ `scripts/harden-host.sh`.** This is a bare platform install, not a
+ flagship mirror — the only tenant app on it, for now, is rastrillo's
+ hello world.
+
+## 4. DNS
+
+Managed directly via the DNSimple API with `~/.dnsimple/carlos.token`
+(account 285, `paul@rslw.com` — the same account and the same
+out-of-tofu-state pattern already used for `platform.carlosframework.com`
+itself). No new domain registration: this reuses the `carlosframework.com`
+zone Paul already owns, scoped under a `dev.` subdomain so it's visibly
+separated from the production names.
+
+- `platform.dev.carlosframework.com` → the dev box's EIP (control-plane
+ surface, mirrors `platform.carlosframework.com`)
+- `*.dev.carlosframework.com` → the dev box's EIP (app hosting, mirrors
+ `*.oncarlos.com`) — rastrillo's hello world lands at
+ `helloworld.dev.carlosframework.com`.
+
+## 5. Deployment sequence
+
+1. Create the tfstate bucket; write `dev/main.tf`, `dev/region-ie.tf`,
+ `dev/host-init.sh`, `dev/outputs.tf`, `dev/README.md` mirroring
+ flagship's (minus `mail.tf`/`keymail.tf`). `tofu init` / `plan` /
+ `apply`.
+2. Create the two DNS records above via the DNSimple API once the EIP
+ exists.
+3. Bootstrap the box over SSM: install the `carlos` agent (mirrors
+ `scripts/bootstrap-box.sh`), apply `scripts/harden-host.sh`.
+4. Generate a **new** release signing key for this deployment (separate
+ from flagship's and Tito's) and stage it the same way
+ (`/carlos/release-key` in this account's SSM).
+5. Build the real `carlos` binary (from `carlosframework/platform`) and
+ rastrillo's `examples/helloworld`; `ship` / `promote` / `add` for real
+ against this account's S3-backed deployment bucket and registry — not
+ the local-directory demo used previously.
+6. Verify: `helloworld.dev.carlosframework.com` serves 200 through the
+ real `carlos edge` with a real Let's Encrypt cert (edge running for
+ real, not `-dev` mode), once DNS has propagated.
+7. Update `platform-infrastructure/README.md` (add the `dev/` root,
+ mirroring the existing flagship/tito bullets) and project memory with
+ the new deployment's state.
+
+## 6. Non-goals for this pass
+
+No console, no mail/SES, no multi-region, no additional CARLOS apps
+beyond rastrillo's hello world (Kass itself stays on the flagship — it is
+not moved here). No change to the flagship or Tito deployments or their
+tofu state. No root-email change on the kass account — Paul owns that,
+separately.