Files
transcriber/openspec/changes/archive/2026-08-22-trusted-header-login/review/report.md
T
av 7f33c957e5 вход переехал на доверенный заголовок Authelia вместо собственного OIDC
- пришедшего называет заголовок Remote-User от прокси, и верят ему только с
  адреса из перечня trusted_proxies; своего входа у сервиса не осталось — ни
  корня /auth, ни кук, ни срока сессии, ни секрета клиента в конфиге и в базе
- учётная запись заводится первым обращением: EnsureUser в пакете хранилища,
  шаг схемы 202608220001 с колонкой provider_login и снятыми правилами users
- cmd/oidcstub заменён на cmd/devtools с подкомандой proxy; заодно закрыт
  унаследованный DL3066 — пользователь образа назван числом
2026-08-22 20:24:22 +03:00

46 KiB
Raw Blame History

Ревью изменения trusted-header-login

Сводка

  • Режим прогона: по графу. Метка: large. Размер и сложность в задании триажу не переданы — обоснование разметки воспроизвести нечем, метка взята как объявленная.
  • База диффа: origin/master. Изменение читалось вместе с непрослеженными файлами (cmd/devtools/, internal/adapter/repo/pocketbase/identity.go, migrations/202608220001_trusted_header_login.go, internal/controller/http/identity.go, дельта-спеки change).
  • Состояние гейта: зелёный целиком, кроме шага dockerfilehadolint даёт DL3066 на Dockerfile:94. Отказ унаследован (воспроизведён на чистом origin/master), долг объявлен строкой в CLAUDE.md, раздел «Гейт». Новым красным шагом это изменение гейт не красит.
  • На входе: 31 пронумерованная находка проходов + 4 пункта «дешевле переделать до мерджа» от architecture + 1 замер без дефекта от ops. На выходе: 3 + 4 = 7 в основных секциях, остальное — ниже, ничего не выброшено молча.

План с исходом по каждой теме

тема дом глубина кто закрывает исход
requirements дельта-спеки change + openspec/specs/ разбор specs закрыта, 8 находок (5–12); в отчёт ушли 2, ещё 4 — ниже
autotests CLAUDE.md, «Гейт» autotests закрыта, 4 находки (1–4); в отчёт не ушла ни одна самостоятельно, одна закрывается правкой №3
conventions docs/conventions/ разбор code закрыта, 8 находок (15–22); в отчёт ушли 2
architecture docs/architecture.md + docs/passport.md доказательство architecture закрыта, 2 находки + 4 пункта «до мерджа»; в отчёт ушла 1
security docs/security.md доказательство adversary закрыта, 6 находок (23–28); в отчёт ушли 3
operations docs/architecture.md «Эксплуатация» + docs/database.md доказательство ops закрыта, 3 находки + 1 замер без дефекта; в отчёт не ушла ни одна, старшая — в гипотезах
  • Тем без отчёта нет. Тем без дома в плане нет.
  • basics не запускался — своих тем проекта нет, все шесть разобраны именными проходами.
  • Сигнал о заниженной метке: review-code отработал и возражений по метке не подал; review-basics не запускался, поднять метку было некому. Молчание здесь — молчание одного корректора из двух, а не двух из двух.

Блокирует мердж

Запрос, отвергнутый ограничителем частоты, всё равно заводит учётную запись

  • Файл: internal/controller/http/identity.go:91, internal/controller/http/errors.go:219
  • Severity: major (critical не ставлю: инвариант CLAUDE.md этим не нарушен, а путь ведёт к мусору в коллекции, а не к порче чужих данных)
  • Confidence: high
  • Оракул: свой падающий тест, прогнан на этом прогоне. Слой узнавания стоит на DefaultLoadAuthTokenMiddlewarePriority + 1 = 1019, ограничитель библиотеки — на 1000 (apis/middlewares_rate_limit.go:16), то есть узнавание отрабатывает до ограничителя. Прогон: 120 запросов выбирают бюджет, следующие 50 запросов новыми логинами получают 429все 50, — а учётных записей в коллекции становится 2 → 52.
  • Последствие: ограничитель частоты не защищает ничего из того, что делает слой узнавания. Каждый отвергнутый запрос — это как минимум чтение из базы (поиск по логину идёт на каждом запросе по норме access), а с новым именем — ещё и запись. Учётная запись, заведённая мусором, из сервиса не убирается: спека access называет цену прямо — «слить их или убрать нечем». Барьер «заголовок ставит прокси» этот путь сужает, но не закрывает: docs/review.md, «Недоступно проверке», записывает поведение прокси как непроверяемое отсюда, и строить на нём защиту базы значит держать один барьер вместо двух.
  • Предложение: перенести слой узнавания на apis.DefaultRateLimitMiddlewarePriority + 1 (999), а RequireUser — на + 2 (−998). Порядок «токен (−1020) → ограничитель (−1000) → узнавание (−999) → требование записи (−998) → предел тела (−990)» сохраняется целиком.
  • Найдено проходом: adversary (25); подтверждено собственным прогоном триажа.
  • Действие: инлайн

