baa-conductor

git clone 

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
M apps/conductor-daemon/src/index.test.js
+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 
M apps/conductor-daemon/src/index.ts
+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",
M apps/status-api/src/cli.ts
+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",
M apps/status-api/src/index.test.js
+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 });
M apps/status-api/src/service.ts
+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 }
M bugs/BUG-009-conductor-daemon-index-test-leaks-local-listener.md
+2, -1
 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 
M docs/api/README.md
+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 
M docs/runtime/README.md
+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 
M docs/runtime/environment.md
+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 主接口。
M docs/runtime/launchd.md
+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 
M docs/runtime/node-verification.md
+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 路线
M ops/launchd/so.makefile.baa-conductor.plist
+2, -0
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 -->
M ops/launchd/so.makefile.baa-status-api.plist
+2, -3
 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>
M ops/launchd/so.makefile.baa-worker-runner.plist
+2, -2
 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>
M scripts/runtime/check-launchd.sh
+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 
M scripts/runtime/check-node.sh
+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
M scripts/runtime/common.sh
+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 
M scripts/runtime/install-launchd.sh
+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"
M scripts/runtime/install-mini.sh
+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" \
A tasks/T-S005.md
+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+- 为什么这些残留还需要存在
A tasks/T-S007.md
+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+- 剩余哪些关闭路径风险仍未覆盖