Files
scripts/lab-agent/README.md
T
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

143 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```bash
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
```bash
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):
```bash
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)
```bash
./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.