api-relay-audit
toby-bridges
Local security audit for AI API relays and LLM proxies: detects prompt injection, model substitution, tool-call rewriting, SSE anomalies, error leakage, and Web3 wallet risks.
PROJECT TOPICS
INSTALL REFERENCE
dsh plugin --profile web add github:azazo1/dsh-write-protect
该命令指向仓库当前默认分支;尚无绑定当前 commit 的完整验证结果。
PROJECT README
给 DSH 沙箱补上工作区里某一段路径的只读保护, 典型用途是不让模型改 .git. 也可以在 workspace-write 下声明工作区外的额外可写根, 让 bash 与 write / edit 写到相邻目录, 而不必切到 danger-full-access.
write / edit 工具在所有平台都会挡住保护路径, 并放行额外可写根. bash 等命令在 Linux / macOS 上同样生效; Windows 上 bash / pwsh 既挡不住 .git, 也拿不到额外可写根. 读取不受影响.
官方沙箱只有 "整个工作区可写" 和 "全只读" 两档, 管不到工作区内部的某一段, 也不能把工作区外的个别目录并进 allow-list; Codex 一类实现默认会保护 .git, 本插件补这一块.
dsh plugin --profile web add azazo1/dsh-write-protect
固定版本:
dsh plugin --profile web add azazo1/dsh-write-protect#v0.1.1
GitHub Release 同时挂不带版本号的预构建包, 安装时跳过 allowBuilds:
dsh plugin --profile web add https://github.com/azazo1/dsh-write-protect/releases/latest/download/dsh-write-protect.tgz
安装后会接管沙箱策略和 write / edit 围栏, Linux / macOS 上还会接管命令沙箱. 改配置即时生效, 不用重启 dsh web.
引擎版本线跟随 @deepseek-ai/dsh-* 的 0.1.6-alpha.1 (peerDependencies 同号). 官方 SandboxProvider.confine() 自 0.1.6-alpha.1 起改为异步 (Promise<ConfinedArgv> 加 signal 参数, 0.1.5-rc.1 仍是同步签名), 本插件的覆写同样异步; 还在 0.1.5 及更早引擎上的部署请继续用 v0.1.1.
保护路径的默认值统一定义在 src/constants.ts 的 DEFAULT_READ_ONLY_PATHS, 额外可写根默认空列表 (DEFAULT_WRITABLE_PATHS), macOS broker 加固默认开启 (DEFAULT_HARDEN_BROKER); patch 的 policy 行与设置页部署 base 都由它们兜底. 需要部署级覆盖时在 patch 行显式给出数组或开关:
- 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
readOnlyPaths 的每一项是一行 gitignore 语义的模式, 数组逐行合并为生效文本:
/ 的条目 (如 .git, vendor) 在工作区内任意层级匹配, 覆盖嵌套仓库等场景./ 开头或含中间 / 的条目锚定到工作区根 (如 /.git, dist/a.txt); 字面条目即使尚不存在也保留保护, 例如 git init 之前的 /.git.// 开头的条目是文件系统绝对路径 (如 //etc/pki), 这是本插件额外支持的写法, gitignore 没有这种形态./ 表示只匹配目录 (如 build/).* 匹配单段内任意字符, ? 匹配单字符, [...] 字符类 (含 [:alpha:] 等 POSIX 类), ** 独立成段时递归 (如 a/**/b); \ 转义下一字符 (\#, \!, 尾部空格用 \ 保留).! 开头剔除匹配项, 按 gitignore 的 last-match-wins 顺序解释; 受保护目录内部无法通过取反重新放行后代.[] 即停用保护 (插件仍在, 只是不再多挡任何路径).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:
Web Settings 侧边栏的 "写入保护" 页面有三块内容: 保护路径 (gitignore 语义), 额外可写根 (字面路径) 和 macOS broker 加固开关. 保存后实时生效并持久化:
# 保护路径
.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) 的落点取决于进程当前目录, 会被拒绝并出现在 "未生效" 里.dsh web 的启动路径).mode 与 workspaceRoot 是官方 policy 行字段的复述 (patch 对整行配置做替换, 必须带上), 取值语义与 base bundle 一致.
保护路径与额外可写根会同时作用在下面几个入口:
| 入口 | 哪些系统 | 效果 |
|---|---|---|
| write / edit 工具 | 全平台 | 按保护模式逐路径判定, 命中即拒绝; workspace-write 下额外根放行 |
| bash 等命令 | Linux, macOS | 内核级只读 / 额外可写; Windows 做不到, 见下方限制 |
| 提示词 | 全平台 | 先告诉模型哪些不能写, 哪些额外根可写 |
| macOS broker 加固 | macOS | 堵住 open 经 launchd 把命令挪到沙箱外执行 |
两类入口的判定方式不同, 这是有意的: write / edit 拿得到目标路径, 因此直接按 gitignore 模式判定 —— 深层嵌套, 尚未存在的匹配一样挡得住, 每条写入只做几次正则; bash 的沙箱 (mount / profile) 只能吃具体路径, 所以那一侧才需要枚举展开. 枚举走 fs.promises, 每次 readdir / lstat 让出事件循环; 同步的 resolve() 只注入模式原文和缓存里已有的路径, 不在会话加载时扫盘. 提示词同样只陈述模式, 不枚举绝对路径.
主场景是 workspace-write. read-only 下官方已挡住全部文件写入, 额外可写根不打穿; 但官方 profile 的 (allow default) 在两种模式下都一样, 所以 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 配置和设置页文本走同一套解析.
(allow default) 让 open 能把命令交给 launchd 在沙箱外跑, 本插件追加的拒绝形式堵住这条路. Linux 的 bwrap 用 mount namespace, 没有 launchd 那类代理通道, 但它的网络命名空间未隔离, 沙箱内仍可连宿主守护进程 (Docker socket, ssh-agent 一类) 让外面代劳, 这类问题本插件不处理.--rw. write / edit 两者都生效.danger-full-access): bash 不进沙箱, 挡不住; write / edit 仍然挡./.git); 字面条目即使还不存在, write / edit 也会拒绝.! 放行的目录还会继续找. 目录符号链接不跟随, 避免扫到工作区外. write / edit 不受这条限制: 它直接按模式判定, 新建的 .git 立刻就被挡.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 |
把模式枚举成具体路径, 供 bash 沙箱使用 | node:fs/promises、canonicalPath |
src/fs.ts / src/policy.ts / src/provider.ts |
三个挂载点: write/edit 围栏、沙箱 policy、进程沙箱 argv 叠加 | DSH 引擎 |
src/path-expand.ts 负责额外可写根的字面路径展开 (~ / 环境变量 / 平台差异).
测试覆盖: 纯匹配器语义 (锚定, **, 字符类, 取反, 前缀围栏, 目录标记, 大小写, 工作区外不match; 不需要任何临时目录), 路径解析语义 (相对锚定, 解开符号链接, 去重, 通配枚举与取反, 额外可写字面路径), write / edit 按模式判定 (启动后才出现的深层路径、尾部 / 与同名文件、工作区边界), bwrap / Seatbelt / Landlock 的命令行叠加, write / edit 工具的拒绝与额外根放行矩阵, settings 通道的 base 与用户覆盖分层, client bundle 的 loader 注册, macOS 上真实 sandbox-exec 的内核级端到端 (包括 open broker 逃逸的对照组与加固后的拦截验证), 以及 Linux 上真实 bwrap 的内核级端到端 (保护路径写入 EROFS, 读取照常, 额外可写根可写; 本机 bwrap 不可用时整组跳过).
MIT
CLASSIFICATION EVIDENCE
系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: git、sandbox。