baa-conductor

git clone 

commit
dee2b20
parent
4f0ce32
author
im_wower
date
2026-03-29 03:03:29 +0800 CST
chore: wire conductor D1 sync launchd config
10 files changed,  +359, -15
M docs/runtime/environment.md
+22, -0
 1@@ -38,6 +38,19 @@
 2 - `status-api` 现在会优先读取 `BAA_CONDUCTOR_LOCAL_API`,只有手工启动或旧配置缺少该值时才回退到 `BAA_CONTROL_API_BASE`
 3 - mini 默认兼容值仍是 `https://conductor.makefile.so`
 4 
 5+## 可选 D1 同步变量
 6+
 7+- `D1_ACCOUNT_ID`
 8+- `D1_DATABASE_ID`
 9+- `CLOUDFLARE_API_TOKEN`
10+
11+说明:
12+
13+- 这组三元组只对 `conductor` 生效,用于启用 artifact D1 sync worker
14+- 三个值必须同时存在;只配其中一部分时,launchd 安装脚本会直接报错
15+- `install-launchd.sh` / `install-mini.sh` 会优先读取当前 shell env,也支持从 `~/.config/baa-conductor/runtime-secrets.env` 之类的 env 文件读取
16+- 当前正式运行面只把这组三元组写给 `conductor` 安装副本,不会写给 `codexd` / `status-api`
17+
18 ## codexd 变量
19 
20 `apps/codexd` 当前识别这些变量:
21@@ -122,6 +135,7 @@ Firefox WS 派生规则:
22   --service conductor \
23   --service codexd \
24   --shared-token-file /Users/george/.config/baa-conductor/shared-token.txt \
25+  --d1-secrets-env /Users/george/.config/baa-conductor/runtime-secrets.env \
26   --local-api-base http://100.71.210.78:4317 \
27   --local-api-allowed-hosts 100.71.210.78 \
28   --codexd-local-api-base http://127.0.0.1:4319
29@@ -129,6 +143,14 @@ Firefox WS 派生规则:
30 
31 默认 mini 安装不需要显式传 `--public-api-base`;只有 `conductor` 的 upstream/public API base 不是 `https://conductor.makefile.so` 时才需要覆盖。`--control-api-base` 仍可作为 legacy 兼容别名使用;如果同一轮 CLI 同时给出新旧参数名,`--public-api-base` 优先。脚本现在也只会把这些变量写给 `conductor` 安装副本;`status-api` 默认仍然优先读取 `--local-api-base` 对应的 conductor 主接口。
32 
33+如果你希望 launchd 安装副本稳定带上 D1 同步配置,可以在 `runtime-secrets.env` 里补这三行:
34+
35+```text
36+D1_ACCOUNT_ID=3cb181c015e004e4d6f81891c0d66fec
37+D1_DATABASE_ID=<cloudflare-d1-database-id>
38+CLOUDFLARE_API_TOKEN=<api-token>
39+```
40+
41 如果要安装可选状态观察面,再单独执行:
42 
43 ```bash
M docs/runtime/launchd.md
+10, -0
 1@@ -51,6 +51,7 @@
 2 说明:
 3 
 4 - `codexd` 独立安装时不需要共享 token
 5+- 如果 `runtime-secrets.env` 里同时提供 `D1_ACCOUNT_ID`、`D1_DATABASE_ID`、`CLOUDFLARE_API_TOKEN`,安装脚本会把它们写进 `conductor` LaunchAgent,从而启用 D1 sync worker
 6 - `status-api` 默认真相源是 `BAA_CONDUCTOR_LOCAL_API`
 7 - `--public-api-base` 是 canonical 参数名;`--control-api-base` 仍保留为 legacy 兼容别名,但都只影响 `conductor` 安装副本
 8 - `--codexd-local-api-base` 会同时写给 `codexd` 和 `conductor`
 9@@ -112,6 +113,7 @@
10   --service codexd \
11   --install-dir /Users/george/Library/LaunchAgents \
12   --shared-token-file /Users/george/.config/baa-conductor/shared-token.txt \
13+  --d1-secrets-env /Users/george/.config/baa-conductor/runtime-secrets.env \
14   --local-api-base http://100.71.210.78:4317 \
15   --local-api-allowed-hosts 100.71.210.78 \
16   --codexd-local-api-base http://127.0.0.1:4319
17@@ -158,6 +160,14 @@
18 
19 否则 `conductor` 虽然会正常监听 `4317`,但 `/v1/codex*` 会返回 `503 codexd_not_configured`。
20 
21+如果要让 `conductor` 带上 D1 远端同步配置,确保当前 shell 或 `--d1-secrets-env` 指向的 env 文件里完整提供:
22+
23+- `D1_ACCOUNT_ID`
24+- `D1_DATABASE_ID`
25+- `CLOUDFLARE_API_TOKEN`
26+
27+这三个值必须一起存在;只给其中一部分时,安装脚本会直接失败,避免生成“看起来装好了、实际上 sync worker 不会启动”的半配置副本。
28+
29 ## reload 行为
30 
31 `restart-launchd.sh` / `reload-launchd.sh` 不只看 `launchctl` 返回码,还会等待已选服务的 `/healthz` 恢复:
M ops/launchd/so.makefile.baa-conductor.plist
+3, -2
 1@@ -4,8 +4,9 @@
 2   Source template kept in the repo.
 3   Default values target the mini node at /Users/george/code/baa-conductor.
 4   Use scripts/runtime/install-launchd.sh to render the actual install copy.
 5-  Adjust BAA_SHARED_TOKEN, BAA_CODEXD_LOCAL_API_BASE, and the listen-related
 6-  variables before loading if the mini node uses a non-default address.
 7+  Adjust BAA_SHARED_TOKEN, BAA_CODEXD_LOCAL_API_BASE, the optional D1 sync
 8+  env vars, and the listen-related variables before loading if the mini node
 9+  uses a non-default address.
10   BAA_CONDUCTOR_PUBLIC_API_BASE is the canonical upstream/public API base.
11   BAA_CONTROL_API_BASE remains here only as a legacy compatibility fallback.
12   If this file is installed under /Library/LaunchDaemons, add UserName and keep
M scripts/runtime/check-launchd.sh
+83, -0
  1@@ -20,6 +20,13 @@ Options:
  2   --install-dir PATH        Validate installed copies under this directory.
  3   --shared-token TOKEN      Expect this exact token in installed copies.
  4   --shared-token-file PATH  Read the expected token from a file.
  5+  --d1-account-id ID        Expect this exact D1_ACCOUNT_ID in conductor.
  6+  --d1-database-id ID       Expect this exact D1_DATABASE_ID in conductor.
  7+  --cloudflare-api-token TOKEN
  8+                            Expect this exact CLOUDFLARE_API_TOKEN in conductor.
  9+  --d1-secrets-env PATH     Read D1_ACCOUNT_ID, D1_DATABASE_ID, and
 10+                            CLOUDFLARE_API_TOKEN from an env file when not
 11+                            provided explicitly.
 12   --public-api-base URL     Expected conductor BAA_CONDUCTOR_PUBLIC_API_BASE.
 13   --control-api-base URL    Legacy alias for --public-api-base.
 14   --local-api-base URL      Expected BAA_CONDUCTOR_LOCAL_API.
 15@@ -42,6 +49,8 @@ Options:
 16 Notes:
 17   If no service is specified, conductor + codexd are checked.
 18   Use --service status-api to validate the optional local read-only observer.
 19+  D1 sync is optional. When expected for conductor, D1_ACCOUNT_ID,
 20+  D1_DATABASE_ID, and CLOUDFLARE_API_TOKEN must be provided together.
 21   status-api defaults to BAA_CONDUCTOR_LOCAL_API /v1/system/state; launchd no
 22   longer writes conductor public-api base env vars for it.
 23   --public-api-base only applies when conductor is part of the checked set.
 24@@ -62,6 +71,10 @@ home_dir="$(default_home_dir)"
 25 install_dir=""
 26 shared_token=""
 27 shared_token_file=""
 28+d1_account_id="${D1_ACCOUNT_ID:-}"
 29+d1_database_id="${D1_DATABASE_ID:-}"
 30+cloudflare_api_token="${CLOUDFLARE_API_TOKEN:-}"
 31+d1_secrets_env=""
 32 public_api_base=""
 33 legacy_control_api_base=""
 34 local_api_base="http://100.71.210.78:4317"
 35@@ -122,6 +135,22 @@ while [[ $# -gt 0 ]]; do
 36       shared_token_file="$2"
 37       shift 2
 38       ;;
 39+    --d1-account-id)
 40+      d1_account_id="$2"
 41+      shift 2
 42+      ;;
 43+    --d1-database-id)
 44+      d1_database_id="$2"
 45+      shift 2
 46+      ;;
 47+    --cloudflare-api-token)
 48+      cloudflare_api_token="$2"
 49+      shift 2
 50+      ;;
 51+    --d1-secrets-env)
 52+      d1_secrets_env="$2"
 53+      shift 2
 54+      ;;
 55     --public-api-base)
 56       public_api_base="$2"
 57       shift 2
 58@@ -225,6 +254,27 @@ if services_require_shared_token && [[ -n "$shared_token" || -n "$shared_token_f
 59   shared_token="$(load_shared_token "$shared_token" "$shared_token_file")"
 60 fi
 61 
 62+d1_account_id="$(resolve_env_or_file_value "$d1_account_id" "D1_ACCOUNT_ID" "$d1_secrets_env")"
 63+d1_database_id="$(resolve_env_or_file_value "$d1_database_id" "D1_DATABASE_ID" "$d1_secrets_env")"
 64+cloudflare_api_token="$(
 65+  resolve_env_or_file_value "$cloudflare_api_token" "CLOUDFLARE_API_TOKEN" "$d1_secrets_env"
 66+)"
 67+
 68+d1_config_count=0
 69+if [[ -n "$d1_account_id" ]]; then
 70+  d1_config_count=$((d1_config_count + 1))
 71+fi
 72+if [[ -n "$d1_database_id" ]]; then
 73+  d1_config_count=$((d1_config_count + 1))
 74+fi
 75+if [[ -n "$cloudflare_api_token" ]]; then
 76+  d1_config_count=$((d1_config_count + 1))
 77+fi
 78+
 79+if [[ "$d1_config_count" != "0" && "$d1_config_count" != "3" ]]; then
 80+  die "Partial D1 config is not allowed. Provide D1_ACCOUNT_ID, D1_DATABASE_ID, and CLOUDFLARE_API_TOKEN together, or omit all three."
 81+fi
 82+
 83 set -- $(resolve_node_defaults "$node")
 84 conductor_host="$1"
 85 conductor_role="$2"
 86@@ -290,6 +340,34 @@ check_public_api_base_keys() {
 87   fi
 88 }
 89 
 90+check_conductor_d1_keys() {
 91+  local service="$1"
 92+  local plist_path="$2"
 93+
 94+  if [[ "$d1_config_count" == "3" ]]; then
 95+    check_string_equals \
 96+      "${service}:D1_ACCOUNT_ID" \
 97+      "$(plist_print_value "$plist_path" ":EnvironmentVariables:D1_ACCOUNT_ID")" \
 98+      "$d1_account_id"
 99+    check_string_equals \
100+      "${service}:D1_DATABASE_ID" \
101+      "$(plist_print_value "$plist_path" ":EnvironmentVariables:D1_DATABASE_ID")" \
102+      "$d1_database_id"
103+    check_string_equals \
104+      "${service}:CLOUDFLARE_API_TOKEN" \
105+      "$(plist_print_value "$plist_path" ":EnvironmentVariables:CLOUDFLARE_API_TOKEN")" \
106+      "$cloudflare_api_token"
107+    return 0
108+  fi
109+
110+  check_key_missing "${service}:D1_ACCOUNT_ID" "$plist_path" ":EnvironmentVariables:D1_ACCOUNT_ID"
111+  check_key_missing "${service}:D1_DATABASE_ID" "$plist_path" ":EnvironmentVariables:D1_DATABASE_ID"
112+  check_key_missing \
113+    "${service}:CLOUDFLARE_API_TOKEN" \
114+    "$plist_path" \
115+    ":EnvironmentVariables:CLOUDFLARE_API_TOKEN"
116+}
117+
118 check_installed_plist() {
119   local service="$1"
120   local plist_path="$2"
121@@ -339,6 +417,11 @@ check_installed_plist() {
122     check_string_equals "${service}:host-arg" "$(plist_print_value "$plist_path" ":ProgramArguments:4")" "$conductor_host"
123     check_string_equals "${service}:role-arg" "$(plist_print_value "$plist_path" ":ProgramArguments:6")" "$conductor_role"
124     check_string_equals "${service}:BAA_CODEXD_LOCAL_API_BASE" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CODEXD_LOCAL_API_BASE")" "$codexd_local_api_base"
125+    check_conductor_d1_keys "$service" "$plist_path"
126+  else
127+    check_key_missing "${service}:D1_ACCOUNT_ID" "$plist_path" ":EnvironmentVariables:D1_ACCOUNT_ID"
128+    check_key_missing "${service}:D1_DATABASE_ID" "$plist_path" ":EnvironmentVariables:D1_DATABASE_ID"
129+    check_key_missing "${service}:CLOUDFLARE_API_TOKEN" "$plist_path" ":EnvironmentVariables:CLOUDFLARE_API_TOKEN"
130   fi
131 
132   if [[ "$service" == "codexd" ]]; then
M scripts/runtime/common.sh
+30, -0
 1@@ -395,3 +395,33 @@ load_shared_token() {
 2 
 3   printf '%s' "$shared_token"
 4 }
 5+
 6+read_env_file_value() {
 7+  local env_file="$1"
 8+  local key="$2"
 9+
10+  if [[ -z "$env_file" || ! -f "$env_file" ]]; then
11+    return 0
12+  fi
13+
14+  awk -F= -v key="$key" '
15+    $1 == key {
16+      sub(/^[^=]*=/, "");
17+      print;
18+      exit;
19+    }
20+  ' "$env_file"
21+}
22+
23+resolve_env_or_file_value() {
24+  local current_value="$1"
25+  local key="$2"
26+  local env_file="$3"
27+
28+  if [[ -n "$current_value" ]]; then
29+    printf '%s' "$current_value"
30+    return 0
31+  fi
32+
33+  read_env_file_value "$env_file" "$key"
34+}
M scripts/runtime/install-launchd.sh
+64, -0
  1@@ -20,6 +20,13 @@ Options:
  2   --install-dir PATH        Override launchd install directory.
  3   --shared-token TOKEN      Shared token written into the install copy.
  4   --shared-token-file PATH  Read the shared token from a file.
  5+  --d1-account-id ID        Write D1_ACCOUNT_ID into the conductor install copy.
  6+  --d1-database-id ID       Write D1_DATABASE_ID into the conductor install copy.
  7+  --cloudflare-api-token TOKEN
  8+                            Write CLOUDFLARE_API_TOKEN into the conductor install copy.
  9+  --d1-secrets-env PATH     Read D1_ACCOUNT_ID, D1_DATABASE_ID, and
 10+                            CLOUDFLARE_API_TOKEN from an env file when not
 11+                            provided explicitly.
 12   --public-api-base URL     Override conductor BAA_CONDUCTOR_PUBLIC_API_BASE.
 13   --control-api-base URL    Legacy alias for --public-api-base.
 14   --local-api-base URL      Override BAA_CONDUCTOR_LOCAL_API.
 15@@ -40,6 +47,8 @@ Notes:
 16   If no service is specified, conductor + codexd are installed.
 17   Use --service codexd to render codexd independently; it does not require a
 18   shared token.
 19+  D1 sync is optional. When enabled for conductor, D1_ACCOUNT_ID,
 20+  D1_DATABASE_ID, and CLOUDFLARE_API_TOKEN must be provided together.
 21   Use --service status-api to install the optional local read-only observer.
 22   status-api defaults to BAA_CONDUCTOR_LOCAL_API /v1/system/state; launchd no
 23   longer writes conductor public-api base env vars for it.
 24@@ -62,6 +71,10 @@ home_dir="$(default_home_dir)"
 25 install_dir=""
 26 shared_token="${BAA_SHARED_TOKEN:-}"
 27 shared_token_file=""
 28+d1_account_id="${D1_ACCOUNT_ID:-}"
 29+d1_database_id="${D1_DATABASE_ID:-}"
 30+cloudflare_api_token="${CLOUDFLARE_API_TOKEN:-}"
 31+d1_secrets_env=""
 32 public_api_base=""
 33 legacy_control_api_base=""
 34 local_api_base="http://100.71.210.78:4317"
 35@@ -121,6 +134,22 @@ while [[ $# -gt 0 ]]; do
 36       shared_token_file="$2"
 37       shift 2
 38       ;;
 39+    --d1-account-id)
 40+      d1_account_id="$2"
 41+      shift 2
 42+      ;;
 43+    --d1-database-id)
 44+      d1_database_id="$2"
 45+      shift 2
 46+      ;;
 47+    --cloudflare-api-token)
 48+      cloudflare_api_token="$2"
 49+      shift 2
 50+      ;;
 51+    --d1-secrets-env)
 52+      d1_secrets_env="$2"
 53+      shift 2
 54+      ;;
 55     --public-api-base)
 56       public_api_base="$2"
 57       shift 2
 58@@ -216,6 +245,27 @@ if services_require_shared_token; then
 59   fi
 60 fi
 61 
 62+d1_account_id="$(resolve_env_or_file_value "$d1_account_id" "D1_ACCOUNT_ID" "$d1_secrets_env")"
 63+d1_database_id="$(resolve_env_or_file_value "$d1_database_id" "D1_DATABASE_ID" "$d1_secrets_env")"
 64+cloudflare_api_token="$(
 65+  resolve_env_or_file_value "$cloudflare_api_token" "CLOUDFLARE_API_TOKEN" "$d1_secrets_env"
 66+)"
 67+
 68+d1_config_count=0
 69+if [[ -n "$d1_account_id" ]]; then
 70+  d1_config_count=$((d1_config_count + 1))
 71+fi
 72+if [[ -n "$d1_database_id" ]]; then
 73+  d1_config_count=$((d1_config_count + 1))
 74+fi
 75+if [[ -n "$cloudflare_api_token" ]]; then
 76+  d1_config_count=$((d1_config_count + 1))
 77+fi
 78+
 79+if [[ "$d1_config_count" != "0" && "$d1_config_count" != "3" ]]; then
 80+  die "Partial D1 config is not allowed. Provide D1_ACCOUNT_ID, D1_DATABASE_ID, and CLOUDFLARE_API_TOKEN together, or omit all three."
 81+fi
 82+
 83 if [[ -z "$install_dir" ]]; then
 84   install_dir="$(default_install_dir "$scope" "$home_dir")"
 85 fi
 86@@ -302,6 +352,20 @@ for service in "${services[@]}"; do
 87     plist_set_string "$install_path" ":ProgramArguments:6" "$conductor_role"
 88     plist_set_string "$install_path" ":EnvironmentVariables:BAA_CODEXD_LOCAL_API_BASE" "$codexd_local_api_base"
 89     plist_set_string "$install_path" ":EnvironmentVariables:BAA_CLAUDE_CODED_LOCAL_API_BASE" "$claude_coded_local_api_base"
 90+
 91+    if [[ "$d1_config_count" == "3" ]]; then
 92+      plist_set_string "$install_path" ":EnvironmentVariables:D1_ACCOUNT_ID" "$d1_account_id"
 93+      plist_set_string "$install_path" ":EnvironmentVariables:D1_DATABASE_ID" "$d1_database_id"
 94+      plist_set_string "$install_path" ":EnvironmentVariables:CLOUDFLARE_API_TOKEN" "$cloudflare_api_token"
 95+    else
 96+      plist_delete_key "$install_path" ":EnvironmentVariables:D1_ACCOUNT_ID"
 97+      plist_delete_key "$install_path" ":EnvironmentVariables:D1_DATABASE_ID"
 98+      plist_delete_key "$install_path" ":EnvironmentVariables:CLOUDFLARE_API_TOKEN"
 99+    fi
100+  else
101+    plist_delete_key "$install_path" ":EnvironmentVariables:D1_ACCOUNT_ID"
102+    plist_delete_key "$install_path" ":EnvironmentVariables:D1_DATABASE_ID"
103+    plist_delete_key "$install_path" ":EnvironmentVariables:CLOUDFLARE_API_TOKEN"
104   fi
105 
106   if [[ "$service" == "codexd" ]]; then
M scripts/runtime/install-mini.sh
+5, -0
 1@@ -20,6 +20,9 @@ Options:
 2                             Falls back to legacy
 3                             ~/.config/baa-conductor/control-api-worker.secrets.env
 4                             only if the default file is missing.
 5+                            If the file also defines D1_ACCOUNT_ID,
 6+                            D1_DATABASE_ID, and CLOUDFLARE_API_TOKEN, the
 7+                            conductor LaunchAgent will carry D1 sync config.
 8   --with-status-api         Also install/restart/check the optional local status-api service.
 9   --skip-build              Skip pnpm build.
10   --skip-restart            Skip launchd restart.
11@@ -200,6 +203,7 @@ run_or_print 0 "${SCRIPT_DIR}/install-launchd.sh" \
12   "${install_services[@]}" \
13   --install-dir "$install_dir" \
14   --shared-token-file "$shared_token_file" \
15+  --d1-secrets-env "$secrets_env" \
16   --local-api-base "http://100.71.210.78:4317" \
17   --local-api-allowed-hosts "100.71.210.78" \
18   --codexd-local-api-base "$codexd_api_base" \
19@@ -225,6 +229,7 @@ if [[ "$skip_check" != "1" ]]; then
20     "${install_services[@]}" \
21     --install-dir "$install_dir" \
22     --shared-token-file "$shared_token_file" \
23+    --d1-secrets-env "$secrets_env" \
24     --local-api-base "http://100.71.210.78:4317" \
25     --local-api-allowed-hosts "100.71.210.78" \
26     --codexd-local-api-base "$codexd_api_base" \
M scripts/runtime/public-api-base.test.mjs
+90, -6
  1@@ -1,6 +1,6 @@
  2 import assert from "node:assert/strict";
  3 import { execFileSync } from "node:child_process";
  4-import { mkdtempSync, rmSync } from "node:fs";
  5+import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
  6 import { tmpdir } from "node:os";
  7 import { join } from "node:path";
  8 import test from "node:test";
  9@@ -14,11 +14,31 @@ const plistBuddy = "/usr/libexec/PlistBuddy";
 10 const sharedToken = "script-test-shared-token";
 11 const publicApiBase = "https://public.example.test";
 12 const conductorPlistLabel = "so.makefile.baa-conductor.plist";
 13+const d1AccountId = "acc-test";
 14+const d1DatabaseId = "db-test";
 15+const cloudflareApiToken = "token-test";
 16+
 17+function ensureRuntimeDirs() {
 18+  for (const relativePath of [
 19+    "state",
 20+    "runs",
 21+    "worktrees",
 22+    "logs",
 23+    "logs/launchd",
 24+    "tmp"
 25+  ]) {
 26+    mkdirSync(join(repoDir, relativePath), { recursive: true });
 27+  }
 28+}
 29 
 30-function runScript(scriptPath, args) {
 31+function runScript(scriptPath, args, envOverrides = {}) {
 32+  ensureRuntimeDirs();
 33   execFileSync("bash", [scriptPath, ...args], {
 34     cwd: repoDir,
 35-    env: process.env,
 36+    env: {
 37+      ...process.env,
 38+      ...envOverrides
 39+    },
 40     stdio: "pipe"
 41   });
 42 }
 43@@ -83,7 +103,8 @@ test("install-launchd writes canonical and legacy conductor public API env names
 44       "--public-api-base",
 45       publicApiBase,
 46       "--codexd-local-api-base",
 47-      "http://127.0.0.1:4319"
 48+      "http://127.0.0.1:4319",
 49+      "--skip-dist-check"
 50     ])
 51   );
 52 
 53@@ -94,6 +115,7 @@ test("install-launchd writes canonical and legacy conductor public API env names
 54       publicApiBase,
 55       "--codexd-api-base",
 56       "http://127.0.0.1:4319",
 57+      "--skip-static-check",
 58       "--skip-port-check",
 59       "--skip-process-check",
 60       "--skip-http-check",
 61@@ -130,7 +152,8 @@ test("check-launchd accepts legacy conductor install copies with only BAA_CONTRO
 62       "--public-api-base",
 63       publicApiBase,
 64       "--codexd-local-api-base",
 65-      "http://127.0.0.1:4319"
 66+      "http://127.0.0.1:4319",
 67+      "--skip-dist-check"
 68     ])
 69   );
 70   runScript(
 71@@ -139,7 +162,68 @@ test("check-launchd accepts legacy conductor install copies with only BAA_CONTRO
 72       "--control-api-base",
 73       publicApiBase,
 74       "--codexd-local-api-base",
 75-      "http://127.0.0.1:4319"
 76+      "http://127.0.0.1:4319",
 77+      "--skip-dist-check"
 78     ])
 79   );
 80 });
 81+
 82+test("install-launchd writes conductor D1 sync env vars from a secrets env file", (t) => {
 83+  const installDir = mkdtempSync(join(tmpdir(), "baa-conductor-d1-install-"));
 84+  const secretsDir = mkdtempSync(join(tmpdir(), "baa-conductor-d1-secrets-"));
 85+  const plistPath = join(installDir, conductorPlistLabel);
 86+  const secretsPath = join(secretsDir, "runtime-secrets.env");
 87+
 88+  t.after(() => {
 89+    rmSync(installDir, { force: true, recursive: true });
 90+    rmSync(secretsDir, { force: true, recursive: true });
 91+  });
 92+
 93+  writeFileSync(
 94+    secretsPath,
 95+    [
 96+      `D1_ACCOUNT_ID=${d1AccountId}`,
 97+      `D1_DATABASE_ID=${d1DatabaseId}`,
 98+      `CLOUDFLARE_API_TOKEN=${cloudflareApiToken}`
 99+    ].join("\n"),
100+    "utf8"
101+  );
102+
103+  runScript(
104+    installScript,
105+    conductorInstallArgs(installDir, [
106+      "--d1-secrets-env",
107+      secretsPath,
108+      "--codexd-local-api-base",
109+      "http://127.0.0.1:4319"
110+    ]),
111+    {
112+      D1_ACCOUNT_ID: "",
113+      D1_DATABASE_ID: "",
114+      CLOUDFLARE_API_TOKEN: ""
115+    }
116+  );
117+
118+  assert.equal(plistValue(plistPath, ":EnvironmentVariables:D1_ACCOUNT_ID"), d1AccountId);
119+  assert.equal(plistValue(plistPath, ":EnvironmentVariables:D1_DATABASE_ID"), d1DatabaseId);
120+  assert.equal(
121+    plistValue(plistPath, ":EnvironmentVariables:CLOUDFLARE_API_TOKEN"),
122+    cloudflareApiToken
123+  );
124+
125+  runScript(
126+    checkLaunchdScript,
127+    conductorInstallArgs(installDir, [
128+      "--d1-secrets-env",
129+      secretsPath,
130+      "--codexd-local-api-base",
131+      "http://127.0.0.1:4319",
132+      "--skip-dist-check"
133+    ]),
134+    {
135+      D1_ACCOUNT_ID: "",
136+      D1_DATABASE_ID: "",
137+      CLOUDFLARE_API_TOKEN: ""
138+    }
139+  );
140+});
M scripts/runtime/verify-mini.sh
+11, -2
 1@@ -20,6 +20,11 @@ Options:
 2   --install-dir PATH           Validate installed copies under this directory.
 3   --shared-token TOKEN         Expect this exact token in installed copies.
 4   --shared-token-file PATH     Read the expected token from a file.
 5+  --d1-account-id ID           Expect this exact D1_ACCOUNT_ID in conductor.
 6+  --d1-database-id ID          Expect this exact D1_DATABASE_ID in conductor.
 7+  --cloudflare-api-token TOKEN
 8+                               Expect this exact CLOUDFLARE_API_TOKEN in conductor.
 9+  --d1-secrets-env PATH        Read the D1 sync trio from an env file.
10   --public-api-base URL        Expected conductor BAA_CONDUCTOR_PUBLIC_API_BASE.
11   --control-api-base URL       Legacy alias for --public-api-base.
12   --local-api-base URL         Conductor local API base URL.
13@@ -68,10 +73,14 @@ while [[ $# -gt 0 ]]; do
14       shift 2
15       ;;
16     --scope | --service | --repo-dir | --home-dir | --install-dir | --shared-token | \
17-    --shared-token-file | --public-api-base | --control-api-base | --local-api-base | \
18+    --shared-token-file | --d1-account-id | --d1-database-id | --cloudflare-api-token | \
19+    --d1-secrets-env | --public-api-base | --control-api-base | --local-api-base | \
20     --local-api-allowed-hosts | --status-api-host | --username | --domain)
21       launchd_args+=("$1" "$2")
22-      node_args+=("$1" "$2")
23+      if [[ "$1" != "--d1-account-id" && "$1" != "--d1-database-id" \
24+        && "$1" != "--cloudflare-api-token" && "$1" != "--d1-secrets-env" ]]; then
25+        node_args+=("$1" "$2")
26+      fi
27       shift 2
28       ;;
29     --all-services | --check-loaded)
M tasks/T-S052.md
+41, -5
 1@@ -2,10 +2,10 @@
 2 
 3 ## 状态
 4 
 5-- 当前状态:`待开始`
 6+- 当前状态:`已完成`
 7 - 规模预估:`S`
 8 - 依赖任务:`T-S042`(代码已完成)
 9-- 建议执行者:`均可`(运维操作,无代码改动)
10+- 建议执行者:`均可`(运维 + runtime 配置修正)
11 
12 ## 直接给对话的提示词
13 
14@@ -123,21 +123,57 @@ wrangler d1 execute baa-conductor-artifact --command="SELECT COUNT(*) FROM messa
15 
16 ### 开始执行
17 
18-- 执行者:
19-- 开始时间:
20+- 执行者:Codex
21+- 开始时间:2026-03-29 02:44:00 CST
22 - 状态变更:`待开始` → `进行中`
23 
24 ### 完成摘要
25 
26-- 完成时间:
27+- 完成时间:2026-03-29 03:02:19 CST
28 - 状态变更:`进行中` → `已完成`
29 - 修改了哪些文件:
30+  - `tasks/T-S052.md`
31+  - `scripts/runtime/common.sh`
32+  - `scripts/runtime/install-launchd.sh`
33+  - `scripts/runtime/check-launchd.sh`
34+  - `scripts/runtime/install-mini.sh`
35+  - `scripts/runtime/verify-mini.sh`
36+  - `scripts/runtime/public-api-base.test.mjs`
37+  - `docs/runtime/environment.md`
38+  - `docs/runtime/launchd.md`
39+  - `ops/launchd/so.makefile.baa-conductor.plist`
40 - 核心实现思路:
41+  - 在 Cloudflare 创建新的 `baa-conductor-artifact` D1 数据库,得到 `database_id = bedf49ae-96de-438b-a6ee-59262e5fb3b0`,并用 `packages/d1-client/src/d1-setup.sql` 完成远端建表
42+  - 实际排查发现当前运行中的 conductor LaunchAgent 没有任何 D1 环境变量,且 `launchctl kickstart` 不会重新读取修改后的 plist;因此补齐 runtime 安装/检查脚本,让 `D1_ACCOUNT_ID`、`D1_DATABASE_ID`、`CLOUDFLARE_API_TOKEN` 可从 shell env 或 secrets env 稳定写入 conductor 安装副本,并用 `reload-launchd.sh` 重新 bootstrap job
43+  - 实际运行面指向主仓库 `/Users/george/code/baa-conductor`,而主仓库源码已包含 T-S042 逻辑但 `dist/` 未构建完全;因此先对 `@baa-conductor/conductor-daemon` 做定向 build,再重载 launchd,使 D1 sync worker 真正启动
44+  - 用本机 Firefox bridge WebSocket 手工发送一条 `browser.final_message`(`assistant_message_id = msg-d1-sync-1774724481762`,内容为最小 ` ```baa @conductor::describe ``` `),验证本地 `d1_sync_queue` 从 pending → synced,远端 D1 `messages` 计数从 `0` 变为 `1`,且能查到对应消息
45 - 跑了哪些测试:
46+  - `node --test /Users/george/code/baa-conductor-d1-database-init/scripts/runtime/public-api-base.test.mjs`
47+  - `bash -n /Users/george/code/baa-conductor-d1-database-init/scripts/runtime/install-launchd.sh /Users/george/code/baa-conductor-d1-database-init/scripts/runtime/check-launchd.sh /Users/george/code/baa-conductor-d1-database-init/scripts/runtime/install-mini.sh /Users/george/code/baa-conductor-d1-database-init/scripts/runtime/verify-mini.sh /Users/george/code/baa-conductor-d1-database-init/scripts/runtime/common.sh`
48+  - `npx pnpm -C /Users/george/code/baa-conductor -F @baa-conductor/conductor-daemon build`
49+  - `wrangler d1 create baa-conductor-artifact`
50+  - `wrangler d1 execute baa-conductor-artifact --remote --file=/Users/george/code/baa-conductor-d1-database-init/packages/d1-client/src/d1-setup.sql`
51+  - `wrangler d1 execute baa-conductor-artifact --remote --command="SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"`
52+  - `/Users/george/code/baa-conductor-d1-database-init/scripts/runtime/install-launchd.sh --repo-dir /Users/george/code/baa-conductor --service conductor --install-dir /Users/george/Library/LaunchAgents ...`
53+  - `/Users/george/code/baa-conductor-d1-database-init/scripts/runtime/check-launchd.sh --repo-dir /Users/george/code/baa-conductor --service conductor --install-dir /Users/george/Library/LaunchAgents ... --check-loaded`
54+  - `/Users/george/code/baa-conductor-d1-database-init/scripts/runtime/reload-launchd.sh --service conductor --install-dir /Users/george/Library/LaunchAgents`
55+  - `curl -sS http://100.71.210.78:4317/healthz`
56+  - `sqlite3 /Users/george/code/baa-conductor/state/artifact.db ".tables"`
57+  - `sqlite3 -header -column /Users/george/code/baa-conductor/state/artifact.db "SELECT id, table_name, record_id, operation, status, attempts, last_attempt_at FROM d1_sync_queue ORDER BY id DESC LIMIT 5;"`
58+  - `wrangler d1 execute baa-conductor-artifact --remote --command="SELECT COUNT(*) AS message_count FROM messages"`
59+  - `wrangler d1 execute baa-conductor-artifact --remote --command="SELECT id, platform, conversation_id, role FROM messages WHERE id = 'msg-d1-sync-1774724481762'"`
60+  - `wrangler d1 execute baa-conductor-artifact --remote --command="SELECT COUNT(*) AS execution_count FROM executions"`
61 
62 ### 执行过程中遇到的问题
63 
64 > 记录执行过程中遇到的阻塞、环境问题、临时绕过方案等。合并时由合并者判断是否需要修复或建新任务。
65 
66+- `wrangler d1 execute` 在没有 wrangler 配置绑定时默认走 local,需要显式加 `--remote`
67+- 当前运行中的 `/Users/george/Library/LaunchAgents/so.makefile.baa-conductor.plist` 没有任何 D1 变量,不是任务文档描述的“旧库 ID 配错”,而是“D1 三元组整体缺失”
68+- 单独执行 `launchctl kickstart -k gui/$(id -u)/so.makefile.baa-conductor` 只会重启进程,不会让 launchd 重新读取更新后的 plist;必须 `bootout + bootstrap`,本次通过 `scripts/runtime/reload-launchd.sh` 完成
69+- 主仓库 `/Users/george/code/baa-conductor` 的源码已有 T-S042,但运行中的 `dist/` 没把 `@baa-conductor/d1-client` 等产物构建出来;如果不先 build,重启后不会真正启动 sync worker
70+
71 ### 剩余风险
72 
73+- 当前主机上的已安装 LaunchAgent 已经带上新的 D1 配置,但如果在分支合并前再次使用旧版 runtime 脚本重渲染 install copy,这组三元组可能被覆盖掉;需要以本分支的脚本合并为准
74+- 本次真实链路验证覆盖了 `messages` 和 `executions` 同步;`sessions` 表已建好,但这次最小 `browser.final_message` 验证没有额外产生新的 session upsert 记录