设备内部有两种不同职责的适配:dock 集成层把设备执行契约接到 ACPX,codex-acp 则实现 ACP 的 Agent 端并接入原生 Codex。本文把仓库内 Driver 明确称为 dock 集成层,把依赖 @agentclientprotocol/codex-acp 称为 ACP 协议适配器。
应用只通过 client SDK 使用设备;这些集成代码属于独立 Daemon。Codex 的固定调用链与下图为:
DeviceHost → ProviderAdapter → CodexHost → CodexExecutor → ACPX → codex-acp → Codex
执行分层与协议边界#
| 名称 | 职责 | 所在位置 |
|---|---|---|
| DeviceHost | 准入、runId 去重、并发互斥、持久化与资源生命周期 | Daemon 内 |
| dock 集成层 | 选择本机安装与环境,映射配置、回调、事件和恢复证据 | packages/drivers |
| ACP | Agent Client Protocol,客户端与 Agent 端之间的通信协议 | ACPX 与 codex-acp 之间 |
| ACPX | ACP 客户端运行库,管理协议会话、执行请求与后端进程 | Daemon 内引用的依赖库 |
| codex-acp | 实现 ACP 的 Agent 端,把协议请求接入 Codex | 由 ACPX 管理的后端进程 |
| Codex | 原生模型循环、会话、工具、MCP、skills 与权限规则 | 所选设备原生环境 |
ACPX 位于 codex-acp 上层,dock 集成层位于 ACPX 上层。 packages/drivers 是源码模块的名称,不能据此把 ACPX 和 codex-acp 合成同一层。跨机器的 dock JSON-RPC/WSS 协议与这里的 ACP 协议分开;前者连接 SDK、Server 和 Daemon,后者连接设备内部的 ACP 客户端与 Agent 端。
接口与所有权#
ProviderAdapter 提供 inspect、validate、environment、execute、close 和 forgetSession。ProviderAdapters 创建具名适配器并提供只读 reconcile。
- DeviceHost 拥有设备准入、runId 去重、容量、审批归属、持久化与资源管理。
- dock 集成层选择原生环境、声明能力,处理安装依赖及协议、会话、模式、事件和结果差异。
- environment 是设备端私有的不透明持久化上下文,不是远程指定原生路径的入口。
- reconcile 按原 runId 只读查回;不支持或无完成证据时返回 null,不派发 prompt 或建立替代会话。
公共 wire 类型位于 protocol,并由 client 导出。dock 集成层不依赖调用方业务类型。原生交互类型与公共类型的兼容性由编译 fixture 校验。
Codex 环境与执行#
使用所选本机安装及 home,dock 状态独立放在 stateDir。保留原生配置、认证、工具、插件、skills 和 memory;调用方内容只使用明确支持的增量入口。
本机 Provider 环境只接受具名代理/证书变量,不继承整个父进程环境,不接受远程命令、home 或权限变量。原生宿主不可用时报告实际限制,不绕过身份、签名或进程链检查。
Codex 执行后端固定走 ACPX → codex-acp → Codex 路径,版本见 依赖清单。不添加异常时自动切换的并行执行后端。
从代码读起#
| 入口 | 实现位置 | 读什么 |
|---|---|---|
| DeviceHost.execute | apps/daemon/src/device-host.ts | 准入、持久化、审批归属与执行结果 |
| ProviderAdapter / CodexHost | packages/drivers/src/provider-adapter.ts、codex-host.ts | 原生环境、配置校验、MCP Bridge 与会话生命周期 |
| CodexExecutor | packages/drivers/src/codex.ts | ensureSession、配置、startTurn、事件解码与只读 read_turn |
| createCodexRuntime | packages/drivers/src/codex.ts | 调用 ACPX createAcpRuntime,并在 agentRegistry 指定 codex-acp 启动入口 |
ACPX 与 codex-acp 是固定依赖;dock 集成层不重写 Codex 的模型循环。过程回调与结果保存顺序见 执行时序。
能力、交互和结果#
独立声明 sessionResume、resultQuery、exactFinalAnswer、callerSkills、permissionInteractions、desktopTools 和 browser。协议 ready 不代表模型或所有原生工具已执行。
原生审批从 Codex 经 codex-acp、ACPX 和 dock 集成层回到 Daemon,再经 Server 回调应用;有效决定沿原路径返回。应用声明的工具由本机 MCP Bridge 暴露给 Codex,执行时回调应用;原生工具仍由 Codex 管理。审批保留精确 optionId、form/url、作用域与有效期。投递确认只报告原生协议 ACK。工具结果保留调用方 deliveryContext,未确认投递为 unknown。
工具事实优先复用标准 ACP 事件;dock 集成层提供有界展示投影,权威消息完成和终态不依赖展示截断。原生 thread/turn 身份、完整消息完成和只读查询采用具名协议接缝,见 补丁说明。
验证要求#
确定性测试覆盖身份、审批、取消、迟到回答、状态未知、恢复及进程清理边界;真实 Provider 验证使用隔离身份和合成内容,分别记录结果。
原生安装、配置、登录和用户文件不是清理对象。模拟后端只证明控制边界,不能据此新增对外 Provider 支持。验证方法见 验证与验收。
dsh(DeepSeek Harness)实验接入#
DeviceHost → ProviderAdapter → DshHost → ACPX → dsh --profile acp。已发布开发者 SDK sdk-v0.1.2 的配套 macOS arm64 Daemon/CLI 与 App v0.1.3 均包含此实验路径,dsh 保持实验性。
Daemon 经 ACPX 的具名 dsh registry 启动已选安装的 dsh --profile acp,无需 ACPX 内置快捷命令。设备仍限 macOS arm64,首个适配版本为 @deepseek-ai/dsh 0.1.2-rc.1。不安装另一套 dsh,不修改用户登录或 patch;原生 CLI 自己管理 profile 的初始化。
Daemon 发现 PATH / nvm 中的 dsh,home 使用 DSH_HOME 或默认 ~/.dsh。有多个安装时,设备所有者在私有本地配置中设置 providers.dsh.executable 和 providers.dsh.home;可用 providers.dsh.disabled 禁用。显式代理和证书仍放该 Provider 的本地 environment 配置。
应用仍只使用连接 SDK:从 inventory 选择 key 为 dsh 且 ready 的 Provider,模型标识从其 models 数组原样传入 execute;模型值为原生不透明值,不能自行拼接裸模型名。配置仅接收已报告的 effort。调用方 skills 通过独立命名 root 叠加,保留原生默认目录与配置。
审批转发原生 options,应用返回其中一个 { optionId };笼统 allow 不会升级为原生许可。支持 session/resume,不依赖 dsh 未实现的 session/load。dsh 的 ACP 文本为已提交消息,但没有 Codex 的 final_answer phase,因此 exactFinalAnswer: false;无只读原生结果查询,因此 resultQuery: unsupported。completed 只表示原生 prompt 正常结束,业务验收仍由调用方负责。连接恢复和 Daemon 重启不会重派;缺终止证据仍为 unknown。
App v0.1.3 首页展示已检测到的 Harness 数量和安装列表、服务连接及“设置与维护”入口,不常驻展示任务统计;点击 Harness 条目查看安装信息、固定回显、诊断与分环境历史,正常回到调用方应用。日志和版本信息收纳在可返回首页的“设置与维护”独立页面。安装发现不等于模型就绪。Codex 使用原生确认的只读模式;dsh 实验测试仅核对固定回显,沿用原生权限配置,不提供只读保证,出现工具活动或额外审批不会判定通过。App 与 SDK 的 dsh 适配均保持实验性,具体工具与业务用途需分别验收。维护者运行 pnpm accept:dsh 可通过本机隔离的 SDK、Server、DeviceHost 与原生 dsh 验收 MCP 成功/错误、送达、调用方和原生项目 skills 共存、审批允许/拒绝/取消后旧决定失效及一次性授权作用域。脚本使用模型额度,实际输入、回调、原生结果及源码摘要留在 ignored .local/dsh-acceptance-*。MCP 错误响应在原生投影为 failed 时,送达状态保守保持 unknown。
审批 fixture 只为其原生子进程选择 read-only,以验证最窄的一次性 workspace-write 授权;不修改用户 settings 或已安装 Daemon。必须实际观察原生只读拒绝与审批回调,未触发审批不能计通过。本机合成验收不替代独立 Daemon 进程、独立设备、发行包或业务验收。App、SDK 发行物与源码 fixture 的验收范围分别记录,不能相互替代。
本地配置与 SDK 示例
自动发现不明确时,设备所有者可在私有设备配置中加入以下片段,替换示例路径;不传给远端 execute,也不修改原生认证:
{"providers":{"dsh":{"executable":"/absolute/path/to/dsh","home":"/absolute/path/to/native-dsh-home"}}}SDK 延续本页 SDK 接入契约;以下示例仅发送合成回显。真实审批 UI 应展示原生 options,并返回用户选定的 optionId。
import { connectDock } from '@harness-dock/client';
import { randomUUID } from 'node:crypto';
const dock = await connectDock('.local/client.json');
try {
// 唯一设备可自动选择;多设备时显式选取,不默认调用列表第一台。
const nodes = dock.listNodes();
const nodeId = nodes.length === 1 ? nodes[0].nodeId : undefined;
const peer = nodeId ? dock.client.node(nodeId) : undefined;
if (!peer?.connected) throw new Error('select_one_connected_device');
const inventory = await peer.request('inventory');
const dsh = inventory.providers.find(p => p.key === 'dsh' && p.status === 'ready');
if (!dsh?.models.length) throw new Error('dsh_unavailable');
const response = await dock.run({
nodeId, provider: 'dsh', model: dsh.models[0],
receipt: '.local/dsh-' + randomUUID() + '.json',
prompt: 'Reply exactly DOCK_DSH_OK. Do not call tools or perform other actions.',
});
console.log(response.runId, response.result);
// run 在 prepare/execute 前保存原身份,默认拒绝原生审批。
// 业务结果由应用验收;按原 receipt 查询,不通过新建任务恢复。
} finally { await dock.close(); }