SkillSpector 多语言批量扫描器设计全记录从概念到落地的五阶段演进【免费下载链接】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本篇技术指南完整还原了 SkillSpector 项目中多语言批量扫描模块contrib/batch_scan从问题定义、架构设计、关键决策、关键 Bug 修复到最终交付的整个设计历程。读者将掌握该模块如何在零侵入上游的前提下实现数百个 Skill 的并行扫描、如何用 7 个精准 monkey-patch 打通 DeepSeek 等不支持结构化输出的 LLM 提供商、如何用 Unicode 脚本比例算法做零依赖语言检测以及ApiKeyPool多 Key 调度与 gap-fill 补漏机制的工作原理与实战配置。一、设计起点上游的两个结构性缺口1.1 单 Skill 扫描的上游限制SkillSpector 主项目的skillspector scan命令见 src/skillspector/cli.py一次调用只处理一个 Skill。当面对一个包含数百个 Skill 的仓库时用户只能自行编写外部循环逐一遍历既没有并发能力也没有聚合报告。这是contrib/batch_scan模块诞生的直接动因问题陈述记录于 contrib/batch_scan/docs/archive/DESIGN_HISTORY.md Phase 1。1.2 多语言覆盖缺口64 条规则中的 25 条依赖英文关键词截至设计时点SkillSpector 共有 64 条检测规则其中 25 条是英文关键词正则模式。对于非英文zh/ja/koSkill这些规则的召回率大约损失 60%。进一步分析这 25 条规则17 条有对应的语义分析器覆盖SSD 语义安全发现 / SDI 语义开发者意图 / SQP 语义质量策略语义分析器理解的是意图而非特定英文短语因此天然多语言8 条没有任何等价覆盖——P5有害内容、P6–P8系统提示词泄露、MP1–MP3记忆投毒、RA1–RA2流氓 Agent。这 8 条正是后续 gap-fill 补漏分析器的目标。1.3 确立的四大设计原则对src/skillspector/零改动——上游代码一个字节都不动子类化与包装而不是重写——尽量继承LLMAnalyzerBase等基类输出与标准单 Skill 扫描可比——保证结果字段结构对齐所有扩展都放在contrib/batch_scan/——独立贡献模块便于整体合入或移除。二、架构设计四层模型与组件规划2.1 四层模型CLI layer python -m contrib.batch_scan.batch_scan Scheduling layer ThreadPoolExecutor(max_workersN) API Pool layer ApiKeyPool (multi-key scheduler) Graph layer graph.invoke() per skill (upstream, untouched)其中 Graph 层调用的是上游skillspector.graph.graphsrc/skillspector/graph.py完全不加修改批量模块在其上层叠加调度、API 池与 CLI。完整架构图另见 contrib/batch_scan/docs/DESIGN.md 与 contrib/batch_scan/docs/archive/ARCHITECTURE_DEEP_DIVE.md。2.2 组件规划25 项任务5 个阶段阶段内容落地文件1. Foundation递归发现、语言检测、工作线程池discovery.py、detection.py2. API Pool多 Key 调度器 限流退避api_pool.py3. Gap-fill覆盖 8 条无等价规则的多语言 LLM 分析器gap_fill.py4. Reports终端 / JSON / Markdown 聚合报告reports.py5. Integration端到端流水线与上游结果对比batch_scan.py、runner.py2.3 三层并发模型设计文档中明确区分了三个互不知晓的并发层见 contrib/batch_scan/docs/DESIGN.mdLayer 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]每个 Skill 在独立线程中执行完整的graph.invoke(state)而图内部还有 20 个分析器的扇出与每个分析器内部的信号量并发。Graph 不知道自己在被并发调用工作线程也不知道 Graph 内部会再扇出。三、关键设计决策及取舍依据3.1 为什么选 ThreadPoolExecutor 而非 ProcessPoolExecutormacOS Python 3.13 的spawn模式会在每个子进程中重新导入 LangGraph/LangChain导致启动超时实测 30 秒以上而 macOS 自 Python 3.8 起又不提供fork模式。因此选择了ThreadPoolExecutor。推论线程共享内存所有共享状态进度打印、结果收集、API 池内部计数器都必须做严格的线程安全处理。从 batch_scan.py 源码可见其落地方式进度输出通过_print_lock threading.Lock()串行化Rich 控制台本身也不是线程安全的results列表只在as_completed的主收集循环里追加语言检测在提交任务前就预先解析成lang_map避免工作线程在文件 I/O 上竞争。此外设计文档还否定了全 asyncio方案graph.invoke(state)是同步阻塞调用LangGraph 不暴露异步入口用asyncio.to_thread()包裹只会增加一层调度而没有吞吐收益同时会让argparse与 Rich 输出层被迫全部异步化。3.2 横向节流--workersvs 全局信号量最终选择--workers横向、按 Skill 粒度而不是全局共享信号量纵向、按请求粒度理由有三对上游arun_batches(sem10)零侵入给用户一个直观可调的旋钮概念上简单。工作线程数由 CLI 的--workers参数控制默认 4可降至 1免费 API Key或升至 20高吞吐场景。3.3 DeepSeek 原始 JSON 模式DeepSeek 的 API 不支持response_format结构化输出。方案不是新建一个 provider而是通过补丁在LLMAnalyzerBase.__init__中注入实例属性response_schema None随后在parse_response中手工解析 JSON。这成为后面七个补丁体系的入口详见第四节。3.4 Unicode 脚本比例语言检测选择标准库unicodedata而非 ML 检测器如langdetect、fasttext理由零额外依赖且上游的mcp_tool_poisoning.py本来就已经导入了该模块。检测阈值与代码实现detection.py完全一致_CJK_UNIFIED (0x4E00, 0x9FFF) # CJK 统一表意文字 _CJK_EXT_A (0x3400, 0x4DBF) # CJK 扩展 A 区 _HIRAGANA (0x3040, 0x309F) _KATAKANA (0x30A0, 0x30FF) _HANGUL (0xAC00, 0xD7AF) # 谚文音节 _CJK_THRESHOLD 0.10 # CJK ≥ 10% → zh _KANA_THRESHOLD 0.05 # kana ≥ 5% → ja _HANGUL_THRESHOLD 0.10 # Hangul ≥ 10% → ko判定顺序为先看 kana 占比日语再看 Hangul韩语最后看 CJK中文均未超过阈值则判定为英文。detect_skill_language按文件逐一检测后以多数投票聚合出整个 Skill 的语言。已知局限汉字密度高而假名密度低的日文文本会被误判为中文阿拉伯语、印地语、西里尔文会被归为英文而失去 gap-fill 覆盖。四、七个补丁DeepSeek 兼容方案的演进与关键 Bug 修复4.1 从类属性补丁到实例属性补丁Bug 1BLOCKER最初方案是保存 → 把类属性response_schema置为 None → 运行 → 恢复类属性。在多线程下这是竞态炸弹四个线程同时读写同一个类属性线程 A 可能在线程 B 的 meta-analyzer 实例化之前就恢复了原值导致with_structured_output()被触发、上游返回 HTTP 400。修复Patch 1用__init__包装器把self.response_schema None写成实例属性见 runner.py 中_patched_base_init。Python 的 MRO 保证实例__dict__永远先于类属性被查到这是语言级语义而不是库内部细节——每个分析器实例各自持有None零共享状态、零竞态。4.2 七个补丁全景setup_deepseek_compat()或deepseek_compat()上下文管理器在首次 LLM 活动前一次性应用 7 个补丁。补丁不会在 import 时自动生效而是由调用方显式控制作用域上下文管理器用嵌套计数器跟踪深度只有最外层退出才恢复原状。补丁清单#目标机制目的1LLMAnalyzerBase.__init__注入实例属性self.response_schema None关闭结构化输出实例隔离2LLMAnalyzerBase.parse_responsejson.loads→ Pydantic 校验处理无response_format的原始字符串3LLMMetaAnalyzer.parse_response同上 null/none清理处理 LLM 输出怪癖4LLMAnalyzerBase.build_prompt追加 JSON 输出指令模型需要格式提示5LLMMetaAnalyzer.build_prompt同上同上6ChatOpenAI.__init__注入httpx.Timeout(connect8s, read30s)防止连接挂死7asyncio.run异常处理器丢弃Event loop is closed抑制清理噪音4.3 原始 JSON 解析与 Pydantic 校验Bug 2、Patch 2/3/4/5没有with_structured_output()之后prompt 里缺少 JSON 格式指令LLM 开始返回自然语言。修复分两步Patch 4/5 在所有分析器 prompt 后追加显式的 JSON 输出指令_JSON_OUTPUT_INSTRUCTION/_META_JSON_PROMPTPatch 2/3 重写两个parse_response。解析管线为raw LLM string → _strip_markdown_fences() → json.loads() → model_validate() → Finding objects两步解析的设计理由runner.pyjson.loads快速、确定格式错误会抛出清晰的JSONDecodeError——捕获后返回[]model_validate强制 schema必填字段、字面量枚举、confidence 范围、字符串长度。schema 违规同样返回[]并记WARNING日志。错误传播策略单个 LLM 响应损坏只会让该文件零发现扫描继续绝不阻塞流水线操作者通过WARNING日志监控解析失败率。Patch 3 额外增加_sanitize_meta_finding后处理null字符串字段 →无法识别的枚举值如none→low——这些是可恢复的软错误而非硬性 schema 违规。4.4 HTTP 超时注入Bug 3BLOCKERhttpx 的默认配置是readNone无限等待首个响应字节。一个建立了 TCP 连接却永不回包的服务器会让工作线程永久阻塞而ThreadPoolExecutor无法杀死线程。修复Patch 6在 OpenAI 客户端被缓存之前通过ChatOpenAI.__init__注入httpx.Timeout(connect8s, read30s)。这里有一个 Pydantic v2 的别名陷阱ChatOpenAI的 Pydantic 模型以request_timeout为规范字段名、timeout为别名populate_by_nameTrue。当两者同时出现在**kwargs时 Pydantic v2 优先采用别名。因此补丁直接覆写kwargs[timeout]同时把kwargs[request_timeout]也一并设置避免依赖别名优先这个 Pydantic 内部行为——从第一次实例化起httpx.Timeout(connect8s, read30s)就流入每一个root_client和async_client。4.5 其余两个 BugBug 4cleanup_result卡在陈旧文件描述符上。macOS 上shutil.rmtree会因损坏 httpx 连接残留的悬挂 fd 而阻塞。修复为主备双层shutil.rmtree(temp_dir, ignore_errorsTrue)失败后回退到subprocess.run([rm, -rf, temp_dir], timeout10)子进程独立于 Python 进程不受影响Windows 上则用rmdir /s /q见 runner.py 的cleanup_result与 DESIGN.md 的说明。Bug 6LLM 输出怪癖。偶发返回字符串字段为null、枚举为none。由 Patch 3 的_sanitize_meta_finding统一修正null→none→low同时更新了 prompt 规则never use null / never use none。4.6 补丁的防御性验证为了不让补丁在上游演进后静默失效_apply_patches之前会调用_verify_patch_targets()检查每个补丁目标的函数签名参数名、是否从位置参数迁移为关键字参数、深依赖Pydanticmodel_validate、LLMFinding.to_finding、Batch的file_path字段与file_label属性等是否存在。任何一项不满足就抛出带具体描述的RuntimeError且检查是原子的——任一失败则一个补丁都不应用。这一点由 tests/test_monkeypatch_fragility.py 的 28 个测试逐补丁验证另有 tests/test_monkeypatch_invasiveness.py 的 14 个测试证明补丁的导入隔离、线程隔离含 50 实例并发。五、实现落地文件结构与性能5.1 交付文件9 个源码文件 测试 文档contrib/batch_scan/ ├── __init__.py # 包初始化 dotenv 预加载 ├── discovery.py # 递归 SKILL.md 查找器 ├── detection.py # Unicode 脚本比例检测 ├── annotation.py # 发现项语言兼容性标注 ├── api_pool.py # ApiKeyPool PooledChatModel set_api_pool() ├── gap_fill.py # GapFillAnalyzer(LLMAnalyzerBase) ├── batch_scan.py # CLI ThreadPoolExecutor ├── runner.py # Graph 包装 setup_deepseek_compat() ├── reports.py # 终端 / JSON / Markdown 输出 ├── tests/ │ ├── test_api_pool.py # 45 个测试acquire/backoff │ ├── test_gap_fill.py # 41 个测试JSON 解析 │ ├── test_pool_wiring.py # 4 项池接线冒烟检查 │ └── test_runner_patches.py # 24 个测试上下文管理器 ├── docs/ │ ├── README.md # 用户指南 │ ├── DESIGN.md # 架构与并发模型 │ └── archive/ # 深度解析、设计史、未来工作discover_skillsdiscovery.py的实现非常朴素可靠root.rglob(SKILL.md)递归找出所有直接包含SKILL.md的目录根目录本身不算 Skill按路径排序返回。5.2 性能数据23-Skill 测试集Mac Mini M4模式Workers耗时相对上游上游串行循环15.97s1×Batch--no-llm40.84s7.1×Batch--no-llm7~0.7s8.5×Batch LLM7~3 minN/A上游无 LLM 批量说明以上数据来自该模块设计文档对特定硬件Mac Mini M4与特定测试集23 个 fixture的实测记录不同环境、模型与网络条件下的绝对值会不同但静态扫描的并行加速趋势一致。5.3 每 Skill 的扫描流水线run_one(skill_dir) ├─ scan_state() # 构建初始 LangGraph state ├─ graph.invoke(state) # 上游完整流水线未改动 │ ├─ build_context # 文件缓存、manifest │ ├─ 20 analyzers # 扇出15 静态 5 LLM │ └─ meta_analyzer # LLM 验证与增强 ├─ entry_from_result() # 提取 语言兼容性标注 └─ cleanup_result() # shutil.rmtree → subprocess 回退run_one返回(entry, error_message_or_None)失败时返回一个severityERROR的桩条目并携带异常文本且finally中确保清理临时目录见 runner.py。每个 Skill 还有 90 秒超时超时被标记为TIMEOUT并跳过不重试线程还卡着重试只会再占一个槽位其余 worker 继续。HTTP 级超时Patch 6让大多数挂起根本到不了 90 秒上限。六、ApiKeyPool多 Key 调度与限流退避6.1 设计动机与接线批量扫描中每个 Skill 会触发约 20 次图内 LLM 调用SSD/SDI/SQP/meta再加 gap-fill 一轮。单 Key 在--workers 4下会立刻撞上限流。因此设计了一个 Kubernetes 调度器风格的多 Key 池acquire → 选取负载最低的空闲 Key release(successTrue) → 标记空闲 release(successFalse) → 标记限流退避 30s × 2^n上限 300s acquire after 429 → 自动换 Key核心实现api_pool.py每个ApiKey有max_concurrent个并发槽默认 5一个 Key 可同时服务多个调用方只有 HTTP 429 才把 Key 移出轮换。acquire优先恢复退避到期的 Key再在可用 Key 中选active_requests最少的全满时才阻塞等待。6.2 双模块接线一个容易被忽略的from-import陷阱set_api_pool(pool)runner.py同时补丁两个模块的get_chat_modelskillspector.llm_utils.get_chat_modelskillspector.llm_analyzer_base.get_chat_model原因llm_analyzer_base在模块顶层用from ... import引入了get_chat_model在模块内形成了局部引用只补丁llm_utils会让图内分析器约 95% 的 LLM 调用绕过池。set_api_pool(None)则恢复两个模块的原工厂。tests/test_pool_wiring.py 验证了三条调用路径全部接入llm_utils、LLMAnalyzerBase._llm、GapFillAnalyzer.chat_model。PooledChatModel是 LangChain 兼容的透明换 Key 包装每次invoke/ainvoke从池里取一个槽、现场构造ChatOpenAI、完成后释放遇到限流错误以successFalse释放当前 Key、换 Key 重试最多max_retries次默认 5。_is_rate_limit同时识别 OpenAI SDK 的RateLimitError与消息文本中的429/rate limit/too many requests标记。6.3 环境变量配置# 多 Key 模式推荐每行 key|base_url|model支持换行或分号分隔 export 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 # 单 Key 模式向后兼容 export OPENAI_API_KEYsk-or-xxx1 export OPENAI_BASE_URLhttps://api.deepseek.com/v1create_api_key_pool_from_env在未设置SKILLSPECTOR_API_KEYS时返回None调用方回退到单 Key 提供商路径。多 Key 模式下 10 个 Key × 5 槽 50 个聚合并发槽配合--workers 8比较安全只有 1 个 Key 时建议--workers 1或--no-llm。七、Gap-fill为非英文 Skill 补上 8 条规则的召回7.1 入选标准8 条 gap-fill 规则P5、P6–P8、MP1–MP3、RA1–RA2是三个条件的交集依赖英文关键词对应静态分析器只匹配英文短语例如内存投毒规则的正则(clear|erase|wipe|forget)\s(your|my|the)\s(memory|context|instructions)非英文文本完全绕过无语义分析器等价物SSD/SDI/SQP 覆盖的是语义意图、策略违规而 P5/P6-P8/MP1-MP3/RA1-RA2 检测的是具体安全概念LLM 可解给出针对性 prompt 后LLM 能在任何语言中识别这些概念。25 条英文关键词规则的全部分布与 DESIGN.md 一致分组规则 ID检测方式提示注入P1-P4英文关键词正则有害内容P5英文关键词正则系统提示词泄露P6-P8英文关键词正则数据外泄E1-E4英文关键词正则权限提升PE1-PE3英文关键词正则过度授权EA1-EA4英文关键词正则输出处理OH1-OH3英文关键词正则触发器滥用TR1-TR3英文关键词正则记忆投毒MP1-MP3英文关键词正则流氓 AgentRA1-RA2英文关键词正则7.2 GapFillAnalyzer 实现GapFillAnalyzer继承LLMAnalyzerBasegap_fill.py以检测到的语言格式化GAP_FILL_ANALYZER_PROMPT把四类安全标准有害内容、系统提示词泄露、记忆投毒、流氓 Agent逐条写进 prompt。它继承了基类的 token 预算感知分批get_batches与并行执行arun_batches能力并可通过api_pool参数接入PooledChatModel获得多 Key 故障转移。parse_response与 Patch 2 同一管线去 markdown 围栏 →json.loads→GapFillResult.model_validate随后两层过滤rule_id必须在_GAP_FILL_RULE_IDS集合内、confidence 0.7prompt 也要求只报告高置信发现无问题时返回空数组不要编造。结构化输出默认关闭response_schema: type | None None从而兼容不支持response_format的提供商。在 batch_scan.py 的_scan_skill中gap-fill 在graph.invoke返回之后执行非英文 启用 LLM 且无错误时把 gap-fill 发现经annotate_findings标注后追加到entry[issues]并在enhancements中记录gap_fill_applied: True与gap_fill_findings计数。7.3 语言兼容性标注annotation.py把每条发现按rule_id分为四类语义规则SSD1-SSD4、SDI1-SDI4、SQP1-SQP3、TP4、gap-fill 规则P5、P6-P8、MP1-MP3、RA1-RA2、代码级规则AST/TT/YR/SC/LP/TP/TM 系列天然与语言无关、英文关键词规则P1-P4、E1-E4、PE1-PE3、EA1-EA4、OH1-OH3、TR1-TR3。is_language_compatible的判定是英文 Skill 全兼容非英文 Skill 仅语义、代码级与 gap-fill 规则兼容。每条发现据此获得language_compatible布尔字段报告中的LRLanguage Reliability列即源于此✓ 英文静态LLM 全覆盖⚠ 非英文已应用 gap-fill覆盖 8 条额外规则。八、CLI 用法、输出与退出码8.1 核心命令# 发现 Skill 并递归扫描 python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal --workers 7 # 静态模式无需 API Key速度快 python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --no-llm # 非英文 Skill 也不强制 LLM结果会不完整 python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --no-require-llm --no-llm # 语言覆盖auto 为默认 Unicode 脚本比例检测 python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --lang zh -f terminal --workers 4 python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --lang ja -f terminal --workers 4 python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --lang ko -f terminal --workers 4 # 输出格式 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 # 调试单 worker 详细日志 python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --workers 1 -VCLI 参数batch_scan.pyinput_dir必填-f/--format三选一默认 terminal-o/--output写文件默认 stdout--no-llm跳过 LLM 仅静态--workers N默认 4-V/--verbose开启 DEBUG 日志--lang auto|en|zh|ja|ko默认 auto--require-llm默认开/--no-require-llm控制非英文 Skill 是否必须走 LLM。8.2 退出码可直接用于 CI代码含义0全部安全无 HIGH/CRITICAL1≥1 个 Skill 为 HIGH 或 CRITICAL2出现扫描错误python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f json -o report.json if [ $? -eq 0 ]; then echo All clean; fi8.3 与上游对比的溯源字段批量输出刻意带上可比对字段见 runner.py 的entry_from_result与scan_mode: multilingual-enhancedskill.language—— 检测出的语言标签enhancements.gap_fill_applied—— 是否应用了 LLM gap-fillenhancements.gap_fill_findings—— gap-fill 追加的发现数enhancements.english_keyword_rules_skipped—— 被跳过的静态规则数非英文 Skill 记为 25英文为 0。--workers调优建议来自 README免费 API Key 用 1付费基础档用 4默认企业多 Key 用 7–10调试用 1 -V。九、设计原则回顾与可复用经验零侵入——src/skillspector/一行未改子类化而非重写——GapFillAnalyzer extends LLMAnalyzerBase包装而非钻孔——ApiKeyPool包装ChatOpenAI打标签而非重构——在既有输出形状上追加元数据字段可比较而非隐藏——scan_mode标签支持与上游结果 diff先证明再合并——contrib 模块保持独立直到价值被验证。这套方法论最可复用的部分是零侵入补丁 防御性验证的组合所有 monkey-patch 都通过显式上下文管理器deepseek_compat()作用域化、可逆、可嵌套_verify_patch_targets()在上游演进时把静默失效变成即时报错实例属性代替类属性解决了多线程竞态。类似provider 不支持结构化输出HTTP 连接无限挂起第三方清理函数阻塞等问题在任何对接多 LLM 提供商的项目中都可能复现本文档记录的诊断路径与修复模式可以直接迁移。如需继续深入可阅读 contrib/batch_scan/docs/DESIGN.md并发模型与双模块补丁机制、contrib/batch_scan/docs/README.md全部命令与故障排查、contrib/batch_scan/docs/archive/ARCHITECTURE_DEEP_DIVE.md架构深潜、contrib/batch_scan/docs/archive/FUTURE_WORK.md未来方向以及 contrib/batch_scan/tests/docs/TEST_DESIGN.md测试设计思路。【免费下载链接】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),仅供参考