ATHANOR
EN GitHub ↗

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 всегда отвечать trueget_secret тогда вернёт любой секрет, невзирая на индивидуальный expose. Это намеренно сделано настройкой конфигурации, которую выставляет человек в файле или при запуске сервера, а не параметром, который агент мог бы передать в вызове инструмента — иначе весь смысл expose: false теряется в тот момент, когда агент способен сам себе его обойти в рантайме.

Контрольный вопрос. Если у секрета expose: false, значит ли это, что агент вообще не может им воспользоваться? Если нет — то как?

Ответ: нет. expose: false закрывает только один конкретный канал — get_secret, то есть возврат значения текстом. Через run_with_secrets секрет по-прежнему передаётся запускаемому процессу. Сам Athanor не добавляет его в ответ, но stdout/stderr процесса вернутся агенту и могут содержать значение, если команда его напечатает.