尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SkillSpector Batch Scan 架构流程详解:从 CLI 入口到多线程 LangGraph 扫描流水线

SkillSpector Batch Scan 架构流程详解:从 CLI 入口到多线程 LangGraph 扫描流水线 SkillSpector Batch Scan 架构流程详解从 CLI 入口到多线程 LangGraph 扫描流水线【免费下载链接】SkillSpectorSecurity scanner for AI agent skills. Detect vulnerabilities, malicious patterns, security risks, prompt injection, data exfiltration, and supply-chain risks in Claude Code, Codex, and MCP skills before you install them.项目地址: https://gitcode.com/GitHub_Trending/sk/SkillSpector本文围绕 contrib/batch_scan/docs/archive/FLOW_DIAGRAM.md 中记录的批处理扫描架构流程展开逐段拆解 SkillSpector 多语言批处理扫描器contrib.batch_scan的完整数据流从 CLI 参数解析、SKILL.md 发现、语言检测、API Key 池构建、ThreadPoolExecutor多线程并发到每个技能内部的 LangGraph 流水线与 7 个 DeepSeek 兼容性安全补丁。读完本文你将掌握该模块的调用链全貌、单技能扫描的 fan-out/fan-in 过程以及如何排查并发扫描下的挂起、限流与 JSON 解析问题。一、模块定位零侵入地复用 SkillSpector 核心图contrib/batch_scan是一个建立在 SkillSpector 核心之上的贡献模块其设计约束是零修改src/skillspector/。它把原本单技能扫描的skillspector scan skill扩展为按目录批量并行扫描并额外提供了三个能力见 contrib/batch_scan/batch_scan.py 模块 docstring并发扫描目录下所有技能ThreadPoolExecutor--workers控制基于 Unicode 字符比率的语言检测en / zh / ja / ko对非英语技能执行定向 LLM gap-fill弥补 8 条英文关键词静态规则在非英语文本上的召回损失。整体架构可以用下图概括与原文档Batch Entry Point流程一一对应CLI │ python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --workers 4 [--no-llm] ▼ batch_scan.py :: main() ① discovery.discover_skills(root) └─ rglob(SKILL.md) → [Path, ...] sorted ② detection.detect_skill_language(file_cache) per skill └─ main thread pre-reads → Unicode script ratio → zh/ja/ko/en ③ api_pool.create_api_key_pool_from_env() optional └─ SKILLSPECTOR_API_KEYS → ApiKeyPool(N keys) ④ ThreadPoolExecutor(max_workersN) ├─ Thread A: skill_1 → _scan_skill() ├─ Thread B: skill_2 → _scan_skill() └─ ... 每技能 90s 超时 ⑤ Collect results, sort by risk_score descending ⑥ reports._format_terminal / _format_json / _format_markdown二、批处理入口六个阶段的流水线① 技能发现discoverymain()首先调用discover_skills(root)其实现位于 contrib/batch_scan/discovery.py用root.rglob(SKILL.md)递归查找所有SKILL.md每个SKILL.md的父目录即一个技能目录根目录本身即使包含SKILL.md也不会被当作技能返回结果按路径字典序排序保证批处理输出的稳定性。若未发现任何技能CLI 会打印No skills found并以退出码 2 结束。② 语言检测detection语言检测发生在主线程预解析阶段见 batch_scan.py而不是在工作线程内这样做的目的是避免多个工作线程在文件 I/O 上互相竞争。具体逻辑在 contrib/batch_scan/detection.py用标准库unicodedata统计 CJK 统一表意文字0x4E00–0x9FFF与扩展 A 区0x3400–0x4DBF、平假名/片假名0x3040–0x30FF、谚文音节0xAC00–0xD7AF占全部字母字符unicodedata.category以L开头的比例判定阈值日文假名占比 5% →ja谚文占比 10% →koCJK 占比 10% →zh否则en跨文件聚合时采用多数投票detect_skill_language对 file_cache 中的每个文件分别检测后取票数最多者零第三方依赖只复用上游已经引入的标准库。当 CLI 显式传入--lang zh/ja/ko/en时跳过检测直接使用指定值_resolve_language。③ API Key 池可选create_api_key_pool_from_env()读取SKILLSPECTOR_API_KEYS环境变量其格式为每行一个key|base_url|model也兼容分号分隔支持#注释。实现细节见 contrib/batch_scan/api_pool.py每个 key 默认有 5 个并发槽位_DEFAULT_MAX_CONCURRENT_PER_KEY 510 个 key 即 50 个聚合槽位调度策略借鉴 Kubernetes 调度器acquire()永远选择当前负载最低且未被限流的 key仅在所有未限流 key 都满载时才阻塞等待遇到 HTTP 429 时该 key 进入指数退避30 × 2^n秒上限 300 秒_BACKOFF_BASE_S 30.0、_BACKOFF_CAP_S 300.0最多重试 5 次若环境变量未配置且只存在单个OPENAI_API_KEY函数返回None单 key 模式不需要池。配置示例来自 api_pool.py 模块 docstringexport SKILLSPECTOR_API_KEYS sk-or-xxx1|https://api.openai.com/v1|gpt-5.4 sk-or-xxx2|https://api.openai.com/v1|gpt-5.4 池创建后通过set_api_pool(pool)挂接它会同时替换skillspector.llm_utils.get_chat_model与skillspector.llm_analyzer_base.get_chat_model见 contrib/batch_scan/runner.py。后者必须一起替换是因为llm_analyzer_base通过from ... import在模块级建立了本地引用只替换一个模块会导致图内分析器约占全部 LLM 调用的 95%绕过池。④ 并行扫描与 90 秒超时with ThreadPoolExecutor(max_workersargs.workers) as executor: future_map { executor.submit(_scan_skill, skill_dir, root, use_llmuse_llm, langlang_map[skill_dir], require_llmargs.require_llm, api_poolapi_pool): idx for idx, skill_dir in enumerate(skill_dirs, 1) }对应代码在 batch_scan.py。要点每个技能在独立线程中执行完整的graph.invoke(state)future.result(timeout90)施加每技能 90 秒超时超时或异常的任务不重试工作线程仍被占用重试只会消耗新的槽位直接记录TIMEOUT (90s)/CRASH后继续处理其他技能Rich 控制台输出通过_print_lock串行化避免多线程同时写终端产生乱序。选择ThreadPoolExecutor而非ProcessPoolExecutor的原因在 DESIGN.md 中有明确记录macOS 的spawn模式会为每个子进程重新导入 LangGraph/LangChain造成 30 秒以上的启动超时而graph.invoke()是纯函数同一状态得到同一结果线程间通过各自独立的 state dict 隔离天然适合线程池。⑤⑥ 结果收集与报告扫描完成后所有 entry 按risk_assessment.score降序排序results.sort(keylambda x: x.get(risk_assessment, {}).get(score, 0), reverseTrue)随后进入 contrib/batch_scan/reports.py 的三种格式化器_format_terminalRich 表格含 LRLanguage Reliability列、Source/Language 分布、严重性汇总_format_json结构化输出含batch信封language_detection、gap_fill_applied、gap_fill_findings与每个技能的enhancements元数据_format_markdownMarkdown 表格 HIGH/CRITICAL 问题明细适合直接贴到 PR 评论。退出码约定见 batch_scan.py 与 README.md0表示全部安全1表示至少一个技能为 HIGH/CRITICAL2表示发生扫描错误。三、单技能扫描流程_scan_skill内部的两段式结构_scan_skill是整个批处理的核心工作单元batch_scan.py它由两段组成。第一段调用run_one执行完整 LangGraph 流水线run_onerunner.py依次执行state scan_state(skill_dir, use_llmuse_llm) result graph.invoke(state) # 同步阻塞调用 entry entry_from_result(result, skill_dir, root, ...)scan_state构造初始状态{input_path: str(skill_dir), output_format: json, use_llm: use_llm}。graph.invoke是上游 LangGraph 编译图的同步入口会依次驱动build_context下载/解压/构建文件缓存并记录temp_dir_for_cleanup临时目录20 个分析器并行 fan-out静态规则不调用 LLMAST1-8代码注入、TT1-5工具使用、YR1-4YARA 规则、SC1-6供应链、LP1-4循环/递归、TP1-3工具投毒、TM1-3工具滥用LLM 语义规则SSD1-4敏感数据泄露、SDI1-4直接注入、SQP1-3可疑权限提升meta_analyzerfan-out 之后的 fan-inLLM 复核与富化过滤与风险评分Results → filter → risk_score。entry_from_resultrunner.py将原始结果转换为批次报告标准结构skill / risk_assessment / components / issues并补充source_group、language、scan_mode: multilingual-enhanced、enhancementsgap_fill_applied、gap_fill_findings、english_keyword_rules_skipped等溯源字段。无论成功失败finally块都会调用cleanup_result删除temp_dir_for_cleanup删除失败时降级为subprocess调用系统rm -rfWindows 为rmdir /s /q规避 macOS 上shutil.rmtree因悬挂文件描述符如损坏的 httpx 连接而阻塞的问题。第二段非英语技能 LLM 模式下的 gap-fill当lang ! en and use_llm and not error_msg时执行run_gap_fill(fc, lang, modelMODEL_CONFIG.get(default), api_poolapi_pool)gap_fill.py。Gap-fill 的动机上游有 25 条英文关键词静态规则其中 17 条已被 SSD/SDI/SQP 语义分析器覆盖剩余8 条没有语义分析器等价物——P5有害内容、P6-P8系统提示词泄露、MP1-MP3记忆投毒、RA1-RA2流氓 Agent。这些规则的正则只匹配英文短语如clear|erase|wipe|forget ... memory|context|instructions对非英语文本召回为零。GapFillAnalyzer继承LLMAnalyzerBase类属性response_schema None刻意设计见下节关于补丁的讨论不依赖response_format结构化输出构造时把检测到的语言注入提示词模板parse_response手动剥除 Markdown 代码围栏 →json.loads→ PydanticGapFillResult.model_validate→ 过滤confidence 0.7且rule_id属于 8 条规则集合若配置了api_poolself.chat_model会被替换为PooledChatModel从而获得 key 故障转移能力。gap-fill 结果经annotate_findings追加到entry[issues]同时写入entry[enhancements][gap_fill_applied] True与gap_fill_findings便于报告展示哪些增强被应用了。四、三种执行路径并发修复后的行为矩阵原文档用三种路径刻画--no-llm与 LLM 模式在不同并发/连接条件下的行为路径 1 ——--no-llm快速、确定性use_llmFalse时图跳过 SSD/SDI/SQP 与 meta_analyzer7 个补丁虽然仍然生效但不产生 LLM 调用纯静态模式与上游行为完全一致cleanup_result正常执行。路径 2 ——use_llmTrue且所有线程正常补丁 1 保证每个分析器实例拿到自己的self.response_schema None实例字典隔离、无共享状态、无竞态补丁 6 注入httpx.Timeout(connect8s, read30s)挂起连接快速以干净异常失败补丁 7 抑制 Event loop is closed 噪音补丁 2/3 处理原始 JSONfindings 正确填充。路径 3 ——use_llmTrue但连接出错httpx 连接/读取超时触发异常 → 异常经 asyncio 传播 → 图捕获 → 该技能返回错误 entry 而非 findings→cleanup_result用shutil.rmtree subprocess 兜底清理 →其余工作线程不受影响继续执行。这正是批处理扫描器一个技能挂掉不拖垮整批的容错设计。五、7 个安全补丁deepseek_compat()上下文管理器7 个补丁统一由deepseek_compat()上下文管理器管理runner.py遵循Save → Patch → Yield → Restorefinally模式。batch_scan.main()中整个扫描体都被with deepseek_compat():包裹即使发生异常也会在退出时恢复原始实现。补丁目标机制目的1LLMAnalyzerBase.__init__self.response_schema None实例属性禁用结构化输出实例级隔离2LLMAnalyzerBase.parse_responsejson.loads→ Pydanticmodel_validate处理无response_format的原始字符串3LLMMetaAnalyzer.parse_response同上 _sanitize_meta_finding处理 LLM 输出怪癖null→、none→low4LLMAnalyzerBase.build_prompt追加 JSON 输出格式指令给模型格式提示5LLMMetaAnalyzer.build_prompt追加 JSON 输出格式指令同上6ChatOpenAI.__init__注入httpx.Timeout(connect8s, read30s)阻止挂起连接无限阻塞7asyncio.run异常处理器丢弃 Event loop is closed抑制 httpx 清理噪音补丁 1 是并发安全的基石原实现在 DESIGN.md 中记录会修改类属性LLMAnalyzerBase.response_schema这在多线程下存在竞态——线程 A 恢复原值而线程 B 仍在创建实例with_structured_output()就会触发 400。补丁改为写入实例__dict__Python MRO 保证实例属性优先于类属性因此每个分析器实例拿到各自的None零共享状态、零竞态。且嵌套深度被跟踪_patches_depth计数只有最外层上下文管理器退出时才恢复原值支持可重入。补丁 6 的注入时机很关键httpx 默认connect5.0、readNone无限。一个建立了 TCP 连接却从不返回数据字节的服务端会永久阻塞工作线程而ThreadPoolExecutor无法杀死线程。补丁在 OpenAI 内部客户端被缓存之前通过timeoutPydantic 别名注入超时值同时写入kwargs[timeout]和kwargs[request_timeout]确保httpx.Timeout(connect8s, read30s)从第一次实例化起就流入每个root_client/async_clientPydantic v2 的别名优先级细节见 DESIGN.md 的 Patch 6 一节。补丁的健壮性保障应用补丁前会调用_verify_patch_targets()runner.py逐一检查 7 个补丁目标的函数签名与深层依赖如LLMAnalysisResult.model_validate、Batch.file_path字段、MetaAnalyzerResult.findings字段、asyncio.new_event_loop等。任何上游 API 变更都会在补丁应用时立即抛出明确的RuntimeError把静默失效转变成即时可诊断错误。这与 test_monkeypatch_fragility.py26 个测试和 test_monkeypatch_invasiveness.py14 个测试含 50 实例并发隔离验证互为印证。六、为什么必须补丁而不是 fork 上游DESIGN.md 明确记录了这一取舍fork 会造成永久分叉每次上游发布都要 rebase 和重新验证monkey-patch 是即插即用适配器自动跟随上游演进如果未来上游提供response_schema覆盖能力如环境变量SKILLSPECTOR_RAW_LLM补丁会自动变成 no-op可无代码变更地移除。补丁之所以必要是因为 DeepSeek 等提供商的 API 不支持response_format结构化输出而上游无条件调用with_structured_output()——不打补丁会返回 HTTP 400 并污染 httpx 连接池。修复链条为补丁 1 禁用结构化输出 → 补丁 4/5 给每个提示词追加 JSON 格式指令 → 补丁 2/3 手动解析原始 JSON 字符串并用 Pydantic 校验。校验失败时分析器返回空 findings不崩溃扫描继续实现优雅降级。七、三层并发模型与调优建议原文档揭示了模块建立在上游两层并发之上的第三层并发Layer 3 — batch_scan.py ThreadPoolExecutor(max_workersN) [CONTRIB] Layer 2 — llm_analyzer_base asyncio.Semaphore(10) [UPSTREAM] Layer 1 — graph.py 20 analyzers fan-out [UPSTREAM]每层互不知晓图不知道自己在被并发调用工作线程不知道图内部会 fan-out。这意味着峰值并发 LLM 请求数远大于--workers。README 给出的经验值场景Workers峰值并发 LLM 请求免费额度 key110–15付费基础4默认25–40企业级/多 key7–1050–80调试1 -V顺序执行因此并行 LLM 扫描建议配置至少与 workers 数量相当的 API keysREADME 建议 10 个 key 配--workers 8单 key 场景请使用--workers 1或--no-llm否则会立即触发限流。八、全链路速查与延伸阅读常用命令完整列表见 contrib/batch_scan/docs/README.md# 纯静态扫描无需 API key快速确定性 python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --no-llm # 完整 LLM 扫描 python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal --workers 7 # 输出 JSON/Markdown 报告 python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f json -o report.json python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f markdown -o report.md # 指定语言 / 强制英语跳过 gap-fill python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --lang zh --workers 4 python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --lang en -f terminal --workers 4 # 调试单线程 详细日志 python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --workers 1 -V如需深入建议按以下路径阅读仓库源码入口与 CLI 参数contrib/batch_scan/batch_scan.py图封装与 7 补丁实现contrib/batch_scan/runner.py技能发现 / 语言检测contrib/batch_scan/discovery.py · contrib/batch_scan/detection.pyKey 池与故障转移contrib/batch_scan/api_pool.pyGap-fill 分析器contrib/batch_scan/gap_fill.py语言兼容性标注contrib/batch_scan/annotation.py三种报告格式contrib/batch_scan/reports.py架构设计决策与替代方案评估contrib/batch_scan/docs/DESIGN.md并发挂接与补丁健壮性测试contrib/batch_scan/tests/test_pool_wiring.py · contrib/batch_scan/tests/test_monkeypatch_invasiveness.py · contrib/batch_scan/tests/test_monkeypatch_fragility.py已知局限来自 README语言检测仅覆盖 4 种文字阿拉伯语、印地语、西里尔文会被归类为英语并失去 gap-fill 覆盖无断点续扫能力parse_response的 JSON 恢复是尽力而为——当 LLM 返回畸形 JSON 时分析器返回空 findings 而非崩溃属于刻意的优雅降级选择但用户不会知道哪些 findings 因此丢失。理解这些边界有助于正确评估批处理扫描结果的置信度。【免费下载链接】SkillSpectorSecurity scanner for AI agent skills. Detect vulnerabilities, malicious patterns, security risks, prompt injection, data exfiltration, and supply-chain risks in Claude Code, Codex, and MCP skills before you install them.项目地址: https://gitcode.com/GitHub_Trending/sk/SkillSpector创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表