macOSSwiftPier

宁可漏报,不可误杀——一个 fail-closed 的清理分级引擎

Pier 3.0 复盘:一键清理只包含「绿色」项,而一个进程要进绿色,得同时越过一整排硬护栏。讲讲确定性规则怎么设计、为什么绿色的默认是「不合格」、以及一份把这些规则钉成发布门槛的「安全不变量」清单。

Pier 3.0 的一键清理,只碰「绿色」项——Pier 有足够信心可以安全停掉的开发残留。整个产品的安全性,压在「一个进程凭什么被判成绿色」这个问题上。这篇讲那台 Safety Engine 的分级逻辑,核心思想只有一句:绿色的默认是「不合格」,要升绿,得同时越过一整排硬护栏;任何一条过不去,就退回黄色或红色。

为什么是确定性规则,不是模型

先说选型:MVP 用的是确定性规则,不是机器学习。

理由和给 AI agent 敲命令做审批门是同一个——安全判定必须可单测、可解释、可复现。一个 ML 分类器说「这个进程 87% 可以清理」,你没法向用户解释那 87% 是怎么来的,也没法写测试锁住「数据库永远不进绿色」。确定性规则相反:每条护栏是一段看得见的逻辑,判定输出一组稳定的原因码——known_dev_serverproject_cwdno_live_connectionsdatabase_protectedsystem_ownedmissing_restart_commandeditor_still_running——既能本地化成人话给用户看,又能在测试里逐条断言。

绿色的硬护栏:全部满足才合格

三色里,红色和黄色是「宽进」的——有任何一点可疑就归进去。真正难的是绿色,它是「严出」:下面每一条硬护栏必须同时成立,缺一条就出局:

  • 属于当前用户,不是 root 或系统账户
  • cwd 位于用户项目目录;
  • 命中已知的无状态 dev server 或 watcher 模式;
  • 不是数据库、队列、持久容器、LaunchAgent、编辑器、AI agent 本体或系统服务;
  • 采样窗口内没有 established 连接
  • 最近 CPU 和网络活动都低;
  • 有足够的重启证据:cwd 和完整命令可用。

注意这排护栏的形状:它不是「攒够几个正面信号就放行」,而是「任何一个负面信号都一票否决」。数据库、live connection、系统账户、缺 cwd——每一个都是硬阻断。这正是「宁可漏报,不可误杀」翻译成代码的样子:把默认值设成拒绝,让证据去逐条解锁,而不是把默认设成放行、再去找理由拦。

藏在细节里的几条硬规则

护栏的真正难度在边界,几条实测钉出来的规则:

只有内核 argv 支持的服务指纹才配进绿色。 识别「这是 Vite / Next.js」有两种证据强度:内核捕获的可执行路径是高置信的,而命令行、被进程改写过的标题、正则命中都是低置信的——后者只能作为黄色提示。因为绿色项后面要靠这份命令把服务重启回来,而批量 ps 拿到的短命令、被压平的 command 根本不足以重启。可重启证据不全,就没资格进绿色。

AI 归因只认精确证据。 项目路径里出现 claudecursor 字符串,不能产生任何归因。agent 本体只接受精确的可执行文件名或 App bundle 证据。而且 Claude Code、Codex、Cursor 这些 agent 本体始终受保护——但它们启动的 nodepython 子服务,按子服务自己的证据判定,不因为「爹是 agent」就被连坐保护,也不因此升绿。

父进程还活着,最多是黄色。 一个绿色候选的进程树根,必须已经孤儿化、或被重新挂到 launchd。如果父终端、编辑器、coding agent 还活着,Pier 不能替用户判断「这一轮开发到底结束没有」——只能归黄色,让用户自己确认。

package script 不能凭端口升绿。 npm / pnpm 这种入口特别危险,因为 package script 能执行任意用户代码。它只有在自身不持有监听端口、同用户同项目、进程树已完整孤儿化、且树里有已验证的无状态监听后代时,才拿得到绿色证据。shell 永远是黄色;esbuild / swc 这类 build helper 还必须来自当前项目的 node_modules 路径才可升绿。

发布级安全不变量:把规则钉成门槛

这套逻辑最有意思的产出,是一份「安全不变量」清单——它们不是可选优化,是 3.0 的发布门槛,任何一条被破坏就不能发版。摘几条最能体现思路的:

  • 原始数据源 ≠ 展示数据。 ProcessScanner 能读全系统进程,但它只是原始数据。首页的内存、数量、列表,只统计经过 DevResourceBuilder 边界过滤后的开发候选。普通的 Documents、Downloads 目录不能仅凭「里面有个进程」就进清理首页;
  • owner 必须来自真实用户字段。 进程 owner 取 ps 的真实 user,绝不能把读不到端口信息的 root 进程回退成当前用户——那会把系统进程伪装成可清理项;
  • 单一真相源。 一键清理的绿色规则,只有 SafetyEngine 一个真相源。动作层可以增加实时护栏,但不能复制并放宽绿色规则——安全逻辑一旦被复制,两份就会漂移,其中一份迟早被人「顺手」放松;
  • 顶层统计按资源 ID 去重。 同一棵进程树的护栏节点被多个视图引用,统计内存和数量时不能重复计算;
  • 诊断失败要显式失败。 拿不到数据时必须显示独立的失败状态,绝不能显示 0 B 或「没有遗留」——把「我没查到」伪装成「这里很干净」,是最危险的一种谎。

最后一条尤其是 fail-closed 的精髓:不确定的时候,系统要明确地表达「我不确定」,而不是乐观地假装安全。 一个把「查询失败」渲染成「一切正常」的清理工具,比不装还危险。

复盘

  • 危险的默认值要设成「拒绝」。 绿色(可清理)的默认是不合格,靠证据逐条解锁;而不是默认放行、再找理由拦。把否决权交给任意一个负面信号,「宁可漏报」就自动成立了;
  • 证据要分强度,别一视同仁。 内核 argv 是高置信、命令行正则是低置信;高危动作(放进一键清理、用它重启)只认高置信证据,低置信的只能降级成「需要确认」;
  • 单一真相源不可复制。 安全规则只留一份权威实现,其它层只能加严不能放宽。复制出来的第二份安全逻辑,就是未来某次「优化」里被悄悄放松的那份;
  • fail-closed 包括「诚实地失败」。 查询失败、证据缺失、状态未知,都要显式表达不确定,不能用乐观的空值(0 B、「无残留」)糊过去——把不确定伪装成安全,是清理工具最不该犯的错。

下一篇讲更动态的一层:绿色判定用的是诊断那一刻的快照,但用户点下清理到信号真正发出之间,世界还在变——怎么停一个随时可能长出新连接、新子进程的目标。

留言

  • 加载中…

留言先审后发,通过后公开显示;邮箱只有站主可见。