FourEyes
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.

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 | Envuelve el texto del cliente en límites explícitos de datos no confiables; señala patrones de inyección (marcadores | |
② Estructura | topología del grafo + dos servidores MCP | La ruta de escritura físicamente pasa por |
③ Barrera de negocio | 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 |
Dos propiedades son verificadas por pruebas, no por comentarios:
Ninguna ruta desde START hasta
execute_actionevitainterrupt()— se comprueba eliminando el nodo de interrupción del grafo y demostrando queexecute_actionse vuelve inalcanzable.La autorización proviene de la fila de base de datos aprobada, no del estado mutable del grafo.
execute_actionrelee la fila deapprovalsque 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éasefailures.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
│
ENDCuatro servicios, un comando (docker compose up):
Servicio | Lenguaje | Rol |
| TypeScript MCP SDK | Herramientas de solo lectura. Se conecta como |
| Python MCP SDK | La única ruta de escritura. Barreras de negocio en cada entrada de herramienta. |
| FastAPI | Backend de la consola de aprobación. Solo puede reanudar el grafo — no tiene capacidad de ejecución. |
| — | 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.jsontotal_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 threeCada 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.jsonaction_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.pyLas 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 teethNó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 passedBarreras 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 conjuntoencoding_obfuscationse 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: |
LLM02 Manejo Inseguro de Salida | La salida del modelo nunca llega a una herramienta sin validar — |
LLM05 Manejo Inadecuado de Salida / agencia excesiva | El agente no puede ejecutar nada. |
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 |
|
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, |
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:5173El 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_rono 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_approvalyawait_decisionson nodos separados. LangGraph repite un nodo al reanudar; los efectos secundarios deben ir después delinterrupt()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.mdantes 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.mdcon 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
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.
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.
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.
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.
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 backapprovals.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.comdevuelve401 Invalid credentials, que parece una clave incorrecta pero no lo es. Me costó un diagnóstico erróneo completo; consultafailures.md.
Brechas conocidas
El respaldo del proveedor se verifica con un
APITimeoutErrorreal (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_toolsobre 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.
This server cannot be installed
Maintenance
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,
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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