diff --git a/README.md b/README.md index 1a14728..a19997f 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ - 当前 `scripts/browser-control-smoke.mjs` 已经能对目标 URL 做一次真实最小探测:抓取页面标题并写回聊天结果;桌面 GUI 控制默认先走 `scripts/codex-computer-use-runtime.mjs`,由 Codex App Server 发起 Codex Computer Use 执行;失败后自动回退 `scripts/cua-driver-computer-use-runtime.mjs`,通过外部 `cua-driver` 执行 `launch_app -> get_window_state -> 可选 type_text/press_key -> get_window_state` 闭环;`scripts/computer-use-smoke.mjs` 仍保留为旧兜底和回归资产 - 受控 Mac 需要先安装并授权 `cua-driver`;Boss runtime 会优先搜索 `PATH`,再搜索 `~/.local/bin/cua-driver`、`/usr/local/bin/cua-driver`、`/opt/homebrew/bin/cua-driver` 和 `/Applications/CuaDriver.app/Contents/MacOS/cua-driver`;如果仍找不到,会明确返回 `CUA_DRIVER_COMMAND_NOT_FOUND`,不会伪装成执行成功 - 当前默认本机配置已把 `browserAutomation / computerUse` 两项能力直接上报为在线起步态,所以 Boss App 里这台 Mac 会显示“可做浏览器控制 / 桌面控制”;如果某条链路要临时收起,只需要改 `local-agent/config.cloud.json` -- 当前 `local-agent` 已新增 `Codex App Server` runner:boss-agent 默认打开 `codexAppServerEnabled`,通过 `codex app-server` stdio 接入 `conversation_reply / dispatch_execution`,也可灰度切到 `ws://127.0.0.1:` 或 `unix://PATH` 本机长驻 App Server;WebSocket/Unix WebSocket handshake 支持 `Authorization: Bearer `,优先用 `codexAppServerAuthTokenFile` 保存本地 token。失败时只在 turn 未启动前回退 `codex exec resume`,避免重复执行同一轮对话。设备 heartbeat 会单独上报 `codexAppServer` capability。2026-05-31 起,runner 会吸收 App Server 的 plan / diff / item / subagent 事件并归一到 Boss `execution_progress` 进度卡,执行中通过 `POST /api/v1/master-agent/tasks/[taskId]/progress` 实时刷新;本机 `codex-cli 0.135.0-alpha.1` 协议快照已生成在 `docs/protocol-snapshots/codex-app-server/0.135.0-alpha.1/`。同日新增第一版 Inter-Thread Broker:任务携带源/目标 Codex 线程时可通过 `thread/read -> thread/inject_items -> turn/start` 完成受控线程协作;服务端新增 `POST /api/v1/projects/[projectId]/thread-collaboration` 作为 APP/后台可调用入口;任务携带 `targetCodexTurnId` 时 runner 会改用 `turn/steer` 干预活跃 turn。 +- 当前 `local-agent` 已新增 `Codex App Server` runner:boss-agent 默认打开 `codexAppServerEnabled`,通过 `codex app-server` stdio 接入 `conversation_reply / dispatch_execution`,也可灰度切到 `ws://127.0.0.1:` 或 `unix://PATH` 本机长驻 App Server;WebSocket/Unix WebSocket handshake 支持 `Authorization: Bearer `,优先用 `codexAppServerAuthTokenFile` 保存本地 token。失败时只在 turn 未启动前回退 `codex exec resume`,避免重复执行同一轮对话。设备 heartbeat 会单独上报 `codexAppServer` capability。2026-05-31 起,runner 会吸收 App Server 的 plan / diff / item / subagent 事件并归一到 Boss `execution_progress` 进度卡,执行中通过 `POST /api/v1/master-agent/tasks/[taskId]/progress` 实时刷新;同日第二批已补 `item/*/requestApproval`、`item/autoApprovalReview/*`、`guardianWarning`、`serverRequest/resolved`、`item/fileChange/patchUpdated` 到 `approvals / warnings / fileChanges`,Android 原生进度卡可显示安全提醒、审批状态和文件变更摘要,且不展示完整命令、diff、系统提示词或密钥。本机 `codex-cli 0.135.0-alpha.1` 协议快照已生成在 `docs/protocol-snapshots/codex-app-server/0.135.0-alpha.1/`。同日新增第一版 Inter-Thread Broker:任务携带源/目标 Codex 线程时可通过 `thread/read -> thread/inject_items -> turn/start` 完成受控线程协作;服务端新增 `POST /api/v1/projects/[projectId]/thread-collaboration` 作为 APP/后台可调用入口;任务携带 `targetCodexTurnId` 时 runner 会改用 `turn/steer` 干预活跃 turn。 - `GET http://127.0.0.1:4317/api/v1/skills` 正常,已返回本机扫描到的 Codex Skill - `POST http://127.0.0.1:4317/api/v1/heartbeat` 正常,且会顺带触发 `thread-context` 上报 - `local-agent` 当前每 5 秒轮询一次本机 Skill lifecycle 请求;默认打开 `skillLifecycleEnabled=true`。远程 `install` 或带 `sourceUrl` 的更新必须命中 `skillLifecycleAllowedSources` 或 `skillLifecycleTrustedSources`,为空时只允许既有本地 Skill 的 `update / rollback / uninstall / version_lock`;请求携带 `checksum / expectedChecksum` 时会校验 `manifest.json` 或 `SKILL.md` 的 sha256,失败会清理半安装目录或尽量恢复备份。卸载 / 更新 / 回滚前会在 `skillsDir/.boss-skill-backups` 保留备份,卸载仍限制在 `skillsDir` 目录内,版本锁写入 `.boss-skill-locks.json` diff --git a/android/app/src/main/java/com/hyzq/boss/BossUi.java b/android/app/src/main/java/com/hyzq/boss/BossUi.java index ff7a51d..bebb195 100644 --- a/android/app/src/main/java/com/hyzq/boss/BossUi.java +++ b/android/app/src/main/java/com/hyzq/boss/BossUi.java @@ -1296,6 +1296,74 @@ public final class BossUi { return card; } + JSONArray warnings = progress == null ? null : progress.optJSONArray("warnings"); + if (warnings != null && warnings.length() > 0) { + card.addView(divider(context)); + card.addView(sectionTitle(context, "安全提醒")); + int shown = 0; + for (int i = 0; i < warnings.length(); i += 1) { + JSONObject warning = warnings.optJSONObject(i); + String message = warning == null ? "" : warning.optString("message", "").trim(); + if (TextUtils.isEmpty(message)) { + continue; + } + card.addView(detailRow(context, "!", message, "", false)); + shown += 1; + if (shown >= 3) { + break; + } + } + } + + JSONArray approvals = progress == null ? null : progress.optJSONArray("approvals"); + if (approvals != null && approvals.length() > 0) { + card.addView(divider(context)); + card.addView(sectionTitle(context, "审批状态")); + int shown = 0; + for (int i = 0; i < approvals.length(); i += 1) { + JSONObject approval = approvals.optJSONObject(i); + String label = approval == null ? "" : approval.optString("label", "").trim(); + if (TextUtils.isEmpty(label)) { + continue; + } + String status = approval.optString("status", "").trim(); + String detail = approval.optString("detail", "").trim(); + String riskLevel = approval.optString("riskLevel", "").trim(); + String rowLabel = TextUtils.isEmpty(status) ? label : label + " · " + status; + card.addView(detailRow(context, "◇", rowLabel, riskLevel, false)); + if (!TextUtils.isEmpty(detail)) { + card.addView(detailRow(context, "", detail, "", false, true)); + } + shown += 1; + if (shown >= 4) { + break; + } + } + } + + JSONArray fileChanges = progress == null ? null : progress.optJSONArray("fileChanges"); + if (fileChanges != null && fileChanges.length() > 0) { + card.addView(divider(context)); + card.addView(sectionTitle(context, "文件变更")); + int shown = 0; + for (int i = 0; i < fileChanges.length(); i += 1) { + JSONObject fileChange = fileChanges.optJSONObject(i); + String path = fileChange == null ? "" : fileChange.optString("path", "").trim(); + if (TextUtils.isEmpty(path)) { + continue; + } + String kind = fileChange.optString("kind", "").trim(); + card.addView(detailRow(context, "▣", path, kind, false)); + shown += 1; + if (shown >= 6) { + break; + } + } + if (fileChanges.length() > shown) { + card.addView(detailRow(context, "", "再显示 " + (fileChanges.length() - shown) + " 个", "", false, true)); + } + } + card.addView(divider(context)); card.addView(sectionTitle(context, "分支详情")); JSONObject branch = progress == null ? null : progress.optJSONObject("branch"); diff --git a/android/app/src/test/java/com/hyzq/boss/ProjectDetailActivityUiTest.java b/android/app/src/test/java/com/hyzq/boss/ProjectDetailActivityUiTest.java index 1c5ec7c..3fc7c47 100644 --- a/android/app/src/test/java/com/hyzq/boss/ProjectDetailActivityUiTest.java +++ b/android/app/src/test/java/com/hyzq/boss/ProjectDetailActivityUiTest.java @@ -904,6 +904,60 @@ public class ProjectDetailActivityUiTest { assertTrue(viewTreeContainsText(messageView, "Mendel(explorer)")); } + @Test + public void executionProgressMessageRendersCodexApprovalAndFileChangeSections() throws Exception { + Intent intent = new Intent() + .putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "thread-approval") + .putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "Boss开发主线程"); + TestProjectDetailActivity activity = Robolectric + .buildActivity(TestProjectDetailActivity.class, intent) + .setup() + .get(); + + JSONObject message = new JSONObject() + .put("id", "progress-approval-1") + .put("sender", "master") + .put("senderLabel", "主 Agent") + .put("body", "执行进度") + .put("kind", "execution_progress") + .put("sentAt", "2026-05-31T10:16:00+08:00") + .put("executionProgress", new JSONObject() + .put("status", "running") + .put("steps", new JSONArray() + .put(new JSONObject().put("text", "等待 Codex 审批事件").put("status", "running"))) + .put("approvals", new JSONArray() + .put(new JSONObject() + .put("label", "命令执行审批") + .put("status", "resolved") + .put("detail", "需要确认命令执行") + .put("riskLevel", "medium"))) + .put("warnings", new JSONArray() + .put(new JSONObject() + .put("message", "检测到需要用户确认的命令执行。") + .put("severity", "warning"))) + .put("fileChanges", new JSONArray() + .put(new JSONObject() + .put("path", "src/app/page.tsx") + .put("kind", "update") + .put("status", "updated")))); + + View messageView = ReflectionHelpers.callInstanceMethod( + activity, + "buildMessageView", + ReflectionHelpers.ClassParameter.from(JSONObject.class, message) + ); + + assertTrue(viewTreeContainsText(messageView, "安全提醒")); + assertTrue(viewTreeContainsText(messageView, "检测到需要用户确认的命令执行。")); + assertTrue(viewTreeContainsText(messageView, "审批状态")); + assertTrue(viewTreeContainsText(messageView, "命令执行审批 · resolved")); + assertTrue(viewTreeContainsText(messageView, "需要确认命令执行")); + assertTrue(viewTreeContainsText(messageView, "文件变更")); + assertTrue(viewTreeContainsText(messageView, "src/app/page.tsx")); + assertFalse(viewTreeContainsText(messageView, "sk-secret")); + assertFalse(viewTreeContainsText(messageView, "diff")); + } + @Test public void nativeRemoteExecutionProgressDoesNotRenderCodexSections() throws Exception { Intent intent = new Intent() diff --git a/docs/architecture/ai_handoff_index_cn.md b/docs/architecture/ai_handoff_index_cn.md index c6deebd..e98327c 100644 --- a/docs/architecture/ai_handoff_index_cn.md +++ b/docs/architecture/ai_handoff_index_cn.md @@ -153,7 +153,7 @@ - Web 和原生 Android 当前都已经接上“新设备导入草稿 -> 勾选 -> 决议预览 -> 应用导入”的前台页面;已绑定生产设备继续保留 heartbeat 自动导入链路 - 原生首页的刷新失败策略当前已改成按当前 tab 独立判错,不会再因为 `设备 / 设置 / OTA` 的旁路请求失败把会话页刷新一并判成失败 - 当前量产方向已经明确为“Boss 企业控制面 + 可插拔执行协议”:多租户、权限、审批、审计、备份、回退和 Skill 治理由 Boss 承担,Codex App Server / Codex MCP / Codex CLI / Computer Use / 业务系统 API 都作为 provider 接入;详见 `docs/architecture/enterprise_ai_ops_architecture_cn.md` -- 当前 Codex App Server 已完成第一批接入:boss-agent 默认开启 `local-agent/codex-app-server-runner.mjs` 作为 Codex 绑定入口,优先走 `codex app-server` stdio,也可灰度连接 `ws://127.0.0.1:` 或 `unix://PATH` 同机长驻 App Server;长驻连接支持 `Authorization: Bearer `,配置上优先使用 `codexAppServerAuthTokenFile`。turn 启动前失败才回退 CLI,turn 启动后不重复执行;桌面远程控制默认先走 `codex-computer-use`,失败后回退 `cua-driver-computer-use`。2026-05-31 已按本机 `codex-cli 0.135.0-alpha.1` 生成协议快照 `docs/protocol-snapshots/codex-app-server/0.135.0-alpha.1/`,并把 `turn/plan/updated`、`turn/diff/updated`、`item/started|completed`、`thread/started` 归一到 Boss `execution_progress` 卡片;同批已补 `turn/steer` 活跃 turn 干预和 `POST /api/v1/projects/[projectId]/thread-collaboration` 服务端线程协作排队入口。 +- 当前 Codex App Server 已完成第一批和第二批接入:boss-agent 默认开启 `local-agent/codex-app-server-runner.mjs` 作为 Codex 绑定入口,优先走 `codex app-server` stdio,也可灰度连接 `ws://127.0.0.1:` 或 `unix://PATH` 同机长驻 App Server;长驻连接支持 `Authorization: Bearer `,配置上优先使用 `codexAppServerAuthTokenFile`。turn 启动前失败才回退 CLI,turn 启动后不重复执行;桌面远程控制默认先走 `codex-computer-use`,失败后回退 `cua-driver-computer-use`。2026-05-31 已按本机 `codex-cli 0.135.0-alpha.1` 生成协议快照 `docs/protocol-snapshots/codex-app-server/0.135.0-alpha.1/`,并把 `turn/plan/updated`、`turn/diff/updated`、`item/started|completed`、`thread/started`、`item/*/requestApproval`、`item/autoApprovalReview/*`、`guardianWarning`、`serverRequest/resolved`、`item/fileChange/patchUpdated` 归一到 Boss `execution_progress` 卡片;同批已补 `turn/steer` 活跃 turn 干预和 `POST /api/v1/projects/[projectId]/thread-collaboration` 服务端线程协作排队入口。 - 当前 boss-agent 已支持 Mac OTA:`local-agent/boss-agent-ota-runner.mjs` 默认开启,每 5 分钟检查服务端最新包;状态页可手动检查或下载并安装,安装时保留原绑定配置,只更新版本号和本机 runtime 路径。最新验证版本为 `20260516221619`,已在 MacBook Air `macbook-air` 上确认 OTA 下载校验、暂存、覆盖安装后不会误切到默认 `config.cloud.json`。正式分发脚本已预留 Developer ID 公证路径:`BOSS_AGENT_NOTARIZE=1` 配合 notary profile 或 Apple ID 凭据。 - 当前量产治理已补设备撤权和任务可靠性底座:`revoke_device` 会清空设备 token、标记离线并阻断 heartbeat / 任务认领 / Skill 同步 / 日志上报 / boss-agent OTA;`MasterAgentTask` claim 会记录 attempt 和 lease,运行中任务可按租约重试,超过上限转 `timed_out`,用户或管理员可通过 cancel 接口转 `canceled` 且迟到 complete 不覆盖终态。 - 当前群聊 `dispatch_execution` 完成回写已补幂等,重复完成不会再向群聊重复追加结果 diff --git a/docs/architecture/api_and_service_inventory_cn.md b/docs/architecture/api_and_service_inventory_cn.md index 5111fc8..275c9ef 100644 --- a/docs/architecture/api_and_service_inventory_cn.md +++ b/docs/architecture/api_and_service_inventory_cn.md @@ -117,7 +117,7 @@ - 当前 `RemoteRuntimeAdapter` 还负责拦截固定模式的线程内部环境提示;命中后会直接改写成失败,避免把只读/cwd 这类脏文本写进聊天记录 - 当前普通单线程 `conversation_reply` 在真正执行 `codex exec resume` 前,会先把 Boss 用户消息镜像进目标 Codex Desktop rollout;定位优先走 `state_5.sqlite`,不可用时回退扫描 `~/.codex/sessions`,并按 `sourceMessageId` 去重 - 当前 Codex Desktop 同步新增常驻刷新桥:`scripts/codex-desktop-refresh-bridge-daemon.mjs` 通过 launchd 监听 `127.0.0.1:4318`,暴露 `POST /api/v1/codex-desktop/refresh`、`GET /api/v1/codex-desktop/events`、`GET /api/v1/codex-desktop/events/recent` 和 `GET /api/v1/codex-desktop/capabilities`;`local-agent` 会优先调用 refresh endpoint,失败时回退到 `scripts/codex-desktop-refresh-hint.mjs` 命令式刷新。SSE 事件只包含线程引用、消息 ID、状态、deep link 等安全元数据,不包含用户正文或内部 prompt;`scripts/codex-desktop-event-consumer.mjs` 可作为 Desktop 插件/IPC 接入前的订阅 smoke;`scripts/codex-desktop-integration-probe.mjs` 负责只读探测 Codex.app 能力 -- 当前新增 Codex App Server runner:`local-agent/codex-app-server-runner.mjs`。boss-agent 默认配置 `codexAppServerEnabled=true`,会接管 `conversation_reply / dispatch_execution`;它默认通过 stdio 启动 `codex app-server`,也支持 `codexAppServerTransport=ws + codexAppServerUrl=ws://127.0.0.1:` 或 `codexAppServerTransport=unix + codexAppServerUrl=unix:///absolute/path.sock` 连接同机长驻 App Server,bearer token 可通过 `codexAppServerAuthTokenFile` 读取并在握手时发送 `Authorization: Bearer `。runner 执行 `initialize -> thread/resume|thread/start -> turn/start|turn/steer`,并把 `item/agentMessage/delta` 或 `item/completed` 归一成 Boss 任务回复;当 App Server 对单个 JSON-RPC 请求返回 `-32001 / retry later` 时,runner 会做最多 3 次指数退避重试。turn 启动前失败可回退 CLI,turn 启动后失败不回退,避免重复执行。2026-05-31 起,runner 会把 `turn/plan/updated`、`turn/diff/updated`、`item/started|completed`、`thread/started` 归一成 `executionProgress.steps / branch / artifacts / agents`,服务端 complete 回写会与本地 Git/GitHub 进度合并。 +- 当前新增 Codex App Server runner:`local-agent/codex-app-server-runner.mjs`。boss-agent 默认配置 `codexAppServerEnabled=true`,会接管 `conversation_reply / dispatch_execution`;它默认通过 stdio 启动 `codex app-server`,也支持 `codexAppServerTransport=ws + codexAppServerUrl=ws://127.0.0.1:` 或 `codexAppServerTransport=unix + codexAppServerUrl=unix:///absolute/path.sock` 连接同机长驻 App Server,bearer token 可通过 `codexAppServerAuthTokenFile` 读取并在握手时发送 `Authorization: Bearer `。runner 执行 `initialize -> thread/resume|thread/start -> turn/start|turn/steer`,并把 `item/agentMessage/delta` 或 `item/completed` 归一成 Boss 任务回复;当 App Server 对单个 JSON-RPC 请求返回 `-32001 / retry later` 时,runner 会做最多 3 次指数退避重试。turn 启动前失败可回退 CLI,turn 启动后失败不回退,避免重复执行。2026-05-31 起,runner 会把 `turn/plan/updated`、`turn/diff/updated`、`item/started|completed`、`thread/started` 归一成 `executionProgress.steps / branch / artifacts / agents`,并把 `item/*/requestApproval`、`item/autoApprovalReview/*`、`guardianWarning`、`serverRequest/resolved`、`item/fileChange/patchUpdated` 归一成 `executionProgress.approvals / warnings / fileChanges`;服务端 complete 回写会与本地 Git/GitHub 进度合并。 - 当前 Codex App Server runner 已新增第一版 Boss Inter-Thread Broker:任务携带 `intentCategory=thread_collaboration`、`sourceCodexThreadRef` 和 `targetCodexThreadRef` 时,会先 `thread/read` 源线程,再通过 `thread/inject_items` 向目标线程注入受控摘要,最后 `turn/start` 目标线程;服务端入口是 `POST /api/v1/projects/[projectId]/thread-collaboration`,负责权限、源/目标线程校验和任务排队。这不是假设官方线程 P2P,而是 Boss 自己做线程协作编排。 - 当前 boss-agent Mac OTA 已接入:`local-agent/boss-agent-ota-runner.mjs` 会用设备 token 调 Boss 服务端 `/api/v1/boss-agent/ota` 检查最新 Mac 运行包,`/api/v1/boss-agent/ota/apply` 会下载 `boss-agent-mac-latest.zip`、校验 sha256、暂存安装 wrapper,并拉起本机安装器;安装脚本会保留绑定配置并只更新版本号与本机 runtime 路径。安装器会优先沿用当前 LaunchAgent active config,并保留所有 `config*.json`,避免多电脑场景中误绑定到默认设备配置。当前最新验证包为 `20260516221619`;构建脚本支持 `BOSS_AGENT_NOTARIZE=1` 的 Developer ID 公证路径。 - 当前 `local-agent` 还新增了两条统一电脑控制 runtime: diff --git a/docs/architecture/codex_server_progress_card_cn.md b/docs/architecture/codex_server_progress_card_cn.md index adccba3..08d06d3 100644 --- a/docs/architecture/codex_server_progress_card_cn.md +++ b/docs/architecture/codex_server_progress_card_cn.md @@ -16,7 +16,7 @@ Codex App Server 是更适合 Boss 长期接入的协议层,因为它面向富 - model/list、skills/list、plugin/list、app/list - command execution、file change、tool input、MCP tool-call approvals -Boss 不能直接把 App Server 原始 Thread / Turn / Item 字段写进业务层。当前第一批已经新增 `local-agent/codex-app-server-runner.mjs`,把 App Server 的 `thread/resume | thread/start -> turn/start -> item/agentMessage/delta -> turn/completed` 映射成 Boss 的普通任务完成回写;2026-05-31 已继续把 `turn/plan/updated`、`turn/diff/updated`、`item/started|completed`、`thread/started` 这类协议事件归一化为 Boss `execution_progress` 的步骤、分支变更、产物和后台智能体。 +Boss 不能直接把 App Server 原始 Thread / Turn / Item 字段写进业务层。当前第一批已经新增 `local-agent/codex-app-server-runner.mjs`,把 App Server 的 `thread/resume | thread/start -> turn/start -> item/agentMessage/delta -> turn/completed` 映射成 Boss 的普通任务完成回写;2026-05-31 已继续把 `turn/plan/updated`、`turn/diff/updated`、`item/started|completed`、`thread/started` 这类协议事件归一化为 Boss `execution_progress` 的步骤、分支变更、产物和后台智能体。同日第二批补齐 `item/*/requestApproval`、`item/autoApprovalReview/*`、`guardianWarning`、`serverRequest/resolved` 和 `item/fileChange/patchUpdated` 的安全摘要映射,APP 只展示审批状态、风险提醒和文件路径,不展示完整命令、diff、系统提示词或密钥。 官方文档入口:`https://developers.openai.com/codex/app-server` @@ -69,6 +69,9 @@ Boss 不能直接把 App Server 原始 Thread / Turn / Item 字段写进业务 APP 展示结构对齐截图: - `进度`:步骤列表,显示已完成 / 进行中 / 待处理 / 失败 +- `安全提醒`:展示 Guardian warning 的用户可读摘要 +- `审批状态`:展示命令 / 文件 / 权限审批与自动复核状态 +- `文件变更`:展示 App Server patchUpdated 中的文件路径和变更类型,不展示 diff - `分支详情`:变更行、Git 操作、GitHub CLI 可用状态 - `生成结果`:从执行结果里提取文件、图片、APK、文档等产物名 - `后台智能体`:预留 OMX / Hermes / explorer 等多智能体来源展示 @@ -115,6 +118,7 @@ UI 参考: - `device-heartbeat` 设备能力新增 `codexAppServer`,用于前台和后台知道该设备是否具备 App Server provider - 新增 `scripts/codex-app-server-protocol-snapshot.mjs`,可把本机 Codex App Server 的 help、JSON Schema、TypeScript bindings 和方法清单生成到 `docs/protocol-snapshots/codex-app-server//` - `local-agent/codex-app-server-runner.mjs` 已吸收 App Server 协议进度事件,并把 plan、diff、artifact、subagent 归一成 Boss `executionProgress`,服务端 complete 回写会与本地 Git/GitHub 进度合并,不再覆盖协议原生进度 +- `local-agent/codex-app-server-runner.mjs` 已把 App Server 审批、Guardian warning 和 file-change patch 事件归一成 `executionProgress.approvals / warnings / fileChanges`;服务端和 Android 原生进度卡已支持展示,且测试覆盖了密钥和 diff 不外泄 - 新增实时进度入口 `POST /api/v1/master-agent/tasks/[taskId]/progress`,设备端可在任务执行中持续刷新同一张 `execution_progress` 卡;`local-agent` 的 App Server runner 已在收到协议进度事件时调用该接口,complete 仍携带最终进度作为兜底 - 新增服务端线程协作入口 `POST /api/v1/projects/[projectId]/thread-collaboration`,由 Boss 校验源/目标项目权限并创建 `intentCategory=thread_collaboration` 的 `conversation_reply` 任务;设备端继续通过 App Server runner 执行 `thread/read -> thread/inject_items -> turn/start`,避免把“线程互通”误做成无监管 P2P - 新增活跃 turn 干预:任务携带 `targetCodexTurnId` / `targetTurnId` 时,App Server runner 会调用 `turn/steer`,并把 `turnControl=steer`、`turnId` 写回执行结果;没有活跃 turn id 时仍使用 `turn/start` diff --git a/docs/architecture/current_runtime_and_deploy_status_cn.md b/docs/architecture/current_runtime_and_deploy_status_cn.md index 1be6024..11046b4 100644 --- a/docs/architecture/current_runtime_and_deploy_status_cn.md +++ b/docs/architecture/current_runtime_and_deploy_status_cn.md @@ -249,7 +249,7 @@ cd /Users/kris/code/boss - 当前 `local-agent` 已新增 `Codex App Server` provider:boss-agent 默认配置 `codexAppServerEnabled=true`,`conversation_reply / dispatch_execution` 会先通过 `codex app-server` 的 stdio JSON-RPC 恢复或创建线程,也可配置 `codexAppServerTransport=ws + codexAppServerUrl=ws://127.0.0.1:` 或 `codexAppServerTransport=unix + codexAppServerUrl=unix:///absolute/path.sock` 连接同机长驻 App Server;长驻连接可通过 `codexAppServerAuthTokenFile` 或 `BOSS_CODEX_APP_SERVER_AUTH_TOKEN_FILE` 提供 bearer token。随后 runner 下发 `turn/start` 并收集流式 agent 回复;如果单个 JSON-RPC 请求返回 `-32001 / retry later`,runner 会先做指数退避重试;如果任务携带 `targetCodexTurnId`,会改用 `turn/steer` 干预活跃 turn;如果 App Server 在 turn 启动前失败,默认允许回退到 `codex exec resume`,如果 turn 已经启动则不再回退,避免同一轮用户消息被重复执行。桌面控制另有 `codexComputerUseEnabled=true`,默认先走 Codex Computer Use,再回退 CUA Driver。 - 当前已新增 Boss 自有 Inter-Thread Broker 第一版:服务端入口 `POST /api/v1/projects/[projectId]/thread-collaboration` 会创建带源/目标 Codex 线程引用的协作任务;App Server runner 执行 `thread/read(source) -> thread/inject_items(target) -> turn/start(target)`,用于让一个线程的结论受控进入另一个线程,不依赖官方任意线程 P2P 互聊能力 - 当前 `local-agent` 对 `dispatch_execution` 任务会按 `orchestrationBackendId` 分流:默认走 `codex exec resume`;当任务显式选择 `omx-team` 且本机 `omxEnabled + omxCommand/omxArgs` 可用时,会改走 `OMX Team Runtime` JSON 协议执行并回写 `rawThreadReply / replyBody` -- 当前 `local-agent` 会在 Codex 任务执行中和完成时回传 `executionProgress`:服务端把同一任务的进度卡从 queued / running 更新到 completed / failed,Android 原生聊天页会显示“进度 / 分支详情 / 生成结果 / 后台智能体”。2026-05-31 起,Codex App Server 的 `turn/plan/updated`、`turn/diff/updated`、`item/started|completed`、`thread/started` 会直接映射为进度步骤、变更统计、生成产物和后台智能体,并通过 `POST /api/v1/master-agent/tasks/[taskId]/progress` 实时刷新;complete 回写仍会携带最终进度兜底 +- 当前 `local-agent` 会在 Codex 任务执行中和完成时回传 `executionProgress`:服务端把同一任务的进度卡从 queued / running 更新到 completed / failed,Android 原生聊天页会显示“进度 / 安全提醒 / 审批状态 / 文件变更 / 分支详情 / 生成结果 / 后台智能体”。2026-05-31 起,Codex App Server 的 `turn/plan/updated`、`turn/diff/updated`、`item/started|completed`、`thread/started` 会直接映射为进度步骤、变更统计、生成产物和后台智能体;第二批已把 `item/*/requestApproval`、`item/autoApprovalReview/*`、`guardianWarning`、`serverRequest/resolved`、`item/fileChange/patchUpdated` 映射为审批、安全提醒和文件变更摘要,并通过 `POST /api/v1/master-agent/tasks/[taskId]/progress` 实时刷新;complete 回写仍会携带最终进度兜底 - 当前 `MasterAgentTask` 已具备服务端租约和取消基础状态机:claim 会写入 `attemptCount / maxAttempts / leaseExpiresAt`,运行中任务租约过期后可被重新认领,超过重试上限会转 `timed_out`;`POST /api/v1/master-agent/tasks/[taskId]/cancel` 会把任务转 `canceled`,迟到的成功 complete 不会覆盖终态 - 当前 `local-agent` 对 `browser_control / desktop_control` 已从占位骨架升级成外部 runtime 桥:当本机配置了 `browserControlEnabled + browserControlCommand` 或 `computerUseEnabled + computerUseCommand` 时,会把标准化 JSON 请求透传给外部进程,并解析单行 JSON 结果;未启用时会 fail closed,返回明确的 runtime disabled 错误,不再假装执行成功 - 远程电脑控制链路当前已有可复用压测基线:`npm run stress:remote-control` 可按参数压测 `local-agent -> MasterAgentTask -> browser_control / desktop_control runtime -> complete 回写` 全链路;`npm run stress:remote-control:ci` 固定 120 条链路任务和 360 条 runtime 并发任务,并用 p95 延迟预算判断是否退化。压测报告可通过 `--report-json=PATH` 落盘,便于后续接入真实 macOS AX / Windows UIA helper 后复用同一套稳定性判断。 diff --git a/local-agent/codex-app-server-runner.mjs b/local-agent/codex-app-server-runner.mjs index 896e26a..15eff26 100644 --- a/local-agent/codex-app-server-runner.mjs +++ b/local-agent/codex-app-server-runner.mjs @@ -601,10 +601,139 @@ function extractAgentFromThreadStarted(params) { }; } +function safeProgressText(value, maxLength = 180) { + return String(value ?? "") + .replace(/sk-[A-Za-z0-9_-]{8,}/g, "[redacted]") + .replace(/(api[_-]?key|token|secret|password)\s*[=:]\s*[^ \n\r\t]+/gi, "$1=[redacted]") + .replace(/\s+/g, " ") + .trim() + .slice(0, maxLength); +} + +function normalizeApprovalKindFromMethod(method) { + if (/commandExecution|execCommand/i.test(method)) { + return "command"; + } + if (/fileChange|applyPatch/i.test(method)) { + return "file_change"; + } + if (/permissions/i.test(method)) { + return "permissions"; + } + if (/autoApprovalReview/i.test(method)) { + return "auto_review"; + } + return "approval"; +} + +function labelForApprovalKind(kind) { + if (kind === "command") return "命令执行审批"; + if (kind === "file_change") return "文件变更审批"; + if (kind === "permissions") return "权限申请审批"; + if (kind === "auto_review") return "自动审批复核"; + return "审批请求"; +} + +function normalizeApprovalStatus(value) { + const normalized = String(value ?? "").trim().toLowerCase(); + if (normalized === "approved" || normalized === "accept" || normalized === "accepted") return "approved"; + if (normalized === "denied" || normalized === "declined" || normalized === "rejected") return "declined"; + if (normalized === "cancel" || normalized === "cancelled" || normalized === "canceled") return "cancelled"; + if (normalized === "resolved" || normalized === "complete" || normalized === "completed") return "resolved"; + if (normalized === "running" || normalized === "in_progress" || normalized === "reviewing") return "reviewing"; + return "pending"; +} + +function extractApprovalDetail(params, kind) { + const reason = safeProgressText(params?.reason, 120); + if (reason) { + return reason; + } + if (kind === "command") return "需要确认命令执行"; + if (kind === "file_change") { + const grantRoot = basename(params?.grantRoot); + return grantRoot ? `需要确认文件写入:${grantRoot}` : "需要确认文件变更"; + } + if (kind === "permissions") return "需要确认权限申请"; + return undefined; +} + +function extractApprovalFromServerRequest(message) { + const method = String(message?.method ?? ""); + const kind = normalizeApprovalKindFromMethod(method); + const id = safeProgressText(message?.id ?? message?.params?.approvalId ?? message?.params?.itemId, 80); + if (!id) { + return null; + } + const detail = extractApprovalDetail(message.params, kind); + return { + id, + kind, + label: labelForApprovalKind(kind), + status: "pending", + ...(detail ? { detail } : {}), + }; +} + +function extractApprovalFromAutoReview(message) { + const id = safeProgressText(message?.params?.reviewId, 80); + if (!id) { + return null; + } + const completed = String(message?.method ?? "").endsWith("/completed"); + const review = message?.params?.review && typeof message.params.review === "object" ? message.params.review : {}; + const status = completed ? normalizeApprovalStatus(review.status ?? "resolved") : "reviewing"; + const riskLevel = safeProgressText(review.riskLevel, 40); + return { + id, + kind: "auto_review", + label: labelForApprovalKind("auto_review"), + status, + ...(riskLevel ? { riskLevel } : {}), + }; +} + +function extractFileChangeEntries(params) { + return asArray(params?.changes) + .map((change, index) => { + const path = safeProgressText(change?.path, 240); + if (!path) { + return null; + } + const kind = safeProgressText(change?.kind, 40) || "update"; + return { + id: `${safeProgressText(params?.itemId, 80) || "file-change"}-${index + 1}`, + path, + kind, + status: "updated", + }; + }) + .filter(Boolean); +} + +function buildServerRequestFallbackResponse(message) { + const method = String(message?.method ?? ""); + if (/commandExecution\/requestApproval|execCommandApproval/i.test(method)) { + return { result: { decision: "cancel" } }; + } + if (/fileChange\/requestApproval|applyPatchApproval/i.test(method)) { + return { result: { decision: "cancel" } }; + } + return { + error: { + code: -32000, + message: "BOSS_APPROVAL_REQUIRES_USER_ACTION", + }, + }; +} + function createProgressCollector() { const steps = []; const artifacts = []; const agents = []; + const approvals = []; + const fileChanges = []; + const warnings = []; const branch = {}; const upsertArtifact = (artifact) => { @@ -621,6 +750,56 @@ function createProgressCollector() { agents.push(agent); }; + const upsertApproval = (approval) => { + if (!approval) { + return; + } + const existing = approvals.find((item) => item.id === approval.id); + if (existing) { + Object.assign(existing, approval); + if (approval.detail === undefined && existing.detail === undefined) { + delete existing.detail; + } + if (approval.riskLevel === undefined && existing.riskLevel === undefined) { + delete existing.riskLevel; + } + return; + } + approvals.push(approval); + }; + + const markApprovalResolved = (requestId) => { + const normalizedId = safeProgressText(requestId, 80); + const existing = approvals.find((item) => item.id === normalizedId); + if (existing) { + existing.status = "resolved"; + } + }; + + const upsertFileChange = (fileChange) => { + if (!fileChange || fileChanges.some((item) => item.path === fileChange.path && item.kind === fileChange.kind)) { + return; + } + fileChanges.push(fileChange); + upsertArtifact({ + id: fileChange.id, + label: basename(fileChange.path), + kind: "file", + path: fileChange.path, + }); + }; + + const pushWarning = (warning) => { + if (!warning?.message || warnings.some((item) => item.message === warning.message)) { + return; + } + warnings.push({ + id: warning.id ?? `guardian-warning-${warnings.length + 1}`, + severity: warning.severity ?? "warning", + message: warning.message, + }); + }; + return { observe(message) { if (!message || typeof message !== "object") { @@ -659,6 +838,44 @@ function createProgressCollector() { } return; } + if ( + message.method === "item/commandExecution/requestApproval" || + message.method === "item/fileChange/requestApproval" || + message.method === "item/permissions/requestApproval" || + message.method === "applyPatchApproval" || + message.method === "execCommandApproval" + ) { + upsertApproval(extractApprovalFromServerRequest(message)); + return; + } + if ( + message.method === "item/autoApprovalReview/started" || + message.method === "item/autoApprovalReview/completed" + ) { + upsertApproval(extractApprovalFromAutoReview(message)); + return; + } + if (message.method === "serverRequest/resolved") { + markApprovalResolved(message.params?.requestId); + return; + } + if (message.method === "guardianWarning") { + const warningMessage = safeProgressText(message.params?.message, 180); + if (warningMessage) { + pushWarning({ message: warningMessage }); + } + return; + } + if (message.method === "item/fileChange/patchUpdated") { + const entries = extractFileChangeEntries(message.params); + for (const entry of entries) { + upsertFileChange(entry); + } + if (entries.length > 0) { + branch.changedFiles = Math.max(branch.changedFiles ?? 0, fileChanges.length); + } + return; + } if (message.method === "thread/started") { upsertAgent(extractAgentFromThreadStarted(message.params)); } @@ -677,6 +894,15 @@ function createProgressCollector() { if (agents.length > 0) { result.agents = agents.slice(0, 8); } + if (approvals.length > 0) { + result.approvals = approvals.slice(0, 8); + } + if (warnings.length > 0) { + result.warnings = warnings.slice(0, 6); + } + if (fileChanges.length > 0) { + result.fileChanges = fileChanges.slice(0, 12); + } return Object.keys(result).length > 0 ? result : undefined; }, }; @@ -861,6 +1087,11 @@ export async function executeCodexAppServerTask(runnerConfig, task) { rpcTransport.send(JSON.stringify({ method, params })); }; + const respond = (id, payload) => { + if (closed) return; + rpcTransport.send(JSON.stringify({ id, ...payload })); + }; + const emitProgress = () => { if (typeof runnerConfig.onProgress !== "function") { return; @@ -935,6 +1166,11 @@ export async function executeCodexAppServerTask(runnerConfig, task) { pendingRequest.resolve(message.result ?? {}); } } + if (!pendingRequest && message.method) { + progressCollector.observe(message); + emitProgress(); + respond(message.id, buildServerRequestFallbackResponse(message)); + } return; } diff --git a/src/lib/boss-data.ts b/src/lib/boss-data.ts index 6df09db..dd18310 100644 --- a/src/lib/boss-data.ts +++ b/src/lib/boss-data.ts @@ -116,6 +116,28 @@ export interface ExecutionProgressAgent { status?: string; } +export interface ExecutionProgressApproval { + id?: string; + kind?: string; + label: string; + status?: string; + riskLevel?: string; + detail?: string; +} + +export interface ExecutionProgressWarning { + id?: string; + message: string; + severity?: string; +} + +export interface ExecutionProgressFileChange { + id?: string; + path: string; + kind?: string; + status?: string; +} + export interface ExecutionProgressSnapshot { taskId: string; projectId: string; @@ -131,6 +153,9 @@ export interface ExecutionProgressSnapshot { branch?: ExecutionProgressBranchDetails; artifacts?: ExecutionProgressArtifact[]; agents?: ExecutionProgressAgent[]; + approvals?: ExecutionProgressApproval[]; + warnings?: ExecutionProgressWarning[]; + fileChanges?: ExecutionProgressFileChange[]; updatedAt: string; } @@ -139,6 +164,9 @@ export interface ExecutionProgressInput { branch?: Partial; artifacts?: Array & { label?: string }>; agents?: Array & { name?: string }>; + approvals?: Array & { label?: string }>; + warnings?: Array & { message?: string }>; + fileChanges?: Array & { path?: string }>; } export interface ForwardSource { @@ -3805,6 +3833,9 @@ function normalizeExecutionProgressSnapshot(raw: Partial { + const label = safeExecutionProgressText(approval?.label); + if (!label) { + return null; + } + return { + id: safeExecutionProgressText(approval?.id) || `approval-${index + 1}`, + kind: safeExecutionProgressText(approval?.kind) || undefined, + label, + status: safeExecutionProgressText(approval?.status) || undefined, + riskLevel: safeExecutionProgressText(approval?.riskLevel) || undefined, + detail: safeExecutionProgressText(approval?.detail) || undefined, + }; + }) + .filter((approval): approval is ExecutionProgressApproval => Boolean(approval)) + .slice(0, 8); +} + +function normalizeExecutionProgressWarnings(input?: ExecutionProgressInput["warnings"]) { + return (input ?? []) + .map((warning, index): ExecutionProgressWarning | null => { + const message = safeExecutionProgressText(warning?.message); + if (!message) { + return null; + } + return { + id: safeExecutionProgressText(warning?.id) || `warning-${index + 1}`, + message, + severity: safeExecutionProgressText(warning?.severity) || "warning", + }; + }) + .filter((warning): warning is ExecutionProgressWarning => Boolean(warning)) + .slice(0, 6); +} + +function normalizeExecutionProgressFileChanges(input?: ExecutionProgressInput["fileChanges"]) { + return (input ?? []) + .map((fileChange, index): ExecutionProgressFileChange | null => { + const path = safeExecutionProgressText(fileChange?.path); + if (!path) { + return null; + } + return { + id: safeExecutionProgressText(fileChange?.id) || `file-change-${index + 1}`, + path, + kind: safeExecutionProgressText(fileChange?.kind) || undefined, + status: safeExecutionProgressText(fileChange?.status) || undefined, + }; + }) + .filter((fileChange): fileChange is ExecutionProgressFileChange => Boolean(fileChange)) + .slice(0, 12); +} + function defaultExecutionProgressStepTexts(task: Pick) { if (task.taskType === "browser_control") { return [ @@ -5408,6 +5494,9 @@ function buildExecutionProgressSnapshot( branch: nativeRemoteControl ? undefined : normalizeExecutionProgressBranch(input?.branch), artifacts: normalizeExecutionProgressArtifacts(input?.artifacts), agents: nativeRemoteControl ? undefined : normalizeExecutionProgressAgents(input?.agents), + approvals: nativeRemoteControl ? undefined : normalizeExecutionProgressApprovals(input?.approvals), + warnings: nativeRemoteControl ? undefined : normalizeExecutionProgressWarnings(input?.warnings), + fileChanges: nativeRemoteControl ? undefined : normalizeExecutionProgressFileChanges(input?.fileChanges), updatedAt: nowIso(), }; } diff --git a/tests/fixtures/codex-app-server-runtime.mjs b/tests/fixtures/codex-app-server-runtime.mjs index 9cfbbb4..17a9140 100644 --- a/tests/fixtures/codex-app-server-runtime.mjs +++ b/tests/fixtures/codex-app-server-runtime.mjs @@ -176,6 +176,99 @@ rl.on("line", (line) => { }, }); } + if (process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_GUARDIAN_EVENTS === "1") { + send({ + id: "cmd-approval-1", + method: "item/commandExecution/requestApproval", + params: { + threadId: message.params?.threadId, + turnId: "turn-fixture", + itemId: "command-item-1", + startedAtMs: Date.now(), + reason: "需要确认命令执行", + command: "npm run build -- --token sk-secret-should-not-leak", + cwd: "/Users/kris/code/boss", + }, + }); + send({ + method: "item/autoApprovalReview/started", + params: { + threadId: message.params?.threadId, + turnId: "turn-fixture", + reviewId: "review-1", + startedAtMs: Date.now(), + targetItemId: "command-item-1", + action: { + type: "command", + command: "npm run build -- --token sk-secret-should-not-leak", + cwd: "/Users/kris/code/boss", + }, + review: { + status: "running", + riskLevel: "medium", + userAuthorization: null, + rationale: "contains a fake secret that must not be surfaced", + }, + }, + }); + send({ + method: "guardianWarning", + params: { + threadId: message.params?.threadId, + message: "检测到需要用户确认的命令执行。", + }, + }); + send({ + method: "item/fileChange/patchUpdated", + params: { + threadId: message.params?.threadId, + turnId: "turn-fixture", + itemId: "file-change-item-1", + changes: [ + { + path: "src/app/page.tsx", + kind: "update", + diff: "+ const secret = 'sk-secret-should-not-leak'", + }, + { + path: "docs/architecture/codex_server_progress_card_cn.md", + kind: "add", + diff: "+ internal prompt should not leak", + }, + ], + }, + }); + send({ + method: "item/autoApprovalReview/completed", + params: { + threadId: message.params?.threadId, + turnId: "turn-fixture", + reviewId: "review-1", + startedAtMs: Date.now(), + completedAtMs: Date.now(), + targetItemId: "command-item-1", + decisionSource: "auto_review", + action: { + type: "command", + command: "npm run build -- --token sk-secret-should-not-leak", + cwd: "/Users/kris/code/boss", + }, + review: { + status: "approved", + riskLevel: "medium", + userAuthorization: null, + rationale: "contains a fake secret that must not be surfaced", + }, + }, + }); + send({ + method: "serverRequest/resolved", + params: { + requestId: "cmd-approval-1", + threadId: message.params?.threadId, + }, + }); + } send({ method: "item/agentMessage/delta", params: { diff --git a/tests/local-agent-codex-app-server-runner.test.mjs b/tests/local-agent-codex-app-server-runner.test.mjs index b45d11f..996ef3e 100644 --- a/tests/local-agent-codex-app-server-runner.test.mjs +++ b/tests/local-agent-codex-app-server-runner.test.mjs @@ -287,6 +287,78 @@ test("codex app-server runner converts protocol progress events into Boss execut } }); +test("codex app-server runner maps guardian approval and file-change events without leaking secrets", async () => { + const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_GUARDIAN_EVENTS; + process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_GUARDIAN_EVENTS = "1"; + try { + const runnerConfig = getCodexAppServerRunnerConfig(process.env, { + codexAppServerEnabled: true, + codexAppServerCommand: process.execPath, + codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"], + codexAppServerWorkdir: repoRoot, + codexAppServerTimeoutMs: 5000, + masterAgentModel: "gpt-5.4", + }); + + const result = await executeCodexAppServerTask(runnerConfig, { + taskId: "task-app-server-guardian", + taskType: "dispatch_execution", + targetCodexThreadRef: "019d-app-server-thread", + targetCodexFolderRef: repoRoot, + executionPrompt: "需要审批和文件变更摘要", + }); + + assert.equal(result.status, "completed"); + assert.equal(result.replyBody, "APP_SERVER_REPLY:需要审批和文件变更摘要"); + assert.deepEqual(result.executionProgress.approvals, [ + { + id: "cmd-approval-1", + kind: "command", + label: "命令执行审批", + status: "resolved", + detail: "需要确认命令执行", + }, + { + id: "review-1", + kind: "auto_review", + label: "自动审批复核", + status: "approved", + riskLevel: "medium", + }, + ]); + assert.deepEqual(result.executionProgress.warnings, [ + { + id: "guardian-warning-1", + message: "检测到需要用户确认的命令执行。", + severity: "warning", + }, + ]); + assert.deepEqual(result.executionProgress.fileChanges, [ + { + id: "file-change-item-1-1", + path: "src/app/page.tsx", + kind: "update", + status: "updated", + }, + { + id: "file-change-item-1-2", + path: "docs/architecture/codex_server_progress_card_cn.md", + kind: "add", + status: "updated", + }, + ]); + assert.equal(result.executionProgress.branch.changedFiles, 2); + assert.equal(JSON.stringify(result.executionProgress).includes("sk-secret-should-not-leak"), false); + assert.equal(JSON.stringify(result.executionProgress).includes("internal prompt"), false); + } finally { + if (previous === undefined) { + delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_GUARDIAN_EVENTS; + } else { + process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_GUARDIAN_EVENTS = previous; + } + } +}); + test("codex app-server runner bridges source thread context into target thread through inject_items", async () => { const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_INTER_THREAD; process.env.BOSS_CODEX_APP_SERVER_FIXTURE_INTER_THREAD = "1"; diff --git a/tests/master-agent-task-progress-route.test.ts b/tests/master-agent-task-progress-route.test.ts index 6d202d8..776071a 100644 --- a/tests/master-agent-task-progress-route.test.ts +++ b/tests/master-agent-task-progress-route.test.ts @@ -84,3 +84,73 @@ test("POST task progress accepts device-token updates and keeps task running", a assert.equal(progressMessage?.executionProgress?.steps[0]?.text, "连接 Codex App Server"); assert.equal(progressMessage?.executionProgress?.branch?.additions, 12); }); + +test("POST task progress preserves Codex approval, warning, and file-change summaries", async () => { + const task = await data.queueMasterAgentTask({ + taskId: "route-progress-approval-task", + projectId: "group-progress-test", + taskType: "dispatch_execution", + requestMessageId: "msg-route-progress-approval", + requestText: "让目标线程继续开发并回写审批状态", + executionPrompt: "让目标线程继续开发并回写审批状态", + requestedBy: "krisolo", + requestedByAccount: "krisolo", + deviceId: "mac-studio", + targetProjectId: "master-agent", + targetThreadId: "master-agent-thread", + }); + await data.claimNextMasterAgentTask("mac-studio"); + + const response = await postProgress( + new NextRequest(`http://127.0.0.1:3000/api/v1/master-agent/tasks/${task.taskId}/progress`, { + method: "POST", + headers: { + "content-type": "application/json", + "x-boss-device-token": "boss-mac-studio-token", + }, + body: JSON.stringify({ + deviceId: "mac-studio", + status: "running", + executionProgress: { + steps: [{ text: "等待 Codex 审批事件", status: "running" }], + approvals: [ + { + id: "cmd-approval-1", + kind: "command", + label: "命令执行审批", + status: "resolved", + detail: "需要确认命令执行", + }, + ], + warnings: [ + { + id: "guardian-warning-1", + message: "检测到需要用户确认的命令执行。", + severity: "warning", + }, + ], + fileChanges: [ + { + id: "file-change-1", + path: "src/app/page.tsx", + kind: "update", + status: "updated", + }, + ], + }, + }), + }), + { params: Promise.resolve({ taskId: task.taskId }) }, + ); + + assert.equal(response.status, 200); + + const state = await data.readState(); + const progress = state.projects + .find((project) => project.id === "master-agent") + ?.messages.find((message) => message.executionProgress?.taskId === task.taskId) + ?.executionProgress; + assert.equal(progress?.approvals?.[0]?.label, "命令执行审批"); + assert.equal(progress?.warnings?.[0]?.message, "检测到需要用户确认的命令执行。"); + assert.equal(progress?.fileChanges?.[0]?.path, "src/app/page.tsx"); +});