baa-conductor

git clone 

commit
6110f23
parent
be74d58
author
im_wower
date
2026-03-22 03:32:36 +0800 CST
Merge feat/T-028-real-rollout-v2 into main
7 files changed,  +270, -9
M coordination/tasks/T-028-real-rollout.md
+58, -9
  1@@ -1,10 +1,10 @@
  2 ---
  3 task_id: T-028
  4 title: 真实 Cloudflare / VPS 上线执行
  5-status: todo
  6-branch: feat/T-028-real-rollout
  7+status: review
  8+branch: feat/T-028-real-rollout-v2
  9 repo: /Users/george/code/baa-conductor
 10-base_ref: main
 11+base_ref: main@be74d58
 12 depends_on:
 13   - T-018
 14   - T-019
 15@@ -22,7 +22,7 @@ write_scope:
 16   - apps/control-api-worker/**
 17   - scripts/ops/**
 18   - scripts/runtime/**
 19-updated_at: 2026-03-22
 20+updated_at: 2026-03-22T03:29:42+0800
 21 ---
 22 
 23 # T-028 真实 Cloudflare / VPS 上线执行
 24@@ -79,23 +79,72 @@ updated_at: 2026-03-22
 25 
 26 ## files_changed
 27 
 28-- 待填写
 29+- `coordination/tasks/T-028-real-rollout.md`
 30+- `scripts/runtime/check-node.sh`
 31+- `docs/runtime/launchd.md`
 32+- `docs/runtime/node-verification.md`
 33+- `docs/runtime/real-rollout-2026-03-22.md`
 34+- `docs/ops/README.md`
 35+- `docs/ops/real-rollout-2026-03-22.md`
 36 
 37 ## commands_run
 38 
 39-- 待填写
 40+- 本地干净 worktree:`git -C /Users/george/code/baa-conductor-main-merge worktree add -b feat/T-028-real-rollout-v2 /Users/george/code/baa-conductor-T028-v2 be74d585251f20ff5bd74ae17f6f6b011ff0bd34`
 41+- 本地新 worktree 首命令:`cd /Users/george/code/baa-conductor-T028-v2 && npx --yes pnpm install`
 42+- 本地构建:`cd /Users/george/code/baa-conductor-T028-v2 && npx --yes pnpm -r build`
 43+- `mini` 本机:`./scripts/runtime/bootstrap.sh --repo-dir /Users/george/code/baa-conductor-T028-v2`
 44+- `mini` 本机:`set -a; source /Users/george/.config/baa-conductor/control-api-worker.secrets.env; set +a; ./scripts/runtime/install-launchd.sh --repo-dir /Users/george/code/baa-conductor-T028-v2 --node mini --service conductor --service status-api --install-dir /Users/george/Library/LaunchAgents --local-api-base http://100.71.210.78:4317 --local-api-allowed-hosts 100.71.210.78 --status-api-host 100.71.210.78`
 45+- `mini` 本机:`./scripts/runtime/check-launchd.sh --repo-dir /Users/george/code/baa-conductor-T028-v2 --node mini --service conductor --service status-api --install-dir /Users/george/Library/LaunchAgents --local-api-base http://100.71.210.78:4317 --local-api-allowed-hosts 100.71.210.78 --status-api-host 100.71.210.78`
 46+- `mini` 本机:`./scripts/runtime/reload-launchd.sh --service conductor --service status-api --install-dir /Users/george/Library/LaunchAgents`
 47+- `mini` 本机:`./scripts/runtime/check-node.sh --repo-dir /Users/george/code/baa-conductor-T028-v2 --node mini --service conductor --service status-api --install-dir /Users/george/Library/LaunchAgents --local-api-base http://100.71.210.78:4317 --local-api-allowed-hosts 100.71.210.78 --status-api-base http://100.71.210.78:4318 --status-api-host 100.71.210.78 --expected-rolez leader --check-loaded`
 48+- 本地到双节点探活:`curl http://100.71.210.78:4317/healthz`、`curl http://100.71.210.78:4318/healthz`、`curl http://100.112.239.13:4317/healthz`、`curl http://100.112.239.13:4318/healthz`
 49+- 同步到 `mac`:`rsync -az --delete --exclude '.git' --exclude 'node_modules' /Users/george/code/baa-conductor-T028-v2/ george@100.112.239.13:/Users/george/code/baa-conductor-T028-v2/`
 50+- `mac` 远端:`cd /Users/george/code/baa-conductor-T028-v2 && npx --yes pnpm install`
 51+- `mac` 远端:`cd /Users/george/code/baa-conductor-T028-v2 && npx --yes pnpm -r build`
 52+- `mac` 远端:`./scripts/runtime/bootstrap.sh --repo-dir /Users/george/code/baa-conductor-T028-v2`
 53+- `mac` 远端:`set -a; source /Users/george/.config/baa-conductor/control-api-worker.secrets.env; set +a; ./scripts/runtime/install-launchd.sh --repo-dir /Users/george/code/baa-conductor-T028-v2 --node mac --service conductor --service status-api --install-dir /Users/george/Library/LaunchAgents --local-api-base http://100.112.239.13:4317 --local-api-allowed-hosts 100.112.239.13 --status-api-host 100.112.239.13`
 54+- `mac` 远端:`./scripts/runtime/check-launchd.sh --repo-dir /Users/george/code/baa-conductor-T028-v2 --node mac --service conductor --service status-api --install-dir /Users/george/Library/LaunchAgents --local-api-base http://100.112.239.13:4317 --local-api-allowed-hosts 100.112.239.13 --status-api-host 100.112.239.13`
 55+- `mac` 远端:`./scripts/runtime/reload-launchd.sh --service conductor --service status-api --install-dir /Users/george/Library/LaunchAgents`
 56+- `mac` 远端:`./scripts/runtime/check-node.sh --repo-dir /Users/george/code/baa-conductor-T028-v2 --node mac --service conductor --service status-api --install-dir /Users/george/Library/LaunchAgents --local-api-base http://100.112.239.13:4317 --local-api-allowed-hosts 100.112.239.13 --status-api-base http://100.112.239.13:4318 --status-api-host 100.112.239.13 --expected-rolez standby --check-loaded`
 57+- 生成 DNS / Nginx 计划:`scripts/ops/cloudflare-dns-plan.sh --env /Users/george/.config/baa-conductor/ops.env --fetch-current --emit-shell .tmp/ops/cloudflare-dns-preview.sh --output .tmp/ops/cloudflare-dns-plan.json`、`scripts/ops/nginx-sync-plan.sh --env /Users/george/.config/baa-conductor/ops.env --bundle-dir .tmp/ops/baa-conductor-nginx`
 58+- VPS 基线:`ssh -p 2222 root@192.210.137.113 'hostname; tailscale ip -4; nginx -v; ss -ltnp | grep -E ":(80|443|2222)\\b"'`
 59+- VPS upstream 探活:`ssh -p 2222 root@192.210.137.113 'curl http://100.71.210.78:4317/healthz && curl http://100.112.239.13:4317/healthz'`
 60+- VPS status-api 探活:`ssh -p 2222 root@192.210.137.113 'curl http://100.71.210.78:4318/healthz && curl http://100.112.239.13:4318/healthz'`
 61+- VPS 依赖:`ssh -p 2222 root@192.210.137.113 'apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y apache2-utils python3-certbot-dns-cloudflare'`
 62+- VPS Cloudflare DNS challenge 凭据:`printf 'dns_cloudflare_api_token = %s\n' "$CLOUDFLARE_API_TOKEN" | ssh -p 2222 root@192.210.137.113 'install -d -m 700 /root/.secrets/certbot && cat > /root/.secrets/certbot/cloudflare.ini && chmod 600 /root/.secrets/certbot/cloudflare.ini'`
 63+- VPS 三张证书:三次 `ssh -p 2222 root@192.210.137.113 'certbot certonly --non-interactive --agree-tos --register-unsafely-without-email --preferred-challenges dns-01 --authenticator dns-cloudflare --dns-cloudflare-credentials /root/.secrets/certbot/cloudflare.ini --dns-cloudflare-propagation-seconds 30 --cert-name <host> -d <host>'`
 64+- 本地直连域名密码文件:`/Users/george/.config/baa-conductor/direct-node-basic-auth.env`
 65+- VPS htpasswd:`printf '%s\n' "$BAA_DIRECT_NODE_BASIC_AUTH_PASSWORD" | ssh -p 2222 root@192.210.137.113 "htpasswd -i -c /etc/nginx/.htpasswd-baa-conductor conductor-ops && chmod 640 /etc/nginx/.htpasswd-baa-conductor && chown root:www-data /etc/nginx/.htpasswd-baa-conductor"`
 66+- Nginx bundle 同步:`rsync -az -e 'ssh -p 2222' /Users/george/code/baa-conductor-T028-v2/.tmp/ops/baa-conductor-nginx/ root@192.210.137.113:/tmp/baa-conductor-nginx/`
 67+- VPS 安装 / 校验 / reload:`ssh -p 2222 root@192.210.137.113 'cd /tmp/baa-conductor-nginx && ./deploy-on-vps.sh'`、`ssh -p 2222 root@192.210.137.113 'cd /tmp/baa-conductor-nginx && ./deploy-on-vps.sh --reload'`
 68+- Cloudflare DNS 初次切换:`bash /Users/george/code/baa-conductor-T028-v2/.tmp/ops/cloudflare-dns-preview.sh`
 69+- Cloudflare DNS 改为 DNS-only:`scripts/ops/cloudflare-dns-plan.sh --env /Users/george/.config/baa-conductor/ops.env --fetch-current --emit-shell .tmp/ops/cloudflare-dns-proxy-off.sh --output .tmp/ops/cloudflare-dns-plan-proxy-off.json`、`bash /Users/george/code/baa-conductor-T028-v2/.tmp/ops/cloudflare-dns-proxy-off.sh`
 70+- 验收:`curl -H "Authorization: Bearer $CONTROL_API_OPS_ADMIN_TOKEN" https://control-api.makefile.so/v1/system/state`、`curl --resolve conductor.makefile.so:443:192.210.137.113 https://conductor.makefile.so/healthz`、`curl --resolve conductor.makefile.so:443:192.210.137.113 https://conductor.makefile.so/rolez`、`curl -u conductor-ops:... --resolve mini-conductor.makefile.so:443:192.210.137.113 https://mini-conductor.makefile.so/rolez`、`curl -u conductor-ops:... --resolve mac-conductor.makefile.so:443:192.210.137.113 https://mac-conductor.makefile.so/rolez`
 71+- 收尾校验:`bash -n scripts/ops/*.sh scripts/runtime/*.sh`、`git diff --check`
 72+- 提交与推送:`git commit -m "Complete T-028 real rollout"`、`git push -u origin feat/T-028-real-rollout-v2`
 73 
 74 ## result
 75 
 76-- 待填写
 77+- `mini` 与 `mac` 已从旧的 loopback-only 配置切到各自的 Tailscale `100.x` 监听:`mini 100.71.210.78:4317/4318`、`mac 100.112.239.13:4317/4318`,两台节点都通过了真实 `check-node.sh --check-loaded`。
 78+- VPS `racknerd-ff37952` 已真实打通到两台节点的 `4317` 与 `4318`;`curl http://100.71.210.78:4317/healthz`、`curl http://100.112.239.13:4317/healthz` 以及对应 `4318` 探针都返回 `ok`。
 79+- VPS 已安装 `apache2-utils` 与 `python3-certbot-dns-cloudflare`,并通过 Cloudflare DNS challenge 签发三张证书:`conductor.makefile.so`、`mini-conductor.makefile.so`、`mac-conductor.makefile.so`,到期日均为 `2026-06-19`。
 80+- `/etc/nginx/.htpasswd-baa-conductor` 已创建,仓库生成的 Nginx bundle 已同步到 VPS,`./deploy-on-vps.sh` 与 `./deploy-on-vps.sh --reload` 都真实通过,`nginx -t` 成功。
 81+- Cloudflare DNS 已真实切换。三条 conductor 记录最终状态为 `A -> 192.210.137.113 proxied=false`;Cloudflare API 回读计划为 `noop`,权威 NS `giancarlo.ns.cloudflare.com` 对三个 host 都返回 `192.210.137.113`。
 82+- 公网验收闭环完成:`https://control-api.makefile.so/v1/system/state` 返回 `ok=true` 且 `holder_id=mini-main`;`conductor.makefile.so` 入口返回 `healthz=ok`、`rolez=leader`;`mini-conductor.makefile.so` / `mac-conductor.makefile.so` 未认证为 `401`,带 Basic Auth 后分别返回 `leader` / `standby`。
 83+- 为了让 Tailscale rollout 的静态校验真实可用,本分支顺手修复了 `scripts/runtime/check-node.sh`,让它把 `--local-api-allowed-hosts` 和 `--status-api-host` 正确传给 `check-launchd.sh`。
 84+- 分支已推送:`origin/feat/T-028-real-rollout-v2`
 85 
 86 ## risks
 87 
 88-- 待填写
 89+- 这三条 conductor DNS 记录当前是 `proxied=false`。原因不是源站问题,而是当前 Cloudflare token 只有 DNS 权限,无法把 zone SSL mode 从疑似 `Flexible` 改到 `Full` / `Full (strict)`;若重新开代理,公网会回到 `301 Location: https://$host$request_uri` 循环。
 90+- `mini` 与 `mac` 运行时现在都指向 `/Users/george/code/baa-conductor-T028-v2`,不是文档默认的 `/Users/george/code/baa-conductor`;如果后续切回 canonical 路径,需要重新渲染并 reload launchd 副本。
 91+- `mini/mac` 直连域名的 Basic Auth 密码只保存在仓库外的 `/Users/george/.config/baa-conductor/direct-node-basic-auth.env` 和 VPS htpasswd 文件里,没有进入 repo;交接时需要确保整合者知道这份私有文件的位置。
 92 
 93 ## next_handoff
 94 
 95-- 待填写
 96+- 如需把 `conductor.makefile.so`、`mini-conductor.makefile.so`、`mac-conductor.makefile.so` 重新放回 Cloudflare 代理,先准备带 zone settings 权限的 token,把 `makefile.so` 的 SSL mode 调到 `Full` 或 `Full (strict)`,再把私有 inventory `/Users/george/.config/baa-conductor/ops.env` 里的 `BAA_CF_PROXY_*` 改回 `true` 并重新执行 DNS 计划脚本。
 97+- 观察三张证书的首次自动续期是否正常;若要显式演练,可在 VPS 上执行 `certbot renew --dry-run`。
 98+- 后续若进入 `T-029`,就直接基于当前 `mini leader / mac standby / VPS ingress` 现网做 smoke、切换与长时间稳定性回归,不需要再重新铺节点监听或公网入口。
 99 
100 开始时建议直接把 `status` 改为 `in_progress`。
101 
M docs/ops/README.md
+5, -0
 1@@ -133,6 +133,11 @@ scripts/ops/cloudflare-dns-plan.sh \
 2 - 脚本本身不会发 `POST` / `PATCH` / `DELETE`
 3 - `--emit-shell` 只是把预览命令写到文件里,不会自动执行
 4 
 5+实战注意:
 6+
 7+- 如果 `conductor*.makefile.so` 已经有有效源站证书、`--resolve` 直打 VPS 的 `https://.../healthz` 正常,但经 Cloudflare 代理访问时返回 `301 Location: https://$host$request_uri` 自重定向,通常说明 zone 仍在 `Flexible`。
 8+- 当前 token 如果没有 zone settings 权限,无法直接把 SSL mode 改到 `Full` / `Full (strict)`;这种情况下先把 `BAA_CF_PROXY_*` 设成 `false`,切成 DNS-only,避免公网入口卡在 Cloudflare 边缘循环。
 9+
10 ## Nginx 渲染与部署 bundle
11 
12 ### 1. 渲染并打包
A docs/ops/real-rollout-2026-03-22.md
+101, -0
  1@@ -0,0 +1,101 @@
  2+# 2026-03-22 Real Rollout Record
  3+
  4+## Snapshot
  5+
  6+| Surface | Target | Actual state |
  7+| --- | --- | --- |
  8+| `control-api.makefile.so` | Cloudflare Worker + D1 | Already deployed and verified |
  9+| `conductor.makefile.so` | VPS `192.210.137.113` -> `mini/mac 100.x:4317` | Live |
 10+| `mini-conductor.makefile.so` | VPS `192.210.137.113` -> `mini 100.71.210.78:4317` | Live with Basic Auth |
 11+| `mac-conductor.makefile.so` | VPS `192.210.137.113` -> `mac 100.112.239.13:4317` | Live with Basic Auth |
 12+
 13+## Cloudflare
 14+
 15+- zone: `makefile.so`
 16+- zone id: `f3507ab962df815d93e7ad3f1a390615`
 17+- public IPv4: `192.210.137.113`
 18+- Worker custom domain kept as-is: `https://control-api.makefile.so`
 19+
 20+DNS records created on 2026-03-22 CST:
 21+
 22+- `conductor.makefile.so` record id `535f46affdb393e30a24e5f9fb5b95c5`
 23+- `mini-conductor.makefile.so` record id `41a6d2fab5071ea6de09eacabd5ffca6`
 24+- `mac-conductor.makefile.so` record id `e7011abac5c5684e22a047f5e2ced5d7`
 25+
 26+Final record state:
 27+
 28+- `A conductor.makefile.so -> 192.210.137.113 proxied=false`
 29+- `A mini-conductor.makefile.so -> 192.210.137.113 proxied=false`
 30+- `A mac-conductor.makefile.so -> 192.210.137.113 proxied=false`
 31+
 32+Why DNS-only:
 33+
 34+- With `proxied=true`, `--resolve ...:443:192.210.137.113` direct-to-origin checks were healthy, but public requests through Cloudflare returned `301 Location: https://$host$request_uri`.
 35+- The token available to this rollout could edit DNS but could not read or change zone SSL settings; `GET /zones/<zone>/settings/ssl` returned `9109 Unauthorized`.
 36+- The observed behavior is consistent with the zone still being on `Flexible`, so the rollout switched these three conductor records to DNS-only instead of leaving public traffic on a redirect loop.
 37+
 38+Authoritative DNS confirmation after the final PATCH:
 39+
 40+- `@giancarlo.ns.cloudflare.com conductor.makefile.so -> 192.210.137.113`
 41+- `@giancarlo.ns.cloudflare.com mini-conductor.makefile.so -> 192.210.137.113`
 42+- `@giancarlo.ns.cloudflare.com mac-conductor.makefile.so -> 192.210.137.113`
 43+
 44+## VPS / Nginx
 45+
 46+VPS facts:
 47+
 48+- host: `racknerd-ff37952`
 49+- SSH: `root@192.210.137.113 -p 2222`
 50+- Tailscale IPv4: `100.68.201.85`
 51+- Nginx: `1.24.0`
 52+
 53+Packages installed during this rollout:
 54+
 55+- `apache2-utils`
 56+- `python3-certbot-dns-cloudflare`
 57+
 58+TLS and auth:
 59+
 60+- Cloudflare DNS challenge credentials written to `/root/.secrets/certbot/cloudflare.ini`
 61+- `certbot certonly --authenticator dns-cloudflare ... --cert-name conductor.makefile.so -d conductor.makefile.so`
 62+- `certbot certonly --authenticator dns-cloudflare ... --cert-name mini-conductor.makefile.so -d mini-conductor.makefile.so`
 63+- `certbot certonly --authenticator dns-cloudflare ... --cert-name mac-conductor.makefile.so -d mac-conductor.makefile.so`
 64+- all three certificates expire on `2026-06-19`
 65+- Basic Auth file created at `/etc/nginx/.htpasswd-baa-conductor`
 66+- local secret copy stored outside the repo at `/Users/george/.config/baa-conductor/direct-node-basic-auth.env`
 67+
 68+Nginx deployment:
 69+
 70+- bundle rendered from `/Users/george/code/baa-conductor-T028-v2/.tmp/ops/baa-conductor-nginx`
 71+- synced to `/tmp/baa-conductor-nginx` on the VPS
 72+- `./deploy-on-vps.sh` passed `nginx -t`
 73+- `./deploy-on-vps.sh --reload` passed and reloaded Nginx
 74+
 75+## Public verification
 76+
 77+Control plane:
 78+
 79+- `curl -H "Authorization: Bearer $CONTROL_API_OPS_ADMIN_TOKEN" https://control-api.makefile.so/v1/system/state`
 80+- result: `ok=true`, `holder_id=mini-main`, `mode=running`
 81+
 82+Pre-cutover ingress verification against the fresh VPS config:
 83+
 84+- `curl --resolve conductor.makefile.so:443:192.210.137.113 https://conductor.makefile.so/healthz -> ok`
 85+- `curl --resolve conductor.makefile.so:443:192.210.137.113 https://conductor.makefile.so/rolez -> leader`
 86+- `curl --resolve mini-conductor.makefile.so:443:192.210.137.113 https://mini-conductor.makefile.so/healthz -> 401` without auth
 87+- `curl -u conductor-ops:... --resolve mini-conductor.makefile.so:443:192.210.137.113 https://mini-conductor.makefile.so/healthz -> ok`
 88+- `curl -u conductor-ops:... --resolve mini-conductor.makefile.so:443:192.210.137.113 https://mini-conductor.makefile.so/rolez -> leader`
 89+- `curl --resolve mac-conductor.makefile.so:443:192.210.137.113 https://mac-conductor.makefile.so/healthz -> 401` without auth
 90+- `curl -u conductor-ops:... --resolve mac-conductor.makefile.so:443:192.210.137.113 https://mac-conductor.makefile.so/healthz -> ok`
 91+- `curl -u conductor-ops:... --resolve mac-conductor.makefile.so:443:192.210.137.113 https://mac-conductor.makefile.so/rolez -> standby`
 92+
 93+VPS upstream probes:
 94+
 95+- `curl http://100.71.210.78:4317/healthz -> ok`
 96+- `curl http://100.112.239.13:4317/healthz -> ok`
 97+- `curl http://100.71.210.78:4318/healthz -> ok`
 98+- `curl http://100.112.239.13:4318/healthz -> ok`
 99+
100+## Follow-up
101+
102+- If these three conductor domains need to go back behind Cloudflare proxy, use a token that can change zone SSL settings and switch the zone from `Flexible` to `Full` or `Full (strict)` before turning `proxied=true` back on.
M docs/runtime/launchd.md
+4, -0
 1@@ -163,7 +163,9 @@ AGENTS_DIR="$HOME/Library/LaunchAgents"
 2   --all-services \
 3   --install-dir "$AGENTS_DIR" \
 4   --local-api-base http://100.71.210.78:4317 \
 5+  --local-api-allowed-hosts 100.71.210.78 \
 6   --status-api-base http://100.71.210.78:4318 \
 7+  --status-api-host 100.71.210.78 \
 8   --expected-rolez leader
 9 ```
10 
11@@ -238,7 +240,9 @@ AGENTS_DIR="$HOME/Library/LaunchAgents"
12   --all-services \
13   --install-dir "$AGENTS_DIR" \
14   --local-api-base http://100.112.239.13:4317 \
15+  --local-api-allowed-hosts 100.112.239.13 \
16   --status-api-base http://100.112.239.13:4318 \
17+  --status-api-host 100.112.239.13 \
18   --expected-rolez standby
19 ```
20 
M docs/runtime/node-verification.md
+12, -0
 1@@ -70,7 +70,9 @@ AGENTS_DIR="$HOME/Library/LaunchAgents"
 2   --all-services \
 3   --install-dir "$AGENTS_DIR" \
 4   --local-api-base http://100.71.210.78:4317 \
 5+  --local-api-allowed-hosts 100.71.210.78 \
 6   --status-api-base http://100.71.210.78:4318 \
 7+  --status-api-host 100.71.210.78 \
 8   --expected-rolez leader
 9 ```
10 
11@@ -82,6 +84,10 @@ AGENTS_DIR="$HOME/Library/LaunchAgents"
12   --node mini \
13   --all-services \
14   --install-dir "$AGENTS_DIR" \
15+  --local-api-base http://100.71.210.78:4317 \
16+  --local-api-allowed-hosts 100.71.210.78 \
17+  --status-api-base http://100.71.210.78:4318 \
18+  --status-api-host 100.71.210.78 \
19   --expected-rolez leader \
20   --check-loaded
21 ```
22@@ -134,7 +140,9 @@ AGENTS_DIR="$HOME/Library/LaunchAgents"
23   --all-services \
24   --install-dir "$AGENTS_DIR" \
25   --local-api-base http://100.112.239.13:4317 \
26+  --local-api-allowed-hosts 100.112.239.13 \
27   --status-api-base http://100.112.239.13:4318 \
28+  --status-api-host 100.112.239.13 \
29   --expected-rolez standby
30 ```
31 
32@@ -151,6 +159,10 @@ sudo ./scripts/runtime/check-node.sh \
33   --scope daemon \
34   --install-dir /Library/LaunchDaemons \
35   --username george \
36+  --local-api-base http://100.112.239.13:4317 \
37+  --local-api-allowed-hosts 100.112.239.13 \
38+  --status-api-base http://100.112.239.13:4318 \
39+  --status-api-host 100.112.239.13 \
40   --expected-rolez standby \
41   --check-loaded
42 ```
A docs/runtime/real-rollout-2026-03-22.md
+75, -0
 1@@ -0,0 +1,75 @@
 2+# 2026-03-22 Runtime Rollout Record
 3+
 4+## Nodes
 5+
 6+| Node | Host | Repo path used for rollout | Result |
 7+| --- | --- | --- | --- |
 8+| `mini` | local machine `Mac`, Tailscale `100.71.210.78` | `/Users/george/code/baa-conductor-T028-v2` | launchd reloaded, on-node checks passed, `rolez=leader` |
 9+| `mac` | remote `MacBookPro`, Tailscale `100.112.239.13` | `/Users/george/code/baa-conductor-T028-v2` | repo synced, launchd reloaded, on-node checks passed, `rolez=standby` |
10+
11+## Shared runtime inputs
12+
13+- repo path used for this rollout: `/Users/george/code/baa-conductor-T028-v2`
14+- launchd install dir: `/Users/george/Library/LaunchAgents`
15+- shared token source: `/Users/george/.config/baa-conductor/control-api-worker.secrets.env`
16+- control API base: `https://control-api.makefile.so`
17+
18+## mini
19+
20+Executed locally on 2026-03-22 CST:
21+
22+1. `npx --yes pnpm -r build`
23+2. `./scripts/runtime/bootstrap.sh --repo-dir /Users/george/code/baa-conductor-T028-v2`
24+3. `./scripts/runtime/install-launchd.sh --repo-dir /Users/george/code/baa-conductor-T028-v2 --node mini --service conductor --service status-api --install-dir /Users/george/Library/LaunchAgents --local-api-base http://100.71.210.78:4317 --local-api-allowed-hosts 100.71.210.78 --status-api-host 100.71.210.78`
25+4. `./scripts/runtime/check-launchd.sh ...`
26+5. `./scripts/runtime/reload-launchd.sh --service conductor --service status-api --install-dir /Users/george/Library/LaunchAgents`
27+6. `./scripts/runtime/check-node.sh --repo-dir /Users/george/code/baa-conductor-T028-v2 --node mini --service conductor --service status-api --install-dir /Users/george/Library/LaunchAgents --local-api-base http://100.71.210.78:4317 --local-api-allowed-hosts 100.71.210.78 --status-api-base http://100.71.210.78:4318 --status-api-host 100.71.210.78 --expected-rolez leader --check-loaded`
28+
29+Observed state after reload:
30+
31+- conductor PID: `79697`
32+- status-api PID: `79700`
33+- `http://100.71.210.78:4317/healthz -> ok`
34+- `http://100.71.210.78:4317/rolez -> leader`
35+- `http://100.71.210.78:4318/healthz -> ok`
36+
37+## mac
38+
39+Executed remotely over `ssh george@100.112.239.13` on 2026-03-22 CST:
40+
41+1. `rsync -az --delete --exclude '.git' --exclude 'node_modules' /Users/george/code/baa-conductor-T028-v2/ george@100.112.239.13:/Users/george/code/baa-conductor-T028-v2/`
42+2. `cd /Users/george/code/baa-conductor-T028-v2 && npx --yes pnpm install`
43+3. `cd /Users/george/code/baa-conductor-T028-v2 && npx --yes pnpm -r build`
44+4. `./scripts/runtime/bootstrap.sh --repo-dir /Users/george/code/baa-conductor-T028-v2`
45+5. `./scripts/runtime/install-launchd.sh --repo-dir /Users/george/code/baa-conductor-T028-v2 --node mac --service conductor --service status-api --install-dir /Users/george/Library/LaunchAgents --local-api-base http://100.112.239.13:4317 --local-api-allowed-hosts 100.112.239.13 --status-api-host 100.112.239.13`
46+6. `./scripts/runtime/check-launchd.sh ...`
47+7. `./scripts/runtime/reload-launchd.sh --service conductor --service status-api --install-dir /Users/george/Library/LaunchAgents`
48+8. `./scripts/runtime/check-node.sh --repo-dir /Users/george/code/baa-conductor-T028-v2 --node mac --service conductor --service status-api --install-dir /Users/george/Library/LaunchAgents --local-api-base http://100.112.239.13:4317 --local-api-allowed-hosts 100.112.239.13 --status-api-base http://100.112.239.13:4318 --status-api-host 100.112.239.13 --expected-rolez standby --check-loaded`
49+
50+Observed state after reload:
51+
52+- conductor PID: `60338`
53+- status-api PID: `60341`
54+- `http://100.112.239.13:4317/healthz -> ok`
55+- `http://100.112.239.13:4317/rolez -> standby`
56+- `http://100.112.239.13:4318/healthz -> ok`
57+
58+## Cross-node reachability
59+
60+Validated after both nodes switched to Tailscale listeners:
61+
62+- local host:
63+  - `curl http://100.71.210.78:4317/healthz -> ok`
64+  - `curl http://100.71.210.78:4318/healthz -> ok`
65+  - `curl http://100.112.239.13:4317/healthz -> ok`
66+  - `curl http://100.112.239.13:4318/healthz -> ok`
67+- VPS `root@192.210.137.113 -p 2222`:
68+  - `curl http://100.71.210.78:4317/healthz -> ok`
69+  - `curl http://100.112.239.13:4317/healthz -> ok`
70+  - `curl http://100.71.210.78:4318/healthz -> ok`
71+  - `curl http://100.112.239.13:4318/healthz -> ok`
72+
73+## Notes
74+
75+- This rollout replaced the old `/Users/george/code/baa-conductor-T028` runtime path with `/Users/george/code/baa-conductor-T028-v2` on both nodes.
76+- `check-node.sh` now forwards `--local-api-allowed-hosts` and `--status-api-host` to `check-launchd.sh`; without that fix, Tailscale rollout checks would fail even though the installed plist values were correct.
M scripts/runtime/check-node.sh
+15, -0
 1@@ -22,7 +22,10 @@ Options:
 2   --shared-token-file PATH     Read the expected token from a file.
 3   --control-api-base URL       Expected BAA_CONTROL_API_BASE in installed copies.
 4   --local-api-base URL         Conductor local API base URL. Defaults to 127.0.0.1:4317.
 5+  --local-api-allowed-hosts CSV
 6+                               Expected BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS in installed copies.
 7   --status-api-base URL        Status API base URL. Defaults to 127.0.0.1:4318.
 8+  --status-api-host HOST       Expected BAA_STATUS_API_HOST in installed copies.
 9   --username NAME              Expected UserName for LaunchDaemons.
10   --domain TARGET              launchctl domain target for --check-loaded.
11   --check-loaded               Also require launchctl print to succeed for each service.
12@@ -55,7 +58,9 @@ shared_token=""
13 shared_token_file=""
14 control_api_base="${BAA_RUNTIME_DEFAULT_CONTROL_API_BASE}"
15 local_api_base="${BAA_RUNTIME_DEFAULT_LOCAL_API}"
16+local_api_allowed_hosts="${BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS:-}"
17 status_api_base="${BAA_RUNTIME_DEFAULT_STATUS_API}"
18+status_api_host="${BAA_STATUS_API_HOST:-127.0.0.1}"
19 username="$(default_username)"
20 domain_target=""
21 check_loaded="0"
22@@ -120,10 +125,18 @@ while [[ $# -gt 0 ]]; do
23       local_api_base="$2"
24       shift 2
25       ;;
26+    --local-api-allowed-hosts)
27+      local_api_allowed_hosts="$2"
28+      shift 2
29+      ;;
30     --status-api-base)
31       status_api_base="$2"
32       shift 2
33       ;;
34+    --status-api-host)
35+      status_api_host="$2"
36+      shift 2
37+      ;;
38     --username)
39       username="$2"
40       shift 2
41@@ -323,6 +336,8 @@ run_static_checks() {
42     --install-dir "$install_dir"
43     --control-api-base "$control_api_base"
44     --local-api-base "$local_api_base"
45+    --local-api-allowed-hosts "$local_api_allowed_hosts"
46+    --status-api-host "$status_api_host"
47     --username "$username"
48   )
49