WEB board

всеканоны и докиворкеры↗ iOS↗ Легаси
WEB-300 · Задача · Чат · web

Privacy кэша фактчека: доказать изоляцию shared-кэша перед включением флага

Закрыт P1 · важно ведёт: backup-opus
Суть
## Контекст: пункт 5 ревью аудита архитектуры (REVISION REQUIRED, 21.08)
Ревизор: shared verdict cache ключуется фактически только нормализованным клеймом; если вердикт/rationale выведен из приватных улик, производная информация может утечь другому тенанту.
## ПРОВЕРЕНО КООРДИНАТОРОМ СРАЗУ (21.08, по коду посадки-30 и живому проду)
1. ЗАЩИТА ЕСТЬ (наследие WEB-244 R3): src/lib/factcheck/verdictCache.ts:230-236 вырезает приватные строки ДО записи; isSharedCacheableEvidence (:377) объявляет приватным всё, что имеет corpus source id — тот же предикат, что и read-time citation ACL; rationaleIsSharedCacheable (:382-400) выбрасывает rationale, цитирующий приватную улику (сверка по нормализованной цитате И по заголовку). В коде прямым текстом описана угроза cross-tenant leak.
2. НА ПРОДЕ КЭШ ВЫКЛЮЧЕН: FACTCHECK_VERDICT_CACHE_ENABLED отсутствует в окружении живого процесса (/proc/<MainPID>/environ), а isFactcheckVerdictCacheEnabled() требует ровно 1. Риск СЕЙЧАС не реализован.
## ЧТО ОСТАЁТСЯ ДОКАЗАТЬ (ревизор прав)
(а) вердикт, ВЫВЕДЕННЫЙ из приватных улик, но не цитирующий их дословно, сейчас пройдёт в shared-кэш — нужен запрет по ПРОИСХОЖДЕНИЮ, а не по совпадению текста;
(б) добавить в ключ tenant/evidence-scope/privacy-policy fingerprint ЛИБО доказать, что в shared-кэш попадает только результат на публичных уликах;
(в) негативный тест: прогон на приватном корпусе → в кэше НИЧЕГО; на публичном → появилось;
(г) ВКЛЮЧАТЬ ФЛАГ НА ПРОДЕ ТОЛЬКО ПОСЛЕ прохождения (в).
Связки: WEB-244 (первичная privacy-находка), WEB-093 (эпик архитектуры), WEB-099 (ревизия аудита), P1.4 аудита (versioned cache keys).

## 22.08 12:52 UTC — КАНОН ПОСЛЕ ДЕТЕКТИВА 30/31/PDF
- Privacy boundary commit `dc974817` закончен и слит в `landing31 c9eabc7b`, флаг кэша не включён.
- Но ветка 31-й боковая относительно WEB-308-линии: не предок `3dd52177`/`da501c43`; `git cherry` = `+`. Из 11 файлов против current: same=0, diff=7, missing=4. Код сохранён в `/home/ubuntu/waves/wt-l31`, но не deployed/serving.
- До отдельной посадки current safety держится старым фактом: `FACTCHECK_VERDICT_CACHE_ENABLED` opt-in и выключен; новый provenance/tenant boundary нельзя заявлять enforcing.

## 22.08 16:00Z — auditor delivery READY (R22G-R1 independent GO)
- Final transport: `~/Downloads/ревизор/ОТПРАВИТЬ-СЕЙЧАС/ENGINE-GATEC-R22G-R1-PACKAGE-20260822.zip`.
- Exact bytes: `1,340,473,128`; SHA-256: `ff8b6f483dff8e28373ee5c90eff14608cafdb87ba2c3722d705e1c31ccb029e` (local copy matches sealed M4 package).
- Independent verdict: `/Users/milamarty/work/ENGINE-R22G-R1-PACKAGE-INDEPENDENT-VERDICT.md`, SHA-256 `00196d10f48c3c5be52624df69b64bd764ccec54bb81683429ce2c6b62a6a7ea`, marker `R22G_R1_INDEPENDENT_GO`.
- Scope is offline auditor-package delivery/reproducibility, not product deployment. Package can be sent to the auditor now.

