docs: record full receipt service audit

This commit is contained in:
2026-08-31 20:42:02 +03:00
parent a8a65185ef
commit 3bc8e5a996
+126
View File
@@ -518,6 +518,132 @@ S. по 1т 1/р в день
30 днів.
```
## 2026-08-31: полный критический анализ текущей реализации
Проведён аудит текущего состояния проекта после реализации backend-фиксов,
развёртывания сервисов и добавления nginx rate limiting. Ниже отдельно
зафиксированы подтверждённые свойства, риски и гипотезы, которые нельзя
считать доказанными без дополнительной проверки.
### recipe_service/app.py
- Bearer-токен сравнивается через `hmac.compare_digest`; health endpoint не
требует авторизации.
- `ProxyFix` доверяет одному уровню `X-Forwarded-*`, что соответствует схеме
nginx -> Flask при условии отсутствия дополнительных прокси.
- Все завершения запроса проходят через единый `finalize()`, который записывает
статус, размер ответа и длительность на базе `time.monotonic()`.
- При ошибке upstream клиент получает стабильный контракт
`502 upstream_error`; деталь ответа Gemini записывается только в лог.
- При ошибке конфигурации `GEMINI_GENERATION_CONFIG` `RuntimeError` сейчас
возвращается клиенту через `str(exc)`. Это подтверждённый риск раскрытия
внутренней конфигурационной ошибки; безопаснее логировать деталь и отдавать
фиксированный ответ.
- Flask rate limit считает запросы от начала текущей UTC-минуты. Это fixed
window, а не скользящее окно: на границе минут возможен burst до 40 запросов
за короткий период. Это ограничение текущей реализации, а не устранённая
защита от burst.
- Для пустого `remote_addr` rate limit отключается. В рабочей схеме nginx
должен передавать адрес клиента, но это поведение требует явного контроля.
### metrics.py
- Инициализация базы выполняется при импорте приложения, а `record()` больше не
пытается создавать схему на каждом запросе.
- WAL и индекс по `(client_ip, started_at)` соответствуют запросу rate limit.
- `count_since()` фильтрует только четыре POST-маршрута recipe/receipt.
- Отдельная проверка конкуренции двух gunicorn workers и поведения SQLite при
одновременных записях в рабочей нагрузке ещё не выполнялась.
### gemini_proxy/app.py
- `api_key_override` удалён; proxy использует только `GEMINI_API_KEY` из
environment. Тест подтверждает отказ при наличии только override.
- Изображение не сохраняется на диск, размер ограничен 10 MB, MIME ограничен
JPEG/PNG/WEBP.
- Между recipe_service и gemini_proxy нет отдельной авторизации. Сейчас proxy
защищён только bind-адресом `127.0.0.1` и изоляцией хоста. Это приемлемо для
текущей схемы, но становится риском при появлении другого локального
процесса или изменении bind-конфигурации.
- Модель по умолчанию задана как `gemini-3.6-flash`. Фактические тесты проекта
проходили с этим именем и возвращали HTTP 200, поэтому утверждение о
несуществующей модели не подтверждено. Однако конфигурацию модели следует
проверять через официальный список доступных моделей и не полагаться на
значение default без такой проверки.
- `ReadWritePaths=/run` в systemd-юнитe не связан с известной необходимостью
proxy. Это избыточное разрешение, но влияние на текущий запуск не выявлено.
### nginx
- Rate limit объявлен в `http`-конфигурации как `20r/m` по
`$binary_remote_addr`, а в четырёх API location задан `burst=5 nodelay`.
- Nginx и Flask используют разные алгоритмы ограничения: nginx token bucket,
Flask fixed window. Поэтому их суммарная семантика не совпадает; nginx
ограничивает внешний поток, Flask дополнительно ограничивает успешно дошедшие
запросы.
- `limit_req_status 429` объявлен глобально в подключаемом `conf.d`-файле и
потенциально влияет на другие nginx location, если они используют
`limit_req`. Фактического конфликта на текущей ВМ не обнаружено.
- `client_max_body_size 10m` согласован с лимитом приложения. `nginx -t` и
фактическое срабатывание 429 на ВМ подтверждены.
### Android
- Токен `RECEIPT_API_TOKEN` передаётся через `BuildConfig` и поэтому встраивается
в APK. Любой пользователь APK потенциально может извлечь этот токен. Для
публичного распространения нужен отдельный пользовательский или
краткоживущий механизм авторизации.
- `ApiClient` превращает любой HTTP-ответ вне 2xx в исключение с кодом HTTP.
Сейчас 401, 429, 502 и 503 не имеют отдельных пользовательских состояний;
повторная отправка и сообщение для rate limit не реализованы.
- Prompt распознавания захардкожен на английском, хотя интерфейс приложения
русскоязычный. Это не гарантирует ошибку модели, но является отдельным
параметром качества, который следует сравнить на тестовой выборке.
- Privacy gate блокирует отправку, если OCR находит любую дату подходящего
формата или один из пациентских маркеров. Дата на упаковке или дата рецепта
может вызвать false positive; это ожидаемое консервативное поведение, но его
частоту нужно измерить.
- Текущая геометрия `scaleToView()` предполагает прямое соответствие размеров
bitmap и PreviewView. Crop/preview может иметь aspect-ratio или rotation,
поэтому проверка на реальном устройстве или эмуляторе остаётся необходимой.
- Эмулятор в текущем окружении не запускается из-за отсутствия KVM; unit-тесты
и сборка выполнялись, но end-to-end camera-проверка не завершена.
### systemd и эксплуатация
- `recipe.service` зависит от `gemini-tunnel.service`, который не представлен
среди файлов текущего репозитория. Это нужно считать внешней инфраструктурной
зависимостью и отдельно проверить на ВМ.
- Независимость `elmer.service` соблюдена: receipt-service размещён отдельно и
не требует изменения или перезапуска `elmer`.
- Сервисы используют loopback bind и отдельные virtualenv/systemd units.
- Секреты в код и HISTORY не записываются; реальные ключи находятся только в
EnvironmentFile на соответствующих ВМ.
### Приоритет дальнейших проверок
1. Проверить доступность и жизненный цикл модели `gemini-3.6-flash` через
официальный API; default оставить только при подтверждении.
2. Решить, нужен ли скользящий rate limit или достаточно текущего fixed window,
и согласовать его семантику с nginx.
3. Убрать возврат `str(RuntimeError)` наружу и добавить тест на безопасный 503.
4. Проверить Android crop и privacy gate на обезличенной выборке; отдельно
измерить false positive для дат упаковки.
5. Для публичного APK заменить общий встроенный токен на пользовательскую или
краткоживущую авторизацию.
6. Подтвердить наличие `gemini-tunnel.service` и воспроизводимость запуска
после перезагрузки ВМ.
### Итог аудита
Текущая реализация работоспособна для контролируемого закрытого MVP: тесты
backend проходят, сервисы изолированы, nginx rate limit работает, секреты не
попадают в репозиторий, а privacy gate действует до отправки crop. Основные
остаточные риски находятся в модели авторизации Android, fixed-window rate
limit, консервативном privacy gate, непроверенной геометрии camera pipeline и
внешней зависимости `gemini-tunnel.service`. Ни один из этих пунктов не следует
считать закрытым только на основании успешного HTTP health или unit-тестов.
`thoughtsTokenCount` в ответе отсутствует, что подтверждает отсутствие
отдельно тарифицируемых reasoning-токенов в этом запросе. Usage: prompt 1119,
image 1102, text 17, candidate output 34, total 1153.