docs: record full receipt service audit
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user