CreaRack-SL

Fix: Vigilante del auto-update funciona por fin (Agent 2.13.3, causa raíz cazada)

Descripción

Versión: Agent 2.13.3 (APP v1.45.9) — commit be210c6 (05-07-2026 11:24 UTC)
Componente: Watchdog (vigilante) post-swap del auto-update (terminal/agent/core/updater.py)
Cambio: Corrección de flags de creación de proceso Windows que hacía morir silenciosamente al vigilante.

El watchdog que debería relanzar automáticamente el Agente si una actualización falla o revertir a la versión anterior nunca llegaba a ejecutarse. Causa raíz: conflicto entre flags DETACHED_PROCESS y CREATE_NO_WINDOW en subprocess.Popen, que privaba a PowerShell de una consola válida → muere antes de la primera línea del script.


Lo que cambia

Antes (Agent ≤2.13.2)

# updater.py:_launch_swap_watchdog()
proc = subprocess.Popen(
    [ps_exe, "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-File", str(ps1)],
    creationflags=subprocess.DETACHED_PROCESS
    | subprocess.CREATE_NEW_PROCESS_GROUP
    | getattr(subprocess, "CREATE_NO_WINDOW", 0),
)

Síntoma:

  • El watchdog PID se registra en update_watchdog.log (“watchdog lanzado (pid 20072)”).
  • Pero el script PowerShell jamás escribe su primera línea.
  • El vigilante muere silenciosamente; el Agente permanece muerto si el AV bloqueó el arranque.
  • Observado en incidentes 03-07 y 05-07-2026 (recovery manual necesario).

Después (Agent 2.13.3+)

# updater.py:_launch_swap_watchdog()
proc = subprocess.Popen(
    [ps_exe, "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-File", str(ps1)],
    creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | getattr(subprocess, "CREATE_NO_WINDOW", 0),
    stdin=subprocess.DEVNULL,
    stdout=subprocess.DEVNULL,
    stderr=subprocess.DEVNULL,
    close_fds=True,
)

Cambios:

  1. ❌ Quitar: subprocess.DETACHED_PROCESS (anula CREATE_NO_WINDOW — modos excluyentes).
  2. ✅ Mantener: CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW (consola propia, oculta).
  3. ✅ Añadir: stdin/stdout/stderr=DEVNULL (PowerShell obtiene handles válidos).

Comportamiento:

  • El script PowerShell inicia correctamente.
  • El proceso sobrevive al padre (Windows permite hijos sin padre).
  • El vigilante monitorea /health, relanza si es necesario, revierte si todo falla.

Verificación

Diagnóstico reproducido en laboratorio

El commit incluye una sección en terminal/agent/version.py documentando la reproducción del problema fuera del Agente:

  • Mismo setup de flags (DETACHED_PROCESS + CREATE_NO_WINDOW) → PowerShell muere.
  • Nuevo setup (sin DETACHED_PROCESS, + DEVNULL) → PowerShell corre entero aunque el padre haga os._exit(0) al instante.

Test de regresión

Archivo: tests/agent/test_agent_updater_trace.py::test_watchdog_launch_sin_detached_process

  • Inspecciona el source de _launch_swap_watchdog.
  • Verifica ausencia de DETACHED_PROCESS.
  • Verifica presencia de CREATE_NO_WINDOW y DEVNULL.
  • Nota: El test es por inspección (CI Linux no tiene flags de Windows).

Impacto en la flota

VersiónWatchdogAuto-update fallido → Resultado
≤2.13.2❌ Mudo (muere silenciosamente)AV bloquea → Agente muerto hasta relanzar a mano
2.13.3+✅ Vivo y funcionalAV bloquea → Watchdog relanza o revierte automáticamente

⚠️ Transición hacia 2.13.3

  • El updater aún es viejo → watchdog sigue mudo durante ese swap.
  • Si el AV bloquea 2.13.3, es necesario un relanzamiento manual una última vez.
  • Desde 2.13.3 en adelante, todos los swaps posteriores usan el nuevo vigilante.

Detalles técnicos

¿Por qué DETACHED_PROCESS mata el watchdog?

En Windows, subprocess.Popen(creationflags=...) acepta un conjunto de flags de CreateProcessW:

  • DETACHED_PROCESS (0x00000008): el nuevo proceso se desacopla del proceso padre (sin consola heredada).
  • CREATE_NO_WINDOW (0x08000000): el nuevo proceso abre una ventana de consola privada pero la oculta.

Son mutuamente excluyentes en la práctica: si usas ambos, Windows prioriza DETACHED_PROCESS e ignora CREATE_NO_WINDOW. El resultado es un proceso sin consola válida.

Cuando Python no proporciona handles explícitos (stdin/stdout/stderr):

  • DETACHED_PROCESS deja los handles sin inicializar.
  • PowerShell.exe en su host initialization necesita escribir en la consola estándar.
  • Sin handles válidos, SetConsoleMode falla → PowerShell aborta antes de cargar el script.

¿Por qué funciona con CREATE_NO_WINDOW + DEVNULL?

  • CREATE_NO_WINDOW sin DETACHED_PROCESS abre una consola invisible (válida internamente).
  • stdin=DEVNULL, stdout=DEVNULL, stderr=DEVNULL proporciona handles de dispositivo nulo (siempre válidos).
  • PowerShell inicializa correctamente; los hijos en Windows permanecen vivos aunque el padre haga os._exit(0).

Código afectado

ArchivoLíneasCambio
terminal/agent/core/updater.py249–269Flags de subprocess.Popen en _launch_swap_watchdog()
terminal/agent/version.py11–23Documentación de causa raíz en comentarios
tests/agent/test_agent_updater_trace.py53–69Test de regresión (src inspection)
config/settings/base.py10APP_VERSION 1.45.8 → 1.45.9
CHANGELOG.md9–15Entrada de release
RELEASE_NOTES.md12–22Nota técnica para usuarios

Cimiento arquitectónico

Esta es una corrección de bug crítico del cimiento agent-robustez F2 (introducido en 2.11.4):

El watchdog post-swap monitorea que la versión nueva responde (/health); si el AV la mató al arrancar, la relanza; si tampoco arranca, restaura la versión anterior — el auto-update ya no puede dejar un Agente muerto.

Hasta hoy, el watchdog no corría de verdad por el conflicto de flags. Ahora sí.


Véase también

  • [[incident—20260705—watchdog-auto-update-mudo]]
  • [[decision—20260705—flags-createprocess-windows-watchdog]]
  • [[entity—terminal—service—updater-swap-watchdog]]
  • [[concept—infra—auto-update-resilience]]
  • [[runbook—terminal—diagnose-agent-update-failure]]