docs: record full receipt service audit
This commit is contained in:
@@ -518,6 +518,132 @@ S. по 1т 1/р в день
|
|||||||
30 днів.
|
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` в ответе отсутствует, что подтверждает отсутствие
|
`thoughtsTokenCount` в ответе отсутствует, что подтверждает отсутствие
|
||||||
отдельно тарифицируемых reasoning-токенов в этом запросе. Usage: prompt 1119,
|
отдельно тарифицируемых reasoning-токенов в этом запросе. Usage: prompt 1119,
|
||||||
image 1102, text 17, candidate output 34, total 1153.
|
image 1102, text 17, candidate output 34, total 1153.
|
||||||
|
|||||||
Reference in New Issue
Block a user