DeepSeek Harness 插件:dsh-plugin-subagent-bridge

前言

DeepSeek Harness(DSH)默认不支持把任务派给另一个智能体并等结果返回。如果想让 Codex 把 DSH 当 subagent 调用,或者反过来,需要自己写一个桥接层。dsh-plugin-subagent-bridge 插供了一个本机回环 MCP 端点,让 Codex 能通过标准 MCP 调用 DSH 的会话,DSH 也能调用 Codex。插件运行在 DSH 宿主进程内,直接使用公开的 sessionController 服务,不需要读取凭证文件或伪造 cookie。

工具

插件在 127.0.0.1:47821 监听,提供以下工具:

  • dsh_task:同步派活。开会话、投递、阻塞到这一轮结束,只返回最终回答(最后一条 assistant 消息)。
  • dsh_spawn:异步派活。立刻返回 task_name + sessionId,可同时开多个 worker 并行跑。
  • dsh_followup:给已有 worker 再派一轮,复用它的上下文。
  • dsh_close:关闭 worker,释放并发名额(不删除会话)。
  • dsh_list_workers:列出本桥开的 worker 及其实时状态。
  • dsh_wait:一次等多批 worker。参数 targets + timeoutMs,任一到达终态即返回 {completed} / {errored} / "running" / "not_found"。
  • dsh_dispatch:向指定会话投递一条用户消息并开始一轮,返回 requestId 与 seqAtDispatch。
  • dsh_read_result:长轮询读进度。返回该会话在 seqAtDispatch 之后的全部助手文本,用于看过程;要结果用 dsh_wait。
  • dsh_list_sessions:列出会话元数据(id、标题、工作目录、是否运行、最后活动时间)。

反向:DSH → Codex

插件同时给本部署的 Agent 注册了一个工具 codex_dispatch:

  • 参数:prompt(必填)、cwd、resume(线程 id)、sandbox。
  • 返回:{ ok, text, threadId, exitCode, durationMs, error? }。

它执行 codex exec [resume <id>] --json -o <临时文件>,从事件流取 thread.started,从 -o 文件取最终消息;工具结果里会带 [codex thread <id> · 12s],把那个 id 传给 resume 就能继续同一个 Codex 线程。

注意:resume 时 sandbox 与 cwd 不生效 —— codex exec resume 子命令本身不接受 -s / -C,线程沿用它原来的设置。

安装

git clone https://github.com/Dogtiti/dsh-plugin-subagent-bridge

Plugins → 添加 → 选该目录。

启动后插件在 127.0.0.1:47821 监听,并写入端点文件(权限 0700/0600):

{
  "url": "http://127.0.0.1:47821/mcp/<token>",
  "port": 47821,
  "token": "<token>",
  "pid": 12345,
  "startedAt": "2026-10-22T12:30:00.000Z"
}

路径:~/.dsh/bridge/endpoint.json

接到 Codex

codex mcp add dsh-bridge --url "$(node -p "require(process.env.HOME+'/.dsh/bridge/endpoint.json').url")"

验证:

codex mcp list
codex mcp get dsh-bridge

移除:

codex mcp remove dsh-bridge

配置

插件配置项如下:

键 默认 说明
enabled true 总开关;关闭后不监听、不写端点文件
port 47821 回环监听端口;固定端口是为了让 Codex 侧那条 URL 在重启后依然有效
endpointFile ~/.dsh/bridge/endpoint.json 端点与令牌的落盘位置
maxResultChars 20000 单次 dsh_read_result 返回的最大字符数(超出保留尾部)
taskTimeoutSeconds 300 dsh_task 等待一轮结束的默认上限(客户端可用 timeoutSeconds 覆盖,最大 600)
workersFile ~/.dsh/bridge/workers.json worker 注册表与重基线的落盘位置(0600)
maxHops 1 跨工具委派层数上限(1 = 只允许一层,和 Codex / DSH 的默认 maxDepth: 1 一致)
dispatchPerMinute 10 滚动窗口内允许的派发次数(spawn / task / dispatch / followup / codex_dispatch 合计)
dispatchWindowMs 60000 上述窗口长度
maxWorkersPerRoot 4 每个工作目录的 worker 上限(cwd 自动分桶,调用方不需要传 owner)
maxWorkers 12 全局兜底上限,防止多个项目一起把机器压满
workerTtlMs 86400000 记录超过这个时间没更新就不再占名额(过期自动释放,无需手动清理)
waitTimeoutMs 240000 dsh_wait 的默认超时(1s ~ 240s,客户端可用 timeoutMs 覆盖)
readWaitSeconds 25 dsh_read_result 的默认长轮询时长(上限 55,客户端可用 waitSeconds 覆盖)
codexBin codex Codex 可执行文件;含 / 时按路径严格解析,找不到就报错而不回退
codexSandbox workspace-write codex_dispatch 的默认 sandbox(resume 时无效)
codexTimeoutMs 300000 单次 codex_dispatch 超时,超时先 SIGTERM 再 SIGKILL

防环护栏