Ответ на первый вопрос задания — перестановка проверена запуском. С приоритетами −999/−998 прогнан весь go test ./...: единственным красным остался мой временный тест на находку про общий бюджет (ниже, №4), все существующие проверки пакета internal/controller/http — включая оба критерия приёмки, победу предъявленного токена над заголовком и «проба здоровья учётной записи не заводит» — прошли. Заведение записи после 429 при этом пропало: 2 → 2. Предел тела на −990 действительно остаётся после требования учётной записи, «отказ неузнанному до чтения тела» сохраняется.

Одно последствие перестановки задание не называет, и оно настоящее. Сейчас RequireUser (−1018) отказывает неузнанному до ограничителя, и отказы бюджета не тратят. После переноса — тратят: замер на этом прогоне, 125 запросов без заголовка, и следующий запрос узнанного человека получает 429 (до перестановки — 200). Само по себе это правильнее (счёт отказов и есть работа ограничителя), но вместе с общим бюджетом (№4) оно даёт анониму, дотянувшемуся до контейнера, выключение сервиса для всех. Поэтому №1 и №4 едут одной правкой, а не порознь.

Ключ учётной записи правится рукой в панели, и архив уезжает следующему с этим именем

  • Файл: internal/adapter/repo/pocketbase/migrations/202608220001_trusted_header_login.go:77-82, internal/adapter/repo/pocketbase/panel.go:33-34

  • Severity: major

  • Confidence: high

  • Оракул: дословное требование дельта-спеки openspec/changes/trusted-header-login/specs/access/spec.md: «Ключ учётной записи MUST не правиться ничем, кроме заведения самим сервисом. Ни запросом снаружи, ни рукой в панели». Плюс grep -rn "OnRecordUpdateRequest" --include=*.go internal/ cmd/ → единственная привязка стоит на migrations.RecordsCollection, на users хука нет. Шаг схемы закрывает правила API (UpdateRule = nil), а панель работает суперпользователем — это записано комментарием самого шага, строки 48–50.

  • Последствие: переписанный в панели provider_login отдаёт весь архив прежнего владельца тому, кто придёт с этим именем следующим. Владелец записи назначается один раз и не меняется — вернуть архив нечем. Правка своя, сознательная, но необратимая и молчаливая: журнала событий у коллекции пользователей нет.

  • Предложение: развилка ниже.

  • Найдено проходом: specs (6)

  • Действие: развилка

    Вопрос владельцу. Требование спеки «ключ не правится рукой в панели» сегодня не держит ничто: правила API закрыты, но панель ходит суперпользователем. Три варианта: (А) завести хук OnRecordUpdateRequest(users), отвергающий смену непустого provider_login — в проекте уже есть тот же приём для записей (pbrepo.BindPanelRules), цена — десяток строк и проверка; (Б) переписать требование, назвав панель доверенной и записав цену («ключ правится владельцем сервиса, откатить правку нечем»); (В) оставить как есть — тогда спека и код расходятся молча, и это расхождение переживёт мердж.

