WEB board

всеканоны и докиворкеры↗ iOS↗ Легаси
WEB-664 · Дефект · — · web

P1 [данные]: 121 документ без владельца (userId IS NULL), 117 из них проиндексированы

Закрыт P1 · важно ведёт: — эпик: WEB-093
Суть
# 121 документ не принадлежит никому

## 1. Как нашли

Учение по бас-фактору (WEB-641) впервые выполнялось **на живой мишени** — восстановленной копии боевой базы. Исполнитель выполнил проверки раздела D по-настоящему и среди прочего доложил: `Document.userId IS NULL: 121 из 3430`.

Проверила на проде — подтвердилось. Находка побочная: её никто не искал, она выпала из учения.

## 2. Что измерено (только чтение, живой прод)

```
Document всего                       3430
из них userId IS NULL                 121   (3.5 %)
```

По типам и датам:

| тип | штук | создано |
|---|---|---|
| pdf | 31 | 07.06.2026 |
| markdown | 27 | 07.06.2026 |
| email | 22 | 18.07–20.08.2026 |
| text | 22 | 07.06–19.06.2026 |
| docx | 9 | 07.06.2026 |
| text/plain | 8 | 19.06–23.06.2026 |

⚠️ **Больше половины создано в один день — 07.06.2026.** Похоже на разовую загрузку или миграцию, потерявшую владельца, а не на постоянную течь. Но `email` тянется до 20.08 — значит один путь продолжает создавать документы без владельца и позже.

## 3. Это НЕ та же проблема, что WEB-593

Пересечение посчитано:

```
без владельца И без кусков            4
без владельца, НО проиндексированы  117
с владельцем, но без кусков         346   <- это популяция WEB-593
```

**Два разных дефекта.** WEB-593 — документы, которых нет в поиске. Здесь — документы, которые **в поиске есть (117 из 121), но не принадлежат никому**.

## 4. Чем это опасно — вопрос, а не утверждение

Надо выяснить, **как поиск фильтрует выдачу**:
- если по `userId` — эти 117 не покажутся никому и лежат в индексе мёртвым грузом (тратят место и время поиска);
- если по принадлежности блокноту или иначе — **их может увидеть не тот человек**.

Второй вариант — класс утечки доступа, и его надо **проверить прежде**, чем что-то чинить. Не утверждаю, что он реализуется: это вопрос к коду выдачи.

## 5. ОСТАТОК

1. Проверить путь выдачи поиска: по какому полю режется доступ; ответить на вопрос из п.4 кодом, а не рассуждением.
2. Разобрать, что случилось 07.06.2026 — разовая загрузка или миграция; и почему `email` продолжает создавать документы без владельца до 20.08.
3. Решить судьбу 121 документа: назначить владельца, удалить или пометить системными.
4. Закрыть путь, по которому документ вообще может быть создан без владельца (`Document.userId` сейчас **nullable**).

