--- description: "dsh-embedded-codex 的 Bundle 组合、Runtime 路由、App Server 生命周期与 DSH 事件映射。" kind: "architecture" --- # 架构 ## 目标 本仓库通过一个外部 DSH Bundle 提供 Codex Agent Runtime,不修改 DeepSeek Harness 源码或 Desktop 安装目录。声明式 `embedded-codex` Agent 预设选择 Codex App Server;其他预设继续选择 DSH `agent-loop`。默认开发、交付和验收入口是 Electron Desktop,renderer 仍使用 `dsh.client.platform: web`。 ## Profile 组合 Cordis 的 patch 不允许通过目标行的 `name` 字段替换插件名;`name` 只用于校验目标身份。因此 [`cordis.patch.yml`](../cordis.patch.yml) 先以 `id + name` 禁用六个原 provider,再插入六个 replacement、一个声明式 preset 和主 Runtime。Agent Preset Registry replacement 特意沿用官方 `agent-preset-registry` id:Loader 对重复 id 采用后写覆盖,而原版 Agent Preset UI 固定通过该 id 写入默认 preset 设置。 | 原条目 | 操作 | 替代条目 | |---|---|---| | `agent` | 校验 `@deepseek-ai/dsh-agent` 后禁用 | `embedded-codex-agent-registry` | | `agent-preset-registry` | 校验 `@deepseek-ai/dsh-agent-preset-registry` 后禁用 | `agent-preset-registry`(同 id,后写覆盖) | | `config-editor` | 校验 `@deepseek-ai/dsh-config-editor` 后禁用 | `embedded-codex-config-editor` | | `permission` | 校验 `@deepseek-ai/dsh-permission-presets` 后禁用 | `embedded-codex-permission-presets` | | `session-controller` | 校验 `@deepseek-ai/dsh-api-session-controller` 后禁用 | `embedded-codex-session-controller` | | `ui-permission` | 校验 `@deepseek-ai/dsh-client-ui-permission-presets` 后禁用 | `embedded-codex-ui-permission-presets` | `preset-embedded-codex` 使用 `@deepseek-ai/dsh-agent-preset` 声明空插件组合;空数组表示没有额外上下文插件,不表示缺少 Agent Runtime。`agent-loop`、`ui-conversation`、`llm-deepseek`、工具和持久化不会被禁用。主 Runtime 启动时验证 replacement、preset、Runtime 及两个必须保留的 stock row;缺失、重复或被改写时立即失败。 ## 兼容 provider 兼容 provider 不是运行时 monkey patch。构建脚本从固定 DSH submodule 复制五个 Host provider 和 Permission UI 的 TypeScript/TSX/CSS 输入,在仓库自己的 `compat/generated` 中应用逐文件可审查 patch,再生成独立发布 bundle。安装阶段不包含也不执行源码 patch。 Agent Registry 保留默认 `AgentFactory`,并增加按 Agent Preset id 注册的具名 factory。创建读取 `CreateAgentOptions.meta.agentPreset`,恢复读取 `ResumeAgentOptions.agentPreset`。Registry 记录每个 live Agent 的实际 factory,从而阻止标准 Runtime 选择 `codex` provider,也阻止 Codex Runtime 选择普通 LLM provider。 Agent Preset Registry 保留目标 DSH 的声明注册、组合、选择、持久化和编辑能力,并增加 effect-scoped runtime switcher。只有 idle、无 queued/steering 输入且从未开始 turn 的 Session 可以跨 Runtime 切换;同 factory 使用原生 recompose,跨 factory 交给 Session Controller 事务性替换。 Config Editor 保留目标 DSH 的校验、原子写入、HMR 和 layer 优先级,只将同 id 行的配置读取从 first-write-wins 改为 Loader 使用的 last-write-wins。这样兼容 Registry 可以沿用官方设置命名空间,同时 profile 写回不会误判成 home/命令行覆盖。 Permission Presets 保留标准 DSH 的 Read only、Workspace write 和 Full access 表。当 `permissions` projection 中的 Agent preset 为 `embedded-codex` 时,同一个服务改为投影 Ask for approval、Approve for me 和 Full access。Ask 与 Auto-review 共用 `workspace-write + ask` 底层元组,最近的 `permission/preset` 事件保存两者的精确选择;切回标准 Runtime 时自然恢复 Workspace write。标准只读状态切到 Codex 时投影为 `custom`,不会静默扩大为可写。 Session Controller 在创建、恢复与 fork 前解析 Agent Preset,并将具名 factory 的模型默认值传给 Agent Registry。跨 Runtime 切换会先释放空白 Session 的旧 Agent,再恢复目标 Agent;目标恢复失败时尝试恢复旧 Runtime。Host 只在新 Agent 已发布后完成选择请求。 Web 客户端保留同一 Session id 的 resident 对象。`api-session/removed` 暂时将它标记为 removed;紧随其后的 `api-session/added` 清除此状态并继续使用已有 event window 与 projection store,因此模型选择器和聊天输入不会永久禁用。 Conversation UI 完全使用目标 DSH 原实现。最小 Permission UI replacement 只改变选项来源:当前 Session projection 提供 `options` 时使用 provider-aware 表,否则使用 process catalog;General Settings 仍使用标准默认项。Host 提供名称与说明,UI 不硬编码 Codex 文案。兼容 Session Controller 与 Permission UI 由本包的 immediate client bundle 注册,不再对 minified bundle 做字符串替换。 ## Runtime 路由 ```text Session create/resume/fork -> resolve Agent Preset -> ctx.agents -> no named factory: agent-loop -> embedded-codex: EmbeddedCodexRuntime -> CodexAppServerHost -> CodexRemoteThread -> EmbeddedCodexAgent ``` `ctx.embeddedCodex` 是 App Server、模型目录与 Codex Agent factory 的生命周期所有者。它向 `ctx.llm` 注册一个仅供模型目录使用的 adapter,并用 `ctx.agents.setPresetFactory()` 将声明式 `embedded-codex` preset 绑定到具名 factory。 `codex/default` 表示不在 `turn/start` 指定原生模型,让 Codex 使用账户和 Runtime 默认值。其他模型 id 原样传给 App Server。Reasoning effort 来自 `model/list`。`ctx.llm.stream()` 不执行 Codex 对话,并返回 `CODEX_RUNTIME_ONLY`。 ## Step 输入生命周期 Runtime 在从 Inbox claim 输入后、写入 `user/message` 或调用 App Server 前,先组装一次 agent-scoped system prompt,并执行标准 `agent/pre-step` waterfall。组装结果不会覆盖 Codex App Server 自己管理的 system prompt;这一步保留 DSH 插件所依赖的 step 生命周期和作用域副作用。 只有 waterfall 返回的 `enter.messages` 会写入 Session 并转换为 App Server input。因此插件可以改写直接用户消息,也可以追加带独立 source 的文本或图片上下文,而原始引用不会泄漏到模型输入。首轮返回 `reject` 时,turn 以 `blocked` 结束且不打开 step;返回空消息时,turn 以 `completed` 结束,同样不启动原生 turn。 运行中 `agent.steer()` 也走相同准备函数。原生 `turn/steer` 是当前 Codex turn 的追加输入,不是新的 DSH model-call step,所以 hook 收到当前本地 turn/step,批准后的消息记录在当前开放 step 中。`reject` 或空消息只抑制这批 steering,不中断已经运行的 Codex turn;`startsRequestSeries` 会写入对应的 `request/header` series 边界。 Runtime 不识别 `dsh-context:` 或任何 Context Picker 私有格式。两类图片最终汇入同一个 `messageInputs()` 边界: - 直接粘贴图片由 Session Controller 先保存为 attachment ref。 - Context Picker 先提交持久快照引用,再由它自己的 `agent/pre-step` 将引用改写为可读文本,并追加 metadata 与 image content block。 - Embedded Runtime 将 image attachment 转为 App Server `localImage`;没有 Host 路径时读取可信存储并转为 `image` data URL。Session 中仍保留 attachment ref 的 `mediaType`、bytes、宽高和 name,而不是依赖浏览器或模型可见的临时磁盘路径。 ## App Server 生命周期 Runtime 通过 `@openai/codex` package manifest 解析准确的包内 wrapper,以 Node 启动 `app-server --stdio`。Electron 宿主下显式设置 `ELECTRON_RUN_AS_NODE=1`;`ctx.subprocess` 拥有进程树、stdin/stdout、stderr 上限和终止宽限。`initialize.clientInfo.version` 来自本插件 package manifest。 Host 延迟创建一个共享 App Server 进程,完成 `initialize`、`initialized`、`account/read` 与分页 `model/list`。默认配置只接受 ChatGPT 登录。一个连接内,原生 thread id 最多绑定一个 live DSH Agent;通知和请求按 thread id 路由。 每个 DSH Session 对应一个持久 Codex thread。新 Session 调用 `thread/start`,恢复调用 `thread/resume`,DSH fork 使用继承边界中保存的原生 turn id 调用 `thread/fork`。成功或中断的 Assistant message 在 `source.replayState.embeddedCodex` 保存 binding version、thread id 与 turn id。 可见 DSH transcript 不包含 Codex 隐藏历史,因此含模型对话但没有有效绑定的 seeded Session 会失败,不会降级为有损回放。 ## 权限同步 Session 的 `permissions` projection 是正常 Web Profile 中的唯一权限真源。Runtime 在恢复原生 thread 和开始每个新 turn 前读取一次当前 preset,并通过 `resolveCodexPermission()` 生成完整快照: ```text Ask for approval -> workspaceWrite + networkAccess:false + on-request + user Approve for me -> workspaceWrite + networkAccess:false + on-request + auto_review Full access -> dangerFullAccess + never + user ``` `thread/start`、`thread/resume` 和 `thread/fork` 使用快照中的 legacy sandbox mode、approval policy 与 reviewer 初始化原生 thread;返回值若改变 reviewer、approval policy,或扩大网络和 writable roots,attach 失败。`turn/start` 再发送完整 `sandboxPolicy`、`approvalPolicy` 与 `approvalsReviewer`,并在每个新 turn 重发,避免恢复或外部配置造成漂移。Steering 沿用活动 turn 的快照,`/permission` 在 Agent running 时拒绝切换。 旧 `approvalPolicy` 和 `sandbox` 插件配置只在没有 Permission Presets provider 的非标准组合中作为弃用 fallback;发布 Bundle 总会组合兼容 provider,因此不会使用该路径。未知、`custom` 或标准 preset 名无法启动 Codex turn,错误要求用户先明确选择 Codex 权限模式。 ## 事件映射与顺序 Runtime 只写固定 DSH 版本已经定义的 Session 事件,不增加持久格式。一个 Codex turn 对应一个 DSH turn,并在工具阶段与最终答案阶段之间加入兼容 step: ```text turn/start step/start (1) user/message request/header assistant/chunk(reasoning/tool-call) tool/call + tool/result assistant/message(process) step/end (1) step/start (2) assistant/chunk(final text/usage) assistant/message(final) step/end (2) turn/end ``` 工具的 `tool-call` block、`tool/call` 与 `tool/result` 共享 `codex:`。过程 Assistant 包含 reasoning 与 tool-call block;最终 Assistant 只包含答案。DSH Chat 因而依次投影系统提示、用户、过程、工具与最终答案,Trajectory 也按相同 turn/step 关联工具。原生工具事件是观测,不会调用 `ctx.tools.execute()`。 命令和文件审批映射到 `ctx.approval`,权限审批只返回 App Server 请求的 network/fileSystem 字段,request-user-input 映射到 `ctx.userQuestions`。缺少交互 provider 时安全拒绝。当前 DSH 没有结构化 MCP elicitation 回答服务,因此该请求返回 decline。未知 App Server 请求会失败当前 turn。 ## Token 映射 App Server 的 `inputTokens` 同时包含 cache read 与 cache write。DSH `inputTokens` 记录 `input - cacheRead - cacheWrite`,并分别保存 `cacheReadTokens`、`cacheWriteTokens`、`outputTokens`、`reasoningTokens` 与原生 `totalTokens`。ChatGPT 配额、重置时间和费用不属于 turn token usage,不进入该映射。 ## 发布边界 发布包只包含编译入口、类型、Bundle patch 与许可证说明。声明式 preset 位于 patch 中,不再发布 preset asset。`upstream/`、`compat/generated/`、测试、缓存、Session、日志与认证文件均不在 npm `files` 清单中。运行时代码没有 submodule 路径依赖。 日常 `pnpm dev` 测试并打包插件,将 tgz 安装到同时隔离 DSH home 与 Electron userData 的 Desktop Profile,然后启动 Electron。`package:desktop*` 采用 external 模式:先通过 packaged Desktop command 安装 tgz 并启动 packaged Host/frontend 冒烟,再把上游 Electron 制品与精确插件 tgz 配对到 `dist/desktop/`,并写入带 SHA-256、版本、target/arch、签名状态和冒烟记录的 manifest;任一侧缺失都会失败。unsigned Windows 测试输出隔离在 `dist/desktop/unsigned/win-x64`。