MCP stdioGo, один бинарникбез сетиMIT English

effectgate

Прокси между MCP-клиентом и MCP-сервером. Записывает каждое сообщение и отказывает вызову по тому, что он делает, а не по тому, как он написан.

клиент effectgate сервер

write_file path = ./src/proxy.go allow

write_file path = ~/.cursor/mcp.json deny

правило, которому подчинено всё

Управляй эффектами, не содержимым

«Это команда?» — вопрос без ответа. «Этот вызов сейчас запишет в ~/.ssh?» — факт.

В MCP инструкция и данные — одни и те же байты. Поэтому решение принимается не о тексте, а о последствии: что будет записано, что будет выполнено, откуда пришли данные. Атаку класса CurXecute останавливает не распознавание инструкции внутри сообщения, а отказ в записи в файл, который решает, какие серверы запускаются.

Такое решение детерминированно, покрывается тестами, у него считается доля ложных срабатываний — и перефразирование его не обходит. Сигнатуры по содержимому в проекте есть, но они только повышают уровень taint и делают правила об эффектах строже. Блокировать они не могут и не должны: правила лежат в публичном репозитории, обойти их — тридцать секунд, а ложные срабатывания достаются пользователю каждый день.

политика — это данные

Три формы шаблона, и больше никаких

Пути приводятся к одной форме до сравнения, поэтому написания в обход правила не существует: ~, %USERPROFILE%, $HOME, сегменты .., симлинки, регистр, короткие имена 8.3, префиксы \\?\ и альтернативные потоки NTFS сводятся к одному виду первыми.

# internal/policy/default.yaml — или свой файл через --policy
paths:
  deny:               ["~/.ssh/**", "**/.env", "**/mcp.json"]
  confirm:            ["**/package.json"]
  confirm_if_tainted: ["**/*.sh"]

~/.ssh/** — поддерево, ~/.bashrc — один файл, **/.env — это имя где угодно. Отдельный класс exec — для инструментов с семантикой оболочки.

taint ставится по источнику, а не по тексту: результат от fetch, почты, issues помечает сессию, и запись, обычная во время честной работы, после этого требует подтверждения. Решение о происхождении содержимого, а не о его формулировке — поэтому переписывание его не снимает.

уровни

По умолчанию при установке — наблюдение, никогда не запрет

Инструмент, который ломает чужую работу в первый день, в первый же день и удаляют. Поэтому deny и confirm включаются не вместе: первый — про учётные данные, конфигурацию агента и стартовые файлы оболочки, куда честная работа не пишет; второй — про package.json, Makefile и скрипты, куда пишет весь день.

уровеньdenyconfirm
observe по умолчанию записано записано
enforce рабочий режим отказ записано, объявлено в stderr, передано дальше
strict пока непригоден отказ отказ

mode: в файле политики принимает те же три значения, но флаг важнее файла — в любую сторону. Файл политики предназначен для коммита и может принадлежать кому-то другому, а конфигурацией клиента владеет тот, кто разбирается с поломкой в девять утра.

числа, а не обещания

Метрика, которая решает судьбу проекта — блокировки в неделю

28вызовов инструментов обычной работы за 75 часов
0блокировок на enforce за этот период
5корректных отказов на намеренные пробы
(исключены из счёта слева)
24.5отказа в неделю на strict — цель меньше одного

strict здесь не используется — и не из-за плохого правила. Он отказывает и в confirm, а execute_blender_code относится к классу exec, так что один рабочий процесс в Blender даёт все эти 24.5 отказа целиком. Это не лечится большим объёмом данных; это лечится возможностью ответить на подтверждение вместо отказа.

Каждое число здесь печатает effectgate eval --benign — по 28 вызовам за 75 часов, так что его можно проверить, а не принять на веру. Если эта страница когда-то разойдётся с тем, что печатает команда, права команда. Recall на опубликованных сценариях атак — 100 %, и это самое лёгкое число из набора: правила писались после их прочтения, поэтому оно показывает, что правила совпадают со своими же примерами, а не что они обобщаются.

подкоманды

Что с этим делают

effectgate -- npx -y @modelcontextprotocol/server-filesystem C:\Users\me\tmp

Запускает настоящий сервер, передаёт stdin/stdout/stderr без изменений и пишет по одному JSON-объекту на сообщение. Свой файл на каждый запуск: клиент держит по прокси на сервер, и общий файл превращает ротацию в гонку процессов.

effectgate approve -- …  →  effectgate.lock

