rtk TOML 过滤器体系详解:编写 Filter 文件、构建期嵌入、运行时管线与信任机制
本文围绕 rtk(Rust Token Killer)仓库中的 src/filters/README.md 展开,系统讲解内置 TOML 过滤器的工作方式:哪些命令适合用 TOML 过滤器、Filter 文件的完整字段与编写步骤、build.rs 如何把 src/filters/*.toml 拼接并嵌入二进制、运行时 8 阶段过滤管线与查找优先级,以及自定义过滤器(项目级/用户级)的信任(trust)门控机制。读完本文,你可以直接为新的 CLI 命令写一个可构建、可测试、可信用的 TOML 过滤器,并理解它在 rtk 内部从构建到执行的完整链路。
一、内置过滤器是什么:一个 .toml 文件 = 一个过滤器 + 内联测试
rtk 是一个面向 LLM/Agent 的 CLI 代理:它在命令输出进入模型上下文之前,对 bash 输出做过滤和压缩,官方描述中可削减常见开发命令 60%–90% 的输出体积(见 README)。内置过滤器的核心约定非常简洁,来自 src/filters/README.md:
- 每个
.toml文件定义一个过滤器及其内联测试([[tests.*]]段落与过滤器放在同一文件里); - 构建时由
build.rs将所有文件按字母序拼接成一个 TOML 大块(blob),整体嵌入二进制。
二、何时使用 TOML 过滤器(以及何时不该用)
原文档给出了清晰的适用边界,这里完整继承并结合仓库佐证:
TOML 过滤器只做噪声行剔除(strip noise lines),不重排输出。过滤后的结果必须仍然"看起来像真实命令的输出"(设计原则见 CONTRIBUTING.md — Design Philosophy)。
TOML 适合逐行可预测的文本输出、且正则过滤能达到 60% 以上节省率的命令,典型场景:
| 类别 | 例子 | 过滤策略 |
|---|---|---|
| 安装/更新日志 | brew、composer、poetry | 剔除 Using ... / Already installed 行 |
| 系统监控 | df、ps、systemctl | 保留关键行,去掉表头/装饰 |
| 简单 Linter | shellcheck、yamllint、hadolint | 去掉上下文,保留 findings |
| 基础设施工具 | terraform plan、helm、rsync | 去掉进度信息,保留 summary |
TOML 与 Rust 模块的取舍标准在 CONTRIBUTING.md — TOML vs Rust: Which One? 中有完整对照表:输出是纯文本且行结构可预测、正则能砍掉 60%+ 字节、不需要注入 CLI 参数(如 --format json)、不需要跨命令路由时,选 TOML;反之(JSON 结构化输出、需要状态机解析、需要注入参数、需要路由到其他命令)应写成 Rust 模块(如 vitest、pytest、golangci-lint)。
"看起来像真实命令输出"这一点在源码中有硬性执行者:CONTRIBUTING.md 提到的 guard::never_worse(raw, filtered) 保证 rtk 永远不发出比原始输出更多的 token。此外还有一个容易踩的坑:被 Clap 路由的保留命令不能被 TOML 过滤器接管。src/core/toml_filter.rs 定义了 RUST_HANDLED_COMMANDS(ls、git、gh、cargo、npm、tsc、vitest 等 50 余个),这些命令在到达 TOML 匹配(run_fallback())之前就被专用 Rust 模块处理。如果某个过滤器的 match_command 命中了保留命令,加载时会打印警告:
[rtk] warning: filter 'x' match_command matches 'git' which is already handled by a Rust module — this filter will never activate for that command
三、文件格式与全部字段
一个 Filter 文件的完整格式如下(继承自原文档,可直接复制使用;注意真实文件中 schema_version = 1 是必填的,build.rs 在拼接内置过滤器时会自动写入该行,见 build.rs):
[filters.my-tool]
description = "Short description of what this filter does"
match_command = "^my-tool\\b" # 正则,匹配完整命令字符串
strip_ansi = true # 可选:先剥离 ANSI 转义序列
strip_lines_matching = [ # 可选:丢弃匹配任一正则的行
"^\\s*$",
"^noise pattern",
]
max_lines = 40 # 可选:过滤后只保留前 N 行
on_empty = "my-tool: ok" # 可选:过滤后输出为空时的兜底消息
[[tests.my-tool]]
name = "descriptive test name"
input = "raw command output here"
expected = "expected filtered output"
可用字段一览
| 字段 | 类型 | 说明 |
|---|---|---|
description |
string | 人类可读描述 |
match_command |
regex | 匹配命令字符串(如 "^docker\\s+inspect") |
strip_ansi |
bool | 处理前先剥离 ANSI 转义码 |
filter_stderr |
bool | 捕获 stderr 并合并进 stdout 再过滤(适用于把横幅打到 stderr 的工具,如 liquibase) |
strip_lines_matching |
regex[] | 丢弃匹配任一正则的行 |
keep_lines_matching |
regex[] | 只保留匹配至少一个正则的行 |
replace |
array | 逐行正则替换({ pattern, replacement }) |
match_output |
array | 短路规则({ pattern, message }) |
truncate_lines_at |
int | 截断超过 N 字符的行 |
max_lines |
int | 只保留前 N 行 |
tail_lines |
int | 只保留最后 N 行(在其他过滤之后应用) |
on_empty |
string | 过滤后输出为空时的兜底消息 |
源码补充:表格里没有、但实际存在的字段与约束
结合 src/core/toml_filter.rs 的 TomlFilterDef 反序列化定义,有几个文档表格未列出、实操时需要注意的事实:
head_lines:源码同时支持head_lines(保留前 N 行)与tail_lines(保留后 N 行),两者可同时配置——超出部分会以... (N lines omitted)提示行衔接,这符合"截断必须附带恢复提示"的设计原则。match_output支持unless字段:若unless正则也命中整个输出,该短路规则被跳过——用于防止"输出里出现错误信息时仍然被替换成成功短语"(见 src/core/toml_filter.rs 与 apply 实现)。strip_lines_matching与keep_lines_matching互斥,同时设置会导致该过滤器编译失败并被跳过(compile_filter)。schema_version = 1必填:parse_and_compile对非 1 的 schema 版本直接报错(src/core/toml_filter.rs)。deny_unknown_fields:过滤器定义开启未知字段拒绝(src/core/toml_filter.rs),意味着字段名拼错会让整个文件的 TOML 解析失败,运行时只会打一条 warning 然后静默忽略该文件——写自定义过滤器时务必跑一遍cargo test/rtk verify验证。
真实过滤器示例
仓库内 60 余个内置过滤器都遵循同一套模式,举四个有代表性的:
- src/filters/make.toml:
match_command = "^make\\b",剔除make[N]: Entering/Leaving directory、Nothing to be done和空行,max_lines = 50,on_empty = "make: ok"; - src/filters/terraform-plan.toml:
strip_ansi = true,剔除Refreshing state...、Acquiring/Releasing state lock、"unchanged" 资源行,max_lines = 80,空结果输出terraform plan: no changes detected; - src/filters/df.toml:
match_command = "^df(\\s|$)"(锚定单词边界,避免误伤dfx之类命令),truncate_lines_at = 80+max_lines = 20,是"截断型"过滤器的典型; - src/filters/liquibase.toml:
filter_stderr = true的教科书案例——Liquibase 把 ASCII 横幅与INFO [liquibase.core] Reading resource等日志打到 stderr,合并后再用 10 余条strip_lines_matching规则清洗,最终只留版本行与真正的变更集执行结果。
以 make.toml 的输入/输出为例,可以看到"透明度"约束:
输入: 输出:
make[1]: Entering directory '/home/user' gcc -O2 foo.c
gcc -O2 foo.c
make[1]: Leaving directory '/home/user'
输出仍是一条像样的 make 编译输出,而非 rtk 自创的格式。
四、内联测试与构建期校验
[[tests.<filter-name>]] 段落是 TOML 过滤器的内联测试:每个测试给 input(原始输出)与 expected(过滤后期望),由测试框架驱动 apply_filter 逐条比对。构建与测试链路(原文档 mermaid 图的核心,完整继承如下):
flowchart TD
A[["src/filters/my-tool.toml\n(new file)"]] --> B
subgraph BUILD ["cargo build"]
B["build.rs\n1. ls src/filters/*.toml\n2. sort alphabetically\n3. concat → BUILTIN_TOML"] --> C
C{"TOML valid?\nDuplicate names?"} -->|"fail"| D[["Build fails\nerror points to bad file"]]
C -->|"ok"| E[["OUT_DIR/builtin_filters.toml\n(generated)"]]
E --> F["rustc embeds via include_str!"]
F --> G[["rtk binary\nBUILTIN_TOML embedded"]]
end
subgraph TESTS ["cargo test"]
H["test_builtin_filter_count\nassert_eq!(filters.len(), N)"] -->|"wrong count"| I[["FAIL"]]
J["test_builtin_all_filters_present\nassert!(names.contains('my-tool'))"] -->|"name missing"| K[["FAIL"]]
L["test_builtin_all_filters_have_inline_tests\nassert!(tested.contains(name))"] -->|"no tests"| M[["FAIL"]]
end
subgraph RUNTIME ["rtk my-tool args"]
R["TomlFilterRegistry::load()\n1. .rtk/filters.toml\n2. ~/.config/rtk/filters.toml\n3. BUILTIN_TOML\n4. passthrough"] --> S
S{"match_command\nmatches?"} -->|"no match"| T[["exec raw (passthrough)"]]
S -->|"match"| U["exec command\ncapture stdout"]
U --> V["8-stage pipeline\nstrip_ansi → replace → match_output\n→ strip/keep_lines → truncate\n→ tail_lines → max_lines → on_empty"]
V --> W[["print filtered output + exit code"]]
end
G --> H & J & L & R
build.rs:拼接、校验、嵌定的三步
build.rs 的实现与上图完全对应:
- 读取
src/filters/下全部.toml,按文件名排序保证确定性顺序(build.rs); - 以
schema_version = 1为头,逐文件拼接(每个文件带# --- <文件名> ---注释分隔)(build.rs); - 整体解析一次 TOML,语法错误直接
panic让构建失败(build.rs); - 跨文件重复的过滤器名也会让构建失败(build.rs);
- 结果写入
OUT_DIR/builtin_filters.toml,再由 src/core/toml_filter.rs 的include_str!在编译期嵌入二进制:
const BUILTIN_TOML: &str = include_str!(concat!(env!("OUT_DIR"), "/builtin_filters.toml"));
cargo test 中的三道"内置过滤器"守门测试
src/core/toml_filter.rs 的测试模块用硬断言防止内置过滤器集合被悄悄改动:
test_builtin_filter_count:锁定当前内置过滤器恰好为 63 个——增删src/filters/文件后必须同步更新该数字,否则cargo test失败;test_builtin_all_filters_present:断言预期过滤器名全部存在,文件被误删会报 "was its .toml file deleted from src/filters/?";test_builtin_all_filters_have_inline_tests:断言每个内置过滤器至少有一条内联测试,禁止零测试覆盖率出厂;- 另有
test_new_filter_discoverable_after_concat:模拟"新增一个my-new-tool过滤器后拼接",验证新过滤器 63+1=64 个且可被find_filter_in发现——即你提交一个新.toml后,构建+测试通过就意味着它在运行时可被发现。
除 cargo test 外,源码中的 run_filter_tests(src/core/toml_filter.rs)还是 rtk verify 命令的执行引擎,可以按过滤器名单独运行内联测试并报告"没有内联测试的过滤器"。
五、命名规范
原文档约定:
- 以命令名作为文件名:
terraform-plan.toml、docker-inspect.toml、mix-compile.toml; - 有子命令的,优先
<cmd>-<subcommand>.toml,而不是把多个过滤器塞进一个文件。
这个约定配合 build.rs 的"重名即构建失败"机制,使得"一个文件一个过滤器"在物理上不可违背。
六、运行时:注册、匹配与 8 阶段管线
查找优先级(First Match Wins)
运行时按以下顺序查找过滤器(完整继承原文档):
flowchart LR
CMD["rtk my-tool args"] --> P1
P1{"1. .rtk/filters.toml\n(project-local)"}
P1 -->|"match"| WIN["apply filter"]
P1 -->|"no match"| P2
P2{"2. ~/.config/rtk/filters.toml\n(user-global)"}
P2 -->|"match"| WIN
P2 -->|"no match"| P3
P3{"3. BUILTIN_TOML\n(binary)"}
P3 -->|"match"| WIN
P3 -->|"no match"| P4[["exec raw (passthrough)"]]
项目过滤器与内置过滤器同名时会遮蔽(shadow)内置版本并触发警告:
[rtk] warning: filter 'make' is shadowing a built-in filter
源码中的加载顺序见 TomlFilterRegistry::load:先加载两个磁盘上的受信任自定义文件(gated_filter_paths(),见下一节),再追加内置 BUILTIN_TOML;匹配用 find_filter_in 线性扫描,先注册者先命中(src/core/toml_filter.rs)。两个环境变量可用于调试/旁路(src/core/toml_filter.rs):
RTK_NO_TOML=1—— 完全绕过 TOML 引擎;RTK_TOML_DEBUG=1—— 在 stderr 打印加载了多少过滤器、哪个过滤器命中。
8 阶段过滤管线
apply_filter_with_info(src/core/toml_filter.rs)按固定顺序应用各阶段,与上表字段一一对应:
- strip_ansi — 剥离 ANSI 转义码;
- replace — 逐行正则替换,规则顺序串联(支持
$1回引); - match_output — 全量输出 blob 匹配即短路返回
message(首条命中规则生效,unless命中则跳过该规则); - strip/keep_lines — 基于
RegexSet的行过滤(二者互斥); - truncate_lines_at — 每行截断到 N 字符(unicode-safe);
- head/tail_lines — 保留前/后 N 行,被丢弃处以
... (N lines omitted)提示行标注; - max_lines — 绝对行数上限,超量行以
... (N lines truncated)收尾; - on_empty — 结果为空时输出兜底消息。
值得注意的设计细节:管线同时返回一个 Lossiness 分类(src/core/toml_filter.rs),区分"无损剔除噪声"、"尾部可经 tee 恢复"与"整体有损",供上层决定是否附带恢复提示(tee hint)——这正是 CONTRIBUTING.md 中"截断必须附带恢复提示"原则在管线层的落地。
七、自定义过滤器与信任(trust)机制
两个自定义位置
过滤器可以加在磁盘上的两个位置(格式与内置完全相同,均需 schema_version = 1):
- 项目级 —
.rtk/filters.toml(随仓库提交,仅对该项目生效); - 用户级 —
~/.config/rtk/filters.toml(Linux 下即此路径;源码通过dirs::config_dir()定位,见 src/hooks/trust.rs,跨平台时对应各系统的用户配置目录)。
信任门控:未信任的过滤器被静默跳过
由于过滤器可以改写 Agent 看到的命令输出,自定义过滤器文件必须先被信任才会生效。未信任(或内容被修改过)的文件在命令路径上被静默跳过——rtk 从不围绕一条被改写的命令打印警告。发现并启用未信任过滤器的入口是你主动执行的命令:
rtk trust # 列出每个检测到的过滤器(标注 project/global)+ 风险摘要,然后确认([y/N],或 --yes)
rtk untrust # 撤销信任
信任的存储与失效规则:
- 信任记录为文件内容的 SHA-256,因此修改一个已信任的文件会使信任失效,必须重新执行
rtk trust(源码中对应的ContentChanged状态在 TomlFilterRegistry::extend_with_trusted 中与Untrusted一样被跳过); rtk init会检测已存在的自定义过滤器并交互式确认是否启用([y/N],脚本场景可用--trust-filters/--no-trust-filters);对只有注释的空模板保持静默,非交互运行时保持过滤器禁用;- 内置过滤器编译进二进制,始终受信任,只有磁盘上的项目级/用户级文件受门控。
原文档同时给出了诚实的边界说明(完整继承):
Honest limitation: this is consent + tamper-evidence, not a sandbox. An attacker who can write your filter file can usually also write the trust store (
~/.local/share/rtk/trusted_filters.json) and bypass the gate. It defends the common case — a filter dropped in by a script, a dotfile sync, or an untrusted repo — not a same-user attacker who specifically targets RTK.
信任存储文件名在 src/core/constants.rs 中定义为 trusted_filters.json,信任校验与撤销逻辑集中在 src/hooks/trust.rs。
八、新增一个过滤器的完整步骤
综合原文档与源码校验机制,实操清单如下(原文档 4 步 + 源码侧约束):
- 复制任一现有
.toml文件并重命名,如my-tool.toml(文件名即过滤器名,遵循第五节命名规范); - 修改三个必备字段:
description、match_command,以及至少一个动作字段(strip_lines_matching/keep_lines_matching/replace/max_lines/on_empty等);文件需带schema_version = 1,且不得同时设置strip_lines_matching与keep_lines_matching; - 添加
[[tests.my-tool]]内联测试,覆盖"典型输出"与"全部行被剔除后触发 on_empty"两类场景(参考 make.toml 的三条测试写法); - 运行
cargo test:构建步骤会校验 TOML 语法、重名与内联测试存在性。此时还必须同步更新 src/core/toml_filter.rs 中test_builtin_filter_count的过滤器总数断言(当前锁定为 63)。
额外检查项(源自源码行为):
match_command不要命中 RUST_HANDLED_COMMANDS 中的保留命令,否则过滤器永远不会生效(加载时会有警告);- 字段名拼写错误会让整个文件解析失败并被静默忽略,务必依赖
cargo test或rtk verify验证; - 若输出里可能包含错误信息而你想用
match_output做"成功短语替换",记得配unless防止误吞错误。
对于需要注册 hook 重写规则的新命令(例如让 Agent 直接把 my-tool ... 写成 rtk my-tool ...),完整贡献清单见 src/cmds/README.md — Adding a New Command Filter,整体架构与端到端流程可进一步参阅 docs/contributing/TECHNICAL.md。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00