feat(frontend): Platform > IA > Providers/Modelos/Uso/Custos + achado real de autorização

Providers/Modelos reaproveitam os endpoints /ai/providers e /ai/models já
existentes (só filtram scope GLOBAL na tela); Modelos expõe custo unitário
(inputCost/outputCost/audioCost) que a tela de tenant nunca mostra, porque só
platform precisa cadastrar preço. GET /platform/ai-usage (novo) agrega uso bruto de
AIUsageRecord de todos os tenants no mês corrente (Uso) e estima custo casando cada
registro com o AIModel correspondente (Custos) — quando não dá pra casar, o registro
fica de fora da soma e o tenant é marcado costIncomplete, nunca um número inventado.

Achado real de autorização ao revisar AIModelsController antes de construir a tela
de Modelos: POST/DELETE /ai/models não checava isPlatformUser quando o alvo era
scope=GLOBAL — como a RLS híbrida (OR tenant_id IS NULL) deixa qualquer tenant
ENXERGAR um provider/modelo GLOBAL, qualquer Tenant Admin com `ai.manage` (permission
de escopo TENANT) conseguia injetar um modelo no catálogo global ou desabilitar um
modelo GLOBAL só sabendo o id. O endpoint irmão (AIProvidersController) já tinha o
check certo; corrigido com o mesmo padrão. Confirmado com um teste de ataque real:
403 depois do fix (era 201/sucesso antes), com regressão confirmando que BYOK do
próprio tenant continua funcionando normalmente.

Testado ponta a ponta: provider+modelo GLOBAL com custo real, uso de teste inserido
direto no Postgres, /platform/ai-usage devolvendo o valor exato esperado (bate com a
conta manual), tudo removido no final.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
2026-08-30 08:53:35 -03:00
parent 12af12f276
commit cc80310cb5
20 changed files with 877 additions and 8 deletions

40
TODO.md
View File

@@ -1617,6 +1617,46 @@ Usuários (agente.md secao 169)
Puppeteer, mostrando a explicação por que falha aqui (mesmo texto
já usado em Infraestrutura > Saúde)
## PHASE 45 — Platform > IA > Providers/Modelos/Uso/Custos (agente.md
secao 96-103, 124, 169) + achado real de autorização em `/ai/models`
- [x] achado real (achado revisando `AIModelsController` antes de
escrever a tela de Modelos): `POST /ai/models` e `DELETE /ai/models/
:id` não checavam `isPlatformUser` quando o provider/modelo era
`scope=GLOBAL` — como a RLS híbrida de `ai_models`/`ai_providers`
(secao 100, `OR tenant_id IS NULL`) deixa qualquer tenant ENXERGAR
um provider GLOBAL, qualquer Tenant Admin com a permission
`ai.manage` (escopo TENANT) conseguia **injetar um modelo no
catálogo visível por todos os tenants**, ou **desabilitar** um
modelo GLOBAL só sabendo o id — mesma classe de escalação já
corrigida em `AgentsController` (PHASE 27) e já prevenida
corretamente em `AIProvidersController` (o irmão deste controller,
que já tinha o check certo). Corrigido com o mesmo padrão: 403
explícito quando o alvo é GLOBAL e quem chama não é platform.
Confirmado com um teste de ataque de verdade: tenant Acme tentando
anexar um modelo a um provider GLOBAL (403 depois do fix, 201 antes)
e apagar um modelo GLOBAL (403 depois, sucesso antes) — e um teste
de regressão confirmando que Acme continua livre pra gerenciar o
próprio BYOK
- [x] Platform > IA > Providers/Modelos reaproveitam os mesmos endpoints
`/ai/providers`/`/ai/models` já existentes (só filtram scope
GLOBAL na tela, backend já filtra por RLS+isPlatformUser); Modelos
expõe os campos de custo unitário (inputCost/outputCost/audioCost)
que a tela de tenant nunca mostra, porque só platform precisa
cadastrar preço
- [x] `GET /platform/ai-usage` (novo) — uso bruto de `AIUsageRecord` de
TODOS os tenants no mês corrente (Uso) + custo estimado casando
cada registro com o `AIModel` correspondente por
`providerId`+`externalModelId` (Custos). Quando não dá pra casar
(provider apagado, custo nunca cadastrado), aquele registro fica
de fora da soma e o tenant vem marcado `costIncomplete: true` —
nunca um número inventado (secao 138/233)
- [x] Testado ponta a ponta: criado provider GLOBAL + modelo com custo
real (US$0,00015/tokenin, US$0,0006/tokenout), inserido uso de
teste direto no Postgres (100k tokens in + 20k out), confirmado
`/platform/ai-usage` devolvendo US$27,00 exatos (bate com a conta
manual) e a tela de Custos mostrando o mesmo número — tudo removido
no final
---
## Riscos conhecidos