diff --git a/HISTORY/2026-08-28-obdai-receipt.md b/HISTORY/2026-08-28-obdai-receipt.md index 3ff644b..1179b83 100644 --- a/HISTORY/2026-08-28-obdai-receipt.md +++ b/HISTORY/2026-08-28-obdai-receipt.md @@ -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.