- пришедшего называет заголовок Remote-User от прокси, и верят ему только с адреса из перечня trusted_proxies; своего входа у сервиса не осталось — ни корня /auth, ни кук, ни срока сессии, ни секрета клиента в конфиге и в базе - учётная запись заводится первым обращением: EnsureUser в пакете хранилища, шаг схемы 202608220001 с колонкой provider_login и снятыми правилами users - cmd/oidcstub заменён на cmd/devtools с подкомандой proxy; заодно закрыт унаследованный DL3066 — пользователь образа назван числом
512 lines
46 KiB
Markdown
512 lines
46 KiB
Markdown
# Ревью изменения `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).
|
||
- **Состояние гейта:** зелёный целиком, кроме шага `dockerfile` — `hadolint`
|
||
даёт `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 status` → `D 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`;
|
||
- пустой журнал на заведении учётной записи;
|
||
- расход бюджета анонимными отказами до и после перестановки приоритетов
|
||
(`200` → `429`).
|
||
- Перестановка приоритетов на −999/−998 применена **временно** и откачена;
|
||
на ней прогнан весь `go test ./...`. Код изменения не правился.
|