@@ -25,7 +25,7 @@ import {
2525 type LinhaDeBinding ,
2626} from "@/lib/ai/pontos/resolver" ;
2727import { PAPEIS , PONTOS_DE_IA , PONTO_POR_ID } from "@/lib/ai/pontos/registro" ;
28- import { PROVEDORES } from "@/lib/ai/pontos/provedores" ;
28+ import { PROVEDORES , ehProvedorSuportado } from "@/lib/ai/pontos/provedores" ;
2929import { validarBinding } from "@/lib/ai/pontos/validar-binding" ;
3030import { createClient } from "@/lib/supabase/server" ;
3131
@@ -121,8 +121,18 @@ export async function GET(): Promise<Response> {
121121 exige : ponto . exige ,
122122 sintomaDeFalha : ponto . sintomaDeFalha ,
123123 fixo : ponto . fixo ?? null ,
124- /** Escolha do agente publicado — a tela mostra como leitura, com link. */
125- mandadoPeloAgente : PONTOS_DO_AGENTE_PUBLICADO . has ( ponto . id ) ,
124+ /**
125+ * Escolha do agente publicado — a tela mostra como leitura, com link.
126+ *
127+ * Depende de EXISTIR versão publicada: é a mesma condição que o resolvedor
128+ * usa (`resolver.ts` exige `agentePublicado !== null`). Sem o `&&`, uma
129+ * instalação recém-feita — nenhum agente publicado ainda — abria o painel
130+ * com os DOIS pontos que respondem o cliente sem seletor, dizendo que são
131+ * governados por uma versão publicada que não existe e mandando
132+ * configurar num lugar vazio. É a primeira tela da feature; travá-la no
133+ * primeiro uso é o pior lugar para esse defeito estar.
134+ */
135+ mandadoPeloAgente : agentePublicado !== null && PONTOS_DO_AGENTE_PUBLICADO . has ( ponto . id ) ,
126136 efetivo : {
127137 provider : decisao . provider ,
128138 modelId : decisao . modelId ,
@@ -156,7 +166,19 @@ export async function GET(): Promise<Response> {
156166
157167const corpoDoPut = z . object ( {
158168 purpose : z . string ( ) . min ( 1 ) ,
159- provider : z . string ( ) . min ( 1 ) ,
169+ // A migration 0127 removeu os CHECKs do banco dizendo que "a garantia de que
170+ // a tela não oferece opção inválida passa a morar" na lista de provedores —
171+ // mas a lista não era aplicada em NENHUM ponto de escrita. Um PUT direto (e a
172+ // API é pública) gravava `provider: "foobar"`, a rota respondia 200, e todo
173+ // uso daquele ponto morria em produção com provedor desconhecido. Metade da
174+ // defesa transferida e nunca instalada.
175+ provider : z
176+ . string ( )
177+ . min ( 1 )
178+ . refine ( ehProvedorSuportado , {
179+ message :
180+ "provedor não suportado por esta instalação — escolha um da lista em Agente de IA → Provedores" ,
181+ } ) ,
160182 model_id : z . string ( ) . min ( 1 ) ,
161183 credential_id : z . string ( ) . uuid ( ) . nullable ( ) . optional ( ) ,
162184 base_url : z . string ( ) . url ( ) . nullable ( ) . optional ( ) ,
@@ -240,7 +262,7 @@ export async function PUT(req: NextRequest): Promise<Response> {
240262 } ,
241263 { onConflict : "organization_id,purpose" } ,
242264 )
243- . select ( "purpose, provider, model_id, credential_id, base_url, is_enabled" )
265+ . select ( "id, purpose, provider, model_id, credential_id, base_url, is_enabled" )
244266 . maybeSingle ( ) ;
245267
246268 if ( error ) return fail ( "save_failed" , error . message , 500 ) ;
@@ -255,11 +277,19 @@ export async function PUT(req: NextRequest): Promise<Response> {
255277 organizationId : org . orgId ,
256278 actorUserId : user . id ,
257279 resourceType : "ai_purpose_binding" ,
258- resourceId : corpo . purpose ,
280+ // O ID DA LINHA, não o `purpose`. `api_audit_log.resource_id` é **uuid**, e
281+ // `purpose` é texto (`stage_classifier`): o INSERT falhava com 22P02
282+ // (`invalid input syntax for type uuid`) e — como o audit é
283+ // fire-and-forget — o erro ia só para o log do servidor. Resultado: NENHUMA
284+ // troca de modelo era auditada, num painel cujo efeito é justamente mudar
285+ // para onde o dinheiro e os dados do cliente vão. Achado dirigindo a tela;
286+ // nenhum gate via, porque nada assertava a linha de auditoria.
287+ resourceId : ( gravado as { id ?: string } ) . id ?? null ,
259288 // O modelo entra no metadata, a credencial NÃO — só o id dela seria
260289 // inócuo, mas o hábito de mandar campo de credencial para o audit é o que
261290 // acaba vazando a chave quando alguém troca o campo de lugar.
262291 metadata : {
292+ purpose : corpo . purpose ,
263293 provider : corpo . provider ,
264294 model_id : corpo . model_id ,
265295 tem_endpoint_proprio : Boolean ( corpo . base_url ) ,
0 commit comments