carlos / platform Public

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>
Paul Campbell 1d67c651e5b1ae02fbdd61ffd1051b545aeb419f parent df6ded2
1 file changed, +533 −0
  • docs/superpowers/plans/2026-08-02-platform-dev-environment.md +533 −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).