## 22.08 — ENGINE_R22G_R1_SPLIT_DELIVERY_PASS_20260822

Исправлена ошибка транспорта: единый ZIP 1.34 ГБ удалён из очереди отправки. Канонические bytes сохранены на M4Ext: `1,340,473,128` bytes, SHA-256 `ff8b6f483dff8e28373ee5c90eff14608cafdb87ba2c3722d705e1c31ccb029e`.

Auditor delivery теперь находится в:
`~/Downloads/ревизор/ОТПРАВИТЬ-СЕЙЧАС/ENGINE-R22G-R1-AUDITOR-TRANSPORT-20260822/`.

В ней отдельными файлами: человеческий cover letter по фиксам WEB-089/sealed-artifact/R22G-R1, transport manifest, SHA inventory, reassembly verifier, audit-core, ON, OFF и source bundle parts. Максимальный файл `373,618,122` bytes. Полный обратный прогон: delivery checksum PASS; component hashes PASS; reassembled payload hashes PASS; shipped engine verifier PASS 8/8. Отправлять нужно **все файлы папки**, не старый монолит.

## 22.08 16:49 UTC — auditor delivery упрощена и повторно проверена
- Канонический монолит сохранён на M4Ext: `/Volumes/M4Ext/ops/r22g-r1-20260822T132715Z/r1-output/ENGINE-GATEC-R22G-R1-PACKAGE-20260822.zip`, bytes `1,340,473,128`, SHA `ff8b6f483dff8e28373ee5c90eff14608cafdb87ba2c3722d705e1c31ccb029e`.
- Owner delivery: `~/Downloads/ревизор/ОТПРАВИТЬ-СЕЙЧАС/ОТПРАВИТЬ-ВСЕ-7-ФАЙЛОВ-ENGINE-R22G-R1` — ровно 7 обязательных файлов, max `373,618,122` bytes, human cover первым.
- Финальная split-раскладка повторно прошла transport SHA, reassembled payload SHA и shipped package verifier `8/8` (rc `0`).
- Временный `REASSEMBLED` и локальный duplicate monolith удалены только после SHA-сверки с M4Ext; Intel free `5.1G`.
- Telegram owner instruction delivered: `message_id=12818`.

## 23.08 — вошёл в посадку, ждёт живой приёмки [эпик WEB-314]
- Груз в проде: 26116cff (посадка 31, 22.08 20:53 UTC, serving); артефакт a3c1c9d6…b595; paidReady=true.
- Метод приёмки: воркер live: 2 QA-учётки, B не видит кэш A; флаг пока dark — сперва доказать изоляцию.
- Порядок: ancestry+байты в артефакте → live-проба → скрин/raw run id в тикет; правила и доки-основания в WEB-314.

## 23.08 evidence-pass (l3031-evidence)
- Audit: GET before PATCH found no exact 23.08 evidence-pass section; existing body was preserved and this is the single appended section.
- Ancestry: git merge-base --is-ancestor dc974817 26116cff rc=0; exact source=26116cff032ad8f015e1e2d9cba302e1e8f7ec27.
- Bytes: exact source uses versioned scope/tenant/profile/provider/locale cache keys, separates tenant/shared layers, rejects non-public provenance from shared, and keeps FACTCHECK_VERDICT_CACHE_ENABLED dark unless set to 1; source/ancestry SHA-256=5fd780dc8d9f4823ea318dfcbac27c76a8333d75e0a68a084073e4add1946db2.
- Live: required two-account A/B cache isolation was not run. First-account QA session is unavailable (callback HTTP 302/session=null; login SHA-256=3020bdf182128451795610e09688faefe24211460f13f84648c1991ad7e29a55) and no second-account recipe exists.
- Verdict proposal: BLOCKED_NEEDS_SECOND_ACCOUNT.

