toby-bridges/api-relay-audit
toby-bridges/api-relay-audit从 DeepSeek Harness 对 AI API 中转站和 LLM 代理运行本地安全审计,生成 Markdown 报告,覆盖提示词注入、模型替换信号、工具调用改写、错误泄漏、流完整性和按 profile 启用的 Web3 风险。
把任何沙箱模式(read-only / workspace-write / danger-full-access / auto)下代理的文件访问限制在会话工作区内,越界访问一律请用户裁决;每个会话还能用 composer 旁的档位选择框把越界门禁放宽为「工作区外只读」或「完全放开」。
DeepSeek Harness (dsh) 插件:把任何沙箱模式(read-only / workspace-write / danger-full-access / auto)下代理的文件访问限制在会话工作区内,越界访问一律请用户裁决;每个会话还能用 composer 旁的档位选择框把越界门禁放宽为「工作区外只读」或「完全放开」。适配 DSH 0.2.x(已在 0.2.1-rc.2 验证)。
Config.enabled):开启后,越出工作区的 read/write/edit/read_image/glob/grep/bash/pwsh 调用都会停下来问用户;关闭则完全不干预。仅此一次 / 总是允许该目录 / 拒绝):Enter=仅此一次、Esc=拒绝;/dsh-workspace-only-plugin/sessions//whitelist.json,不进 profile 的 cordis.patch.yml),同目录后续直接放行;Config.dialogTimeoutMs,默认 120s)→ 拒绝该次调用;askDone 结算后即时递减消退;ctx.userQuestions,TUI 等)→ 再回退官方 approval/request 弹窗(与旧版行为一致,依然绕过 `never` 策略,Full access 会话也弹);完全没有应答渠道时拒绝(fail closed)。plugin: 记录)+ 进程内权威,跨重启保留、会话间独立;子代理会话启动时继承父会话档位快照(一次定格,之后父子各自独立)。读写口径:read/read_image/glob/grep 算读,write/edit 与 shell(bash/pwsh,无法静态分辨读写)算写。敏感文件弹窗与「永不放行」规则在任何档位都生效。总开关关闭或未打开会话时选择框隐藏。C:...、C:/...)、UNC(\\server\share)、反斜杠逃逸(..)与 POSIX 形态同等对待;路径包含判定对 Windows 形态使用 path.win32 语义(大小写不敏感)。workspace-write 预设改名 "Write Only" 改为可选项(默认关闭,与 auto 插件零冲突)。DSH 0.2 自带完整插件安装器(依赖安装、dsh.bundle 校验、版本兼容预检,不兼容会拒装并回滚),任选一条路径:
方式一:web GUI 安装
github:devilspiderx/dsh-workspace-only-plugin#main,点「安装」;方式二:一行 CLI
dsh plugin --profile web add "github:devilspiderx/dsh-workspace-only-plugin#main"然后重启 dsh web(--profile 换成你的 profile 名)。
github:直连不走安装源,机器需能直接访问 GitHub。访问不了时可下载.tgz后填绝对路径安装;插件若发布到 npm 后也可直接填包名。
升级 dsh 后请跑一次自检:node scripts/check-compat.mjs(探测审批词汇、pre-execute 词汇、插件记录通道、问答通道、volatile 配置、client 模块表等契约,漂移会打印差异报告)。
为什么package.json不声明peerDependencies:DSH 内部包的版本随发行版以 prerelease 形态演进(如0.2.1-rc.2),semver 对 prerelease 的匹配规则是「只允许同major.minor.patch且带 prerelease 标记的比较器」——任何固定范围(包括*)都会在下一次 rc 升级时误报「不兼容」。本插件按官方 Host-only bundle 的做法不声明依赖,真实兼容性由scripts/check-compat.mjs探测实际契约来判定。
配置住在插件的 DSH 0.2 Config 里(当前 profile 的 cordis.patch.yml → config),设置卡片与插件共享同一份实时值:
字段 · 默认 · 说明 enabled · false · 门禁总开关(运行时可写,.volatile()) always · [] · 总是允许的目录白名单(运行时可写,.volatile()):只存全局条目(字符串,设置页管理,处处生效)。会话级条目 T25 起不存这里——持久化到 /dsh-workspace-only-plugin/sessions//whitelist.json(会话页「白名单目录」管理,仅本会话及其子代理生效),启动时会把残留的 {session, dir} 条目自动迁往该文件;schema 仍接受 {session, dir} 形状(迁移失败的残留继续生效并重试迁移)。{root, dir} 条目=旧项目级(T9 遗留,软忽略:不匹配不显示,仅为不炸配置加载而保留接受) sensitivePatterns · ["*.env","*.env.*","*.pem","*.key","*.p12","*.pfx","id_rsa*","id_ed25519*"] · 敏感文件 basename glob:工作区内也弹三键 sensitiveAllowPatterns · ["*.env.example","*.env.sample","*.env.template","*.env.defaults"] · 敏感豁免(优先于 sensitivePatterns,如 .env.example 不弹) sensitiveAlways · [] · "总是允许此类文件"持久化的 pattern(运行时可写,.volatile();与目录白名单正交,"总是允许该目录"不会放行敏感文件) deny · [] · 永不放行规则:以 / 或 ` 结尾按目录前缀、否则按 glob 全路径匹配(运行时可写,.volatile());命中直接拒绝、不弹窗、**优先级最高** renamePresetLabel · false · 把 workspace-write 预设显示名改为 "Write Only";开启后与 @nanmicoder/dsh-auto-mode 的标签匹配可能冲突 dialogTimeoutMs · 120000 · 三键问题的等待上限,超时按拒绝处理 fastDenyMs · 0 · 非交互快速 fail-closed 预算(毫秒):>0 时无应答渠道(无 UI)的调用在该预算内直接拒绝、不走系统弹窗兜底;0 保持完整回退链 scanAllTools · false · 全量保守扫描(opt-in):白名单 8 工具之外的工具也按参数里的路径形状判定(可能对含路径样文本的参数误报) scanIgnoreTools · [] · 保守扫描的工具豁免名单(误报逃生舱) realpathCheck · false · realpath 复核(opt-in,T11):对词法判「工作区内」的路径做一次 fs.realpath` 复核,工作区内符号链接指向区外时改判越界、照走三键;每条目一次同步 fs 调用(性能代价),解析失败按越界询问(fail-closed)
手动编辑示例(profile 的 cordis.patch.yml 追加):
- id: workspace-only
config:
enabled: true
always:
- C:\data\shared\ # 全局条目(字符串):处处生效会话级条目不在always里维护:它住在/dsh-workspace-only-plugin/sessions//whitelist.json(见下节);手写进always的{session, dir}条目会在下次启动时被自动迁往该文件。
注意:patch 的 override 语义是整体替换 config,写 override 时带上需要的全部字段。
会话级白名单(询问卡片「总是允许该目录」、会话页「白名单目录」写入的条目)按主会话隔离存放在:
<DSH_HOME>/dsh-workspace-only-plugin/sessions/<主会话id>/whitelist.json
{ "version": 1, "dirs": ["C:\\data\\project-cache\\"] } 是授权会话的**血缘根**(T20 语义不变):主会话及其所有子代理/fork 共享同一个文件,不同主会话互相隔离;DSH_HOME 与内置默认白名单同源解析($DSH_HOME 非空白优先,否则 ~/.dsh`);cordis.patch.yml,profile 的配置 override 整体替换也冲不掉它们;always 里残留的 {session, dir} 条目会自动搬进对应文件并在成功后从 Config 移除(按会话分组提交,某组失败只留该组、下次启动重试,期间条目照常生效)。除配置里的 always 外,还有一条内置默认白名单:/dsh-runtimes/dsh-primary-runtime 目录树始终按全局条目放行(等效设置页条目,处处生效)。
desktop-host 的 root: join(resolveDshHome(), 'dsh-runtimes', 'dsh-primary-runtime'));home 解析逐条对齐宿主 resolveDshHome()(@deepseek-ai/dsh-home-paths):$DSH_HOME 非空白时以它为准(空白值按未设置处理,~/~//~ 拼写展开,结果解析为绝对路径),否则回退 ~/.dsh;条目在每次判定时从环境解析,不写死绝对路径、不持久化进 cordis.patch.yml,换机器/换用户自动适配(check-compat.mjs probe 12 盯住该契约的漂移);sensitivePatterns)照旧弹三键,与目录白名单正交的语义不变。renamePresetLabel 默认 false,本插件不再改预设名,与 @nanmicoder/dsh-auto-mode 零冲突(也不再需要它的标签补丁)。若显式开启改名,auto 插件按标签匹配权限菜单的逻辑需要自行适配。
会话档位 · 越界读(read/read_image/glob/grep) · 越界写(write/edit/bash/pwsh) · 敏感文件 · deny 规则 仅工作区内(默认) · 弹三键 · 弹三键 · 弹三键 · 命中即拒 工作区外只读 · 放行 · 弹三键(shell 保守按写) · 弹三键 · 命中即拒 完全放开 · 放行 · 放行 · 弹三键 · 命中即拒
子代理会话的初值:subagent/subagent_fork 创建的子会话在启动时继承父会话当时的档位作为初值并一次定格(写入子会话自己的日志快照记录),之后父子各自独立调整、互不影响;subagent_fork 的子会话复制了父日志前缀,快照天然来自 fork 时刻。普通新会话仍默认「仅工作区内」;父会话不可解析或血缘成环时也回落默认档。
三键门禁与官方沙箱是叠加关系:门禁只决定"越界要不要问你",实际执行仍受官方模式约束(批准 ≠ 越权)。以下均基于官方内置沙箱后端(含 win32 ACL runner)。
官方模式 · 仅工作区内(默认) · 工作区外只读 · 完全放开 read-only · 越界读写都弹三键;批准后读可以,写仍被官方只读围栏拒绝 · 越界读放行;越界写弹三键(批准了也写不了) · 越界读写都不弹,但官方只读围栏仍在:写全拒 workspace-write · 越界读弹 → 允许即读;越界写弹 → 允许后仍被写围栏拦(FS_SANDBOX_DENIED)→ 再走 sandbox_permissions 升权确认(双确认);shell 命令内越界路径照弹 · 越界读放行;越界写照左侧的双确认链弹 · 越界读写都不弹;越界写仍走官方写围栏 + 升权链(workspace-write 的官方语义不变) danger-full-access · 核心用例:官方无任何围栏,越界读/写/搜索/shell 全部弹三键(approval policy 为 never 也照弹);批准后完全放行 · 越界读放行;越界写弹三键 · 越界读写全放行(仅敏感文件仍弹、deny 仍拦)
总开关=关:任何档位都不生效(composer 的档位选择框也隐藏),纯官方行为——read-only 读随意、写全拒;workspace-write 越界读随意、越界写靠写围栏 + 升权;danger-full-access ⚠️ 完全裸奔(无围栏、无门禁,代理可读写任意路径)。
敏感文件(弹三键)与「永不放行」规则(命中即拒)在上表任何格子都生效,与模式、档位无关。
通用规则(所有格子适用):
sessions//whitelist.json 文件,见「会话级白名单文件」);询问卡片「总是允许本目录」写入会话级条目;另有内置默认条目 /dsh-runtimes/dsh-primary-runtime(每次判定实时解析,处处生效、不可删除,见「内置默认白名单」);sensitivePatterns 命中的 basename)工作区内也弹三键,且不受会话档位影响(「完全放开」档也弹):一次调用既越界又敏感只弹一次(原因合并);"总是允许此类文件"按 pattern 持久化(sensitiveAlways),与目录白名单正交——"总是允许该目录"不会放行敏感文件;%TEMP%/os.tmpdir() 下 dsh-* 或 .dsh-* 一级子目录(含子树)是 DSH 自己生成的落盘目录——dsh-subprocess-*(子进程大输出 spill)、dsh-spill-*(工具结果溢出)、dsh-*(沙箱受限进程的私有 TEMP)等,工具跟随 spillPath 或展开 %TEMP% 时不再询问;临时根本身、其下普通文件、其他位置的同名前缀目录照旧弹;sandbox_permissions 的升权调用仅当目标模式严格宽于会话当前模式(官方升权弹窗必现)时才跳过门禁(避免双弹窗);重复(等于当前模式)或畸形的升权请求在官方侧零弹窗直接放行/报错,一律照走三键门禁,堵死"重复升权绕过";ask/never)不影响门禁:弹窗不走 ApprovalService,never 会话也照弹。推荐组合:
想要的效果 · 官方模式 · 总开关 · 会话档位 最强约束(默认建议) · workspace-write · 开 · 仅工作区内 只防读泄露、允许自由写 · 任意 · 开 · 工作区外只读 临时放开一个会话(探索 / 迁移数据 / 演示) · 任意 · 开 · 完全放开(敏感仍弹、deny 仍拦) 只要官方围栏 · 任意 · 关 · —(不生效) ⚠️ 不推荐 · danger-full-access · 关 · —(裸奔,仅限一次性隔离环境)
门禁开启时,插件向运行上下文快照注入一段 workspace-only 叙事(systemPrompt.context,order 126——排在官方 sandbox:policy(110)、approval:policy(115)与子代理 delegation(120/125)之后,作为权限语义的最后一句)。官方三段在 danger-full-access / never / 子代理会话里分别宣称"不限制文件修改""审批弹窗已禁用自动拒绝""权限固定不可放宽",与门禁语义直接冲突(模型左右脑互搏),因此每档文案都以优先级声明开头:Workspace Only 开启期间,与本段冲突的 sandbox/approval 表述一律以本段为准。
文案按会话档位裁剪,内容包括:
toolAccessKind 一致):read/read_image/glob/grep 算读;write/edit 算写;命令行工具 `bash`/`pwsh` 一律按写计(命令文本无法静态分辨读写,命令内任何越界路径都弹审批,即使命令只是读);其他/未知工具保守按写。仅「工作区外只读」档展开此表(该档的口径边界最容易被误解)。dsh-primary-runtime),标注"这些目录命中即放行、无提示";超过 20 条截断显示。白名单变更后下一次上下文组装即生效(文案是活的回调,不是一次性快照)。sandbox_permissions 逃逸(仅严格升权跳过门禁)、卡片弹出后等待用户决策且拒绝后不要原样重试。「完全放开」档只声明放行范围与残留约束(敏感仍弹、deny 仍拦),不列白名单(全部放行无意义)。
~/$HOME/${HOME}/%USERPROFILE%/%HOMEPATH%/$env:USERPROFILE/$env:HOMEPATH)已统一展开为真实路径判定(单引号内的字面量除外);扫描器看不到的路径(完全动态计算、其他环境变量拼接)是文档化的残余面,与 auto-mode 插件同级哲学。AppFileSystem.contains 同哲学(两边一致)。开启 realpathCheck 后,命中「工作区内」的路径经 fs.realpath 复核(对尚不存在的目标取最深存在祖先 canonicalize 后重拼余段,覆盖「写新文件、父目录是链接」场景;root 与目标双方都解析,macOS /tmp→/private/tmp 不误拒):解析到区外改判越界照走三键,解析失败 fail-closed 按越界询问。残余面收窄但仍存:白名单 / deny 规则仍按词法路径匹配(链接路径与其真身不互通)。read/write/edit/read_image/glob/grep/bash/pwsh,误报低);MCP/插件注册的其他工具默认不设防。开启 scanAllTools 进入全量保守模式:任何工具的参数里出现路径形状值都会判定,配合 scanIgnoreTools 豁免误报工具;shell 命令内的相对敏感路径(如 cat .env)不在词法扫描范围内。FS_SANDBOX_DENIED),随后走 dsh 既有 sandbox_permissions 升权再确认一次——写操作始终需要明确同意;携带 sandbox_permissions 的调用仅在官方升权弹窗必现(目标模式严格宽于会话当前模式)时跳过本插件门禁(避免双弹窗),重复/畸形升权照走三键。conversation.composer 槽位接管);无 Web 客户端时回退通用问题卡片(TUI 等)→ 官方审批弹窗。卡片超时视为拒绝。设置 fastDenyMs > 0 可让无应答渠道的调用(后台/无 UI)秒级拒绝而不挂满超时(fast-deny 审计通道)。卡片席位按 requestId 去重;询问在 Host 侧结算(超时、批量放行、其他客户端作答)后广播 askDone,各端残留卡片自动撤下。残留卡片的双保险(T16):回退问答卡在本地超时/迟到决策后一律 abort(平台随之撤卡,否则问答卡会永远留在 composer);迟到挂载的卡片收到 announce: {ok:false}(Host 已结算)立即自清;另有本地自毁计时器(dialogTimeoutMs + 15 秒宽限)兜住 askDone 丢失的极端情形——本地撤卡不改裁决(Host 超时本来就 fail-closed 拒绝)。conversation.input.left / conversation.view,用 @deepseek-ai/dsh-client-ui-primitives 渲染);TUI 等无此界面时档位保持「仅工作区内」,白名单只能从设置页或询问卡片维护。待审批徽章依赖 Web 端槽位(conversation.session.header.actions,紧邻官方子代理下拉);子代理 ask 在父会话行的「待审批」黄点走平台 pending 投影(precedence -1 的影子交互,永不遮蔽官方审批卡与本插件卡片),不依赖额外槽位。bash/pwsh)命令内无法静态分辨读写,一律按写处理(「工作区外只读」下 shell 越界照弹);scanAllTools 覆盖的未知工具同样保守按写。dsh-fs-sandbox 写围栏):本插件只在工具层加门禁,不改动任何沙箱后端。pnpm install && pnpm build # 构建 src/ → lib/(产物入库;改源码后必须重建并随源码同 commit)
node scripts/verify-scanner.mjs # 词法扫描用例(POSIX + Windows,含误报对照)
node scripts/check-compat.mjs # 对本机 dsh 安装做契约自检实现结构:源码在 src/、构建产物在 lib/(pnpm build 生成,随 git 分发)。src/host/scanner.js(纯词法扫描 + 读写口径 toolAccessKind + DSH_HOME 解析,可独立测试)、src/host/ask.js(三键会合点与回退链)、src/host/approval-compat.js(官方审批瀑布封装)、src/host/session-state.js(会话档位三态与审计记录)、src/host/session-whitelist.js(会话级白名单的文件持久化与迁移)、src/host/remote-service.js(client→host 管理 RPC)、src/host/index.js(门禁装配)、src/client/**(React JSX 源码:设置页、composer 会话档位选择框、会话页「白名单目录」视图、会话标题栏待审批徽章与父会话行黄点,UI 组件用 @deepseek-ai/dsh-client-ui-primitives 的 Menu/Input/Button,与官方权限选择器同款)→ 构建为单文件 lib/client.js。
alwaysScope 配置项已移除:询问卡片「总是允许该目录」现在固定写会话级条目(在会话页「白名单目录」管理),要全局放行请在 设置 → Workspace Only 手动添加;profile 的 cordis.patch.yml 若显式写过 alwaysScope,升级后删掉该行即可;{root, dir} 条目已被软忽略(不匹配不显示,配置里留着也无害),重新授权即写成新会话级条目;cordis.patch.yml(always 只留全局字符串条目),改存 /dsh-workspace-only-plugin/sessions//whitelist.json。升级后首次启动自动把 always 里残留的 {session, dir} 条目迁往对应文件(迁移失败的组留在 Config 里继续生效并自动重试);管理 RPC 形状不变(snapshot.always 仍返回 {session, dir} / 字符串条目)。workspaceOnly/*):setBypass(sessionId, enabled) → setScope(sessionId, scope)(scope ∈ workspace / outside-read / outside-write),新增 addGlobalAlways(dir),snapshot 的 sessions[] 由 bypass: boolean 改为 scope: string。args 字段名(sessionId/scope/dir/entry/requestId/choice)即各方法的 wire 契约,由插件注册的 strict 调用描述符固定(此前源码签名推断的方式在压缩产物上会全部报 args fields do not match the descriptor);改方法签名或调用载荷后跑 node scripts/verify-remote-service.mjs 核对三方一致。设计与 DSH 运行时契约详见 docs/increment-plan.md(含实现中发现的契约出入记录)。