- commit
- 097621a
- parent
- d1c9090
- author
- im_wower
- date
- 2026-03-26 00:12:46 +0800 CST
merge: land T-S009 and T-S010
22 files changed,
+812,
-94
+29,
-21
1@@ -20,23 +20,26 @@
2 - Tailscale IP: `100.71.210.78`
3 - canonical local base: `http://100.71.210.78:4317`
4 - 当前 conductor 已暴露:
5+ - `/describe`
6+ - `/health`
7+ - `/version`
8 - `/healthz`
9 - `/readyz`
10 - `/rolez`
11 - `/v1/runtime`
12-
13-后续要继续并到同一接口面的能力:
14-
15-- `/describe`
16-- `/version`
17-- `/health`
18-- `/v1/capabilities`
19-- `/v1/system/state`
20-- `/v1/controllers`
21-- `/v1/tasks`
22-- `/v1/runs`
23-- `pause / resume / drain`
24-- 来自 `baa-shell` 语义的小型本机能力
25+ - `/v1/capabilities`
26+ - `/v1/system/state`
27+ - `/v1/controllers`
28+ - `/v1/tasks`
29+ - `/v1/runs`
30+ - `pause / resume / drain`
31+ - `/v1/exec`
32+ - `/v1/files/read`
33+ - `/v1/files/write`
34+ - `/v1/browser/*`
35+ - `/v1/codex*`(配置了 `codexd` 时)
36+
37+后续如还要扩展,默认继续往这同一个接口面收口,不再回到 legacy control plane。
38
39 ### 公网入口
40
41@@ -80,10 +83,11 @@
42 ### `apps/status-api`
43
44 - `mini` 本地状态读取面
45-- 当前仍依赖 `BAA_CONTROL_API_BASE`
46+- 默认读取 `BAA_CONDUCTOR_LOCAL_API` 对应的 conductor `/v1/system/state`
47+- `BAA_CONTROL_API_BASE` 只剩手工兼容回退语义
48 - 定位是迁移期本地只读观察服务,不是主控制面
49
50-### `codexd`(设计中,尚未完整实现)
51+### `codexd`(已实现独立 daemon,仍在继续收口)
52
53 - `mini` 本地常驻的独立 Codex 代理/执行组件
54 - 不是 TUI,不要求人工盯界面
55@@ -109,8 +113,10 @@
56 当前状态:
57
58 - 仓库里已经有 `codex` step kind、planner 和 step template 抽象
59-- `apps/codexd` 已有 daemon scaffold,但还没有完成 conductor 集成
60-- 所以 `codexd` 现在仍是半成品,不是已落地组件
61+- `apps/codexd` 已有独立 daemon、本地 HTTP 面、event stream、session / turn 状态与最近事件缓存
62+- `conductor-daemon` 已能通过本地 `/v1/codex*` 代理到 `codexd`
63+- transport 尾包冲刷和“未收到合法 completed 就提前断流”的 failure classification 已落地
64+- 但 `codexd` 仍不是已上线、已承诺稳定性的组件
65 - 正式产品口径只保留 session / turn / status,不把 `runs` / `exec` 当作 Codex 能力面
66 - 后续实现时,不再把 `exec` 当作主双工方案,也不再把它做成正式接口
67 - 原因:`codex exec` 卡顿明显且容易假死,不符合 `codexd` 的常驻双工目标
68@@ -162,8 +168,9 @@
69
70 说明:
71
72-- 当前脚本仍保留 `BAA_CONTROL_API_BASE` 参数以兼容 `status-api`
73-- 这不是 canonical 主路径,只是删旧前的过渡残留
74+- 当前脚本仍保留 legacy 参数名 `--control-api-base` / `BAA_CONTROL_API_BASE`
75+- 这套名字现在只影响 `conductor` 自己的 upstream/public API base,不再用于 `status-api`
76+- 这不是 canonical 主路径,只是删旧前尚未改名的过渡残留
77
78 ## 7. Nginx 与 DNS
79
80@@ -211,10 +218,11 @@
81
82 ## 11. 当前已知残留
83
84-- `status-api` 仍通过 `BAA_CONTROL_API_BASE` 读取 legacy truth source
85+- `conductor-daemon` 和 runtime 脚本仍使用 legacy 名字 `BAA_CONTROL_API_BASE` / `--control-api-base`
86+- `status-api` 仍是独立的本地只读观察面,是否并入 `conductor-daemon` 尚未定案
87 - `conductor.makefile.so` 目前只回源 `conductor-daemon` 已有路由,尚未承接完整业务 API
88 - 线上和历史文档里仍可能残留 `control-api.makefile.so`、Cloudflare / D1 相关资产或表述
89-- `codexd` 尚未实现;当前仍没有“Codex 常驻代理 / Codex daemon”能力
90+- 如果后续再次出现 app-server 未发出合法 `turn/completed` 就提前断流,应视为新的 child / transport 故障,而不是 reopen 已修复的 BUG-008 / BUG-010
91
92 ## 12. 历史回溯
93
+5,
-4
1@@ -72,7 +72,6 @@ scripts/
2 runtime/
3 plans/
4 STATUS_SUMMARY.md
5- WORKFLOW.md
6 tasks/
7 TASK_OVERVIEW.md
8 bugs/
9@@ -121,12 +120,14 @@ legacy 兼容说明:
10
11 - 保持 `mini` launchd、自启动和本地探针稳定
12 - 保持 `conductor.makefile.so -> 100.71.210.78:4317` 的链路稳定
13-- 推进 local-api cutover,清掉 legacy control plane 依赖
14+- 继续收掉剩余 legacy 配置名和兼容入口
15+- 给仓库根补齐真实可跑的验证入口
16
17 ## 当前已知 gap
18
19-- `status-api`、launchd 模板和部分运行脚本仍引用 `BAA_CONTROL_API_BASE`
20-- `status-api` 还需要完全切到 `conductor-daemon local-api /v1/system/state`
21+- `conductor-daemon`、launchd 安装/检查脚本仍用 legacy 名字 `BAA_CONTROL_API_BASE` / `--control-api-base` 表示 conductor upstream/public API base
22+- 仓库根 `pnpm lint` / `pnpm test` 还是占位脚本,缺少统一质量入口
23+- `status-api` 已经默认读取 `conductor-daemon` 的 `/v1/system/state`,但仍是单独的本地只读观察服务;是否继续保留还是并入主接口,仍待后续任务决定
24
25 ## 本机能力层
26
+50,
-2
1@@ -1113,7 +1113,7 @@ test("parseConductorCliRequest merges launchd env defaults with CLI overrides",
2 BAA_NODE_ID: "mini-main",
3 BAA_CONDUCTOR_HOST: "mini",
4 BAA_CONDUCTOR_ROLE: "primary",
5- BAA_CONTROL_API_BASE: "https://control.example.test/",
6+ BAA_CONDUCTOR_PUBLIC_API_BASE: "https://public.example.test/",
7 BAA_CODEXD_LOCAL_API_BASE: "http://127.0.0.1:4323/",
8 BAA_CONDUCTOR_LOCAL_API: "http://127.0.0.1:4317/",
9 BAA_SHARED_TOKEN: "replace-me",
10@@ -1129,12 +1129,60 @@ test("parseConductorCliRequest merges launchd env defaults with CLI overrides",
11 assert.equal(request.runOnce, true);
12 assert.equal(request.config.role, "standby");
13 assert.equal(request.config.nodeId, "mini-main");
14- assert.equal(request.config.controlApiBase, "https://control.example.test");
15+ assert.equal(request.config.publicApiBase, "https://public.example.test");
16+ assert.equal(request.config.controlApiBase, "https://public.example.test");
17 assert.equal(request.config.codexdLocalApiBase, "http://127.0.0.1:4323");
18 assert.equal(request.config.localApiBase, "http://127.0.0.1:4317");
19 assert.equal(request.config.paths.runsDir, "/tmp/runs");
20 });
21
22+test("parseConductorCliRequest prefers the canonical public API env name over the legacy alias", () => {
23+ const request = parseConductorCliRequest(["config"], {
24+ BAA_NODE_ID: "mini-main",
25+ BAA_CONDUCTOR_HOST: "mini",
26+ BAA_CONDUCTOR_ROLE: "primary",
27+ BAA_CONDUCTOR_PUBLIC_API_BASE: "https://public.example.test/",
28+ BAA_CONTROL_API_BASE: "https://legacy.example.test/",
29+ BAA_SHARED_TOKEN: "replace-me"
30+ });
31+
32+ assert.equal(request.action, "config");
33+
34+ if (request.action !== "config") {
35+ throw new Error("expected config action");
36+ }
37+
38+ assert.equal(request.config.publicApiBase, "https://public.example.test");
39+ assert.equal(request.config.controlApiBase, "https://public.example.test");
40+});
41+
42+test("parseConductorCliRequest prefers --public-api-base over --control-api-base", () => {
43+ const request = parseConductorCliRequest(
44+ [
45+ "config",
46+ "--control-api-base",
47+ "https://legacy-cli.example.test/",
48+ "--public-api-base",
49+ "https://public-cli.example.test/"
50+ ],
51+ {
52+ BAA_NODE_ID: "mini-main",
53+ BAA_CONDUCTOR_HOST: "mini",
54+ BAA_CONDUCTOR_ROLE: "primary",
55+ BAA_SHARED_TOKEN: "replace-me"
56+ }
57+ );
58+
59+ assert.equal(request.action, "config");
60+
61+ if (request.action !== "config") {
62+ throw new Error("expected config action");
63+ }
64+
65+ assert.equal(request.config.publicApiBase, "https://public-cli.example.test");
66+ assert.equal(request.config.controlApiBase, "https://public-cli.example.test");
67+});
68+
69 test("parseConductorCliRequest allows an explicitly listed Tailscale local API host", () => {
70 const request = parseConductorCliRequest(["config"], {
71 BAA_NODE_ID: "mini-main",
+71,
-20
1@@ -101,7 +101,8 @@ export interface ConductorConfig {
2 nodeId: string;
3 host: string;
4 role: ConductorRole;
5- controlApiBase: string;
6+ publicApiBase?: string;
7+ controlApiBase?: string;
8 priority?: number;
9 version?: string | null;
10 preferred?: boolean;
11@@ -128,7 +129,9 @@ export interface ConductorRuntimeConfig extends ConductorConfig {
12 sharedToken?: string | null;
13 }
14
15-export interface ResolvedConductorRuntimeConfig extends ConductorConfig {
16+export interface ResolvedConductorRuntimeConfig
17+ extends Omit<ConductorConfig, "controlApiBase" | "publicApiBase"> {
18+ controlApiBase: string;
19 heartbeatIntervalMs: number;
20 leaseRenewIntervalMs: number;
21 localApiAllowedHosts: string[];
22@@ -138,6 +141,7 @@ export interface ResolvedConductorRuntimeConfig extends ConductorConfig {
23 paths: ConductorRuntimePaths;
24 preferred: boolean;
25 priority: number;
26+ publicApiBase: string;
27 renewFailureThreshold: number;
28 sharedToken: string | null;
29 version: string | null;
30@@ -320,6 +324,7 @@ interface CliValueOverrides {
31 nodeId?: string;
32 preferred?: boolean;
33 priority?: string;
34+ publicApiBase?: string;
35 renewFailureThreshold?: string;
36 role?: string;
37 runOnce: boolean;
38@@ -1046,7 +1051,7 @@ export class ConductorDaemon {
39 private readonly autoStartLoops: boolean;
40 private readonly clearIntervalImpl: (handle: TimerHandle) => void;
41 private readonly client: ConductorControlApiClient;
42- private readonly config: ConductorConfig;
43+ private readonly config: CanonicalConductorConfig;
44 private heartbeatTimer: TimerHandle | null = null;
45 private readonly hooks?: ConductorDaemonHooks;
46 private leaseTimer: TimerHandle | null = null;
47@@ -1065,13 +1070,23 @@ export class ConductorDaemon {
48 };
49
50 constructor(config: ConductorConfig, options: ConductorDaemonOptions = {}) {
51- this.config = config;
52+ const publicApiBase = resolveConfiguredPublicApiBase(config);
53+
54+ if (!publicApiBase) {
55+ throw new Error("Conductor config requires a non-empty publicApiBase.");
56+ }
57+
58+ this.config = {
59+ ...config,
60+ controlApiBase: normalizeBaseUrl(publicApiBase),
61+ publicApiBase: normalizeBaseUrl(publicApiBase)
62+ };
63 this.autoStartLoops = options.autoStartLoops ?? true;
64 this.clearIntervalImpl =
65 options.clearIntervalImpl ?? ((handle) => globalThis.clearInterval(handle));
66 this.client =
67 options.client ??
68- createFetchControlApiClient(config.controlApiBase, {
69+ createFetchControlApiClient(this.config.publicApiBase, {
70 bearerToken: options.controlApiBearerToken,
71 fetchImpl: options.fetchImpl
72 });
73@@ -1079,7 +1094,7 @@ export class ConductorDaemon {
74 this.now = options.now ?? defaultNowUnixSeconds;
75 this.setIntervalImpl =
76 options.setIntervalImpl ?? ((handler, intervalMs) => globalThis.setInterval(handler, intervalMs));
77- this.startedAt = config.startedAt ?? this.now();
78+ this.startedAt = this.config.startedAt ?? this.now();
79 }
80
81 getStartupChecklist(): StartupChecklistItem[] {
82@@ -1380,12 +1395,35 @@ function resolvePathConfig(paths?: Partial<ConductorRuntimePaths>): ConductorRun
83 };
84 }
85
86+function resolveConfiguredPublicApiBase(
87+ values: Pick<ConductorConfig, "controlApiBase" | "publicApiBase">
88+): string | null {
89+ return normalizeOptionalString(values.publicApiBase ?? values.controlApiBase);
90+}
91+
92+function resolveSourcePublicApiBase(
93+ env: ConductorEnvironment,
94+ overrides: CliValueOverrides
95+): string | null {
96+ return normalizeOptionalString(
97+ overrides.publicApiBase ??
98+ overrides.controlApiBase ??
99+ env.BAA_CONDUCTOR_PUBLIC_API_BASE ??
100+ env.BAA_CONTROL_API_BASE
101+ );
102+}
103+
104+type CanonicalConductorConfig = Omit<ConductorConfig, "controlApiBase" | "publicApiBase"> & {
105+ controlApiBase: string;
106+ publicApiBase: string;
107+};
108+
109 export function resolveConductorRuntimeConfig(
110 config: ConductorRuntimeConfig
111 ): ResolvedConductorRuntimeConfig {
112 const nodeId = normalizeOptionalString(config.nodeId);
113 const host = normalizeOptionalString(config.host);
114- const controlApiBase = normalizeOptionalString(config.controlApiBase);
115+ const publicApiBase = resolveConfiguredPublicApiBase(config);
116
117 if (!nodeId) {
118 throw new Error("Conductor config requires a non-empty nodeId.");
119@@ -1395,8 +1433,8 @@ export function resolveConductorRuntimeConfig(
120 throw new Error("Conductor config requires a non-empty host.");
121 }
122
123- if (!controlApiBase) {
124- throw new Error("Conductor config requires a non-empty controlApiBase.");
125+ if (!publicApiBase) {
126+ throw new Error("Conductor config requires a non-empty publicApiBase.");
127 }
128
129 const heartbeatIntervalMs = config.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
130@@ -1427,7 +1465,7 @@ export function resolveConductorRuntimeConfig(
131 nodeId,
132 host,
133 role: parseConductorRole("Conductor role", config.role),
134- controlApiBase: normalizeBaseUrl(controlApiBase),
135+ controlApiBase: normalizeBaseUrl(publicApiBase),
136 codexdLocalApiBase: resolveLocalApiBase(config.codexdLocalApiBase),
137 heartbeatIntervalMs,
138 leaseRenewIntervalMs,
139@@ -1437,6 +1475,7 @@ export function resolveConductorRuntimeConfig(
140 paths: resolvePathConfig(config.paths),
141 preferred: config.preferred ?? config.role === "primary",
142 priority,
143+ publicApiBase: normalizeBaseUrl(publicApiBase),
144 renewFailureThreshold,
145 sharedToken: normalizeOptionalString(config.sharedToken),
146 version: normalizeOptionalString(config.version)
147@@ -1468,18 +1507,20 @@ function resolveRuntimeConfigFromSources(
148 const host = normalizeOptionalString(overrides.host ?? env.BAA_CONDUCTOR_HOST) ?? "localhost";
149 const nodeId =
150 normalizeOptionalString(overrides.nodeId ?? env.BAA_NODE_ID) ?? createDefaultNodeId(host, role);
151- // Keep reading the legacy env name until conductor-daemon gets a dedicated upstream base setting.
152- const controlApiBase = normalizeOptionalString(overrides.controlApiBase ?? env.BAA_CONTROL_API_BASE);
153+ const publicApiBase = resolveSourcePublicApiBase(env, overrides);
154
155- if (!controlApiBase) {
156- throw new Error("Missing control API base URL. Use --control-api-base or BAA_CONTROL_API_BASE.");
157+ if (!publicApiBase) {
158+ throw new Error(
159+ "Missing conductor public API base URL. Use --public-api-base or BAA_CONDUCTOR_PUBLIC_API_BASE. Legacy aliases: --control-api-base / BAA_CONTROL_API_BASE."
160+ );
161 }
162
163 return resolveConductorRuntimeConfig({
164 nodeId,
165 host,
166 role,
167- controlApiBase,
168+ publicApiBase,
169+ controlApiBase: publicApiBase,
170 priority: parseIntegerValue("Conductor priority", overrides.priority ?? env.BAA_CONDUCTOR_PRIORITY, {
171 minimum: 0
172 }),
173@@ -1579,6 +1620,10 @@ export function parseConductorCliRequest(
174 overrides.role = readOptionValue(tokens, token, index);
175 index += 1;
176 break;
177+ case "--public-api-base":
178+ overrides.publicApiBase = readOptionValue(tokens, token, index);
179+ index += 1;
180+ break;
181 case "--control-api-base":
182 overrides.controlApiBase = readOptionValue(tokens, token, index);
183 index += 1;
184@@ -1721,7 +1766,7 @@ function formatChecklistText(checklist: StartupChecklistItem[]): string {
185 function formatConfigText(config: ResolvedConductorRuntimeConfig): string {
186 return [
187 `identity: ${config.nodeId}@${config.host}(${config.role})`,
188- `control_api_base: ${config.controlApiBase}`,
189+ `public_api_base: ${config.publicApiBase}`,
190 `codexd_local_api_base: ${config.codexdLocalApiBase ?? "not-configured"}`,
191 `local_api_base: ${config.localApiBase ?? "not-configured"}`,
192 `firefox_ws_url: ${buildFirefoxWebSocketUrl(config.localApiBase) ?? "not-configured"}`,
193@@ -1766,7 +1811,8 @@ function getUsageText(): string {
194 " --node-id <id>",
195 " --host <host>",
196 " --role <primary|standby>",
197- " --control-api-base <url> conductor upstream/public API base",
198+ " --public-api-base <url> conductor upstream/public API base",
199+ " --control-api-base <url> legacy alias for --public-api-base",
200 " --codexd-local-api <url>",
201 " --local-api <url>",
202 " --shared-token <token>",
203@@ -1790,7 +1836,8 @@ function getUsageText(): string {
204 " BAA_NODE_ID",
205 " BAA_CONDUCTOR_HOST",
206 " BAA_CONDUCTOR_ROLE",
207- " BAA_CONTROL_API_BASE (legacy env name for conductor upstream/public API base)",
208+ " BAA_CONDUCTOR_PUBLIC_API_BASE",
209+ " BAA_CONTROL_API_BASE (legacy env alias for conductor upstream/public API base)",
210 " BAA_CODEXD_LOCAL_API_BASE",
211 " BAA_CONDUCTOR_LOCAL_API",
212 " BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS",
213@@ -1806,7 +1853,11 @@ function getUsageText(): string {
214 " BAA_WORKTREES_DIR",
215 " BAA_LOGS_DIR",
216 " BAA_TMP_DIR",
217- " BAA_STATE_DIR"
218+ " BAA_STATE_DIR",
219+ "",
220+ "Precedence:",
221+ " CLI values override environment values.",
222+ " --public-api-base / BAA_CONDUCTOR_PUBLIC_API_BASE override legacy control-api aliases."
223 ].join("\n");
224 }
225
226@@ -1893,7 +1944,7 @@ export class ConductorRuntime {
227 loops: this.daemon.getLoopStatus(),
228 paths: { ...this.config.paths },
229 controlApi: {
230- baseUrl: this.config.controlApiBase,
231+ baseUrl: this.config.publicApiBase,
232 firefoxWsUrl,
233 localApiBase,
234 hasSharedToken: this.config.sharedToken != null,
+2,
-2
1@@ -277,8 +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` 只保留两个兼容点:`conductor-daemon` 仍读取这个历史变量名作为 upstream/public API base,`status-api` 只在手工或旧配置缺少 `BAA_CONDUCTOR_LOCAL_API` 时回退使用
6-- 默认 launchd 不再给 `status-api` 写入 `BAA_CONTROL_API_BASE`
7+- `BAA_CONDUCTOR_PUBLIC_API_BASE` 是 `conductor-daemon` upstream/public API base 的 canonical 变量名;legacy `BAA_CONTROL_API_BASE` 只保留两个兼容点:`conductor-daemon` 把它当别名,`status-api` 只在手工或旧配置缺少 `BAA_CONDUCTOR_LOCAL_API` 时回退使用
8+- conductor launchd 安装副本会同时写入 `BAA_CONDUCTOR_PUBLIC_API_BASE` 和 legacy `BAA_CONTROL_API_BASE`;默认 launchd 不再给 `status-api` 写入这些变量
9 - 如果旧文档或配置里还出现 legacy `control-api.makefile.so`,只能按迁移兼容 / 残留依赖盘点目标理解,绝不再是默认或 canonical truth source
10 - `status-api` 负责把该状态整理成 JSON 或 HTML
11
+4,
-0
1@@ -11,6 +11,10 @@
2
3 主备切换、直连 `mac` 的公网域名和历史切换 runbook 已从当前主线移除。
4
5+仓库根验证入口说明见:
6+
7+- [`repo-verification.md`](./repo-verification.md): `pnpm lint` / `pnpm test` 的覆盖范围与边界
8+
9 ## 当前 inventory
10
11 使用一份最小 inventory:
+55,
-0
1@@ -0,0 +1,55 @@
2+# Repo Verification
3+
4+仓库根提供两条默认验证入口:
5+
6+- `pnpm lint`
7+- `pnpm test`
8+
9+它们的目标是给协作者一个在仓库根可重复执行的最小验收口径,而不是替代所有 smoke / runtime / on-node 检查。
10+
11+## `pnpm lint`
12+
13+`pnpm lint` 依次执行:
14+
15+1. `pnpm typecheck`
16+2. `git diff --check`
17+
18+覆盖范围:
19+
20+- `pnpm typecheck` 会跑 `pnpm-workspace.yaml` 里的全部 workspace 包级 `typecheck`
21+- `git diff --check` 负责收口常见 repo hygiene 问题,例如尾随空格或冲突标记
22+
23+刻意不覆盖:
24+
25+- ESLint / Prettier 之类的新 lint 框架
26+- 需要启动服务或依赖外部环境的 smoke / runtime 检查
27+
28+如果只想单独跑 TypeScript 静态检查,直接执行 `pnpm typecheck` 即可。
29+
30+## `pnpm test`
31+
32+`pnpm test` 当前收口到已有稳定 `test` 脚本的关键包:
33+
34+- `@baa-conductor/db`
35+- `@baa-conductor/host-ops`
36+- `@baa-conductor/codex-app-server`
37+- `@baa-conductor/codex-exec`
38+- `@baa-conductor/conductor-daemon`
39+- `@baa-conductor/codexd`
40+
41+脚本会按顺序执行每个包自己的 `pnpm --filter <pkg> test`,这样失败点能直接定位到具体包。
42+
43+刻意不覆盖:
44+
45+- `apps/status-api`、`apps/worker-runner` 等当前没有稳定 `test` 脚本的工作区
46+- 包内 `smoke` 命令
47+- `scripts/runtime/*.sh`、`tests/**/*.mjs` 这类更慢或需要额外运行时条件的端到端检查
48+
49+## 额外检查
50+
51+以下验证仍建议按场景单独执行:
52+
53+- `pnpm typecheck`
54+- `./scripts/runtime/check-node.sh --node mini`
55+- `./scripts/runtime/codexd-e2e-smoke.sh`
56+- `./scripts/runtime/browser-control-e2e-smoke.sh`
+2,
-1
1@@ -20,7 +20,8 @@
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-- 默认 launchd 只把 legacy 变量名 `BAA_CONTROL_API_BASE` 写给 `conductor`;`status-api` 只保留代码层兼容回退,不再把它当 canonical truth source
6+- `BAA_CONDUCTOR_PUBLIC_API_BASE` 是 `conductor` upstream/public API base 的 canonical 变量名;launchd 会连同 legacy `BAA_CONTROL_API_BASE` 一起写给 `conductor`
7+- `status-api` 只保留对 legacy `BAA_CONTROL_API_BASE` 的代码层兼容回退,不再把它当 canonical truth source
8 - 推荐仓库路径:`/Users/george/code/baa-conductor`
9 - repo 内的 plist 只作为模板;真正加载的是脚本渲染出来的安装副本
10
+10,
-5
1@@ -9,7 +9,8 @@
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-- legacy 变量名 `BAA_CONTROL_API_BASE` 只剩两个保留点:`conductor` 运行时仍用它解析 upstream/public API base,`status-api` 只在手工兼容场景下回退读取
6+- `conductor` upstream/public API base 的 canonical 变量名是 `BAA_CONDUCTOR_PUBLIC_API_BASE`
7+- legacy 变量名 `BAA_CONTROL_API_BASE` 只剩两个保留点:`conductor` 把它当兼容别名,`status-api` 只在手工兼容场景下回退读取
8
9 ## 共享变量
10
11@@ -24,13 +25,16 @@
12
13 - `codexd` 独立安装时不要求 `BAA_SHARED_TOKEN`
14
15-## 兼容变量
16+## conductor public API 变量
17
18+- `BAA_CONDUCTOR_PUBLIC_API_BASE`
19 - `BAA_CONTROL_API_BASE`
20
21 说明:
22
23-- 变量名本身是历史兼容名,但 `conductor-daemon` 当前仍用它解析 upstream/public API base;默认 launchd 只把它写给 `conductor`
24+- `BAA_CONDUCTOR_PUBLIC_API_BASE` 是 canonical 配置名,`BAA_CONTROL_API_BASE` 是 legacy 兼容别名
25+- 如果两个 env 同时存在,`BAA_CONDUCTOR_PUBLIC_API_BASE` 优先
26+- launchd 安装脚本会把这两个名字都写给 `conductor` 安装副本,避免新旧副本/脚本混跑时失配
27 - `status-api` 现在会优先读取 `BAA_CONDUCTOR_LOCAL_API`,只有手工启动或旧配置缺少该值时才回退到 `BAA_CONTROL_API_BASE`
28 - mini 默认兼容值仍是 `https://conductor.makefile.so`
29
30@@ -86,10 +90,11 @@ BAA_CONDUCTOR_LOCAL_API=http://100.71.210.78:4317
31 BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS=100.71.210.78
32 BAA_CODEXD_LOCAL_API_BASE=http://127.0.0.1:4319
33 BAA_STATUS_API_HOST=100.71.210.78
34+BAA_CONDUCTOR_PUBLIC_API_BASE=https://conductor.makefile.so
35 BAA_CONTROL_API_BASE=https://conductor.makefile.so
36 ```
37
38-最后一项现在只写给 `conductor` 安装副本;`status-api` 的默认真相源仍然是 `BAA_CONDUCTOR_LOCAL_API=http://100.71.210.78:4317`,默认 launchd 不再额外携带 `BAA_CONTROL_API_BASE`。
39+最后两项现在都只写给 `conductor` 安装副本;`status-api` 的默认真相源仍然是 `BAA_CONDUCTOR_LOCAL_API=http://100.71.210.78:4317`,默认 launchd 不再额外携带这些 conductor public-api base 变量。
40
41 说明:
42
43@@ -124,4 +129,4 @@ Firefox WS 派生规则:
44 --status-api-host 100.71.210.78
45 ```
46
47-默认 mini 安装不需要显式传 `--control-api-base`;这个参数名本身也是 legacy 兼容名,只有 `conductor` 的 upstream/public API base 不是 `https://conductor.makefile.so` 时才需要覆盖。即使显式传入,脚本现在也只会把它写给 `conductor` 安装副本;`status-api` 默认仍然优先读取 `--local-api-base` 对应的 conductor 主接口。
48+默认 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 主接口。
+2,
-2
1@@ -46,7 +46,7 @@
2
3 - `codexd` 独立安装时不需要共享 token
4 - `status-api` 默认真相源是 `BAA_CONDUCTOR_LOCAL_API`
5-- `--control-api-base` 仍然保留为 legacy 兼容参数名,但只影响 `conductor` 安装副本;默认值就是 `https://conductor.makefile.so`
6+- `--public-api-base` 是 canonical 参数名;`--control-api-base` 仍保留为 legacy 兼容别名,但都只影响 `conductor` 安装副本
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@@ -113,7 +113,7 @@
11 --status-api-host 100.71.210.78
12 ```
13
14-默认 mini 安装不需要显式传 `--control-api-base`;这个参数名本身是 legacy 兼容名,只有 `conductor` 的 upstream/public API base 不是 `https://conductor.makefile.so` 时才需要覆盖。即使显式传入,脚本现在也只会把它写给 `conductor` 安装副本;`status-api` 实际默认仍然优先读 `--local-api-base http://100.71.210.78:4317`。
15+默认 mini 安装不需要显式传 `--public-api-base`;只有 `conductor` 的 upstream/public API base 不是 `https://conductor.makefile.so` 时才需要覆盖。`--control-api-base` 仍可作为 legacy 兼容别名使用;如果新旧参数同时出现,`--public-api-base` 优先。脚本现在会把 `BAA_CONDUCTOR_PUBLIC_API_BASE` 和 legacy `BAA_CONTROL_API_BASE` 一起写给 `conductor` 安装副本;`status-api` 实际默认仍然优先读 `--local-api-base http://100.71.210.78:4317`。
16
17 单独安装 `codexd`:
18
+2,
-2
1@@ -28,9 +28,9 @@ npx --yes pnpm -r build
2
3 说明:
4
5-- 默认 mini 静态检查不需要显式传 `--control-api-base`;这个参数名本身是 legacy 兼容名,只有 `conductor` 的 upstream/public API base 不是 `https://conductor.makefile.so` 时才需要覆盖
6+- 默认 mini 静态检查不需要显式传 `--public-api-base`;只有 `conductor` 的 upstream/public API base 不是 `https://conductor.makefile.so` 时才需要覆盖
7 - `status-api` 的有效默认真相源仍然是 `--local-api-base http://100.71.210.78:4317`
8-- `check-launchd.sh` 现在只校验 `conductor` 安装副本里的 `BAA_CONTROL_API_BASE`;其他服务要求该变量不存在
9+- `check-launchd.sh` 现在会优先校验 `conductor` 安装副本里的 `BAA_CONDUCTOR_PUBLIC_API_BASE`,同时接受只带 legacy `BAA_CONTROL_API_BASE` 的旧安装副本;其他服务要求这两个变量都不存在
10 - `check-launchd.sh` 现在也会校验 `conductor` 安装副本里的 `BAA_CODEXD_LOCAL_API_BASE`
11 - `check-launchd.sh` 现在会校验 `codexd` 的监听地址、事件流路径、日志目录、状态目录和 `app-server` child 配置
12 - 这些静态检查不包含 run/exec 路线
+2,
-2
1@@ -6,8 +6,8 @@
2 "build": "pnpm -r build",
3 "build:runtime-postprocess": "node --input-type=module -e \"import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; const distDir = process.env.BAA_DIST_DIR; const entry = process.env.BAA_DIST_ENTRY; if (!distDir || !entry) { throw new Error('BAA_DIST_DIR and BAA_DIST_ENTRY are required.'); } const aliasPairs = (process.env.BAA_IMPORT_ALIASES ?? '').split(';').filter(Boolean).map((pair) => { const separatorIndex = pair.indexOf('='); if (separatorIndex <= 0) { throw new Error('Invalid alias pair: ' + pair); } return [pair.slice(0, separatorIndex), pair.slice(separatorIndex + 1)]; }); const fixRelativeExtensions = process.env.BAA_FIX_RELATIVE_EXTENSIONS === 'true'; const jsFiles = []; const collect = (directory) => { for (const entryName of readdirSync(directory)) { const entryPath = join(directory, entryName); const stats = statSync(entryPath); if (stats.isDirectory()) { collect(entryPath); continue; } if (entryPath.endsWith('.js')) { jsFiles.push(entryPath); } } }; collect(distDir); for (const filePath of jsFiles) { let source = readFileSync(filePath, 'utf8'); for (const [from, to] of aliasPairs) { source = source.replaceAll('\\\"' + from + '\\\"', '\\\"' + to + '\\\"').replaceAll(\\\"'\\\" + from + \\\"'\\\", \\\"'\\\" + to + \\\"'\\\"); } if (fixRelativeExtensions) { source = source.replace(/((?:from|import)\\s*[\\\"'])(\\.\\.?\\/[^\\\"'()]+?)([\\\"'])/g, (match, prefix, specifier, suffix) => /\\.[cm]?[jt]sx?$/.test(specifier) || specifier.endsWith('.json') ? match : prefix + specifier + '.js' + suffix); } writeFileSync(filePath, source); } mkdirSync(distDir, { recursive: true }); const entrySpecifier = './' + entry; const shimLines = ['export * from ' + JSON.stringify(entrySpecifier) + ';']; if (process.env.BAA_EXPORT_DEFAULT === 'true') { shimLines.push('export { default } from ' + JSON.stringify(entrySpecifier) + ';'); } writeFileSync(join(distDir, 'index.js'), shimLines.join('\\n') + '\\n');\"",
4 "typecheck": "pnpm -r typecheck",
5- "lint": "echo 'TODO: add linting'",
6- "test": "echo 'TODO: add tests'",
7+ "lint": "node ./scripts/verify-workspace.mjs lint",
8+ "test": "node ./scripts/verify-workspace.mjs test",
9 "tasks": "ls tasks"
10 },
11 "devDependencies": {
+7,
-4
1@@ -6,10 +6,10 @@
2
3 ## 当前代码基线
4
5-- 主线基线:`main@d349be1`
6+- 主线基线:`main@d1c9090`
7 - 任务文档已统一收口到 `tasks/`
8 - 当前活动任务见 `tasks/TASK_OVERVIEW.md`
9-- `T-S001` 到 `T-S004` 已经合入主线
10+- `T-S001` 到 `T-S008` 已经合入主线
11
12 ## 当前状态
13
14@@ -51,15 +51,18 @@
15 - `T-S005`:默认 launchd 现在只把 `BAA_CONTROL_API_BASE` 写给 `conductor`;`status-api` 和 `worker-runner` 不再携带这个变量
16 - `T-S006`:`tasks/`、`plans/`、`README` 与 `docs/api/**` / `docs/runtime/**` 已把 `control-api` 收口为 legacy 背景,不再写成当前默认入口
17 - `T-S007`:`ConductorRuntime.stop()` 已补专项测试,覆盖 listener 释放、Firefox bridge client 关闭和重复 stop 的幂等行为
18+- `T-S008`:`codexd` 现在会把“未收到合法 completed 就提前断流”单独归类成新的 child / transport 故障,并写入 reopen 规则
19
20 ## 下一步任务
21
22-- `T-S008`:给 codexd child / transport 提前断流补诊断与 reopen 规则
23+- `T-S009`:把 conductor upstream/public API base 从 legacy 名字里解耦
24+- `T-S010`:补仓库根验证入口
25
26 ## 当前仍需关注
27
28 - 如果后续再次出现 app-server 根本没有发出合法 `turn/completed` 就提前断流,这属于新的 child / transport 故障,应单独新开 bug
29-- `BAA_CONTROL_API_BASE` 仍保留在仓库里,定位是兼容入口,不是 canonical truth source
30+- `BAA_CONTROL_API_BASE` / `--control-api-base` 仍保留在运行时代码和脚本里,定位是兼容名字,不是 canonical truth source
31 - 仍保留的 `control-api` 命名已经限定在历史任务卡、legacy 测试路径、兼容变量名和外部残留资产说明里;如果未来要继续删旧,需要单独评估文件名和兼容面
32 - 如果未来新增 runtime 测试绕开 `withRuntimeFixture(...)`,同类 listener 泄漏仍可能重新出现
33 - 这次没有改 `ConductorRuntime.stop()` 内部逻辑;如果未来关闭路径本身阻塞,还需要单独补运行时层测试
34+- 仓库根 `pnpm lint` / `pnpm test` 仍不是可信入口;后续应把常用验证收口到根脚本
+54,
-7
1@@ -20,7 +20,8 @@ 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 conductor BAA_CONTROL_API_BASE.
6+ --public-api-base URL Expected conductor BAA_CONDUCTOR_PUBLIC_API_BASE.
7+ --control-api-base URL Legacy alias for --public-api-base.
8 --local-api-base URL Expected BAA_CONDUCTOR_LOCAL_API.
9 --local-api-allowed-hosts CSV
10 Expected BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS.
11@@ -41,8 +42,10 @@ Options:
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; launchd no
15- longer writes BAA_CONTROL_API_BASE for it.
16- --control-api-base only applies when conductor is part of the checked set.
17+ longer writes conductor public-api base env vars for it.
18+ --public-api-base only applies when conductor is part of the checked set.
19+ check mode accepts legacy conductor copies that only carry
20+ BAA_CONTROL_API_BASE, but new installs are expected to write both names.
21 codexd static checks only validate app-server launchd wiring; they do not
22 require /v1/codexd/runs* or codex exec as formal runtime capabilities.
23 EOF
24@@ -58,7 +61,8 @@ home_dir="$(default_home_dir)"
25 install_dir=""
26 shared_token=""
27 shared_token_file=""
28-control_api_base="${BAA_RUNTIME_DEFAULT_CONTROL_API_BASE}"
29+public_api_base=""
30+legacy_control_api_base=""
31 local_api_base="http://100.71.210.78:4317"
32 local_api_allowed_hosts="${BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS:-100.71.210.78}"
33 codexd_local_api_base="${BAA_CODEXD_LOCAL_API_BASE:-${BAA_RUNTIME_DEFAULT_CODEXD_LOCAL_API}}"
34@@ -117,8 +121,12 @@ while [[ $# -gt 0 ]]; do
35 shared_token_file="$2"
36 shift 2
37 ;;
38+ --public-api-base)
39+ public_api_base="$2"
40+ shift 2
41+ ;;
42 --control-api-base)
43- control_api_base="$2"
44+ legacy_control_api_base="$2"
45 shift 2
46 ;;
47 --local-api-base)
48@@ -178,6 +186,18 @@ done
49 validate_node "$node"
50 validate_scope "$scope"
51
52+if [[ -z "$public_api_base" ]]; then
53+ if [[ -n "$legacy_control_api_base" ]]; then
54+ public_api_base="$legacy_control_api_base"
55+ elif [[ -n "${BAA_CONDUCTOR_PUBLIC_API_BASE:-}" ]]; then
56+ public_api_base="${BAA_CONDUCTOR_PUBLIC_API_BASE}"
57+ elif [[ -n "${BAA_CONTROL_API_BASE:-}" ]]; then
58+ public_api_base="${BAA_CONTROL_API_BASE}"
59+ else
60+ public_api_base="${BAA_RUNTIME_DEFAULT_PUBLIC_API_BASE}"
61+ fi
62+fi
63+
64 if [[ "${#services[@]}" -eq 0 ]]; then
65 while IFS= read -r service; do
66 services+=("$service")
67@@ -243,6 +263,32 @@ check_key_missing() {
68 fi
69 }
70
71+check_public_api_base_keys() {
72+ local service="$1"
73+ local plist_path="$2"
74+ local found_any="0"
75+
76+ if plist_has_key "$plist_path" ":EnvironmentVariables:BAA_CONDUCTOR_PUBLIC_API_BASE"; then
77+ found_any="1"
78+ check_string_equals \
79+ "${service}:BAA_CONDUCTOR_PUBLIC_API_BASE" \
80+ "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CONDUCTOR_PUBLIC_API_BASE")" \
81+ "$public_api_base"
82+ fi
83+
84+ if plist_has_key "$plist_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE"; then
85+ found_any="1"
86+ check_string_equals \
87+ "${service}:BAA_CONTROL_API_BASE" \
88+ "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE")" \
89+ "$public_api_base"
90+ fi
91+
92+ if [[ "$found_any" != "1" ]]; then
93+ die "${service}: missing conductor public API base env keys"
94+ fi
95+}
96+
97 check_installed_plist() {
98 local service="$1"
99 local plist_path="$2"
100@@ -270,9 +316,10 @@ check_installed_plist() {
101 check_string_equals "${service}:stderr" "$(plist_print_value "$plist_path" ":StandardErrorPath")" "$stderr_path"
102 check_string_equals "${service}:entry" "$(plist_print_value "$plist_path" ":ProgramArguments:2")" "$dist_entry"
103
104- if service_uses_control_api_base "$service"; then
105- check_string_equals "${service}:BAA_CONTROL_API_BASE" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE")" "$control_api_base"
106+ if service_uses_public_api_base "$service"; then
107+ check_public_api_base_keys "$service" "$plist_path"
108 else
109+ check_key_missing "${service}:BAA_CONDUCTOR_PUBLIC_API_BASE" "$plist_path" ":EnvironmentVariables:BAA_CONDUCTOR_PUBLIC_API_BASE"
110 check_key_missing "${service}:BAA_CONTROL_API_BASE" "$plist_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE"
111 fi
112
+24,
-6
1@@ -20,7 +20,8 @@ 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 conductor BAA_CONTROL_API_BASE in installed copies.
6+ --public-api-base URL Expected conductor BAA_CONDUCTOR_PUBLIC_API_BASE in installed copies.
7+ --control-api-base URL Legacy alias for --public-api-base.
8 --local-api-base URL Conductor local API base URL. Defaults to 127.0.0.1:4317.
9 --local-api-allowed-hosts CSV
10 Expected BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS in installed copies.
11@@ -42,7 +43,7 @@ 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; launchd no
15- longer writes BAA_CONTROL_API_BASE for it.
16+ longer writes conductor public-api base env vars for it.
17 conductor HTTP probes include /v1/codex to ensure proxy wiring to codexd.
18 For codexd, the HTTP probes only cover /healthz and /v1/codexd/status.
19 /v1/codexd/runs* and codex exec are not part of node verification.
20@@ -61,7 +62,8 @@ home_dir="$(default_home_dir)"
21 install_dir=""
22 shared_token=""
23 shared_token_file=""
24-control_api_base="${BAA_RUNTIME_DEFAULT_CONTROL_API_BASE}"
25+public_api_base=""
26+legacy_control_api_base=""
27 local_api_base="http://100.71.210.78:4317"
28 local_api_allowed_hosts="${BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS:-100.71.210.78}"
29 codexd_api_base="${BAA_RUNTIME_DEFAULT_CODEXD_LOCAL_API}"
30@@ -123,8 +125,12 @@ while [[ $# -gt 0 ]]; do
31 shared_token_file="$2"
32 shift 2
33 ;;
34+ --public-api-base)
35+ public_api_base="$2"
36+ shift 2
37+ ;;
38 --control-api-base)
39- control_api_base="$2"
40+ legacy_control_api_base="$2"
41 shift 2
42 ;;
43 --local-api-base)
44@@ -196,6 +202,18 @@ done
45 validate_node "$node"
46 validate_scope "$scope"
47
48+if [[ -z "$public_api_base" ]]; then
49+ if [[ -n "$legacy_control_api_base" ]]; then
50+ public_api_base="$legacy_control_api_base"
51+ elif [[ -n "${BAA_CONDUCTOR_PUBLIC_API_BASE:-}" ]]; then
52+ public_api_base="${BAA_CONDUCTOR_PUBLIC_API_BASE}"
53+ elif [[ -n "${BAA_CONTROL_API_BASE:-}" ]]; then
54+ public_api_base="${BAA_CONTROL_API_BASE}"
55+ else
56+ public_api_base="${BAA_RUNTIME_DEFAULT_PUBLIC_API_BASE}"
57+ fi
58+fi
59+
60 case "$expected_rolez" in
61 any | leader) ;;
62 *)
63@@ -353,8 +371,8 @@ run_static_checks() {
64 )
65
66 for service in "${services[@]}"; do
67- if service_uses_control_api_base "$service"; then
68- static_args+=(--control-api-base "$control_api_base")
69+ if service_uses_public_api_base "$service"; then
70+ static_args+=(--public-api-base "$public_api_base")
71 break
72 fi
73 done
+7,
-2
1@@ -7,7 +7,8 @@ fi
2 readonly BAA_RUNTIME_COMMON_SH_LOADED=1
3 readonly BAA_RUNTIME_SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
4 readonly BAA_RUNTIME_REPO_DIR_DEFAULT="$(cd -- "${BAA_RUNTIME_SCRIPT_DIR}/../.." && pwd)"
5-readonly BAA_RUNTIME_DEFAULT_CONTROL_API_BASE="https://conductor.makefile.so"
6+readonly BAA_RUNTIME_DEFAULT_PUBLIC_API_BASE="https://conductor.makefile.so"
7+readonly BAA_RUNTIME_DEFAULT_CONTROL_API_BASE="${BAA_RUNTIME_DEFAULT_PUBLIC_API_BASE}"
8 readonly BAA_RUNTIME_DEFAULT_LOCAL_API="http://127.0.0.1:4317"
9 readonly BAA_RUNTIME_DEFAULT_CODEXD_LOCAL_API="http://127.0.0.1:4319"
10 readonly BAA_RUNTIME_DEFAULT_CODEXD_EVENT_STREAM_PATH="/v1/codexd/events"
11@@ -102,7 +103,7 @@ service_requires_shared_token() {
12 esac
13 }
14
15-service_uses_control_api_base() {
16+service_uses_public_api_base() {
17 case "$1" in
18 conductor)
19 return 0
20@@ -113,6 +114,10 @@ service_uses_control_api_base() {
21 esac
22 }
23
24+service_uses_control_api_base() {
25+ service_uses_public_api_base "$1"
26+}
27+
28 service_label() {
29 case "$1" in
30 conductor)
+29,
-9
1@@ -20,7 +20,8 @@ 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 conductor BAA_CONTROL_API_BASE.
6+ --public-api-base URL Override conductor BAA_CONDUCTOR_PUBLIC_API_BASE.
7+ --control-api-base URL Legacy alias for --public-api-base.
8 --local-api-base URL Override BAA_CONDUCTOR_LOCAL_API.
9 --local-api-allowed-hosts CSV
10 Override BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS.
11@@ -40,9 +41,10 @@ Notes:
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; launchd no
15- longer writes BAA_CONTROL_API_BASE for it.
16- --control-api-base only affects conductor install copies, and the default is
17- already https://conductor.makefile.so.
18+ longer writes conductor public-api base env vars for it.
19+ --public-api-base only affects conductor install copies, and the default is
20+ already https://conductor.makefile.so. install copies write both
21+ BAA_CONDUCTOR_PUBLIC_API_BASE and legacy BAA_CONTROL_API_BASE.
22 codexd launchd wiring stays on app-server mode and does not expose
23 /v1/codexd/runs* or codex exec as a formal service contract.
24 EOF
25@@ -59,7 +61,8 @@ home_dir="$(default_home_dir)"
26 install_dir=""
27 shared_token="${BAA_SHARED_TOKEN:-}"
28 shared_token_file=""
29-control_api_base="${BAA_RUNTIME_DEFAULT_CONTROL_API_BASE}"
30+public_api_base=""
31+legacy_control_api_base=""
32 local_api_base="http://100.71.210.78:4317"
33 local_api_allowed_hosts="${BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS:-100.71.210.78}"
34 codexd_local_api_base="${BAA_CODEXD_LOCAL_API_BASE:-${BAA_RUNTIME_DEFAULT_CODEXD_LOCAL_API}}"
35@@ -116,8 +119,12 @@ while [[ $# -gt 0 ]]; do
36 shared_token_file="$2"
37 shift 2
38 ;;
39+ --public-api-base)
40+ public_api_base="$2"
41+ shift 2
42+ ;;
43 --control-api-base)
44- control_api_base="$2"
45+ legacy_control_api_base="$2"
46 shift 2
47 ;;
48 --local-api-base)
49@@ -166,6 +173,18 @@ done
50 validate_node "$node"
51 validate_scope "$scope"
52
53+if [[ -z "$public_api_base" ]]; then
54+ if [[ -n "$legacy_control_api_base" ]]; then
55+ public_api_base="$legacy_control_api_base"
56+ elif [[ -n "${BAA_CONDUCTOR_PUBLIC_API_BASE:-}" ]]; then
57+ public_api_base="${BAA_CONDUCTOR_PUBLIC_API_BASE}"
58+ elif [[ -n "${BAA_CONTROL_API_BASE:-}" ]]; then
59+ public_api_base="${BAA_CONTROL_API_BASE}"
60+ else
61+ public_api_base="${BAA_RUNTIME_DEFAULT_PUBLIC_API_BASE}"
62+ fi
63+fi
64+
65 if [[ "${#services[@]}" -eq 0 ]]; then
66 while IFS= read -r service; do
67 services+=("$service")
68@@ -260,10 +279,11 @@ for service in "${services[@]}"; do
69 plist_delete_key "$install_path" ":EnvironmentVariables:BAA_SHARED_TOKEN"
70 fi
71
72- if service_uses_control_api_base "$service"; then
73- # conductor-daemon still reads the legacy env name for its upstream/public API base.
74- plist_set_string "$install_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE" "$control_api_base"
75+ if service_uses_public_api_base "$service"; then
76+ plist_set_string "$install_path" ":EnvironmentVariables:BAA_CONDUCTOR_PUBLIC_API_BASE" "$public_api_base"
77+ plist_set_string "$install_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE" "$public_api_base"
78 else
79+ plist_delete_key "$install_path" ":EnvironmentVariables:BAA_CONDUCTOR_PUBLIC_API_BASE"
80 plist_delete_key "$install_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE"
81 fi
82
+145,
-0
1@@ -0,0 +1,145 @@
2+import assert from "node:assert/strict";
3+import { execFileSync } from "node:child_process";
4+import { mkdtempSync, rmSync } from "node:fs";
5+import { tmpdir } from "node:os";
6+import { join } from "node:path";
7+import test from "node:test";
8+import { fileURLToPath } from "node:url";
9+
10+const repoDir = fileURLToPath(new URL("../..", import.meta.url));
11+const installScript = fileURLToPath(new URL("./install-launchd.sh", import.meta.url));
12+const checkLaunchdScript = fileURLToPath(new URL("./check-launchd.sh", import.meta.url));
13+const checkNodeScript = fileURLToPath(new URL("./check-node.sh", import.meta.url));
14+const plistBuddy = "/usr/libexec/PlistBuddy";
15+const sharedToken = "script-test-shared-token";
16+const publicApiBase = "https://public.example.test";
17+const conductorPlistLabel = "so.makefile.baa-conductor.plist";
18+
19+function runScript(scriptPath, args) {
20+ execFileSync("bash", [scriptPath, ...args], {
21+ cwd: repoDir,
22+ env: process.env,
23+ stdio: "pipe"
24+ });
25+}
26+
27+function plistValue(plistPath, key) {
28+ return execFileSync(plistBuddy, ["-c", `Print ${key}`, plistPath], {
29+ encoding: "utf8"
30+ }).trim();
31+}
32+
33+function deletePlistKey(plistPath, key) {
34+ execFileSync(plistBuddy, ["-c", `Delete ${key}`, plistPath], {
35+ stdio: "pipe"
36+ });
37+}
38+
39+function conductorInstallArgs(installDir, extraArgs = []) {
40+ return [
41+ "--repo-dir",
42+ repoDir,
43+ "--install-dir",
44+ installDir,
45+ "--service",
46+ "conductor",
47+ "--shared-token",
48+ sharedToken,
49+ "--local-api-base",
50+ "http://100.71.210.78:4317",
51+ "--local-api-allowed-hosts",
52+ "100.71.210.78",
53+ ...extraArgs
54+ ];
55+}
56+
57+test("install-launchd writes canonical and legacy conductor public API env names", (t) => {
58+ const installDir = mkdtempSync(join(tmpdir(), "baa-conductor-public-api-install-"));
59+ const plistPath = join(installDir, conductorPlistLabel);
60+
61+ t.after(() => {
62+ rmSync(installDir, { force: true, recursive: true });
63+ });
64+
65+ runScript(
66+ installScript,
67+ conductorInstallArgs(installDir, [
68+ "--public-api-base",
69+ publicApiBase,
70+ "--codexd-local-api-base",
71+ "http://127.0.0.1:4319"
72+ ])
73+ );
74+
75+ assert.equal(
76+ plistValue(plistPath, ":EnvironmentVariables:BAA_CONDUCTOR_PUBLIC_API_BASE"),
77+ publicApiBase
78+ );
79+ assert.equal(plistValue(plistPath, ":EnvironmentVariables:BAA_CONTROL_API_BASE"), publicApiBase);
80+
81+ runScript(
82+ checkLaunchdScript,
83+ conductorInstallArgs(installDir, [
84+ "--public-api-base",
85+ publicApiBase,
86+ "--codexd-local-api-base",
87+ "http://127.0.0.1:4319"
88+ ])
89+ );
90+
91+ runScript(
92+ checkNodeScript,
93+ conductorInstallArgs(installDir, [
94+ "--public-api-base",
95+ publicApiBase,
96+ "--codexd-api-base",
97+ "http://127.0.0.1:4319",
98+ "--skip-port-check",
99+ "--skip-process-check",
100+ "--skip-http-check",
101+ "--skip-log-check"
102+ ])
103+ );
104+});
105+
106+test("check-launchd accepts legacy conductor install copies with only BAA_CONTROL_API_BASE", (t) => {
107+ const installDir = mkdtempSync(join(tmpdir(), "baa-conductor-public-api-legacy-"));
108+ const plistPath = join(installDir, conductorPlistLabel);
109+
110+ t.after(() => {
111+ rmSync(installDir, { force: true, recursive: true });
112+ });
113+
114+ runScript(
115+ installScript,
116+ conductorInstallArgs(installDir, [
117+ "--control-api-base",
118+ publicApiBase,
119+ "--codexd-local-api-base",
120+ "http://127.0.0.1:4319"
121+ ])
122+ );
123+
124+ deletePlistKey(plistPath, ":EnvironmentVariables:BAA_CONDUCTOR_PUBLIC_API_BASE");
125+
126+ assert.equal(plistValue(plistPath, ":EnvironmentVariables:BAA_CONTROL_API_BASE"), publicApiBase);
127+
128+ runScript(
129+ checkLaunchdScript,
130+ conductorInstallArgs(installDir, [
131+ "--public-api-base",
132+ publicApiBase,
133+ "--codexd-local-api-base",
134+ "http://127.0.0.1:4319"
135+ ])
136+ );
137+ runScript(
138+ checkLaunchdScript,
139+ conductorInstallArgs(installDir, [
140+ "--control-api-base",
141+ publicApiBase,
142+ "--codexd-local-api-base",
143+ "http://127.0.0.1:4319"
144+ ])
145+ );
146+});
+65,
-0
1@@ -0,0 +1,65 @@
2+#!/usr/bin/env node
3+
4+import { spawnSync } from "node:child_process";
5+import { dirname, resolve } from "node:path";
6+import { fileURLToPath } from "node:url";
7+
8+const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
9+
10+const testPackages = [
11+ "@baa-conductor/db",
12+ "@baa-conductor/host-ops",
13+ "@baa-conductor/codex-app-server",
14+ "@baa-conductor/codex-exec",
15+ "@baa-conductor/conductor-daemon",
16+ "@baa-conductor/codexd"
17+];
18+
19+const entrypoints = {
20+ lint: [
21+ {
22+ label: "workspace typecheck",
23+ command: ["pnpm", "typecheck"]
24+ },
25+ {
26+ label: "git diff hygiene",
27+ command: ["git", "diff", "--check"]
28+ }
29+ ],
30+ test: testPackages.map((pkg) => ({
31+ label: `${pkg} test`,
32+ command: ["pnpm", "--filter", pkg, "test"]
33+ }))
34+};
35+
36+const mode = process.argv[2];
37+const steps = entrypoints[mode];
38+
39+if (!steps) {
40+ console.error("Usage: node scripts/verify-workspace.mjs <lint|test>");
41+ process.exit(1);
42+}
43+
44+for (const step of steps) {
45+ console.log(`\n==> ${step.label}`);
46+ console.log(`$ ${step.command.join(" ")}`);
47+
48+ const result = spawnSync(step.command[0], step.command.slice(1), {
49+ cwd: repoRoot,
50+ env: process.env,
51+ stdio: "inherit"
52+ });
53+
54+ if (result.error) {
55+ throw result.error;
56+ }
57+
58+ if (typeof result.status === "number" && result.status !== 0) {
59+ process.exit(result.status);
60+ }
61+
62+ if (result.signal) {
63+ console.error(`Command terminated by signal: ${result.signal}`);
64+ process.exit(1);
65+ }
66+}
+120,
-0
1@@ -0,0 +1,120 @@
2+# Task T-S009:把 conductor upstream/public API base 从 legacy 名字里解耦
3+
4+## 直接给对话的提示词
5+
6+读 `/Users/george/code/baa-conductor/tasks/T-S009.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/scripts/runtime/install-launchd.sh`
13+- `/Users/george/code/baa-conductor/scripts/runtime/check-launchd.sh`
14+- `/Users/george/code/baa-conductor/scripts/runtime/check-node.sh`
15+- `/Users/george/code/baa-conductor/docs/runtime/environment.md`
16+- `/Users/george/code/baa-conductor/docs/runtime/launchd.md`
17+
18+## 当前基线
19+
20+- 仓库:`/Users/george/code/baa-conductor`
21+- 分支:`main`
22+- 提交:`d1c9090`
23+- 开工要求:不要从其他任务分支切出;如需新分支,从当前 `main` 新切
24+
25+## 建议分支名
26+
27+- `chore/rename-conductor-public-api-base`
28+
29+## 目标
30+
31+把 conductor upstream/public API base 的配置入口从 legacy `control-api` 命名里解耦,同时保留兼容别名,不破坏现有安装副本和脚本。
32+
33+## 背景
34+
35+- 当前主线已经不再把 `control-api.makefile.so` 当默认控制面。
36+- 但 `conductor-daemon`、`install-launchd.sh`、`check-launchd.sh`、`check-node.sh` 仍用 `BAA_CONTROL_API_BASE` / `--control-api-base` 这套历史名字表示 conductor 自己的 upstream/public API base。
37+- 这会继续误导协作者,把“legacy 名字残留”误读成“系统仍依赖 legacy control plane”。
38+
39+## 涉及仓库
40+
41+- `/Users/george/code/baa-conductor`
42+
43+## 范围
44+
45+- 给 conductor 增加 dedicated 的 env / CLI 名字
46+- 把运行脚本、帮助文本、测试和文档切到新名字
47+- 保留 `BAA_CONTROL_API_BASE` / `--control-api-base` 作为兼容别名,并明确优先级和弃用语义
48+
49+## 路径约束
50+
51+- 这个任务只处理 conductor upstream/public API base 的命名收口
52+- 不要顺手改 `status-api` 的 truth-source 逻辑
53+- 不要扩展成新的部署设计或域名迁移任务
54+
55+## 推荐实现边界
56+
57+建议优先做:
58+
59+- 为 `conductor-daemon` 引入 dedicated 名字,例如 `BAA_CONDUCTOR_PUBLIC_API_BASE` 和 `--public-api-base`
60+- 让旧名字继续可用,但只作为兼容别名,并在帮助文本/文档里标明 legacy
61+- 同步更新 launchd 安装/检查脚本与测试
62+
63+## 允许修改的目录
64+
65+- `/Users/george/code/baa-conductor/apps/conductor-daemon/`
66+- `/Users/george/code/baa-conductor/scripts/runtime/`
67+- `/Users/george/code/baa-conductor/docs/runtime/`
68+- `/Users/george/code/baa-conductor/docs/api/README.md`
69+
70+## 尽量不要修改
71+
72+- `/Users/george/code/baa-conductor/apps/status-api/`
73+- `/Users/george/code/baa-conductor/apps/codexd/`
74+- `/Users/george/code/baa-conductor/tasks/`
75+
76+## 必须完成
77+
78+### 1. 引入 dedicated 配置名字
79+
80+- conductor 运行时代码应支持 dedicated env / CLI 名字
81+- 旧名字继续兼容,但不能再被描述为 canonical 配置名
82+
83+### 2. 保持兼容行为可解释
84+
85+- 明确新旧名字同时存在时谁优先
86+- 帮助文本、脚本 usage、runtime 文档都要写清兼容关系
87+
88+### 3. 补测试和验证
89+
90+- 至少覆盖 CLI / env 解析和脚本预期
91+- 验证安装/检查脚本没有被这次改名打坏
92+
93+## 需要特别注意
94+
95+- 不要破坏现有 launchd 安装副本读取旧环境变量的兼容性
96+- 不要顺手改 service 名字或 plist label
97+- 与根脚本任务并行时,尽量不要修改同一批总览文档
98+
99+## 验收标准
100+
101+- conductor 代码和脚本已有 dedicated 配置名
102+- `BAA_CONTROL_API_BASE` / `--control-api-base` 只剩兼容别名语义
103+- conductor 测试和 runtime 脚本检查通过
104+- `git diff --check` 通过
105+
106+## 推荐验证命令
107+
108+- `npx --yes pnpm -C /Users/george/code/baa-conductor -F @baa-conductor/conductor-daemon test`
109+- `bash -n /Users/george/code/baa-conductor/scripts/runtime/install-launchd.sh`
110+- `bash -n /Users/george/code/baa-conductor/scripts/runtime/check-launchd.sh`
111+- `bash -n /Users/george/code/baa-conductor/scripts/runtime/check-node.sh`
112+- `git -C /Users/george/code/baa-conductor diff --check`
113+
114+## 交付要求
115+
116+完成后请说明:
117+
118+- 修改了哪些文件
119+- 新旧配置名分别是什么,兼容优先级如何
120+- 跑了哪些测试
121+- 还有哪些剩余风险
+120,
-0
1@@ -0,0 +1,120 @@
2+# Task T-S010:补仓库根验证入口
3+
4+## 直接给对话的提示词
5+
6+读 `/Users/george/code/baa-conductor/tasks/T-S010.md` 任务文档,完成开发任务。
7+
8+如需补背景,再读:
9+
10+- `/Users/george/code/baa-conductor/package.json`
11+- `/Users/george/code/baa-conductor/apps/conductor-daemon/package.json`
12+- `/Users/george/code/baa-conductor/apps/codexd/package.json`
13+- `/Users/george/code/baa-conductor/apps/status-api/package.json`
14+- `/Users/george/code/baa-conductor/packages/host-ops/package.json`
15+- `/Users/george/code/baa-conductor/packages/db/package.json`
16+- `/Users/george/code/baa-conductor/docs/runtime/README.md`
17+
18+## 当前基线
19+
20+- 仓库:`/Users/george/code/baa-conductor`
21+- 分支:`main`
22+- 提交:`d1c9090`
23+- 开工要求:不要从其他任务分支切出;如需新分支,从当前 `main` 新切
24+
25+## 建议分支名
26+
27+- `chore/add-root-verification-entrypoints`
28+
29+## 目标
30+
31+把仓库根的 `pnpm lint` / `pnpm test` 从占位脚本改成真实、可重复执行的验证入口。
32+
33+## 背景
34+
35+- 当前根 `package.json` 里的 `lint` 和 `test` 只是 `echo TODO`。
36+- 实际仓库已经有不少稳定可跑的包级 `build` / `typecheck` / `test`,但缺少统一入口。
37+- 这会让协作者不知道“在根目录该跑什么”,也让主线验收口径分散在聊天记录里。
38+
39+## 涉及仓库
40+
41+- `/Users/george/code/baa-conductor`
42+
43+## 范围
44+
45+- 让根 `pnpm lint` / `pnpm test` 成为真实入口
46+- 如有必要,新增少量 helper 脚本收口验证矩阵
47+- 写清这两个入口覆盖什么、不覆盖什么
48+
49+## 路径约束
50+
51+- 优先复用现有包级脚本,不要大范围改包内部测试逻辑
52+- 不要引入重量级新 lint 框架,除非确有必要且收益明确
53+- 这个任务是“入口收口”,不是“给全仓新增 ESLint/Prettier/CI 平台”
54+
55+## 推荐实现边界
56+
57+建议优先做:
58+
59+- 把根 `test` 收口到当前稳定的关键包测试矩阵
60+- 把根 `lint` 改成真实的仓库级静态检查或 repo-hygiene 检查
61+- 在文档里说明根入口的覆盖范围和刻意不覆盖的部分
62+
63+## 允许修改的目录
64+
65+- `/Users/george/code/baa-conductor/package.json`
66+- `/Users/george/code/baa-conductor/scripts/`
67+- `/Users/george/code/baa-conductor/docs/ops/`
68+- `/Users/george/code/baa-conductor/docs/runtime/README.md`
69+
70+## 尽量不要修改
71+
72+- `/Users/george/code/baa-conductor/apps/status-api/`
73+- `/Users/george/code/baa-conductor/apps/codexd/`
74+- `/Users/george/code/baa-conductor/apps/conductor-daemon/`
75+- `/Users/george/code/baa-conductor/tasks/`
76+
77+## 必须完成
78+
79+### 1. 去掉根占位脚本
80+
81+- 根 `pnpm lint` 不能再只是 `echo`
82+- 根 `pnpm test` 不能再只是 `echo`
83+
84+### 2. 让入口可重复执行
85+
86+- 默认在干净仓库即可运行
87+- 不依赖人工交互或线上环境
88+
89+### 3. 写清覆盖边界
90+
91+- 说明根入口覆盖哪些包或脚本
92+- 说明哪些更慢或更特殊的验证仍需单独跑
93+
94+## 需要特别注意
95+
96+- 不要把“根入口”做成超长、脆弱、难维护的一串命令
97+- 尽量让失败点可定位,不要只给一个笼统失败
98+- 与 runtime 命名收口任务并行时,避免同时改同一段 runtime 文档
99+
100+## 验收标准
101+
102+- 根 `pnpm lint` / `pnpm test` 都是可执行的真实检查
103+- 文档已说明这两个入口的覆盖范围
104+- 相关命令在当前主线可跑通
105+- `git diff --check` 通过
106+
107+## 推荐验证命令
108+
109+- `pnpm -C /Users/george/code/baa-conductor lint`
110+- `pnpm -C /Users/george/code/baa-conductor test`
111+- `pnpm -C /Users/george/code/baa-conductor typecheck`
112+- `git -C /Users/george/code/baa-conductor diff --check`
113+
114+## 交付要求
115+
116+完成后请说明:
117+
118+- 根入口最终跑了哪些子检查
119+- 有没有新增 helper 脚本
120+- 跑了哪些验证
121+- 还有哪些检查仍未被根入口覆盖
+7,
-5
1@@ -9,7 +9,7 @@
2 - `control-api.makefile.so`、Cloudflare Worker、D1 只剩迁移期 legacy 兼容残留和依赖盘点用途
3 - `baa-hand` / `baa-shell` 只保留为接口语义参考,不再作为主系统维护
4 - 当前任务卡都放在本目录
5-- 当前任务基线:`main@d349be1`
6+- 当前任务基线:`main@d1c9090`
7
8 ## 最近完成任务
9
10@@ -22,6 +22,7 @@
11 5. [`T-S005.md`](./T-S005.md):收口 `BAA_CONTROL_API_BASE` 兼容入口
12 6. [`T-S006.md`](./T-S006.md):清理历史 `control-api` 命名残留
13 7. [`T-S007.md`](./T-S007.md):补 `ConductorRuntime.stop()` 关闭路径专项测试
14+8. [`T-S008.md`](./T-S008.md):补 codexd child / transport 断流诊断与 reopen 规则
15
16 说明:
17
18@@ -29,14 +30,15 @@
19
20 ## 当前活动任务
21
22-围绕剩余风险,当前建议继续推进这 1 张任务卡:
23+围绕剩余技术债,当前建议继续推进这 2 张任务卡:
24
25-1. [`T-S008.md`](./T-S008.md):补 codexd child / transport 断流诊断与 reopen 规则
26+1. [`T-S009.md`](./T-S009.md):把 conductor upstream/public API base 从 legacy 名字里解耦
27+2. [`T-S010.md`](./T-S010.md):补仓库根验证入口
28
29 说明:
30
31-- `T-S006` 已完成并回写当前文档口径
32-- `T-S008` 主要改 codexd 与 bug/runtime 文档
33+- `T-S009` 主要改 `apps/conductor-daemon`、`scripts/runtime/**` 和 `docs/runtime/**`
34+- `T-S010` 主要改根 `package.json` 与新增的验证脚本;尽量不要和 `T-S009` 同时改同一批文档
35
36 ## 任务文档约定
37