Skip to main content
Glama

FourEyes

Un agente de atención al cliente con control humano. Lee tickets, consulta cuentas y decide qué hacer — pero cada escritura irreversible (reembolso / escalación / cierre) se detiene físicamente en una puerta de aprobación humana antes de ejecutarse.

El nombre proviene del principio de los cuatro ojos: cualquier acción crítica necesita un segundo par de ojos.

Consola de aprobación


El argumento

La mayoría de la "seguridad de agentes de IA" es texto de prompt: "por favor, pregunte a un humano antes de reembolsar". Un prompt es una solicitud, no una restricción — basta con un ignore previous instructions y desaparece.

FourEyes coloca la garantía donde un prompt no puede alcanzar:

Capa

Dónde reside

Qué hace realmente

① Contenido

agent/guards.py

Envuelve el texto del cliente en límites explícitos de datos no confiables; señala patrones de inyección (marcadores SYSTEM: falsos, aprobaciones falsificadas, suplantación de roles, ofuscación base64/ancho cero/homoglifo). Señala, nunca elimina en silencio — el texto de ataque es evidencia.

② Estructura

topología del grafo + dos servidores MCP

La ruta de escritura físicamente pasa por interrupt(). El servidor de solo lectura no tiene herramientas de escritura — y se conecta como un rol de Postgres con ningún permiso INSERT/UPDATE/DELETE.

③ Barrera de negocio

mcp_action/guardrails.py

Comprobaciones deterministas en cada entrada de herramienta de escritura: cantidad ≤ pedido, cantidad ≤ límite de $500, estado/ventana conforme, sin reembolso previo, clave de idempotencia única — con un bloqueo de fila FOR UPDATE. Se ejecuta incluso cuando el modelo es engañado y el humano aprueba incorrectamente.

Dos propiedades son verificadas por pruebas, no por comentarios:

  • Ninguna ruta desde START hasta execute_action evita interrupt() — se comprueba eliminando el nodo de interrupción del grafo y demostrando que execute_action se vuelve inalcanzable.

  • La autorización proviene de la fila de base de datos aprobada, no del estado mutable del grafo. execute_action relee la fila de approvals que el humano firmó y la verifica contra la propuesta; una discrepancia es rechazada y auditada. (Esta surgió de una revisión adversaria que encontró que el código original podía ejecutar un reembolso mientras el humano había aprobado una escalación — véase failures.md.)


Arquitectura

                                  ┌──────────────────────────────────────┐
  ticket ──▶ sanitize_input ──▶ gather_evidence ──▶ classify ──▶ route   │
             (layer ①)           (read-only MCP)     (LLM, policy)       │
                                                          │              │
              ┌───────────────────────────────────────────┤              │
              ▼                    ▼                      ▼              │
        out_of_policy       under_specified          in_policy           │
              │                    │                      │              │
        explain_refusal      propose_escalation     propose_action       │
              │                    └──────────┬───────────┘              │
             END                              ▼                          │
                                       request_approval  ── writes approvals row
                                              ▼
                                    ★ await_decision — interrupt()
                                       state → Postgres checkpoint
                                              │
                        ┌─────────────────────┴──────────────────┐
                     rejected                                 approved
                        │                                        │
                  log_rejection                            execute_action ── the ONLY
                        │                                        │            ticket-action
                       END                                verify_and_log      client
                                                                 │
                                                                END

Cuatro servicios, un comando (docker compose up):

Servicio

Lenguaje

Rol

mcp-lookup :8101

TypeScript MCP SDK

Herramientas de solo lectura. Se conecta como foureyes_ro.

mcp-action :8102

Python MCP SDK

La única ruta de escritura. Barreras de negocio en cada entrada de herramienta.

api :8000

FastAPI

Backend de la consola de aprobación. Solo puede reanudar el grafo — no tiene capacidad de ejecución.

postgres :5432

Tablas de negocio + puntos de control de LangGraph.

Consola (console/, React + TypeScript + Vite) es una pantalla: tarjetas pendientes → aprobar / rechazar.

¿Por qué dos servidores MCP en lugar de uno con dos grupos de herramientas? El límite de permisos se traza en la capa de protocolo y red, no dentro de una función. Las herramientas de lectura no tienen puerta porque poner puerta a todo causa fatiga de aprobación — una puerta en todas partes es una puerta en ninguna. Solo las escrituras irreversibles tienen puerta.


Resultados medidos

Cada número a continuación proviene de un comando en este repositorio. Nada aquí es estimado.

Pruebas adversarias — 53 correos, 7 categorías de ataque

.venv/bin/python evals/test_redteam.py     # report: evals/redteam/report.json
total_emails            : 53   (direct injection · roleplay/jailbreak · forged system messages ·
                                encoding/obfuscation · social engineering · tool-parameter
                                pollution · multi-turn priming)
