Agent protocol: работа с секретами через MCP
Translation note: the canonical version is the English
agent-protocol.md(per the OSS-docs-in-English rule). This Russian copy is kept for the team; if it diverges, the English version is authoritative.
Для авторов AI-агентов и MCP-клиентов. Документ объясняет инвариант «agent never sees the value» (ADR-023 §3.7), полный перечень MCP-инструментов в семействе secrets_*, их семантику, лайфтайм запросов и формат ответов.
См. ADR-023 §3.7 для формальной спецификации и crates/devboy-mcp/src/secrets_tool.rs + secrets_provision.rs для реализации.
Базовый инвариант
Агент никогда не видит значения секретов. Только метаданные.
Это не «лучшая практика» и не runtime-проверка — это типовая граница в коде. Reply-структуры всех инструментов secrets_* физически не имеют поля value. Если в будущем рефакторинг добавит SecretString в reply — компиляция упадёт на маркерном трейте AgentSafeReply. Дополнительно работают:
- CI grep gate в
crates/devboy-mcp/tests/no_expose_secret_outside_allowlist.rs. Любой.expose_secret()вне allowlist → fail. - Negative integration test в
crates/devboy-mcp/tests/secret_tool_responses_never_leak_value.rs. Прогоняет фикстурный sentinel через каждыйsecrets_*инструмент и проверяет, что значения нет в ответе.
На стороне агента это значит: нельзя написать secrets.get и получить токен. Если нужно использовать значение — оно достаётся через high-level provider tool (например, get_issues с уже разрешённым в proxy токеном). Обход доступен только пользователю — через UI-диалог, который никогда не возвращается агенту в виде строки.
Перечень инструментов
Запросы UI-диалогов — асинхронные: агент инициирует диалог и опрашивает статус. Пользователь в свободном темпе вводит значение в окно/TUI; агент видит только ход событий.
secrets_list
Перечислить пути, объявленные в манифесте активного контекста.
Запрос
Все аргументы опциональны. Фильтры комбинируются по AND. include_internal по умолчанию false — внутренние пути (__sys/...) скрыты.
Ответ
Поля:
path— ADR-020 путь (<scope>/<provider>/<purpose>).status—registered/expiring(≤14 дней до истечения) /expired. Считается поexpires_at, не по live-probe источника. Это умышленно: probe мог бы случайно раскрыть факт отзыва токена пользователем.expires_at— ISO-8601 (YYYY-MM-DD) илиnull.source_name— на текущий момент всегдаnull(роутер не пробрасывается в MCP-сервер; field зарезервирован в wire-format).capabilities_hint—"read"(manual rotation) или"read,rotate"(provider-ui / provider-api).
Ответ отсортирован по path для стабильности тестов.
secrets_describe
Карточка одного пути.
Запрос
Ответ
Строгий superset secrets_list элемента + дополнительные поля метаданных:
Ошибки:
not-found— путь не виден в активном контексте (ни в проектном манифесте, ни в глобальном индексе с overrides).invalid-path— синтаксис не соответствует ADR-020.merge-failed— конфликт в манифесте (дубликат entry, недопустимая структура).
Manifest-gating: видны только пути, на которые ссылается активный проектный манифест. Глобальный индекс не утекает целиком.
secrets_request_provision
Попросить framework открыть UI-диалог, в который пользователь введёт новое значение секрета.
Запрос
mode опционален, по умолчанию "provision". Допустимое значение "rotation" форсирует destructive-confirm — но обычно для ротации удобнее использовать отдельный secrets_request_rotation (см. ниже).
Ответ
request_id — opaque строка вида prov-<12-hex>. Хранить и опрашивать через secrets_poll_status.
Lifecycle
Запрос expirится через 5 минут, если пользователь не нажал ни Save, ни Cancel. Агент должен предусмотреть таймаут на свой polling.
secrets_request_rotation
Та же семантика, что request_provision { mode: "rotation" }, но без двусмысленности на стороне вызова. Инструмент сам передаёт mode = Rotation в registry; UI поднимает диалог с явным destructive-confirm checkbox («I understand this overwrites the current secret»).
Запрос
Ответ
Lifecycle — идентичен secrets_request_provision. Используйте secrets_poll_status для отслеживания.
secrets_propose_metadata
Предложить пользователю правки метаданных существующего пути. Открывается edit-metadata диалог с diff preview: текущие значения слева читаются из манифеста (надёжный источник), proposed — справа из payload агента.
Запрос
Поля в fields — все опциональные. Опущенные поля не предлагаются к изменению; в diff они остаются прежними.
Ответ
Trust boundary против prompt injection
Это критическая часть протокола. UI рендерит только current колонку из манифеста — строки агента появляются исключительно в proposed. Это значит:
- Агент не может «переписать» метаданные через хитро составленные значения, которые выглядят как существующие.
- Пользователь видит точно, что предложено к изменению.
- Любая попытка «подменить» путь через description со специальными символами разбивается о то, что путь рендерится из
manifest, а не из payload.
Для деталей — секция «Trust boundary» в crates/devboy-mcp/src/secrets_provision.rs.
secrets_propose_new_path
Предложить регистрацию нового пути в манифесте проекта. UI открывает discovery-style диалог с suggested_path как редактируемой стартовой точкой.
Запрос
Ответ
В отличие от propose_metadata, путь редактируем — пользователь может отказаться от предложения агента и выбрать собственное имя. Финальное решение остаётся за человеком.
secrets_poll_status
Опрос статуса любого request_id от любого request_* или propose_* инструмента. Один endpoint — общая семантика лайфтайма.
Запрос
Ответ
Поля:
request_id— эхо запроса.path— путь, по которому изначально открылся диалог. Эхо для подтверждения.kind—provision/rotation/metadata-proposal/new-path-proposal.status.kind— один из:pending— диалог открыт, пользователь ещё не нажал Save или Cancel.ok— пользователь сохранил значение / принял proposal.cancelled— пользователь закрыл диалог без сохранения.expired— 5-минутный TTL истёк, registry пометил запись как Expired.failed { reason }— диалог не открылся (нет launcher'а, daemon down и т.п.) или provider отклонил submission.
age_seconds— сколько времени прошло с момента создания запроса.
Если request_id не существует (был sweep'нут), ответ — error unknown request_id: <id>.
Polling pattern
Не делайте polling чаще раза в 2 секунды — диалог не быстрее, чем человек печатает. Tight loop разогревает CPU и telemetry, но не приближает результат.
Полный сценарий: provision + retry
Обратите внимание: агент никогда не запрашивает значение токена. Он узнаёт только то, что save случился; токен попал из диалога в daemon, оттуда — в proxy alias, и get_issues подхватил его через тот же proxy без участия агента.
Чего на agent surface нет
Эти инструменты не существуют и не появятся:
- ❌
secrets_get/secret.get— выдача значения агенту. Замена: high-level provider tool с алиасом. - ❌
secrets_set/secret.put— прямая запись из payload агента. Замена:secrets_request_provision→ пользователь вводит руками. - ❌
secrets_export/ dump — массовая выгрузка. Не предусмотрено архитектурой.
Если new-tool кажется удобной упрощалкой — посмотрите ещё раз. Удобство «передать токен напрямую» противоречит инварианту, и framework не предложит обходного пути.
Допустимые operational tools (не агентский surface)
Для DevOps / setup-сценариев, где удобство выше, чем agent-safety, есть отдельные CLI-команды (доступные только локально, не через MCP):
devboy secrets validate— формат-чек манифеста и live-probe источников.devboy secrets rotate <path>— интерактивная ротация для разработчика (P13.1).devboy secrets ui— открыть TUI/GUI inventory для ручного просмотра/правок (P12.2).
Эти команды требуют присутствия человека за терминалом и не доступны через JSON-RPC. Агент может рекомендовать пользователю их запустить, но сам выполнить не может.
Версионирование протокола
PROTOCOL_VERSION = "1.0" (см. crates/devboy-storage/src/plugin_protocol.rs). Семантический breakdown:
- Major bump (2.0) — поля reply убраны/переименованы, поведение
kindenum не совместимо. Старые агенты получат ошибку при попытке использовать новый сервер. - Minor bump (1.x) — добавление новых полей (всегда optional), новых вариантов status (агент должен трактовать неизвестные kind как
failed), новых инструментов в семействоsecrets_*. - Patch (1.x.y) — bugfix без изменения wire-format.
Для агентского кода рекомендация: парсите ответы tolerant — игнорируйте неизвестные поля, неизвестные status.kind интерпретируйте как failed. Это даёт совместимость с будущими minor-версиями без обновления агента.
См. также
onboarding.md— настройка манифеста и роутера, без которойsecrets_listничего не покажет.local-vault.md— где значения физически живут после save из диалога.source-plugin-protocol.md— как добавить собственный источник, который видят агентские инструменты.- ADR-023 §3.7 — формальная спецификация trust boundary и инвариантов.
crates/devboy-mcp/src/secrets_tool.rs+secrets_provision.rs— исходный код реализации.