carlos / platform Public

fix: rehearsal lessons — route-before-adopt, per-host live link, /data 711, ship keys binaries by target

Kass rehearsal VERIFIED on the flagship: api/library 200 at
kass.99.81.104.219.sslip.io serving restored production data via
ship -> promote canary/rehearsal -> adopt. The stable promotion
bakes until 2026-08-05 (supply-chain hold working as designed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell f4523b660e7fc42b9cb3a33c4fc8c83fc326399e parent 3018079
5 files changed, +33 −4
  • cli.go +11 −0
  • infra/flagship/host-init.sh +3 −1
  • infra/scripts/promote-kass-rehearsal.sh +10 −0
  • infra/scripts/rehearse-kass.sh +6 −2
  • infra/tito/host-init.sh +3 −1
diff --git a/cli.go b/cli.go
index 441b40f2a..089135fcf 100644
--- a/cli.go
+++ b/cli.go
@@ -163,6 +163,7 @@ func runShip(args []string) {
app := fs.String("app", "", "app name")
kind := fs.String("kind", "binary", "binary | static")
ver := fs.String("version", "", "version (git short sha)")
+ target := fs.String("target", "linux-arm64", "GOOS-GOARCH the binary was built for (binary kind, single file)")
fs.Parse(args)
if *app == "" || *ver == "" || fs.NArg() == 0 {
log.Fatal("usage: carlos ship -app <a> -version <sha> [-kind binary|static] <files-or-site-dir>")
@@ -185,7 +186,17 @@ func runShip(args []string) {
log.Fatal(err)
}
addFile("site.tar.gz", data)
+ } else if fs.NArg() == 1 {
+ // The adopt pass looks artifacts up by GOOS-GOARCH, so a binary's
+ // artifact key is its target, not its build filename (learned live
+ // 2026-08-01: a basename-keyed artifact is invisible to every host).
+ data, err := os.ReadFile(fs.Arg(0))
+ if err != nil {
+ log.Fatal(err)
+ }
+ addFile(*target, data)
} else {
+ // Multiple files: pre-named per target (linux-arm64, linux-amd64, ...).
for _, path := range fs.Args() {
data, err := os.ReadFile(path)
if err != nil {
diff --git a/infra/flagship/host-init.sh b/infra/flagship/host-init.sh
index 5d97c61f9..c5e9f194c 100644
--- a/infra/flagship/host-init.sh
+++ b/infra/flagship/host-init.sh
@@ -8,7 +8,9 @@ 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
-chmod 0700 /data
+# 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'
diff --git a/infra/scripts/promote-kass-rehearsal.sh b/infra/scripts/promote-kass-rehearsal.sh
new file mode 100755
index 000000000..30936023c
--- /dev/null
+++ b/infra/scripts/promote-kass-rehearsal.sh
@@ -0,0 +1,10 @@
+#!/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/rehearse-kass.sh b/infra/scripts/rehearse-kass.sh
index 966375de6..b4801b2ff 100755
--- a/infra/scripts/rehearse-kass.sh
+++ b/infra/scripts/rehearse-kass.sh
@@ -39,7 +39,7 @@ After=network-online.target
[Service]
User=kass
EnvironmentFile=/data/kass.env
-ExecStart=/opt/carlos/live/kass serve
+ExecStart=/opt/carlos/live/$HOST serve
Restart=always
NoNewPrivileges=yes
ProtectSystem=strict
@@ -48,9 +48,13 @@ 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 adopt
/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
diff --git a/infra/tito/host-init.sh b/infra/tito/host-init.sh
index 6406cb6e7..ea0ea3e66 100644
--- a/infra/tito/host-init.sh
+++ b/infra/tito/host-init.sh
@@ -7,7 +7,9 @@ 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
-chmod 0700 /data
+# 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 -