Matriz de interoperabilidade: agentes vivos nos quatro SDKs¶
Tradução informativa da versão em inglês, atualizada em 25/10/2026. Em caso de divergência, prevalece a versão em inglês. Identificadores, comandos e blocos de código são mantidos sem tradução.
run.mjs inicia um processo de agente por SDK (TypeScript, Python, Go, Rust) e os faz
trabalhar entre si: um agente construído sobre um SDK cria a sua identidade, declara intenções, age e
assina os seus bundles de cidadania; um agente construído sobre outro SDK os verifica, como faria antes de
confiar em uma contraparte. Verificar aqui significa que o bundle é coerente e assinado pelas chaves que ele lista; não é
confiança na contraparte, admissão nem autorização para agir (o cenário 2 cobre a admissão, que também não autoriza
nenhuma ação). Os processos não compartilham nada além das mensagens que o orquestrador retransmite,
então cada par (16 com quatro SDKs, incluindo cada SDK contra si mesmo) é uma interoperação real, não
uma comparação de arquivos. Um quinto agente, só de verificação, ref, executa o verificador de referência
lib/verify.js (o que node bin/dcp.js verify-bundle e o servidor de verificação executam): ele valida cada
artefato contra schemas/v2 antes do verificador do @dcp-ai/sdk (o SDK do workspace até que a 2.2.0 esteja
no npm), algo que os verificadores dos SDKs não fazem.
O orquestrador é também a rede intermediária: ele adultera os bundles em trânsito, e cada verificador deve rejeitar cada bundle adulterado da mesma forma. Qualquer veredito inesperado, falha ou tempo esgotado faz a execução falhar.
O cenário 1 (run.mjs) exercita partes do DCP-01 (identidade), do DCP-02 (intenção e política) e do DCP-03 (auditoria)
entre os SDKs; não é um teste de conformidade completo dessas especificações.
O cenário 2 (admission.mjs) cobre a admissão sob o perfil strict-v1
(spec/profiles/admission) nos quatro SDKs. O handshake A2A do DCP-04 precisa antes de trabalho nos SDKs
(veja o roadmap no fim).
Execução¶
(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 e uma toolchain estável de Rust devem estar no PATH; run.mjs compila agents/go e agents/rust.
Opções: --agents a,b (qualquer um de ts, py, go, rust, ref), --python PATH (padrão $INTEROP_PYTHON ou python3), --timeout MS
(por requisição, padrão 60000), --out DIR (padrão tools/interop-matrix/out, onde são escritos o relatório
e cada bundle que produziu um veredito inesperado). admission.mjs aceita as mesmas
opções exceto ref (padrão --out tools/interop-matrix/out/admission), além de
--skip-negative-control, --negative-control, --check-negative-control e --replay FILE
(veja abaixo).
O protocolo dos agentes¶
Um agente lê uma requisição JSON por linha na stdin e escreve exatamente uma linha de resposta JSON por requisição na stdout, em ordem; todo o resto vai para a 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"}
Um agente mantém o seu próprio estado (chaves, identidade, intenções, trilhas de auditoria) em memória durante a vida do
processo. As chaves nunca saem dele. Cada valor que o contrato fixa abaixo deve ser produzido exatamente
como escrito; todo o resto é assunto do próprio SDK, que é o que a matriz testa (um
campo opcional que um SDK adiciona por conta própria, como o audit_merkle_root_secondary SHA3 do Go, é aceitável
desde que todos os verificadores o aceitem).
identity {owner_name, jurisdiction} → {agent_id, human_id, session_nonce, principal_keys, agent_keys}¶
Gera dois pares de chaves compostas (Ed25519 + ML-DSA-65): um para o principal humano e outro para o
agente; um nonce de sessão novo (64 hex); human_id = rpr:<uuid4> e agent_id = agent:<uuid4>.
Entradas de chave: {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_ato instante atual,expires_at null,contact null,binding_keys= as duas entradas de chave humanas. UmSignedPayloadcom assinatura composta (pq_over_classical) das chaves humanas sob o contextoResponsiblePrincipal. - Passaporte (
agent_passport.schema.json):dcp_version "2.0",agent_id,session_nonce,keys= as duas entradas de chave do agente,principal_binding_reference=human_id,capabilities ["api_call", "payments"],risk_tier "medium",created_ato instante atual,status "active". Com assinatura composta das chaves do agente sobAgentPassport.
principal_keys e agent_keys são as metades públicas, [{kid, alg, public_key_b64}], a clássica
primeiro. O cenário 2 inscreve o principal a partir delas, fora de banda.
Um segundo identity substitui o primeiro, junto com as intenções e as trilhas que o acompanhavam.
intent {action_type, channel, data_classes, estimated_impact, risk_score, applied_policy_hash} → {intent_id, decision, security_tier}¶
- Intenção (
intent.schema.json):dcp_version "2.0",intent_id=intent:<uuid4>,session_nonce,agent_id,human_id,timestampo instante atual,action_type,target {channel, to: null},data_classes,estimated_impact,requires_consent= (estimated_impacté"high"),security_tier= o cálculo do nível de segurança do SDK (DCP-AI v2.0 §14.2) para esta intenção erisk_score. - Decisão de política (
policy_decision.schema.json):dcp_version "2.0",intent_id,session_nonce,decision=approveserisk_score< 500,escalatese < 800, senãoblock;risk_score;reasons ["interop-matrix: risk_score <n>"];required_confirmationnullem approve, senão{"type": "human_approve", "fields": ["action_type"]};applied_policy_hashdos argumentos;timestampo instante atual;resolved_tier= osecurity_tierda intenção.
Ambas com assinatura composta das chaves do agente (contextos Intent, PolicyDecision).
act {intent_id, outcome, tool, result_ref} → {audit_id, count}¶
Acrescenta uma entrada de auditoria (audit_entry.schema.json) à trilha dessa intenção: dcp_version "2.0",
audit_id = audit:<uuid4>, session_nonce, prev_hash = "GENESIS" para a primeira entrada da trilha,
senão "sha256:" + hex(sha256(canonical(previous entry))); hash_alg "sha256";
timestamp o instante atual; agent_id; human_id; intent_id;
intent_hash = "sha256:" + hex(sha256(canonical(intent payload)));
policy_decision = approved / escalated / blocked conforme a decisão; outcome;
evidence {tool, result_ref, evidence_hash: "sha256:" + hex(sha256(utf8(result_ref)))};
pq_checkpoint_ref null. count é o comprimento da trilha. A entrada é um registro sintético: act não executa nada,
e outcome, tool e result_ref são as strings que o orquestrador envia.
bundle {intent_id, session_issued_at?, session_expires_at?, intended_verifier?} → {signed_bundle}¶
O bundle de cidadania dessa intenção (signed_bundle.schema.json): o RPR, o passaporte,
a intenção e a decisão de política assinados, a trilha completa da intenção e o manifesto: session_nonce,
rpr_hash / passport_hash / intent_hash / policy_hash (os hashes das cargas úteis),
audit_merkle_root ("sha256:" + a raiz de Merkle de auditoria do SDK), audit_count,
canonicalization_profile "dcp-jcs-v1". A assinatura: hash_alg "sha256", created_at o instante atual,
signer {type: "human", id: human_id, kids: [human classical kid, human pq kid]},
manifest_hash, composite_sig das chaves humanas sob o contexto Bundle. Use o
construtor e o assinador de bundles do SDK. Os construtores de Go e Rust também escrevem audit_merkle_root_secondary.
Os argumentos opcionais entram no manifesto antes de ele ser assinado, exatamente como dados.
verify {signed_bundle, mode} → {valid, errors}¶
O verificador de bundles V2 do SDK. O mode "" é a política de verificador padrão; qualquer outro valor é um
modo de verificação aplicado a default_mode e a todos os níveis de risco, como fazem os testes de interoperabilidade. Um
bundle malformado é um resultado valid: false, nunca uma resposta de erro.
rotate_principal {} → {human_id, principal_keys}¶
Um novo par de chaves compostas para o principal e um novo RPR com o mesmo human_id, issued_at
o instante atual e as novas binding_keys, assinado pelas novas chaves. Os bundles posteriores as usam.
configure {tenant, trust_set, profile} → {} ou {config_error}¶
A configuração de operador de um inquilino, carregada com os carregadores próprios do SDK: o conjunto de confiança
(createPrincipalTrustStore e os seus equivalentes) e o perfil strict-v1
(resolveStrictV1Profile). Um erro de configuração é {config_error: <CONFIG_* code>}.
admit {tenant, signed_bundle, revocation, now_ms} → o resultado da admissão¶
A API combinada de admissão do SDK (admitSignedBundleV2 e os seus equivalentes) com o
perfil e o conjunto de confiança do inquilino, o snapshot de revogação e o relógio. O resultado é a forma JSON própria do SDK
(spec §8) sem verification, mais verification_errors.
shutdown {} → {}¶
O agente sai depois de responder.
Cenário 1: do DCP-01 ao DCP-03¶
Para cada SDK como produtor: identity; uma intenção rotineira (api_call, canal api,
["none"], impacto low, risco 100) com três act; uma intenção de pagamento (initiate_payment,
canal payments, ["financial_data"], impacto high, risco 900, portanto bloqueada) com um act
(a recusa também é evidência); bundle para as duas. Depois cada SDK como verificador confere os dois
bundles sob a política padrão e hybrid_required: os dois devem ser verificados.
Então o orquestrador adultera o bundle rotineiro em trânsito, e cada verificador deve rejeitar cada cópia, nos dois modos salvo indicação:
| Ataque | O que muda |
|---|---|
intent_changed |
intent.payload.estimated_impact passa a high depois da assinatura |
audit_reordered |
as duas primeiras entradas de auditoria trocam de lugar |
audit_entry_dropped |
a última entrada de auditoria é removida e o manifesto é mantido |
session_spliced |
o session_nonce do manifesto é substituído |
bundle_signature_swapped |
a assinatura do bundle vem do bundle do pagamento |
passport_swapped |
o passaporte é o de outro agente (o produtor do SDK seguinte) |
pq_stripped |
a posição pq da assinatura do bundle é removida (classical_only); apenas hybrid_required |
intent_from_other_bundle |
a intenção e a decisão de política vêm do bundle do pagamento (mesma sessão e assinante) |
trail_from_other_bundle |
a trilha de auditoria vem do bundle do pagamento (mesma sessão e assinante) |
Um agente que falha, esgota o tempo ou responde fora de vez é interrompido e cada requisição posterior a ele falha imediatamente, então um agente quebrado aparece como falhas, nunca como uma execução travada.
Cenário 2: admissão sob strict-v1¶
Cada SDK como produtor: identity, uma intenção rotineira (risco 100, portanto approve) com um act,
e três bundles cujos manifestos assinam a janela de sessão [T0, T0 + 300 s] (o
máximo do perfil): um para este verificador (verifier:interop-admission), um para outra audiência e,
após rotate_principal, um assinado com as chaves rotacionadas do principal.
O orquestrador é o operador. Ele inscreve cada principal a partir das chaves públicas que o produtor informou
(nunca a partir de um bundle em admissão), constrói os conjuntos de confiança e serve os snapshots de
revogação: o agente e as chaves do produtor, checked_at 10 s antes do relógio. Cada admissão roda
no instante fixo T0 + 120 s (o caso de expiração, na expiração mais 1 ms). Os inquilinos A e B são
uma seleção de configuração de operador feita pelo harness; nenhuma integração ou cache está envolvida.
Cada SDK como consumidor executa os 13 casos abaixo para cada produtor, nesta ordem, em um
único processo. Os valores esperados de verified, principal_trust, profile, profile_compliance, admitted,
action_authorized (sempre false), recorded_decision, trust_set_version e o conjunto de códigos
vêm da especificação, escritos em admission.mjs, nunca de um SDK.
| Caso | Configuração | Esperado |
|---|---|---|
trusted_admitted |
o inquilino A confia em todos os principais inscritos | admitido |
trust_withdrawn |
o inquilino A sem este principal, o mesmo bundle | TRUST_PRINCIPAL_UNKNOWN |
trust_restored |
o inquilino A como antes, o mesmo bundle | admitido |
tenant_b_empty_trust |
o inquilino B, conjunto de confiança vazio | TRUST_PRINCIPAL_UNKNOWN |
principal_key_revoked |
um snapshot recente revoga uma chave do principal | REVOCATION_REVOKED |
agent_key_revoked |
um snapshot recente revoga uma chave do agente | REVOCATION_REVOKED |
other_audience |
o bundle assinado para outro verificador | PROFILE_AUDIENCE_MISMATCH, VERIFICATION_FAILED |
session_expired |
o relógio 1 ms depois da expiração assinada | PROFILE_SESSION_EXPIRED, VERIFICATION_FAILED |
rotation_new_keys_not_enrolled |
bundle rotacionado, conjunto de confiança não atualizado | TRUST_KEY_NOT_ANCHORED |
rotation_overlap_new_keys |
chaves antigas e novas ativas | admitido |
rotation_overlap_old_keys |
o mesmo, bundle original | admitido |
rotation_old_keys_retired |
chaves antigas revoked, bundle original |
TRUST_KEY_REVOKED |
rotation_new_keys_after_retirement |
o mesmo, bundle rotacionado | admitido |
Uma divergência escreve uma reprodução mínima no diretório de saída: tudo o que o consumidor recebeu
para essa admissão, incluindo o conjunto de confiança e o perfil efetivos do inquilino (os últimos
carregados, também quando um caso anterior os carregou, com configuration_loaded_in), e o que ele
devolveu. Apenas material público: nenhuma chave privada.
node tools/interop-matrix/admission.mjs --replay tools/interop-matrix/out/admission/<producer>-<consumer>-<case>.json
--replay inicia um consumidor novo do SDK registrado, configura o inquilino registrado, admite o
bundle registrado no relógio registrado e exige exatamente a saída registrada: cada chave de
ambos os lados (os campos normalizados, os códigos, os erros do verificador e error quando uma chamada
falhou), e uma chave presente em apenas um lado é uma diferença, então dois erros diferentes nunca contam
como reprodução. Sai com 0 quando reproduz, 1 quando difere e 2 quando o registro está incompleto.
Controle negativo. Após a matriz, o executor inicia a si mesmo com --negative-control no
par ts, em um diretório temporário novo e com um novo id de execução, por no máximo 120 s. Essa execução inverte o
admitted esperado de trust_withdrawn. O controle só passa se o filho sair com 1 e o seu
relatório for o que ele escreveu nesta execução (mesmo id de execução) com negative_control_run: true, o único
par ts>ts com 12 de 13, e exatamente uma falha, o campo admitted de trust_withdrawn.
--check-negative-control testa o próprio controle: ele deve aceitar uma execução correta e recusar um
filho que não consegue iniciar (sem relatório, e com um relatório deixado por uma execução anterior no mesmo diretório)
e um filho que trava (interrompido no limite de tempo). --negative-control-case NAME inverte outro
caso em vez disso, para produzir de propósito um arquivo de reprodução.
Primeira execução: 16 pares × 13 casos = 208 resultados como esperado, e o controle negativo falhou como exigido.
O que a primeira execução encontrou¶
Os 16 pares de SDKs e o verificador de referência concordam em cada bundle e em cada ataque. Escrever os agentes revelou estas lacunas dos SDKs, nenhuma delas uma divergência de veredito entre os verificadores dos SDKs:
- O
SignBundleV2do Go deixavasignature.signer.idvazio: ele não recebia nenhum id de assinante. Os quatro verificadores dos SDKs aceitavam esse bundle; o verificador de referência, a CLI e o servidor o rejeitavam (signer/id must NOT have fewer than 6 characters). Corrigido: o assinante agora é ohuman_iddo RPR do bundle, e o agente de Go usaSignBundleV2como está. O blocosignature.signer(id,kids),created_atemanifest_hashsão metadados não autenticados fora do manifesto assinado: nenhum verificador os confere com o RPR nem recalculamanifest_hash(os cinco aceitam zeros); apenas o schema restringe a sua forma. - Os payloads tipados do Go não tinham campos de nível:
IntentV2não tinhasecurity_tierePolicyDecisionV2não tinharesolved_tier. Os dois existem agora. (contactetarget.tosão opcionais no schema, então os mapas do agente de Go escrevemnullapenas para corresponder ao contrato.) - A API nativa do Rust não tinha construtor nem assinador de bundles, então o agente de Rust montava o manifesto
e o assinava a partir das primitivas. Corrigido:
build_bundle_v2esign_bundle_v2(dcp_ai::v2::bundle), que o agente usa agora.wasm_build_bundleewasm_sign_bundleda featurewasmagora os chamam; antes,wasm_sign_bundleescreviacreated_at ""e nenhumsigner.id, então o verificador de referência rejeitava todo bundle que ele assinava. - Uma trilha de auditoria vazia: todo verificador aceita um bundle assinado validamente sem entradas de auditoria,
como o
spec/VERIFICATION.mdpermite (raiz de 64 zeros); os auxiliaresauditEventsMerkleRoot, que servem aos checkpoints PQ, recusam uma lista vazia nos quatro SDKs igualmente.
Nota (25/10/2026). Os achados acima são mantidos como foram registrados na primeira execução. O primeiro não vale mais em
main: os quatro verificadores dos SDKs agora conferem o blocosignaturenão assinado com o que está assinado (spec/VERIFICATION.mdV2-4):signer.iddeve ser ohuman_iddo RPR,signer.kidsos kids da assinatura composta,manifest_hasho SHA-256 do manifesto canônico ehash_algum valor permitido, então zeros são recusados.signature.created_atcontinua sendo um metadado não assinado que nenhum verificador autentica.
Roadmap¶
- Cenários 1 (do DCP-01 ao 03) e 2 (admissão), na CI.
- DCP-04: hoje todos os SDKs constroem
A2A_HELLOeA2A_WELCOME, mas nenhum implementaA2A_CONFIRM/A2A_ESTABLISHEDnem a derivação da chave de sessão do DCP-04 §3.4, só o TypeScript tem o KEM híbrido X25519 + ML-KEM-768 que as mensagens anunciam, e o Rust não tem camada de sessão. Quando o fizerem, o protocolo ganharáhello,welcome,confirm,sendereceive, e o orquestrador reenviará, reordenará e emendará mensagens entre os dois agentes. - Cadeias de mandatos do DCP-09 construídas salto a salto por agentes em SDKs diferentes.