Заведение учётной записи не оставляет в журнале ни строки, а поломка контура пишется на уровне, которого в бою нет

  • Файл: internal/controller/http/identity.go:101-108,128-151, internal/adapter/repo/pocketbase/identity.go:52-87
  • Severity: major
  • Confidence: high
  • Оракул: свой прогон. Запрос новым логином с доверенного адреса → ответ 200, учётная запись заведена, перехваченный журнал — пустая строка (""). Дословное требование дельта-спеки access: «Исход узнавания MUST оставлять строку журнала — и когда заголовок пришёл с недоверенного адреса, и когда учётная запись заведена. […] Строка несёт адрес пира и идентификатор учётной записи». Второй симптом: ветвь «более одного значения Remote-User» (строки 103–106) пишет Debug, боевой уровень — Info (docs/conventions/logging.md, «Расхождение: уровень зашит константой»).
  • Последствие: сервис заводит учётные записи молча — владелец не отличит «никто не заходил» от «завелось двадцать записей», а по спорной учётной записи не скажет, когда и с какого адреса она появилась. Хуже второе: docs/security.md называет прокси, добавляющий заголовок вместо замены, главным барьером контура, и половину этой беды сервис закрывает сам — но закрывает невидимо: строка о двух значениях в боевом журнале не появляется вовсе. Поломка контура, которую сервис поймал, владельцу неотличима от тишины.
  • Предложение: logger.Info на исходе «учётная запись заведена» — с адресом пира и идентификатором записи, без значения заголовка (инвариант «Содержимое записи остаётся приватным» и требование спеки «MUST не нести значения заголовка»); уровень ветви «два значения» поднять до Warn — ровно как у ветви недоверенного пира, по той же причине и с той же ценой.
  • Найдено проходами: specs (5) и adversary (26) — две находки об одной причине, слиты; оракула у согласия проходов нет, оракул свой.
  • Действие: инлайн

Стоит исправить сейчас

Бюджет ограничителя частоты общий на весь сервис: один человек выключает сервис остальным

  • Файл: internal/controller/http/rate_limit.go:20-23, internal/controller/http/app.go:43-63, internal/config/config.go:91-96, cmd/transcriber/main.go:238-246

  • Severity: major

  • Confidence: high

  • Оракул: свой падающий тест. 125 запросов первого человека выбирают бюджет, первый запрос второго человека получает 429. Причина — в исходниках библиотеки: checkRateLimit берёт ключом e.RealIP() (apis/middlewares_rate_limit.go), а RealIP() при пустом Settings.TrustedProxy.Headers откатывается к адресу пира (core/event_request.go:40-75). Пир теперь всегда обратный прокси.

  • Последствие: объявленная сервисом частота опроса выводится из доли бюджета (pollBudgetShare = 8, PollIntervalMs = 4000) в расчёте на то, что бюджет делят немногие. Делят его все: восемь одновременных опросов выбирают его целиком, и девятый человек получает 429 на пустом месте. Отказ шумный, но причина невидима — по журналу он неотличим от собственной активности.

  • Дефект старше этой задачи (правило заведено spa-skeleton 2026-08-15, прокси перед сервисом стоял и тогда). Уточнение к формулировке прохода: docs/database.md:344 и app.go:43-49 не утверждают обратного — там прямо написано «бюджет считается по адресу спрашивающего, а не по учётной записи», и даже назван случай «двое за одним домашним адресом делят его пополам». Сломалось не правило, а основание правила: «адрес спрашивающего» выродился в один адрес на всех, и число 120/60 выбиралось не под это.

  • Найдено проходами: architecture (13) и adversary (23); подтверждено собственным прогоном.

  • Действие: развилка

    Ответ на второй вопрос задания: чинить здесь. Не потому, что дефект этой задачи — он не её, — а потому, что правка №1 делает его достижимым для анонима: после переноса RequireUser за ограничитель отказы неузнанному начинают тратить общий бюджет (замерено на этом прогоне: 125 анонимных запросов → узнанный человек получает 429). Мерджить №1 без ответа на №4 значит завести новый путь к отказу сервиса.

    Вопрос владельцу. Чем считать бюджет ограничителя, когда весь трафик приходит с одного адреса: (А) заполнять Settings.TrustedProxy.Headers при подъёме (там же, где ApplyAppRateLimit) — тогда ключом станет адрес человека из X-Forwarded-For. Доверие к этому заголовку той же природы, что доверие к Remote-User, и опирается на тот же перечень адресов; цена — ещё одно требование к контуру, которое отсюда не проверить; (Б) признать бюджет общим на сервис: поднять числа под ожидаемое число людей, переписать docs/database.md и обоснование pollBudgetShare; (В) отложить в урожай и мерджить №1 с известным ухудшением — тогда анонимный поток через прокси выключает приложение всем.

