互操作矩阵:跨四个 SDK 的运行中代理¶
本文是英文版的参考译文,更新于 25/10/2026。如有出入,以英文版为准。标识符、命令和代码块保持原文,不作翻译。
run.mjs 为每个 SDK 启动一个代理进程(TypeScript、Python、Go、Rust),并让它们
相互协作:基于一个 SDK 构建的代理创建其身份、声明意图、执行操作并
签名其公民身份 bundle;基于另一个 SDK 构建的代理会像在信任对方之前那样
验证它们。这里的验证是指 bundle 内部一致,并由它自己列出的密钥签名;它不是
对对方的信任、准入或执行操作的授权(场景 2 涵盖准入,而准入同样不授权
任何操作)。这些进程除了编排器转发的消息外不共享任何东西,
因此每一对(四个 SDK 共 16 对,包括每个 SDK 与自身)都是真正的互操作,而不是
文件比较。第五个只做验证的代理 ref 运行参考验证器
lib/verify.js(即 node bin/dcp.js verify-bundle 和验证服务器所运行的):它在 @dcp-ai/sdk 验证器
(在 2.2.0 发布到 npm 之前为工作区 SDK)之前,根据 schemas/v2 校验每个
工件,而 SDK 验证器不做这一步。
编排器也是中间的网络:它在传输中篡改 bundle,每个 验证器都必须以同样的方式拒绝每个被篡改的 bundle。任何意外的判定、崩溃或超时 都会使运行失败。
场景 1(run.mjs)在 SDK 之间演练 DCP-01(身份)、DCP-02(意图和策略)和 DCP-03(审计)的部分内容;
它不是这些规范的完整一致性测试。
场景 2(admission.mjs)涵盖四个 SDK 在 strict-v1 配置文件
(spec/profiles/admission)下的准入。DCP-04 的 A2A 握手需要先完成 SDK 方面的工作
(见末尾的路线图)。
运行¶
(cd sdks/typescript && npm install && npm run build) # agents/ts imports sdks/typescript/dist
npm install # agents/ref: the reference verifier, on that SDK
pip install -e sdks/python # dependencies of agents/py
node tools/interop-matrix/run.mjs # all four SDKs and the reference verifier
node tools/interop-matrix/run.mjs --agents ts,py # a subset
node tools/interop-matrix/admission.mjs # scenario 2, the four SDKs, with its negative control
Go 和稳定版 Rust 工具链必须在 PATH 上;run.mjs 会构建 agents/go 和 agents/rust。
选项:--agents a,b(ts、py、go、rust、ref 中的任意几个)、--python PATH(默认 $INTEROP_PYTHON 或 python3)、--timeout MS
(每个请求,默认 60000)、--out DIR(默认 tools/interop-matrix/out,报告
以及每个产生意外判定的 bundle 都写在这里)。admission.mjs 接受相同的
选项,ref 除外(默认 --out tools/interop-matrix/out/admission),另外还有
--skip-negative-control、--negative-control、--check-negative-control 和 --replay FILE
(见下文)。
代理协议¶
代理从 stdin 每行读取一个 JSON 请求,并按顺序为每个请求向 stdout 写入恰好一行 JSON 响应;其他任何内容都写到 stderr。
{"id": 1, "cmd": "identity", "args": {"owner_name": "Interop Owner", "jurisdiction": "CL"}}
{"id": 1, "ok": true, "result": {"agent_id": "agent:…", "human_id": "rpr:…", "session_nonce": "…"}}
{"id": 2, "ok": false, "error": "message"}
代理在进程存续期间把自己的状态(密钥、身份、意图、审计轨迹)保存在内存中。
密钥从不离开它。下文中契约规定的每个值都必须严格按所写内容生成;
其他一切都由 SDK 自行决定,而这正是矩阵所测试的(SDK 自行添加的
可选字段,例如 Go 的 SHA3 audit_merkle_root_secondary,只要所有验证器都接受
就没有问题)。
identity {owner_name, jurisdiction} → {agent_id, human_id, session_nonce, principal_keys, agent_keys}¶
生成两个复合密钥对(Ed25519 + ML-DSA-65):一个给人类委托人,一个给
代理;一个新的会话 nonce(64 个十六进制字符);human_id = rpr:<uuid4>,agent_id = agent:<uuid4>。
密钥条目:{kid, alg, public_key_b64, created_at: <now RFC 3339>, expires_at: null, status: "active"}。
- RPR(
schemas/v2/responsible_principal_record.schema.json):dcp_version "2.0"、human_id、session_nonce、legal_name=owner_name、entity_type "natural_person"、jurisdiction、liability_mode "owner_responsible"、override_rights true、issued_at为当前时间、expires_at null、contact null、binding_keys= 两个人类密钥条目。在ResponsiblePrincipal上下文下由人类密钥 复合签名(pq_over_classical)的SignedPayload。 - 护照(
agent_passport.schema.json):dcp_version "2.0"、agent_id、session_nonce、keys= 两个代理密钥条目、principal_binding_reference=human_id、capabilities ["api_call", "payments"]、risk_tier "medium"、created_at为当前时间、status "active"。在AgentPassport下由代理密钥复合签名。
principal_keys 和 agent_keys 是公开的那一半,[{kid, alg, public_key_b64}],经典密钥
在前。场景 2 以带外方式根据它们登记委托人。
第二次 identity 会替换第一次,连同与之相关的意图和轨迹。
intent {action_type, channel, data_classes, estimated_impact, risk_score, applied_policy_hash} → {intent_id, decision, security_tier}¶
- 意图(
intent.schema.json):dcp_version "2.0"、intent_id=intent:<uuid4>、session_nonce、agent_id、human_id、timestamp为当前时间、action_type、target {channel, to: null}、data_classes、estimated_impact、requires_consent= (estimated_impact是否为"high")、security_tier= SDK 针对此意图和risk_score的安全等级计算结果(DCP-AI v2.0 §14.2)。 - 策略决定(
policy_decision.schema.json):dcp_version "2.0"、intent_id、session_nonce、decision=risk_score< 500 时为approve,< 800 时为escalate,否则为block;risk_score;reasons ["interop-matrix: risk_score <n>"];required_confirmation在 approve 时为null,否则为{"type": "human_approve", "fields": ["action_type"]};applied_policy_hash取自参数;timestamp为当前时间;resolved_tier= 意图的security_tier。
两者都由代理密钥复合签名(Intent、PolicyDecision 上下文)。
act {intent_id, outcome, tool, result_ref} → {audit_id, count}¶
向该意图的轨迹追加一个审计条目(audit_entry.schema.json):dcp_version "2.0"、
audit_id = audit:<uuid4>、session_nonce、prev_hash = 轨迹的第一个条目为 "GENESIS",
否则为 "sha256:" + hex(sha256(canonical(previous entry)));hash_alg "sha256";
timestamp 为当前时间;agent_id;human_id;intent_id;
intent_hash = "sha256:" + hex(sha256(canonical(intent payload)));
policy_decision = 根据决定为 approved / escalated / blocked;outcome;
evidence {tool, result_ref, evidence_hash: "sha256:" + hex(sha256(utf8(result_ref)))};
pq_checkpoint_ref null。count 是轨迹长度。该条目是合成记录:act 不执行任何操作,
outcome、tool 和 result_ref 都是编排器发送的字符串。
bundle {intent_id, session_issued_at?, session_expires_at?, intended_verifier?} → {signed_bundle}¶
该意图的公民身份 bundle(signed_bundle.schema.json):已签名的 RPR、护照、
意图和策略决定,该意图的完整轨迹,以及清单:session_nonce、
rpr_hash / passport_hash / intent_hash / policy_hash(载荷哈希)、
audit_merkle_root("sha256:" + SDK 的审计 Merkle 根)、audit_count、
canonicalization_profile "dcp-jcs-v1"。签名:hash_alg "sha256"、created_at 为当前时间、
signer {type: "human", id: human_id, kids: [human classical kid, human pq kid]}、
manifest_hash、在 Bundle 上下文下由人类密钥生成的 composite_sig。请使用 SDK 的
bundle 构建器和签名器。Go 和 Rust 的构建器还会写入 audit_merkle_root_secondary。
可选参数在签名之前按原样放入清单。
verify {signed_bundle, mode} → {valid, errors}¶
SDK 的 V2 bundle 验证器。mode 为 "" 时使用默认验证器策略;任何其他值都是
应用于 default_mode 和所有风险等级的验证模式,与互操作测试的做法相同。格式错误的
bundle 得到 valid: false 结果,绝不会是错误响应。
rotate_principal {} → {human_id, principal_keys}¶
为委托人生成新的复合密钥对,以及一个新的 RPR,其 human_id 相同、issued_at
为当前时间、带有新的 binding_keys,并由新密钥签名。之后的 bundle 使用它们。
configure {tenant, trust_set, profile} → {} 或 {config_error}¶
租户的运营方配置,用 SDK 自己的加载器加载:信任集
(createPrincipalTrustStore 及其对应项)和 strict-v1 配置文件
(resolveStrictV1Profile)。配置错误为 {config_error: <CONFIG_* code>}。
admit {tenant, signed_bundle, revocation, now_ms} → 准入结果¶
使用租户的配置文件和信任集、吊销快照及时钟调用 SDK 的组合准入 API(admitSignedBundleV2 及其对应项)。
结果是 SDK 自己的 JSON 形式(spec §8),去掉 verification,再加上 verification_errors。
shutdown {} → {}¶
代理在响应后退出。
场景 1:DCP-01 至 DCP-03¶
每个 SDK 作为生产方:identity;一个带三次 act 的常规意图(api_call、通道 api、
["none"]、影响 low、风险 100);一个带一次 act 的支付意图(initiate_payment、
通道 payments、["financial_data"]、影响 high、风险 900,因此被阻止)
(拒绝同样是证据);两者都执行 bundle。然后每个 SDK 作为验证方在默认策略和 hybrid_required 下
检查这两个 bundle:两者都必须通过验证。
接着编排器在传输中篡改常规 bundle,每个验证器都必须拒绝 每个副本,除另有说明外在两种模式下都是如此:
| 攻击 | 改变了什么 |
|---|---|
intent_changed |
签名后 intent.payload.estimated_impact 变为 high |
audit_reordered |
前两个审计条目交换位置 |
audit_entry_dropped |
删除最后一个审计条目,保留清单 |
session_spliced |
替换清单中的 session_nonce |
bundle_signature_swapped |
bundle 签名来自支付 bundle |
passport_swapped |
护照属于另一个代理(下一个 SDK 的生产方) |
pq_stripped |
删除 bundle 签名的 pq 位置(classical_only);仅 hybrid_required |
intent_from_other_bundle |
意图和策略决定来自支付 bundle(同一会话和签名者) |
trail_from_other_bundle |
审计轨迹来自支付 bundle(同一会话和签名者) |
崩溃、超时或不按顺序作答的代理会被停止,之后对它的每个请求都会 立即失败,因此损坏的代理会表现为失败,而绝不会让运行挂起。
场景 2:strict-v1 下的准入¶
每个 SDK 作为生产方:identity、一个带一次 act 的常规意图(风险 100,因此 approve),
以及三个 bundle,其清单签名的会话时间窗口为 [T0, T0 + 300 s](配置文件的
最大值):一个面向本验证器(verifier:interop-admission),一个面向另一个受众,以及
在 rotate_principal 之后用轮换后的委托人密钥签名的一个。
编排器就是运营方。它根据生产方报告的公钥登记每个委托人
(绝不根据正在准入的 bundle),构建信任集,并提供吊销
快照:生产方的代理和密钥,checked_at 比时钟早 10 秒。每次准入都在
固定时刻 T0 + 120 s 运行(过期用例在过期时间加 1 毫秒时运行)。租户 A 和 B 是
测试框架所做的运营方配置选择;不涉及任何集成或缓存。
每个 SDK 作为消费方,针对每个生产方按此顺序、在同一个
进程中运行下面的 13 个用例。预期的 verified、principal_trust、profile、profile_compliance、admitted、
action_authorized(始终为 false)、recorded_decision、trust_set_version 和代码集合
来自规范,写在 admission.mjs 中,绝不来自某个 SDK。
| 用例 | 配置 | 预期 |
|---|---|---|
trusted_admitted |
租户 A 信任所有已登记的委托人 | 准入 |
trust_withdrawn |
去掉此委托人的租户 A,同一 bundle | TRUST_PRINCIPAL_UNKNOWN |
trust_restored |
恢复如前的租户 A,同一 bundle | 准入 |
tenant_b_empty_trust |
租户 B,空信任集 | TRUST_PRINCIPAL_UNKNOWN |
principal_key_revoked |
新的快照吊销一个委托人密钥 | REVOCATION_REVOKED |
agent_key_revoked |
新的快照吊销一个代理密钥 | REVOCATION_REVOKED |
other_audience |
为另一个验证器签名的 bundle | PROFILE_AUDIENCE_MISMATCH, VERIFICATION_FAILED |
session_expired |
时钟比签名的过期时间晚 1 毫秒 | PROFILE_SESSION_EXPIRED, VERIFICATION_FAILED |
rotation_new_keys_not_enrolled |
轮换后的 bundle,信任集未更新 | TRUST_KEY_NOT_ANCHORED |
rotation_overlap_new_keys |
新旧密钥均有效 | 准入 |
rotation_overlap_old_keys |
同上,原始 bundle | 准入 |
rotation_old_keys_retired |
旧密钥为 revoked,原始 bundle |
TRUST_KEY_REVOKED |
rotation_new_keys_after_retirement |
同上,轮换后的 bundle | 准入 |
出现不匹配时,会向输出目录写入一个最小复现:为那一次准入提供给消费方的全部内容,
包括租户的生效信任集和配置文件(最后加载的那些,即使是由更早的用例加载的,
也附带 configuration_loaded_in),以及它返回的内容。只有公开材料:没有私钥。
node tools/interop-matrix/admission.mjs --replay tools/interop-matrix/out/admission/<producer>-<consumer>-<case>.json
--replay 会启动所记录 SDK 的一个新消费方,配置所记录的租户,在所记录的时钟下对所记录的
bundle 执行准入,并要求输出与记录完全一致:双方的每个键
(规范化字段、代码、验证器错误,以及调用失败时的 error),只出现在一方的键也算差异,
因此两个不同的错误绝不会被算作复现。复现时退出码为 0,不同时为 1,记录不完整时为 2。
阴性对照。 矩阵运行之后,运行器会以 --negative-control 在 ts 组合上再次启动自身,
使用新的临时目录和新的运行 id,最多 120 秒。该次运行会反转 trust_withdrawn 的预期
admitted。只有当子进程以 1 退出,且其报告是它在本次运行中写出的(相同运行 id)、带有
negative_control_run: true、唯一的组合 ts>ts 为 13 中 12,并且恰好只有一个失败,即 trust_withdrawn 的
admitted 字段时,对照才通过。--check-negative-control 测试对照本身:它必须接受正确的运行,并拒绝
无法启动的子进程(没有报告,以及同一目录中留有早先运行的报告)和挂起的子进程
(在时间限制处被停止)。--negative-control-case NAME 改为反转另一个
用例,以便有意生成复现文件。
首次运行:16 个组合 × 13 个用例 = 208 个结果均符合预期,阴性对照也按要求 失败。
首次运行的发现¶
所有 16 个 SDK 组合和参考验证器在每个 bundle 和每种攻击上都一致。编写 代理的过程中暴露了以下 SDK 缺口,其中没有一个是 SDK 验证器之间的判定分歧:
- Go 的
SignBundleV2让signature.signer.id为空:它不接收签名者 id。四个 SDK 验证器接受这样的 bundle;参考验证器、CLI 和服务器则拒绝它 (signer/id must NOT have fewer than 6 characters)。已修复:签名者现在是 bundle 的 RPRhuman_id, Go 代理直接使用SignBundleV2。signature.signer块(id、kids)、created_at和manifest_hash是已签名清单之外的未经认证的元数据:没有 验证器将它们与 RPR 比对,也不重新计算manifest_hash(五者都接受全零); 只有 schema 约束它们的形式。 - Go 的类型化载荷缺少等级字段:
IntentV2没有security_tier,PolicyDecisionV2没有resolved_tier。现在两者都已存在。(contact和target.to在 schema 中是可选的,因此 Go 代理的映射写入null只是为了符合契约。) - Rust 的原生 API 没有 bundle 构建器或签名器,因此 Rust 代理从原语组装清单
并签名。已修复:
build_bundle_v2和sign_bundle_v2(dcp_ai::v2::bundle),代理现在使用它们。wasm特性的wasm_build_bundle和wasm_sign_bundle现在调用它们;此前wasm_sign_bundle写入created_at ""且没有signer.id,因此参考验证器会拒绝它签名的每个 bundle。 - 空的审计轨迹:如
spec/VERIFICATION.md所允许(根为 64 个零),每个验证器都接受 没有审计条目、签名有效的 bundle;用于 PQ 检查点的auditEventsMerkleRoot辅助函数 在四个 SDK 中同样都拒绝空列表。
说明(2026 年 10 月 25 日)。 上述发现按首次运行时的记录保留。第一条在
main上 已不再成立:四个 SDK 验证器现在会将未签名的signature块与已签名内容进行比对 (spec/VERIFICATION.mdV2-4):signer.id必须是 RPR 的human_id,signer.kids必须是复合签名的 kid,manifest_hash必须是规范清单的 SHA-256,hash_alg必须是允许的值,因此全零会被 拒绝。signature.created_at仍是没有任何验证器认证的未签名元数据。
路线图¶
- 场景 1(DCP-01 至 03)和场景 2(准入)纳入 CI。
- DCP-04:如今每个 SDK 都会构建
A2A_HELLO和A2A_WELCOME,但没有一个实现A2A_CONFIRM/A2A_ESTABLISHED或 DCP-04 §3.4 的会话密钥推导,只有 TypeScript 具备消息所宣告的混合 X25519 + ML-KEM-768 KEM,Rust 也没有会话层。 一旦它们实现,协议将增加hello、welcome、confirm、send和receive, 编排器会在两个代理之间重放、重排和拼接消息。 - 由不同 SDK 上的代理逐跳构建的 DCP-09 授权链。