feat: surface codex account runtime notices

This commit is contained in:
AI Bot
2026-06-01 17:40:03 +08:00
parent 26b5e97614
commit defa3da185
11 changed files with 505 additions and 5 deletions

View File

@@ -1437,6 +1437,65 @@ public final class BossUi {
}
}
JSONObject accountStatus = progress == null ? null : progress.optJSONObject("accountStatus");
JSONObject modelVerification = progress == null ? null : progress.optJSONObject("modelVerification");
JSONArray verifications = modelVerification == null ? null : modelVerification.optJSONArray("verifications");
boolean hasAccountStatus = accountStatus != null ||
(verifications != null && verifications.length() > 0);
if (hasAccountStatus) {
card.addView(divider(context));
card.addView(sectionTitle(context, "账号状态"));
if (accountStatus != null) {
String authMode = accountStatus.optString("authMode", "").trim();
String planType = accountStatus.optString("planType", "").trim();
if (!TextUtils.isEmpty(authMode) || !TextUtils.isEmpty(planType)) {
String label = !TextUtils.isEmpty(authMode) && !TextUtils.isEmpty(planType)
? "认证 " + authMode + " · " + planType
: !TextUtils.isEmpty(authMode) ? "认证 " + authMode : "套餐 " + planType;
card.addView(detailRow(context, "", label, "", false));
}
String limitName = accountStatus.optString("limitName", "").trim();
int usedPercent = accountStatus.optInt("usedPercent", -1);
if (!TextUtils.isEmpty(limitName) || usedPercent >= 0) {
String label = "额度";
if (!TextUtils.isEmpty(limitName)) {
label += " " + limitName;
}
if (usedPercent >= 0) {
label += " · " + usedPercent + "%";
}
card.addView(detailRow(context, "", label, "", false));
}
int windowDurationMins = accountStatus.optInt("windowDurationMins", 0);
if (windowDurationMins > 0) {
card.addView(detailRow(context, "", "窗口 " + windowDurationMins + " 分钟", "", false, true));
}
String creditsBalance = accountStatus.optString("creditsBalance", "").trim();
if (!TextUtils.isEmpty(creditsBalance)) {
card.addView(detailRow(context, "", "余额 " + creditsBalance, "", false, true));
}
}
if (verifications != null && verifications.length() > 0) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < verifications.length(); i += 1) {
String verification = verifications.optString(i, "").trim();
if (TextUtils.isEmpty(verification)) {
continue;
}
if (builder.length() > 0) {
builder.append(", ");
}
builder.append(verification);
if (builder.length() > 120) {
break;
}
}
if (builder.length() > 0) {
card.addView(detailRow(context, "", "模型校验 " + builder, "", false));
}
}
}
JSONObject modelRoute = progress == null ? null : progress.optJSONObject("modelRoute");
JSONObject tokenUsage = progress == null ? null : progress.optJSONObject("tokenUsage");
JSONArray mcpServers = progress == null ? null : progress.optJSONArray("mcpServers");

View File

