feat(agents): add DeepSeek Harness (dsh) as a code agent runtime - #3053
Merged
Conversation
Adds `deepseek_harness` (`dsh`) alongside claude_code / codex_cli / qwen_code / goose_code / opencode / zed_agent, with its official mark and label in the org Providers → Coding harnesses policy surface. Unlike every other harness we ship, upstream's product CLI has no acp subcommand: the ACP server is a separate cordis composition (@deepseek-ai/dsh-acp-demo) configured by a cordis.yml naming each plugin the agent gets. So the integration owns desktop/shared/dsh/cordis.yml and a dsh-acp wrapper, and settings-sync-daemon supplies only the per-session values that composition resolves from the environment. dsh requires Node >= 22.19 and the desktop image ships Node 20 for qwen-code and the MCP servers, so it gets a private Node 24 under /opt/helix/dsh rather than moving every other npm consumer. Verified live against the shipped artifacts: the dsh-build stage was built and a real ACP turn driven over stdio inside it — initialize, session/new, and a session/prompt in which the agent used its own filesystem tool and returned the file's contents. NOT tested under Zed, and the Helix proxy hop is unexercised because every API key on this dev stack fails the same org billing check via plain curl. Both gaps, and the upstream server's automation-only limits (no streaming, no tool-call updates, no session/load), are recorded in the design doc. Design doc: design/2026-08-17-deepseek-harness-agent-runtime.md
Zed puts the project's context servers in session/new.mcpServers, and DeepSeek Harness's ACP transport rejects a non-empty list outright with -32602 "mcpServers is not supported". Session creation failed, Zed surfaced nothing, and the task hung with no visible error. Upstream documents this; every pre-Zed probe passed an empty list and missed it. Zed has no per-agent filter and stdio MCP has no ACP capability bit, so contextServersForZed() withholds the servers from settings.json for this runtime alone. The capability moves rather than disappearing: the daemon renders the same set as dsh-mcp-client loader entries and the composition pulls them in via cordis-plugin-include, so the model still sees mcp__<server>__<tool>. The include path is a literal on both sides because the loader resolves it without evaluating js tags; the file is written 0400 via temp+rename so cordis never writes back and no partial read is possible; and it is rewritten even when empty so an agent switch cannot leak the previous session's servers and bearer tokens. Also bumps the image to Node 24 (Node 20 is out of maintenance and below dsh's floor), which removes the private interpreter dsh carried, and replaces version-string pinning with a committed package-lock.json — naming exact versions did not pin the peers npm resolves by range, so upstream cutting rc.7 mid-build broke a previously-green build. Verified live: with a real helix-session MCP server mounted through the include file, the agent called mcp__helix-session__current_session and returned the live session id. NOT yet verified: a green turn under Zed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Task spt_01m07y52t029tjz5ra6sdcmcb9 on helix-ubuntu:870e62, driven from the helix CLI: session/new succeeded with no -32602, the Helix proxy served two qwen3.8-27b calls, and the turn returned the provisioned branch name and the primary repo's README heading with no files changed. Narrows the NOT-verified list to what is genuinely untested (multi-turn, thread lifecycle, code-editing turns, MCP tool use under Zed) and records two debugging traps observed live: the UI is blank for the whole turn because dsh commits rather than streams, and its JSONL session log lags the wire so it cannot be used as a liveness check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
deepseek_harness(dsh) alongside claude_code / codex_cli / qwen_code / goose_code / opencode / zed_agent as a selectable coding harness, with DeepSeek's official mark and label in the org Providers → Coding harnesses policy surface.Upstream: https://github.com/deepseek-ai/deepseek-harness. Structurally this follows the opencode change (#3018), with two differences that made it materially bigger.
Why this isn't just "another ACP subcommand"
Every other harness we ship exposes ACP on its product CLI:
codex acp,goose acp,opencode acp,qwen --experimental-acp.dshdoes not. Its published CLI offersdsh web,dsh --profile <name>, anddsh plugin— nothing else. The ACP server is a separate cordis composition (@deepseek-ai/dsh-acp-demo) configured by acordis.ymlthat names every plugin the agent gets: LLM adapter, sandbox, bash executor, filesystem, approval policy, compaction, tools.So this integration owns a composition file, not just a command line:
desktop/shared/dsh/package.json+package-lock.jsondesktop/shared/dsh/cordis.ymldesktop/shared/dsh/dsh-acpDockerfile.ubuntu-helix(dsh-buildstage)api/cmd/settings-sync-daemon/deepseek_harness.goThe composition is static and env-driven, so a config change is a reviewable YAML diff rather than a Go string builder — the opposite of opencode's marshalled
OPENCODE_CONFIG_CONTENT, justified by dsh's config surface being an order of magnitude larger.MCP servers cannot ride ACP for this harness
Zed puts the project's context servers in
session/new.mcpServers. dsh's ACP transport rejects a non-empty list outright:Session creation fails, Zed surfaces nothing, and the task hangs with no visible error. This is documented upstream and I still missed it, because every pre-Zed probe passed
mcpServers: []— it only appeared under a real Zed session.Zed has no per-agent filter and stdio MCP has no ACP capability bit to gate on, so
contextServersForZed()withholds the servers for this runtime alone. The capability moves rather than disappearing: the daemon renders the same set as@deepseek-ai/dsh-mcp-clientloader entries (Zed'scommandshape → stdio,urlshape → streamable-http) into a JSON file the composition pulls in viacordis-plugin-include. The model still seesmcp__<server>__<tool>.Three details are load-bearing and commented as such:
!!js process.env.…— the loader resolves an include'spathwithout evaluating js tags, so an env reference silently arrives as""and fails withextension "" not supported.0400via temp+rename —cordis-plugin-includewrites entries back when the file is writable; read-only keeps the daemon the sole writer.Residual cost: Zed's own agent panel in a dsh session has no MCP tools, since
context_serversis shared state. Fixing that properly means teaching Zed not to sendmcpServersto agents that can't take them — a cross-repo change.Node 20 → 24
dsh requires
^22.19.0 || >=24.0.0and the image shipped Node 20, itself out of maintenance. Rather than have dsh carry a private interpreter, the shared Node moves to the 24 LTS line; every Node consumer moves with it.Qwen Code is still built against
node:20-slimand only runs on the image interpreter, so I loaded each of its three bundled native modules under Node 24 first (process.dlopen, MODULE_VERSION 137) — all N-API, all fine.node-pty(a hard dep of the bash executor) has no Node 24 prebuild and is compiled in the build stage; the runtime layerrequire()s it as a build gate, so a future Node major drift fails the build instead of shipping a broken harness.Pinning is a lockfile, not a version string
Exact versions in the Dockerfile were not enough:
dsh-acp-demopulls its plugins in as peer dependencies, which npm resolves by range. Upstream cut0.1.0-rc.7mid-development, npm satisfied the peer with rc.7 against our explicit rc.6 pins, and the install died on the conflict. The first build had only passed because it predated rc.7 and the second reused a cached layer — the pinning looked fine right up until a layer was invalidated.desktop/shared/dsh/package.json+package-lock.jsonnow pin all 181 packages including peers, installed withnpm ci.Verification
Live, under Zed, in a real spec task (
spt_01m07y52t029tjz5ra6sdcmcb9, imagehelix-ubuntu:870e62), driven end-to-end from thehelixCLI:helix api /spec-tasks/<id>/execution-config -X PATCH→agent_name: "dsh"Using deepseek_harness runtime: command=/usr/local/bin/dsh-acp … mcp_servers=3context_servers: {}withagent_servers: ['dsh'];mcp.cordis.json(0400) with all three servers split correctly by transportchrome-devtools-mcp's banner on the agent's stderr — the mounted MCP servers really launchsession/newsucceeded (zed_thread_id b1eae838-…, no-32602)qwen3.8-27bcalls, no error, ~2s eachfeature/000381-dsh-cli-e2e) and the primary repo's README heading (# Helix Next), no files modifiedStandalone, before Zed: ACP handshake, tool-using turns that wrote a file and ran
cat(verified on disk), and anmcp__helix-session__current_sessioncall returning the live session id.Automated: daemon tests for agent_servers shape, env contents, localhost rewriting, defer-without-credentials, the MCP withholding, both transports, and the stale-file overwrite; a
deepseek_harnessrow in thepkg/serverruntime table;AgentHarness.test.tsxfor the mark.go build+yarn buildpass.What is NOT verified
Upstream ACP limitations worth knowing before merge
These are properties of
@deepseek-ai/dsh-acp(self-described as automation-only), not bugs here, and they make dsh a worse Zed experience than the other harnesses:sessionevent on disk — do not use it as a liveness check.session/load— fresh sessions only; no resume or fork.Upstream is in developer preview and states there will be compatibility-breaking changes — hence the exact lockfile and the note to re-test the composition on every bump.
Design doc:
design/2026-08-17-deepseek-harness-agent-runtime.md🤖 Generated with Claude Code