baa-conductor

git clone 

commit
393dc22
parent
23a84de
author
im_wower
date
2026-03-23 22:24:48 +0800 CST
feat: add codexd describe surface
7 files changed,  +229, -5
M README.md
+1, -0
1@@ -30,6 +30,7 @@
2 
3 - `codex app-server` 是未来 `codexd` 的主接口
4 - 不实现 `codex exec` 式无交互正式模式
5+- 直接调用 `codexd` 时,先读 `GET /describe`
6 
7 ## 面向 AI 的推荐阅读顺序
8 
M apps/codexd/src/index.test.js
+28, -0
 1@@ -358,6 +358,34 @@ test("CodexdLocalService starts the local HTTP surface and supports status, sess
 2     assert.equal(healthz.status, 200);
 3     assert.equal(healthz.json.ok, true);
 4 
 5+    const describe = await fetchJson(`${baseUrl}/describe`);
 6+    assert.equal(describe.status, 200);
 7+    assert.equal(describe.json.ok, true);
 8+    assert.equal(describe.json.name, "codexd");
 9+    assert.equal(describe.json.surface, "local-api");
10+    assert.equal(describe.json.base_url, baseUrl);
11+    assert.equal(describe.json.mode.current, "app-server");
12+    assert.equal(describe.json.mode.conductor_role, "proxy");
13+    assert.equal(describe.json.event_stream.path, "/v1/codexd/events");
14+    assert.match(describe.json.event_stream.url, /^ws:\/\/127\.0\.0\.1:\d+\/v1\/codexd\/events$/u);
15+    assert.deepEqual(
16+      describe.json.routes.map((route) => `${route.method} ${route.path}`),
17+      [
18+        "GET /healthz",
19+        "GET /describe",
20+        "GET /v1/codexd/status",
21+        "GET /v1/codexd/sessions",
22+        "GET /v1/codexd/sessions/:session_id",
23+        "POST /v1/codexd/sessions",
24+        "POST /v1/codexd/turn",
25+        "WS /v1/codexd/events"
26+      ]
27+    );
28+    assert.equal(describe.json.capabilities.turn_create, true);
29+    assert.equal(describe.json.capabilities.websocket_events, true);
30+    assert.doesNotMatch(JSON.stringify(describe.json), /runs/iu);
31+    assert.doesNotMatch(JSON.stringify(describe.json), /exec/iu);
32+
33     const status = await fetchJson(`${baseUrl}/v1/codexd/status`);
34     assert.equal(status.status, 200);
35     assert.equal(status.json.ok, true);
M apps/codexd/src/local-service.ts
+125, -1
  1@@ -23,6 +23,86 @@ interface CodexdHttpResponse {
  2 
  3 type JsonRecord = Record<string, unknown>;
  4 
  5+export interface CodexdDescribeRoute {
  6+  description: string;
  7+  method: "GET" | "POST" | "WS";
  8+  path: string;
  9+}
 10+
 11+export interface CodexdDescribeResponse {
 12+  ok: true;
 13+  name: string;
 14+  surface: string;
 15+  description: string;
 16+  mode: {
 17+    current: CodexdResolvedConfig["server"]["mode"];
 18+    daemon: string;
 19+    supervisor: string;
 20+    transport: string;
 21+    conductor_role: string;
 22+  };
 23+  base_url: string;
 24+  event_stream: {
 25+    path: string;
 26+    transport: string;
 27+    url: string;
 28+  };
 29+  routes: CodexdDescribeRoute[];
 30+  capabilities: {
 31+    health_probe: boolean;
 32+    session_create: boolean;
 33+    session_list: boolean;
 34+    session_read: boolean;
 35+    turn_create: boolean;
 36+    turn_readback_via_session: boolean;
 37+    websocket_events: boolean;
 38+  };
 39+  notes: string[];
 40+}
 41+
 42+const CODEXD_FORMAL_ROUTES: CodexdDescribeRoute[] = [
 43+  {
 44+    description: "Lightweight health probe for the local daemon.",
 45+    method: "GET",
 46+    path: "/healthz"
 47+  },
 48+  {
 49+    description: "Machine-readable description of the official codexd surface.",
 50+    method: "GET",
 51+    path: "/describe"
 52+  },
 53+  {
 54+    description: "Current daemon, child, session, and recent event snapshot.",
 55+    method: "GET",
 56+    path: "/v1/codexd/status"
 57+  },
 58+  {
 59+    description: "List the currently known codexd sessions.",
 60+    method: "GET",
 61+    path: "/v1/codexd/sessions"
 62+  },
 63+  {
 64+    description: "Read a single codexd session and its recent events.",
 65+    method: "GET",
 66+    path: "/v1/codexd/sessions/:session_id"
 67+  },
 68+  {
 69+    description: "Create or resume a codexd session.",
 70+    method: "POST",
 71+    path: "/v1/codexd/sessions"
 72+  },
 73+  {
 74+    description: "Submit one turn to an existing codexd session.",
 75+    method: "POST",
 76+    path: "/v1/codexd/turn"
 77+  },
 78+  {
 79+    description: "WebSocket event stream for live codexd session and daemon updates.",
 80+    method: "WS",
 81+    path: "/v1/codexd/events"
 82+  }
 83+];
 84+
 85 export interface CodexdLocalServiceRuntimeInfo {
 86   configuredBaseUrl: string;
 87   eventStreamPath: string;
 88@@ -86,6 +166,46 @@ export class CodexdLocalService {
 89     };
 90   }
 91 
 92+  getDescribe(): CodexdDescribeResponse {
 93+    const baseUrl = this.resolvedBaseUrl ?? this.config.service.localApiBase;
 94+
 95+    return {
 96+      base_url: baseUrl,
 97+      capabilities: {
 98+        health_probe: true,
 99+        session_create: true,
100+        session_list: true,
101+        session_read: true,
102+        turn_create: true,
103+        turn_readback_via_session: true,
104+        websocket_events: true
105+      },
106+      description:
107+        "Independent local Codex daemon for session lifecycle, turn submission, status reads, and live event streaming.",
108+      event_stream: {
109+        path: this.config.service.eventStreamPath,
110+        transport: "websocket",
111+        url: buildEventStreamUrl(baseUrl, this.config.service.eventStreamPath)
112+      },
113+      mode: {
114+        conductor_role: "proxy",
115+        current: this.config.server.mode,
116+        daemon: "independent",
117+        supervisor: "launchd",
118+        transport: "codex app-server"
119+      },
120+      name: "codexd",
121+      notes: [
122+        "Use GET /describe first when an AI client needs to discover the official local codexd surface.",
123+        "codexd is the long-running Codex runtime; conductor-daemon only proxies this service and does not host the Codex session truth.",
124+        "This surface is limited to health, status, sessions, turn submission, and websocket event consumption."
125+      ],
126+      ok: true,
127+      routes: CODEXD_FORMAL_ROUTES.map((route) => ({ ...route })),
128+      surface: "local-api"
129+    };
130+  }
131+
132   async start(): Promise<CodexdLocalServiceStatus> {
133     if (this.server != null) {
134       return this.getStatus();
135@@ -260,6 +380,10 @@ export class CodexdLocalService {
136       });
137     }
138 
139+    if (method === "GET" && pathname === "/describe") {
140+      return jsonResponse(200, this.getDescribe());
141+    }
142+
143     if (method === "GET" && pathname === "/v1/codexd/status") {
144       return jsonResponse(200, {
145         data: this.getStatus(),
146@@ -358,7 +482,7 @@ function isLoopbackHost(hostname: string): boolean {
147   return hostname === "127.0.0.1" || hostname === "::1" || hostname === "localhost";
148 }
149 
150-function jsonResponse(status: number, payload: JsonRecord): CodexdHttpResponse {
151+function jsonResponse(status: number, payload: unknown): CodexdHttpResponse {
152   return {
153     body: `${JSON.stringify(payload, null, 2)}\n`,
154     headers: {
M docs/api/README.md
+51, -1
 1@@ -29,7 +29,7 @@
 2 | --- | --- | --- |
 3 | conductor public host | `https://conductor.makefile.so` | 唯一公网入口;VPS Nginx 回源到同一个 `conductor-daemon` local-api |
 4 | conductor-daemon local-api | `BAA_CONDUCTOR_LOCAL_API`,默认可用值如 `http://127.0.0.1:4317` | 本地真相源;承接 describe/health/version/capabilities/system/controllers/tasks/codex/host-ops |
 5-| codexd local-api | `BAA_CODEXD_LOCAL_API_BASE`,默认可用值如 `http://127.0.0.1:4323` | 独立 `codexd` 本地服务;`conductor-daemon` 的 `/v1/codex/*` 只代理到这里 |
 6+| codexd local-api | `BAA_CODEXD_LOCAL_API_BASE`,默认可用值如 `http://127.0.0.1:4319` | 独立 `codexd` 本地服务;支持 `GET /describe` 自描述;`conductor-daemon` 的 `/v1/codex/*` 只代理到这里 |
 7 | conductor-daemon local-firefox-ws | 由 `BAA_CONDUCTOR_LOCAL_API` 派生,例如 `ws://127.0.0.1:4317/ws/firefox` | 本地 Firefox 插件双向 bridge;复用同一个 listener,不单独开公网端口 |
 8 | status-api local view | `http://127.0.0.1:4318` | 本地只读状态 JSON 和 HTML 视图,不承担公网入口角色 |
 9 
10@@ -45,6 +45,12 @@
11 6. 只有在明确需要写操作时,再调用 `pause` / `resume` / `drain` 或 `host-ops`
12 7. 只有在明确需要浏览器双向通讯时,再手动连接 `/ws/firefox`
13 
14+如果是直接调用 `codexd`:
15+
16+1. 先调 `GET ${BAA_CODEXD_LOCAL_API_BASE}/describe`
17+2. 再按 `routes` 里的正式接口调用 `status`、`sessions`、`turn`
18+3. 只有在需要双工事件时,再连接 `WS ${BAA_CODEXD_LOCAL_API_BASE}` 对应的 `/v1/codexd/events`
19+
20 如果是给 AI 写操作说明,优先引用:
21 
22 - [`business-interfaces.md`](./business-interfaces.md)
23@@ -94,6 +100,45 @@
24 - turn 的回读统一通过 `GET /v1/codex/sessions/:session_id` 里的 `lastTurn*` 和 `recentEvents`
25 - 正式业务面不包含 run/exec 路线
26 
27+## codexd Direct Local API
28+
29+AI 或自动化如果不经过 `conductor-daemon`,应先读取:
30+
31+- `GET /describe`
32+
33+当前 `codexd` 自描述会直接返回:
34+
35+- `ok`
36+- `name`
37+- `surface`
38+- `description`
39+- `mode`
40+- `base_url`
41+- `event_stream`
42+- `routes`
43+- `capabilities`
44+- `notes`
45+
46+当前正式 `codexd` 直连接口:
47+
48+| 方法 | 路径 | 说明 |
49+| --- | --- | --- |
50+| `GET` | `/healthz` | 最小健康探针 |
51+| `GET` | `/describe` | `codexd` 正式自描述入口;AI 应先读这里 |
52+| `GET` | `/v1/codexd/status` | 读取 daemon / child / session / recent events 摘要 |
53+| `GET` | `/v1/codexd/sessions` | 列出当前 sessions |
54+| `GET` | `/v1/codexd/sessions/:session_id` | 读取单个 session 和 recent events |
55+| `POST` | `/v1/codexd/sessions` | 创建或恢复 session |
56+| `POST` | `/v1/codexd/turn` | 提交一轮 turn |
57+| `WS` | `/v1/codexd/events` | 订阅本地双工事件流 |
58+
59+说明:
60+
61+- `codexd` 是独立常驻进程
62+- `conductor-daemon` 只代理这组接口,不承载 Codex session 真相
63+- 当前正式模式固定为 `codex app-server`
64+- 自描述和正式接口都不引入一次性 batch 命令面
65+
66 ### 本机 Host Ops 接口
67 
68 | 方法 | 路径 | 说明 |
69@@ -197,6 +242,11 @@ LOCAL_API_BASE="${BAA_CONDUCTOR_LOCAL_API:-http://127.0.0.1:4317}"
70 curl "${LOCAL_API_BASE}/v1/codex"
71 ```
72 
73+```bash
74+CODEXD_API_BASE="${BAA_CODEXD_LOCAL_API_BASE:-http://127.0.0.1:4319}"
75+curl "${CODEXD_API_BASE}/describe"
76+```
77+
78 ```bash
79 LOCAL_API_BASE="${BAA_CONDUCTOR_LOCAL_API:-http://127.0.0.1:4317}"
80 curl -X POST "${LOCAL_API_BASE}/v1/codex/sessions" \
M docs/runtime/README.md
+2, -0
 1@@ -32,8 +32,10 @@
 2 `codexd` 正式能力面只保留:
 3 
 4 - `GET /healthz`
 5+- `GET /describe`
 6 - `GET /v1/codexd/status`
 7 - `GET /v1/codexd/sessions`
 8+- `GET /v1/codexd/sessions/:session_id`
 9 - `POST /v1/codexd/sessions`
10 - `POST /v1/codexd/turn`
11 - `WS /v1/codexd/events`
M docs/runtime/codexd.md
+21, -2
 1@@ -61,15 +61,34 @@ BAA_CODEXD_STATE_DIR=/Users/george/code/baa-conductor/state/codexd
 2 `apps/codexd` 当前已经有本地 HTTP / WS 入口,正式运行时由 `launchd` 长期托管:
 3 
 4 - `GET /healthz`
 5+- `GET /describe`
 6 - `GET /v1/codexd/status`
 7 - `GET /v1/codexd/sessions`
 8+- `GET /v1/codexd/sessions/:session_id`
 9 - `POST /v1/codexd/sessions`
10 - `POST /v1/codexd/turn`
11 - `WS /v1/codexd/events`
12 
13-正式口径只保留 `status / sessions / turn / events`。
14+推荐 AI 或自动化客户端直接先读:
15 
16-`codexd` 的正式对外描述不包含 run/exec 路线。
17+- `GET /describe`
18+
19+`/describe` 当前返回:
20+
21+- `ok`
22+- `name`
23+- `surface`
24+- `description`
25+- `mode`
26+- `base_url`
27+- `event_stream`
28+- `routes`
29+- `capabilities`
30+- `notes`
31+
32+正式口径只保留 `describe / status / sessions / turn / events`。
33+
34+`/describe` 只列正式能力,不包含批处理一次性调用路线。
35 
36 当前骨架已经会维护:
37 
M docs/runtime/environment.md
+1, -1
1@@ -63,7 +63,7 @@ BAA_CODEXD_STATE_DIR=/Users/george/code/baa-conductor/state/codexd
2 说明:
3 
4 - 正式运行面只支持 `app-server`
5-- 正式 API 只保留 `/healthz`、`/v1/codexd/status`、`/v1/codexd/sessions`、`/v1/codexd/turn`、`/v1/codexd/events`
6+- 正式 API 只保留 `/healthz`、`/describe`、`/v1/codexd/status`、`/v1/codexd/sessions`、`/v1/codexd/sessions/:session_id`、`/v1/codexd/turn`、`/v1/codexd/events`
7 - 正式运行面只保留这条 `app-server` 会话链路所需变量
8 - `BAA_CODEXD_LOCAL_API_BASE` 必须保持 loopback host;当前默认是 `127.0.0.1:4319`
9