carlos / platform Public

infra: deployment roots move to carlosframework/platform-infrastructure

Modules stay here (open, consumable by git URL); the flagship and tito
instantiations live in the private infra repo. Both verified planning
zero-change from their new home.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell a91a7ae4ab5039cf7ca560f226b1452a515419c8 parent 63a8a16
31 files changed, +12 −1114
  • infra/README.md +12 −0
  • infra/flagship/.gitignore +0 −3
  • infra/flagship/.terraform.lock.hcl +0 −75
  • infra/flagship/README.md +0 −55
  • infra/flagship/dns.tf +0 −31
  • infra/flagship/host-init.sh +0 −33
  • infra/flagship/mail.tf +0 −106
  • infra/flagship/main.tf +0 −90
  • infra/flagship/outputs.tf +0 −15
  • infra/flagship/plan.out binary
  • infra/flagship/region-ie.tf +0 −56
  • infra/flagship/scripts/copy-kass-secrets.sh +0 −23
  • infra/scripts/bootstrap-box.sh +0 −47
  • infra/scripts/promote-kass-rehearsal.sh +0 −10
  • infra/scripts/promote-pending.sh +0 −17
  • infra/scripts/promote-platform-site.sh +0 −10
  • infra/scripts/promote-titogo-canary.sh +0 −9
  • infra/scripts/provision-keys.sh +0 −15
  • infra/scripts/rehearse-kass.sh +0 −73
  • infra/scripts/ship-app.sh +0 −13
  • infra/scripts/smoke-flagship.sh +0 −35
  • infra/tito/.gitignore +0 −3
  • infra/tito/.terraform.lock.hcl +0 −39
  • infra/tito/README.md +0 −43
  • infra/tito/dns.tf +0 −18
  • infra/tito/host-init.sh +0 −30
  • infra/tito/main.tf +0 −86
  • infra/tito/outputs.tf +0 −15
  • infra/tito/region-ie.tf +0 −60
  • infra/tito/svc-pdf-init.sh +0 −44
  • infra/tito/svc-pdf.tf +0 −60
