Skip to content

03 · 静态评估引擎 ​

src/auto —— 不依赖数据库的纯判定

host 编排在 src/index.ts,真正「长脑子」的静态规则引擎在 src/auto/。三者移植自 @nanmicoder/dsh-auto-mode(MIT),并在本项目重写为独立实现。

下表「行」列为该文件当前总行数快照(行号后的 # 是整文件锚点,只声明文件存在、不声明任何行号),随源码增删会漂移;本页所有 <span class="lnum"> 锚点由 node scripts/check-anchors.mjs 核对,摘要分开报「声明/字面量定位」与「仅范围核对」两档——无符号可查的锚点永远不会被算作已验证。

文件行职责
constants.ts constants.ts#141数值阈值默认值唯一事实源:5/8/10s、3/20、4000、8s/1024,及学习族(阈值 3/TTL 30d/100 条/会话放行帽 50);同时承载设置卡「默认放行工具」显示目录(显示镜像,非放行面)
risk-tokens.ts risk-tokens.ts#24HIGH 风险正则(NAME/REASON),供分类器与 policy 共用,防漂移
paths.ts paths.ts#462路径规范化(Windows 命名空间/NT 别名折叠、~ 展开、win32 小写)、受保护/关键路径判定、运行态文件名单
shell.ts shell.ts#3712Bash/PowerShell 词法分解(sticky 正则状态机)+ 整行熔断 + 逐段静态分类
policy.ts policy.ts#655每次工具调用的确定性第一遍分类 assessTool(推断型、保留类型检查)
rules.ts rules.ts#459Claude-Code 风格声明规则解析/求值(纯函数,host 与浏览器共用)
classifier.ts classifier.ts#100预分类提示词、参数脱敏、严格响应解析
dsh-classifier.ts dsh-classifier.ts#141复用 ctx.llm 做低 token 分类请求(temperature 0)
decision.ts decision.ts#1052纯决策函数:评审解析、人机竞速、来源标注、熔断状态机、静态名单
trust.ts trust.ts#307web 路由信任平面(loopback/LAN 边界、Host 伪造防护、在线端点 URL 校验)
artifacts.ts artifacts.ts#140本会话成功创建路径的内存出处登记(删除豁免依据)
audit.ts audit.ts#149append-only 审批审计(清空留墓碑、5MiB 裁剪;审计路径可被测试接缝重定向)
review-mode.ts review-mode.ts#51每会话评审模式持久化快照

同层的其余模块(类别层 category.ts、学习层 learning.ts、耗时遥测 latency.ts、结果脱敏 redact.ts、重试 retry.ts)各有专章或见 §14 全量清单。

3.1 assessTool —— 每一次调用的 18 步判定 policy.ts:LassessTool ​

下面 18 个分支与 assessTool 的判定顺序一一对应。一级分支数没有变,但读/写/补丁/编辑四步内部各自长出了子闸(受保护读、敏感名熔丝、插件运行态无条件拒);每一步的代码判别点见 3.1.1 表。

3.1.1 每个分支的代码判别点 ​

锚点写成符号/字面量而非行号:行号会随同文件其它改动漂移,符号与字面量只有在判别点本身被改写时才会失效。

步分支判别点锚点
①同步硬拒policy.ts:LhardDenyReason
②bash/pwsh → assessShellpolicy.ts:L"assessShell(args.command, exec.name, roots, artifacts, owner)"
③command 缺失/非法policy.ts:L"command argument is missing or invalid"
④读家族policy.ts:L"const readTools = new Set";受保护读 policy.ts:L"silently read through the read tool family";敏感名熔丝 policy.ts:L"A relaxation that newly admits a path outside the"
⑤write/edit运行态无条件拒 policy.ts:L"silently rewriting the audit trail"
⑥apply_patch目标读取 policy.ts:LpatchTargetPaths;不可读 → ask policy.ts:L"apply_patch target paths are missing or unreadable"
⑦str_replace_editor命令校验 policy.ts:L"str_replace_editor command is missing or invalid";view 读子闸 policy.ts:L"Mirror of the read family above"
⑧SESSION_STATE_TOOLSpolicy.ts:LSESSION_STATE_TOOLS
⑨HARNESS_READ_TOOLSpolicy.ts:LHARNESS_READ_TOOLS
⑩AGENT_TEAMS_CONTROL_TOOLSpolicy.ts:LAGENT_TEAMS_CONTROL_TOOLS
⑪OWNER_CONTROL_TOOLSpolicy.ts:LOWNER_CONTROL_TOOLS
⑫持久终端policy.ts:L"stateful terminal execution requires explicit approval"
⑬只读外查policy.ts:L"read-only external information lookup"
⑭编排类policy.ts:L"orchestration call; child tool actions remain independently checked"
⑮外部写policy.ts:L"external write requires specific user"
⑯风险工具名正则policy.ts:LriskyPluginToolReason
⑰DIRECT_HUMAN_TOOLpolicy.ts:L"direct human request"
⑱fail-closed 兜底policy.ts:L"unrecognized registered plugin tool requires independent classification"

第⑱步语义

兜底方向是「拿不准就问人」:一个注册插件工具若没有任何已知形态可对号入座,一律转人工并允许语义分类器介入,绝不因为「名字无害」而静默放行。

3.2 硬拒闸门 hardDenyReason policy.ts:LhardDenyReason ​

  • 凭据物质:web_fetch/curl/wget 或外部写工具,参数里含 PEM 私钥、sk- / ghp_ / github_pat_ / xox*、AKIA[0-9A-Z]{16}、aws 密钥赋值、Bearer …、.ssh 路径等 → 拒(判别点 policy.ts:LcontainsCredentialMaterial)。判通道而非判名字:宿主机内部工具(send_message / subagent / spawn_* / team_task_* 等,名单复用既有 owner——category.ts:LORCHESTRATION_TOOLS 与 AGENT_TEAMS_CONTROL_TOOLS,两份由跨副本契约测试钉成相等)的载荷出不了本机,其自由文本不参与该熔断;send_email/upload_*/post_* 一类真出网通道照拒。
  • shell 熔断:bash/pwsh 命令走 hardDenyShellReason(见 §3.3)。
  • 变更目标不可读:write/edit/apply_patch/str_replace_editor(≠view) 目标解析不出 → 拒(fail-closed,宁可误拒)。
  • 破坏性工具指向受保护路径 → 拒。

3.3 shell 命令分析管线 shell.ts# ​

管线入口 assessShell(shell.ts:LassessShell)、词法分解 decomposeCommandLine(shell.ts:LdecomposeCommandLine)、整行熔断 hardDenyShellReason(shell.ts:LhardDenyShellReason):

  • 只读名单刻意不含 cd(会改变后续段 cwd 解析基准)。
  • 目录切换段不计效果,但按 && 携带解析基准(effectiveCwdAfter,shell.ts:LeffectiveCwdAfter):切换器(cd/set-location/sl)不读写任何东西,因此只有它一个命令的段判为无效果,判定基准交给后续段据此结算。基准只在 && 上移动——进入下一段即证明该 cd 成功;跨 ;/|/& 证明不了(cd /nodir; printf x > package.json 的 cd 失败、写入仍落在工作区),故切换器在那里保留「未识别」裁决,整行仍走分类。四类不可读的情形(动态、glob、无目标、拼写族不匹配)一律不移动基准。识别不等于开口:目标是凭据树、家目录中未开放的路径、DSH_HOME 或关键树时该段仍判 ask;带重定向的段(cd x > out.txt 是写)同样保留原裁决。附带副作用:删除的会话工件豁免改为按携带后的基准结算目标,cd <ws> && rm own.txt 仍命中而 cd <ws>/sub && rm own.txt 不再继承。残留假设是被 alias 遮蔽的 cd——静态平面看不见别名,此条件写在 effectiveCwdAfter 自身的契约里(cd-prefix-static-decision.test.mjs:L"alias-shadowing residual assumption")。
  • 时钟写是终裁:date 的 -s/--set 家族(含 --s/--se 缩写、-s2020-01-01 融合、-us… 簇、date --set= 取值形态,组内行内由 opaque 目标 owner 回收)判硬拒,不再只是脱离只读快径。理由:改系统时钟既非会话任务、也无法被会话恢复——它会重定所有记录的时间线并可能使凭据/会话失效;留在 ask 层意味着「分类器判对」+「运维未开 timeoutAction=allow」两个前提同时成立才安全,而实测中 date --se=2020-01-01 被分类器读成「malformed read-only invocation」并自动放行、真的改动了机器时钟。只读拼写(date、-u、+%s、-d @0、-r f、-I…、--iso-8601=…)维持静态放行。
  • routineInlineProbe:python -c 只放行 import/print 字面量,node -e 只放行 require/console.log(process.version) —— 内联代码只认可「绝对安全」形态。
  • 危险 token 提取:sensitiveMarker(.ssh/.env/密钥关键词)、dshHomeExfil(4 组模式抓 DSH_HOME 外传)、dynamicHomeTarget($HOME 动态目标)→ 全部绕过静态判定。
  • 整行熔断读的是「数据载荷剥离视图」(fuseScanView,shell.ts:LfuseScanView):只把可证惰性的跨度等长涂白——仅限 commit 类命令(git commit / tag / notes)的 -m/--message 取值(含 -mX/--message=X/短簇 -am),且该 token 必须是完整引号跨度、内部无活替换。活替换($( )、反引号、进程替换)、引号提前闭合或未闭合、无引号操作数、表外命令(python -m、curl -m)一律原样保留。heredoc 正文不在剥离面:定界符是否被引用、消费者如何使用 stdin、正文是否被同一行写盘后执行这三件事都无法在本层判定,故正文保持被判——要写含熔断目标的提交正文请用 -F <file>。(既有边界,如实写明:opaque 恢复面仍由既有 owner stripHeredocBodies 丢正文行,该 owner 的逐行引号状态与「动态解释器名」判定各有一处既有缺口,会把该类行从硬拒降为可应答 ask;不在本轮剥离面内,已登记 backlog。)定位器漏判只等于维持现状。剥离视图只喂这四条整行熔断;decomposeCommandLine 与逐段判据仍读原文,目标级熔断必须看到每个操作数。
  • 写重定向脱离只读快径:命令含真实文件写重定向(>/>>/>|/&>/N>,非 discard sink)时,其段不得走只读命令快径放行——落入既有评估流;/dev/null、NUL、$null 等 discard sink 维持快径。只读命令自带的输出 flag 同判:sort -o(含 -oFILE/-uo)、tree -o、git diff --output= 等取出的值同样是写目标,先过同一组写目标熔断(破坏性目标 / 运行态文件 / 区内 DSH_HOME;区内熔断的常量例外 = 插件自身开发区 ∪ 会话工作区(当它是 DSH_HOME plugins 直接子目录时),见 §3.4),命中即硬拒、未命中则脱离快径——按命令建表(-o 对 rg/grep 是 only-matching,不可共用短旗标表)。
  • 写目标提取不吃相对拼法:只读命令的 .. 中段与工作区外相对目标一律进入显式路径判定(cat b/../../../../x 与 cat ../../../../x 同裁决)。
  • win32 段归一覆盖别名拼法:MSYS 裸盘根(/c、//c)、盘根通配(C:\*、/c/*)与 NTFS 默认数据流后缀(file::$DATA/file:$DATA)在 normalizePath 的同一处归一到 C:\ / 文件名本体,故盘根熔断与全部 basename 级保护(插件契约文件、受保护元数据、凭据名)不被拼法绕过。
  • build/test 与版本探测快径目标守卫:快径仅保留给「写目标全为 discard sink 或工作区内非敏感非受保护非运行态路径」——区外/敏感/受保护/运行态目标一律脱离快径进入正常评估(categoryMode: aggressive 与 trustedDirs 放宽模式同样生效)。
  • 循环防护(loopDetectionThreshold,默认关):同一调用(工具名+脱敏参数哈希,独立于学习签名的循环键)在自动放行面(static-allow / classifier-allow 的 pre-execute 站点与 answerer 的 static-allow / 无评审 auto-allow 站点,共四处)连续静默放行达到阈值时,第 N 次在写 allow 记录之前改判为 ask,并经 one-shot 跨面标记在 answerer 落入 LOCKED 同款钉死拒绝倒计时形状(不接 LLM takeover、不可学习;有人看=面板可放行,无人值守=超时自动拒)。计数严格连续(不同键即断链)、触发即清零(人工放行不买永久豁免);allowlist 豁免仅指 pre-execute 的静态名单放行分支——answerer 的 static-policy allow 分支(同一显式名单、source=allowlist-allow)在门内,两平面 authority 谓词不同,绕过 pre-execute 抵达 answerer 的调用也要计数;门自身不写 history、不碰熔断计数(触发留 loop-guard 非决策审计行,见 docs/11)。边界:只防卡死空转,微调参数即换键,不是安全边界;已学习签名的人工确认不被门控 ask 自动应答。

3.4 路径保护清单 paths.ts# ​

判定函数:hardDestructiveTargetReason paths.ts:LhardDestructiveTargetReason、isProtectedProjectPath paths.ts:LisProtectedProjectPath、运行态名单 runtimeStateTargetReason paths.ts:LruntimeStateTargetReason。

类别内容判定
家目录根 / DSH_HOME 树~ 、~/.dsh(env DSH_HOME 或默认)hardDestructiveTargetReason → 硬拒(allowedDshSubpaths 白名单可豁免);shell 写向量另过 shellWriteToDshHomeDenied → 区内硬拒,常量例外 = 插件自身开发区 ∪ 会话工作区(当它是 DSH_HOME plugins 直接子目录时)(非 operator 开口);被拒理由只在目标落在某个开口内时点名可用的替代写法(write/edit,或单段字面量内容写)
凭据根.ssh .gnupg .aws .azure .kube .config/gcloudisCriticalPath
shell 启动文件(12).bashrc .bash_profile .bash_login .bash_logout .profile .zshrc .zprofile .zlogin .zlogout .kshrc .cshrc .tcshrcisCriticalPath(家目录变体硬拒;工作区变体 ask)
系统关键目录POSIX: /etc /bin /sbin /usr /system /library /private/etc /boot ;Win: windows/program files/boot…isCriticalPath
工作区元数据首段.git .vscode .idea .husky .dshisProtectedProjectPath → 读/写都交人工
秘密文件 basename.gitconfig .gitmodules .bashrc .bash_profile .zshrc .zprofile .profile .mcp.json .netrc .npmrc .pypircisProtectedProjectPath
环境密钥文件.env / .env.*(.env.example 例外)isProtectedProjectPath
Git 元数据(读取).git/HEAD、.git/refs/**isGitRefReadPath 打开的固定例外(无凭据内容,四种读者 + shell 读命令都放行)
Git 元数据(其余 / 一切写入).git/config、.git/hooks/**、.git/objects/**、.gitmodules 等仍 protected(isProtectedReadMetadata 只减去 HEAD/refs 的读取)
Windows 设备/NT 命名空间\\.\ \device\ \\?\ \??\(非 UNC/X: 变体)canonicalizeWindowsNamespace 折叠后再判包含
保留设备名con prn aux nul com1-9 lpt1-9硬拒

symlink 逃逸:文本层判定无法识破快捷方式/软链接指向工作区外(如 ws/ln → ~/.bashrc)。宿主侧守卫 symlinkEscapeReason(symlink.ts:LsymlinkEscapeReason,宿主在 guard 处调用)取每个工具的真实路径操作数,对「文本上在工作区内/受信区」的目标做 realpath 最深祖先解析,一旦真实路径离开工作区/受信区就硬拒。该守卫必须覆盖每个读者:官方宿主自己不拒绝 symlink 逃逸(dsh-tools 产物内 realpath/lstatSync/readlink 零命中,它只在 allow 后征求已注册 guard),所以「谁的路径操作数没被提取」就等于「谁没有逃逸复检」——str_replace_editor 的 view 曾因按「仅突变才可能逃逸」的假设被排除,使工作区 junction 指向的凭据文件可经该读者静默读取,现已纳入(tests/view-reader-parity.test.mjs)。bash / pwsh 同样已纳入(shellGuardTargets,shell.ts:LshellGuardTargets):shell 的目标藏在命令串里,故由一个复用同一词法器(decomposeCommandLine)、同一路径归一化(normalizePath)与同一 changer 基准 owner(effectiveCwdAfter)的操作数提取器负责——裸相对拼写(cat ext/id_rsa)与**cd <目录> && cat <相对名>** 这两类此前完全不可见;bash 把 \ 当转义符而吞掉未加引号的反斜杠绝对路径,故另有一条字面绝对拼写回收(仅 drive-letter / UNC 形状,非第二套词法)。shell 的裁定刻意比结构化读者窄、且与位置模式无关:只有「realpath 离开工作区∪受信区」且落在凭据树 / DSH_HOME / 插件运行态文件时才硬拒——与 read 全量对齐会把这本仓库自用的 node_modules junction 普通读一并硬拒;node_modules 这类普通外部落点在 aggressive 与 standard 下都保持原行为(这是与结构化读者的已知差异)。区域比较同时用解析后的工作区(realWsNormalized)与文本工作区:本仓库自身的会话工作区就位于 ~/.dsh/plugins/…,只看文本会让每个普通读都算逃逸、再由 DSH_HOME 子句成片硬拒。两条已知边界(如实写明):①引号内的字面量会被当作真实目标——git commit -m "…C:/Users/…/.ssh/…"、rg "~/.ssh" docs/ 这类只是提及该路径的命令会被硬拒(方向上 fail-closed,与 opaque 重定向恢复已接受的同类误报一致;若运维反馈频繁再评估按 tokenizer 精确化);②opaque 行(含 (/{/$(/heredoc)不猜操作数,保持原行为,除非行内出现字面绝对拼写。另三条可达性边界:宿主只在 allow 之后征询 guard,所以该复检只覆盖旧行为是静态放行的落点(junction 逃逸、DSH_HOME 内读取);pre-execute 已判 ask/deny 的形状(如 cat <绝对 ~/.ssh/id_rsa>、含 cd 的整行)在 guard 之前就被拦,本复检不改其行为。③拼写与平台:包含型熔断(isWithin/isCriticalPath)在两侧路径风格不一致时判「不在范围内」。win32 宿主上,MSYS/Git-Bash 拼写(/c/Users/…、裸 /c、前斜杠 UNC //server/share)在 normalizePath/explicitStyleOf 入口被归一为 win32 风格(paths.ts:LcanonicalizeMsysPath),因此它与同目标的 C:\… 拼写落入同一组 DSH_HOME/凭据树/插件区/系统关键路径熔断;归一是按平台门控的——POSIX 宿主上 /c/... 是真路径而非别名,不做翻译,行为与既有版本一致。修前该归一缺失,/c/… 拼写只因风格不等就整组熔断静默失效(444f548 的提交说明记录了这一形状:posix base 会让 every win32 fuse miss),只剩语义评审兜底。tests/audit-msys-spelling-fuses.test.mjs 钉「posix 拼写 → 与 win32 同裁决」与「平台门控」;tests/contract-devloop.test.mjs、tests/cd-relative-anchor.test.mjs 钉的是更弱的不变量(普通位置仍 classifier-eligible、相对穿越仍 denied),归一后两者仍成立。守卫解析的是归一化后的文本路径(resolveDeepest(textual),symlink.ts:LresolveDeepest):若拿原始参数 realpath,相对路径会被锚定到 process.cwd() 而非会话工作区,dsh web 下会把所有相对路径调用误硬拒(PR #4 修复)。受信区不止插件目录:trustedDirs 成员与 allowedDshSubpaths 一并构成复检区;aggressive 模式下「普通出区」是设计目标故放行,但落在 critical 树 / DSH_HOME / 插件运行态文件上的逃逸仍硬拒——运行态文件复检与位置模式无关,改审批/审计/学习状态在任何模式下都不算例行写。

3.5 声明式规则 rulesText rules.ts# —— 用户写的「最优先纸条」 ​

text
# 语法(每行一条,空行与 # 注释忽略)
Tool(pattern) | allow|deny|human [| reason|toolName|arguments]
pattern       | allow|deny|human [| field]

# 例:禁止任何含 "rm -rf" 的 bash
bash(rm\s+-\s*rf) | deny
# 例:任何工具的 any_dangling 参数命中即交人
(any_dangling)     | human
  • 工具作用域可逗号多选(含 * 通配);无括号则适配所有工具;field 缺省 arguments。
  • 最严命中即胜:同时命中多条时按 deny > human > allow 取最严,与声明顺序无关;同严重度并列保持声明序、取首条作为审计对象。evaluateRules 先用 extractRuleTargets 抽取动作面,顺序为主投影优先:command/script/code/prompt/text → file_path/path/cwd/workdir → content(全部候选,content 不再是「前两者全空时才用」的兜底);防锚定正则(如 ^git push)被 JSON 信封击穿。deny/human 规则对整条动作面逐一匹配(多一个无关字段不能把规则点名的值藏起来;因此未加锚的规则也会命中文件内容 content——写 deny/human 规则时请用锚点收窄),allow 规则只匹配主投影(即动作面里第一个非空候选;allow 是显式预授权,需锚在主投影上才生效。注意 pattern 是无尾锚的子串匹配——^ls 同样命中主投影为 ls; rm -rf / 的调用,allow 规则请把结尾一并锚住);取严意味着遍历全部声明规则(不短路),扫描保持规则数线性。
  • ReDoS 防护:长度 ≤2000;拒绝嵌套无界量词 (a+)+、交替外套量词、嵌套重复组、{n,} 计数重复。
  • host 与浏览器设置卡共用同一份 parseRulesText(错误逐行红字显示)。
  • 干跑 rulesDryRun:只记命中不执法(host 端 if (config.rulesDryRun) 两处:pre-execute 与 answerer)。

3.6 产物登记 ArtifactRegistry artifacts.ts:LArtifactRegistry ​

3.7 已知边界 · 嵌套解释器与 opaque 锁定(2026-09-16 登记,不再加宽) ​

  • 已闭合:hardDenyShellReason 按解释器边界有界递归(MAX_NESTED_HARD_DENY_DEPTH=3,超出即硬拒);pwsh/cmd 归一同一平面;解释器 flag 认 -c/-Command 的无歧义前缀(如 -Com)、cmd 大小写(/C//K)与 -EncodedCommand 家族(-en/-enc/-encoded/-EncodedCommand:<b64>,编码体一律 hard-deny);pwsh 写 cmdlet 的路径/身份参数按无歧义前缀提取(-Pa/-Tar/-T 等),mklink 两个 positional 同判身份写。
  • opaque/heredoc 锁定:若其可执行正文含破坏性命令(含引号内 os.system('rm -rf …')、eval "$(cat <<EOF …)")或读凭据,携带结构化 opaqueLocked 进入 LOCKED 同款钉死拒绝倒计时,不再落在线评审器直放;普通数据正文(cat <<EOF)、commit 消息与只读命令维持原档。
  • 已知残余(如实写明):bash -c 多层转义套娃在深度 ≥4 时,解析器可能取不到内层 source 而退出递归,落回可应答 ask(深度 ≤3 与单层包装已闭合)。当前决定不再加宽(继续加宽会扩大 over-block),登记于此备查。
  • AD(面板 marker):breaker/locked/awaiting 文案改读 host reason(按 approval key 的有界 map),不再信任含工具命令回显的 panel.textContent;onDetach 仅清 canonical key,宿主 pending.key 若非 canonical 会残留(受 MAX_PENDING_REASONS=500 封顶)。