关于 SayDo
SayDo 检查工具是否按声明运行,并为每次运行生成可验证凭据。它面向工具发布者和评估者,提供符合性证据,并区分失败与未能证明任何行为的运行。
主要功能
- 01依据签名声明测试工具
- 02在本地或沙箱中运行工具
- 03将结果标记为通过、失败或未覆盖
- 04生成哈希链式签名凭据
- 05检测不同运行间的行为变化
- 06在独立浏览器页面验证凭据
适用场景
- 01以工具符合性检查作为 CI 构建门槛
- 02使用或不使用自有声明检查 MCP 服务
- 03评估外发数据是否取决于工具输入
- 04无需信任凭据签发者即可审查凭据
MCP 相关
SayDo 可测试 MCP 服务,包括从 npm 和 PyPI 发现的服务。它依据声明运行工具,并将运行所能证明的内容记录为可验证凭据。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
4收录的工具列表 · 更新于 3 周前
此工具无需参数。
详细介绍
╔════════════════════════════════════════════════════════════════════════════════════════════╗
║ ║
║ ███████╗ █████╗ ██╗ ██╗██████╗ ██████╗ ║
║ ██╔════╝██╔══██╗╚██╗ ██╔╝██╔══██╗██╔═══██╗ ║
║ ███████╗███████║ ╚████╔╝ ██║ ██║██║ ██║ ║
║ ╚════██║██╔══██║ ╚██╔╝ ██║ ██║██║ ██║ ║
║ ███████║██║ ██║ ██║ ██████╔╝╚██████╔╝ ║
║ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ║
║ ║
║ does the tool do what it says ║
║ ║
╚════════════════════════════════════════════════════════════════════════════════════════════╝
草稿内容——待所有者审核。 以下内容均为测量结果或机械事实,不包含任何营销宣传,但措辞尚未获批。
发布者在一份经过签名、机器可读的契约中,明确声明工具的具体功能。测试框架负责运行工具,并根据契约对其进行检查。每次运行都会生成一份哈希链式收据,任何人都可以在浏览器中验证,无需账户,也无需信任签发者。
它要回答的问题刻意限定得很窄:这个工具是否按其所述行事? 不是它是否安全,也不是它是否出色。一个拒绝执行操作的工具并不能因此被证明是安全的——它什么都没被证明,而 SayDo 会如实说明这一点,而不是给出绿色对勾。
可能的判定结果
pass 本次运行中,声明的行为得到了验证
fail 工具做了它声明不会做的事
not-covered 本次运行无法得出任何结论
not-covered 绝不会悄悄升级为 pass。这个领域里,大多数值得关注的失败都源于把沉默误当成良好行为。
而一次完整运行的结果有四种,而非两种:
CONFORMANT 工具按其所述行事,且有证据证明
NOT CONFORMANT 工具做了它声明不会做的事
INCONCLUSIVE 未发现失败,但也未能证实任何内容
(已撤销 / 已过期,等注册表发出判定后)
INCONCLUSIVE 承担了大部分判定责任。一个拒绝所有调用的服务——公开扫描中最常见的情况——不会发出网络请求、写入文件或启动进程,因此,简单的测试框架会将这三项全部标记为通过,并为一个什么都没做的程序颁发合规证明。SayDo 要求在这些检查中的任何一项通过之前,运行必须展示出实际行为;服务对自身的描述不算行为。
在 CI 中使用
- uses: vince-gonzalez/saydo@main
with:
command: python -m my_server
declaration: saydo.declaration.json
如果工具违反其声明,或者运行未能证实任何内容,构建都会失败——因为一个无法证明任何事实的绿色检查,比没有检查还糟糕;它会宣称符合要求,却没有人真正作出过这种判定。设置 require-coverage: false 可允许此类构建通过;收据仍会将结果记录为 inconclusive,绝不会将其记为合规证明。
在这里,工具的价值最大。覆盖情况取决于工具是否实际执行其工作,而这需要它接受的凭据和输入——这些东西存在于发布者自己的 CI 中,几乎不会出现在其他地方。
在任何工具上运行
saydo 会对单个工具进行测试,并生成一份收据。工具不必属于你,也不必是 F-Keys 的工具:
python cli/saydo.py verify --npm @modelcontextprotocol/server-memory --sandbox
python cli/saydo.py verify --pypi mcp-server-time --sandbox
python cli/saydo.py verify --command "python my_server.py" --declaration mine.json
使用 --declaration 时,测试对象是作者自己的契约。不提供该参数时,系统会推断一份刻意保守的契约,大多数不变量都会得到 not-covered——对于一个没有凭据、只能试探陌生服务的人来说,这才是诚实的结果。
--sandbox 会在容器中运行服务,容器唯一的外部通道是一个记录流量的代理(Linux 和 Docker;隔离能力已在 CI 中得到验证,tools/SANDBOX.md 明确说明其覆盖范围和局限)。不使用该参数时,运行只会被观测,而不会受到强制约束,收据会标记为 observed,因此任何声明都不会超出生成该声明的机制所能证明的范围。
然后自行检查结果:
python cli/saydo.py status certivl # Agent 读取的一次性判定结果
python cli/saydo.py selfcheck # 证明测试框架能够判定失败
打开 verifier/index.html,粘贴收据和锚点,页面便会在你的浏览器中重新计算完整的链和签名。不会有任何请求离开浏览器。
四层
1 声明 发布者承诺的内容,已签名 (spec/)
2 执行环境 让工具运行并进行监控 (tools/harness.py)
3 回执 哈希链式、已签名、可验证 (tools/receipt.py)
4 注册表 当前仍然有效的内容及其有效期 (tools/registry.py)
第 4 层之所以存在,是因为回执只记录某一时刻的事实,单独看它会回答错误的问题。选择工具的人不会想知道它在 8 月的表现。因此,声明会在读取时过期,这样就不会仅仅因为没人重新运行测试,就一直显示为绿色;撤销也具有持续效力:在某项已撤回的声明上发布更新的通过回执,只会记录这次尝试,不会解除撤回。
这里有两项不属于惯常做法
反事实测试。 观察工具发出网络请求,只能证明它发出了请求,无法说明你的数据是否离开了。因此,工具会使用不同的输入运行两次,并根据各个目的地收到的内容是否随输入变化来分类:input-dependent 表示负载携带了你提供的内容,input-independent 表示没有,无法检查的正文则报告为 unexamined,而不是判定为干净。它把你的输入发送到了 example.com 与 它发出了请求 是两种不同的说法,只有第一种对做决定的人有意义。
seeded/leakserver.py 展示了这一点;它之所以存在,是因为此处曾提出这种说法,却没有进行测试。两个工具每次调用都会联系同一主机。submit 会提交收到的文本;ping 则会提交固定正文:
FAIL network.none egress: ping->example.com; submit->example.com
FAIL data.stays-put 工具将自己的输入数据发送了出去:
submit -> example.com。通过在运行之间更改
输入确认:example.com 是 input-dependent
network.none 能看到两者,却无法区分它们;观察流量只能做到这一点。data.stays-put 只指出了 submit。
让这项能力成为现实的修正。 在 2026-08-31 之前,标记只写入环境变量 SAYDO_CANARY,唯一曾将它发送出去的测试夹具也从那里读取——这正是已有方案采用的机制,并非另一种机制。没有任何东西把标记放进工具参数中,因此代理在 279 个第三方服务中搜索一个从未出现在任何输入里的字符串,结果 279 个全部都是 unexamined。现在,标记会被放入每次运行收到的参数中,每次运行一个;这才是上面那段文字一直声称的做法。
漂移。 在版本号未变的情况下重新定义工具,在任何单份回执中看起来都完全正常;只有比较两次运行,才能发现这种欺骗。因此,回执会与前一份回执组成链;如果定义发生变化但版本号没有变化,就会被评为 serious,而不是仅作记录。
这项做法并不罕见;在认真了解业界情况之前,这里曾把它说成罕见——详见下文的相关工作。askalf/truecopy 会通过内容哈希固定经过审查的工具定义,并在字节发生变化时让 CI 运行失败;这换个名字就是漂移检测,而且它更早就已发布。与最初看起来相比,这里的独特主张范围更窄:上述反事实测试,以及依据作者编写的契约测试行为,而不只是检查字节是否与经过审查的版本一致。
布局
spec/ 声明模式及其字段语义
tools/harness.py 一致性测试工具 (tools/HARNESS.md)
tools/runner.py 本地与隔离执行 (tools/SANDBOX.md)
tools/egress_proxy.py 记录代理、允许列表强制执行、TLS 检查
tools/canary.py 标记生成与正文检查
tools/differential.py 双运行反事实分类器
tools/drift.py 自上次回执以来的变化
tools/registry.py 过期与永久撤销
tools/status.py 回执 -> Agent 读取的精简对象
tools/declare.py 根据观测到的运行起草声明
tools/discover.py 在 npm 和 PyPI 上查找 MCP 服务
tools/sweep_scale.py 分批测量其中许多服务
action.yml 将 SayDo 作为 GitHub Action
verifier/index.html 自包含的浏览器验证器,无需网络
seeded/malserver.py 一个专门说谎的服务,用于测试测试工具
seeded/silentserver.py 一个拒绝所有请求的服务,用于测试测试工具能否将
沉默报告为干净
验证验证器
selfcheck 会运行一个植入种子的服务,该服务会故意违背自己的六项承诺。如果测试工具报告它符合要求,或将发现归因于错误的不变量,那么测试工具就有问题,它曾生成的任何回执都毫无价值。声明验证器也会以同样的方式进行检查,测试它必须拒绝的五种变更。
另一个测试样例恰恰相反,也同样重要:它会启动、列出自己的工具,并拒绝每次调用。测试工具必须将其报告为 INCONCLUSIVE。当它报告为 CONFORMANT 时——它确实如此,而且回执也已签名——这个标记毫无意义,因为大多数公开 MCP 服务在陌生人调用它们时都会这样做。
CI 会在库和 Action 上都断言这两种情况:遇到说谎的工具时,构建必须失败;遇到没有任何结果的运行时,构建也必须失败。
如果一项检查无法因你所关心的原因而失败,那它就不算检查。
本项目借鉴的先行成果
- TBOM v1.0.2,Jason M. Lovell,2026 — 10.5281/zenodo.18459260
- CTMS 1.0,George Kanellopoulos,2026 — github.com/gkanellopoulos/ctms
两者都是来源证明:它们确立工具元数据就是发布者发布的内容,也都声明根据该元数据验证行为不在其范围之内。本仓库正是为了填补这一范围之外的空缺。SayDo 声明通过 TBOM 现有的 attestations[] 附加,并复用其 ToolDigest 格式,因此这里的内容不会分叉出它们中的任何一个。
相关工作,以及界限在哪里
这一节不断变长,这正是它有用的地方。这里的某项内容被描述为不寻常之后,我们才发现了下面这些项目,因此,这份列表也记录了那些不得不收窄的说法。
- Pipelock,github.com/luckyPipewrench/pipelock — 一种 Agent 防火墙,可调解运行中 Agent 的流量,并生成签名回执。
- truecopy,github.com/askalf/truecopy — 审查工具定义,使用内容哈希固定其版本,并可选用 Ed25519 签名;字节发生变化时会让 CI 失败。与 redstamp 搭配用于运行时遏制。
- Proofpane,github.com/Proofpane/releases — 一种治理代理,会将每次工具调用记录到哈希链审计日志中,并导出为由 Ed25519 签名、可离线验证的证据包。闭源;该仓库是下载镜像。
- dcl-webhook,github.com/Fronesis-Labs/dcl-webhook — 将策略判定写入防篡改的 SHA-256 哈希链。
界限不在于密码学。哈希链、签名且可离线验证的记录如今已是常见设计,本仓库并未声称这是自己的独创。
界限在于记录的内容。Pipelock 和 Proofpane 为已部署 Agent 执行过的操作提供证明。truecopy 证明工具与你审查过的字节完全相同——这属于完整性,可发现静默更新,但无法说明一个始终行为不当的工具。SayDo 回执则证明某个工具的某个版本确实按其作者所说的那样工作;这是通过运行该工具,并依据运行前编写的契约进行测试得出的。
完整性问的是这是不是我批准过的东西? 符合性问的是这东西是否按其声称的那样工作? 两者都值得拥有,且都无法回答对方的问题。
根据目前收集到的证据,似乎仍未见他人采用的做法是:通过两次运行的反事实比较,区分你的数据已离开与发生了一次请求;以及在工具仅仅拒绝执行操作时,不把它判定为干净。如果其中任何一项已有先例,就应将其列入此列表,并删去上面的说法,而不是为其辩护。
这里曾有一周把第一项说得过头了。当时给出的与 Pipelock 的区别是他们的金丝雀是环境变量,我们的则是工具自身的输入——而我们的也确实是环境变量。截至 2026-08-31,它已是工具的输入,并附有一个展示该论点的测试样例。让这句话保持准确的是这个测试样例,而不是本段文字。
状态
公开的工作仓库。此处生成的回执使用由 F-Keys Creative LLC 持有的概念验证密钥签名;生产环境的签名方案尚未确定。语料库测量结果仍是草案,等待所有者审核,其中的每项数据都是下限——服务器是在无凭据的情况下运行的,因此有凭据的服务器可能会做出记录之外的操作。
许可:开放层——声明模式和规范文本(spec/)、参考工具(tools/)、浏览器验证器(verifier/)以及带种子的测试样例(seeded/)——采用 Apache-2.0 许可(参见 LICENSE、NOTICE)。本仓库不会为签名的公开制品授予任何许可。
╔════════════════════════════════════════════════════════════╗
║ ║
║ ███████╗ ██╗ ██╗███████╗██╗ ██╗███████╗ ║
║ ██╔════╝ ██║ ██╔╝██╔════╝╚██╗ ██╔╝██╔════╝ ║
║ █████╗ █████╗█████╔╝ █████╗ ╚████╔╝ ███████╗ ║
║ ██╔══╝ ╚════╝██╔═██╗ ██╔══╝ ╚██╔╝ ╚════██║ ║
║ ██║ ██║ ██╗███████╗ ██║ ███████║ ║
║ ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚══════╝ ║
║ ║
║ · C R E A T I V E · ║
║ ║
║ ──────────────────────────────────────── ║
║ ║
║ Vincent Gonzalez ║
║ f-keys.com ║
║ ORCID 0009-0005-3640-014X ║
║ ║
╚════════════════════════════════════════════════════════════╝
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。