Auditoría Suprema 2 · Cola config-ia: IA del workspace, drivers CNS y saneado de prompts
Cuándo
05-09-2026 · commit f5792359 (PR #506, v1.114.0). Cola MEDIA/BAJA del dominio config-ia en la Auditoría Suprema 2 (task #286): de 19 hallazgos del catálogo, 17 se cierran en este commit, 1 ya estaba cerrado en v1.112.0 (#16, whitelist Junos) y 1 se descarta tras verificación (#1). Implementado por un agente Opus en worktree con las decisiones fijadas por hallazgo; revisado, ajustado y verificado por Claude Fable 5.1 — el agente no pudo ejecutar nada por un fallo de aislamiento del harness, así que ruff, pytest y mypy los corrió la sesión principal sobre el worktree. 48 tests nuevos en tests/config_ia/test_cola_auditoria_config_ia.py.
Síntomas visibles
- La síntesis de IA del panel interno (informe de rendimiento, informe FinOps) devolvía la frase fija “AI analysis temporarily unavailable” como si fuera un veredicto real: nada distinguía un fallo del proveedor de un análisis genuino que dijera “todo va bien” (#5).
- Si un helper del informe de rendimiento o de costes fallaba a mitad de recogida, el
exceptlo tragaba en silencio y el informe salía incompleto sin avisar (#3). - Los dos SDK de la cadena de respaldo de IA (
google-genai, Anthropic) no llevaban timeout: un proveedor colgado se llevaba consigo, sin límite de tiempo, la petición web que lo había invocado (#6). - El log de fallo de esos mismos SDK podía arrastrar la URL con la API key o el cuerpo de la petición (#7).
- El bloque
monitors.alertsdel endpoint de métricas del workspace no reflejaba alertas reales (#8). - Con Valkey caído, las llamadas de pago del workspace a Gemma corrían sin ningún tope de gasto (#11).
- Una clave o contraseña escrita al estilo de la consola de un equipo de red —
enable secret 5 <hash>,username admin password 7 <hash>,snmp-server community <valor>— llegaba íntegra al LLM: el filtro de secretos solo cubría el formatoclave=valor(#12). - Una recomendación de la IA de diagnóstico marcada como riesgo ALTO, con comandos de escritura y sin comandos de vuelta atrás, se guardaba igual que una completa: la ausencia solo restaba 0,05 al score de validación (#13).
config/wsgi.pyapuntaba a un módulo de settings (config.settings.prod) que nunca existió: un arranque WSGI sin la variable de entorno puesta a mano reventaba conModuleNotFoundError(#2).config/settings/production.pypisaba con 10 MB el límite de 100 MB que fijabase.pypara el tamaño de una petición sin ficheros — un cuerpo JSON/base64 grande pasaba en desarrollo y moría en producción conRequestDataTooBig(#4).
Causa raíz
AIOperations._call_aiconvertía el fallo del proveedor en una cadena de texto en vez de propagar la excepción: el llamador no podía distinguir “no hay análisis” de “el análisis dice que todo va bien”.- Los recolectores del informe (
_collect()y sus helpers) no separaban “no se pudo recoger esta parte” de “no había nada que recoger”. - Ningún cliente SDK de la cadena de respaldo llevaba timeout ni límite de reintentos, y sus mensajes de error se logueaban sin pasar por el redactor de secretos ya existente en el proyecto.
- El filtro de secretos (
redact_secrets) solo reconocía el formatoclave: valor/clave=valor; la sintaxis de consola de red, con clave y valor separados por un espacio, quedaba fuera. - El validador de diagnósticos trataba la ausencia de rollback en una recomendación de riesgo ALTO como un aviso menor, no como motivo para invalidarla.
core/ratelimit.py::fixed_window_exceededfallaba siempre abierto (deja pasar la petición) cuando el caché no respondía — correcto para topes de disponibilidad, pero no para un tope que protege gasto real de dinero.config/wsgi.pyyconfig/settings/production.pyarrastraban un nombre de módulo que nunca existió y un valor que pisaba en silencio al debase.py, sin que nadie los hubiera revisado desde que se escribieron.
Fix aplicado
Commit f5792359 (PR #506):
AIOperations._call_aipropagaAIProviderErroren vez de devolver una cadena;core/workspace_api.pyla captura y responde conai_analysis: null+ai_errorde mensaje fijo, dejando el detalle real en el log redactado.- Los recolectores del informe de rendimiento/FinOps mueven su
excepta_collect(), que loguea el motivo redactado y añadepartial: true+errors: [<helper>]al payload. core/services/ai_providers/router.py: nueva constanteAI_TIMEOUT_SECONDS = 30; Anthropic contimeout/max_retries=1, google-genai conHttpOptions(timeout=ms); los mensajes de error de ambos SDK pasan porredact_secretsantes de loguearse.monitors.alertsdel endpoint de métricas del workspace pasa a construirse desdeAlertEventsinresolved_ata nivel plataforma; el payload declara enunavailablelas claves de API que Pro no puede medir.- Los prompts de
AIOperationsenvuelven los datos conwrap_untrusted— misma defensa que ya tenían los prompts de CNS —; se retiranexecutive_summaryyclassify_incident, sin llamadores en el resto del repo. core/ratelimit.py::fixed_window_exceededadmite el parámetrofail_open: bool; el cap de llamadas de pago del workspace lo pasa aFalse— con Valkey caído, el gasto se bloquea en vez de correr sin tope. El resto de llamadores del proyecto sigue fail-open.monitoring/services/ai_providers/sanitize.py::redact_secretsañade tres patrones para el formato CLI con espacio:password|secret|psk|… <valor>(con el prefijo numérico de cifrado tipo Cisco opcional),tacacs-server key/radius key/isakmp key, yntp authentication-key N md5 <valor>. La palabrakeysuelta se deja sin redactar a propósito, para no tapar frases como “key features”.monitoring/services/ai_providers/diagnosis_validator.py: una recomendación HIGH con comandos y sin rollback pasa aerrors(marcais_valid=False), resta 0,3 al score y vacíaadjustments["commands"]— porquecreate_insightpersiste el insight aunque no sea válido. MEDIUM sigue como aviso, pero con −0,15 en vez de −0,05.- Los modelos Pydantic de diagnóstico normalizan
confidence_scorea [0, 1] yrisk_levela mayúsculas validado contra LOW/MEDIUM/HIGH;parse_llm_jsoncuenta solo comillas no escapadas al reparar JSON truncado; los driversclaude.pyyollama.pyadoptanparse_llm_jsoncon guard de forma, como ya teníagoogle_genai.py;gemini.pyredacta sus logs y toleraresponse.text=None. is_on_topic(filtro de temas del Tutor): los términos de 3 caracteres o menos casan por palabra completa vía regex precompilada, así que una frase como “el mapa del tesoro” deja de colar por contener “ap”.config/wsgi.pyapunta aconfig.settings.production(el móduloprodnunca existió);config/settings/production.pydeja de redefinirDATA_UPLOAD_MAX_MEMORY_SIZE, así que prevalece el valor de 100 MB debase.py.
Lecciones
- Un helper que “no puede fallar” —
_call_aidevolviendo un string fijo en vez de propagar la excepción — es la forma más barata de que un fallo de proveedor externo se disfrace de veredicto: cuando la IA falla, el llamador necesita SABER que falló, no recibir una frase que parece una respuesta. - Un filtro de secretos escrito para un formato (
clave=valor) no cubre gratis el formato hermano (clave valor) de otra fuente de datos; cada sintaxis de entrada nueva necesita su propio patrón. - Fail-open es la opción correcta por defecto porque prioriza disponibilidad, pero cualquier tope que protege GASTO real necesita fail-closed explícito — es una decisión por endpoint, no una política global.
- Una constante de configuración que pisa a otra en un módulo distinto (
production.pysobrebase.py) queda invisible hasta que el límite real hace falta en producción; el mismo valor en un solo sitio evita la sorpresa.
Preventivos futuros
- Descartado por un verificador: #1 (GUC de RLS en WebSocket/Huey) — falla cerrado y el rol ya lleva bypass explícito, no era una brecha real.
- Ya resuelto antes de este PR: #16 (
delete interfacesen la whitelist Junos, v1.112.0). - Límite honesto declarado en el propio CHANGELOG del commit: los patrones nuevos de
redact_secretsusan\s+, que también casa un salto de línea — una palabra clave al final de una línea puede tapar la primera palabra de la siguiente. - Sin migraciones ni cambios en
static/jsen este commit.
Véase también
- [[feature—config-ia—auditoria-suprema-etapa-3]]
- [[decision—20260611—auditoria-suprema-etapa-3-cierre-findings-m1-b6-e]]
- [[feature—monitoring—auditoria-suprema-sa3-cns-insights-ia]]
- [[incident—20260904—auditoria-suprema-2-cola-monitoring-a-sondas-y-targets]]
- [[incident—20260904—auditoria-suprema-2-cola-monitoring-d-deep-discovery-wireless-ups]]
- [[incident—20260904—auditoria-suprema-2-cola-racks-libreria-backup-restore]]
- [[incident—20260901—auditoria-suprema-2-tanda8-workspace-token-doctor-sin-acotar]]
- [[entity—core—middleware—rate-limit]]
- [[workspace—seguridad—auditoria-suprema-informe]]
- [[concept—auditoria-suprema—plan-y-metodo]]