Skip to content

Latest commit

 

History

History
168 lines (148 loc) · 20.6 KB

GUARDRAILS.md

File metadata and controls

168 lines (148 loc) · 20.6 KB

🛡️ Guardarraíles — capa de seguridad de los agentes

Este documento reúne, en un solo sitio, qué impide que los agentes hagan algo que no deben y lo mapea contra el OWASP Top 10 for LLM Applications (2025). Es la contraparte de CONSTITUTION.md (principios) y ARCHITECTURE.md (estructura): aquí está el control efectivo.

Distinguimos tres clases de control, porque no todos valen lo mismo:

  • Determinista (código): lo aplica un hook o un script, no depende del criterio de ningún LLM. Es el control fuerte.
  • Humano en el bucle (HITL): aprobación humana por acción, configurable por el operador (approval_mode: full/critical/auto, def. critical). Las puertas DETERMINISTAS (alcance/presupuesto) no se relajan en ningún modo. Ver CONSTITUTION §2.
  • Mediado por LLM (soft): una instrucción del prompt que el modelo debería seguir. Útil, pero no es una barrera.

Modelo mental

flowchart TB
    UNTRUSTED["🌐 Contenido NO confiable<br/>(banners, HTML, respuestas del target,<br/>output del LLM objetivo en ai-security)"]
    AGENT["🤖 Agente especialista"]
    ORQ["🧭 Orquestador (hub)"]
    BB[("🗒️ Blackboard<br/>engagement.json")]
    TARGET["🎯 Target en scope"]

    UNTRUSTED -->|ingesta| G_INJECT
    G_INJECT -->|datos, no órdenes| AGENT
    ORQ -->|delega + valida handoff| AGENT
    AGENT -->|escribe findings/targets| G_SCHEMA
    AGENT -->|escribe findings/targets| G_REDACT
    AGENT -->|cada acción Bash| G_BUDGET
    AGENT -->|comando de escaneo| G_NOISE
    AGENT -->|acción Bash repetida| G_LOOP
    AGENT -->|comando contra target| G_SCOPE
    AGENT -->|petición HTTP a herramienta| G_HDR
    AGENT -->|comando ofensivo| G_HITL
    AGENT -->|mensaje A2A a otro agente| G_A2A
    AGENT -->|escribe en su memoria de aprendizaje| G_MEMORY
    ORQ -->|retorno de un subagente (Task)| G_INTEGRITY

    subgraph GATES["Guardarraíles"]
        G_INJECT{{"🧱 datos ≠ instrucciones<br/>anti-inyección · prompt (LLM01)"}}
        G_SCOPE{{"🛡️ scope_guard.py<br/>PreToolUse · determinista"}}
        G_BUDGET{{"⏱️ budget_guard.py<br/>PreToolUse · determinista (LLM10)"}}
        G_NOISE{{"🔇 noise_guard.py<br/>PreToolUse · C18 (LLM10/§9)"}}
        G_LOOP{{"🔁 loop_guard.py<br/>PreToolUse · C19 (LLM10)"}}
        G_HITL{{"🙋 tiers de riesgo + aprobación<br/>HITL (bot)"}}
        G_PERM{{"🔒 permissions ask/deny<br/>settings.json"}}
        G_TOOLS{{"🧰 allowlist + denylist + maxTurns<br/>frontmatter por agente"}}
        G_SCHEMA{{"📐 validate_blackboard.py<br/>PostToolUse · determinista"}}
        G_REDACT{{"✂️ secret_scan.py<br/>PostToolUse · determinista (LLM02)"}}
        G_A2A{{"✉️ a2a_guard.py<br/>PostToolUse · C14/C15 (LLM01/LLM10)"}}
        G_MEMORY{{"🧠 memory_guard.py<br/>PreToolUse · determinista (LLM02)"}}
        G_FS{{"📁 fs_guard.py<br/>PreToolUse · C20 (LLM02/§1·§6)"}}
        G_BB{{"📝 blackboard_guard.py<br/>PreToolUse·Bash · C21 (LLM02)"}}
        G_CB{{"🔌 circuit_breaker.py<br/>Pre+PostToolUse·Bash · C22 (LLM10/§9)"}}
        G_INTEGRITY{{"🩺 blackboard_integrity_check.py<br/>PostToolUse·Task · C23 (LLM02/LLM05)"}}
        G_HDR{{"🏷️ header_guard.py<br/>PreToolUse·Bash · C24 (LLM06/§9)"}}
    end

    G_SCOPE -->|en scope| TARGET
    G_SCOPE -.->|fuera de scope| BLOCK["⛔ bloqueado"]
    G_BUDGET -.->|techo superado| BLOCK
    G_NOISE -.->|ruidoso/DoS-adjacent| BLOCK
    G_LOOP -.->|bucle/oscilación| BLOCK
    G_HITL -->|✅ autorizado| TARGET
    G_SCHEMA -->|válido| BB
    G_SCHEMA -.->|inválido| FIX["↩️ feedback al orquestador"]
    G_REDACT -.->|secreto del operador| FIX
    G_A2A -->|emisor/destino válidos, sin bucle| BB
    G_A2A -.->|spoofing / techo de hops| FIX
    G_MEMORY -->|técnica limpia| MEM["🧠 memoria de agente<br/>.claude/agent-memory*/"]
    G_MEMORY -.->|datos de cliente| FIX
    G_FS -->|dentro de recon/src| TARGET
    G_FS -.->|symlink/`..` fuera de la zona| BLOCK
    G_BB -.->|escritura del blackboard por Bash| BLOCK
    G_CB -.->|target caído: N fallos de conexión| BLOCK
    G_INTEGRITY -.->|recuentos encogidos / JSON inválido / esquema roto| FIX
    G_HDR -->|cabecera del programa presente| TARGET
    G_HDR -.->|sin cabecera de programa (o fuera de un flag real)| BLOCK
