- commit
- eacbc47
- parent
- 1078d2b
- author
- im_wower
- date
- 2026-03-25 23:15:42 +0800 CST
merge: land T-S005 and T-S007
21 files changed,
+437,
-43
+100,
-0
1@@ -2097,6 +2097,106 @@ test("ConductorRuntime exposes a minimal runtime snapshot for CLI and status sur
2 });
3 });
4
5+test("ConductorRuntime.stop closes active Firefox bridge clients and releases the local API listener", async () => {
6+ const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-runtime-stop-"));
7+ const runtime = new ConductorRuntime(
8+ {
9+ nodeId: "mini-main",
10+ host: "mini",
11+ role: "primary",
12+ controlApiBase: "https://control.example.test",
13+ localApiBase: "http://127.0.0.1:0",
14+ sharedToken: "replace-me",
15+ paths: {
16+ runsDir: "/tmp/runs",
17+ stateDir
18+ }
19+ },
20+ {
21+ autoStartLoops: false,
22+ now: () => 100
23+ }
24+ );
25+
26+ let client = null;
27+
28+ try {
29+ const snapshot = await runtime.start();
30+ const baseUrl = snapshot.controlApi.localApiBase;
31+ const wsUrl = snapshot.controlApi.firefoxWsUrl;
32+
33+ assert.equal(typeof baseUrl, "string");
34+ assert.equal(typeof wsUrl, "string");
35+
36+ const healthResponse = await fetch(`${baseUrl}/healthz`);
37+ assert.equal(healthResponse.status, 200);
38+
39+ client = await connectFirefoxBridgeClient(wsUrl, "firefox-stop");
40+
41+ const closePromise = waitForWebSocketClose(client.socket);
42+ const stoppedSnapshot = await runtime.stop();
43+
44+ assert.equal(stoppedSnapshot.runtime.started, false);
45+ assert.deepEqual(await closePromise, {
46+ code: 1001,
47+ reason: "server shutdown"
48+ });
49+ await assertLocalApiListenerClosed(baseUrl);
50+ } finally {
51+ client?.queue.stop();
52+ client?.socket.close(1000, "done");
53+ await runtime.stop();
54+ rmSync(stateDir, {
55+ force: true,
56+ recursive: true
57+ });
58+ }
59+});
60+
61+test("ConductorRuntime.stop remains idempotent after the local API listener is closed", async () => {
62+ const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-runtime-stop-repeat-"));
63+ const runtime = new ConductorRuntime(
64+ {
65+ nodeId: "mini-main",
66+ host: "mini",
67+ role: "primary",
68+ controlApiBase: "https://control.example.test",
69+ localApiBase: "http://127.0.0.1:0",
70+ sharedToken: "replace-me",
71+ paths: {
72+ runsDir: "/tmp/runs",
73+ stateDir
74+ }
75+ },
76+ {
77+ autoStartLoops: false,
78+ now: () => 100
79+ }
80+ );
81+
82+ try {
83+ const snapshot = await runtime.start();
84+ const baseUrl = snapshot.controlApi.localApiBase;
85+
86+ assert.equal(typeof baseUrl, "string");
87+
88+ const firstStoppedSnapshot = await runtime.stop();
89+ assert.equal(firstStoppedSnapshot.runtime.started, false);
90+ await assertLocalApiListenerClosed(baseUrl);
91+
92+ const secondStoppedSnapshot = await runtime.stop();
93+ assert.equal(secondStoppedSnapshot.runtime.started, false);
94+ assert.equal(secondStoppedSnapshot.controlApi.localApiBase, baseUrl);
95+ await assertLocalApiListenerClosed(baseUrl);
96+ } finally {
97+ await runtime.stop();
98+ rmSync(stateDir, {
99+ force: true,
100+ recursive: true
101+ });
102+ }
103+});
104+
105 test("ConductorRuntime fixture closes the local API listener when a started test aborts", async () => {
106 let baseUrl = null;
107
+3,
-2
1@@ -1468,6 +1468,7 @@ function resolveRuntimeConfigFromSources(
2 const host = normalizeOptionalString(overrides.host ?? env.BAA_CONDUCTOR_HOST) ?? "localhost";
3 const nodeId =
4 normalizeOptionalString(overrides.nodeId ?? env.BAA_NODE_ID) ?? createDefaultNodeId(host, role);
5+ // Keep reading the legacy env name until conductor-daemon gets a dedicated upstream base setting.
6 const controlApiBase = normalizeOptionalString(overrides.controlApiBase ?? env.BAA_CONTROL_API_BASE);
7
8 if (!controlApiBase) {
9@@ -1765,7 +1766,7 @@ function getUsageText(): string {
10 " --node-id <id>",
11 " --host <host>",
12 " --role <primary|standby>",
13- " --control-api-base <url>",
14+ " --control-api-base <url> conductor upstream/public API base",
15 " --codexd-local-api <url>",
16 " --local-api <url>",
17 " --shared-token <token>",
18@@ -1789,7 +1790,7 @@ function getUsageText(): string {
19 " BAA_NODE_ID",
20 " BAA_CONDUCTOR_HOST",
21 " BAA_CONDUCTOR_ROLE",
22- " BAA_CONTROL_API_BASE",
23+ " BAA_CONTROL_API_BASE (legacy env name for conductor upstream/public API base)",
24 " BAA_CODEXD_LOCAL_API_BASE",
25 " BAA_CONDUCTOR_LOCAL_API",
26 " BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS",
+1,
-1
1@@ -155,7 +155,7 @@ function renderStatusApiCliHelp(): string {
2 "",
3 `Default listen address: http://${getDefaultStatusApiHost()}:${getDefaultStatusApiPort()}`,
4 "Default truth source: BAA_CONDUCTOR_LOCAL_API or http://100.71.210.78:4317",
5- "Compatibility override: BAA_CONTROL_API_BASE",
6+ "Legacy ad-hoc override: BAA_CONTROL_API_BASE",
7 "Routes:",
8 "- GET /healthz",
9 "- GET /describe",
+2,
-2
1@@ -48,7 +48,7 @@ test("status snapshots mark conductor-api as the upstream source", () => {
2 assert.equal(snapshot.source, "conductor-api");
3 });
4
5-test("status-api describe reports conductor local truth with compatibility note", async () => {
6+test("status-api describe reports conductor local truth with legacy compatibility note", async () => {
7 const handler = createStatusApiHandler(new StaticStatusSnapshotLoader(), {
8 truthSourceBaseUrl: "http://100.71.210.78:4317"
9 });
10@@ -65,6 +65,6 @@ test("status-api describe reports conductor local truth with compatibility note"
11 assert.deepEqual(payload.data.notes, [
12 "Status API is read-only.",
13 "Default truth source comes from BAA_CONDUCTOR_LOCAL_API.",
14- "Use BAA_CONTROL_API_BASE only when you need a compatibility override."
15+ "Use BAA_CONTROL_API_BASE only for legacy ad-hoc compatibility overrides."
16 ]);
17 });
+1,
-1
1@@ -192,7 +192,7 @@ function buildStatusApiDescribeData(options: StatusApiHandlerOptions): Record<st
2 notes: [
3 "Status API is read-only.",
4 "Default truth source comes from BAA_CONDUCTOR_LOCAL_API.",
5- "Use BAA_CONTROL_API_BASE only when you need a compatibility override."
6+ "Use BAA_CONTROL_API_BASE only for legacy ad-hoc compatibility overrides."
7 ]
8 };
9 }
1@@ -79,11 +79,12 @@ jq '.libuv' /Users/george/code/baa-conductor/report.*.<child_node_pid>.*.json
2 - 将 runtime snapshot 用例和 Firefox WS 用例都切到该 helper,避免任一断言失败时跳过 cleanup
3 - Firefox WS 用例额外把 `queue.stop()`、`socket.close(...)` 和等待 `close` 事件放进自身 `finally`,确保测试侧 WebSocket 也不会悬挂
4 - 新增一个最小回归用例,显式在 `runtime.start()` 之后抛错,并验证原端口已经返回 `ECONNREFUSED`,证明 listener 会在异常路径下被关闭
5+- 后续又补了 `ConductorRuntime.stop()` 自身专项回归,直接验证正常关闭会释放本地 listener、关闭活跃 Firefox WS client,且重复 `stop()` 不会残留端口资源
6
7 ## 剩余风险
8
9 - 其它需要本地 listener 的测试如果将来绕过 `withRuntimeFixture(...)`、重新把 cleanup 写回用例尾部,仍可能再次引入同类泄漏
10-- 该修复没有改动 `ConductorRuntime.stop()` 本身;如果未来运行时关闭逻辑内部出现阻塞,这个测试层收口只能保证“会调用 stop”,不能替代运行时关闭路径的专项测试
11+- 现有 runtime 层覆盖已验证 `ConductorRuntime.stop()` 的正常关闭和重复调用幂等,但还没有对 `server.close()`、底层 socket 关闭失败或阻塞做 fault injection
12
13 ## 严重程度
14
+2,
-1
1@@ -277,7 +277,8 @@ truth source:
2
3 - 当前默认应优先回源 `BAA_CONDUCTOR_LOCAL_API`,也就是当前 canonical local API `http://100.71.210.78:4317/v1/system/state`
4 - `https://conductor.makefile.so` 是同一套 conductor 主接口的公网入口;只有本地 `4317` 不可达时才需要显式改到公网
5-- `BAA_CONTROL_API_BASE` 只保留为兼容覆盖入口,供旧配置或遗留脚本继续工作
6+- `BAA_CONTROL_API_BASE` 只保留两个兼容点:`conductor-daemon` 仍读取这个历史变量名作为 upstream/public API base,`status-api` 只在手工或旧配置缺少 `BAA_CONDUCTOR_LOCAL_API` 时回退使用
7+- 默认 launchd 不再给 `status-api` 写入 `BAA_CONTROL_API_BASE`
8 - legacy `control-api.makefile.so` 不再是默认或 canonical truth source
9 - `status-api` 负责把该状态整理成 JSON 或 HTML
10
+1,
-1
1@@ -20,7 +20,7 @@
2 - canonical public host: `https://conductor.makefile.so`
3 - `status-api` `http://100.71.210.78:4318` 只作为本地只读观察面,默认回源 `BAA_CONDUCTOR_LOCAL_API`,当前 canonical 值是 `http://100.71.210.78:4317`
4 - `https://conductor.makefile.so` 是同一套 conductor 主接口的公网入口
5-- `BAA_CONTROL_API_BASE` 仍保留为兼容变量名;只在旧脚本或需要兼容覆盖时才使用,不再是 canonical truth source
6+- 默认 launchd 只把 `BAA_CONTROL_API_BASE` 写给 `conductor`;`status-api` 只保留代码层兼容回退,不再把它当 canonical truth source
7 - 推荐仓库路径:`/Users/george/code/baa-conductor`
8 - repo 内的 plist 只作为模板;真正加载的是脚本渲染出来的安装副本
9
+13,
-8
1@@ -9,7 +9,7 @@
2 - canonical public host: `https://conductor.makefile.so`
3 - local status view: `http://100.71.210.78:4318`
4 - `status-api` 默认真相源:`BAA_CONDUCTOR_LOCAL_API` -> `http://100.71.210.78:4317/v1/system/state`
5-- `BAA_CONTROL_API_BASE` 默认只保留为兼容覆盖,当前兼容值是 `https://conductor.makefile.so`
6+- `BAA_CONTROL_API_BASE` 只剩两个保留点:`conductor` 运行时仍用这个历史变量名解析 upstream/public API base,`status-api` 只在手工兼容场景下回退读取
7
8 ## 共享变量
9
10@@ -19,15 +19,21 @@
11 - `BAA_LOGS_DIR`
12 - `BAA_TMP_DIR`
13 - `BAA_STATE_DIR`
14-- `BAA_CONTROL_API_BASE`
15
16 说明:
17
18-- `BAA_CONTROL_API_BASE` 是兼容变量名,当前主要给 `status-api` 和遗留脚本使用
19-- `status-api` 现在会优先读取 `BAA_CONDUCTOR_LOCAL_API`,只有缺少该值时才回退到 `BAA_CONTROL_API_BASE`
20-- 它的默认兼容值已经收口到 `https://conductor.makefile.so`
21 - `codexd` 独立安装时不要求 `BAA_SHARED_TOKEN`
22
23+## 兼容变量
24+
25+- `BAA_CONTROL_API_BASE`
26+
27+说明:
28+
29+- 变量名本身是历史兼容名,但 `conductor-daemon` 当前仍用它解析 upstream/public API base;默认 launchd 只把它写给 `conductor`
30+- `status-api` 现在会优先读取 `BAA_CONDUCTOR_LOCAL_API`,只有手工启动或旧配置缺少该值时才回退到 `BAA_CONTROL_API_BASE`
31+- mini 默认兼容值仍是 `https://conductor.makefile.so`
32+
33 ## codexd 变量
34
35 `apps/codexd` 当前识别这些变量:
36@@ -83,7 +89,7 @@ BAA_STATUS_API_HOST=100.71.210.78
37 BAA_CONTROL_API_BASE=https://conductor.makefile.so
38 ```
39
40-最后一项只是兼容旧代码的变量名;`status-api` 的默认真相源仍然是 `BAA_CONDUCTOR_LOCAL_API=http://100.71.210.78:4317`。
41+最后一项现在只写给 `conductor` 安装副本;`status-api` 的默认真相源仍然是 `BAA_CONDUCTOR_LOCAL_API=http://100.71.210.78:4317`,默认 launchd 不再额外携带 `BAA_CONTROL_API_BASE`。
42
43 说明:
44
45@@ -112,11 +118,10 @@ Firefox WS 派生规则:
46 --service codexd \
47 --service status-api \
48 --shared-token-file /Users/george/.config/baa-conductor/shared-token.txt \
49- --control-api-base https://conductor.makefile.so \
50 --local-api-base http://100.71.210.78:4317 \
51 --local-api-allowed-hosts 100.71.210.78 \
52 --codexd-local-api-base http://127.0.0.1:4319 \
53 --status-api-host 100.71.210.78
54 ```
55
56-这里的 `--control-api-base` 只是在安装副本里写入兼容变量;`status-api` 默认仍然优先读取 `--local-api-base` 对应的 conductor 主接口。
57+默认 mini 安装不需要显式传 `--control-api-base`;只有 `conductor` 的 upstream/public API base 不是 `https://conductor.makefile.so` 时才需要覆盖。即使显式传入,脚本现在也只会把它写给 `conductor` 安装副本;`status-api` 默认仍然优先读取 `--local-api-base` 对应的 conductor 主接口。
+2,
-3
1@@ -46,7 +46,7 @@
2
3 - `codexd` 独立安装时不需要共享 token
4 - `status-api` 默认真相源是 `BAA_CONDUCTOR_LOCAL_API`
5-- `--control-api-base` 仍然保留,只是为了写入兼容变量 `BAA_CONTROL_API_BASE`
6+- `--control-api-base` 仍然保留,但只影响 `conductor` 安装副本;默认值就是 `https://conductor.makefile.so`
7 - `--codexd-local-api-base` 会同时写给 `codexd` 和 `conductor`
8 - `codexd` 正式运行面只写入 `app-server` 会话链路所需默认值
9 - `codexd` 正式服务面只保留 `/healthz`、`/v1/codexd/status`、`/v1/codexd/sessions`、`/v1/codexd/turn`、`/v1/codexd/events`
10@@ -107,14 +107,13 @@
11 --service status-api \
12 --install-dir /Users/george/Library/LaunchAgents \
13 --shared-token-file /Users/george/.config/baa-conductor/shared-token.txt \
14- --control-api-base https://conductor.makefile.so \
15 --local-api-base http://100.71.210.78:4317 \
16 --local-api-allowed-hosts 100.71.210.78 \
17 --codexd-local-api-base http://127.0.0.1:4319 \
18 --status-api-host 100.71.210.78
19 ```
20
21-这里保留 `--control-api-base https://conductor.makefile.so` 只是为了把兼容变量写进安装副本;`status-api` 实际默认仍然优先读 `--local-api-base http://100.71.210.78:4317`。
22+默认 mini 安装不需要显式传 `--control-api-base`;只有 `conductor` 的 upstream/public API base 不是 `https://conductor.makefile.so` 时才需要覆盖。即使显式传入,脚本现在也只会把它写给 `conductor` 安装副本;`status-api` 实际默认仍然优先读 `--local-api-base http://100.71.210.78:4317`。
23
24 单独安装 `codexd`:
25
+2,
-2
1@@ -20,7 +20,6 @@ npx --yes pnpm -r build
2 --service status-api \
3 --install-dir /Users/george/Library/LaunchAgents \
4 --shared-token-file /Users/george/.config/baa-conductor/shared-token.txt \
5- --control-api-base https://conductor.makefile.so \
6 --local-api-base http://100.71.210.78:4317 \
7 --local-api-allowed-hosts 100.71.210.78 \
8 --codexd-local-api-base http://127.0.0.1:4319 \
9@@ -29,8 +28,9 @@ npx --yes pnpm -r build
10
11 说明:
12
13-- `--control-api-base` 仍是当前静态检查参数,但只用于校验兼容变量 `BAA_CONTROL_API_BASE`
14+- 默认 mini 静态检查不需要显式传 `--control-api-base`;只有 `conductor` 的 upstream/public API base 不是 `https://conductor.makefile.so` 时才需要覆盖
15 - `status-api` 的有效默认真相源仍然是 `--local-api-base http://100.71.210.78:4317`
16+- `check-launchd.sh` 现在只校验 `conductor` 安装副本里的 `BAA_CONTROL_API_BASE`;其他服务要求该变量不存在
17 - `check-launchd.sh` 现在也会校验 `conductor` 安装副本里的 `BAA_CODEXD_LOCAL_API_BASE`
18 - `check-launchd.sh` 现在会校验 `codexd` 的监听地址、事件流路径、日志目录、状态目录和 `app-server` child 配置
19 - 这些静态检查不包含 run/exec 路线
1@@ -6,6 +6,8 @@
2 Use scripts/runtime/install-launchd.sh to render the actual install copy.
3 Adjust BAA_SHARED_TOKEN, BAA_CODEXD_LOCAL_API_BASE, and the listen-related
4 variables before loading if the mini node uses a non-default address.
5+ BAA_CONTROL_API_BASE stays here because conductor-daemon still reads that
6+ legacy env name for its upstream/public API base.
7 If this file is installed under /Library/LaunchDaemons, add UserName and keep
8 every path absolute; launchd will not read shell rc files for you.
9 -->
1@@ -3,7 +3,8 @@
2 <!--
3 Local read-only status API for the mini runtime.
4 Default truth comes from BAA_CONDUCTOR_LOCAL_API /v1/system/state.
5- BAA_CONTROL_API_BASE is kept only as a compatibility override.
6+ Ad-hoc legacy BAA_CONTROL_API_BASE overrides stay in code only; launchd no
7+ longer writes that variable here.
8 Keep the same runtime paths as conductor and worker-runner so that service
9 logs and temporary files stay under one repo-owned runtime root.
10 Use scripts/runtime/install-launchd.sh to render the actual install copy.
11@@ -30,8 +31,6 @@
12 <string>mini</string>
13 <key>BAA_CONDUCTOR_ROLE</key>
14 <string>primary</string>
15- <key>BAA_CONTROL_API_BASE</key>
16- <string>https://conductor.makefile.so</string>
17 <key>BAA_CONDUCTOR_LOCAL_API</key>
18 <string>http://100.71.210.78:4317</string>
19 <key>BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS</key>
1@@ -4,6 +4,8 @@
2 Source template kept in the repo.
3 Defaults target the mini node and share the same runtime tree as conductor.
4 Use scripts/runtime/install-launchd.sh to render the actual install copy.
5+ worker-runner follows the canonical local-api variables and no longer carries
6+ BAA_CONTROL_API_BASE in launchd.
7 Adjust BAA_SHARED_TOKEN and runtime paths before copying the file into the
8 launchd install path.
9 -->
10@@ -29,8 +31,6 @@
11 <string>mini</string>
12 <key>BAA_CONDUCTOR_ROLE</key>
13 <string>primary</string>
14- <key>BAA_CONTROL_API_BASE</key>
15- <string>https://conductor.makefile.so</string>
16 <key>BAA_CONDUCTOR_LOCAL_API</key>
17 <string>http://100.71.210.78:4317</string>
18 <key>BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS</key>
+20,
-4
1@@ -20,7 +20,7 @@ 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- --control-api-base URL Expected compatibility BAA_CONTROL_API_BASE.
6+ --control-api-base URL Expected conductor BAA_CONTROL_API_BASE.
7 --local-api-base URL Expected BAA_CONDUCTOR_LOCAL_API.
8 --local-api-allowed-hosts CSV
9 Expected BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS.
10@@ -40,8 +40,9 @@ Options:
11
12 Notes:
13 If no service is specified, conductor + codexd + status-api are checked.
14- status-api defaults to BAA_CONDUCTOR_LOCAL_API /v1/system/state; this flag
15- only validates the legacy compatibility override.
16+ status-api defaults to BAA_CONDUCTOR_LOCAL_API /v1/system/state; launchd no
17+ longer writes BAA_CONTROL_API_BASE for it.
18+ --control-api-base only applies when conductor is part of the checked set.
19 codexd static checks only validate app-server launchd wiring; they do not
20 require /v1/codexd/runs* or codex exec as formal runtime capabilities.
21 EOF
22@@ -232,6 +233,16 @@ check_string_equals() {
23 fi
24 }
25
26+check_key_missing() {
27+ local name="$1"
28+ local plist_path="$2"
29+ local key="$3"
30+
31+ if plist_has_key "$plist_path" "$key"; then
32+ die "${name} should be absent"
33+ fi
34+}
35+
36 check_installed_plist() {
37 local service="$1"
38 local plist_path="$2"
39@@ -247,7 +258,6 @@ check_installed_plist() {
40 check_string_equals "${service}:HOME" "$(plist_print_value "$plist_path" ":EnvironmentVariables:HOME")" "$home_dir"
41 check_string_equals "${service}:BAA_CONDUCTOR_HOST" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CONDUCTOR_HOST")" "$conductor_host"
42 check_string_equals "${service}:BAA_CONDUCTOR_ROLE" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CONDUCTOR_ROLE")" "$conductor_role"
43- check_string_equals "${service}:BAA_CONTROL_API_BASE" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE")" "$control_api_base"
44 check_string_equals "${service}:BAA_CONDUCTOR_LOCAL_API" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CONDUCTOR_LOCAL_API")" "$local_api_base"
45 check_string_equals "${service}:BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS")" "$local_api_allowed_hosts"
46 check_string_equals "${service}:BAA_RUNS_DIR" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_RUNS_DIR")" "$runs_dir"
47@@ -260,6 +270,12 @@ check_installed_plist() {
48 check_string_equals "${service}:stderr" "$(plist_print_value "$plist_path" ":StandardErrorPath")" "$stderr_path"
49 check_string_equals "${service}:entry" "$(plist_print_value "$plist_path" ":ProgramArguments:2")" "$dist_entry"
50
51+ if service_uses_control_api_base "$service"; then
52+ check_string_equals "${service}:BAA_CONTROL_API_BASE" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE")" "$control_api_base"
53+ else
54+ check_key_missing "${service}:BAA_CONTROL_API_BASE" "$plist_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE"
55+ fi
56+
57 if service_requires_shared_token "$service"; then
58 local actual_shared_token
59
+11,
-4
1@@ -20,7 +20,7 @@ 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- --control-api-base URL Expected compatibility BAA_CONTROL_API_BASE in installed copies.
6+ --control-api-base URL Expected conductor BAA_CONTROL_API_BASE in installed copies.
7 --local-api-base URL Conductor local API base URL. Defaults to 127.0.0.1:4317.
8 --local-api-allowed-hosts CSV
9 Expected BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS in installed copies.
10@@ -41,8 +41,8 @@ Options:
11 Notes:
12 The default runtime check set is conductor + codexd + status-api. Use
13 --service to narrow the scope or --all-services to include worker-runner.
14- status-api defaults to BAA_CONDUCTOR_LOCAL_API /v1/system/state; this flag
15- only checks the installed compatibility override.
16+ status-api defaults to BAA_CONDUCTOR_LOCAL_API /v1/system/state; launchd no
17+ longer writes BAA_CONTROL_API_BASE for it.
18 conductor HTTP probes include /v1/codex to ensure proxy wiring to codexd.
19 For codexd, the HTTP probes only cover /healthz and /v1/codexd/status.
20 /v1/codexd/runs* and codex exec are not part of node verification.
21@@ -337,6 +337,7 @@ check_loaded_services() {
22
23 run_static_checks() {
24 local static_args=()
25+ local service
26
27 static_args+=(
28 --node "$node"
29@@ -344,7 +345,6 @@ run_static_checks() {
30 --repo-dir "$repo_dir"
31 --home-dir "$home_dir"
32 --install-dir "$install_dir"
33- --control-api-base "$control_api_base"
34 --local-api-base "$local_api_base"
35 --local-api-allowed-hosts "$local_api_allowed_hosts"
36 --codexd-local-api-base "$codexd_api_base"
37@@ -352,6 +352,13 @@ run_static_checks() {
38 --username "$username"
39 )
40
41+ for service in "${services[@]}"; do
42+ if service_uses_control_api_base "$service"; then
43+ static_args+=(--control-api-base "$control_api_base")
44+ break
45+ fi
46+ done
47+
48 for service in "${services[@]}"; do
49 static_args+=(--service "$service")
50 done
+18,
-0
1@@ -102,6 +102,17 @@ service_requires_shared_token() {
2 esac
3 }
4
5+service_uses_control_api_base() {
6+ case "$1" in
7+ conductor)
8+ return 0
9+ ;;
10+ *)
11+ return 1
12+ ;;
13+ esac
14+}
15+
16 service_label() {
17 case "$1" in
18 conductor)
19@@ -311,6 +322,13 @@ plist_print_value() {
20 /usr/libexec/PlistBuddy -c "Print ${key}" "$plist_path"
21 }
22
23+plist_has_key() {
24+ local plist_path="$1"
25+ local key="$2"
26+
27+ /usr/libexec/PlistBuddy -c "Print ${key}" "$plist_path" >/dev/null 2>&1
28+}
29+
30 print_shell_command() {
31 printf '+'
32
+12,
-6
1@@ -20,7 +20,7 @@ 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- --control-api-base URL Override compatibility BAA_CONTROL_API_BASE.
6+ --control-api-base URL Override conductor BAA_CONTROL_API_BASE.
7 --local-api-base URL Override BAA_CONDUCTOR_LOCAL_API.
8 --local-api-allowed-hosts CSV
9 Override BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS.
10@@ -39,8 +39,10 @@ Notes:
11 If no service is specified, conductor + codexd + status-api are installed.
12 Use --service codexd to render codexd independently; it does not require a
13 shared token.
14- status-api defaults to BAA_CONDUCTOR_LOCAL_API /v1/system/state; this flag
15- only writes the legacy compatibility override.
16+ status-api defaults to BAA_CONDUCTOR_LOCAL_API /v1/system/state; launchd no
17+ longer writes BAA_CONTROL_API_BASE for it.
18+ --control-api-base only affects conductor install copies, and the default is
19+ already https://conductor.makefile.so.
20 codexd launchd wiring stays on app-server mode and does not expose
21 /v1/codexd/runs* or codex exec as a formal service contract.
22 EOF
23@@ -240,9 +242,6 @@ for service in "${services[@]}"; do
24 plist_set_string "$install_path" ":EnvironmentVariables:LC_ALL" "$BAA_RUNTIME_DEFAULT_LOCALE"
25 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CONDUCTOR_HOST" "$conductor_host"
26 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CONDUCTOR_ROLE" "$conductor_role"
27- # Keep the legacy env name for compatibility; status-api now defaults to
28- # BAA_CONDUCTOR_LOCAL_API when both are present.
29- plist_set_string "$install_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE" "$control_api_base"
30 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CONDUCTOR_LOCAL_API" "$local_api_base"
31 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS" "$local_api_allowed_hosts"
32 plist_set_string "$install_path" ":EnvironmentVariables:BAA_RUNS_DIR" "$runs_dir"
33@@ -261,6 +260,13 @@ for service in "${services[@]}"; do
34 plist_delete_key "$install_path" ":EnvironmentVariables:BAA_SHARED_TOKEN"
35 fi
36
37+ if service_uses_control_api_base "$service"; then
38+ # conductor-daemon still reads the legacy env name for its upstream/public API base.
39+ plist_set_string "$install_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE" "$control_api_base"
40+ else
41+ plist_delete_key "$install_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE"
42+ fi
43+
44 if [[ "$service" == "conductor" ]]; then
45 plist_set_string "$install_path" ":ProgramArguments:4" "$conductor_host"
46 plist_set_string "$install_path" ":ProgramArguments:6" "$conductor_role"
+0,
-2
1@@ -184,7 +184,6 @@ run_or_print 0 "${SCRIPT_DIR}/install-launchd.sh" \
2 --service status-api \
3 --install-dir "$install_dir" \
4 --shared-token-file "$shared_token_file" \
5- --control-api-base "https://conductor.makefile.so" \
6 --local-api-base "http://100.71.210.78:4317" \
7 --local-api-allowed-hosts "100.71.210.78" \
8 --codexd-local-api-base "$codexd_api_base" \
9@@ -211,7 +210,6 @@ if [[ "$skip_check" != "1" ]]; then
10 --service status-api \
11 --install-dir "$install_dir" \
12 --shared-token-file "$shared_token_file" \
13- --control-api-base "https://conductor.makefile.so" \
14 --local-api-base "http://100.71.210.78:4317" \
15 --local-api-allowed-hosts "100.71.210.78" \
16 --codexd-local-api-base "$codexd_api_base" \
+127,
-0
1@@ -0,0 +1,127 @@
2+# Task T-S005:收口 `BAA_CONTROL_API_BASE` 兼容入口
3+
4+## 直接给对话的提示词
5+
6+读 `/Users/george/code/baa-conductor/tasks/T-S005.md` 任务文档,完成开发任务。
7+
8+如需补背景,再读:
9+
10+- `/Users/george/code/baa-conductor/apps/status-api/src/data-source.ts`
11+- `/Users/george/code/baa-conductor/apps/status-api/src/service.ts`
12+- `/Users/george/code/baa-conductor/apps/conductor-daemon/src/index.ts`
13+- `/Users/george/code/baa-conductor/scripts/runtime/install-launchd.sh`
14+- `/Users/george/code/baa-conductor/scripts/runtime/check-launchd.sh`
15+- `/Users/george/code/baa-conductor/scripts/runtime/check-node.sh`
16+- `/Users/george/code/baa-conductor/ops/launchd/so.makefile.baa-status-api.plist`
17+- `/Users/george/code/baa-conductor/docs/runtime/environment.md`
18+- `/Users/george/code/baa-conductor/docs/runtime/launchd.md`
19+- `/Users/george/code/baa-conductor/docs/api/README.md`
20+
21+## 当前基线
22+
23+- 仓库:`/Users/george/code/baa-conductor`
24+- 分支:`main`
25+- 提交:`1078d2b`
26+- 开工要求:不要从其他任务分支切出;如需新分支,从当前 `main` 新切
27+
28+## 建议分支名
29+
30+- `chore/retire-control-api-base-compat`
31+
32+## 目标
33+
34+把 `BAA_CONTROL_API_BASE` 从“残留但仍到处可见的兼容入口”继续收口到最小范围,并明确最终保留点。
35+
36+## 背景
37+
38+- `status-api` 已经默认切到 `BAA_CONDUCTOR_LOCAL_API` / `4317`。
39+- 当前剩余风险之一是 `BAA_CONTROL_API_BASE` 仍保留在代码、launchd、检查脚本和文档里,容易继续被误当成 canonical truth source。
40+- 现在需要决定它到底保留在哪些点,哪些地方可以彻底去掉。
41+
42+## 涉及仓库
43+
44+- `/Users/george/code/baa-conductor`
45+
46+## 范围
47+
48+- 盘点并收口 `BAA_CONTROL_API_BASE` 的剩余使用点
49+- 去掉不再必要的兼容入口或说明
50+- 把仍需保留的入口明确写成“兼容用途”
51+
52+## 路径约束
53+
54+- 优先处理 `status-api`、runtime 脚本、launchd 模板和相关文档。
55+- 如果 `conductor-daemon` 内部仍需要兼容入口,必须在结果里明确解释原因。
56+
57+## 推荐实现边界
58+
59+建议优先做:
60+
61+- 区分“运行时仍需要”与“只是历史遗留还没删”的 `BAA_CONTROL_API_BASE`
62+- 让脚本、plist 和文档口径完全一致
63+- 能删的直接删,不能删的缩到最小并写明兼容性质
64+
65+## 允许修改的目录
66+
67+- `/Users/george/code/baa-conductor/apps/status-api/`
68+- `/Users/george/code/baa-conductor/apps/conductor-daemon/src/index.ts`
69+- `/Users/george/code/baa-conductor/apps/conductor-daemon/src/index.test.js`
70+- `/Users/george/code/baa-conductor/scripts/runtime/`
71+- `/Users/george/code/baa-conductor/ops/launchd/`
72+- `/Users/george/code/baa-conductor/docs/runtime/`
73+- `/Users/george/code/baa-conductor/docs/api/README.md`
74+
75+## 尽量不要修改
76+
77+- `/Users/george/code/baa-conductor/apps/codexd/`
78+- `/Users/george/code/baa-conductor/tests/control-api/`
79+- `/Users/george/code/baa-conductor/ops/cloudflare/`
80+- `/Users/george/code/baa-conductor/bugs/`
81+
82+## 必须完成
83+
84+### 1. 盘点并收口兼容入口
85+
86+- 明确现在还剩哪些 `BAA_CONTROL_API_BASE` 使用点
87+- 去掉不再必要的使用点
88+
89+### 2. 统一运行时口径
90+
91+- launchd 模板、安装脚本、检查脚本和运行文档必须一致
92+- 不再把 `BAA_CONTROL_API_BASE` 写成默认真相源
93+
94+### 3. 回写最终结论
95+
96+- 在结果里明确说明:
97+ - 哪些使用点被删除
98+ - 哪些使用点被保留
99+ - 为什么保留
100+
101+## 需要特别注意
102+
103+- 不要顺手清历史文档命名,那是另一张任务卡
104+- 不要把本任务扩展成 `status-api` 或 `conductor-daemon` 的功能重构
105+- 与其它并行任务共享 `docs/runtime/**` 风险较高,避免和文档清理类任务同时落地
106+
107+## 验收标准
108+
109+- `BAA_CONTROL_API_BASE` 的剩余使用点明显减少或被明确限定
110+- 所有仍保留的文案都明确写成兼容用途
111+- 运行脚本、plist 和文档口径一致
112+- `git diff --check` 通过
113+
114+## 推荐验证命令
115+
116+- `npx --yes pnpm -C /Users/george/code/baa-conductor -F @baa-conductor/status-api build`
117+- `npx --yes pnpm -C /Users/george/code/baa-conductor -F @baa-conductor/conductor-daemon test`
118+- `git -C /Users/george/code/baa-conductor diff --check`
119+
120+## 交付要求
121+
122+完成后请说明:
123+
124+- 修改了哪些文件
125+- 删除了哪些 `BAA_CONTROL_API_BASE` 使用点
126+- 还保留了哪些兼容点
127+- 跑了哪些测试
128+- 为什么这些残留还需要存在
+114,
-0
1@@ -0,0 +1,114 @@
2+# Task T-S007:补 `ConductorRuntime.stop()` 关闭路径专项测试
3+
4+## 直接给对话的提示词
5+
6+读 `/Users/george/code/baa-conductor/tasks/T-S007.md` 任务文档,完成开发任务。
7+
8+如需补背景,再读:
9+
10+- `/Users/george/code/baa-conductor/apps/conductor-daemon/src/index.ts`
11+- `/Users/george/code/baa-conductor/apps/conductor-daemon/src/index.test.js`
12+- `/Users/george/code/baa-conductor/bugs/BUG-009-conductor-daemon-index-test-leaks-local-listener.md`
13+
14+## 当前基线
15+
16+- 仓库:`/Users/george/code/baa-conductor`
17+- 分支:`main`
18+- 提交:`1078d2b`
19+- 开工要求:不要从其他任务分支切出;如需新分支,从当前 `main` 新切
20+
21+## 建议分支名
22+
23+- `test/runtime-stop-close-path`
24+
25+## 目标
26+
27+给 `ConductorRuntime.stop()` 自身补专项测试,覆盖关闭路径的关键行为,而不是只依赖测试层的 `withRuntimeFixture(...)` 收口。
28+
29+## 背景
30+
31+- `BUG-009` 已经在测试层通过 `withRuntimeFixture(...)` 收口 cleanup。
32+- 但当前剩余风险仍然存在:如果未来 `ConductorRuntime.stop()` 内部关闭逻辑阻塞或异常,现有测试层 helper 只能保证“会调用 stop”,不能证明 stop 自身正确。
33+- 现在需要补 runtime 层的专项回归,缩小这个盲区。
34+
35+## 涉及仓库
36+
37+- `/Users/george/code/baa-conductor`
38+
39+## 范围
40+
41+- 给 `ConductorRuntime.stop()` 增加关闭路径测试
42+- 必要时做最小修复,但不要扩散成大重构
43+- 回写 `BUG-009` 剩余风险说明
44+
45+## 路径约束
46+
47+- 优先补测试;只有测出真实问题时才做最小代码修复。
48+- 不改 codexd、status-api 或 browser bridge 逻辑。
49+
50+## 推荐实现边界
51+
52+建议优先覆盖:
53+
54+- 启动后正常 stop,listener 真的释放
55+- 异常路径或重复 stop,不会残留资源
56+- 关闭后端口/HTTP 入口确实不可用
57+
58+## 允许修改的目录
59+
60+- `/Users/george/code/baa-conductor/apps/conductor-daemon/src/index.ts`
61+- `/Users/george/code/baa-conductor/apps/conductor-daemon/src/index.test.js`
62+- `/Users/george/code/baa-conductor/bugs/BUG-009-conductor-daemon-index-test-leaks-local-listener.md`
63+
64+## 尽量不要修改
65+
66+- `/Users/george/code/baa-conductor/apps/codexd/`
67+- `/Users/george/code/baa-conductor/apps/status-api/`
68+- `/Users/george/code/baa-conductor/docs/runtime/`
69+- `/Users/george/code/baa-conductor/tests/control-api/`
70+
71+## 必须完成
72+
73+### 1. 补 stop 专项测试
74+
75+- 明确覆盖 `ConductorRuntime.stop()` 的正常关闭路径
76+- 至少有一条断言能证明 stop 后 listener 已释放
77+
78+### 2. 覆盖边界情况
79+
80+- 评估并测试重复 stop 或关闭时异常路径
81+- 不要求做复杂 fault injection,但要覆盖最关键的资源释放边界
82+
83+### 3. 回写剩余风险
84+
85+- 如果 stop 自身没有 bug,要在结果里明确说明现有覆盖边界
86+- 如果发现 runtime 层真实缺陷,做最小修复并写清根因
87+
88+## 需要特别注意
89+
90+- 这不是再修一次 `withRuntimeFixture(...)`
91+- 不要把任务扩展成整个 runtime 生命周期重构
92+- 与其它并行任务不要共享 `docs/runtime/**`
93+
94+## 验收标准
95+
96+- `ConductorRuntime.stop()` 有明确的专项回归覆盖
97+- stop 后 listener / 端口不可继续访问
98+- 如有修复,范围控制在 runtime 关闭路径本身
99+- `git diff --check` 通过
100+
101+## 推荐验证命令
102+
103+- `npx --yes pnpm -C /Users/george/code/baa-conductor -F @baa-conductor/conductor-daemon test`
104+- `node --test /Users/george/code/baa-conductor/apps/conductor-daemon/src/index.test.js`
105+- `git -C /Users/george/code/baa-conductor diff --check`
106+
107+## 交付要求
108+
109+完成后请说明:
110+
111+- 修改了哪些文件
112+- 新增了哪些 `stop()` 关闭路径测试
113+- 是否发现 runtime 层真实缺陷
114+- 跑了哪些测试
115+- 剩余哪些关闭路径风险仍未覆盖