ATHANOR
EN GitHub ↗

6 / 8

MCP-сервер

Все прошлые главы описывали инструмент для человека. Эта — про то, как ровно те же секреты становятся доступны ИИ-агенту, ради чего весь проект и затевался (глава 1).

Что такое MCP в двух словах

Model Context Protocol — открытый стандарт для инструментов, которые агент может вызывать. Athanor реализует сервер этого протокола (athanor serve-mcp), говорящий по stdio — обычному stdin/stdout процесса, не по сети. К Claude это никак не привязано: любой MCP-совместимый клиент (Claude Code, Claude Desktop, Cursor, Windsurf, Cline и другие) подключает один и тот же бинарник через свой конфиг. Athanor не знает и не спрашивает, какой именно клиент к нему подключился — с точки зрения сервера это просто вызовы двух инструментов.

Инструмент get_secret

вход:  { "name": "STRIPE_TEST_KEY" }
выход: { "value": "sk-test-..." }             — если Exposable(name) == true
       ошибка: 'secret "DB_PASSWORD" is not exposable
                (expose:false in athanor.yaml); use run_with_secrets instead,
                or set access_mode: full to override'   — если false

Это единственный инструмент, где значение секрета буквально возвращается текстом — то есть единственное место во всей системе, где решение Exposable() из главы 3 реально применяется. Если сервер запущен с --full-access (или в манифесте стоит access_mode: full), проверка пропускается для всех секретов, а сервер при старте печатает в stderr явное предупреждение — чтобы это нельзя было включить и забыть.

Инструмент run_with_secrets

вход:  { "command": ["sh", "-c", "psql -c 'select 1' $DB_URL"] }
выход: { "stdout": "...", "stderr": "...", "exitCode": 0,
         "stdoutTruncated": false, "stderrTruncated": false }

Здесь expose вообще не проверяется — все секреты проекта подставляются в окружение команды. Ответ содержит stdout/stderr запущенной команды, а значит, команда может сама напечатать секрет и тем самым вернуть его агенту. Каждый поток ограничен 1 МиБ; соответствующий *Truncated сообщает об обрезании. command — это argv, массив аргументов, а не строка shell: exec.Command(argv[0], argv[1:]...) не проходит через интерпретатор командной строки, так что спецсимволы внутри значения секрета не могут случайно сломать разбор команды или привести к command injection. Если агенту нужны возможности shell — пайпы, подстановки — он сам оборачивает команду в ["sh", "-c", "..."]; тогда уже текст команды интерпретируется shell'ом, но значения секретов всё равно приходят через переменные окружения, а не через текст команды.

Честно о границах этой защиты

run_with_secrets — не непреодолимый барьер, а сдвиг барьера туда, где он приносит больше пользы. Агент, которому разрешено выполнять произвольные команды, технически может составить curl с секретом в URL и утащить значение наружу — с точки зрения ОС это ничем не отличается от любой другой команды с переменной окружения. Athanor не пытается сделать это невозможным — тогда пришлось бы резко ограничить, что агент вообще способен выполнять, и инструмент потерял бы смысл. Вместо этого он делает утечку видимой постфактум.

Audit-лог

Каждый вызов run_with_secretsathanor run из CLI) добавляет строку в ~/.local/state/athanor/audit.log:

2026-08-29T10:34:58Z    command="sh -c echo $TEST_KEY"  secrets=TEST_KEY

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

Контрольный вопрос. Почему run_with_secrets не проверяет флаг expose для каждого секрета, а get_secret — проверяет?

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