Loading

Inventario de controles (estado real)

# Control Clase Dónde OWASP LLM 2025
C1 Gate de alcance — bloquea todo comando contra un host/IP/CIDR fuera de scope.json Determinista .claude/hooks/scope_guard.py (PreToolUse) LLM06 Excessive Agency
C2 Aprobación por acción configurable (approval_mode full/critical/auto, def. critical; 5 tiers de riesgo safe→auto … crítico→doble confirmación) HITL configurable bot/intel/risk.py + runner.py + .claude/hooks/approval_gate.py LLM06 Excessive Agency
C3 Permisos ask/deny (deniega leer/escribir scope.json; pide confirmación a nmap/sqlmap/…) Determinista .claude/settings.json → permissions LLM06, LLM07 System-prompt leakage
C4 Mínimo privilegio por agente — cada agente declara las tools que necesita (tools:), deniega el resto y en especial Agent+Task para no spawnear subagentes (candado hub-and-spoke; el cierre, además, sin Bash), y acota sus turnos con maxTurns Determinista frontmatter tools:/disallowedTools:/maxTurns: de cada agente LLM06 Excessive Agency, LLM10 Unbounded Consumption
C5 Validación de esquema del blackboard — un finding/target sin campos obligatorios dispara feedback correctivo; además invariantes de valor: una hipótesis white-box (code_ref) no puede ir confirmed/exploited sin evidence, y (mejora F) un proof_state que afirma prueba dinámica (evidenced/proven-by-exploit) exige evidence mientras que roe-capped exige una FUENTE Determinista .claude/hooks/validate_blackboard.py (PostToolUse) + tools/blackboard.py:validate_engagement LLM05 Improper Output Handling
C6 Escritura atómica del blackboard — tmp + os.replace, evita estados a medias Determinista tools/blackboard.py LLM05
C7 Zonas de aislamiento E1/E2/E3 — red y datos separados por fase Organizativo ARCHITECTURE.md §3 LLM02 Sensitive Info Disclosure
C8 Regla de evidencia — "sin fuente no se explota; sin evidencia no es un hallazgo" Soft (LLM) CONSTITUTION.md, agentes LLM09 Misinformation
C9 Auditoría de coherencia pre-informe — targets fuera de scope, autorización caducada, y reportabilidad por proof_state efectivo (mejora F): un reportable que afirma prueba dinámica sin evidence, o cualquier reportable (incl. roe-capped) sin FUENTE, es un FALLO; los roe-capped se cuentan y avisan (reales, no explotados por ROE — se reportan, no se descartan) Determinista tools/analyze_engagement.py (reusa tools/blackboard.py:is_reportable) LLM05, LLM09
C10 Trazabilidad inmutable — cada acción que toca un target va a evidence[] (ts/agente/acción/hash) Determinista esquema engagement.json (defensa legal)
C11 Separación datos/instrucciones — los 27 agentes que ingieren contenido del target/host comprometido (o mensajes A2A) lo tratan como DATOS, nunca como instrucciones Soft (prompt), uniforme bloque "Anti-inyección" en osint-recon, recon-suite, active-recon, api-recon, code-recon, auth-recon, mobile-recon, firmware-recon, vuln-triage, nuclei, web-exploit, web-fuzzing, sqlmap, api-exploit, network-exploit, metasploit, ai-security, mobile-exploit, firmware-exploit, ad-enum, adcs, kerberos, netexec, post-exploit, lateral-discovery, sliver, c2-exfil (todos salvo reporting/knowledge-postmortem, que no ingieren contenido del target) LLM01 Prompt Injection
C12 Detector de secretos del operador — bloquea si una clave del motor/operador (privada, sk-ant, token del bot) aparece en el blackboard; redact() además sanea el informe Determinista tools/redactor.py + .claude/hooks/secret_scan.py (PostToolUse) LLM02 Sensitive Info Disclosure
C13 Kill-switch de consumo — cuenta las acciones Bash por engagement y bloquea al superar el techo (constraints.max_actions, def. 1000) Determinista .claude/hooks/budget_guard.py (PreToolUse) LLM10 Unbounded Consumption
C14 Validador del bus A2A — cada mensaje agente→agente debe tener envelope sano y from_agent/to_agent que sean agentes conocidos (registro agent-cards.json); rechaza emisor/destino inventado (anti-spoofing) y exige que to_agent sea un peer declarado de from_agent (topología a2a_peers) o el hub — los relevos fuera de pareja van por el Orquestador. Los mensajes A2A se tratan como DATOS (la regla soft es C11) Determinista .claude/hooks/a2a_guard.py (PostToolUse) LLM01 Prompt Injection
C15 Kill-switch A2A — acota la conversación entre agentes: bloquea si los mensajes del engagement superan el techo o si una cadena acumula demasiados hops (anti-bucle). Equivalente A2A de C13 (constraints.max_a2a_hops, def. 50) Determinista .claude/hooks/a2a_guard.py (PostToolUse) LLM10 Unbounded Consumption
C16 Auditoría del ciclo de subagentes — cada fin de subagente deja un registro JSONL inmutable por anexado (agente, id, sesión, engagement, transcript). Observacional: no bloquea la finalización (fail-safe a exit 0) Determinista (audit) .claude/hooks/subagent_stop.py (SubagentStop) → engagements/<id>/evidence/subagents.jsonl (o .claude/audit/) (trazabilidad / defensa legal)
C17 Guard de sanitización de la memoria de aprendizaje — antes de escribir en la memoria persistente de un agente (.claude/agent-memory*/: local per-operador y project compartida por git) bloquea si el contenido trae secretos, identificadores del scope (IPs/dominios in/out), IPs públicas enrutables o loot (hashes). Convierte "sin datos de cliente en memoria" en garantía de código (aislamiento de cliente, CONSTITUTION §1) Determinista .claude/hooks/memory_guard.py (PreToolUse) + tools/redactor.py + helpers de scope_guard LLM02 Sensitive Info Disclosure
C18 Anti-alboroto — bloquea el escaneo ruidoso/DoS-adjacent (nmap -T5, masscan/zmap sin --rate o sobre el cap, rustscan con --batch-size/--ulimit excesivos, fuerza bruta con demasiados hilos, fuzzing web con cientos de hilos); en modo stealth endurece (-T4/-A/-p- rápido, rustscan sin acotar). Configurable: constraints.allow_noisy lo desactiva si la ROE autoriza ruido; stealth/max_scan_rate ajustan umbrales. Aplica CONSTITUTION §9 (bajo ruido) Determinista .claude/hooks/noise_guard.py (PreToolUse) LLM10 Unbounded Consumption (+ no-daño §5/§9)
C19 Anti-bucle (nivel de acción) — detecta el mismo comando repetido (thrashing) y la oscilación A/B sin progreso, y bloquea tras constraints.max_repeat (def. 3) en la ventana reciente. Obliga a cambiar de hipótesis o escalar. Complementa C13 (kill-switch global de acciones) y C15 (techo de hops A2A) llevándolo al nivel de ACCIÓN Determinista .claude/hooks/loop_guard.py (PreToolUse) LLM10 Unbounded Consumption
C21 Anti-escritura del blackboard por Bash — bloquea los patrones comunes de mutación de contracts/engagement.json vía Bash (redirección >/>>, tee, sed -i, cp/mv/dd/install/truncate, open(...,'w')/​.write() de Python), forzando que toda mutación pase por las tools Write/Edit — que SÍ están gateadas por validate_blackboard/secret_scan/a2a_guard. Cierra el hueco (councils A y D) de un agente con Bash (p.ej. auth-recon) volcando un secreto al blackboard esquivando los guards de secreto. Contrato consciente (no un sandbox: la ofuscación se contiene con el contenedor efímero de C); LEER el blackboard no se bloquea Determinista .claude/hooks/blackboard_guard.py (PreToolUse·Bash) LLM02 Sensitive Info Disclosure (+ integridad del blackboard)
C20 Confinamiento de lectura de código de cliente — bloquea una lectura cuando (a) su destino real cae bajo ~/.claude (credenciales del operador), (b) el ancla recon/src es un symlink que resuelve fuera del repo, (c) un symlink o un .. escapa del árbol de código de cliente (engagements/<id>/recon/src/), o (d) un symlink interno del repo escapa del repo. Verifica el file_path de cada Read y la ruta-consulta (path) de Grep/Glob; el recorrido profundo de Grep/Glob no sigue symlinks por defecto (ripgrep) y su confinamiento DURO lo aporta el contenedor. Cierra el hueco de FS que dejó code-recon (mejora "A"); el contenedor efímero por-engagement (deploy/engagement-run.sh, monta solo ese engagement, sin egress) da el confinamiento por namespace de montaje (mejora "C") Determinista .claude/hooks/fs_guard.py (PreToolUse) + deploy/engagement-run.sh / docker-compose.engagement.yml LLM02 Sensitive Info Disclosure (+ aislamiento §1/§6)
C22 Circuit-breaker por target — cuenta los fallos de CONEXIÓN consecutivos por host (rechazo/timeout/DNS/host-down; NO un 4xx/5xx, que significa que el host responde) y, al superar el umbral (constraints.circuit_breaker_threshold, def. 5), ABRE el breaker de ese host: los siguientes comandos contra él se bloquean hasta un cooldown (circuit_breaker_cooldown_s, def. 300) —medio-abierto— o hasta que vuelva a responder (se cierra). Corta el machaque de un target caído/IP baneada; complementa C13 (global), C18 (ruido) y C19 (comando idéntico) con el eje TARGET. PostToolUse observa la salida y cuenta; PreToolUse bloquea. Reusa la extracción de host de scope_guard. FAIL-OPEN Determinista .claude/hooks/circuit_breaker.py (Pre+PostToolUse·Bash) LLM10 Unbounded Consumption (+ no-daño §5/§9)
C23 Integridad post-retorno del blackboard — tras CADA retorno de un subagente (Task), comprueba que contracts/engagement.json siga siendo JSON válido, que pase la validación de esquema, y que len(targets)/len(findings)/len(tasks)/len(evidence) NO hayan ENCOGIDO respecto a un watermark ratchet monótono (nunca baja, aunque los recuentos actuales encojan — una alerta ignorada NO borra la evidencia; la siguiente comparación sigue viendo el encogimiento). Complementa C21 (bloquea vías de escritura CONOCIDAS por el comando) mirando el RESULTADO en vez del comando — caza el daño aunque la vía de escritura no estuviera en los patrones bloqueados. No bloquea (un PostToolUse no puede deshacer una tool ya ejecutada): inyecta additionalContext con la alerta para que el Orquestador la vea y decida verificar/restaurar (sin asumir que existe un backup automático). Un cambio de engagement_id es reset legítimo (nunca alerta). Límites conscientes (ver docstring del módulo): solo mira retornos de Task (un Bash directo del Orquestador no lo dispara — blackboard_guard.py sigue siendo la barrera dura ahí); el watermark no tiene lock ante delegaciones paralelas (riesgo aceptado, detección no gate). FAIL-OPEN Determinista (detección, no bloqueo) .claude/hooks/blackboard_integrity_check.py (PostToolUse·Task) LLM02 Sensitive Info Disclosure + LLM05 Improper Output Handling
C24 Cabecera HTTP obligatoria del programa — cuando scope.json declara constraints.required_http_header (p.ej. programas bug bounty que exigen identificar el tráfico, BUGCROWD: <handle>), bloquea (fail-closed) cualquier herramienta HTTP conocida (curl/httpx/ffuf/nuclei/sqlmap/wpscan/…) contra el target salvo que la cabecera vaya en un flag de cabecera REAL (-H/--header/…) — no cuela por la URL, el body, el user-agent, un comentario o un nombre de fichero. Herramientas sin cabecera arbitraria por CLI (nikto/whatweb/wafw00f/testssl) quedan solo-proxy; un proxy explícito (-x/proxychains) exime (responsabilidad del operador). No-op si el programa no exige cabecera. Activo desde v2.48.0; inventariado aquí en v2.68.1 (hueco de documentación cerrado) Determinista .claude/hooks/header_guard.py (PreToolUse·Bash) LLM06 Excessive Agency (trazabilidad exigida por el programa)

