前言¶
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();兩者不可用時工具會返回明確錯誤。