Files
scripts/lab-agent
devops 5c1cb3aa5e Agregar lab-agent: wrapper headless de diagnóstico/corrección (Paso 2)
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).
2026-08-25 22:18:31 -05:00
..

lab-agent — agente de diagnóstico/corrección del lab (Paso 2)

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 y su justificación de gobernanza en apps-registry → docs/arquitectura/agente-monitoreo.md (Paso 6, pendiente de escribir).

Este documento cubre solo el Paso 2 (el script). Los Pasos 3 (notificaciones), 4 (systemd timer) y 5 (prueba controlada) son posteriores y requieren tu confirmación explícita antes de activarse en continuo — ver instrucciones al final.

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)

Dos archivos root-only que este script espera pero no crea (por diseño — la creación de credenciales no se hace desde una sesión interactiva de Claude Code en este lab, ver política del proyecto):

sudo mkdir -p /etc/claude-lab-agent/secrets
sudo tee /etc/claude-lab-agent/secrets/anthropic_api_key >/dev/null   # pega la key, Ctrl-D
sudo tee /etc/claude-lab-agent/secrets/gitea_token >/dev/null         # token con scope read:repository (actions)
sudo chown root:root /etc/claude-lab-agent/secrets/*
sudo chmod 600 /etc/claude-lab-agent/secrets/*

GITEA_TOKEN_FILE es opcional: 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).

Probar (Paso 2 — humo, sin secretos)

./lab-agent.sh --dry-run

Ya validado en vivo 2026-08-25 hasta el paso de invocar claude -p (falla ahí, correctamente, por falta de ANTHROPIC_API_KEY_FILE — comportamiento esperado sin el secreto configurado). El diagnóstico de los 3 deployments de ecommerce, gitea-runner y los 5 dominios públicos funcionó correctamente contra el cluster real.

Con los secretos configurados, --dry-run corre el flujo completo (incluida la llamada real a claude -p, que sí tiene costo) pero sustituye la ejecución de kubectl rollout restart/docker restart por un mensaje [DRY_RUN] — no toca el cluster ni el contenedor de verdad. DRY_RUN=1 es el valor por defecto en lab-agent.env.example; pasar --live (o DRY_RUN=0 en el env) para el modo real.

No actives el timer de systemd (Paso 4) todavía. Falta: Paso 3 (canal de notificación real), Paso 5 (prueba controlada con un escenario real de CrashLoopBackOff inducido, mostrándote los logs antes de habilitar el timer en continuo), y Paso 6 (documentar todo esto en docs-portal). Este script se puede correr manualmente para más pruebas de humo mientras tanto.