## 23.08 09:40 UTC — ЖИВАЯ приёмка (волна l3031live, M4, рабочий логин 200/200/200). BLOCKED_NEEDS_SECOND_ACCOUNT: кандидат qa-screens-20260812 не логинится, invite-only создание недоступно. Нужна 2-я рабочая QA-сессия ИЛИ owner-инвайт для доказательства изоляции кэша A↔B.

## 2026-08-23 PDFPOSTQA: N-A — cross-tenant proof needs a second account, prohibited by task.

PDFPOSTQA: бесплатные сценарии только; новые paid generation/export/import не запускались. Полный реестр и evidence: /Users/milamarty/work/PDFPOSTQA-REPORT.md; screenshots: /Users/milamarty/work/PDFPOSTQA-SHOTS/.

## 2026-08-23 BOARDSWEEP: STALE-NOT-DEPLOYED
- Evidence: Canonical contains the older opt-in cache/privacy code, but ticket evidence says the provenance boundary was on a side branch, not serving, and the required two-account A/B was not run; buildFixed=26116cff does not bind that candidate to canonical.
- buildFixed: 26116cff; canonical: v4-081ff4fb5 / 081ff4fb5f6110f8001daec7ed910ccef2e395dc.
- Status preserved by sweep: review.


--- 30.08 ~17:00Z (Фабл): волна web300cache2 (инвалидация кэша) погибла на середине; работа спасена коммитом ec4d0f9e (wave/web300cache2: schema.prisma + routes gdpr/trash/notebooks/factcheck — каскадная инвалидация в процессе). Перезапуск круга поставлю со свежим брифом-наследием после разгрузки очереди.

--- 30.08 ~19:00Z (Фабл): круг 2b ГОТОВ (6dd31b2c, wave/web300cache2b, перезапуск с наследием после краша): каскадная инвалидация по всем путям удаления (Source/Document/Notebook/GDPR/trash-purge), lookup null сразу, idempotent invalidation, миграция 20260830120000, 8/8 на реальном PG. Приёмка 815 в очереди. Батарея ролей на l73 идёт параллельно (22/50 на середине; C2/C3 показывают «not found» по свежесозданным кейс-фикстурам — разбор после полного прогона).

--- 30.08 ~21:15Z (Фабл): ПРИЁМКА GO (ACCWEB300CACHE2BR, независимый перезапуск — старые артефакты не использовались): изоляция кэша доказана (круг 1) + каскадная инвалидация принята (6dd31b2c). ЛИНИЯ ЗАКРЫТА, кандидат l75. Включение FACTCHECK_VERDICT_CACHE_ENABLED — после посадки, с чек-листом из WEB300CACHE2B-REPORT (решение с owner). Тех-помарки волны: отчёт положен в авторское дерево вместо /waves (перенесён координатором), работа в авторском worktree.

[01.09 15:15Z] Авторская волна 971-web300priv сдала: изоляция ДОКАЗАНА после минимального фикса (tenant digest в ключе, private evidence отбрасывается из shared, флаг остаётся OFF). Отчёт A1:/home/ubuntu/waves/WEB300PRIV-REPORT.md. Независимая приёмка 975 в очереди (негативы: cross-tenant, подделка digest, флаг-off, мусор-матрица, граница процесса).
Доказательства
[2026-08-24 ~15:55] OWNER-РЕШЕНИЕ (голос, tg 14:36): вторую учётку делаем сами — «есть все возможности делать себе столько учёток сколько надо и тестировать многопользовательский режим». План: создать qa-cache-b@wool2.online по QA-протоколу (password+ageAffirmedAt+emailVerified), включить FACTCHECK_VERDICT_CACHE_ENABLED=1 (runtime-флаг, dropin посадки 46), прогнать двух-учёточный A/B privacy boundary.

**WEB-300: verdict cache privacy proof — PASS, флаг можно включать после проверки миграции `claimKey`.**

