baa-conductor


baa-conductor / docs / api
im_wower  ·  2026-03-24

local-host-ops.md

  1# Local Host Ops Contract
  2
  3`@baa-conductor/host-ops` 现在已经作为本机能力层挂到 `conductor-daemon` 的本地 HTTP 面:
  4
  5- `POST /v1/exec`
  6- `POST /v1/files/read`
  7- `POST /v1/files/write`
  8
  9当前状态:
 10
 11- 已有最小 Node 实现
 12- 已有结构化输入输出合同
 13- 已有 package smoke / HTTP 集成测试
 14- 已挂到 `conductor-daemon` 本地 API
 15- 不再挂到单独 Worker / D1 兼容层
 16
 17## Authentication
 18
 19这三条 HTTP 路由现在统一要求:
 20
 21- `Authorization: Bearer <BAA_SHARED_TOKEN>`
 22- token 来自 daemon 进程启动时配置的 `BAA_SHARED_TOKEN`
 23- 缺少 token 或 token 错误时,HTTP 层直接返回 `401` JSON 错误,不会进入实际 host operation
 24- 即使经由 `conductor.makefile.so`、Nginx 或 tunnel 转发,这三条路由也不是匿名公网接口
 25
 26## Operations
 27
 28| operation | request | success.result | failure.error |
 29| --- | --- | --- | --- |
 30| `exec` | `command`, `cwd?`, `timeoutMs?`, `maxBufferBytes?` | `stdout`, `stderr`, `exitCode`, `signal`, `durationMs`, `startedAt`, `finishedAt`, `timedOut` | `INVALID_INPUT`, `EXEC_TIMEOUT`, `EXEC_EXIT_NON_ZERO`, `EXEC_OUTPUT_LIMIT`, `EXEC_FAILED`, `TCC_PERMISSION_DENIED` |
 31| `files/read` | `path`, `cwd?`, `encoding?` | `absolutePath`, `content`, `sizeBytes`, `modifiedAt`, `encoding` | `INVALID_INPUT`, `FILE_NOT_FOUND`, `NOT_A_FILE`, `FILE_READ_FAILED` |
 32| `files/write` | `path`, `content`, `cwd?`, `encoding?`, `createParents?`, `overwrite?` | `absolutePath`, `bytesWritten`, `created`, `modifiedAt`, `encoding` | `INVALID_INPUT`, `FILE_ALREADY_EXISTS`, `NOT_A_FILE`, `FILE_WRITE_FAILED` |
 33
 34当前文本编码只支持 `utf8` 35
 36## Input Semantics
 37
 38- `cwd`:可选字符串。省略时使用 `conductor-daemon` 进程当前工作目录。
 39- `exec` 会在启动 shell 之前先校验 `cwd`:不存在时返回 `INVALID_INPUT``message: "exec.cwd does not exist: <cwd>"`;存在但不是目录时返回 `INVALID_INPUT``message: "exec.cwd is not a directory: <cwd>"`。两种情况都会在 `error.details.cwd` 里回传原始 `cwd` 40- `path`:可为绝对路径;相对路径会相对 `cwd` 解析。
 41- `timeoutMs`:仅 `exec` 使用。可选整数 `>= 0`,默认 `30000`;`0` 表示不启用超时。
 42- `maxBufferBytes`:仅 `exec` 使用。可选整数 `> 0`,默认 `10485760` 43- `exec` 子进程默认不会继承 `conductor-daemon` 的完整环境变量。当前在 macOS / Linux 只透传最小集合:`PATH`、`HOME`、`USER`、`LOGNAME`、`LANG`、`TERM`、`TMPDIR`;在 Windows 只透传启动 shell 和常见可执行文件解析所需的最小集合(如 `ComSpec`、`PATH`、`PATHEXT`、`SystemRoot`、`TEMP`、`TMP`、`USERNAME`、`USERPROFILE`、`WINDIR`)。因此 `BAA_SHARED_TOKEN`、`*_TOKEN`、`*_SECRET`、`*_KEY`、`*_PASSWORD` 等 daemon 自身环境变量不会默认暴露给 `exec` 44- 在 macOS 上,`exec` 只会对显式传入的 `cwd` 做 TCC 受保护目录前置检查,避免后台进程把工作目录直接设到 `Desktop` / `Documents` / `Downloads` 时无提示挂住 30 秒。`command` 仍按 shell 字符串原样执行,这个预检不是安全边界,也不会尝试解析 `command` 里的变量、`cd`、命令替换或其他间接路径。
 45- `createParents`:仅 `files/write` 使用。可选布尔值,默认 `true`;会递归创建缺失父目录。
 46- `overwrite`:仅 `files/write` 使用。可选布尔值,默认 `true`;为 `false` 且目标文件已存在时返回 `FILE_ALREADY_EXISTS` 47
 48## Response Shape
 49
 50包级返回 union 仍保持不变。
 51
 52`exec` 的失败返回现在也保证带完整的 `result` 结构,不再出现 `result: null` 或空对象:
 53
 54```json
 55{
 56  "result": {
 57    "stdout": "",
 58    "stderr": "",
 59    "exitCode": null,
 60    "signal": null,
 61    "durationMs": 0,
 62    "startedAt": null,
 63    "finishedAt": null,
 64    "timedOut": false
 65  }
 66}
 67```
 68
 69如果子进程已经启动,则 `result` 会继续返回实际采集到的 `stdout` / `stderr` / `durationMs` / 时间戳。
 70
 71成功返回:
 72
 73```json
 74{
 75  "ok": true,
 76  "operation": "files/read",
 77  "input": {
 78    "path": "README.md",
 79    "cwd": "/Users/george/code/baa-conductor",
 80    "encoding": "utf8"
 81  },
 82  "result": {
 83    "absolutePath": "/Users/george/code/baa-conductor/README.md",
 84    "content": "# baa-conductor\n...",
 85    "sizeBytes": 2783,
 86    "modifiedAt": "2026-03-22T09:01:00.000Z",
 87    "encoding": "utf8"
 88  }
 89}
 90```
 91
 92失败返回:
 93
 94```json
 95{
 96  "ok": false,
 97  "operation": "files/write",
 98  "input": {
 99    "path": "tmp/demo.txt",
100    "cwd": "/Users/george/code/baa-conductor",
101    "content": "hello",
102    "encoding": "utf8",
103    "createParents": true,
104    "overwrite": false
105  },
106  "error": {
107    "code": "FILE_ALREADY_EXISTS",
108    "message": "File already exists at /Users/george/code/baa-conductor/tmp/demo.txt and overwrite=false.",
109    "retryable": false,
110    "details": {
111      "overwrite": false
112    }
113  },
114  "result": {
115    "absolutePath": "/Users/george/code/baa-conductor/tmp/demo.txt",
116    "encoding": "utf8"
117  }
118}
119```
120
121对于 `conductor-daemon` 的 HTTP `POST /v1/exec`,所有失败路径现在都返回稳定的 `result` 骨架。
122也就是说,即使请求在 macOS TCC 快速预检或输入校验阶段就被拦截,`data.result` 里也至少会有这些字段:
123
124- `stdout`
125- `stderr`
126- `exitCode`
127- `signal`
128- `durationMs`
129- `startedAt`
130- `finishedAt`
131- `timedOut`
132
133对于未真正启动子进程的提前失败,当前默认值为:
134
135- `stdout: ""`
136- `stderr: ""`
137- `exitCode: null`
138- `signal: null`
139- `durationMs: 0`
140- `timedOut: false`
141
142## HTTP Envelope
143
144`conductor-daemon` 的 HTTP 返回外层仍然使用统一 envelope:
145
146```json
147{
148  "ok": true,
149  "request_id": "req_xxx",
150  "data": {
151    "ok": true,
152    "operation": "exec",
153    "input": {
154      "command": "printf 'hello'",
155      "cwd": "/tmp",
156      "timeoutMs": 2000,
157      "maxBufferBytes": 10485760
158    },
159    "result": {
160      "stdout": "hello",
161      "stderr": "",
162      "exitCode": 0,
163      "signal": null,
164      "durationMs": 12,
165      "startedAt": "2026-03-22T09:10:00.000Z",
166      "finishedAt": "2026-03-22T09:10:00.012Z",
167      "timedOut": false
168    }
169  }
170}
171```
172
173也就是说:
174
175- 外层 `ok` / `request_id` 属于 `conductor-daemon` HTTP 协议
176- 内层 `data.ok` / `data.operation` / `data.error` 属于 `host-ops` 结构化结果
177
178未授权示例:
179
180```json
181{
182  "ok": false,
183  "request_id": "req_xxx",
184  "error": "unauthorized",
185  "message": "POST /v1/exec requires Authorization: Bearer <BAA_SHARED_TOKEN>.",
186  "details": {
187    "auth_scheme": "Bearer",
188    "env_var": "BAA_SHARED_TOKEN",
189    "route_id": "host.exec",
190    "reason": "missing_authorization_header"
191  }
192}
193```
194
195## Minimal Curl
196
197```bash
198LOCAL_API_BASE="${BAA_CONDUCTOR_LOCAL_API:-http://127.0.0.1:4317}"
199SHARED_TOKEN="${BAA_SHARED_TOKEN:?set BAA_SHARED_TOKEN}"
200curl -X POST "${LOCAL_API_BASE}/v1/exec" \
201  -H "Authorization: Bearer ${SHARED_TOKEN}" \
202  -H 'Content-Type: application/json' \
203  -d '{"command":"printf '\''hello from conductor'\''","cwd":"/tmp","timeoutMs":2000}'
204```
205
206```bash
207LOCAL_API_BASE="${BAA_CONDUCTOR_LOCAL_API:-http://127.0.0.1:4317}"
208SHARED_TOKEN="${BAA_SHARED_TOKEN:?set BAA_SHARED_TOKEN}"
209curl -X POST "${LOCAL_API_BASE}/v1/files/read" \
210  -H "Authorization: Bearer ${SHARED_TOKEN}" \
211  -H 'Content-Type: application/json' \
212  -d '{"path":"README.md","cwd":"/Users/george/code/baa-conductor","encoding":"utf8"}'
213```
214
215```bash
216LOCAL_API_BASE="${BAA_CONDUCTOR_LOCAL_API:-http://127.0.0.1:4317}"
217SHARED_TOKEN="${BAA_SHARED_TOKEN:?set BAA_SHARED_TOKEN}"
218curl -X POST "${LOCAL_API_BASE}/v1/files/write" \
219  -H "Authorization: Bearer ${SHARED_TOKEN}" \
220  -H 'Content-Type: application/json' \
221  -d '{"path":"tmp/demo.txt","cwd":"/Users/george/code/baa-conductor","content":"hello from conductor","overwrite":false,"createParents":true}'
222```
223
224## Package API
225
226导出函数:
227
228- `executeCommand(request)`
229- `readTextFile(request)`
230- `writeTextFile(request)`
231- `runHostOperation(request)`
232
233它们全部返回结构化 result union,不依赖 HTTP 层;HTTP 层只是把这份 union 包进 `data`234
235## Exec cwd Validation
236
237`POST /v1/exec` 传入不存在的 `cwd` 时,会在调用 `child_process.exec()` 之前直接返回:
238
239```json
240{
241  "ok": false,
242  "operation": "exec",
243  "input": {
244    "command": "echo ok",
245    "cwd": "/tmp/does-not-exist",
246    "timeoutMs": 30000,
247    "maxBufferBytes": 10485760
248  },
249  "error": {
250    "code": "INVALID_INPUT",
251    "message": "exec.cwd does not exist: /tmp/does-not-exist",
252    "retryable": false,
253    "details": {
254      "cwd": "/tmp/does-not-exist"
255    }
256  },
257  "result": {
258    "stdout": "",
259    "stderr": "",
260    "exitCode": null,
261    "signal": null,
262    "durationMs": 0,
263    "startedAt": null,
264    "finishedAt": null,
265    "timedOut": false
266  }
267}
268```
269
270如果 `cwd` 指向的是文件而不是目录,返回同样的 `INVALID_INPUT` 结构,但 message 会改成 `exec.cwd is not a directory: <cwd>`271
272## macOS TCC Fast-Fail
273
274如果 `exec` 在 macOS 上检测到 `cwd` 落在受 TCC 保护的用户目录内,会在启动子进程前直接返回:
275
276```json
277{
278  "ok": false,
279  "operation": "exec",
280  "input": {
281    "command": "pwd",
282    "cwd": "/Users/george/Desktop/project",
283    "timeoutMs": 2000,
284    "maxBufferBytes": 10485760
285  },
286  "error": {
287    "code": "TCC_PERMISSION_DENIED",
288    "message": "Command cwd resolves inside macOS TCC-protected path /Users/george/Desktop. Grant Full Disk Access to the Node.js binary running conductor-daemon (/opt/homebrew/bin/node) and retry.",
289    "retryable": false,
290    "details": {
291      "accessPoint": "cwd",
292      "protectedPath": "/Users/george/Desktop",
293      "nodeBinary": "/opt/homebrew/bin/node",
294      "requiresFullDiskAccess": true
295    }
296  },
297  "result": {
298    "stdout": "",
299    "stderr": "",
300    "exitCode": null,
301    "signal": null,
302    "durationMs": 0,
303    "startedAt": null,
304    "finishedAt": null,
305    "timedOut": false
306  }
307}
308```
309
310当前这只是针对 `cwd` 的快速预检,用来减少已知的 TCC 挂起场景;真正是否允许访问仍由 macOS 自身权限决定。
311当前这只是针对 `cwd` 的快速预检,用来减少已知的 TCC 挂起场景;真正是否允许访问仍由 macOS 自身权限决定。
312它不会解析 shell 变量、命令替换或更复杂的间接路径展开。
313
314补充说明:底层 `@baa-conductor/host-ops` 包返回原始 union;如果经由 `conductor-daemon` HTTP `/v1/exec` 调用,适配层还会把失败 `result` 补齐为稳定骨架,并把 `startedAt` / `finishedAt` 归一化为字符串时间戳。