npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dsh-write-protect

v0.3.0

Published

DSH sandbox extension: keep declared workspace subpaths (e.g. .git) read-only, honor a read-only rules file at the workspace root, grant extra writable roots under workspace-write, and let the model request session-scoped write access; enforced for sandbo

Readme

dsh-write-protect

给 DSH 沙箱补上工作区里某一段路径的只读保护, 典型用途是不让模型改 .git. 也可以在 workspace-write 下声明工作区外的额外可写根, 让 bash 与 write / edit 写到相邻目录, 而不必切到 danger-full-access.

工作区根还能放一份只读规则文件 (默认 .readonly), 与插件页配置里的保护路径同语义; 任务要反复写同一片受保护区域时, 模型也可以申请本会话的可写授权 (见下文两节). 会话区还有一个 "写入权限" tab: 在那里可以手动加临时可写根, 也可以把某条授权撤回, 撤回会通知模型.

write / edit 工具在所有平台都会挡住保护路径, 并放行额外可写根. bash 等命令在 Linux / macOS 上同样生效; Windows 上 bash / pwsh 既挡不住 .git, 也拿不到额外可写根. 读取不受影响.

官方沙箱只有 "整个工作区可写" 和 "全只读" 两档, 管不到工作区内部的某一段, 也不能把工作区外的个别目录并进 allow-list; Codex 一类实现默认会保护 .git, 本插件补这一块.

安装

Web 端装进 web profile:

dsh plugin --profile web add azazo1/dsh-write-protect

固定版本:

dsh plugin --profile web add azazo1/dsh-write-protect#v0.3.0

GitHub Release 同时挂不带版本号的预构建包, 安装时跳过 allowBuilds:

dsh plugin --profile web add https://github.com/azazo1/dsh-write-protect/releases/latest/download/dsh-write-protect.tgz

桌面端装进 desktop profile. 它由 Electron 应用独占管理, dsh plugin 会拒绝 --profile desktop, 所以要用应用内的插件管理器: 在插件页的安装入口填上面命令里对应的包名或 tarball 地址. 装上后重启应用, 窗口刷新一次.

安装后会接管沙箱策略和 write / edit 围栏, Linux / macOS 上还会接管命令沙箱. 改配置即时生效, 不用重启 dsh web. web 与 desktop 两个 profile 跑的是同一套 Web 应用, 桌面端只是多起一个 Host 子进程并给 <html> 打上平台标记, 所以同一份包在两边通用, 不需要分别构建.

引擎版本线要求 @deepseek-ai/dsh-* 不低于 0.2.0-rc.1, 且仍在 0.2.x 上 (peerDependencies 与 devDependencies 都写作 >=0.2.0-rc.1 <0.3.0). 已在 0.2.0-rc.2 上日常验证. 官方 SandboxProvider.confine() 自 0.1.6-alpha.1 起改为异步 (Promise<ConfinedArgv> 加 signal 参数), 插件配置自 0.1.7 起走 volatile Config (插件页的配置卡片经 configForms 读写); 本插件的覆写与配置面都按这条线走. 仍在 0.1.7 引擎线上的部署请继续用 v0.2.0, 更早的部署请继续用 v0.1.1.

配置

保护路径, 额外可写根, 规则文件名, 各项上限与三个开关的默认值都定义在 src/constants.ts 的 DEFAULT_*, patch 的 policy 行与插件页保存的值都由它们兜底; 需要部署级覆盖时在 patch 行显式给出字段 (整行替换, mode / workspaceRoot 必须带上):

- id: dsh-write-protect-policy
  name: dsh-write-protect
  config:
    mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
    workspaceRoot: !!js process.cwd()
    # 部署级覆盖示例.
    # readOnlyPaths: ['.git', '//etc/pki']
    # writablePaths: ['../shared-scratch', '//tmp/dsh-extra']
    # hardenBroker: false
    # readonlyFileName: '.readonly'   # 置空即关闭工作区规则文件识别
    # maxReadOnlyEntries: 200
    # maxGrants: 8
    # allowWritableRequests: true   # 置 false 即不许模型申请可写路径
    # watchProtectedPaths: true   # 置 false 即不监听工作区变化, 只按时间兜底
    # watchTtlMinMs: 2000
    # watchTtlMaxMs: 30000

