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

Обоснование
Большинство 'безопасности AI-агентов' — это текст промпта: 'пожалуйста, спросите человека перед возвратом.' Промпт — это запрос, а не ограничение — один ignore previous instructions и его нет.
FourEyes помещает гарантию туда, куда промпт добраться не может:
Уровень | Где находится | Что на самом деле делает |
① Контент | Оборачивает текст клиента в явные границы ненадёжных данных; помечает шаблоны инъекций (поддельные маркеры | |
② Структура | топология графа + два MCP-сервера | Путь записи физически проходит через |
③ Бизнес-ограничение | Детерминированные проверки на входе каждого инструмента записи: сумма ≤ заказ, сумма ≤ лимит $500, соответствие статусу/окну, отсутствие предыдущего возврата, уникальный ключ идемпотентности — с блокировкой строк |
Два свойства обеспечиваются тестами, а не комментариями:
Ни один путь от START к
execute_actionне обходитinterrupt()— проверяется удалением узла прерывания из графа и доказательством того, чтоexecute_actionстановится недостижимым.Авторизация происходит из утверждённой строки базы данных, а не из изменяемого состояния графа.
execute_actionповторно читает строкуapprovals, которую подписал человек, и сверяет её с предложением; несоответствие отклоняется и аудируется. (Это выявилось в ходе состязательного обзора, который показал, что исходный код мог выполнить возврат, в то время как человек утвердил эскалацию — см.failures.md.)
Related MCP server: MCP Customer Support Demo
Архитектура
┌──────────────────────────────────────┐
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 deployed
Maintenance
Related MCP Connectors
Human-in-the-loop review and approval for AI agents. Audit trail, approval policies, native MCP.
Zero-secret MCP gateway for AI agents: risk-scored, audited calls with human-in-the-loop approval.
A paid remote MCP for agent memory MCP, built to return verdicts, receipts, usage logs, and audit-re
Build and manage AI-native customer support agents from Claude or any MCP client.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceA governed MCP server for integrating AI agents with customer data, featuring role-based access control, field redaction, and human-in-the-loop approval for secure support operations.1-
- FlicenseNot gradedqualityCmaintenanceEnables customer support operations such as order lookup, store credit, refunds, and audit log review through an agent using safe, typed MCP tools.-
- AlicenseAqualityAmaintenanceEnables AI agents to perform helpdesk tasks over MCP, including ticket management, knowledge base search, and reply drafting, with optional pay-per-action USDC settlement and human approval workflows.232MIT
- FlicenseNot gradedqualityCmaintenanceAn MCP server that gives an agent product data, feedback, metrics, sandbox analysis, and gated Jira tickets — so it can investigate drops, write PRDs, and file work with evidence.-