Refuerzo del router A2A (no es un gate): a2a_router_nudge.py (PostToolUse sobre Task) recuerda al Orquestador, tras cada retorno de subagente, que entregue los mensajes A2A pending del blackboard (inyecta la lista por additionalContext). No bloquea nada: convierte el ciclo del router de "instrucción de prompt" en un recordatorio determinista para que no se pierda ningún relevo (trazabilidad C10).

Refuerzo del pilotaje interactivo (no es un gate, mejora v2.61): steering_nudge.py (PostToolUse sobre Task) recuerda al Orquestador las DIRECTIVAS del operador pending en engagements/<id>/control/steering.json para que las aplique en el seam y las marque (tools/steering.py ack). No bloquea ni relaja nada: las directivas son INTENCIÓN del operador (repriorizar/pausar/abortar/ pista/subir-supervisión); steering.py rechaza en origen cualquier tipo que relajaría una puerta (no hay add-scope/disable-guard/lower-approval; raise-approval solo endurece), y los gates deterministas (scope/aprobación) siguen mandando fuera del prompt.

Dónde corren los controles (repo vs plugin)

Los guardarraíles deterministas se activan a nivel de repo vía .claude/settings.json (hooks scope_guard, budget_guard, validate_blackboard y secret_scan) — es el despliegue real en la Kali, y cubre tanto la CLI claude como el bot (que dispara los mismos hooks del proyecto). El plugin de VS Code, en cambio, empaqueta solo scope_guard (C1): es el único portable. Los otros tres están acoplados a rutas del repo (contracts/, tools/) y no tendrían sentido fuera de él. No es una incongruencia: el plugin es un atajo de distribución; la postura de seguridad completa vive en el repo desplegado.

