dsh-plugin-codemode
yum-wu/dsh-plugin-codemode
JavaScript orchestration over DeepSeek Harness tools using Node.js VM or QuickJS WebAssembly backends.
インストール
dsh plugin --profile web add github:yum-wu/dsh-plugin-codemodeREADME
dsh-plugin-codemode
English | 中文说明
Pi-style Code Mode (Programmatic Tool Calling) for DeepSeek Harness (DSH).
Turn verbose O(N) multi-turn ReAct loops into a single O(1) JavaScript orchestration script executed in a memory-isolated sandbox — see Security Model for what that does and does not guarantee.
⚡ The Benchmark: Hard Numbers
Measured 2026-10-15 against the 17 real markdown documents in the DeepSeekHarness repository's docs/ folder (including the 282,842-char operational ledger known-issues.md) — 414,346 raw characters in total. The task: scan all documents, filter by keywords (JEV, Polymarket, Architecture), extract titles & line metrics, and return a sorted Top-5 summary JSON.
| Metric | Traditional ReAct Loop | Code Mode (Sandbox Orchestration) | Measured Improvement |
|---|---|---|---|
| Data Loaded into Context | 414,346 chars | 677 chars | 99.8% Reduction (612:1 distillation) |
| Estimated Input Tokens | ~129,483 tokens | ~212 tokens | 99.8% Saved (129k tokens spared) |
| Agent / LLM Turns | 19 round trips | 1 turn | 94.7% Fewer Turns (18 turns eliminated) |
| End-to-End Elapsed Time | ~47.5 s (at 2.5s / LLM turn) | ~2.5 s | 19× Faster (94.7% latency drop) |
| Computation Accuracy | Prone to LLM counting hallucinations | Native JS Array.filter().sort() | 100% Deterministic & Accurate |
⚠ The absolute figures are a dated snapshot, not a constant. The corpus keeps growing — it was 215,011 chars when this table was first written, and is 414,346 as of 2026-10-15. The ratios are the stable part; refresh the absolutes by re-running:
node <DeepSeekHarness>/tests/benchmark-codemode-vs-react.mjs— from the DeepSeekHarness repo root, because the corpus is that repo'sdocs/, not this plugin's (this plugin's owndocs/holds a single file).
End-to-End Elapsed Timeis modelled (19 turns × 2.5 s/turn assumed), not stopwatch-measured.
💡 Why Code Mode?
Modern LLM agents suffer from tool explosion and context pollution:
- Context Bloat: Reading 20 files fills the transcript with 200k tokens of raw data. The model loses track, forgets system instructions, or triggers expensive context compaction.
- High Latency: Every single tool call requires a full round trip of network latency and model inference.
- Calculation Flaws: Asking LLMs to count lines, sort arrays, or calculate statistics over huge text chunks often yields subtle hallucinations.
The Code Mode Paradigm
"Put deterministic things in code, non-deterministic in LLM." — Hacker News Community Consensus on Code Mode
Inspired by Pi 1.0 (Earendil)'s Code Mode architecture and Cloudflare's production agent rewrite:
- Pi.dev 1.0 Official Codemode Docs: Established the Programmatic Tool Calling paradigm via memory-isolated sandboxing and deferred tool exposure.
- Cloudflare / CamelAI Case Study: Rewrote agents from heavy VM containers to Pi Code Mode in Durable Objects, reporting an order-of-magnitude reduction in latency and token costs.
- Hacker News Discussion (#49019301): Highlighted that orchestrating multi-tool workflows via local sandboxed code delivers up to a 99.2% cost reduction compared to traditional ReAct loops.
With Code Mode, the agent writes a concise JavaScript async function. The script executes inside an isolated sandbox, concurrently calls registered host and MCP tools, filters out unnecessary data in memory, and only returns the final distilled result to the conversation context.
🏗 Architecture
┌──────────────────────────────────────────────────────────┐
│ DeepSeek Harness (DSH) │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ LLM Conversation │ │ ctx.tools / MCP │ │
│ │ (Main Context) │ │ (read, pwsh, mcp..) │ │
│ └──────────┬───────────┘ └──────────▲───────────┘ │
│ │ script │ │
│ ▼ │ invoke │
│ ┌─────────────────────────────────────────┴──────────┐ │
│ │ dsh-plugin-codemode (Cordis Extension) │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ Dual-Engine Sandbox (Isolated Context) │ │ │
│ │ │ │ │ │
│ │ │ • V8 VM Sandbox (Default): native speed, │ │ │
│ │ │ zero memory caps, unconstrained concurrency│ │ │
│ │ │ • QuickJS-WASM: WebAssembly memory sandbox │ │ │
│ │ │ │ │ │
│ │ │ - Proxy bridge: tools.<name>(args) │ │ │
│ │ │ - Supports Promise.all concurrency │ │ │
│ │ │ - Hard timeout guard (default 60s) │ │ │
│ │ │ - Recursion & leak prevention │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ └──────────────────────────┬─────────────────────────┘ │
│ │ distilled output only │
│ ▼ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Return to LLM: [Logs] + [Return Value] │ │
│ └────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
✨ Features
- 🚀 Dual-Engine Execution:
- V8 VM Engine (
engine: 'vm', Default): Aligns with DSH's official PTC workflow architecture. Zero overhead, handles multi-megabyte payloads, native microsecond performance. - QuickJS-WASM Engine (
engine: 'quickjs'): Strict WebAssembly memory isolation, zero Node/OS footprint.
- V8 VM Engine (
- 🔄 Universal Tool Proxy & Dynamic Introspection:
- Access all DSH built-in tools (
read,edit,pwsh,glob) and MCP tools (mcp__github__*,mcp__cua*,mcp__tavily__*) viatools.<tool_name>(args). - Zero-Token Tool Discovery: Introspect tools dynamically at runtime without polluting system prompts or wasting context tokens:
tools.list(): Returns the full array of registered tool names in memory.tools.help('namePattern'): Inspects parameters and JSON Schemas on-demand inside the sandbox, cutting system prompt bloat by over 90%.
- Access all DSH built-in tools (
- 🛡 Hard-Safety Protections:
- Hard timeout (default 60,000 ms). The host channel yields a macrotask and re-checks the
deadline on every tool call, so
await-driven infinite loops are terminated instead of starving the event loop. Pure synchronous loops are caught by V8's ownrunInContexttimeout. - Escape guard test suite (
test/escape.test.js, 11 cases) pins 10 knownnode:vmprototype-chain escape vectors plus a host-process probe. See Security Model. - QuickJS tool-loop guard (
test/quickjs-tool-loop.test.js, 6 cases) pins sequential cross-WASM tool orchestration. - Recursion blocker prevents scripts from invoking
codemodeinsidecodemode. - Result truncator caps output at 50,000 characters to prevent accidental prompt floods.
- Hard timeout (default 60,000 ms). The host channel yields a macrotask and re-checks the
deadline on every tool call, so
- 📦 Zero-Patch, 100% Reversible: Pure dynamic Cordis extension. Modifies zero core DSH runtime files.
🔐 Security Model
The sandbox executes model-generated code. Treat that code as untrusted — prompt injection can reach it through any tool result or file it reads.
What is enforced
| Guarantee | Engine | How |
|---|---|---|
| No host object crosses into the sandbox | vm, quickjs | The host exposes exactly one channel and it carries strings only; the sandbox-side tools proxy and its JSON.parse are built inside the context |
| Sandbox global has no host prototype | vm | vm.createContext(Object.create(null)) — a plain literal's prototype is the host Object.prototype, which handed globalThis.constructor straight to the host Function constructor |
| Tool return values cannot be an escape channel | vm, quickjs | Values are serialized on the host and re-parsed by the sandbox's own JSON |
await-driven infinite loops terminate | vm | The host channel yields a macrotask and re-checks a deadline on every call |
| Synchronous infinite loops terminate | vm | V8 runInContext({ timeout }) |
Any infinite loop terminates, including a pure await loop that never calls a tool | vm + hardIsolation: true | The sandbox runs in a worker thread, so the host's event loop stays free and worker.terminate() always fires. Without this flag the host and the sandbox share one event loop and no in-process timer can fire |
| WASM / QuickJS-level crashes cannot take down DSH | quickjs + hardIsolation: true | The crash kills the worker; the host receives a clean failure result |
| Sequential tool orchestration cannot kill the process | quickjs | Manual newPromise instead of newAsyncifiedFunction |
Output cannot exceed maxResultChars | both | The cap is applied to the whole rendered result, logs included |
What is not guaranteed
node:vmis not a security boundary. Node's own documentation states: "Thenode:vmmodule is not a security mechanism. Do not use it to run untrusted code." The hardening above closes 10 known prototype-chain vectors (pinned bytest/escape.test.js), but avmcontext still shares the host process and heap. It is a defense-in-depth measure, not a boundary. For a real boundary, run the whole DSH process inside a container or a separate OS user.hardIsolationdefaults tofalse. With it off, a script that loops onawaitwithout ever calling a tool starves the shared event loop and hangs the DSH process until it is restarted. SethardIsolation: truein the profile config if you want that class closed.- No network isolation. The sandbox has no
fetchbecause none is injected — a convenience property, not an enforced capability. - No approval gate. Every tool call the script makes executes immediately; DSH's own approval surface is not consulted by codemode.
- The
quickjsengine is slower. It is an interpreter (roughly 10–50× slower than V8 JIT per the comparison in Simon Willison's sandboxing research), and it has nosetTimeout.
Fixed vulnerabilities (2026-10-14 audit)
Three defects were found by PoC and fixed the same day. Full evidence, sources and remaining gaps:
docs/best-practice-research-2026-10-14.md.
| ID | Defect | Evidence before fix | Fix |
|---|---|---|---|
| P0-1 | Host-realm builtins injected into the vm context → constructor.constructor chain reached the host Function constructor | 7 of 10 escape vectors succeeded, all reaching the host process (envKeyCount: 80; kill/dlopen available) | Host channel reduced to strings; sandbox global is Object.create(null); console/text/tools built inside the context |
| P0-2 | The Promise.race timeout used setTimeout, a macrotask; an await loop starved the microtask queue so it never fired | Script hung indefinitely; a 15 s in-script watchdog also never fired | Host channel yields a macrotask + deadline check |
| P0-3 | newAsyncifiedFunction corrupted the QuickJS refcount across sequential suspensions | Aborted(Assertion failed: p->ref_count == 0, quickjs.c:5512) / memory access out of bounds, escaping try/catch and killing the host process (4/4 runs) | Manual newPromise + pump loop (4/4 runs pass) |
| P0-2 (residual) | A pure await loop that never calls a tool starves the shared event loop, so no in-process timer can ever fire | Script hung; a 20 s watchdog never returned | hardIsolation runs the sandbox in a worker thread; worker.terminate() is a real hard kill. Guarded by test/worker-isolation.test.js |
| P1 | maxResultChars capped only the return value; the [Logs] section was unbounded, so for(...) console.log(...) could flood the context past the advertised 50,000-char limit | Code inspection of truncator.ts | The cap is now applied to the whole rendered result |
| P2 | test/bundle-declaration.test.js silently skipped on machines that do have DSH installed — it read start-dsh.ps1 as UTF-8, but that file is UTF-16LE (FF FE), so $dshEntry never matched | Test reported skipped: 1 on a machine with the runtime present | BOM-aware read; the test now fails loudly if DSH is installed but the path cannot be derived, and only skips when start-dsh.ps1 is absent |
📖 Example Scripts
1. Parallel File Scan & Distillation
// Scan 10 files simultaneously and extract matching headers
const files = await tools.glob({ path: 'src', pattern: '**/*.ts' });
const results = await Promise.all(files.slice(0, 10).map(async (file) => {
const code = await tools.read({ file_path: file });
const exportedFns = code.match(/export (?:async )?function \w+/g) || [];
return { file, count: exportedFns.length, functions: exportedFns };
}));
// Sort by number of exports and return top 3
return results.sort((a, b) => b.count - a.count).slice(0, 3);
2. Zero-Token Dynamic Introspection & On-Demand MCP Execution
// Discover tools dynamically without polluting the prompt with 140+ tool schemas
const available = tools.list();
console.log(`Discovered ${available.length} active tools`);
// Inspect parameters on-demand only when needed
const issueHelp = tools.help('github__list_issues');
console.log(issueHelp);
// Fetch latest issues via GitHub MCP and summarize in memory
const issues = await tools.mcp__github__list_issues({ owner: 'deepseek-ai', repo: 'deepseek-harness' });
const topIssues = issues.slice(0, 3).map(i => ({
number: i.number,
title: i.title,
author: i.user?.login
}));
return topIssues;
🚀 Installation & Setup
Option A: dsh plugin add (Recommended)
dsh plugin add dsh-plugin-codemode
The package declares dsh.bundle.patch (cordis.patch.yml), so the host row is inserted
into your profile automatically — nothing to hand-edit. Restart DSH and the codemode
tool appears.
Option B: Install from npm manually
In your DSH profile directory (e.g. ~/.dsh/profiles/web):
pnpm add dsh-plugin-codemode
# or: npm install dsh-plugin-codemode
Then add the row yourself to ~/.dsh/profiles/web/cordis.patch.yml:
- insert:
- id: plugin-codemode
name: 'dsh-plugin-codemode'
config: # 全部可选;省略即用默认值
toolName: 'codemode'
engine: 'vm' # 'vm' (default) or 'quickjs'
maxResultChars: 50000
timeoutMs: 60000
injectGuidance: true
Option C: Install from Source (Development)
git clone https://github.com/Yum-wu/dsh-plugin-codemode.git
cd dsh-plugin-codemode
npm install
npm run build
Link into your DSH web profile (~/.dsh/profiles/web/package.json):
{
"dependencies": {
"dsh-plugin-codemode": "link:C:/Users/Yum/Desktop/dsh-plugin-codemode"
}
}
Then add the row from Option B.
Restart DSH
Restart the DSH service from your desktop management console. The agent will immediately receive the codemode tool declaration and execution guidance.
⚙️ Configuration Reference
| Option | Type | Default | Description |
|---|---|---|---|
toolName | string | 'codemode' | Name of the tool declared to the LLM. |
engine | 'vm' | 'quickjs' | 'vm' | Execution engine: high-throughput V8 VM or WASM sandbox. |
hardIsolation | boolean | false | Run the sandbox in a worker thread and kill it with worker.terminate(). Closes the one remaining hang class (a pure await loop that never calls a tool) and turns WASM-level crashes into clean failures. Costs one worker spawn per call. Recommended for untrusted input — see Security Model. |
maxResultChars | number | 50000 | Maximum length of distilled output returned to context. |
timeoutMs | number | 60000 | Hard deadline per script before auto-termination. |
injectGuidance | boolean | true | Injects Code Mode orchestration tips into system prompt. |
collapseTopLevelTools | boolean | false | When on, only whitelisted tools stay visible at the top level; the rest vanish from the model’s view. |
allowedTopLevelTools | string[] | DEFAULT_CORE_TOOLS (37 items) | The whitelist. ⚠ Full-replacement semantics (not additive) — see warning below. |
⚠️
allowedTopLevelToolsREPLACES the default, it does not extend it. The implementation isnew Set(config.allowedTopLevelTools || DEFAULT_CORE_TOOLS)— once you set this array,DEFAULT_CORE_TOOLSis ignored entirely. Tools you leave out disappear from the top level silently: no error, no warning, nothing in the logs. The model simply cannot see them. Measured 2026-10-05: the oldDEFAULT_CORE_TOOLShad only 11 entries while the runtime registered 36 non-MCP tools — 24 were swallowed,subagentamong them. Now completed to 37 entries with a guard test. |
🧠 Auto Reasoning Effort — moved out (2026-10-05)
This plugin no longer implements reasoning-effort selection. It now lives in a standalone plugin: dsh-auto-reasoning.
Why it moved. The old code here mounted the platform hook agent/request, read
agent/session + session/event, called ctx.llm.resolveModelInfo() and registered a
host /api route — not one line of it belonged to codemode's sandbox / tool-bridge /
truncator. The hook position was right; the plugin identity was wrong.
How the two are wired now. The declaration row for dsh-auto-reasoning is shipped by the
dsh-jev-preset bundle (same insert group, second row), so installing the JEV preset brings
auto effort with it. codemode is not involved either way.
⚠️ Remove
autoReasoning: truefrom your profile'scordis.patch.yml. The config key no longer exists, and cordis validates a whole entry at once — a leftover unknown key makes the entireplugin-codemodeentry fail to activate, which shows up as "the codemode tool vanished".
⚠️ Do not add
auto: autoto a model'sreasoningEfforts. That advice used to live in this README and it is wrong:autois not a legal effort key (the legal set isoff/minimal/low/medium/high/xhigh/max), and declaring it there makes the whole provider entry fail to activate — every model under it disappears from the picker. Verified against a real profile on 2026-10-04; a guard test now ships in DeepSeekHarness (tests/11-cordis-efforts-schema.test.mjs).
🛟 3-Level Zero-Risk Rollback Strategy
| Level | Scenario | Action | Recovery Time | Impact |
|---|---|---|---|---|
| Level 1 (Soft Disable) | Unstable model behavior | Add disabled: true to plugin-codemode in cordis.patch.yml and restart DSH. | < 15 seconds | Unregisters codemode, reverts to standard single-turn tools. |
| Level 2 (Runtime Fallback) | Script syntax or runtime error | Automatic: plugin catches errors and prompts model to fallback to native step-by-step tools. | 0 seconds (instant) | Session is never interrupted; graceful degradation. |
| Level 3 (Physical Clean) | Complete uninstall | Remove from cordis.patch.yml and package.json, delete directory. | < 15 seconds | 0 runtime traces, 0 configuration residues. |
🧪 Testing
npm test
Runs the unit suite (37 cases, node --test test/*.test.js):
sandbox.test.js(4): basic evaluation and return values,console.log/text()capture, isolation (no Node process/filesystem leaks), hard timeout on infinite loops.bridge.test.js(3):Promise.allparallel tool execution, rejection of recursivecodemodecalls, oversized-output truncation.apply-guard.test.js(2): regression guards —apply()must never callctx.waterfall(it is an emitter; doing so took the host down as fatal on 2026-10-03), andapply()no longer depends on thellm/connectionservices after auto-effort moved out.escape.test.js(11): escape guard — 10 knownnode:vmprototype-chain vectors (this.constructor.constructor,globalThis.constructor.constructor,Object.constructor,JSON.constructor.constructor,Promise.constructor.constructor,JSON.parse("{}").constructor.constructor,(await tools.read({})).constructor.constructor,(function(){}).constructor,[].constructor.constructor,String("").constructor.constructor) must all fail to reach the hostprocess, plus a host-process probe that must stay unreachable. Baseline before the 2026-10-14 fix: 3 pass / 8 fail.quickjs-tool-loop.test.js(6): QuickJS orchestration guard — 1/2/3 suspend-resume cycles, a parallel batch,seq + par + seq, and a realistic 5-call read loop must all complete without killing the process. Baseline before the fix: the file crashed the test process outright.worker-isolation.test.js(10): hard-isolation guard — tool calls, logs, parallel batches, error propagation and thetools.list()/tools.help()snapshot all work across the worker boundary; and three separate infinite-loop shapes (pureawait, synchronous,awaiton a tool) are all killed inside the watchdog.bundle-declaration.test.js(1): thedsh.bundle.patchdeclaration is parsed by the real hostbundlePatchPaths; skips only when DSH is not installed at all.
📄 License
MIT © Yum-wu