Repository navigation
dsh-write-protect v0.2.0
dsh-write-protect v0.2.0
Date: 2026-09-25
这一版把写入保护从 "一份静态清单" 扩成 "可声明, 可申请, 可保鲜" 的一整套: 工作区根可以放一份只读规则文件, 模型能在任务确实需要时申请本会话的可写授权, 命令侧的清单也会跟着工作区变化及时重算. 配置入口从 Web 设置页的独立页面迁到插件页的配置卡片, 引擎版本线抬到 @deepseek-ai/dsh-* 0.1.7-rc.1 —— 仍在更早引擎线上的部署请先读 Upgrade Notes, 继续使用 v0.1.1.
Highlights
- 新增
request_writable_path工具: 任务要反复写同一片受保护区域时, 由模型提出申请, 你在审批弹窗里逐次决定; 授权按路径生效并覆盖其下全部后代, 且只在本会话内存里. - 新增工作区只读规则文件 (默认
.readonly): 与 "保护路径" 同语义, 随工作区走, 因此可以配成 "整个工作区只读 + 逐个目录批准"; 这份文件自身是唯一硬保护, 任何授权都不放行. - 命令侧清单开始保鲜: macOS 上字面条目改按 Seatbelt 正则拒绝, 会话中途
git init出来的.git也挡得住; 全平台按 agent 运行状态监听工作区, 变化之后立即重算. - 配置迁到插件页卡片: 两个多行文本, 规则文件名与各项上限, 三个开关, 刷新下界与上界都在 dsh-write-protect 卡片上, 预览面板按草稿展开, 不必先保存.
danger-full-access下完全不再介入 (含规则文件硬保护), 与 "保护只在read-only/workspace-write下生效" 的口径对齐.
Breaking Changes
- 引擎版本线: 依赖抬到
@deepseek-ai/dsh-*0.1.7-rc.1 (cordis^4.0.4), 配置面改用 volatile Config 与configForms, 并新增@deepseek-ai/dsh-tools这条 peer. 迁移方式: 先把 DSH 升到 0.1.7-rc.1 一档; 仍在 0.1.6 及更早引擎线上的部署继续用v0.1.1, 不要升到本版. - 配置入口迁移: Web 设置页里独立的 "写入保护" 页被移除, 改由插件页的 dsh-write-protect 卡片承载. 旧页面保存过的值 (保护路径, 额外可写根, broker 加固开关) 走的是 settings namespace, 本插件不再读取; patch 层的
readOnlyPaths/writablePaths/hardenBroker照常作为部署 base 生效. 迁移方式: 打开插件页, 在卡片上按 Upgrade Notes 重新填写. danger-full-access行为变更: 该模式下不再应用写保护围栏, 也不再应用规则文件硬保护 (此前 write / edit 仍会拒绝保护路径). 迁移方式: 需要保护就改用read-only或workspace-write, 这个模式的语义就是 "不设限".- 预览不再回退部署工作区根: 没有当前会话 (或会话记录里没有 cwd) 时预览直接报错, 不再拿
dsh web的启动目录去展开. 迁移方式: 先在界面上选中一个会话再点预览.
Upgrade Notes
- 升级前确认引擎版本: 本版要求
@deepseek-ai/dsh-*0.1.7-rc.1 一档 (peerDependencies 同号). 停在这条线之前的部署请继续用v0.1.1. - 升级后打开插件页, 在 dsh-write-protect 卡片上重新填写保护路径与额外可写根, 并按需调整规则文件名, 各项上限与三个开关; 旧设置页保存过的值不会被读取. 保存后实时生效, 不用重启
dsh web. - 若之前依赖 "
danger-full-access下 write / edit 仍然挡保护路径", 请把模式改回read-only或workspace-write. - 想在项目里用 "按需申请" 的玩法: 在工作区根放一份
.readonly, 用**(或/**) 保护整段工作区, 再让模型按目录逐个申请; 注意这份文件本身任何工具与命令都改不了, 要改只能在 DSH 之外编辑, 或换掉readonlyFileName. - 验证方式: 让模型写
.git里的文件, 应当被拒并提示可以申请授权; 让它在会话中途执行git init后立刻写.git, macOS 与 bwrap 可用的 Linux 上都应当被拒; 用卡片的预览按钮确认生效路径, 规则文件条目与本会话授权三组列表.
Features
- 可写申请:
request_writable_path接受path(字面路径, 支持~/$NAME//// 相对含..) 与justification(给你看的一句话理由), 经审批弹窗逐次同意. 工作区外的目标成为本会话的额外可写根 (bash 与 write / edit 都能写); 工作区内被保护的目标只放开那棵子树. 授权按路径生效并覆盖后代, 批过父目录之后再写子目录不会再次弹窗; 上限maxGrants(默认 8), 只存在内存里, 不跨会话也不写配置. - 申请开关
allowWritableRequests(默认开启): 关掉后工具仍然注册, 但任何调用都被拒且不弹审批, 提示词改为告知模型本部署不授予额外写权限. - 命令沙箱认本会话授权: bwrap 在保护路径的只读挂载之后追加授权子树的
--bind, Seatbelt 在保护 deny 之后追加授权 allow, 顺序保证授权不被只读压回去; Landlock 是纯 allow-list, 不叠加并告警一次 (这种 runner 下授权只作用于 write / edit). - 工作区只读规则文件: 只认工作区根那一份, gitignore 语义, 逐行追加在卡片文本之后 (因此既能新增保护条目, 也能用
!放行卡片里的条目). 只接受普通文件 (符号链接拒绝);//绝对条目与越出工作区的..条目拒绝; 条目超过上限 (默认 200) 截断; 三类情况都只告警. 文件名与上限可在卡片或 patch 里配置, 文件名置空即关闭这项识别. - 命令侧清单保鲜: macOS 上字面条目直接翻译成
(deny file-write* (regex ...)), 覆盖条目本体与全部后代, 与路径当时是否存在, 展开缓存新旧都无关; 含通配或转义的条目, 以及文本里出现!取反时, 退回枚举清单. 全平台另按 agent 运行状态给工作区根装递归监听 (同一根被多个会话共用时最后一个结束才摘), 事件在 50ms 窗口内合并后立即后台重扫; 没有事件时按 "上次展开耗时 x 10" 兜底, 夹在watchTtlMinMs(默认 2 秒) 与watchTtlMaxMs(默认 30 秒) 之间. - write / edit 围栏改为按保护模式原文逐路径判定, 不再依赖扫盘得到的清单: 深层嵌套与尚不存在的匹配一样挡得住, 每条写入只做几次正则; 拒绝信息会指出命中的模式与围栏起点, 并提示可以申请本会话授权 (#1, @readlotusx).
- 插件页卡片: 保护路径与额外可写根两个多行文本, 只读规则文件名, 最多条目数, 单会话可写授权上限, macOS broker 逃逸加固, 模型申请可写路径, 监听工作区变化, 刷新下界与上界; 预览面板新增规则文件条目与本会话授权两组, 并按当前草稿展开.
- 提示词: 改为陈述生效的保护模式原文 (不再是枚举出来的绝对路径), 并说明规则文件来源, 本会话授权与申请工具的用法; 保护文本为空时不输出该段.
Bug Fixes
- 兼容 DSH 0.1.6 起改为异步的
confine(): 官方把SandboxProvider.confine()的返回值换成Promise<ConfinedArgv>并加了signal参数之后, 插件原先的同步覆写会在那个 Promise 上读.argv并直接抛错, 于是每条命令都失败, 表现为载入插件后沙箱整体不可用; 现在await官方结果后再叠加保护路径与额外可写根, 并把官方signal原样透传, 取消语义一并恢复 (#1, @readlotusx). - macOS 命令侧: 会话中途才出现的受保护路径 (展开当时还不存在的
.git) 以前挡不住, 叠加展开缓存窗口后表现为时灵时不灵; 字面条目现在按正则拒绝, 不经过枚举, 也不受缓存影响. - provider 取 policy service 改用
ctx.get('sandboxPolicy'): 之前直接读ctx.sandboxPolicy, 真实实例里会因为官方LocalSandboxProvider未声明该 inject 而抛错并落到退路 (单测的 Context 不活跃, 走的是另一条分支, 所以没暴露). - 可写申请里的相对路径先按工作区根解析: 之前申请工作区内的相对路径会被判成工作区外 (审批文案说错), 批准之后写入仍然被拒 —— 授权按相对形态记录, 下一次展开又把它当成工作区内条目丢掉.
- 保护路径展开不再回退部署根: 从家目录启动
dsh web时, 无会话的resolve()会把.git这类非锚定条目展开到部署根上, 实测同步扫了 101.5 秒, 期间事件循环被占死 (审批弹窗不出现, HTTP 全挂). 现在没有会话 cwd 就不展开, 预览也必须带当前会话 cwd, 否则报 400. - 预览改用被主视图持有的那条会话判定当前会话:
sessions.list快照里没有current字段, 之前读它恒为undefined, 于是预览请求不带工作区根, Host 侧一律报 400. - 会话授权覆盖其下全部后代: 批过父目录之后再申请它的子目录不再重复弹窗 (此前 override 那一路会漏).
- 只读展开缓存与会话授权解耦: 授权批准不再作废展开缓存, 也就不会紧接着触发一次同步全量扫盘, 批准后的卡顿消失.
danger-full-access下不再应用写保护围栏, 规则文件硬保护也不再介入, 与该模式 "用户显式选择不设限" 的语义一致.
Performance
- 保护路径展开不再发生在同步的
resolve()里: 遍历改走fs.promises, 每次 readdir / lstat 让出事件循环, 结果进缓存;confine()与预览走异步的materialize(). 此前工作区一大, 一次resolve()就能把 Host 事件循环占死几十秒到几分钟, 表现为整个 Web 连首页都打不开. - 监听只服务正在运行 agent 的会话, agent 跑完或会话销毁即摘除, 空闲时不占资源; 同一次展开的并发请求会合并到同一次扫描上.
- write / edit 的每条判定只做几次正则, 与工作区大小无关.
Known Limitations
- Windows 上 bash / pwsh 既挡不住保护路径, 也拿不到额外可写根; write / edit 两者都生效.
- Linux 落到 Landlock 时无法单独保护子路径, 命令按官方沙箱跑并告警一次; 额外可写根可以加
--rw, 本会话授权在这种 runner 下只作用于 write / edit. - 命令侧的授权与 bwrap 都要求路径真实存在: bwrap 挂不上还不存在的授权子树时会跳过并告警, 等目录出现后下一条命令生效 (write / edit 与 Seatbelt 不受这条限制).
- 命令侧的枚举窗口仍然存在 (macOS 字面条目除外): Linux bwrap / Landlock 与 macOS 上的通配条目只能保护枚举出来的路径. 有监听时改动后的下一条命令就吃到新清单, 监听不可用时退回时间兜底 (最多
watchTtlMaxMs, 默认 30 秒). - 正则拒绝只覆盖字面条目, 且文本里只要出现
!取反就整体退回枚举 (纯 deny 表达不了 last-match-wins); 目录标记条目在正则通道上不区分文件与目录, 同名文件也会被一并挡住, 属于收紧. - 规则文件内容按 1 秒 TTL 重读, 改完最迟 1 秒后按新内容判定; write / edit 侧立刻按新条目判定, 命令侧要等下一次枚举才会进清单.
- 授权是会话级内存态: 不跨会话, 也不写配置文件; 需要长期生效的额外可写根请写卡片配置或 patch.
- broker 加固只在 macOS 生效, 且属于纵深加固: 它关掉的是 launchd / LaunchServices 这条代理通道, 沙箱内的进程仍可通过其他本地守护进程 (Docker socket, ssh-agent 一类) 让沙箱外的服务代劳, 这类问题本插件不处理.
Thanks
- @readlotusx (PR #1): 异步
confine()的适配 (含 Linux 上真实 bwrap 的内核级端到端用例) 与 write / edit 围栏按保护模式逐路径判定的实现, 具体效果见上面 Bug Fixes 与 Features 两节.
What's Changed
- 适配dsh 0.1.6 alpha by @readlotusx in #1
New Contributors
- @readlotusx made their first contribution in #1
Full Changelog: v0.1.1...v0.2.0