dsh-operation-security
DSH 操作安全插件:工具 pre-execute 分类、单调 guard、插件生命周期 DPIP 门禁与审计留痕。
- Stars
- 0
- Language
- JavaScript
- Created
- Oct 22, 2026
- Updated
- Oct 22, 2026
Introduction
dsh-operation-security
桌面版(Desktop / app.asar 0.2.1-rc.2)的操作安全层移植。npm-global CLI 安装树里有两样桌面版看不到的东西,这个插件把它们带过来,并补上桌面版唯一可行的插件生命周期门禁位置。
它做的三件事
| 能力 | 来源 | 在桌面版怎么工作 |
|---|---|---|
| 工具调用分级与策略执行 | @deepseek-ai/dsh-operation-security@0.2.0-rc.2 的构建产物(vendor/opsec/,逐字移植) | 注册 tools/pre-execute + 单调 ctx.tools.guard():READ / DOWNLOAD / EXECUTE / PLUGIN_* / EXPORT / PUBLISH;EXPORT/PUBLISH 额外做凭据扫描;拒绝不可被后续监听器翻回允许 |
| 插件生命周期 DPIP 门禁 | CLI 里被插入 lib/plugin-*.js 的 gatePluginInvocation | 包装 pluginManager.installBundle(以及启用路径的硬停检查)。CLI 的 dsh plugin 在桌面版无法接管:desktop profile 由 Electron 独占,且 GUI / Agent / 自安装页的写入都走 pluginManager 的 Gateway 方法,不经过工具流水线 |
| 审计 / 状态 / 恢复边界 | 同上包 | 写入 $DSH_HOME/operation-security/,与 CLI 门禁同一份 audit.jsonl 与 plugin-state.json |
门禁语义(与 CLI 一致)
装/升级(installBundle)
├─ 该插件记录为 BLOCKED / QUARANTINED / ROLLBACK_REQUIRED → 拒绝(确认也不能放行)
├─ 记录为 ACTIVE → 放行(coverage = active)
├─ 有有效确认(ack 文件,或 DSH_DPIP_ACK=1) → 放行(consumed = 一次)
└─ 其余 → 拒绝,并给出可执行指令
启用(setBundleEnabled / setPluginEnabled,仅 enabled = true)
└─ 只有硬停状态会被拒绝;未完成的协议记录不拦(CLI 也不拦启用)
拒绝会抛出一个 code = 'DSH_DPIP_REFUSED' 的错误,消息里带三样东西:缺什么(协议五步)、怎么确认(脚本绝对路径与命令)、硬停无法用确认绕过。
给未经审查的第三方插件开一次确认
node "<工作区>/dsh-operation-security/scripts/ack.mjs" @scope/name --note "看过 changelog,无 postinstall"
- 确认写入
$DSH_HOME/operation-security/dpip-ack.json,默认 24 小时有效;名称会按门禁的规则归一化(pkg@1.2.3与pkg是同一个键); --ttl 0表示「不设记录级上限」,实际仍受gate.ackTtlHours这个上限约束;只有把上限配成 0 才真的不过期;- 一次成功安装后自动作废;
- 只有操作员(你)能写:Agent 和插件自己都不会调用这个脚本。
协议状态可以直接改(脚本不做协议迁移合法性校验,它是操作员入口而不是验证器):
node scripts/state.mjs list
node scripts/state.mjs set @scope/name ACTIVE # 视为协议已完成
node scripts/state.mjs set @scope/name BLOCKED # 硬停,确认也无效
node scripts/state.mjs remove @scope/name
配置
行配置在 cordis.patch.yml 里,全部字段见该文件注释。两个总开关:
enabled: false→ 两层都关闭,profile 回到装插件之前的状态;gate.enabled: false→ 只保留工具层策略,不拦插件生命周期写入。
其余是上游 schema 的同名字段(requireApprovalForExecute / requireApprovalForOutput / scanRoots / scanMaxFiles / scanMaxFileBytes / enforcedOperations)。
外发扫描根:EXPORT/PUBLISH 的扫描根按「调用声明的 workdir(向上找 .git)→ 会话的沙箱工作区 → scanRoots」顺序解析。桌面宿主的进程 cwd 是 profile 目录,所以默认会跟随会话工作区;一旦显式配置了 scanRoots,就以你的配置为准。
目录
lib/index.js 插件入口:装工具层,再装服务门禁
lib/config.js 无 schemastery 的字段校验与默认值
lib/paths.js $DSH_HOME / profile 目录解析(只用 node 内建)
lib/scan-roots.js 按调用解析外发扫描根(会话工作区)
lib/gate/spec.js 安装 spec → 包名(状态键)
lib/gate/decision.js 纯决策:ACTIVE / 确认 / 硬停 / 拒绝文案
lib/gate/ack.js dpip-ack.json 的原子读写
lib/gate/audit.js 审计记录(复用上游 buildAuditRecord)
lib/gate/manager.js 在 pluginManager 原始实例上定义/回收方法
vendor/opsec/ 逐字移植的上游构建(改动见 docs/PATCH-NOTES.md)
scripts/ack.mjs 操作员确认
scripts/state.mjs 协议状态查看/设置
验收
cd dsh-operation-security
node --test "test/*.test.mjs"
node --check lib/index.js && node --check vendor/opsec/index.js
安装后一次真实验收:在一个没有协议记录的包上调用插件安装,应得到 DSH_DPIP_REFUSED,并新增一条 finalStatus: BLOCKED 的审计行。
已知边界
- 只管运行中的宿主。 桌面版 CLI(
runtime/cli/bin/dsh.cmd)对非 desktop profile 的dsh plugin仍然没有门禁——那需要改app.asar里的 CLI 代码,升级即失效,本插件不做。 - 桌面 profile 不能被 CLI 管理,所以先有插件、后有门禁:本插件的安装这次是门禁生效前的最后一次无约束安装。
- enable 路径按 best effort 解析包名(
listPlugins()查不到时跳过额外检查),与 CLI 一致——CLI 完全不检查启用。 plugin_manager工具本身没有加进 detector:它的写入已经由服务门禁覆盖,再加一层会在门禁拒绝之前先弹一次审批。- 恢复边界只覆盖 profile 的
package.json/pnpm-lock.yaml/pnpm-workspace.yaml/cordis.patch.yml,不含进程、服务与插件写到 profile 之外的文件(审计记录里写明了这条limits)。 - 状态键是「按 spec 归一化出来的名字」:仓库名与本地目录能还原成包名,但 git / tarball URL 只能用 URL 本身作键。也就是说,一个已记录
BLOCKED的包若改从 tarball 地址安装,不会复用按包名记的硬停,需要执行者显式确认(确认后仍会留下审计)。这是 CLI 门禁「按原始参数取值」的继承限制。 - 安装失败时审计沿用上游语义:
finalStatus: FAILED,但securityResult仍是PASS(上游用它表示「没有安全门禁失败」)。判断成败看finalStatus。 - 每次放行安装都会在
recovery/留一份 profile 四文件副本,没有自动清理;需要时手工删旧目录。 - 相对
auditDir按宿主进程 cwd 解析(当前是 profile 目录),建议只写绝对路径或留空。 - 外发扫描口径较严:工作区里只要有文件名命中凭据规则(例如任何
.npmrc),EXPORT/PUBLISH 都会判 CRITICAL。要缩小范围,给调用传workdir,或在行配置里显式设scanRoots。