Связано: WEB-641 (где нашли), WEB-593 (соседняя, но другая популяция), WEB-99.
Лента
2026-09-22T18:38:18.912Z · triage-neo
РЕШЕНИЕ=todo
ОСНОВАНИЕ=Актуально: карточка создана 14.09.2026; тело фиксирует 121 userId IS NULL (117 проиндексированы), приёмка/вердикт и посадка в карточке не записаны; канон WEB-449 фиксирует l115o/68e25d8df (15.09.2026), пробел доступа и происхождения данных не закрыт.
ЧТО НУЖНО=Ответственный не назначен; первый шаг — на текущей линии проверить фильтр доступа поиска по userId и зафиксировать результат с доказательством.
triage-neo 4618
2026-09-23T08:09:24.607Z · coordinator
[23.09 08:09Z координатор] ## 4674 (M1, Luna) — в работе: аудит кодом — предикат доступа на каждом пути чтения Document (утечка чужому? видит ли владелец блокнота?), точки создания без userId (e-mail-ингест), правка инварианта + dry-run скрипт ремонта. Координатор параллельно снимает с боя (SELECT-only) распределение 121 документа по блокнотам и их владельцам.
2026-09-23T08:10:01.525Z · coordinator
[23.09 08:10Z координатор] ## Факты с боя 23.09 08:1xZ (SELECT-only, координатор): `Document` всего 3430, без владельца 121 (как в тикете). По типу/месяцу: 2026-06 — pdf 31, markdown 27, text 22, docx 9, text/plain 8 (июньский разовый пакет = 97); 2026-07 — email 11, text/markdown 2; 2026-08 — email 11. У `Document` в схеме НЕТ связи с блокнотом или источником (поля: userId?, name, type, content, stats, temporalDate; связи только `chunks`, `documentEntities`) → владельца из блокнота вывести нельзя; кандидаты на восстановление — по совпадению `name`/содержимого с `Source`/письмами (проверит 4674 по коду создания). Путь e-mail продолжает создавать документы без владельца по 11 в месяц (июль, август) — течь живая.
2026-09-23T08:30:27.109Z · coordinator
[23.09 08:30Z координатор] ## 4674 (M1, GO по существу; в отчёте нет строки VERDICT — принято по содержанию и манифесту 8/8) — ответ на п.4 КОДОМ: все 6 путей чтения (semantic/text search, `queryDocuments`, `retrieveDocuments`, `chatWithSources`, global chat/inventory) режут доступ по ACL блокнота/источника (`src/lib/rag/accessScope.ts:46-71`), не по `Document.userId`; тест `ownerlessDocumentAccess.audit.test.ts`: чужой B — НЕ видит, владелец блокнота A и член команды C — видят. **Утечки чужому нет**; документ без владельца не «мёртвый груз», а доступен по ACL блокнота (связь через соглашение `Source.id = Document.id`). Пути создания без userId: `documentStore.ts:96` (общий upsert), `sourceReadGateway.ts:1030-1040` (raw INSERT dedup-копии), `scripts/migrate-sqlite-devdb-to-postgres.ts:294-313` (импорт — вероятная причина пакета 07.06) — все три закрыты патчами (владелец из Source/Notebook; новые неатрибутированные документы отклоняются). E-mail-путь в текущем коде передаёт userId — историческая течь до 20.08 в архивном дереве не восстанавливается. `scripts/repair-ownerless-documents.ts` — dry-run (владелец из `Source.notebook.userId`, где связь однозначна), `--apply` только с `--confirm-owner`. Ветка A2 `l115s-web664-ownerless-docs` (3 коммита). Решение владельцу (п.3): назначить владельца по блокноту / удалить / системная сущность — п.5 `design.md`.
2026-09-23T11:57:17.042Z · coordinator
[23.09 11:57Z координатор] ## Решение владельца 23.09 11:56 (голос): 121 документ без владельца — УДАЛИТЬ («только тестеры и я, ничего ценного»). Исполнение: после посадки l115s (в ней `scripts/repair-ownerless-documents.ts` из 4674 — добавить режим `--delete` с `--confirm-owner`, dry-run по умолчанию) — прогон на бою с резервной копией строк Document/DocumentChunk/DocumentEntity перед удалением; до посадки — ничего не трогать. Утечки нет (4674), спешки нет.
2026-09-23T17:11:51.569Z · coordinator
[23.09 17:11Z координатор] ## 4775 (M1, Luna) — в работе: режим удаления для `repair-ownerless-documents.ts` (dry-run по умолчанию, `--delete --confirm-owner`, резервная копия Document/DocumentChunk/DocumentEntity в jsonl.gz, одна транзакция, receipts, `--restore`), тесты с фальшивой Prisma, `runbook.md` для прогона на бою после посадки. На бою не запускается.
2026-09-23T17:26:49.874Z · coordinator
[23.09 17:26Z координатор] ## 4775 (M1, Luna) — GO: инструмент удаления готов (dry-run по умолчанию; `--delete --confirm-owner=…` с резервной копией Document/DocumentChunk/DocumentEntity в jsonl.gz, одной транзакцией, receipts; `--restore` для отката; тесты 8/8; typecheck 0); `runbook.md` с точной последовательностью для будущей волны на A1. Патчи 0004–0006 наложены на A2 `wt-l115s-web664`. НА БОЮ НЕ ЗАПУСКАЛОСЬ — прогон после посадки l115s (в all7).
2026-09-23T17:44:32.691Z · coordinator
[23.09 17:44Z координатор] ## 4781 (M1, независимое ревью) — WEB-664 инструмент: **NO-GO**, критичное: `repair-ownerless-documents.ts:163,364` — удаление авторизует ЛЮБАЯ строка `--confirm-owner`; репродуктор `4781…-evidence/patches/0002-web664-confirm-token-reproducer.patch`. → раунд 2 (4786): подтверждение = токен, привязанный к dry-run (хеш receipt: число кандидатов + id), печатается dry-run-ом, другой токен — отказ.
2026-09-23T17:56:45.945Z · coordinator
[23.09 17:56Z координатор] ## 4786 (M1, Luna, раунд 2) — GO: подтверждение удаления = токен `WEB-664:<count>:<sha256(sorted ids)[:12]>`, который печатает dry-run и пересчитывает delete в момент удаления (иной набор/иная строка → отказ); репродуктор 4781 красный до / зелёный после; тесты 3/3; runbook обновлён. Патчи 0007/0008 наложены на A2 `wt-l115s-web664`. Инструмент готов к посадке (all8); прогон на бою — отдельной волной после посадки.
2026-09-23T18:15:11.709Z · coordinator
[23.09 18:15Z координатор] ## 4789 (M1, независимое ревью WEB-664 р2) — NO-GO, 2 находки: (крит.) квитанция dry-run подделываема и не обязательна — токен вычисляется из ID кандидатов без подписи, delete его принимает; (высокая) токен усечён до 12 hex (48 бит). Раунд 3 — следующая волна.
2026-09-23T18:51:32.540Z · coordinator
[23.09 18:51Z координатор] ## 4796 (M1, WEB-664 раунд 3) — GO: квитанция dry-run = подписанный HMAC-SHA256 объект (набор кандидатов, счётчик, фильтр, issuedAt, TTL 15 мин, одноразовость), delete принимает только её, без ключа — fail-closed; токен 256 бит, constant-time; изменившиеся между dry-run и delete кандидаты исключаются (`skippedChanged`). Репродукторы 4789 2/2 красные→зелёные, область 20/20, typecheck 0. Патчи 0009–0012 на A2 `l115s-web664-ownerless-docs` fb8984c1b2 (12). Дальше: ревью р3.
2026-09-23T19:27:27.425Z · coordinator
[23.09 19:27Z координатор] ## 4802 (M1, независимое ревью WEB-664 р3) — NO-GO, 2 крит.: (1) квитанция dry-run живёт в памяти процесса — dry-run и delete это РАЗНЫЕ запуски CLI, квитанцию нельзя предъявить; (2) настоящий delete через Prisma на PostgreSQL прерывается до удаления (см. review.md). Прошлые 8 находок закрыты. Раунд 4 — следующая волна.
2026-09-23T20:02:50.398Z · coordinator
[23.09 20:02Z координатор] ## СОСТОЯНИЕ НА 23.09 20:1xZ (для нулевого агента)
- Что это: 121 документ без владельца на бою (userId IS NULL) — инструмент `scripts/repair-ownerless-documents.ts` (dry-run → подписанная квитанция → delete), владелец одобрил запуск на бою ПОСЛЕ посадки.
- Где код: A2 ветка `l115s-web664-ownerless-docs` = fb8984c1b2 (12 коммитов; worktree `wt-l115s-web664`). Раунд 2 (8) — в all8; р3+ — в all9.
- Раунды: 1–3 (4775/4786/4796), ревью 3 (4781/4789/4802); последнее 4802 (19:08Z) NO-GO 2 крит.: квитанция хранится в памяти одного процесса (dry-run и delete — разные запуски CLI), delete на настоящем PostgreSQL откатывается (имя модели `ownerlessDocumentOperationLog` vs `ownerlessOperationLog`).
- Сейчас: раунд 4 — волна 4805 на M1 (`~/wt-l115s-b`, вместе с WEB-593, общий модуль квитанции `src/lib/ops/dryRunReceipt.ts`), 19:30Z.
2026-09-23T20:07:29.899Z · coordinator
[23.09 20:07Z координатор] ## 4805 (M1, WEB-664 раунд 4) — GO: квитанция dry-run вынесена в общий модуль `src/lib/ops/dryRunReceipt.ts` (с WEB-593), передаётся между запусками CLI файлом (`--receipt`), одноразовость — долговечная отметка в БД; delete на настоящем PostgreSQL 17.8 доходит до COMMIT (имя модели починено, fallback убран). Репродукторы 4802 зелёные, 22/22, typecheck 0. Патчи 0013–0017 на A2 `l115s-web664-ownerless-docs` = 7d6839f595 (17). СОСТОЯНИЕ: раунд 4 принят → ревью 4810 (M1).
2026-09-23T20:43:49.808Z · coordinator
[23.09 20:43Z координатор] ## 4810 (M1, независимое ревью WEB-664 р4, третье) — NO-GO: (F-01 крит.) подписанная квитанция не привязана к БАЗЕ — выданная на стенде принимается на бою; (F-03) файл квитанции пишется по symlink без проверки владельца/типа; (F-04) fence изменений только `userId`+`updatedAt`. Прошлые 5 находок закрыты; delete на PG до COMMIT, 6 FK-таблиц, гонка одной квитанции — прошли. СОСТОЯНИЕ: раунд 5 — волна 4816 (M1, b), с WEB-593 р4.
2026-09-23T21:16:22.081Z · coordinator
[23.09 21:16Z координатор] ## 4816 (M1, WEB-664 раунд 5) — GO: квитанция привязана к БД (system_identifier + database + host + хеш applied-миграций), файл квитанции — O_NOFOLLOW/lstat/владелец/0600, fence изменений = userId,updatedAt,contentHash,deletedAt,notebookId,notebookUserId; 25/25 на PostgreSQL 17.8. Патчи 0018–0020 на A2 `l115s-web664-ownerless-docs` = 2c2a12c47f (20). СОСТОЯНИЕ: → ревью 4821 (M1, 4-е).
2026-09-23T21:50:23.341Z · coordinator
[23.09 21:50Z координатор] ## 4821 (M1, независимое ревью WEB-664 р5, 4-е) — NO-GO: F-01 (крит.) квитанция воспроизводима на ФИЗИЧЕСКОМ клоне БД (тот же system_identifier, имя базы, хост, набор миграций). Оговорка координатора: байт-в-байт клон с теми же секретами неотличим изнутри по построению — граница = секрет деплоя вне БД (ключ HMAC из env/файла) + идентичность линии/хоста; раунд 6 добавляет привязку к `RELEASE_LINE`/hostname/ключу деплоя и фиксирует границу в design.md. F-03/F-04 закрыты. СОСТОЯНИЕ: раунд 6 — волна 4828 (M1, b), с WEB-593 р5.
2026-09-23T23:00:28.332Z · coordinator
[23.09 23:00Z координатор] Волна 4828 (WEB-664 раунд 6): GO. Квитанция удаления привязана к развёртыванию: отпечаток ключа, линия релиза, ID развёртывания, хост, system_identifier и применённые миграции. Граница клона задокументирована. Серия 24 коммита → A2 `l115s-web664-ownerless-docs-r6` (29951cba15). Независимое ревью — волна 4840.
2026-09-23T23:25:32.462Z · coordinator
[23.09 23:25Z координатор] СОСТОЯНИЕ НА 23.09 23:26Z — ревью 4840 (независимое, Codex Luna, M1): V_WEB664=GO (24 патча, области 6/6, PG 17.8, прежние находки закрыты). Но находка F-07 (P1, бэкап без fsync и хэша до DELETE) касается и `writeBackup` WEB-664 → раунд 7 в волне 4845 вместе с WEB-593 (общий хелпер). Статус review.
2026-09-23T23:54:55.180Z · coordinator
[23.09 23:54Z координатор] Волна 4845 (раунд 7): GO. Закрыты F-07 и F-08 из ревью 4840. Бэкап перед DELETE теперь пишется с fsync, хэш сверяется до удаления, при подмене файла удаление отказывает; помощник общий с WEB-593. Тесты 26/26 на PostgreSQL 17.8, прежние репродукторы зелёные. Патчи на A2: ветка l115s-web664-ownerless-docs-r7 (31 коммит). Следующий шаг: независимое ревью 4849.
2026-09-24T00:25:02.359Z · coordinator
[24.09 00:25Z координатор] Волна 4849 (независимое ревью раунда 7 / 6): NO-GO по обоим. fsync и хэш по байтам диска закрыты, 4 из 5 прежних находок закрыты. Открыто: F-07 (критично) — между сверкой хэша и DELETE бэкап можно подменить; F-09 — каталог бэкапа не проверяется (симлинк, открытый всем каталог); F-10 — тест WEB-593 противоречит исключению истории DeepenAnalysisJob; точное восстановление bytea не доказано. Раунд 8/7 — волна 4853 (M1).
2026-09-24T00:48:44.923Z · coordinator
[24.09 00:48Z координатор] Волна 4853 (WEB-664 раунд 8 + WEB-593 раунд 7): GO по отчёту волны. Закрыто по 4849: TOCTOU (сверка хэша бэкапа и DELETE без окна подмены), каталог бэкапа доверенный (lstat, владелец, без group/other-write, 0700), F-10 DeepenAnalysisJob, восстановление bytea. PostgreSQL 17.8: WEB-664 34/34, WEB-593 29/29, typecheck 0. Замечание: волна сложила патчи и манифест в каталог без суффикса -evidence (манифест 60/60 OK, число совпадает с ветками). Серии → A2 l115s-web664-ownerless-docs-r8 (1969339fcc, 34) и l115s-web593-orphan-purge-r7 (5e882e40d3, 26) от 1a5822a8. Независимое ревью — 4857 (M1).
2026-09-24T01:06:33.888Z · coordinator
[24.09 01:06Z координатор] Ревью 4857 (независимое, M1): NO-GO по WEB-664 и WEB-593. Остались: F-07 (критично) — дескриптор бэкапа закрывается после сверки, между закрытием и DELETE путь можно подменить; F-09 (критично) — проверка каталога по путям, компонент подменяется симлинком после проверки; F-11 (тест) — тест миграции web593 не исполняет ветку с БД (многооператорный файл в один prepared statement). Закрыто: F-10, bytea/типы, квитанции, клон-граница; частичный бэкап блокирует удаление. Исправление — 4859 (раунд 9/8, дескриптор держится до COMMIT, каталог через O_NOFOLLOW/dev-ino).
2026-09-24T01:36:51.132Z · coordinator
[24.09 01:36Z координатор] 4859 (M1, Luna) GO: WEB-664 р9 + WEB-593 р8 — F-07 TOCTOU закрыт дескриптором от сверки до коммита DELETE, F-09 каталог бэкапа от дескриптора, F-10 исправлен, восстановление точное; PG 17.8, 38/38 и 48/48. Ветки M1 l115s-web664-r9 (4ae3a41b0), l115s-web593-r8 (419092773). Независимое ревью — 4863 (M1 tree e). До его GO в all9 не берём.
2026-09-24T01:54:29.544Z · coordinator
[24.09 01:54Z координатор] 4863 (M1, Luna, независимое ревью 664 р9 / 593 р8) NO-GO: закрыто 2/4 (F-10, точное восстановление bytea/Date/BigInt/NULL/JSONB, F-11 миграция на PG 17.8), частичный бэкап блокирует DELETE. Открыто 2 критичных: F-07 — восстановление читает бэкап по пути, уже не привязанному к сверенному объекту; F-09 — проверяется только последний каталог, родитель 0777 принимается. → 4868 (M1 tree c, раунд 10): проверка всей цепочки каталогов, квитанция sha256+размер в той же транзакции, что DELETE, восстановление сверяет хэш и отказывает при подмене; граница модели угроз зафиксирована (тот же uid/root — вне модели).
2026-09-24T02:22:13.853Z · coordinator
[24.09 02:22Z координатор] 4868 (M1, Luna, WEB-664 р10 + WEB-593 р9) GO по обоим: закрыты критичные находки ревью 4863 — F-07 (восстановление читает бэкап только через сверенный дескриптор, хэш бэкапа записан в квитанцию в той же транзакции удаления, подменённый бэкап — отказ, восстановление точное) и F-09 (проверяется вся цепочка родительских каталогов бэкапа). PostgreSQL 17.8, прошлые репродукторы зелёные, манифест 87 OK. → A2: l115s-web664-ownerless-docs-r10 = a80e8688be (43), l115s-web593-orphan-purge-r9 = 0f638ad10b (36). Независимое ревью 4873 (M1 tree c). Статус → review.
2026-09-24T02:38:26.578Z · coordinator
[24.09 02:38Z координатор] 4873 (M1, независимое состязательное ревью: WEB-664 р10 + WEB-593 р9) GO, находок 0: области 7/7 и 9/9, PG 17.8, 19/19 атак, прежние находки 2/2 закрыты, частичный бэкап блокирует удаление, окна TOCTOU нет, восстановление точное. Ветки A2: l115s-web664-ownerless-docs-r10 a80e8688be, l115s-web593-orphan-purge-r9 0f638ad10b. Статус: принято, идёт в all9; запуск на бою — после посадки, с разрешения владельца.
2026-09-25T08:42:14.281Z · coordinator
[25.09 08:42Z координатор] VERDICT=CLOSE — WEB-664.

