SwiftAIShellby

让 AI 在你的服务器上敲命令——Shellby 的三级审批门与五道护栏

Shellby 复盘:一个能自主执行命令的 AI Agent,最难的部分不是让它干活,是不让它闯祸。

Shellby 里有个 AI Agent 模式:你用一句话说清运维目标——「装好 nginx 并反代到 :3000」——它自己规划步骤、一条条执行命令、看输出决定下一步,直到做完。

这个功能价值最大,风险也最大。让一个语言模型在你的生产服务器上敲命令,稍有不慎就是 rm -rf 加身。所以整个设计的核心不是「怎么让它干活」,而是「怎么在它干活的同时不让它闯祸」。这篇讲两层安全机制:命令级的三级审批门,和循环级的五道护栏。

为什么是「工具循环」而不是「先规划后执行」

先说架构选型,因为它决定了安全边界长在哪。

候选里最诱人的是 plan-then-execute——先让模型列出完整计划,用户批准整个计划,再逐条跑。听起来干净,但计划在第二步就过时了:端口可能被占、发行版可能不是你以为的那个、上一条命令的输出会推翻后面的假设。运维任务的本质是「边看边重规划」。

最终选的是单 Agent + 客户端工具循环:模型发一个 tool call → 客户端经审批门执行 → 结果回填 → 模型看结果决定下一步 → 直到它自己说结束。计划的价值没丢,只是降级成循环首轮的「任务理解 + 预计步骤」,让用户先心里有数。

还有一个决定是安全的前提:循环必须跑在客户端。工具要在你的 SSH 连接上执行,而连接和凭据都在你的设备里。服务端托管的 Agent 根本碰不到你的服务器——这不是取舍,是物理约束。也正因为循环在本地,每一步执行前才有地方插入审批门。

第一层:三级审批门——分类器是边界,不是模型

每条命令执行前,由一个纯逻辑的 CommandClassifier 在本地分类:

级别 判据 行为
只读 白名单正则:ls / cat / grep / ps / df / systemctl status / journalctl / uname 自动放行,折叠显示
变更 不在只读白名单,且非破坏模式 弹卡,点击批准
破坏 rm -rf / dd / mkfs / shutdown / reboot / 管道到 sh/bash / 重定向到系统路径 / chmod -R 红框强确认,二次点击

这里有一条铁律,它是整个安全模型的地基:模型自报的 rationale(执行理由)只用来给用户看,绝不参与安全判定

为什么这么强调?因为如果你信模型的自我描述来决定放不放行,那攻击面就变成了「能不能骗模型说这条命令是只读的」——而语言模型是可以被 prompt 注入、可以自己搞错的。分类器是本地的、确定性的、可单测的一段代码,它看的是命令本身。模型可以撒谎或犯错,分类器不会。安全边界必须长在你控制的确定性代码里,而不是长在模型的判断里。

配套两个细节:

  • 拒绝要带理由回传:用户点「拒绝并说明」时,原因作为 tool_resultis_error: true)回给模型,它会调整方案,而不是傻乎乎地把同一条命令再发一遍。
  • Agent 命令绝不走广播:Shellby 有个「命令广播」功能(一次输入发往多个会话)。Agent 的命令只发当前会话,绝不经过广播路径——你不会希望 AI 的一条 rm 同时飞到十台机器上。

第二层:五道循环护栏——LLM 会漂移,兜底要多层

命令级的门管住了「单条命令危不危险」。但一个自主循环还有另一类风险:它会漂移——空转、假装完成、原地打转、烧钱。这些不是安全漏洞,是「模型不靠谱」的常态,得靠护栏兜。

护栏一:最大迭代数(默认约 15)。最朴素的兜底,但它只是最后一道——真正治漂移的是下面几道。

护栏二:无进展护栏。连续 3 轮全是「未知工具调用」,就判定这个 provider 的工具格式不兼容,提前停并明确报因,而不是空转到迭代上限。未知调用还会作为可见步骤暴露出来,方便诊断——静默的失败比响亮的失败更难查

护栏三:计划未完成追问。模型有个典型漂移:它宣布「我接下来去做 X」,然后不带任何工具调用就停了(end_turn)。如果此时它自己列的计划还有没做完的叶子步骤,我们不轻信它的「已完成」——自动注入一条「请继续剩余步骤」的追问,最多补两次。它跟踪最近一次计划,按叶子节点算真实完成度。

护栏四:停滞检测。有计划的前提下,连续 6 轮「计划完成度不涨」就判定在原地打转。典型场景是模型反复只读探查「想先理解全貌」却从不动手改。这时提前停,并给出明确诊断(提示可以在追问里让它开始动手、或换个更强的模型),而不是烧满迭代上限才 Done。

停滞护栏有个反直觉的坑,也是实测踩出来的:执行了变更/破坏命令或写了文件,就重置停滞计数。因为曾经把一个正在用 sed 改配置、只是懒得更新 plan 的会话误判成「打转」而中断了。真在干活(哪怕没更新计划)不该被误伤,只有「连续多轮既无进展、又只在只读探查」才算停滞。护栏要防的是空转,不是防它闷头干活。

护栏五:脱敏。命令输出回传给模型前,一律过一遍 Redactor。Agent 比只读诊断更危险——它会主动跑命令去拉更多输出,撞上密钥、token 的概率更高。脱敏这一步不能省。另外输出截断也有讲究:回填给模型前保头保尾,中间截断处显式标注「已省略 N 字节」,绝不静默截断——否则模型会基于残缺的信息做出错误判断。

write_file:为什么要先读旧内容做 diff

四个工具里,write_file 的设计单独说一下,因为它体现了同一套安全哲学。

它不走 shell 的 heredoc,而是走 SFTP。写之前先用 SFTP 读回旧内容,算出 diff,把 diff 渲染成审批卡让你看清「这次改动到底动了哪几行」,你批准了才写。

为什么值得这么麻烦?因为「让 AI 改配置文件」是个高危动作,而 diff 是人类审查改动最自然的界面。直接让模型 echo ... > /etc/nginx/nginx.conf 你根本看不清它改了什么。先读后 diff,等于把「审查」这个环节做成了产品的一等公民。

写入用登录用户的权限;需要 root 时提示先写 /tmpsudo mv——不偷偷提权。系统关键路径的写入,按路径直接归到破坏级,走强确认。

小结

做这个功能最大的心得是:当你把一个不可靠的执行者(LLM)接到一个高危系统(生产服务器)上,安全不能靠执行者自觉,只能靠你在它周围搭的确定性结构。

  • 命令的危险判定,长在本地可单测的分类器里,不长在模型的自我描述里;
  • 循环的漂移,用多道正交的护栏兜——每道治一种失败模式,且允许「真在干活」豁免,避免误伤;
  • 高危动作(改文件、破坏命令)做成需要人类点头的一等交互,而不是藏在自动流程里。

模型会越来越强,但「不信任执行者、把安全边界握在自己手里」这条原则不会过时。Agent 的价值在于它能自主,Agent 的安全在于它的自主始终被你框着。

留言

  • 加载中…

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