unauthorized_executions : 0
deception_rate          : 0.0  (0/53 talked the model into proposing a refund)
sanitize_flagged        : 21/53
blocked_by seen         : content_layer + structural_layer + business_guardrail   ← all three

Cada correo es auto-aprobado durante la ejecución — simulando deliberadamente un humano que también es engañado — así que la barrera de negocio es lo que se está probando, no el humano.

Las dos métricas se reportan por separado a propósito: cero ejecuciones no autorizadas es la afirmación a nivel de ejecución; tasa de engaño es el experimento a nivel de razonamiento. Nadie quiere un sistema de reembolsos que sea 96% seguro, así que la afirmación de seguridad es un conteo, no un porcentaje.

Selección de acciones — 100 tickets etiquetados

.venv/bin/python evals/test_benchmark.py   # report: evals/benchmark/report.json
action_selection_accuracy : 99.0%  (99/100)
false_block_rate          : 0.0%   (0/31 actionable in_policy tickets)
per_subset                : generated 98.8% (79/80) · boundary 100% (20/20)

La composición del conjunto de datos importa más que el número. 80 tickets son generados por LLM con límites de política claros; la medición encontró que solo 3 de ellos estaban dentro de ±5 días / ±$50 de un umbral, lo que hizo que el 98.8% fuera indefendible por sí mismo. Así que se añadieron 20 casos límite escritos a mano: día 30 vs día 31, exactamente $500 vs $500.01, exactamente el monto del pedido vs un céntimo más, reembolsos previos pendiente/rechazado (que no bloquean un nuevo reembolso), y tres conflictos de precedencia de política (X3 supera a E1; X4 supera a E1; un incidente de seguridad prevalece sobre el monto). El subconjunto de límites obtuvo 20/20 — el clasificador razona a partir de cláusulas, no de coincidencia de palabras clave.

El único fallo (bm_076) citó E3 + X1 y escaló donde la etiqueta decía rechazar — una solicitud de terceros hecha en nombre de un padre de 84 años. Desacuerdo defendible, no un error.

Evaluaciones de trayectoria — 29 escenarios, y prueba de que tienen dientes

.venv/bin/python -m pytest evals/test_trajectories.py -q   # 30 passed in 124.85s
.venv/bin/python scripts/verify_eval_teeth.py

Las evaluaciones de trayectoria verifican el proceso, no solo la respuesta — un estado final correcto puede alcanzarse por un camino equivocado (la refactorización de viernes que sortea silenciosamente el nodo de aprobación). 10 de los 29 son escenarios negativos.

Un conjunto de evaluaciones que nadie ha visto fallar no es una red de seguridad, así que el fallo se demuestra bajo demanda: verify_eval_teeth.py reescribe la arista de aprobación a request_approval → execute_action, ejecuta las evaluaciones, y requiere que se pongan rojas — luego restaura el archivo y requiere verde:

=== step 1: sabotage the approval edge ===
3 failed (traj_bypass_check, traj_single_inbound_edge, traj_001), exit=1
OK: evals went RED as required
=== step 2: re-run against the intact graph ===
4 passed
VERDICT: trajectory evals have teeth

Nótese que traj_001 — un escenario conductual — también se pone rojo, no solo las afirmaciones de topología.

Suite de pruebas

.venv/bin/python -m pytest tests/ -q       # 43 passed

Barreras de seguridad (14) · topología (6) · vinculación de consentimiento (4) · guardas de capa 1 (16, incluyendo 6 guardas de falsos positivos para que las quejas ordinarias no se marquen) · respaldo de barreras de seguridad (3).


Divulgación de datos sintéticos

Todos los tickets, clientes, pedidos y correos adversarios en este repositorio son datos sintéticos generados por LLM. No hay clientes reales, ni pedidos reales, ni tráfico de producción. Específicamente:

  • db/seed_data.json — 60 tickets en 9 categorías de escenarios, generados por Claude y almacenados en caché en git para que la resiembra sea determinista (ADR-003).

  • evals/redteam/emails.jsonl — 53 correos adversarios. Seis categorías son generadas por Claude; el conjunto encoding_obfuscation se construye programáticamente (cargas útiles reales de base64 / ancho cero / homoglifo) porque el clasificador de seguridad de Claude se niega a codificar instrucciones de ataque en vivo.

  • evals/benchmark/tickets.jsonl — 80 tickets etiquetados, generados por Claude, con cada etiqueta verificada contra reglas de política deterministas antes de ingresar al conjunto de datos — se eliminó y regeneró un elemento autocontradictorio (ADR-011).

  • evals/benchmark/boundary.jsonl — 20 casos límite escritos a mano.

