1. 从 .yardopts 的 --load 说起:审计脚本为什么要接 LLM
最近 Ruby 生态里有一类恶意 gem 的讨论值得写脚本的人警惕:触发点不是常见的require,而是.yardopts里的--load。YARD 在生成文档、安装依赖或 RubyDoc.info 处理文档时,会读取.yardopts,一旦其中通过--load指向某个 Ruby 文件,就可能在文档链路里执行任意代码。如果此时容器还能出网,恶意逻辑就可以继续做爬取、拉取二段载荷或回传环境信息。我们要做的事情不是围观事件,而是把已有的 YARD 审计脚本接上 LLM,让它对.yardopts、Rakefile、*.gemspec、extconf.rb、lib/**/*.rb这些候选入口做语义分级。开始接模型前,先去 TaoToken 官网创建 Key:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=yard_audit_intro,Base URL 统一用https://taotoken.net/api。这篇文章按脚本集成工程师的视角,给出一套可复现的接入步骤、Key 配置对照和运行命令。
为什么不用纯正则?因为.yardopts的写法可以很碎:--load ./script.rb、--load=script.rb、多行拼接、变量展开、注释干扰,甚至把真正入口藏在YARD::Handlers或自定义插件里。正则能抓关键词,但很难判断“这个--load指向的文件是否在文档生成时执行了系统命令”。LLM 适合做第二层判断:给它文件片段、调用上下文和审计规则,让它输出风险等级、证据链和建议动作。注意,LLM 不执行代码,也不替代沙箱;它只是审计流水线里的语义分析器。
在这篇文章里,你会看到:
- YARD 审计脚本的威胁模型和静态扫描输出格式;
- 在 TaoToken 控制台准备 Key、设置环境变量、用
curl验证通道; - 用 Ruby 写一个调用 TaoToken 的 LLM 审计脚本;
- YARD 脚本、Claude Code、Codex、CC Switch 的 Key 配置对照;
- 401、404、429 等常见错误的排障方式;
- 如何把静态阻断和 LLM 复核放进本地或 CI 流程。
2. 先固定威胁模型:哪些文件会被 YARD 当作代码入口
把 YARD 审计脚本接上 LLM 之前,先要明确审计范围。YARD 不是只读注释,它会加载配置、解析 Ruby 文件、运行 handler。下面这些文件应进入第一轮扫描:
.yardopts:重点看--load、--plugin、--require、-e等参数;Rakefile、Gemfile、*.gemspec:安装和构建阶段可能执行;extconf.rb:原生扩展编译入口;lib/**/*.rb:YARD handler、自定义 tag、monkey patch;.yardopts中--load指向的文件,以及这些文件再require的文件。
下面是一个不执行任何 gem 代码的静态扫描脚本。它把候选文件切成片段,输出 JSON,后续再交给 LLM 分析。这个阶段即使放在无网络容器里也能跑。
#!/usr/bin/env ruby # yard_surface_scan.rb # 用法: ruby yard_surface_scan.rb /path/to/gem > surface.json require 'json' require 'find' root = ARGV[0] || '.' patterns = [ '.yardopts', 'Rakefile', 'Gemfile', '*.gemspec', 'extconf.rb', 'lib/**/*.rb', 'tasks/**/*.rake' ] files = [] patterns.each do |pat| Dir.glob(File.join(root, pat), File::FNM_DOTMATCH).each do |f| next if File.directory?(f) files << f end end # 额外抓取 .yardopts 中 --load 指向的路径 yardopts = File.join(root, '.yardopts') if File.file?(yardopts) File.readlines(yardopts).each do |line| if line =~ /--load[=\s]+([^\s]+)/ candidate = $1 candidate = candidate.delete_prefix('./') full = File.join(root, candidate) files << full if File.file?(full) end end end def snippets(path, max_lines: 160) lines = File.readlines(path, chomp: true) lines.each_slice(40).with_index(1).map do |chunk, idx| { part: idx, start_line: (idx - 1) * 40 + 1, end_line: (idx - 1) * 40 + chunk.length, text: chunk.join("\n") } end.first(max_lines / 40) end report = { root: File.expand_path(root), generated_at: Time.now.utc.iso8601, files: files.uniq.map do |f| { path: f.sub(%r{\A#{Regexp.escape(File.expand_path(root))}/?}, ''), size: File.size(f), snippets: snippets(f) } end } puts JSON.pretty_generate(report)运行方式:
ruby yard_surface_scan.rb ./gems/suspect-gem > surface.json输出里不要带 token、cookie、私钥路径。审计脚本只读文件,不require、不eval、不system。如果需要在 Docker 里跑,先给容器--network none,把静态扫描结果落盘,再在主机侧调用 LLM。这样即使样本里真有恶意--load,它也没有网络可用。
3. 在 TaoToken 准备 Key:控制台、Base URL 与最小权限
TaoToken 的 Key 在控制台创建。打开官网入口:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=yard_audit_key_setup,登录后进入 API Keys 页面,创建一个专用于审计脚本的 Key。不要复用生产应用的 Key,也不要把 Key 写进yard_audit_llm.rb。推荐用环境变量:
export TAOTOKEN_API_KEY="YOUR_API_KEY" export TAOTOKEN_BASE_URL="https://taotoken.net/api" export TAOTOKEN_MODEL="YOUR_MODEL_ID"TAOTOKEN_BASE_URL就是https://taotoken.net/api。手动拼 OpenAI 兼容端点时,完整 URL 是:
https://taotoken.net/api/v1/chat/completions先验证 Key 是否可用:
curl -sS "${TAOTOKEN_BASE_URL}/v1/chat/completions" \ -H "Authorization: Bearer ${TAOTOKEN_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "model": "'"${TAOTOKEN_MODEL}"'", "messages": [ { "role": "user", "content": "只回复 pong" } ], "max_tokens": 8 }'如果返回 JSON 且包含choices,说明 Key、Base URL、模型 ID 三者匹配。如果返回 401,优先检查Authorization是否少了Bearer;如果返回 404,检查 Base URL 是否被 SDK 自动追加了/v1导致重复;如果返回 429,降低并发,不要在一个循环里对每个文件片段发请求。审计脚本应该批量合并片段,而不是按行调用。
模型 ID 从哪里来?在 TaoToken 的模型对话页面可以查看和切换可用模型。不要凭记忆硬编码,先复制控制台里展示的模型 ID,再写入TAOTOKEN_MODEL。这样脚本、Claude Code、Codex 的配置都使用同一个来源。
4. YARD 审计脚本接入 LLM:文件收集、提示词与 API 调用
静态扫描产出surface.json后,第二步是 LLM 审计。下面的 Ruby 脚本读取surface.json,按文件分批拼接提示词,调用 TaoToken 的 OpenAI 兼容接口。它不会执行被审代码,只把文本发给模型。
#!/usr/bin/env ruby # yard_audit_llm.rb # 用法: ruby yard_audit_llm.rb surface.json > audit_report.json require 'json' require 'net/http' require 'uri' API_KEY = ENV.fetch('TAOTOKEN_API_KEY') BASE_URL = ENV.fetch('TAOTOKEN_BASE_URL', 'https://taotoken.net/api') MODEL = ENV.fetch('TAOTOKEN_MODEL', 'YOUR_MODEL_ID') ENDPOINT = "#{BASE_URL.chomp('/')}/v1/chat/completions" SYSTEM_PROMPT = <<~PROMPT 你是 Ruby gem 供应链审计助手。你只分析给定文本,不执行代码,不假设文件已经运行。 重点关注: 1. .yardopts 中 --load、--require、--plugin、-e 是否加载了可疑脚本; 2. 被加载脚本是否包含 system、exec、spawn、Open3、IO.popen、`反引号`、eval、class_eval; 3. 是否读取 ENV、~/.ssh、~/.aws、浏览器 cookie、CI token 等敏感信息; 4. 是否发起 HTTP、DNS、WebSocket 请求,或下载二段载荷; 5. 是否伪装成文档处理、tag handler、monkey patch 触发。 输出必须是 JSON 数组,每项包含: path, risk, evidence, suggestion。 risk 只能是 high、medium、low。 evidence 必须引用片段中的行号或原文短句。 不要输出 Markdown,不要输出多余解释。 PROMPT def chat(payload) uri = URI(ENDPOINT) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = uri.scheme == 'https' http.read_timeout = 120 http.open_timeout = 20 req = Net::HTTP::Post.new(uri) req['Authorization'] = "Bearer #{API_KEY}" req['Content-Type'] = 'application/json' req.body = JSON.generate(payload) res = http.request(req) unless res.is_a?(Net::HTTPSuccess) warn "HTTP #{res.code}: #{res.body}" exit 1 end JSON.parse(res.body) end surface = JSON.parse(File.read(ARGV[0] || 'surface.json')) all_findings = [] surface['files'].each do |file| next if file['snippets'].empty? user_content = <<~CONTENT 文件路径:#{file['path']} 文件大小:#{file['size']} bytes 以下是文件片段: #{file['snippets'].map { |s| "### 片段 #{s['part']} 行 #{s['start_line']}-#{s['end_line']}\n#{s['text']}" }.join("\n\n")} CONTENT body = { model: MODEL, messages: [ { role: 'system', content: SYSTEM_PROMPT }, { role: 'user', content: user_content } ], temperature: 0.1, max_tokens: 1500 } resp = chat(body) content = resp.dig('choices', 0, 'message', 'content').to_s begin findings = JSON.parse(content) all_findings.concat(findings) if findings.is_a?(Array) rescue JSON::ParserError all_findings << { path: file['path'], risk: 'medium', evidence: '模型返回非 JSON,需要人工复核', suggestion: content[0, 500] } end end puts JSON.pretty_generate({ reviewed_at: Time.now.utc.iso8601, model: MODEL, findings: all_findings })运行命令:
export TAOTOKEN_API_KEY="YOUR_API_KEY" export TAOTOKEN_BASE_URL="https://taotoken.net/api" export TAOTOKEN_MODEL="YOUR_MODEL_ID" # 1. 静态扫描,建议在无网络容器中执行 ruby yard_surface_scan.rb ./gems/suspect-gem > surface.json # 2. LLM 审计,在可访问 TaoToken API 的环境执行 ruby yard_audit_llm.rb surface.json > audit_report.json # 3. 只看 high 风险 jq '.findings[] | select(.risk=="high")' audit_report.json提示词里要强调“引用证据”,否则模型容易给出泛泛结论。对于.yardopts里只有--load但对应文件不存在的情况,可以让模型标记为medium,因为它可能是残留配置,也可能是条件生成后才会出现的入口。审计脚本最终输出应保留原始路径和行号,方便人工打开文件复核。
5. Key 配置对照:脚本环境变量、Claude Code、Codex、CC Switch
YARD 审计脚本使用 TaoToken 时,配置方式和其他 AI 工具有相似之处,但字段名不能混。下面这张对照表可以直接收藏。官网配置入口在这里:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=yard_audit_config_compare。
| 场景 | 配置文件/位置 | 关键字段 | Base URL |
|---|---|---|---|
| YARD 审计脚本 | Shell 环境变量 | TAOTOKEN_API_KEY、TAOTOKEN_BASE_URL、TAOTOKEN_MODEL | https://taotoken.net/api |
| Claude Code | settings.json | ANTHROPIC_BASE_URL、ANTHROPIC_AUTH_TOKEN、ANTHROPIC_MODEL | https://taotoken.net/api |
| Codex | config.toml | model_providers.taotoken.base_url、env_key | https://taotoken.net/api/v1 |
| CC Switch | 供应商三件套 | Base URL、API Key、Model ID | https://taotoken.net/api |
Claude Code 的settings.json示例:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "YOUR_API_KEY", "ANTHROPIC_MODEL": "YOUR_MODEL_ID" } }这里注意:ANTHROPIC_*是 Claude Code 的配置字段,不要把它们写进 Codex 的config.toml。Codex 使用独立的 provider 配置:
model = "YOUR_MODEL_ID" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api/v1" env_key = "TAOTOKEN_API_KEY" wire_api = "chat"然后在 shell 里导出:
export TAOTOKEN_API_KEY="YOUR_API_KEY"CC Switch 的三件套可以按同样逻辑填写:Base URL 用https://taotoken.net/api,API Key 用 TaoToken 控制台创建的YOUR_API_KEY,Model ID 用控制台模型对话页面展示的值。切换供应商后,先跑一次最小请求,再启动 YARD 审计脚本。不要在脚本里同时读取ANTHROPIC_AUTH_TOKEN和TAOTOKEN_API_KEY,否则排障时很难判断到底哪套配置生效。
6. 运行与排障:401、404、429 与 Docker 断网
YARD 审计脚本接 LLM 时,常见错误集中在鉴权、路径和并发。先给一个最小验证命令,任何报错都从这个命令开始复现:
curl -i -sS "${TAOTOKEN_BASE_URL}/v1/chat/completions" \ -H "Authorization: Bearer ${TAOTOKEN_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "model": "'"${TAOTOKEN_MODEL}"'", "messages": [{"role":"user","content":"只回复 ok"}], "max_tokens": 4 }'排查对照:
401 Unauthorized:Key 缺失、拼错、用了其他平台的 Key,或者Bearer后多了空格。重新从 TaoToken 控制台复制。403 Forbidden:Key 权限不足或模型不可用。换控制台里可用的模型 ID。404 Not Found:Base URL 与 SDK 自动拼接冲突。手动拼时用https://taotoken.net/api/v1/chat/completions;如果 SDK 自动加/v1,Base URL 填https://taotoken.net/api。429 Too Many Requests:按文件循环调用太密。把多个片段合并成一个请求,或在脚本里加退避:失败后等待 1s、2s、4s。Timeout:审计样本太大。减少每个请求的片段数,先发.yardopts和被--load指向的文件。
Docker 运行建议分两段。第一段完全断网,只做静态收集:
docker run --rm --network none \ -v "$PWD:/work" \ -w /work \ ruby:3.3 \ ruby yard_surface_scan.rb ./gems/suspect-gem > surface.json第二段在主机或受控网络环境调用 LLM:
export TAOTOKEN_API_KEY="YOUR_API_KEY" export TAOTOKEN_BASE_URL="https://taotoken.net/api" export TAOTOKEN_MODEL="YOUR_MODEL_ID" ruby yard_audit_llm.rb surface.json > audit_report.json不要把恶意 gem 直接放进有网络、有云凭证、有 SSH key 的容器里跑bundle install。静态扫描和 LLM 分析都在样本代码未执行的前提下完成。如果必须动态验证,单独建无凭证、无网络、只读文件系统的沙箱,并且只用于确认静态判断。
7. 把审计结果固化到 CI:静态阻断、LLM 异步复核
在 CI 里,静态扫描适合做阻断,LLM 审计适合做异步复核。因为 LLM 请求有网络依赖和成本,不要让每个 PR 都无限制调用。下面是一个 GitHub Actions 片段,先跑静态扫描,命中高危模式时失败;LLM 审计只在需要时手动触发。
name: yard-gem-audit on: pull_request: paths: - "gems/**" - "*.gemspec" - ".yardopts" jobs: static-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: "3.3" - name: Static scan run: | ruby yard_surface_scan.rb . > surface.json if grep -E -- '--load|--require|--plugin|system\\(|Open3|IO\\.popen|eval\\(' surface.json; then echo "发现高危入口,请人工复核" exit 1 fi - name: Upload surface uses: actions/upload-artifact@v4 with: name: yard-surface path: surface.json llm-review: if: github.event_name == 'workflow_dispatch' needs: static-scan runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: "3.3" - name: LLM audit env: TAOTOKEN_API_KEY: ${{ secrets.TAOTOKEN_API_KEY }} TAOTOKEN_BASE_URL: "https://taotoken.net/api" TAOTOKEN_MODEL: ${{ vars.TAOTOKEN_MODEL }} run: | ruby yard_surface_scan.rb . > surface.json ruby yard_audit_llm.rb surface.json > audit_report.json - name: Upload report uses: actions/upload-artifact@v4 with: name: yard-audit-report path: audit_report.json注意,CI 里不要把TAOTOKEN_API_KEY写进 YAML 明文,放到 Secrets。TAOTOKEN_MODEL可以放 Variables,方便切换模型。LLM 报告里可能包含模型生成的证据片段,上传 artifact 前确认没有把私有仓库敏感内容带到公共产物。
本地也可以写一个只跑高危文件的小命令:
jq -r '.files[].path' surface.json | grep -E '\.yardopts|gemspec|extconf|Rakefile' | while read -r f; do echo "== $f ==" sed -n '1,120p' "$f" done先看人工最容易判断的.yardopts和extconf.rb,再把不确定的片段交给 LLM。这样能减少无效请求,也能让审计报告更聚焦。
8. 文末 CTA:模型对话 → Coding Plan → 创建 Key → Claude Code 文档
如果你已经按上面的步骤跑通 YARD 审计脚本,下一步可以把模型选择、额度和 Key 管理固定下来。建议路径如下:
- 先到模型对话页面确认可用模型 ID,把
TAOTOKEN_MODEL填对:https://taotoken.net/models/detail/chat?utm_source=taotoken_aicg_blog_end&utm_content=yard_audit_model_chat - 如果审计脚本要长期跑 CI,可以了解 Coding Plan 的额度与调用方式:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=yard_audit_coding_plan
- 为审计脚本单独创建一个 Key,不要复用生产 Key:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=yard_audit_api_keys
- 如果你的团队同时使用 Claude Code,按官方文档配置
settings.json与ANTHROPIC_*:https://taotoken.net/doc/ClaudeCodeAnthropic?utm_source=taotoken_aicg_blog_end&utm_content=yard_audit_claude_code_doc
最后再强调一次配置边界:YARD 审计脚本用TAOTOKEN_API_KEY、TAOTOKEN_BASE_URL、TAOTOKEN_MODEL;Claude Code 用settings.json里的ANTHROPIC_*;Codex 用config.toml里的model_providers.taotoken;CC Switch 填 Base URL、API Key、Model ID 三件套。Base URL 统一从https://taotoken.net/api开始,Key 使用YOUR_API_KEY占位,真实 Key 只放环境变量或 Secrets。把静态扫描放在无网络沙箱,把 LLM 复核放在受控环境,你的 YARD 审计脚本就能稳定分析恶意 gem 的.yardopts --load入口,而不是在文档生成链路里被动触发风险。