dsh-rewind-plugin 0.20.0-alpha.4 → 0.21.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/README.en.md CHANGED
@@ -15,7 +15,7 @@ A deliberately focused plugin with one job: **rewind to any user message, no mat
15
15
 
16
16
  - **In-place rewind, no new session** — the target message and everything after it (replies, tool calls) are withdrawn from the model context *and* the rendered transcript, and the target's text is filled back into the composer for editing and re-sending. It all happens inside the original session: no new branch, no leftover copy.
17
17
  - **Conversation and files rewind together** — Claude Code-aligned behavior: files edited by the write tools are tracked and restored by the same rewind, and external changes to already-tracked files are restorable too. **Partial tracking, before-write backup, store only on change**, with no git dependency.
18
- - **Safety guarantees** — the session log only ever appends rewind markers and is never deleted or rewritten; file backup and restore are hardened; tests are comprehensive, and maintenance continues across DSH upgrades. Security model: [SECURITY.md](SECURITY.md).
18
+ - **Safety guarantees** — the session log only ever appends rewind markers and is never deleted or rewritten; file backup and restore are hardened; tests are comprehensive, and maintenance continues across DSH upgrades. Security model: [docs/security-model.md](docs/security-model.md).
19
19
 
20
20
  ## Preview
21
21
 
@@ -34,19 +34,17 @@ Every user message carries a **↶ rewind** button in its action row. Clicking i
34
34
 
35
35
  ## Install
36
36
 