readOnlyPaths 的每一项是一行 gitignore 语义的模式, 数组逐行合并为生效文本:

  • 不含 / 的条目 (如 .git, vendor) 在工作区内任意层级匹配, 覆盖嵌套仓库等场景.
  • 以 / 开头或含中间 / 的条目锚定到工作区根 (如 /.git, dist/a.txt); 字面条目即使尚不存在也保留保护, 例如 git init 之前的 /.git.
  • // 开头的条目是文件系统绝对路径 (如 //etc/pki), 这是本插件额外支持的写法, gitignore 没有这种形态.
  • 尾部 / 表示只匹配目录 (如 build/).
  • 通配: * 匹配单段内任意字符, ? 匹配单字符, [...] 字符类 (含 [:alpha:] 等 POSIX 类), ** 独立成段时递归 (如 a/**/b); \ 转义下一字符 (\#, \!, 尾部空格用 \ 保留).
  • ! 开头剔除匹配项, 按 gitignore 的 last-match-wins 顺序解释; 受保护目录内部无法通过取反重新放行后代.
  • write / edit 按模式逐路径判定, 与路径是否存在无关; bash / pwsh 侧的保护路径是枚举出来的, 只覆盖展开时刻已存在的路径. 解析时会解开符号链接并去重.
  • 置为空列表 [] 即停用保护 (插件仍在, 只是不再多挡任何路径).

writablePaths 的每一项是一行字面路径, 不是 gitignore glob:

  • 行首 ~ 或 ~/... 展开为当前用户家目录; ~other 不支持.
  • $NAME 与 ${NAME} 展开为环境变量; 未设置或空值的变量整行丢弃并告警. \$ 保留字面 $.
  • 宿主绝对路径 (/tmp/extra) 或 // 前缀 (//tmp/extra) 按文件系统解析.
  • 其余相对当前会话工作区, 含 .. (如 ../sibling-project).
  • 工作区内的路径本来就可写, 展开时忽略并告警; 文件系统根 (/ 或盘符根) 拒绝, 避免把只读宿主根整棵翻成可写.
  • 不支持通配与 ! 取反. 不存在的路径仍保留词法形态: write / edit 与 Seatbelt 可按前缀放行, bwrap / Landlock 在叠加时跳过并告警.
  • 只在 workspace-write 下并进 allow-list, 不打穿 read-only. 保护路径优先: 额外根内部仍可被保护.
  • 置为空列表即不额外放行.

hardenBroker 是 macOS broker 逃逸加固的部署 base, 布尔值, 缺省 true:

  • 开启时在 Seatbelt profile 末尾追加 broker 拒绝形式 (见 "保护范围").
  • 关掉后命令按官方 profile 运行, 只影响 macOS, 只影响这一个加固; 保护路径与额外可写根照常.
  • 用户在插件页拨动开关后该值不再生效.

readonlyFileName / maxReadOnlyEntries / maxGrants / allowWritableRequests / watchProtectedPaths / watchTtlMinMs / watchTtlMaxMs 同理, 是只读规则文件, 可写申请与命令侧刷新的部署 base (见后两节), 用户保存过对应字段后该值不再生效.

源码分三块, 边界是"有没有文件系统依赖":

| 文件 | 职责 | 依赖 | |---|---|---| | src/gitignore.ts | gitignore 语义的解析与逐路径匹配 (PatternSet.match), 规则文件校验与可写申请的保护判定共用 | 纯字符串/正则, 零运行时依赖 | | src/patterns.ts | 把模式枚举成具体路径, 供保护路径清单与命令沙箱使用 | node:fs、canonicalPath | | src/fs.ts / src/policy.ts / src/provider.ts | 三个挂载点: write/edit 围栏、沙箱策略、进程沙箱 argv 叠加 | DSH 引擎 |

src/readonly-file.ts 负责工作区只读规则文件的读取与缓存, src/path-expand.ts 负责额外可写根的字面路径展开, src/request-writable-path.ts 是可写申请的授权表与 request_writable_path 工具, src/grants-route.ts 与 src/grant-notice.ts 分别是会话面板的 /api 路由与撤回通知.

插件页配置

插件页里 dsh-write-protect 卡片上的配置表单, 从上到下是: 保护路径与额外可写根两个多行文本, 工作区只读规则文件名与最多条目数, 单会话可写授权上限, macOS broker 逃逸加固, 模型申请可写路径, 监听工作区变化, 以及刷新下界与上界两个毫秒值. 草稿只留在卡片里, 点 "保存" 才写进 profile 的 patch 层并实时生效; 预览面板按当前草稿展开, 不必先保存:

# 保护路径
.git
secrets/*.pem
!secrets/example.pem
# 额外可写根
../shared-scratch
~/scratch
$HOME/scratch
/tmp/dsh-extra
  • 保护路径: # 开头是注释, 空行忽略; ! 排除, 按最后匹配生效; 不能在仍受保护的目录内部重新放行后代. 通配与锚定语义同 "配置" 一节. Windows 上的绝对条目写作 //C:/Users/me/secret: gitignore 语义里 \ 是转义符, / 才是分隔符.
  • 额外可写根: 每行一条字面路径, 不要通配. ~ / ~/... 为家目录, $NAME / ${NAME} 为环境变量; 绝对路径按文件系统解析, 相对路径 (含 ..) 相对当前会话工作区. Windows 上 \ 是分隔符而不是转义符, C:\Users\me\caches 与 ~\caches 都按字面解析; 盘符相对路径 (C:caches) 的落点取决于进程当前目录, 会被拒绝并出现在 "未生效" 里.
  • 两份文本都按当前会话的工作区根解析, 每个会话各自生效. 开关是全局的, 与工作区无关.
  • 预览按钮把当前草稿交给 Host 展开, 不必先保存: 列出生效的保护路径与额外可写根, 工作区规则文件的条目, 本会话已批准的可写授权, 以及被忽略或拒绝的行. 展开使用当前选中会话的 cwd (即被主视图持有的那条会话记录); 没有选中会话时预览直接报错, 不回退部署工作区根 —— 部署根是进程 cwd, 可能是一棵极大的树, 在那里同步展开会堵住 Host 事件循环. 这一页只列不改: 加临时可写根与撤回授权在会话区的 "写入权限" tab 上 (见下节).

mode 与 workspaceRoot 是官方 policy 行字段的复述 (patch 对整行配置做替换, 必须带上), 取值语义与 base bundle 一致.

只读规则文件

工作区根可以放一份与 "保护路径" 同语义的规则文件 (默认 .readonly, 可在插件页配置里改名或置空关闭), 逐行追加在保护路径文本之后:

# <工作区根>/.readonly
secrets/
/vendor
!vendor/public/**
**
  • 只认工作区根这一份, 每个会话各读一份; 整段保护工作区写 ** 或 /**, 根下第一层用 /*; / 与 . 匹配不到任何路径, 等于什么都不保护.
  • 只接受普通文件 (符号链接拒绝); // 绝对条目与越出工作区的 .. 条目拒绝, 超限条目丢弃, 都只告警不生效.
  • 这份文件本身是唯一的硬保护: 在 read-only 与 workspace-write 下任何授权都不放行它, 也没有 request_writable_path 可受理它, 会话面板的手动添加同样拒绝它; 要改只能换文件名或由你在 DSH 之外编辑. (整个 danger-full-access 模式下本插件不介入, 见 "边界与已知限制".)

模型申请可写路径

提示词会引导模型: 任务要反复写同一片受保护区域时 (一个目录里的多个文件, 构建产物树, 若干次写入都依赖的工作区外路径), 调 request_writable_path 申请本会话的可写授权, 参数是 path (字面路径, 支持 ~ / $VAR / // / 相对含 ..) 与 justification (给你看的一句话理由); 单个文件照旧用 write / edit 工具, 那次被拒就算了.

审批理由会写明在放开什么: 工作区外的路径批准后成为本会话的额外可写根 (bash 与 write / edit 都能写), 工作区内被保护路径则放开那棵子树 —— write / edit 与命令沙箱 (macOS Seatbelt 的 allow, Linux bwrap 的 bind) 都认这份授权. 授权按路径生效且覆盖其下全部后代, 所以批过父目录之后再写子目录、子文件不会再次弹窗. 授权只在本会话内存里, 不写 settings, 条数上限 maxGrants (默认 8); 关掉 allowWritableRequests 后工具的任何调用都被拒, 提示词也不再引导. 保护路径与规则文件条目都在可申请范围内, 因此可以配成 "整个工作区只读 + 逐个目录批准".

批准过的授权可以在会话区撤回, 撤回之后模型会收到一条通知 (见下节).

会话写入权限面板

会话区 (与 "对话" / "轨迹" 并排) 有一个 "写入权限" tab, 它只处理本会话内存里的授权, 也就是 request_writable_path 批准过的那几条:

  • 列出本会话当前的授权: 路径 + 类型 (保护旁路 / 额外可写根), 以及当前用掉了几条 (maxGrants 上限).
  • 每条后面一个 "撤回": 删掉这条授权, 那片路径立刻回到保护判定里 —— 后续的 write / edit 与命令沙箱都按没批过处理. 已经跑起来的命令不受影响, 命令侧的挂载与 profile 在启动时就定好了.
  • 下面的输入框手动加临时可写根: 支持 ~ / $VAR / .. / 相对会话工作区, 落点在工作区内时记作保护旁路 (放开命中它的保护条目), 在工作区外时记作额外可写根. 记录与模型申请批准后的完全同质: 同样只在本会话内存里, 同样受 maxGrants 限制, 不写配置文件 —— 所谓 "临时" 就是这个意思, 会话结束或重启 dsh 之后自动消失.
  • 当前会话是 read-only 时面板会提示: 这时加进来的授权要等会话切到 workspace-write 或 danger-full-access 之后才生效.

撤回之后会往该会话投一条模型可见的消息, 说明用户撤掉的是哪条路径, 那里已经重新受保护, 需要时应当重新申请而不是硬写. 提示词里那份授权清单是状态 (撤回之后那一行自己就没了), 这条消息补的是事件 (谁撤的, 撤的是哪条), 两者不是重复: 只看清单的话, 模型可能正按旧快照继续往那里写, 撞上拒绝之后开始瞎试.

投递走 agent.inject, 也就是排进模型下一步而不唤醒空闲会话 (不会因为一次撤回就替用户烧掉一轮 token); 消息本身按 user/message 写进会话日志, 因此 dsh 重启、同一会话恢复之后模型照样看得到. 这条通知没有落盘记录, 也不做跨会话的 "撤回名单": 授权本来就是会话级的, 新建会话没有历史授权可谈, 需要长期不写的位置请写插件页配置或工作区规则文件.

面板本身是 Host 的 /api 路由 (/api/dsh-write-protect.grants), 调用者就是用户本人, 因此不经过审批弹窗; 路径解析与保护判定复用模型申请那一套, 规则文件本身同样不接受授权.

保护范围

保护路径与额外可写根会同时作用在下面几个入口:

| 入口 | 哪些系统 | 效果 | |---|---|---| | write / edit 工具 | 全平台 | 按保护模式逐路径判定, 命中即拒绝; workspace-write 下额外根放行; danger-full-access 下全部放行 | | bash 等命令 | Linux, macOS | 内核级只读 / 额外可写; macOS 上字面条目按正则拒绝, 与路径当前是否存在无关; Windows 做不到, 见下方限制 | | 提示词 | 全平台 | 先告诉模型哪些不能写, 哪些额外根可写, 需要反复写时怎么申请 | | 本会话授权 (request_writable_path) | 全平台 | 工作区外路径按额外根生效; 保护旁路对 write / edit 与命令沙箱 (bwrap / Seatbelt) 都生效 | | 会话写入权限面板 | 全平台 | 列出并撤回本会话授权, 手动加临时可写根; 撤回后向会话投一条通知消息 | | macOS broker 加固 | macOS | 堵住 open 经 launchd 把命令挪到沙箱外执行 |

两类入口的判定方式不同, 这是有意的: write / edit 拿得到目标路径, 因此直接按 gitignore 模式判定 —— 深层嵌套, 尚未存在的匹配一样挡得住, 每条写入只做几次正则; bash 的沙箱 (mount / profile) 只能吃具体路径, 所以那一侧才需要枚举展开. 枚举走 fs.promises, 每次 readdir / lstat 让出事件循环; 同步的 resolve() 只注入模式原文和缓存里已有的路径, 不在会话加载时扫盘. 提示词同样只陈述模式, 不枚举绝对路径.

macOS 上命令侧还有第二条通道: Seatbelt profile 支持按正则匹配路径, 因此字面条目会被直接翻译成 (deny file-write* (regex ...)), 不经过枚举 —— 展开当时还不存在的路径 (会话中途 git init 出来的 .git) 同样挡得住, 也不受展开缓存窗口影响. 含通配或转义的条目仍走枚举清单, 原因见下方限制.

命令侧清单的保鲜

bwrap 只能挂载真实存在的目录, 所以命令侧的保护清单来自一次"展开", 而展开结果会缓存. 会话中途才出现的受保护路径 (例如刚 git init 出来的 .git) 若不在清单里, 那条命令就写得进去. 为此有两层保障:

  • 正则拒绝 (macOS): 字面条目不经过展开, 见上一节.
  • 监听 + 自适应刷新 (全平台): 只给"正在运行 agent 的会话"的工作区根装递归监听; agent 跑完或会话销毁就摘掉, 空闲不占资源. 监听一报变化就立即在后台重算清单 (短时间内的多次事件合并成一次), 所以下一条命令直接用新清单, 不必等缓存过期.
  • 时间兜底: 没有事件时, 按"上次展开耗时 × 10"兜底重算, 并夹在 watchTtlMinMs (默认 2 秒) 与 watchTtlMaxMs (默认 30 秒) 之间; 监听装不上或漏事件时不会一直陈旧. 关掉 watchProtectedPaths 后不装监听, 只剩这一层.
  • 命令遇到已过期的缓存时会等这次扫描完成再执行, 因此刷新不会变成"先放行、事后补救".

这一步只影响命令侧. write / edit 围栏始终按模式原文判定, 与展开、监听、缓存都无关.

主场景是 workspace-write. read-only 下官方已挡住全部文件写入, 额外可写根不打穿; 但官方 profile 的 (allow default) 在两种模式下都一样, 所以 broker 加固不区分模式.

macOS broker 逃逸加固

官方 macOS profile 是 (version 1) (allow default) (deny file-write*) ..., mach-lookup 与 process-exec 全开. 而经 launchd 代理启动的进程不继承 Seatbelt profile, 于是沙箱内一条 open x.app 就能让启动的进程在沙箱外任意读写, deny file-write* 被整条绕开 —— read-only 同样会被打穿. 本插件在 profile 末尾追加:

(deny mach-lookup (global-name-prefix "com.apple.coreservices"))
(deny appleevent-send)
(deny mach-priv-task-port)

SBPL 按 last-match-wins 解释, 追加在末尾才能盖过 (allow default). com.apple.coreservices 是 LaunchServices 的服务名段, open / NSWorkspace 靠它把请求交给 launchd; 名称过滤器按 reverse-DNS 分段匹配, 所以只能整段拒绝, 收窄到子服务无效. appleevent-send 关掉让别的 app 代劳那条路, mach-priv-task-port 关掉注入已运行进程的 task port.

加固只做收紧, 不放宽任何位置; 常规命令 (node, git, pnpm, python, curl, tar, rsync 等) 不受影响.

插件页的 "macOS broker 逃逸加固" 开关与 patch 的 hardenBroker 控制这一个加固是否生效, 缺省开启. 关掉后 provider 原样返回官方 argv, 适合确实需要从沙箱内驱动宿主 GUI 的场景; 关掉即恢复可以被 open 打穿的状态. 保护路径与额外可写根的叠加不受这个开关影响.

patch 配置和插件页文本走同一套解析.

边界与已知限制

  • Windows 上 bash 挡不住, 也放不宽: write / edit 能挡保护路径、能放行额外根; bash / pwsh 两者都不行. Windows 沙箱只能把整个工作区设成可写或不可写.
  • broker 加固只在 macOS 生效: 官方 macOS profile 的 (allow default) 让 open 能把命令交给 launchd 在沙箱外跑, 本插件追加的拒绝形式堵住这条路. Linux 的 bwrap 用 mount namespace, 没有 launchd 那类代理通道, 但它的网络命名空间未隔离, 沙箱内仍可连宿主守护进程 (Docker socket, ssh-agent 一类) 让外面代劳, 这类问题本插件不处理.
  • Linux 没有 bwrap, 落到 Landlock 时: 没法单独保护子路径, 命令按官方沙箱跑并告警一次; 额外可写根可以加 --rw. write / edit 两者都生效.
  • 完全放开沙箱时 (danger-full-access): 本插件整体不介入 —— bash 不进沙箱, write / edit 的保护路径与规则文件判定也跳过. 该模式是用户显式选择的"不设限", 保护只在 read-only 与 workspace-write 下生效.
  • Linux bwrap 要求路径真实存在: 通配扫出来的保护路径如果当时还不在磁盘上, 会跳过这条只读挂载并告警. 需要无条件保护的工作区根路径请用字面条目 (如 /.git); 字面条目即使还不存在, write / edit 也会拒绝.
  • 命令侧的枚举窗口 (macOS 字面条目除外): Linux bwrap / Landlock 与 macOS 上的通配条目只能按枚举出来的路径保护. 有 watcher 时, 变化之后的下一条命令就会吃到新清单; watcher 不可用时退回时间兜底 (最多 watchTtlMaxMs, 默认 30 秒). macOS 上的字面条目没有这个窗口 (按正则拒绝, 见上节), 因此 git init 也建不出 .git. 已经要保护的目录不会再往里扫, 里面的匹配项不再单独列出; 被 ! 放行的目录还会继续找. 目录符号链接不跟随, 避免扫到工作区外. write / edit 不受这条限制: 它直接按模式判定.
  • 正则拒绝只覆盖字面条目, 且遇到 ! 就整体放弃: 含 * ? [ 或 \ 的条目仍走枚举清单; 文本里只要出现 ! 取反, 命令侧就退回纯枚举 (纯 deny 表达不了 last-match-wins). 目录标记条目在正则通道上不区分目标是文件还是目录, 同名文件也会被一并挡住, 属于收紧.
  • 尾部 /** 按那个目录本身保护: 和保护其下全部后代等价, 同时避免枚举全部后代, 代价是该目录自己也写不了.
  • 规则文件的条目同样受缓存窗口影响: 规则文件内容按 1 秒 TTL 重读, 改完最迟 1 秒后按新内容判定; 新增的匹配路径要等下一次枚举 (监听报变化后立即重算, 没有事件时最迟 watchTtlMaxMs, 默认 30 秒) 才会进命令沙箱的清单, write / edit 侧立刻按新条目判定.
  • 授权是会话级内存态: request_writable_path 得到的授权, 以及会话面板里手动加的临时可写根, 都不跨会话, 也不写配置文件; 撤回只是删掉这条内存记录, 不产生任何持久状态. 需要长期生效的额外可写根请写插件页配置或 patch.
  • 撤回通知依赖该会话有活着的 agent: 投递对象是会话的 agent runtime; 该会话当前没有活动 agent 时授权照常撤回, 但通知投不出去, 面板会把这件事如实写出来 (不假装模型已经知道).
  • 授权在 bwrap 上要求路径真实存在: 命令侧要把授权子树 bind 回可写, bwrap 要求挂载源存在; 还不存在的授权路径在命令侧要等它被创建出来 (write / edit 与 Seatbelt 不受这条限制).
  • Landlock 上没有命令侧的授权: 纯 allow-list 无法表达"父目录只读, 其中一棵子树可写", 强行加 --rw 会连上方一起放开, 因此这种 runner 下授权只作用于 write / edit (告警一次).
  • 指向保护目录内部的符号链接会被拒绝, 指向外部的不受影响.

本地开发

just install    # 安装依赖
just typecheck  # TypeScript 类型检查
just build      # 构建 lib/
just test       # 测试套件 (Seatbelt e2e 仅在 macOS 上运行)
just verify     # 以上全流程 + 打包预览

源码分三块, 边界是"有没有文件系统依赖":

| 文件 | 职责 | 依赖 | |---|---|---| | src/gitignore.ts | gitignore 语义的解析、编译与逐路径匹配 (含 PatternSet.match), write / edit 围栏与可写申请的保护判定共用 | 纯字符串/正则, 零运行时依赖 | | src/patterns.ts | 把模式异步枚举成具体路径, 供命令沙箱与预览面板使用 (另有额外可写根的字面路径解析) | node:fs/promises、canonicalPath | | src/fs.ts / src/policy.ts / src/provider.ts | 三个挂载点: write/edit 围栏、沙箱 policy (含规则文件与授权)、进程沙箱 argv 叠加 | DSH 引擎 |

src/readonly-file.ts 读取并校验工作区只读规则文件, src/request-writable-path.ts 是 request_writable_path 工具与会话授权表 (含撤回), src/path-expand.ts 负责额外可写根的字面路径展开 (~ / 环境变量 / 平台差异), src/grants-route.ts 是会话写入权限面板的 /api 路由, src/grant-notice.ts 负责撤回之后投给会话的那条通知.

测试覆盖: 纯匹配器语义 (锚定, **, 字符类, 取反, 前缀围栏, 目录标记, 大小写, 工作区外不match; 不需要任何临时目录), 路径解析语义 (相对锚定, 解开符号链接, 去重, 通配枚举与取反, 额外可写字面路径), 只读规则文件的解析与校验 (符号链接拒绝, 绝对与越界条目, 条目上限, 缓存与重读), 可写申请的判定矩阵 (直通, 规则文件硬保护, 工作区内旁路, 超限, 四种未获同意的结果) 与授权落到 policy 的通道, 会话面板路由的三个动作 (清单, 手动添加的内外分类与拒绝, 撤回与通知投递), 撤回通知消息的来源标记, 面板数据面的请求体与失败解读, write / edit 按模式判定 (启动后才出现的深层路径、尾部 / 与同名文件、工作区边界), bwrap / Seatbelt / Landlock 的命令行叠加, write / edit 工具的拒绝与额外根放行矩阵, 配置通道的 base 与用户覆盖分层, client bundle 的 loader 注册, macOS 上真实 sandbox-exec 的内核级端到端 (包括 open broker 逃逸的对照组与加固后的拦截验证), 以及 Linux 上真实 bwrap 的内核级端到端 (保护路径写入 EROFS, 读取照常, 额外可写根可写; 本机 bwrap 不可用时整组跳过).

License

MIT