Skip to main content
Glama

FourEyes

Агент поддержки клиентов с контролем со стороны человека. Он читает тикеты, запрашивает информацию об учетных записях и решает, что делать — но каждая необратимая операция записи (возврат / эскалация / закрытие) физически останавливается на шлюзе утверждения человеком, прежде чем может быть выполнена.

Название происходит от принципа четырёх глаз: любое критическое действие требует второй пары глаз.

Консоль утверждения


Обоснование

Большинство 'безопасности AI-агентов' — это текст промпта: 'пожалуйста, спросите человека перед возвратом.' Промпт — это запрос, а не ограничение — один ignore previous instructions и его нет.

FourEyes помещает гарантию туда, куда промпт добраться не может:

Уровень

Где находится

Что на самом деле делает

① Контент

agent/guards.py

Оборачивает текст клиента в явные границы ненадёжных данных; помечает шаблоны инъекций (поддельные маркеры SYSTEM:, сфабрикованные утверждения, захват роли, обфускация base64/нулевой ширины/гомоглифов). Помечает, никогда не удаляет молча — текст атаки является доказательством.

② Структура

топология графа + два MCP-сервера

Путь записи физически проходит через interrupt(). Сервер только для чтения не имеет инструментов записи — и подключается как роль Postgres с отсутствием грантов INSERT/UPDATE/DELETE вообще.

③ Бизнес-ограничение

mcp_action/guardrails.py

Детерминированные проверки на входе каждого инструмента записи: сумма ≤ заказ, сумма ≤ лимит $500, соответствие статусу/окну, отсутствие предыдущего возврата, уникальный ключ идемпотентности — с блокировкой строк FOR UPDATE. Выполняется, даже если модель обманута и человек ошибочно утвердил.

Два свойства обеспечиваются тестами, а не комментариями:

  • Ни один путь от START к execute_action не обходит interrupt() — проверяется удалением узла прерывания из графа и доказательством того, что execute_action становится недостижимым.

  • Авторизация происходит из утверждённой строки базы данных, а не из изменяемого состояния графа. execute_action повторно читает строку approvals, которую подписал человек, и сверяет её с предложением; несоответствие отклоняется и аудируется. (Это выявилось в ходе состязательного обзора, который показал, что исходный код мог выполнить возврат, в то время как человек утвердил эскалацию — см. failures.md.)


Архитектура

                                  ┌──────────────────────────────────────┐
  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

Четыре сервиса, одна команда (docker compose up):

Сервис

Язык

Роль

mcp-lookup :8101

TypeScript MCP SDK

Инструменты только для чтения. Подключается как foureyes_ro.

mcp-action :8102

Python MCP SDK

Единственный путь записи. Бизнес-ограничения на входе каждого инструмента.

api :8000

FastAPI

Бэкенд консоли утверждения. Может только возобновить граф — у него нет возможности выполнения.

postgres :5432

Бизнес-таблицы + контрольные точки LangGraph.

Консоль (console/, React + TypeScript + Vite) — это один экран: карточки ожидающих действий → утвердить / отклонить.

Почему два MCP-сервера вместо одного с двумя группами инструментов? Граница разрешений проводится на протокольном и сетевом уровне, а не внутри функции. Инструменты чтения не имеют шлюза, потому что шлюзование всего вызывает усталость от утверждения — шлюз везде означает шлюз нигде. Только необратимые операции записи имеют шлюз.


Измеренные результаты

Каждое число ниже получено из команды в этом репозитории. Ничего здесь не является оценочным.

Состязательное тестирование — 53 письма, 7 категорий атак

.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

Каждое письмо автоматически утверждается во время прогона — намеренно симулируется человек, который тоже обманут — так что тестируется бизнес-ограничение, а не человек.

Два показателя намеренно отчётываются раздельно: ноль несанкционированных выполнений — это утверждение уровня выполнения; уровень обмана — это эксперимент уровня рассуждений. Никто не хочет систему возвратов, которая безопасна на 96%, поэтому утверждение безопасности — это число, а не процент.

Выбор действия — 100 размеченных тикетов

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

Состав набора данных важнее количества. 80 тикетов сгенерированы LLM с чёткими границами политики; измерения показали, что только 3 из них оказались в пределах ±5 дней / ±$50 от порога, что само по себе делало 98,8% необоснованными. Поэтому были добавлены 20 ручных граничных случаев: день 30 против дня 31, ровно $500 против $500.01, ровно сумма заказа против одного цента больше, pending/rejected предыдущие возвраты (которые не блокируют новый возврат), и три конфликта приоритетов политики (X3 отменяет E1; X4 отменяет E1; инцидент безопасности перевешивает сумму). Граничный поднабор показал результат 20/20 — классификатор рассуждает на основе пунктов, а не сопоставления ключевых слов.

