Laya guard:为AI编码代理命令执行加一道本地安全闸门 做 AI 编码代理相关开发的朋友应该都有同感编码代理最让人没底的不是写代码而是跑命令。代码生成错了最多报个错命令跑错了轻则丢文件重则把环境搞乱。Laya guard 这个名字就是“守门人”的意思它解决的问题很直接——在编码代理执行每一条命令之前先在本地做一次合法性检查。你可以把它理解成一个独立的本地进程代理要跑什么命令先发给它它看完规则后告诉你放行、询问还是拦截。我用这个方式接了自己的编码代理工作流之后基本告别了那种“眼睛一闭让代理随便跑”的失控感。项目整体不大但设计上有不少值得展开的地方下面我把从需求、设计到落地踩坑的完整过程记录下来适合正在折腾编码代理自动化的朋友参考。1. 编码代理的命令执行为什么会失控1.1 模型看不到命令的“后果”先说一个容易被忽略的事实编码代理上下文中的“命令”对它来说只是一段文本。它并没有真正去评估这条命令会对文件系统、网络、进程环境造成什么影响。代理说“运行rm -rf ./temp”它是按照自然语言含义“清理临时目录”来理解的但路径一旦拼错落在./temp/*和./tempdir/*之间结果是完全不一样的。人的优势在于看到命令行那一瞬间会本能地意识到“这条命令会把我整个分支目录删掉”但模型的优势恰恰也是它的劣势——它对文本的语义理解很表面对系统的破坏性语义理解很有限。所以执行前的检查不应该放在模型头脑里应该放在执行现场。我以前也试过单纯在 prompt 里写“不要执行危险命令”比如“运行删除命令前要停下来问用户”效果时好时坏。prompt 约束是概率性的不是确定性的。同样是rm -rf这次它问了下次它在执行一个自动清理脚本时可能顺手就跑了。真正靠得住的是命令发出后、进入 shell 前有一个没有任何模型推理参与的环节用确定性规则去挡。1.2 几类最常见的翻车现场我这里列举几类我在实际使用中见过的有代表性的事故类型细节都做过模糊处理。第一种是误删类代理在跑测试时想清理旧产物一条find . -name *.log -exec rm {} \;把包含本地仓库索引在内的文件搞乱了幸好还能从远程仓库恢复。第二种是下载执行类代理为了装一个所谓“增强分析库”直接执行了curl ... | bash命令一直挂着等反应过来的时候已经不知道下载了什么。第三种是覆盖类代理在调整某个服务配置时把重定向写到了系统级的配置路径直接把原文件覆盖了。这几类事故的共同点就是代理在执行前根本没有“危险意识”。有人说“我在旁边盯着命令执行前它会问我”。这确实是一种解决方案但实际体验并不好。高频度的“要不要运行”弹窗会出现两种极端要么你失去耐心全部回车放行等于形同虚设要么为了安全你逐个检查编码代理带来的效率优势被抵消大半。Laya guard 的思路是把“可自动判断”的部分交给规则把真正模糊的部分留给人工而不是让每一条命令都占用你的注意力。1.3 这类工具的定位所以 Laya guard 面向的场景不只是“防止代理乱来”更准确地说是在“信任”和“效率”之间找一个可控的平衡点。它只在本地运行不把命令内容上传到任何远端服务它不接管编码代理的推理过程只负责命令执行链路的前置检查。对资产敏感、环境不固定的开发场景来说这样一个本地小工具比依赖云端的审查方案更让人放心。它不需要你改变日常编码习惯也不用给代理喂大量上下文代码层面的改动全部集中在“命令执行”这一个小环节上。2. 守卫怎么判断“要不要放行”2.1 核心判定模型三段式Laya guard 对一条命令的判定结果分三类allow放行、ask询问、deny拦截。很多类似工具做的是“放行 / 拦截”二选一但我在实际设计里坚持加了一个“ask”中间态。为什么因为二元判定会把太多本来应该人工介入的模糊案例强行塞进规则里。规则只有“安全”和“危险”两种判断时你为了不误杀正常操作就必须把规则写得很具体具体到某个命令的某个参数组合维护成本非常高。加一个中间态之后规则可以写得粗一点比如“只要包含rm -rf就去询问”这样既不误杀正常清理又能拦住真正的高危场景。再往下每个判定结果还要带上两个字段原因和有效期。原因用于展示给用户让“为什么拦”这件事可解释。有效期TTL的作用是减少重复询问同一个会话里同一类命令放行过一次之后短时间内的后续请求可以自动沿用前面的判断。我实际做的时候把 TTL 默认设为 5 分钟太长会让风险累积太短又起不到减少打断的作用。2.2 规则编排黑名单、白名单、兜底用户最终写的是规则文件我用的是 YAML。规则文件分三层黑名单、白名单、兜底。黑名单里匹配到的命令默认拦截白名单匹配到的默认放行两条规则都命中时白名单优先级更高。规则名匹配模式默认动作说明高危删除\brm\s-rf\bask给一次人工确认机会远程执行(curl|wget).*\|.*\b(sh|bash)deny禁止来历不明的管道安装磁盘操作dd\sof/dev/deny防止覆盖系统设备项目内清理rm -rf (dist|build|.cache)allow进入白名单不打断这里面黑名单的优先级低于白名单这一点很重要。因为我真的遇到过一种情况rm -rf dist这种命令会被“高危删除”规则命中并触发询问但用户后续要反复重建构建产物同一个弹窗出现几十次。所以我加入了“项目内清理”白名单规则。规则引擎按优先级从高到低扫描命中即返回如果没有规则命中就走兜底策略。兜底策略默认是 ask如果有人想在完全隔离的容器里跑也可以把兜底改成 allow但默认必须保守。2.3 为什么做成独立本地进程而不是库最开始我考虑过直接把检查逻辑做成一个 Python 库让编码代理在执行命令的时候调用guard.check(cmd)。但后来放弃了这个方案原因有三个。第一编码代理的执行模型各不相同有的是插件机制有的是外部工具调用如果做成库每个代理工具都要写适配层。第二规则需要持久化、需要热更新一个独立进程可以把规则文件放内存里改完配置立刻生效嵌入库的话反而还要考虑缓存和重新加载的问题。第三也是最重要的一个独立进程意味着检查逻辑完全可以复用不光是编码代理还可以用在你自己写的 CI 脚本、本地封装脚本、任何会拼出 shell 命令并执行的地方。所以最终形态是一个本地守护进程监听 loopback 地址的 HTTP 端口 一个命令行包装器把某条具体命令发送给守护进程根据返回结果决定执行还是退出。3. 核心实现落地从协议到代码3.1 一次检查请求长什么样守护进程暴露两个端点一个是 POST /check一个是 GET /health。POST /check 接收 JSON里面最基本的结构是这样{ id: 31337-001, command: rm -rf ./dist, cwd: /home/user/projects/demo, env: { PATH: /usr/local/bin:/usr/bin:/bin, HOME: /home/user }, agent: demo-agent, session: worker-1 }字段里面command 是希望执行的完整字符串cwd 是该命令的工作目录env 按需截取一些关键环境变量。因为很多命令的行为跟当前目录环境强相关比如npm install在项目中还是项目外.gitignore生效范围完全不同。response 是{ id: 31337-001, verdict: ask, action: block_if_user_rejects, reason: 命中高危删除规则rm -rf 删除操作需要人工确认, ttl: 300, suggested_command: ls -la }suggested_command 这个字段可有可无但它是我设计后期觉得特别有用的一个东西当守护进程判定现有命令是高风险但看起来只是用户操作顺序有问题时可以顺手给一个更安全的替换建议我把它返回给调用方调用方再决定要不要展示给用户。3.2 规则引擎的实现思路规则引擎本身不复杂但有几个细节要处理好。我先用一段 Python 伪代码示意核心逻辑# laya_guard/engine.py import re from dataclasses import dataclass from enum import Enum class Verdict(str, Enum): ALLOW allow ASK ask DENY deny dataclass class Rule: name: str pattern: str action: Verdict priority: int 50 enabled: bool True DEFAULT_RULES [ Rule(高危删除, r\brm\s-rf\b, Verdict.ASK, priority100), Rule(远程代码执行, r(curl|wget).*\|.*\b(sh|bash)\b, Verdict.DENY, 95), Rule(覆盖系统目录, r\s*/etc/, Verdict.DENY, 90), Rule(构建产物清理, rrm -rf\s(dist|build|\.cache)\b, Verdict.ALLOW, 5), ] def evaluate(command: str, context: dict) - tuple[Verdict, str, str]: for rule in sorted(DEFAULT_RULES, keylambda r: -r.priority): if re.search(rule.pattern, command): return rule.action, rule.name, f命中规则[{rule.name}] return Verdict.ASK, 兜底策略, 没有规则命中默认人工询问实际的代码会更完整规则从 YAML 加载、支持通配、支持按目录限定。但核心逻辑就这一条按优先级扫规则第一个命中的规则决定结果。这个设计的好处是可解释、易调试。任何一个命令被拦截你都能明确说出是哪条规则、为什么。有一个必须注意的点规则匹配的正则最好做“预编译 缓存”因为代理干活的时候命令密集程度很高一条命令一个正则现场编译会白白吃掉很多 CPU。我在项目里是把规则文件变更时的校验和记住文件不变就用内存里的编译缓存。实际写代码时re.compile之后存到一个 dict 里key 是规则名命中时直接取缓存对象。3.3 询问交互终端里最舒服的几种方式当 verdict 是 ask 的时候调用方需要做交互。我做交互时试过几种方式体验差异挺大。第一种是命令行参数方式比如laya-guard run --allow-ask直接放行所有 ask或者--deny-ask全部拒绝适合无人值守的脚本。第二种是读取标准输入在命令执行前打印提示等待用户输入 y/N。这种方式在普通终端里没问题但编码代理如果不是跑在终端而是通过 API 调用就没有交互通道所以需要退回到“调用方配置的策略”。第三种是后面加的轻量 TUI 界面直接在当前会话里显示候选命令、风险原因和“放行/拒绝”按钮适合自己手动审查多命令的场景。最终我保留了第二种和第三种接口上用同一个回调函数通知调用方“用户在界面上选择了什么”。在实际接入编码代理时我建议把整个交互做成分层降级有终端界面就走 TUI没有终端界面就走 stdout 提示和读取输入如果再没有可用输入就按用户的默认策略处理。默认策略我没有做成全局 allow而是建议做成 deny宁可让代理多等一下也不要让一条高风险命令在无人确认时静默通过。3.4 把编码代理的执行器指向包装器编码代理最终还是要通过某个方式执行命令。大多数情况下你可以通过配置或插件把它的命令执行工具指向我们提供的包装器。包装器的核心逻辑是这样的#!/usr/bin/env bash # laya-sh: 本地命令执行的保护壳 cmd$* payload$(python3 - PY import json, os, sys print(json.dumps({ command: sys.argv[1] if len(sys.argv) 1 else , cwd: os.getcwd(), env: {PATH: os.environ.get(PATH, ), HOME: os.environ.get(HOME, )}, session: os.environ.get(LAYA_SESSION, unknown) })) PY resp$(curl -s -X POST http://127.0.0.1:8747/check \ -H Content-Type: application/json \ -d $payload) verdict$(echo $resp | python3 -c import json,sys; djson.load(sys.stdin); print(d[verdict])) if [ $verdict deny ]; then echo [laya-guard] 拦截: $cmd 2 exit 1 fi if [ $verdict ask ]; then echo -n [laya-guard] 请确认命令是否放行 [y/N]: read -r choice /dev/tty || true case $choice in y|Y) ;; *) echo [laya-guard] 已拒绝 2; exit 1 ;; esac fi bash -c $cmd这个包装器的作用简单说就是代理原本要执行bash -c git push --force你把它配置成执行laya-sh git push --force那么整条命令先到守护进程那走一轮检查拿到 allow 之后才真正进 shell。中间的 curl 调用用的是本地回环网络延迟和开销都可以忽略。4. 日常工作流接入与实测调优4.1 跑起来只要三步第一步启动守护进程直接在项目目录执行后台运行命令比如laya-guard serve --config config.yaml健康检查根接口返回 200。第二步把代理的命令执行器替换成我们写好的保护壳。具体到不同代理做法不同有的是改一个环境变量有的是改工具定义共同点是最终都指向laya-sh。第三步跑一个小例子验证让代理执行一个简单命令比如ls -la你应该能观察到守卫有放行记录再让它跑curl ... | bash应该直接被拦下。我第一次接入的代理是命令行交互型的配置并不复杂。真正麻烦的是要改代理的工具定义也就是把它默认的运行命令工具改成一个自定义命令调用。改完之后第一件事就是验证“代理杀不掉守卫”我试过故意让代理执行kill -9去杀守卫进程幸好保护壳在启动时把守卫的配置写进了环境变量代理根本拿不到系统层面的进程信息这个场景也属于需要默认 deny 的一类。4.2 一次性把规则调到不烦人的门槛接入之后最影响体验的是“弹窗频率”。刚开始规则写得粗任何rm都会触发询问哪怕是代理执行rm -f output.tmp这种几乎无风险的操作。我调了两轮之后才掌握一个比较合适的原则明确无副作用的命令放行进白名单git status、git diff、ls、cat这类只读操作直接 allow。有写操作但范围明确的走 asknpm install、pip install、git push、rm指定文件。有不可逆影响、或者涉及系统目录、或者存在远程下载执行链路的默认 deny不询问。兜底策略保持 ask不轻易改成 allow。这样调完之后普通开发流程里大部分只读命令不会打断真正到删除、覆盖、推送这些关键动作时才跳一次确认效率和安全基本平衡。还有一个经验白名单规则的作用域一定要带目录前缀。我在项目里维护了一套白名单其中一条是“在当前项目目录下允许git clean -fdx”。一开始这条规则是全局的有一次在另一个仓库里代理顺手执行了同样的命令因为没有经过确认把整个本地构建缓存全部清掉了。那次之后我把所有白名单规则都加上了cwd前缀条件。现在再看这个设计目录隔离规则的主意量级很小但实际保护效果差别巨大。4.3 多代理、多会话的隔离一个编码代理在工作时可能会创建多个会话。为了不互相干扰我让每个会话独立的 session ID规则命中记录和 TTL 缓存的 key 都带上会话 ID。如果多个代理同时跑就为每个代理分配独立的 Laya guard 实例或者用 agent 字段做隔离。这里有一个容易踩的坑如果两个会话共享同一个 TTL 缓存A 会话里人工放行过的一条命令B 会话还没经过确认就自动放行了这在并发场景下非常危险。所以我强烈建议 TTL 缓存的 key 至少要包含 session更严格一点还应该包含工作目录。5. 常见问题与排查速查5.1 高频问题速查表我在开发中和给别人部署时碰到了不少问题挑几个高频的列在这里。现象可能原因排查与解决所有命令都能直接执行守卫像不存在代理没真正使用保护壳还在直接调用系统 shell检查代理工具定义看日志里有没有 POST /check 请求我踩过这种改了工具配置但代理缓存了旧的执行器启动守护进程报端口被占用之前有残留进程没退出先查看端口占用情况再决定清理旧进程还是换端口更新版本前记得先停旧进程明明配置了白名单命令还是被拦截规则优先级问题或者白名单规则的作用域不对检查规则优先级白名单调高检查规则文件是否保存成 UTF-8 且无 BOMask 交互在无人值守时卡住编码代理通过 API 调用没有终端交互通道给调用方配置default_on_askdeny避免重要命令在无人确认时被静默放行代理跑异常慢本地 HTTP 调用延迟累计 规则正则预编译缺失确认守卫监听在 127.0.0.1不是非回环地址确认正则做了编译缓存建议把 POST /check 的响应时间控制在 5ms 内5.2 两个容易踩的半隐蔽陷阱第一个陷阱是 ask 交互在无人值守时的表现。你在自己终端里跑得很顺畅不代表代理在 API 模式下也顺畅。很多代理工具的执行上下文根本拿不到用户交互通道stdin 是关闭的这种情况下如果一条命令被判为 ask程序就会无限期等待输入。我在设计接口时特意留了一个interactive参数调用方明确告知“我有没有交互通道”如果没有ask 直接降级成 deny。这样至少不会出现“命令卡死但不知道原因”的尴尬。第二个陷阱是规则误匹配。正则匹配命令字符串是很容易出事的比如你想拦截rm -rf开头的删除操作结果命令rm -rf --no-preserve-root ./tmp可能会漏也可能cat rm -rf.md这种文本阅读命令被误伤。我的处理方法是做简单的命令词法裁剪先把命令按空格和引号拆成 token然后只对可执行命令名和它的参数做模式匹配对管道两侧分别判断。这块比开始想象的复杂但它直接决定了误报率值得多花时间。6. 一点实操心得6.1 调校规则之后最大的体会这个项目我前前后后做了两三个版本第一版很粗糙就是把所有命令直接丢给一个正则列表结果误报多、配置复杂、用起来很烦。后来痛定思痛把判定模型收敛成“白名单优先、黑名单兜底、中间态询问”整个工具的可维护性才上来。我个人最大的体会是给编码代理做安全闸门核心原则是“确定性的部分让代码管不确定的部分让人管不要让人管确定的部分也不要让代码管不确定的部分”。实际用下来它不只是保住了文件系统的安全还让编码代理在我这边更容易被信任。因为出了事你知道会有拦截写 prompt 的时候不用反复叮嘱“不要删文件”心态也松弛很多。6.2 值得继续做的几个扩展方向如果后面要继续扩展这个方向我觉得有几个点值得做把规则文件改成可以按项目模板发放一个仓库配一套默认规则在守卫端记录完整的命令审计日志方便事后追溯再加一个“试运行沙箱”模式在执行前先把命令放容器里跑一遍用输出判断是否安全。这里面沙箱模式的价值最大但成本也最高至少要等有需要的时候再说。对我而言手头这个版本已经能让日常工作舒服不少后面的扩展更多是锦上添花。