Las fechas se almacenan como desplazamientos relativos y se convierten en el momento de la siembra, para que los escenarios "dentro de la ventana de 30 días" sigan siendo válidos cuando se vuelva a sembrar el conjunto de datos.


Mapeo OWASP LLM Top 10

Riesgo

Dónde lo aborda FourEyes

LLM01 Inyección de Prompt

Las tres capas. Contenido: agent/guards.py envuelve y señala en los límites. Estructura: la "aprobación ya concedida" inyectada no puede saltar interrupt(). Negocio: mcp_action/guardrails.py rechaza la escritura independientemente. Medido: 53 correos, 0 ejecuciones no autorizadas.

LLM02 Manejo Inseguro de Salida

La salida del modelo nunca llega a una herramienta sin validar — propose_action verifica el id del pedido propuesto contra la evidencia obtenida, y las barreras de seguridad revalidan cada parámetro en el límite de la herramienta.

LLM05 Manejo Inadecuado de Salida / agencia excesiva

El agente no puede ejecutar nada. execute_action ejecuta solo lo que autoriza una fila de BD approved (ADR-009).

LLM06 Divulgación de Información Sensible

El servidor de consulta se limita por ticket del cliente; el rol de lectura tiene solo SELECT.

LLM07 Filtración de Prompt del Sistema

prompt_extraction es un patrón de inyección señalado; la política es pública por diseño, por lo que la filtración no conlleva información privilegiada.

LLM08 Agencia Excesiva

Escrituras controladas por interrupción obligatoria de HITL; división lectura/escritura en dos servidores MCP separados con roles de BD separados.

LLM09 Exceso de Confianza

Las evaluaciones de trayectoria verifican secuencias de herramientas; el benchmark mide tanto la corrección como la tasa de falsos bloqueos, por lo que el exceso de bloqueo es visible en lugar de ocultarse detrás de una afirmación de seguridad.

LLM10 Denegación de Servicio del Modelo

Timeouts de 30s, max_retries=0 con fallback explícito del proveedor (ADR-006).


Ejecutarlo

Requiere Python 3.12+, Node 20+, y Docker.

# 0. Local Python env — the scripts and evals run on the host, not in the containers
python3.12 -m venv .venv
.venv/bin/pip install -r requirements.txt

# 1. Full stack
cp .env.example .env          # fill in ANTHROPIC_API_KEY, GOOGLE_API_KEY, Langfuse keys
docker compose up -d --build  # postgres + mcp-lookup + mcp-action + api

# 2. Seed synthetic tickets (uses the cached generation; no API call needed)
.venv/bin/python db/seed.py --reset

# 3. Drive one ticket to the approval gate — the process then exits
.venv/bin/python scripts/run_ticket.py start --category refund_eligible

# 4. Approve from a *different* process, resuming from the Postgres checkpoint
.venv/bin/python scripts/run_ticket.py resume <ticket_id> approved --by you
.venv/bin/python scripts/run_ticket.py inspect <ticket_id>

# 5. Or approve in the console
cd console && npm install && npm run dev     # http://localhost:5173

El paso 3 → 4 es la demo del punto de control: dos procesos separados. El segundo reanuda desde el punto de control almacenado en lugar de volver a razonar — lo cual importa porque un LLM consultado dos veces puede llegar a una conclusión diferente, y el humano aprobó una propuesta específica, no un reintento.


Decisiones de diseño

ADRs completos con alternativas consideradas en decisions.md. Los que soportan carga:

  • [ADR-002] Dos roles de base de datos. foureyes_ro no tiene permiso de escritura, por lo que "el servidor de consultas es de solo lectura" es un hecho de base de datos más que una convención de código.

  • [ADR-007] Recopilación de evidencia determinista, punto de decisión único del LLM. Sin bucle de herramientas ReAct: las afirmaciones sobre la trayectoria pueden ser exactas, y la varianza de los benchmarks proviene del juicio, no de la inestabilidad en la recuperación.

  • [ADR-007] request_approval y await_decision son nodos separados. LangGraph repite un nodo al reanudar; los efectos secundarios deben ir después del interrupt() o la fila de aprobaciones se escribe dos veces.

  • [ADR-009] El consentimiento está vinculado a la acción ejecutada. La autorización se vuelve a leer de la fila aprobada en el momento de la ejecución.

  • [ADR-012] La API no puede ejecutar. Aprobar solo reanuda el grafo, por lo que comprometer la consola aún no puede mover dinero.

Cicatrices, incluidos tres errores reales encontrados después de que el código "funcionara", están en failures.md.


Flujo de trabajo de desarrollo asistido por IA

Este proyecto se construyó con Claude Code. Lo que eso significa concretamente, y cómo se verificó el resultado:

