MCP stdioGo, один бинарникбез сетиMIT English
Прокси между MCP-клиентом и MCP-сервером. Записывает каждое сообщение и отказывает вызову по тому, что он делает, а не по тому, как он написан.
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 и скрипты, куда пишет весь день.
| уровень | deny | confirm |
|---|---|---|
| observe по умолчанию | записано | записано |
| enforce рабочий режим | отказ | записано, объявлено в stderr, передано дальше |
| strict пока непригоден | отказ | отказ |
mode: в файле политики принимает те же три значения, но флаг важнее файла —
в любую сторону. Файл политики предназначен для коммита и может принадлежать кому-то другому,
а конфигурацией клиента владеет тот, кто разбирается с поломкой в девять утра.
числа, а не обещания
enforce за этот период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 на корпусе атак и ложные срабатывания на настоящем трафике. Заголовочная «блокировок в неделю» отказывается считаться меньше чем по часу прошедшего времени — ставка, посчитанная по четырём вызовам, не ставка.
честно о границах
Отказать в нём — оставить клиента без инструментов, поэтому отравленное описание всё равно доходит до модели. Пиннинг ловит изменение и останавливает эффект, но не чтение.
Протокольный способ — elicitation/create, и лог хэндшейка показывает, что ни один подключённый клиент его не объявляет. Пока так, на strict подтверждение — это отказ с объяснением, а не молчаливое «как будто спросили».
Один процесс на сервер — одна сессия на сервер. Агент читает страницу через fetch, помечая тот прокси, и запускает код через другой, у которого сессия чистая. Модель угрозы кросс-серверная по своей природе, так что это настоящая дыра, а не деталь; общее состояние между процессами — сознательное изменение дизайна, а не заплатка.
Сервер, добавленный по 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", так что незащищённую
сессию потом видно.