Secure AI Atlas mark Secure AI Atlas SECURITY & GOVERNANCE

Learning Log

Construyendo un pipeline de inteligencia editorial: ingesta de señales, revisión doctrinal y publicación en Secure AI Atlas

Cómo Secure AI Atlas convierte señales externas de seguridad en contenido revisado y publicado, mediante un pipeline de siete scripts con bandas de riesgo explícitas, resolución de conflictos y una capa de triage local/cloud todavía en evolución.

pipeline automatizacion llm-ops gobierno-editorial ollama deepseek

Por qué existe esto

Secure AI Atlas no es solo un sitio de contenidos — es un intento de gestionar el proceso editorial de una base de conocimiento sobre seguridad de la misma forma que lo haría una redacción pequeña y disciplinada, con la diferencia de que la mayor parte de esa redacción es código. Entran señales externas (feeds, noticias, búsqueda genérica), se clasifican contra una taxonomía de amenazas propia (TR-*), una revisión doctrinal decide si cambian lo que el sitio afirma como verdad, y solo entonces se publica algo.

El problema de ingeniería interesante no es “llamar a un LLM y publicar la salida”. Es: ¿cómo se construye un pipeline donde un modelo de lenguaje puede participar en la clasificación y en el borrador, sin que nunca sea él quien decide en silencio qué se convierte en verdad pública en un portal de seguridad?

Esta entrada documenta la forma actual de ese pipeline, los seis fallos estructurales que se detectaron y corrigieron en una auditoría v2, y lo que hemos aprendido usando modelos locales como primer filtro delante de un modelo en la nube.

Los siete scripts y las cuatro bandas de riesgo

El sistema se organiza en siete scripts, cada uno con una única responsabilidad, conectados mediante un STATE_DIR compartido donde cada script escribe solo en su propio subdirectorio y lee contratos JSON de los demás. Ningún script puede escribir fuera de su carril.

01 atlas-intelligence-cycle   → radar externo: ingesta, clasificación, decide acción
02 atlas-web-scan             → foto de lo realmente publicado
03 atlas-conceptual-scan      → diferencia entre la web y la verdad doctrinal
04 atlas-conceptual-review    → decisiones doctrinales sobre candidatos conceptuales
05 atlas-source-review        → promociona o rechaza fuentes candidatas
06 atlas-editorial-apply      → único script autorizado a tocar src/content
07 atlas-publish-release      → git push, redes sociales, correo

Lo que convierte esto en algo más que un pipeline lineal es que cada script pertenece a una de cuatro bandas de riesgo, y un script de banda superior nunca puede dispararse automáticamente como efecto colateral de uno de banda inferior:

  • Observación (web-scan, conceptual-scan) — solo lectura, autonomía total, cron sin supervisión humana.
  • Inteligencia externa (intelligence-cycle) — la ingesta y la clasificación corren sin supervisión; solo el paso final de “decidir acción” toca el límite de la siguiente banda.
  • Decisión doctrinal (conceptual-review, source-review) — las decisiones se generan automáticamente, pero se aplican únicamente detrás de un gate humano, durante la fase actual. Solo un criterio de salida objetivo y definido (30 días de histórico revisado, tasa de discrepancia <5%, cero incidentes de aprobación revertida) abre camino a relajar ese gate — y aun entonces, las decisiones de baja confianza siguen exigiendo un humano.
  • Acción pública (editorial-apply, publish-release) — siempre bajo demanda, siempre con confirmación explícita, sin ningún camino de autonomía definido en esta versión.

El orquestador (atlas-run-all) impone esto: ejecutará toda la cadena de inteligencia externa sin supervisión, incluido el paso que propone crear contenido, pero nunca encadenará esa propuesta hacia editorial-apply sin un --confirm-publish-chain --reason "..." explícito. Esa única regla hace la mayor parte del trabajo de seguridad del sistema — no es alineación del modelo ni ingeniería de prompts, es simplemente “un script de banda más arriesgada no puede ser un efecto colateral”.

