- AI 技能
- AI 插件
- 应用安全
- 网络安全
- AI 评测
【免费下载链接】skills
Trail of Bits Claude Code skills for security research, vulnerability detection, and audit workflows
导读
本篇技术指南以 Trail of Bits Claude Code skills 仓库中的property-based-testing插件为对象,系统拆解它在属性测试的编写、审查、失败解读与智能合约不变量验证四个方向上的完整方法论,并深入剖析其配套的"触发率(trigger rate)与有效性(effectiveness)"双轨评估体系。读完本文,你将掌握一套可复用的属性测试决策框架——从属性目录、强度排序、策略设计到assume()的正确用法,以及如何像维护一个测试套件一样用测量数据驱动地维护一个 Agent 技能的定义与提示词。
技能定位:一句话能做什么
插件根目录的 README.md 将本技能概括为三个任务:写属性测试、审查现有测试是否"断言了等于没断言"、以及把缩小(shrunk)后的反例分诊为错误属性、歧义规格或真实缺陷。而技能自身的 README.md 将其细化为四个具体职责:
- 发现 PBT 机会(Spots PBT opportunities)——encode/decode 配对、validator、normalizer、宽输入域纯函数、智能合约不变量;
- 编写属性测试(Writes property tests)——策略设计、已知边界的钉扎(edge-case pinning)、settings 配置;
- 审查已有测试(Reviews existing ones)——tautology、空洞的
assume()、缺失的更强属性; - 分诊失败(Triages failures)——把一个失败区分为"属性写错了""规格本身含糊""真实 bug"三种情况。
其中"发现机会"与"判断某段代码不适合写属性测试"同样是有效产出——SKILL.md 开篇即点明:一个示例测试断言一个点,一个属性断言整个输入域上的规则,让生成器去搜索反例;只有代码具备代数形状(反函数、不变量、oracle)时才值得做这笔交换,否则就给它写示例测试,并明确说出"此代码不适合 PBT"。
技能结构:SKILL.md 与 references 的分层设计
技能本体位于 skills/property-based-testing/:
plugins/property-based-testing/skills/property-based-testing/ ├── SKILL.md # Property catalog, failure modes, routing └── references/ ├── generating.md # Strategy design, settings, edge cases ├── refactoring.md # Rearrangements that expose a property ├── reviewing.md # Quality issues by severity ├── interpreting-failures.md # Grounding and classifying a failure └── libraries.md # Library per language; Echidna/Medusa设计理念是"路由优先":SKILL.md 承担属性目录、失败模式与路由表(按任务类型指向对应 references 文件),references 则按职责拆分,让 Agent 按需加载,避免一次性吞入全部上下文。
值得记录的是 README 中专门保留的"What was cut, and why"一节——它解释了哪些参考文件被删除以及理由,因为"一次没有理由的删除与一次疏忽是无法区分的":
design.md(删除)——一份 Phase 1–5 的散文式工作流。仓库的 AGENTS.md 明确要求:模型应当逐步执行的流程应放在脚本里(要么运行、要么失败),而不是放在散文里任其漂移;strategies.md(删除)——逐语言的生成器语法。st.integers(min_value=1)、fc.string()这类语法不是当前模型缺失的知识,为它们支付上下文会挤占模型真正缺失的判断力。generating.md只保留"属于决策而非语法"的部分:约束放策略而非assume()、@example钉扎、deadline=None;refactoring.md(先删后恢复)——删除是错误的。SKILL.md 与强度排序都会导向"这段代码不适合 PBT"的结论,而缺少该文件时这便成了死胡同——其实答案存在:提取纯核心、补上缺失的逆操作、结构化表示加渲染器、返回而非原地修改、注入依赖。evals/03的 fixture 正是例子:send_welcome_email是带有副作用(SMTP)的函数,但其消息构造部分是可分离、可测属性的,实测运行也真的在无提示下找到了这条接缝。恢复时裁剪掉了脆弱的rg检测单行命令(其中两条还有错)、一张 effort/risk 表格、一份重复了强度排序的优先级清单,以及一个已被generating.md的st.composite覆盖的"validators 用生成器"模式。
核心方法论:从属性目录到失败分诊
属性目录(Property catalog)
SKILL.md 给出了一张可直接对照的属性目录表,共九类:
| 属性 | 公式 | 适用场景 |
|---|---|---|
| Roundtrip(往返) | decode(encode(x)) == x | 序列化、转换配对 |
| Inverse(逆操作) | f(g(x)) == x | 加密/解密、压缩/解压 |
| Oracle(参照实现) | new(x) == reference(x) | 优化、重构、重实现 |
| Idempotence(幂等) | f(f(x)) == f(x) | 归一化、格式化、排序 |
| Invariant(不变量) | 操作前后均成立 | 任意变换、合约状态 |
| Easy to verify(易验证) | is_sorted(sort(x)) | 带廉价检查器的复杂算法 |
| Commutativity(交换律) | f(a, b) == f(b, a) | 二元与集合操作 |
| Associativity(结合律) | f(f(a,b), c) == f(a, f(b,c)) | 组合操作 |
| Identity(幺元) | f(x, e) == x | 带中性元素的运算 |
强度排序(由弱到强):no crash → type preservation → invariant → idempotence → roundtrip / oracle。实践原则是"断言代码支持的最强属性":仅靠"不崩溃"很少值得引入 PBT 依赖——如果只能找到这一条,要么一次小重构能暴露更强的属性,要么诚实报告"该代码不适合 PBT"。先排除前者,再接受后者。
两种"断言了等于没断言"
- 同义反复(Tautology):
assert add(a, b) == a + b复述了实现本身,任何两者共享的 bug 都无法使其失败。应选择"约束函数但不重算它"的属性。注意例外:当f并非显然纯函数时,f(x) == f(x)是真正的确定性属性——dict/set 上的序列化器、散列、任何读取时钟的代码都属于此类。 - 空洞(Vacuity):过滤掉几乎所有输入的
assume()会通过测试却不执行任何实质内容;自相矛盾的assume()甚至在不运行任何用例的情况下通过。正确做法是把约束推进策略里,让生成器直接产出合法输入。
编写:策略设计、边钉与 settings
generating.md 强调"写@given装饰器是最容易的部分",真正拉开差距的是下列决策:
约束放策略,不放assume()。assume()在生成之后丢弃输入,一个拒绝多数候选的过滤器既浪费预算,又最终触发 Hypothesis 的 exhausted-filter 守卫(以一条没人读的警告形式出现):
# 慢,且大多被丢弃 @given(st.integers()) def test_positive(x): assume(x > 0) ... # 只生成你想要的 @given(st.integers(min_value=1)) def test_positive(x): ...assume()只应保留给真正无法用生成器表达的条件——通常是两个已生成值之间的关系。复合输入用st.builds构建,依赖字段用st.composite或.flatmap派生,而不是独立生成后过滤:
@st.composite def sized_list_and_index(draw): xs = draw(st.lists(st.integers(), min_size=1)) i = draw(st.integers(min_value=0, max_value=len(xs) - 1)) return xs, i钉扎已知边界。随机生成迟早会撞到边界,@example则在每次运行都覆盖它们,并记录下你确实思考过这些边界:
@given(st.lists(st.integers())) @example([]) @example([1]) @example([1, 1, 1]) def test_sort(xs): ...空、单元素、全重复、零、负数、最大可表示值,是反复出现的那几类边界。
Settings 分场景。默认值(100 个示例、200ms deadline)在工作流两端都是错的:
@settings(max_examples=10) # 本地迭代 @settings(max_examples=200) # CI @settings(max_examples=1000, deadline=None) # 夜间任务任何做真实工作的测试都应设deadline=None——默认 deadline 会把一台慢机器变成失败测试,而这个 flake 最终会导致整套测试被删除。
确定性作为属性、错误路径也要测:st.binary()打 decoder 值得一写——契约通常是"抛DecodeError或成功,绝不抛IndexError、绝不挂起"。只捕获文档化异常,让其余异常直接让测试失败。
重构:把属性暴露出来
refactoring.md 指出:"这段代码没有代数形状"往往是对代码排列方式的描述而非对其行为的描述。建议重构时要说出它解锁了哪个属性、并交由作者决定——为测试改动生产代码,与引入 PBT 依赖一样,是作者的决策。五种重构按收益降序:
- 提取纯核心——收益远超其他四种,也是大多数"不可测"代码可测的原因。I/O 放边缘、计算放中间、对中间断言。例如把混合了
db.fetch/db.save的process_order拆出纯函数order_total(order, rules) -> Decimal,后者立刻支持不变量(永不为负、不高于未折扣总额)、折扣率的单调性、以及对参照计算的 oracle;原函数保留带 mockdb的示例测试。同样的手法适用于任何以副作用为可观察物的代码:先构造消息、请求、查询对象,再发送——构造可测,投递用 mock。 - 补上缺失的逆操作——单向操作天然没有往返属性。即使生产代码从不解码,也应说明"只为了测试而存在的逆操作":没人能读回的序列化器通常是潜在 bug 而非设计。
decode_message(data) -> dict解锁了目录中最强的 roundtrip。 - 结构化表示加渲染器——字符串拼接除了"包含某子串"外无可断言。把值与其渲染分开,逆操作就出现了:
render(q: Query) -> str与parse(sql: str) -> Query构成往返,这正是转义 bug 的藏身之处——对生成出的 filter 值做往返测试能找到手写示例永远找不到的引号错误。 - 返回值而非原地修改——原地修改后输入已消失,无从对比。
sorted_tasks(tasks) -> list[Task]解锁is_sorted、排列、幂等、长度保持;若必须保留原地签名,一个"复制后返回"的包装器就足以让测试有东西可握。 - 注入依赖——读全局、模块常量或
os.environ的函数只能在偶然值下被测试,其输入域边界不可达。把validate(data) -> bool(读CONFIG.max_length)参数化为validate(data, max_len) -> bool,生成器才能驱动max_len走向 0、1 与最大可表示值——validator 真正在边界处崩坏。
何时不建议建议重构:解锁的属性只是"不崩溃"(为目录中最弱属性重构生产代码是坏交易,直接说代码不适合 PBT 并停下);模块需要整体重写(一次性说清,二十条各自合理的小建议在同一个文件上就是噪音,读起来像重写请求而非测试建议);重构破坏公开 API(标记为 breaking 并提供向后兼容版本);已有测试覆盖(重构后运行它们并明说已运行——"启用了一个属性测试却弄坏了两个示例测试"不是进展)。
审查:按严重度分级报告
reviewing.md 的出发点是一句残酷的事实:一个属性测试可以通过多年却什么也没断言。问题按严重度从高到低:
| 问题 | 严重度 | 表现 |
|---|---|---|
| 同义反复 | CRITICAL | 断言与实现无关,恒真 |
| 空洞 | CRITICAL | assume()过滤掉几乎一切,或自相矛盾 |
| 无断言 | HIGH | 函数体调用了函数就结束 |
| 重实现 | HIGH | 断言重算了函数自身的逻辑 |
| 可用更强属性 | MEDIUM | 只查了长度,没查顺序 |
| 过度过滤 | MEDIUM | 层层assume()叠在本属于策略约束的地方 |
| Settings 问题 | LOW | max_examples=5,或昂贵策略未设 deadline |
每个问题都要附严重度上报,不要替作者决定一个 MEDIUM 值不值得提。其中两条辨析值得注意:f(x) == f(x)在f并非显然纯函数时不是自动的同义反复(pickle.dumps(obj) == pickle.dumps(obj)对__reduce__不确定的对象真的会失败);assume(x == 42)是更隐蔽的空洞——它运行、它通过,它只是一个穿着@given装饰器的示例测试。
审查还要对照属性目录点名"代码支持但套件未断言的更强属性"(len(sort(xs)) == len(xs)却从不查顺序是常见案例),并标记三类 flake 源:无容差的浮点相等、dict/set 迭代顺序断言、读取时钟的断言——它们被归咎于 Hypothesis 后会被整套删除。查找测试可用:
rg "@given\(|from hypothesis import" --type py rg "fc\.(assert|property)" --type ts --type js rg "proptest!|#\[quickcheck\]" --type rust失败解读:先锚定契约,再分诊
interpreting-failures.md 断言一个失败的属性测试只说了三件事之一:属性写错了(断言了代码从未承诺过的事)、规格含糊(这个边缘行为从未被决定)、代码错了(违反文档化保证)。大部分工作是把三者区分开——跳过这一步正是 PBT 落得"噪音"名声的原因。
先按权威递减顺序锚定代码的真实承诺:外部规格(RFC、格式定义)→ 类型标注 → docstring → 既有测试 → 函数名。函数名是最弱且最易误导的信号——大量名为normalize的函数做的事比这个词窄得多。书中示例:Hypothesis 报告test_normalize(s='\x00')违反幂等。若 docstring 写"任意 Unicode 输入",则空字节在域内、属性被锚定、这是一个真 bug;若改为"仅 ASCII 可打印字符",同一失败就变成策略 bug——修复是st.text(alphabet=...),而不是一份 bug 报告。
分类决策表:
| 症状 | 原因 | 动作 |
|---|---|---|
| 违反文档化保证 | 代码 bug | 连同缩小输入与文档引文上报 |
| 输入违反文档化前置条件 | 策略过宽 | 约束策略 |
| 属性与 docstring 或类型矛盾 | 属性错误 | 修属性 |
| 规格从未覆盖的边缘 | 规格含糊 | 问维护者;这是讨论而非 bug 报告 |
| 在现实约束下消失 | 测试伪影 | 修策略 |
| 行为与兄弟函数不同 | 可能不一致 | 值得提出,标注不确定性 |
前置条件违反与显式未定义的行为不是 bug——向一个文档声明只接受正整数的函数传-1什么也说明不了。要连分类一并上报,包括不确定的案例(说"规格含糊,需维护者决定"而不要保持沉默,被压下的发现无法被任何人分诊)。
反复出现的失败模式:孤立代理对(lone surrogate)破坏文本往返(decode(encode(s)) == s在'\uD800'上失败,是否 bug 取决于格式是否声明接受任意str);非规格化数(denormal)破坏数值不变量(概率函数对x=1e-320返回负值,是违反文档化[0,1]范围的真 bug,且正是人类不会手写的输入);哈希/相等性分歧违反语言契约而非仅 docstring(Python 中a == b必须蕴含hash(a) == hash(b),无需锚定直接上报);自定义迭代器差一(off-by-one)表现为list(it(xs)) == xs丢掉最后一个元素,几乎总是真 bug。
库选择与智能合约不变量
libraries.md 的首要原则是"匹配项目既有选择"——向已有 PBT 库的代码库引入第二个库,不值你想写的那个属性。按语言对照:Python 用 Hypothesis;TS/JS 用 fast-check;Rust 用 proptest(quickcheck 备选,API 更简、按类型收缩);Go 用 rapid(gopter 备选);Java 用 jqwik;Scala 用 ScalaCheck;C# 用 FsCheck;Elixir 用 StreamData;Haskell 用 QuickCheck(Hedgehog 备选,集成式收缩、无类型类);Clojure 用 test.check;Ruby 用 PropCheck;Kotlin 用 Kotest;C++ 用 RapidCheck;Swift 用 SwiftCheck(已无人维护,推荐前先确认)。提出建议前先探测仓库既有用法:
rg "from hypothesis import|fast-check|use proptest|pgregory.net/rapid|net.jqwik|echidna_|invariant_"**智能合约(EVM/Solidity)**是 PBT 收益最大的领域——合约状态是敌对的,输入域是"所有可能的调用序列"。Trail of Bits 维护两个工具:Echidna(属性 fuzzer,成熟,默认选择)与Medusa(并行执行、覆盖率引导,大合约套件上更快)。两种测试模式,选错是常见错误:
属性模式——返回bool且永不得为 false 的函数:
// Echidna 在每条交易序列后调用此函数。 function echidna_total_matches_sum() public view returns (bool) { return token.totalSupply() == trackedSum; }断言模式——fuzzer 可直接调用的函数内部的assert,针对特定操作而非全局状态:
function testDepositIncreasesBalance(uint256 amount) public { uint256 before = vault.balanceOf(address(this)); vault.deposit(amount); assert(vault.balanceOf(address(this)) >= before); }值得断言的合约不变量:偿付能力(sum(balances) <= totalAssets)、供给守恒、访问控制(非 owner 调用序列永远到不了 owner-only 状态变更)、单调计数器、share/asset 转换的往返(convertToShares后convertToAssets从不返回多于投入)。
Solidity 特有的同义反复也要警惕:类型边界不是属性——uint256 x >= 0恒真,address(this).balance >= 0也是编译器保证的;只读取 fuzzer 无法到达的状态的属性是空洞的。echidna_函数必须是view/pure且无参数——改变状态的属性会悄悄改变它正在测试的对象。可参考 secure-contracts.com 的教程(外部链接,仅作背景参考)。
把 PBT 引入尚无它的项目
SKILL.md 给出的边界是:项目已用 PBT 库就直接写;若没有,引入一个库是属于用户的依赖决策——只提一次,附上你具体要写的属性,接受任何一种答复。
评估体系:为什么叫 evals-extra 而不是 evals
技能的评估套件刻意不放在 skill 内部,而是位于插件根目录的evals-extra/——这样"模型读取指导的目录"不会同时携带 900 行 bash、一份点名 hypothesis 的requirements.txt和一个装满故意写坏的测试的 fixture:
plugins/property-based-testing/evals-extra/ # 手工运行,绝不被 `make check` 自动执行 ├── run.sh # 触发率评估 ├── effectiveness.sh # 生成的套件能否发现真实缺陷 ├── *.md # 带标签的查询(query/should_trigger) └── fixture/ # 查询所指向的小型仓库命名而非放入evals/的核心理由是成本:一次完整扫描是 45 个 Claude 会话、约 52 分钟、约 $36。这不该挂进例行检查,因此目录名使其远离自动化——没有任何东西自动运行这些扫描,开发者只在描述或指导变更时才手动调用。但--self-test入口是例外,它们运行在make check里:用桩二进制、零成本、约九秒,其作用是证明 harness 仍有判别力——一个悄悄停止测量的 eval 会永远报告绿色技能。Makefile 用evals*通配发现它们,因此改名不会把它们偷运出 CI。两条机器 glob 依赖同一前缀,且漂移时都会大声失败:python-tests排除evals*/fixture/(其中含一个故意空洞的assume()测试,pytest 理应失败);插件校验器解析引用链接时跳过evals*。
下文所有命令均从插件根目录(plugins/property-based-testing/)运行。
触发率评估:run.sh 测的是"技能会不会被调用"
"技能会触发"与"技能有帮助"是两个不同的命题。evals-extra/run.sh测前者:把 evals-extra/ 下每个带标签的查询(如 01-roundtrip-codec.md 携带query与should_trigger前置元数据)丢进真实会话,比较触发率与should_trigger。15 个查询中有 8 个是近似未命中负例(libFuzzer harness、mutation-testing 活动、Slither 扫描)——因为"什么都触发"的描述与"从不触发"的一样坏。
./evals-extra/run.sh # 每个查询 3 轮,4 个并发 RUNS=1 ./evals-extra/run.sh # smoke 冒烟 PLUGIN_DIR=/tmp/old ./evals-extra/run.sh # 给技能的另一份拷贝打分 ./evals-extra/run.sh --self-test # 免费:证明 harness 仍有判别力每个查询每轮一个会话,因此一次扫描是查询数 x RUNS——当前为 45。脚本启动时会打印这个数字,请相信它而不是任何写在此处的数。
从测量而非口味定出的两个参数
TIMEOUT_S=600:干净扫描中最慢的合法会话花了449s。旧的 300s 会把它杀掉,四次这样的击杀会作废整个扫描。600s 的取值源于实测,而非口味。TURNS=200:最慢的会话花了32 轮。旧上限 14 在一个样本中截断了八个会话中的五个;中间值 30 仍会抓住这个案例。200 是观察到的自然完成轮数的 10 倍,必然晚于 timeout 触发——为何保留而非移除,run.sh内的注释有完整说明。
失败会话不等于未触发
崩溃、超时或限流不会产生任何 Skill 调用,这与"模型考虑过技能然后拒绝了"无法区分——旧逻辑把它计为 miss 并被 floor 的余量吸收:10 个查询各 3 轮、floor 27 允许 3 次 miss,一个三连崩的查询仍能凑够 27 并报告通过。现在扫描把每次失败都记在NOTE列(区分timeout与crash:*),把该查询标为INVALID(其分母未知),并无论分数如何都退出 3。其余查询照常运行、照常报告。
但一次调用压过失败:Skill 调用是正向且终局的——会话后续任何事都无法"撤销"它,因此检测器在退出状态阶梯之前运行,yes无论如何都成立。只有"未调用"才取决于会话是否到达了决策。把这个顺序搞反正是旧数字被污染的原因。
每个会话的原始 stdout 与 stderr 都会保留,存放在运行开始与结束时打印的目录里,且不受清理 trap 影响。这是必需的仪表而非可选:两次扫描产生了 10 个事后无法诊断的失败,因为捕获在分类完成的瞬间就被删了。失败报告在最终result记录的stdout上——一次干净的 45 会话扫描中 90 个 stderr 文件有 0 个含任何内容,只读 stderr 的人什么都学不到。产物会累积(约 4MB/扫描)且从不清理。
退出码契约:
| exit | 含义 |
|---|---|
| 0 | 每个查询都达到预期,且每个会话都返回了裁决 |
| 1 | 回归——通过的查询少于EXPECT_PASS |
| 2 | harness 失败——未发现查询,或 eval 文件畸形 |
| 3 | 无效——会话崩溃、超时或未返回任何内容 |
实测数据与一次自我纠错
在opus上、每查询 3 轮、45/45 会话返回裁决且run.sh退出 0——这是该套件第一次成为"测量"而非"产物":
| 查询 | 预期 | 实测 |
|---|---|---|
| 01-roundtrip-codec | true | 0/3 FAIL |
| 02-normalizer-idempotence | true | 2/3 |
| 03-hypothesis-existing | true | 3/3 |
| 04-echidna-invariant | true | 3/3 |
| 05-review-weak-tests | true | 3/3 |
| 07-fuzz-serializer-noname | true | 3/3 |
| 08-sort-comparator | true | 3/3 |
| 全部 8 个负例 | false | 各 0/3 |
总计:15 个查询通过 14 个,recall 6/7,precision 8/8,原始触发命中 17/21。
该 README 明确要求读者不要相信早前记录的 13/15、recall 5/7、14/21(其中 04 与 07 为 1/3)。旧数字由一个"先看会话退出状态、再检查技能是否被调用"的分类器产生,任何调用了技能后又撞上 14 轮上限的会话都被当作崩溃丢弃。在一个抢救回来的样本中,12 个会话有 9 个被扔掉,而全部 12 个都调用了技能——包括 04 的全部三轮,那个被记录为 1/3 且后来实测为 3/3 的查询。偏差不是随机的:更长、更探索性的查询既最可能撞轮数上限,又恰恰是其正证据最要紧的那些,于是反转恰好压低了被研究查询的 recall——任何从旧表得出的结论(尤其是"Echidna/Solidity 路径触发不佳")都不再成立。
门槛 floor 保持13而非升到实测的 14:三个不同正例(01、04、07)在不同轮次里都当过唯一失败者,floor 14 将不给这套套件处处记录的随机性留任何余地——一次有效扫描不足以收紧一个门槛。其中01(wire-format roundtrip)是当前 miss(0/3),且是真实反转:在抢救样本中它三轮全调用了技能。值得在把它当作描述问题之前再做一次有效扫描。
已知缺口:triage 请求不触发
套件里没有任何东西覆盖第三个任务——判断缩小后的反例是真 bug、错属性还是规格未决的边缘。曾有一个这样的查询,后被移除:它停在 0/3——把伪造输入与代码交给模型,模型直接作答,从不伸手取指导,而描述措辞无法改变这一点。移除而非保留为"已记录的失败",是因为两个标签都不成立:should_trigger: true断言了描述无法产生的触发;should_trigger: false断言技能应远离其广告宣传的任务。字段是二元的,诚实的答案是"未测量"——没人验证过加载 references/interpreting-failures.md 是否改善了无辅助作答的分类。作为回归测试它也毫无价值:因从不触发,无论该参考文件完整还是被删除其分数都相同。若想重新接纳它:带与不带插件各跑一次该查询并比较答案——若指导改善了分类,should_trigger: true便站得住脚,0/3 才成为值得追的真 bug。附带警告:那个 0/3 同样由误判 04 的分类器测得,也不可信(影响小于 04,但从未被重新测量)。
两个脚本都钉住--model(MODEL,默认opus)并在表格上方打印它。触发率是"描述和模型"两者的性质,未记录模型记录的数字不可与下一个比较。要评判描述改动,就在同一模型上给旧拷贝与新拷贝打分——PLUGIN_DIR正是为此存在,它接受一个插件目录,无需弄脏工作树。
有效性评估:effectiveness.sh 测的是"套件能否抓到真 bug"
effectiveness.sh 的注释开宗明义:触发率只说明描述是否触发,不说明技能加载后是否帮上忙;本脚本测量真正重要的结果——生成的测试套件是否发现真实缺陷。
fixture 的 src/codec.py 内置一个真实缺陷:canonicalize_url用不含%的安全集做百分号编码,第二次经过会对自己刚产生的转义再编码,于是canonicalize_url("a b")不是不动点:
canonicalize_url("a b") == "a%20b" canonicalize_url("a%20b") == "a%2520b" # 不同这正是经典的双重编码bug。一个断言f(f(x)) == f(x)的属性套件在普通st.text()策略的几乎任何输入上都能使其失败(实测 30/30,因此这里的失败是真回归而非抽样运气);而从 happy path 写出的示例套件永远不会。评分是差分的、从不是散文:套件先对缺陷版canonicalize_url跑一次,再对补丁版跑一次;任何"之前失败、之后通过"的测试都在检测这个具体缺陷——无论模型给它起了什么名字。裁决永远不来自模型自述的表现,也从不靠匹配测试名。补丁替换用恒等函数(恒等天然幂等),并拒绝无法应用补丁的 fixture 漂移。
EFFORTS=low ./evals-extra/effectiveness.sh # 按发布形态给技能打分 NOPLUGIN=1 ./evals-extra/effectiveness.sh # 基线:不加载技能裸跑./evals-extra/effectiveness.sh会请求 low/medium/high 并被拒绝(exit 2)——因为 SKILL.md 钉住了effort,多级扫描的所有臂都会以同一被钉住的值运行,而三行相同也正是健康扫描的样子。正确流程:请求被钉住的那一级单独打分,或在拷贝上剥掉 pin 并设PLUGIN_DIR。自测(--self-test)用桩断言六件事:真正的幂等属性检测到缺陷得yes、仅类型属性不得分得no、无法导入的套件是ERR而非干净 miss、补丁无法应用的 fixture 是ERR而非评分、被钉住的 effort 拒绝多级扫描、被钉住的单独一级允许。另需注意NOPLUGIN=1基线应在向本技能添加内容之前运行——opus 已经能无辅助写出像样的 Hypothesis 套件,不推动NOPLUGIN=1之差的任何内容都在"花上下文而一无所获"。
插件根 README 还补充了第三种评估:evals/目录下的消融套件(claude plugin eval . --ablation with-without,每例带插件与不带各跑一次,报 Δ 而非原始分),并记录了两条用钱买来的操作经验:需要ANTHROPIC_API_KEY(沙箱配置目录看不见交互登录,订阅 OAuth token 被忽略,缺 key 时每会话瞬间以Not logged in死亡、花一分钱,而法官会给那个字符串打零分,看起来像真结果);--allow-tools Write是承重墙而非便利(03 的最强 grader 在模型创建requirements.txt时触发,禁掉Write它就永远无法创建,grader 免费通过)。实测(opus、每臂 3 轮、约 $4.30/整轮):02-neg-cargo-fuzz-coverage(过度触发守卫)带插件 0 触发、Δ0 正确;03-dependency-is-users-call 从无辅助的 0.00 提升到 0.50。该套件不声称建立了"技能帮助你写出更好的属性测试"——两例低于值得信任的四正例下限,且三条流仍未测量:审查既有测试、对无代数形状的代码拒绝 PBT、triage(即技能 README 已记录的缺口)。
为什么钉effort: low
这个取值是扫描出来的,不是猜的:low、medium、high三档都能检测 fixture 缺陷,而low在 4/4 轮中做到;low下的审查路径还独立地把 fixture/tests/test_parser.py 中植入的两个缺陷都标为 CRITICAL。没有任何测量为支付更多辩护——这正是仓库 AGENTS.md 中sweep downward on your own evals所要求的。
改动前须知:effort双向覆盖会话层级,所以这个钉会把一次有意的xhigh会话拉低——这是"设置它"的真实代价,也因此若生成路径将来开始回归,论据指向提高而非降低该值。这个 pin 还打破了当初证明它的那次扫描:技能加载后--effort被忽略,三臂都按被请求的标签下的low运行。effectiveness.sh因此在 pin 存在时拒绝多级扫描(exit 2),并指引你从拷贝上剥掉它、用PLUGIN_DIR指向——改钉值前请用那种方式重新扫描。
示例提示词
技能覆盖四类任务,开箱即用:
"Write property-based tests for this JSON serializer" "Review this Hypothesis test for quality issues" "Write Echidna invariants for this staking contract" "Hypothesis shrank to '\x00' — is this a real bug?"小结
property-based-testing插件把"属性测试"从一句口号落实为一张可对照的属性目录、一套强度排序、五种重构暴露属性的手法、一张审查严重度表、一张失败分诊表与一份逐语言库对照——并以 SKILL.md 做路由、五份 references 按需加载。更难能可贵的是它把 Agent 技能当作被测软件来维护:run.sh测触发率、effectiveness.sh测有效性、evals/测消融 Δ,参数从测量而非口味中定出,坏掉的 harness 会大声失败而非永远报告绿色。对任何想用测量数据驱动地打磨 Claude Code 技能定义与描述的人,skill 的 README 与 插件根 README 本身就是一套可以复制的工程范式。
- AI 技能
- AI 插件
- 应用安全
- 网络安全
- AI 评测
【免费下载链接】skills
Trail of Bits Claude Code skills for security research, vulnerability detection, and audit workflows
相关推荐
属性测试技能触发边界评测:property-based-testing 插件 14-neg-benchmark 负样本用例深度解析
属性测试技能触发边界评测:property based testing 插件 14 neg benchmark 负样本用例深度解析 导读 本文以 Trail o
AI 技能AI 插件应用安全网络安全AI 评测PostHog SQLV2 节点结果交付机制解析:从短轮询到 pub/sub 推送与结果分页存储设计
PostHog SQLV2 节点结果交付机制解析:从短轮询到 pub/sub 推送与结果分页存储设计 SQLV2 是 PostHog 新版 Notebook(n
AI 技能AI 插件应用安全网络安全AI 评测Foundry 并行 Invariant 测试的 Corpus 持久化修复:外部终止不再丢失语料库
Foundry 并行 Invariant 测试的 Corpus 持久化修复:外部终止不再丢失语料库 导读 Foundry 的 invariant 测试(不变式测
AI 技能AI 插件应用安全网络安全AI 评测
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考