@@ -1069,6 +1069,58 @@ public class ProjectDetailActivityUiTest {
assertFalse(viewTreeContainsText(messageView, "sk-secret"));
}
@Test
public void executionProgressMessageRendersCodexAccountStatusAndVerificationSections() throws Exception {
Intent intent = new Intent()
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "thread-account")
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "Boss开发主线程");
TestProjectDetailActivity activity = Robolectric
.buildActivity(TestProjectDetailActivity.class, intent)
.setup()
.get();
JSONObject message = new JSONObject()
.put("id", "progress-account-1")
.put("sender", "master")
.put("senderLabel", "主 Agent")
.put("body", "执行进度")
.put("kind", "execution_progress")
.put("sentAt", "2026-06-01T10:28:00+08:00")
.put("executionProgress", new JSONObject()
.put("status", "running")
.put("steps", new JSONArray()
.put(new JSONObject().put("text", "同步 Codex 账号运行态").put("status", "running")))
.put("accountStatus", new JSONObject()
.put("authMode", "chatgpt")
.put("planType", "team")
.put("limitName", "Codex")
.put("usedPercent", 88)
.put("windowDurationMins", 180)
.put("resetsAt", 1770003600)
.put("creditsBalance", "120.5")
.put("hasCredits", true)
.put("unlimitedCredits", false)
.put("accessToken", "sk-secret-should-not-render"))
.put("modelVerification", new JSONObject()
.put("verifications", new JSONArray().put("trustedAccessForCyber"))
.put("turnId", "turn-secret-should-not-render")));
View messageView = ReflectionHelpers.callInstanceMethod(
activity,
"buildMessageView",
ReflectionHelpers.ClassParameter.from(JSONObject.class, message)
);
assertTrue(viewTreeContainsText(messageView, "账号状态"));
assertTrue(viewTreeContainsText(messageView, "认证 chatgpt · team"));
assertTrue(viewTreeContainsText(messageView, "额度 Codex · 88%"));
assertTrue(viewTreeContainsText(messageView, "窗口 180 分钟"));
assertTrue(viewTreeContainsText(messageView, "余额 120.5"));
assertTrue(viewTreeContainsText(messageView, "模型校验 trustedAccessForCyber"));
assertFalse(viewTreeContainsText(messageView, "sk-secret"));
assertFalse(viewTreeContainsText(messageView, "turn-secret"));
}
@Test
public void executionProgressMessageRendersCodexThreadGoalSettingsAndCompactionSections() throws Exception {
Intent intent = new Intent()

View File

@@ -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:<port>``unix://PATH` 同机长驻 App Server长驻连接支持 `Authorization: Bearer <token>`,配置上优先使用 `codexAppServerAuthTokenFile`。turn 启动前失败才回退 CLIturn 启动后不重复执行;桌面远程控制默认先走 `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` 归一到 Boss `execution_progress` 卡片realtime 只保留状态、文本摘要和计数运行状态只保留模型切换、上下文用量、MCP 状态和远控连接摘要,线程配置只保留目标、模型、审批、沙箱、协作模式和压缩状态,不保存 SDP、音频原始数据、raw item、remote installationId、cwd、turnId 或未清洗密钥。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:<port>``unix://PATH` 同机长驻 App Server长驻连接支持 `Authorization: Bearer <token>`,配置上优先使用 `codexAppServerAuthTokenFile`。turn 启动前失败才回退 CLIturn 启动后不重复执行;桌面远程控制默认先走 `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` 归一到 Boss `execution_progress` 卡片realtime 只保留状态、文本摘要和计数运行状态只保留模型切换、上下文用量、MCP 状态和远控连接摘要,线程配置只保留目标、模型、审批、沙箱、协作模式和压缩状态,账号状态只保留认证方式、套餐、额度窗口、积分余额和模型校验摘要,不保存 SDP、音频原始数据、raw item、remote installationId、cwd、turnId、配置文件路径或未清洗密钥。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` 完成回写已补幂等,重复完成不会再向群聊重复追加结果

View File

@@ -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:<port>``codexAppServerTransport=unix + codexAppServerUrl=unix:///absolute/path.sock` 连接同机长驻 App Serverbearer token 可通过 `codexAppServerAuthTokenFile` 读取并在握手时发送 `Authorization: Bearer <token>`。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 启动前失败可回退 CLIturn 启动后失败不回退避免重复执行。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`;服务端 complete 回写会与本地 Git/GitHub 进度合并,且不保存 SDP、音频 base64、raw realtime item、remote installationId 或未清洗的 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:<port>``codexAppServerTransport=unix + codexAppServerUrl=unix:///absolute/path.sock` 连接同机长驻 App Serverbearer token 可通过 `codexAppServerAuthTokenFile` 读取并在握手时发送 `Authorization: Bearer <token>`。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 启动前失败可回退 CLIturn 启动后失败不回退避免重复执行。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` 归一成线程配置、账号状态、模型校验和安全提醒摘要;服务端 complete 回写会与本地 Git/GitHub 进度合并,且不保存 SDP、音频 base64、raw realtime item、remote installationId、cwd、turnId、配置路径或未清洗的 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

View File

@@ -35,7 +35,7 @@ Boss 不能直接把 App Server 原始 Thread / Turn / Item 字段写进业务
- 本机 `codex --version``codex-cli 0.135.0-alpha.1`
- 本机 `codex app-server --help` 已可用;本机 help 当前显示 `--listen` 支持 `stdio://``unix://``unix://PATH``ws://IP:PORT``off`
- 本机 `codex app-server --help` 当前已经支持 `--ws-auth capability-token|signed-bearer-token``--ws-token-file``--ws-token-sha256``--ws-shared-secret-file`、issuer/audience/clock-skew 等 WebSocket 认证参数
- 本机协议快照已生成到 `docs/protocol-snapshots/codex-app-server/0.135.0-alpha.1/`,共识别 137 个协议方法;确认支持 `thread/inject_items``thread/rollback``thread/goal/*``turn/steer``command/exec``thread/realtime/*``model/list`
- 本机协议快照已生成到 `docs/protocol-snapshots/codex-app-server/0.135.0-alpha.1/`,共识别 137 个协议方法;确认支持 `thread/inject_items``thread/rollback``thread/goal/*``turn/steer``command/exec``thread/realtime/*``account/*``model/verification``configWarning``deprecationNotice``model/list`
- Boss 当前默认仍以 `stdio` 作为本机 agent 接入方式;`ws://127.0.0.1:<port>``unix://PATH` 本地长驻 transport 已可灰度接入WebSocket/Unix WebSocket handshake 支持 `Authorization: Bearer <token>`;非 loopback signed bearer/JWT、自动重连和健康探测仍保留为后续增强不直接替换当前稳定链路
- 官方文档提示 WebSocket ingress 满载时会返回 JSON-RPC `-32001 / Server overloaded; retry later.`Boss runner 已对该错误做最多 3 次指数退避重试,避免长驻连接瞬时拥塞直接把用户任务打失败
- Boss heartbeat 已新增 App Server 能力发现缓存:按 `codexAppServerDiscoveryTtlMs` 拉取 `model/list``modelProvider/capabilities/read``skills/list``plugin/list``app/list`,归一成设备 `capabilities.codexAppServer.metadata`;发现失败只记录 warn不阻塞心跳
@@ -127,6 +127,7 @@ UI 参考:
- `local-agent/codex-app-server-runner.mjs` 已把 App Server `thread/status/changed``thread/realtime/started|transcript|outputAudio|itemAdded|error|closed` 归一成 `executionProgress.threadStatus / realtime`;服务端进度路由和 Android 原生进度卡已支持展示,测试覆盖 SDP、音频原始数据和 raw item 不外泄
- `local-agent/codex-app-server-runner.mjs` 已把 App Server `model/rerouted``thread/tokenUsage/updated``mcpServer/startupStatus/updated``remoteControl/status/changed` 归一成 `executionProgress.modelRoute / tokenUsage / mcpServers / remoteControl`;服务端进度路由和 Android 原生进度卡已支持展示,测试覆盖 installationId 和密钥不外泄
- `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 和密钥不外泄
- 新增实时进度入口 `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`

View File

@@ -35,7 +35,7 @@
- `launchd` 已安装:`~/Library/LaunchAgents/com.hyzq.boss.local-agent.plist`
- 当前执行底座抽象层已落地在 `src/lib/execution/`,并已补齐 `ExecutionBackend / PromptAssembler / PermissionPolicy / RemoteRuntimeAdapter / OrchestrationBackend` 默认实现
- 当前生产主链仍然沿用 `local-agent -> codex exec resume -> /api/v1/master-agent/tasks/[taskId]/complete`,执行底座重构以“先抽象、不改行为”为准
- 当前 Codex server 调研结论已记录在 `docs/architecture/codex_server_progress_card_cn.md`:长期优先方向更新为 `Codex App Server / Remote Control -> Inter-Thread Broker -> CodexMcpBackendAdapter -> codex exec resume` 的分层 provider 策略;当前 boss-agent 默认打开 `Codex App Server` runner 作为 Codex 绑定入口Boss 仍保留 `codex exec resume` 兜底,并继续用 `execution_progress` 结构化进度卡作为 APP 可见执行态。本机 `codex-cli 0.135.0-alpha.1` 协议快照已生成到 `docs/protocol-snapshots/codex-app-server/0.135.0-alpha.1/`,确认支持 WebSocket auth、`thread/inject_items``turn/steer``thread/realtime/*``thread/goal/*``thread/settings/updated``thread/compacted``command/exec``model/list`
- 当前 Codex server 调研结论已记录在 `docs/architecture/codex_server_progress_card_cn.md`:长期优先方向更新为 `Codex App Server / Remote Control -> Inter-Thread Broker -> CodexMcpBackendAdapter -> codex exec resume` 的分层 provider 策略;当前 boss-agent 默认打开 `Codex App Server` runner 作为 Codex 绑定入口Boss 仍保留 `codex exec resume` 兜底,并继续用 `execution_progress` 结构化进度卡作为 APP 可见执行态。本机 `codex-cli 0.135.0-alpha.1` 协议快照已生成到 `docs/protocol-snapshots/codex-app-server/0.135.0-alpha.1/`,确认支持 WebSocket auth、`thread/inject_items``turn/steer``thread/realtime/*``thread/goal/*``thread/settings/updated``thread/compacted``account/*``model/verification``configWarning``deprecationNotice``command/exec``model/list`
- 当前量产 B+ 架构开发文档已新增:`docs/architecture/enterprise_ai_ops_architecture_cn.md`。该文档把 PPT 中的主 Agent / 业务 Agent / 老板端 / 经理端 / 员工端 / 治理层 / 系统层 / 设备层 / 执行层 / 接入层整理成后续产品架构约束并明确数据库备份、业务回退、Codex 协议扩展和 Skill 治理方向;它是规划文档,不代表当前全部已落地
- 当前 `claw-code` 已以最小 `ClawBackendAdapter` 形式接入执行底座,但默认关闭;只有显式配置 `BOSS_CLAW_*` 且可用性探测通过时,`master-agent` 当前对话中才会出现并允许选择 `claw-runtime`
- 当前已新增最小 `Telegram Gateway`Boss 当前可直接暴露 Telegram webhook把 Telegram 私聊或受控群聊文本桥接进 `master-agent` 或按群 / Topic 路由到指定 Boss 项目,并在主 Agent 异步任务完成后自动回推 Telegram配置入口已接到 Web `/me/telegram` 和原生 Android `我的 > Telegram 接入`
@@ -249,7 +249,7 @@ cd /Users/kris/code/boss
- 当前 `local-agent` 已新增 `Codex App Server` providerboss-agent 默认配置 `codexAppServerEnabled=true``conversation_reply / dispatch_execution` 会先通过 `codex app-server` 的 stdio JSON-RPC 恢复或创建线程,也可配置 `codexAppServerTransport=ws + codexAppServerUrl=ws://127.0.0.1:<port>``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 / failedAndroid 原生聊天页会显示“进度 / 线程状态 / 实时状态 / 线程配置 / 运行状态 / 安全提醒 / 审批状态 / 文件变更 / 分支详情 / 生成结果 / 后台智能体”。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` 映射为线程配置摘要,只展示目标、模型、审批、沙箱、协作模式和上下文压缩状态,不保存 cwd、turnId 或 collaboration settings 内部 prompt。所有进度均通过 `POST /api/v1/master-agent/tasks/[taskId]/progress` 实时刷新complete 回写仍会携带最终进度兜底
- 当前 `local-agent` 会在 Codex 任务执行中和完成时回传 `executionProgress`:服务端把同一任务的进度卡从 queued / running 更新到 completed / failedAndroid 原生聊天页会显示“进度 / 线程状态 / 实时状态 / 线程配置 / 账号状态 / 运行状态 / 安全提醒 / 审批状态 / 文件变更 / 分支详情 / 生成结果 / 后台智能体”。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` 映射为账号状态、模型校验和安全提醒摘要。所有进度均通过 `POST /api/v1/master-agent/tasks/[taskId]/progress` 实时刷新;字段白名单会剥离 cwd、turnId、配置文件路径、内部 prompt 和未清洗密钥,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 错误,不再假装执行成功

View File

@@ -846,6 +846,55 @@ function extractTokenUsageSnapshot(tokenUsage) {
};
}
function extractRateLimitWindowSnapshot(window) {
if (!window || typeof window !== "object") {
return {};
}
return {
usedPercent: extractNumber(window.usedPercent),
windowDurationMins: extractNumber(window.windowDurationMins),
resetsAt: extractNumber(window.resetsAt),
};
}
function extractAccountRateLimitSnapshot(rateLimits) {
if (!rateLimits || typeof rateLimits !== "object") {
return null;
}
const primary = extractRateLimitWindowSnapshot(rateLimits.primary);
const credits = rateLimits.credits && typeof rateLimits.credits === "object" ? rateLimits.credits : {};
const snapshot = Object.fromEntries(Object.entries({
limitId: safeProgressText(rateLimits.limitId, 80),
limitName: safeProgressText(rateLimits.limitName, 80),
planType: safeProgressText(rateLimits.planType, 80),
rateLimitReachedType: safeProgressText(rateLimits.rateLimitReachedType, 80),
usedPercent: primary.usedPercent,
windowDurationMins: primary.windowDurationMins,
resetsAt: primary.resetsAt,
creditsBalance: safeProgressText(credits.balance, 80),
...(typeof credits.hasCredits === "boolean" ? { hasCredits: credits.hasCredits } : {}),
...(typeof credits.unlimited === "boolean" ? { unlimitedCredits: credits.unlimited } : {}),
}).filter(([, value]) => value !== undefined && value !== ""));
return Object.values(snapshot).some((value) => value !== undefined && value !== "") ? snapshot : null;
}
function extractModelVerificationSnapshot(params) {
const verifications = asArray(params?.verifications)
.map((verification) => safeProgressText(verification, 120))
.filter(Boolean)
.slice(0, 8);
return verifications.length > 0 ? { verifications } : null;
}
function buildNoticeWarningMessage(summary, details) {
const cleanSummary = safeProgressText(summary, 140);
const cleanDetails = safeProgressText(details, 180);
if (!cleanSummary) {
return cleanDetails;
}
return cleanDetails ? `${cleanSummary}${cleanDetails}` : cleanSummary;
}
function buildServerRequestFallbackResponse(message) {
const method = String(message?.method ?? "");
if (/commandExecution\/requestApproval|execCommandApproval/i.test(method)) {
@@ -1099,6 +1148,8 @@ function createProgressCollector() {
let threadGoal;
let threadSettings;
let compaction;
let accountStatus;
let modelVerification;
const upsertArtifact = (artifact) => {
if (!artifact || artifacts.some((item) => item.label === artifact.label)) {
@@ -1396,6 +1447,66 @@ function createProgressCollector() {
};
return;
}
if (message.method === "account/updated") {
const authMode = safeProgressText(message.params?.authMode, 80);
const planType = safeProgressText(message.params?.planType, 80);
accountStatus = {
...(accountStatus ?? {}),
...(authMode ? { authMode } : {}),
...(planType ? { planType } : {}),
};
return;
}
if (message.method === "account/rateLimits/updated") {
const nextAccountStatus = extractAccountRateLimitSnapshot(message.params?.rateLimits);
if (nextAccountStatus) {
accountStatus = {
...(accountStatus ?? {}),
...nextAccountStatus,
};
}
return;
}
if (message.method === "model/verification") {
const nextVerification = extractModelVerificationSnapshot(message.params);
if (nextVerification) {
modelVerification = nextVerification;
}
return;
}
if (message.method === "warning") {
const warningMessage = safeProgressText(message.params?.message, 180);
if (warningMessage) {
pushWarning({
id: `codex-warning-${warnings.length + 1}`,
severity: "warning",
message: warningMessage,
});
}
return;
}
if (message.method === "configWarning") {
const warningMessage = buildNoticeWarningMessage(message.params?.summary, message.params?.details);
if (warningMessage) {
pushWarning({
id: `config-warning-${warnings.length + 1}`,
severity: "warning",
message: warningMessage,
});
}
return;
}
if (message.method === "deprecationNotice") {
const warningMessage = buildNoticeWarningMessage(message.params?.summary, message.params?.details);
if (warningMessage) {
pushWarning({
id: `deprecation-notice-${warnings.length + 1}`,
severity: "info",
message: warningMessage,
});
}
return;
}
if (message.method === "thread/started") {
upsertAgent(extractAgentFromThreadStarted(message.params));
}
@@ -1457,6 +1568,12 @@ function createProgressCollector() {
if (compaction) {
result.compaction = { ...compaction };
}
if (accountStatus) {
result.accountStatus = { ...accountStatus };
}
if (modelVerification) {
result.modelVerification = { ...modelVerification };
}
return Object.keys(result).length > 0 ? result : undefined;
},
};

View File

@@ -175,6 +175,24 @@ export interface ExecutionProgressTokenUsage {
contextPercent?: number;
}
export interface ExecutionProgressAccountStatus {
authMode?: string;
planType?: string;
limitId?: string;
limitName?: string;
usedPercent?: number;
windowDurationMins?: number;
resetsAt?: number;
rateLimitReachedType?: string;
creditsBalance?: string;
hasCredits?: boolean;
unlimitedCredits?: boolean;
}
export interface ExecutionProgressModelVerification {
verifications: string[];
}
export interface ExecutionProgressMcpServer {
name: string;
status?: string;
@@ -236,6 +254,8 @@ export interface ExecutionProgressSnapshot {
realtime?: ExecutionProgressRealtime;
modelRoute?: ExecutionProgressModelRoute;
tokenUsage?: ExecutionProgressTokenUsage;
accountStatus?: ExecutionProgressAccountStatus;
modelVerification?: ExecutionProgressModelVerification;
mcpServers?: ExecutionProgressMcpServer[];
remoteControl?: ExecutionProgressRemoteControl;
threadGoal?: ExecutionProgressThreadGoal;
@@ -256,6 +276,8 @@ export interface ExecutionProgressInput {
realtime?: Partial<ExecutionProgressRealtime>;
modelRoute?: Partial<ExecutionProgressModelRoute>;
tokenUsage?: Partial<ExecutionProgressTokenUsage>;
accountStatus?: Partial<ExecutionProgressAccountStatus> & { accessToken?: unknown; apiKey?: unknown };
modelVerification?: Partial<ExecutionProgressModelVerification> & { turnId?: unknown };
mcpServers?: Array<Partial<ExecutionProgressMcpServer> & { name?: string }>;
remoteControl?: Partial<ExecutionProgressRemoteControl> & { installationId?: unknown };
threadGoal?: Partial<ExecutionProgressThreadGoal>;
@@ -3936,6 +3958,10 @@ function normalizeExecutionProgressSnapshot(raw: Partial<ExecutionProgressSnapsh
realtime: nativeRemoteControl ? undefined : normalizeExecutionProgressRealtime(raw.realtime),
modelRoute: nativeRemoteControl ? undefined : normalizeExecutionProgressModelRoute(raw.modelRoute),
tokenUsage: nativeRemoteControl ? undefined : normalizeExecutionProgressTokenUsage(raw.tokenUsage),
accountStatus: nativeRemoteControl ? undefined : normalizeExecutionProgressAccountStatus(raw.accountStatus),
modelVerification: nativeRemoteControl
? undefined
: normalizeExecutionProgressModelVerification(raw.modelVerification),
mcpServers: nativeRemoteControl ? undefined : normalizeExecutionProgressMcpServers(raw.mcpServers),
remoteControl: nativeRemoteControl ? undefined : normalizeExecutionProgressRemoteControl(raw.remoteControl),
threadGoal: nativeRemoteControl ? undefined : normalizeExecutionProgressThreadGoal(raw.threadGoal),
@@ -5567,6 +5593,43 @@ function normalizeExecutionProgressTokenUsage(
};
}
function normalizeExecutionProgressAccountStatus(
input?: ExecutionProgressInput["accountStatus"],
): ExecutionProgressAccountStatus | undefined {
if (!input) {
return undefined;
}
const status: ExecutionProgressAccountStatus = {
authMode: safeExecutionProgressText(input.authMode) || undefined,
planType: safeExecutionProgressText(input.planType) || undefined,
limitId: safeExecutionProgressText(input.limitId) || undefined,
limitName: safeExecutionProgressText(input.limitName) || undefined,
usedPercent: normalizeOptionalNumber(input.usedPercent),
windowDurationMins: normalizeOptionalNumber(input.windowDurationMins),
resetsAt: normalizeOptionalNumber(input.resetsAt),
rateLimitReachedType: safeExecutionProgressText(input.rateLimitReachedType) || undefined,
creditsBalance: safeExecutionProgressText(input.creditsBalance) || undefined,
hasCredits: typeof input.hasCredits === "boolean" ? input.hasCredits : undefined,
unlimitedCredits: typeof input.unlimitedCredits === "boolean" ? input.unlimitedCredits : undefined,
};
return Object.values(status).some((value) => value !== undefined && value !== "") ? status : undefined;
}
function normalizeExecutionProgressModelVerification(
input?: ExecutionProgressInput["modelVerification"],
): ExecutionProgressModelVerification | undefined {
if (!input) {
return undefined;
}
const verifications = Array.isArray(input.verifications)
? input.verifications
.map((verification) => safeExecutionProgressText(verification))
.filter(Boolean)
.slice(0, 8)
: [];
return verifications.length > 0 ? { verifications } : undefined;
}
function normalizeExecutionProgressMcpServers(input?: ExecutionProgressInput["mcpServers"]) {
return (input ?? [])
.map((server): ExecutionProgressMcpServer | null => {
@@ -5799,6 +5862,10 @@ function buildExecutionProgressSnapshot(
realtime: nativeRemoteControl ? undefined : normalizeExecutionProgressRealtime(input?.realtime),
modelRoute: nativeRemoteControl ? undefined : normalizeExecutionProgressModelRoute(input?.modelRoute),
tokenUsage: nativeRemoteControl ? undefined : normalizeExecutionProgressTokenUsage(input?.tokenUsage),
accountStatus: nativeRemoteControl ? undefined : normalizeExecutionProgressAccountStatus(input?.accountStatus),
modelVerification: nativeRemoteControl
? undefined
: normalizeExecutionProgressModelVerification(input?.modelVerification),
mcpServers: nativeRemoteControl ? undefined : normalizeExecutionProgressMcpServers(input?.mcpServers),
remoteControl: nativeRemoteControl ? undefined : normalizeExecutionProgressRemoteControl(input?.remoteControl),
threadGoal: nativeRemoteControl ? undefined : normalizeExecutionProgressThreadGoal(input?.threadGoal),

View File

@@ -581,6 +581,67 @@ rl.on("line", (line) => {
},
});
}
if (process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_ACCOUNT_NOTICE_EVENTS === "1") {
send({
method: "account/updated",
params: {
authMode: "chatgpt",
planType: "team",
},
});
send({
method: "account/rateLimits/updated",
params: {
rateLimits: {
limitId: "codex",
limitName: "Codex",
primary: {
usedPercent: 88,
windowDurationMins: 180,
resetsAt: 1770003600,
},
secondary: null,
credits: {
hasCredits: true,
unlimited: false,
balance: "120.5",
},
planType: "team",
rateLimitReachedType: null,
},
},
});
send({
method: "model/verification",
params: {
threadId: message.params?.threadId,
turnId: "turn-fixture",
verifications: ["trustedAccessForCyber"],
},
});
send({
method: "warning",
params: {
threadId: message.params?.threadId,
message: "模型切换提醒 token=sk-secret-should-not-leak",
},
});
send({
method: "configWarning",
params: {
summary: "项目配置已忽略",
details: "openai_base_url 不能放在项目配置里",
path: "/Users/kris/code/boss/.codex/config.toml",
},
});
send({
method: "deprecationNotice",
params: {
summary: "on-failure 已废弃",
details: "请改用 on-request",
},
});
}
send({
method: "item/agentMessage/delta",
params: {

View File

@@ -529,6 +529,72 @@ test("codex app-server runner maps thread goal, settings, and compaction events
}
});
test("codex app-server runner maps account, quota, verification, and notices without leaking config paths", async () => {
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_ACCOUNT_NOTICE_EVENTS;
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_ACCOUNT_NOTICE_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-account-notices",
taskType: "conversation_reply",
targetCodexThreadRef: "019d-app-server-thread",
targetCodexFolderRef: repoRoot,
executionPrompt: "同步账号与告警状态",
});
assert.equal(result.status, "completed");
assert.deepEqual(result.executionProgress.accountStatus, {
authMode: "chatgpt",
planType: "team",
limitId: "codex",
limitName: "Codex",
usedPercent: 88,
windowDurationMins: 180,
resetsAt: 1770003600,
creditsBalance: "120.5",
hasCredits: true,
unlimitedCredits: false,
});
assert.deepEqual(result.executionProgress.modelVerification, {
verifications: ["trustedAccessForCyber"],
});
assert.deepEqual(result.executionProgress.warnings, [
{
id: "codex-warning-1",
message: "模型切换提醒 token=[redacted]",
severity: "warning",
},
{
id: "config-warning-2",
message: "项目配置已忽略openai_base_url 不能放在项目配置里",
severity: "warning",
},
{
id: "deprecation-notice-3",
message: "on-failure 已废弃:请改用 on-request",
severity: "info",
},
]);
const serialized = JSON.stringify(result.executionProgress);
assert.equal(serialized.includes("/Users/kris"), false);
assert.equal(serialized.includes("sk-secret-should-not-leak"), false);
} finally {
if (previous === undefined) {
delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_ACCOUNT_NOTICE_EVENTS;
} else {
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_ACCOUNT_NOTICE_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";

View File

@@ -370,3 +370,80 @@ test("POST task progress preserves Codex thread goal, settings, and compaction s
assert.equal(serialized.includes("/Users/kris"), false);
assert.equal(serialized.includes("turn-secret-should-not-persist"), false);
});
test("POST task progress preserves Codex account, quota, verification, and notice summaries", async () => {
const task = await data.queueMasterAgentTask({
taskId: "route-progress-account-notices-task",
projectId: "group-progress-test",
taskType: "dispatch_execution",
requestMessageId: "msg-route-progress-account-notices",
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" }],
accountStatus: {
authMode: "chatgpt",
planType: "team",
limitId: "codex",
limitName: "Codex",
usedPercent: 88,
windowDurationMins: 180,
resetsAt: 1770003600,
creditsBalance: "120.5",
hasCredits: true,
unlimitedCredits: false,
accessToken: "sk-secret-should-not-persist",
},
modelVerification: {
verifications: ["trustedAccessForCyber"],
turnId: "turn-secret-should-not-persist",
},
warnings: [
{
id: "config-warning-1",
message: "项目配置已忽略openai_base_url 不能放在项目配置里",
severity: "warning",
path: "/Users/kris/code/boss/.codex/config.toml",
},
],
},
}),
}),
{ 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?.accountStatus?.authMode, "chatgpt");
assert.equal(progress?.accountStatus?.planType, "team");
assert.equal(progress?.accountStatus?.usedPercent, 88);
assert.equal(progress?.modelVerification?.verifications?.[0], "trustedAccessForCyber");
assert.equal(progress?.warnings?.[0]?.message, "项目配置已忽略openai_base_url 不能放在项目配置里");
const serialized = JSON.stringify(progress);
assert.equal(serialized.includes("/Users/kris"), false);
assert.equal(serialized.includes("sk-secret-should-not-persist"), false);
assert.equal(serialized.includes("turn-secret-should-not-persist"), false);
});