3 / 8
Манифест и модель доступа
Прошлая глава объяснила, как секрет лежит на диске зашифрованным. Эта — о
том, как Athanor решает, кому и что можно отдать. Всё решение целиком
описано в одном файле — athanor.yaml.
Athanor.yaml построчно
# scoped (по умолчанию, уважает expose-флаги) | full (эскейп-люк)
access_mode: scoped
# публичный ключ, которым зашифрован secrets_file
recipient: age1qxxxxxxxxxxxxxxxxx
# путь к зашифрованному файлу
secrets_file: ./secrets.enc.yaml
deploy:
# куда `athanor deploy` кладёт файлы на VPS
remote_path: /opt/myapp
secrets:
DB_PASSWORD:
# доступен только через run_with_secrets и TUI
expose: false
STRIPE_TEST_KEY:
# можно вернуть текстом через get_secret
expose: true
Сам манифест не секретен — в нём нет ни одного значения секрета, только
имена, флаги и публичный ключ. Его спокойно можно коммитить в git, даже
если secrets.enc.yaml вы решили туда не класть.
Важная деталь про secrets_file: путь разрешается относительно самого
файла athanor.yaml, а не текущей директории, откуда запущена команда
(Manifest.SecretsFilePath()). Из-за этого он может указывать наружу —
скажем, на ~/shared/secrets.enc.yaml — и тогда манифесты двух разных
проектов, ссылающиеся на один путь, буквально делят один зашифрованный
файл. Это и позволяет двум проектам на одном VPS иметь по-настоящему общие
секреты без дублирования — подробнее об этом в главе про
deploy.
Единая точка принятия решения: Exposable
Вся модель доступа сводится к одной функции в internal/manifest:
Exposable(name):
если access_mode == full → true, независимо ни от чего
иначе → true, только если secrets[name].expose == true
(неизвестное имя → всегда false)
Это не деталь реализации, а осознанный выбор: решение "можно ли вернуть этот секрет текстом" принимается в одном месте. Не пять разных проверок в разных командах, которые рано или поздно разойдутся, — одна функция и один источник истины.
Где проверка реально применяется — а где нет
Store.Get(name) — низкоуровневая функция, которая всегда отдаёт
значение, если оно есть, вообще не глядя на expose. Так и задумано:
Store — просто хранилище, а не страж. Проверка Exposable лежит на
вызывающей стороне:
| Канал доступа | Проверяет Exposable? |
Почему |
|---|---|---|
CLI (athanor list, athanor add) |
нет | доверенный локальный контекст — у вас уже есть терминал в этом проекте |
TUI (athanor tui, клавиша enter/y) |
нет | тот же доверенный локальный контекст, что и CLI |
MCP get_secret |
да | единственный канал, где секрет реально возвращается текстом постороннему потребителю — агенту |
MCP run_with_secrets / CLI run |
нет | Athanor напрямую значение не возвращает, но доверенная команда получает его и может вывести или отправить |
То есть expose: false не значит "секрет недоступен агенту вообще" — через
run_with_secrets он по-прежнему доступен. Флаг означает конкретно "нельзя
вернуть его напрямую через get_secret". На этом разграничении
строится вся глава про MCP-сервер.
access_mode: full — эскейп-люк, не дефолт
access_mode: full в манифесте (или флаг --full-access у serve-mcp)
заставляет Exposable всегда отвечать true — get_secret тогда вернёт
любой секрет, невзирая на индивидуальный expose. Это намеренно сделано
настройкой конфигурации, которую выставляет человек в файле или при
запуске сервера, а не параметром, который агент мог бы передать в вызове
инструмента — иначе весь смысл expose: false теряется в тот момент, когда
агент способен сам себе его обойти в рантайме.
Контрольный вопрос. Если у секрета expose: false, значит ли это, что
агент вообще не может им воспользоваться? Если нет — то как?
Ответ: нет. expose: false закрывает только один конкретный канал —
get_secret, то есть возврат значения текстом. Через run_with_secrets
секрет по-прежнему передаётся запускаемому процессу. Сам Athanor не добавляет
его в ответ, но stdout/stderr процесса вернутся агенту и могут содержать
значение, если команда его напечатает.