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_secrets (и athanor 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.