Skip to main content
Glama
README.md
<div align="center">

<img src="assets/banner.svg" alt="citadel" width="100%"/>

**Шлюз безопасности для MCP-агентов.**
Стоит между ИИ-клиентом и MCP-серверами: применяет политики, не даёт заражённым данным уйти наружу, ловит подмену инструментов, а работу защиты показывает цифрами на воспроизводимом полигоне.

![tests](https://img.shields.io/github/actions/workflow/status/rephuren/citadel/ci.yml?style=flat-square&labelColor=050505&label=tests) ![node](https://img.shields.io/badge/node-20%2B-050505?style=flat-square&labelColor=050505&color=b3000f) ![dependencies](https://img.shields.io/badge/runtime%20deps-0-050505?style=flat-square&labelColor=050505&color=b3000f) ![license](https://img.shields.io/badge/license-MIT-050505?style=flat-square&labelColor=050505&color=b3000f)

</div>

<div align="center">
<img src="assets/benchmark.png" alt="результаты бенчмарка" width="100%"/>
<sub>результат <code>npm run bench</code>: девять атак и семь обычных задач против шести уровней защиты. Агент в бенчмарке намеренно наивный: см. раздел «Как читать цифры»</sub>
</div>

## Зачем это

ИИ-агент с доступом к инструментам опасен не из-за одной ошибки, а из-за сочетания трёх вещей: он читает **приватные данные**, он читает **недоверенный контент** (веб-страницы, письма, описания чужих инструментов) и у него есть **канал наружу** (почта, запросы). Если все три есть в одной сессии, достаточно одной фразы в чужой странице, и данные уходят атакующему. Остальное то же семейство: отравленные описания инструментов, невидимые символы, подмена определения после одобрения, одноимённые инструменты разных серверов.

`citadel` подключается к клиенту как один MCP-сервер, а за ним запускает настоящие. Все вызовы идут через проверки, а каждое решение записывается в журнал. Статический аудит конфигов и описаний делает отдельный проект [basilisk](https://github.com/rephuren/basilisk), его правила встроены сюда как копия: шлюз проверяет определения ещё и во время работы.

## Быстрый старт

```bash
git clone https://github.com/rephuren/citadel && cd citadel
npm ci && npm run build

# 1. описать серверы и политику (пример: examples/citadel.json), затем проверить, что шлюз увидит
node dist/src/cli.js check --config examples/citadel.json

# 2. закрепить честные определения, пока серверам доверяете
node dist/src/cli.js pin --config examples/citadel.json

# 3. подключить шлюз к клиенту вместо серверов: в конфиге клиента один сервер
#    { "command": "node", "args": ["/путь/к/citadel/dist/src/cli.js", "serve", "--config", "/путь/к/citadel.json"] }

# 4. смотреть, что происходит, и одобрять вызовы
node dist/src/cli.js dashboard --audit .citadel/audit.jsonl      # http://127.0.0.1:8120
node dist/src/cli.js approvals --config examples/citadel.json
node dist/src/cli.js approve <номер> --config examples/citadel.json
```

Справочник по всем настройкам: [docs/configuration.md](docs/configuration.md).

## Что делает шлюз

| слой | что происходит |
| :-- | :-- |
| **пространства имён** | инструменты видны как `сервер__инструмент`, поэтому сервер не может затенить чужой инструмент одним и тем же именем |
| **определения** | невидимые символы и Unicode-теги вырезаются, инструменты с опасными описаниями (правила [basilisk](https://github.com/rephuren/basilisk)) скрываются от модели |
| **закрепление** | `pin` запоминает хеши определений; после этого изменённый или новый инструмент скрывается, даже если сервер сообщил об обновлении (rug pull) |
| **политики** | запрет, разрешение или подтверждение по маске имени, ограничения аргументов (шаблон, список, длина, запрещённые подстроки), лимиты вызовов, размера аргументов и результата, таймаут |
| **taint tracking** | метки `private`, `untrusted`, `sink`. Режим `trifecta` не даёт вызвать «канал наружу», пока в сессии были и приватные данные, и недоверенный контент. Дополнительно находится прямое перетекание: общая подстрока длиннее 31 символа между аргументами и результатом |
| **результаты** | поиск инъекций в ответах инструментов (`block`, `annotate`, `log`); ответ с признаками инъекции делает сессию недоверенной |
| **подтверждения** | вызов может потребовать человека; одобрение даётся вне канала агента командой `citadel approve` и действует один раз на ровно эти аргументы |
| **изоляция сервера** | серверы получают только минимальное окружение; запросы серверов к клиенту (`sampling`, `roots`, `elicitation`) отклоняются |
| **журнал** | JSON по строке на событие; значения аргументов не пишутся, только форма и отпечаток |

## Полигон и бенчмарк

В `range/` лежат намеренно уязвимые учебные MCP-серверы: приватные заметки, «веб» со страницей-инъекцией, почта как единственный канал наружу, файлы с обходом пути, задачи с разрушительным действием и недобросовестный сторонний сервер (отравленное описание, скрытый текст в Unicode-тегах, подмена после первого вызова, одноимённый `send_email`). Они ничего не выполняют и не ходят в сеть, а каждое действие пишут в журнал эффектов, по которому и считается успех атаки. Все «секреты» синтетические (`CANARY-…`), адрес атакующего зарезервирован под документацию.

`npm run bench` прогоняет девять атак и семь обычных задач через шесть уровней защиты и обновляет [docs/benchmark.md](docs/benchmark.md):

- **D0** прямое подключение, без шлюза
- **D1** шлюз с настройками по умолчанию
- **D2** + очистка и скрытие опасных определений, закрепление
- **D3** + политики аргументов и подтверждение разрушительных вызовов
- **D4** + отслеживание заражённых данных
- **D5** + проверка результатов на инъекции

Что видно из таблицы:

- Без шлюза удаются все девять атак, а с полной защитой ни одна.
- Каждый слой закрывает свой класс: пространства имён останавливают затенение и засорение контекста, закрепление ловит rug pull, очистка находит скрытые Unicode-теги, политики аргументов останавливают обход пути, SSRF и удаление, а утечку «приватное + недоверенное + наружу» не останавливает никто, кроме taint tracking.
- **Цена защиты видна честно:** обычная задача B7 (страница, затем заметки, затем письмо с текстом от пользователя) на D4 и D5 требует подтверждения человека, потому что шлюз не умеет отличить её от атаки по составу вызовов. Это осознанный компромисс режима `trifecta`: в нём лучше спросить человека, чем промолчать.

### Как читать цифры

Агент в бенчмарке не языковая модель. Это детерминированный «послушный» агент: он выполняет любые указания вида `call сервер.инструмент with {json}`, найденные в описаниях и результатах, как самая наивная модель. Это сделано ради воспроизводимости и нулевой стоимости. Цифры показывают, **что останавливает защита при гарантированно уязвимом агенте**, а не насколько уязвимы настоящие модели и как часто они поддаются таким фразам. Настоящие модели могут вести себя лучше или хуже, а атакующий может перефразировать. Подключение локальной модели вместо наивного агента сделано отдельной задачей и пока не реализовано.

## Безопасность самого шлюза

- Нулевые зависимости во время работы: только стандартная библиотека Node.
- Лимиты на всё: строка протокола 8 МБ, аргументы, результат, число инструментов (2000) и страниц списка (50), глубина обхода схем, рейт вызовов.
- Дашборд слушает только `127.0.0.1`, принимает только GET, отдаёт страницу с CSP по nonce, а данные из журнала выводит через `textContent`.
- Файл подтверждений пишется атомарно с правами `0600`, а модель его не видит и не может одобрить свой вызов.
- Журнал не содержит значений аргументов. Если писать журнал нельзя, шлюз продолжает работать и пишет об этом в stderr.

## Как проверялось

- 50 тестов: протокол и разбор строк, конфигурация (14 видов ошибок), политики, taint, очистка, подтверждения, журнал, окружение сервера; шлюз целиком на настоящих учебных серверах (пространства имён, скрытие, политики, taint в трёх режимах, подтверждение, результаты, лимиты, закрепление и rug pull, падение сервера, запросы сервера к клиенту, таймауты, stdio-режим); дашборд; бенчмарк.
- Тест бенчмарка проверяет свойства, а не только числа: без шлюза удаются все атаки, с полной защитой ни одна, защита монотонна (остановленная атака не возвращается на более сильном уровне), ни одна обычная задача не ломается молча, а таблица в `docs/benchmark.md` совпадает с тем, что считает код сейчас.
- В CI на каждый коммит: сборка со строгим TypeScript и тесты на Node 20, 22 и 24.

## Ограничения

- Только инструменты MCP и только серверы по stdio. Ресурсы, промпты, sampling и удалённые серверы по HTTP не поддерживаются.
- Шлюз не песочница: процесс сервера выполняется с вашими правами. Он контролирует то, что проходит между агентом и сервером, но не изолирует сам сервер.
- Правила обнаружения инъекций это эвристики по тексту, в основном английские. Перефразирование или другой язык могут их обойти. Поэтому главный упор сделан на структурные защиты (закрепление, taint, ограничения аргументов), которые не зависят от формулировки.
- Taint-tracking работает на уровне сессии и меток, которые вы расставляете в политике. Неразмеченный инструмент не считается ни приватным, ни исходящим. Прямое перетекание определяется по общей подстроке, а пересказ или изменённый текст ей не поймать, поэтому основу составляет правило «тройки».
- Подтверждение через `citadel approve` требует, чтобы человек был рядом. Для полностью автономных агентов нужен режим `deny`.
- Бенчмарк синтетический и по числу сценариев небольшой: это демонстрация и регрессионный тест, а не оценка защищённости реальных систем.

## Лицензия

MIT, см. [LICENSE](LICENSE).