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).
This commit is contained in:
1 parent
e08d03b7f4
commit
5c1cb3aa5e
4 files changed
+799
No files matched your search
@@ -0,0 +1,142 @@
|
||||
# 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.
|
||||
Reference in new issue
Block a user