Skip to main content
Glama
SgfKrc

dsh-codex-bridge

by SgfKrc

dsh-subagent-bridge

DSH headless / ACP harness 发布成 Codex 可调用的 MCP 工具 —— 零依赖的 stdio MCP server。

位置:本仓库在工作区里的目录名是 tools/dsh-codex-bridge/(Git 远端 SgfKrc/dsh-codex-bridge), 而仓库内自称 dsh-subagent-bridge —— 两者指同一个东西。下文示例里的路径一律写实际目录名

仓库沿革:本仓库原名 dsh-codex-bridge,早期内容是 Reasonix worker 桥接器的 vendor 副本(含 DSH 装配模板)。自 v0.2.0 起改为本桥接器:不再 vendor 任何 Reasonix 代码,接入的 harness 也从 Reasonix 换成了 DSH 本身。旧的 vendor 内容与 VENDOR.json 已移除。Git 远端仍为 SgfKrc/dsh-codex-bridge

为什么要它

它补上了对比实验里缺的那一环。Reasonix 通道让 Codex 能通过 MCP 调用 Reasonix 子智能体; 但 DSH 侧只有一个 CLIdsh --profile headless),Codex 无法直接调用。本桥接器把那个 CLI 包成 MCP 工具,于是 Codex 可以这样对比:

GPT(Codex,主控)
   ├─ 子 agent harness = DSH       → mcp__dsh_subagent__dsh_run
   └─ 子 agent harness = Reasonix  → reasonix_local / reasonix_write

两条链路的模型可对齐到同一个 deepseek 模型,因此产出的差异可归因于 harness 本身, 而不是模型。

Related MCP server: AgentsGate

工具面

刻意只有两个工具 —— 这一层的职责是"跑一个任务并拿回结果",不是复刻 Reasonix 通道的 全套编排能力:

工具

作用

dsh_run

跑一个任务,返回子 agent 的最终答复;支持 per-call(默认)与 acp 两种传输

dsh_status

报告配置与限额,不调用模型

dsh_run 返回结构化结果:

{
  "schema": "qlh.dsh.subagent.result.v1",
  "result": "ok",
  "elapsedMs": 4597,
  "truncated": false,
  "answer": "..."
}

两种传输:per-call 与 ACP

与 Reasonix 通道的 transport 语义对齐:默认 per-call,ACP 需逐次显式 opt-in。

per-call(默认)

acp

实现

dsh --profile headless

dsh --profile acp + ACP JSON-RPC

生命周期

一进程一任务,跑完即退

持久会话,可跨调用复用

上下文

每次全新,无记忆

同一 session_id 保留上下文

多轮追问

前置

DSH_ACP_ENABLED=true

用法:

# 第一次:不传 session_id ⇒ 新建会话,响应里返回 session_id
dsh_run({ task: "...", transport: "acp" })
  → { sessionId: "8f8be338-…", sessionCreated: true, turnsInSession: 1, answer: "..." }

# 后续:带上同一个 session_id ⇒ 复用会话,保留上下文
dsh_run({ task: "...", transport: "acp", session_id: "8f8be338-…" })
  → { sessionId: "8f8be338-…", sessionCreated: false, turnsInSession: 2, answer: "..." }

(已实测:同 id 复用、turnsInSession 递增。)

⚠️ ACP 路由必须打 patch

dsh --profile acp 自身dsh-acp 的 provider/model 钉死为 deepseek-official / deepseek-v4-flash,这会绕过 settings.yamlagent-default-model 的选择。在走 DEEPSEEK_API_KEY + 自定义 baseURL 的本机上, 后果是 session/prompt 直接返回:

Internal error: turn failed: Authentication Fails, Your api key: ****c328 is invalid

注意 headless 不受影响(它读 settings 层的 deepseek 路由,实测正常)。 两边因此会落在不同网关上 —— 这会让 harness 对比失去意义。

修复方式是给 ACP 打一个按 entry id 覆盖的路由 patch(本仓库自带 acp-route.patch.yml):

- id: acp
  config:
    provider: deepseek
    model: deepseek-v4.1-flash

然后在环境变量里挂上它:

DSH_ACP_ENABLED = "true"
DSH_ACP_PATCH = "<workspace-root>/tools/dsh-codex-bridge/acp-route.patch.yml"

如果你换了网关或 model id,记得同步改这个 patch,否则 ACP 会重新掉回 deepseek-official 并鉴权失败。

环境变量

变量

作用

默认

DSH_BIN

