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

Обоснование
Большинство 'безопасности AI-агентов' — это текст промпта: 'пожалуйста, спросите человека перед возвратом.' Промпт — это запрос, а не ограничение — один ignore previous instructions и его нет.
FourEyes помещает гарантию туда, куда промпт добраться не может:
Уровень | Где находится | Что на самом деле делает |
① Контент | Оборачивает текст клиента в явные границы ненадёжных данных; помечает шаблоны инъекций (поддельные маркеры | |
② Структура | топология графа + два MCP-сервера | Путь записи физически проходит через |
③ Бизнес-ограничение | Детерминированные проверки на входе каждого инструмента записи: сумма ≤ заказ, сумма ≤ лимит $500, соответствие статусу/окну, отсутствие предыдущего возврата, уникальный ключ идемпотентности — с блокировкой строк |
Два свойства обеспечиваются тестами, а не комментариями:
Ни один путь от 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):
Сервис | Язык | Роль |
| TypeScript MCP SDK | Инструменты только для чтения. Подключается как |
| Python MCP SDK | Единственный путь записи. Бизнес-ограничения на входе каждого инструмента. |
| FastAPI | Бэкенд консоли утверждения. Может только возобновить граф — у него нет возможности выполнения. |
| — | Бизнес-таблицы + контрольные точки LangGraph. |
Консоль (console/, React + TypeScript + Vite) — это один экран: карточки ожидающих действий → утвердить / отклонить.
Почему два MCP-сервера вместо одного с двумя группами инструментов? Граница разрешений проводится на протокольном и сетевом уровне, а не внутри функции. Инструменты чтения не имеют шлюза, потому что шлюзование всего вызывает усталость от утверждения — шлюз везде означает шлюз нигде. Только необратимые операции записи имеют шлюз.
Измеренные результаты
Каждое число ниже получено из команды в этом репозитории. Ничего здесь не является оценочным.
Состязательное тестирование — 53 письма, 7 категорий атак
.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 threeКаждое письмо автоматически утверждается во время прогона — намеренно симулируется человек, который тоже обманут — так что тестируется бизнес-ограничение, а не человек.
Два показателя намеренно отчётываются раздельно: ноль несанкционированных выполнений — это утверждение уровня выполнения; уровень обмана — это эксперимент уровня рассуждений. Никто не хочет систему возвратов, которая безопасна на 96%, поэтому утверждение безопасности — это число, а не процент.
Выбор действия — 100 размеченных тикетов
.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)Состав набора данных важнее количества. 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 | Все три уровня. Контент: |
LLM02 Insecure Output Handling | Вывод модели никогда не достигает инструмента непроверенным — |
LLM05 Improper Output Handling / excessive agency | Агент не может ничего выполнить. |
LLM06 Sensitive Information Disclosure | Сервер поиска ограничен клиентом тикета; роль чтения имеет только SELECT. |
LLM07 System Prompt Leakage |
|
LLM08 Excessive Agency | Операции записи имеют шлюз через обязательное прерывание (человек в цикле); чтение/запись разделены между двумя отдельными MCP-серверами с отдельными ролями БД. |
LLM09 Overreliance | Оценки траекторий проверяют последовательности инструментов; бенчмарк измеряет как правильность, так и уровень ложных блокировок, поэтому излишняя блокировка видна, а не скрыта за утверждением безопасности. |
LLM10 Model Denial of Service | Тайм-ауты 30 с, |
Запуск
Требуется 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].
Как проверялся вывод ИИ
Состязательная проверка кода. Четыре независимых агента проверки (топология HITL, обход инъекций, полнота защиты, корректность) дали 23 необработанных результата; каждый затем был передан отдельному агенту с инструкцией опровергнуть его на основе реального кода. 23 → 3 подтвержденных. Без этапа опровержения реальная ошибка была бы похоронена под ложными срабатываниями.
Подтвержденный HIGH был реальным недостатком дизайна, а не опечаткой: согласие и действие были разъединены, поэтому при повторном воспроизведении человек мог одобрить эскалацию, пока выполнялся возврат средств. Исправлено структурно (ADR-009) плюс 4 регрессионных теста.
Сквозные демонстрации выявили то, что не могли выявить модульные тесты. Защита третьего уровня и пробелы в регулярных выражениях первого уровня были обнаружены демонстрациями red-team после того, как модульные тесты и протокольные дымовые тесты прошли — ошибки были в стыках между компонентами.
Истинное значение самого стенда red-team сначала было неверным. Изначально он сообщал о 10 несанкционированных выполнениях; заказы перевозчика случайно подлежали законному возврату. Опасный режим отказа для метрики безопасности — это не уродливое число, а красивое число, измеренное относительно неправильного базового уровня.
Сгенерированные метки проверяются машиной. Метки бенчмарка проверяются на соответствие детерминированным правилам политики перед добавлением в набор данных, поэтому метрика измеряет соответствие политике, а не соответствие другой модели.
Вне области видимости (намеренно)
Нет голоса/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 backapprovals.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", запустите его самостоятельно сначала.
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