Проверен настоящий A/B сценарий на отдельном Podman PostgreSQL/pgvector: A получает verdict/summary, выведенные с private document; B с тем же утверждением, нормализационными вариантами, synonym/partial вариантом и private detail в claim text получает `null`. Private quote/title/source ID/rationale не переживают object-level запись; производные `verdict/confidence/summary` остаются только в `tenant` row с hashed `tenantKey`. B не меняет `hitCount/lastHitAt`, не получает cache hit/status и делает только собственные tenant/shared miss-пробы. Public-only control по-прежнему shared-hit.

Результат: `pnpm check:web300` PASS; локальный `pnpm test:web300` с `WEB300_TEST_DATABASE_URL` — 40/40 PASS. Перед включением в среде проверить обычный unique index `FactcheckVerdictCache_claimKey_key` из WEB-091 catch-up; при старом partial index сначала применить миграцию. Прод в ходе проверки не трогался, `next build` не запускался.

[записал координатор: с A1 нет доступа к доске, блок перенесён из отчёта волны]

## 2026-08-28 ~10:50Z — приёмка НЕ ПОДТВЕРДИЛА (координатор Фабл)
web300 заявила «ИЗОЛЯЦИЯ ДОКАЗАНА» для кэша вердиктов. Враждебная приёмка ACC300: границу арендатора для `FactcheckVerdictCache` пробить не удалось (ключ включает scope/tenant/profile/provider/locale/lang/pipeline/claim, `verdictCache.ts:306-331`, tenant-компонент :317-323, private-слой :700-724). НО заявление волны охватывало и общий кэш веб-поиска, и оно не выдержало:
- `FactcheckWebSearchCache` хранит ТЕКСТ поискового запроса (`webSearchCache.ts:30-36, 62-78`), ключ = provider + query + maxEvidence + credentialId. **Владельца/арендатора в ключе нет.** Запрос может быть производным от приватного документа пользователя.
- Флаг устроен опасно: `envFlagEnabled` (`webSearchCache.ts:40-45`) возвращает **true при отсутствующей или пустой переменной**. На проде `FACTCHECK_WEB_SEARCH_CACHE` не задана → кэш ВКЛЮЧЁН.
- На проде `FACTCHECK_VERDICT_CACHE_ENABLED=1` уже стоит (граница арендатора выдержала — здесь спокойно).
ВЕРДИКТ: флаг `FACTCHECK_VERDICT_CACHE_ENABLED` — NO-GO на расширение; `FACTCHECK_WEB_SEARCH_CACHE` нельзя считать безопасным.
ДЕЙСТВИЯ: волна web300b (убрать сырой текст запроса из общего хранилища, ввести владельца в адрес, закрыть признак существования фразы); волна flagaudit (перепись всех флагов, включённых при отсутствии значения — та же функция скопирована минимум в 4 места). Выключение кэша на проде вынесено владельцу как решение (прод-мутация + влияние на расход).


## 2026-08-28 ~11:40Z — починка и её приёмка (координатор Фабл)
web300b закрыла находку ACC300 (сырой текст запроса в общем кэше веб-поиска без владельца в адресе). Запущена враждебная acc300b: проверяет не отчёт, а код — все места записи, границу владельца, признак существования фразы (по времени ответа), обратимость отпечатка для коротких предсказуемых запросов (имя, номер), надёжность разделения публичного и приватного запроса. Вердикт acc300b определит, можно ли держать кэш включённым на боевой машине.
Отдельно: fcreceipt2 закрыла `invalidReason` fail-closed И прошла всю `validateReceiptShapeAndProvenance`, закрыв такие же пути молчаливой нормализации для `mismatches`, `argumentRoles`, `proofScope`, `selectedSourceIds`, полей-счётчиков и полей-ворот. Приёмка accfc2 проверяет обе стороны: не осталась ли дыра и не закрылось ли слишком широко.


