commerce.cruzcloud.net's root path now 301s to /app (Medusa admin) via
a Traefik Middleware — without -L the check reported a false 404 on an
otherwise healthy service. Generic fix (curl -L) instead of
hardcoding a per-domain path, since routing now owns the redirect.
OnCalendar cada 60 min en vez de 15 (06:00-22:00 hora local, 17
corridas/día en vez de 68). Aplicado en vivo: unidad actualizada,
timer reiniciado, próxima corrida confirmada vía
systemctl --user list-timers.
El manager de systemd --user de devops arranca con HOME=/DATA (el de
/etc/passwd), no /DATA/AppData/home-devops -- ese override solo se
aplica en una shell interactiva vía /etc/profile.d/zz-devops-home.sh,
que el manager de systemd nunca carga. Sin Environment=HOME=... en
lab-agent.service, claude -p habría buscado las credenciales en
/DATA/.claude/ (no existe) y fallado en cada corrida. Detectado y
corregido en vivo corriendo el servicio manualmente vía
systemctl --user start antes de habilitar el timer.
README documenta la instalación real: unidades en
/DATA/.config/systemd/user/ (no ~/.config/... de una shell
interactiva), loginctl enable-linger devops como único paso con sudo.
Timer activo desde 2026-08-26 (lab-agent.timer, cada 15 min,
06:00-23:00 hora local), tras confirmación explícita y ver el costo
mensual estimado.
log() pasa a escribir en stderr (transparente, sigue yendo al mismo
RUN_LOG vía el "exec > tee 2>&1"): necesario para que notify_send
pueda devolver el message_id por stdout sin que un log() interno se
cuele en la captura -- mismo patrón de bug que ya se había corregido
en execute_corrective_action.
notify_send ahora acepta un reply_to_message_id opcional y manda
disable_notification=true para nivel "info" (ruido de rutina no debe
sonar; fallas/acciones/alertas de seguridad sí). Cuando ejecuta una
acción correctiva, el message_id enviado se guarda por recurso en
state.json (attach_telegram_message_id); si ese recurso se recupera
solo en una corrida posterior, compute_eligibility manda un aviso
silencioso como respuesta al mensaje original antes de limpiar el
estado -- agrupa visualmente todo un incidente en un hilo.
Validado en vivo con el canario desechable: acción real -> message_id
capturado en state.json -> parcheado a imagen sana -> reply en hilo
confirmado al recuperarse, silencioso.
Reemplaza ntfy.sh por Telegram Bot API (preferencia del usuario, no
reduce el costo — el costo real es de la API de Claude/Sonnet, no del
canal). Texto plano sin parse_mode: el motivo que devuelve Claude es
texto libre y puede traer caracteres que rompen el parser Markdown de
Telegram. Token en TELEGRAM_BOT_TOKEN_FILE (dueño devops, no root-only
-- el servicio corre como devops por el login de suscripción, mismo
criterio ya aplicado a GITEA_TOKEN_FILE). Validado en vivo: entrega
confirmada con el mismo código de notify_send().
Agrega pausa nocturna (ACTIVE_HOURS_START/END, default 06:00-23:00
hora local) para no acumular costo de API sin necesidad: el script
chequea el horario ANTES de tocar kubectl/docker/curl o invocar
claude -p, no solo antes de notificar. Nota documentada: esta ventana
no se solapa con COMMERCE_BACKEND_WINDOW_* (00:00-06:00), por lo que
medusa-deploy queda efectivamente inalcanzable para auto-restart en la
práctica -- pendiente de decisión aparte.
Agrega unidades systemd de usuario (lab-agent/systemd/*.service,
*.timer) con límites de CPU/memoria: OnCalendar cada 15 min dentro del
horario activo, corre como devops (systemctl --user), sin habilitar
todavía -- requiere confirmación explícita tras ver el costo mensual
estimado (~US$60-65, documentado en agente-monitoreo.md).
Auth: ANTHROPIC_API_KEY_FILE queda vacío por defecto — claude -p usa el
login de suscripción ya activo (~/.claude/.credentials.json) en vez de
una API key root-only. Se descartó ese diseño en vivo al confirmar que
no había API key real disponible; además elimina la necesidad de crear
secretos root-only vía sudo. Consecuencia: el futuro servicio systemd
debe correr como devops, no root. CLAUDE_BIN/KUBECTL_BIN se agregan por
ruta absoluta (ninguno está en el PATH de root/systemd).
Bugs encontrados y corregidos durante la prueba end-to-end contra el
cluster real (deployment canario desechable):
- execute_corrective_action se invocaba en un subshell ($()), perdiendo
sus variables de resultado (LAST_ACTION_*) -> crash "unbound
variable". Ahora usa el exit status de la función, invocada directo.
- DRY_RUN marcaba blocked=true en state.json como si la acción hubiera
sido real, contaminando una corrida real posterior sobre el mismo
recurso. Ahora el estado solo se actualiza fuera de DRY_RUN.
También: GITEA_TOKEN_FILE pasa a /DATA (dueño devops, sin sudo), y
media.cruzcloud.net se excluye de PUBLIC_DOMAINS (uso personal, no es
parte del lab).
Prueba e2e completa documentada en apps-registry:
docs/arquitectura/agente-monitoreo.md.
Invoca claude -p con --allowedTools "" y --json-schema para que el
modelo solo devuelva una decisión estructurada, nunca ejecute comandos
directamente. Se descartó dar un Bash tool con patrón por prefijo tras
confirmar en vivo que --allowedTools "Bash(cmd)" también permite
"cmd && inyectado" (coincidencia por prefijo, no exacta) — inseguro
para comandos root-capaces como kubectl/docker. El wrapper bash es el
único ejecutor, y revalida cada decisión contra su propio cálculo de
elegibilidad (umbral 10min, rate limit 30min, ventana horaria para
medusa-deploy, bloqueo tras intento sin recuperación) antes de actuar.
Validado en vivo (dry-run) contra el cluster real hasta el paso de
invocar claude -p, que falla correctamente por falta del secreto
ANTHROPIC_API_KEY_FILE (no creado a propósito, ver README).