两个 agent 都接了同一个桥时,存在真实的环路:DSH worker → codex_dispatch → 那次 Codex 运行也能看到 dsh-bridge → 又 dsh_spawn → …。两边各自进程内的深度规则都管不到这条链,所以插件自带两道防护:

① 深度(maxHops,默认 1)

  • 每次都计算这次派发会产生的深度:max(当前在飞的链路深度, 调用方深度) + 1。
  • 调用方是桥建的 worker 时按其深度 1 起算。
  • codex_dispatch 运行期间会把链路标记为”在飞”,嵌套回来的调用继承该深度。
  • 超限直接报错,措辞可执行:
    codex_dispatch would create delegation depth 2, but maxHops is 1. Carry the work out directly instead of delegating again.

默认 1 的含义:Codex 可以派给 DSH,但 DSH 不能再用桥派回去 —— 与两个产品各自的 maxDepth: 1 语义对齐。

② 速率(dispatchPerMinute,默认 10 / 60s)

拦截广度型失控(一次开很多 worker)和客户端紧循环重试,与提示词无关,是纯兜底。

名额怎么算

worker 名额按工作目录分桶,调用方不用传任何 owner:

  • 每个 cwd 默认 4 个(maxWorkersPerRoot)。
  • 全局默认 12 个(maxWorkers),跨项目兜底。
  • 完成但没 dsh_close 的仍占名额(与 Codex 一致)。
  • 记录 24 小时没更新就自动不再占名额(workerTtlMs),不需要手动清理。
  • 阻塞式 dsh_task 不计入名额,只登记。

为什么不是”每个会话 4 个”:MCP 端点是共享的,请求里没有会话或线程身份(Codex 的 CODEX_THREAD_ID 只存在于 shell 环境)。cwd 是唯一由调用方稳定提供、且语义上对应”一个项目”的维度。

跨重启

worker 注册表(task_name / sessionId / 终态结果)和每轮的时间基线都落在 ~/.dsh/bridge/workers.json:

  • 派发、完成、关闭都会更新它。
  • 重启后 dsh_list_workers 仍能看到之前的 worker。
  • 已经结束的 worker,dsh_wait 直接返回缓存里的最终回答(不必重新打开会话)。
  • 重启时还在跑的 worker,会话未打开时状态是 offline(结果未知,去 DSH 侧边栏打开那个会话即可查看)。
  • dsh_followup 会给 worker 开新一轮,同时清掉缓存的旧回答。
  • 文件最多保留 200 条,超出时先淘汰最旧的已关闭 worker。

安全模型

  • 只监听 127.0.0.1,不对外暴露。
  • 请求必须携带令牌:路径 /mcp/<token>,或 Authorization: Bearer <token>;比较使用定长时间比较。
  • 令牌首次启动时生成并持久化(重启后复用),因此能力 URL 稳定。
  • 能力等价于”向指定会话投递消息”:本机任意进程只要能读到端点文件就能派活。文件权限 0600、目录 0700;不要把它复制到共享位置。
  • DSH 侧的权限策略不受影响:派发进去的消息仍按会话自身的 sandbox / 审批策略执行。

实现方式

Codex / 任意 MCP 客户端
  │  POST /mcp/<token>   JSON-RPC(stateless,单次 JSON 响应)
  ▼
dsh-plugin-subagent-bridge(宿主进程内)
  · node:http 监听 127.0.0.1:47821
  · initialize / tools/list / tools/call / ping
  · sessionController.prompt({sessionId, requestId, mode, content})
  · sessions.get(id).snapshotEvents() 读回助手文本

协议实现为 streamable HTTP 的无状态子集:每个 POST 返回一段 application/json,不做服务端推送,因此取结果用长轮询——dsh_read_result 在服务端等,客户端一次调用即可拿到答案;客户端断开时服务端立即停止等待。

配套 skill

skills/ 下有两份可直接安装的 skill,把”什么时候该派活、怎么派、怎么收尾”写成模型能读的规则:

Skill 装给谁 作用
deepseek-worker Codex 让 Codex 把任务当 subagent 派给 DSH(dsh_task),一次调用等结果
delegate-to-codex DSH 让 DSH 的 agent 在需要独立视角/复核时派给 Codex(codex_dispatch)
# Codex 侧
mkdir -p ~/.codex/skills && cp -R skills/deepseek-worker ~/.codex/skills/

# DSH 侧
mkdir -p ~/.dsh/skills && cp -R skills/delegate-to-codex ~/.dsh/skills/

两份都是标准 SKILL.md:YAML frontmatter(name + description)+ 正文。触发靠 description——模型平时只看到名字和描述,判断相关了才读正文,所以描述里写满了中英触发语和”不要用”的边界。改完立刻生效,不用重启:Codex 下一次会话、DSH 下一轮就能看到。

兼容性

  • DSH ^0.2.1-0(engines.dsh)。
  • MCP 协议版本:2025-06-18、2025-03-26、2024-11-05(按客户端请求回显,未知版本回落到最新)。
  • 依赖宿主公开服务 sessionController(list / prompt)与 sessions.get();两者不可用时工具会返回明确错误。
羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

Xiaoye