Skip to content

fix(sdk): versao que o artefato realmente e, e o tipo que o servidor realmente manda (oss-sdk#11, #13, #6) - #124

Open
fabianocruz wants to merge 1 commit into
mainfrom
fix/sdk-contrato-e-guards
Open

fix(sdk): versao que o artefato realmente e, e o tipo que o servidor realmente manda (oss-sdk#11, #13, #6)#124
fabianocruz wants to merge 1 commit into
mainfrom
fix/sdk-contrato-e-guards

Conversation

@fabianocruz

@fabianocruz fabianocruz commented Aug 28, 2026

Copy link
Copy Markdown
Member

Lane ent-oss da auditoria, metade SDK. Dois achados inteiros e a metade SDK de um terceiro. Os outros estao em codespar/codespar-enterprise#895 e codespar/codespar#141, porque nenhum PR atravessa repositorio.

Nada aqui publica nem bumpa versao. O que falta publicar esta listado no fim.


oss-sdk#11 — versao mentida em dois artefatos publicados

Mecanismo. Tres lugares escreviam a versao a mao e nenhum conferia os outros.

TypeScript: packages/cli/src/version.ts:7 dizia 0.5.5 enquanto packages/cli/package.json:3 dizia 0.6.0. Essa constante alimenta --version, o banner (index.ts:548) e o User-Agent de toda requisicao (api.ts:56). O proprio cabecalho do arquivo pedia "keep in sync with package.json on release" e nada garantia isso.

Python: packages/python/src/codespar/_http.py:54 mandava codespar-python/0.10.0, __init__.py:144 declarava __version__ = "0.10.2" e pyproject.toml:7 declara 0.11.0. Tres valores. Uma requisicao do SDK atual chegava rotulada como um release duas versoes atras, e toda pergunta do lado servidor da forma "qual versao do SDK esta fazendo isso?" era respondida errado.

O que mudou. Uma fonte por pacote, e um teste que prende essa fonte ao manifesto publicado, lido em tempo de teste.

  • TS: a constante passa a 0.6.0 e o cabecalho aponta para o teste que a mantem honesta.
  • Python: src/codespar/_version.py novo, com __version__ e USER_AGENT; __init__.py e _http.py importam dele. O modulo nao importa nada do pacote, porque __init__ importa o transporte e o transporte precisa da versao — qualquer coisa menos isolada e ciclo.

Divergencia da proposta da triagem. Ela sugeria importlib.metadata.version("codespar") para o User-Agent. Nao fiz, e a razao esta no cabecalho do _version.py: a busca de metadata so responde quando a distribuicao esta instalada, entao precisaria de um literal de fallback de qualquer jeito — acrescenta um ramo em vez de remover a duplicata, e faz a versao que o pacote reporta depender de como ele foi carregado (um checkout de fonte responderia diferente de um pip install). Isso tambem tornaria o proprio teste dependente do ambiente, que e a armadilha 4. O literal unico mais um teste que o prende ao pyproject.toml fecha a deriva no ponto onde ela realmente aconteceu.

Para o TS, o mesmo raciocinio no outro sentido: version.ts nao le package.json em runtime porque dist/index.js e o bin publicado e subir de dist/ atras de um manifesto resolve diferente sob npx, install global e bundler. O literal fica; o teste e o que nao deixa ele separar de novo.

Testes. packages/cli/src/__tests__/version.test.ts e packages/python/tests/test_version.py. O teste de header que ja existia (api.test.ts) afirmava que o User-Agent casa com uma forma semver — verdade para qualquer versao, inclusive a errada. Os novos afirmam o valor, contra o manifesto lido em disco, e exercitam a funcao de verdade: o TS captura o header de uma requisicao real do ApiClient com fetch espionado, o Python chama build_headers de verdade nos dois ramos (com e sem project_id). Cada um traz um controle de que o manifesto foi de fato lido — sem ele, uma leitura falha compararia undefined com undefined.

VERMELHO (antes)                          VERDE (depois)
Tests  2 failed | 1 passed (3)            Tests  48 passed (48)  (suite do cli)
  × is the version package.json publishes      → expected '0.5.5' to be '0.6.0'
  × is the version the User-Agent carries      → expected 'codespar-cli/0.5.5' to be 'codespar-cli/0.6.0'

3 failed, 1 passed                        118 passed, 8 skipped  (suite python)
  FAILED test_dunder_version_is_the_published_version
  FAILED test_user_agent_carries_the_published_version   → 'codespar-python/0.10.0' == 'codespar-python/0.11.0'
  FAILED test_user_agent_is_present_on_the_project_scoped_path_too

O que NAO fecha. O ci.yml deste repo nao tem job de Python — nao ha pytest em lugar nenhum dele. O teste de versao do Python roda localmente (PYTHONPATH=src python3.12 -m pytest tests -q) e nao vai proteger nada no CI ate existir esse job. Acrescentar o job esta fora do escopo deste PR e vale uma issue propria. O lado TS roda no CI, dentro de npx turbo run build typecheck test.


oss-sdk#13 — o tipo AgenticReceipt publicado e um subconjunto do que a API devolve

Mecanismo. packages/types/src/types.ts:519-531 e o tipo com que um consumidor do SDK tipa uma resposta de codespar_ledger action=receipt. O receiptRowToJson do enterprise (packages/api/src/agentic-receipt.ts:945-996) emite seis campos sob payment que a interface nao declarava: amount_atomic, amount_authorized, amount_charged, amount_refunded, metering e sandbox. Um consumidor nao conseguia le-los sem as any, e o TypeScript acusava cada um como excedente em qualquer literal.

O que mudou. So o tipo. Os seis campos entram como opcionais, com a nulidade que o servidor realmente emite (amount_atomic vai em todo recibo e e null num trilho fiat; os outros so aparecem num recibo medido), mais a interface ReceiptMetering que faltava. Nenhum byte do que o servidor emite muda, e nao pode mudar: o recibo e selado e assinado, entao "alinhar o JSON com o tipo" quebraria chain e receipt_sig. O tipo se move; o fio nao.

Teste. packages/types/src/agentic-receipt-surface.test.ts. O vermelho e do proprio compilador — a fixture e a forma emitida, campo a campo, com satisfies AgenticReceipt:

VERMELHO (antes)
src/agentic-receipt-surface.test.ts(51,5): error TS2353: Object literal may only specify known
  properties, and 'amount_atomic' does not exist in type '{ rail: string; provider: string | null;
  tx_id: string | null; amount_minor: number; attempt_id: string; money_moved: boolean;
  at: string | null; }'.
... 8 erros TS2353/TS2339 no total

VERDE (depois)
npx tsc --noEmit → 0 erros
Test Files 1 passed (1), Tests 2 passed (2)

O segundo teste e o controle: um recibo fiat, sem nenhum dos campos medidos, continua tipando — sem ele, declarar os seis como obrigatorios passaria o primeiro teste e quebraria todo recibo Pix.

O que NAO fecha, e o que falta publicar. A outra metade do achado fica no enterprise e nao foi feita: subir o pin de @codespar/types (packages/api/package.json:98, hoje 0.10.14) e marcar o literal de receiptRowToJson com satisfies AgenticReceipt. Isso depende de um publish: 0.10.15 — a versao que este repo declara — ja esta na registry, entao estes campos precisam sair em 0.10.16. O bump de versao e o npm publish sao acao publica do Fabiano e nao estao neste PR. Ordem: bumpar packages/types/package.json para 0.10.16, publicar, depois subir o pin no enterprise e acrescentar o satisfies la.


oss-sdk#6 — metade SDK: um 201 magro nao pode virar Invalid Date

Mecanismo. packages/core/src/session.ts:148-151. O runtime MIT devolvia { id, status } no 201 (consertado em codespar/codespar#141), e o SDK montava a sessao direto desse corpo. new Date(undefined) nao lanca: e um Invalid Date que formata como "Invalid Date", compara falso com tudo e serializa para null. user_id e servers chegavam undefined do mesmo jeito. E em :594-606, cachedTools = payload.tools guardava undefined quando a chave faltava — um cache que nunca enche.

Vale registrar o alcance real: createdAt, userId e servers existem no objeto concreto mas nao na interface publica Session, entao um consumidor TypeScript nao chega neles e um de JavaScript chega. Foi assim que um Invalid Date ficou ali sem ninguem ver.

O que mudou. O piso do proprio SDK, para que um backend que responde magro degrade para um valor verdadeiro em vez de um quebrado. Nada e inventado: userId e servers caem para o que esta chamada pediu, e createdAt para o instante em que a resposta chegou — dentro de um round trip do horario real de criacao. cachedTools cai para a lista vazia uma vez, em vez de ?? [] escondendo o problema em cada call site. E BackendSessionResponse/BackendConnectionsResponse passam a declarar esses campos como opcionais: a opcionalidade e uma afirmacao sobre o que este codigo precisa sobreviver, nao sobre o que um backend correto manda.

Teste. packages/core/src/__tests__/session-shape-tolerance.test.ts. O controle e a metade que importa: quando o backend manda os campos, o backend ganha. Um fallback que sobrescrevesse o servidor em silencio seria um bug pior que o consertado.

VERMELHO (antes)                          VERDE (depois)
Tests  3 failed | 1 passed (4)            Tests  4 passed (4)

  × has a real createdAt, not an Invalid Date          → expected true to be false
  × falls back to what this call asked for             → expected undefined to be 'user_demo'
  × control: an unparseable created_at degrades        → expected true to be false

O que NAO fecha. A metade runtime esta em codespar/codespar#141. A suite de contrato compartilhada (packages/types/src/testing/contract-suite.ts:325) continua so afirmando "id e connected" em connections() — foi essa assercao fraca que deixou a divergencia passar, e alarga-la obriga o runtime gerenciado ao mesmo criterio, entao ficou fora deste PR.


Estado real do CI

O CI deste PR esta VERMELHO em 5 jobs, e nenhum deles e o trabalho deste PR. Run 33183207992.

O job ci — o que roda npm ci + npx turbo run build typecheck test — passou em todos os passos menos o ultimo:

passo resultado
Lock file drift check success
Install dependencies success
Build, typecheck, and test success
Security audit failure

O Security audit e npm audit --omit=dev --audit-level=high. Este PR nao muda nenhuma dependencia: git diff main -- package.json package-lock.json packages/*/package.json e vazio. As duas advisories high sao de ip-address, uma dependencia transitiva, e reproduzem identicas no main local com o mesmo lockfile (3 vulnerabilities (1 moderate, 2 high)). O ultimo run verde do main foi no mesmo commit base (08d2cc1) e antes destas advisories serem publicadas — ou seja, o main de hoje fica vermelho aqui tambem. Nao consertei: bumpar dependencia transitiva nao e o achado desta lane e mudaria o lockfile no meio do congelamento.

Os outros quatro sao os quatro jobs de exemplo que apontam para ghcr.io/codespar/codespar:main, e falham todos no mesmo ponto, antes de qualquer codigo deste PR rodar:

validate.sh: starting runtime from ghcr.io/codespar/codespar:main (port 3000)…
validate.sh: polling http://localhost:3000/health …
validate.sh: runtime did not become healthy in 30s
Error response from daemon: No such container: codespar-example-payment-failure-triage-2354

Os tres jobs de exemplo que usam ghcr.io/codespar/codespar:latestskeleton, nfse-from-natural-language, whatsapp-installment-negotiation — passaram. A divisao e exatamente por tag de imagem, e o container morre no boot, antes de tocar em SDK. A imagem :main foi publicada pela ultima vez em 2026-08-22 e o main do codespar/codespar andou desde entao. Isso e um problema da imagem, nao deste diff, e vale uma issue propria — nao consegui consertar daqui.

Local, neste clone, no commit deste PR:

  • npx turbo run build typecheck test: 54 tasks successful, 54 total
  • packages/python: PYTHONPATH=src python3.12 -m pytest tests -q118 passed, 8 skipped. Este comando nao e rodado pelo CI (nao ha job de Python), e isso e um buraco que este PR nao fecha.

Reversao conferida

revertido para main (teste mantido) resultado
packages/cli/src/version.ts 3 tests, 2 fail
_http.py + __init__.py, _version.py removido 4 tests, 3 fail
packages/types/src/types.ts tsc --noEmit: 8 erros
packages/core/src/session.ts 4 tests, 3 fail

O que falta publicar (acao do Fabiano)

  1. @codespar/types@0.10.16 — bump em packages/types/package.json e npm publish. Sem isso o enterprise nao pode subir o pin nem acrescentar o satisfies AgenticReceipt, que e a outra metade do oss-sdk#13.
  2. @codespar/cli@0.6.0 — este PR so faz o codigo dizer 0.6.0; se a 0.6.0 ja estiver publicada com a string 0.5.5 dentro, e preciso um republish (0.6.1) para o artefato na registry parar de mentir.
  3. codespar (PyPI) 0.11.0 — mesma coisa: o User-Agent so fica certo no artefato publicado depois de um novo build.

🤖 Generated with Claude Code

…realmente manda (oss-sdk#11, #13, #6)

- CLI imprimia 0.5.5 sendo 0.6.0, e o User-Agent do Python dizia 0.10.0
  sendo 0.11.0 (com um terceiro numero, 0.10.2, no __version__). Uma
  fonte por pacote, e um teste que a prende ao manifesto publicado.
- AgenticReceipt publicado nao declarava seis campos que a API emite
  sob payment. Só o tipo muda; o recibo e selado e assinado.
- Um 201 magro nao vira mais Invalid Date: os fallbacks sao o que a
  propria chamada pediu, e o backend sempre ganha quando responde.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fabianocruz added a commit that referenced this pull request Sep 3, 2026
…/nanoid/qs

Unblocks the CI security-audit step, red on every PR since the new
advisories landed (same failure on PR #124's run; main's last CI run
predates them). Lockfile-only, all within existing semver ranges;
turbo build 18/18 and test 36/36 green after the bump.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BtvPxidmzsA22yoVJSWdba
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.

1 participant