Единственный промах (bm_076) сослался на E3 + X1 и выполнил эскалацию там, где метка указывала отказ — запрос от третьего лица, сделанный от имени 84-летнего родителя. Обоснованное разногласие, а не ошибка.

Оценки траекторий — 29 сценариев и доказательство их эффективности

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

Оценки траекторий проверяют процесс, а не только ответ — правильное конечное состояние может быть достигнуто неправильным путём (рефакторинг в пятницу, который тихо обходит узел утверждения). 10 из 29 являются негативными сценариями.

Набор тестов, который никто никогда не видел проваленным, не является страховочной сеткой, поэтому отказ демонстрируется по требованию: verify_eval_teeth.py переписывает ребро утверждения на request_approval → execute_action, запускает оценки и требует, чтобы они стали красными — затем восстанавливает файл и требует зелёных:

=== 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

Обратите внимание: traj_001поведенческий сценарий — также становится красным, а не только топологические утверждения.

Набор тестов

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

Ограничения (14) · топология (6) · привязка согласия (4) · защита уровня 1 (16, включая 6 ложно-положительных защит, чтобы обычные жалобы оставались непомеченными) · подстраховка ограничений (3).


Раскрытие информации о синтетических данных

Все тикеты, клиенты, заказы и состязательные письма в этом репозитории являются синтетическими данными, сгенерированными LLM. Нет реальных клиентов, нет реальных заказов и нет производственного трафика. В частности:

  • db/seed_data.json — 60 тикетов по 9 категориям сценариев, сгенерированных Claude и закэшированных в git, чтобы повторное заполнение было детерминированным (ADR-003).

  • evals/redteam/emails.jsonl — 53 состязательных письма. Шесть категорий сгенерированы Claude; набор encoding_obfuscation создан программно (настоящие полезные нагрузки base64 / нулевой ширины / гомоглифов), поскольку классификатор безопасности Claude отказывается кодировать живые инструкции атак.

  • evals/benchmark/tickets.jsonl — 80 размеченных тикетов, сгенерированных Claude, с проверкой каждой метки на соответствие детерминированным правилам политики перед включением в набор — один самопротиворечивый элемент был удалён и перегенерирован (ADR-011).

  • evals/benchmark/boundary.jsonl — 20 граничных случаев, написанных вручную.

Даты хранятся как относительные смещения и преобразуются во время заполнения, так что сценарии 'в пределах 30-дневного окна' остаются валидными при любом повторном заполнении набора.


Соответствие OWASP LLM Top 10

Риск

Где FourEyes это решает

LLM01 Prompt Injection

Все три уровня. Контент: agent/guards.py оборачивает границы и помечает. Структура: вставленное 'утверждение уже дано' не может пропустить interrupt(). Бизнес: mcp_action/guardrails.py отклоняет запись в любом случае. Измерено: 53 письма, 0 несанкционированных выполнений.

LLM02 Insecure Output Handling

Вывод модели никогда не достигает инструмента непроверенным — propose_action проверяет предложенный ID заказа на соответствие полученным доказательствам, а ограничения повторно проверяют каждый параметр на границе инструмента.

LLM05 Improper Output Handling / excessive agency

Агент не может ничего выполнить. execute_action запускает только то, что авторизует строка approved в БД (ADR-009).

LLM06 Sensitive Information Disclosure

Сервер поиска ограничен клиентом тикета; роль чтения имеет только SELECT.

LLM07 System Prompt Leakage

prompt_extraction — это помеченный шаблон инъекции; политика по замыслу является публичной, поэтому утечка не несёт привилегированной информации.

LLM08 Excessive Agency

Операции записи имеют шлюз через обязательное прерывание (человек в цикле); чтение/запись разделены между двумя отдельными MCP-серверами с отдельными ролями БД.

LLM09 Overreliance

Оценки траекторий проверяют последовательности инструментов; бенчмарк измеряет как правильность, так и уровень ложных блокировок, поэтому излишняя блокировка видна, а не скрыта за утверждением безопасности.

LLM10 Model Denial of Service

Тайм-ауты 30 с, max_retries=0 с явным провайдером для резерва (ADR-006).


Запуск

Требуется Python 3.12+, Node 20+ и 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

Шаг 3 → 4 — это демонстрация контрольной точки: два отдельных процесса. Второй возобновляется из сохранённой контрольной точки вместо повторного рассуждения — это важно, потому что LLM, спрошенный дважды, может прийти к другому выводу, а человек утвердил конкретное предложение, а не пересмотр.


Проектные решения