diff --git a/infra/README.md b/infra/README.md
new file mode 100644
index 000000000..52faa45e7
--- /dev/null
+++ b/infra/README.md
@@ -0,0 +1,12 @@
+# ๐Ÿค– Platform infra modules
+
+> ๐Ÿค– Written by an LLM (Claude), per factor X.
+
+`modules/` holds the reusable OpenTofu modules any CARLOS deployment
+builds on โ€” see `modules/README.md` for docs and a self-hosting example.
+
+The deployments Paul operates (flagship, Tito) instantiate these modules
+from the private repo
+[carlosframework/platform-infrastructure](https://github.com/carlosframework/platform-infrastructure),
+consuming them by git source URL โ€” exactly the way a self-hoster's own
+infra repo would.
diff --git a/infra/flagship/.gitignore b/infra/flagship/.gitignore
deleted file mode 100644
index 58401f092..000000000
--- a/infra/flagship/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-*.tfstate*
-.terraform/
-plan.out
diff --git a/infra/flagship/.terraform.lock.hcl b/infra/flagship/.terraform.lock.hcl
deleted file mode 100644
index b47796b99..000000000
--- a/infra/flagship/.terraform.lock.hcl
+++ /dev/null
@@ -1,75 +0,0 @@
-# This file is maintained automatically by "tofu init".
-# Manual edits may be lost in future updates.
-
-provider "registry.opentofu.org/dnsimple/dnsimple" {
- version = "1.10.0"
- constraints = "~> 1.5"
- hashes = [
- "h1:EVDDjVKBij3ivkK6+tyS9jWxSExxVvOoGtMS8/M43f8=",
- "h1:HK5nqc5bZFdth/BcNc9GqUfXAYsDn8Ev/tgfmB0jMYI=",
- "h1:Hhbv3mnLhgY9NuvjzUqDo0PQKNTfStULx+m+S3wHKQg=",
- "h1:IHnPrj3/ayQ7PmKNkaq2nzoT87F+hnoSp5c/xdsPjnY=",
- "h1:O13KzReeaEXMiUe6qx2HDgHVVhBan2FnSiq41BMtSgo=",
- "h1:RN2gojQH/bA1zTRUOGdGH8w03uIa2agezDZyMY1VhV8=",
- "h1:UOCA/dFp2ORSVi/WlCCZrjjfXPUy9vpgk4PEBv6h4w0=",
- "h1:YyaCa5mDiLhM8lddBZyjKo5iM9lfunk0uA1X0uOSvoo=",
- "h1:ZNsEnMVMw39wrFufI7N5ypjnl92awiUlZ97z92MUPrs=",
- "h1:cQvKfEDJeGr3a0wWbGLoS+x0Orx8+9mWwSNAYfnqljM=",
- "h1:oV3AqgcHjXhMmoYmdHAerNEDRCkVq9QKvMnsyodsnSc=",
- "h1:r9RpaCOvjJjEZ6S3v3KPB6eNqf/STR9dYc94ippxhGk=",
- "h1:rIdsl1pgd2fqiSyB7lyn6gRh8RPq3MfCA5bt7q4NTWc=",
- "h1:zROoguvUNCxV46qrJGwbH7vEoUwxQJIfZrxb20teJa0=",
- "zh:025dbad287c9a9ca61b84fc4a14d3f595dbbc1139d5cf034597922264b756f6f",
- "zh:0c2fa8513004732f3aae6a408eb68ff9c2ad119f8bcefe491f0420ad0f4fe7d5",
- "zh:1ebb4cebfc29dfd8dd7f4a03f57aebe118a9dda15caf17eace06a16b40507317",
- "zh:5585c10af512519f006491bec5e63504e3682f61af4ab4c4807721de7764bf0f",
- "zh:571ead8072d43334d8ac169c86da21c9b325439508344898988f925f89ea7593",
- "zh:5bdd4d83e0831f0094f06c9d56aed7e1388967e9f4f4144d69c5c37d3d170e45",
- "zh:67982e17e937d71c1b2c83d740cd37a28c9c6ad16d325dddb91327303d6e96ff",
- "zh:7de0afaaed2668c27c7189180ceda3bc73e17b21a150ecec8eb3979e95875323",
- "zh:88911647fc4f8eaac3699b72ca7a0bee8f33521725b839b20235138f5072bfb7",
- "zh:8ad2a7f599864602e3126a9c31ec28b8c277090e98f80cd0d5e29eddb1bebae1",
- "zh:ac47ee0fd0837b57945fe961656f1f8e3c20e31178c49aafa7836b93aa5966a6",
- "zh:acdc1a9a5553d8d0e7aa887acfa0cf440f9c428dabc0c46b61d1b3eae6443e09",
- "zh:b3e3d236b72b5d975098f29e42a8eff071064de99cd81b27e264e5641aed7365",
- "zh:d21daf975282bdddc7c66580d0fde5395a0ed67fdfe3c3c87aec952a6604ba24",
- "zh:f809ab383cca0a5f83072981c64208cbd7fa67e986a86ee02dd2c82333221e32",
- ]
-}
-
-provider "registry.opentofu.org/hashicorp/aws" {
- version = "6.57.1"
- constraints = "~> 6.0"
- hashes = [
- "h1:8wbrCS3R5lunN2HMVfQ+oMB3uFVAO7gfUbItdZa+tdA=",
- "h1:C4XEcAWUKXExKCF6NhvMxkSyZ/JqNWjtwtwwEqQtl70=",
- "h1:Fah+p3BD+/E7wdgazQFp4fsjuvTh5/Q7fQFyMsv9juM=",
- "h1:S6eLURiAh6L00nr+9s2zwuhHQOU04rEG60Z9xO1ORRM=",
- "h1:UdllVJ4482tLgz5q8se3SPzpPoXuFXwvxiykpgdipWs=",
- "h1:ZAkIIdmQryKnldenUI2fqThlAciNz2UYKVTZ5F/57ME=",
- "h1:a/i6TVZX4jSNSvHIWWTqLIlaugg7SYfQMZNVtzDWlKM=",
- "h1:gGtYen1GfQvynKT8lHguY+cvmxNww+TOqCaapWoNW1w=",
- "h1:iFr4+F/b94MEadHCiaNm6o6TapyvLT6TrwmbPyDQs9E=",
- "h1:k0OzZI0aBgeEhCUcoK00/6C3hBK59ETo7g5Jq62wRqQ=",
- "h1:nlvu+qO8eMFdnMptYSUK1c5dOXoK6r3HG87jzE7EYvQ=",
- "h1:rVP8Z9tc37nwsW+NLSvEQyUBoDG/zsMtPSMeHhBrR3k=",
- "h1:rxnmHWTUE2Aou/otNocOI4Rvcm69LQsoNYssj0s7hIo=",
- "h1:va0Q0eDSt7xyvkSX7GbXmAO/QUqugZb/05gMdcQlR0I=",
- "h1:yXF1EzUTMepZv4SeQnBNUvjOj2HKEKSl+jVndIKksGU=",
- "zh:053910cce06bba506fb7a3dcecdcd719280d7df8b8a7cce06ae3e2dd20b1cc0d",
- "zh:14feabeefef742b528c978a78f6896240a7ca3464a4714a5b1e1b2d448db4fd5",
- "zh:22dd549e3c479b2ff2121e00a005aa3b6b43a166e727ef3208a529a59feb1608",
- "zh:3ca5eab63ac77da335d42ebe0af8569c5c617dc5b239750b111f5ba9f2f889cd",
- "zh:3d686f9fca9752850958a353971cd7eb1f5a48e55c2c56fcd96d18cc6e54f8ae",
- "zh:512e46d35abfec0ba85fc0f2da7dc39fece9106f02165e95096b6549f920453a",
- "zh:5fe83903b4bef5b7318de4b61e0ff9c88aa8ac42b7abeae83d1599cb054b0908",
- "zh:61ec8a18a7270e28f43c0f810c82a147af7a5de622eb1f243c3a773e234b2172",
- "zh:6428cd62e615ccaaa3a26a22f68a84f80a0d497a04f0405648119ef69e709ff9",
- "zh:776e57bb2c94716f452eea5803c90b97d304c9f343e6746780a255d8217b95f9",
- "zh:99bacf79ce92568d086ed62b287d9b77fa0c4e86a6c25ad398bec04ea0e44897",
- "zh:b4361d4ad5074df3e83dc0e40452b63f231ae5959b4007763739d3a02caffe29",
- "zh:d96c0f8603a1f8ab61c633a7a707922313f6bdfa8d17fa21d9ccf103fade6af7",
- "zh:edfd955636489371e5ad1f40fc77a023d7eb7d09140342b151f0ea15451ef41b",
- "zh:eeb03674754bd059fafa62b58cf4015c2c07bc25477def3c64eef487d583a63c",
- ]
-}
diff --git a/infra/flagship/README.md b/infra/flagship/README.md
deleted file mode 100644
index 129e3d494..000000000
--- a/infra/flagship/README.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# ๐Ÿค– Flagship deployment โ€” operator notes
-
-> ๐Ÿค– Written by an LLM (Claude), per factor X.
-
-AWS account **271376211898** (adopted from Keymail, spec ยง12), profile
-`keymail`, eu-west-1. State: `s3://carlos-tfstate-271376211898/flagship/`.
-
-## State (2026-08-01)
-
-- โœ… Base stack applied (15 resources); IE box `i-0da792cabf8951fda`,
- EIP **99.81.104.219**, `carlos-edge` ACTIVE (build 2024e50).
-- โœ… Mail stack applied (13 resources): `kass.training` SES identity +
- DKIM CNAMEs in DNSimple, `kass-signin` config set, `carlos-kass-smtp`
- user. DKIM propagation/verification pending โ€” check
- `aws --profile keymail sesv2 get-email-identity --email-identity kass.training`.
-- โœ… Secrets copied and byte-verified: `/carlos/kass/{prod,devbox}/pepper`,
- `/carlos/kass/prod/smtp-{user,pass}`.
-- โœ… Release keys provisioned (`/carlos/release-key`); smoke static site
- serves at `https://smoke.99.81.104.219.sslip.io/` with a real LE cert.
-- โœ… Kass `aedcae3` shipped to the deployment bucket (not yet promoted).
-
-## Operator queue
-
-1. **Promote pending releases** (needs the SSM signing keys):
-
- ```
- CARLOS_BIN=./carlos bash infra/scripts/promote-pending.sh
- ```
-
-2. **Kass rehearsal** (after 1; runs a restored copy on a rehearsal
- hostname, no production impact):
-
- ```
- bash infra/scripts/rehearse-kass.sh
- ```
-
- Needs the restored data at /tmp/claude-1001/kass-data (this session
- restored + integrity-checked it: registry ok, 3 people, 3 instances).
-
-3. **Root email re-home** (whenever): move this account's root email to a
- carlosframework.com / carlosplatform.com address (spec ยง12).
-
-## Kass cutover go/no-go checklist (Plan B task B6)
-
-- [ ] Peppers copied to `/carlos/kass/...` SSM and byte-verified
-- [ ] SES DKIM Success + test send delivered (production access, no sandbox)
-- [ ] Kass restored on flagship box; `/api/library` 200 via `--resolve`
-- [ ] `carlos ops restore-verify` green against `carlos-data-ie-โ€ฆ/kass/`
-- [ ] Rehearsal freeze-to-serving time recorded (target < 10 min)
-- [ ] Rollback rehearsed: DNSimple `home.kass.training` A back to old EIP
- + old `kass.service` restart
-
-Cutover: stop old kass โ†’ final litestream delta โ†’ start new โ†’ smoke via
-`--resolve` โ†’ flip DNSimple `home` A record (TTL 300) โ†’ watch cert issue +
-real passkey sign-in. Old account frozen 30 days, then a teardown plan.
diff --git a/infra/flagship/dns.tf b/infra/flagship/dns.tf
deleted file mode 100644
index 9a4f427b9..000000000
--- a/infra/flagship/dns.tf
+++ /dev/null
@@ -1,31 +0,0 @@
-# Flagship addressing (spec ยง11 naming, resolved 2026-08-01):
-# platform.carlosframework.com -> the flagship edge (control-plane surface)
-# <sqid>.oncarlos.com -> tenant apps, region-neutral addresses
-#
-# carlosframework.com stays on GitHub Pages at the apex (the website repo)
-# until it migrates onto the platform's static class; only the `platform`
-# subdomain points here. oncarlos.com is NOT yet registered โ€” the wildcard
-# block below activates once it exists in DNSimple (operator queue).
-
-# platform.carlosframework.com: the carlosframework.com zone lives in the
-# Paul Campbell DNSimple account (285), not the Team Tito account this
-# stack's provider uses โ€” the A record (-> the flagship EIP) was created
-# there directly on 2026-08-01 and is managed outside this state.
-
-# oncarlos.com registered 2026-08-01 in account 74881 (registration
-# id 557301, auto-renew, whois privacy).
-resource "dnsimple_zone_record" "oncarlos_wildcard" {
- zone_name = "oncarlos.com"
- name = "*"
- type = "A"
- value = module.host_ie.eip_public_ip
- ttl = 300
-}
-
-resource "dnsimple_zone_record" "oncarlos_apex" {
- zone_name = "oncarlos.com"
- name = ""
- type = "A"
- value = module.host_ie.eip_public_ip
- ttl = 300
-}
diff --git a/infra/flagship/host-init.sh b/infra/flagship/host-init.sh
deleted file mode 100644
index c5e9f194c..000000000
--- a/infra/flagship/host-init.sh
+++ /dev/null
@@ -1,33 +0,0 @@
-#!/bin/bash
-# ๐Ÿค– First-boot init for a carlos flagship host (Debian arm64). Deliberately
-# minimal: the carlos binary arrives via bootstrap after the box is up โ€”
-# this script only prepares the ground the binary expects.
-set -euo pipefail
-
-# The service user and the three directories every carlos host has.
-useradd --system --home /opt/carlos --shell /usr/sbin/nologin carlos || true
-mkdir -p /data /opt/carlos/versions /opt/carlos/live /etc/carlos
-chown -R carlos:carlos /data /opt/carlos
-# 711: tenant service users (kass, ...) need traversal into their own
-# /data/<app> subdirs; contents stay private per-owner.
-chmod 0711 /data
-
-# /run/carlos is tmpfs-backed; recreate on every boot.
-cat >/etc/tmpfiles.d/carlos.conf <<'EOF'
-d /run/carlos 0755 carlos carlos -
-EOF
-systemd-tmpfiles --create /etc/tmpfiles.d/carlos.conf
-
-# Keep the OS patched; everything else ships through carlos channels.
-export DEBIAN_FRONTEND=noninteractive
-apt-get update -q
-apt-get install -qy unattended-upgrades curl awscli
-systemctl enable --now unattended-upgrades
-
-# Admin access is SSM Session Manager only โ€” no SSH ingress exists.
-curl -fsSL -o /tmp/amazon-ssm-agent.deb \
- "https://s3.eu-west-1.amazonaws.com/amazon-ssm-eu-west-1/latest/debian_arm64/amazon-ssm-agent.deb"
-dpkg -i /tmp/amazon-ssm-agent.deb
-systemctl enable --now amazon-ssm-agent
-
-echo "carlos host ready โ€” awaiting bootstrap" >/etc/motd
diff --git a/infra/flagship/mail.tf b/infra/flagship/mail.tf
deleted file mode 100644
index 312ffda60..000000000
--- a/infra/flagship/mail.tf
+++ /dev/null
@@ -1,106 +0,0 @@
-# Outbound email for flagship tenants โ€” multi-tenant SES (platform spec
-# ยง7 svc/mail, v1 shape: per-app identities + configuration sets + scoped
-# SMTP users; the fronting service comes later). This account has
-# production SES access โ€” the reason it was adopted as the flagship
-# account (spec ยง12).
-#
-# First tenant: Kass. Its domain identity re-verifies here with NEW DKIM
-# tokens; the old account's DKIM records coexist in DNSimple until the
-# old account winds down.
-
-# Reads DNSIMPLE_TOKEN from the environment; never written to a file here.
-provider "dnsimple" {
- account = "74881"
-}
-
-resource "aws_ses_domain_identity" "kass" {
- domain = "kass.training"
-}
-
-resource "aws_ses_domain_dkim" "kass" {
- domain = aws_ses_domain_identity.kass.domain
-}
-
-resource "dnsimple_zone_record" "kass_dkim" {
- count = 3
- zone_name = "kass.training"
- name = "${aws_ses_domain_dkim.kass.dkim_tokens[count.index]}._domainkey"
- type = "CNAME"
- value = "${aws_ses_domain_dkim.kass.dkim_tokens[count.index]}.dkim.amazonses.com"
- ttl = 3600
-}
-
-resource "aws_sns_topic" "deliverability" {
- name = "carlos-ses-deliverability"
-}
-
-resource "aws_sns_topic_subscription" "deliverability_email" {
- topic_arn = aws_sns_topic.deliverability.arn
- protocol = "email"
- endpoint = "paul@rslw.com"
-}
-
-resource "aws_sns_topic_policy" "ses" {
- arn = aws_sns_topic.deliverability.arn
- policy = jsonencode({
- Version = "2012-10-17"
- Statement = [{
- Effect = "Allow"
- Principal = { Service = "ses.amazonaws.com" }
- Action = "sns:Publish"
- Resource = aws_sns_topic.deliverability.arn
- Condition = { StringEquals = { "AWS:SourceAccount" = local.account } }
- }]
- })
-}
-
-# Per-tenant configuration set: reputation and suppression isolated per
-# app (the svc/mail promise, delivered by SES config rather than a
-# service front for now).
-resource "aws_sesv2_configuration_set" "kass_signin" {
- configuration_set_name = "kass-signin"
- delivery_options { tls_policy = "REQUIRE" }
- reputation_options { reputation_metrics_enabled = true }
- suppression_options { suppressed_reasons = ["BOUNCE", "COMPLAINT"] }
-}
-
-resource "aws_sesv2_configuration_set_event_destination" "kass_signin_sns" {
- configuration_set_name = aws_sesv2_configuration_set.kass_signin.configuration_set_name
- event_destination_name = "deliverability"
- event_destination {
- enabled = true
- matching_event_types = ["BOUNCE", "COMPLAINT", "REJECT", "RENDERING_FAILURE"]
- sns_destination { topic_arn = aws_sns_topic.deliverability.arn }
- }
-}
-
-# Send-only SMTP user for the Kass tenant.
-resource "aws_iam_user" "kass_smtp" {
- name = "carlos-kass-smtp"
-}
-
-resource "aws_iam_user_policy" "kass_smtp_send" {
- name = "send-only"
- user = aws_iam_user.kass_smtp.name
- policy = jsonencode({
- Version = "2012-10-17"
- Statement = [{
- Effect = "Allow"
- Action = ["ses:SendRawEmail", "ses:SendEmail"]
- Resource = "*"
- }]
- })
-}
-
-resource "aws_iam_access_key" "kass_smtp" {
- user = aws_iam_user.kass_smtp.name
-}
-
-output "kass_smtp_user" {
- value = aws_iam_access_key.kass_smtp.id
-}
-
-output "kass_smtp_password" {
- value = aws_iam_access_key.kass_smtp.ses_smtp_password_v4
- sensitive = true
-}
diff --git a/infra/flagship/main.tf b/infra/flagship/main.tf
deleted file mode 100644
index 5081f33ae..000000000
--- a/infra/flagship/main.tf
+++ /dev/null
@@ -1,90 +0,0 @@
-# Flagship CARLOS deployment โ€” AWS account 271376211898 (adopted from
-# Keymail; platform spec ยง12). Tofu owns boxes, buckets, IAM, and static
-# DNS; the platform owns apps, releases, instances, and dynamic DNS.
-# Nothing account-shaped (tenant hostnames, routes) 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
-# flagship-specific instantiation: names, sizes, DNS, mail.
-
-terraform {
- required_version = ">= 1.10"
- backend "s3" {
- bucket = "carlos-tfstate-271376211898"
- key = "flagship/terraform.tfstate"
- region = "eu-west-1"
- profile = "keymail"
- 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 = "keymail"
-}
-
-locals {
- account = "271376211898"
-}
-
-# 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 = "../modules/deployment-buckets"
- deployment_bucket_name = "carlos-flagship-${local.account}"
- data_bucket_name = "carlos-data-ie-${local.account}"
-}
-
-# State surgery from the pre-module layout (refactor 2026-08-01) โ€” pure
-# bookkeeping, no real resource is touched.
-moved {
- from = aws_s3_bucket.deployment
- to = module.buckets.aws_s3_bucket.deployment
-}
-
-moved {
- from = aws_s3_bucket.data_ie
- to = module.buckets.aws_s3_bucket.data
-}
-
-moved {
- from = aws_s3_bucket_versioning.deployment
- to = module.buckets.aws_s3_bucket_versioning.deployment
-}
-
-moved {
- from = aws_s3_bucket_versioning.data_ie
- to = module.buckets.aws_s3_bucket_versioning.data
-}
-
-moved {
- from = aws_s3_bucket_public_access_block.deployment
- to = module.buckets.aws_s3_bucket_public_access_block.deployment
-}
-
-moved {
- from = aws_s3_bucket_public_access_block.data_ie
- to = module.buckets.aws_s3_bucket_public_access_block.data
-}
-
-moved {
- from = aws_s3_bucket_server_side_encryption_configuration.deployment
- to = module.buckets.aws_s3_bucket_server_side_encryption_configuration.deployment
-}
-
-moved {
- from = aws_s3_bucket_server_side_encryption_configuration.data_ie
- to = module.buckets.aws_s3_bucket_server_side_encryption_configuration.data
-}
diff --git a/infra/flagship/outputs.tf b/infra/flagship/outputs.tf
deleted file mode 100644
index c58058ade..000000000
--- a/infra/flagship/outputs.tf
+++ /dev/null
@@ -1,15 +0,0 @@
-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
-}
diff --git a/infra/flagship/plan.out b/infra/flagship/plan.out
deleted file mode 100644
index 8d821cc29..000000000
Binary files a/infra/flagship/plan.out and /dev/null differ
diff --git a/infra/flagship/region-ie.tf b/infra/flagship/region-ie.tf
deleted file mode 100644
index 07f6dbd7b..000000000
--- a/infra/flagship/region-ie.tf
+++ /dev/null
@@ -1,56 +0,0 @@
-# The IE region of the flagship: one Graviton box running `carlos agent`,
-# behind an Elastic IP. The box is disposable by construction โ€” all
-# durable state is in the buckets โ€” so there is no backup story for hosts
-# at all, only for buckets. Residency is enforced in the module: the host
-# role reaches the deployment bucket plus ITS OWN region's data bucket
-# and nothing else.
-
-module "host_ie" {
- source = "../modules/region-host"
- name = "carlos-flagship-ie"
- sg_description = "carlos flagship IE host"
- iam_name = "carlos-flagship-host"
- instance_type = "t4g.small"
- 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/*"
-}
-
-# State surgery from the pre-module layout (refactor 2026-08-01) โ€” pure
-# bookkeeping, no real resource is touched.
-moved {
- from = aws_security_group.host_ie
- to = module.host_ie.aws_security_group.host
-}
-
-moved {
- from = aws_iam_role.host
- to = module.host_ie.aws_iam_role.host
-}
-
-moved {
- from = aws_iam_role_policy.host_buckets
- to = module.host_ie.aws_iam_role_policy.host_buckets
-}
-
-moved {
- from = aws_iam_role_policy_attachment.host_ssm
- to = module.host_ie.aws_iam_role_policy_attachment.host_ssm
-}
-
-moved {
- from = aws_iam_instance_profile.host
- to = module.host_ie.aws_iam_instance_profile.host
-}
-
-moved {
- from = aws_instance.host_ie
- to = module.host_ie.aws_instance.host
-}
-
-moved {
- from = aws_eip.host_ie
- to = module.host_ie.aws_eip.host
-}
diff --git a/infra/flagship/scripts/copy-kass-secrets.sh b/infra/flagship/scripts/copy-kass-secrets.sh
deleted file mode 100755
index 5e2a6724d..000000000
--- a/infra/flagship/scripts/copy-kass-secrets.sh
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/bin/bash
-# ๐Ÿค– B4 step 1: copy Kass's irreplaceable SSM parameters into the flagship
-# account, byte-verifying each. The peppers are the single point of data
-# loss for the whole Kass migration โ€” restoring a registry without the
-# original pepper orphans every account, by design, with no recovery path.
-# Values never touch disk or the terminal; only OK/MISMATCH is printed.
-set -euo pipefail
-
-for p in /kass/prod/pepper /kass/devbox/pepper /kass/prod/smtp-user /kass/prod/smtp-pass; do
- v=$(aws --profile kass ssm get-parameter --name "$p" --with-decryption \
- --query Parameter.Value --output text)
- aws --profile keymail ssm put-parameter --name "/carlos$p" \
- --type SecureString --value "$v" --overwrite >/dev/null
- n=$(aws --profile keymail ssm get-parameter --name "/carlos$p" \
- --with-decryption --query Parameter.Value --output text)
- if [ "$v" = "$n" ]; then
- echo "/carlos$p OK (byte-verified)"
- else
- echo "/carlos$p MISMATCH โ€” STOP, do not proceed with migration" >&2
- exit 1
- fi
-done
-echo "all secrets copied and verified"
diff --git a/infra/scripts/bootstrap-box.sh b/infra/scripts/bootstrap-box.sh
deleted file mode 100755
index e092cd7a0..000000000
--- a/infra/scripts/bootstrap-box.sh
+++ /dev/null
@@ -1,47 +0,0 @@
-#!/bin/bash
-# ๐Ÿค– Bootstrap a carlos host over SSM: install the binary from the
-# deployment bucket, run `carlos bootstrap`, write host.env, start the
-# edge. Idempotent โ€” safe to re-run.
-# Usage: bootstrap-box.sh <aws-profile> <instance-id> <deployment-bucket> <data-bucket> <pubkey-file>
-set -euo pipefail
-profile=$1; instance=$2; depbucket=$3; databucket=$4; pubfile=$5
-pub=$(cat "$pubfile")
-d=$(mktemp -d)
-
-cat > "$d/bootstrap.sh" <<EOF
-set -e
-aws s3 cp s3://$depbucket/bootstrap/carlos /opt/carlos/carlos.new --no-progress
-install -m755 -o carlos -g carlos /opt/carlos/carlos.new /opt/carlos/carlos && rm /opt/carlos/carlos.new
-/opt/carlos/carlos bootstrap
-cat > /etc/carlos/host.env <<ENV
-CARLOS_DATA_DIR=/data
-CARLOS_DEPLOYMENT_BUCKET=$depbucket
-CARLOS_RELEASE_PUBKEY=$pub
-CARLOS_HIBERNATE_BUCKET=$databucket
-CARLOS_HIBERNATE_REGION=eu-west-1
-CARLOS_HIBERNATE_PREFIX=instances
-AWS_REGION=eu-west-1
-ENV
-chmod 600 /etc/carlos/host.env
-chown carlos:carlos /etc/carlos/host.env
-systemctl start carlos-edge
-sleep 3
-systemctl is-active carlos-edge
-/opt/carlos/carlos version
-EOF
-
-jq -n --rawfile s "$d/bootstrap.sh" '{commands: ($s | split("\n"))}' > "$d/params.json"
-cmdid=$(aws --profile "$profile" ssm send-command --instance-ids "$instance" \
- --document-name AWS-RunShellScript --parameters "file://$d/params.json" \
- --query Command.CommandId --output text)
-echo "command $cmdid sent; polling..."
-for i in $(seq 1 30); do
- sleep 5
- status=$(aws --profile "$profile" ssm get-command-invocation --command-id "$cmdid" \
- --instance-id "$instance" --query Status --output text 2>/dev/null || echo Pending)
- [ "$status" = "Success" ] || [ "$status" = "Failed" ] && break
-done
-echo "status: $status"
-aws --profile "$profile" ssm get-command-invocation --command-id "$cmdid" \
- --instance-id "$instance" --query StandardOutputContent --output text | tail -8
-[ "$status" = "Success" ]
diff --git a/infra/scripts/promote-kass-rehearsal.sh b/infra/scripts/promote-kass-rehearsal.sh
deleted file mode 100755
index 30936023c..000000000
--- a/infra/scripts/promote-kass-rehearsal.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/bash
-# ๐Ÿค– Promote kass aedcae3 to canary/rehearsal (zero bake hold) so the B5
-# rehearsal can adopt immediately; the stable promotion stays baking until
-# 2026-08-05 for the real cutover, exactly as designed.
-set -euo pipefail
-carlos=${CARLOS_BIN:-./carlos}
-AWS_PROFILE=keymail AWS_REGION=eu-west-1 \
-CARLOS_DEPLOYMENT_BUCKET=carlos-flagship-271376211898 \
-CARLOS_RELEASE_KEY=$(aws --profile keymail ssm get-parameter --name /carlos/release-key --with-decryption --query Parameter.Value --output text) \
- "$carlos" promote -app kass aedcae3-r2 canary/rehearsal
diff --git a/infra/scripts/promote-pending.sh b/infra/scripts/promote-pending.sh
deleted file mode 100755
index f47d22242..000000000
--- a/infra/scripts/promote-pending.sh
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/bin/bash
-# ๐Ÿค– Promote the two shipped-but-unpromoted releases (signing keys come
-# from each account's SSM; never printed). Run when ready:
-# kass aedcae3 -> stable (flagship)
-# titogo ff61d3b -> canary/platform-adoption (tito)
-set -euo pipefail
-carlos=${CARLOS_BIN:-./carlos}
-
-AWS_PROFILE=keymail AWS_REGION=eu-west-1 \
-CARLOS_DEPLOYMENT_BUCKET=carlos-flagship-271376211898 \
-CARLOS_RELEASE_KEY=$(aws --profile keymail ssm get-parameter --name /carlos/release-key --with-decryption --query Parameter.Value --output text) \
- "$carlos" promote -app kass aedcae3 stable
-
-AWS_PROFILE=titogo AWS_REGION=eu-west-1 \
-CARLOS_DEPLOYMENT_BUCKET=carlos-tito-platform-665106592142 \
-CARLOS_RELEASE_KEY=$(aws --profile titogo ssm get-parameter --name /carlos/release-key --with-decryption --query Parameter.Value --output text) \
- "$carlos" promote -app titogo ff61d3b canary/platform-adoption
diff --git a/infra/scripts/promote-platform-site.sh b/infra/scripts/promote-platform-site.sh
deleted file mode 100755
index b7ff38366..000000000
--- a/infra/scripts/promote-platform-site.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/bash
-# ๐Ÿค– Promote the platform marketing site draft to canary/site (zero bake
-# hold โ€” it's a draft for Paul's review, served at the real hostname
-# because review happens on something deployed, never localhost).
-set -euo pipefail
-carlos=${CARLOS_BIN:-./carlos}
-AWS_PROFILE=keymail AWS_REGION=eu-west-1 \
-CARLOS_DEPLOYMENT_BUCKET=carlos-flagship-271376211898 \
-CARLOS_RELEASE_KEY=$(aws --profile keymail ssm get-parameter --name /carlos/release-key --with-decryption --query Parameter.Value --output text) \
- "$carlos" promote -app platform-site site1 canary/site
diff --git a/infra/scripts/promote-titogo-canary.sh b/infra/scripts/promote-titogo-canary.sh
deleted file mode 100755
index a7534ba40..000000000
--- a/infra/scripts/promote-titogo-canary.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/bash
-# ๐Ÿค– Promote titogo ff61d3b-r2 (target-keyed re-ship) to its adoption
-# canary channel on the Tito deployment. Key from SSM, never printed.
-set -euo pipefail
-carlos=${CARLOS_BIN:-./carlos}
-AWS_PROFILE=titogo AWS_REGION=eu-west-1 \
-CARLOS_DEPLOYMENT_BUCKET=carlos-tito-platform-665106592142 \
-CARLOS_RELEASE_KEY=$(aws --profile titogo ssm get-parameter --name /carlos/release-key --with-decryption --query Parameter.Value --output text) \
- "$carlos" promote -app titogo ff61d3b-r2 canary/platform-adoption
diff --git a/infra/scripts/provision-keys.sh b/infra/scripts/provision-keys.sh
deleted file mode 100755
index 7aa29e8e2..000000000
--- a/infra/scripts/provision-keys.sh
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/bin/bash
-# ๐Ÿค– Generate each deployment's Ed25519 release keypair: seed to SSM
-# (SecureString, never printed), pubkey to a local file for host.env.
-# Run once per deployment; -overwrite is intentional only on first setup.
-# Usage: provision-keys.sh <aws-profile> <pubkey-out-file>
-set -euo pipefail
-profile=$1; out=$2
-carlos=${CARLOS_BIN:-./carlos}
-kout=$("$carlos" release-keygen 2>/dev/null)
-seed=$(echo "$kout" | grep '^CARLOS_RELEASE_KEY=' | cut -d= -f2-)
-pub=$(echo "$kout" | grep '^CARLOS_RELEASE_PUBKEY=' | cut -d= -f2-)
-aws --profile "$profile" ssm put-parameter --name /carlos/release-key \
- --type SecureString --value "$seed" --overwrite >/dev/null
-echo "$pub" > "$out"
-echo "seed stored in SSM /carlos/release-key ($profile); pubkey in $out"
diff --git a/infra/scripts/rehearse-kass.sh b/infra/scripts/rehearse-kass.sh
deleted file mode 100755
index b4801b2ff..000000000
--- a/infra/scripts/rehearse-kass.sh
+++ /dev/null
@@ -1,73 +0,0 @@
-#!/bin/bash
-# ๐Ÿค– B5: Kass migration rehearsal on the flagship box โ€” no production
-# impact (old prod keeps serving; this runs a restored COPY on a
-# rehearsal hostname). Prereqs: promote-pending.sh ran (kass on stable);
-# restored data at /tmp/claude-1001/kass-data (or re-run the restore).
-# Usage: rehearse-kass.sh
-set -euo pipefail
-EIP=99.81.104.219
-INSTANCE=i-0da792cabf8951fda
-HOST="kass.${EIP}.sslip.io"
-D=${KASS_DATA_DIR:-/tmp/claude-1001/kass-data}
-
-# 1. Stage the restored data where the box's role can read it.
-AWS_PROFILE=keymail aws s3 sync "$D/" \
- s3://carlos-data-ie-271376211898/kass-restore/ --no-progress
-
-# 2. On the box: data, env (pepper from SSM), unit, adopt, route, start.
-d=$(mktemp -d)
-cat > "$d/rehearse.sh" <<EOF
-set -e
-useradd --system --home /data/kass --shell /usr/sbin/nologin kass || true
-mkdir -p /data/kass
-aws s3 sync s3://carlos-data-ie-271376211898/kass-restore/ /data/kass/ --no-progress
-pepper=\$(aws ssm get-parameter --name /carlos/kass/prod/pepper --with-decryption --query Parameter.Value --output text --region eu-west-1)
-cat > /data/kass.env <<ENV
-KASS_ADDR=127.0.0.1:8080
-KASS_DATA=/data/kass
-KASS_ORIGIN=https://$HOST
-KASS_RPID=$HOST
-KASS_PEPPER=\$pepper
-ENV
-# No SMTP in the rehearsal: sign-in links log to the journal instead of
-# sending โ€” validates the flow without emailing real people from a copy.
-chmod 600 /data/kass.env; chown -R kass:kass /data/kass /data/kass.env
-cat > /etc/systemd/system/kass.service <<'UNIT'
-[Unit]
-Description=Kass (CARLOS flagship tenant)
-After=network-online.target
-[Service]
-User=kass
-EnvironmentFile=/data/kass.env
-ExecStart=/opt/carlos/live/$HOST serve
-Restart=always
-NoNewPrivileges=yes
-ProtectSystem=strict
-ReadWritePaths=/data
-PrivateTmp=yes
-[Install]
-WantedBy=multi-user.target
-UNIT
-# Order matters: the route must exist BEFORE adopt, or the pass has
-# nothing to stage (learned live 2026-08-01). /data needs traversal for
-# the kass user. The live link is per-host by design.
-chmod 711 /data
-export \$(grep -v '^#' /etc/carlos/host.env | xargs)
-/opt/carlos/carlos add -app kass -kind instance -addr 127.0.0.1:8080 -channel stable $HOST
-/opt/carlos/carlos adopt
-systemctl daemon-reload && systemctl restart kass
-sleep 4
-systemctl is-active kass
-curl -s -o /dev/null -w 'api/library: %{http_code}\n' --max-time 20 https://$HOST/api/library
-EOF
-jq -n --rawfile s "$d/rehearse.sh" '{commands: ($s | split("\n"))}' > "$d/params.json"
-cmdid=$(aws --profile keymail ssm send-command --instance-ids "$INSTANCE" \
- --document-name AWS-RunShellScript --parameters "file://$d/params.json" \
- --query Command.CommandId --output text)
-for i in $(seq 1 36); do sleep 5
- s=$(aws --profile keymail ssm get-command-invocation --command-id "$cmdid" --instance-id "$INSTANCE" --query Status --output text 2>/dev/null || echo Pending)
- [ "$s" = Success ] || [ "$s" = Failed ] && break; done
-echo "status: $s"
-aws --profile keymail ssm get-command-invocation --command-id "$cmdid" --instance-id "$INSTANCE" \
- --query '[StandardOutputContent,StandardErrorContent]' --output text | tail -10
-[ "$s" = Success ]
diff --git a/infra/scripts/ship-app.sh b/infra/scripts/ship-app.sh
deleted file mode 100755
index 55781e2c2..000000000
--- a/infra/scripts/ship-app.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-# ๐Ÿค– Ship and promote a release to a deployment. The signing key is read
-# from SSM inside this script and exported only to the promote call โ€”
-# it never appears in a terminal or transcript.
-# Usage: ship-app.sh <aws-profile> <deployment-bucket> <app> <version> <channel> <kind> <file-or-dir>
-set -euo pipefail
-profile=$1; bucket=$2; app=$3; ver=$4; channel=$5; kind=$6; artifact=$7
-carlos=${CARLOS_BIN:-./carlos}
-export AWS_PROFILE=$profile AWS_REGION=eu-west-1 CARLOS_DEPLOYMENT_BUCKET=$bucket
-"$carlos" ship -app "$app" -kind "$kind" -version "$ver" "$artifact"
-CARLOS_RELEASE_KEY=$(aws ssm get-parameter --name /carlos/release-key \
- --with-decryption --query Parameter.Value --output text) \
- "$carlos" promote -app "$app" "$ver" "$channel"
diff --git a/infra/scripts/smoke-flagship.sh b/infra/scripts/smoke-flagship.sh
deleted file mode 100755
index a50c3c9ce..000000000
--- a/infra/scripts/smoke-flagship.sh
+++ /dev/null
@@ -1,35 +0,0 @@
-#!/bin/bash
-# ๐Ÿค– B2 smoke: on the flagship box, add a static route for the shipped
-# smokesite and verify the edge serves it end to end (bucket -> channel
-# pointer -> verified tar -> edge). Uses sslip.io so the hostname resolves
-# to the box for real ACME issuance.
-# Usage: smoke-flagship.sh <aws-profile> <instance-id> <eip>
-set -euo pipefail
-profile=$1; instance=$2; eip=$3
-host="smoke.${eip}.sslip.io"
-d=$(mktemp -d)
-
-cat > "$d/smoke.sh" <<EOF
-set -e
-export \$(grep -v '^#' /etc/carlos/host.env | xargs)
-/opt/carlos/carlos add -app smokesite -kind static -channel canary/smoke $host
-sleep 4
-curl -s --max-time 20 https://$host/ | head -2
-curl -s -o /dev/null -w 'docs: %{http_code}\n' https://$host/docs/
-curl -s -o /dev/null -w 'missing: %{http_code}\n' https://$host/nope
-EOF
-
-jq -n --rawfile s "$d/smoke.sh" '{commands: ($s | split("\n"))}' > "$d/params.json"
-cmdid=$(aws --profile "$profile" ssm send-command --instance-ids "$instance" \
- --document-name AWS-RunShellScript --parameters "file://$d/params.json" \
- --query Command.CommandId --output text)
-for i in $(seq 1 24); do
- sleep 5
- status=$(aws --profile "$profile" ssm get-command-invocation --command-id "$cmdid" \
- --instance-id "$instance" --query Status --output text 2>/dev/null || echo Pending)
- [ "$status" = "Success" ] || [ "$status" = "Failed" ] && break
-done
-echo "status: $status"
-aws --profile "$profile" ssm get-command-invocation --command-id "$cmdid" \
- --instance-id "$instance" --query '[StandardOutputContent,StandardErrorContent]' --output text | tail -12
-[ "$status" = "Success" ]
diff --git a/infra/tito/.gitignore b/infra/tito/.gitignore
deleted file mode 100644
index 951fb8e3a..000000000
--- a/infra/tito/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-plan.out
-*.tfstate*
-.terraform/
diff --git a/infra/tito/.terraform.lock.hcl b/infra/tito/.terraform.lock.hcl
deleted file mode 100644
index ab67445ee..000000000
--- a/infra/tito/.terraform.lock.hcl
+++ /dev/null
@@ -1,39 +0,0 @@
-# This file is maintained automatically by "tofu init".
-# Manual edits may be lost in future updates.
-
-provider "registry.opentofu.org/hashicorp/aws" {
- version = "6.57.1"
- constraints = "~> 6.0"
- hashes = [
- "h1:8wbrCS3R5lunN2HMVfQ+oMB3uFVAO7gfUbItdZa+tdA=",
- "h1:C4XEcAWUKXExKCF6NhvMxkSyZ/JqNWjtwtwwEqQtl70=",
- "h1:Fah+p3BD+/E7wdgazQFp4fsjuvTh5/Q7fQFyMsv9juM=",
- "h1:S6eLURiAh6L00nr+9s2zwuhHQOU04rEG60Z9xO1ORRM=",
- "h1:UdllVJ4482tLgz5q8se3SPzpPoXuFXwvxiykpgdipWs=",
- "h1:ZAkIIdmQryKnldenUI2fqThlAciNz2UYKVTZ5F/57ME=",
- "h1:a/i6TVZX4jSNSvHIWWTqLIlaugg7SYfQMZNVtzDWlKM=",
- "h1:gGtYen1GfQvynKT8lHguY+cvmxNww+TOqCaapWoNW1w=",
- "h1:iFr4+F/b94MEadHCiaNm6o6TapyvLT6TrwmbPyDQs9E=",
- "h1:k0OzZI0aBgeEhCUcoK00/6C3hBK59ETo7g5Jq62wRqQ=",
- "h1:nlvu+qO8eMFdnMptYSUK1c5dOXoK6r3HG87jzE7EYvQ=",
- "h1:rVP8Z9tc37nwsW+NLSvEQyUBoDG/zsMtPSMeHhBrR3k=",
- "h1:rxnmHWTUE2Aou/otNocOI4Rvcm69LQsoNYssj0s7hIo=",
- "h1:va0Q0eDSt7xyvkSX7GbXmAO/QUqugZb/05gMdcQlR0I=",
- "h1:yXF1EzUTMepZv4SeQnBNUvjOj2HKEKSl+jVndIKksGU=",
- "zh:053910cce06bba506fb7a3dcecdcd719280d7df8b8a7cce06ae3e2dd20b1cc0d",
- "zh:14feabeefef742b528c978a78f6896240a7ca3464a4714a5b1e1b2d448db4fd5",
- "zh:22dd549e3c479b2ff2121e00a005aa3b6b43a166e727ef3208a529a59feb1608",
- "zh:3ca5eab63ac77da335d42ebe0af8569c5c617dc5b239750b111f5ba9f2f889cd",
- "zh:3d686f9fca9752850958a353971cd7eb1f5a48e55c2c56fcd96d18cc6e54f8ae",
- "zh:512e46d35abfec0ba85fc0f2da7dc39fece9106f02165e95096b6549f920453a",
- "zh:5fe83903b4bef5b7318de4b61e0ff9c88aa8ac42b7abeae83d1599cb054b0908",
- "zh:61ec8a18a7270e28f43c0f810c82a147af7a5de622eb1f243c3a773e234b2172",
- "zh:6428cd62e615ccaaa3a26a22f68a84f80a0d497a04f0405648119ef69e709ff9",
- "zh:776e57bb2c94716f452eea5803c90b97d304c9f343e6746780a255d8217b95f9",
- "zh:99bacf79ce92568d086ed62b287d9b77fa0c4e86a6c25ad398bec04ea0e44897",
- "zh:b4361d4ad5074df3e83dc0e40452b63f231ae5959b4007763739d3a02caffe29",
- "zh:d96c0f8603a1f8ab61c633a7a707922313f6bdfa8d17fa21d9ccf103fade6af7",
- "zh:edfd955636489371e5ad1f40fc77a023d7eb7d09140342b151f0ea15451ef41b",
- "zh:eeb03674754bd059fafa62b58cf4015c2c07bc25477def3c64eef487d583a63c",
- ]
-}
diff --git a/infra/tito/README.md b/infra/tito/README.md
deleted file mode 100644
index 71a582455..000000000
--- a/infra/tito/README.md
+++ /dev/null
@@ -1,43 +0,0 @@
-# ๐Ÿค– Tito deployment โ€” operator notes
-
-> ๐Ÿค– Written by an LLM (Claude), per factor X.
-
-AWS account **665106592142** (Tito's own; self-hosted deployment), profile
-`titogo`, eu-west-1 (IE) first. State: `s3://carlos-tfstate-665106592142/tito/`.
-
-## State (2026-08-01)
-
-- โœ… Stack applied (21 resources): IE box `i-025a0b70f3d93304d`, EIP
- **54.228.234.85**, `carlos-edge` ACTIVE (build 2024e50);
- `ie.go.tito.io` โ†’ the EIP; svc/pdf Gotenberg box up
- (`i-0bb7069abfa36b2aa`, private 172.31.11.159). Hetzner DNS untouched.
-- โœ… Release key provisioned (`/carlos/release-key` in 665106592142).
-- โœ… titogo `ff61d3b` shipped to the deployment bucket (not yet promoted).
-
-## Operator queue
-
-1. **Promote pending releases**: `bash infra/scripts/promote-pending.sh`
- (also covers the flagship's Kass promote).
-2. **titogo canary** (after 1): stage a canary instance at
- `platform.ie.go.tito.io` per Plan C task C2 โ€” route via
- `carlos add -app titogo -kind instance -addr 127.0.0.1:8443
- -channel canary/platform-adoption platform.ie.go.tito.io` on the box,
- plus a Route53 A record for `platform.ie.go.tito.io` (or reuse the
- `ie.go.tito.io` alias with a CNAME).
-3. **Set svc/pdf basic-auth** via SSM session: write a
- `user bcrypt-hash` line to `/etc/caddy/pdf-basicauth` on the svc-pdf
- box and `systemctl restart caddy`.
-
-## Hetzner cutover go/no-go checklist (Plan C task C4)
-
-- [ ] titogo canary green at `https://platform.ie.go.tito.io`
- (full canary checklist: magic-link sign-in, seeded event, checkout)
-- [ ] Fleet restore rehearsed from `titogo-litestream-โ€ฆ/hetzner/`;
- duration recorded (= freeze window)
-- [ ] `carlos ops restore-verify` green against `carlos-tito-ie-โ€ฆ/titogo/`
-- [ ] Rollback rehearsed: Route53 `go.tito.io` + `*.go.tito.io` back to
- 62.238.45.61 + restart Hetzner units
-
-Cutover: stop `titogo-router` + `titogo@*` on Hetzner โ†’ final litestream
-delta restore on IE โ†’ start under carlos โ†’ smoke via `--resolve` โ†’ flip
-Route53 A records (TTL 60) โ†’ watch. Hetzner box frozen 30 days.
diff --git a/infra/tito/dns.tf b/infra/tito/dns.tf
deleted file mode 100644
index 998aeeced..000000000
--- a/infra/tito/dns.tf
+++ /dev/null
@@ -1,18 +0,0 @@
-# Static DNS skeleton in the existing tito.io Route53 zone. ONLY the
-# regional alias is created here โ€” go.tito.io and *.go.tito.io keep
-# pointing at the Hetzner box until cutover (Tito infra spec ยง12), and
-# the cutover flip is deliberately NOT a tofu change (it is the
-# platform/operator's dynamic-DNS territory; nothing account-shaped in
-# tofu state).
-
-data "aws_route53_zone" "tito" {
- name = "tito.io."
-}
-
-resource "aws_route53_record" "ie_alias" {
- zone_id = data.aws_route53_zone.tito.zone_id
- name = "ie.go.tito.io"
- type = "A"
- ttl = 300
- records = [module.host_ie.eip_public_ip]
-}
diff --git a/infra/tito/host-init.sh b/infra/tito/host-init.sh
deleted file mode 100644
index ea0ea3e66..000000000
--- a/infra/tito/host-init.sh
+++ /dev/null
@@ -1,30 +0,0 @@
-#!/bin/bash
-# ๐Ÿค– First-boot init for the carlos tito IE host (Debian arm64).
-# Deliberately minimal: the carlos binary arrives via bootstrap after the
-# box is up โ€” this script only prepares the ground the binary expects.
-set -euo pipefail
-
-useradd --system --home /opt/carlos --shell /usr/sbin/nologin carlos || true
-mkdir -p /data /opt/carlos/versions /opt/carlos/live /etc/carlos
-chown -R carlos:carlos /data /opt/carlos
-# 711: tenant service users need traversal into their own /data/<app>
-# subdirs; contents stay private per-owner.
-chmod 0711 /data
-
-cat >/etc/tmpfiles.d/carlos.conf <<'EOF'
-d /run/carlos 0755 carlos carlos -
-EOF
-systemd-tmpfiles --create /etc/tmpfiles.d/carlos.conf
-
-export DEBIAN_FRONTEND=noninteractive
-apt-get update -q
-apt-get install -qy unattended-upgrades curl awscli
-systemctl enable --now unattended-upgrades
-
-# Admin access is SSM Session Manager only โ€” no SSH ingress exists.
-curl -fsSL -o /tmp/amazon-ssm-agent.deb \
- "https://s3.eu-west-1.amazonaws.com/amazon-ssm-eu-west-1/latest/debian_arm64/amazon-ssm-agent.deb"
-dpkg -i /tmp/amazon-ssm-agent.deb
-systemctl enable --now amazon-ssm-agent
-
-echo "carlos host ready โ€” awaiting bootstrap" >/etc/motd
diff --git a/infra/tito/main.tf b/infra/tito/main.tf
deleted file mode 100644
index ab1d62a3a..000000000
--- a/infra/tito/main.tf
+++ /dev/null
@@ -1,86 +0,0 @@
-# Tito's self-hosted CARLOS deployment โ€” AWS account 665106592142 (Tito's
-# own; platform spec ยง12, Tito infra spec). 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.
-#
-# The reusable shapes live in ../modules; from Tito's own repo the same
-# modules are consumable by git source URL (see infra/modules/README.md).
-# This root is the thin Tito-specific instantiation: names, sizes, DNS,
-# svc/pdf.
-
-terraform {
- required_version = ">= 1.10"
- backend "s3" {
- bucket = "carlos-tfstate-665106592142"
- key = "tito/terraform.tfstate"
- region = "eu-west-1"
- profile = "titogo"
- use_lockfile = true
- }
- required_providers {
- aws = {
- source = "hashicorp/aws"
- version = "~> 6.0"
- }
- }
-}
-
-provider "aws" {
- region = "eu-west-1"
- profile = "titogo"
-}
-
-locals {
- account = "665106592142"
-}
-
-# The deployment bucket + the IE regional data bucket (the residency
-# promise, Tito infra spec ยง4). Versioning, SSE, public-access block,
-# prevent_destroy โ€” all inside the module.
-module "buckets" {
- source = "../modules/deployment-buckets"
- deployment_bucket_name = "carlos-tito-platform-${local.account}"
- data_bucket_name = "carlos-tito-ie-${local.account}"
-}
-
-# State surgery from the pre-module layout (refactor 2026-08-01) โ€” pure
-# bookkeeping, no real resource is touched.
-moved {
- from = aws_s3_bucket.deployment
- to = module.buckets.aws_s3_bucket.deployment
-}
-
-moved {
- from = aws_s3_bucket.data_ie
- to = module.buckets.aws_s3_bucket.data
-}
-
-moved {
- from = aws_s3_bucket_versioning.deployment
- to = module.buckets.aws_s3_bucket_versioning.deployment
-}
-
-moved {
- from = aws_s3_bucket_versioning.data_ie
- to = module.buckets.aws_s3_bucket_versioning.data
-}
-
-moved {
- from = aws_s3_bucket_public_access_block.deployment
- to = module.buckets.aws_s3_bucket_public_access_block.deployment
-}
-
-moved {
- from = aws_s3_bucket_public_access_block.data_ie
- to = module.buckets.aws_s3_bucket_public_access_block.data
-}
-
-moved {
- from = aws_s3_bucket_server_side_encryption_configuration.deployment
- to = module.buckets.aws_s3_bucket_server_side_encryption_configuration.deployment
-}
-
-moved {
- from = aws_s3_bucket_server_side_encryption_configuration.data_ie
- to = module.buckets.aws_s3_bucket_server_side_encryption_configuration.data
-}
diff --git a/infra/tito/outputs.tf b/infra/tito/outputs.tf
deleted file mode 100644
index f73fc30fd..000000000
--- a/infra/tito/outputs.tf
+++ /dev/null
@@ -1,15 +0,0 @@
-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 "svc_pdf_private_ip" {
- value = aws_instance.svc_pdf.private_ip
-}
diff --git a/infra/tito/region-ie.tf b/infra/tito/region-ie.tf
deleted file mode 100644
index 33b48aaab..000000000
--- a/infra/tito/region-ie.tf
+++ /dev/null
@@ -1,60 +0,0 @@
-# The IE region of Tito's deployment: edge/control-plane home (Tito infra
-# spec ยง2.2). One Graviton box running `carlos agent`. t4g.medium โ€” the
-# Hetzner fleet's whole workload fits with headroom (its redline was 3.8GB
-# with ~20 instances at ~190MB).
-#
-# Residency is enforced in the module: this host's role reaches the
-# deployment bucket plus the IE data bucket and nothing else. A future
-# Sydney host's module instance will not include the IE bucket โ€” the
-# wrong-region wake the spec requires to fail loudly fails at the IAM
-# layer.
-
-module "host_ie" {
- source = "../modules/region-host"
- name = "carlos-tito-ie"
- sg_description = "carlos tito IE host"
- iam_name = "carlos-tito-host-ie"
- instance_type = "t4g.medium"
- root_volume_size = 40
- 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/*"
-}
-
-# State surgery from the pre-module layout (refactor 2026-08-01) โ€” pure
-# bookkeeping, no real resource is touched.
-moved {
- from = aws_security_group.host_ie
- to = module.host_ie.aws_security_group.host
-}
-
-moved {
- from = aws_iam_role.host_ie
- to = module.host_ie.aws_iam_role.host
-}
-
-moved {
- from = aws_iam_role_policy.host_ie_buckets
- to = module.host_ie.aws_iam_role_policy.host_buckets
-}
-
-moved {
- from = aws_iam_role_policy_attachment.host_ie_ssm
- to = module.host_ie.aws_iam_role_policy_attachment.host_ssm
-}
-
-moved {
- from = aws_iam_instance_profile.host_ie
- to = module.host_ie.aws_iam_instance_profile.host
-}
-
-moved {
- from = aws_instance.host_ie
- to = module.host_ie.aws_instance.host
-}
-
-moved {
- from = aws_eip.host_ie
- to = module.host_ie.aws_eip.host
-}
diff --git a/infra/tito/svc-pdf-init.sh b/infra/tito/svc-pdf-init.sh
deleted file mode 100644
index 498376478..000000000
--- a/infra/tito/svc-pdf-init.sh
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/bin/bash
-# ๐Ÿค– First-boot init for svc/pdf: Gotenberg (pinned image) in Docker,
-# loopback-only, fronted by Caddy. Stateless by construction โ€” this box
-# has no bucket access, no database, and nothing worth backing up.
-set -euo pipefail
-
-export DEBIAN_FRONTEND=noninteractive
-apt-get update -q
-apt-get install -qy docker.io curl unattended-upgrades
-systemctl enable --now unattended-upgrades docker
-
-# Admin access is SSM Session Manager only.
-curl -fsSL -o /tmp/amazon-ssm-agent.deb \
- "https://s3.eu-west-1.amazonaws.com/amazon-ssm-eu-west-1/latest/debian_arm64/amazon-ssm-agent.deb"
-dpkg -i /tmp/amazon-ssm-agent.deb
-systemctl enable --now amazon-ssm-agent
-
-# Gotenberg, pinned, loopback only โ€” Caddy is the only way in.
-docker run -d --name gotenberg --restart=always -p 127.0.0.1:3000:3000 \
- gotenberg/gotenberg:8.21.1
-
-# Caddy terminates TLS on the instance's internal DNS name with a
-# self-signed cert (fleet-internal traffic; the caller pins basic-auth,
-# not the cert). Basic-auth credentials arrive via SSM at first use โ€”
-# the operator sets /etc/caddy/pdf.env through a session.
-apt-get install -qy caddy
-cat >/etc/caddy/Caddyfile <<'EOF'
-{
- auto_https disable_redirects
-}
-:443 {
- tls internal
- basic_auth {
- import /etc/caddy/pdf-basicauth
- }
- reverse_proxy 127.0.0.1:3000
-}
-EOF
-# Placeholder until the operator writes the real bcrypt line via SSM:
-# format: "<user> <bcrypt-hash>"
-touch /etc/caddy/pdf-basicauth
-systemctl restart caddy
-
-echo "svc/pdf ready โ€” set /etc/caddy/pdf-basicauth via SSM" >/etc/motd
diff --git a/infra/tito/svc-pdf.tf b/infra/tito/svc-pdf.tf
deleted file mode 100644
index 1eb92c6b7..000000000
--- a/infra/tito/svc-pdf.tf
+++ /dev/null
@@ -1,60 +0,0 @@
-# svc/pdf: one Gotenberg behind Caddy basic-auth โ€” the platform spec's
-# PDF/render service (seeing, therefore stateless BY CONSTRUCTION: no
-# bucket grant, no database, no volume beyond the OS). Tito's invoice
-# work can consume this before any platform code exists.
-
-resource "aws_security_group" "svc_pdf" {
- name = "carlos-tito-svc-pdf"
- description = "Gotenberg, reachable from the fleet only"
- ingress {
- from_port = 443
- to_port = 443
- protocol = "tcp"
- security_groups = [module.host_ie.security_group_id]
- }
- egress {
- from_port = 0
- to_port = 0
- protocol = "-1"
- cidr_blocks = ["0.0.0.0/0"]
- }
-}
-
-# SSM only; NO bucket or SSM-parameter grants โ€” statelessness is imposed,
-# not promised.
-resource "aws_iam_role" "svc_pdf" {
- name = "carlos-tito-svc-pdf"
- assume_role_policy = jsonencode({
- Version = "2012-10-17"
- Statement = [{
- Effect = "Allow"
- Principal = { Service = "ec2.amazonaws.com" }
- Action = "sts:AssumeRole"
- }]
- })
-}
-
-resource "aws_iam_role_policy_attachment" "svc_pdf_ssm" {
- role = aws_iam_role.svc_pdf.name
- policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
-}
-
-resource "aws_iam_instance_profile" "svc_pdf" {
- name = "carlos-tito-svc-pdf"
- role = aws_iam_role.svc_pdf.name
-}
-
-resource "aws_instance" "svc_pdf" {
- ami = module.host_ie.ami_id
- instance_type = "t4g.small"
- vpc_security_group_ids = [aws_security_group.svc_pdf.id]
- iam_instance_profile = aws_iam_instance_profile.svc_pdf.name
- user_data = file("${path.module}/svc-pdf-init.sh")
- user_data_replace_on_change = false
- root_block_device {
- volume_size = 20
- volume_type = "gp3"
- encrypted = true
- }
- tags = { Name = "carlos-tito-svc-pdf" }
-}