37
- Check your local DSH version, then find the matching plugin version in
38
- [Releases](https://github.com/SiriLee/dsh-rewind/releases).
39
-
40
37
  **Command line**:
41
38
 
42
39
  ```sh
43
- dsh plugin --profile <name> add dsh-rewind-plugin@<version>
40
+ dsh plugin --profile <name> add dsh-rewind-plugin
44
41
  ```
45
42
 
46
- **Graphical interface**: the **Plugins** page in the sidebar → **Add plugin** → enter `dsh-rewind-plugin@<version>` → install → restart DSH.
43
+ **Graphical interface**: the **Plugins** page in the sidebar → **Add plugin** → enter `dsh-rewind-plugin` → install → restart DSH.
44
+
45
+ <img src="assets/screenshots/add-plugin.png" width="600" alt="Add plugin dialog: enter dsh-rewind-plugin in the field"><br>
47
46
 
48
- <img src="assets/screenshots/add-plugin.png" width="600" alt="Add plugin dialog: enter dsh-rewind-plugin@version in the field"><br>
49
- <sub>The screenshot is an example only — use the version that matches your DSH version.</sub>
47
+ The default install is `latest`, which targets the latest stable DSH release. If you are on an older or alpha DSH version, find the matching plugin version on the [Releases](https://github.com/SiriLee/dsh-rewind/releases) page and install that version explicitly.
50
48
 
51
49
  > [!WARNING]
52
50
  > The npm name `dsh-rewind` belongs to another author's package — install with `dsh-rewind-plugin`.
@@ -130,21 +128,21 @@ The whole design rests on two principles, simple but deliberate: **the conversat
130
128
 
131
129
  ### 2. File restore: lightweight checkpointing, "before-write backup"
132
130
 
133
- The file half follows Claude Code's checkpoint semantics — **partial tracking + before-write backup, plus a re-scan of tracked files at each message**, not a whole-tree snapshot. This trade-off saves space, and it's actually more complete:
131
+ The file half follows Claude Code's checkpoint semantics — **partial tracking + before-write backup, plus a re-scan of tracked files at each message**, not a whole-tree snapshot. This trade-off saves space while still covering external changes to tracked files:
134
132
 
135
133
  - **Before-write backup**: tracks only the write-class tools (`write`, `edit`) — backs up the original content before a write and records/tracks the files it touches; it never backs up the whole workspace, so it's lightweight; **a single file that is too large is not backed up**.
136
134
  - **External changes count too**: at every user-message boundary the plugin re-checks all tracked files — external changes such as a command run or a manual edit are recorded as well and restored by a later rewind. "Lightweight" but not "incomplete".
137
135
  - **Unchanged-not-recorded**: an entry is written only when something changed — at the message-boundary re-check, an unchanged file is never backed up (no record); at before-write time, when the new content matches the path's prior record, only a **link to it** (`ref`) is stored instead of a copy.
138
- - **Accurate restore**: backups are the sole standard, checked against the real disk — **only files that actually differ are touched**. Backups are stored byte for byte, so the restored result matches the backups exactly, with no "ghost impact". There is a limit, though: [File-rewind tracking boundary](docs/compat/tracking-boundary.md).
136
+ - **Accurate restore**: backups are the sole standard, checked against the real disk — **only files that actually differ are touched**. Backups are stored byte for byte, so the restored result matches the backups exactly, with no "ghost impact". Coverage is bounded by design: [File-rewind tracking boundary](docs/compat/tracking-boundary.md).
139
137
  - **Safety and integrity**: paths are sanitized so nothing ever escapes the backup root; symlinks / hard links are skipped so one restore can't clobber another name of the same file; a per-file failure never aborts the pass; backups and the restore journal are written atomically and kept across restarts, so a half-applied restore after a crash can be continued or rolled back.
140
138
 
141
139
  > **Design highlight**: this checkpoint's light footprint comes from **recording only what was actually touched and really changed** — before-write backup makes it restorable, unchanged-not-recorded and content-as-link drop the repetition; only the files that differ are touched at restore time.
142
140
 
143
- ## What it deliberately does NOT do
141
+ ## Scope and division of labour
144
142
 
145
- This plugin deliberately stays lightweight and focused on one thing — "conversation rewind". The following are **out of its scope**:
143
+ This plugin deliberately stays lightweight and focused on one thing — "conversation rewind". The following are **out of its scope**, and each has a better owner (see [design boundaries](docs/boundaries.md)):
146
144
 
147
- - **Whole-tree / Git-level snapshots** — only write-class tool edits plus external changes to already-tracked files are backed up; files never touched by a tool are not restored. For a worktree-level full snapshot rollback, use a more specialized snapshot tool (git).
145
+ - **Whole-tree / Git-level snapshots** — only write-class tool edits plus external changes to already-tracked files are backed up; files never touched by a tool are not restored. Workspace history — any file, any moment — belongs to Git; the agent already uses it, and the plugin does not rebuild that layer. For a worktree-level full snapshot rollback, use Git or the snapshot tool the project already uses.
148
146
  - **Subagent edits** — not tracked, and no rewind inside a subagent session (same as Claude Code): a subagent runs its own session, so its backups could never be restored by a rewind of the parent session, and none are kept for one.
149
147
  - **Fork / branch rewind** — DSH already provides this ("branch in new chat"); no need to reinvent the wheel.
150
148
 
@@ -158,19 +156,17 @@ This plugin deliberately stays lightweight and focused on one thing — "convers
158
156
  > This project and DeepSeek Harness are both in developer preview. Pin exact
159
157
  > versions in reproducible environments and review the behavior notes above.
160
158
 
161
- ## Known issues
159
+ ## Known limitations
162
160
 
163
- 1. **Exported logs are complete** — `/export` exports the whole session log (raw JSONL), **withdrawn content included**; this plugin cannot alter exports.
164
- 2. **Lightweight file rewind has a cost** — in specific cases not all changes can be rewound. Consistent with Claude Code. See: [File-rewind tracking boundary](docs/compat/tracking-boundary.md).
165
- 3. **Turn navigation keeps rewound turns** — the right-side turn navigation keeps an "unloaded" tick for each rewound turn, and hovering previews the withdrawn content. Only a display difference; no functional impact.
166
- 4. **A far rewind may show a blank transcript** — when the target predates the loaded history, the conversation area goes blank; click **Load earlier** to page back until the history appears.
161
+ 1. **Turn navigation keeps rewound turns** — the right-side turn navigation keeps an "unloaded" tick for each rewound turn, and hovering previews the withdrawn content. Only a display difference; no functional impact.
162
+ 2. **A far rewind may show a blank transcript** — when the target predates the loaded history, the conversation area goes blank; click **Load earlier** to page back until the history appears.
167
163
 
168
164
  > [!NOTE]
169
165
  > Browser diagnostics are available; see [Browser diagnostics](docs/compat/diagnostics.md).
170
166
 
171
167
  ## Security
172
168
 
173
- This plugin only appends rewind-marker events to the session log; it never deletes or rewrites logged history. Workspace files are written only when you choose "conversation and files"; backups are stored under `<dsh home>/rewind-snapshots/`; restores draw only from those backups. It never touches your git repository, makes no network requests, and accesses no credentials. For sessions you've left inactive for a long time, a snapshot auto-cleanup (off by default) can remove their snapshot directory in whole, leaving the active session and the chat log untouched. Full security model: [SECURITY.md](SECURITY.md).
169
+ This plugin only appends rewind-marker events to the session log; it never deletes or rewrites logged history. Workspace files are written only when you choose "conversation and files"; backups are stored under `<dsh home>/rewind-snapshots/`; restores draw only from those backups. It never touches your git repository, makes no network requests, and accesses no credentials. For sessions you've left inactive for a long time, a snapshot auto-cleanup (off by default) can remove their snapshot directory in whole, leaving the active session and the chat log untouched. Full security model: [docs/security-model.md](docs/security-model.md); to report a vulnerability, see [SECURITY.md](SECURITY.md).
174
170
 
175
171
  ## Development
176
172
 
@@ -185,7 +181,7 @@ node scripts/verify-host.mjs # end-to-end verification of the built artifact
185
181
 
186
182
  `prepack` runs the full build, so `npm pack` / `npm publish` always ship a complete `lib/`. Install from npm or a packed tarball — a git checkout has no built `lib/`.
187
183
 
188
- Maintainers: the module map and harness interface reference live in [docs/harness-reference.md](docs/harness-reference.md).
184
+ Maintainers: the source layout and architecture are in [docs/architecture.md](docs/architecture.md); the harness interface reference is in [docs/harness-reference.md](docs/harness-reference.md).
189
185
 
190
186
  Contributing guide: [CONTRIBUTING.md](CONTRIBUTING.md).
191
187
 
package/README.md CHANGED
@@ -15,7 +15,7 @@ DeepSeek Harness 插件:**一键就地回退对话到任意更早的用户消
15
15
 
16
16
  - **就地消息回退**——目标消息及其之后的内容同时从**模型上下文**和**渲染对话**中撤回,目标消息文本自动回填输入框。始终在原会话内完成,不新建分支、不残留副本。
17
17
  - **轻量备份还原**——对齐 Claude Code:追踪写类工具编辑过的文件,回退时一并还原,已跟踪文件的外部变更也能还原。**局部追踪、写前备份、不变不存**,不依赖 git。
18
- - **安全保障**——会话日志仅追加回退标记,从不删改;文件备份与还原经安全加固;**完备测试**,随 DSH 升级长期维护。安全模型:[SECURITY.md](SECURITY.md)。
18
+ - **安全保障**——会话日志仅追加回退标记,从不删改;文件备份与还原经安全加固;**完备测试**,随 DSH 升级长期维护。安全模型:[docs/security-model.md](docs/security-model.md)。
19
19
 
20
20
  ## 效果预览
21
21
 
@@ -34,18 +34,17 @@ DeepSeek Harness 插件:**一键就地回退对话到任意更早的用户消
34
34
 
35
35
  ## 安装
36
36
 
37
- 确认本机 DSH 版本后,在 [Release](https://github.com/SiriLee/dsh-rewind/releases) 找到适配的插件版本。
38
-
39
37
  **命令行**:
40
38
 
41
39
  ```sh
42
- dsh plugin --profile <name> add dsh-rewind-plugin@<版本>
40
+ dsh plugin --profile <name> add dsh-rewind-plugin
43
41
  ```
44
42
 
45
- **图形界面**:侧栏「插件」页 →「添加插件」→ 填 `dsh-rewind-plugin@<版本>` → 安装 → 重启 DSH。
43
+ **图形界面**:侧栏「插件」页 →「添加插件」→ 填 `dsh-rewind-plugin` → 安装 → 重启 DSH。
44
+
45
+ <img src="assets/screenshots/add-plugin.png" width="600" alt="「添加插件」弹窗:在输入框填入 dsh-rewind-plugin"><br>
46
46
 
47
- <img src="assets/screenshots/add-plugin.png" width="600" alt="「添加插件」弹窗:在输入框填入 dsh-rewind-plugin@版本号"><br>
48
- <sub>图片仅为示例,请以实际适配版本号为准。</sub>
47
+ 默认安装 `latest`,它适配 DSH 最新稳定版本。若使用的是旧版或内测版 DSH,请在 [Releases](https://github.com/SiriLee/dsh-rewind/releases) 页面找到适配的插件版本,并指定版本安装。
49
48
 
50
49
  > [!WARNING]
51
50
  > npm 上的 `dsh-rewind` 属于其他作者,请用 `dsh-rewind-plugin` 安装。
@@ -116,23 +115,23 @@ rm <dsh home>/snapshot-cleanup-last-sweep.json
116
115
 
117
116
  ### 2. 文件还原:轻量检查点,「写前备份」
118
117
 
119
- 文件部分对齐 Claude Code 的检查点语义——**局部追踪、写前备份 + 每条消息重扫已跟踪文件**,而不是整树快照。这项取舍既省空间,又更完整:
118
+ 文件部分对齐 Claude Code 的检查点语义——**局部追踪、写前备份 + 每条消息重扫已跟踪文件**,而不是整树快照。这项取舍省空间,也让已跟踪文件的外部改动同样可还原:
120
119
 
121
120
  - **写前备份**:只追踪写类工具(`write`、`edit`),写前**备份原内容**,并**记录、追踪**被处理的文件——从不备份整个工作区,因此轻量;**单个文件过大时不备份**。
122
121
  - **外部变更也追**:每条用户消息边界,插件重新检查所有已跟踪文件——命令执行、手动修改等外部变更同样被记录,回退时一并还原。这让「轻量」却不「残缺」。
123
122
  - **不变不存**:记录只在有变化时发生——消息边界重扫时无变更的**不备份**(不留记录);写前备份时若与前一条记录一致,只存指向它的链接(`ref`)而非复制内容。
124
- - **还原准确**:以备份为唯一标准,对照真实磁盘,只动真正不一致的文件;备份逐字节存储。还原结果与备份一致,无“幽灵影响”。但存在限制:[文件回退的追踪边界](docs/compat/tracking-boundary.zh.md)。
123
+ - **还原准确**:以备份为唯一标准,对照真实磁盘,只动真正不一致的文件;备份逐字节存储。还原结果与备份一致,无“幽灵影响”。覆盖范围按设计限定:[文件回退的追踪边界](docs/compat/tracking-boundary.zh.md)。
125
124
  - **安全与完整性**:路径经安全化处理,绝不越出备份根目录;符号/硬链接跳过,避免透过一次还原误伤同名的另一份文件;单个文件失败绝不中止整轮还原;备份与还原日志均原子写落盘(跨重启仍在),断电或崩溃后的半还原可续做或回滚。
126
125
 
127
126
  > **设计点睛**:这套检查点的「轻」,来自**只记录被工具动过、且确实变化的文件**——写前备份保证可还原,不变不存与存链接压掉重复;还原时再对照真实磁盘,只动不一致的文件。
128
127
 
129
- ## 明确不做的事
128
+ ## 范围与分工
130
129
 
131
- 本插件刻意保持轻量、聚焦「对话回退」这一件事,以下场景**不属于它的职责**:
130
+ 本插件刻意保持轻量、聚焦「对话回退」这一件事,以下场景**不属于它的职责**,各自有更合适的归属(见[设计边界](docs/boundaries.zh.md)):
132
131
 
133
- - **整树 / Git 级快照**——只跟踪写类工具编辑 + 已跟踪文件的外部改动,从未被工具碰过的文件不还原。需要工作树级的完整快照回退时,请交给更专业的快照工具(git)。
132
+ - **整树 / Git 级快照**——只跟踪写类工具编辑 + 已跟踪文件的外部改动,从未被工具碰过的文件不还原。工作区历史(任意文件、任意时刻)归 Git;Agent 本来就在用 Git,插件不重复造这层。需要工作树级的完整快照回退时,请交给 Git 或项目自选的快照工具。
134
133
  - **子代理的编辑**——不追踪,子代理会话内也不提供回退(同 Claude Code):子代理运行在自己的会话里,其备份无法由父会话的回退还原,因此也不会为子会话保留备份。
135
- - **fork / 分支回退**——DSH 已内置「在新对话中分支」,不重复造轮子。
134
+ - **fork / 分支回退**——DSH 已内置「在新对话中分支」功能,不重复造轮子。
136
135
 
137
136
  ## 兼容性
138
137
 
@@ -144,19 +143,17 @@ rm <dsh home>/snapshot-cleanup-last-sweep.json
144
143
  > 本项目与 DeepSeek Harness 均处于开发者预览阶段。可复现环境请 pin 精确版本,
145
144
  > 并阅读上述行为说明。
146
145
 
147
- ## 已知问题
146
+ ## 已知限制
148
147
 
149
- 1. **导出的日志是完整内容**——`/export` 导出整个会话日志(原始 JSONL),包含**已撤回的内容**;本插件无法改动导出。
150
- 2. **轻量文件回退存在代价**——特定情况可能无法回退所有修改。行为与 Claude Code 一致。详见:[文件回退的追踪边界](docs/compat/tracking-boundary.zh.md)。
151
- 3. **轮次导航保留已回退轮次**——右侧轮次导航为已回退轮次保留「未加载」刻度,悬浮可预览已撤回内容。仅显示差异,无功能影响。
152
- 4. **较远回退可能显示空白**——回退目标早于已加载的历史时,对话区会显示空白;需点击「加载更早」往前翻,才能看到历史对话。
148
+ 1. **轮次导航保留已回退轮次**——右侧轮次导航为已回退轮次保留「未加载」刻度,悬浮可预览已撤回内容。仅显示差异,无功能影响。
149
+ 2. **较远回退可能显示空白**——回退目标早于已加载的历史时,对话区会显示空白;需点击「加载更早」往前翻,才能看到历史对话。
153
150
 
154
151
  > [!NOTE]
155
152
  > 本插件提供浏览器端诊断输出;详见 [浏览器诊断](docs/compat/diagnostics.zh.md)。
156
153
 
157
154
  ## 安全
158
155
 
159
- 本插件只向会话日志追加回退标记事件,从不删除或改写已记录的历史。工作区文件仅在「回退对话和文件」时被改写,备份存储于 `<dsh home>/rewind-snapshots/`;还原以备份为唯一来源。不触碰你的 git 仓库,无网络请求,不访问任何凭据。对**长期不活跃**的会话,另有默认关闭的快照自动清理可整目录移除其快照,不影响活动会话与对话日志。完整安全模型:[SECURITY.md](SECURITY.md)。
156
+ 本插件只向会话日志追加回退标记事件,从不删除或改写已记录的历史。工作区文件仅在「回退对话和文件」时被改写,备份存储于 `<dsh home>/rewind-snapshots/`;还原以备份为唯一来源。不触碰你的 git 仓库,无网络请求,不访问任何凭据。对**长期不活跃**的会话,另有默认关闭的快照自动清理可整目录移除其快照,不影响活动会话与对话日志。完整安全模型:[docs/security-model.md](docs/security-model.md);漏洞报告政策见 [SECURITY.md](SECURITY.md)。
160
157
 
161
158
  ## 开发
162
159
 
@@ -171,7 +168,7 @@ node scripts/verify-host.mjs # 端到端验证构建产物
171
168
 
172
169
  `prepack` 在打包前执行完整构建,所以 `npm pack` / `npm publish` 总会产出完整的 `lib/`。请从 npm 或打好的 tarball 安装——git 检出的源码里没有构建好的 `lib/`。
173
170
 
174
- 维护者:模块地图与 harness 接口参考见 [docs/harness-reference.md](docs/harness-reference.md)
171
+ 维护者:源码布局与架构见 [docs/architecture.md](docs/architecture.md),harness 接口参考见 [docs/harness-reference.md](docs/harness-reference.md)。
175
172
 
176
173
  贡献指南:[CONTRIBUTING.md](CONTRIBUTING.md)
177
174
 
package/SECURITY.md CHANGED
@@ -1,206 +1,80 @@
1
- # Security model
2
-
3
- This document is the security model of **dsh-rewind** — how the plugin treats
4
- untrusted input, what it is allowed to mutate, and how it survives crashes.
5
- It is derived from the implementation (`src/index.ts`, `src/snapshot.ts`); if
6
- this document and the code ever disagree, the code wins and this document is
7
- a bug.
8
-
9
- ## Trusted boundary
10
-
11
- The plugin runs in the DSH host process and therefore holds the host user's
12
- filesystem authority — it reads and writes files with plain `node:fs`. The
13
- following are treated as **untrusted inputs**:
14
-
15
- - **Model arguments** — the `file_path` field of `write` / `edit` tool calls
16
- (it names the paths the checkpoint store records).
17
- - **Session log contents** — events are parsed structurally; a hostile or
18
- malformed id must never escape the store root.
19
- - **Current worktree state** — restore planning reconciles against the live
20
- disk, which may have been changed by anything.
21
- - **Concurrent external modifications** — a restore never assumes the disk
22
- still matches its records.
23
-
24
- The DSH host (and its other plugins) is trusted; this plugin does not
25
- re-verify the host's own authority boundaries.
26
-
27
- ## Mutation gates
28
-
29
- When `fileCheckpointingEnabled` is on, the plugin **automatically captures**
30
- before-backups of tracked mutations, but **never automatically applies** one.
31
- A workspace restore happens only through an explicit user-invoked
32
- `/rewind-plugin @<seq> both` (the per-message ↶ button or the command channel),
33
- and only when all of the following hold:
34
-
35
- 1. **A validated target**: `planRewind` accepts only a `user/message` seq that
36
- is currently on the session surface (`parseRewindTarget` → `RewindPlan`).
37
- 2. **A fresh plan**: the plan is derived from the current `events` + `surface`
38
- at execution time — never cached across events.
39
- 3. **Committed backups exist**: every planned action comes from a committed
40
- checkpoint entry anchored at or after the target.
41
- 4. **Live-disk reconciliation**: `planRestore` compares each entry against the
42
- current disk and plans only actions that would actually change it — an
43
- already-matching state is a no-op (restores are idempotent).
44
- 5. **Exclusive execution**: a per-session in-flight guard rejects concurrent
45
- rewinds; a running turn is force-cancelled (`keepInbox`) and quiescence is
46
- awaited before the surface is cut.
47
- 6. **Session binding**: the restore reads/writes only the store of the rewound
48
- session (paths are resolved display paths the session's own tools touched).
49
-
50
- **Concurrency scope**: the in-flight guard above only serializes concurrent
51
- *rewinds* of one session; it does not stop other processes from modifying the
52
- workspace. `planRestore` reconciles against the live disk at plan time, but
53
- there is no re-validation between planning and applying — a file changed by
54
- another process in that window is overwritten by the restore. The rescue state
55
- is captured at apply start, so `rollbackRestore` still returns the workspace
56
- to exactly the apply-start state, and per-file failures are reported rather
57
- than hidden.
58
-
59
- A failed gate fails closed: an invalid target, a missing store, an absent
60
- backup, or a cancelled invocation aborts the rewind with an error. A store
61
- written by a **newer** build is one of those gates: it is refused as a whole —
62
- no partial restore and no new entry written into it, and the automatic
63
- format-change clear below is skipped — while the conversation rewind itself
64
- still works. An explicit clear (`run --current --apply`) or the age-based sweep,
65
- being a user-directed deletion of whole session directories, is deliberately not
66
- version-gated.
67
-
68
- **Automatic store deletion**: two things delete session snapshots. The opt-in
69
- `snapshot-auto-cleanup` sweep (default off) removes the whole directories of
70
- **long-inactive** sessions, unrelated to restores. Separately, a session's
71
- snapshots are cleared automatically when the conversation log's session format
72
- has changed (a DSH upgrade), because those backups are anchored to message
73
- positions that no longer line up. Both stay confined to the store root, use
74
- `lstat` (so they never follow a symlink out of the root), the sweep never targets
75
- the active session, and neither touches the conversation log. When auto-cleanup
76
- is disabled (the default), only the format-change clear runs, and only for an
77
- affected session. The store-format guard runs before that clear, so a session
78
- whose store is **newer** than this build understands is not cleared by it (see
79
- `docs/format.md`).
80
-
81
- ## Conversation integrity
82
-
83
- The session log is **append-only** — the plugin never deletes or rewrites
84
- recorded history. A rewind appends a single marker event (a `user/message` whose
85
- content is the constant `(empty message)` placeholder) whose `surfaceOp`
86
- replaces every surface node after the target. The raw log (audit trail, search,
87
- `/export`) is untouched — only the model-visible surface is cut, so the next
88
- request derives its context from the target onward. The marker's content is a
89
- constant, so it carries no untrusted text into the model context; the client
90
- hides the `[target, marker]` span from the rendered transcript.
91
-
92
- ## Filesystem containment
93
-
94
- - **Store root**: `<dsh home>/rewind-snapshots/` by default (resolved via
95
- `resolveDshHome`, so `$DSH_HOME` feeds the base and `~/.dsh` is the fallback);
96
- the `snapshotDir` config overrides the whole path. The plugin reads no
97
- environment variable of its own. Deleting the root only removes file backups
98
- and the store rebuilds from scratch.
99
- - **Path sanitization**: session ids and call ids are scrubbed to
100
- `[a-zA-Z0-9._-]` (`safeSessionId` / `safeFileId`) before joining the store
101
- root; `.` and `..` bare values are replaced — hostile ids cannot traverse
102
- out of the root.
103
- - **Links and moved directories are refused**: two checks guard a tracked path.
104
- (1) Planning skips a path whose last component is a symlink or a hard link
105
- (`lstat().nlink > 1`) and reports it, never restoring it — a symlink would
106
- redirect the write outside the checkpoint, and a hard link would clobber every
107
- other name of the same inode (e.g. pnpm-installed files); a link swapped in
108
- after planning is not re-checked. (2) At apply time, every record this build
109
- writes — entry, link and
110
- journal action — carries the `realpath` of the file's directory at commit time
111
- (the location pin; best-effort, and absent on released-v1 data or a commit
112
- whose parent could not be resolved), and a restore re-checks it before
113
- touching the path, including a post-restart continue or rollback, which reads
114
- the pin from the journal: a directory that no longer resolves there (a
115
- repointed or moved ancestor) is refused and reported instead of writing
116
- outside the recorded location, while data with no pin falls back to check (1)
117
- alone. A parent chain that
118
- is gone is still recreated (files whose directory was deleted are restorable),
119
- but only while its nearest surviving ancestor resolves inside the pin. A
120
- stable symlinked ancestor resolves identically on both sides, so a symlinked
121
- workspace or temp root is never a false skip. This matches Claude Code's
122
- checkpoint behavior since v2.1.216.
123
- - **Restores name only recorded paths**: the store contains resolved display
124
- paths of the session's own write-class tool calls (plus boundary re-checks
125
- over that same tracked set) — a restore can never write an arbitrary path.
126
- - **Path resolution rule**: relative paths resolve against the session's current
127
- working directory, read from the same service the fs tools resolve through.
128
- - **Bounded backups**: `prune` keeps the newest 100 anchor groups per session
129
- (`MAX_ANCHOR_GROUPS`), except groups a non-terminal restore journal still
130
- references (pinned so the op can be finished), so backup accumulation stays
131
- bounded by the cap plus the number of unresolved journals (the exact cap is
132
- pinned in `docs/format.md`). Across sessions, the opt-in `pruneStale` sweep
133
- removes whole **long-inactive** session directories (measured by the newest
134
- member being idle past the configured `autoCleanupMaxAgeDays`); it uses
135
- `lstat` (no symlink following), skips dot-prefixed temp files, and never
136
- targets the active session (`keepActiveId`).
137
-
138
- ## Crash safety
139
-
140
- - **Ordered commits**: a checkpoint commit places the before-sidecar (a raw
141
- byte copy) before publishing the entry JSON, so a crash can leave an
142
- unreferenced sidecar but never an entry whose bytes are missing. Every JSON
143
- write (checkpoint entries, restore journals) goes to a sibling temp file and
144
- is renamed over the target: a host crash mid-write can leave only an inert
145
- `<target>.tmp` — never a readable half-written file — and readers never pick
146
- up temp files.
147
- - **Journaled restores**: before mutating anything, the restore captures each
148
- planned path's pre-restore ("rescue") state and persists an intent journal,
149
- then marks each action done as it is applied. A crash at any point leaves
150
- the journal on disk.
151
- - **Disk is truth**: an interrupted op keeps its journal on disk, and the store's
152
- recovery API re-derives the true state from the real disk — `reconcileRestores`
153
- reports which paths already match the goal (restored) and which are pending,
154
- `continueRestore` finishes the op, `rollbackRestore` undoes it to the exact
155
- pre-restore state, and a journal whose goal is already reached auto-heals to
156
- its terminal state. The plugin never runs them on its own: an interrupted op
157
- stays interrupted until an explicit caller resumes or undoes it.
158
- - **Fail-loud vs fail-soft**: a corrupt **journal** is reported
159
- `recovery-required` — never silently dropped (dropping it would erase the
160
- interrupted restore's recovery record). Corrupt **checkpoint entries** are
161
- silently ignored (they only lose one backup, not the recovery path), and a
162
- record whose sidecar is missing or truncated fails only that file — never a
163
- delete of the live file.
164
- - **Journal IO never fails the restore**: if the journal cannot be written the
165
- restore proceeds with pre-journal semantics (crash safety degrades,
166
- behavior does not).
167
-
168
- ## Explicit non-goals
169
-
170
- - This plugin does **not** sandbox other processes or stop them from changing
171
- files concurrently.
172
- - It does **not** provide confidentiality or tamper resistance against the
173
- same operating-system user: **store** files are created with the process
174
- default permissions (a standard umask applies), and the host user remains
175
- trusted. Restored **workspace** files do carry their recorded permission
176
- bits: a content restore reapplies them (`chmod`, best-effort), while a mode
177
- difference on its own never triggers a restore.
178
- - It does **not** exclude paths from the store, and it never oversteps: the
179
- plugin only adds backup/restore on top of permissions DSH already holds. Two
180
- distinct cases follow:
181
- 1. **Sensitive / personal-information files** (e.g. a `.env` file, which may
182
- sit inside the workspace) the model may read or edit are backed up and
183
- restorable, unless the file is over the size cap.
184
- 2. **Files outside the session workspace** the model is allowed to touch are
185
- likewise backed up and restored.
186
- In both cases the plugin introduces no authority of its own — DSH already
187
- granted the read/edit; the plugin merely records a before-backup and can
188
- restore it. Keeping a path out is a **DSH model-permission** concern (a
189
- per-path deny), not a rewind feature.
190
- - It does **not** touch git (no refs, index, or worktree operations), makes
191
- **no network requests**, and does **not** access credentials.
192
- - It does **not** roll back whole-log state: telemetry, search, and `/export`
193
- still see the withdrawn messages (documented behavior, not a bug).
194
- - It does **not** restore files written by a cancelled tool call that never
195
- committed a backup.
196
- - **Subagent session edits are not tracked** (Claude Code alignment): a
197
- subagent runs its own session, so the files it changes are not backed up and
198
- cannot be restored by a rewind of the parent session; the plugin keeps no
199
- snapshot for a subagent session and offers no rewind inside one.
200
-
201
- ## Reporting
202
-
203
- Report a vulnerability through the repository's GitHub security channel or to
204
- a repository maintainer. Include: the plugin version/commit, the DSH
205
- (`@deepseek-ai/*`) version, the platform, and a minimal reproduction — and
206
- whether the failure happened before or after workspace mutation.
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ Only the latest stable release receives security fixes.
6
+
7
+ | Version | Supported |
8
+ | --- | --- |
9
+ | Latest stable `0.x` release | Yes |
10
+ | Earlier stable releases | No — upgrade to the latest release |
11
+ | `alpha` / `rc` pre-releases | No — published for testing only |
12
+
13
+ ## Reporting a vulnerability
14
+
15
+ Report privately through GitHub's private vulnerability reporting:
16
+ **[Report a vulnerability](https://github.com/SiriLee/dsh-rewind/security/advisories/new)**
17
+ — or open **Security** → **Report a vulnerability** from the repository. It
18
+ creates a private advisory that only you and the maintainer can see.
19
+
20
+ Please do not open a public issue, pull request, or discussion for a
21
+ vulnerability before a fix has been released.
22
+
23
+ Include as much of the following as you can:
24
+
25
+ - the plugin version or commit;
26
+ - the DeepSeek Harness version (`@deepseek-ai/*` packages);
27
+ - the platform and Node.js version;
28
+ - a minimal reproduction: configuration, steps, and the relevant log output;
29
+ - whether the failure happened before or after a workspace file was mutated.
30
+
31
+ If the problem is in DSH itself or in another plugin, report it to that project.
32
+
33
+ ## What to expect
34
+
35
+ - We aim to acknowledge a report within 7 days, with a first assessment
36
+ (in scope / out of scope / need more information) within a further 7 days.
37
+ - The report stays private while a fix is developed, and you are kept updated in
38
+ the advisory.
39
+ - We ask for up to 90 days before public disclosure; the advisory can be
40
+ published earlier once a fix is ready and you agree.
41
+ - With your consent, the published advisory credits you, and it enters the
42
+ GitHub Advisory Database so `npm audit` and Dependabot can flag affected
43
+ installs.
44
+ - If the report is not a vulnerability, or is out of scope, we explain why and
45
+ close the advisory.
46
+
47
+ ## Scope
48
+
49
+ **In scope** — behavior that breaks a guarantee in the security model:
50
+
51
+ - a restore that writes outside the paths and locations the session recorded, or
52
+ that runs without an explicit user action;
53
+ - path traversal, or a symlink/hard-link escape, through a session id, a store
54
+ entry, or restore planning;
55
+ - rewriting, deleting, or truncating the session log, or otherwise changing
56
+ recorded history;
57
+ - a restore crossing sessions, or touching a path the session's own tools did
58
+ not record;
59
+ - a crash or a corrupt journal that loses a live file, or that silently drops a
60
+ recovery journal;
61
+ - a hostile session-log entry or tool argument escaping the containment that
62
+ document describes.
63
+
64
+ **Out of scope**:
65
+
66
+ - attackers who already have the same OS account, the host process, or the
67
+ workspace: the host user is trusted, and store files use the process umask;
68
+ - vulnerabilities in DSH or other plugins — report them upstream;
69
+ - actions the model is allowed to take under DSH's permission model (for
70
+ example, editing a `.env` file), unless they escape that model;
71
+ - behavior the security model explicitly lists as a non-goal.
72
+
73
+ ## Security model
74
+
75
+ The guarantees, the containment rules, and the explicit non-goals are
76
+ documented at
77
+ **https://github.com/SiriLee/dsh-rewind/blob/main/docs/security-model.md**.
78
+
79
+ Reporters are welcome to test those claims: a mismatch between that document and
80
+ the code is itself worth reporting.