OpenAI Agents SDK 工程笔记:LocalShellTool 与 ApplyPatchTool 执行面边界与生产禁区

千笔-AIWritePaper · https://www.aiwritepaper.com

LocalShellTool / ShellTool 与 ApplyPatchTool 让模型能在你的机器上执行命令、改文件。SDK 不会 替你做工作区隔离:executor / editor 收到什么就执行什么。本文在 openai-agents 0.23.1 下用脚本化假模型发出真实的 local_shell_call / apply_patch_call,对比「朴素实现」与「加闸实现」,留下 13 行 CSV(_w/exec-smoke/)。

图:左侧为朴素 executor 的越界路径;中部为五类失败;右侧为加闸后的工作区根与白名单边界。

目标说明

  1. 说清 LocalShellTool 只保证「调用 executor」,不保证沙箱。
  2. 为工作区根、命令白名单、路径穿越、超时、输出截断、删除审批各写一条可观测规则。
  3. 复现朴素实现的五条失败与加闸后的拦截。
  4. 区分本工具闸门与 OS 级沙箱(见 9-28 E)的分工。

适用与边界

适合:在受控工作区跑只读/有限写入 Agent。不适合:把白名单含 python3 -c 当成安全边界。解释器可以读任意路径(需 OS 沙箱)。本实验全在临时目录,密钥均为演示字符串。

机制

LocalShellTool(executor=...):模型给出 command / working_directory / timeout_ms,SDK 原样交给你。ApplyPatchTool(editor=...):对 create/update/delete 调 editor;needs_approval 可在删除前打断 run。ShellTool 是下一代接口,本地模式同样要求 executor。

实跑摘要

text 复制代码
N1 FAIL cat ../host/secret.env → 回传 DB_PASSWORD=...
N2 FAIL env → DEMO_TOKEN 泄漏
N3 FAIL rm -rf ../host/data → 宿主目录被删
N4 FAIL 20 万字符 stdout 原样进上下文
N5 FAIL apply_patch 创建 ../host/evil.txt
G1--G7 PASS 路径/白名单/截断/超时/补丁越界均拦截;正常 create report.md 成功
G8 PASS needs_approval 删除 → interruptions=1,文件仍在

生产禁区

编号 禁区 证据 替代
D1 信任模型给的 cwd/argv N1/N3 解析后做 resolve() 前缀检查
D2 白名单含通用解释器却不拦代码参数 需 OS 沙箱 bwrap/microVM
D3 不截断 stdout N4 硬顶 4KB--64KB
D4 补丁路径不校验 N5 editor 内拒绝 ..
D5 删除无审批 --- needs_approval 对 delete

加闸 executor 节选

python 复制代码
ALLOW = {"ls", "cat", "wc", "head", "python3"}
def guarded_exec(req):
    cmd = req.data.action.command
    if os.path.basename(cmd[0]) not in ALLOW:
        return f"[BLOCKED] {cmd[:1]}"
    # 再查 cwd/argv 是否在 WS.resolve() 下;timeout;截断

验收清单与当天落地

  • 仓库内所有 Shell 与 Patch 工具已登记。
  • CI 中 N1 到 N5 状态为 FAIL,G1 到 G8 状态为 PASS。
  • 删除类操作可中断,且中断后文件仍在。
  • 文档写明本闸门并非 OS 沙箱。
  • 预发 WS 路径与生产布局一致。

当天 30 分钟可拷贝 smoke,改 WS 路径,接入预发 Agent 的 executor,先跑朴素组再跑加闸组。

踩坑

只拦 rm 却放行 python3 -c 或 sh -c,解释器仍能读任意路径。把 timeout_ms 交给模型却不设服务器上限,等于把拒绝服务按钮交给提示词。不要把 needs_approval=True 理解成已隔离。它只打断 run,不缩小文件系统视图。

假模型怎么发出真实工具调用

本实验不调用真实大模型。脚本构造符合 SDK 期望的 local_shell_call 与 apply_patch_call 载荷,直接驱动 Runner。好处是失败可重复,不烧配额。坏处是它不覆盖模型是否会主动尝试逃逸的分布问题。那一层应另做红队提示词集,与本烟测分开记账。CSV 里用 case_id 区分 N 与 G,方便 CI 过滤。假模型还要覆盖空命令、超长 argv、非 UTF-8 输出等边界,避免加闸逻辑只在「幸福路径」上被测到。