Негодное значение Remote-Name запирает человека в сервисе навсегда пятисоткой

  • Файл: internal/adapter/repo/pocketbase/identity.go:71-87,147-159, internal/controller/http/identity.go:133-147
  • Severity: major
  • Confidence: high
  • Оракул: свой прогон. Запрос с доверенного адреса, Remote-User: namebearer, Remote-Name из 5000 знаков → ответ 500 {"error_code":"internal","message":"Внутренняя ошибка сервиса"}, в журнале level=ERROR msg="Failed to resolve account by login header" error="failed to create user account: name: Must be no more than 255 character(s)..". Значение заголовка в ответ и в журнал при этом не уехало — инвариант приватности цел.
  • Последствие: приёму подвергается только логин (AcceptProviderLogin), а имя и почта уезжают в колонку как есть. Человек, чьё имя у провайдера длиннее 255 знаков, получает 500 на каждом запросе и в сервис не попадёт никогда; владелец получает ERROR на каждый такой запрос. Смежный симптом той же причины: разбор отказа судит по наличию колонки в validation.Errors, а не по причине отказа — негодный адрес почты неотличим от занятого, и запись молча заводится без почты вместо честного разбора.
  • Предложение: привести имя и почту к годным значениям в одном месте с логином — имя усечь до предела колонки, негодную почту не ставить вовсе. Этой же правкой закрывается непокрытая ветвь «отказ хранилища → отказ сервиса» (identity.go:144-146), которую autotests называет единственным намеренным исключением из «слой отказа не выдаёт»: у неё появляется достижимый и проверяемый вход.
  • Найдено проходами: specs (7, 10), code (15, 16), adversary (24) — пять формулировок, две причины, одно место правки; слиты. Согласие пяти проходов Confidence не повышает — оракул свой.
  • Действие: инлайн

Образец конфига велит верить всей подсети docker: любой контейнер на хосте входит под любым именем

  • Файл: config.example.toml:69-77

  • Severity: major

  • Confidence: medium (путь не построен: второго контейнера в прогоне нет)

  • Оракул: положение docs/security.md:109 дословно — источник заголовков есть «Обратный прокси — и всякий, кто дотянулся до сервиса с доверенного адреса». Образец задаёт этим адресом 172.16.0.0/12, то есть весь умолчательный диапазон docker: под него попадает любой контейнер на хосте, а не только Caddy.

  • Последствие: барьер узнавания держится только на адресе пира (кук и токенов сервис не выдаёт). Любой сосед по хосту — а хост держит пет-проекты, а не один сервис — ставит Remote-User: <чужой логин> и получает чужой архив целиком, молча. Класс риска в security.md назван, ширина перечня — нет.

  • Предложение: развилка ниже.

  • Найдено проходом: adversary (27)

  • Действие: развилка

    Вопрос владельцу. Чем сузить перечень доверенных адресов до самого прокси: (А) выделенная сеть compose с фиксированной подсетью под пару «прокси ↔ сервис» — правка живёт в pet-project-server, здесь остаётся значение образца и строка требования к контуру; (Б) статический адрес прокси в сети docker — то же, но проще и хрупче; (В) принять /12 как цену и записать её строкой в docs/security.md рядом с уже названным классом — тогда это осознанная цена, а не умолчание образца.

Три конвенции проекта описывают снятый механизм, и одна противоречит себе внутри файла

  • Файл: docs/conventions/web-ui.md:107-109, docs/conventions/config.md:60-64 против :99, docs/conventions/logging.md:34-37
  • Severity: minor
  • Confidence: high
  • Оракул: чтение файлов на этом прогоне. web-ui.md: «Сессия живёт кукой transcriber_session» — кук сервис больше не ставит вовсе. config.md строка 60–64: «Секретов в этой секции больше нет», строка 99 в том же файле по-прежнему перечисляет auth.client_secret среди секретов. logging.md строки 34–37 описывают расхождение у пакета cmd/oidcstub, который этим же изменением удалён (git statusD cmd/oidcstub/main.go).
  • Последствие: конвенции — канон проекта, по ним работает следующая задача. Запись про куку заставит следующего агента искать куку и объяснять её отсутствие; запись про client_secret заставит охранять секрет, которого нет. Машина этого не ловит: согласованность документов между собой и с кодом судят агенты, и звать их надо руками (CLAUDE.md, «Гейт»).
  • Предложение: снять три записи, поправить перечень секретов в config.md.
  • Найдено проходом: code (20)
  • Действие: инлайн