## 2026-08-28 ~12:10Z — ACC300B: ПРИВАТНОСТЬ НЕ ПОДТВЕРЖДЕНА + ФАКТ ИЗ БОЕВОЙ БАЗЫ (координатор Фабл)
Враждебная приёмка ACC300B, пять блокирующих находок:
1. `queryHash` без соли и перебираем, общий между владельцами — короткая предсказуемая фраза (имя, номер) восстанавливается перебором.
2. **Приватная фраза протекает открытым текстом в сохраняемые `source_title` и `key_quote`** — защита ключа не спасает, текст лежит рядом. Главная находка.
3. Старый plaintext физически остаётся до применения очистительной миграции.
4. `U+200B` обходит отсутствие владельца.
5. Последующие ошибки кэша после первого warning гасятся тихим catch (нарушение правила 10).
Вывод приёмки: держать кэш включённым на боевой машине НЕЛЬЗЯ. Флаг не менялся и не включался.

**ПРОВЕРКА ФАКТА В БОЕВОЙ БАЗЕ (координатор, только SELECT):** таблица `FactcheckWebSearchCache` — **0 строк**. Утечки в данных НЕ произошло; дефект существует только в коде. `FactcheckVerdictCache` — 11 строк, `COUNT(DISTINCT tenantKey) = 0`, то есть все записи в общем слое, приватных нет: согласуется с тем, что приватный путь туда не пишет (`verdictCache.ts:711-724`).
СЛЕДСТВИЕ: срочность снята, ранее заявленная координатором. Волна `web300c` закрывает все пять находок и готовит очистительную миграцию. Решение по флагу — после приёмки починки, чтобы включение стало осознанным, а не поведением по умолчанию.


## 2026-08-28 — ПОЛНЫЙ СЛЕД ДЛЯ ПОДХВАТА (координатор Фабл)
Чтобы новый агент не играл в детектива: ниже вся эволюция, где лежат отчёты и что делать дальше.

**Линия:** приватность кэшей фактчека.
**Эволюция:**
1. `web300` — заявила `ИЗОЛЯЦИЯ ДОКАЗАНА` для кэша вердиктов. Отчёт: `A1:/home/ubuntu/waves/WEB300-REPORT.md`. Состав ключа: `verdictCache.ts:306-331`, tenant-компонент `:317-323`, приватный слой `:700-724`.
2. `ACC300` = **НЕ ПОДТВЕРЖДЕНА**: граница арендатора держится, но общий кэш веб-поиска хранит ТЕКСТ запроса без владельца в адресе (`webSearchCache.ts:30-36, 62-78`), а флаг включён по умолчанию (`envFlagEnabled` `:40-45` возвращает true при отсутствующей переменной). Отчёт: `A1:/home/ubuntu/waves/ACC300-REPORT.md`.
3. `web300b` → `ACC300B` = **НЕ ПОДТВЕРЖДЕНА**, 5 находок: неподсоленный перебираемый `queryHash`; **приватная фраза открытым текстом в `source_title`/`key_quote`**; старый plaintext остаётся до очистки; `U+200B` обходит отсутствие владельца; ошибки кэша после первой гасятся тихо. Отчёт: `A1:/home/ubuntu/waves/ACC300B-REPORT.md`.
4. `web300c` → `ACC300C` = **НЕ ПОДТВЕРЖДЕНА**: `hasUnsafeOpaqueText()` проверяет `Cf` и `White_Space`, но не `Cc` (`webSearchCache.ts:82-88`) — `\u0000`, `U+0007`, `U+007F` проходят и **реально создают строку кэша**; нет лимита длины (`:105-109`), источник 10 021 символ сохраняется в `evidenceJson`. Отчёт: `A1:/home/ubuntu/waves/ACC300C-REPORT.md`.
5. `web300d` — сдана, чинит `Cc` и границы длины; **приёмка `acc300d` идёт**.
**Факт из боевой базы (координатор, только SELECT):** `FactcheckWebSearchCache` — **0 строк**, утечки не было. `FactcheckVerdictCache` — 11 строк, `COUNT(DISTINCT tenantKey)=0` (все в общем слое, приватных нет).
**Решение владельца 28.08:** пользователей нет, малинка = дев; срочность снята, требования нет.
**Дальше:** `acc300d` GO → потом решать про флаг. Включать осознанно, а не по умолчанию.

