From 2ca27375209a1df270d0067376d73f7107c7af49 Mon Sep 17 00:00:00 2001 From: AI Bot Date: Mon, 1 Jun 2026 18:04:39 +0800 Subject: [PATCH] feat: surface codex tool activity progress --- .../src/main/java/com/hyzq/boss/BossUi.java | 41 ++++++ .../boss/ProjectDetailActivityUiTest.java | 59 ++++++++ docs/architecture/ai_handoff_index_cn.md | 2 +- .../api_and_service_inventory_cn.md | 4 +- .../codex_server_progress_card_cn.md | 6 +- .../current_runtime_and_deploy_status_cn.md | 2 +- local-agent/codex-app-server-runner.mjs | 103 ++++++++++++++ src/lib/boss-data.ts | 49 ++++++- tests/fixtures/codex-app-server-runtime.mjs | 129 ++++++++++++++++++ ...cal-agent-codex-app-server-runner.test.mjs | 76 +++++++++++ .../master-agent-task-progress-route.test.ts | 94 +++++++++++++ 11 files changed, 558 insertions(+), 7 deletions(-) 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 36f85f4..1dfda26 100644 --- a/android/app/src/main/java/com/hyzq/boss/BossUi.java +++ b/android/app/src/main/java/com/hyzq/boss/BossUi.java @@ -1598,6 +1598,47 @@ public final class BossUi { } } + JSONArray toolActivities = progress == null ? null : progress.optJSONArray("toolActivities"); + if (toolActivities != null && toolActivities.length() > 0) { + card.addView(divider(context)); + card.addView(sectionTitle(context, "工具活动")); + int shown = 0; + for (int i = 0; i < toolActivities.length(); i += 1) { + JSONObject activity = toolActivities.optJSONObject(i); + String kind = activity == null ? "" : activity.optString("kind", "").trim(); + String name = activity == null ? "" : activity.optString("name", "").trim(); + if (TextUtils.isEmpty(name)) { + continue; + } + String status = activity.optString("status", "").trim(); + String detail = activity.optString("detail", "").trim(); + String label; + if ("mcp".equals(kind)) { + label = "MCP " + name; + } else if ("dynamic".equals(kind)) { + label = "动态工具 " + name; + } else if ("web_search".equals(kind)) { + label = "搜索 " + name; + } else if ("image_view".equals(kind)) { + label = "图片 " + name; + } else if ("review".equals(kind)) { + label = "Review " + name; + } else if ("command".equals(kind)) { + label = "命令 " + name; + } else { + label = "工具 " + name; + } + if (!TextUtils.isEmpty(status)) { + label += " · " + status; + } + card.addView(detailRow(context, "◇", label, detail, "failed".equals(status))); + shown += 1; + if (shown >= 6) { + break; + } + } + } + JSONArray warnings = progress == null ? null : progress.optJSONArray("warnings"); if (warnings != null && warnings.length() > 0) { card.addView(divider(context)); 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 d9552b4..8d8671b 100644 --- a/android/app/src/test/java/com/hyzq/boss/ProjectDetailActivityUiTest.java +++ b/android/app/src/test/java/com/hyzq/boss/ProjectDetailActivityUiTest.java @@ -1226,6 +1226,65 @@ public class ProjectDetailActivityUiTest { assertFalse(viewTreeContainsText(messageView, "sk-secret-should-not-render")); } + @Test + public void executionProgressMessageRendersCodexToolActivitySection() throws Exception { + Intent intent = new Intent() + .putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "tool-activity") + .putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "Boss开发主线程"); + TestProjectDetailActivity activity = Robolectric + .buildActivity(TestProjectDetailActivity.class, intent) + .setup() + .get(); + + JSONObject message = new JSONObject() + .put("id", "progress-tool-activity-1") + .put("sender", "master") + .put("senderLabel", "主 Agent") + .put("body", "执行进度") + .put("kind", "execution_progress") + .put("sentAt", "2026-06-01T11:40:00+08:00") + .put("executionProgress", new JSONObject() + .put("status", "running") + .put("steps", new JSONArray() + .put(new JSONObject().put("text", "同步 Codex 工具活动").put("status", "running"))) + .put("toolActivities", new JSONArray() + .put(new JSONObject() + .put("kind", "mcp") + .put("name", "github/pull_request/list") + .put("status", "failed") + .put("detail", "token=[redacted] failed") + .put("arguments", new JSONObject().put("token", "sk-secret-should-not-render"))) + .put(new JSONObject() + .put("kind", "web_search") + .put("name", "openPage") + .put("status", "running") + .put("detail", "Codex App Server ThreadItem") + .put("url", "https://example.com/private?token=sk-secret-should-not-render")) + .put(new JSONObject() + .put("kind", "command") + .put("name", "commandExecution") + .put("status", "completed") + .put("detail", "exit 0 · 2345ms") + .put("command", "cat /Users/kris/.ssh/id_rsa")))); + + View messageView = ReflectionHelpers.callInstanceMethod( + activity, + "buildMessageView", + ReflectionHelpers.ClassParameter.from(JSONObject.class, message) + ); + + assertTrue(viewTreeContainsText(messageView, "工具活动")); + assertTrue(viewTreeContainsText(messageView, "MCP github/pull_request/list · failed")); + assertTrue(viewTreeContainsText(messageView, "搜索 openPage · running")); + assertTrue(viewTreeContainsText(messageView, "Codex App Server ThreadItem")); + assertTrue(viewTreeContainsText(messageView, "命令 commandExecution · completed")); + assertTrue(viewTreeContainsText(messageView, "exit 0 · 2345ms")); + assertFalse(viewTreeContainsText(messageView, "sk-secret-should-not-render")); + assertFalse(viewTreeContainsText(messageView, "https://example.com/private")); + assertFalse(viewTreeContainsText(messageView, "/Users/kris")); + assertFalse(viewTreeContainsText(messageView, "id_rsa")); + } + @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 baf2332..d659fd4 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`、`item/*/requestApproval`、`item/autoApprovalReview/*`、`guardianWarning`、`serverRequest/resolved`、`item/fileChange/patchUpdated`、`thread/status/changed`、`thread/realtime/*`、`model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated`、`remoteControl/status/changed`、`thread/goal/*`、`thread/settings/updated`、`thread/compacted`、`account/updated`、`account/rateLimits/updated`、`model/verification`、`warning`、`configWarning`、`deprecationNotice`、`ThreadItem.collabToolCall`、`ThreadItem.contextCompaction` 归一到 Boss `execution_progress` 卡片;realtime 只保留状态、文本摘要和计数,运行状态只保留模型切换、上下文用量、MCP 状态和远控连接摘要,线程配置只保留目标、模型、审批、沙箱、协作模式和压缩状态,线程协作只保留工具名、状态、目标类型和智能体状态,账号状态只保留认证方式、套餐、额度窗口、积分余额和模型校验摘要,不保存 SDP、音频原始数据、raw item、remote installationId、cwd、turnId、配置文件路径、collab 源/目标线程 ID、collab prompt 或未清洗密钥。heartbeat 已能缓存 `model/list / skills/list / plugin/list / app/list / modelProvider/capabilities/read` 的能力摘要;同批已补 `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`、`thread/status/changed`、`thread/realtime/*`、`model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated`、`remoteControl/status/changed`、`thread/goal/*`、`thread/settings/updated`、`thread/compacted`、`account/updated`、`account/rateLimits/updated`、`model/verification`、`warning`、`configWarning`、`deprecationNotice`、`ThreadItem.collabToolCall`、`ThreadItem.contextCompaction`、`mcpToolCall`、`dynamicToolCall`、`webSearch`、`imageView`、`enteredReviewMode`、`exitedReviewMode`、`commandExecution` 归一到 Boss `execution_progress` 卡片;realtime 只保留状态、文本摘要和计数,运行状态只保留模型切换、上下文用量、MCP 状态和远控连接摘要,线程配置只保留目标、模型、审批、沙箱、协作模式和压缩状态,线程协作只保留工具名、状态、目标类型和智能体状态,工具活动只保留类型、名称、状态和安全摘要,账号状态只保留认证方式、套餐、额度窗口、积分余额和模型校验摘要,不保存 SDP、音频原始数据、raw item、remote installationId、cwd、turnId、配置文件路径、collab 源/目标线程 ID、collab prompt、tool arguments/result/contentItems、web URL token、命令正文/输出或未清洗密钥。heartbeat 已能缓存 `model/list / skills/list / plugin/list / app/list / modelProvider/capabilities/read` 的能力摘要;同批已补 `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 65bd385..947d3a1 100644 --- a/docs/architecture/api_and_service_inventory_cn.md +++ b/docs/architecture/api_and_service_inventory_cn.md @@ -73,7 +73,7 @@ - 当前已支持聊天附件主链:输入框左侧 `+` 会打开底部抽屉,支持图片 / 视频 / 文件发送;图片 / 视频先确认,文件直接发送 - 当前附件消息支持下载、原生打开、手动分析和自动分析状态展示 - 当前线程聊天消息会按该线程绑定的 Codex 电脑显示来源头像:单线程会话使用项目绑定设备头像,多设备 / 群聊消息会优先根据发送人里的设备名匹配对应电脑头像;主 Agent 总入口自身仍保留主 Agent 对话样式 - - 当前已支持 `execution_progress` 执行进度卡:普通线程对话、主 Agent 托管线程和群聊目标线程执行时,会在对应聊天窗口显示“进度 / 线程状态 / 实时状态 / 线程配置 / 线程协作 / 账号状态 / 运行状态 / 安全提醒 / 审批状态 / 文件变更 / 分支详情 / 生成结果 / 后台智能体”结构化卡片;线程过程噪音仍走 `thread_process` 折叠 + - 当前已支持 `execution_progress` 执行进度卡:普通线程对话、主 Agent 托管线程和群聊目标线程执行时,会在对应聊天窗口显示“进度 / 线程状态 / 实时状态 / 线程配置 / 线程协作 / 工具活动 / 账号状态 / 运行状态 / 安全提醒 / 审批状态 / 文件变更 / 分支详情 / 生成结果 / 后台智能体”结构化卡片;线程过程噪音仍走 `thread_process` 折叠 - `线程详情 / 运维调试` 仍保留对应原生活动页,但已退出主聊天面 - 当前已补上本地发送中气泡、发送按钮状态控制,以及“只有接近底部才自动滚到底”的消息流行为 - 当前根页导航: @@ -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`,把 `item/*/requestApproval`、`item/autoApprovalReview/*`、`guardianWarning`、`serverRequest/resolved`、`item/fileChange/patchUpdated` 归一成 `executionProgress.approvals / warnings / fileChanges`,把 `thread/status/changed`、`thread/realtime/started|transcript|outputAudio|itemAdded|error|closed` 归一成 `executionProgress.threadStatus / realtime`,把 `model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated`、`remoteControl/status/changed` 归一成 `executionProgress.modelRoute / tokenUsage / mcpServers / remoteControl`,并把 `thread/goal/*`、`thread/settings/updated`、`thread/compacted`、`account/updated`、`account/rateLimits/updated`、`model/verification`、`warning`、`configWarning`、`deprecationNotice`、`ThreadItem.collabToolCall`、`ThreadItem.contextCompaction` 归一成线程配置、账号状态、模型校验、安全提醒、线程协作和上下文压缩摘要;服务端 complete 回写会与本地 Git/GitHub 进度合并,且不保存 SDP、音频 base64、raw realtime item、remote installationId、cwd、turnId、配置路径、collab 源/目标线程 ID、collab prompt 或未清洗的 MCP 错误。heartbeat 同时支持按 TTL 拉取 `model/list / skills/list / plugin/list / app/list / modelProvider/capabilities/read`,并把摘要保存在 `capabilities.codexAppServer.metadata`。 +- 当前新增 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`,把 `thread/status/changed`、`thread/realtime/started|transcript|outputAudio|itemAdded|error|closed` 归一成 `executionProgress.threadStatus / realtime`,把 `model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated`、`remoteControl/status/changed` 归一成 `executionProgress.modelRoute / tokenUsage / mcpServers / remoteControl`,并把 `thread/goal/*`、`thread/settings/updated`、`thread/compacted`、`account/updated`、`account/rateLimits/updated`、`model/verification`、`warning`、`configWarning`、`deprecationNotice`、`ThreadItem.collabToolCall`、`ThreadItem.contextCompaction`、`mcpToolCall`、`dynamicToolCall`、`webSearch`、`imageView`、`enteredReviewMode`、`exitedReviewMode`、`commandExecution` 归一成线程配置、账号状态、模型校验、安全提醒、线程协作、上下文压缩和工具活动摘要;服务端 complete 回写会与本地 Git/GitHub 进度合并,且不保存 SDP、音频 base64、raw realtime item、remote installationId、cwd、turnId、配置路径、collab 源/目标线程 ID、collab prompt、tool arguments/result/contentItems、web URL token、命令正文/输出或未清洗的 MCP 错误。heartbeat 同时支持按 TTL 拉取 `model/list / skills/list / plugin/list / app/list / modelProvider/capabilities/read`,并把摘要保存在 `capabilities.codexAppServer.metadata`。 - 当前 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 4416e19..89e0554 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` 的步骤、分支变更、产物和后台智能体。同日第二批补齐 `item/*/requestApproval`、`item/autoApprovalReview/*`、`guardianWarning`、`serverRequest/resolved` 和 `item/fileChange/patchUpdated` 的安全摘要映射,APP 只展示审批状态、风险提醒和文件路径,不展示完整命令、diff、系统提示词或密钥。第三批已把 `thread/status/changed` 与 `thread/realtime/*` 归一成 `executionProgress.threadStatus / realtime`,APP 只展示活跃/等待审批/等待用户输入、realtime 文本摘要、音频片段计数和关闭/错误原因;第四批已把 `model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated` 和 `remoteControl/status/changed` 归一成 `executionProgress.modelRoute / tokenUsage / mcpServers / remoteControl`,用于 APP “运行状态”区块。2026-06-01 第五批已把 `thread/goal/updated|cleared`、`thread/settings/updated` 和 `thread/compacted` 归一成 `executionProgress.threadGoal / threadSettings / compaction`,用于 APP “线程配置”区块;第六批已把 `account/updated`、`account/rateLimits/updated`、`model/verification`、`warning`、`configWarning`、`deprecationNotice` 归一成 `executionProgress.accountStatus / modelVerification / warnings`;第七批已把官方 `ThreadItem.collabToolCall` 归一成 `executionProgress.threadCollaboration`,并按官方建议把新版 `ThreadItem.contextCompaction` 映射回 `executionProgress.compaction`。`thread/realtime/sdp`、音频 base64、原始 realtime item、remote installationId、thread settings 的 `cwd`、compaction `turnId`、collaboration settings 内部 prompt、collabToolCall 源/目标线程 ID 和未清洗的 MCP 错误不入账。 +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、系统提示词或密钥。第三批已把 `thread/status/changed` 与 `thread/realtime/*` 归一成 `executionProgress.threadStatus / realtime`,APP 只展示活跃/等待审批/等待用户输入、realtime 文本摘要、音频片段计数和关闭/错误原因;第四批已把 `model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated` 和 `remoteControl/status/changed` 归一成 `executionProgress.modelRoute / tokenUsage / mcpServers / remoteControl`,用于 APP “运行状态”区块。2026-06-01 第五批已把 `thread/goal/updated|cleared`、`thread/settings/updated` 和 `thread/compacted` 归一成 `executionProgress.threadGoal / threadSettings / compaction`,用于 APP “线程配置”区块;第六批已把 `account/updated`、`account/rateLimits/updated`、`model/verification`、`warning`、`configWarning`、`deprecationNotice` 归一成 `executionProgress.accountStatus / modelVerification / warnings`;第七批已把官方 `ThreadItem.collabToolCall` 归一成 `executionProgress.threadCollaboration`,并按官方建议把新版 `ThreadItem.contextCompaction` 映射回 `executionProgress.compaction`;第八批已把 `mcpToolCall`、`dynamicToolCall`、`webSearch`、`imageView`、`enteredReviewMode`、`exitedReviewMode` 和 `commandExecution` 归一成 `executionProgress.toolActivities`。`thread/realtime/sdp`、音频 base64、原始 realtime item、remote installationId、thread settings 的 `cwd`、compaction `turnId`、collaboration settings 内部 prompt、collabToolCall 源/目标线程 ID、tool arguments/result/contentItems、web URL token、命令正文/输出和未清洗的 MCP 错误不入账。 官方文档入口:`https://developers.openai.com/codex/app-server` @@ -77,6 +77,7 @@ APP 展示结构对齐截图: - `实时状态`:展示 realtime 启动、同步、关闭或错误状态,附带安全清洗后的 transcript 预览和计数 - `线程配置`:展示 thread goal、模型 / provider、审批 / 沙箱、协作模式和上下文压缩状态 - `线程协作`:展示 `collabToolCall` 的工具名、执行状态、目标类型和智能体状态,不展示源/目标线程 ID 或 prompt +- `工具活动`:展示 MCP / dynamic tool / web search / image view / Review / command 的类型、名称、状态和安全摘要,不展示参数、结果、URL token、命令正文或命令输出 - `运行状态`:展示模型重路由、上下文用量、MCP 启动状态和远控连接状态 - `分支详情`:变更行、Git 操作、GitHub CLI 可用状态 - `生成结果`:从执行结果里提取文件、图片、APK、文档等产物名 @@ -85,7 +86,7 @@ APP 展示结构对齐截图: UI 参考: - image2 生成稿:`design/image2/boss-app-codex-app-server-progress-card-20260531.png` -- 当前生成稿保持微信效率型:顶部保留项目目标 / 版本记录固定入口,聊天区只展示最终用户消息和结构化进度卡,进度卡分为 `进度 / 线程状态 / 实时状态 / 线程配置 / 线程协作 / 账号状态 / 运行状态 / 安全提醒 / 审批状态 / 文件变更 / 分支详情 / 生成结果 / 后台智能体` +- 当前生成稿保持微信效率型:顶部保留项目目标 / 版本记录固定入口,聊天区只展示最终用户消息和结构化进度卡,进度卡分为 `进度 / 线程状态 / 实时状态 / 线程配置 / 线程协作 / 工具活动 / 账号状态 / 运行状态 / 安全提醒 / 审批状态 / 文件变更 / 分支详情 / 生成结果 / 后台智能体` - 后续 Android / Web 继续按该稿收口,不新增无关功能,不把协议字段、系统提示词或执行 envelope 暴露给用户 ## 3. 安全边界 @@ -130,6 +131,7 @@ UI 参考: - `local-agent/codex-app-server-runner.mjs` 已把 App Server `thread/goal/updated|cleared`、`thread/settings/updated`、`thread/compacted` 归一成 `executionProgress.threadGoal / threadSettings / compaction`;服务端进度路由和 Android 原生进度卡已支持展示,测试覆盖 cwd、turnId、内部 prompt 不外泄 - `local-agent/codex-app-server-runner.mjs` 已把 App Server `account/updated`、`account/rateLimits/updated`、`model/verification`、`warning`、`configWarning`、`deprecationNotice` 归一成 `executionProgress.accountStatus / modelVerification / warnings`;服务端进度路由和 Android 原生进度卡已支持展示,测试覆盖配置路径、turnId 和密钥不外泄 - `local-agent/codex-app-server-runner.mjs` 已把 App Server `ThreadItem.collabToolCall` 和 `ThreadItem.contextCompaction` 归一成 `executionProgress.threadCollaboration / compaction`;服务端进度路由和 Android 原生进度卡已支持展示,测试覆盖源/目标线程 ID、内部 prompt、turnId 和密钥不外泄 +- `local-agent/codex-app-server-runner.mjs` 已把 App Server `mcpToolCall`、`dynamicToolCall`、`webSearch`、`imageView`、`enteredReviewMode`、`exitedReviewMode`、`commandExecution` 归一成 `executionProgress.toolActivities`;服务端进度路由和 Android 原生进度卡已支持展示,测试覆盖 tool arguments/result、URL token、命令正文/输出、本地路径和密钥不外泄 - 新增实时进度入口 `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 4b2cec5..86db3a5 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` 会直接映射为进度步骤、变更统计、生成产物和后台智能体;第二批已把 `item/*/requestApproval`、`item/autoApprovalReview/*`、`guardianWarning`、`serverRequest/resolved`、`item/fileChange/patchUpdated` 映射为审批、安全提醒和文件变更摘要;第三批已把 `thread/status/changed` 与 `thread/realtime/*` 安全映射为线程状态和实时状态摘要;第四批已把 `model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated`、`remoteControl/status/changed` 安全映射为运行状态摘要;第五批已把 `thread/goal/*`、`thread/settings/updated` 和 `thread/compacted` 映射为线程配置摘要;第六批已把 `account/updated`、`account/rateLimits/updated`、`model/verification`、`warning`、`configWarning`、`deprecationNotice` 映射为账号状态、模型校验和安全提醒摘要;第七批已把 `ThreadItem.collabToolCall` 和 `ThreadItem.contextCompaction` 映射为线程协作和上下文压缩摘要。所有进度均通过 `POST /api/v1/master-agent/tasks/[taskId]/progress` 实时刷新;字段白名单会剥离 cwd、turnId、配置文件路径、内部 prompt、collab 源/目标线程 ID 和未清洗密钥,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` 映射为审批、安全提醒和文件变更摘要;第三批已把 `thread/status/changed` 与 `thread/realtime/*` 安全映射为线程状态和实时状态摘要;第四批已把 `model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated`、`remoteControl/status/changed` 安全映射为运行状态摘要;第五批已把 `thread/goal/*`、`thread/settings/updated` 和 `thread/compacted` 映射为线程配置摘要;第六批已把 `account/updated`、`account/rateLimits/updated`、`model/verification`、`warning`、`configWarning`、`deprecationNotice` 映射为账号状态、模型校验和安全提醒摘要;第七批已把 `ThreadItem.collabToolCall` 和 `ThreadItem.contextCompaction` 映射为线程协作和上下文压缩摘要;第八批已把 `mcpToolCall`、`dynamicToolCall`、`webSearch`、`imageView`、`enteredReviewMode`、`exitedReviewMode`、`commandExecution` 映射为工具活动摘要。所有进度均通过 `POST /api/v1/master-agent/tasks/[taskId]/progress` 实时刷新;字段白名单会剥离 cwd、turnId、配置文件路径、内部 prompt、collab 源/目标线程 ID、tool arguments/result、web URL token、命令正文/输出和未清洗密钥,complete 回写仍会携带最终进度兜底 - 当前 `local-agent` heartbeat 已新增 Codex App Server capability discovery:按 TTL 拉取模型、provider 能力、Skill、Plugin、App 摘要,写入 `capabilities.codexAppServer.metadata`;Web 设备详情会展示 App Server 连接状态、模型数量、默认/快速/深度模型和扩展数量 - 当前 `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 错误,不再假装执行成功 diff --git a/local-agent/codex-app-server-runner.mjs b/local-agent/codex-app-server-runner.mjs index 16ceae8..3ef2f31 100644 --- a/local-agent/codex-app-server-runner.mjs +++ b/local-agent/codex-app-server-runner.mjs @@ -616,6 +616,88 @@ function extractThreadCollaborationSnapshot(item) { return Object.values(snapshot).some((value) => value !== undefined && value !== "") ? snapshot : null; } +function extractToolActivitySnapshot(item, lifecycleStatus = "running") { + if (!item || typeof item !== "object") { + return null; + } + const type = safeProgressText(item.type, 80); + const status = safeProgressText(item.status, 40) || lifecycleStatus; + if (type === "mcpToolCall") { + const server = safeProgressText(item.server, 80); + const tool = safeProgressText(item.tool, 120); + return { + kind: "mcp", + name: server && tool ? `${server}/${tool}` : tool || server || "mcpToolCall", + status, + detail: safeProgressText(item.error, 180) || undefined, + }; + } + if (type === "dynamicToolCall") { + const detailParts = []; + if (typeof item.success === "boolean") { + detailParts.push(item.success ? "成功" : "失败"); + } + const durationMs = extractNumber(item.durationMs); + if (durationMs !== undefined) { + detailParts.push(`${durationMs}ms`); + } + return { + kind: "dynamic", + name: safeProgressText(item.tool, 120) || "dynamicToolCall", + status, + detail: detailParts.join(" · ") || undefined, + }; + } + if (type === "webSearch") { + const action = item.action && typeof item.action === "object" ? item.action : {}; + return { + kind: "web_search", + name: safeProgressText(action.type, 80) || "search", + status, + detail: + safeProgressText(item.query, 180) || + safeProgressText(action.query, 180) || + safeProgressText(asArray(action.queries)[0], 180) || + undefined, + }; + } + if (type === "imageView") { + return { + kind: "image_view", + name: "imageView", + status, + detail: safeProgressText(basename(item.path), 180) || undefined, + }; + } + if (type === "enteredReviewMode" || type === "exitedReviewMode") { + const review = item.review && typeof item.review === "object" ? item.review : {}; + return { + kind: "review", + name: "reviewMode", + status: type === "enteredReviewMode" ? "entered" : "exited", + detail: safeProgressText(review.status, 120) || undefined, + }; + } + if (type === "commandExecution") { + const detailParts = []; + const exitCode = extractNumber(item.exitCode); + const durationMs = extractNumber(item.durationMs); + if (exitCode !== undefined) { + detailParts.push(`exit ${exitCode}`); + } + if (durationMs !== undefined) { + detailParts.push(`${durationMs}ms`); + } + return { + kind: "command", + name: "commandExecution", + status, + detail: detailParts.join(" · ") || undefined, + }; + } + return null; +} + function extractSubAgentSource(value) { if (!value || typeof value !== "object") { return undefined; @@ -1166,6 +1248,7 @@ function createProgressCollector() { let threadSettings; let compaction; let threadCollaboration; + const toolActivities = []; let accountStatus; let modelVerification; @@ -1256,6 +1339,20 @@ function createProgressCollector() { mcpServers.push(server); }; + const upsertToolActivity = (activity) => { + if (!activity?.kind || !activity?.name) { + return; + } + const existing = toolActivities.find((item) => item.kind === activity.kind && item.name === activity.name); + if (existing) { + Object.assign(existing, activity); + return; + } + if (toolActivities.length < 8) { + toolActivities.push(activity); + } + }; + return { observe(message) { if (!message || typeof message !== "object") { @@ -1297,6 +1394,9 @@ function createProgressCollector() { if (nextCollaboration) { threadCollaboration = nextCollaboration; } + upsertToolActivity( + extractToolActivitySnapshot(item, message.method === "item/completed" ? "completed" : "running"), + ); if (item?.type === "contextCompaction") { compaction = { status: "completed", @@ -1600,6 +1700,9 @@ function createProgressCollector() { if (threadCollaboration) { result.threadCollaboration = { ...threadCollaboration }; } + if (toolActivities.length > 0) { + result.toolActivities = toolActivities.map((item) => ({ ...item })); + } if (accountStatus) { result.accountStatus = { ...accountStatus }; } diff --git a/src/lib/boss-data.ts b/src/lib/boss-data.ts index 9d8d42f..4f05013 100644 --- a/src/lib/boss-data.ts +++ b/src/lib/boss-data.ts @@ -239,6 +239,13 @@ export interface ExecutionProgressThreadCollaboration { agentStatus?: string; } +export interface ExecutionProgressToolActivity { + kind: string; + name: string; + status: string; + detail?: string; +} + export interface ExecutionProgressSnapshot { taskId: string; projectId: string; @@ -269,6 +276,7 @@ export interface ExecutionProgressSnapshot { threadSettings?: ExecutionProgressThreadSettings; compaction?: ExecutionProgressCompaction; threadCollaboration?: ExecutionProgressThreadCollaboration; + toolActivities?: ExecutionProgressToolActivity[]; updatedAt: string; } @@ -297,6 +305,17 @@ export interface ExecutionProgressInput { newThreadId?: unknown; prompt?: unknown; }; + toolActivities?: Array< + Partial & { + arguments?: unknown; + result?: unknown; + contentItems?: unknown; + url?: unknown; + path?: unknown; + command?: unknown; + cwd?: unknown; + } + >; } export interface ForwardSource { @@ -3984,6 +4003,7 @@ function normalizeExecutionProgressSnapshot(raw: Partial { + const kind = safeExecutionProgressText(item.kind); + const name = safeExecutionProgressText(item.name); + if (!kind || !name) { + return null; + } + return { + kind, + name, + status: safeExecutionProgressText(item.status) || "running", + detail: safeExecutionProgressText(item.detail) || undefined, + }; + }) + .filter((item): item is ExecutionProgressToolActivity => Boolean(item)) + .slice(0, 8); + return activities.length > 0 ? activities : undefined; +} + function defaultExecutionProgressStepTexts(task: Pick) { if (task.taskType === "browser_control") { return [ @@ -5908,6 +5954,7 @@ function buildExecutionProgressSnapshot( threadCollaboration: nativeRemoteControl ? undefined : normalizeExecutionProgressThreadCollaboration(input?.threadCollaboration), + toolActivities: nativeRemoteControl ? undefined : normalizeExecutionProgressToolActivities(input?.toolActivities), updatedAt: nowIso(), }; } diff --git a/tests/fixtures/codex-app-server-runtime.mjs b/tests/fixtures/codex-app-server-runtime.mjs index ac93934..dbd63f3 100644 --- a/tests/fixtures/codex-app-server-runtime.mjs +++ b/tests/fixtures/codex-app-server-runtime.mjs @@ -689,6 +689,135 @@ rl.on("line", (line) => { }, }); } + if (process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_TOOL_ACTIVITY_EVENTS === "1") { + send({ + method: "item/started", + params: { + threadId: message.params?.threadId, + turnId: "turn-fixture", + item: { + type: "mcpToolCall", + id: "mcp-tool-secret-should-not-leak", + server: "github", + tool: "pull_request/list", + status: "running", + arguments: { + token: "sk-secret-should-not-leak", + }, + }, + }, + }); + send({ + method: "item/completed", + params: { + threadId: message.params?.threadId, + turnId: "turn-fixture", + item: { + type: "mcpToolCall", + id: "mcp-tool-secret-should-not-leak", + server: "github", + tool: "pull_request/list", + status: "failed", + error: "token=sk-secret-should-not-leak failed", + }, + }, + }); + send({ + method: "item/completed", + params: { + threadId: message.params?.threadId, + turnId: "turn-fixture", + item: { + type: "dynamicToolCall", + id: "dynamic-tool-secret-should-not-leak", + tool: "browser.open", + status: "completed", + success: true, + durationMs: 1234, + contentItems: [ + { + text: "internal tool result token=sk-secret-should-not-leak", + }, + ], + }, + }, + }); + send({ + method: "item/started", + params: { + threadId: message.params?.threadId, + turnId: "turn-fixture", + item: { + type: "webSearch", + id: "web-search-secret-should-not-leak", + query: "Codex App Server ThreadItem", + action: { + type: "openPage", + url: "https://example.com/private?token=sk-secret-should-not-leak", + }, + }, + }, + }); + send({ + method: "item/completed", + params: { + threadId: message.params?.threadId, + turnId: "turn-fixture", + item: { + type: "imageView", + id: "image-view-secret-should-not-leak", + path: "/Users/kris/code/boss/private-screenshot.png", + }, + }, + }); + send({ + method: "item/started", + params: { + threadId: message.params?.threadId, + turnId: "turn-fixture", + item: { + type: "enteredReviewMode", + id: "review-mode-secret-should-not-leak", + review: { + status: "started", + prompt: "internal review prompt token=sk-secret-should-not-leak", + }, + }, + }, + }); + send({ + method: "item/completed", + params: { + threadId: message.params?.threadId, + turnId: "turn-fixture", + item: { + type: "exitedReviewMode", + id: "review-mode-secret-should-not-leak", + review: { + status: "completed", + prompt: "internal review prompt token=sk-secret-should-not-leak", + }, + }, + }, + }); + send({ + method: "item/completed", + params: { + threadId: message.params?.threadId, + turnId: "turn-fixture", + item: { + type: "commandExecution", + id: "command-secret-should-not-leak", + command: "cat /Users/kris/.ssh/id_rsa && echo sk-secret-should-not-leak", + cwd: "/Users/kris/code/boss", + status: "completed", + exitCode: 0, + aggregatedOutput: "private output token=sk-secret-should-not-leak", + durationMs: 2345, + }, + }, + }); + } 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 ac3599a..3f35d33 100644 --- a/tests/local-agent-codex-app-server-runner.test.mjs +++ b/tests/local-agent-codex-app-server-runner.test.mjs @@ -642,6 +642,82 @@ test("codex app-server runner maps collab tool calls and context compaction with } }); +test("codex app-server runner maps tool, search, image, review, and command activities without leaking payloads", async () => { + const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_TOOL_ACTIVITY_EVENTS; + process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_TOOL_ACTIVITY_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-tool-activity-events", + taskType: "conversation_reply", + targetCodexThreadRef: "019d-app-server-thread", + targetCodexFolderRef: repoRoot, + executionPrompt: "检查 Codex 工具活动", + }); + + assert.equal(result.status, "completed"); + assert.deepEqual(result.executionProgress.toolActivities, [ + { + kind: "mcp", + name: "github/pull_request/list", + status: "failed", + detail: "token=[redacted] failed", + }, + { + kind: "dynamic", + name: "browser.open", + status: "completed", + detail: "成功 · 1234ms", + }, + { + kind: "web_search", + name: "openPage", + status: "running", + detail: "Codex App Server ThreadItem", + }, + { + kind: "image_view", + name: "imageView", + status: "completed", + detail: "private-screenshot.png", + }, + { + kind: "review", + name: "reviewMode", + status: "exited", + detail: "completed", + }, + { + kind: "command", + name: "commandExecution", + status: "completed", + detail: "exit 0 · 2345ms", + }, + ]); + const serialized = JSON.stringify(result.executionProgress); + assert.equal(serialized.includes("sk-secret-should-not-leak"), false); + assert.equal(serialized.includes("/Users/kris"), false); + assert.equal(serialized.includes("internal tool result"), false); + assert.equal(serialized.includes("internal review prompt"), false); + assert.equal(serialized.includes("id_rsa"), false); + assert.equal(serialized.includes("https://example.com/private"), false); + } finally { + if (previous === undefined) { + delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_TOOL_ACTIVITY_EVENTS; + } else { + process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_TOOL_ACTIVITY_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 f917073..3c19308 100644 --- a/tests/master-agent-task-progress-route.test.ts +++ b/tests/master-agent-task-progress-route.test.ts @@ -517,3 +517,97 @@ test("POST task progress preserves Codex thread collaboration summaries without assert.equal(serialized.includes("sk-secret-should-not-persist"), false); assert.equal(serialized.includes("turn-secret-should-not-persist"), false); }); + +test("POST task progress preserves Codex tool activity summaries without leaking raw payloads", async () => { + const task = await data.queueMasterAgentTask({ + taskId: "route-progress-tool-activity-task", + projectId: "group-progress-test", + taskType: "dispatch_execution", + requestMessageId: "msg-route-progress-tool-activity", + requestText: "检查 Codex 工具活动", + executionPrompt: "检查 Codex 工具活动", + 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" }], + toolActivities: [ + { + kind: "mcp", + name: "github/pull_request/list", + status: "failed", + detail: "token=sk-secret-should-not-persist failed", + arguments: { token: "sk-secret-should-not-persist" }, + result: "internal tool result", + }, + { + kind: "web_search", + name: "openPage", + status: "running", + detail: "Codex App Server ThreadItem", + url: "https://example.com/private?token=sk-secret-should-not-persist", + }, + { + kind: "command", + name: "commandExecution", + status: "completed", + detail: "exit 0 · 2345ms", + command: "cat /Users/kris/.ssh/id_rsa", + cwd: "/Users/kris/code/boss", + }, + ], + }, + }), + }), + { 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.deepEqual(progress?.toolActivities, [ + { + kind: "mcp", + name: "github/pull_request/list", + status: "failed", + detail: "token=[redacted] failed", + }, + { + kind: "web_search", + name: "openPage", + status: "running", + detail: "Codex App Server ThreadItem", + }, + { + kind: "command", + name: "commandExecution", + status: "completed", + detail: "exit 0 · 2345ms", + }, + ]); + const serialized = JSON.stringify(progress); + assert.equal(serialized.includes("sk-secret-should-not-persist"), false); + assert.equal(serialized.includes("internal tool result"), false); + assert.equal(serialized.includes("https://example.com/private"), false); + assert.equal(serialized.includes("/Users/kris"), false); + assert.equal(serialized.includes("id_rsa"), false); +});