dsh-loopx-plugin
loopx-project/loopxLoopX——面向长周期 Agent 的提供商中立、本地优先状态内核与控制平面:在 DeepSeek Harness 执行层之上持久化 Goal、Todo、门禁、证据、配额、恢复与交接状态;插件负责引导安装 CLI 与技能、准入有界的同会话续跑,并为精确绑定的工作循环提供本地 GoalBar。
让 Codex 通过 dsh_env、dsh_run、dsh_resume 三个 MCP 工具启动和续跑 DSH headless 会话的桥。
A DSH host plugin that runs a loopback-only Streamable HTTP MCP server inside the DeepSeek Harness process, so Codex can dispatch coding work to DSH over MCP — no shell wrapper, no per-call script.
中文说明 · MIT · zero runtime dependencies
Codex CLI
└─ MCP (Streamable HTTP + Bearer token) http://127.0.0.1:39391/mcp
└─ DSH host plugin dsh-codex-mcp (lives inside the DSH process)
└─ node <dsh install>/lib/bin.js --profile headless --json
└─ DSH reads/writes code, runs commands, streams NDJSON events
DSH is still the agent that writes the code. Codex only dispatches the task, reads back a structured result, and re-runs the acceptance command itself.


A regular DSH agent tool (ctx.tools) is visible only to DSH's own model — Codex cannot see it. To be callable from another agent, the capability has to leave the process. This plugin therefore:
127.0.0.1 when the DSH host loads it, and closes it when the plugin unloads (ctx.effect returns a disposer);@deepseek-ai/* package to resolve inside a profile;@deepseek-ai/dsh's lib/bin.js next to resources/app, or wherever DSH_APP_ROOT / dshCli points.dsh_env, dsh_run, dsh_resume. New sessions and follow-ups both carry the session id back to the caller.session, cwd, exit, turn_end, per-turn token usage, step count, duration; failures also return the stderr tail and a classified reason.turn_end was not completed, no final event arrived, or the final answer is empty. DSH's own "I'm done" is never taken at face value by the plugin.taskkill /T /F on Windows, process group elsewhere), so no orphaned agents.maxConcurrentRuns, default 4) — extra calls fail fast instead of queueing silently.32 random bytes, 0600), compared with sha256 + timingSafeEqual, path $DSH_HOME/codex-mcp.token by default.locale/en.json, locale/zh.json) so the calling model reads instructions in its own language.| DSH | Desktop or CLI install whose @deepseek-ai/dsh supports --profile headless --json |
| Node | `^22.19.0 |
| DSH credentials | a model API key already stored in your ~/.dsh profile (the GUI Models page or the credentials service) |
| DSH profile | a headless profile; the desktop profile rejects --json and is managed by the desktop app |
# from GitHub (recommended)
dsh plugin --profile desktop add github:GEB-lab/dsh-codex-mcp
# from a release tarball
dsh plugin --profile desktop add https://github.com/GEB-lab/dsh-codex-mcp/releases/latest/download/dsh-codex-mcp.tgz
# local checkout, for development (changes apply after a host restart)
dsh plugin --profile desktop add link:/absolute/path/to/dsh-codex-mcp
dsh plugin is the DSH plugin command: it forwards to the profile's package manager, then selects the new bundle in dsh.profile.bundles. If HMR is on, the plugin loads immediately; otherwise restart DSH.
Check that it came up:
curl -i http://127.0.0.1:39391/mcp -X POST \
-H "authorization: Bearer $(cat ~/.dsh/codex-mcp.token)" \
-H "content-type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{}}}'
or, without curl:
node scripts/smoke.mjs # protocol + dsh_env only, spends no tokens
node scripts/smoke.mjs --task "reply SMOKE_OK" --cwd /path/to/project
The smoke script prints the exact config.toml block for your machine.
~/.codex/config.toml:
[mcp_servers.dsh_codex]
url = "http://127.0.0.1:39391/mcp"
http_headers = { Authorization = "Bearer <contents of $DSH_HOME/codex-mcp.token>" }
startup_timeout_sec = 30
tool_timeout_sec = 3600
enabled = true
codex mcp list should then show dsh_codex … enabled Bearer token.
Then, from Codex:
Use
dsh_runto have DSH, inC:\work\proj, add typed input support tosrc/sum.mjsplus threenode:testcases; only touchsrc/andtest/. After it returns, runnode --test test\*.test.mjsyourself and report the result.
dsh_env()No side effects, no tokens spent. Returns JSON: plugin{name,version,pid,startedAt}, mcp{url,token,tokenFile}, dsh{cli,version,profile,dshHome,permissionMode,node,nodeVersion}, defaults{cwd,timeoutSec}, runtime{activeRuns,maxConcurrentRuns}, and a ready-to-paste codexConfig snippet.
dsh_run(task, cwd?, timeout_sec?, include_events?)Starts a new headless session. task is sent on stdin (no quoting problems, no argv length limit). On success the text result looks like:
[DSH done] turn 1 · steps 1 · 2.9s
session: session-1a2b3c4d-...
cwd: /work/proj
exit: 0 · turn_end: completed
usage: in 469 / out 8 / cacheRead 5760 / total 6237
--- final answer ---
DSH_PLUGIN_OK
--- continue this session ---
dsh_resume({ "session_id": "session-1a2b3c4d-...", "task": "<next task>" })
dsh_resume(session_id, task, cwd?, timeout_sec?)Continues an existing session. It must run in the same cwd as the original, otherwise DSH refuses before starting (and the plugin says so).
Failure payloads carry the classification (timed out, exit code N, turn_end was X, no final answer, empty final answer), the stderr tail, event counts, and a next-step hint for the usual causes (missing credentials, wrong profile, unknown session, cwd mismatch, unauthorized).
All keys live under this entry's config in cordis.patch.yml (bundle default) or in ~/.dsh/profiles/<profile>/cordis.patch.yml (your override):
- id: dsh-codex-mcp
name: dsh-codex-mcp
config:
port: 39400
defaultCwd: 'D:\code'
| key | default | meaning |
|---|---|---|
host | 127.0.0.1 | bind address; non-loopback is refused unless allowNonLoopback: true |
port | 39391 | 0 picks a free port (dsh_env reports the real one) |
path | /mcp | endpoint path |
tokenFile | $DSH_HOME/codex-mcp.token | Bearer token; created when missing |
regenerateToken | false | rotate the token on next start |
dshHome | $DSH_HOME or ~/.dsh | child DSH_HOME |
profile | headless | child DSH profile |
permissionMode | danger-full-access | child DSH_PERMISSION_MODE |
defaultCwd | host cwd | used when a call omits cwd |
defaultTimeoutSec | 3600 | per-call timeout |
maxConcurrentRuns | 4 | parallel headless tasks |
dshCli | auto-detected | explicit path to …/@deepseek-ai/dsh/lib/bin.js |
nodePath | auto-detected | explicit Node executable |
locale | zh | en or zh for MCP tool text |
includeEvents | false | append the raw NDJSON event tail to results |
~/, relative paths and $DSH_HOME are expanded; unknown keys are ignored.
127.0.0.1 by default. Requests whose peer is not loopback are rejected even if you widen the bind, unless you opt in with allowNonLoopback: true.401 with WWW-Authenticate: Bearer realm="dsh-codex-mcp" otherwise. Token comparison is constant-time over SHA-256 digests.-32601.DSH_PERMISSION_MODE=danger-full-access because headless DSH is unattended — it really can edit files and run commands with your user's rights. Always pass a cwd inside the project you mean to change, and keep the token out of repositories and screenshots.0600; rotate with regenerateToken: true (then update Codex's config).| symptom | cause / fix |
|---|---|
codex mcp list shows the server but calls fail | nothing listening, or token mismatch. Run node scripts/smoke.mjs; a 401 means copy the token again |
EADDRINUSE | another process owns the port — change port |
MISSING_CREDENTIAL: … no API key for provider route | the headless profile cannot see your key; enable the credentials/authorization providers in ~/.dsh/profiles/headless/cordis.patch.yml |
unknown option '--json' | the child ran a non-headless profile; keep profile: headless |
session … does not exist / cwd mismatch | resume must use the original cwd; otherwise start a new dsh_run |
| plugin edits do not take effect | the host already loaded the module — restart DSH (config changes behave the same way) |
| agent cannot be found | set dshCli explicitly to <dsh install>/node_modules/@deepseek-ai/dsh/lib/bin.js |
npm test # node --test test/*.test.mjs — 19 cases, no network, uses a fake dsh CLI
node scripts/smoke.mjs --url http://127.0.0.1:39391/mcp --task "reply SMOKE_OK"
node index.js # standalone server, no DSH host needed
The test suite covers auth (401 / wrong token), session headers, batching, notifications, unknown methods and paths, SSE keep-alive, DELETE, tools/list, dsh_env contents, a successful run, malformed event lines, resume argument passing, non-zero exit with diagnostics, empty answers, turn_end other than completed, and timeout kill.
dsh_run call returns when the task finishes (bounded by timeout_sec).MIT
一句话:在 DSH 宿主进程里起一个只监听回环地址的 Streamable HTTP MCP 服务,让 Codex 通过 MCP 把开发任务派给 DSH headless agent 执行,不再需要 PowerShell 包装器。
为什么做成插件:DSH 的普通 agent 工具只对 DSH 自己的模型可见,Codex 看不到。要让另一个 agent 调用,能力必须暴露到进程外——所以插件在宿主里起 HTTP MCP 端点,卸载时由 ctx.effect 注册的清理逻辑关掉。
安装:
dsh plugin --profile desktop add github:GEB-lab/dsh-codex-mcp
Codex 侧(~/.codex/config.toml):
[mcp_servers.dsh_codex]
url = "http://127.0.0.1:39391/mcp"
http_headers = { Authorization = "Bearer <token 文件内容>" }
tool_timeout_sec = 3600
enabled = true
token 默认在 ~/.dsh/codex-mcp.token(首次启动自动生成,权限 0600)。
三个工具:
dsh_env() — 查 dsh 版本、CLI 路径、profile、权限模式、token 位置,并给出可直接粘贴的 Codex 配置片段;dsh_run(task, cwd?, timeout_sec?) — 开新会话,任务走 stdin,回传最终答案 + session + cwd + 退出码 + turn_end + token 用量;dsh_resume(session_id, task, cwd?, timeout_sec?) — 同一 cwd 续跑上一个会话。失败判据(插件内建,调用方不必自己判断):超时 / 非零退出 / turn_end 不是 completed / 没有 final 事件 / 最终答案为空 —— 任一命中都返回 MCP 错误,并附 stderr 尾部与下一步提示。超时会 taskkill /T /F 杀掉整棵进程树。
安全:只绑 127.0.0.1,Bearer token(sha256 + 定时安全比较),请求体上限 4 MiB;子进程以 danger-full-access 运行(headless 无人值守必须如此),所以派任务时把 cwd 限定在项目目录内,别把 token 提交进仓库。
自测:npm test(19 个用例,用假 dsh CLI,不联网)、node scripts/smoke.mjs(像 Codex 一样连端点跑一轮)。
局限:单实例;改配置/改代码需重启宿主生效;不给调用方推送流式进度(调用在任务结束时返回)。