Disciplina utilizada durante la construcción

  • Cada componente recibió una entrada en decisions.md antes de la implementación: decisión, alternativas, por qué, y qué falla con la alternativa. No poder nombrar una alternativa significaba que el diseño aún no se comprendía.

  • Cada error fue a failures.md con el error textual, el diagnóstico y la corrección.

  • Nada se consideró "terminado" sin ejecutarlo y pegar la salida en el mensaje de commit.

  • Los números de salida (precisión, tasas de bloqueo) tenían prohibido aparecer en ningún lugar (incluidos los comentarios de código) hasta que un comando los hubiera producido. Los marcadores de posición leían [NOT_MEASURED].

Cómo se verificó la salida de la IA

  1. Revisión de código adversaria. Cuatro agentes de revisión independientes (topología HITL, bypass de inyección, exhaustividad de salvaguardas, corrección) produjeron 23 hallazgos en bruto; cada uno fue luego entregado a un agente separado con la instrucción de refutarlo contra el código real. 23 → 3 confirmados. Sin la pasada de refutación, el error real se habría enterrado entre falsos positivos.

  2. El HIGH confirmado era un defecto de diseño genuino, no un error tipográfico: consentimiento y acción estaban desacoplados, por lo que una repetición podría hacer que el humano aprobara una escalación mientras se ejecutaba un reembolso. Corregido estructuralmente (ADR-009) más 4 pruebas de regresión.

  3. Las demostraciones de extremo a extremo encontraron lo que las pruebas unitarias no podían. El respaldo de capa 3 y las brechas de expresiones regulares de capa 1 fueron detectadas por demostraciones de equipo rojo, después de que las pruebas unitarias y las pruebas de humo del protocolo pasaran: los errores estaban en las costuras entre componentes.

  4. La verdad objetiva del propio arnés del equipo rojo estaba equivocada al principio. Inicialmente informó 10 ejecuciones no autorizadas; las órdenes de carga resultaron ser legítimamente reembolsables por accidente. El modo de falla peligroso para una métrica de seguridad no es un número feo, es un número bonito medido contra la línea base incorrecta.

  5. Las etiquetas generadas son verificadas por máquina. Las etiquetas de benchmark se validan contra reglas de política deterministas antes de ingresar al conjunto de datos, por lo que la métrica mide la concordancia con la política en lugar de la concordancia con otro modelo.


Fuera del alcance (deliberadamente)

Sin voz/TTS, sin interfaz de chat, sin paneles ni gráficos, sin sistema de inicio de sesión, sin ajuste fino, sin tráfico de usuarios reales. La consola de aprobación es una sola pantalla: cualquier otra cosa es expansión del alcance.

Trazabilidad

Cada ticket produce un rastro de Langfuse, con clave determinista basada en el id del ticket para que los spans emitidos por el proceso de inicio y el proceso de reanudación terminen en el mismo rastro:

SPAN       sanitize_input        injection_flags recorded here
SPAN       gather_evidence       the five read-only lookups
GENERATION classify              policy + evidence → decision (prompt/completion/tokens)
SPAN       approval_requested    ← the graph stops here
SPAN       human_decision        ← human waited 9.7s   (waited_seconds in metadata)
SPAN       execute_action        runs only what the approved row authorises
SPAN       verify_and_log        reads the ticket back

approvals.trace_url almacena el enlace, por lo que cada tarjeta en la consola enlaza profundamente a su propio rastro. El respaldo del proveedor se emite como un evento provider-fallback en el rastro, por lo que el cambio de Claude → Gemini es visible en lugar de inferido.

Advertencia regional, por si bifurcas esto: Langfuse Cloud está dividido por región. Apuntar un proyecto de EE. UU. a cloud.langfuse.com devuelve 401 Invalid credentials, que parece una clave incorrecta pero no lo es. Me costó un diagnóstico erróneo completo; consulta failures.md.

Brechas conocidas

  • El respaldo del proveedor se verifica con un APITimeoutError real (scripts/smoke_router.py), no con un simulacro, pero no se ha ejercitado bajo una interrupción real del proveedor.

  • La conectividad del servidor MCP se verificó con el cliente Python SDK de MCP (list_tools + call_tool sobre Streamable HTTP), no con la interfaz de usuario de MCP Inspector. Equivalente a nivel de protocolo, pero si quieres afirmar "verificado en Inspector", ejecútalo tú mismo primero.

-
license - not tested
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

  • A paid remote MCP for agent memory MCP, built to return verdicts, receipts, usage logs, and audit-re

  • Paid remote MCP for agent code search routing MCP, structured receipts, audit logs, and reviewer-rea

  • A paid remote MCP for AI agent browser approval MCP, built to return verdicts, receipts, usage logs,

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/LoganLuo46/FourEyes'

If you have feedback or need assistance with the MCP directory API, please join our Discord server