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>
31 files changed,
+12
−1114
infra/README.md+12 −0infra/flagship/.gitignore+0 −3infra/flagship/.terraform.lock.hcl+0 −75infra/flagship/README.md+0 −55infra/flagship/dns.tf+0 −31infra/flagship/host-init.sh+0 −33infra/flagship/mail.tf+0 −106infra/flagship/main.tf+0 −90infra/flagship/outputs.tf+0 −15infra/flagship/plan.outbinaryinfra/flagship/region-ie.tf+0 −56infra/flagship/scripts/copy-kass-secrets.sh+0 −23infra/scripts/bootstrap-box.sh+0 −47infra/scripts/promote-kass-rehearsal.sh+0 −10infra/scripts/promote-pending.sh+0 −17infra/scripts/promote-platform-site.sh+0 −10infra/scripts/promote-titogo-canary.sh+0 −9infra/scripts/provision-keys.sh+0 −15infra/scripts/rehearse-kass.sh+0 −73infra/scripts/ship-app.sh+0 −13infra/scripts/smoke-flagship.sh+0 −35infra/tito/.gitignore+0 −3infra/tito/.terraform.lock.hcl+0 −39infra/tito/README.md+0 −43infra/tito/dns.tf+0 −18infra/tito/host-init.sh+0 −30infra/tito/main.tf+0 −86infra/tito/outputs.tf+0 −15infra/tito/region-ie.tf+0 −60infra/tito/svc-pdf-init.sh+0 −44infra/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" } |
| -} |