dsh-patch-edit-plus 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/CHANGELOG.zh.md +14 -0
- package/README.ja.md +3 -2
- package/README.ko.md +3 -2
- package/README.md +6 -6
- package/README.zh.md +6 -6
- package/dsh.plugin.json +1 -1
- package/lib/apply/plan.js +7 -3
- package/lib/config.js +10 -2
- package/lib/index.js +53 -22
- package/lib/policy.js +63 -0
- package/lib/tool.js +6 -2
- package/lib/types/apply/plan.d.ts +5 -1
- package/lib/types/config.d.ts +38 -3
- package/lib/types/index.d.ts +33 -9
- package/lib/types/policy.d.ts +54 -0
- package/package.json +14 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.2] — 2026-09-19
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **The settings section never registered.** The settings namespace `patch_edit_plus` contains an underscore, which `dsh-settings`' namespace pattern (`^[a-z][a-z0-9-]*$`) rejects, so `register()` threw before anything persisted — no settings entry appeared in the panel or in `~/.dsh/settings.yaml`. Renamed to `patch-edit-plus` (no migration needed: the old section could never exist).
|
|
8
|
+
- **Config changes from the settings layer never reached the tool.** The `setSource`/`onChange` hooks were no-ops and the resolved config was memoized once at load. The plugin now consumes the resolved source: every committed settings change (or legacy `register` watch) re-resolves the config and re-registers the tool — disposing the old registration first, since re-registering over a live name silently renames it. `allowCodexPatch` and every other field can now be toggled from the DSH settings panel and take effect immediately, with the tool description (the list of accepted patch styles) staying in sync; no process restart required. Detaching the settings service falls back to the composition entry, and re-judging is idempotent (the whole resolved config is compared, not just one field).
|
|
9
|
+
|
|
10
|
+
## [0.1.1] — 2026-09-17
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Every write was denied under `workspace-write`.** The tool passed no per-call `sandboxPolicy`, so the enforcing filesystem fell back to `ctx.sandboxPolicy.resolve()` with no scope — the DEPLOYMENT workspace root (the server's launch directory) instead of the session cwd. A path plainly inside the session workspace therefore failed containment and returned `file access denied under workspace-write mode`, even in a `danger-full-access` session, where the mode was not even read. The policy is now resolved per call with the calling session in scope (`resolve({ session })`) and stamped on every write and every Delete/Move shell request, exactly as the native `write`/`edit` tools do. Both halves of the mode are honored now: the session's `sandbox/mode` override and its cwd as the workspace root.
|
|
15
|
+
- Path resolution and the fence now share one root: the plan resolves every target against the policy's `workspaceRoot` (falling back to the session cwd), so the path the engine writes is the path the fence measures.
|
|
16
|
+
|
|
3
17
|
## Unreleased
|
|
4
18
|
|
|
5
19
|
### Added
|
package/CHANGELOG.zh.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
+
## [0.1.2] — 2026-09-19
|
|
4
|
+
|
|
5
|
+
### 修复
|
|
6
|
+
|
|
7
|
+
- **设置区从未注册成功。** settings 命名空间 `patch_edit_plus` 含下划线,不满足 `dsh-settings` 的命名空间校验(`^[a-z][a-z0-9-]*$`),`register()` 在校验处即抛错——面板与 `~/.dsh/settings.yaml` 中都不会出现该插件的设置段。现改名为 `patch-edit-plus`(无需迁移:旧段不可能存在过)。
|
|
8
|
+
- **设置层的配置改动从未传导到工具。** `setSource`/`onChange` 两个钩子是空实现,且解析后的配置在加载时被一次固化。插件现在真正消费解析来源:设置层的每次提交变更(或旧版 `register` 的 watch)都会重新解析配置并重新注册工具——先注销旧注册再注册,避免重名时被静默改名。`allowCodexPatch` 等全部字段现在都可以在 DSH 设置面板中修改并立即生效,工具描述(可用补丁语法列表)同步更新,无需重启进程。settings 服务卸载时回落到组合条目,且重新判定是幂等的(比对整份 resolved config,而非单个字段)。
|
|
9
|
+
|
|
10
|
+
## [0.1.1] — 2026-09-17
|
|
11
|
+
|
|
12
|
+
### 修复
|
|
13
|
+
|
|
14
|
+
- **所有写入在 `workspace-write` 下被拒绝。** 工具没有传递 per-call `sandboxPolicy`,强制执行的文件系统于是回退到无作用域的 `ctx.sandboxPolicy.resolve()`——拿到的是**部署级**工作区根(服务进程的启动目录)而非会话 cwd。因此明明位于会话工作区内的路径未通过围栏检查,返回 `file access denied under workspace-write mode`;连 `danger-full-access` 会话也一样,该模式下这个 mode 甚至根本没被读取。现在改为每次调用都带上调用会话作用域解析策略(`resolve({ session })`),并同时盖在每一次写入与每一次 Delete/Move 的 shell 请求上,与官方 `write`/`edit` 完全一致。mode 的两半都生效了:会话的 `sandbox/mode` 覆盖,以及以会话 cwd 作为工作区根。
|
|
15
|
+
- 路径解析与围栏现在共用同一个根:计划阶段以策略的 `workspaceRoot`(缺省回退到会话 cwd)解析每个目标,写入引擎实际写的路径就是围栏实际度量的路径。
|
|
16
|
+
|
|
3
17
|
## Unreleased
|
|
4
18
|
|
|
5
19
|
### 新增
|
package/README.ja.md
CHANGED
|
@@ -87,7 +87,7 @@ dsh web --dump-config # verify the plugin row appears
|
|
|
87
87
|
| `conflictPolicy` | `rename` | ツール名が使用済みの場合の `rename` / `skip` / `fail`。 |
|
|
88
88
|
| `renameSuffix` | `_1` | リネーム回避で使用される接尾辞。 |
|
|
89
89
|
| `allowUnifiedDiff` | `true` | git/unified diff を受け付けます。 |
|
|
90
|
-
| `allowCodexPatch` | `false` | Codex `apply_patch` 構文を受け付けます。 |
|
|
90
|
+
| `allowCodexPatch` | `false` | Codex `apply_patch` 構文を受け付けます。DSH 設定パネルからも変更でき、変更は即座に反映されます(再起動不要)。 |
|
|
91
91
|
| `deleteBackend` | `shell` | `shell` または `none`(削除/移動は構造化エラーを返します)。 |
|
|
92
92
|
| `shellDialect` | `auto` | `auto`(win32 では pwsh)/ `posix` / `pwsh`。 |
|
|
93
93
|
| `deleteCommand` / `moveCommand` | 組み込み | カスタムコマンドテンプレート。パスは依然として **env 経由のみ**(`DSH_PATCH_TARGET` / `DSH_PATCH_SOURCE`)で渡され、コマンド文字列に補間されることはありません。 |
|
|
@@ -100,6 +100,7 @@ dsh web --dump-config # verify the plugin row appears
|
|
|
100
100
|
## 制限事項(開示)
|
|
101
101
|
|
|
102
102
|
- **削除/移動は `ctx.shell` を通じて実行されます。** サンドボックスの強度は、ロードされたシェルエグゼキュータが強制する内容(`bash-sandbox` は境界を設け、`bash-local` は設けません)と同じであり、ネイティブ bash ツールと同じリスク態勢です。すべてのリクエストは `sandboxPolicy` を伴い、サンドボックスの事実を報告するため、「ポリシーによる拒否」と「コマンド失敗」を区別できます。
|
|
103
|
+
- **すべての書き込みとすべての削除/移動がセッションスコープのポリシーを伴います。** `apply_patch` は呼び出しごとに `ctx.sandboxPolicy.resolve({ session })` を解決し(セッションのモードオーバーライドと、ワークスペースルートとしてのセッション cwd)、ネイティブの `write`/`edit` ツールとまったく同じにし、パスの計画も同じルートに対して行います。これがないと、強制実行するファイルシステムはデプロイメントルートにフォールバックし、`danger-full-access` セッションであってもワークスペース内の書き込みが `workspace-write` 拒否として失敗します。`workspace-write` での拒否はバックエンドの文言を含む `PatchError` として表面化します(構造化された `[sandbox: …]` マーカーと同じターン内のエスカレーションフィールドは未実装です。エスカレーションが必要な場合はネイティブの `write`/`edit` を使用してください)。
|
|
103
104
|
- **追加は親ディレクトリを作成しません。** これはネイティブ `write` ツールと同じです(`ctx.fs` に mkdir はありません)。エラーには不足しているディレクトリ名が示されます。
|
|
104
105
|
- ファジー/オフセットマッチングはありません。ハンクの位置は正確一致 → `trimEnd` → `trim` で、これは意図的なものです(ファジーマッチングは破壊的操作に対して安全ではありません。ロードマップに含まれています)。
|
|
105
106
|
- バイナリパッチは明確なエラーで拒否されます。
|
|
@@ -110,7 +111,7 @@ dsh web --dump-config # verify the plugin row appears
|
|
|
110
111
|
npm install
|
|
111
112
|
npm run typecheck # against 0.1.2-rc.1 peers (devDependencies)
|
|
112
113
|
npm run typecheck:0.1.5 # against 0.1.5-rc.2 peers (static dual-version proof)
|
|
113
|
-
npm test # vitest,
|
|
114
|
+
npm test # vitest, 90 tests
|
|
114
115
|
npm run lint
|
|
115
116
|
npm run build # lib/
|
|
116
117
|
npm run verify:source # static safety assertions (intent dance, no node:fs, …)
|
package/README.ko.md
CHANGED
|
@@ -87,7 +87,7 @@ dsh web --dump-config # verify the plugin row appears
|
|
|
87
87
|
| `conflictPolicy` | `rename` | 도구 이름이 이미 사용 중일 때의 동작으로, `rename` / `skip` / `fail` 중 하나입니다. |
|
|
88
88
|
| `renameSuffix` | `_1` | 이름 회피에 사용되는 접미사입니다. |
|
|
89
89
|
| `allowUnifiedDiff` | `true` | git/unified diff를 허용합니다. |
|
|
90
|
-
| `allowCodexPatch` | `false` | Codex `apply_patch` 문법을 허용합니다. |
|
|
90
|
+
| `allowCodexPatch` | `false` | Codex `apply_patch` 문법을 허용합니다. DSH 설정 패널에서도 변경할 수 있으며, 변경은 즉시 적용됩니다(재시작 불필요). |
|
|
91
91
|
| `deleteBackend` | `shell` | `shell` 또는 `none`(`none`이면 삭제/이동이 구조화된 오류를 반환합니다). |
|
|
92
92
|
| `shellDialect` | `auto` | `auto`(win32에서는 pwsh) / `posix` / `pwsh` 중 하나입니다. |
|
|
93
93
|
| `deleteCommand` / `moveCommand` | 내장 | 사용자 지정 명령 템플릿입니다. 경로는 여전히 **환경 변수로만**(`DSH_PATCH_TARGET` / `DSH_PATCH_SOURCE`) 전달되며, 명령 문자열에 끼워 넣어서는 안 됩니다. |
|
|
@@ -100,6 +100,7 @@ dsh web --dump-config # verify the plugin row appears
|
|
|
100
100
|
## 한계 (공개)
|
|
101
101
|
|
|
102
102
|
- **삭제/이동은 `ctx.shell`을 통해 실행됩니다.** 샌드박스 강도는 로드된 셸 실행기가 실제로 강제하는 수준을 따릅니다(`bash-sandbox`는 격리하고, `bash-local`은 격리하지 않습니다) — 네이티브 bash 도구와 동일한 위험 구도입니다. 모든 요청은 `sandboxPolicy`를 함께 전달하고 샌드박스 관련 사실을 보고하므로, "정책 거부"와 "명령 실패"를 구별할 수 있습니다.
|
|
103
|
+
- **모든 쓰기와 모든 삭제/이동이 세션 범위 정책을 함께 전달합니다.** `apply_patch`는 호출마다 `ctx.sandboxPolicy.resolve({ session })`을 해석하며(세션의 모드 재정의와 워크스페이스 루트로서의 세션 cwd), 네이티브 `write`/`edit` 도구와 완전히 동일하게 동작하고 경로 계획도 같은 루트를 사용합니다. 이것이 없으면 강제 적용하는 파일 시스템이 배포 루트로 폴백하여, `danger-full-access` 세션에서도 워크스페이스 내부 쓰기가 `workspace-write` 거부로 실패합니다. `workspace-write` 거부는 백엔드 문구를 담은 `PatchError`로 표면화됩니다(구조화된 `[sandbox: …]` 마커와 같은 턴의 에스컬레이션 필드는 구현되지 않았습니다. 에스컬레이션이 필요하면 네이티브 `write`/`edit`를 사용하십시오).
|
|
103
104
|
- **추가(Add)는 상위 디렉터리를 생성하지 않습니다.** 이는 네이티브 `write` 도구와 동일한 동작이며(`ctx.fs`에는 mkdir이 없습니다), 오류 메시지에 누락된 디렉터리 이름이 표시됩니다.
|
|
104
105
|
- 퍼지/오프셋 매칭은 지원하지 않습니다. hunk 위치 탐색은 정확 일치 → `trimEnd` → `trim` 순으로 의도적으로 수행합니다(퍼지 매칭은 파괴적 연산에 안전하지 않으며, 로드맵에 올라 있습니다).
|
|
105
106
|
- 바이너리 패치는 명확한 오류와 함께 거부됩니다.
|
|
@@ -110,7 +111,7 @@ dsh web --dump-config # verify the plugin row appears
|
|
|
110
111
|
npm install
|
|
111
112
|
npm run typecheck # against 0.1.2-rc.1 peers (devDependencies)
|
|
112
113
|
npm run typecheck:0.1.5 # against 0.1.5-rc.2 peers (static dual-version proof)
|
|
113
|
-
npm test # vitest,
|
|
114
|
+
npm test # vitest, 90 tests
|
|
114
115
|
npm run lint
|
|
115
116
|
npm run build # lib/
|
|
116
117
|
npm run verify:source # static safety assertions (intent dance, no node:fs, …)
|
package/README.md
CHANGED
|
@@ -6,11 +6,10 @@ Patch-style file editing for [DeepSeek Harness (DSH)](https://github.com/deepsee
|
|
|
6
6
|
|
|
7
7
|
| DSH version | Status | Notes |
|
|
8
8
|
|---|---|---|
|
|
9
|
-
| `0.1.
|
|
10
|
-
| `0.1.
|
|
11
|
-
| `0.1.5-rc.2` | ✅ statically verified | `npm run typecheck:0.1.5` typechecks `src/` against the real `0.1.5-rc.2` peer packages. No `readByteRange` usage, no 0.1.5-only PTC identifiers. |
|
|
9
|
+
| `0.1.7-rc.1+` | ✅ supported (this line, v0.2.0+) | Declarative settings: `allowCodexPatch` is marked `.volatile()` — the settings form renders automatically and `loader/volatile-update` re-registers the tool without a remount. No registration call. |
|
|
10
|
+
| `0.1.2-rc.1` ~ `0.1.5-rc.2` | ↗ maintenance line | Served by the pre-0.1.7 branch / `dsh-0.1.5` dist-tag. 0.1.7 removed the imperative settings APIs the dual-API fallback relied on, so one codebase cannot serve both. |
|
|
12
11
|
|
|
13
|
-
Settings
|
|
12
|
+
Settings are declarative on this line: fields marked `.volatile()` in `Config` become the settings form; the plugin subscribes to `loader/volatile-update` instead of receiving registration hooks.
|
|
14
13
|
|
|
15
14
|
## Why patch at all? (tool routing guide)
|
|
16
15
|
|
|
@@ -87,7 +86,7 @@ Enable with `allowCodexPatch: true`. When a Codex patch arrives while disabled,
|
|
|
87
86
|
| `conflictPolicy` | `rename` | `rename` / `skip` / `fail` when the tool name is taken. |
|
|
88
87
|
| `renameSuffix` | `_1` | Suffix used by rename avoidance. |
|
|
89
88
|
| `allowUnifiedDiff` | `true` | Accept git/unified diffs. |
|
|
90
|
-
| `allowCodexPatch` | `false` | Accept Codex `apply_patch` syntax. |
|
|
89
|
+
| `allowCodexPatch` | `false` | Accept Codex `apply_patch` syntax. Also editable in the DSH settings panel; changes take effect immediately, no restart needed. |
|
|
91
90
|
| `deleteBackend` | `shell` | `shell` or `none` (Delete/Move return a structured error). |
|
|
92
91
|
| `shellDialect` | `auto` | `auto` (pwsh on win32) / `posix` / `pwsh`. |
|
|
93
92
|
| `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. |
|
|
@@ -100,6 +99,7 @@ Enable with `allowCodexPatch: true`. When a Codex patch arrives while disabled,
|
|
|
100
99
|
## Limitations (disclosed)
|
|
101
100
|
|
|
102
101
|
- **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".
|
|
102
|
+
- **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).
|
|
103
103
|
- **Add does not create parent directories.** This matches the native `write` tool (`ctx.fs` has no mkdir); the error names the missing directory.
|
|
104
104
|
- No fuzzy/offset matching: hunk location is exact → `trimEnd` → `trim`, deliberately (fuzzy matching is unsafe for destructive ops; it is on the roadmap).
|
|
105
105
|
- Binary patches are rejected with a clear error.
|
|
@@ -110,7 +110,7 @@ Enable with `allowCodexPatch: true`. When a Codex patch arrives while disabled,
|
|
|
110
110
|
npm install
|
|
111
111
|
npm run typecheck # against 0.1.2-rc.1 peers (devDependencies)
|
|
112
112
|
npm run typecheck:0.1.5 # against 0.1.5-rc.2 peers (static dual-version proof)
|
|
113
|
-
npm test # vitest,
|
|
113
|
+
npm test # vitest, 90 tests
|
|
114
114
|
npm run lint
|
|
115
115
|
npm run build # lib/
|
|
116
116
|
npm run verify:source # static safety assertions (intent dance, no node:fs, …)
|
package/README.zh.md
CHANGED
|
@@ -6,11 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
| DSH 版本 | 状态 | 说明 |
|
|
8
8
|
|---|---|---|
|
|
9
|
-
| `0.1.
|
|
10
|
-
| `0.1.
|
|
11
|
-
| `0.1.5-rc.2` | ✅ 静态核验 | `npm run typecheck:0.1.5` 用真实 `0.1.5-rc.2` peer 包对 `src/` 做类型核验;不调用 `readByteRange`,不含 0.1.5-only PTC 字符串。 |
|
|
9
|
+
| `0.1.7-rc.1+` | ✅ 支持(本线,v0.2.0+) | 声明式设置:`allowCodexPatch` 标记 `.volatile()`——设置表单自动生成,`loader/volatile-update` 驱动工具原位重注册(不 remount),无任何注册调用。 |
|
|
10
|
+
| `0.1.2-rc.1` ~ `0.1.5-rc.2` | ↗ 维护线 | 由 0.1.7 前的分支 / `dsh-0.1.5` dist-tag 服务。0.1.7 删除了双 API 回退所依赖的命令式注册 API,一份代码无法同时兼容两代。 |
|
|
12
11
|
|
|
13
|
-
settings
|
|
12
|
+
本线的 settings 是声明式的:`Config` 中标记 `.volatile()` 的字段即设置表单;插件订阅 `loader/volatile-update`,不再接收注册 hooks。
|
|
14
13
|
|
|
15
14
|
## 工具路由指引(为什么该用谁)
|
|
16
15
|
|
|
@@ -86,7 +85,7 @@ dsh web --dump-config # 确认插件行出现
|
|
|
86
85
|
| `conflictPolicy` | `rename` | 重名时 `rename` / `skip` / `fail`。 |
|
|
87
86
|
| `renameSuffix` | `_1` | 改名避让使用的后缀。 |
|
|
88
87
|
| `allowUnifiedDiff` | `true` | 接受 git/unified diff。 |
|
|
89
|
-
| `allowCodexPatch` | `false` | 接受 Codex `apply_patch`
|
|
88
|
+
| `allowCodexPatch` | `false` | 接受 Codex `apply_patch` 语法。也可在 DSH 设置面板中修改,改动立即生效、无需重启。 |
|
|
90
89
|
| `deleteBackend` | `shell` | `shell` 或 `none`(Delete/Move 返回结构化错误)。 |
|
|
91
90
|
| `shellDialect` | `auto` | `auto`(win32 上 pwsh)/ `posix` / `pwsh`。 |
|
|
92
91
|
| `deleteCommand` / `moveCommand` | 内置 | 自定义命令模板。路径仍然**只经 env** 传入(`DSH_PATCH_TARGET` / `DSH_PATCH_SOURCE`),请勿把路径插值进命令串。 |
|
|
@@ -99,6 +98,7 @@ dsh web --dump-config # 确认插件行出现
|
|
|
99
98
|
## 限制(明确披露)
|
|
100
99
|
|
|
101
100
|
- **Delete/Move 走 `ctx.shell`**:沙箱强度取决于装载的 shell 执行器(`bash-sandbox` 有围栏;`bash-local` 没有)—— 与原生 bash 工具的风险姿态一致。每次请求都携带 `sandboxPolicy` 并回传沙箱事实,"策略拒绝"与"命令失败"可区分。
|
|
101
|
+
- **每次写入、每次 Delete/Move 都携带会话作用域策略**:`apply_patch` 每次调用都解析 `ctx.sandboxPolicy.resolve({ session })` —— 会话的 mode 覆盖,加上以会话 cwd 作为工作区根 —— 与官方 `write`/`edit` 完全一致,且路径规划用同一个根。缺了它,强制执行的文件系统会回退到部署级根,导致工作区内的写入以 `workspace-write` 拒绝告终,连 `danger-full-access` 会话也不例外。`workspace-write` 下的拒绝以 `PatchError` 呈现,消息里带后端原文(结构化的 `[sandbox: …]` 标记与同轮提权字段未实现;需要提权时请改用官方 `write`/`edit`)。
|
|
102
102
|
- **Add 不创建父目录**:与原生 `write` 工具一致(`ctx.fs` 无 mkdir);错误信息会指出缺失的目录。
|
|
103
103
|
- 不做模糊/偏移匹配:定位容错为精确 → `trimEnd` → `trim` 三级,刻意保守(模糊匹配对删除类操作不安全,已列入路线图)。
|
|
104
104
|
- 二进制补丁被拒绝并给出明确错误。
|
|
@@ -109,7 +109,7 @@ dsh web --dump-config # 确认插件行出现
|
|
|
109
109
|
npm install
|
|
110
110
|
npm run typecheck # 针对 0.1.2-rc.1 peer 包(devDependencies)
|
|
111
111
|
npm run typecheck:0.1.5 # 针对 0.1.5-rc.2 peer 包(双版本静态核验)
|
|
112
|
-
npm test # vitest,
|
|
112
|
+
npm test # vitest,90 个用例
|
|
113
113
|
npm run lint
|
|
114
114
|
npm run build # lib/
|
|
115
115
|
npm run verify:source # 静态安全断言(intent 舞蹈、无 node:fs 等)
|
package/dsh.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-patch-edit-plus",
|
|
3
3
|
"name": "dsh-patch-edit-plus",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.2",
|
|
5
5
|
"description": {
|
|
6
6
|
"en": "Patch-style file editing: one apply_patch tool for git/unified diff (default) and Codex apply_patch syntax (opt-in), with all-or-nothing application and sandbox-aware delete/move.",
|
|
7
7
|
"zh": "补丁风格文件编辑:单个 apply_patch 工具,接受 git/unified diff(默认)与 Codex apply_patch 语法(可选开启),全量验证后原子应用,删除/移动走沙箱感知 shell。"
|
package/lib/apply/plan.js
CHANGED
|
@@ -7,15 +7,19 @@
|
|
|
7
7
|
import { duplicatePathError, limitError, missingParentError, pathEscapeError, symlinkRejectedError, } from '../errors.js';
|
|
8
8
|
/**
|
|
9
9
|
* Plan a parsed patch against the filesystem backend.
|
|
10
|
+
* @param sandboxPolicy - the per-call policy the mutations will carry; its
|
|
11
|
+
* workspace root, when set, is the resolution cwd AND the containment root, so
|
|
12
|
+
* the path the engine writes is exactly the path the fence will measure.
|
|
10
13
|
* @throws {@link PatchError} on limits, path escapes, symlinks, missing parents or duplicate targets.
|
|
11
14
|
*/
|
|
12
|
-
export async function buildPlan(ctx, exec, parsed, cfg) {
|
|
15
|
+
export async function buildPlan(ctx, exec, parsed, cfg, sandboxPolicy) {
|
|
13
16
|
if (parsed.ops.length > cfg.maxFiles)
|
|
14
17
|
throw limitError('file section count', parsed.ops.length, cfg.maxFiles);
|
|
15
18
|
const fs = ctx.fs;
|
|
16
|
-
const cwd = exec.agent?.session.header.cwd;
|
|
19
|
+
const cwd = sandboxPolicy?.workspaceRoot ?? exec.agent?.session.header.cwd;
|
|
17
20
|
const resolveOpts = { cwd, signal: exec.signal };
|
|
18
|
-
// Containment root: the
|
|
21
|
+
// Containment root: the fencing root when the call carries a policy, else the
|
|
22
|
+
// session workspace. Mirrors the native write/edit tools' resolve options.
|
|
19
23
|
const rootTarget = cwd === undefined ? undefined : await fs.resolve(cwd, resolveOpts);
|
|
20
24
|
const seen = new Map();
|
|
21
25
|
const entries = [];
|
package/lib/config.js
CHANGED
|
@@ -12,7 +12,7 @@ export const Config = z.object({
|
|
|
12
12
|
conflictPolicy: z.union(['rename', 'skip', 'fail']).default('rename'),
|
|
13
13
|
renameSuffix: z.string().default('_1'),
|
|
14
14
|
allowUnifiedDiff: z.boolean().default(true),
|
|
15
|
-
allowCodexPatch: z.boolean().default(false),
|
|
15
|
+
allowCodexPatch: z.boolean().default(false).volatile(),
|
|
16
16
|
deleteBackend: z.union(['shell', 'none']).default('shell'),
|
|
17
17
|
shellDialect: z.union(['auto', 'posix', 'pwsh']).default('auto'),
|
|
18
18
|
deleteCommand: z.string(),
|
|
@@ -23,6 +23,14 @@ export const Config = z.object({
|
|
|
23
23
|
maxPatchBytes: z.number().step(1).min(1024).default(512 * 1024),
|
|
24
24
|
maxDiffBytes: z.number().step(1).min(256).default(16 * 1024),
|
|
25
25
|
});
|
|
26
|
+
/** Read a `.volatile()` field: a live ref on 0.1.7+, a plain boolean otherwise. */
|
|
27
|
+
export function readVolatileBoolean(value, fallback) {
|
|
28
|
+
if (typeof value === 'boolean')
|
|
29
|
+
return value;
|
|
30
|
+
if (value && typeof value.get === 'function')
|
|
31
|
+
return Boolean(value.get());
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
26
34
|
/** Fill user config with defaults; `undefined`/null entries fall back too. */
|
|
27
35
|
export function resolveConfig(config) {
|
|
28
36
|
const c = config ?? {};
|
|
@@ -31,7 +39,7 @@ export function resolveConfig(config) {
|
|
|
31
39
|
conflictPolicy: c.conflictPolicy ?? 'rename',
|
|
32
40
|
renameSuffix: typeof c.renameSuffix === 'string' ? c.renameSuffix : '_1',
|
|
33
41
|
allowUnifiedDiff: c.allowUnifiedDiff ?? true,
|
|
34
|
-
allowCodexPatch: c.allowCodexPatch
|
|
42
|
+
allowCodexPatch: readVolatileBoolean(c.allowCodexPatch, false),
|
|
35
43
|
deleteBackend: c.deleteBackend ?? 'shell',
|
|
36
44
|
shellDialect: c.shellDialect ?? 'auto',
|
|
37
45
|
deleteCommand: typeof c.deleteCommand === 'string' && c.deleteCommand !== '' ? c.deleteCommand : undefined,
|
package/lib/index.js
CHANGED
|
@@ -4,36 +4,67 @@
|
|
|
4
4
|
* One `apply_patch` tool accepting git/unified diff (default) and Codex
|
|
5
5
|
* `apply_patch` syntax (opt-in). All-or-nothing application through the
|
|
6
6
|
* official fs write-intent dance; delete/move through the sandbox-aware
|
|
7
|
-
* shell.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* shell. Targets DSH 0.1.7+: settings are declarative — the fields marked
|
|
8
|
+
* `.volatile()` in `Config` render the settings form automatically (no
|
|
9
|
+
* registration call), and `loader/volatile-update` drives re-registration
|
|
10
|
+
* without a plugin remount. This line drops the pre-0.1.7 hosts; the
|
|
11
|
+
* 0.1.2-rc.1 ~ 0.1.5-rc.2 line stays on its maintenance branch.
|
|
11
12
|
*
|
|
12
13
|
* @module dsh-patch-edit-plus
|
|
13
14
|
*/
|
|
14
|
-
import {
|
|
15
|
+
import { resolveConfig } from './config.js';
|
|
15
16
|
import { registerApplyPatchTool } from './register.js';
|
|
16
17
|
/** Cordis plugin name used by Loader diagnostics. */
|
|
17
18
|
export const name = 'dsh-patch-edit-plus';
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Services this plugin reads, declared for the loader BEFORE `apply` runs.
|
|
21
|
+
*
|
|
22
|
+
* Cordis resolves `ctx.<service>` through a proxy that throws
|
|
23
|
+
* `cannot get property "<name>" without inject` for anything not declared
|
|
24
|
+
* here, and the loader surfaces that as a fatal `plugin tree failed to load`.
|
|
25
|
+
* Both entries below are read unconditionally on the load path:
|
|
26
|
+
* - `tools` — `registerApplyPatchTool` probes and registers (`ctx.tools`).
|
|
27
|
+
* - `fs` — reads plus the official write-intent dance (`ctx.fs`).
|
|
28
|
+
*
|
|
29
|
+
* `shell` is deliberately NOT declared. Delete/Move is the only consumer and
|
|
30
|
+
* `resolveShell` probes it inside a `try/catch`, returning `undefined` when no
|
|
31
|
+
* executor is mounted; declaring it would instead refuse to load the plugin
|
|
32
|
+
* in every profile that has no shell capability.
|
|
33
|
+
*/
|
|
34
|
+
export const inject = ['tools', 'fs'];
|
|
35
|
+
export { Config } from './config.js';
|
|
36
|
+
/**
|
|
37
|
+
* Register the tool from the current config snapshot.
|
|
38
|
+
*
|
|
39
|
+
* 登记级 key。工具「描述」把可用语法固化进去了(tool.ts 拼 `Accepts ${styles}`),
|
|
40
|
+
* 所以判定必须覆盖整份 resolved config,而不是只看 allowCodexPatch。
|
|
41
|
+
*/
|
|
22
42
|
export function apply(ctx, config) {
|
|
23
|
-
|
|
43
|
+
// 组合条目(0.1.7 起 volatile 字段在其中是 live 引用):rejudge 每次整体
|
|
44
|
+
// 重解析一次,一次调用内是同一份快照;跨调用的最新值由事件驱动重取。
|
|
45
|
+
const compositionEntry = config ?? {};
|
|
46
|
+
let disposer = null;
|
|
47
|
+
let registeredKey = null;
|
|
48
|
+
const rejudge = () => {
|
|
49
|
+
const next = resolveConfig(compositionEntry);
|
|
50
|
+
const key = JSON.stringify(next);
|
|
51
|
+
if (key === registeredKey)
|
|
52
|
+
return;
|
|
53
|
+
disposer?.();
|
|
54
|
+
disposer = registerApplyPatchTool(ctx, next) ?? null;
|
|
55
|
+
registeredKey = key;
|
|
56
|
+
};
|
|
24
57
|
ctx.effect(() => {
|
|
25
|
-
|
|
26
|
-
return
|
|
58
|
+
rejudge();
|
|
59
|
+
return () => {
|
|
60
|
+
disposer?.();
|
|
61
|
+
disposer = null;
|
|
62
|
+
registeredKey = null;
|
|
63
|
+
};
|
|
27
64
|
});
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (typeof settings?.installSection === 'function') {
|
|
33
|
-
settings.installSection(ctx, SETTINGS_NAMESPACE, Config, base, { setSource: () => { }, onChange: () => { } });
|
|
34
|
-
}
|
|
35
|
-
else if (typeof settings?.register === 'function') {
|
|
36
|
-
settings.register(SETTINGS_NAMESPACE, Config, { base });
|
|
37
|
-
}
|
|
65
|
+
// 0.1.7+: 只有 volatile 字段变更才走这里(不 remount);普通字段变更会整体
|
|
66
|
+
// remount 插件,新一次 `apply` 自然读到全部新值。等值变更宿主不通知。
|
|
67
|
+
ctx.on('loader/volatile-update', () => {
|
|
68
|
+
rejudge();
|
|
38
69
|
});
|
|
39
70
|
}
|
package/lib/policy.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox-policy seam: the per-call policy every mutation must carry.
|
|
3
|
+
*
|
|
4
|
+
* `ctx.fs.writeText` and a shell run both take an OPTIONAL `sandboxPolicy`, and
|
|
5
|
+
* omitting it is not "no policy" — the enforcing backends fall back to
|
|
6
|
+
* `ctx.sandboxPolicy.resolve()` with NO scope, which resolves the DEPLOYMENT
|
|
7
|
+
* defaults: the configured fallback workspace root (the server's launch
|
|
8
|
+
* directory) rather than this call's session cwd, plus the deployment default
|
|
9
|
+
* mode rather than the session's `sandbox/mode` override.
|
|
10
|
+
*
|
|
11
|
+
* That fallback is what made every `apply_patch` write fail with
|
|
12
|
+
* `file access denied under workspace-write mode` in a `danger-full-access`
|
|
13
|
+
* session: the fenced path was inside the session workspace, but containment
|
|
14
|
+
* was measured against the launch directory. The error text names the wrong
|
|
15
|
+
* party, so the failure reads like a bad path rather than a lost policy.
|
|
16
|
+
*
|
|
17
|
+
* The official `write`/`edit` tools avoid it by resolving the policy PER CALL
|
|
18
|
+
* with the calling session in scope, which stamps both the session's mode
|
|
19
|
+
* override and `session.header.cwd` as the workspace root. This module does the
|
|
20
|
+
* same, so `apply_patch` is fenced exactly like the native tools: a wider
|
|
21
|
+
* session mode is honored and a narrower one is enforced against the real
|
|
22
|
+
* workspace instead of the launch directory.
|
|
23
|
+
*
|
|
24
|
+
* @module dsh-patch-edit-plus/policy
|
|
25
|
+
*/
|
|
26
|
+
function isPolicyFace(value) {
|
|
27
|
+
return typeof value === 'object' && value !== null && typeof value.resolve === 'function';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Structurally resolve `ctx.sandboxPolicy`, the way {@link resolveShell} resolves
|
|
31
|
+
* `ctx.shell`: `ctx.get` is the optional-service accessor, so a profile without a
|
|
32
|
+
* policy owner yields `undefined` instead of a load failure.
|
|
33
|
+
* @param ctx - the plugin context.
|
|
34
|
+
* @returns the policy service, or `undefined` when none is mounted.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveSandboxPolicyService(ctx) {
|
|
37
|
+
try {
|
|
38
|
+
const get = ctx.get;
|
|
39
|
+
if (typeof get !== 'function')
|
|
40
|
+
return undefined;
|
|
41
|
+
const service = get.call(ctx, 'sandboxPolicy');
|
|
42
|
+
return isPolicyFace(service) ? service : undefined;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The policy to stamp onto every mutation of one tool call: the calling
|
|
50
|
+
* session's standing policy (its mode override, its cwd as the workspace root).
|
|
51
|
+
* An agentless call resolves the unscoped deployment policy, which is the correct
|
|
52
|
+
* fence when there is no session to name.
|
|
53
|
+
* @param ctx - the plugin context.
|
|
54
|
+
* @param exec - the tool-execution context supplying the calling session.
|
|
55
|
+
* @returns the policy to pass to the mutation, or `undefined` when the mounted
|
|
56
|
+
* filesystem is not sandboxed (the argument is then ignored by the backend).
|
|
57
|
+
*/
|
|
58
|
+
export function resolveSandboxPolicy(ctx, exec) {
|
|
59
|
+
const service = resolveSandboxPolicyService(ctx);
|
|
60
|
+
if (service === undefined)
|
|
61
|
+
return undefined;
|
|
62
|
+
return exec.agent === undefined ? service.resolve() : service.resolve({ session: exec.agent.session });
|
|
63
|
+
}
|
package/lib/tool.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
11
11
|
import { PatchError, codexDisabledError, limitError, unifiedDisabledError } from './errors.js';
|
|
12
12
|
import { detectFormat } from './detect.js';
|
|
13
|
+
import { resolveSandboxPolicy } from './policy.js';
|
|
13
14
|
import { parseCodexPatch } from './parse/codex.js';
|
|
14
15
|
import { parseUnifiedDiff } from './parse/unified.js';
|
|
15
16
|
import { buildPlan } from './apply/plan.js';
|
|
@@ -108,10 +109,13 @@ export function buildApplyPatchTool(toolName, ctx, cfg) {
|
|
|
108
109
|
const dryRun = typeof args.dryRun === 'boolean'
|
|
109
110
|
? args.dryRun
|
|
110
111
|
: cfg.dryRunByDefault;
|
|
111
|
-
|
|
112
|
+
// Resolve the sandbox policy BEFORE planning: the plan resolves every path
|
|
113
|
+
// against the policy's workspace root, and the same policy is stamped on
|
|
114
|
+
// every mutation below, so planned paths and fenced paths share one root.
|
|
115
|
+
const sandboxPolicy = resolveSandboxPolicy(ctx, exec);
|
|
116
|
+
const plan = await buildPlan(ctx, exec, parsed, cfg, sandboxPolicy);
|
|
112
117
|
const changes = await verifyPatch(ctx, exec, plan, cfg);
|
|
113
118
|
if (!dryRun) {
|
|
114
|
-
const sandboxPolicy = undefined;
|
|
115
119
|
await applyPatch(ctx, exec, changes, cfg, sandboxPolicy);
|
|
116
120
|
}
|
|
117
121
|
return buildResult(format, !dryRun, changes, cfg, Date.now() - start);
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { Context } from '@deepseek-ai/cordis';
|
|
8
8
|
import type { ToolRunContext } from '@deepseek-ai/dsh-tools';
|
|
9
9
|
import type { FsTarget } from '@deepseek-ai/dsh-fs';
|
|
10
|
+
import type { SandboxExecutionPolicy } from '@deepseek-ai/dsh-sandbox';
|
|
10
11
|
import type { ResolvedConfig } from '../config.js';
|
|
11
12
|
import type { ParsedPatch, PatchOp } from '../parse/types.js';
|
|
12
13
|
/** One planned, fully-resolved operation. */
|
|
@@ -24,8 +25,11 @@ export interface PlannedPatch {
|
|
|
24
25
|
}
|
|
25
26
|
/**
|
|
26
27
|
* Plan a parsed patch against the filesystem backend.
|
|
28
|
+
* @param sandboxPolicy - the per-call policy the mutations will carry; its
|
|
29
|
+
* workspace root, when set, is the resolution cwd AND the containment root, so
|
|
30
|
+
* the path the engine writes is exactly the path the fence will measure.
|
|
27
31
|
* @throws {@link PatchError} on limits, path escapes, symlinks, missing parents or duplicate targets.
|
|
28
32
|
*/
|
|
29
|
-
export declare function buildPlan(ctx: Context, exec: ToolRunContext, parsed: ParsedPatch, cfg: ResolvedConfig): Promise<PlannedPatch>;
|
|
33
|
+
export declare function buildPlan(ctx: Context, exec: ToolRunContext, parsed: ParsedPatch, cfg: ResolvedConfig, sandboxPolicy?: SandboxExecutionPolicy): Promise<PlannedPatch>;
|
|
30
34
|
/** True when the operation needs the shell backend (delete or move). */
|
|
31
35
|
export declare function needsShell(op: PatchOp): boolean;
|
package/lib/types/config.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* @module dsh-patch-edit-plus/config
|
|
7
7
|
*/
|
|
8
8
|
import z from '@deepseek-ai/schemastery';
|
|
9
|
+
import type { Volatile } from '@deepseek-ai/cosmokit';
|
|
9
10
|
/** What to do when the configured tool name is already taken at registration. */
|
|
10
11
|
export type ConflictPolicy = 'rename' | 'skip' | 'fail';
|
|
11
12
|
/** Backend used for Delete/Move operations (`ctx.fs` has no delete/move). */
|
|
@@ -22,8 +23,10 @@ export interface Config {
|
|
|
22
23
|
renameSuffix?: string;
|
|
23
24
|
/** Accept git/unified diff patches. Defaults to true. */
|
|
24
25
|
allowUnifiedDiff?: boolean;
|
|
25
|
-
/** Accept Codex `apply_patch` syntax. Defaults to false.
|
|
26
|
-
|
|
26
|
+
/** Accept Codex `apply_patch` syntax. Defaults to false.
|
|
27
|
+
* 0.1.7 `.volatile()`: the loader hands `apply` a live `Volatile` ref for this
|
|
28
|
+
* field — always read it through `readVolatileBoolean`, never cache the ref. */
|
|
29
|
+
allowCodexPatch?: boolean | Volatile<boolean>;
|
|
27
30
|
/** Delete/Move backend. Defaults to `shell`. `none` rejects Delete/Move with a structured error. */
|
|
28
31
|
deleteBackend?: DeleteBackend;
|
|
29
32
|
/** Shell dialect for the constant delete/move templates. Defaults to `auto` (pwsh on win32). */
|
|
@@ -44,7 +47,37 @@ export interface Config {
|
|
|
44
47
|
maxDiffBytes?: number;
|
|
45
48
|
}
|
|
46
49
|
/** Schemastery schema for Loader defaults and generated configuration docs. */
|
|
47
|
-
export declare const Config: z<
|
|
50
|
+
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
51
|
+
toolName: z<string, string, "defined">;
|
|
52
|
+
conflictPolicy: z<"rename" | "skip" | "fail", "rename" | "skip" | "fail", "defined">;
|
|
53
|
+
renameSuffix: z<string, string, "defined">;
|
|
54
|
+
allowUnifiedDiff: z<boolean, boolean, "defined">;
|
|
55
|
+
allowCodexPatch: z<boolean, boolean, "volatile-defined">;
|
|
56
|
+
deleteBackend: z<"shell" | "none", "shell" | "none", "defined">;
|
|
57
|
+
shellDialect: z<"auto" | "posix" | "pwsh", "auto" | "posix" | "pwsh", "defined">;
|
|
58
|
+
deleteCommand: z<string, string, "plain">;
|
|
59
|
+
moveCommand: z<string, string, "plain">;
|
|
60
|
+
dryRunByDefault: z<boolean, boolean, "defined">;
|
|
61
|
+
followSymlinks: z<boolean, boolean, "defined">;
|
|
62
|
+
maxFiles: z<number, number, "defined">;
|
|
63
|
+
maxPatchBytes: z<number, number, "defined">;
|
|
64
|
+
maxDiffBytes: z<number, number, "defined">;
|
|
65
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
66
|
+
toolName: z<string, string, "defined">;
|
|
67
|
+
conflictPolicy: z<"rename" | "skip" | "fail", "rename" | "skip" | "fail", "defined">;
|
|
68
|
+
renameSuffix: z<string, string, "defined">;
|
|
69
|
+
allowUnifiedDiff: z<boolean, boolean, "defined">;
|
|
70
|
+
allowCodexPatch: z<boolean, boolean, "volatile-defined">;
|
|
71
|
+
deleteBackend: z<"shell" | "none", "shell" | "none", "defined">;
|
|
72
|
+
shellDialect: z<"auto" | "posix" | "pwsh", "auto" | "posix" | "pwsh", "defined">;
|
|
73
|
+
deleteCommand: z<string, string, "plain">;
|
|
74
|
+
moveCommand: z<string, string, "plain">;
|
|
75
|
+
dryRunByDefault: z<boolean, boolean, "defined">;
|
|
76
|
+
followSymlinks: z<boolean, boolean, "defined">;
|
|
77
|
+
maxFiles: z<number, number, "defined">;
|
|
78
|
+
maxPatchBytes: z<number, number, "defined">;
|
|
79
|
+
maxDiffBytes: z<number, number, "defined">;
|
|
80
|
+
}>>, "plain">;
|
|
48
81
|
/** Fully-resolved configuration used by the tool body. */
|
|
49
82
|
export interface ResolvedConfig {
|
|
50
83
|
toolName: string;
|
|
@@ -62,5 +95,7 @@ export interface ResolvedConfig {
|
|
|
62
95
|
maxPatchBytes: number;
|
|
63
96
|
maxDiffBytes: number;
|
|
64
97
|
}
|
|
98
|
+
/** Read a `.volatile()` field: a live ref on 0.1.7+, a plain boolean otherwise. */
|
|
99
|
+
export declare function readVolatileBoolean(value: boolean | Volatile<boolean> | undefined, fallback: boolean): boolean;
|
|
65
100
|
/** Fill user config with defaults; `undefined`/null entries fall back too. */
|
|
66
101
|
export declare function resolveConfig(config: Config | undefined): ResolvedConfig;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -4,20 +4,44 @@
|
|
|
4
4
|
* One `apply_patch` tool accepting git/unified diff (default) and Codex
|
|
5
5
|
* `apply_patch` syntax (opt-in). All-or-nothing application through the
|
|
6
6
|
* official fs write-intent dance; delete/move through the sandbox-aware
|
|
7
|
-
* shell.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* shell. Targets DSH 0.1.7+: settings are declarative — the fields marked
|
|
8
|
+
* `.volatile()` in `Config` render the settings form automatically (no
|
|
9
|
+
* registration call), and `loader/volatile-update` drives re-registration
|
|
10
|
+
* without a plugin remount. This line drops the pre-0.1.7 hosts; the
|
|
11
|
+
* 0.1.2-rc.1 ~ 0.1.5-rc.2 line stays on its maintenance branch.
|
|
11
12
|
*
|
|
12
13
|
* @module dsh-patch-edit-plus
|
|
13
14
|
*/
|
|
14
15
|
import type { Context } from '@deepseek-ai/cordis';
|
|
15
|
-
import { Config } from './config.js';
|
|
16
16
|
import type { Config as PluginConfig } from './config.js';
|
|
17
|
+
declare module '@deepseek-ai/cordis' {
|
|
18
|
+
interface Events {
|
|
19
|
+
'loader/volatile-update': (paths: string[]) => void;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
17
22
|
/** Cordis plugin name used by Loader diagnostics. */
|
|
18
23
|
export declare const name = "dsh-patch-edit-plus";
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Services this plugin reads, declared for the loader BEFORE `apply` runs.
|
|
26
|
+
*
|
|
27
|
+
* Cordis resolves `ctx.<service>` through a proxy that throws
|
|
28
|
+
* `cannot get property "<name>" without inject` for anything not declared
|
|
29
|
+
* here, and the loader surfaces that as a fatal `plugin tree failed to load`.
|
|
30
|
+
* Both entries below are read unconditionally on the load path:
|
|
31
|
+
* - `tools` — `registerApplyPatchTool` probes and registers (`ctx.tools`).
|
|
32
|
+
* - `fs` — reads plus the official write-intent dance (`ctx.fs`).
|
|
33
|
+
*
|
|
34
|
+
* `shell` is deliberately NOT declared. Delete/Move is the only consumer and
|
|
35
|
+
* `resolveShell` probes it inside a `try/catch`, returning `undefined` when no
|
|
36
|
+
* executor is mounted; declaring it would instead refuse to load the plugin
|
|
37
|
+
* in every profile that has no shell capability.
|
|
38
|
+
*/
|
|
39
|
+
export declare const inject: string[];
|
|
40
|
+
export { Config } from './config.js';
|
|
41
|
+
/**
|
|
42
|
+
* Register the tool from the current config snapshot.
|
|
43
|
+
*
|
|
44
|
+
* 登记级 key。工具「描述」把可用语法固化进去了(tool.ts 拼 `Accepts ${styles}`),
|
|
45
|
+
* 所以判定必须覆盖整份 resolved config,而不是只看 allowCodexPatch。
|
|
46
|
+
*/
|
|
23
47
|
export declare function apply(ctx: Context, config?: PluginConfig): void;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox-policy seam: the per-call policy every mutation must carry.
|
|
3
|
+
*
|
|
4
|
+
* `ctx.fs.writeText` and a shell run both take an OPTIONAL `sandboxPolicy`, and
|
|
5
|
+
* omitting it is not "no policy" — the enforcing backends fall back to
|
|
6
|
+
* `ctx.sandboxPolicy.resolve()` with NO scope, which resolves the DEPLOYMENT
|
|
7
|
+
* defaults: the configured fallback workspace root (the server's launch
|
|
8
|
+
* directory) rather than this call's session cwd, plus the deployment default
|
|
9
|
+
* mode rather than the session's `sandbox/mode` override.
|
|
10
|
+
*
|
|
11
|
+
* That fallback is what made every `apply_patch` write fail with
|
|
12
|
+
* `file access denied under workspace-write mode` in a `danger-full-access`
|
|
13
|
+
* session: the fenced path was inside the session workspace, but containment
|
|
14
|
+
* was measured against the launch directory. The error text names the wrong
|
|
15
|
+
* party, so the failure reads like a bad path rather than a lost policy.
|
|
16
|
+
*
|
|
17
|
+
* The official `write`/`edit` tools avoid it by resolving the policy PER CALL
|
|
18
|
+
* with the calling session in scope, which stamps both the session's mode
|
|
19
|
+
* override and `session.header.cwd` as the workspace root. This module does the
|
|
20
|
+
* same, so `apply_patch` is fenced exactly like the native tools: a wider
|
|
21
|
+
* session mode is honored and a narrower one is enforced against the real
|
|
22
|
+
* workspace instead of the launch directory.
|
|
23
|
+
*
|
|
24
|
+
* @module dsh-patch-edit-plus/policy
|
|
25
|
+
*/
|
|
26
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
27
|
+
import type { ToolRunContext } from '@deepseek-ai/dsh-tools';
|
|
28
|
+
import type { SandboxExecutionPolicy } from '@deepseek-ai/dsh-sandbox';
|
|
29
|
+
/** The `ctx.sandboxPolicy` face this plugin reads (structurally — never a runtime import). */
|
|
30
|
+
export interface SandboxPolicyFace {
|
|
31
|
+
/** Resolve the complete policy for one call; with a session it stamps that session's mode and cwd. */
|
|
32
|
+
resolve(request?: {
|
|
33
|
+
session?: unknown;
|
|
34
|
+
}): SandboxExecutionPolicy;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Structurally resolve `ctx.sandboxPolicy`, the way {@link resolveShell} resolves
|
|
38
|
+
* `ctx.shell`: `ctx.get` is the optional-service accessor, so a profile without a
|
|
39
|
+
* policy owner yields `undefined` instead of a load failure.
|
|
40
|
+
* @param ctx - the plugin context.
|
|
41
|
+
* @returns the policy service, or `undefined` when none is mounted.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveSandboxPolicyService(ctx: Context): SandboxPolicyFace | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* The policy to stamp onto every mutation of one tool call: the calling
|
|
46
|
+
* session's standing policy (its mode override, its cwd as the workspace root).
|
|
47
|
+
* An agentless call resolves the unscoped deployment policy, which is the correct
|
|
48
|
+
* fence when there is no session to name.
|
|
49
|
+
* @param ctx - the plugin context.
|
|
50
|
+
* @param exec - the tool-execution context supplying the calling session.
|
|
51
|
+
* @returns the policy to pass to the mutation, or `undefined` when the mounted
|
|
52
|
+
* filesystem is not sandboxed (the argument is then ignored by the backend).
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveSandboxPolicy(ctx: Context, exec: ToolRunContext): SandboxExecutionPolicy | undefined;
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-patch-edit-plus",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Patch-style file editing for DeepSeek Harness: one apply_patch tool accepting git/unified diff (default) and Codex apply_patch syntax (opt-in), with all-or-nothing application, read-before-write intent dance, sandbox-aware delete/move,
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Patch-style file editing for DeepSeek Harness: one apply_patch tool accepting git/unified diff (default) and Codex apply_patch syntax (opt-in), with all-or-nothing application, read-before-write intent dance, sandbox-aware delete/move, with a DSH 0.1.7+ line (declarative volatile settings).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/drscrewdriver/dsh-patch-edit-plus.git"
|
|
8
|
+
"url": "git+https://github.com/drscrewdriver/dsh-patch-edit-plus.git"
|
|
9
9
|
},
|
|
10
10
|
"main": "lib/index.js",
|
|
11
11
|
"types": "lib/types/index.d.ts",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
|
49
49
|
"node": ">=20",
|
|
50
|
-
"dsh": ">=0.1.
|
|
50
|
+
"dsh": ">=0.1.7-rc.1 <0.2.0-0"
|
|
51
51
|
},
|
|
52
52
|
"license": "MIT",
|
|
53
53
|
"keywords": [
|
|
@@ -62,10 +62,10 @@
|
|
|
62
62
|
],
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
65
|
-
"@deepseek-ai/dsh-fs": ">=0.1.
|
|
66
|
-
"@deepseek-ai/dsh-sandbox": ">=0.1.
|
|
67
|
-
"@deepseek-ai/dsh-shell": ">=0.1.
|
|
68
|
-
"@deepseek-ai/dsh-tools": ">=0.1.
|
|
65
|
+
"@deepseek-ai/dsh-fs": ">=0.1.7-rc.1 <0.2.0-0",
|
|
66
|
+
"@deepseek-ai/dsh-sandbox": ">=0.1.7-rc.1 <0.2.0-0",
|
|
67
|
+
"@deepseek-ai/dsh-shell": ">=0.1.7-rc.1 <0.2.0-0",
|
|
68
|
+
"@deepseek-ai/dsh-tools": ">=0.1.7-rc.1 <0.2.0-0"
|
|
69
69
|
},
|
|
70
70
|
"peerDependenciesMeta": {
|
|
71
71
|
"@deepseek-ai/cordis": {
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
}
|
|
86
86
|
},
|
|
87
87
|
"dependencies": {
|
|
88
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
88
|
+
"@deepseek-ai/schemastery": "^3.18.4"
|
|
89
89
|
},
|
|
90
90
|
"devDependencies": {
|
|
91
91
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
@@ -98,6 +98,10 @@
|
|
|
98
98
|
"eslint": "^9.0.0",
|
|
99
99
|
"typescript": "^5.6.0",
|
|
100
100
|
"typescript-eslint": "^8.0.0",
|
|
101
|
-
"vitest": "^3.0.0"
|
|
101
|
+
"vitest": "^3.0.0",
|
|
102
|
+
"@deepseek-ai/schemastery": "^3.18.4"
|
|
103
|
+
},
|
|
104
|
+
"publishConfig": {
|
|
105
|
+
"tag": "dsh-0.1.7"
|
|
102
106
|
}
|
|
103
107
|
}
|