dsh-baseline
DSH 的安全基线闸门:网络出口独立成层,子代理权限只能收窄不能放宽,每一次判定与结果写进同一条可校验的哈希链。
版本
0.1.0· DSH peer>=0.2.1-rc.2 <0.3.0(真机验证环境:0.2.1-rc.2 CLI Web、0.2.1-rc.3 desktop,范围见验证状态) · Node.js>= 22.5(node:sqlite;22.13 起无需额外 flag)Host 半区与客户端均已实现;真机验证范围见验证状态与待验证项:桌面版验证服务端加载、审计、链与 HTTP 路由,CLI Web 验证客户端渲染、主题与 API。
快速开始
dsh plugin --profile web add dsh-baseline
装好后重启 DSH,左侧栏出现「基线审计」即生效。桌面版请在 Plugin Manager 里安装 dsh-baseline。
本包安装即启用。若想先审查再启用,见安装与启用第 3 条。
首屏:五条不能含糊的话
1. 不魔改 DSH 源码
只使用官方公开的扩展点和事件:tools/pre-execute、tools/result、ctx.tools.guard()、ctx.approval、ctx.agents、webServer.register()、客户端 slots / locale。不依赖未文档化的内部 API,不替换、不补丁、不包装 DSH 自身的运行时文件。
2. fail-closed
任何不确定的判定、审批缺失、审批超时、子代理 lineage 断裂、审计库不可用,一律拒绝,并给出明确原因。宁可让一次调用失败,也不让它在无法验证时继续执行。
3. 与相邻产品互补,可共存
- 与
dsh-permgate:互补。它做 8 类权限拦截;本插件不替代权限分类,只守网络、子代理上限、凭据与破坏性操作这几条底线。 - 与
auditrail:互补。它做 SQLite 哈希链审计;本插件的链只记录自身的判定与结果,不接管全局审计。 - 与
dsh-plugin-product-subagents:互补。它提供子代理上限;本插件在ctx.tools.guard()里再保一次单调上限,任何后注册的 listener 或 guard 都不能把子代理放宽。 - 与
dsh-plugin-vet:互补。它做运行时观察;本插件做阻断、审批与可校验审计。
4. 能力边界:明确不覆盖什么
- 不覆盖“恶意插件直接
require('child_process')”的逃逸。纯插件层看不到绕过 ToolRuntime 的原生进程调用,这需要进程级隔离、容器或操作系统策略。 - 不覆盖 DSH Code Mode
run_code的 worker thread 逃逸。worker 内执行的代码不经过可拦截的工具调用边界,同样需要进程级隔离。 - 不解析 DNS。公网域名解析到
127.0.0.1或发生 DNS rebinding 时,本插件只能看到主机名字符串。 - 不是防火墙,不是沙箱,不是杀毒软件,也不是完整的能力权限系统。完整清单见能力边界。
5. “baseline”的含义:有些底线,任何配置都不能推翻
启用插件后,以下两类规则无条件、优先于所有自定义规则执行,配置文件和面板都无法关闭:
- 破坏性系统命令(根目录递归删除、格式化磁盘、写块设备、fork bomb 等);
- 对设备或内核接口的原始写入(
/dev/sd*、/proc/self/mem、/sys/*、\\.\PhysicalDrive*)。
网络底线分两部分:
- 不可关闭:始终阻断的内部主机名与后缀(云元数据、
.internal、.svc、.local等); - 默认开启、可运营调整:私网、回环、链路本地、CGNAT 的 IP 段由
blockedCIDRs定义。默认列表覆盖这些范围;删除条目会明确削弱该范围的阻断,这是操作者的显式选择,README 不把它伪装成不可推翻。
凭据读取/外发拒绝与子代理权限上限默认开启,只能通过配置文件中的 guard 开关显式关闭,面板不能关闭。
“默认开启”指策略在安装后即生效;本包安装即启用(见安装与启用)。如需先审查再启用,见该节第 3 条。
它解决什么问题
大多数 Agent 安全产品在“工具名”层面做权限控制。但真实的逃逸往往发生在三个地方:网络出口、子代理继承、以及“谁在什么时候基于什么理由允许了什么”。dsh-baseline 把这三处做成同一套可验证的机制:
| 层 | 机制 | 不可绕过的原因 |
|---|---|---|
| 网络出口 | 私网/元数据默认阻断、可选主机白名单、按会话出口预算、严格模式逐个审批 | 网络命令与 URL 在 pre-execute 中提取,硬规则在 guard 中二次复核 |
| 子代理上限 | 沿 session.header.parentSession 回溯祖先,重放同一次调用的策略 |
在 ctx.tools.guard() 中执行;guard 只有“拒绝/不干预”两种结果,没有 allow,后注册者无法放宽。fork 会话不计入 |
| 审计 | 判定与结果写入同一条 SHA-256 哈希链,客户端只读查询 | 写入前脱敏;verifyChain() 返回第一条断链记录;客户端不直接接触 SQLite |
它不做的:
- 不替代
dsh-permgate的权限类别; - 不把每条审计记录上传到远端;
- 不读取业务数据,不保存文件内容、patch、diff、图片或消息体;
- 不提供自定义审批 UI,审批一律复用 DSH 标准
ctx.approval; - 不在客户端保存 token,不使用
localStorage。
安装与启用
前置条件
- DSH
>=0.2.1-rc.2 <0.3.0(peerDependencies与engines.dsh的区间;见下方兼容矩阵) - Node.js
>= 22.5,且 Node 带node:sqlite(本包使用DatabaseSync)。22.13 起无需额外 flag;22.5–22.12 需要以--experimental-sqlite启动 DSH - DSH 以 Web 模式运行(只有 Web 模式会挂载
webServer;面板与 HTTP API 依赖它)
兼容矩阵
| DSH | Cordis | 状态 |
|---|---|---|
| 0.2.1-rc.3 | 4.x | 已验证(桌面版:加载 / deny / 审批拒绝 / 审计链 / HTTP 路由) |
| 0.2.1-rc.2 | 4.0.5 | 已验证(CLI Web:客户端渲染 / API 全路由)+ 服务端真实 ToolRuntime 集成测试 |
| 其它 0.2.x | 4.x | 未验证(peer 与 engines.dsh 允许,但未跑过) |
package.json 的 dsh.compatibility.dsh 记录首个完成真机验证的版本(0.2.1-rc.2),不代表只支持该版本;实际允许范围由 peerDependencies['@deepseek-ai/dsh-tools'] 与 engines.dsh 的区间决定。
构建
npm install
npm run build
产物:
lib/index.js与lib/types/:Host 半区lib/client.js:客户端 bundle(约 60 KB)locale/zh.json、locale/en.json:面板与 Plugin Manager 的显示元数据icon.svg:插件图标
安装
在 DSH 的 Plugin Manager 中安装
dsh-baseline(从 npm),或使用install_bundle安装本目录 /npm pack后的 tarball。不要手工复制node_modules或改写 Loader 配置来替代安装步骤。注:已在 DSH
0.2.1-rc.3desktop 上通过 Plugin Manager 从 npm 安装并验证(侧栏入口、面板、导出、熔断解除均通过)。安装后先看 bundle 详情与插件行:标题应为“安全基线”/
Security baseline,描述与图标来自locale/*.json与icon.svg。本包安装即启用。 随包发布的
cordis.patch.yml只做一次 insert,不带disabled键:- insert: - id: dsh-baseline name: dsh-baseline config: {}原因:Plugin Manager 的安装开关只会把包写进
dsh.profile.bundles,不会翻转条目的disabled。早期版本把disabled: true写死在包里,结果是「装上了、列表里有、重启后什么都没发生」——插件被合成进树里,却从未实例化。想先审查再启用,不要改包内文件,而是在 profile 的
cordis.patch.yml末尾加一行覆盖:- id: dsh-baseline disabled: true审查完改成
false或删掉该行,再重启 DSH。替换已安装的包后需要重启 DSH,才能加载新的 JavaScript 模块;仅安装新 bundle 可能通过 HMR 生效。
重启后打开 Web UI:左侧栏出现“基线审计”图标,主面板 key 为
baseline。面板需要 Host 半区已启用且webserver/index-inject已注入启动 token。
配置覆盖语义
DSH Loader 中,匹配到同一插件的 config 会整体替换原有配置,而不是逐字段合并。Schemastery 会为缺失字段补默认值,但不会保留你上一次写的值。覆盖配置时,请把需要偏离默认值的字段一次写全。
推荐的最小启用配置:
- insert:
- id: dsh-baseline
name: dsh-baseline
disabled: false
config:
defaultMode: standard
策略引擎
三种模式
| 行为 | loose |
standard(默认) |
strict |
|---|---|---|---|
| 硬规则(含凭据/网络) | 执行 | 执行 | 执行 |
| 自定义规则 | 执行 | 执行 | 执行 |
| 内置审批规则 | 不触发 | 触发 | 触发 |
| 严格模式兜底 | 不启用 | 不启用 | 启用 |
| 无规则命中时的默认 | 允许 | 允许 | 只读放行;网络或可能修改状态的调用需审批 |
| 熔断 / 出口预算 / 审计降级 | fail-closed | fail-closed | fail-closed |
| 子代理上限(启用时) | 执行 | 执行 | 执行 |
standard 是低误报默认:只在有明确信号时弹审批(下载管道执行、破坏性 Git、破坏性 SQL、持久化、监听端口、读 .env、写系统目录、证据不完整,以及配置了白名单后的白名单外目标)。
硬规则(优先于自定义规则)
这些规则在自定义规则之前评估,effect 固定为 deny,risk 为 critical(出口预算的 guard 拒绝为 high)。
guard-private-network 的规则本身不能被配置或面板关闭,但其 IP 段集合来自 blockedCIDRs,删除某个 CIDR 会使其不再命中;始终阻断的内部主机名不受列表影响。凭据类规则受 guard.enforceCredentialDeny 控制。
| 规则 ID | 触发条件 |
|---|---|
guard-destructive-command |
递归删除根目录/home(操作数先做 POSIX 规范化,/tmp/..、/./、// 视为根);删除盘符根;format 磁盘;dd 写块设备;mkfs* / wipefs / blkdiscard;对设备 shred;在根目标递归 chmod/chown;在根目标 find -delete/-exec;fork bomb |
guard-root-write |
任意命令或工具的写入目标规范化后等于文件系统根(/、C:\、//)或当前用户主目录(~、$HOME);覆盖 mv x /、> / 等非 rm 变体 |
guard-device-write |
写 /dev/sd*、/dev/nvme*、/proc/self/mem、/sys/*、\\.\PhysicalDrive* 等设备或内核接口 |
guard-credential-read |
读真正的凭据/密钥:~/.ssh/ 下的 id_rsa/id_dsa/id_ecdsa/id_ed25519 与 *.pem/*.key/*.p12/*.pfx/*.jks/*.keystore,以及 .npmrc、.netrc、.pgpass、~/.aws/credentials、~/.azure/、~/.kube/config、~/.docker/config.json、~/.git-credentials、~/.gnupg/、gcloud 凭据、~/.password-store/。~/.ssh/config 仅审批(见 builtin-credential-config-read);known_hosts 与 *.pub 不拒绝 |
guard-credential-write |
写入任意 ~/.ssh/authorized_keys(持久化后门) |
guard-credential-exfiltration |
同一次调用既读凭据又访问网络或写私钥;或读取 .env/敏感配置同时访问网络(即使 loose 模式也拒绝) |
guard-private-network |
目标命中 blockedCIDRs,或命中始终阻断的内部主机名(见网络策略) |
凭据三条规则只在 guard.enforceCredentialDeny: true(默认)时执行。其余四条不可关闭。
内置审批规则
| 规则 ID | 触发条件 | standard |
strict |
|---|---|---|---|
builtin-pipe-to-shell |
下载内容直接管道给 sh/bash/python/node 等解释器;PowerShell irm | iex |
审批 | 审批 |
builtin-destructive-git |
git push --force、reset --hard、clean -f/-x、checkout/restore .、branch -d、stash drop/clear、filter-branch/filter-repo |
审批 | 审批 |
builtin-destructive-database |
DROP TABLE/DATABASE、TRUNCATE TABLE、ALTER TABLE ... DROP、无 WHERE 的 DELETE FROM |
审批 | 审批 |
builtin-persistence |
crontab 写入、systemctl enable、launchctl load、schtasks /create、注册表 Run 自启动、chkconfig --add、update-rc.d,以及写入 shell rc/systemd/cron 等持久化位置;authorized_keys 写入由 guard-credential-write 直接拒绝 |
审批 | 审批 |
builtin-listener |
nc -l、socat ... listen、ssh -R、python -m http.server、php -S、ngrok/cloudflared/chisel/frpc |
审批 | 审批 |
builtin-sensitive-read |
读取 .env / .env.*(.example、.sample、.template、.dist、.md 除外) |
审批 | 审批 |
builtin-credential-config-read |
读取 ~/.ssh/config(可能包含 ProxyCommand);known_hosts 与 *.pub 不触发 |
审批 | 审批 |
builtin-protected-write |
写 /etc、/usr、/bin、/sbin、/boot、/dev、/proc、/sys、/var/lib、/var/spool、/Library、/System、C:\Windows、C:\Program Files 等受保护位置(/dev/null、/dev/stderr、nul 等无害设备目标除外) |
审批 | 审批 |
builtin-unverifiable-evidence |
参数遍历触顶(truncated)且涉及网络或写入 |
审批 | 拒绝 |
builtin-network-unverifiable |
配置了 allowedHosts、调用涉及网络、但无法确定目标主机 |
审批 | 拒绝 |
builtin-network-not-allowed |
配置了 allowedHosts、目标不在白名单中 |
审批 | 拒绝 |
builtin-egress-budget |
仅严格模式:maxEgressBytes > 0 且检测到上传标志 |
— | 审批 |
builtin-workspace-escape |
仅严格模式:写入工作区之外的绝对路径 | — | 审批 |
严格模式兜底(没有规则命中时):
- 网络调用且所有目标都在显式白名单内 → 允许(
default-strict-allow); - 其他网络调用 → 审批(
builtin-strict-network); - 非只读调用 → 审批(
builtin-strict-mutation); - 只读调用 → 允许(
default-allow)。
自定义规则
rules:
- id: allow-npm-registry
effect: allow
priority: 100
risk: low
reason: Package installs may reach the npm registry
tools: [bash]
commands: ["npm *", "pnpm *"]
hosts: ["registry.npmjs.org"]
匹配语义:
- 不同维度之间是 AND:写了
tools和hosts,就必须同时满足。 - 同一维度内部是 OR:
hosts中任意一个命中即可。 - 未写的维度视为不限制;但至少要写
tools/paths/commands/hosts/agents之一。 tools、agents:大小写不敏感的 glob。agents匹配agent.id(会话 ID)。paths:同时按路径 glob 与普通 glob 匹配,覆盖真实路径、词法路径和工作区相对路径;路径模式中*不跨/。commands:默认是“包含”匹配;模式中出现*、?、[时按 glob 匹配。会匹配完整命令与每个解析后的命令段。hosts:主机名 glob 或 CIDR。- 排序:
priority降序;相同优先级按声明顺序;第一条命中的规则生效。 effect可为allow/deny/ask,risk为low/medium/high/critical。- 硬规则优先于自定义规则;自定义
allow不能放行破坏性命令、设备写入、命中当前blockedCIDRs的网段或已启用的凭据拒绝。 - 自定义
allow仍会经过 guard 的子代理上限检查。 - 规则 ID 必须唯一,长度 ≤128,不能使用
builtin-、guard-、network-、__前缀;每个维度最多 64 个模式。
单调 guard:子代理权限上限
ctx.tools.guard() 的契约是“只返回拒绝原因或不干预”,没有 allow。因此它是唯一能保证“后注册的 listener 不能放宽前人的拒绝”的位置。本插件的 guard 按以下顺序检查:
- 审计库不可用 → 拒绝(
guard-audit-unavailable); - 会话已熔断 → 拒绝(
guard-fuse); - 出口预算已超限且本次涉及网络 → 拒绝(
guard-egress-budget); - 重新执行全部硬规则(此时已经过审批;审批不能放宽硬规则);
- 找不到本次调用的
pre-execute记录 → 拒绝(guard-policy-bypass,防止绕过判定阶段); - 子代理上限未启用,或本次是根代理 → 放过;
- lineage 无法验证 → 拒绝(
guard-subagent-ceiling); - 对每个祖先,按祖先身份重放同一次调用的策略;若祖先的判定等级高于子代理本次判定(
deny > ask > allow),拒绝(guard-subagent-ceiling)。
lineage 的来源:
- 从当前 agent 的
session.header.parentSession向上走; header.origin === 'subagent'但没有parentSession→ lineage 断裂;ctx.agents.get(parentSession)找不到父会话 → 断裂;- 出现环、或深度超过 32 层 → 断裂;
- 断裂一律拒绝,不放宽。
这是“对同一次调用重放祖先策略”的单调上限,不是完整的能力集合格。它保证:如果祖先在同一调用上会被更严格地对待,子代理就不能凭自己的配置获得更宽的结果。若 ctx.agents 不可用,所有子代理调用都会被拒绝(fail-closed)。
审批
- 只使用 DSH 标准
ctx.approval.request(),不渲染自定义阻塞 UI。 - 只有
allowed-once视为通过;rejected、cancelled、unavailable全部拒绝。 approvalTimeoutMs(默认 30,000 ms)到期即拒绝,并标记timedOut;审计结果记为timeout。apply时ctx.approval不存在只记 warning;真正需要审批而服务仍缺失时,显式拒绝并写明“approval is required but ctx.approval is unavailable”,不会静默等待到超时。- 审批通过后 guard 会重新检查硬规则与子代理上限;一次审批只对本次调用有效。
网络策略
networkPolicy 是独立的策略层,不依赖工具名:
networkPolicy:
allowedHosts: [] # 空 = 允许公网主机;非空 = 白名单
blockedCIDRs: [ ... ] # 默认覆盖常见私网、回环、链路本地、元数据段
maxEgressBytes: 0 # 0 = 不启用出口预算
默认 blockedCIDRs:
10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
127.0.0.0/8 169.254.0.0/16 0.0.0.0/8
100.64.0.0/10 ::1/128 ::/128
fc00::/7 fe80::/10
判定规则:
- IP 字面量:命中
blockedCIDRs即阻断。默认列表覆盖私网、回环、链路本地、CGNAT、未指定地址与 IPv6 ULA/链路本地。 - 始终阻断的内部主机名:
metadata.google.internal、metadata.goog、instance-data、instance-data.ec2.internal、host.docker.internal、gateway.docker.internal、kubernetes.default.svc。 - 始终阻断的内部后缀:
.local、.internal、.lan、.home.arpa、.intranet、.corp、.private、.svc、.cluster.local、.nip.io、.sslip.io、.xip.io、.lvh.me。 - 回环主机名:
localhost、*.localhost等只有在回环仍在blockedCIDRs中时才阻断;删除127.0.0.0/8与::1/128可放行本地开发。 - 空主机名视为阻断。
- 主机名比较统一小写、去掉末尾点和端口;支持
[::1]形式。 allowedHosts为空时,公网主机不做额外限制;非空时,白名单外的目标在standard下审批、在strict下拒绝。- 白名单支持主机 glob(如
*.npmjs.org)与 CIDR 条目。 - 不做 DNS 解析,不观察真实 socket。这是刻意的:
pre-execute不能把每次工具调用变成一次 DNS 查询,也挡不住 DNS rebinding。
出口预算
maxEgressBytes 是事后估算,不是实时流量计:
- 工具执行完成后,用
JSON.stringify(exec.arguments)的 UTF-8 字节数估算本次出口量; - 按会话累计;累计超过预算后,该会话后续网络调用被拒绝 60 秒,然后清零重新累计;
- 当前这次已执行的调用不会被追回;
strict模式下,若预算开启且检测到上传标志(curl -d/-F/-T、wget --post-*、scp/rsync/sftp远端目标),会先要求审批;- 设为
0关闭预算。
熔断
- 同一会话在 60 秒内累计达到
denyBurstThreshold(默认 10)次拒绝 → 熔断; - 熔断状态只在内存中,重启即清空;触发熔断会写一条
__fuse__审计控制记录,解除操作只复位内存、不写链; - 面板可解除;解除同时重置该会话的出口预算窗口;
denyBurstThreshold: 0关闭熔断。
审计
写入模型
pre-execute只把 pending 记录放在内存(WeakMap<ToolExecution, DecisionRecord>),不写库;- 结果已知时,在
tools/result只写一次; - 被拒绝、取消、超时的调用同样有结果行;
result取值:ok/error/denied/timeout;effect取值:allow/deny/ask/fuse(fuse只用于熔断与出口预算的控制记录,不是审批判定)。
数据库默认位置:
dataDir显式配置 → 该目录;- 否则
$DSH_HOME/dsh-baseline; - 否则
~/.dsh/dsh-baseline。
文件:audit.sqlite(SQLite,WAL 模式)与 feedback.jsonl。
哈希链
每条记录包含:ts、session_id、agent_id、tool_name、params_json、effect、risk、reason、rule_id、result、prev_hash、hash。
hash = SHA-256(canonical JSON of {
ts, sessionId, agentId, toolName, paramsJson,
effect, risk, reason, ruleId, result, prevHash
})
- 第一条的
prev_hash是 64 个0; - 写链与插入在同一个
BEGIN IMMEDIATE事务里,避免两个写入者扩展同一个前驱; verifyChain()逐条校验prev_hash与重算的hash,返回第一条断链的id;- 审计库打不开或校验过程出错时返回
{ ok: false }(没有brokenAt);面板显示“审计链不可用”,不会伪装成完整; - 保留策略清理旧记录后,会把链基线更新为最后一条被清理记录的
hash,因此清理不会造成误报断链; /status不会每次重扫全链:激活时做一次全量校验,之后返回缓存结果,并附带checkedAt(上次全量校验时间)、checkedThroughId(已校验到的最大id)与stale(校验后又有写入或清理)。调用/verify或重启进程会重新全量校验并清除stale。node:sqlite是同步 API,这样轮询状态不会阻塞工具调用路径。
保留与导出
auditRetentionDays默认 30 天;启动时清理一次,之后每 6 小时清理一次;0关闭清理;- 清理按
id切掉一段连续前缀,而不是按ts逐条删除:ts是 pre-execute 时间,并发下可能乱序,逐条删会把中间行挖掉、导致其后所有prev_hash失配。清理后chain.baseHash更新为被清理的最后一条记录(按id排序的最新过期行)的hash; - 查询按
id降序,单页最多 500 条;sessionId、agentId、toolName是转义后的子串匹配(%、_、\按字面量处理),effect、from、to是精确条件; - 导出按
id升序,以 1,000 条为一批、按id游标分批读取并流式写入响应,最多 50,000 条;超过时响应x-dsh-baseline-truncated: true头。面板当前只完成下载,不展示该头;大库导出请直接调用 API。 - 导出格式:JSON 或 CSV(CSV 单元格做了公式注入转义;JSON 输出为数组,CSV 先写表头)。
保留期边界:清理按 id 切连续前缀。由于 ts 在并发调用下可能乱序,边界附近少量“时间戳未过期但 id 较小”的记录可能被一并清理;影响范围限于乱序跨度(通常为秒级)。
反馈
面板可对单条记录写反馈:追加到 dataDir/feedback.jsonl,不修改既有审计记录;note 最长 2,000 字符,写入前脱敏。新建文件时以 0600 创建。
脱敏:写入之前,而不是之后
- 私钥 PEM 块、
sk-...、sk-ant-...、AIza...、GitHub token、Slack token、AWSAKIA/ASIA、JWT、Bearer/Basic、TOKEN=...、URL 用户信息、查询参数里的 token、--password类 CLI 参数、Cookie/Authorization头都会被替换为[REDACTED]; - 键名命中 token/secret/password/api_key/credential 等语义的字段整体替换为
[REDACTED](max_tokens这类计量字段除外); - 内容型字段(
content、new_string、body、patch、diff、base64、image等)整体替换为[omitted],从不解析、从不保存; - 参数 JSON 最多 65,536 字符,reason 最多 2,000 字符,递归深度最多 12;超限写
[TRUNCATED]或截断标记; - 脱敏发生在哈希之前,因此库里存的就是被哈希的文本。
注意:脱敏是模式匹配,不是数据分类器。未知格式的密钥、写在命令字符串里的自定义凭据仍可能残留;导出前请按组织要求复核。
数据最小化与自身透明
- 插件自身不发起任何外部网络请求;客户端只请求同源的本地 API;
- 不读取、不转发业务数据;文件内容、patch、图片、消息体不进入审计库;
- 数据库与反馈文件都在本地
dataDir;请用文件系统权限保护该目录; - 客户端不直接连接 SQLite,所有查询通过 Host 半区的只读 HTTP 接口;
- 源码随 npm 包分发(
files含src/),许可证 MIT。公开仓库地址待补,暂未提供在线源码浏览入口。
审计面板与 HTTP API
面板
- 左侧栏入口:
sidebar.panellist,id 为baseline,标签“基线审计”; - 主面板:
mainkeyed slot,key 为baseline; - “审计记录”页:按判定/工具/会话/每页数量筛选,展开行可看脱敏参数、规则、判定原因、前后哈希,并可提交反馈;支持 JSON/CSV 导出、分页、链校验;
- “策略与运行状态”页:编辑
allowedHosts与blockedCIDRs(即时生效,持久化到审计库meta表),查看只读的出口预算、熔断会话并解除熔断; - 未保存的策略草稿在切换页签时保留;离开面板或重载后以已保存值为准;
- 找不到 Host 注入的 token 时显示明确的“面板尚不可用”,不会静默空白。
API
Host 半区在 web 模式下注册 prefix 路由 /dsh-baseline/api,并通过 webserver/index-inject 注入:
window.__DSH_BASELINE__ = { token, apiBase: '/dsh-baseline/api' }
| 方法 | 路径 | 作用 |
|---|---|---|
GET |
/status |
记录数、保留天数、时间范围、链状态、降级原因、熔断列表、策略快照 |
GET / POST |
/verify |
校验整条哈希链 |
POST |
/query |
分页查询,body { filter: {...} } |
POST |
/export |
导出 JSON/CSV,body { filter, format } |
POST |
/feedback |
写反馈,body { auditId, note } |
POST |
/fuse/reset |
解除指定会话熔断,body { sessionId } |
GET / POST |
/policy |
读取或更新 allowedHosts/blockedCIDRs |
鉴权与边界:
- 每个 Host 激活周期生成一个 32 字节随机 token,前端从注入的全局对象读取;不落盘、不进
localStorage; - 请求头:
x-dsh-baseline-token;比较使用timingSafeEqual; - 带
Origin的请求必须与webServer.getOrigin()完全一致,否则 403; - 响应统一
cache-control: no-store、x-content-type-options: nosniff; - JSON body 上限 64 KiB;查询单页 1–500 条;导出 50,000 条;
/query的sessionId、agentId、toolName为子串匹配(LIKE,%/_/\已转义为字面量);effect、from、to为精确条件;/export按id游标分批读取并流式输出,不会把整次导出结果先堆在内存里;/status的chain是缓存结果(含checkedAt/checkedThroughId/stale),只有/verify或进程重启才重新全量校验;- 变更策略只能改这两个列表;
guard开关、maxEgressBytes、defaultMode等只能通过配置文件修改。
配置参考
所有字段都有默认值,由 Schemastery 校验;apply 中不再写硬编码兜底。
| 字段 | 默认值 | 说明 |
|---|---|---|
defaultMode |
standard |
loose / standard / strict |
approvalTimeoutMs |
30000 |
审批超时,1,000–600,000 ms |
auditRetentionDays |
30 |
审计保留天数,0–3,650;0 关闭清理 |
dataDir |
'' |
空则解析为 $DSH_HOME/dsh-baseline 或 ~/.dsh/dsh-baseline |
networkPolicy.allowedHosts |
[] |
空表示允许公网主机;最多 200 项 |
networkPolicy.blockedCIDRs |
见上表 | 默认覆盖私网/回环/链路本地/元数据段;最多 200 项 |
networkPolicy.maxEgressBytes |
0 |
0 关闭;否则按会话累计估算 |
denyBurstThreshold |
10 |
60 秒内拒绝次数达到即熔断;0 关闭 |
guard.enforceCredentialDeny |
true |
凭据读取/外发拒绝;只能配置文件修改 |
guard.enforceSubagentCeiling |
true |
子代理权限上限;只能配置文件修改 |
rules |
[] |
自定义规则,见上节 |
配置校验会拒绝:重复规则 ID、保留前缀、空维度、空模式、重复项超限、非法 CIDR、URL 形式的 allowedHost、无效枚举值等。
完整示例
- insert:
- id: dsh-baseline
name: dsh-baseline
disabled: false
config:
defaultMode: standard
approvalTimeoutMs: 30000
auditRetentionDays: 30
networkPolicy:
allowedHosts:
- registry.npmjs.org
- "*.githubusercontent.com"
blockedCIDRs:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- 127.0.0.0/8
- 169.254.0.0/16
- 0.0.0.0/8
- 100.64.0.0/10
- ::1/128
- ::/128
- fc00::/7
- fe80::/10
maxEgressBytes: 0
denyBurstThreshold: 10
guard:
enforceCredentialDeny: true
enforceSubagentCeiling: true
rules:
- id: allow-npm-registry
effect: allow
priority: 100
risk: low
reason: Package installs may reach the npm registry
tools: [bash]
commands: ["npm *", "pnpm *"]
hosts: ["registry.npmjs.org"]
再次强调:匹配到同一插件时,
config整体替换。上例把默认值显式写全,避免覆盖时意外丢失设置。
运行时覆盖
只有 allowedHosts 与 blockedCIDRs 可以面板修改,保存在 audit.sqlite 的 audit_meta 表中,重启后仍然生效。配置文件里的值作为“重置基线”:审计库损坏或持久值非法时回退到配置值。
契约标注
本 README 与代码按三类标注依据。所有签名已对照本机安装的 0.2.1-rc.2 包类型定义或官方文档核对;运行期集成仍有待验证项。
公共契约
这些是包公开文档/类型中承诺的接口,本插件按契约使用:
| 契约 | 依据包 |
|---|---|
Cordis 插件导出 name / inject / Config / apply,ctx.effect / ctx.on / ctx.get / ctx.inject |
@deepseek-ai/cordis@4.0.5 |
tools/pre-execute waterfall、PreToolDecision(allow/deny/cancel/ask)、tools/result、ctx.tools.guard() 单调且只返回拒绝原因 |
@deepseek-ai/dsh-tools@0.2.1-rc.2 |
审批 request({ agent, toolName, callId, reason, displayReason, signal }),结果 allowed-once / rejected / cancelled / unavailable |
@deepseek-ai/dsh-user-approval@0.2.1-rc.2 |
ctx.agents.get(sessionId)、agent.session.header 的 id / cwd / parentSession / origin / delegationDepth |
@deepseek-ai/dsh-agent@0.2.1-rc.2、@deepseek-ai/dsh-session@0.2.1-rc.2 |
webServer.register({ kind: 'prefix', path, handler })、getOrigin()、webserver/index-inject |
@deepseek-ai/dsh-host-webserver@0.2.1-rc.2 |
ctx.slots.inject / ctx.slots.register,主面板 main keyed slot 与 sidebar.panellist list;ctx.locale.register / bind |
@deepseek-ai/dsh-client-ui-layout、@deepseek-ai/dsh-client-ui-sidebar、@deepseek-ai/dsh-client-locale |
Schemastery Config 校验与默认值 |
@deepseek-ai/schemastery@3.18.5 |
仓库惯例
这些是官方开发文档要求的做法,不是类型级强制:
| 惯例 | 说明 |
|---|---|
| bundle patch 安装即启用 | cordis.patch.yml 不带 disabled 键;如需停用,用 profile 的 cordis.patch.yml 覆盖 |
通过 Plugin Manager install_bundle 安装 |
不手工复制依赖或绕过 bundle 选择 |
同 ID 的 config 整体替换 |
覆盖配置时必须写全非默认字段 |
包根 package.json 的 icon 优先于 ./icon |
图标 ≤256 KiB,路径必须在包内;显示元数据来自 locale/*.json 的 meta.title / meta.description |
客户端构建包装成 window.__ModuleLoader__.load(...) |
客户端 require 仅允许 react、cordis、@deepseek-ai/dsh-client-*;不直接 require 具体 client 包 |
主题使用 --dsw-alias-* token |
不在客户端写死品牌色;不向 document.body 追加节点 |
| 客户端不直连 SQLite | Host 暴露只读 HTTP 接口,客户端通过同源 fetch 访问 |
当前实现
这些是 dsh-baseline@0.1.0-rc.1 自己的选择,可能随版本调整,不构成对外契约:
| 内容 | 当前值 |
|---|---|
| 网络证据提取 | 键驱动的参数遍历 + 小型 shell 分词器;支持常见 shell、PowerShell、sudo/env/timeout 等包装器;嵌套 sh -c、$(...)、反引号递归解析(最多 4 层) |
| 证据上限 | 5,000 节点、1,000 字符串、2,000 路径、深度 12;越过即 truncated |
| 出口预算窗口 | 60 秒;按会话累计 arguments JSON 字节;超出后拒绝后续网络调用 |
| 熔断窗口 | 60 秒;默认阈值 10 |
| 审计 schema | audit + audit_meta,schema 版本 1;id 自增;索引落在 ts、session_id、agent_id、effect |
| 保留清理 | 启动时 + 每 6 小时;按 id 切连续前缀;清理后更新 chain.baseHash |
| 链状态缓存 | 激活与 /verify 时全量校验;/status 返回缓存;写入或清理后 stale: true |
| 导出上限 | 50,000 条;按 id 游标 1,000 条/批流式输出;查询 500 条/页,文本字段子串匹配 |
| API 路径与 token 注入 | /dsh-baseline/api、__DSH_BASELINE__ |
| 规则保留前缀 | builtin-、guard-、network-、__ |
| 特殊内部主机名/后缀 | 见网络策略 |
待验证项
以下是仍未验证的部分;已验证项的准确范围见验证状态。不要把它们当作已经获得运行期证明:
| 待验证内容 | 需要验证的包/环境 |
|---|---|
| 桌面应用窗口内的面板视觉效果(CLI Web 已渲染,但未在 Electron 窗口截图核对) | Electron 桌面版 + 真实 DSH Web profile |
| “导出 JSON / CSV”按钮的浏览器下载行为 | 真实 DSH Web/Electron UI |
| 熔断“解除”按钮、验链按钮的 UI 点击路径 | 真实 DSH Web/Electron UI |
审批弹窗的中英文 displayReason 视觉与文案(拒绝结果已进入桌面版审计) |
@deepseek-ai/dsh-user-approval、真实审批 UI |
| Plugin Manager 中标题、描述、图标的实际渲染 | @deepseek-ai/dsh-client-ui-plugin-manager、@deepseek-ai/dsh-client-locale |
通过官方 install_bundle 安装(本轮为手工 pnpm add + manifest 装配) |
本机 DSH 安装与 Plugin Manager |
| headless 环境下的真实任务(无法复用桌面账号登录,见运行环境限制) | 带 DEEPSEEK_API_KEY 的 headless profile |
Electron / file:// 模式下面板行为 |
按设计不提供面板(依赖 webServer);未测试 |
多个 Host 进程共享同一 dataDir |
未验证;设计假设单写入者 |
人工验收步骤见 docs/dsh-ui-checklist.md。其中仍有未验项:审批弹窗文案、导出下载、熔断解除按钮、验链按钮、桌面窗口视觉、官方安装通道;服务端的 deny/ask/审计/熔断已有自动化测试与真实 ToolRuntime.execute() 覆盖。
验证状态
自动化
npm test:130 个测试,15 个 suite 全部通过(node:test+node:assert/strict,通过tsx运行;SSR 用例使用tsconfig.client.json以启用 JSX 转换);- 覆盖
engine、extractEvidence、guard(含 fork 不按子代理处理、真实子代理 lineage、subagent 上限、bypass 拒绝审计)、audit.verifyChain(含 100 条记录篡改测试、缓存状态、按id前缀清理、模糊查询、分批导出)、真实 ToolRuntime 集成、客户端 API、破坏性命令与凭据规则回归、无害设备重定向回归; - fork bomb 硬规则:
test/forkbomb.test.ts覆盖 3 种写法、bash -c嵌套、字符串打印的已知误报,以及rm -rf /tmp/cache、ls -la、echo 'hello world'不被误伤; npm run typecheck:服务端与客户端两个 tsconfig 均通过;npm run build:服务端编译 + 客户端 bundle 成功(约 60 KB);- 中英文字典键集一致:
test/client.test.ts的zh and en dictionaries keep the same key set; - 面板 SSR:
test/ssr.test.ts用react-dom/server的renderToStaticMarkup真实渲染AuditPanel三种初始状态(无 boot token、英文、中文),并断言具体文案(如The audit panel is not available yet、Security baseline、安全基线、共 0 条记录)。这是 Node SSR 冒烟,不是真实 DSH Web UI 验证; grep -rn "trace(" src/server/index.ts无输出。
真机验证
Verified on DSH 0.2.1-rc.3 desktop (Windows): bundle load, deny/ask audit, hash chain.
- DSH 0.2.1-rc.3 desktop(Electron,Windows):插件被正确合成并加载,
apply()实际执行,dataDir生效,audit.sqlite自动创建;26 条审计记录,result全部回填,pending = 0;pwsh cat .env→deny/builtin-sensitive-read/denied,理由approval timed out after 30000 ms;grep/read工作区内的.env→deny/denied,理由含(approval rejected);其余 23 条allow+result=ok;插件自身verifyChain()返回{"ok":true},checkedThroughId=26;宿主端口上/dsh-baseline/api/status无令牌返回401。 - DSH 0.2.1-rc.2 CLI Web(独立测试 profile):合并资源中含
id: "dsh-baseline"与面板文案;「基线审计」出现在sidebar.panellist,可点开;面板渲染标题「安全基线」、审计链徽标、筛选器、导出按钮、策略页 11 条 CIDR;深浅主题切换后颜色跟随,无写死品牌色;0 条 console 错误、0 页面异常、0 失败请求;/status/verify/query/policy带令牌正常。 - DSH 0.2.1-rc.3 desktop 从 npm 安装(
dsh-baseline@0.1.0-rc.1,经 Plugin Manager):包被拉取并写入 profile,apply()执行、默认dataDir下audit.sqlite自动创建、/dsh-baseline/api/status无令牌返回401;侧栏「基线审计」出现;面板在 Electron 窗口内正常渲染并跟随明暗主题;「导出 JSON / CSV」触发浏览器下载;熔断生效后「解除」可恢复。该次验证在 rc.1 上进行,当时需手工补disabled: false;0.1.0起 bundle patch 不再带disabled,安装即启用。
没有做:
- 桌面应用窗口内的面板视觉效果(只验证了 CLI Web 运行时下的渲染,未在 Electron 窗口里截图核对);
- 「导出 JSON / CSV」按钮的浏览器下载行为;
- 熔断「解除」按钮、验链按钮的 UI 点击路径;
- 通过应用内 Plugin Manager 的
install_bundle安装(本轮是手工pnpm add+ manifest 装配,不等于官方安装通道); - headless 环境下的真实任务(无法复用桌面账号登录,见运行环境限制);
- Electron
file://、多 Host 进程共享同一dataDir。
因此,Host 半区的策略行为有单元/集成测试与桌面版真机证据支撑;客户端已在 CLI Web 验证渲染与 API,但 UI 交互与官方安装链路仍有未验项,见待验证项。人工验收步骤见 docs/dsh-ui-checklist.md。
- 发布元数据:本轮不提供
repository/homepage/bugs字段;源码通过 npm 包的files分发(含src/),许可证 MIT。
能力边界
明确不覆盖
- 恶意插件直接
require('child_process'):绕过 ToolRuntime 的原生进程调用不在纯插件层可见范围内。需要进程隔离、容器、seccomp/AppArmor 或操作系统策略。 - Code Mode
run_code的 worker thread 逃逸:worker 内代码不经过可拦截的工具调用边界。需要 worker 级隔离或禁用该模式。 - DNS 解析与 DNS rebinding:只检查词法目标。公网域名指向私网地址、DNS 重绑定、代理转发都不在本层能力内。
- 真实 socket 流量:不观察实际连接、不计算真实字节、不解析 TLS/HTTP。出口预算是参数大小的估算。
- 完整 shell/语言语义:分词器是启发式的;高度混淆的命令、编码载荷、自定义解释器、二进制程序内部的网络行为可能漏检。
- 非 ToolRuntime 的副作用:插件自身的代码、其他插件的后台任务、模型直接调用的外部 API 不在拦截路径上。
- 操作系统级破坏:硬规则只覆盖高置信度的模式;不是防病毒,也不是系统完整性保护。
运行环境限制
- headless 无法复用桌面账号登录:
dsh --profile headless默认走deepseek-officialAPI Key 路由;切到deepseek-account后需要credentials+authorization服务,而 headless 里authorization默认关闭,开启后仍pending (waiting for service: credentials),最终报ACCOUNT_SIGN_IN_REQUIRED。根因:账号 provider 的desktopPlatform仅在 profile 名为desktop且运行于 Electron(win32/darwin)时启用。headless 部署需要自备DEEPSEEK_API_KEY,否则任务起不来。 - 审批超时即拒绝:
approval服务 30 秒无响应 → 本次调用按deny处理并落审计,理由写作approval timed out after 30000 ms。这是 fail-closed 的设计取舍:宁可卡住,也不放行。长耗时人工确认场景请调高approvalTimeoutMs(默认 30000,范围 1,000–600,000 ms)。
已知近似与权衡
maxEgressBytes是事后估算:当前调用执行后才计量,无法阻止本次超额;只对后续网络调用生效。- 清空
blockedCIDRs会削弱 IP 字面量拦截:这是面板允许的显式操作;.internal/.svc/元数据主机名仍然阻断,但私网 IP 与localhost的阻断取决于列表内容。默认值不建议删改。 - 哈希链不是 WORM:能发现单条记录的修改、删除、插入和乱序,但拥有数据库写权限的人可以重算整条链。若需要防整链重写,请定期把链头锚定到外部系统。
- 审计降级即全拒:SQLite 打不开时,所有经 ToolRuntime 的调用都会被拒绝。这是 fail-closed 的可用性代价。
- 崩溃可能丢失在途记录:pending 在内存中,进程在
pre-execute与tools/result之间崩溃时,该次调用可能没有审计行;已完成的结果行是持久的。 - 单写入者假设:SQLite 的写锁能串行化写入,但熔断、出口预算与策略缓存是进程内的;未设计/未验证多 Host 进程共享同一
dataDir。 - 脱敏是模式匹配:未知格式的密钥可能残留;命令字符串与路径本身也可能包含敏感信息。导出前请复核。
- 子代理上限是“同一次调用的判定等级”比较:不是完整的能力集合包含关系;lineage 或
ctx.agents不可用时拒绝所有子代理调用。 - 面板只在 Web 模式可用:Electron/
file://没有webServer,面板与 API 不注册;Host 半区的拦截仍可在插件加载时工作。 - 严格模式可能更吵:
strict会对未显式放行的网络调用和非只读调用逐次审批;把defaultMode设为strict前请先准备白名单与规则。standard是低误报默认。 - fork bomb 按整条命令的文本子串判定:为覆盖
:(){ :|:& };:这类包含|、;、&的语法,检测发生在分词之前。因此echo ':(){ :|:& };:'这种把该字符串当普通参数打印的命令也会被拒绝。这是 fail-closed 的已知误报取舍,锁定用例见test/forkbomb.test.ts。
未实现的可选项
阶段四的 AST 静态扫描(plugin-risk.json)未实现;dsh-plugin-vet 已提供运行时 watch,本插件不重复该能力。本插件不在安装或启动时解析插件源码、不生成静态风险画像,只对经过 ToolRuntime 的调用做运行时策略判定与审计。
开发
目录
src/server/
types.ts 结构化依赖类型(不 import DSH 运行时值)
config.ts Schemastery schema + 跨字段校验
engine.ts 硬规则、内置审批、严格兜底、自定义规则
shell.ts 命令分词、路径/主机/写入提取、破坏性判定
network.ts IP/CIDR、主机名策略、白名单
evidence.ts 从工具参数提取路径/命令/主机
guard.ts lineage 与单调 guard
state.ts WeakMap pending 记录
fuse.ts 熔断与出口预算
policy-state.ts 面板可改的运行时覆盖
audit.ts SQLite 哈希链、查询、导出、清理
api.ts 本地 HTTP API
redact.ts 写入前脱敏
index.ts Cordis 插件装配
src/client/
index.tsx 面板与侧栏注册
AuditPanel.tsx 审计/策略界面
api.ts 同源 REST 客户端
locales.ts 中英文字典
client-types.ts 客户端结构化契约
scripts/build-client.mjs esbuild 包装与 require 白名单
test/ 单元与集成测试
命令
npm test # node:test 全量测试
npm run typecheck # 服务端 + 客户端类型检查
npm run build # tsc + 客户端 bundle
本机如果 PATH 中的 Node 低于 22.5,请显式使用满足版本要求的 Node 运行 npm,否则 node:sqlite 与 npm 自身都可能不可用。
发布前检查
-
package.json的dsh.bundle.patch指向真实存在的cordis.patch.yml -
cordis.patch.yml不带disabled键,安装即启用 -
dsh.client.inject只包含@deepseek-ai/dsh-client-*包 -
lib/client.js的require只有react/react/jsx-runtime -
locale/*.json键集一致,meta.title/meta.description非模板文案 -
icon.svg有效、≤256 KiB、路径在包内 -
files包含lib/、src/、cordis.patch.yml、locale/、icon.svg、README.md、CHANGELOG.md、docs/、LICENSE -
npm test、npm run typecheck、npm run build全绿 - 在真实 DSH Web/Electron profile 中完成
docs/dsh-ui-checklist.md里仍标记为“待人工”的项:官方install_bundle、导出下载、熔断解除、验链按钮、桌面窗口视觉(当前未完成)
License
MIT。见 LICENSE。
No comments yet. Be the first to write one.