Skip to content

feat(agents): add DeepSeek Harness (dsh) as a code agent runtime - #3053

Merged
nessie993 merged 3 commits into
mainfrom
feat/deepseek-harness-agent-runtime
Aug 17, 2026
Merged

feat(agents): add DeepSeek Harness (dsh) as a code agent runtime#3053
nessie993 merged 3 commits into
mainfrom
feat/deepseek-harness-agent-runtime

Conversation

@nessie993

Copy link
Copy Markdown
Contributor

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.

dsh does not. Its published CLI offers dsh web, dsh --profile <name>, and dsh plugin — nothing else. The ACP server is a separate cordis composition (@deepseek-ai/dsh-acp-demo) configured by a cordis.yml that 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:

Piece Lives at
Manifest desktop/shared/dsh/package.json + package-lock.json
Composition desktop/shared/dsh/cordis.yml
Launcher desktop/shared/dsh/dsh-acp
Install Dockerfile.ubuntu-helix (dsh-build stage)
Per-session values api/cmd/settings-sync-daemon/deepseek_harness.go

The 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/new params: { cwd: '/home/retro/work', mcpServers: [ [Object] x3 ] }
-32602 Invalid params: mcpServers is not supported

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-client loader entries (Zed's command shape → stdio, url shape → streamable-http) into a JSON file the composition pulls in via cordis-plugin-include. The model still sees mcp__<server>__<tool>.

Three details are load-bearing and commented as such:

  • The include path is a literal, not !!js process.env.… — the loader resolves an include's path without evaluating js tags, so an env reference silently arrives as "" and fails with extension "" not supported.
  • The file is written 0400 via temp+renamecordis-plugin-include writes entries back when the file is writable; read-only keeps the daemon the sole writer.
  • It is rewritten even when empty — a stale file would leak the previous agent's servers and its bearer tokens into the next session.

Residual cost: Zed's own agent panel in a dsh session has no MCP tools, since context_servers is shared state. Fixing that properly means teaching Zed not to send mcpServers to agents that can't take them — a cross-repo change.

Node 20 → 24

dsh requires ^22.19.0 || >=24.0.0 and 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-slim and 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 layer require()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-demo pulls its plugins in as peer dependencies, which npm resolves by range. Upstream cut 0.1.0-rc.7 mid-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.json now pin all 181 packages including peers, installed with npm ci.

Verification

Live, under Zed, in a real spec task (spt_01m07y52t029tjz5ra6sdcmcb9, image helix-ubuntu:870e62), driven end-to-end from the helix CLI:

  • helix api /spec-tasks/<id>/execution-config -X PATCHagent_name: "dsh"
  • daemon: Using deepseek_harness runtime: command=/usr/local/bin/dsh-acp … mcp_servers=3
  • context_servers: {} with agent_servers: ['dsh']; mcp.cordis.json (0400) with all three servers split correctly by transport
  • chrome-devtools-mcp's banner on the agent's stderr — the mounted MCP servers really launch
  • session/new succeeded (zed_thread_id b1eae838-…, no -32602)
  • the Helix proxy served the turn — two qwen3.8-27b calls, no error, ~2s each
  • the turn returned checkable facts: the branch Helix provisioned (feature/000381-dsh-cli-e2e) and the primary repo's README heading (# Helix Next), no files modified

Standalone, before Zed: ACP handshake, tool-using turns that wrote a file and ran cat (verified on disk), and an mcp__helix-session__current_session call 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_harness row in the pkg/server runtime table; AgentHarness.test.tsx for the mark. go build + yarn build pass.

What is NOT verified

  • One model, one turn. Multi-turn, stop/resume follow-ups, clear-thread, agent switching, and a turn that actually edits and commits code are all unexercised, as is any other model.
  • MCP under Zed — proven to launch in-container and to be callable in the standalone harness, but no Zed-driven turn has invoked an MCP tool.
  • Non-dsh Node consumers under Node 24 — chrome-devtools-mcp, the GitHub/Drone MCP servers, and the Claude Code / Codex ACP wrappers have been built but not run on the new interpreter.

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:

  • No streaming — only committed messages, at end of turn. Confirmed live: the UI is blank for the whole turn, which reads as a hang on anything slow. Corollary: dsh's JSONL session log is checkpointed behind the wire, so a finished turn can still show only the session event on disk — do not use it as a liveness check.
  • No tool-call updates — no visible work in Zed's thread view.
  • No session/load — fresh sessions only; no resume or fork.
  • Text-only prompts and no reasoning-effort control (a hand-declared model carries no catalogue entry; sending an effort a model rejects is a hard 400 that aborts the turn).

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

nessie and others added 3 commits August 17, 2026 15:33
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>
@nessie993
nessie993 merged commit 2fc4433 into main Aug 17, 2026
@nessie993
nessie993 deleted the feat/deepseek-harness-agent-runtime branch August 17, 2026 17:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant