Merge branch 'fix/lab-agent-telegram-horario' into main
Documenta el cambio a Telegram (silencioso para info, hilos al recuperarse, log adjunto), la pausa nocturna 06:00-23:00 con su tensión sin resolver con la ventana de medusa-deploy, la activación del timer de systemd (60 min, bajado de 15 para reducir costo) con el gotcha de Environment=HOME= para systemd --user, y el costo mensual estimado actualizado.
This commit is contained in:
commit
56ab9d1862
1 file changed
+132
-16
@@ -8,9 +8,11 @@ como decisión de gobernanza — la lista blanca de abajo es lo que
|
|||||||
autoriza al agente a actuar sin intervención humana, y debe poder
|
autoriza al agente a actuar sin intervención humana, y debe poder
|
||||||
auditarse.
|
auditarse.
|
||||||
|
|
||||||
Estado: **script y prueba end-to-end validados en vivo (2026-08-26)**.
|
Estado: **timer de systemd ACTIVO desde 2026-08-26** (`lab-agent.timer`,
|
||||||
El timer de systemd (automatización continua) **todavía no está
|
usuario `devops`, cada 60 min entre 06:00–23:00 hora local — bajado de
|
||||||
activo** — pendiente de decisión explícita para habilitarlo.
|
15 a 60 min el mismo día para reducir costo). Script,
|
||||||
|
Telegram y las unidades systemd, todo validado en vivo antes de
|
||||||
|
activar.
|
||||||
|
|
||||||
## Lista blanca de acciones correctivas automáticas
|
## Lista blanca de acciones correctivas automáticas
|
||||||
|
|
||||||
@@ -172,20 +174,134 @@ no se purgaron.
|
|||||||
|
|
||||||
## Notificaciones
|
## Notificaciones
|
||||||
|
|
||||||
Canal: [ntfy](https://ntfy.sh) — sin sistema de notificación previo en
|
**Canal: Telegram Bot API** (reemplaza a ntfy, cambiado 2026-08-26 por
|
||||||
el lab, se eligió la opción sin infraestructura nueva (tópico público
|
preferencia — no por costo, ver sección de costo abajo). Bot creado
|
||||||
de ntfy.sh, nombre privado difícil de adivinar) para validar la prueba
|
manualmente vía [@BotFather](https://t.me/BotFather); el token vive en
|
||||||
end-to-end sin bloquear en instalar y exponer un contenedor propio.
|
`TELEGRAM_BOT_TOKEN_FILE` (dueño `devops`, `600` — **no** root-only,
|
||||||
Migrar a un ntfy self-hosted (mismo patrón que gitea/nginxproxymanager/
|
mismo criterio que `GITEA_TOKEN_FILE`: el servicio corre como `devops`
|
||||||
cloudflared) es un cambio de una sola variable (`NTFY_URL`) cuando se
|
por el login de suscripción, un archivo `root:root` le sería ilegible).
|
||||||
decida.
|
`TELEGRAM_CHAT_ID` no es secreto, va directo en `lab-agent.env`.
|
||||||
|
|
||||||
## Pendiente antes de activar el timer en continuo
|
El mensaje se manda como texto plano, sin `parse_mode`: el motivo que
|
||||||
|
devuelve Claude es texto libre y puede traer `_`, `*`, `[`, `]` — con
|
||||||
|
Markdown/MarkdownV2 esos caracteres rompen el parser de Telegram (`400
|
||||||
|
Bad Request`) si no se escapan con cuidado. Se prioriza entrega
|
||||||
|
confiable sobre formato.
|
||||||
|
|
||||||
|
**Validado en vivo 2026-08-26** con el mismo código que usa
|
||||||
|
`notify_send()` (lectura del token, `sendMessage` vía `curl
|
||||||
|
--data-urlencode`, formato `[title]\nmessage`): entrega confirmada
|
||||||
|
(`"ok":true`) y contenido recibido tal cual en Telegram. Gotcha
|
||||||
|
encontrado durante la prueba: el primer intento falló con `400 chat not
|
||||||
|
found` porque el `chat_id` pegado correspondía a otro bot — el `chat_id`
|
||||||
|
de un usuario es el mismo across bots, pero Telegram igual devuelve
|
||||||
|
`chat not found` si *ese bot puntual* nunca recibió un mensaje de esa
|
||||||
|
persona. Se resolvió confirmando el `chat_id` real vía `getUpdates` del
|
||||||
|
bot correcto.
|
||||||
|
|
||||||
|
**Formato:** cada notificación lleva un ícono según su naturaleza
|
||||||
|
(✅ acción ok, 🛑 acción falló/rechazada, 🚨 alerta de seguridad,
|
||||||
|
❌ error operativo, 👀 solo diagnóstico) y el log completo de esa
|
||||||
|
corrida va adjunto como documento (`sendDocument`), no solo como ruta —
|
||||||
|
útil para revisar desde el celular sin acceso a shell.
|
||||||
|
|
||||||
|
**Notificación silenciosa y respuestas en hilo (2026-08-26):** el nivel
|
||||||
|
`info` (nada elegible, todo diagnóstico rutinario) se manda con
|
||||||
|
`disable_notification=true` — no interrumpe; acciones, fallas y alertas
|
||||||
|
de seguridad sí suenan. Cuando se ejecuta una acción correctiva, el
|
||||||
|
`message_id` de Telegram queda guardado por recurso en `state.json`
|
||||||
|
(`telegram_message_id`); si ese recurso se recupera solo en una corrida
|
||||||
|
posterior, se manda un aviso silencioso como **respuesta** (hilo) al
|
||||||
|
mensaje original antes de limpiar el estado — agrupa visualmente todo
|
||||||
|
un incidente. Limitación conocida: si un humano desbloquea el recurso a
|
||||||
|
mano borrando su entrada de `state.json` (ver más abajo), el
|
||||||
|
`message_id` se pierde con esa entrada, así que ese caso puntual no
|
||||||
|
queda threadeado — solo el "se recuperó solo" está cubierto hoy.
|
||||||
|
Validado en vivo con el canario: acción real → `message_id` capturado →
|
||||||
|
canario parcheado a una imagen sana → reply en hilo confirmado,
|
||||||
|
silencioso, al recuperarse.
|
||||||
|
|
||||||
|
## Horario activo y costo
|
||||||
|
|
||||||
|
**Pausa nocturna (`ACTIVE_HOURS_START`/`END`, hora local del host):**
|
||||||
|
activo `06:00`–`23:00`, pausado `23:00`–`06:00`. El script chequea esto
|
||||||
|
como lo primero que hace tras inicializar logging — si está fuera de
|
||||||
|
horario, sale **antes** de tocar `kubectl`/`docker`/`curl` o invocar
|
||||||
|
`claude -p`, así que la pausa ahorra el costo completo de la corrida,
|
||||||
|
no solo la notificación. El timer de systemd además solo dispara dentro
|
||||||
|
de esa ventana (`OnCalendar=*-*-* 06..22:00:00`) — la doble
|
||||||
|
verificación es deliberada: si alguien edita la unidad de systemd sin
|
||||||
|
tocar `lab-agent.env` (o corre el script a mano), el chequeo interno
|
||||||
|
sigue protegiendo.
|
||||||
|
|
||||||
|
**Tensión sin resolver con la ventana de `medusa-deploy`:** la ventana
|
||||||
|
de auto-restart para `medusa-deploy` (`COMMERCE_BACKEND_WINDOW_*`,
|
||||||
|
`00:00`–`06:00`) **no se solapa** con el horario activo del agente
|
||||||
|
(`06:00`–`23:00`). Esto significa que, en la práctica, `medusa-deploy`
|
||||||
|
nunca es candidato real a auto-restart: la única ventana en la que la
|
||||||
|
política lo permitiría es exactamente cuando el agente está pausado y
|
||||||
|
ni siquiera corre. La regla sigue viva en el código (por si el horario
|
||||||
|
activo cambia más adelante) pero hoy es efectivamente inalcanzable.
|
||||||
|
Pendiente de decisión: mover la ventana de `medusa-deploy` a que caiga
|
||||||
|
dentro del horario activo, o aceptar que ese recurso queda cubierto
|
||||||
|
solo por diagnóstico/notificación manual.
|
||||||
|
|
||||||
|
**Estimación de costo mensual — actualizada 2026-08-26, frecuencia
|
||||||
|
bajada de 15 a 60 min explícitamente para reducir costo:**
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Corridas/día (17h activas ÷ 60 min) | 17 |
|
||||||
|
| Costo por corrida | ~US$0.04–0.12 — **más alto por corrida que a 15 min**: el cache de prompt de Claude Code dura ~1h, y con corridas espaciadas 60 min casi siempre expira entre una y la siguiente, así que la mayoría de las corridas pagan cache "fría" en vez de "tibia" |
|
||||||
|
| **Estimado mensual (× 30 días)** | **~US$20–60** (punto medio ~US$35) |
|
||||||
|
|
||||||
|
Ojo con la intuición: bajar la frecuencia 4× (68→17 corridas/día) NO
|
||||||
|
baja el costo 4× — el ahorro real es más cercano a 1.5–2× frente a la
|
||||||
|
estimación anterior (~US$60–65/mes), porque cada corrida individual
|
||||||
|
sale más cara al perder la ventaja del cache tibio. Sigue siendo menos
|
||||||
|
costo total, solo que no linealmente. Basado en pocas corridas reales
|
||||||
|
medidas — revisar el costo real después de la primera semana en
|
||||||
|
producción (Telegram/Gitea Actions no cambian este número, el costo es
|
||||||
|
100% de la API de Claude).
|
||||||
|
|
||||||
|
**Trade-off de latencia de detección:** con corridas cada 60 min en vez
|
||||||
|
de 15, un problema que recién cruza el umbral de 10 minutos puede
|
||||||
|
tardar hasta ~70 min en ser detectado y potencialmente corregido (antes
|
||||||
|
era hasta ~25 min). Aceptado a cambio del ahorro de costo — si en la
|
||||||
|
práctica resulta muy lento para algún incidente real, la frecuencia se
|
||||||
|
puede volver a subir sin tocar nada más (`OnCalendar` en
|
||||||
|
`lab-agent.timer`).
|
||||||
|
|
||||||
|
## Activación del timer (2026-08-26)
|
||||||
|
|
||||||
|
Habilitado tras confirmación explícita del propietario del lab, viendo
|
||||||
|
antes el costo mensual estimado. Unidad de usuario (`systemctl --user`,
|
||||||
|
corre como `devops`, no `root`), con límites de CPU/memoria (`CPUQuota`,
|
||||||
|
`MemoryMax`) — instalada en `/DATA/.config/systemd/user/`, no en el
|
||||||
|
`~/.config/...` de una shell interactiva.
|
||||||
|
|
||||||
|
**Gotcha encontrado y corregido en vivo:** el manager de `systemd
|
||||||
|
--user` de `devops` arranca con `HOME=/DATA` (el valor de
|
||||||
|
`/etc/passwd`), no `/DATA/AppData/home-devops` — ese override solo se
|
||||||
|
aplica al abrir una shell interactiva vía
|
||||||
|
`/etc/profile.d/zz-devops-home.sh`, que el manager de systemd nunca
|
||||||
|
carga. Sin corregirlo, `claude -p` habría buscado
|
||||||
|
`~/.claude/.credentials.json` en `/DATA/.claude/` (no existe) y la
|
||||||
|
autenticación habría fallado en cada corrida del timer, silenciosamente,
|
||||||
|
hasta la primera vez que alguien mirara el log. `lab-agent.service`
|
||||||
|
fija `Environment=HOME=/DATA/AppData/home-devops` explícitamente para
|
||||||
|
evitarlo. Se detectó y corrigió *antes* de habilitar el timer, corriendo
|
||||||
|
el servicio manualmente vía `systemctl --user start` con el entorno
|
||||||
|
real (no desde una shell interactiva) como último paso de validación.
|
||||||
|
|
||||||
|
También requirió `sudo loginctl enable-linger devops` (una vez) — sin
|
||||||
|
eso, `systemctl --user` ni siquiera conecta (no hay `/run/user/<uid>`
|
||||||
|
sin una sesión de login o linger activo).
|
||||||
|
|
||||||
|
**Pendiente, no bloqueante:**
|
||||||
|
|
||||||
- **Confirmación explícita del propietario del lab** para habilitar el
|
|
||||||
timer de systemd (Paso 4 original) — no se activa automáticamente
|
|
||||||
solo por haber pasado la prueba end-to-end.
|
|
||||||
- Unidad systemd corriendo como `devops` (no root), con límites de
|
|
||||||
CPU/memoria.
|
|
||||||
- Opcional: `GITEA_TOKEN_FILE` para activar la señal de "runner
|
- Opcional: `GITEA_TOKEN_FILE` para activar la señal de "runner
|
||||||
atascado" vía API de Gitea Actions (hoy se omite, degradación segura).
|
atascado" vía API de Gitea Actions (hoy se omite, degradación segura).
|
||||||
|
- Decidir la tensión con la ventana de `medusa-deploy` (ver arriba) —
|
||||||
|
sigue sin resolver, el timer se activó igual porque no bloquea el
|
||||||
|
resto de la lista blanca.
|
||||||
Reference in new issue
Block a user