dsh-patch-edit-plus
English | 简体中文 | 日本語 | 한국어 | Français | Deutsch | Italiano | Русский | Español
Patch-style file editing for DeepSeek Harness (DSH): one model-facing apply_patch tool that accepts git/unified diff (default) and Codex apply_patch syntax (opt-in), applies every change all-or-nothing, and stays completely out of DSH's way.
DSH version compatibility
v0.4.0 is a single release covering the whole host line: one version serves every DSH from 0.1.0-rc.2 to 0.2.1-rc.5 (all 20 rc releases plus desktop). The four retired lines (compat/0.1.2, compat/0.1.5, compat/0.1.7, and the main split-line scheme) are gone; after publishing, the npm dist-tags dsh-0.2.0, dsh-0.1.7, dsh-0.1.5 and dsh-0.1.2 all point to 0.4.0.
| DSH version | Status | Notes |
|---|
0.2.0-rc.1+ | ✅ supported (same v0.4.0) | Declarative settings: fields marked .volatile() become the settings form, and loader/volatile-update re-registers the tool in place without a remount. The plugin-page configuration card (client half) has a seat on this line. |
0.1.0-rc.2 ~ 0.1.7-rc.2 | ✅ supported (same v0.4.0) | Settings per generation: instance settings.installSection/register (0.1.2/0.1.5) or the module-level installSettingsSection of @deepseek-ai/dsh-settings (0.1.0/0.1.1). The plugin-page configuration card is gracefully absent here. |
Behavior change: allowCodexPatch now defaults to false (opt-in, aligned with the 0.2.0 line). If you upgrade from the old 0.1.7 line (where the default was true), enable the switch manually after upgrading.
Settings span three host generations, all served by this single version: on 0.1.7+ (and desktop) the config is declarative — fields marked .volatile() in Config are the settings form, and the plugin subscribes to loader/volatile-update; on 0.1.2/0.1.5 it uses the instance settings.installSection/register APIs; on 0.1.0/0.1.1 it uses the module-level installSettingsSection of @deepseek-ai/dsh-settings. The plugin-page configuration card (client half) only has a seat on 0.1.7+/desktop and is gracefully absent on older lines.
Tool capabilities are identical on every line: one apply_patch tool, git/unified diff by default plus Codex opt-in, all-or-nothing application, the read-before-write intent dance (on host lines whose fs lacks write-intent, writes degrade to a direct write with a single console.warn), and sandbox-aware delete/move.
Why patch at all? (tool routing guide)
The tool description explicitly routes the model:
- Single small change in one already-read file → prefer the native
edit tool (a patch costs more tokens than a literal edit).
- Small change inside a LARGE file →
apply_patch (no need to echo the whole file).
- Batched changes across MANY files →
apply_patch (one call, verified atomically).
- New file with substantial content / delete / rename →
apply_patch.
Installation
dsh plugin --profile web add <path-to-dsh-patch-edit-plus>
dsh web --dump-config # verify the plugin row appears
# restart DSH
Usage
Unified diff (default, enabled)
--- a/src/app.ts
+++ b/src/app.ts
@@ -10,4 +10,4 @@ export function main() {
init()
- start(oldPort)
+ start(newPort)
await shutdown()
}
Codex apply_patch syntax (opt-in)
*** Begin Patch
*** Update File: src/app.ts
@@
init()
- start(oldPort)
+ start(newPort)
*** Add File: docs/notes.md
+# Notes
*** Delete File: tmp/junk.txt
*** Move to: src/app.ts (via `*** Move to:` after *** Update File:)
*** End Patch
Enable with allowCodexPatch: true. When a Codex patch arrives while disabled, the tool returns an actionable hint (how to enable or how to retry in unified diff) instead of a generic parse error — Codex syntax is a strong prior for GPT-family models and a silent failure traps them in retry loops.
Operations
| Operation | Unified diff | Codex | Execution path |
|---|
| Add | new file mode + /dev/null | *** Add File: | ctx.fs.writeText (official intent dance) |
| Update (multi-hunk) | @@ hunks | @@ hunks | ctx.fs.writeText |
| Delete | deleted file mode + /dev/null | *** Delete File: | ctx.shell (sandbox-aware, env-passed path) |
| Move / rename | rename from/to | *** Move to: | ctx.shell |
Guarantees
- All-or-nothing: the whole patch is verified against current file content first (context location, workspace containment, symlink policy, duplicate paths); any failure aborts with zero bytes written.
- Read-before-write gate respected: every write replicates the official
write tool's intent dance — fs/write-intent waterfall → guarded writeText → fs/observed emit. A bare writeText would silently bypass the gate because DSH providers never dispatch fs/* events themselves.
- Precise failure diagnostics: on hunk mismatch the error carries file + hunk index, search origin line, expected-line preview (whitespace made visible), the file's actual excerpt, and a targeted hint.
- Newline fidelity: CRLF files are written back as CRLF; no-trailing-newline tails are preserved unless the patch changes the tail.
- Pure addition: the plugin registers exactly one tool, never calls
tools.restrict(), never overrides a native tool, registers no global services, and removes everything on unload. Name conflicts with other apply_patch providers (bainianlaoyao/dsh-codex-mode, shuind/dsh-codex-harness, …) are resolved by rename avoidance (apply_patch_1, …_2, …) so DSH never fails to start.
Configuration
| Option | Default | Description |
|---|
toolName | apply_patch | Model-facing tool name. |
conflictPolicy | rename | rename / skip / fail when the tool name is taken. |
renameSuffix | _1 | Suffix used by rename avoidance. |
allowUnifiedDiff | true | Accept git/unified diffs. |
allowCodexPatch | false | Accept Codex apply_patch syntax. Also editable in the DSH settings panel; changes take effect immediately, no restart needed. |
deleteBackend | shell | shell or none (Delete/Move return a structured error). |
shellDialect | auto | auto (pwsh on win32) / posix / pwsh. |
deleteCommand / moveCommand | built-in | Custom command templates. Paths still arrive only via env (DSH_PATCH_TARGET / DSH_PATCH_SOURCE); never interpolate them into the command string. |
dryRunByDefault | false | Treat calls as dry runs unless dryRun: false. |
followSymlinks | false | Allow patch paths to traverse symlinks. |
maxFiles | 50 | Max file sections per patch. |
maxPatchBytes | 524288 | Max patch text size in bytes. |
maxDiffBytes | 16384 | Max per-file diff kept in the result metadata (larger diffs are emptied and marked truncated). |
Limitations (disclosed)
- Delete/Move run through
ctx.shell. The sandbox strength is whatever the loaded shell executor enforces (bash-sandbox fences; bash-local does not) — the same risk posture as the native bash tool. Every request carries a sandboxPolicy and reports sandbox facts so "policy denied" is distinguishable from "command failed".
- Every write and every Delete/Move carries the session-scoped policy.
apply_patch resolves ctx.sandboxPolicy.resolve({ session }) per call — the session's mode override plus its cwd as the workspace root — exactly like the native write/edit tools, and resolves plan paths against that same root. Without it the enforcing filesystem falls back to the deployment root, which makes in-workspace writes fail as workspace-write denials even in a danger-full-access session. A workspace-write denial surfaces as a PatchError whose message carries the backend text (the structured [sandbox: …] marker and the same-turn escalation fields are not implemented; use the native write/edit tools when you need to escalate).
- Add does not create parent directories. This matches the native
write tool (ctx.fs has no mkdir); the error names the missing directory.
- No fuzzy/offset matching: hunk location is exact →
trimEnd → trim, deliberately (fuzzy matching is unsafe for destructive ops; it is on the roadmap).
- Binary patches are rejected with a clear error.
Development
npm install
npm run typecheck # against 0.2.1-rc.1 peers (devDependencies)
npm run typecheck:0.2.1 # against real 0.2.1-rc.1 peer packages installed from the registry (dual-baseline proof)
npm test # vitest, 92 tests
npm run lint
npm run build # lib/
npm run verify:source # static safety assertions (intent dance, no node:fs, …)
npm run smoke # load-level smoke against a stub host
Manual verification on a live DSH (dsh plugin --profile web add → restart → call the tool in a session) is intentionally left to the operator.
License
MIT