baa-conductor

git clone 

commit
0d7bd28
parent
eab74d1
author
im_wower
date
2026-03-24 23:35:55 +0800 CST
Merge branch 'fix/tcc-exec-result-shape'

# Conflicts:
#	apps/conductor-daemon/src/index.test.js
#	docs/api/local-host-ops.md
4 files changed,  +107, -6
M apps/conductor-daemon/src/index.test.js
+41, -1
 1@@ -1,7 +1,7 @@
 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 { homedir, tmpdir } from "node:os";
 7 import { join } from "node:path";
 8 import test from "node:test";
 9 
10@@ -1738,6 +1738,46 @@ test("handleConductorHttpRequest returns a clear 503 for Claude browser actions
11   assert.equal(payload.error, "browser_bridge_unavailable");
12 });
13 
14+test(
15+  "handleConductorHttpRequest normalizes exec failures that are blocked by macOS TCC preflight",
16+  { concurrency: false },
17+  async () => {
18+    const { repository, snapshot } = await createLocalApiFixture();
19+
20+    const execResponse = await withMockedPlatform("darwin", () =>
21+      handleConductorHttpRequest(
22+        {
23+          body: JSON.stringify({
24+            command: "pwd",
25+            cwd: join(homedir(), "Downloads", "nested"),
26+            timeoutMs: 2_000
27+          }),
28+          method: "POST",
29+          path: "/v1/exec"
30+        },
31+        {
32+          repository,
33+          snapshotLoader: () => snapshot
34+        }
35+      )
36+    );
37+
38+    assert.equal(execResponse.status, 200);
39+    const execPayload = parseJsonBody(execResponse);
40+    assert.equal(execPayload.data.ok, false);
41+    assert.equal(execPayload.data.operation, "exec");
42+    assert.equal(execPayload.data.error.code, "TCC_PERMISSION_DENIED");
43+    assert.equal(execPayload.data.result.stdout, "");
44+    assert.equal(execPayload.data.result.stderr, "");
45+    assert.equal(execPayload.data.result.exitCode, null);
46+    assert.equal(execPayload.data.result.signal, null);
47+    assert.equal(execPayload.data.result.durationMs, 0);
48+    assert.equal(execPayload.data.result.timedOut, false);
49+    assert.equal(typeof execPayload.data.result.startedAt, "string");
50+    assert.equal(typeof execPayload.data.result.finishedAt, "string");
51+  }
52+);
53+
54 test("ConductorRuntime serves health and migrated local API endpoints over HTTP", async () => {
55   const codexd = await startCodexdStubServer();
56   const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-runtime-"));
M apps/conductor-daemon/src/local-api.ts
+40, -5
 1@@ -19,6 +19,8 @@ import {
 2   executeCommand,
 3   readTextFile,
 4   writeTextFile,
 5+  type ExecOperationResponse,
 6+  type ExecOperationResult,
 7   type ExecOperationRequest,
 8   type FileReadOperationRequest,
 9   type FileWriteOperationRequest
10@@ -550,6 +552,42 @@ function buildExecOperationRequest(body: JsonObject): ExecOperationRequest {
11   };
12 }
13 
14+function buildDefaultExecFailureResult(timestamp: string): ExecOperationResult {
15+  return {
16+    durationMs: 0,
17+    exitCode: null,
18+    finishedAt: timestamp,
19+    signal: null,
20+    startedAt: timestamp,
21+    stderr: "",
22+    stdout: "",
23+    timedOut: false
24+  };
25+}
26+
27+function normalizeExecOperationResponse(response: ExecOperationResponse): ExecOperationResponse {
28+  if (response.ok) {
29+    return response;
30+  }
31+
32+  const fallbackResult = buildDefaultExecFailureResult(new Date().toISOString());
33+  const result = response.result;
34+
35+  return {
36+    ...response,
37+    result: {
38+      durationMs: result?.durationMs ?? fallbackResult.durationMs,
39+      exitCode: result?.exitCode ?? fallbackResult.exitCode,
40+      finishedAt: result?.finishedAt ?? result?.startedAt ?? fallbackResult.finishedAt,
41+      signal: result?.signal ?? fallbackResult.signal,
42+      startedAt: result?.startedAt ?? fallbackResult.startedAt,
43+      stderr: result?.stderr ?? fallbackResult.stderr,
44+      stdout: result?.stdout ?? fallbackResult.stdout,
45+      timedOut: result?.timedOut ?? fallbackResult.timedOut
46+    }
47+  };
48+}
49+
50 function buildFileReadOperationRequest(body: JsonObject): FileReadOperationRequest {
51   return {
52     cwd: readBodyField(body, "cwd") as FileReadOperationRequest["cwd"],
53@@ -3148,12 +3186,9 @@ async function handleSystemMutation(
54 
55 async function handleHostExec(context: LocalApiRequestContext): Promise<ConductorHttpResponse> {
56   const body = readBodyObject(context.request, true);
57+  const response = normalizeExecOperationResponse(await executeCommand(buildExecOperationRequest(body)));
58 
59-  return buildSuccessEnvelope(
60-    context.requestId,
61-    200,
62-    (await executeCommand(buildExecOperationRequest(body))) as unknown as JsonValue
63-  );
64+  return buildSuccessEnvelope(context.requestId, 200, response as unknown as JsonValue);
65 }
66 
67 async function handleHostFileRead(context: LocalApiRequestContext): Promise<ConductorHttpResponse> {
M docs/api/control-interfaces.md
+1, -0
1@@ -98,6 +98,7 @@
2 - `path`:可为绝对路径,也可相对 `cwd`
3 - `timeoutMs`:仅 `/v1/exec` 使用,默认 `30000`
4 - `overwrite`:仅 `/v1/files/write` 使用,默认 `true`
5+- `POST /v1/exec` 在成功和失败时都返回稳定的 `data.result`;失败时 AI caller 也可以直接依赖 `stdout`、`stderr`、`exitCode`、`signal`、`durationMs`、`startedAt`、`finishedAt`、`timedOut`
6 
7 ### 低层诊断
8 
M docs/api/local-host-ops.md
+25, -0
 1@@ -118,6 +118,27 @@
 2 }
 3 ```
 4 
 5+对于 `conductor-daemon` 的 HTTP `POST /v1/exec`,所有失败路径现在都返回稳定的 `result` 骨架。
 6+也就是说,即使请求在 macOS TCC 快速预检或输入校验阶段就被拦截,`data.result` 里也至少会有这些字段:
 7+
 8+- `stdout`
 9+- `stderr`
10+- `exitCode`
11+- `signal`
12+- `durationMs`
13+- `startedAt`
14+- `finishedAt`
15+- `timedOut`
16+
17+对于未真正启动子进程的提前失败,当前默认值为:
18+
19+- `stdout: ""`
20+- `stderr: ""`
21+- `exitCode: null`
22+- `signal: null`
23+- `durationMs: 0`
24+- `timedOut: false`
25+
26 ## HTTP Envelope
27 
28 `conductor-daemon` 的 HTTP 返回外层仍然使用统一 envelope:
29@@ -287,3 +308,7 @@ curl -X POST "${LOCAL_API_BASE}/v1/files/write" \
30 ```
31 
32 当前这只是针对 `cwd` 的快速预检,用来减少已知的 TCC 挂起场景;真正是否允许访问仍由 macOS 自身权限决定。
33+当前这只是针对 `cwd` 的快速预检,用来减少已知的 TCC 挂起场景;真正是否允许访问仍由 macOS 自身权限决定。
34+它不会解析 shell 变量、命令替换或更复杂的间接路径展开。
35+
36+补充说明:底层 `@baa-conductor/host-ops` 包返回原始 union;如果经由 `conductor-daemon` HTTP `/v1/exec` 调用,适配层还会把失败 `result` 补齐为稳定骨架,并把 `startedAt` / `finishedAt` 归一化为字符串时间戳。