Skip to content

dsh-write-protect v0.2.0

Choose a tag to compare

@github-actions github-actions released this 25 Sep 03:37
· 8 commits to main since this release

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

  1. 升级前确认引擎版本: 本版要求 @deepseek-ai/dsh-* 0.1.7-rc.1 一档 (peerDependencies 同号). 停在这条线之前的部署请继续用 v0.1.1.
  2. 升级后打开插件页, 在 dsh-write-protect 卡片上重新填写保护路径与额外可写根, 并按需调整规则文件名, 各项上限与三个开关; 旧设置页保存过的值不会被读取. 保存后实时生效, 不用重启 dsh web.
  3. 若之前依赖 "danger-full-access 下 write / edit 仍然挡保护路径", 请把模式改回 read-only 或 workspace-write.
  4. 想在项目里用 "按需申请" 的玩法: 在工作区根放一份 .readonly, 用 ** (或 /**) 保护整段工作区, 再让模型按目录逐个申请; 注意这份文件本身任何工具与命令都改不了, 要改只能在 DSH 之外编辑, 或换掉 readonlyFileName.
  5. 验证方式: 让模型写 .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