Skip to content

fix(types): recipient publica a união que o contrato sempre exigiu (core#128) - #148

Merged
fabianocruz merged 2 commits into
mainfrom
fix/meta-tool-recipient-anyof
Sep 14, 2026
Merged

fabianocruz merged 2 commits into
mainfrom
fix/meta-tool-recipient-anyof

Conversation

@fabianocruz

Copy link
Copy Markdown
Member

Closes #128.

codespar_pay.recipient era publicado como type: "string" e a própria descrição mandava passar um OBJETO de conta bancária — com a frase "the tool-call argument type, not this schema's declared string type, is what determines routing". O schema contradizia a si mesmo, por escrito.

Prosa não conserta schema. Um cliente que ENFORÇA o declarado — MCP com validação estrita, OpenAI strict tool mode, um gateway rodando ajv — rejeita o objeto antes de sair da máquina, ou coage para string, que a mesma descrição proíbe. O Pix cash-out MANUAL e o destino do method=ted ficam indisponíveis exatamente nos clientes bem-comportados.

Medido antes de mexer

No documento que a produção serve, hoje:

GET /v1/meta-tools.json
  codespar_pay.recipient   type=string   enum=None
  codespar_pay.action      enum=[boleto_quote, dda_list_due, dda_status, dda_subscribe,
                                 dda_unsubscribe, dict_claim_cancel, ... 18 ações]

A segunda linha é uma boa notícia que eu não procurava: o enum de action já foi ampliado. A metade "ações vivas que o schema não anuncia" do levantamento de 08/09 fechou antes. Sobrou esta.

A união vai para o schema

MetaToolInputProperty ganha anyOf, e type vira opcional — exatamente um dos dois está presente. anyOf e não array de tipos porque é o que os modos estritos aceitam na prática. O ramo de objeto declara bank, account, branch, tax_id, name e account_type campo a campo, onde antes havia uma frase entre chaves.

O trabalho de verdade foi ensinar a conformidade a enxergar união

  • O caminhante de prosa e o de vocabulário descem nos ramos. Sem isso, a descrição do ramo de conta bancária ficaria invisível para todo portão de prosa — a classe que esses portões existem para pegar, criada por mim.
  • O comparador entre runtimes compara assinatura de forma, não o campo type cru. Com os dois lados em anyOf, comparar type daria undefined dos dois lados e passaria calado.

Três controles, e um achou defeito meu

controle resultado
runtime achata a união de volta para um tipo reprova, nomeando as duas formas
ramos escritos em outra ordem passa — união é conjunto, e portão que reprova reordenação é portão que se aprende a ignorar
runtime TIRA um campo do ramo de objeto passava calado

O terceiro era buraco real: o comparador só lia properties do nível de cima, e os campos de uma união moram um nível abaixo, dentro dos ramos. Mesmo buraco, mais fundo. Fechado na mesma mudança.

E o teste que escrevi para a CLI pegou outro defeito meu: uma chave Pix de 11 dígitos (um CPF) é JSON válido, então o parse cru transformava a chave no número 12345678901. O parse agora só vale quando cai num ramo estruturado.

FICA DE FORA — e é metade do conserto

O codespar-enterprise publica o seu próprio input_schema em packages/api/src/meta-tools.ts:136, e é dele que sai o /v1/meta-tools.json que os clientes leem. Enquanto ele não subir a mesma união, o documento servido continua dizendo type: "string".

Esse PR tem duas dependências, nesta ordem: este aqui publicado, e o pino de @codespar/types no enterprise — hoje em 0.10.14, três versões atrás do que está no npm — subindo junto, porque o teste de conformidade de lá compara contra as definições publicadas.

Versões

types minor (o type opcional é quebra de tipo para quem o lia como string, e a forma de recipient muda), sdk minor porque reexporta o contrato, cli minor pela mudança de comportamento do --arg, e os 13 adapters em patch só pela faixa de peer. 18 suítes verdes, typecheck limpo, portão de deriva sem deriva nova.

🤖 Generated with Claude Code

Fabiano Cruz and others added 2 commits September 14, 2026 13:37
…ore#128)

`codespar_pay.recipient` era publicado como `type: "string"` e a própria
descrição mandava passar um OBJETO de conta bancária, com a frase "the
tool-call argument type, not this schema's declared string type, is what
determines routing". O schema contradizia a si mesmo.

Prosa não conserta schema. Cliente que ENFORÇA o declarado — MCP com
validação estrita, OpenAI strict tool mode, gateway com ajv — rejeita o
objeto antes de sair da máquina, ou coage para string, que a mesma
descrição proíbe. O Pix cash-out MANUAL e o destino do method=ted ficam
indisponíveis exatamente nos clientes bem-comportados.

Medido hoje no documento servido (`GET /v1/meta-tools.json`): `recipient`
seguia `type: "string"`. De carona, medi também que o enum de `action` JÁ
foi ampliado (18 ações, com dict_*, dda_* e boleto_quote) — aquela metade
da divergência fechou antes.

**A união vai para o schema.** `MetaToolInputProperty` ganha `anyOf` e o
`type` vira opcional; exatamente um dos dois está presente. `anyOf` e não
array de tipos porque é o que os modos estritos aceitam na prática. O
ramo de objeto declara `bank`, `account`, `branch`, `tax_id`, `name` e
`account_type` CAMPO A CAMPO, onde antes havia uma frase.

**A maquinaria de conformidade passa a enxergar união**, que é o trabalho
de verdade: o caminhante de prosa e o de vocabulário descem nos ramos
(uma descrição dentro de um ramo era invisível para todo portão de prosa),
e o comparador entre runtimes compara assinatura de forma em vez do campo
`type` cru — que, com os dois lados em `anyOf`, seria `undefined` dos dois
lados e passaria calado.

Três controles, e um deles achou defeito meu:

- runtime que achata a união de volta para um tipo -> reprovado, com a
  mensagem nomeando as duas formas;
- ramos escritos em outra ordem -> passa (união é conjunto, e portão que
  reprova reordenação é portão que se aprende a ignorar);
- runtime que TIRA um campo do ramo de objeto -> passava calado. O
  comparador só olhava `properties` do nível de cima, e os campos de uma
  união moram um nível abaixo. Era o mesmo buraco, mais fundo.

Na CLI, `--arg` aceita as duas formas pelo mesmo flag. O teste que
escrevi pegou outro defeito meu: uma chave Pix de 11 dígitos é JSON
válido, então o parse cru transformava um CPF no número 12345678901. O
parse só vale quando cai num ramo estruturado.

Versões: types minor (o `type` opcional é quebra de tipo para quem lia
como `string`, e a forma de `recipient` muda), sdk minor porque reexporta
o contrato, cli minor pela mudança de comportamento do `--arg`, adapters
em patch só pela faixa de peer.

FICA DE FORA, e é metade do conserto: o `codespar-enterprise` publica o
seu próprio `input_schema` em `meta-tools.ts:136`, e é dele que sai o
`/v1/meta-tools.json` servido. Enquanto ele não subir a mesma união (e o
pino de `@codespar/types`, hoje em 0.10.14, três versões atrás), o
documento que os clientes leem continua dizendo `type: "string"`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
O `@codespar/mcp` declara `@codespar/sdk` em dependencies E em
peerDependencies, e o 0.5.6 foi publicado com as duas discordando: a
dependência aceitava ^0.14.0 e o peer parava em ^0.13.0. O script de
release que alargou "a faixa" achou uma das duas e deixou a outra —
que é o que acontece com um fato escrito em dois lugares. O erro é meu,
da rodada do v0.14.0.

Impacto medido, para não vender por mais do que é: `npm i
@codespar/mcp@0.5.6` instala limpo, e com `--strict-peer-deps`
também — o npm deixa a dependência direta satisfazer o peer. O defeito
é um pacote que se descreve errado, e quem lê a metadata (um resolvedor
que não seja o npm, uma pessoa decidindo o que pinar) recebe a resposta
errada dele.

Portão novo, hermético e com controle próprio, rodando no ci.yml ao lado
do self-test do portão de deriva: pacote que nomeia @codespar/sdk nos
dois campos tem de nomear a MESMA faixa. Plantado o desalinhamento de
volta, ele reprova; alinhado, passa.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fabianocruz
fabianocruz merged commit 0a85456 into main Sep 14, 2026
11 checks passed
@fabianocruz
fabianocruz deleted the fix/meta-tool-recipient-anyof branch September 14, 2026 16:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

recipient publicado como type:string mas o contrato manda passar objeto — cliente que valida schema rejeita o uso correto

1 participant