Files
devops 09048dd772 Bajar frecuencia del timer a 60 min para reducir costo
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.
2026-08-26 00:27:53 -05:00
..

lab-agent — agente de diagnóstico/corrección del lab

Wrapper headless que corre claude -p para diagnosticar el estado del lab (kubectl, docker, curl a endpoints públicos, API de Gitea Actions) y, solo para un conjunto explícito y aprobado de recursos, ejecutar una acción correctiva de bajo riesgo. Ver la lista blanca completa, la justificación de gobernanza y la evidencia de la prueba end-to-end en apps-registry → docs/arquitectura/agente-monitoreo.md.

Estado (2026-08-26): timer de systemd ACTIVO (lab-agent.timer, usuario devops, cada 60 min entre 06:00–23:00 hora local — bajado de 15 a 60 min el 2026-08-26 para reducir costo). Validado en vivo contra el cluster real: DRY_RUN → ejecución real → rate limit/bloqueo → recuperación con hilo en Telegram → limpieza, y corrida manual vía systemctl --user start con el entorno real del timer antes de habilitarlo. Ver docs/arquitectura/agente-monitoreo.md para la evidencia completa y el costo mensual estimado.

Diseño de seguridad (por qué Claude no ejecuta comandos directamente)

claude -p se invoca con --allowedTools "" (cero herramientas) y --json-schema, así que el modelo nunca ejecuta un comando — solo devuelve una decisión estructurada ({"actions":[{"resource", "decision", "reason"}], "summary"}). Este script bash es el único que ejecuta kubectl/docker, y solo después de revalidar la decisión contra su propio cálculo de elegibilidad (rate limit, umbral de tiempo, ventana horaria, bloqueo tras intento previo).

Se descartó dar a Claude un Bash tool con un patrón tipo Bash(kubectl rollout restart deployment/medusa-deploy -n ecommerce) porque se comprobó en vivo que --allowedTools hace coincidencia por prefijo, no coincidencia exacta: un patrón Bash(echo pintest) también permite ejecutar echo pintest && echo INJECTED o echo pintest; rm -rf /. Para un comando root-capaz eso no es un límite seguro. Con --allowedTools "", Claude no tiene ninguna superficie de ejecución — el peor caso posible es que proponga una decisión incorrecta en el JSON, y el wrapper la rechaza si el recurso no está en su propia lista de elegibles (ver RESOURCE_ELIGIBILITY en el script).

El prompt también advierte explícitamente contra prompt injection: los datos de diagnóstico (logs, estado de pods) podrían contener texto arbitrario si algo en el cluster está comprometido, e instruye a Claude a ignorar cualquier instrucción incrustada ahí.

Qué hace cada corrida

  1. Lee kubectl -n ecommerce get deployment -o json y calcula salud por deployment (readyReplicas vs replicas, condición Available).
  2. Chequea docker inspect gitea-runner (estado del contenedor) y, si existe GITEA_TOKEN_FILE, la API de Gitea Actions (/repos/{owner}/{repo}/actions/runs) buscando runs en estado running atascados por más de UNHEALTHY_THRESHOLD_MIN minutos — la señal específica del incidente de runner zombie del 2026-08-16. Si el token no está o la API falla, esta señal se omite (fail-safe: nunca dispara una acción sobre una lectura no verificada) y solo queda la señal de "contenedor no está running".
  3. Curl a los dominios públicos — puramente informativo, nunca gatilla una acción.
  4. Calcula elegibilidad por recurso contra el estado persistido (STATE_FILE): umbral de 10 min, rate limit de 30 min entre acciones sobre el mismo recurso, ventana horaria 00:00–06:00 para medusa-deploy (el deployment real de "commerce-backend" en CLAUDE.md — confirmado en vivo 2026-08-25, no existe un deployment llamado literalmente commerce-backend), y bloqueo permanente tras un primer intento cuyo problema no se resolvió (sin reintentos silenciosos — requiere intervención humana para desbloquear, ver abajo).
  5. Invoca claude -p con el diagnóstico + la lista de qué es elegible este ciclo. Cero herramientas, salida JSON validada por schema.
  6. Para cada decisión "restart": revalida que el recurso esté REALMENTE en la lista de elegibles del wrapper (no confía en el JSON de Claude por sí solo); si pasa, ejecuta el comando fijo correspondiente (kubectl rollout restart deployment/<nombre> -n ecommerce o docker restart gitea-runner), captura el resultado real (no lo que Claude cree que pasó) y marca el recurso como bloqueado para reintentos automáticos futuros.
  7. Registra todo en ${LOG_DIR}/run-<timestamp>.log (log completo de la corrida) y ${LOG_DIR}/actions.jsonl (una línea JSON por decisión: timestamp, recurso, decisión, resultado, motivo — el registro auditable pedido en la lista blanca).
  8. Notifica (Paso 3) si hubo alguna anomalía o acción — nunca en silencio si actuó.

Desbloquear un recurso tras un intento fallido