Записывает имя, описание и канонизированную схему каждого инструмента. Файл коммитится: сервер, который позже переписал описание или расширил схему, появляется в diff во время review — там, где это и надо замечать. --diff выходит с ненулевым кодом и годится для сборки.

effectgate verify · watch

Прокси — это строка в том же файле конфигурации, который он охраняет, поэтому эта часть живёт вне пути трафика. Записываются только объявления серверов, никогда файлы целиком: клиенты держат в них кеши и позиции окон и перезаписывают их постоянно. Имена переменных окружения — да, значения — никогда.

effectgate replay

Читаемая расшифровка сессии: направления, методы, какой ответ на какой запрос и за сколько, целевой инструмент каждого tools/call. Офлайн и детерминированно — сервер не запускается, один и тот же лог всегда даёт один и тот же текст.

effectgate eval --attack corpus/attack --benign ~/.effectgate/sessions

Recall на корпусе атак и ложные срабатывания на настоящем трафике. Заголовочная «блокировок в неделю» отказывается считаться меньше чем по часу прошедшего времени — ставка, посчитанная по четырём вызовам, не ставка.

честно о границах

Чего он не делает

tools/list никогда не блокируется

Отказать в нём — оставить клиента без инструментов, поэтому отравленное описание всё равно доходит до модели. Пиннинг ловит изменение и останавливает эффект, но не чтение.

confirm не умеет спросить

Протокольный способ — elicitation/create, и лог хэндшейка показывает, что ни один подключённый клиент его не объявляет. Пока так, на strict подтверждение — это отказ с объяснением, а не молчаливое «как будто спросили».

taint не переходит между серверами

Один процесс на сервер — одна сессия на сервер. Агент читает страницу через fetch, помечая тот прокси, и запускает код через другой, у которого сессия чистая. Модель угрозы кросс-серверная по своей природе, так что это настоящая дыра, а не деталь; общее состояние между процессами — сознательное изменение дизайна, а не заплатка.

только stdio, и это тихо

Сервер, добавленный по HTTP, не защищён, и об этом ничто не сообщает. Кроме того, stdio-прокси не видит, что процесс сервера делает на своих собственных сетевых соединениях: сервер со встроенной телеметрией выгружает данные, не отправив ни байта через прокси.

никакой тихой правки содержимого

Блокировка с понятным сообщением честна. Молча отредактированный результат инструмента ломает агента незаметно, поэтому санитизации здесь нет и не будет.

лог сессии — секрет

В нём дословно каждый вызов и каждый результат: содержимое файлов, ответы API и любые учётные данные, которые агент прочитал по дороге. Права сужены до владельца — на Windows через явный DACL, потому что 0600 в Go там почти ничего не значит. Обращаться как с файлом паролей.

установка

Один бинарник и одна строка в конфиге

Готовый бинарник — на странице релизов, рядом лежит SHA256SUMS.txt для проверки. Либо собрать самому. Зависимости за пределами стандартной библиотеки: golang.org/x/sys, fsnotify, yaml.v3.

go install github.com/pterbsgame-netizen/effectgate/cmd/effectgate@latest

Дальше — поставить его перед серверами, которые уже запущены. install находит конфиги клиентов на этой машине, печатает, что собирается изменить, и не пишет ничего, пока не убрать --dry-run. Переписываются только те объявления, которые меняются, — остальное в этих файлах остаётся байт в байт, — а uninstall достаёт исходные команды из обёрнутых.

effectgate install --dry-run
effectgate install
effectgate uninstall

Вручную это та же правка, один раз: команду сервера заменяют на effectgate плюс исходную команду. Для Claude Desktop это %APPDATA%\Claude\claude_desktop_config.json:

{
  "mcpServers": {
    "filesystem": {
      "command": "C:\\tools\\effectgate.exe",
      "args": [
        "--policy", "default",
        "--",
        "npx", "-y", "@modelcontextprotocol/server-filesystem", "C:\\Users\\me\\tmp"
      ]
    }
  }
}

После перезапуска клиента, если всё верно, не меняется ничего: те же инструменты, те же вызовы, те же ошибки — и сессия оказывается в логе.

Если что-то пошло не так: EFFECTGATE_OFF=1 в окружении клиента — и передача идёт вообще без проверок. Переменная читается раньше, чем открывается любой файл конфигурации, поэтому опечатка в политике не может стоить пользователю его инструментов. Лог при этом продолжает писаться и отмечает from: "off", так что незащищённую сессию потом видно.