baa-conductor

git clone 

commit
1e75818
parent
a7ba96e
author
im_wower
date
2026-03-23 01:02:54 +0800 CST
feat(conductor-daemon): proxy codex routes to codexd
10 files changed,  +1225, -258
M DESIGN.md
+2, -2
 1@@ -94,7 +94,7 @@
 2   - 持续记录日志
 3 - 公开接口结论已经明确:
 4   - 主接口:`codex app-server`
 5-  - 辅助兜底:`codex exec`
 6+  - `conductor-daemon` 对外只保留 `/v1/codex`、`/v1/codex/sessions`、`/v1/codex/sessions/:session_id`、`POST /v1/codex/sessions`、`POST /v1/codex/turn`
 7   - 不驱动 TUI
 8 - 负责 Codex 特有的会话、子进程、重试、超时和恢复语义
 9 - 必须作为独立进程运行,不接受长期内嵌在 `conductor-daemon` 里的方案
10@@ -110,7 +110,7 @@
11 - 仓库里已经有 `codex` step kind、planner 和 step template 抽象
12 - `apps/codexd` 已有 daemon scaffold,但还没有完成 conductor 集成
13 - 所以 `codexd` 现在仍是半成品,不是已落地组件
14-- 后续实现时,不再把 `exec` 当作主双工方案;`exec` 只用于简单调用、测试和兜底
15+- 正式产品口径只保留 session / turn / status,不把 `runs` / `exec` 当作 Codex 能力面
16 - 后续实现时,`conductor-daemon` 只能通过本地接口调用 `codexd`,不能自己长期维护一套并行 bridge
17 
18 ### `apps/worker-runner`
M apps/conductor-daemon/src/index.test.js
+602, -188
  1@@ -1,4 +1,5 @@
  2 import assert from "node:assert/strict";
  3+import { createServer } from "node:http";
  4 import { mkdtempSync, rmSync } from "node:fs";
  5 import { tmpdir } from "node:os";
  6 import { join } from "node:path";
  7@@ -179,6 +180,9 @@ async function createLocalApiFixture() {
  8   });
  9 
 10   const snapshot = {
 11+    codexd: {
 12+      localApiBase: null
 13+    },
 14     controlApi: {
 15       baseUrl: "https://control.example.test",
 16       firefoxWsUrl: "ws://127.0.0.1:4317/ws/firefox",
 17@@ -214,6 +218,250 @@ async function createLocalApiFixture() {
 18   };
 19 }
 20 
 21+async function startCodexdStubServer() {
 22+  const requests = [];
 23+  const sessions = [
 24+    {
 25+      sessionId: "session-demo",
 26+      purpose: "duplex",
 27+      threadId: "thread-demo",
 28+      status: "active",
 29+      endpoint: "http://127.0.0.1:0",
 30+      childPid: 43210,
 31+      createdAt: "2026-03-23T00:00:00.000Z",
 32+      updatedAt: "2026-03-23T00:00:00.000Z",
 33+      cwd: "/Users/george/code/baa-conductor",
 34+      model: "gpt-5.4",
 35+      modelProvider: "openai",
 36+      serviceTier: "default",
 37+      reasoningEffort: "medium",
 38+      currentTurnId: null,
 39+      lastTurnId: "turn-demo",
 40+      lastTurnStatus: "completed",
 41+      metadata: {
 42+        origin: "stub"
 43+      }
 44+    }
 45+  ];
 46+
 47+  const server = createServer(async (request, response) => {
 48+    const method = (request.method ?? "GET").toUpperCase();
 49+    const url = new URL(request.url ?? "/", "http://127.0.0.1");
 50+    let rawBody = "";
 51+
 52+    request.setEncoding("utf8");
 53+    for await (const chunk of request) {
 54+      rawBody += chunk;
 55+    }
 56+
 57+    const parsedBody = rawBody === "" ? null : JSON.parse(rawBody);
 58+    requests.push({
 59+      body: parsedBody,
 60+      method,
 61+      path: url.pathname
 62+    });
 63+
 64+    const address = server.address();
 65+    const port = typeof address === "object" && address ? address.port : 0;
 66+    const baseUrl = `http://127.0.0.1:${port}`;
 67+    const eventStreamUrl = `ws://127.0.0.1:${port}/v1/codexd/events`;
 68+
 69+    const writeJson = (status, payload) => {
 70+      response.statusCode = status;
 71+      response.setHeader("content-type", "application/json; charset=utf-8");
 72+      response.end(`${JSON.stringify(payload, null, 2)}\n`);
 73+    };
 74+
 75+    const currentSessions = sessions.map((session) => ({
 76+      ...session,
 77+      endpoint: baseUrl
 78+    }));
 79+
 80+    if (method === "GET" && url.pathname === "/v1/codexd/status") {
 81+      writeJson(200, {
 82+        ok: true,
 83+        data: {
 84+          service: {
 85+            configuredBaseUrl: baseUrl,
 86+            eventStreamPath: "/v1/codexd/events",
 87+            eventStreamUrl,
 88+            listening: true,
 89+            resolvedBaseUrl: baseUrl,
 90+            websocketClients: 0
 91+          },
 92+          snapshot: {
 93+            identity: {
 94+              daemonId: "codexd-demo",
 95+              nodeId: "mini",
 96+              repoRoot: "/Users/george/code/baa-conductor",
 97+              version: "1.2.3"
 98+            },
 99+            daemon: {
100+              started: true,
101+              startedAt: "2026-03-23T00:00:00.000Z",
102+              updatedAt: "2026-03-23T00:00:01.000Z",
103+              child: {
104+                endpoint: baseUrl,
105+                lastError: null,
106+                pid: 43210,
107+                status: "running",
108+                strategy: "spawn"
109+              }
110+            },
111+            sessionRegistry: {
112+              updatedAt: "2026-03-23T00:00:02.000Z",
113+              sessions: currentSessions
114+            },
115+            recentEvents: {
116+              updatedAt: "2026-03-23T00:00:03.000Z",
117+              events: [
118+                {
119+                  seq: 1,
120+                  createdAt: "2026-03-23T00:00:03.000Z",
121+                  level: "info",
122+                  type: "session.created",
123+                  message: "Created session session-demo.",
124+                  detail: {
125+                    sessionId: "session-demo",
126+                    threadId: "thread-demo"
127+                  }
128+                }
129+              ]
130+            }
131+          }
132+        }
133+      });
134+      return;
135+    }
136+
137+    if (method === "GET" && url.pathname === "/v1/codexd/sessions") {
138+      writeJson(200, {
139+        ok: true,
140+        data: {
141+          sessions: currentSessions
142+        }
143+      });
144+      return;
145+    }
146+
147+    if (method === "POST" && url.pathname === "/v1/codexd/sessions") {
148+      const nextSession = {
149+        ...sessions[0],
150+        sessionId: `session-${sessions.length + 1}`,
151+        purpose: parsedBody?.purpose ?? "duplex",
152+        cwd: parsedBody?.cwd ?? sessions[0].cwd,
153+        model: parsedBody?.model ?? sessions[0].model,
154+        updatedAt: "2026-03-23T00:00:04.000Z"
155+      };
156+      sessions.push(nextSession);
157+      writeJson(201, {
158+        ok: true,
159+        data: {
160+          session: {
161+            ...nextSession,
162+            endpoint: baseUrl
163+          }
164+        }
165+      });
166+      return;
167+    }
168+
169+    if (method === "POST" && url.pathname === "/v1/codexd/turn") {
170+      const sessionId = parsedBody?.sessionId;
171+      const session = sessions.find((entry) => entry.sessionId === sessionId);
172+
173+      if (!session) {
174+        writeJson(404, {
175+          ok: false,
176+          error: "not_found",
177+          message: `Unknown codexd session "${sessionId}".`
178+        });
179+        return;
180+      }
181+
182+      session.lastTurnId = "turn-created";
183+      session.lastTurnStatus = "accepted";
184+      session.updatedAt = "2026-03-23T00:00:05.000Z";
185+      writeJson(202, {
186+        ok: true,
187+        data: {
188+          accepted: true,
189+          session: {
190+            ...session,
191+            endpoint: baseUrl
192+          },
193+          turnId: "turn-created"
194+        }
195+      });
196+      return;
197+    }
198+
199+    const sessionMatch = url.pathname.match(/^\/v1\/codexd\/sessions\/([^/]+)$/u);
200+
201+    if (method === "GET" && sessionMatch?.[1]) {
202+      const session = sessions.find((entry) => entry.sessionId === decodeURIComponent(sessionMatch[1]));
203+
204+      if (!session) {
205+        writeJson(404, {
206+          ok: false,
207+          error: "not_found",
208+          message: `Unknown codexd session "${decodeURIComponent(sessionMatch[1])}".`
209+        });
210+        return;
211+      }
212+
213+      writeJson(200, {
214+        ok: true,
215+        data: {
216+          session: {
217+            ...session,
218+            endpoint: baseUrl
219+          },
220+          recentEvents: [
221+            {
222+              seq: 1,
223+              type: "turn.completed"
224+            }
225+          ]
226+        }
227+      });
228+      return;
229+    }
230+
231+    writeJson(404, {
232+      ok: false,
233+      error: "not_found",
234+      message: `Unknown codexd route ${method} ${url.pathname}.`
235+    });
236+  });
237+
238+  await new Promise((resolve, reject) => {
239+    server.once("error", reject);
240+    server.listen(0, "127.0.0.1", resolve);
241+  });
242+
243+  const address = server.address();
244+  const port = typeof address === "object" && address ? address.port : 0;
245+
246+  return {
247+    baseUrl: `http://127.0.0.1:${port}`,
248+    requests,
249+    async stop() {
250+      await new Promise((resolve, reject) => {
251+        server.close((error) => {
252+          if (error) {
253+            reject(error);
254+            return;
255+          }
256+
257+          resolve();
258+        });
259+        server.closeAllConnections?.();
260+      });
261+    }
262+  };
263+}
264+
265 function parseJsonBody(response) {
266   return JSON.parse(response.body);
267 }
268@@ -577,6 +825,7 @@ test("parseConductorCliRequest merges launchd env defaults with CLI overrides",
269     BAA_CONDUCTOR_HOST: "mini",
270     BAA_CONDUCTOR_ROLE: "primary",
271     BAA_CONTROL_API_BASE: "https://control.example.test/",
272+    BAA_CODEXD_LOCAL_API_BASE: "http://127.0.0.1:4323/",
273     BAA_CONDUCTOR_LOCAL_API: "http://127.0.0.1:4317/",
274     BAA_SHARED_TOKEN: "replace-me",
275     BAA_RUNS_DIR: "/tmp/runs"
276@@ -592,6 +841,7 @@ test("parseConductorCliRequest merges launchd env defaults with CLI overrides",
277   assert.equal(request.config.role, "standby");
278   assert.equal(request.config.nodeId, "mini-main");
279   assert.equal(request.config.controlApiBase, "https://control.example.test");
280+  assert.equal(request.config.codexdLocalApiBase, "http://127.0.0.1:4323");
281   assert.equal(request.config.localApiBase, "http://127.0.0.1:4317");
282   assert.equal(request.config.paths.runsDir, "/tmp/runs");
283 });
284@@ -648,6 +898,9 @@ test("parseConductorCliRequest rejects unlisted or non-Tailscale local API hosts
285 
286 test("handleConductorHttpRequest keeps degraded runtimes observable but not ready", async () => {
287   const snapshot = {
288+    codexd: {
289+      localApiBase: null
290+    },
291     daemon: {
292       nodeId: "mini-main",
293       host: "mini",
294@@ -708,11 +961,15 @@ test("handleConductorHttpRequest keeps degraded runtimes observable but not read
295 
296 test("handleConductorHttpRequest serves the migrated local business endpoints from the local repository", async () => {
297   const { repository, sharedToken, snapshot } = await createLocalApiFixture();
298+  const codexd = await startCodexdStubServer();
299   const hostOpsDir = mkdtempSync(join(tmpdir(), "baa-conductor-local-host-http-"));
300   const authorizedHeaders = {
301     authorization: `Bearer ${sharedToken}`
302   };
303+  snapshot.codexd.localApiBase = codexd.baseUrl;
304   const localApiContext = {
305+    codexdLocalApiBase: codexd.baseUrl,
306+    fetchImpl: globalThis.fetch,
307     repository,
308     sharedToken,
309     snapshotLoader: () => snapshot
310@@ -737,9 +994,13 @@ test("handleConductorHttpRequest serves the migrated local business endpoints fr
311     assert.equal(describePayload.data.system.mode, "running");
312     assert.equal(describePayload.data.describe_endpoints.business.path, "/describe/business");
313     assert.equal(describePayload.data.describe_endpoints.control.path, "/describe/control");
314+    assert.equal(describePayload.data.codex.enabled, true);
315+    assert.equal(describePayload.data.codex.target_base_url, codexd.baseUrl);
316     assert.equal(describePayload.data.host_operations.enabled, true);
317     assert.equal(describePayload.data.host_operations.auth.header, "Authorization: Bearer <BAA_SHARED_TOKEN>");
318     assert.equal(describePayload.data.host_operations.auth.configured, true);
319+    assert.doesNotMatch(JSON.stringify(describePayload.data.codex.routes), /\/v1\/codex\/runs/u);
320+    assert.doesNotMatch(JSON.stringify(describePayload.data.capabilities.read_endpoints), /\/v1\/runs/u);
321 
322     const businessDescribeResponse = await handleConductorHttpRequest(
323       {
324@@ -752,8 +1013,11 @@ test("handleConductorHttpRequest serves the migrated local business endpoints fr
325     const businessDescribePayload = parseJsonBody(businessDescribeResponse);
326     assert.equal(businessDescribePayload.data.surface, "business");
327     assert.match(JSON.stringify(businessDescribePayload.data.endpoints), /\/v1\/tasks/u);
328+    assert.match(JSON.stringify(businessDescribePayload.data.endpoints), /\/v1\/codex/u);
329     assert.doesNotMatch(JSON.stringify(businessDescribePayload.data.endpoints), /\/v1\/system\/pause/u);
330     assert.doesNotMatch(JSON.stringify(businessDescribePayload.data.endpoints), /\/v1\/exec/u);
331+    assert.doesNotMatch(JSON.stringify(businessDescribePayload.data.endpoints), /\/v1\/runs/u);
332+    assert.equal(businessDescribePayload.data.codex.backend, "independent_codexd");
333 
334     const controlDescribeResponse = await handleConductorHttpRequest(
335       {
336@@ -768,6 +1032,7 @@ test("handleConductorHttpRequest serves the migrated local business endpoints fr
337     assert.match(JSON.stringify(controlDescribePayload.data.endpoints), /\/v1\/system\/pause/u);
338     assert.match(JSON.stringify(controlDescribePayload.data.endpoints), /\/v1\/exec/u);
339     assert.doesNotMatch(JSON.stringify(controlDescribePayload.data.endpoints), /\/v1\/tasks/u);
340+    assert.equal(controlDescribePayload.data.codex.target_base_url, codexd.baseUrl);
341     assert.equal(controlDescribePayload.data.host_operations.auth.header, "Authorization: Bearer <BAA_SHARED_TOKEN>");
342 
343     const healthResponse = await handleConductorHttpRequest(
344@@ -780,6 +1045,19 @@ test("handleConductorHttpRequest serves the migrated local business endpoints fr
345     assert.equal(healthResponse.status, 200);
346     assert.equal(parseJsonBody(healthResponse).data.status, "ok");
347 
348+    const capabilitiesResponse = await handleConductorHttpRequest(
349+      {
350+        method: "GET",
351+        path: "/v1/capabilities"
352+      },
353+      versionedLocalApiContext
354+    );
355+    assert.equal(capabilitiesResponse.status, 200);
356+    const capabilitiesPayload = parseJsonBody(capabilitiesResponse);
357+    assert.equal(capabilitiesPayload.data.codex.backend, "independent_codexd");
358+    assert.match(JSON.stringify(capabilitiesPayload.data.read_endpoints), /\/v1\/codex/u);
359+    assert.doesNotMatch(JSON.stringify(capabilitiesPayload.data.read_endpoints), /\/v1\/runs/u);
360+
361     const controllersResponse = await handleConductorHttpRequest(
362       {
363         method: "GET",
364@@ -835,6 +1113,77 @@ test("handleConductorHttpRequest serves the migrated local business endpoints fr
365     assert.equal(runsPayload.data.count, 1);
366     assert.equal(runsPayload.data.runs[0].run_id, "run_demo");
367 
368+    const codexStatusResponse = await handleConductorHttpRequest(
369+      {
370+        method: "GET",
371+        path: "/v1/codex"
372+      },
373+      localApiContext
374+    );
375+    assert.equal(codexStatusResponse.status, 200);
376+    const codexStatusPayload = parseJsonBody(codexStatusResponse);
377+    assert.equal(codexStatusPayload.data.backend, "independent_codexd");
378+    assert.equal(codexStatusPayload.data.proxy.target_base_url, codexd.baseUrl);
379+    assert.equal(codexStatusPayload.data.sessions.count, 1);
380+    assert.equal(codexStatusPayload.data.sessions.active_count, 1);
381+    assert.doesNotMatch(JSON.stringify(codexStatusPayload.data.routes), /\/v1\/codex\/runs/u);
382+
383+    const codexSessionsResponse = await handleConductorHttpRequest(
384+      {
385+        method: "GET",
386+        path: "/v1/codex/sessions"
387+      },
388+      localApiContext
389+    );
390+    assert.equal(codexSessionsResponse.status, 200);
391+    const codexSessionsPayload = parseJsonBody(codexSessionsResponse);
392+    assert.equal(codexSessionsPayload.data.sessions.length, 1);
393+    assert.equal(codexSessionsPayload.data.sessions[0].sessionId, "session-demo");
394+
395+    const codexSessionReadResponse = await handleConductorHttpRequest(
396+      {
397+        method: "GET",
398+        path: "/v1/codex/sessions/session-demo"
399+      },
400+      localApiContext
401+    );
402+    assert.equal(codexSessionReadResponse.status, 200);
403+    const codexSessionReadPayload = parseJsonBody(codexSessionReadResponse);
404+    assert.equal(codexSessionReadPayload.data.session.sessionId, "session-demo");
405+
406+    const codexSessionCreateResponse = await handleConductorHttpRequest(
407+      {
408+        body: JSON.stringify({
409+          cwd: "/Users/george/code/baa-conductor",
410+          model: "gpt-5.4",
411+          purpose: "duplex"
412+        }),
413+        method: "POST",
414+        path: "/v1/codex/sessions"
415+      },
416+      localApiContext
417+    );
418+    assert.equal(codexSessionCreateResponse.status, 201);
419+    const codexSessionCreatePayload = parseJsonBody(codexSessionCreateResponse);
420+    assert.equal(codexSessionCreatePayload.data.session.sessionId, "session-2");
421+    assert.equal(codexSessionCreatePayload.data.session.purpose, "duplex");
422+
423+    const codexTurnResponse = await handleConductorHttpRequest(
424+      {
425+        body: JSON.stringify({
426+          input: "Summarize pending work.",
427+          sessionId: "session-demo"
428+        }),
429+        method: "POST",
430+        path: "/v1/codex/turn"
431+      },
432+      localApiContext
433+    );
434+    assert.equal(codexTurnResponse.status, 202);
435+    const codexTurnPayload = parseJsonBody(codexTurnResponse);
436+    assert.equal(codexTurnPayload.data.accepted, true);
437+    assert.equal(codexTurnPayload.data.turnId, "turn-created");
438+
439     const runResponse = await handleConductorHttpRequest(
440       {
441         method: "GET",
442@@ -974,13 +1323,11 @@ test("handleConductorHttpRequest serves the migrated local business endpoints fr
443         body: JSON.stringify({
444           command: ["echo", "hello"]
445         }),
446+        headers: authorizedHeaders,
447         method: "POST",
448         path: "/v1/exec"
449       },
450-      {
451-        repository,
452-        snapshotLoader: () => snapshot
453-      }
454+      localApiContext
455     );
456     assert.equal(invalidExecResponse.status, 200);
457     const invalidExecPayload = parseJsonBody(invalidExecResponse);
458@@ -988,28 +1335,6 @@ test("handleConductorHttpRequest serves the migrated local business endpoints fr
459     assert.equal(invalidExecPayload.data.error.code, "INVALID_INPUT");
460     assertEmptyExecResultShape(invalidExecPayload.data.result);
461 
462-    const tccExecResponse = await withMockedPlatform("darwin", () =>
463-      handleConductorHttpRequest(
464-        {
465-          body: JSON.stringify({
466-            command: "pwd",
467-            cwd: join(homedir(), "Desktop", "project"),
468-            timeoutMs: 2000
469-          }),
470-          method: "POST",
471-          path: "/v1/exec"
472-        },
473-        {
474-          repository,
475-          snapshotLoader: () => snapshot
476-        }
477-      );
478-      assert.equal(execResponse.status, 200);
479-      const execPayload = parseJsonBody(execResponse);
480-      assert.equal(execPayload.data.ok, true);
481-      assert.equal(execPayload.data.operation, "exec");
482-      assert.equal(execPayload.data.result.stdout, "host-http-ok");
483-
484     const systemStateResponse = await handleConductorHttpRequest(
485       {
486         method: "GET",
487@@ -1018,15 +1343,56 @@ test("handleConductorHttpRequest serves the migrated local business endpoints fr
488       localApiContext
489     );
490     assert.equal(parseJsonBody(systemStateResponse).data.mode, "paused");
491-    } finally {
492-      rmSync(hostOpsDir, {
493-        force: true,
494-        recursive: true
495-      });
496+  } finally {
497+    await codexd.stop();
498+    rmSync(hostOpsDir, {
499+      force: true,
500+      recursive: true
501+    });
502   }
503-);
504+
505+  assert.deepEqual(
506+    codexd.requests.map((request) => request.path),
507+    [
508+      "/v1/codexd/status",
509+      "/v1/codexd/sessions",
510+      "/v1/codexd/sessions/session-demo",
511+      "/v1/codexd/sessions",
512+      "/v1/codexd/turn"
513+    ]
514+  );
515+  assert.equal(codexd.requests[3].body.model, "gpt-5.4");
516+  assert.equal(codexd.requests[4].body.sessionId, "session-demo");
517+});
518+
519+test("handleConductorHttpRequest returns a codexd-specific availability error when the proxy target is down", async () => {
520+  const { repository, snapshot } = await createLocalApiFixture();
521+  snapshot.codexd.localApiBase = "http://127.0.0.1:65535";
522+
523+  const response = await handleConductorHttpRequest(
524+    {
525+      method: "GET",
526+      path: "/v1/codex"
527+    },
528+    {
529+      codexdLocalApiBase: snapshot.codexd.localApiBase,
530+      fetchImpl: async () => {
531+        throw new Error("connect ECONNREFUSED");
532+      },
533+      repository,
534+      snapshotLoader: () => snapshot
535+    }
536+  );
537+
538+  assert.equal(response.status, 503);
539+  const payload = parseJsonBody(response);
540+  assert.equal(payload.error, "codexd_unavailable");
541+  assert.match(payload.message, /Independent codexd is unavailable/u);
542+  assert.doesNotMatch(payload.message, /bridge/u);
543+});
544 
545 test("ConductorRuntime serves health and migrated local API endpoints over HTTP", async () => {
546+  const codexd = await startCodexdStubServer();
547   const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-runtime-"));
548   const hostOpsDir = mkdtempSync(join(tmpdir(), "baa-conductor-runtime-host-"));
549   const runtime = new ConductorRuntime(
550@@ -1035,6 +1401,7 @@ test("ConductorRuntime serves health and migrated local API endpoints over HTTP"
551       host: "mini",
552       role: "primary",
553       controlApiBase: "https://control.example.test",
554+      codexdLocalApiBase: codexd.baseUrl,
555       localApiBase: "http://127.0.0.1:0",
556       sharedToken: "replace-me",
557       paths: {
558@@ -1048,170 +1415,217 @@ test("ConductorRuntime serves health and migrated local API endpoints over HTTP"
559     }
560   );
561 
562-  const snapshot = await runtime.start();
563-  assert.equal(snapshot.daemon.schedulerEnabled, true);
564-  assert.match(snapshot.controlApi.localApiBase, /^http:\/\/127\.0\.0\.1:\d+$/u);
565-  assert.match(snapshot.controlApi.firefoxWsUrl, /^ws:\/\/127\.0\.0\.1:\d+\/ws\/firefox$/u);
566+  try {
567+    const snapshot = await runtime.start();
568+    assert.equal(snapshot.daemon.schedulerEnabled, true);
569+    assert.equal(snapshot.codexd.localApiBase, codexd.baseUrl);
570+    assert.match(snapshot.controlApi.localApiBase, /^http:\/\/127\.0\.0\.1:\d+$/u);
571+    assert.match(snapshot.controlApi.firefoxWsUrl, /^ws:\/\/127\.0\.0\.1:\d+\/ws\/firefox$/u);
572+
573+    const baseUrl = snapshot.controlApi.localApiBase;
574+    const hostOpsHeaders = {
575+      authorization: "Bearer replace-me",
576+      "content-type": "application/json"
577+    };
578 
579-  const baseUrl = snapshot.controlApi.localApiBase;
580-  const hostOpsHeaders = {
581-    authorization: "Bearer replace-me",
582-    "content-type": "application/json"
583-  };
584+    const healthResponse = await fetch(`${baseUrl}/healthz`);
585+    assert.equal(healthResponse.status, 200);
586+    assert.equal(await healthResponse.text(), "ok\n");
587+
588+    const apiHealthResponse = await fetch(`${baseUrl}/health`);
589+    assert.equal(apiHealthResponse.status, 200);
590+    const apiHealthPayload = await apiHealthResponse.json();
591+    assert.equal(apiHealthPayload.ok, true);
592+    assert.equal(apiHealthPayload.data.status, "ok");
593+
594+    const readyResponse = await fetch(`${baseUrl}/readyz`);
595+    assert.equal(readyResponse.status, 200);
596+    assert.equal(await readyResponse.text(), "ready\n");
597+
598+    const roleResponse = await fetch(`${baseUrl}/rolez`);
599+    assert.equal(roleResponse.status, 200);
600+    assert.equal(await roleResponse.text(), "leader\n");
601+
602+    const runtimeResponse = await fetch(`${baseUrl}/v1/runtime`);
603+    assert.equal(runtimeResponse.status, 200);
604+    const payload = await runtimeResponse.json();
605+    assert.equal(payload.ok, true);
606+    assert.equal(payload.data.identity, "mini-main@mini(primary)");
607+    assert.equal(payload.data.controlApi.firefoxWsUrl, snapshot.controlApi.firefoxWsUrl);
608+    assert.equal(payload.data.controlApi.localApiBase, baseUrl);
609+    assert.equal(payload.data.runtime.started, true);
610+
611+    const systemStateResponse = await fetch(`${baseUrl}/v1/system/state`);
612+    assert.equal(systemStateResponse.status, 200);
613+    const systemStatePayload = await systemStateResponse.json();
614+    assert.equal(systemStatePayload.ok, true);
615+    assert.equal(systemStatePayload.data.holder_id, "mini-main");
616+    assert.equal(systemStatePayload.data.mode, "running");
617+
618+    const codexStatusResponse = await fetch(`${baseUrl}/v1/codex`);
619+    assert.equal(codexStatusResponse.status, 200);
620+    const codexStatusPayload = await codexStatusResponse.json();
621+    assert.equal(codexStatusPayload.data.proxy.target_base_url, codexd.baseUrl);
622+    assert.equal(codexStatusPayload.data.sessions.count, 1);
623+
624+    const codexSessionsResponse = await fetch(`${baseUrl}/v1/codex/sessions`);
625+    assert.equal(codexSessionsResponse.status, 200);
626+    const codexSessionsPayload = await codexSessionsResponse.json();
627+    assert.equal(codexSessionsPayload.data.sessions[0].sessionId, "session-demo");
628+
629+    const codexSessionCreateResponse = await fetch(`${baseUrl}/v1/codex/sessions`, {
630+      method: "POST",
631+      headers: {
632+        "content-type": "application/json"
633+      },
634+      body: JSON.stringify({
635+        cwd: "/Users/george/code/baa-conductor",
636+        purpose: "duplex"
637+      })
638+    });
639+    assert.equal(codexSessionCreateResponse.status, 201);
640+    const codexSessionCreatePayload = await codexSessionCreateResponse.json();
641+    assert.equal(codexSessionCreatePayload.data.session.sessionId, "session-2");
642 
643-  const healthResponse = await fetch(`${baseUrl}/healthz`);
644-  assert.equal(healthResponse.status, 200);
645-  assert.equal(await healthResponse.text(), "ok\n");
646+    const codexTurnResponse = await fetch(`${baseUrl}/v1/codex/turn`, {
647+      method: "POST",
648+      headers: {
649+        "content-type": "application/json"
650+      },
651+      body: JSON.stringify({
652+        input: "Continue.",
653+        sessionId: "session-demo"
654+      })
655+    });
656+    assert.equal(codexTurnResponse.status, 202);
657+    const codexTurnPayload = await codexTurnResponse.json();
658+    assert.equal(codexTurnPayload.data.accepted, true);
659+    assert.equal(codexTurnPayload.data.turnId, "turn-created");
660 
661-  const apiHealthResponse = await fetch(`${baseUrl}/health`);
662-  assert.equal(apiHealthResponse.status, 200);
663-  const apiHealthPayload = await apiHealthResponse.json();
664-  assert.equal(apiHealthPayload.ok, true);
665-  assert.equal(apiHealthPayload.data.status, "ok");
666+    const pauseResponse = await fetch(`${baseUrl}/v1/system/pause`, {
667+      method: "POST",
668+      headers: {
669+        "content-type": "application/json"
670+      },
671+      body: JSON.stringify({
672+        requested_by: "integration_test",
673+        reason: "pause_for_verification"
674+      })
675+    });
676+    assert.equal(pauseResponse.status, 200);
677+    const pausePayload = await pauseResponse.json();
678+    assert.equal(pausePayload.data.mode, "paused");
679 
680-  const readyResponse = await fetch(`${baseUrl}/readyz`);
681-  assert.equal(readyResponse.status, 200);
682-  assert.equal(await readyResponse.text(), "ready\n");
683+    const pausedStateResponse = await fetch(`${baseUrl}/v1/system/state`);
684+    const pausedStatePayload = await pausedStateResponse.json();
685+    assert.equal(pausedStatePayload.data.mode, "paused");
686 
687-  const roleResponse = await fetch(`${baseUrl}/rolez`);
688-  assert.equal(roleResponse.status, 200);
689-  assert.equal(await roleResponse.text(), "leader\n");
690+    const describeResponse = await fetch(`${baseUrl}/describe`);
691+    assert.equal(describeResponse.status, 200);
692+    const describePayload = await describeResponse.json();
693+    assert.equal(describePayload.ok, true);
694+    assert.equal(describePayload.data.name, "baa-conductor-daemon");
695+    assert.equal(describePayload.data.codex.target_base_url, codexd.baseUrl);
696 
697-  const runtimeResponse = await fetch(`${baseUrl}/v1/runtime`);
698-  assert.equal(runtimeResponse.status, 200);
699-  const payload = await runtimeResponse.json();
700-  assert.equal(payload.ok, true);
701-  assert.equal(payload.data.identity, "mini-main@mini(primary)");
702-  assert.equal(payload.data.controlApi.firefoxWsUrl, snapshot.controlApi.firefoxWsUrl);
703-  assert.equal(payload.data.controlApi.localApiBase, baseUrl);
704-  assert.equal(payload.data.runtime.started, true);
705+    const businessDescribeResponse = await fetch(`${baseUrl}/describe/business`);
706+    assert.equal(businessDescribeResponse.status, 200);
707+    const businessDescribePayload = await businessDescribeResponse.json();
708+    assert.equal(businessDescribePayload.data.surface, "business");
709+    assert.match(JSON.stringify(businessDescribePayload.data.endpoints), /\/v1\/codex/u);
710 
711-  const systemStateResponse = await fetch(`${baseUrl}/v1/system/state`);
712-  assert.equal(systemStateResponse.status, 200);
713-  const systemStatePayload = await systemStateResponse.json();
714-  assert.equal(systemStatePayload.ok, true);
715-  assert.equal(systemStatePayload.data.holder_id, "mini-main");
716-  assert.equal(systemStatePayload.data.mode, "running");
717-
718-  const pauseResponse = await fetch(`${baseUrl}/v1/system/pause`, {
719-    method: "POST",
720-    headers: {
721-      "content-type": "application/json"
722-    },
723-    body: JSON.stringify({
724-      requested_by: "integration_test",
725-      reason: "pause_for_verification"
726-    })
727-  });
728-  assert.equal(pauseResponse.status, 200);
729-  const pausePayload = await pauseResponse.json();
730-  assert.equal(pausePayload.data.mode, "paused");
731-
732-  const pausedStateResponse = await fetch(`${baseUrl}/v1/system/state`);
733-  const pausedStatePayload = await pausedStateResponse.json();
734-  assert.equal(pausedStatePayload.data.mode, "paused");
735-
736-  const describeResponse = await fetch(`${baseUrl}/describe`);
737-  assert.equal(describeResponse.status, 200);
738-  const describePayload = await describeResponse.json();
739-  assert.equal(describePayload.ok, true);
740-  assert.equal(describePayload.data.name, "baa-conductor-daemon");
741-
742-  const businessDescribeResponse = await fetch(`${baseUrl}/describe/business`);
743-  assert.equal(businessDescribeResponse.status, 200);
744-  const businessDescribePayload = await businessDescribeResponse.json();
745-  assert.equal(businessDescribePayload.data.surface, "business");
746-
747-  const controlDescribeResponse = await fetch(`${baseUrl}/describe/control`);
748-  assert.equal(controlDescribeResponse.status, 200);
749-  const controlDescribePayload = await controlDescribeResponse.json();
750-  assert.equal(controlDescribePayload.data.surface, "control");
751-  assert.match(JSON.stringify(controlDescribePayload.data.endpoints), /\/v1\/exec/u);
752-  assert.equal(controlDescribePayload.data.host_operations.auth.header, "Authorization: Bearer <BAA_SHARED_TOKEN>");
753-
754-  const unauthorizedExecResponse = await fetch(`${baseUrl}/v1/exec`, {
755-    method: "POST",
756-    headers: {
757-      "content-type": "application/json"
758-    },
759-    body: JSON.stringify({
760-      command: "printf 'runtime-host-op'",
761-      cwd: hostOpsDir,
762-      timeoutMs: 2000
763-    })
764-  });
765-  assert.equal(unauthorizedExecResponse.status, 401);
766-  const unauthorizedExecPayload = await unauthorizedExecResponse.json();
767-  assert.equal(unauthorizedExecPayload.error, "unauthorized");
768-
769-  const execResponse = await fetch(`${baseUrl}/v1/exec`, {
770-    method: "POST",
771-    headers: hostOpsHeaders,
772-    body: JSON.stringify({
773-      command: "printf 'runtime-host-op'",
774-      cwd: hostOpsDir,
775-      timeoutMs: 2000
776-    })
777-  });
778-  assert.equal(execResponse.status, 200);
779-  const execPayload = await execResponse.json();
780-  assert.equal(execPayload.data.ok, true);
781-  assert.equal(execPayload.data.result.stdout, "runtime-host-op");
782-
783-  const writeResponse = await fetch(`${baseUrl}/v1/files/write`, {
784-    method: "POST",
785-    headers: hostOpsHeaders,
786-    body: JSON.stringify({
787-      path: "runtime/demo.txt",
788-      cwd: hostOpsDir,
789-      content: "hello from runtime host ops",
790-      overwrite: false,
791-      createParents: true
792-    })
793-  });
794-  assert.equal(writeResponse.status, 200);
795-  const writePayload = await writeResponse.json();
796-  assert.equal(writePayload.data.ok, true);
797-  assert.equal(writePayload.data.result.created, true);
798-
799-  const duplicateWriteResponse = await fetch(`${baseUrl}/v1/files/write`, {
800-    method: "POST",
801-    headers: hostOpsHeaders,
802-    body: JSON.stringify({
803-      path: "runtime/demo.txt",
804-      cwd: hostOpsDir,
805-      content: "should not overwrite",
806-      overwrite: false
807-    })
808-  });
809-  assert.equal(duplicateWriteResponse.status, 200);
810-  const duplicateWritePayload = await duplicateWriteResponse.json();
811-  assert.equal(duplicateWritePayload.data.ok, false);
812-  assert.equal(duplicateWritePayload.data.error.code, "FILE_ALREADY_EXISTS");
813-
814-  const readResponse = await fetch(`${baseUrl}/v1/files/read`, {
815-    method: "POST",
816-    headers: hostOpsHeaders,
817-    body: JSON.stringify({
818-      path: "runtime/demo.txt",
819-      cwd: hostOpsDir
820-    })
821-  });
822-  assert.equal(readResponse.status, 200);
823-  const readPayload = await readResponse.json();
824-  assert.equal(readPayload.data.ok, true);
825-  assert.equal(readPayload.data.result.content, "hello from runtime host ops");
826+    const controlDescribeResponse = await fetch(`${baseUrl}/describe/control`);
827+    assert.equal(controlDescribeResponse.status, 200);
828+    const controlDescribePayload = await controlDescribeResponse.json();
829+    assert.equal(controlDescribePayload.data.surface, "control");
830+    assert.match(JSON.stringify(controlDescribePayload.data.endpoints), /\/v1\/exec/u);
831+    assert.equal(controlDescribePayload.data.host_operations.auth.header, "Authorization: Bearer <BAA_SHARED_TOKEN>");
832 
833-  const stoppedSnapshot = await runtime.stop();
834-  assert.equal(stoppedSnapshot.runtime.started, false);
835-  rmSync(stateDir, {
836-    force: true,
837-    recursive: true
838-  });
839-  rmSync(hostOpsDir, {
840-    force: true,
841-    recursive: true
842-  });
843+    const unauthorizedExecResponse = await fetch(`${baseUrl}/v1/exec`, {
844+      method: "POST",
845+      headers: {
846+        "content-type": "application/json"
847+      },
848+      body: JSON.stringify({
849+        command: "printf 'runtime-host-op'",
850+        cwd: hostOpsDir,
851+        timeoutMs: 2000
852+      })
853+    });
854+    assert.equal(unauthorizedExecResponse.status, 401);
855+    const unauthorizedExecPayload = await unauthorizedExecResponse.json();
856+    assert.equal(unauthorizedExecPayload.error, "unauthorized");
857+
858+    const execResponse = await fetch(`${baseUrl}/v1/exec`, {
859+      method: "POST",
860+      headers: hostOpsHeaders,
861+      body: JSON.stringify({
862+        command: "printf 'runtime-host-op'",
863+        cwd: hostOpsDir,
864+        timeoutMs: 2000
865+      })
866+    });
867+    assert.equal(execResponse.status, 200);
868+    const execPayload = await execResponse.json();
869+    assert.equal(execPayload.data.ok, true);
870+    assert.equal(execPayload.data.result.stdout, "runtime-host-op");
871+
872+    const writeResponse = await fetch(`${baseUrl}/v1/files/write`, {
873+      method: "POST",
874+      headers: hostOpsHeaders,
875+      body: JSON.stringify({
876+        path: "runtime/demo.txt",
877+        cwd: hostOpsDir,
878+        content: "hello from runtime host ops",
879+        overwrite: false,
880+        createParents: true
881+      })
882+    });
883+    assert.equal(writeResponse.status, 200);
884+    const writePayload = await writeResponse.json();
885+    assert.equal(writePayload.data.ok, true);
886+    assert.equal(writePayload.data.result.created, true);
887+
888+    const duplicateWriteResponse = await fetch(`${baseUrl}/v1/files/write`, {
889+      method: "POST",
890+      headers: hostOpsHeaders,
891+      body: JSON.stringify({
892+        path: "runtime/demo.txt",
893+        cwd: hostOpsDir,
894+        content: "should not overwrite",
895+        overwrite: false
896+      })
897+    });
898+    assert.equal(duplicateWriteResponse.status, 200);
899+    const duplicateWritePayload = await duplicateWriteResponse.json();
900+    assert.equal(duplicateWritePayload.data.ok, false);
901+    assert.equal(duplicateWritePayload.data.error.code, "FILE_ALREADY_EXISTS");
902+
903+    const readResponse = await fetch(`${baseUrl}/v1/files/read`, {
904+      method: "POST",
905+      headers: hostOpsHeaders,
906+      body: JSON.stringify({
907+        path: "runtime/demo.txt",
908+        cwd: hostOpsDir
909+      })
910+    });
911+    assert.equal(readResponse.status, 200);
912+    const readPayload = await readResponse.json();
913+    assert.equal(readPayload.data.ok, true);
914+    assert.equal(readPayload.data.result.content, "hello from runtime host ops");
915+
916+    const stoppedSnapshot = await runtime.stop();
917+    assert.equal(stoppedSnapshot.runtime.started, false);
918+  } finally {
919+    await codexd.stop();
920+    rmSync(stateDir, {
921+      force: true,
922+      recursive: true
923+    });
924+    rmSync(hostOpsDir, {
925+      force: true,
926+      recursive: true
927+    });
928+  }
929 });
930 
931 test("ConductorRuntime exposes a minimal runtime snapshot for CLI and status surfaces", async () => {
M apps/conductor-daemon/src/index.ts
+34, -0
  1@@ -109,6 +109,7 @@ export interface ConductorRuntimePaths {
  2 }
  3 
  4 export interface ConductorRuntimeConfig extends ConductorConfig {
  5+  codexdLocalApiBase?: string | null;
  6   localApiAllowedHosts?: readonly string[] | string | null;
  7   localApiBase?: string | null;
  8   paths?: Partial<ConductorRuntimePaths>;
  9@@ -120,6 +121,7 @@ export interface ResolvedConductorRuntimeConfig extends ConductorConfig {
 10   leaseRenewIntervalMs: number;
 11   localApiAllowedHosts: string[];
 12   leaseTtlSec: number;
 13+  codexdLocalApiBase: string | null;
 14   localApiBase: string | null;
 15   paths: ConductorRuntimePaths;
 16   preferred: boolean;
 17@@ -165,6 +167,9 @@ export interface ConductorRuntimeSnapshot {
 18     hasSharedToken: boolean;
 19     usesPlaceholderToken: boolean;
 20   };
 21+  codexd: {
 22+    localApiBase: string | null;
 23+  };
 24   runtime: {
 25     pid: number | null;
 26     started: boolean;
 27@@ -291,6 +296,7 @@ interface LocalApiListenConfig {
 28 }
 29 
 30 interface CliValueOverrides {
 31+  codexdLocalApiBase?: string;
 32   controlApiBase?: string;
 33   heartbeatIntervalMs?: string;
 34   host?: string;
 35@@ -553,6 +559,8 @@ function normalizeIncomingRequestHeaders(
 36 }
 37 
 38 class ConductorLocalHttpServer {
 39+  private readonly codexdLocalApiBase: string | null;
 40+  private readonly fetchImpl: typeof fetch;
 41   private readonly firefoxWebSocketServer: ConductorFirefoxWebSocketServer;
 42   private readonly localApiBase: string;
 43   private readonly now: () => number;
 44@@ -567,10 +575,14 @@ class ConductorLocalHttpServer {
 45     localApiBase: string,
 46     repository: ControlPlaneRepository,
 47     snapshotLoader: () => ConductorRuntimeSnapshot,
 48+    codexdLocalApiBase: string | null,
 49+    fetchImpl: typeof fetch,
 50     sharedToken: string | null,
 51     version: string | null,
 52     now: () => number
 53   ) {
 54+    this.codexdLocalApiBase = codexdLocalApiBase;
 55+    this.fetchImpl = fetchImpl;
 56     this.localApiBase = localApiBase;
 57     this.now = now;
 58     this.repository = repository;
 59@@ -610,6 +622,8 @@ class ConductorLocalHttpServer {
 60             path: request.url ?? "/"
 61           },
 62           {
 63+            codexdLocalApiBase: this.codexdLocalApiBase,
 64+            fetchImpl: this.fetchImpl,
 65             repository: this.repository,
 66             sharedToken: this.sharedToken,
 67             snapshotLoader: this.snapshotLoader,
 68@@ -1396,6 +1410,7 @@ export function resolveConductorRuntimeConfig(
 69     host,
 70     role: parseConductorRole("Conductor role", config.role),
 71     controlApiBase: normalizeBaseUrl(controlApiBase),
 72+    codexdLocalApiBase: resolveLocalApiBase(config.codexdLocalApiBase),
 73     heartbeatIntervalMs,
 74     leaseRenewIntervalMs,
 75     localApiAllowedHosts,
 76@@ -1475,6 +1490,9 @@ function resolveRuntimeConfigFromSources(
 77       overrides.renewFailureThreshold ?? env.BAA_CONDUCTOR_RENEW_FAILURE_THRESHOLD,
 78       { minimum: 1 }
 79     ),
 80+    codexdLocalApiBase: normalizeOptionalString(
 81+      overrides.codexdLocalApiBase ?? env.BAA_CODEXD_LOCAL_API_BASE
 82+    ),
 83     localApiAllowedHosts: overrides.localApiAllowedHosts ?? env.BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS,
 84     localApiBase: normalizeOptionalString(overrides.localApiBase ?? env.BAA_CONDUCTOR_LOCAL_API),
 85     sharedToken: normalizeOptionalString(overrides.sharedToken ?? env.BAA_SHARED_TOKEN),
 86@@ -1546,6 +1564,10 @@ export function parseConductorCliRequest(
 87         overrides.controlApiBase = readOptionValue(tokens, token, index);
 88         index += 1;
 89         break;
 90+      case "--codexd-local-api":
 91+        overrides.codexdLocalApiBase = readOptionValue(tokens, token, index);
 92+        index += 1;
 93+        break;
 94       case "--local-api":
 95         overrides.localApiBase = readOptionValue(tokens, token, index);
 96         index += 1;
 97@@ -1649,6 +1671,10 @@ function buildRuntimeWarnings(config: ResolvedConductorRuntimeConfig): string[]
 98     warnings.push("BAA_CONDUCTOR_LOCAL_API is not configured; only the in-process snapshot interface is available.");
 99   }
100 
101+  if (config.codexdLocalApiBase == null) {
102+    warnings.push("BAA_CODEXD_LOCAL_API_BASE is not configured; /v1/codex routes will stay unavailable.");
103+  }
104+
105   if (config.leaseRenewIntervalMs >= config.leaseTtlSec * 1_000) {
106     warnings.push("lease renew interval is >= lease TTL; leader renewals may race with lease expiry.");
107   }
108@@ -1677,6 +1703,7 @@ function formatConfigText(config: ResolvedConductorRuntimeConfig): string {
109   return [
110     `identity: ${config.nodeId}@${config.host}(${config.role})`,
111     `control_api_base: ${config.controlApiBase}`,
112+    `codexd_local_api_base: ${config.codexdLocalApiBase ?? "not-configured"}`,
113     `local_api_base: ${config.localApiBase ?? "not-configured"}`,
114     `firefox_ws_url: ${buildFirefoxWebSocketUrl(config.localApiBase) ?? "not-configured"}`,
115     `local_api_allowed_hosts: ${config.localApiAllowedHosts.join(",") || "loopback-only"}`,
116@@ -1721,6 +1748,7 @@ function getUsageText(): string {
117     "  --host <host>",
118     "  --role <primary|standby>",
119     "  --control-api-base <url>",
120+    "  --codexd-local-api <url>",
121     "  --local-api <url>",
122     "  --shared-token <token>",
123     "  --priority <integer>",
124@@ -1744,6 +1772,7 @@ function getUsageText(): string {
125     "  BAA_CONDUCTOR_HOST",
126     "  BAA_CONDUCTOR_ROLE",
127     "  BAA_CONTROL_API_BASE",
128+    "  BAA_CODEXD_LOCAL_API_BASE",
129     "  BAA_CONDUCTOR_LOCAL_API",
130     "  BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS",
131     "  BAA_SHARED_TOKEN",
132@@ -1796,6 +1825,8 @@ export class ConductorRuntime {
133             this.config.localApiBase,
134             this.localControlPlane.repository,
135             () => this.getRuntimeSnapshot(),
136+            this.config.codexdLocalApiBase,
137+            options.fetchImpl ?? globalThis.fetch,
138             this.config.sharedToken,
139             this.config.version,
140             this.now
141@@ -1849,6 +1880,9 @@ export class ConductorRuntime {
142         hasSharedToken: this.config.sharedToken != null,
143         usesPlaceholderToken: usesPlaceholderToken(this.config.sharedToken)
144       },
145+      codexd: {
146+        localApiBase: this.config.codexdLocalApiBase
147+      },
148       runtime: {
149         pid: getProcessLike()?.pid ?? null,
150         started: this.started,
M apps/conductor-daemon/src/local-api.ts
+502, -15
  1@@ -36,6 +36,14 @@ const DEFAULT_LOG_LIMIT = 200;
  2 const MAX_LIST_LIMIT = 100;
  3 const MAX_LOG_LIMIT = 500;
  4 const TASK_STATUS_SET = new Set<TaskStatus>(TASK_STATUS_VALUES);
  5+const CODEXD_LOCAL_API_ENV = "BAA_CODEXD_LOCAL_API_BASE";
  6+const CODEX_ROUTE_IDS = new Set([
  7+  "codex.status",
  8+  "codex.sessions.list",
  9+  "codex.sessions.read",
 10+  "codex.sessions.create",
 11+  "codex.turn.create"
 12+]);
 13 const HOST_OPERATIONS_ROUTE_IDS = new Set(["host.exec", "host.files.read", "host.files.write"]);
 14 const HOST_OPERATIONS_AUTH_HEADER = "Authorization: Bearer <BAA_SHARED_TOKEN>";
 15 const HOST_OPERATIONS_WWW_AUTHENTICATE = 'Bearer realm="baa-conductor-host-ops"';
 16@@ -63,7 +71,21 @@ interface LocalApiRouteMatch {
 17   route: LocalApiRouteDefinition;
 18 }
 19 
 20+type UpstreamSuccessEnvelope = JsonObject & {
 21+  data: JsonValue;
 22+  ok: true;
 23+};
 24+
 25+type UpstreamErrorEnvelope = JsonObject & {
 26+  details?: JsonValue;
 27+  error: string;
 28+  message: string;
 29+  ok: false;
 30+};
 31+
 32 interface LocalApiRequestContext {
 33+  codexdLocalApiBase: string | null;
 34+  fetchImpl: typeof fetch;
 35   now: () => number;
 36   params: Record<string, string>;
 37   repository: ControlPlaneRepository | null;
 38@@ -75,6 +97,9 @@ interface LocalApiRequestContext {
 39 }
 40 
 41 export interface ConductorRuntimeApiSnapshot {
 42+  codexd: {
 43+    localApiBase: string | null;
 44+  };
 45   controlApi: {
 46     baseUrl: string;
 47     firefoxWsUrl?: string | null;
 48@@ -103,6 +128,8 @@ export interface ConductorRuntimeApiSnapshot {
 49 }
 50 
 51 export interface ConductorLocalApiContext {
 52+  codexdLocalApiBase?: string | null;
 53+  fetchImpl?: typeof fetch;
 54   now?: () => number;
 55   repository: ControlPlaneRepository | null;
 56   sharedToken?: string | null;
 57@@ -197,6 +224,41 @@ const LOCAL_API_ROUTES: LocalApiRouteDefinition[] = [
 58     pathPattern: "/v1/capabilities",
 59     summary: "读取能力发现摘要"
 60   },
 61+  {
 62+    id: "codex.status",
 63+    kind: "read",
 64+    method: "GET",
 65+    pathPattern: "/v1/codex",
 66+    summary: "读取独立 codexd 代理状态与会话能力摘要"
 67+  },
 68+  {
 69+    id: "codex.sessions.list",
 70+    kind: "read",
 71+    method: "GET",
 72+    pathPattern: "/v1/codex/sessions",
 73+    summary: "列出独立 codexd 当前会话"
 74+  },
 75+  {
 76+    id: "codex.sessions.read",
 77+    kind: "read",
 78+    method: "GET",
 79+    pathPattern: "/v1/codex/sessions/:session_id",
 80+    summary: "读取独立 codexd 的单个会话"
 81+  },
 82+  {
 83+    id: "codex.sessions.create",
 84+    kind: "write",
 85+    method: "POST",
 86+    pathPattern: "/v1/codex/sessions",
 87+    summary: "通过 conductor 代理创建独立 codexd 会话"
 88+  },
 89+  {
 90+    id: "codex.turn.create",
 91+    kind: "write",
 92+    method: "POST",
 93+    pathPattern: "/v1/codex/turn",
 94+    summary: "向独立 codexd 会话提交 turn"
 95+  },
 96   {
 97     id: "system.state",
 98     kind: "read",
 99@@ -276,6 +338,7 @@ const LOCAL_API_ROUTES: LocalApiRouteDefinition[] = [
100   },
101   {
102     id: "runs.list",
103+    exposeInDescribe: false,
104     kind: "read",
105     method: "GET",
106     pathPattern: "/v1/runs",
107@@ -283,6 +346,7 @@ const LOCAL_API_ROUTES: LocalApiRouteDefinition[] = [
108   },
109   {
110     id: "runs.read",
111+    exposeInDescribe: false,
112     kind: "read",
113     method: "GET",
114     pathPattern: "/v1/runs/:run_id",
115@@ -455,6 +519,82 @@ function buildFileWriteOperationRequest(body: JsonObject): FileWriteOperationReq
116   };
117 }
118 
119+function buildCodexSessionCreateRequest(body: JsonObject): JsonObject {
120+  return compactJsonObject({
121+    approvalPolicy: readBodyField(body, "approvalPolicy", "approval_policy"),
122+    baseInstructions: readBodyField(body, "baseInstructions", "base_instructions"),
123+    config: readBodyField(body, "config"),
124+    cwd: readBodyField(body, "cwd"),
125+    developerInstructions: readBodyField(body, "developerInstructions", "developer_instructions"),
126+    ephemeral: readBodyField(body, "ephemeral"),
127+    metadata: readBodyField(body, "metadata"),
128+    model: readBodyField(body, "model"),
129+    modelProvider: readBodyField(body, "modelProvider", "model_provider"),
130+    personality: readBodyField(body, "personality"),
131+    purpose: readBodyField(body, "purpose"),
132+    sandbox: readBodyField(body, "sandbox"),
133+    serviceTier: readBodyField(body, "serviceTier", "service_tier"),
134+    threadId: readBodyField(body, "threadId", "thread_id")
135+  });
136+}
137+
138+function buildCodexTurnCreateRequest(body: JsonObject): JsonObject {
139+  return compactJsonObject({
140+    approvalPolicy: readBodyField(body, "approvalPolicy", "approval_policy"),
141+    collaborationMode: readBodyField(body, "collaborationMode", "collaboration_mode"),
142+    cwd: readBodyField(body, "cwd"),
143+    effort: readBodyField(body, "effort"),
144+    expectedTurnId: readBodyField(body, "expectedTurnId", "expected_turn_id"),
145+    input: readBodyField(body, "input", "prompt"),
146+    model: readBodyField(body, "model"),
147+    outputSchema: readBodyField(body, "outputSchema", "output_schema"),
148+    personality: readBodyField(body, "personality"),
149+    sandboxPolicy: readBodyField(body, "sandboxPolicy", "sandbox_policy"),
150+    serviceTier: readBodyField(body, "serviceTier", "service_tier"),
151+    sessionId: readBodyField(body, "sessionId", "session_id"),
152+    summary: readBodyField(body, "summary")
153+  });
154+}
155+
156+function compactJsonObject(record: Record<string, JsonValue | undefined>): JsonObject {
157+  const result: JsonObject = {};
158+
159+  for (const [key, value] of Object.entries(record)) {
160+    if (value !== undefined) {
161+      result[key] = value;
162+    }
163+  }
164+
165+  return result;
166+}
167+
168+function isUpstreamSuccessEnvelope(value: JsonValue | null): value is UpstreamSuccessEnvelope {
169+  return isJsonObject(value) && value.ok === true && "data" in value;
170+}
171+
172+function isUpstreamErrorEnvelope(value: JsonValue | null): value is UpstreamErrorEnvelope {
173+  return (
174+    isJsonObject(value)
175+    && value.ok === false
176+    && typeof value.error === "string"
177+    && typeof value.message === "string"
178+  );
179+}
180+
181+function isCodexRoute(route: LocalApiRouteDefinition): boolean {
182+  return CODEX_ROUTE_IDS.has(route.id);
183+}
184+
185+function requireRouteDefinition(id: string): LocalApiRouteDefinition {
186+  const route = LOCAL_API_ROUTES.find((entry) => entry.id === id);
187+
188+  if (!route) {
189+    throw new Error(`Unknown local route definition "${id}".`);
190+  }
191+
192+  return route;
193+}
194+
195 function readPositiveIntegerQuery(
196   url: URL,
197   fieldName: string,
198@@ -667,6 +807,256 @@ export async function buildSystemStateData(repository: ControlPlaneRepository):
199   };
200 }
201 
202+function asJsonObject(value: JsonValue | null | undefined): JsonObject | null {
203+  return isJsonObject(value ?? null) ? (value as JsonObject) : null;
204+}
205+
206+function readJsonObjectField(record: JsonObject | null, fieldName: string): JsonObject | null {
207+  if (record == null) {
208+    return null;
209+  }
210+
211+  return asJsonObject(record[fieldName]);
212+}
213+
214+function readJsonArrayField(record: JsonObject | null, fieldName: string): JsonValue[] {
215+  if (record == null) {
216+    return [];
217+  }
218+
219+  const value = record[fieldName];
220+  return Array.isArray(value) ? value : [];
221+}
222+
223+function readStringValue(record: JsonObject | null, fieldName: string): string | null {
224+  if (record == null) {
225+    return null;
226+  }
227+
228+  const value = record[fieldName];
229+  return typeof value === "string" ? value : null;
230+}
231+
232+function readBooleanValue(record: JsonObject | null, fieldName: string): boolean | null {
233+  if (record == null) {
234+    return null;
235+  }
236+
237+  const value = record[fieldName];
238+  return typeof value === "boolean" ? value : null;
239+}
240+
241+function readNumberValue(record: JsonObject | null, fieldName: string): number | null {
242+  if (record == null) {
243+    return null;
244+  }
245+
246+  const value = record[fieldName];
247+  return typeof value === "number" && Number.isFinite(value) ? value : null;
248+}
249+
250+function buildCodexRouteCatalog(): JsonObject[] {
251+  return LOCAL_API_ROUTES.filter((route) => isCodexRoute(route)).map((route) => describeRoute(route));
252+}
253+
254+function buildCodexProxyNotes(snapshot: ConductorRuntimeApiSnapshot): string[] {
255+  if (snapshot.codexd.localApiBase == null) {
256+    return [
257+      "Codex routes stay unavailable until independent codexd is configured.",
258+      `Set ${CODEXD_LOCAL_API_ENV} to the codexd local HTTP base URL before using /v1/codex.`
259+    ];
260+  }
261+
262+  return [
263+    "All /v1/codex routes proxy the independent codexd daemon over local HTTP.",
264+    "This surface is session-oriented: status, session list/read/create, and turn create."
265+  ];
266+}
267+
268+function buildCodexProxyData(snapshot: ConductorRuntimeApiSnapshot): JsonObject {
269+  return {
270+    auth_mode: "local_network_only",
271+    backend: "independent_codexd",
272+    enabled: snapshot.codexd.localApiBase != null,
273+    route_prefix: "/v1/codex",
274+    routes: buildCodexRouteCatalog(),
275+    target_base_url: snapshot.codexd.localApiBase,
276+    transport: "local_http",
277+    notes: buildCodexProxyNotes(snapshot)
278+  };
279+}
280+
281+function buildCodexStatusData(data: JsonValue, proxyTargetBaseUrl: string): JsonObject {
282+  const root = asJsonObject(data);
283+  const service = readJsonObjectField(root, "service");
284+  const snapshot = readJsonObjectField(root, "snapshot");
285+  const identity = readJsonObjectField(snapshot, "identity");
286+  const daemon = readJsonObjectField(snapshot, "daemon");
287+  const child = readJsonObjectField(daemon, "child");
288+  const sessionRegistry = readJsonObjectField(snapshot, "sessionRegistry");
289+  const recentEvents = readJsonObjectField(snapshot, "recentEvents");
290+  const sessions = readJsonArrayField(sessionRegistry, "sessions");
291+  const events = readJsonArrayField(recentEvents, "events");
292+
293+  return {
294+    backend: "independent_codexd",
295+    daemon: {
296+      daemon_id: readStringValue(identity, "daemonId"),
297+      node_id: readStringValue(identity, "nodeId"),
298+      version: readStringValue(identity, "version"),
299+      started: readBooleanValue(daemon, "started"),
300+      started_at: readStringValue(daemon, "startedAt"),
301+      updated_at: readStringValue(daemon, "updatedAt"),
302+      child: {
303+        endpoint: readStringValue(child, "endpoint"),
304+        last_error: readStringValue(child, "lastError"),
305+        pid: readNumberValue(child, "pid"),
306+        status: readStringValue(child, "status"),
307+        strategy: readStringValue(child, "strategy")
308+      }
309+    },
310+    proxy: {
311+      route_prefix: "/v1/codex",
312+      target_base_url: proxyTargetBaseUrl,
313+      transport: "local_http"
314+    },
315+    recent_events: {
316+      count: events.length,
317+      updated_at: readStringValue(recentEvents, "updatedAt")
318+    },
319+    routes: buildCodexRouteCatalog(),
320+    service: {
321+      event_stream_url: readStringValue(service, "eventStreamUrl"),
322+      listening: readBooleanValue(service, "listening"),
323+      resolved_base_url: readStringValue(service, "resolvedBaseUrl"),
324+      websocket_clients: readNumberValue(service, "websocketClients")
325+    },
326+    sessions: {
327+      active_count: sessions.filter((entry) => readStringValue(asJsonObject(entry), "status") === "active").length,
328+      count: sessions.length,
329+      updated_at: readStringValue(sessionRegistry, "updatedAt")
330+    },
331+    surface: ["status", "sessions", "turn"],
332+    notes: [
333+      "This conductor route proxies the independent codexd daemon.",
334+      "Only session/status/turn operations are exposed on this surface."
335+    ]
336+  };
337+}
338+
339+async function requestCodexd(
340+  context: LocalApiRequestContext,
341+  input: {
342+    body?: JsonObject;
343+    method: LocalApiRouteMethod;
344+    path: string;
345+  }
346+): Promise<{ data: JsonValue; status: number }> {
347+  const codexdLocalApiBase =
348+    normalizeOptionalString(context.codexdLocalApiBase) ?? context.snapshotLoader().codexd.localApiBase;
349+
350+  if (codexdLocalApiBase == null) {
351+    throw new LocalApiHttpError(
352+      503,
353+      "codexd_not_configured",
354+      "Independent codexd local API is not configured for /v1/codex routes.",
355+      {
356+        env_var: CODEXD_LOCAL_API_ENV
357+      }
358+    );
359+  }
360+
361+  let response: Response;
362+
363+  try {
364+    response = await context.fetchImpl(`${codexdLocalApiBase}${input.path}`, {
365+      method: input.method,
366+      headers: input.body
367+        ? {
368+            accept: "application/json",
369+            "content-type": "application/json"
370+          }
371+        : {
372+            accept: "application/json"
373+          },
374+      body: input.body ? JSON.stringify(input.body) : undefined
375+    });
376+  } catch (error) {
377+    throw new LocalApiHttpError(
378+      503,
379+      "codexd_unavailable",
380+      `Independent codexd is unavailable at ${codexdLocalApiBase}.`,
381+      {
382+        cause: error instanceof Error ? error.message : String(error),
383+        target_base_url: codexdLocalApiBase,
384+        upstream_path: input.path
385+      }
386+    );
387+  }
388+
389+  const rawBody = await response.text();
390+  let parsedBody: JsonValue | null = null;
391+
392+  if (rawBody !== "") {
393+    try {
394+      parsedBody = JSON.parse(rawBody) as JsonValue;
395+    } catch {
396+      throw new LocalApiHttpError(
397+        502,
398+        "codexd_invalid_response",
399+        `Independent codexd returned invalid JSON for ${input.method} ${input.path}.`,
400+        {
401+          target_base_url: codexdLocalApiBase,
402+          upstream_path: input.path
403+        }
404+      );
405+    }
406+  }
407+
408+  if (!response.ok) {
409+    if (isUpstreamErrorEnvelope(parsedBody)) {
410+      throw new LocalApiHttpError(
411+        response.status,
412+        response.status === 404 ? "not_found" : parsedBody.error,
413+        parsedBody.message,
414+        compactJsonObject({
415+          target_base_url: codexdLocalApiBase,
416+          upstream_details: parsedBody.details,
417+          upstream_error: parsedBody.error,
418+          upstream_path: input.path
419+        })
420+      );
421+    }
422+
423+    throw new LocalApiHttpError(
424+      response.status,
425+      response.status >= 500 ? "codexd_unavailable" : "codexd_proxy_error",
426+      `Independent codexd returned HTTP ${response.status} for ${input.method} ${input.path}.`,
427+      {
428+        target_base_url: codexdLocalApiBase,
429+        upstream_path: input.path
430+      }
431+    );
432+  }
433+
434+  if (!isUpstreamSuccessEnvelope(parsedBody)) {
435+    throw new LocalApiHttpError(
436+      502,
437+      "codexd_invalid_response",
438+      `Independent codexd returned an unexpected payload for ${input.method} ${input.path}.`,
439+      {
440+        target_base_url: codexdLocalApiBase,
441+        upstream_path: input.path
442+      }
443+    );
444+  }
445+
446+  return {
447+    data: parsedBody.data,
448+    status: response.status
449+  };
450+}
451+
452 function buildFirefoxWebSocketData(snapshot: ConductorRuntimeApiSnapshot): JsonObject {
453   return {
454     auth_mode: "local_network_only",
455@@ -929,12 +1319,15 @@ function routeBelongsToSurface(
456       "service.health",
457       "service.version",
458       "system.capabilities",
459+      "codex.status",
460+      "codex.sessions.list",
461+      "codex.sessions.read",
462+      "codex.sessions.create",
463+      "codex.turn.create",
464       "controllers.list",
465       "tasks.list",
466       "tasks.read",
467-      "tasks.logs.read",
468-      "runs.list",
469-      "runs.read"
470+      "tasks.logs.read"
471     ].includes(route.id);
472   }
473 
474@@ -976,13 +1369,15 @@ function buildCapabilitiesData(
475       "GET /describe",
476       "GET /v1/capabilities",
477       "GET /v1/system/state",
478-      "GET /v1/tasks or /v1/runs",
479+      "GET /v1/tasks or /v1/codex",
480+      "Use /v1/codex/* for interactive Codex session and turn work",
481       "GET /describe/control if local shell/file access is needed",
482       "Use POST system routes or host operations only when a write/exec is intended"
483     ],
484     read_endpoints: exposedRoutes.filter((route) => route.kind === "read").map(describeRoute),
485     write_endpoints: exposedRoutes.filter((route) => route.kind === "write").map(describeRoute),
486     diagnostics: LOCAL_API_ROUTES.filter((route) => route.kind === "probe").map(describeRoute),
487+    codex: buildCodexProxyData(snapshot),
488     runtime: {
489       identity: snapshot.identity,
490       lease_state: snapshot.daemon.leaseState,
491@@ -1041,10 +1436,11 @@ async function handleDescribeRead(context: LocalApiRequestContext, version: stri
492     auth: buildHttpAuthData(snapshot),
493     system,
494     websocket: buildFirefoxWebSocketData(snapshot),
495+    codex: buildCodexProxyData(snapshot),
496     describe_endpoints: {
497       business: {
498         path: "/describe/business",
499-        summary: "业务查询入口;适合 CLI AI、网页版 AI、手机网页 AI 先读。"
500+        summary: "业务查询和 Codex session 入口;适合 CLI AI、网页版 AI、手机网页 AI 先读。"
501       },
502       control: {
503         path: "/describe/control",
504@@ -1087,16 +1483,19 @@ async function handleDescribeRead(context: LocalApiRequestContext, version: stri
505         title: "Inspect the narrower capability surface if needed",
506         method: "GET",
507         path: "/v1/capabilities",
508-        curl: buildCurlExample(
509-          origin,
510-          LOCAL_API_ROUTES.find((route) => route.id === "system.capabilities")!
511-        )
512+        curl: buildCurlExample(origin, requireRouteDefinition("system.capabilities"))
513+      },
514+      {
515+        title: "Inspect the codex proxy surface",
516+        method: "GET",
517+        path: "/v1/codex",
518+        curl: buildCurlExample(origin, requireRouteDefinition("codex.status"))
519       },
520       {
521         title: "Read the current automation state",
522         method: "GET",
523         path: "/v1/system/state",
524-        curl: buildCurlExample(origin, LOCAL_API_ROUTES.find((route) => route.id === "system.state")!)
525+        curl: buildCurlExample(origin, requireRouteDefinition("system.state"))
526       },
527       {
528         title: "Pause local automation explicitly",
529@@ -1128,6 +1527,7 @@ async function handleDescribeRead(context: LocalApiRequestContext, version: stri
530     ],
531     notes: [
532       "AI callers should prefer /describe/business for business queries and /describe/control for control actions.",
533+      "All /v1/codex routes proxy the independent codexd daemon; this process does not host Codex sessions itself.",
534       "POST /v1/exec and POST /v1/files/* require Authorization: Bearer <BAA_SHARED_TOKEN>; missing or wrong tokens return 401 JSON.",
535       "These routes read and mutate the mini node's local truth source directly.",
536       "GET /healthz, /readyz, /rolez and /v1/runtime remain available as low-level diagnostics.",
537@@ -1164,28 +1564,39 @@ async function handleScopedDescribeRead(
538       recommended_flow: [
539         "GET /describe/business",
540         "Optionally GET /v1/capabilities",
541-        "Use read-only routes such as /v1/controllers, /v1/tasks and /v1/runs",
542+        "Use business routes such as /v1/controllers, /v1/tasks and /v1/codex",
543         "Use /describe/control if a local shell or file operation is intended"
544       ],
545       system,
546       websocket: buildFirefoxWebSocketData(snapshot),
547+      codex: buildCodexProxyData(snapshot),
548       endpoints: routes.map(describeRoute),
549       examples: [
550         {
551           title: "List recent tasks",
552           method: "GET",
553           path: "/v1/tasks?limit=5",
554-          curl: buildCurlExample(origin, LOCAL_API_ROUTES.find((route) => route.id === "tasks.list")!)
555+          curl: buildCurlExample(origin, requireRouteDefinition("tasks.list"))
556         },
557         {
558-          title: "List recent runs",
559+          title: "Inspect the codex proxy status",
560           method: "GET",
561-          path: "/v1/runs?limit=5",
562-          curl: buildCurlExample(origin, LOCAL_API_ROUTES.find((route) => route.id === "runs.list")!)
563+          path: "/v1/codex",
564+          curl: buildCurlExample(origin, requireRouteDefinition("codex.status"))
565+        },
566+        {
567+          title: "Create a codex session",
568+          method: "POST",
569+          path: "/v1/codex/sessions",
570+          curl: buildCurlExample(origin, requireRouteDefinition("codex.sessions.create"), {
571+            cwd: "/Users/george/code/baa-conductor",
572+            purpose: "duplex"
573+          })
574         }
575       ],
576       notes: [
577         "This surface is intended to be enough for business-query discovery without reading external docs.",
578+        "All /v1/codex routes proxy the independent codexd daemon instead of an in-process bridge.",
579         "If you pivot to /describe/control for /v1/exec or /v1/files/*, those host-ops routes require Authorization: Bearer <BAA_SHARED_TOKEN>.",
580         "Control actions and host-level exec/file operations are intentionally excluded; use /describe/control."
581       ]
582@@ -1215,6 +1626,7 @@ async function handleScopedDescribeRead(
583     system,
584     websocket: buildFirefoxWebSocketData(snapshot),
585     auth: buildHttpAuthData(snapshot),
586+    codex: buildCodexProxyData(snapshot),
587     host_operations: buildHostOperationsData(origin, snapshot),
588     endpoints: routes.map(describeRoute),
589     examples: [
590@@ -1273,6 +1685,7 @@ async function handleScopedDescribeRead(
591     ],
592     notes: [
593       "This surface is intended to be enough for control discovery without reading external docs.",
594+      "The interactive Codex surface is proxied to independent codexd; inspect /v1/codex or /describe/business for those routes.",
595       "Business queries such as tasks and runs are intentionally excluded; use /describe/business.",
596       "POST /v1/exec and POST /v1/files/* require Authorization: Bearer <BAA_SHARED_TOKEN>; missing or wrong tokens return 401 JSON.",
597       "Host operations return the structured host-ops union inside the outer conductor HTTP envelope."
598@@ -1316,6 +1729,7 @@ async function handleCapabilitiesRead(
599     system: await buildSystemStateData(repository),
600     notes: [
601       "Read routes are safe for discovery and inspection.",
602+      "All /v1/codex routes proxy the independent codexd daemon over local HTTP.",
603       "POST /v1/system/* writes the local automation mode immediately.",
604       "POST /v1/exec and POST /v1/files/* require Authorization: Bearer <BAA_SHARED_TOKEN> and return 401 JSON on missing or wrong tokens.",
605       "POST /v1/exec and POST /v1/files/* return the structured host-ops union in data."
606@@ -1506,6 +1920,66 @@ async function handleHostFileWrite(context: LocalApiRequestContext): Promise<Con
607   );
608 }
609 
610+async function handleCodexStatusRead(context: LocalApiRequestContext): Promise<ConductorHttpResponse> {
611+  const result = await requestCodexd(context, {
612+    method: "GET",
613+    path: "/v1/codexd/status"
614+  });
615+
616+  return buildSuccessEnvelope(
617+    context.requestId,
618+    200,
619+    buildCodexStatusData(
620+      result.data,
621+      normalizeOptionalString(context.codexdLocalApiBase) ?? context.snapshotLoader().codexd.localApiBase ?? ""
622+    )
623+  );
624+}
625+
626+async function handleCodexSessionsList(context: LocalApiRequestContext): Promise<ConductorHttpResponse> {
627+  const result = await requestCodexd(context, {
628+    method: "GET",
629+    path: "/v1/codexd/sessions"
630+  });
631+
632+  return buildSuccessEnvelope(context.requestId, result.status, result.data);
633+}
634+
635+async function handleCodexSessionRead(context: LocalApiRequestContext): Promise<ConductorHttpResponse> {
636+  const sessionId = context.params.session_id;
637+
638+  if (!sessionId) {
639+    throw new LocalApiHttpError(400, "invalid_request", "Route parameter \"session_id\" is required.");
640+  }
641+
642+  const result = await requestCodexd(context, {
643+    method: "GET",
644+    path: `/v1/codexd/sessions/${encodeURIComponent(sessionId)}`
645+  });
646+
647+  return buildSuccessEnvelope(context.requestId, result.status, result.data);
648+}
649+
650+async function handleCodexSessionCreate(context: LocalApiRequestContext): Promise<ConductorHttpResponse> {
651+  const result = await requestCodexd(context, {
652+    body: buildCodexSessionCreateRequest(readBodyObject(context.request, true)),
653+    method: "POST",
654+    path: "/v1/codexd/sessions"
655+  });
656+
657+  return buildSuccessEnvelope(context.requestId, result.status, result.data);
658+}
659+
660+async function handleCodexTurnCreate(context: LocalApiRequestContext): Promise<ConductorHttpResponse> {
661+  const result = await requestCodexd(context, {
662+    body: buildCodexTurnCreateRequest(readBodyObject(context.request, false)),
663+    method: "POST",
664+    path: "/v1/codexd/turn"
665+  });
666+
667+  return buildSuccessEnvelope(context.requestId, result.status, result.data);
668+}
669+
670 async function handleControllersList(context: LocalApiRequestContext): Promise<ConductorHttpResponse> {
671   const repository = requireRepository(context.repository);
672   const limit = readPositiveIntegerQuery(context.url, "limit", DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT);
673@@ -1649,6 +2123,16 @@ async function dispatchBusinessRoute(
674       return handleVersionRead(context.requestId, version);
675     case "system.capabilities":
676       return handleCapabilitiesRead(context, version);
677+    case "codex.status":
678+      return handleCodexStatusRead(context);
679+    case "codex.sessions.list":
680+      return handleCodexSessionsList(context);
681+    case "codex.sessions.read":
682+      return handleCodexSessionRead(context);
683+    case "codex.sessions.create":
684+      return handleCodexSessionCreate(context);
685+    case "codex.turn.create":
686+      return handleCodexTurnCreate(context);
687     case "system.state":
688       return handleSystemStateRead(context);
689     case "system.pause":
690@@ -1811,6 +2295,9 @@ export async function handleConductorHttpRequest(
691     return await dispatchRoute(
692       matchedRoute,
693       {
694+        codexdLocalApiBase:
695+          normalizeOptionalString(context.codexdLocalApiBase) ?? context.snapshotLoader().codexd.localApiBase,
696+        fetchImpl: context.fetchImpl ?? globalThis.fetch,
697         now: context.now ?? (() => Math.floor(Date.now() / 1000)),
698         params: matchedRoute.params,
699         repository: context.repository,
M docs/api/README.md
+32, -4
 1@@ -28,7 +28,8 @@
 2 | 服务 | 地址 | 说明 |
 3 | --- | --- | --- |
 4 | conductor public host | `https://conductor.makefile.so` | 唯一公网入口;VPS Nginx 回源到同一个 `conductor-daemon` local-api |
 5-| conductor-daemon local-api | `BAA_CONDUCTOR_LOCAL_API`,默认可用值如 `http://127.0.0.1:4317` | 本地真相源;承接 describe/health/version/capabilities/system/controllers/tasks/runs/host-ops |
 6+| conductor-daemon local-api | `BAA_CONDUCTOR_LOCAL_API`,默认可用值如 `http://127.0.0.1:4317` | 本地真相源;承接 describe/health/version/capabilities/system/controllers/tasks/codex/host-ops |
 7+| codexd local-api | `BAA_CODEXD_LOCAL_API_BASE`,默认可用值如 `http://127.0.0.1:4323` | 独立 `codexd` 本地服务;`conductor-daemon` 的 `/v1/codex/*` 只代理到这里 |
 8 | conductor-daemon local-firefox-ws | 由 `BAA_CONDUCTOR_LOCAL_API` 派生,例如 `ws://127.0.0.1:4317/ws/firefox` | 本地 Firefox 插件双向 bridge;复用同一个 listener,不单独开公网端口 |
 9 | status-api local view | `http://127.0.0.1:4318` | 本地只读状态 JSON 和 HTML 视图,不承担公网入口角色 |
10 
11@@ -39,7 +40,7 @@
12 1. `GET ${BAA_CONDUCTOR_LOCAL_API}/describe/business` 或 `GET ${BAA_CONDUCTOR_LOCAL_API}/describe/control`
13 2. 如有需要,再看 `GET ${BAA_CONDUCTOR_LOCAL_API}/v1/capabilities`
14 3. 如果是控制动作,再看 `GET ${BAA_CONDUCTOR_LOCAL_API}/v1/system/state`
15-4. 按需查看 `controllers`、`tasks`、`runs`
16+4. 按需查看 `controllers`、`tasks`、`codex`
17 5. 如果要做本机 shell / 文件操作,先读 `GET ${BAA_CONDUCTOR_LOCAL_API}/describe/control` 返回里的 `host_operations`,并准备 `Authorization: Bearer <BAA_SHARED_TOKEN>`
18 6. 只有在明确需要写操作时,再调用 `pause` / `resume` / `drain` 或 `host-ops`
19 7. 只有在明确需要浏览器双向通讯时,再手动连接 `/ws/firefox`
20@@ -76,6 +77,23 @@
21 - 成功时直接返回最新 system state,而不是只回一个 ack
22 - 这样 HTTP 客户端和 WS `action_request` 都能复用同一份状态合同
23 
24+### Codex 代理接口
25+
26+这些路由不是 `conductor-daemon` 内嵌 bridge,而是固定代理到独立 `codexd`:
27+
28+| 方法 | 路径 | 说明 |
29+| --- | --- | --- |
30+| `GET` | `/v1/codex` | 读取 `codexd` 代理状态、session 摘要和可用路由 |
31+| `GET` | `/v1/codex/sessions` | 列出当前 Codex sessions |
32+| `GET` | `/v1/codex/sessions/:session_id` | 读取单个 Codex session |
33+| `POST` | `/v1/codex/sessions` | 创建或恢复一个 Codex session |
34+| `POST` | `/v1/codex/turn` | 向现有 session 提交一轮 turn |
35+
36+当前正式口径只保留 session / turn / status:
37+
38+- `conductor-daemon` 不对外代理 `/v1/codex/runs*`
39+- 不把 `codex exec` 当作正式产品能力
40+
41 ### 本机 Host Ops 接口
42 
43 | 方法 | 路径 | 说明 |
44@@ -103,8 +121,6 @@ host-ops 约定:
45 | `GET` | `/v1/tasks?status=queued&limit=20` | 最近 task 摘要,可按 `status` 过滤 |
46 | `GET` | `/v1/tasks/:task_id` | 单个 task 详情 |
47 | `GET` | `/v1/tasks/:task_id/logs?limit=200` | task 关联日志,可选 `run_id` 过滤 |
48-| `GET` | `/v1/runs?limit=20` | 最近 run 摘要 |
49-| `GET` | `/v1/runs/:run_id` | 单个 run 详情 |
50 
51 ### 诊断接口
52 
53@@ -176,6 +192,18 @@ LOCAL_API_BASE="${BAA_CONDUCTOR_LOCAL_API:-http://127.0.0.1:4317}"
54 curl "${LOCAL_API_BASE}/v1/tasks?limit=5"
55 ```
56 
57+```bash
58+LOCAL_API_BASE="${BAA_CONDUCTOR_LOCAL_API:-http://127.0.0.1:4317}"
59+curl "${LOCAL_API_BASE}/v1/codex"
60+```
61+
62+```bash
63+LOCAL_API_BASE="${BAA_CONDUCTOR_LOCAL_API:-http://127.0.0.1:4317}"
64+curl -X POST "${LOCAL_API_BASE}/v1/codex/sessions" \
65+  -H 'Content-Type: application/json' \
66+  -d '{"cwd":"/Users/george/code/baa-conductor","purpose":"duplex"}'
67+```
68+
69 ```bash
70 LOCAL_API_BASE="${BAA_CONDUCTOR_LOCAL_API:-http://127.0.0.1:4317}"
71 curl -X POST "${LOCAL_API_BASE}/v1/system/pause" \
M docs/api/business-interfaces.md
+25, -3
 1@@ -57,8 +57,22 @@
 2 | `GET` | `/v1/tasks?status=queued&limit=20` | 查看任务列表,可按 `status` 过滤 |
 3 | `GET` | `/v1/tasks/:task_id` | 查看单个任务详情 |
 4 | `GET` | `/v1/tasks/:task_id/logs?limit=200` | 查看单个任务日志,可按 `run_id` 过滤 |
 5-| `GET` | `/v1/runs?limit=20` | 查看运行列表 |
 6-| `GET` | `/v1/runs/:run_id` | 查看单个运行详情 |
 7+
 8+### Codex 会话查询与写入
 9+
10+| 方法 | 路径 | 作用 |
11+| --- | --- | --- |
12+| `GET` | `/v1/codex` | 查看当前 `conductor -> codexd` 代理状态 |
13+| `GET` | `/v1/codex/sessions` | 查看当前 Codex sessions |
14+| `GET` | `/v1/codex/sessions/:session_id` | 查看单个 Codex session |
15+| `POST` | `/v1/codex/sessions` | 创建或恢复一个 Codex session |
16+| `POST` | `/v1/codex/turn` | 向已有 session 提交一轮 turn |
17+
18+说明:
19+
20+- 这些 `/v1/codex/*` 路由固定代理到独立 `codexd`
21+- `conductor-daemon` 不自己持有 Codex session 真相
22+- 正式能力只保留 session / turn / status,不对外暴露 `/v1/codex/runs*`
23 
24 ## 当前不在业务面讨论的写接口
25 
26@@ -96,7 +110,14 @@ curl "${BASE_URL}/v1/tasks?limit=5"
27 
28 ```bash
29 BASE_URL="http://100.71.210.78:4317"
30-curl "${BASE_URL}/v1/runs?limit=5"
31+curl "${BASE_URL}/v1/codex"
32+```
33+
34+```bash
35+BASE_URL="http://100.71.210.78:4317"
36+curl -X POST "${BASE_URL}/v1/codex/sessions" \
37+  -H 'Content-Type: application/json' \
38+  -d '{"cwd":"/Users/george/code/baa-conductor","purpose":"duplex"}'
39 ```
40 
41 ```bash
42@@ -123,6 +144,7 @@ curl "${BASE_URL}/v1/tasks/${TASK_ID}/logs?limit=50"
43 ## 当前边界
44 
45 - 业务类接口当前以“只读查询”为主
46+- `/v1/codex/*` 是少数已经正式开放的业务写接口,但后端固定代理到独立 `codexd`
47 - 控制动作例如 `pause` / `resume` / `drain` 不在本文件讨论范围内
48 - 本机能力接口 `/v1/exec`、`/v1/files/read`、`/v1/files/write` 也不在本文件讨论范围内
49 - 控制类接口见 [`control-interfaces.md`](./control-interfaces.md)
M docs/api/control-interfaces.md
+8, -0
 1@@ -32,6 +32,13 @@
 2 5. 如果要做本机 shell / 文件操作,再看 `host_operations`
 3 6. 确认当前模式和动作目标后,再执行 `pause` / `resume` / `drain` 或 host-ops
 4 
 5+如果目标是 Codex 会话而不是控制动作:
 6+
 7+- 改读 [`business-interfaces.md`](./business-interfaces.md)
 8+- 先看 `GET /describe/business`
 9+- 使用 `/v1/codex/*`
10+- 这些路由固定代理到独立 `codexd`
11+
12 不要在未读状态前直接写入控制动作或本机 host-ops。
13 
14 ## Host Ops 鉴权
15@@ -178,5 +185,6 @@ curl -X POST "${BASE_URL}/v1/files/write" \
16 
17 - 当前控制面是单节点 `mini`
18 - 控制动作默认作用于当前唯一活动节点
19+- Codex 会话能力不在本文件主讨论范围;它通过 `/v1/codex/*` 代理到独立 `codexd`
20 - 业务查询不在本文件讨论范围内
21 - 业务类接口见 [`business-interfaces.md`](./business-interfaces.md)
M docs/decisions/0002-codexd-independent-daemon.md
+1, -2
 1@@ -14,7 +14,7 @@
 2 - `codexd` 是唯一 Codex 运行面
 3 - `conductor-daemon` 只通过本地接口调用 `codexd`
 4 - `codexd` 主接口基于 `codex app-server`
 5-- `codex exec` 仅作为 smoke、简单调用和兜底路径
 6+- 不实现 `codex exec` 式无交互正式模式
 7 
 8 ## 原因
 9 
10@@ -43,4 +43,3 @@
11 - 本地命令/查询面:HTTP
12 - 本地双工事件流:WS 或 SSE
13 - 远程 AI / CLI / 网页入口仍通过 `conductor.makefile.so`
14-
M docs/runtime/README.md
+6, -5
 1@@ -35,15 +35,16 @@ Firefox WS 说明:
 2   - 维护 `logs/codexd` 和 `state/codexd`
 3   - 启动或占位一个 `codex app-server` 子进程配置
 4   - 持久化 daemon identity、child state、session registry、recent event cache
 5-  - 提供 `start` / `status` / `smoke`
 6+  - 提供本地 HTTP / WS 服务面
 7+  - 让 `conductor-daemon` 通过 `/v1/codex/*` 代理 session / turn / status
 8 - 运行约束:
 9   - `codexd` 是独立常驻进程,不是 `conductor-daemon` 的内嵌 bridge
10-  - `/v1/codex/*` 后续应以 `conductor-daemon -> codexd` 代理方式提供
11+  - `/v1/codex/*` 现在以 `conductor-daemon -> codexd` 代理方式提供
12   - `launchd` 负责自启动和硬重启,不做两个进程互相直接拉起
13 - 当前还没有:
14-  - 对外 IPC / HTTP 面
15-  - 真正的 `thread` / `turn` 管理
16-  - `conductor-daemon -> codexd` 正式接线
17+  - 完整的会话恢复和断线重放
18+  - 更丰富的增量事件订阅语义
19+  - 任何 `/v1/codex/runs*` 的正式代理面
20 
21 ## 最短路径
22 
M docs/runtime/codexd.md
+13, -39
  1@@ -7,11 +7,9 @@
  2 当前状态:
  3 
  4 - 仓库里已经有 `apps/codexd` 最小骨架
  5-- 它目前是 daemon scaffold,不是完整协议实现
  6-- 主目标仍然是围绕 `codex app-server` 演进
  7-- 已有两个底层适配包:
  8-  - `packages/codex-app-server`: 面向未来主会话 / 双工能力
  9-  - `packages/codex-exec`: 面向 smoke、简单 worker 和降级路径的一次性调用
 10+- 已经有本地 HTTP / WS 服务面
 11+- `conductor-daemon` 已经通过 `/v1/codex/*` 代理到它
 12+- 主目标仍然围绕 `codex app-server` 演进
 13 
 14 ## 目标
 15 
 16@@ -129,23 +127,21 @@
 17 基于当前本机已验证的 Codex CLI 公开接口,`codexd` 的设计结论应明确为:
 18 
 19 - 主会话与双工能力:基于 `codex app-server`
 20-- 简单调用、批处理、测试和兜底 worker:基于 `codex exec`
 21+- `conductor-daemon` 对外正式能力只保留 session / turn / status
 22 - 不驱动 TUI
 23 - 不逆向私有协议
 24 
 25 原因:
 26 
 27-- `codex exec` 适合一次性非交互执行,能输出结构化结果,但天然不是多轮双工模型
 28+- 当前目标是常驻、多轮、可恢复的会话代理,不是一次性命令壳
 29 - `codex app-server` 虽然仍标记为 experimental,但它已经公开了最完整的线程、turn、流式事件和恢复语义
 30 - 已验证单个 `app-server` 进程可承载多个 `thread`,因此默认不需要“一对话一进程”
 31 
 32 当前推荐口径:
 33 
 34 - `codexd v1` 继续围绕 `app-server`
 35-- `exec` 仅保留为:
 36-  - 最小 smoke 测试
 37-  - 简单离线调用
 38-  - app-server 不可用时的兜底 worker
 39+- `conductor-daemon -> codexd` 是唯一正式代理链路
 40+- 不新增、不对外暴露 `/v1/codex/runs*`
 41 
 42 ## 当前骨架已经落下的内容
 43 
 44@@ -190,10 +186,8 @@
 45 
 46 - `thread/start` / `thread/resume` / `turn/start` 的真实代理
 47 - `codex-app-server` 传输层接线
 48-- HTTP / WS / IPC 入口
 49-- conductor-daemon 适配层
 50 - crash recovery 的自动复连和 session 恢复
 51-- `conductor-daemon -> codexd` 的正式代理面
 52+- 更完整的事件恢复和会话恢复能力
 53 
 54 ## 支持的两类工作
 55 
 56@@ -211,8 +205,7 @@
 57 - 有 task / step / run 关联
 58 - 受 timeout / retry / checkpoint 约束
 59 - 由 `worker-runner` / `conductor-daemon` 编排
 60-- 默认仍优先走 `app-server`
 61-- 只有简单批处理或兜底路径才落到 `exec`
 62+- 正式实现只走 `app-server`
 63 
 64 ### 2. duplex 对话模式
 65 
 66@@ -220,7 +213,7 @@
 67 
 68 - 和 AI 的持续双向对话
 69 - 支持 CLI / 网页版 AI 参与
 70-- 长于一次性 `codex exec`
 71+- 长于一次性命令式调用
 72 
 73 特点:
 74 
 75@@ -266,28 +259,9 @@
 76   - 增量事件日志
 77 - 对 `conductor-daemon` 暴露稳定的本地适配层
 78 - 这层至少拆成:
 79-  - 本地 HTTP:session / turn / run / status
 80+  - 本地 HTTP:session / turn / status
 81   - 本地 WS 或 SSE:增量事件流
 82 
 83-### v1 兜底能力
 84-
 85-- 保留一个 `exec` 适配器
 86-- 当前仓库里的落点是 `packages/codex-exec`
 87-- 只做:
 88-  - 健康检查
 89-  - 最小 smoke
 90-  - 简单一次性 worker 调用
 91-  - app-server 不可用时的降级路径
 92-- 适配层当前只覆盖:
 93-  - 一次运行一个 `codex exec`
 94-  - 收集 stdout / stderr
 95-  - 返回 exit code、timeout、last message 和可选 JSONL 事件
 96-- 它不负责:
 97-  - session / thread 生命周期
 98-  - 多轮对话
 99-  - interrupt / steer
100-  - 持久化双工事件桥
101-
102 ### v2
103 
104 - 在 `app-server` 之上继续补:
105@@ -343,8 +317,8 @@
106 如果开始实现 `codexd`,默认遵守这条约束:
107 
108 - `app-server` 是主能力面
109-- `exec` 不是主会话系统,只是简单调用和测试工具
110-- `packages/codex-exec` 只是一个兜底层,不应被扩成主双工实现
111+- 不把 `runs` / `exec` 扩成正式产品能力
112+- `conductor-daemon` 只代理独立 `codexd`
113 
114 不要把当前系统误认为已经有:
115