El estado vive en STATE_FILE (JSON). Para que un recurso vuelva a ser elegible sin esperar a que se recupere solo:

jq 'del(.resources["deploy:medusa-deploy"])' "$STATE_FILE" > /tmp/s.json && mv /tmp/s.json "$STATE_FILE"

(cambiar la clave por docker:gitea-runner u otro deploy:<nombre>).

Configuración

cp lab-agent.env.example lab-agent.env
# editar umbrales/rutas si hace falta — lab-agent.env NO lleva secretos,
# está en .gitignore a propósito (evita que una copia con paths de un
# host distinto termine commiteada)

Autenticación de claude -p: ANTHROPIC_API_KEY_FILE queda vacío por defecto — el script usa el login de suscripción ya activo del usuario que lo corre (~/.claude/.credentials.json), no una API key aparte. Por eso el servicio systemd (cuando se active) debe correr como devops, no como root. Ver la sección "Autenticación" en docs/arquitectura/agente-monitoreo.md para el porqué completo.

El único secreto real es GITEA_TOKEN_FILE (opcional, token de Gitea Actions con scope lectura) — dueño devops, sin sudo:

mkdir -p "$(dirname "$GITEA_TOKEN_FILE")"   # por defecto: /DATA/AppData/k3d-lab/lab-agent/secrets/
echo "<tu-token>" > "$GITEA_TOKEN_FILE"
chmod 600 "$GITEA_TOKEN_FILE"

Si no existe, el agente sigue funcionando pero pierde la señal de "runner atascado" vía API de Gitea Actions (solo detecta el contenedor caído/reiniciando).

Notificaciones (Telegram, reemplaza a ntfy.sh desde 2026-08-26): creá el bot con @BotFather (/newbot) y obtené tu chat_id (mandale un mensaje al bot primero, después https://api.telegram.org/bot<TOKEN>/getUpdates). El token va en un archivo aparte, dueño devops — no root-only pese al patrón original: el servicio corre como devops (necesita su login de suscripción para claude -p), así que un archivo root:root 600 le sería ilegible, mismo criterio ya aplicado a GITEA_TOKEN_FILE.

mkdir -p "$(dirname "$TELEGRAM_BOT_TOKEN_FILE")"
echo "<tu-token-de-botfather>" > "$TELEGRAM_BOT_TOKEN_FILE"
chmod 600 "$TELEGRAM_BOT_TOKEN_FILE"
# y en lab-agent.env: TELEGRAM_CHAT_ID=<tu-chat-id>  (no es secreto)

Horario activo: ACTIVE_HOURS_START/ACTIVE_HOURS_END (hora local, default 06:00–23:00). Fuera de esa ventana el script sale antes de tocar kubectl/docker/curl o invocar claude -p — no gasta nada, no solo omite la notificación. Ojo: esta ventana no se solapa con COMMERCE_BACKEND_WINDOW_* (00:00–06:00) — mientras el agente está pausado, medusa-deploy nunca es elegible para auto-restart en la práctica. Ver docs/arquitectura/agente-monitoreo.md.

Probar

./lab-agent.sh --dry-run   # simulación, no toca el cluster
./lab-agent.sh --live      # ejecución real

Validado en vivo 2026-08-26 con un deployment canario desechable (test-agent-canary, busybox:stable con exit 1 — CrashLoopBackOff garantizado): DRY_RUN detecta y "ejecuta" sin tocar el cluster, --live ejecuta de verdad (confirmado con kubectl get events y la anotación restartedAt), y una segunda corrida sobre el mismo recurso caído respeta el bloqueo (notify_only, sin reintento). Dos bugs reales se encontraron y corrigieron en esa prueba — ver el detalle en docs/arquitectura/agente-monitoreo.md. Telegram (silencioso para info, hilo de respuestas al recuperarse) también se validó en vivo con el mismo canario.

Instalar el timer de systemd

Unidades de usuario (systemctl --user), corren como devops — necesario porque claude -p usa su login de suscripción. Ojo: el manager de systemd --user de devops arranca con HOME=/DATA (el de /etc/passwd), no /DATA/AppData/home-devops — por eso lab-agent.service fija Environment=HOME=... explícitamente; sin eso, claude -p no encuentra las credenciales. Los archivos van en /DATA/.config/systemd/user/, no en el ~/.config/... de una shell interactiva (que resuelve a otra ruta por el mismo motivo).

mkdir -p /DATA/.config/systemd/user
cp systemd/lab-agent.service systemd/lab-agent.timer /DATA/.config/systemd/user/
sudo loginctl enable-linger devops   # una vez — si no, systemctl --user ni conecta
export XDG_RUNTIME_DIR=/run/user/$(id -u devops)
systemctl --user daemon-reload
systemctl --user start lab-agent.service   # probar una corrida manual primero
systemctl --user enable --now lab-agent.timer
systemctl --user list-timers lab-agent.timer

Activo desde 2026-08-26. Ver el costo mensual estimado y el detalle completo en docs/arquitectura/agente-monitoreo.md.