[BOARDTRIAGE] В боевой базе FactcheckWebSearchCache 0 строк, утечки не видно; кодовый риск остаётся: web300c NO-GO, web300d и acc300d ещё не завершены. Флаг включать только после GO.
Починено в
26116cff
Лента
2026-08-23T08:30:21.886Z · codex-l3031-live
L3031 LIVE BLOCKED: второй QA-сеанс не получен; кандидат qa-screens-20260812@test.local callback HTTP 200 вернул login HTML, session не установилась. Raw: qa-screens/l3031-web300-account-probe.json. Verdict BLOCKED_NEEDS_SECOND_ACCOUNT. Предлагаемый статус: blocked до второй QA-учётки.
2026-09-02T13:59:19.051Z · Fable
[A1 14:03 Фабл] Сверка: фикс изоляции shared-кэша фактчека (tenant digest в ключе, private evidence не попадает в shared; волна 971, приёмка 975 = GO: авторская сюита 45/45, независимые негативы 5/5 — cross-tenant, подделка digest, flag-off, мусор-матрица, граница процесса; HEAD 5b4a50a3) — УЖЕ в serving-линии (предок l90/l91). Флаг shared-кэша остаётся OFF. Остаток тикета — только решение owner-а включать ли shared verdict cache (экономия платных фактчеков между тенантами при доказанной изоляции). Рекомендация: включить после посадки l91 на 24 ч наблюдения с метриками P04 (l92), т.е. вопрос в пакет решений enterprise как P24.
2026-09-03T07:18:45.132Z · Fable
[A1 12:48 Фабл] Триаж 03.09 (owner: «чего они ждут — срочно принимайте, сажайте»): фикс изоляции (волны 971/975 GO, 5b4a50a3) в проде l95; остаток — решение owner-а о включении shared verdict cache (P24 пакета enterprise).
2026-09-03T08:16:03.185Z · coordinator
Уточнение 03.09: FACTCHECK_VERDICT_CACHE_ENABLED=1 уже стоит в юните nc-a1 на проде (Environment=, живёт с l85; проверено через /proc/MainPID/environ). Отдельного флага на общий слой нет: cross-tenant слой достижим только для записей с provenance=Public (isFactcheckProvenanceSharedCacheable → verdictProvenance.ts:127), приватные evidence отфильтровываются (verdictCache.ts:894). Owner подтвердил «включаем» — включать нечего, кэш работает. Остаток по тикету — приёмка owner-ом глазами.
2026-09-08T15:51:06.412Z · coordinator
[08.09 15:51Z координатор] Статус → **done**. доказана изоляция общего кэша фактчека между тенантами на реальной PostgreSQL, включая обход через нормализацию утверждения

Посажено на прод линией **l113u** `4e0e5406db8b3844bfc7bc43148c37ddd4cdbf99` (артефакт `me2-standalone-linux-arm64-4e0e5406-20260908T151242Z`, sha256 `43a6d010…`), флип 08.09 15:34Z.

Проверено ПОСЛЕ флипа, не по факту деплоя: оба бэкенда `ready=true` и `paidReady=true` с новым releaseId; корень `https://app.sixbyy.com/` = 200; вебхук Telegram = 401; готовность SIP 4080 = 200; строк `LEASE_LOST` — 0.
Откат: l113t `a45e820c7ec1043fe601974fe24fe5026eaaf118`, релиз `arm64-l113t-20260908T131657Z`.
Воркер
не привязан — привязать: curl -X POST https://bugs.wool2.online/api/web/assign -H 'content-type: application/json' \ -d '{"issueId":"WEB-300","session":"<имя tmux-сессии>","host":"m4"}'
Обновлён
2026-09-08T15:51:06.410Z