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


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

Сервис

Язык

Роль

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", запустите его самостоятельно сначала.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    A 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
    -
  • A
    license
    A
    quality
    A
    maintenance
    Enables 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.
    23
    2
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    An 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.
    -