dsh 入口(.../@deepseek-ai/dsh/lib/bin.js

自动探测:DSH_HOMENPM_CONFIG_PREFIXAPPDATA/LOCALAPPDATA 下的全局 npm → PATH 上的 dsh/dsh.cmd/dsh.ps1

DSH_MIN_VERSION

最低可接受的 dsh 版本;低于它拒绝启动

0.1.5

DSH_PROFILE

要启动的 profile

headless

DSH_WORKSPACE_ROOT

允许的工作区根;cwd 不得逃出

进程 cwd

DSH_SUBAGENT_PROVIDER / DSH_SUBAGENT_MODEL

覆盖子 agent 模型路由(仅报告用;实际路由由 profile 决定)

profile 自身默认

DSH_ACP_ENABLED

true 时开放 transport=acp

关闭

DSH_ACP_PATCH

ACP 路由 patch 文件路径(见上节,本机必需

BRIDGE_LOG

给每次 dsh_run 追加一行 JSON 摘要

不写

headless profile 默认路由即 deepseek-official / deepseek-flash,无需额外配置。

安全与边界(fail-closed)

  • 无 shell:任务文本作为单个 argv 元素传给 node <dsh bin>shell:false, 不经过任何 shell 解释。

  • 启动期拒绝DSH_BIN 指向不存在的文件时直接退出码 2,不静默降级。

  • 调用期拒绝:空任务、超长任务(>16000 字符)、逃出工作区根的 cwd、 非正超时,都在 spawn 之前返回错误。

  • 硬上限:任务 16000 字符、输出 24000 字符、超时上限 1800s、并发上限 4。

  • 超时整树终止:Windows 上用 taskkill /t /f 杀掉整个进程树,避免留下孤儿 (已实测验证无残留)。

  • 不返回中间轨迹:只回传子 agent 的最终答复与元数据。

抗升级

入口解析与版本判定都刻意不绑定具体版本:

  • 入口:候选顺序 DSH_BINDSH_HOME/lib/bin.jsNPM_CONFIG_PREFIXAPPDATA/LOCALAPPDATA 下的全局 npm → PATH 上的 dsh/dsh.cmd/dsh.ps1。shim 一律先取同目录的 npm 布局,再从 shim 文本反解真实 lib/bin.js(识别 %dp0% / %~dp0 / $basedir 前缀,以及绝对与相对路径), 因此调用方从不需要 shell;dsh 换安装形态、换版本都不会失联。

  • 版本门:启动时读入口所属包的 package.json(只认 name === @deepseek-ai/dsh)得到实际版本, 与 DSH_MIN_VERSION(默认 0.1.5)比较。比较只看 major.minor.patch,所以 0.1.5-rc.2 这类 预发布后缀不影响判定。低于下限拒绝启动;版本读不出来(测试桩、异常布局)只警告、不阻断 —— 否则"起点不是 dsh 包"的合法场景会被误杀。

  • 两个结果都在 dsh_status.dsh 里:launcher / source / version / versionSource / minVersion / versionCheckok / below-minimum / unknown)。

⚠️ 升级 dsh 后要留意 profile 层的 provider 冲突:0.1.7-rc.1 的 dsh-base 新增了官方 llm-deepseekDeepSeek Messages adapter)entry,它会和自定义网关的 llm-pi-ai 争用同一个 provider id deepseek。争输的一方若落到官方端点,就表现为 AUTH: Authentication Fails, Your api key: ****xxxx is invalid(即使该 key 在自建网关上有效)。 在用到自定义网关的 profile 的 cordis.patch.yml 里禁用它即可:

- id: llm-deepseek
  disabled: true

已知限制

  • per-call 是一次性的:每次 dsh_run 都是全新的 DSH agent,不共享上下文、无多轮追问。 需要多轮请用 transport=acp

  • ACP 会话存活于桥接器进程内session_id 随桥接器重启而失效,也不跨机器。 ACP 会话上限 8 个(ACP_SESSION_CAP)。

  • 模型由 profile 决定DSH_SUBAGENT_* 仅在 dsh_status如实报告,本桥接器 不会去改写 profile 的 agent-default-model。要换模型请改 profile 或 ACP 路由 patch。

  • 不含权限应答:子 agent 若遇到需要批准的操作,行为由 DSH 自身策略决定。

运行

node --test          # 25 个离线用例,不联网、不调用模型
node --check src/server.mjs

注册到 Codex(~/.codex/config.toml):

[mcp_servers.dsh_subagent]
type = "stdio"
command = "node"
args = ["<workspace-root>/tools/dsh-codex-bridge/src/server.mjs"]
startup_timeout_sec = 30
tool_timeout_sec = 1800.0

[mcp_servers.dsh_subagent.env]
DSH_WORKSPACE_ROOT = "<workspace-root>"
DSH_PROFILE = "headless"

改动后需重启 Codex。

Related MCP Connectors

Related MCP Servers