tombaldwin/candor-rust

GitHub: tombaldwin/candor-rust

一个 Rust 类型感知的能力/副作用检查器,通过传递性跨 crate 分析在 CI 门禁中强制执行架构与功能边界。

Stars: 0 | Forks: 0

# candor

Beaky, the candor canary

**强制执行 AI 生成的代码暗中越过的能力和架构边界 —— 作为你可以信赖的 CI 门禁。** candor 是一个用 Rust 编写的能力/副作用检查器,作为 [dylint](https://github.com/trailofbits/dylint) lint([candor-spec](https://github.com/tombaldwin/candor-spec) 的参考实现)构建。它知道哪些函数触及了网络、 文件系统、数据库、子进程、时钟或环境 —— *传递性地、跨 crate 地* —— 并将诸如“该层保持纯净”、“此服务只能与 Stripe 通信”、“领域 层不得依赖基础设施”等不变量转化为规则,当某次编辑破坏它们时,会**导致 PR 失败**。 **网站:** [candor.poly.io](https://candor.poly.io) —— 五分钟内的实测案例:展览、 预注册评估,以及在您自己的仓库中证明它的路径。 **为什么这对 AI 辅助开发很重要。** Agent 特有的失败不是拼写错误 —— 而是局部看似合理的编辑, 越过了它从未看到的边界。它在 `pricing.rs` 中添加了一个功能,获取数据的最简单方法调用了某些东西,经过三次跳转和一个 crate 之后,打开了一个 socket 或命中了数据库。文件看起来很干净;快速审查看起来也很干净。candor 的 传递性、跨 crate 分析捕获了它,并且门禁阻止了它 —— 随着 Agent 更快地编写更多代码,这种失败模式只会变得*更糟*, 而不是更好。在一次[预注册试验](eval/bet2/RESULTS.md)中,当局部最简单的编辑越过纯边界时, candor 将已发布的违规率从 **80% 降至 0%**。 **门禁只有在它从不说谎时才值得信赖。** candor 的契约是,当函数实际触及某种副作用时,它*绝不*默默地 报告其为纯函数:任何它无法解析的内容都会变为 `Unknown`(一种可靠的过近似),绝不会是错误的“干净”。该契约由 CI 中 的对抗性**可靠性模糊测试器**维护,它通过 Rust 隐藏调用的各种方式——运算符重载、`?`、`.await`、动态分发、闭包和回调、宏、跨 crate 边界—— 穿针引线地传递一个已知副作用,如果有任何可达函数返回纯函数,则构建失败。因此,当 candor 认证某个边界 干净时,您就可以采取行动。`cargo candor policy` 就是门禁本身:禁止的副作用、网络主机 白名单和层依赖规则 (AS-EFF-006/008/009),在整个工作区中强制执行。 **它也提供映射。** 同样的分析可以立即从缓存报告中回答“这个函数传递性地触及了什么?”以及“谁 触及了 `Net`?” —— 对于 Agent 或处于不熟悉代码中的人来说,这是一个低成本的爆炸半径工具。将编辑 Agent 自身更改的*非局部*增量差异交给它,也具有真正的价值(一项 [试点](EVAL.md)显示,Agent **100% 的时间报告了完整的传播,而没有它时仅为 7%**) —— 但 随着模型在局部调用图追踪方面变得更好,candor 持久的优势在于模型*无法*为自身做到的部分:掌握整个副作用图并**阻止 PR**。 ### 让 Agent 使用它 —— 一次粘贴,从零开始 给你的编程 Agent (Claude Code, Cursor, …) 这个: ``` Read https://github.com/tombaldwin/candor-rust/blob/main/AGENTS.md and follow it to map this repo's effects. ``` [AGENTS.md](AGENTS.md) 是独立的 —— 它会安装 candor,在此项目上运行它,并解释 报告和信任规则(`inferred` 是权威的;`unresolved`/`Unknown` → 阅读源代码)。Agent 的单一事实来源。 ### Claude Code:自动见证其工作 上面的粘贴内容要求 Agent 使用 candor —— 但你无法*看到*它是否真的这么做了。[Claude Code 集成](integrations/claude-code/)在你的 Rust 代码发生更改时,会在你的记录中为你提供一个确定性的、不可伪造的**回执** —— 函数计数、副作用分解、新鲜度哈希,以及当依赖项看起来有副作用但未校准时的覆盖率警告: ``` candor · 143 fns · 54 Db, 16 Net, 27 Fs · 0 unresolved · fresh @8c4c9053 · coverage ✓ ``` 一个 `Stop` hook 会在每次涉及 Rust 的回合自动刷新它(否则静默);`/candor` 按需显示它。安装:从你的项目中运行 `integrations/claude-code/install.sh` —— 它会安装轻量级的存根,这些存根将操作委托给此克隆,因此 `cargo candor update` 会同时刷新引擎、脚本和 `AGENTS.md`(每个回执都标有引擎提交,因此它们不会默默地不同步)。有关信任模型和诚实限制,请参阅其 [README](integrations/claude-code/README.md)。 **可选的编辑时自我审查。** 设置 `CANDOR_REVIEW=1`(在 `.candor/config` 中),Stop hook 的作用就不仅是通知人类:当 Agent 的编辑使函数与您提交的基线相比具有了*新*的副作用时,它会将该增量差异作为自我审查检查点*交还给 Agent* —— “你的编辑给了 `foo` 一个新的 `Net`(这会传播给它的调用者);是有意的吗?”。每个副作用提示一次,它从不会循环,并且默认关闭。这就是 candor *通知* Agent 和*改变其行为*之间的区别 —— 参见 [BACKLOG.md](BACKLOG.md) P0。 **MCP 服务器。** [`integrations/mcp/`](integrations/mcp/) 将 candor 的即时查询 (`candor_effects` / `candor_where` / `candor_callers` / `candor_diff`)暴露为原生的 MCP 工具,因此 Agent 会下意识地调用 candor —— 通过一次廉价的调用,而不是 grep 和阅读源代码。将其与 `cargo candor watch` 配对,以便每次调用都从最新的报告中获取数据。 **它在哪里体现价值 —— 以及在哪里没有。** candor 作为传递性地强制执行功能和架构边界的 **CI 门禁**最为锋利 —— 阻止使解析器打开 socket 或将领域层路由到基础设施的 PR([CI 防护栏](#ci-guardrail-lowest-friction-adoption)),不需要 token 穿线或重写 —— 以及作为交给编辑 Agent 的*非局部*增量差异(见上文)。 它的价值是**有条件的**,如实陈述:当代码库*具有*值得捍卫的边界且某次编辑*将要*越过它时,它才体现出价值。如果代码本身已经提供了清晰的接缝,强大的模型会绕过问题,candor 就显得多余 —— 同样的评估也准确地显示了这一点。它故意*不*做一些事情:它不是安全边界([SECURITY.md](SECURITY.md));它不是代码库质量评分(副作用计数依赖于领域 —— 没有可追求的“candor 分数”);这个仓库仅限 Rust(JVM —— Java/Kotlin/Scala/Groovy —— 拥有 [candor-java](https://github.com/tombaldwin/candor-java),相同的规范,相同的报告结构,相同的门禁);并且*可靠的*后端需要 nightly 版本 —— 零安装的[稳定版扫描器](#two-backends-stable-scanner-zero-friction-vs-the-nightly-lint-soundness)是尽力而为的,并且会少报。剩余的不可靠性(假定遵守其边界的非局部 trait 的泛型分发)被标记为 `Unknown` 和覆盖率警告,并列入[已知限制](#known-limitations)。它是一个锋利、狭隘、值得信赖的工具,而不是一个质量平台。 *人类:* [快速开始](#quick-start-humans) · *详情:* [它检测什么](#what-it-detects) · [PRINCIPLES](PRINCIPLES.md) · [CRITIQUE](CRITIQUE.md) ## 布局 | 路径 | 内容 | |---|---| | `src/lib.rs` | 整个 lint —— 分类器、基于每个函数调用图不动点、三种模式 | | `crates/candor-classify` | 副作用分类器 (`crate × path → effect`) —— 纯字符串逻辑,无 `rustc`;lint **和** 稳定版扫描器都调用的唯一事实来源 | | `crates/candor-scan` | **稳定版 Rust** 后端:一个基于 `syn` 的扫描器,可在普通的 `cargo` 上生成相同的报告 JSON,无需 nightly/dylint(见下文) | | `crates/candor-report` | 报告类型和解析,被所有后端和 CLI 共享 (无 `rustc_private`) | | `crates/candor-query` | `cargo-candor` 的只读查询 (`audit`/`show`/`where`/`callers`/`map`/`diff`/`whatif`/`rewire`/`containment`/`reachable`/`path`/`impact`) 作为一个类型化的二进制文件 | | `cargo-candor` | CLI 包装器 —— 轻量级的 bash,用于协调后端 (`cargo dylint` 或 `candor-scan`) 并将查询分发给 `candor-query` | | `sample/` | 一个以能力准则编写的小 crate,用于尝试一致性模式 | | `rust-toolchain` | 固定 lint 链接的 nightly 版本 (`rustc-dev`) | ## 设置 ``` cargo install cargo-dylint dylint-link # once per machine ./install.sh # build + install — then `cargo candor` works in any project ``` `install.sh` 是一次性且幂等的:它构建 lint(rustup 自动从 `rust-toolchain` 获取固定的 nightly 版本 + `rustc-dev` —— 你永远不需要手动管理工具链),将 dylib 和 `candor-query` 二进制文件存储在 `~/.candor` 下(一个即使在克隆中执行了 `cargo clean` 也能保留的稳定位置),并将 `cargo-candor` 符号链接到 `~/.cargo/bin`,以便 `cargo candor …` 在任何地方都能解析。随时重新运行 它(或 `cargo candor setup`)以刷新;`cargo candor update` 会拉取 + 重建 + 刷新。 固定的 nightly 版本是 dylint 所固有的(它链接 rustc 内部组件)并且仅用于 lint —— 它 不会触碰你项目的工具链。 ### 两个后端:稳定版扫描器(零摩擦)对比 nightly lint(可靠性) candor 通过两种方式生成**相同的报告 JSON**,并且每个只读查询 (`show`/`where`/`callers`/`map`) 读取任何一个的结果都是相同的: ``` cargo candor scan # STABLE: a syntactic scan on stock `cargo` — no nightly, no dylint, no rustc-dev cargo candor audit # NIGHTLY lint: the full rustc-backed analysis with the soundness contract ``` 稳定版扫描器是消除摩擦的利器,它只需**一行代码安装** —— 无需克隆,无需 nightly: ``` cargo install candor-scan # https://crates.io/crates/candor-scan candor-scan . # writes .candor/report..scan.json (or --json to stdout) # (a workspace root: one report per member, same prefix) ``` 它遍历 crate 的 `.rs` 文件,使用 [`syn`](https://docs.rs/syn) 解析它们,解析 `use`-别名 调用路径,并通过**与 lint 使用的相同的 [`candor-classify`](crates/candor-classify)** 对它们进行分类 —— 单一的事实来源,因此两个后端在什么算作副作用的问题上不会产生偏差。它只需要 稳定的工具链,因此它可以在任何 `cargo` 运行的地方运行(没有 nightly 的 CI,上锁的 机器)。在完整的 candor 安装中,它也是 `cargo candor scan`。 **默认稳定 —— 你无需选择后端。** 只读查询 (`show`/`where`/`callers`/`map`/ `audit`) 和 [Claude Code 回执](integrations/claude-code/) 在安装了 nightly lint 时 优先使用它(为了可靠性契约),并在未安装时**自动回退到稳定版扫描器** —— 因此 candor 可以在任何机器上零安装运行,当使用语法路径时,回执会显示 `· stable backend`。强制执行 (`guard`/`policy`/`snapshot`/`diff`) 仍然需要 lint: 阻止 PR 需要可靠性保证。 权衡是**精度,如实陈述**。扫描器是语法层面的,所以它看到的是*写出来的*内容,而不是 编译器*解析的*内容。它捕获路径限定的副作用调用 (`std::fs::read`, `Command::new`, `reqwest::Client::execute`)、`use`-别名、crate 内传递性传播,以及**局部 trait 分发** (`&dyn Store`/`impl Store`/`S: Store` 接收器解析为 trait 的本地实现者 —— 语法层面的 CHA,像 JVM 引擎一样有边界 —— 或者当 trait 没有可见的 impl 或实现太多时,如实读取为 `Unknown`)。对于依赖项,回执**指出分类器无法看到的内容**(κ-覆盖率账本:`κ doesn't know N dependencies this code calls into…`),而 `--deps` **关闭它**: 扫描整个 `Cargo.lock` 树一次(未构建的注册表源,实测约 0.23秒/依赖),根扫描通过报告串联起来 —— 副作用跨越每个 crate 边界,而分类器不需要知道这些 crate(规范 §2 报告串联)。它**会遗漏** — *默默地*,不发出 `Unknown` —— 仅通过非路径限定接收器上的方法调用、通过外部 trait发、闭包/函数指针、宏以及通过稳定标识进行的跨 crate 传播而触及的副作用。因此在重度依赖解析的代码上,与 lint 相比,它**少报**。将 `scan` 用于零摩擦的分类和稳定版上的 CI;当你需要可靠性契约 (`Unknown` 过近似、一致性、policy/guard 门禁)时使用 nightly lint。 默认情况下,`scan` 仅报告 crate 的**库/二进制**源码 —— 它跳过 `tests/`, `benches/`, `examples/`, `build.rs`, 和 `#[cfg(test)]` 模块,因此报告展示的是 *crate* 的行为,而不是其测试套件的行为(`--include-tests` 会保留它们)。一项[对 35 个真实 crate 的校准](eval/calibration/CALIBRATION.md) 发现,启用此功能后,扫描器在库代码中**没有误报** —— 它报告的每个副作用都是真实的;它唯一的错误是通过 FFI、方法分发和宏少报了(这是 lint 的工作)。例如,它正确捕获了 chrono 读取 `/etc/localtime`+`$TZ` 和 `which` 解析 `$PATH`, 并诚实地在 reqwest 上显示 `Net: 0`(其 socket I/O 隐藏在 hyper 解析的方法调用后面)。 ## 快速开始(人类) 执行 `install.sh` 之后,在任何 Rust 项目中使用包装器(它会自我修复 —— 如果 dylib 丢失,它会自动重建): ``` cargo candor scan # STABLE backend: produce the report on stock cargo (no nightly) cargo candor audit # at-a-glance effect profile of the whole project (nightly lint) cargo candor audit --all # the full per-function lint (spans in context) cargo candor snapshot .candor/baseline # write a JSON report cargo candor guard .candor/baseline # fail on functions that gained an effect cargo candor diff .candor/baseline # describe the per-function effect delta (--json) cargo candor watch # keep the report fresh in the background → instant `diff` cargo candor show my_function # a function's effects, instant (read from the report) cargo candor where Net # which functions perform an effect, instant cargo candor callers my_function # which functions call this one, instant (who depends on it) cargo candor explain my_function # trace WHY a function has each effect (the call path) cargo candor containment [baseline] # effect-leakage diagnostic; with a baseline, a CI ratchet cargo candor reachable # what the program does at runtime (union over entry points) cargo candor path my_fn Net # the call chain by which a fn comes to perform an effect cargo candor impact my_fn # blast radius: transitive callers + downstream entry points cargo candor policy .candor/policy # enforce effect boundaries (deny/pure rules) cargo candor risk # heuristic: effects on caller-derived input (advisory) cargo candor strict my_module # conformance, scoped to a module cargo candor no-ambient my_module # flag direct ambient-authority use ``` `cargo candor audit` 将项目的 crate 聚合到一个单屏配置文件中 —— 有多少 函数执行了每种副作用,哪些函数调用了 candor 无法解析的内容,是否有未校准的 依赖项,以及对外部世界影响最广的函数: ``` candor @62a9383 143 effectful functions · 7 pgman.Executable · 136 pgman.Rlib effects 56 Db · 53 Clock · 47 Log · 37 Env · 27 Fs · 23 Exec · 21 Clipboard · 18 Net broadest effect surface app::App::run { Clipboard Clock Db Env Exec Fs Log Net } main { Clipboard Clock Db Env Exec Fs Log Net } run_batch { Clock Db Env Exec Fs Log Net } … ``` `cargo candor policy` 是 candor 的**架构即代码**层 —— 也是随着模型在局部推理方面变得更好而体现其价值的部分。模型提供建议;只有掌握整个副作用图的工具才能*阻止 PR*。它强制执行 AI Agent 最容易犯的失败模式 —— 在没有看到整个副作用图的情况下编辑一个函数 —— 并且是在任何人都无法在脑海中记住的规模上进行的:一条命令快照工作区中的每个 crate,然后在加载了同级 crate 的情况下进行强制执行,因此边界可以捕获起因存在于*另一个 crate* 中的违规。策略文件声明不变量,candor 标记任何*传递性*的违规: ``` # .candor/policy deny Net Db Fs domain # the domain layer must reach no I/O — even through a helper pure parse # parsing must be side-effect-free deny Exec # nothing may spawn a subprocess allow Net in billing api.stripe.com # billing may reach the network — but ONLY Stripe allow Exec in build git # the build layer may run subprocesses — but ONLY git allow Db in billing ledger.* # billing may touch the database — but ONLY the ledger schema forbid domain -> infra # the domain layer must not depend on infrastructure ``` ``` [AS-EFF-006] `domain::checkout` performs { Db }, forbidden by policy (scope `domain`): `deny Net Db Fs domain` [AS-EFF-008] `billing::record_activity` reaches { metrics.growthtracker.io:443 } outside the allowlist, forbidden by policy (scope `billing`): `allow Net in billing api.stripe.com` [AS-EFF-009] `domain::checkout` reaches into a forbidden layer (via `infra::db::save`), violating policy: `forbid domain -> infra` ``` 三种边界类型,全部经过*传递性*检查,因此它们能捕获局部 diff 隐藏的内容: - **`deny` / `pure`** (AS-EFF-006) — 一个层可以做什么。`checkout` 不需要直接接触数据库;candor 会捕获它通过任何被调用者到达 `Db` 的行为。 - **`allow in …`** (AS-EFF-008) — 一个副作用可以触及哪些值:`Net` 主机(“计费层只能与 Stripe 通信”)、`Exec` 命令(“构建层只能运行 git”)、`Fs` 路径 (“配置层只能读取 /etc/app”)。模型无法自我检查的供应链边界,因为字面量隐藏在传递性的、通常是**跨 crate** 的被调用者中(按副作用匹配:主机按名称,命令按基本名称,路径按前缀)。 - **`forbid -> `** (AS-EFF-009) — 一个层可以依赖于谁。领域层不得触及 基础设施,即使是通过一系列辅助函数。 `cargo candor policy` 通过一条命令在整个工作区强制执行所有这三项 —— 它快照每个 crate,然后在加载了同级 crate 的情况下进行强制执行,因此存在于*共享 crate* 中的副作用或端点仍然会在禁止它的边界处被捕获。参见 [examples/candor-policy](examples/candor-policy) 和 [eval/bet3](eval/bet3/RESULTS.md)。 `cargo candor risk` 是一个**咨询性的、启发式的**推动,指向注入类别 —— 参数派生自函数参数的副作用 (`fs::read(format!("/var/cache/{key}"))`, `Command::new(name)`): ``` [AS-EFF-007] `read_user_file` performs { Fs } on caller-derived input (an injection surface — …) ``` 它*不是*可靠的污点分析:这是一个语法层面的、过程内的检查,会过度标记或标记不足 (它遗漏了通过结构体字段和跨函数的流,并会标记实际上已验证的参数)。使用它来寻找值得审查的外围接口 —— 切勿将其作为门禁。 ### `cargo candor containment` — 不是“分数”的架构信号 原始的副作用*计数*依赖于领域(数据库应用程序有大量的 `Db` —— 这不是缺陷),所以不存在单一的“candor 分数”。但跨层边界副作用的**分散程度**是 与领域无关的:`Db` 都在一个数据层中是架构良好的;`Db` 分散在 `model`, `actions`, *和* `dao` 中则是存在泄漏的 —— 无论它执行了多少 DB 操作。`containment` 对此进行测量,按 边界副作用 (`Db`/`Net`/`Exec`/`Fs`/`Ipc`);`Log`/`Clock` 是环境性的(被报告,但不评分)。层是从公共 crate 根之后的模块推断出来的,无需配置: ``` effect contained layers owner ← leaked into Db 55% 3 conn (11) ← query:7, app:2 # pgman: DB is mostly in conn/query… ``` (在 `pgman` 上运行 —— 其 DB *旨在*位于 `conn` + `query` 中 —— candor 独立发现了该边界**及其**一个有记录的例外,即 `app:2` 泄漏。)给定基线前缀,它就是一个**棘轮** —— 对变得更糟进行门禁控制,注意变得更好: ``` [containment] a boundary effect leaked into a layer it wasn't in: ← exit 1, fail the PR Db → actions ✓ improved — a boundary effect left a layer: ← informational Db ⊘ legacy ``` 使用 `cargo candor containment` 进行诊断,使用 `cargo candor containment .candor/baseline` 作为门禁。 刻意设计为**诊断 + 趋势门禁,而不是单一的评分** —— 绝对水平依赖于领域且可被操纵,但“边界副作用是否泄漏到了新层中?”是一个真实的、可执行的架构质量信号。 ## 所有模式(显式调用) 在任何 Rust 项目根目录下,将 `LINT` 设置为 dylib 的绝对路径: ``` # AUDIT(默认):每个函数的 transitive effect set。无需修改代码。 cargo dylint --lib-path "$LINT" # JSON:机器可读报告,每个 crate+type 一个文件:...json CANDOR_JSON=/tmp/report cargo dylint --lib-path "$LINT" # CONFORMANCE:强制执行 inferred ⊆ declared。 CANDOR_STRICT=1 cargo dylint --lib-path "$LINT" # whole crate CANDOR_STRICT=mymod::sub cargo dylint --lib-path "$LINT" # one module (incremental adoption) # ENFORCEMENT(cap-std-aligned):标记任何对 ambient authority 的 DIRECT reach。 CANDOR_NO_AMBIENT=mymod cargo dylint --lib-path "$LINT" # AS-EFF-004 per direct ambient call # REGRESSION GUARD:如果任何函数在已保存的 snapshot 之后获得了 effect,则失败。 CANDOR_JSON=.candor/baseline cargo dylint --lib-path "$LINT" # 1. snapshot (commit it) CANDOR_BASELINE=.candor/baseline cargo dylint --lib-path "$LINT" # 2. in CI: AS-EFF-005 on regressions # 可与任何模式结合的 Flags: CANDOR_CONFIG=candor.rules cargo dylint --lib-path "$LINT" # extra classifier rules CANDOR_PARANOID=1 cargo dylint --lib-path "$LINT" # treat generic trait dispatch as Unknown ``` 或者将其注册到项目的 `Cargo.toml` 中,以便普通的 `cargo dylint` 能找到它 —— 通过本地路径, 或者**直接从 git 获取而无需克隆**(dylint 会根据 candor 固定的工具链获取并构建它)。这相当于 dylint 的依赖项;dylint 只会从 `git` 或 `path` 源加载库,而不会从 crates.io,因此 candor **没有**(也无需)发布在那里。 ``` [workspace.metadata.dylint] # clone-free — 固定一个 tag/rev 以实现可复现性: libraries = [{ git = "https://github.com/tombaldwin/candor-rust", tag = "v0.3.0" }] # …或一个本地 checkout: libraries = [{ path = "/abs/path/to/candor" }] ``` ## 它检测什么 candor 回答关于代码库的两个问题: 1. **每个函数执行了哪些副作用?** —— 网络(AWS SDK, `reqwest`/`ureq`/`isahc`, 原始 `std`/`tokio` socket)、数据库 (`sqlx`/`rusqlite`/`postgres`/…)、本地 IPC (Unix socket)、 文件系统、进程生成、环境变量、时钟、随机数、日志、剪贴板 —— 包括通过它调用的函数传递继承的副作用。 2. **签名是否诚实?** —— 一旦你通过模块传递能力 token(或使用 cap-std),它就会标记任何执行了未声明副作用的函数。 它解析每次调用的 `DefId` 并对它落入的 crate/路径进行分类。这种类型解析是关键所在:单纯的 `.send()` 在语法上毫无意义,但解析出的方法告诉我们它属于 `aws_sdk_*` → 一个网络副作用。 ## 能力准则(一致性模式) 函数通过将匹配的**能力 token** 作为参数 (`&Fs`, `&Env`, …) 来声明它可以执行的副作用。Token 是不可伪造的 —— 私有字段意味着它们只能被*接收*,而不能在其定义模块之外构造 —— 并且在入口点被一次性铸造。有关模式,请参见 `sample/src/main.rs`。然后检查器会标记: - **AS-EFF-001** — 函数执行了未声明的副作用。 - **AS-EFF-002** — 函数声明了从未使用的能力。 - **AS-EFF-003** — 函数进行了 candor 无法解析的调用(动态分发、函数指针,或 通过 `impl Fn` 的回调),因此其副作用集合无法被证明是完整的,无法被认证。 - **AS-EFF-004** (`CANDOR_NO_AMBIENT`) — 函数直接获取*环境权限* (`std::fs`, `std::net`, `std::env`, `std::process`, 时钟, …) 而不是接收 能力。这是与 cap-std 对齐的、*可强制执行*的咨询 token 替代方案:它甚至会在持有 token 的函数上触发,因为持有 `&Fs` 并不能阻止你调用 `std::fs`。解决方法是通过注入的能力(例如 cap-std 句柄)来路由调用。 - **AS-EFF-005** (`CANDOR_BASELINE`) — 现有函数*获得*了在已保存快照中没有的副作用。摩擦最低的采用路径:无需 token 穿线,无需重写 —— 只需捕获导致以前纯函数开始执行网络/磁盘等 I/O 操作的 PR。(新函数不会被标记;它们作为新代码进行审查。) 增量采用:将 `CANDOR_STRICT` / `CANDOR_NO_AMBIENT` 限制在一个模块内,修复直到它报告为零,然后进入下一步。 ### 或者使用真正的能力:cap-std candor 将 [cap-std](https://github.com/bytecodealliance/cap-std) 能力*类型*识别为 声明,并将其操作识别为匹配的副作用。一个接受 `&Dir` 并通过它读取 (`dir.read_to_string(..)`) 的函数是一致的 —— 它声明的 `Fs` 与其推断出的 `Fs` 相匹配 —— 而获取环境 `std::fs` 的同级函数则会被标记。与 candor 自己的咨询 token 不同, cap-std 能力是不可伪造且在编译时强制执行的;candor 只是在顶层使副作用表面*可见*。参见 `sample-capstd/`。当前的映射:`Dir`→Fs, `Pool`/`TcpStream`→Net, `SystemClock`→Clock, `UnixStream`→Ipc。 ## CI 防护栏(最低摩擦的采用) 你不需要采用能力准则就能获得价值。最廉价的收益是回归防护:快照副作用报告,提交它,并在函数的副作用表面扩大时让 CI 失败。 ``` # 一次,在一个已知的正常 commit 上 — 然后 `git add .candor/` CANDOR_JSON=.candor/baseline cargo dylint --lib-path "$LINT" # 在 CI 中:仅在 AS-EFF-005(函数获得了 effect)时失败 — 参见 examples/candor-guard.yml out=$(CANDOR_BASELINE=.candor/baseline cargo dylint --lib-path "$LINT" 2>&1); echo "$out" echo "$out" | grep -q AS-EFF-005 && { echo "effect surface grew"; exit 1; } || true ``` 现在,如果一个 PR 突然让解析器打开 socket,或者让渲染函数开始读取文件系统,就会自动导致审查失败 —— 无需 token,无需重写。当有意添加新的副作用时,刻意地(重新运行快照命令)刷新基线。这对于人工审查者和审查 diff 的 AI Agent 同样有用。 ## 它对 Agent 的实际帮助有多大?(诚实的版本) 一项对照试点 ([EVAL.md](EVAL.md)) 让仅使用 JSON 的 Agent 和仅使用源代码的 Agent 在相同的范围界定任务上进行对抗。JSON 大约便宜 3 倍,快约 6.5 倍 —— *并且*它揭示了一个真实的教训:仅使用源代码的 Agent 在某一点上更**准确**,因为 candor 悄悄地将一些 `reqwest` HTTP 调用错误分类了(这是一个分类器漏洞,现已修复)。所以:报告便宜且真正有用, 但**只与其分类器一样正确** —— 这正是存在 `Unknown`/`unresolved` 的原因,也是为什么 Agent 应该将标记为不确定的函数视为“去阅读源代码”,而不是“相信我”的原因。 ## 未解析的调用(可靠的稳健性) candor 无法追溯到具体被调用者的调用 —— `dyn Trait` 分发、函数指针、通过泛型 `impl Fn` 参数到达的闭包 —— 可能执行*任何*副作用。candor 将这些记录为 **`Unknown`** 副作用,而不是默默地假设是纯净的。你将在审计输出和 JSON 的 `unresolved` 标志中看到 `Unknown`;在一致性模式下,它会引发 AS-EFF-003。(*不*这样做的实测代价:在一个真实的约 8k 行代码库中,22% 的函数至少进行了一次无法解析的调用。) 遗留漏洞:静态分发的**泛型** trait 调用(`t.method()`,其中 `t: T: Trait`)假定遵守其边界,而不是标记为 `Unknown` —— 否则每个 `.clone()` / `.to_string()` / 迭代器适配器都会淹没报告。参见 `CRITIQUE.md`。 ## 扩展分类器 `src/lib.rs` 中的 `classify()` 是一个精心策划的表,将 crate/路径映射到副作用。要在不重新构建的情况下识别你 自己有副作用的 crate,将 `CANDOR_CONFIG` 指向一个规则文件 —— 每行一条规则, ` `: ``` # project effect 规则 Net crate reqwest Fs path mycrate::storage:: ``` 匹配实际的 I/O 边界,而不是整个 crate —— 例如 SDK 仅匹配 `.send()`,仅匹配 `::process` 的 `Command`/`Child` —— 否则你会过度报告。 ## 已知限制 - **动态分发 / 函数指针 / 回调**无法被解析为具体的被调用者。这些被如实地呈现为 `Unknown` (→ AS-EFF-003) 而不是被默默丢弃,但 candor 仍然无法告诉你它们背后隐藏了*哪些*副作用。例外:对于传统上纯净的 std trait (`Display`, `Debug`, `Error`, `ToString`, `Clone`, …) 的 `dyn` 被视为纯函数,而不是 `Unknown` —— 否则像 `dyn Error` 格式化这样的普遍模式会导致报告中充斥着误报。 - **泛型静态分发**(`t: T: Trait` 的 `t.method()`)假定遵守其边界 —— 这是为了保持报告可读性而刻意保留的剩余不可靠性(参见 `CRITIQUE.md`)。 - **咨询性的,而非强制的**:`&Fs` token 实际上并不能限制 `std::fs`;candor 只进行报告。 要进行真正的强制执行,请使用 [cap-std](https://github.com/bytecodealliance/cap-std)。 - **宏生成的常量/静态变量被跳过**(以消除诸如 tracing 的每个日志点的 `__CALLSITE` 静态变量之类的噪音)。宏生成的*函数*(`async_trait` 方法、derive-impl 方法、声明宏 fn)**会**被分析和报告 —— 早期全面跳过确实导致了少报, 此问题已修复,并由模糊测试通道 (`macro_call` / 宏定义的接收器) 进行保障。 - **能力必须是直接参数。** `declared_caps` 仅将能力 (`&Fs`, cap-std 的 `&Dir`) 识别为顶级参数。通过结构体字段获取的能力(`fn f(ctx: &AppContext)`,其中 `ctx` 持有 `Dir`)不计入已声明 —— 尽管该函数持有能力,在严格模式下它也会被标记。 - **非局部 trait 的泛型静态分发**假定遵守其边界(CHA 只能看透*局部* trait);`CANDOR_PARANOID` 会标记其余部分,但代价是产生噪音。 - 通过宏进行的日志记录在每个函数中去重,但会计算每个记录日志的函数。 ## 文档 - **[candor-spec](https://github.com/tombaldwin/candor-spec)** — candor 实现的语言无关规范(副作用词汇、报告 schema、信任契约)。由 [JVM 引擎](https://github.com/tombaldwin/candor-java) 和仅基于规范的 [TS 引擎](https://github.com/tombaldwin/candor-ts) 共享;CI 一致性测试套件使三者保持相同的 答案。 - **[AGENTS.md](AGENTS.md)** — AI Agent 的独立说明(安装 → 运行 → 阅读)。 - **[PRINCIPLES.md](PRINCIPLES.md)** — candor(及其开发)所基于的理念。 - **[CRITIQUE.md](CRITIQUE.md)** — 诚实的、批判性的自我评估 + 与先前技术(Cackle、cap-std、Rust 副作用倡议)的比较。 - **[EVAL.md](EVAL.md)** — 关于报告是否真的能帮助 AI Agent 的对照试点。 - **[BACKLOG.md](BACKLOG.md)** — 已完成的工作、已推迟的工作,以及每个工作的具体原因。 - **[CONTRIBUTING.md](CONTRIBUTING.md)** — 构建/测试,以及如何教导分类器识别新的 crate。 - **[SECURITY.md](SECURITY.md)** — 为什么 candor *不是* 安全边界,以及如何报告漏报(最重要的 Bug 类别)。 ## 测试 `cargo test --workspace` 运行 *分类器* 精度规则上的单元测试(例如 `std::net::TcpStream` 是 `Net` 但 `std::net::SocketAddr` 不是)加上加载冒烟测试,以及 `candor-report` / `candor-query` 工具测试(报告解析/发现,查询命令)。**有状态的核心**(调用图 不动点、CHA、一致性)未进行单元测试 —— 它需要 dylint 测试工具,而该工具不支持 bless —— 因此它由 `sample/`+`sample-capstd/` crate 和一个断言真实审计输出的 CI *行为*检查覆盖(因此“candor 不输出任何内容”的回归会导致 CI 失败)。Lint 还会优雅地失败(绝不会发生 ICE)发生在类型检查主体之外的表达式上。 **可靠性契约** —— “绝不默默地纯净” —— 是其自身的门禁,而不是期望。一个对抗性的 模糊测试器 ([`soundness/`](soundness/)) 生成可编译的 crate,这些 crate 将*已知的*副作用从叶子节点通过随机调用的形式链向上传递,并断言每个可达的函数都报告了该副作用或 `Unknown`(纯净/省略的函数是它寻找的 Bug)。模糊测试通道涵盖了历史上隐藏调用的各种形式 —— 直接/闭包/泛型/装箱回调、`dyn` 和任意 self 分发、UFCS、重载运算符、`?`、`.await`、宏、隐式 `Drop`、不透明的 `impl Trait` 返回值,以及跨 crate 边界 —— 每一个都经过了*有效性验证*(恢复修复会导致通道失败)。另外两个 通道在 `strace` 下运行每个程序,并确认 candor 的静态预测过近似了 内核实际观察到的副作用 —— 这种真实情况不信任任何关于测试是如何生成的假设。 ## 状态 原型。在一个真实的约 8k 行代码库(`ebman` AWS Elastic Beanstalk TUI)上进行了验证: 审计标记了约 445 个函数;一个叶子模块被转换为能力准则,并在继续基于稳定版构建的同时达到了零一致性违规。 candor 也**保护自己**:CI 针对 `.candor/baseline` 在 candor 上运行 candor。其有副作用的表面 —— lint 中的五个函数(配置 / 基线 / 跨报告读取 + 报告写入,全部为 `Env`/`Fs`),加上 `candor-report` 的 `report_files` (`Fs`) 和构建脚本 (`Exec`/`Fs`) —— 无法在不知不觉中获得*新*的副作用。请注意防护的范围,老实说:根据 AS-EFF-005 的设计,它标记的是*现有函数中的回归*,而不是全新的函数(那些作为新代码进行审查),因此新添加的有副作用的函数不会触发它。当有意引入新的副作用时,使用 `cargo candor snapshot .candor/baseline` 刷新。 ## 许可证 根据您的选择,采用 [MIT](LICENSE-MIT) 或 [Apache-2.0](LICENSE-APACHE) 双重许可。
标签:Rust, 代码质量检查, 可视化界面, 架构边界, 网络流量审计, 通知系统, 错误基检测, 静态代码分析