Brechas conocidas — ahora cubiertas

Honestidad por delante: estos tres vectores estaban sin control determinista; desde jun 2026 ya tienen uno cada uno (C11, C12 y C13 respectivamente). Queda un residual de fase 2 anotado en la última columna.

Brecha OWASP Por qué nos afecta Mitigación implementada Estado
Prompt injection desde el target LLM01 Los agentes ingieren contenido no confiable (banners, HTML, y en ai-security el output del LLM objetivo). Un target malicioso puede intentar inyectar instrucciones. scope_guard mitiga que toque algo fuera de scope, pero no que filtre datos locales. C11: bloque de separación datos/instrucciones en los 27 agentes que ingieren contenido ✅ C11 (soft) · fase 2 = clasificador local tipo Prompt Guard
Fuga de secretos en evidencia/informe LLM02 La redacción de credenciales era mediada por LLM (C8). E3-ZDR es organizativo, no forzado por código. C12: redactor.py + hook secret_scan.py que bloquea claves del operador en el blackboard y sanea el informe ✅ C12 (determinista)
Consumo no acotado / kill-switch LLM10 Unbounded Consumption No había límite de iteraciones/coste por engagement en código (relevante con cupo Pro). El kill-switch era conceptual. C13: budget_guard.py cuenta acciones Bash por engagement y bloquea al superar el techo ✅ C13 (determinista) · fase 2 = task_budget del bot

Criterio de diseño: replicamos la idea de los frameworks del sector (NeMo Guardrails, Guardrails AI, LLM Guard, Llama Guard / Prompt Guard) con controles ligeros y stdlib, en hooks, sin meter un runtime de guardrails que choque con el modelo "todo sobre Claude Code". Un clasificador local (Prompt Guard 2 / Llama Guard 4) es opción de fase 2 — encaja con el gusto por lo offline, pero compite por los 15 GB de RAM.

Cómo verificar que los guardarraíles están activos

python tools/validate_suite.py     # comprueba que los hooks referenciados existen
python dryrun/run_dryrun.py        # ejercita scope_guard + budget_guard + validación de esquema + secret_scan
python tools/redactor.py <fichero> # escanea un fichero en busca de secretos (sale 1 si hay)

El dryrun lanza comandos in-scope y out-of-scope reales contra scope_guard.py, comandos de sobra para disparar el budget_guard.py, ejercita el bus A2A (a2a_guard.py, C14/C15) y los guardarraíles anti-alboroto (noise_guard.py, C18) y anti-bucle (loop_guard.py, C19) en sandbox, y valida el engagement.json resultante contra los esquemas (incluida la pasada de secret_scan.py): si un control se rompe, salta ahí.