- 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
+1,
-0
1@@ -30,6 +30,7 @@
2
3 - `codex app-server` 是未来 `codexd` 的主接口
4 - 不实现 `codex exec` 式无交互正式模式
5+- 直接调用 `codexd` 时,先读 `GET /describe`
6
7 ## 面向 AI 的推荐阅读顺序
8
+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);
+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: {
+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" \
+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`
+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
+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