- 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
+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
+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` 恢复:
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
+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
+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+}
+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
+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" \
+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+});
+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)
+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 记录