Гипотезы без доказательства

Понижено и не влезло в потолок; каждая с причиной понижения.

  • Откат бинаря поверх шага схемы 202608220001 обрывает вход (ops, 29; было major). Оракул у прохода настоящий — падающий тест на восстановленном старом коде во временном worktree, 401 вместо 302. Понижаю до minor по стадии и по перекрытию: (1) стадия — стройка, сервис на сервере остановлен, данных нет, откатываться не с чего; (2) docs/architecture.md:142 уже объявляет откат неработающим начиная с шага 202608140002 — то есть любой бинарь старше 2026-08-14 и так не поднимается; новое окно отката — только версии между 14 и 22 августа, ни одна из которых не выкладывалась; (3) старый бинарь на новом конфиге и вовсе не стартует, если владелец убрал ключи OIDC по процедуре из «Эксплуатации» — тогда отказ громкий, а не молчаливый. Ответ на третий вопрос задания: да, стадия меняет серьёзность. Правка тем не менее дешёвая и уместная — строка в docs/architecture.md, «Эксплуатация», рядом с уже стоящей про 202608140002: не потому что откат сегодня возможен, а потому что порог перехода там принято называть прямо.
  • Уникальный индекс по provider_login не частичный (code, 17). Оракул прохода — исходники библиотеки (core/collection_model.go:648-671, индекс собирается без WHERE, сосед по коллекции — WHERE email != ''). Понижаю: сервис пустого логина в колонку не кладёт никогда (AcceptProviderLogin отвергает), коллекция пользователей на выкладке пуста, и последствие сводится к «в панели рукой не завести вторую запись без логина». Промах будущего, не дефект сегодня.
  • Предел логина считается в байтах, колонка — в знаках (specs, 8; code, 18). Проверено чтением: len(login) > 255 в identity.go:174 против Max: 255 у текстовой колонки. Понижаю: перекос только в сторону строгости, кириллический логин длиннее 127 знаков у Authelia не встречается, следа Debug хватает. Пути нет — гипотеза.
  • Склейка значений заголовка запятой обходит защиту «два значения» (adversary, 28). Пути нет и построить его отсюда нечем: net/http заголовки запятой не сливает, а прокси, который сливает, — ровно тот класс, который docs/review.md относит к «не проверит ни один проход». Последствие при этом было бы неприятным и тихим (человек попадает в новую пустую учётную запись вместо своей), поэтому строка оставлена, а не выброшена.
  • Откат шага схемы не восстанавливает OAuth2.Enabled и минирует записи без почты (specs, 9; ops, 30). Оракул у ops есть — прогон на временной базе, email: cannot be blank. Понижаю по тому же основанию, что и №29: down на сервере не зовётся, а применённый шаг не переписывается по инварианту.
  • Отказ хранилища при узнавании не покрыт ничем (autotests, 2). Не выведена отдельной строкой: правка про негодное имя даёт этой ветви достижимый вход, и тест на неё пишется той же правкой. Если владелец выберет не чинить негодное имя — эта строка возвращается самостоятельной находкой.
  • Непокрытые ветви retryAfterConflict и web/src/api.ts (autotests, 1, 3, 12). Понижаю по записанному решению проекта: CLAUDE.md, «Гейт» — «покрытие изменённых строк не считается ничем». Своего api.test.ts у нового web/src/api.ts действительно нет (проверено ls web/src/), и это стоит завести — но это задача, а не блокирующая находка.

Promote candidates