Recorriendo la cadena

01 — atlas-intelligence-cycle son cinco jobs internos: ingest-signals (lectura deduplicada de RSS/Atom de las fuentes registradas, sin clasificar) → agentic-review preliminary (filtro de relevancia por palabras clave, determinista por defecto, con LLM opcional para severidad/relevancia más fina) → generic-search (condicional — solo corre cuando una señal necesita más contexto, o semanalmente para descubrimiento de fuentes, nunca como rastreo abierto) → agentic-review final (fusiona contexto, asigna severidad y confianza, marca candidatos doctrinales) → decide-action.

decide-action es la pieza que no existía como especificación formal antes de v2. Se construye deliberadamente por simetría con el modelo de decisión de conceptual-review: seis resultados posibles (create_content, update_content, flag_for_conceptual_review, watch, ignore, needs_human_review), sobre el mismo principio rector — una señal externa no se convierte directamente en contenido publicado. Su única regla no negociable: una señal de alta severidad procedente de una fuente que todavía no ha pasado source-review se fuerza a needs_human_review, sin excepción, incluso con el LLM activo. Es la misma forma de regla que la tabla de resolución de conflictos del script 04 — el sistema vuelve a derivar el mismo instinto en cada capa: sin validar + alto impacto = decide un humano, sin excepciones diseñadas alrededor.

02 — atlas-web-scan es deliberadamente el script más aburrido del sistema, y ese es justamente el objetivo. Recorre las cinco colecciones de contenido (blog, risks, controls, frameworks, learning-log), calcula el hash de cada fichero y lo clasifica como new/changed/carried/removed frente a un índice persistente. Nunca toca doctrina, nunca llama a un modelo, nunca toca red más allá del sistema de ficheros local. Su único reto de diseño real fue distinguir una primera ejecución legítimamente vacía de una pérdida de índice — resuelto comprobando si existe histórico de ejecuciones previas antes de confiar en un índice vacío, y negándose a reconstruirlo en silencio sin advertencia.

03 — atlas-conceptual-scan compara la foto de la web contra el repositorio de verdad y produce señales doctrinales (missing_concept, uncovered_doctrinal_tag, etc.). Aquí vive el primero de los seis fallos corregidos en v2 (ver más abajo): fingerprint por concepto en lugar de un único hash global.

04 — atlas-conceptual-review es el guardián doctrinal. Su implementación actual es conservadora por diseño: valida el análisis conceptual ya generado contra un contrato de evidencia estricto (quote_ids), aplica seis invariantes deterministas (evidencia insuficiente limita la confianza a 0,4; ausencia de delta conceptual corta el flujo; evidencia sin IDs de cita verificables se elimina; el contenido promocional se limita con independencia de lo que diga el modelo; las contradicciones exigen una proposición literal de verdad más una afirmación literal incompatible), y encola todo para revisión humana. No escribe verdad, changelog ni contenido — esa autoridad todavía no se ha concedido en esta fase.

05 — atlas-source-review decide si una fuente candidata se promociona al registro oficial, mediante una puntuación completamente determinista (HTTPS, coincidencia de categoría, apariciones repetidas, score histórico, salud del feed) frente a umbrales fijos. Ningún modelo interviene en la implementación actual — la especificación reserva explícitamente espacio para uno más adelante, pero con una regla dura: un modelo nunca puede promocionar una fuente por sí solo.

06 — atlas-editorial-apply es el único script autorizado a tocar src/content, y es el punto de convergencia de dos cadenas de decisión independientes — el decision.json de la cadena de inteligencia externa y el editorial-impact.json de la cadena doctrinal. Esta convergencia es exactamente donde v1 tenía un bug silencioso (más abajo): nada impedía que ambas cadenas apuntaran al mismo fichero en la misma ejecución.