白名单设计的三档策略

只读档仅允许 ls、cat、head、wc、rg。构建档另加 python3、pytest,但禁止 -c、-m http.server 一类参数模式。运维档才考虑 docker,且必须叠 OS 沙箱。每升一档,测试集要新增对应逃逸用例。禁止把开发方便当作默认档。预发与生产至少差一档,避免开发机上的宽白名单被原样拷贝。白名单变更走代码评审,并在变更说明里写明新增命令的威胁模型。

环境变量与密钥擦除

N2 证明子进程默认继承父进程环境。Agent 宿主若导出了云厂商密钥,env 或任意打印环境的语言运行时都能带走。加闸实现在 subprocess 前构建最小环境,只保留 PATH、LANG、HOME(指向工作区内假 home)以及任务明确需要的变量。演示用的 DEMO_TOKEN 在 G 组中应消失。审计日志亦不得原文记录密钥,只记变量名是否被剥除。若任务必须注入短期令牌,用一次性文件描述符或内存挂载,跑完即焚,不要写进工作区明文。

超时、挂起与僵尸进程

模型可能给出极长 timeout_ms,或启动会挂起的命令。服务器侧应有硬顶,例如 15 秒,到时杀进程组而不是只杀父进程。管道写满导致的阻塞也要覆盖。测试里可放一个写无限输出的脚本,确认截断与超时同时生效。僵尸进程用 start_new_session=True 或显式进程组管理来避免。并发多个 Shell 调用时,还要限制同时存活的子进程数,防止资源耗尽。

ApplyPatch 的三类操作

create 要拒绝已存在路径上的覆盖,除非产品明确允许。update 要校验补丁上下文能否匹配,匹配失败时不得部分写入。delete 必须走审批。路径穿越用 ../、绝对路径、软链三种用例覆盖。N5 只演示了 create 越界。G 组应补 update 与 delete 的越界变体,否则 editor 只对一种操作加闸会留下缺口。补丁编码统一按 UTF-8 处理,遇到无法解码的字节应失败并记录,而不是静默替换。

路径解析的边界细节

resolve() 会展开符号链接。若工作区内存在指向 /var/log 的软链,前缀检查仍可能放行。对策是对最终路径做真实路径比较,并禁止在工作区内创建指向外的软链。Windows 下还要处理盘符大小写与长路径前缀。测试用例应包含 ..\..\host\secret.env 这类变体。补丁路径同样处理。create 与 update 的目标文件在写入前必须通过同一套前缀断言。对 ~ 展开与环境变量拼接出的路径,也要在解析后再做前缀检查。

输出截断与上下文预算

N4 把 20 万字符 stdout 塞进对话,下一轮推理成本与泄露面同时上升。硬顶建议按工具类型分开。只读查看类可到 64KB,构建日志类可到 8KB,并在截断处写入 truncated=true bytes=...。截断后的哈希可留给人工下载完整日志,避免模型侧持有全量敏感输出。stderr 与 stdout 合并计算字节,防止错误流被用来走私数据。

与 OS 沙箱的分工

本篇闸门解决的是模型参数不可信。bubblewrap、Firecracker、gVisor 解决的是进程即使被骗,也看不到宿主。两边都要。只有工具闸门时,解释器逃逸仍能读宿主。只有 OS 沙箱而 executor 不加白名单时,沙箱内仍可被滥用成挖矿或扫内网。9-28 E 的沙箱闸门与本文应在架构图上画成上下两层,缺一不可。联调时先单独证明每一层的失败用例,再叠加。

CI 里怎么固化

把 _w/exec-smoke/ 挂进拉取请求检查。断言 N 组 CSV 状态为 FAIL,G 组为 PASS。任意新人改白名单,必须同步改测试期望。禁用在 CI 跳过危险用例的开关。预发环境用与生产相同的 WS 布局,避免本地宽进、线上窄出造成假通过。CI 日志里只保留状态位与截断摘要,完整机密样本放在受控产物库。

审批流与审计