Претензии на правило, а не на этот код.

  • Поле peer заведено мимо словаря имён журнала (code, 21). docs/conventions/logging.md:99 требует точечной иерархии для системных доменов (http.*, ext.*, webapp.*), а в файле уже стоит Расхождение с перечнем самозаведённых имён. Кандидат в конвенцию: внести http.peer_addr в таблицу полей — тогда следующий агент возьмёт имя из словаря, а не сочинит третье.
  • Третий способ вывода в репозитории (code, 22). cmd/devtools печатает через stdlib log, тогда как logging.md знает два. Записанной конвенции об оснастке у проекта нет — значит это кандидат в правило, а не нарушение.
  • Единственный дом имён заголовков Remote-* (architecture, 14). cmd/devtools/main.go:92-97 пишет "Remote-User", "Remote-Name", "Remote-Email" своими литералами, тогда как нормативные константы лежат в internal/controller/http/identity.go:23-27. Правка на две строки, но правило общее — «нормативное имя объявляется один раз» — и его стоит записать, а не чинить точечно. Смежный вопрос docs/review.md, «Вопросы по темам»: не завёл ли инструмент разработчика второй дом тому, что уже есть в проверках.
  • Второй словарь текстов на клиенте (code, 19). Половина находки — ложноположительная: docs/conventions/web-ui.md:118 сам разрешает клиентский текст «связи нет» состоянием, а не ошибкой. Настоящий остаток — расхождение текстов: сервер на 401 отвечает «Требуется вход» про вход, которого у сервиса больше нет, а экран показывает свой текст поверх. Кандидат в правило: конвенция сегодня не различает «текст под код ответа» и «текст под состояние клиента», и на этом различии находка и разошлась.

Урожай

Настоящее, но не для этого мерджа.

  • Бюджет ограничителя частоты по человеку, а не по адресу — если владелец выберет вариант (В) на развилке №4.
  • Секция конфига называется [auth] при отсутствии всякого входа (architecture). Имя ключа конфига — из перечня необратимого в CLAUDE.md («спрашивается у человека всегда»), но сегодня стадия стройки: на сервере ничего нет, и переименование стоит ноль. После первой выкладки — уже решение человека. Если переименовывать, то сейчас.
  • Колонка почты без единого читателя (architecture). Почта пишется при заведении и не читается ничем. Не дефект: спека объявляет её необязательной и берёт «при заведении». Но колонка без читателя — долг, который стоит либо оправдать спекой, либо снять.
  • Корень /auth перестал быть зарезервированным (architecture). Проверено: упоминаний /auth в app.go больше нет. Освободившееся адресное пространство стоит либо занять, либо назвать свободным в спеке webapp.
  • Свой тест у web/src/api.ts — файл новый, разбор отказов в нём есть, проверки нет.
  • Строка про порог отката в docs/architecture.md, «Эксплуатация» — см. гипотезу №29.

Границы покрытия

План: темы, дома, глубины

Воспроизведён таблицей в сводке выше целиком. Тем без дома в плане нет, тем без отчёта нет.

Что запускалось

  • Режим: по графу, метка large. Запущены specs, autotests, code, architecture, adversary, ops.
  • basics не запускался — своих тем проекта у него на этом прогоне нет, все шесть тем разобраны именными проходами. Следствие: сигнал о заниженной метке мог подать только review-code, и он его не подал.

Чего не хватило триажу, и это находки о прогоне

  • Блоков «Coverage of this pass» в сырых выводах нет ни у одного прохода. Контракт находок требует их у каждого, и сводить мне было нечего: строку «что этот проход не мог проверить в принципе» я по каждому проходу воспроизвести не могу. Ниже — только то, что выводится из плана и из документов проекта.
  • Сработавшие потолки ни один проход не назвал. Сколько находок проход показал, каков был его потолок и что осталось за срезом — не сообщено никем. Значит утверждать «проход показал всё, что нашёл» нельзя ни про один из шести.
  • Размер и сложность изменения триажу не переданы — только метка. Обоснование разметки в отчёте воспроизвести нечем.

Что осталось целиком на человеке

Из docs/review.md, «Недоступно проверке». Два списка, и они не сливаются.

Не проверит ни один проход:

  • operations: поведение внешних сервисов под нагрузкой и на границах — SpeechKit и Object Storage поднять в тесте нечем;
  • operations: реальный профиль нагрузки; утверждения о росте остаются условиями, а не замерами;
  • security: стойкость ffmpeg к вредоносному входу;
  • security: поведение настоящей Authelia и правило обратного прокси на домен сервиса. С 2026-08-22 от прокси зависит весь барьер: он обязан заголовки Remote-* перезаписывать, а не пропускать пришедшие. Правило живёт в pet-project-server, и отсюда его не проверить. Прямо на этом висят находки №1 (насколько узок путь к заведению мусорных записей), №6 (ширина перечня доверенных адресов) и гипотеза про склейку запятой;
  • security: поведение браузера с куками — своих кук сервис больше не ставит, класс сузился до кук панели хранилища.