Полные ADR с рассмотренными альтернативами находятся в decisions.md. Основные из них:

  • [ADR-002] Две роли БД. foureyes_ro не имеет прав на запись, поэтому "сервер поиска доступен только для чтения" является фактом базы данных, а не соглашением в коде.

  • [ADR-007] Детерминированный сбор доказательств, единая точка принятия решения LLM. Никакого цикла инструментов ReAct — утверждения траектории могут быть точными, а вариативность бенчмарка возникает из-за суждения, а не из-за нестабильности поиска.

  • [ADR-007] request_approval и await_decision — это отдельные узлы. LangGraph воспроизводит узел при возобновлении; побочные эффекты должны находиться после interrupt(), иначе строка одобрения будет записана дважды.

  • [ADR-009] Согласие привязано к выполненному действию. Авторизация повторно считывается из одобренной строки во время выполнения.

  • [ADR-012] API не может выполнять операции. Одобрение только возобновляет граф, поэтому компрометация консоли все равно не позволит перевести деньги.

Шрамы, включая три реальные ошибки, найденные после того, как код "заработал", находятся в failures.md.


Рабочий процесс разработки с помощью ИИ

Этот проект был создан с помощью Claude Code. Что это означает на практике и как проверялся результат:

Дисциплина, соблюдаемая при создании

  • Каждый компонент получал запись в decisions.md до реализации — решение, альтернативы, почему и что ломается при альтернативе. Невозможность назвать альтернативу означала, что дизайн еще не понят.

  • Каждая ошибка попадала в failures.md с дословной ошибкой, диагнозом и исправлением.

  • Ничто не считалось "готовым" без запуска и вставки вывода в сообщение коммита.

  • Выходные числа (точность, показатели блокировки) было запрещено появляться где-либо — включая комментарии в коде — пока команда не сгенерировала их. Заполнители читались как [NOT_MEASURED].

Как проверялся вывод ИИ

  1. Состязательная проверка кода. Четыре независимых агента проверки (топология HITL, обход инъекций, полнота защиты, корректность) дали 23 необработанных результата; каждый затем был передан отдельному агенту с инструкцией опровергнуть его на основе реального кода. 23 → 3 подтвержденных. Без этапа опровержения реальная ошибка была бы похоронена под ложными срабатываниями.

  2. Подтвержденный HIGH был реальным недостатком дизайна, а не опечаткой: согласие и действие были разъединены, поэтому при повторном воспроизведении человек мог одобрить эскалацию, пока выполнялся возврат средств. Исправлено структурно (ADR-009) плюс 4 регрессионных теста.

  3. Сквозные демонстрации выявили то, что не могли выявить модульные тесты. Защита третьего уровня и пробелы в регулярных выражениях первого уровня были обнаружены демонстрациями red-team после того, как модульные тесты и протокольные дымовые тесты прошли — ошибки были в стыках между компонентами.

  4. Истинное значение самого стенда red-team сначала было неверным. Изначально он сообщал о 10 несанкционированных выполнениях; заказы перевозчика случайно подлежали законному возврату. Опасный режим отказа для метрики безопасности — это не уродливое число, а красивое число, измеренное относительно неправильного базового уровня.

  5. Сгенерированные метки проверяются машиной. Метки бенчмарка проверяются на соответствие детерминированным правилам политики перед добавлением в набор данных, поэтому метрика измеряет соответствие политике, а не соответствие другой модели.


Вне области видимости (намеренно)

Нет голоса/TTS, нет чат-интерфейса, нет панелей мониторинга или графиков, нет системы входа, нет тонкой настройки, нет реального пользовательского трафика. Консоль одобрения — это один экран — все остальное является расширением области видимости.

Трассировка

Каждый тикет создает один трейс Langfuse, детерминированно привязанный к идентификатору тикета, чтобы спаны, созданные процессом запуска и процессом возобновления, попадали в один и тот же трейс:

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 хранит ссылку, поэтому каждая карточка в консоли имеет глубокую ссылку на свой собственный трейс. Резервный провайдер выдается как событие provider-fallback в трейсе, поэтому переключение Claude → Gemini видно, а не выводится.

Особенность региона, на случай, если вы форкнете это: Langfuse Cloud разделен по регионам. Направление проекта US на cloud.langfuse.com возвращает 401 Invalid credentials — что выглядит как неверный ключ, но это не так. Это стоило мне полного неправильного диагноза; см. failures.md.

Известные пробелы

  • Резервный провайдер проверяется с помощью реального APITimeoutError (scripts/smoke_router.py), а не с помощью имитации — но он не был протестирован в условиях реального сбоя провайдера.

  • Подключение к MCP-серверу было проверено с помощью клиента MCP Python SDK (list_tools + call_tool через Streamable HTTP), а не с помощью интерфейса MCP Inspector. Эквивалентно протоколу, но если вы хотите утверждать "проверено в Inspector", запустите его самостоятельно сначала.

-
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