07 — atlas-publish-release es el último gate: validación de build, borrador social, git push, Bluesky, correo — cada uno detrás de su propio flag de habilitación, con rollback automático de Git si una etapa posterior de la cadena falla, y seguimiento manual marcado explícitamente para los canales que no se pueden despublicar (no se puede deshacer una publicación en Bluesky).

Los seis fallos estructurales que corrigió v2

Una auditoría de arquitectura sacó a la luz seis brechas reales en v1, ninguna exótica — las seis eran instancias del mismo patrón de fallo subyacente: el sistema asumía que un caso no ocurriría, en lugar de especificar qué pasa cuando ocurre.

  1. Sin regla de prioridad cuando una señal interna y un candidato externo apuntan al mismo concepto con evidencia distinta. Corregido con una tabla explícita de resolución en el script 04 (§15): los duplicados exactos se fusionan, la evidencia complementaria se fusiona, las contradicciones fuerzan revisión humana y bloquean la promoción — nunca se resuelven en silencio en ningún sentido.

  2. Sin regla de fusión en editorial-apply cuando ambas cadenas de decisión tocan el mismo fichero. Esta fue la brecha más consecuente, porque es el único punto donde las dos cadenas realmente se encuentran. Corregida con una fase obligatoria detect-overlap antes de cualquier escritura: las acciones compatibles (una revisión de contenido más la adición de una etiqueta) se fusionan en orden; dos revisiones de contenido independientes sobre el mismo fichero, o dos propuestas independientes de “crear este fichero”, siempre hacen fallar la ejecución en lugar de adivinar cuál gana.

  3. Sin mecanismo de rollback doctrinal. Si un concepto aprobado resultaba erróneo más tarde, no había camino de vuelta definido. Corregido con un campo rollback_of en el changelog y un procedimiento de reversión capaz de generar acciones editoriales revert_reference.

  4. El fingerprint global de verdad forzaba un reanálisis completo del portal ante cambios cosméticos. Una simple corrección de una errata en cualquier parte del documento de verdad (26+ páginas) invalidaba todas las páginas. Corregido con fingerprint por concepto: solo se reanalizan las páginas cuyo concepto TR-XXX relacionado cambió realmente; un reformateo sin ningún cambio doctrinal ahora dispara cero reanálisis.

  5. La pérdida silenciosa del índice persistente no tenía detección activa, solo una prohibición declarada. Corregida con una comprobación: si existe histórico de ejecuciones previas pero el índice actual está vacío o ha caído más de un umbral configurable (por defecto, 50%) sin justificación, la ejecución falla con index_loss_suspected en lugar de hacer silenciosamente un full rescan que nadie pidió.

  6. Sin política de circuit-breaker o timeout a nivel de orquestador, solo por script. Corregida con un timeout de cadena, terminación segura del proceso (SIGTERM y luego SIGKILL), y — de forma crítica — la regla de límite de banda descrita arriba, que en realidad es un circuit-breaker de riesgo, no de tiempo.

Lo que seguimos aprendiendo: modelos locales como capa de triage

Parte del trabajo en curso consiste en averiguar cuánta clasificación y revisión puede absorber un modelo local (corriendo en una RTX 5070 vía Ollama), para preservar la cuota de servicios en la nube (DeepSeek, y Codex para trabajo arquitectónico) para los casos que realmente lo necesitan.