Ownerless repair all9 требует signed dry-run receipt, exact confirmation, database/deployment binding, typed backup/restore и fencing против TOCTOU/path substitution. Commit `a80e8688be` (accepted merge `ef41909b1`); ключи `scripts/repair-ownerless-documents.ts:114-124,228-242` и `scripts/__tests__/web664-adversarial-4789-isolated.test.ts:56-90`; 6/6 executable cases pass.

Что ломалось: ревью 4857/4863 нашли backup descriptor TOCTOU и принятие 0777 parent path; 4868 удержал descriptor до COMMIT, записал sha/bytes в той же транзакции и проверил всю parent chain; 4873 independent GO. Остаток: координатор/owner решает судьбу 121 существующей строки и запускает live dry-run/canary. PG ACL audit intentionally not run.

Старт: forged token → reject; exact signed receipt → bounded backup/delete/restore. Доска: http://127.0.0.1:8787/api/web/issues/WEB-664.


Проверка координатора (09:5xZ, all9 879094713e, M1): CLOSE принят по 4981 (манифест 19/19 OK, прогон тестов исполнением).
Воркер
не привязан — привязать: curl -X POST https://bugs.wool2.online/api/web/assign -H 'content-type: application/json' \ -d '{"issueId":"WEB-664","session":"<имя tmux-сессии>","host":"m4"}'
Обновлён
2026-09-25T08:42:38.169Z