A JSON schema Is not an AI governance layer
TL;DR · AI 摘要
JSON schema无法替代代码层的AI治理,治理决策应由应用中的布尔函数直接控制,而非依赖政策或协议规范。
核心要点
- 60%开发者通过代码布尔函数阻止代理系统操作,而非依赖政策
- JSON schema验证可能遗漏重复标识符等关键治理漏洞
- 治理应发生在代码层,而非实验室或监管机构
结构提纲
按章节快速跳转。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- JSON schema与AI治理
- 核心问题
- 治理层错位
- 验证漏洞
- 案例分析
- PIC协议bug
- Stack Overflow调查
- 解决方案
- 代码层布尔控制
- 协议规范强化
金句 / Highlights
值得收藏与分享的关键句。
治理结束于布尔值:60%开发者通过代码布尔函数阻止系统变更
JSON schema验证未能发现重复标识符导致的治理漏洞
OpenAI等公司投入20亿美元用于嵌入式评估,但未解决代码层验证问题
JSON schema 并非 AI 治理层 - Stack Overflow
2026年10月28日
JSON schema 并非 AI 治理层
AI 治理的争论发生在实际阻止你的代理运行的代码层之上。
[
今年九月短短两周内,关于谁应该治理AI的争论产生了四个不同答案,所有答案都来自构建这些系统的工程师。
Demis Hassabis 提议成立一个由行业资助的监督机构,该机构以FINRA为蓝本,负责在部署前沿模型前进行测试。Dario Amodei 呼吁实验室减缓前沿技术开发,并给予独立评估者真实访问权限,几天后Anthropic和Accenture承诺在未来五年内投入至少20亿美元用于嵌入式评估。OpenAI 开始发布关于模型异常行为的报告。据《华尔街日报》报道,Mark Zuckerberg、Elon Musk和Jensen Huang分别与特朗普总统进行了交谈,并说服他放弃设立监督机构,他们认为统一监管机构会保护最大实验室的领先地位,且企业可以在没有监管机构的情况下自行保障系统安全。
无论你支持上述哪种观点,这些讨论都没有涉及决定你应用程序中代理是否被允许发送邮件、轮换密钥、合并分支或转移资金的代码。
这个决定不是由实验室做出的,也没有写在政策中,而是由你代码中的某个函数决定的,该函数返回true或false。
我想向你展示这个函数中的一个漏洞,这不是理论推演:在PIC协议中,一个独立的压力测试发现了这个问题。PIC是一个我维护的开放协议,用于在执行前检查代理操作。该模式接受大写的SHA-256摘要,参考运行时静默地将其规范化并继续处理,而规范文件从未说明这种表示方式是否有效。没有加密技术失败,协议本身只是存在多个可辩护的含义,这种问题比加密技术漏洞更难发现,我怀疑今天运行的许多代理部署都存在同样的问题。
治理最终归结为布尔值
Stack Overflow 2026年4月对1100名开发者和技术人员的脉冲调查显示,60%的受访者会阻止代理进行未经批准的系统更改。这很合理,但忽略了困难的部分:什么才算批准?路径上的每个组件是否都会对答案达成一致?
这是一个提出付款请求的代理。它提供了想要调用的工具、影响决策的输入,以及支持付款已获授权的证据。
{
"action": {"tool": "payments.transfer", "args": {"amount": 500}},
"impact": "money",
"provenance": [
{"id": "invoice", "trust": "trusted"},
{"id": "invoice", "trust": "untrusted"}
],
"claims": [
{"text": "The invoice was approved", "evidence": ["invoice"]}
]
}假设策略很简单:只有当支付所依据的证据来自可信来源时,支付才能执行。(目前先将负载中的信任标签视为真实值;稍后我会讨论谁有权设置这些标签。)为了应用该策略,验证器需要确定“发票”指的是哪个条目,而两位合理的工程师可能会得出不同的结论。
# 实现方案 A,Python:找到第一个匹配项即停止
def resolve(provenance, evidence_id):
for entry in provenance:
if entry["id"] == evidence_id:
return entry
return None// 实现方案 B,TypeScript:按 ID 索引,后写覆盖
const index = Object.fromEntries(provenance.map(e => [e.id, e]));
const resolve = (id) => index[id];单独来看,这两种查找方式都不是错误,它们都是正常且符合习惯的代码,而规范从未说明哪个条目应该优先。根据上述负载,方案 A 将证据解析为可信条目并执行支付,而方案 B 解析为不可信条目并阻止支付。两种验证器使用相同的协议版本、相同的策略和相同的信任密钥,唯一的区别是它们查找 ID 的方式。因此,相同的字节经过相同的模式验证后却得出不同结果,攻击者只需知道调用另一端的服务即可利用这一差异。
模式完成了它的职责,但这一职责是有限的。它检查负载的结构:字段是否存在、类型是否正确、值是否符合允许的范围。它甚至可以表达字段之间的某些规则,例如“如果存在此字段,则必须存在另一个字段”。但它无法表达该负载所需的规则:在声明中作为证据引用的“发票”必须指向 provenance 中的唯一一个条目。标准 JSON Schema 没有办法表达文档中某处的值必须与另一处的唯一值匹配;uniqueItems 也无法提供帮助,因为这两个条目具有不同的信任值,因此模式将它们视为两个不同的项目。支付是否通过取决于这一链接,而模式将这一问题留给了每个验证器自行处理。
这是解析器差异,其历史比代理更悠久
安全领域的人士多次遇到这种情况:两个组件以两种不同方式解析相同输入,它们之间的差异正是漏洞所在。HTTP 参数污染就是一个例子,其中代理读取第一个 ID,而应用程序读取最后一个。XML 签名包装是另一个例子:验证器检查一个元素的签名,而解析器读取另一个元素。还有 JWT 算法混淆,其中令牌本身指定了用于验证它的算法。以及 HTTP 请求走私,其中代理和后端服务器对请求结束和下一个请求开始的位置存在分歧。
这类漏洞已经存在了大约二十年。我们从中学到的很简单:当两个实现需要就相同字节的含义达成一致时,规范中留下的任何细节都可能被解读为两种方式,而攻击者迟早会利用这一点。
协议规范现在也遇到了同样的问题,PIC 也不例外:我提到的压力测试发现了这些漏洞,我们通过收紧规范并添加每个案例到下面描述的符合性测试中解决了这些问题。这些协议大多只有一两年历史,尚未像多年来对令牌和签名格式那样经过充分测试;因此我认为,今年编写的新治理层应像对待新的令牌格式一样,以同样的严谨态度进行验证。
有益的标准化悄然修改了协议
开发者经常对输入进行标准化处理。我们修剪空格、将标识符小写化、修复 Base64 填充、接受替代编码,因为拒绝那些明显可恢复的数据会显得敌对。这种直觉在搜索框中是正确的,但在授权边界处却非常危险。
假设某项策略只允许一个工具:
expected_tool = "payments.transfer"
proposed_tool = "payments.transfer " # 注意末尾的空格如果精确比较这两个字符串,提议将失败。但如果先对两者调用 strip() 方法,提议就会通过。单独来看这两种行为都不荒谬;问题在于具体实现可以自行选择。
对于签名而言,问题看起来比实际更严重。SHA-256 摘要通常以十六进制表示,大写字母 A-F 与小写字母 a-f 表示相同的数字,因此将输入小写化似乎无害。但事实并非如此,因为签名覆盖的是字节而非数字。如果生产者对包含大写形式的有效载荷进行签名,而验证者在构建规范有效载荷前先小写化,验证将失败于一个真实的提议。
反过来情况更糟:验证者先检查原始字符串上的签名,然后在应用策略前对值进行标准化,此时就不再强制执行签名中明确规定的规则。对于十六进制摘要,小写化不会改变数值,因此没有变化;但将这种习惯应用于其他字段时,可能改变匹配的策略规则(例如,修剪或小写化的工具名称可能匹配签名名称无法匹配的白名单条目),此时策略将基于签名者从未签署过的值执行。
Base64 也存在同样的问题:一些库容忍嵌入的空格,一些库恢复缺失的填充,一些库静默转换 URL 安全字母表,而另一些库则拒绝所有这三种情况。当签名决定某个操作是否可以执行时,你的 Base64 库做出的这些选择将成为授权策略的一部分,尽管团队中没有人编写或审查过这些选择。
在这里需要区分两个决策。第一个决策是将签名有效载荷转换为字节的方式。JSON 的 RFC 8785 标准、之前的 XML c14n 以及 COSE 中的确定性编码规则等标准,通过固定字段顺序、数字格式和转义方式等,确保每个实现都对相同的字节进行签名;但它们保留字符串内容不变,因此 RFC 8785 会保持大写摘要为大写。第二个决策是协议接受哪些值。在这里你可以选择标准化处理(例如,在签名和验证前将所有摘要小写化),或者接受单一形式并拒绝其他所有形式;因此大写十六进制会失败,未填充的 Base64 会失败,带有尾随空格的工具名称也会失败。
PIC 同时支持这两种方式。在它的规范证明模式下,它会签署规范序列化(它仍然保留着一种验证原始负载字节的遗留模式),而对于值,它选择了第二种路径,原因很简单:规范化需要为使用该协议的每种语言重新编写一遍代码,而 c14n 错误的历史表明,相同规则的不同实现最终会产生分歧,就像解析器一样。通过严格拒绝,格式错误的负载会在到达时立即被拒绝,并给出明确的错误信息,而不是通过验证后在后续导致两个验证器产生分歧,此时问题会更难追踪。如果你正在设计类似系统,请选择你能在规范中完全描述的方案;但一定要选择一个,否则已签署提案的含义将取决于恰好解码它的库。
权限请求方无法提供信任
最关键的歧义并非来自编码本身,而是关于谁有权声明输入是可信的。
{
"id": "invoice",
"source": "email_attachment",
"trust": "trusted"
}该字段易于序列化,但单独来看,它几乎无法向验证器提供任何可依赖的信息。提案通常在验证器上游生成,经常由请求执行权限的代理方本身生成。按字面意义尊重该字段会让请求方提供自己的权威,这正是经典的“混淆副手”问题,只不过用 JSON 表达了出来。
信任必须在执行边界内推导。生产者可以标识来源并附加证据,而验证器决定这些证据是否能追溯到操作员实际信任的东西,例如已知的签名密钥。
effective_trust = "untrusted"
if signature_is_valid(evidence, operator_keyring):
effective_trust = "trusted"
if proposal.impact == "money" and effective_trust != "trusted":
return BLOCK完整性(数据未被篡改)与权威性(确实有被授权的人批准)之间的区别在这里最为关键。匹配的哈希值可以证明验证器收到了预期的字节,但无法说明这些字节的生产者是谁、谁批准了它们,或者它们是否应该授权付款。攻击者可以提供一个文件并附上该文件的正确哈希值;完整性检查会通过,因为没有东西被修改,但没有任何有权限的人批准了任何内容。
只有当验证器在操作员信任的密钥列表(提案无法添加到该列表)中查找签名密钥时,签名才具有权威性;而且只有当签名涵盖关键字段时才有效:工具、参数、影响类别、相关来源和过期时间。否则,真实的签名可能批准一件事,而实际操作却是另一件事;例如,一个用于转账 500 的签名可能最终被附加到请求 50,000 的请求上。
换句话说,只有在检查证据后,验证器才会将输入标记为可信,且绝不应从代理方获取该标签。
这在你正在运行的堆栈中如何落地
三层架构,而非单一层次(模式验证结构,而非权威性)
JSON Schema 描述 JSON 数据的结构,擅长处理必填属性、类型、枚举值、模式匹配和组合规则。这些控制仍然重要,因为尽早拒绝格式错误的输入可以减少语义层需要处理的场景。
但决定代理是否应采取行动的问题通常涉及多个字段,有时甚至涉及多个系统。证据标识符是否唯一对应一个来源条目?签名是否覆盖了当前提议的相同工具和参数?验证时使用的签名密钥是否可信,而不仅仅是在生成时可信?当多个检查同时失败时,哪个错误具有优先级?
如果将所有这些内容强制放入一个模式中,得到的文档看似精确,但仍然无法说明运行时应该如何行为。更稳健的设计应将验证分为三个层次,每一层都限制下一层可见的内容范围:
- 模式验证:拒绝结构错误和不符合协议的传输表示。
- 语义验证:解析引用、验证绑定并推导有效信任。
- 策略评估:决定已验证的提议是否可以立即执行此操作。
每一层都需要独立的代码和测试,文档中的说明无法替代这些实现。
符合性向量是可执行的规范
书面规范无法涵盖作者未预料到的所有解释。另一个实现通常在几天内就能发现这些漏洞,因为另一种语言、另一个库和另一个开发者会做出不同的合理选择。
实际的解决方案是一组可运行的符合性测试。每个向量包含精确的输入、预期的判断结果以及提案必须失败的稳定错误代码。所有实现都运行相同的向量,测试套件刻意覆盖普通应用程序测试无法触及的场景。
| 输入 | 判断 | 原因 | |------|------|------| | 重复的来源标识符 | 拒绝 | 证据绑定存在歧义 | | 小写 SHA-256 摘要 | 继续 | 传输表示符合规范 | | 自声称可信的来源 | 验证器未推导出信任 | | | 大写 SHA-256 摘要 | 表示不符合协议 | | | URL 安全或无填充 Base64 | 签名编码不符合规范 | | | 工具标识符中的空格 | 工具绑定要求精确匹配 | |
这些向量的作用不仅限于捕捉回归问题。它们还定义了协议在人们可能产生分歧的场景中的行为方式。一旦某个模棱两可的情况得到解决,它就会成为一项永久性测试,而不是留给未来实现者自行解释的又一段文字。
错误报告的顺序也应以相同方式确定。一个提案可能同时包含重复的标识符、错误的工具名称和无效的签名。如果一个实现先报告重复标识符,另一个实现先报告错误签名,操作者将根据调用的服务不同而获得针对相同输入的不同建议;最终每个适配器都会形成自己的顺序。通过决定哪个检查优先执行,并将该规则写入一致性测试,可以使行为变得可预测且更容易调试。
安全默认值必须适用于所有适配器
验证器本身即使严格,也可能被其周围代码推翻。例如,CLI启用了严格检查但HTTP桥接器没有;一个框架适配器在主验证器变更数月后仍保留着旧的宽松默认值;或调用者为节省时间关闭了证据检查,继续接受声称受信任的输入,却未意识到这两者结合时存在安全隐患。
因此,所有进入验证器的入口(CLI、HTTP桥接器、每个框架适配器)都必须从相同的严格设置开始。当协议变更时,可能仍需要暂时接受旧字段或旧的宽松行为,以便基于旧版本构建系统的用户有时间迁移。这种做法是合理的,但旧行为仅应在有人明确启用时才适用,软件在使用时应显示明确警告。如果迁移标志可以在不通知任何人的情况下恢复旧行为,那么部署的安全性将取决于其配置方式,而这种方式很少被明确记录。
为何政策之争会使问题变得更糟而非更好
今年早些时候我曾指出,代理暴露了组织实际上从未明确编写的安全部署检查,因为这些检查一直存在于人工判断中。将它们写下来只是第一步:替代人工判断的代码在所有运行环境中必须含义一致。
这正是四个立场中其中一个在此处比其他立场更重要的原因。主张每家公司应自行保障其AI安全,而不由共同机构定义"安全"含义的观点,实际上描述了多个独立团队在规范化、信任推导和错误优先级方面各自做出独立合理决策的场景。公司之间的分歧与解析器之间的分歧并不相同,但实践中会导致类似情况:不同实现对同一问题给出不同答案。
这种担忧本身是合理的;由最大实验室设立的测试机构确实可能帮助这些实验室保持领先优势。但如果顶层无人就共享规则达成一致,这些规则就必须由编写验证器的人在公开且所有实现都能运行的一致性向量中达成共识。实际上,大量工作将落在开发者身上。
验证器中治理变得具体化
治理在验证器中成为现实
策略、模式、签名和人工审批都有其作用,但它们本身都无法阻止代理执行,直到某些代码在工具运行前将它们转化为明确的“是”或“否”。这些代码无需预测高级模型可能执行的所有操作,它的工作清单要简短得多:拒绝模糊的标识符、严格按照规定比较安全敏感值、保持完整性与权威性分离、仅根据验证者自身可验证的证据做出信任判断,并且对于相同的提案和策略,在协议的所有实现中都应返回一致的判定结果。
如果这些规则的任何一部分都交由各实现自行处理,那么一个服务可能允许某个提案,而另一个遵循相同策略的服务却会阻止该提案。
关于人工智能发展的速度以及谁来监管的公共争论固然重要,但对你自己的代码而言,问题要简单得多:当前控制代理的系统是否能够就它们正在读取的内容达成一致?你可以通过将相同的测试输入传递给每个系统并比较输出结果来验证这一点。
这些文章采用知识共享署名-相同方式共享4.0国际许可证授权。
creativecommons.org/licenses/by-sa/4.0/deed.en