# Хранилище секретов: устройство, права, границы

Решение владельца 2026-10-24: RAGRAF хранит значения секретов. До этого
действовал обратный инвариант («только реестр мест»), и он честно записан в
`docs/00_plan/PLAN-IAM-YOU-GLOBAL-MAP.md` §3.1 как история решения.

## Что защищается

| Угроза | Как закрыто |
|---|---|
| копия `catalog/` или бэкап диска | значения только шифротекстом (PyNaCl, XSalsa20-Poly1305) |
| случайная выдача индекса, поиска, логов | `catalog/**` исключён из корпуса; `redact()` для текста логов |
| пользователь без прав администратора | файл ключа с суженными правами, ключ вне базы и вне git |
| перестановка шифротекста между записями | ключ записи выводится с солью `id`, в теле вложен контекст `id+name+version` |
| утечка списка имён через отказы | отказ неотличим от «не найдено», причина только в аудите |
| подмена журнала | триггеры `audit_no_update` / `audit_no_delete` |
| выдача «забытого» секрета | срок `expires_at`, флаги `expired` / `expiring_soon` в списке |

## Чего хранилище НЕ защищает

- процесс, работающий под той же учётной записью: он вправе прочитать ключ;
- агент, который получил значение: дальнейшая судьба значения — его
  ответственность, поэтому значение печатается только по `reveal`;
- администратора, который вправе расшифровать;
- утечку через содержимое ответа агента в чат (правило: значение не печатать
  без необходимости, подтверждать именем и маской).

## Права

Три режима записи: `all` (по умолчанию — доступна всем агентам, как решил
владелец), `listed` (только перечисленным именам), `owner_only` (только
сохранившему). Общий список `secrets_allowed_agents` сильнее режима записи и
отсекает агента целиком.

Права меняет автор записи или локальный администратор. Локальная команда
`ragraf secrets` работает от имени владельца: она и так владеет ключом, и
изображать ограничение там, где его нет, было бы ложью.

## Файлы

| Файл | Назначение |
|---|---|
| `catalog/secrets.sqlite` | записи (шифротекст + метаданные) и журнал аудита |
| `catalog/secrets.key` | мастер-ключ, base64, 32 байта; права сужены |
| `catalog/backup-secrets-*/` | копия ключа перед ротацией |
| снимок `*.json` (export) | шифрованный scrypt-ключом от парольной фразы |

## Команды и инструменты

CLI: `secrets init | set | list | show | reveal | rm | grant | revoke | check |
rotate | export | import | policy | audit`. MCP-инструменты: `ragraf_secret_list`,
`ragraf_secret_show`, `ragraf_secret_reveal`, `ragraf_secret_set`,
`ragraf_secret_grant`, `ragraf_secret_revoke`, `ragraf_secret_delete`,
`ragraf_secret_policy`.

## Настройки

`secrets_enabled`, `secrets_capture_enabled`, `secrets_default_visibility`,
`secrets_reveal_requires_reason`, `secrets_max_value_bytes`,
`secrets_allowed_agents`, `secrets_expiry_warn_days` — все семь меняют
поведение; переключателей «на будущее» нет.

## Брокер секретов (издание pro)

Хранилище отвечает на вопрос «кому разрешена запись» и хранит значение под
шифром. Право «навсегда» и право «на пять минут ради этой задачи» — разные права,
и журнал хранилища говорит «кто взял», но не «на сколько и по чьему разрешению».
Вторую половину закрывает брокер: `src/ragraf/broker.py`, операции `broker_issue`
и `broker_rotate`.

| что появляется | чем ограничено |
|---|---|
| аренда вместо права | срок обязателен, предел — `broker_max_ttl_minutes`; бессрочной выдачи нет |
| распорядитель | выдать запись ДРУГОМУ агенту может автор записи или администратор, иначе отказ `not_owner` |
| причина | обязательна при `broker_require_reason`; попадает в журнал доступа хранилища той же цепочкой хэшей |
| одноразовое погашение | второй раз — `lease_used`; после смены значения — `lease_stale`; после срока — `lease_expired` |
| план смены | `broker_rotate mode=status` показывает просроченное, `plan` задаёт срок, `run` меняет значение и отзывает все аренды записи |

Состояние аренд и планов лежит отдельным файлом `catalog/broker.json`: **схема
`secrets.sqlite` не меняется**. Файл пишется атомарно (временный файл и
`os.replace`) и под замком; брошенный замок снимается по возрасту, занятый даёт
отказ `store_busy`. Битый файл состояния — отказ `store_broken`, а не «начать с
чистого листа»: пустой список аренд выдал бы права заново, и это было бы
расширением доступа, а не восстановлением.

Настройки брокера: `broker_enabled`, `broker_require_reason`,
`broker_default_ttl_minutes`, `broker_max_ttl_minutes`,
`broker_rotate_default_days`, `broker_rotate_value_bytes`. Выключение брокера
не запирает владельца: `secret_reveal` продолжает работать по правам записи.

Граница, названная честно: имя агента клиент сообщает о себе сам (как и во всём
хранилище), поэтому адресное право держится на устойчивости имени, а не на
криптографической идентичности. Проверка поведения — `python tests/test_broker.py`
(44 свойства).

## Что делать при компрометации ключа

1. Считать все значения раскрытыми и сменить их у источников.
2. `secrets rotate` — перешифровать базу новым ключом (старый ключ
   сохраняется только в `catalog/backup-secrets-*` для сверки).
3. Проверить журнал: `secrets audit` покажет, кто и когда получал значения.

## Проверки

`python tests/test_vault.py` — 60 свойств: крипто, права агентов, отказы без
раскрытия, append-only журнал, лимиты, срок, ротация, снимок, целостность,
замазывание текста, наличие настроек.

`python tests/test_broker.py` — 44 свойства брокера: срок и его предел,
распорядитель, одноразовость, просроченная и отозванная аренда, версия в аренде,
план смены, битое состояние, замок, выключенный брокер.