El benchmarking realizado hasta ahora ha producido algunas conclusiones sólidas, aunque no todas las cifras estén todavía cerradas:

  • GPT-OSS:20b resultó ser el candidato local más fuerte, pero explícitamente en un rol de review_candidate — una primera opinión, nunca una autoridad. Su salida puede escalar una decisión pero nunca rebajar una decisión basada en reglas; un modelo no puede convertir publish en watch o ignore, por muy segura que declare estar. Esa asimetría existe precisamente para que manipular o engañar a un modelo local no pueda usarse para bajar el escrutinio sobre una señal, solo para subirlo.
  • Se probó y se descartó un gate binario duro (modelo local como filtro sí/no antes de DeepSeek) tras un falso negativo costoso — una señal relevante que el modelo local descartó de plano nunca llegó al modelo en la nube. El diseño que lo sustituyó es escalada condicional: condiciones de disparo concretas (una invariante que salta de forma inesperada, una contradicción detectada, un candidato conceptual nuevo, una señal promocional con veredicto no nulo, un concept_id inventado, o la infraestructura local caída) enrutan el caso hacia DeepSeek en lugar de confiar en el juicio local. La lección se generaliza: un modelo local es útil como evidencia, arriesgado como guardián único.
  • Un bug silencioso de infraestructura produjo lo que parecía un fallo del modelo. El num_ctx por defecto de Ollama truncaba los prompts sin ningún aviso, y la entrada truncada generaba salidas que parecían alucinaciones. La solución no fue un mejor prompt ni un mejor modelo — fue comprobar la configuración de la ventana de contexto antes de atribuir nada a la calidad del modelo. Probablemente esta sea la lección más transferible de todo el ejercicio: cuando la salida de un modelo local parece inexplicablemente mala, revisa la fontanería antes de culpar al modelo.

[pendiente: cifras exactas del benchmark — tasas de discrepancia, número de falsos negativos, comparativas de latencia — a rellenar a partir de los informes de benchmark subyacentes]

Lo que realmente significa aquí “humano en el bucle”

Sería fácil describir este sistema como “la IA hace el trabajo, un humano lo aprueba”, pero eso minimiza lo que realmente se impone. El gate no es una casilla al final — es estructural:

  • Un modelo puede proponer, pero una regla siempre decide si esa propuesta es siquiera elegible para aplicarse (estado de validación de la fuente, cumplimiento del contrato de evidencia, comprobación de invariantes).
  • Dos cadenas de decisión independientes nunca pueden sobrescribirse en silencio sobre el mismo fichero.
  • Nada cruza de “decisión doctrinal” a “acción pública” sin una confirmación explícita, razonada y registrada — no por defecto, no por timeout, no solo por una puntuación de confianza alta.
  • El vocabulario tiene que coincidir entre scripts (type, target, priority) precisamente para que la lógica de detección de solapamiento entre dos cadenas que corren de forma independiente pueda siquiera comparar sus salidas — un requisito de gobierno expresado como requisito de contrato de datos.

Ese último punto resultó importar más de lo que parece: si la cadena de inteligencia externa y la cadena doctrinal usaran palabras distintas para el mismo tipo de acción, toda la fase detect-overlap del script 06 — la pieza construida específicamente para evitar colisiones silenciosas de contenido — simplemente no podría funcionar. La consistencia terminológica no es una preferencia de estilo en este sistema; es un requisito estructural.

Hilos abiertos

  • Terminar la capa de triage/mapeo entre señales TR-X clasificadas y acciones de catálogo (matched_entry_id | new_candidate | needs_human_review), con un umbral deliberadamente más alto para crear una entrada nueva que para actualizar una existente, y aprobación humana obligatoria para cualquier entrada genuinamente nueva.
  • Cerrar las tres brechas restantes del script 01: imponer needs_human_review en código (no solo en la especificación) para fuentes de alta severidad sin validar, implementar merge_model_review_with_rules(), y retirar el vocabulario heredado de acciones (draft_blog, update_frameworks) que actualmente impide que detect-overlap vea correctamente las decisiones de intelligence-cycle.
  • Publicar aquí mismo, en el Learning Log, la propia metodología de triage local-vs-cloud, incluyendo los fallos — el falso negativo del gate binario y el bug de la ventana de contexto son más útiles para un lector técnico que una historia de éxito limpia.

El hilo conductor de todo esto: incrementalidad, modos de fallo explícitos en lugar de ausencia asumida de casos límite, y una separación estricta entre detección, decisión, edición y publicación que ninguna capacidad de modelo tiene permitido volver a colapsar en un único paso.