# Принуждение на исходящем запросе в Hermes

Как из правила в промпте сделать механизм: маскировать то, что уходит в модель, и
писать строку в журнал. Ниже — проверенный контракт и решения, которые принимает
владелец, а не агент.

## Точки перехвата

Плагин регистрирует middleware из `register(ctx)`; вызовы применяются в рантайме,
а не в промпте:

| Точка | Что получает | Что может | Где применяется |
|---|---|---|---|
| `tool_request` | `tool_name`, `args`, `original_args` | заменить аргументы инструмента или отказать до выполнения | до проверок доступности, guardrails и approval |
| `llm_request` | `request` (полные kwargs провайдера, включая `messages`/`input`), `original_request` | заменить запрос целиком; исключение прерывает вызов | `agent/turn_api_request.py` → `apply_llm_request_middleware` |
| `llm_execution` | `request`, `next_call` | обернуть или заменить сам вызов провайдера | вокруг исполнения |

Порядок на каждый запрос: собрать kwargs → `llm_request` → наблюдатели
`pre_api_request` → `llm_execution` → `post_api_request`. Регистрация:
`ctx.register_middleware("llm_request", fn)` в `hermes_cli/plugins.py`.

Практический вывод: `hooks/` — это наблюдатели, они ничего не меняют. Пустой `hooks/`
не означает «принуждения быть не может»; смотреть надо на зарегистрированные
middleware (включённые плагины в `config.yaml`).

## Три точки = три задачи

| Точка | Задача |
|---|---|
| `tool_request` на `read_file`, DOCX/PDF-парсер, OCR | определить **тип документа** (имя файла, титул, штамп), сверить с ролью пользователя, поставить метку категории в сессии; при запрете — отказ **до** чтения, содержимое в модель не попадает |
| `llm_request` | замаскировать по правилам категории из метки и записать строку в журнал |
| `llm_execution` | вернуть реальные значения в ответе — иначе модель отвечает про `[РОЛ_01]` и ответ бесполезен |

## Категория — по типу документа и роли, не по шаблону

Шаблоны ПДн дают ложные срабатывания на техническом тексте: номера СП и ГОСТ, ИНН
поставщика в спецификации, кадастровые в выписке. Поэтому решение принимается не по
содержимому, а по типу документа (он известен до чтения) + роли пользователя.
Шаблоны остаются подсказкой для автоклассификации; при сомнении тип подтверждает
человек одним вопросом, ответ живёт в метке сессии — иначе одно и то же спрашивают
десять раз за день и отвечают не глядя.

Политика — отдельный файл в профиле, правится без пересборки:

| Категория | Кому | Что входит | Режим |
|---|---|---|---|
| 🟢 зелёный | всем | открытые нормативы, методики, обезличенные тексты | без маски |
| 🟡 жёлтый | всем | ТЗ, реестры замечаний, переписка, локальные сметы, ВОР | маска + журнал |
| 🔴 красный | только выделенная роль (отдельная учётка) | договоры, сводные сметы, акты, ИД, кадры, финансы | остальным — отказ с подсказкой, к кому идти |

Два ограничения, которые нельзя размывать:

1. **Роль не даёт права отправить красное в облако.** Красное в облако не идёт никому;
   роль разрешает открыть документ агенту (например, чтобы снять структуру и сделать
   обезличенную выписку), но не отправку самого документа. Иначе светофор противоречит
   сам себе, и первый же вопрос «а почему им можно?» его ломает.
2. **Роль вешается на учётку, а не на флаг в текущем профиле.** Переключатель в том же
   профиле сотрудник поставит себе сам, и контроль фиктивный.

Маскировать красное и отправлять нельзя: у договора маска убивает предмет — стороны,
суммы, особые условия. Единственный корректный ответ для красного — не пускать.

## Откуда берутся роли

Роли берутся из таблицы учёток организации (той же, где логины и лимиты), а `policy.yaml`
генерируется из неё скриптом. Руками политику не править: два источника правды
разъедутся на первой же кадровой замене, и узнается об этом на инциденте.

- **Роль — машинный код** (`engineer` / `contracts` / `admin`), а не перечисление задач.
  «инженер ОВиК, ТЗ, ТУ, СТУ, проекты» не парсится и у каждого записано по-своему;
  человекочитаемое описание держать отдельной колонкой рядом с кодом.
- **`root` политику не обходит.** Учётка со статусом root проходит те же проверки;
  отличие — только пометка `admin` в журнале. Иначе достаточно попросить root, и
  контроль отменён.
- После записи перечитывать таблицу и сверять число строк и раскладку по ролям, а не
  только одну изменённую ячейку: пропущенная учётка = сотрудник без роли, и он либо
  блокирован, либо проходит как зелёный.

## Порядок внедрения

1. `tool_request` + политика + журнал в режиме «только запись»: ничего не маскируем,
   смотрим реальный поток и настраиваем правила.
2. Включаем маску на жёлтом.
3. Включаем демаскировку ответа (`llm_execution`).

Обратный порядок (сразу маска без периода наблюдения) даёт ложные маски на нормальном
техническом тексте, и работу уносят на личный телефон — ровно то, от чего защищались.

## Приёмка

- Сотрудник без роли просит проверить договор → отказ до чтения, файл не открывается.
- ТЗ у любого сотрудника → уходит маской, в журнале строка, ответ с реальными значениями.
- 20 запросов без ПДн → ни одной маски и ни одной блокировки.
- В журнале: пользователь, тип документа, категория, сколько замен, модель, время.

## Чего перехват не закрывает

- Картинки (`vision_analyze`) и вложения не маскируются.
- PDF-сканы без текстового слоя требуют OCR до маски.
- Путь «человек скопировал в браузер мимо агента» закрывается только шлюзом или DLP
  на периметре. Плагин закрывает свой участок, а не периметр целиком.