needs_approval 触发后,把 interruption 原因、候选路径、操作类型写入审计日志。人工批准应二次确认路径仍在工作区内,防止批准时路径已被替换。拒绝时给模型的回传要短,避免把宿主绝对路径教给下一轮。审批超时默认拒绝,并分页通知值班人。

文档与值班手册

把禁区表贴进值班手册。告警出现 [BLOCKED] 时,值班人先看是白名单拒识还是路径拒识。前者可能是功能需求,走变更评审。后者往往是提示词被注入或模型乱爬路径,应升高优先级。手册里写明不得为了让演示通过而临时放大白名单。每周抽查一条 G 组用例是否仍在生产配置下可复现。

与托管工具的差异

若改用托管计算机环境,边界由平台侧虚拟机承担,本地 executor 规则会变化。本文结论仅适用于本地 Shell 与本地 Patch。切换托管后应重写烟测,不要直接复用 N1 到 N5 的路径假设。字段名与事件类型以你安装的 openai-agents 版本文档为准。实跑版本为 0.23.1。升级小版本后至少重跑越界读与删除审批两条。

当天联调顺序

先在空临时目录跑完 N1 到 N5,确认朴素实现确实失败。再挂上加闸 executor 跑 G1 到 G8。把两份 CSV 差分贴进议题。最后才接入真实模型做一次手工红队。顺序反了容易在模型偶发没打出危险调用时误判为已经安全。联调记录保存工作区快照哈希,方便事后对比。

发布前签字项

负责人签字确认 N 组仍失败、G 组仍通过、白名单变更有评审记录、审计日志可检索最近七天的 [BLOCKED]。缺签字不得升生产。签字页与 CSV 哈希一并归档。

总结

LocalShellTool 与 ApplyPatchTool 的执行面默认等于你的进程权限。朴素实现会在越界读、环境泄密、删库、输出撑爆与补丁穿越五类问题上失败。加闸后的前缀检查、白名单、截断、超时与删除审批,把失败变成可观测的拦截。它仍不是 OS 沙箱。上线前用 N 组与 G 组两套用例钉死行为,并在文档里写明边界停在哪里。

回归用例最小集

即使来不及维护完整矩阵,也要保证五条回归永远在。越界读 ../secret、env 泄密、工作区外删除、超大 stdout、补丁越界 create。每周在预发跑一次,结果贴到固定频道。缺一次就视为执行面失控。

威胁模型一页纸

资产是宿主密钥、工作区外数据与生产数据库连接串。攻击者能力来自提示词注入与模型胡写路径。不在模型内解决恶意,而在 executor 边界解决不可信参数。成功标准是危险调用被拦截且可审计,而不是模型变得更听话。

相关推荐
AIGC大时代12 小时前
OpenAI Agents SDK 工程笔记:ModelProvider 多模型接入、Responses/Chat Completions 切换与生产禁区
responses·生产禁区·modelprovider·multiprovider
AIGC大时代8 天前
OpenAI Agents SDK 工程笔记:MCP 工具接入与生产禁区
mcp·生产禁区·openai agents sdk·mcpserverstdio·hostedmcptool
AIGC大时代22 天前
OpenAI Agents SDK 工程笔记:sessions 记忆边界再核对与生产禁区
生产禁区·sessions·sqlitesession·sessionsettings
AIGC大时代25 天前
OpenAI Agents SDK 工程笔记:streaming 流式输出与生产禁区
openai·streaming·生产禁区·run_streamed·streamevent
AIGC大时代1 个月前
OpenAI Agents SDK 工程笔记:handoffs 多 Agent 交接与生产禁区
openai·multi agent·生产禁区·handoffs·triage
AIGC大时代1 个月前
OpenAI Agents SDK 工程笔记:tool 调用循环、max_turns 与生产禁区
服务器·数据库·笔记·tool·max_turns·functiontool·生产禁区
爬点儿啥10 个月前
[Ai Agent] 12 Swarm 与 Agents SDK —— 去中心化的多智能体协作
去中心化·区块链·swarm·langgraph·agents sdk·handoff
缘友一世1 年前
Agents-SDK智能体开发[4]之集成MCP入门
llm·mcp·agents sdk