Перестали проверять сознательно:

  • autotests: разбор вывода настоящего ffprobe — решение и цена в ADR-2026-08-11-stub-adapters-in-tests.md;
  • работа сервиса с настоящими внешними собеседниками в остатке: за настоящие SpeechKit и Object Storage живой прогон не отвечает — ключи выдуманные, распознавание подменяется в коде. Вход при этом с 2026-08-22 живой прогон проверяет целиком (cmd/devtools proxy), и это сдвиг в другую сторону.

Плюс общее, что не проверяет ни один прогон: история инцидентов, поведение под реальным потоком, поведение внешних систем в их версиях, завязка потребителей на текущее поведение и вопрос «а нужна ли эта функциональность вообще».

Каких документов проекта не хватило

  • docs/review.md, «Типовые ложноположительные» — есть и не пуст, отсев по нему выполнен. Одна находка (code, 19) отсеяна наполовину по общим критериям, а не по нему: разрешение клиентского текста для состояния «связи нет» стоит в docs/conventions/web-ui.md, а не в разделе ложноположительных.
  • CLAUDE.md, «Инварианты» — есть; ни одна находка этого прогона до critical по этому основанию не поднята, потому что ни одна инвариант не нарушает: проверено поимённо для приватности содержимого (значение заголовка в журнал и в ответ не уходит — замерено), для секрета в конфиге (секретов в секции [auth] больше нет) и для запрета переписывать применённый шаг схемы.
  • Прочих пробелов в документах проходы не назвали — но назвать их было некому: блоков границ у проходов нет (см. выше). Отсутствие строки здесь не означает, что документов хватило.

Четыре строки, которых не принёс ни один проход

  1. Решения проекта не сверялись. docs/adr.* — процессный документ, прогон его не открывает. Изменение заводит ADR-2026-08-22-login-by-trusted-header.md, и сверку его с остальными записанными решениями — включая те, что это изменение помечает (ADR-2026-08-12-oidc-exchange-via-own-route, ADR-2026-08-12-session-without-refresh), — конвейер ревью не делал. Расхождение изменения с записанным решением ловит скилл av-dev:doc-healthcheck, и звать его надо руками.
  2. Записанные наблюдения проекта не использовались. docs/research.* — тоже процессный. Всякое число в этом отчёте снято на этом прогоне: приоритеты слоёв взяты из исходников библиотеки в GOMODCACHE, счёт учётных записей и коды ответов — из прогонов, приложенных к находкам.
  3. Поимённая сверка с руководствами по стилю Go и Vue не задавалась ни одним проходом. Различение «идиоматично против распространено» на этом прогоне не спрашивал никто.
  4. Альтернативной реализации, с которой можно сдиффить решения, у конвейера нет. Проход независимой реализации снят по стоимости, а не по замеру. «Не знаю, чего не знаю» про схему узнавания по доверенному заголовку никто не доставал.

Пятой строки — про метку small — на этом прогоне нет: метка large, дома тем security, operations и architecture открывались.

Что делал сам триаж

  • Прочитаны: дифф изменения, CLAUDE.md, docs/review.md целиком, docs/security.md, docs/architecture.md «Эксплуатация», docs/database.md «Настройки с числовым значением», три конвенции, дельта-спека access, исходники pocketbase@v0.39.10 (apis/middlewares*.go, core/event_request.go).
  • Запущено во временном файле проверок пакета internal/controller/http (удалён после прогона, дерево восстановлено, go build ./... и go test ./internal/controller/http ./internal/adapter/repo/pocketbase зелёные):
    • заведение учётных записей запросами, ответившими 429 (2 → 52);
    • общий бюджет ограничителя между двумя людьми;
    • 500 и ERROR на длинном Remote-Name;
    • пустой журнал на заведении учётной записи;
    • расход бюджета анонимными отказами до и после перестановки приоритетов (200429).
  • Перестановка приоритетов на −999/−998 применена временно и откачена; на ней прогнан весь go test ./.... Код изменения не правился.