WEB board

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

ЛИНИЯ: единая дверь к платным провайдерам (spendGateCall) — структурный запрет обходов

Закрыт P2 ведёт: —
Суть
1. Суть одной фразой: Дверь spendGateCall — 6 кругов escape-analysis в тупик (ACCSPENDGATE6 NO-GO).
2. Где мы сейчас (13.09.2026): круг 7 = b952f809 (фикс экспортной поверхности); slice source-complete, но широкая миграция (73 legacy-файла) не завершена; нужна независимая приёмка + посадка.
3. Хронология: круги 1-6; ACCSPENDGATE6 NO-GO; круг 7 b952f809.
4. Карта документов и кода: 73 legacy-файла; b952f809.
5. Остаток: волна — завершить миграцию, независимая приёмка, посадка.
6. Критерий закрытия: независимый GO + посадка на линию.

---

## история (тело до 13.09.2026)



## ДЛЯ НУЛЕВОГО АГЕНТА (обновлено 2026-09-05 UTC)
- **Суть одной строкой:** ЛИНИЯ: единая дверь к платным провайдерам (spendGateCall) — структурный запрет обходов
- **Текущее состояние:** статус: parked; [A1 12:48 Фабл] Триаж 03.09 (owner: «чего они ждут — срочно принимайте, сажайте»): 6 кругов (a19ae967..7fab5857) не в проде; ACCSPENDGATE6 NO-GO 29.08; spendgate7 умер без маркера → круг 7 = волна 1512 на базе l96 (смена устройства: фиксированная поверхность экспортов двери). | Круг 7 сдан (волна 1512): коммит b952f809 «SPENDGATE7: fix provider door export surface», база l96-candidate 1b448bb5, семь коммитов линии spendgate6 (…7fab5857) перенесены бандлом и вложены до фикса. СМЕНА УСТРОЙСТВА после 6 кругов эскейп-анализа: src/lib/billing/spendGate.ts теперь отдаёт ТОЛЬК… | [2026-09-05 17:40Z] Ревизия доски (волна 2040 boardtriage, M1, luna): статус → parked. Основание: нет активности с 2026-08-29; ACCSPENDGATE6 NO-GO. Если работа жива — верни статус и напиши в тикет, какая волна её ведёт.
- **Кто работал:**
- 2026-09-03T07:18:45.525Z — Fable — [A1 12:48 Фабл] Триаж 03.09 (owner: «чего они ждут — срочно принимайте, сажайте»): 6 кругов (a19ae967..7fab5857) не в проде; ACCSPENDGATE6 NO-GO 29.08; spendgate7 умер без маркера → круг 7 = волна 1512 на базе l96 (смена
- 2026-09-03T08:28:47.874Z — coordinator — Круг 7 сдан (волна 1512): коммит b952f809 «SPENDGATE7: fix provider door export surface», база l96-candidate 1b448bb5, семь коммитов линии spendgate6 (…7fab5857) перенесены бандлом и вложены до фикса. СМЕНА УСТРОЙСТВА по
- 2026-09-05T17:35:28.638Z — fable-coordinator — [2026-09-05 17:40Z] Ревизия доски (волна 2040 boardtriage, M1, luna): статус → parked. Основание: нет активности с 2026-08-29; ACCSPENDGATE6 NO-GO. Если работа жива — верни статус и напиши в тикет, какая волна её ведёт.
- **Ветки/бандлы/отчёты:** нет данных в тикете; sha: a19ae967, 7fab5857, b952f809, 1b448bb5
- **KNOWN ISSUES / ТРАБЛШУТИНГ:**
- [A1 12:48 Фабл] Триаж 03.09 (owner: «чего они ждут — срочно принимайте, сажайте»): 6 кругов (a19ae967..7fab5857) не в проде; ACCSPENDGATE6 NO-GO 29.08; spendgate7 умер без маркера → круг 7 = волна 1512 на базе l96 (смена устройства: фиксированная поверхность экспортов двери).
- Круг 7 сдан (волна 1512): коммит b952f809 «SPENDGATE7: fix provider door export surface», база l96-candidate 1b448bb5, семь коммитов линии spendgate6 (…7fab5857) перенесены бандлом и вложены до фикса. СМЕНА УСТРОЙСТВА после 6 кругов эскейп-анализа: src/lib/billing/spendGate.ts теперь отдаёт ТОЛЬК…
- [2026-09-05 17:40Z] Ревизия доски (волна 2040 boardtriage, M1, luna): статус → parked. Основание: нет активности с 2026-08-29; ACCSPENDGATE6 NO-GO. Если работа жива — верни статус и напиши в тикет, какая волна её ведёт.
- **Эволюция:**
- 2026-09-03 → [A1 12:48 Фабл] Триаж 03.09 (owner: «чего они ждут — срочно принимайте, сажайте»): 6 кругов (a19ae967..7fab5857) не в проде; ACCSPENDGATE6 NO-GO 29.08; spendgate7 умер без маркера → круг 7 = волна 1512 на базе l96 (смена устройства: фиксированная поверхность э
- 2026-09-03 → Круг 7 сдан (волна 1512): коммит b952f809 «SPENDGATE7: fix provider door export surface», база l96-candidate 1b448bb5, семь коммитов линии spendgate6 (…7fab5857) перенесены бандлом и вложены до фикса. СМЕНА УСТРОЙСТВА после 6 кругов эскейп-анализа: src/lib/bil
- 2026-09-05 → [2026-09-05 17:40Z] Ревизия доски (волна 2040 boardtriage, M1, luna): статус → parked. Основание: нет активности с 2026-08-29; ACCSPENDGATE6 NO-GO. Если работа жива — верни статус и напиши в тикет, какая волна её ведёт.
- [A1 12:48 Фабл] Триаж 03.09 (owner: «чего они ждут — срочно принимайте, сажайте»): 6 кругов (a19ae967..7fab5857) не в проде; ACCSPENDGATE6 NO-GO 29.08; spendgate7 умер без маркера → круг 7 = волна 1512 на базе l96 (смена устройства: фиксированная поверхность экспортов двери).
- [2026-09-05 17:40Z] Ревизия доски (волна 2040 boardtriage, M1, luna): статус → parked. Основание: нет активности с 2026-08-29; ACCSPENDGATE6 NO-GO. Если работа жива — верни статус и напиши в тикет, какая волна её ведёт.
- **Следующий шаг:** [A1 12:48 Фабл] Триаж 03.09 (owner: «чего они ждут — срочно принимайте, сажайте»): 6 кругов (a19ae967..7fab5857) не в проде; ACCSPENDGATE6 NO-GO 29.08; spendgate7 умер без маркера → круг 7 = волна 1512 на базе l96 (смена устройства: фиксированная поверхность экспортов двери).


## Актуализация координатора 2026-09-26 12:01Z [refresh-20260926-roles-capacity]
Правила обогащения: WEB-449.

5058 A2 завершена NO-GO, dispatcher done 2026-09-26T11:59:15Z; полный SHA256SUMS координатор проверил: PASS. CHAIN_RC=21, BUILD_LINUX=FAIL, ARTIFACT_PATH=NONE. Последние ошибки: pinned Next send/depd с Error.prepareStackTrace/getFileName и WriteStream с immutable EventEmitter.prototype.emit. Отчёт сообщает удаление исходного рабочего дерева и материалов внешним TTL janitor; причинный механизм координатор ещё не проверил. Сейчас 5058-work содержит только попытку восстановления, 5058-material отсутствует. Новые интеграционные коммиты не поставлены bundle и НЕ приняты. Исходный bundle 5056 сохраняется на ноутбуке. Следующий ответственный: координатор — установить причину удаления, сохранить исходники вне подтверждённой области очистки и организовать продолжение Linux compatibility с отрицательными security tests. Исполнитель 5058 уже НЕ работает.

КАРТА ДОКУМЕНТОВ / доказательства: A2:/home/ubuntu/waves/5058INTEGRATEDLINUXSTANDBUILD-REPORT.md; A2:/home/ubuntu/waves/5058INTEGRATEDLINUXSTANDBUILD-evidence/SHA256SUMS; ноутбук:/Users/annakorin/nc-ops-scripts/material-5058/5056NEXTBOOTSTRAPCOMPATIBILITYCOMPLETION.bundle
Доказательства

[29.08 19:20Z] SPENDGATE4 GO по двум пунктам (89347817): checker в основном npm test (rogue = красный suite), re-export binding ловится. → ACCSPENDGATE4 (queue/664: свой rogue, новые формы re-export: default/star/module.exports/Object.assign).

[29.08 19:50Z] ACCSPENDGATE4 NO-GO: CJS-формы (module.exports = imported binding; Object.assign(module.exports,...)) проходят сканер. → SPENDGATE5 (queue/669: класс «любой вынос binding наружу», не перечисление форм).

[29.08 19:35Z] SPENDGATE5 GO (f09553a0): класс «любой вынос binding» вместо перечисления. → ACCSPENDGATE5 (queue/681: WeakMap/Symbol/closure, проверка классовости).

[29.08 20:00Z] ACCSPENDGATE5 NO-GO: Symbol.for-канал через globalThis проходит. → SPENDGATE6 (queue/685: escape-анализ значений ИЛИ полный запрет provider-import вне двери).

[29.08 22:25Z] SPENDGATE6 GO (7fab5857): Symbol/globalThis-каналы закрыты escape-анализом. → ACCSPENDGATE6 (queue/694: prototype pollution, WeakRef, side-channel через хранилище, вопрос классовости анализа). 6-й круг — при GO линия закрывается.

[29.08 23:30Z] ACCSPENDGATE6 NO-GO: new OpenAI(...) + export default из allowlisted файла проходит (анализ не следит за производными значениями). ВЕРДИКТ КООРДИНАТОРА: 6 кругов эскейп-анализа = тупик → SPENDGATE7 (queue/703, СМЕНА УСТРОЙСТВА: фиксированная поверхность экспортов файлов двери — любой неперечисленный export = violation; производные не могут покинуть дверь легально).
Лента
2026-09-03T07:18:45.525Z · Fable
[A1 12:48 Фабл] Триаж 03.09 (owner: «чего они ждут — срочно принимайте, сажайте»): 6 кругов (a19ae967..7fab5857) не в проде; ACCSPENDGATE6 NO-GO 29.08; spendgate7 умер без маркера → круг 7 = волна 1512 на базе l96 (смена устройства: фиксированная поверхность экспортов двери).
2026-09-03T08:28:47.874Z · coordinator
Круг 7 сдан (волна 1512): коммит b952f809 «SPENDGATE7: fix provider door export surface», база l96-candidate 1b448bb5, семь коммитов линии spendgate6 (…7fab5857) перенесены бандлом и вложены до фикса. СМЕНА УСТРОЙСТВА после 6 кругов эскейп-анализа: src/lib/billing/spendGate.ts теперь отдаёт ТОЛЬКО согласованную поверхность имён (spendGateCall, paidProviderEndpoint, OpenAI, default OpenAI, createOpenAI, createOpenAI-обёртки); чекер отвергает любую другую форму экспорта двери — ESM re-export, CJS exports, Object.assign, globalThis, Symbol, prototype-каналы; импорт SDK провайдера вне двери = нарушение, включая rogue-паттерн круга 6 в разрешённом файле; completion получает createOpenAI через дверь. Приёмка будет отдельной волной (проверка на том же rogue-кейсе, что дал NO-GO кругу 6, + негатив на каждую форму экспорта).
2026-09-05T17:35:28.638Z · fable-coordinator
[2026-09-05 17:40Z] Ревизия доски (волна 2040 boardtriage, M1, luna): статус → parked. Основание: нет активности с 2026-08-29; ACCSPENDGATE6 NO-GO. Если работа жива — верни статус и напиши в тикет, какая волна её ведёт.
2026-09-10T18:55:03.537Z · coordinator
BOARD-WASH-20260910:WAVE-3332
По поручению владельца 18:43Z назначена исполнительская волна 3332 (billing), Codex gpt-5.6-luna xhigh, A1. Полная история карточки и база l115c 1ad52e16b доставлены, brief-guard и проверка Git-базы пройдены. Задача: проверить существующую сдачу, устранить остатки, передать бандл и доказательства. Финальная приёмка и посадка остаются за координатором. Ранее отложенная функция не включается; статус parked/backlog сохранён, идёт проверка технического остатка.
КАРТА ДОКУМЕНТОВ: ноут /Users/annakorin/nc-ops-scripts/board-wash-20260910/3332-wash-billing-brief.md; A1 /home/wave/waves/inputs/board-wash-20260910/billing-tickets.json; ожидаемый отчёт /home/wave/waves/3332WASHBILLING-REPORT.md. Правила обогащения: WEB-449.
2026-09-10T19:29:07.345Z · coordinator
RECEIVED-3332-BILLING:WEB-439:884e072b
Координатор получил и полностью прочитал отчёт 3332. Бандл сохранён локально; SHA256 162bd24a65df39a2f2e008bef0a93c2c11203103b1328cb8938254bc7fc6992f совпал, git bundle verify PASS, HEAD 884e072b88b1c5f942079a3410f67ce939faf5b9, BASE l115c. Ожидаемый DONE отсутствует; это не потеря отчёта/бандла.
Новый код только по WEB-439, частичная миграция. По отчёту native tests 5/5; production-door test и scoped tsc не завершились. Независимая приёмка и посадка НЕ выполнены. Существующие статусы сохраняются; рекомендация исполнителя не отменяет более свежие доказательства координатора (в частности, закрытие WEB-503).
Отчёт: /home/wave/waves/3332WASHBILLING-REPORT.md
Бандл: /home/wave/waves/3332WASHBILLING.bundle
Локальная копия: /Users/annakorin/nc-ops-scripts/board-wash-20260910/

### ДЛЯ ТИКЕТА WEB-439

- `2026-08-29 18:17:25Z`: created as the unified provider-door structural line.
- `2026-09-03 07:18Z`: board triage recorded six earlier circles
  (`a19ae967..7fab5857`) not in production, ACCSPENDGATE6 NO-GO, and assigned
  circle 7 to wave 1512 on l96 for a fixed export surface.
- `2026-09-03 08:28Z`: board comment recorded final circle-7 artifact
  `b952f809` on l96-candidate. Its required surface was `spendGateCall`,
  `paidProviderEndpoint`, `OpenAI`, default OpenAI and factory wrappers; the
  adversarial checker rejects ESM re-export, CJS exports, Object.assign,
  globalThis, Symbol/prototype channels and provider SDK import outside the
  door. `b952f809` is not an ancestor of this BASE, so copying the board claim
  would have been unsound.
- `2026-09-05 17:35Z`: parked by board triage for inactivity; no acceptance
  was recorded for the source BASE used by this wave.
- This wave restored the portable source slice in
  `884e072b...`: canonical accounting module, closed public door surface,
  exact estimate/actual meter seam, mandatory reservation and completion
  factory migration. The two focused checker tests are nonzero and pass.
- **Критерии → факты → остаток:** door contract and completion migration are
  source-complete for this slice; broad structural migration is not complete.
  Existing W3-8 inventory intentionally retains 73 legacy files under declared
  runtime containment; direct SDK caller migration must be a separate scoped
  series, with independent acceptance. Do not mark WEB-439 done from this
  commit alone.
2026-09-13T11:01:35.838Z · coordinator
[13.09 11:01Z координатор] Мойка отложенных, волна 3668. Вердикт: ВЕРНУТЬ-В-РАБОТУ. Жива: slice source-complete, но 73 legacy-файла не мигрированы; нужна независимая приёмка + посадка.
2026-09-13T12:22:59.125Z · coordinator
[13.09 12:22Z координатор] Волна 3690 разобрала путь к закрытию. Цифры честные и с поправкой: в карточке значилось 73 файла мимо единой двери, проверка дерева дала 78, и все 78 живы сегодня. Разница зафиксирована, а не замазана.
План разбит на порции по риску: первая — два статических файла, которые переводятся в бесплатный режим без риска для денег; дальше платные пути; в конце самое опасное. По каждой порции сказано, что меняется, как проверяется и как откатывается.
Главное на будущее: предложена механическая защита — проверка, которая падает, если в дереве появился новый прямой вызов поставщика мимо двери. Без неё список будет отрастать заново.
2026-09-14T15:16:29.949Z · coordinator
[14.09 15:16Z координатор] # WEB-439 - занято, не обогащалось

По вводной задачи WEB-439 сейчас в работе у волн 3865/3866/3868, поэтому я не готовил enrichment-блок и не предлагаю текст для публикации.

Минимальная отметка, проверенная отсюда: live API при финальной сверке показывает статус `in_progress`; в локальном `web-board.sqlite` по WEB-439 видно 7 комментариев, последний `id=4765` от 2026-09-13T12:22:59.125Z.

Ничего на доске не менял.
2026-09-14T15:54:56.512Z · coordinator
[14.09 15:54Z координатор] VERDICT=NO-GO

# WEB-439 — единая дверь к платным провайдерам (spendGateCall): структурная проверка

Волна `3868-web439-spend-door`. База: `refs/waves/l115n` = `d85bb2dad15a3ba52c773b0a2362748009a2c3b9`.
Ветка: `wave/3868-web439-spend-door` = `16e96226598bf4adefd0509dcc4f4fadf1b9eb89`.
Дерево: `/home/wave/waves/wt-3868-spend-door`. Node: `v22.23.2`.

**Почему NO-GO.** Волна свою работу сделала: сторож написан, доказан, один обход закрыт.
Но сам предмет тикета — утверждение «дверь одна» — **опровергнут**. На этом дереве
платных дверей не одна, и найдено **7 файлов платного исходящего трафика мимо двери**
(один закрыт в этой волне, **6 осталось**). Пока они открыты, WEB-439 закрывать нельзя.

---

# ДЛЯ ТИКЕТА — ДЕТСКИМ ЯЗЫКОМ (полный след для нулевого агента)

## 1. Симптом словами пользователя

> «Каждый платный вызов должен идти через одну дверь. Обход двери — это
> неучтённые деньги владельца.»

По-простому. У приложения есть «касса». Когда программа хочет позвонить наружу в
платный сервис (спросить у ИИ, найти в интернете, позвонить по телефону, отправить
письмо), она должна сначала зайти в кассу. Касса делает четыре вещи:

1. **спрашивает разрешение** — а не кончились ли деньги, не нажал ли владелец стоп-кран;
2. **считает, сколько максимум это может стоить** — заранее, до звонка;
3. **записывает в тетрадку, что собирается позвонить** — до того, как байты ушли наружу;
4. **после звонка записывает, сколько реально потратили.**

Если вызов идёт **мимо** кассы — деньги уходят, а в тетрадке пусто. Владелец не
видит траты в своём кабинете, и его стоп-кран на этот вызов не действует.

Вопрос волны был простой: **правда ли касса одна?**
Ответ: **нет.**

## 2. Как нашли — и почему не поймали раньше

### 2.1. Первая неожиданность: двери с именем из тикета вообще нет

Тикет говорит: дверь — это `src/lib/billing/spendGate.ts`, функция `spendGateCall`.
На этом дереве **такого файла нет и такой функции нет**.

```
src/lib/billing/spendGate.ts            ABSENT
src/lib/billing/spendGateAccounting.ts  ABSENT
scripts/check-spendgate-boundary.cjs    ABSENT
```

Вся линия SPENDGATE (7 кругов, `a19ae967..7fab5857`, `b952f809`, и самый свежий
«restore» `884e072b88` от 2026-09-10) **не является предком** нашего HEAD.
Последний живёт на ветке `wash/3332-billing`, а не в `l115n`.

Единственные `spendGateCall`, которые в дереве есть, — это **имена параметров**
внедрения зависимостей в `src/app/api/models/provider-availability/core.ts:37,39`,
и они подключены к `meterVendorCall`.

**Настоящая дверь на этом дереве другая:**
`src/lib/billing/meteredVendorCall.ts:387` — `export async function meterVendorCall<T>(input: MeterVendorCallInput<T>): Promise<T>`

Доказательство: `3868SPEND-evidence/02-spendgatecall-absent-in-l115n.txt`.

> Это важно для нулевого агента: **не ищи `spendGateCall`.** На `l115n` дверь
> называется `meterVendorCall`. Формулировка тикета — это заявка круга 7,
> сделанная на базе `l96`; сюда она не доехала.

### 2.2. Что дверь действительно гарантирует — по коду, не по названию

В `meteredVendorCall.ts` стадии прямо пронумерованы в комментариях:

| стадия | строка | что делает |
|---|---|---|
| 1. identity | 450 | кто платит; подделанный `as TrustedIdentity` падает здесь |
| 2. key provenance | 466 | чей ключ — владельца или пользователя (BYOK) |
| **3. spend guard** | **514** | `evaluateSpendGuard(...)` — **лимит и стоп-кран** |
| **4. ONE estimate** | **542** | потолок цены из прайс-книги; нет цены → отказ |
| 5. request identity | 554 | идемпотентность, чтобы повтор не купил второй раз |
| 6. concurrency | 664 | ограничение параллельности до брони |
| **7. reserve** | **689** | `authorizeAndReserve(...)` — **бронь денег (учёт)** |
| **8. dispatch mark** | **802** | `markDispatched(...)` — **запись до сети**; не записалось → не звоним |
| 9. transport | 862 | собственно звонок провайдеру |
| 10. settle | 970 | списание по факту |

Что из этого следует по-детски:

* **Лимит.** Стадия 3 читает стоп-кран из базы: `spendGuard.ts:516`
  `prisma.spendGate.findMany({ where: { closed: true } })`.
* **Цена.** Стадия 4: если цены нет — `spendGuard.ts:733`
  `if (reservedUsd === null) return rejection('price_missing')`. То есть
  неизвестная цена = **отказ**, а не «бесплатно».
* **Учёт.** Стадии 7/8 пишут бронь и отметку «сейчас позвоню» **до** сети.
* **Outbox.** Если списание не прошло, `spendReservation.ts:2322` пишет строку
  `SETTLEMENT_FAILED` в `SpendSettlementOutbox` (`prisma/schema.prisma:5276`),
  и это **закрывает** `SpendGate`, то есть платный продукт дальше отказывает.
  (Это и есть «закрытый гейт из-за `settlement=failed` в outbox» из памяти проекта.)
* **Что видит владелец.** `spendCabinet.ts` берёт данные только из
  `ledgerEntry`, `tokenUsage`, `spendReservation` — а их пишет **только дверь**.

Доказательство: `3868SPEND-evidence/12-what-the-door-guarantees.txt`.

### 2.3. Почему обходы не поймали раньше — три причины, все структурные

**Причина А. Существующий сторож знает слишком мало провайдеров.**
`scripts/check-ai-import-boundary.cjs` — хороший, живой, зелёный сторож. Но его
словарь — 6 SDK и **10 хостов**. Платный провайдер вне этого словаря для него
просто не существует. Он честно пишет «boundary holds» — и он не врёт, он просто
про других провайдеров не спрашивали.

**Причина Б. Рантайм-охранник тоже знает список, а не понятие «платно».**
`src/lib/billing/paidVendorHosts.ts` — **17 хостов**. `vendorEgressGuard.ts`
перехватывает `fetch`, `node:http(s)`, `undici`, `WebSocket` — механика сильная.
Но решение «это стоит денег?» принимается по списку. Проверил прямо:

```
s.jina.ai          NOT-LISTED
api.telnyx.com     NOT-LISTED
cloud.langfuse.com NOT-LISTED
api.resend.com     NOT-LISTED
api.tavily.com     PAID_LISTED   <- а этот в списке, поэтому он-то защищён
```

**Причина В (главная и самая коварная). Юридическая дверь есть, и её приняли за денежную.**

В дереве есть `src/lib/legal/article28Register.ts` — реестр по GDPR ст.28. У него
есть инвентарь **41 внешнего вызова** и тест, который требует, чтобы каждый вызов
имел «choke point». Но посмотрите на регулярку этого теста
(`__tests__/article28Register.test.ts:319`):

```
/assertArticle28PathEnabled|meterVendorCall|meterZeroCostVendorCall|guardIntegrationConnector/u
```

Это **ИЛИ**. Значит, файл, в котором есть только `assertArticle28PathEnabled`
(разрешение на *данные*), проходит тест — при том что денег он не считает вообще.
Именно так выглядят `jinaGrounding.ts` и `telnyxClient.ts`: юридическая дверь есть,
денежной нет, тест зелёный.

И вторая половина той же причины: `jina` и `telnyx` записаны как `closedProvider`,
и легко подумать «раз closed — значит их и не пускают». **Не так.**
`article28Register.ts:1262`:

```ts
return { providerId, permitted: !enforced, mode, decision, record: decision.record };
```

`permitted: !enforced` — закрытый путь **проходит**, если барьер для этого
провайдера не «взведён». Взводится он переменной `ARTICLE28_RUNTIME_ENFORCEMENT`
(`article28Register.ts:1085`), и **в дереве нет ни одного конфига/шаблона, где она
выставлена** (проверил `*.template`, `*.yaml`, `*.yml`, `*.json`, `*.env*` — ноль
совпадений). Пустое значение → `allArmed = false` → не enforced → **пускаем и лишь
записываем наблюдение**.

Доказательство: `3868SPEND-evidence/06-article28-is-not-a-spend-control.txt`.

**Причина Г, историческая.** Эти поверхности **уже находили и сознательно отложили**.
В дереве лежит `TELSPEND-REPORT.md` (2026-09-01), и там прямым текстом, в таблице
«Найденные обходы и соседние поверхности», со статусом «**не менялось**»:

> `src/lib/factcheck/providers/{tavily,googleCse,azureBing,jinaGrounding}.ts` —
> Web-provider fetch-пути fact-check pipeline — *Generic fact-check surface, не менялось*

То есть волна TELSPEND закрыла телефонные LLM-обходы, аккуратно перечислила соседей
и ограничила себя своим скоупом. Из тех четырёх `tavily` с тех пор починили, а
`googleCse`, `azureBing`, `jinaGrounding` остались. А линия WEB-439 все 7 кругов
занималась **другим вопросом** — «можно ли переэкспортировать OpenAI из двери»
(escape-анализ: re-export, CJS, `Object.assign`, `globalThis`, `Symbol`,
prototype-каналы). Вопрос «а какие провайдеры вообще за дверью?» не задавался ни разу.

## 3. Эволюция моего поиска — с тупиками

1. **Тупик 1.** Пошёл искать `spendGateCall`. Не нашёл определения. Сначала решил,
   что плохо ищу. Проверил историю: файл есть в истории, но
   `git merge-base --is-ancestor` для всех трёх ключевых коммитов → **не предок**.
   Вывод: тикет описывает не это дерево. Переключился на `meterVendorCall`.
2. **Тупик 2.** Запустил существующий `check-ai-import-boundary.cjs` — **зелёный**.
   Соблазн написать «всё хорошо». Прочитал его шапку: «Static-grep level by design»,
   словарь 6 SDK + 10 хостов. Понял: зелёный ответ на другой вопрос.
3. **Ход 3.** Раз словари конечные — надо искать **не по слову «spend»**, а по
   признакам выхода наружу. Сделал полный обход дерева по литералам `http(s)://`
   и `ws(s)://` в `src`, `scripts`, `workers`, `jobs`, `services`, `shared`, `tools`,
   `harness` → **359 различных хостов** (`04-external-host-sweep.txt`). Отфильтровал
   тесты/примеры/бесплатные — остались кандидаты: `api.telnyx.com`, `s.jina.ai`,
   `api.resend.com`, `cloud.langfuse.com`, `messaging.twilio.com`.
4. **Ход 4.** Нашёл в дереве готовый якорь: `ARTICLE28_EXTERNAL_CALL_INVENTORY` —
   41 вызов с файлом и строкой, и он **поддерживается тестом**. Построил матрицу
   «провайдер × есть ли дверь × видит ли рантайм-охранник хост»
   (`05-door-coverage-matrix.txt`).
5. **Тупик 3 (моя ошибка, важная).** Первая версия матрицы показала `ai.google.gemini`
   как «без двери». Проверил файл руками — дверь там **есть**:
   `meterVendorCall<Response>({...})`. Моя регулярка `meterVendorCall\s*\(` не
   учитывала дженерик `<Response>`. Починил регулярку — и та же ошибка тут же
   вскрылась в `fetchJson<AzureBingResponse>(`. **Мораль для сторожа:** если не
   учитывать дженерики, сторож пропустит ровно те обходы, ради которых написан.
   В итоговом стороже дженерики учтены намеренно (см. `TRANSPORT_RE`).
6. **Тупик 4.** Записал `twilio` в обходы. Прочитал `src/lib/runtime-target/twilioOps.ts`
   целиком — там только **чтение/запись настройки** Messaging Service (webhook URL),
   исходящих оплачиваемых отправок в дереве нет. Убрал из обходов, перенёс в «спорное»
   с объяснением. Лучше меньше, да точнее.
7. **Ход 5.** Первая версия сторожа дала **23** находки, из них 11 — мусор
   (прайс-книга, реестр ключей, конфиги — они *называют* хосты как данные).
   Добавил требование: рядом должен быть **реальный транспорт**. Стало **12** находок
   и **ноль** ложных. Проверил отдельно: с пустым списком исключений результат
   тот же — значит фильтр транспорта работает структурно, а не костылём.

## 4. Что сделали — со ссылками на файл и строку

### 4.1. Три списка (главный ответ волны)

**Список 1 — ИДЁТ ЧЕРЕЗ ДВЕРЬ** (платное, дверь на месте; проверено по коду):

| провайдер | файл |
|---|---|
| openai | `src/lib/ai/providers/metered.ts` |
| google (Gemini/video) | `src/lib/videoContext/geminiRoutes.ts:726` |
| openrouter | `src/app/api/models/openrouter/route.ts` |
| perplexity | `src/app/researchActions.ts` |
| tavily | `src/lib/factcheck/providers/tavily.ts:77` |
| elevenlabs | `src/lib/podcast/ttsElevenlabs.ts` |
| google places/routes | `src/lib/ai/tools/locationSearch.ts` |
| **jina (закрыто этой волной)** | `src/lib/factcheck/providers/jinaGrounding.ts:65` |

Плюс ~60 файлов в `src/app/**` и `src/lib/**`, вызывающих `meterVendorCall` /
`meterOpenAIClient` / `meterProviderStream`.

**Список 2 — ИДЁТ МИМО ДВЕРИ** (платное, двери нет; всё подтверждено чтением кода —
у каждого реальный транспорт, ключ владельца и ноль строк учёта):

| # | файл и строка | провайдер | транспорт | почему оба существующих контроля слепы |
|---|---|---|---|---|
| 1 | `src/lib/factcheck/providers/jinaGrounding.ts` | jina | `fetchFactcheckProvider` | **ЗАКРЫТО этой волной** |
| 2 | `src/lib/factcheck/providers/azureBing.ts:65` | azure_bing | `fetchJson<AzureBingResponse>(` | эндпоинт берётся из `AZURE_BING_ENDPOINT` — **литерала хоста в коде нет вообще**, поэтому его не найдёт никакой поиск по хостам |
| 3 | `src/lib/factcheck/providers/googleCse.ts:132,139` | google_cse | `fetchJson<GoogleCseResponse>(` | хост `www.googleapis.com/customsearch` не в списках; **и самого вызова нет в инвентаре ст.28**; вызывает `assertArticle28PathEnabled('google')` — берёт чужую запись (Gemini) |
| 4 | `src/lib/telephony/channels/pstnCall/telnyxClient.ts:130` | telnyx | `fetch` | `api.telnyx.com` не в `paidVendorHosts.ts` и не в `ENDPOINT_PATTERNS` |
| 5 | `src/app/api/webhooks/telnyx/messaging/route.ts:72` | telnyx | `fetch` | **отдельная прямая отправка SMS**, не через `telnyxClient.ts`; в инвентаре ст.28 её нет |
| 6 | `src/lib/emails/transport.ts:64` (`RESEND_API_URL`), транспорт `:519` | resend | `https.request` | `api.resend.com` не в списках |
| 7 | `src/lib/observability/langfuse.ts:59,111` | langfuse | `fetch` | `cloud.langfuse.com` не в списках |

Один чокпоинт `telnyxFetch` (`telnyxClient.ts:106-130`) обслуживает **10 экспортов**,
среди них `purchaseNumber` (`:198` — `POST /number_orders`, **покупка номера**),
`initiateOutboundCall` (`:248` — исходящий звонок, минуты) и `transferCall` (`:293`).
Достигаются из обслуживаемых маршрутов: `src/app/api/webhooks/telnyx/messaging/route.ts:22`,
`src/app/api/pstn/v1/telnyx-outbound-events/route.ts:40`, `src/lib/telephony/runtime/lifecycle.ts:18`,
`src/lib/telephony/outbound/sipPstnAdapter.ts:33`.

**Список 3 — СПОРНОЕ, с объяснением** (не записал в обходы, объясняю почему):

| провайдер | решение | объяснение |
|---|---|---|
| `twilio` | **не обход** | `twilioOps.ts` только читает/пишет конфиг Messaging Service и принимает входящие webhooks. Исходящей оплачиваемой отправки в дереве нет. Если её добавят — станет обходом, и сторож это поймает (хост `messaging.twilio.com` можно внести в `extraPaidEndpoints`). |
| `stripe` | **не обход** | Это приём денег, а не траты. Комиссия удерживается из выручки, её не «тратит» исходящий вызов. Учитывать через дверь расходов — неверная модель. |
| `resend` (#6) | **обход, но другой класс** | Деньги владельца — да. Но это транзакционная почта, а не ИИ-провайдер; у двери нет ни канала, ни estimator-семейства под «письмо». Починка требует продуктового решения (см. «Осталось открытым»). |
| `langfuse` (#7) | **обход, но другой класс** | Платный тариф наблюдаемости, деньги владельца. Но это телеметрия: заводить её под бронь/списание на каждый трейс — вероятно неверно. Нужно решение владельца: мерить или явно объявить «принятый нетарифицируемый расход». |
| `searxng` | **не обход** | Свой хостинг владельца, без ключа. Прямо оговорено в `factcheck/providers/utils.ts:73-81`. |
| `googleFactCheck` | **не обход (но дырка в инвентаре)** | `factchecktools.googleapis.com` — бесплатная квота. Но его, как и `googleCse`, **нет в инвентаре ст.28**, хотя инвентарь заявляет полноту. |
| интеграции (drive/dropbox/notion/slack/github/airtable/onedrive/confluence) | **не обход** | Ходят по OAuth-гранту **пользователя**, на его квоту. Не деньги владельца. |

### 4.2. Для каждого «мимо двери»: цена, видимость, что при исчерпании лимита

> ⚠️ **Цена за вызов — НЕ ИЗМЕРЕНА.** Ни один платный провайдер в этой волне не
> вызывался (это запрещено), и я не беру цифры из головы. Вместо выдуманной цены —
> проверяемый факт из дерева, который для тикета важнее.

**Цена.** В прайс-книге `src/lib/billing/estimatorRegistry.ts`
(`PRICE_BOOK_VERSION = 'w32-pricebook-2'`) на базовом коммите — **ноль упоминаний**
для `jina`, `telnyx`, `resend`, `langfuse`, `azure_bing`, `google_cse`
(для сравнения: `tavily` — 25, `elevenlabs` — 25). Всех per-call цен в книге ровно три:
`openai:web_search`, `perplexity:search`, `tavily:search`.

Значит по-детски: **система не только не считает эти вызовы — она физически не умеет
назвать их потолок.** И это притом что у дерева есть своя написанная политика на
ровно такой случай (`estimatorRegistry.ts:66-72`, про Google Maps):

> «...requires an operator-supplied `NON_TOKEN_PRICE_OVERRIDES` row before enforce
> can admit transport. A missing row is a `price_missing` denial, **never a free or
> guessed call**.»

Правильное поведение для непрайсованного платного вендора — **отказ**. Обход же не
спрашивает цену вообще, поэтому тратит сумму, которую система не может ограничить.
Доказательство: `13-pricebook-coverage.txt`.

**Видит ли владелец расход.** **Нет.** Кабинет владельца (`spendCabinet.ts`) читает
только `ledgerEntry`, `tokenUsage`, `spendReservation`. Эти таблицы пишутся стадиями
7/10 двери. Обход не пишет ни одной строки ни в одну из них → в кабинете **пусто**.
Расход всплывёт только в счёте от провайдера.

**Что произойдёт при исчерпании лимита (или при нажатом стоп-кране).**
**Ничего — вызов пройдёт.** Лимит живёт в стадии 3 (`evaluateSpendGuard`), стоп-кран —
в `spendGuard.ts:516` (`prisma.spendGate` where `closed: true`). Обход не вызывает
`evaluateSpendGuard`, поэтому:

* дневной лимит исчерпан → обход **звонит**;
* владелец закрыл `SpendGate` (аварийный тормоз «без деплоя и SSH») → обход **звонит**;
* сломалось списание, в `SpendSettlementOutbox` лежит `SETTLEMENT_FAILED` и гейт
  закрыт, весь платный продукт отказывает → **эти шесть путей продолжают тратить.**

Последнее — самое неприятное: именно в аварии, когда всё остальное честно
останавливается, обходы продолжают жечь деньги, и не видно, что именно жжёт.

### 4.3. Сторож (задача 5)

**`scripts/check-paid-spend-door.cjs`** (новый, 406 строк, без зависимостей — как и
существующий `check-ai-import-boundary.cjs`, чтобы работал в пустом чекауте).

Пять правил:

* **A.** Каждый провайдер из `ARTICLE28_EXTERNAL_CALL_INVENTORY` обязан быть
  классифицирован в `scripts/paid-spend-door-classification.json` как `ownerPaid`
  или `notOwnerPaid`. **Неклассифицированный провайдер = ошибка, а не «бесплатно».**
  Это и есть защита от нового провайдера: добавил — обязан объяснить.
* **B.** Каждый `ownerPaid` путь обязан дотянуться до символа двери, либо стоять в
  `declaredBypassDebt` с причиной и тикетом (долг виден, а не невидим).
* **C.** Независимо от инвентаря — обход `src/`, `scripts/`, `workers/`, `jobs/` на
  литералы платных эндпоинтов (хосты парсятся **из самого `paidVendorHosts.ts`** +
  `extraPaidEndpoints`). Ловит платный вызов, который добавили **не зарегистрировав**.
* **D.** То же по платным credential-переменным (`paidEnvKeys`). Это ловит провайдеров,
  у которых эндпоинт приходит из окружения и в коде литерала нет вовсе — ровно случай
  `AZURE_BING_ENDPOINT`.
* **E.** Списки ослаблений скреплены хешем (`.lock`), как и `ai-import-allowlist.lock`.

Правила C/D требуют **транспорта в том же файле** (`TRANSPORT_RE`, дженерики учтены).
Поэтому прайс-книга, env-контракт и юридический реестр — которые называют хосты и
имена ключей как **данные** — исключаются структурно, а не растущим списком исключений.
`declaredHostExceptions` в итоге **пуст**: проверил с пустым списком — результат тот же.

**`scripts/__tests__/check-paid-spend-door.test.mjs`** — 7 тестов на `node --test`
(Vitest в проекте нет; `tsx` в дереве нет, поэтому тест намеренно на чистом `.mjs`).
Wired в `package.json`: `check:paid-spend-door`, `test:paid-spend-door`.

## 5. Чем доказано — и ГДЕ ГРАНИЦА

### Доказано (запускал сам, вывод в evidence)

**Сторож падает на нынешнем дереве** (`07-guard-BEFORE-fix-FAIL.txt`, RC=1):
`FAIL — 12 paid path(s) outside the spend door` — 12 записей по **7 файлам**
(файл может сработать и по правилу B, и по C/D).

**Сторож доказан на синтетике** — иначе «упал» ничего не значит.
`10-watchdog-BEFORE-fix.txt`: 5 из 5 механических тестов зелёные —
подложный платный вызов **ловится**; тот же вызов через дверь — **не ловится**;
хост без транспорта — **не вызов**; неклассифицированный провайдер — **падение**;
ослабление без переподписи lock — **падение**. И 2 теста «на живом дереве» — красные.

**Зелёный прогон на исправленном месте** (`09-...jina-scoped-GREEN.txt`, **RC=0**):
```
[paid-spend-door] OK — scope research.jina; 41 inventoried provider path(s),
13 owner-paid provider(s), 22 paid endpoint literal(s) and 8 paid credential(s)
watched, 0 declared debt(s), lock ok.
```

**Полное дерево после починки** (`08-...AFTER-fix...txt`, RC=1): 12 → **10** записей,
`jinaGrounding` исчез из обоих правил. Watchdog: было 5/7, стало **6/7**; тест
`THE TREE: the Jina grounding path goes through the spend door` перешёл в **ok**
(`11-watchdog-AFTER-fix.txt`).

**Регрессий в существующих сторожах нет**: `check-ai-import-boundary.cjs` — зелёный
до и после (`03-...`, `16-...`, RC=0).

> Честная деталь: первая версия моего текста ошибки содержала литерал
> `meterVendorCall({ ... })`, и существующий сторож принял мою строку за настоящий
> вызов двери без `channel`/`method`. Переформулировал; после этого оба сторожа
> сосуществуют чисто.

### ГРАНИЦА — чего я НЕ доказал (важно, не пропускать)

1. **Не запускал ни один платный вызов.** Все выводы о деньгах — из чтения кода и
   из прайс-книги дерева. Утверждение «Telnyx/Jina/Resend стоят денег» — это знание
   о том, как эти продукты продаются, **а не измерение**. Конкретных цен за вызов
   я не привожу: **не измерено**.
2. **Не запускал TypeScript-тесты и не проверял типы.** В ворктри **нет
   `node_modules`** (он в `.gitignore`), TS-тесты проекта идут через
   `node --import tsx`, а `tsx` недоступен; ставить зависимости и собирать на этой
   машине запрещено. Поэтому:
   * **мой патч `jinaGrounding.ts` и `estimatorRegistry.ts` типами НЕ проверен.**
     Это главный риск волны, см. KNOWN ISSUES #1;
   * существующий `src/lib/legal/__tests__/article28RuntimeEnforcement.test.ts` я
     **не прогонял**. Его тесты (`:87` «the flag is disarmed by default…», `:148`
     «NEGATIVE (a): disarmed, an unconfirmed provider **RUNS**…») называю как
     **заявление дерева о себе**, совпадающее с моим чтением
     `article28Register.ts:1262`. Это согласованное чтение двух источников, **но не
     мой прогон**.
3. **Не проверял прод-конфигурацию.** Я доказал, что `ARTICLE28_RUNTIME_ENFORCEMENT`
   не выставлена **ни в одном файле дерева**. Выставлена ли она в проде — за моей
   границей (доступа нет и не должно быть). Если владелец её взведёт на `jina`/`telnyx`,
   эти вызовы будут отказываться по юридическому барьеру — но **всё равно не будут
   учтены**, а остальные обходы (`resend`, `langfuse`, `googleCse` — у последнего
   вообще нет своей записи в реестре) барьер не покрывает.
4. **Сторож — статический grep, как и его сосед.** Он ловит литеральные эндпоинты,
   имена credential-переменных и символы двери. Он **не** поймёт хост, собранный из
   кусков в рантайме, и **не** поймёт платного провайдера, которого нет ни в
   `paidVendorHosts.ts`, ни в `extraPaidEndpoints`, ни в инвентаре ст.28, ни в
   `paidEnvKeys`. Правило A — единственная преграда «совсем нового» провайдера, и
   работает она лишь потому, что инвентарь ст.28 поддерживается тестом. Что этот
   инвентарь **уже не полон** (`googleCse`, `googleFactCheck` в нём отсутствуют) —
   мной проверено и показано; именно поэтому правила C/D существуют.
5. **Классификация `ownerPaid`/`notOwnerPaid` — моё суждение**, а не измерение. Она
   лежит отдельным читаемым файлом с причиной на каждую строку и скреплена хешем
   именно для того, чтобы владелец мог её оспорить построчно.
6. **Обходов может быть больше.** Мой обход дерева — по литералам URL и по инвентарю.
   Провайдер, чей хост целиком в переменной окружения и чья credential-переменная не
   в моём списке, не будет найден ни мной, ни сторожем.

## 6. Что осталось открытым

1. **Шесть обходов не закрыты** (закрыт один из семи). По убыванию риска для денег:
   * `telnyxClient.ts:130` + `webhooks/telnyx/messaging/route.ts:72` — минуты, SMS и
     **покупка номеров**; два независимых чокпоинта, закрывать надо оба;
   * `azureBing.ts:65`, `googleCse.ts:139` — платный поиск, и `googleCse` вообще
     отсутствует в инвентаре ст.28;
   * `emails/transport.ts:519` (resend), `observability/langfuse.ts:111` — деньги владельца,
     но другой класс (см. «спорное»); нужно решение владельца: мерить через дверь или
     объявить принятым расходом.
2. **`jina` теперь будет отказываться с `price_missing`**, пока владелец не задаст
   `NON_TOKEN_PRICE_OVERRIDES="jina:grounding=per_call:<usd>"`. Это **намеренно** и
   следует политике дерева про Google Maps: лучше отказ, чем угаданная цена. Но это
   поведенческое изменение: до патча путь тратил молча, теперь он закрыт до решения
   владельца. Путь и так включается только `JINA_ENABLED` + `JINA_GROUNDING_API_KEY`.
3. **Тест ст.28 стоит ужесточить**: заменить `assertArticle28PathEnabled | meterVendorCall`
   (ИЛИ) на «для `ownerPaid` провайдера юридической двери **недостаточно**». Я это не
   трогал — чужой тест, отдельное решение.
4. **Инвентарь ст.28 не полон** (`googleCse`, `googleFactCheck`). Нужен обратный тест:
   «файл с платным транспортом → обязан быть в инвентаре».
5. **Сторож красный** → в CI его надо включать одновременно с закрытием обходов, либо
   сразу внести шесть остатков в `declaredBypassDebt` с номером тикета. Я **намеренно
   оставил долг пустым**, чтобы волна сдавалась с честным красным, а не с
   самоамнистией.
6. **Типы не проверены** (см. границу #2 и KNOWN ISSUES #1).
7. **Про `spendGateCall`** надо решить на доске: либо переносить линию с
   `wash/3332-billing` в `l115n`, либо признать `meterVendorCall` единственной дверью
   и переписать тело WEB-439. Сейчас тикет описывает дерево, которого нет.

## 7. TROUBLESHOOTER (для следующего агента)

**Как повторить всё, что я сделал.**
```bash
git -C /home/wave/nc-mirror worktree add --detach /tmp/wt refs/waves/l115n
cd /tmp/wt && git checkout -b my-branch
node scripts/check-paid-spend-door.cjs              # RC=1, 10 находок
node scripts/check-paid-spend-door.cjs --json       # то же машинно
node --test scripts/__tests__/check-paid-spend-door.test.mjs   # 6/7, падает «THE TREE»
node scripts/check-ai-import-boundary.cjs           # RC=0, сосед зелёный
```
Ничего собирать не нужно, `node_modules` не нужен.

**«Сторож падает — как понять, я ли виноват?»**
Смотри правило в скобках:
* `[rule B/paid_call_outside_door]` — платный провайдер из инвентаря ст.28 без двери.
  Оберни вызов в `meterVendorCall`, образец — `src/lib/factcheck/providers/tavily.ts:77`
  (или мой `jinaGrounding.ts:65`).
* `[rule C/paid_endpoint_outside_door]` — в файле литерал платного хоста и транспорт,
  двери нет.
* `[rule D/paid_credential_outside_door]` — читается платный ключ и есть транспорт.
* `[rule A/unclassified_provider]` — **ты добавил провайдера и не сказал, платный ли он.**
  Впиши в `scripts/paid-spend-door-classification.json` в `ownerPaid` или
  `notOwnerPaid` **с причиной**.
* `[rule E/lock_stale]` — ты правил классификацию. Это не баг: так и задумано.
  После ревью: `node scripts/check-paid-spend-door.cjs --write-lock`.

**«Я уверен, что мой файл — не вызов, а данные.»** Убери транспорт из файла (правила
C/D тогда молчат сами) или, если транспорт нужен, добавь запись в
`declaredHostExceptions` с причиной и переподпиши lock. Список пуст **намеренно** —
если он начинает расти, это сигнал, что правило надо править, а не список.

**«Сторож ругается на строку в моём сообщении об ошибке / в комментарии.»** Ты
наступил в мои грабли: не пиши в тексте литерал `meterVendorCall(` — соседний
`check-ai-import-boundary.cjs` прочитает это как настоящий вызов двери без
`channel`/`method`. Пиши имя без круглой скобки сразу за ним.

**«Матрица говорит `NO-DOOR`, а дверь в файле есть.»** Почти наверняка дженерик:
`meterVendorCall<Response>({`, `fetchJson<T>(`. Регулярка обязана быть
`имя\s*(?:<[^>]*>)?\s*\(`. Я на этом уже ошибся дважды — см. «Эволюция», п.5.

**«Провайдер `closedProvider` в реестре ст.28 — значит его не пускают?»** **Нет.**
`article28Register.ts:1262` → `permitted: !enforced`. Без
`ARTICLE28_RUNTIME_ENFORCEMENT` закрытый путь **проходит** и лишь пишет наблюдение.
Юридическая дверь ≠ денежная дверь.

**«Не ищи `spendGateCall`.»** На `l115n` его нет. Дверь — `meterVendorCall`,
`src/lib/billing/meteredVendorCall.ts:387`. Файл `spendGate.ts` живёт на ветке
`wash/3332-billing` (`884e072b88`), в `l115n` не влит.

**«Почему `jina` теперь отказывает с `price_missing`?»** Так и должно быть до
решения владельца по цене:
`NON_TOKEN_PRICE_OVERRIDES="jina:grounding=per_call:<usd>"`. Политика дерева —
`estimatorRegistry.ts:66-72`: отказ лучше угаданной цены.

**Как проверить «идёт ли через дверь» руками:**
```bash
grep -nE '\b(meterVendorCall|meterOpenAIClient|meterProviderStream)\b' <файл>
grep -nE 'fetch\s*(<[^>]*>)?\s*\(|fetchJson\s*(<[^>]*>)?\s*\(' <файл>
```
Есть транспорт и нет двери, а провайдер платный — это обход.

---

# KNOWN ISSUES

1. **⚠️ Мой патч не проверен типами.** `src/lib/factcheck/providers/jinaGrounding.ts`
   и `src/lib/billing/estimatorRegistry.ts` изменены без `tsc`: в ворктри нет
   `node_modules`, а сборки на этой машине запрещены. Риски конкретно:
   `meterVendorCall<Response>` + `usageFromResult`, тип `context.identity` в
   `ProviderRequestContext`, три новых экспорта в `estimatorRegistry.ts` и
   расширение union `EstimatorMethod`. **Первое действие приёмки:**
   `tsc -p` на скоуп-конфиге (например `tsconfig.web439-spend-door-scoped.json`)
   плюс `node --import tsx --test src/lib/factcheck/__tests__/...` там, где есть
   `node_modules`.
2. **Сторож красный на дереве (10 находок, 6 файлов) — это ЗАДУМАНО**, так требовала
   задача. Включать в обязательный CI — только вместе с закрытием обходов или после
   внесения остатка в `declaredBypassDebt`.
3. **Поведенческое изменение у `jina`**: до патча тратил молча, теперь до задания
   цены отказывает (`price_missing`). Путь опционален (`JINA_ENABLED` +
   `JINA_GROUNDING_API_KEY`).
4. **Цены за вызов не измерены.** Ни для одного обхода. В отчёте вместо цен —
   покрытие прайс-книги (проверяемый факт дерева).
5. **Поведение ст.28 «disarmed ⇒ закрытый путь проходит» доказано чтением кода**
   (`article28Register.ts:1262`, `:1085`, `:1153`) и совпадает с собственными тестами
   дерева (`article28RuntimeEnforcement.test.ts:87,148`), **но тесты я не прогонял** —
   нужен `tsx`.
6. **Классификация провайдеров — суждение, не измерение.** Особенно спорны `resend`,
   `langfuse` (платные, но не ИИ-класс) и `twilio` (записан как неплатный, потому что
   в дереве только конфиг и входящие webhooks). Требует подтверждения владельца.
7. **Инвентарь ст.28 не полон** (`googleCse`, `googleFactCheck` отсутствуют), хотя
   заявляет полноту. Правило A опирается на него, поэтому правила C/D обязательны.
8. **Правило C по хосту `www.googleapis.com/customsearch`** — подстрока URL, а не
   хост: `www.googleapis.com` обслуживает и бесплатные Google API, и матчить его
   целиком было бы шумно.
9. **`git stash` использовался** для получения чистых «до/после» прогонов одним и тем
   же кодом сторожа. `git stash list` пуст, `git status` чист — проверено.
10. **Ветка не влита никуда** — только бандл. Ворктри `/home/wave/waves/wt-3868-spend-door`
    оставлен на месте для приёмки; снять: `git -C /home/wave/nc-mirror worktree remove`.

---

## Сдача

| артефакт | путь |
|---|---|
| отчёт | `/home/wave/waves/3868SPEND-REPORT.md` |
| бандл | `/home/wave/waves/3868SPEND.bundle` |
| sha256 бандла | `/home/wave/waves/3868SPEND.bundle.sha256` |
| evidence (17 файлов + `SHA256SUMS`) | `/home/wave/waves/3868SPEND-evidence/` |
| ветка | `wave/3868-web439-spend-door` = `16e96226598bf4adefd0509dcc4f4fadf1b9eb89` |
| база | `refs/waves/l115n` = `d85bb2dad15a3ba52c773b0a2362748009a2c3b9` |


---

# ВТОРОЙ ПРОХОД — НЕЗАВИСИМАЯ ПРОВЕРКА (другой агент, всё перезапущено)

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

## A. Что перепроверено и подтвердилось (сам запускал)

| проверка | результат | evidence |
|---|---|---|
| `spendGateCall` не имеет определения в дереве; это только имя DI-параметра, подключённое к `meterVendorCall` (`provider-availability/route.ts:29`) | **подтвердилось** | `19-reverified-door-identity.txt` |
| настоящая дверь — `meteredVendorCall.ts:387` | **подтвердилось** | `19-...` |
| сторож падает на дереве: RC=1, **10** находок по **6** файлам, `jinaGrounding` ушёл | **подтвердилось** | `20-...` |
| `node --test`: 6 из 7 зелёных, красный — только «THE TREE» (намеренно), `jina`-тест зелёный | **подтвердилось** | `21-...` |
| точечный зелёный прогон `--only=research.jina`, RC=0 | **подтвердилось** | `22-...` |
| соседний `check-ai-import-boundary.cjs` зелёный, регрессии нет | **подтвердилось** | `23-...` |
| bundle: sha256 сходится, `bundle verify` ok, head = ветке | **подтвердилось** | `27-bundle-receipt-v2.txt` |
| evidence `SHA256SUMS` — все файлы OK | **подтвердилось** | — |
| `telnyxClient.ts` действительно имеет юридическую дверь и **ноль** денежных символов | **подтвердилось** | `19-...` |
| `article28Register.ts:1262` → `permitted: !enforced` | **подтвердилось** (прочитал строку) | — |

## B. Чего первый проход НЕ доказал, а второй доказал

Красный сторож сам по себе не значит ничего: он может быть красным по глупости.
Нужно было показать, что он **различает**. Второй агент сделал это вживую
(`24-live-injection-and-inverse-proof.txt`):

1. **Подложил новый платный вызов мимо двери** (Tavily через голый `fetch`) →
   находок стало **10 → 11**, и сторож назвал мой файл поимённо.
2. **Тот же самый вызов, но через дверь** (`meterVendorCall<Response>`) →
   осталось **10**, мой файл **не назван**. Ложной тревоги нет.
3. Убрал обе подкладки → снова **10**, `git status` чист.

Это и есть требование п.5 задачи, доказанное на живом дереве, а не только на
синтетике из тестов.

## C. НОВОЕ — изъян, который первый проход не заметил

**Симптом детским языком.** Сторож — это охранник у кассы. Но оказалось, что он
носит с собой **чужой список «этих не проверять»**. Список написал другой охранник
(тот, что следит за импортами ИИ), и написал его для **другого вопроса**: «не утечёт
ли код наружу», а не «не утекут ли деньги». Наш охранник этот список брал молча.

**Что именно.** `scripts/check-paid-spend-door.cjs`, правила C/D, строка
`if (declared.has(relPath) || hostExceptions.has(relPath)) continue;` — где
`declared` собирается из `scripts/ai-import-allowlist.json`. Это **104 файла**,
которые пропускались **невидимо**.

Отчёт при этом заявлял: «`declaredHostExceptions` в итоге **пуст** — проверил с
пустым списком, результат тот же». Формально правда, но читатель делает из этого
вывод «послаблений нет». **Послабления были — просто в чужом файле.**

**Сколько из них реально важны — измерил сам.** Из 104 файлов **13** содержат
литерал платного хоста + настоящий транспорт + **ноль** символов двери, то есть по
собственным правилам сторожа они были бы находками:

| корзина | сколько | есть ли аргумент сдерживания |
|---|---|---|
| `legacy` | 5 | да — `RUNTIME_DENIED` (3), `TOKEN_GATED` (2) |
| `denyRelease1` | 1 | да — posture-gate (`openaiLiveProvider.ts`) |
| **`tooling`** | **7** | **нет никакого** |

**Честный вывод, без раздувания.** Шесть из тринадцати закрыты **реальным другим
контролем** (deny-зона / RUNTIME_DENIED, что подтверждается
`generatedActivationManifest.ts` и зелёным соседним сторожем) — это **не обходы**,
и дополнять ими список «мимо двери» из п.3 было бы неправдой.

Но **семь `tooling`-скриптов** — это скрипты разработчика, которые бьют в
`api.openai.com`, `generativelanguage.googleapis.com` и `openrouter.ai` ключами
владельца, **без двери и без единого аргумента сдерживания**. Они не попадают в
продуктовый рантайм, поэтому это **другой класс**, чем шесть обходов из п.3 — но
это деньги владельца, и до второго прохода они были невидимы вообще ни в одном
списке. Например `scripts/facetime-poc/manual-turn-loop.ts` → `api.openai.com`.

**Почему это важнее, чем кажется.** Первый проход сам описал этот тип провала как
«Причину А» — «существующий сторож знает слишком мало, потому что унаследовал
конечный словарь». И затем **повторил ровно ту же ошибку в своём собственном
стороже**, унаследовав чужой список. Молчаливое послабление — это то, ради поимки
чего волна и затевалась.

## D. Что с этим сделано (коммит `9539419c71`)

Послабление **оставлено** (теми файлами по праву владеет соседний сторож), но
**перестало быть молчаливым**:

* `summary.inheritedExemptions` и `summary.inheritedExemptionsUncontained` —
  в текстовом и в `--json` выводе;
* `inherited[]` в JSON — файл, корзина, аргумент сдерживания, какой платный вендор;
* в тексте — блок `NOTE` со списком всех 13 и строкой
  «7 of these carry no containment argument at all»;
* фраза «If such a file leaves the allowlist, this checker starts reporting it as an
  offender» — то есть связь между двумя сторожами теперь видна, а не подразумевается;
* новый тест №8 «inherited exemptions are disclosed, not silent» — чтобы раскрытие
  не откатилось обратно в тишину.

**Число находок и код возврата НЕ менялись: по-прежнему 10 и RC=1.** Это сделано
намеренно — чтобы все доказательства первого прохода остались верными. Тестов стало
**8**, зелёных **7**, красный по-прежнему один и намеренный.

## E. Граница второго прохода

1. **Платных вызовов по-прежнему ноль.** Цены за вызов — **не измерены**, здесь
   ничего не изменилось.
2. **Типы по-прежнему не проверены** — `node_modules` в ворктри нет, сборки на
   машине запрещены. KNOWN ISSUES #1 остаётся в силе и для моего патча (он на чистом
   CommonJS без типов, но `node --check` проходит, и тесты его гоняют вживую).
3. **«Шесть из тринадцати закрыты deny-зоной» — прочитано, а не исполнено.**
   Основание: `generatedActivationManifest.ts` (`disposition: DENY_RELEASE1`,
   `containment: RUNTIME_DENIED`) плюс зелёный соседний сторож, который требует от
   `denyRelease1`-файла ссылки на posture-gate. Рантайм я не запускал.
4. **Семь `tooling`-скриптов я не чинил.** Они вне продуктового рантайма, и решение
   «мерить ли расход скриптов разработчика» — продуктовое, не моё. Теперь они хотя бы
   **видны** в каждом прогоне.
5. Мой первый собственный обход дерева дал ложную тревогу на `me-bridge/realtime-token`
   (моя регулярка не знала обёртки `meterOpenAIRealtimeBootstrap`). Проверил цепочку:
   обёртка действительно доходит до `meterVendorCall` — **дверь там есть**. Ошибка
   была моя, и она ровно та же, о которой предупреждает «Эволюция», п.5.

## F. Вердикт второго прохода

**VERDICT=NO-GO остаётся.** Причина не изменилась: шесть платных обходов в
продуктовом рантайме открыты. Второй проход добавил к ним **семь невидимых до сих
пор `tooling`-путей** и закрыл дыру в самом стороже. Утверждение тикета «дверь одна»
по-прежнему **опровергнуто**.

| артефакт (обновлено вторым проходом) | значение |
|---|---|
| ветка | `wave/3868-web439-spend-door` = `9539419c714d56c43c7ecd322f080fd0de6010d5` |
| база | `refs/waves/l115n` = `d85bb2dad15a3ba52c773b0a2362748009a2c3b9` |
| сторож | RC=1, 10 находок, 6 файлов, + 13 раскрытых унаследованных послаблений |
| тесты | `node --test`: 8 тестов, 7 зелёных, 1 намеренно красный |
| evidence | 26 файлов + `SHA256SUMS` |
2026-09-14T16:35:37.852Z · coordinator
[14.09 16:35Z координатор] VERDICT=GO

# WEB-439 — закрыты ШЕСТЬ оставшихся обходов платной двери (волна 3887)

База: `refs/waves/l115n` = `d85bb2dad15a3ba52c773b0a2362748009a2c3b9`
Старт: `refs/waves/3868/wave/3868-web439-spend-door` = `9539419c714d56c43c7ecd322f080fd0de6010d5`
Ветка: `wave/3887-web439-close-six` = `7f1e519b22747ad45a1ab5521f418de4e165d94b`
Дерево: `/home/wave/waves/wt-3887-web439`. Node `v22.23.2`.

**Почему GO.** Все шесть обходов закрыты через ту же дверь `meterVendorCall`, тем же
способом, что 3868 закрыла седьмой. Сторож 3868 — тот самый, который она сдала
красным, — теперь **RC=0, ноль находок**, и его собственный тест «THE TREE», который
она намеренно оставила падать, **зелёный**. Регрессий нет: 66 TS-тестов прогнаны на её
базе и на моей ветке одним раннером — вердикты совпали файл-в-файл.

**Что важнее шести мест.** По дороге нашёлся дефект, из-за которого дверь физически
**не могла** пропустить 6 из 12 провайдеров — включая `jina`, который 3868 в дверь
только что завела. Её починка на рантайме бы не сработала. Подробно — §4.

---

# ДЛЯ ТИКЕТА — ДЕТСКИМ ЯЗЫКОМ (полный след для нулевого агента)

## 1. О чём вообще речь

У приложения есть «касса» — одно место, через которое обязан проходить каждый платный
звонок наружу. Касса делает четыре вещи: спрашивает разрешение (не кончились ли деньги,
не нажат ли стоп-кран), считает потолок цены заранее, записывает в тетрадку «сейчас
позвоню» **до** того, как байты ушли, и записывает по факту, сколько потратили.

Если звонок идёт **мимо** кассы — деньги уходят, а в тетрадке пусто. Владелец не видит
трату в своём кабинете, и его стоп-кран на этот звонок не действует.

Волна 3868 проверила утверждение тикета «касса одна» и **опровергла** его: нашла семь
файлов, которые ходят мимо. Один закрыла. Осталось шесть. Эта волна закрыла шесть.

**Важно, не ищи `spendGateCall`.** На этом дереве его нет. Дверь называется
`meterVendorCall`, живёт в `src/lib/billing/meteredVendorCall.ts`. Это установила 3868,
я перепроверил — совпадает.

## 2. Шесть обходов: кто, сколько стоит, видел ли владелец

⚠️ **Про «сколько стоит»: я НЕ измерял.** Ни один платный вызов в этой волне не
делался — это запрещено, и выдумывать цену я не буду. Вместо цены даю проверяемый факт
из дерева, который для тикета важнее: **в прайс-книге дерева для них нет ни одной
строки.** Проверил своей командой (`3887WEB439-evidence/04-price-and-visibility-facts.txt`):
в `src/lib/billing/estimatorRegistry.ts` упоминаний `azure_bing`, `google_cse`,
`telnyx`, `resend`, `langfuse` — **ноль** у каждого (для сравнения `tavily` — 14,
`elevenlabs` — 14). Всех per-call цен в книге было **три**: `openai:web_search`,
`perplexity:search`, `tavily:search`.

То есть система не просто не считала эти вызовы — она **не умела назвать их потолок**.

| # | файл | провайдер | что покупается | цена в прайс-книге | видел ли владелец |
|---|---|---|---|---|---|
| 1 | `src/lib/factcheck/providers/azureBing.ts` | azure_bing | поисковый запрос Bing на ключе владельца | **нет ни строки** | **нет** |
| 2 | `src/lib/factcheck/providers/googleCse.ts` | google_cse | запрос Custom Search | **нет ни строки** | **нет** |
| 3 | `src/lib/telephony/channels/pstnCall/telnyxClient.ts` | telnyx | минуты звонка, перевод звонка (вторая нога), **покупка номера** | **нет ни строки** | **нет** |
| 4 | `src/app/api/webhooks/telnyx/messaging/route.ts` | telnyx | отправка SMS | **нет ни строки** | **нет** |
| 5 | `src/lib/emails/transport.ts` | resend | транзакционное письмо | **нет ни строки** | **нет** |
| 6 | `src/lib/observability/langfuse.ts` | langfuse | событие наблюдаемости (тариф по объёму) | **нет ни строки** | **нет** |

**«Видел ли владелец» — откуда я это знаю.** Кабинет владельца
(`src/lib/billing/spendCabinet.ts`) читает только таблицы `ledgerEntry`, `userBalance`,
`tokenUsage`, `spendReservation`, `dailySpendCounter` — проверил своим grep. Эти строки
пишет **только дверь**. А в каждом из шести файлов — **ноль** символов учёта и ноль
символов двери; проверил по всем шести сразу
(`3887WEB439-evidence/05-six-have-zero-accounting.txt`). Значит в кабинете по ним
пусто, и расход всплывёт только в счёте от провайдера.

**Что было при исчерпании лимита.** Ничего — вызов проходил. Лимит и стоп-кран живут
внутри двери (`evaluateSpendGuard`). Обход в дверь не заходил, поэтому:
дневной лимит исчерпан → обход звонит; владелец закрыл `SpendGate` → обход звонит;
сломалось списание и весь платный продукт отказывает → эти шесть продолжают тратить.

**Юридическая дверь у пяти из шести была — и она не про деньги.** В пяти файлах стоял
`assertArticle28PathEnabled(...)`. Он отвечает на вопрос «можно ли отправлять эти
**данные**», а не «есть ли **деньги**»: он не бронирует, не ограничивает и не
записывает расход. В шестом (`webhooks/telnyx/messaging/route.ts`) не было и его.

## 3. Как закрыл — по каждому

Везде одна и та же форма, скопированная с `jinaGrounding.ts` (как 3868 закрыла седьмой)
и с более старого `tavily.ts`: вызов провайдера уезжает внутрь `run:` у `meterVendorCall`,
рядом объявляются `provider`, `model`, `channel`, `method`, `callsiteId` и `requestArgs`,
из которых считается потолок.

1. **azureBing.ts** — `meterVendorCall<FetchJsonResult<AzureBingResponse>>`. Особый
   случай: адрес берётся целиком из `AZURE_BING_ENDPOINT`, поэтому **литерала хоста в
   коде нет вообще** — его не найдёт никакой поиск по хостам. Дверь — единственный
   контроль, который его видит.
2. **googleCse.ts** — дверь поставлена **после** чтения кэша: попадание в кэш никуда не
   ходит и денег не стоит, значит брони брать не должно. Отдельно: этот вызов брал
   чужую запись ст.28 — `assertArticle28PathEnabled('google')`, то есть запись Gemini, —
   своей у него нет.
3. **telnyxClient.ts** — один чокпоинт обслуживает **десять** экспортов, и они не
   одинаковые. Разметил явно: **платные три** — исходящий звонок, перевод (дозванивается
   **вторая** нога, она тарифицируется отдельно) и покупка номера. **Бесплатных семь** —
   поиск номеров, требования регулятора, привязка, освобождение номера и **hangup**.
   Последние два существуют, чтобы **прекращать** списание, поэтому заворачивать их в
   дверь было бы вредно (см. §6, «цена решения»).
4. **webhooks/telnyx/messaging/route.ts** — здесь был **второй, независимый** `fetch` к
   тому же платному вендору: свой ключ, своя отправка, ни двери, ни ст.28, ни записи в
   инвентаре. Вторая дверь — это не дверь. Удалил транспорт целиком, маршрут теперь
   зовёт единый чокпоинт (`sendSms`).
5. **emails/transport.ts** — платит **владелец**, а не получатель, поэтому плательщик —
   сервисный принципал, а не пользователь: списывать пользователю его же письмо с
   подтверждением было бы неверным счётом.
6. **langfuse.ts** — телеметрия, у неё нет авторизованного пользователя, поэтому тоже
   сервисный принципал. Единица тарификации — **одно событие**, поэтому батч бронирует
   пропорционально своей длине.

**Цену не выдумал ни одной.** Все шесть методов объявлены в прайс-книге, но **ставка
оставлена оператору** — ровно по написанной в этом же дереве политике для Google Maps
(`estimatorRegistry.ts`): «отсутствие строки — это отказ `price_missing`, а не
бесплатный или угаданный вызов». Это проверяется тестом (§5).

## 4. ⚠️ Главное, что нашлось по дороге: дверь не могла пропустить половину провайдеров

Это не про шесть мест, это про механику.

Списков «что стоит денег» было **два**, и оба поддерживались руками:

* `PAID_VENDOR_HOSTS` в `src/lib/billing/paidVendorHosts.ts` — плоский список хостов,
  по которому рантайм-перехватчик решает «этот вызов платный»;
* приватная карта внутри `paidHostsForProvider` в `src/lib/billing/vendorEgressGuard.ts` —
  привязка **провайдер → хосты**, которую дверь применяет в момент отправки.

Они разошлись, и расхождение было **не косметическим**. Оба места, где используется
вторая карта, падают закрыто при пустом ответе:

```
verifyDurableDispatch:                if (allowedHosts.length === 0) return null;   → runAuthorizedVendorDispatch бросает
runAuthorizedZeroCostVendorDispatch:  if (allowedHosts.length === 0) throw ...
```

Значит **провайдер, которого нет во второй карте, не может дойти до вендора через дверь
вообще** — как бы правильно ни был написан его вызов.

Измерил своим инструментом на её ветке и на своей
(`3887WEB439-evidence/06-three-list-divergence-BEFORE-and-AFTER.txt`):

| | БЫЛО (3868) | СТАЛО |
|---|---|---|
| хостов в `PAID_VENDOR_HOSTS` | 17 | 21 |
| ключей-провайдеров в привязке | 10 | 18 |
| `paidHostsForProvider` делегирует одной таблице | **нет, своя копия** | **да** |
| хостов «платный по списку, но привязан ни к кому» | **2** (`api.mistral.ai`, `api.cohere.ai`) | **0** |
| провайдеров, не способных пройти дверь | **6 из 12** | **0 из 12** |

В числе тех шести был **`jina`** — тот самый, который 3868 завела в дверь своим
патчем. **Её починка на рантайме бы не сработала**: вызов дошёл бы до двери и упал
на проверке отправки. Она этого не заметила, потому что TS-тесты в её ворктри не
запускались; это не упрёк — см. §6, я нашёл способ их запустить.

**Как вылечил.** Обе «view» теперь **выводятся из одной таблицы**
`PAID_VENDOR_HOST_BINDINGS`, а `paidHostsForProvider` просто делегирует. Разойтись
больше нечему.

**Но одинаковыми списки делать нельзя, и это важно.** Они отвечают на противоположные
вопросы: первый — «доказывает ли хост сам по себе, что идёт трата» (переполнение
ломает бесплатный трафик), второй — «куда этому провайдеру разрешено сходить внутри уже
авторизованной отправки» (недобор ломает платного провайдера). Поэтому у каждого хоста
есть пометка:

* `exclusive` — весь трафик к хосту платный, хост сам по себе доказательство;
* `shared` — хост несёт и бесплатное: **`www.googleapis.com` — это и Custom Search, и
  Google Drive по OAuth-гранту пользователя**. Пометить его глобально платным — значит
  потребовать платную отправку для каждого обращения к Drive и сломать интеграцию.
  Таких хостов три: `www.googleapis.com`, `cognitiveservices.azure.com`, `api.telnyx.com`.

## 5. Чем доказано — каждый прогон мой

**Красный до и зелёный после — по каждому файлу отдельно, а не суммарно.**
Тем же кодом сторожа: откатывается ровно один файл к состоянию 3868, замеряется,
возвращается, замеряется снова (`3887WEB439-evidence/14-perfile-RED-then-GREEN.txt`):

| # | файл | находок ДО | находок ПОСЛЕ |
|---|---|---|---|
| 1 | `azureBing.ts` | **2** → RED | **0** → GREEN |
| 2 | `googleCse.ts` | **1** → RED | **0** → GREEN |
| 3 | `telnyxClient.ts` | **2** → RED | **0** → GREEN |
| 4 | `webhooks/telnyx/messaging/route.ts` | **1** → RED | **0** → GREEN |
| 5 | `emails/transport.ts` | **2** → RED | **0** → GREEN |
| 6 | `observability/langfuse.ts` | **2** → RED | **0** → GREEN |

Сумма 2+1+2+1+2+2 = **10** — ровно столько находок сторож 3868 показывал на старте
(`02-guard-BASELINE-all-six-RED.txt`, мой прогон, RC=1). После — **0**, RC=0.

**Финальный прогон целиком** (`16-FINAL-verification.txt`):

| проверка | результат |
|---|---|
| `node scripts/check-paid-spend-door.cjs` | **RC=0**, 0 находок |
| `node scripts/check-ai-import-boundary.cjs` (соседний) | **RC=0**, 4628 файлов |
| `node --test scripts/__tests__/check-paid-spend-door.test.mjs` | **8/8**, намеренно красного больше нет |
| `node --test scripts/__tests__/web439-close-six.test.mjs` (новый) | **11/11** |
| TypeScript-разбор 9 изменённых файлов | **9/9 parsed clean** |
| привязка провайдеров | **0 из 12** не могут пройти дверь |
| `git status` | чисто |

**Регрессий нет — проверено сравнением, а не надеждой.** 66 TS-тестов, затрагивающих
изменённые модули, прогнаны **одним раннером на базе 3868 и на моей ветке**
(`09-ts-tests-baseline-vs-mine.txt`): вердикты совпали **файл-в-файл**, 23 pass / 43
fail на обоих. 43 падают из-за отсутствия `node_modules` (`ERR_MODULE_NOT_FOUND` на
`next/server`, `ERR_IMPORT_ATTRIBUTE_MISSING`), а не из-за волны. Среди 23 зелёных —
ровно те, что бьют по моим правкам: `estimatorRegistry.test.ts`,
`vendorEgressGuardProcessGlobal.test.ts`, `egressPolicy.test.ts`, `tavily.test.ts`,
`providerEgressCoverage.test.ts`, `sipPstnAdapter.test.ts`,
`telnyx-outbound-events/route.test.ts`. Перепроверены на финальном коммите: **23/23**
(`17-...`).

**Сторож и тесты доказаны «наоборот».** Красный сам по себе ничего не значит, если не
показать, что он **различает**. Откатил одну починку (`emails/transport.ts`) — новый
набор стал **9/11** с поимённым указанием файла; вернул — снова **11/11**; дерево
чистое (`15-new-test-inverse-proof.txt`).

## 6. ⚠️ ГДЕ ГРАНИЦА — чего я НЕ доказал

1. **Ни одного платного вызова не сделано.** Цен за вызов в отчёте нет — **не измерено**.
   Утверждение «Telnyx/Resend/Langfuse стоят денег» — знание о том, как эти продукты
   продаются, а не измерение. Проверяемый факт — покрытие прайс-книги (§2).
2. **Типы НЕ проверены.** `node_modules` в ворктри нет, сборки на этой машине запрещены.
   Я сделал строго меньше: **синтаксический разбор** каждого изменённого файла
   настоящим парсером TypeScript 5.9.3 (`scripts/web439-ts-syntax-check.cjs` —
   без tsconfig, без разрешения импортов, без emit). Это закрывает KNOWN ISSUES #1
   волны 3868 (её патч не был даже разобран) и **не закрывает** проверку типов.
   Что проверить первым при приёмке: `meterVendorCall<FetchJsonResult<T>>` в двух
   fact-check провайдерах, новое поле `vendorUnits` в `VendorReceipt`, новое семейство
   `vendor_unit` в `EstimatorFamilyId`, `deps.identity` в `EmailTransportDependencies`,
   `identity?` в трёх экспортах `telnyxClient.ts`.
3. **Классификация «платный / бесплатный» — суждение, не измерение.** Особенно разметка
   десяти операций Telnyx (три платных, семь нет). Она записана явным списком с
   причинами в шапке `telnyxClient.ts`, чтобы владелец мог оспорить построчно.
4. **Потолок звонка = 600 секунд взят из дерева, а не из головы** — это его собственный
   `CALL_DEFAULTS.maxDurationSec` в `src/lib/telephony/channels.ts`. Но **фактические
   минуты не сверяются**: звонок тарифицируется до отбоя, а API отвечает сразу, поэтому
   бронь **закрывается по максимуму**. Короткий звонок будет списан как длинный. Это
   консервативно (недобор невозможен), но неточно — см. «осталось открытым» п.1.
5. **Шесть новых callsite-id НЕ зарегистрированы** в `paidEntrypointRegistry.json`.
   Я это выяснил и **намеренно не стал трогать**: тест `w34Activation.test.ts:79`
   требует, чтобы каждая запись реестра присутствовала в **подписанном** Ed25519
   манифесте `generatedActivationManifest.ts`, а подписывать релизный манифест — не мой
   мандат. Следствие, честно: **в режиме enforce эти вызовы будут отказаны** с
   `callsite_unregistered`. Это отказ в безопасную сторону (деньги не уходят), но это
   не «работает». `factcheck.evidence.jina` волны 3868 — **в том же состоянии**, это
   не я создал. См. «осталось открытым» п.2.
6. **Прод-конфигурацию не смотрел** и не должен.
7. **Сторож остаётся статическим grep’ом** — как и его сосед. Он не поймёт хост,
   собранный из кусков в рантайме.

## 7. Задача 3 — проверка, которая ловит исходящий вызов **вообще**, и её цена числом

Словарный сторож слепнет на каждом новом провайдере — это причина А из отчёта 3868,
и шесть закрытых обходов были её проявлением. Поэтому написал
**`scripts/check-any-egress-has-door.cjs`**, который **не знает ни одного вендора,
ни одного хоста, ни одного SDK**. Он спрашивает одно: какие файлы делают исходящий
вызов и какие из них доходят до двери. Всё, что не доходит, должно иметь одну строку
причины в `scripts/egress-ledger.json`; необъявленное — находка.

Цена измерена, не заявлена (`13-dictionary-free-FINAL.txt`), на **дереве ДО починки**:

| | вариант 1: транспорт в самом файле | вариант 2: + один шаг по локальным импортам |
|---|---|---|
| файлов с исходящим вызовом | 351 | 784 |
| из них уже через дверь | 12 | 17 |
| «только свой origin», тратой быть не может | 244 | 304 |
| **помечает (цена ложных срабатываний)** | **95** | **463** |
| **ловит из шести обходов** | **4 / 6** | **6 / 6** |

**Честный вывод.** Вариант 1 дешевле в четыре с лишним раза, но **пропускает
`azureBing.ts` и `googleCse.ts`**: их транспорт спрятан на один шаг, в локальном
хелпере `fetchJson`. Вариант 2 ловит **все шесть без единого имени вендора** — ценой
**463 мест**, каждое из которых требует одной строки объяснения, и подавляющее
большинство из них — бесплатные вызовы.

Отдельно стоит назвать, что именно делает вариант 2 полным: правило «свой origin»
требует **доказательства** — хотя бы один относительный литерал адреса и ни одного
внешнего. Файл, у которого литерала адреса нет вовсе, считается уходящим наружу.
Это не крайний случай: `azureBing.ts` берёт адрес целиком из переменной окружения и
не содержит ни одного URL — **именно поэтому его не мог найти никакой поиск по хостам**.

**Рекомендация.** Не включать как блокирующий CI-гейт сразу. Включить как **реестр
ревью**: 463 записи разбираются один раз, дальше новый провайдер не может стать
невидимым — он может быть только необъявленным, а это состояние, которое видно на
ревью. Реестр уже создан и работает: в нём одна настоящая запись — маршрут
`webhooks/telnyx/messaging/route.ts`, который двери не держит, а делегирует чокпоинту
(вариант 2 честно его помечает, и это правильное поведение).

## 8. Задача 5 — расхождение списков названо и устранено

Дефект назван в §4. Сведено: `PAID_VENDOR_HOSTS` и `paidHostsForProvider` теперь
**выводятся из одной таблицы**; словарь сторожа `check-paid-spend-door.cjs` читает ту
же таблицу (его парсер переписан под новую форму, старая плоская форма по-прежнему
поддерживается — на ней написаны фикстуры watchdog’а). Расхождение «платный по списку,
но привязан ни к кому» — **было 2, стало 0**; провайдеров, не способных пройти дверь, —
**было 6 из 12, стало 0 из 12**.

Третий словарь — `ENDPOINT_PATTERNS` в `check-ai-import-boundary.cjs`, **10 шаблонов**
(пересчитал сам: `api.openai.com, api.anthropic.com, generativelanguage.googleapis.com,
api.x.ai, openrouter.ai/api, api.groq.com, api.perplexity.ai, api.elevenlabs.io,
texttospeech.googleapis.com, speech.googleapis.com`) — **я не трогал**: он отвечает на
другой вопрос (не утечёт ли код наружу), это чужой сторож, и сводить его с денежным
списком было бы ошибкой категории. Именно поэтому существует §7.

## 9. Что осталось открытым

1. **Минуты звонка не сверяются по факту** — бронь закрывается по потолку (граница #4).
   Правильное лечение: отложенное списание по терминальному событию жизненного цикла
   PSTN, как это уже сделано для realtime (`deferSettlementUntilSessionEnd`). Не делал:
   требует правки жизненного цикла звонка, которую нечем проверить без БД.
2. **Зарегистрировать шесть callsite-id** в `paidEntrypointRegistry.json` и
   перегенерировать+переподписать манифест
   (`node scripts/monet-w34-generate-activation-evidence.mjs`), **включая
   `factcheck.evidence.jina` волны 3868**. До этого в enforce-режиме — отказ (граница #5).
3. **Задать цены**, иначе все шесть отказывают с `price_missing` (это задумано):
   `NON_TOKEN_PRICE_OVERRIDES="azure_bing:search=per_call:<usd>,google_cse:search=per_call:<usd>,resend:email=per_call:<usd>,langfuse:ingestion=per_call:<usd>,telnyx:sms=per_call:<usd>,telnyx:call_seconds=per_second:<usd>,telnyx:number_order=per_call:<usd>"`
4. **Сервисные принципалы** для `email-transport`, `observability-langfuse`,
   `telephony-telnyx` должны быть настроены, иначе почта/телеметрия/PSTN отказывают
   (в безопасную сторону).
5. **Самостоятельно хостимый Langfuse** (`LANGFUSE_BASE_URL`) не покрыт привязкой
   хостов — перечислить произвольный адрес оператора нельзя. Записано в самом файле.
6. **Семь `tooling`-скриптов** с ключами владельца и без аргумента сдерживания, которые
   раскрыла 3868, — **не чинил**: они вне продуктового рантайма, решение продуктовое.
   По-прежнему видны в каждом прогоне сторожа.
7. **Тест ст.28 стоит ужесточить**: его регулярка принимает
   `assertArticle28PathEnabled` **ИЛИ** `meterVendorCall`. Для `ownerPaid`-провайдера
   юридической двери недостаточно — но это чужой тест, отдельное решение.
8. **Инвентарь ст.28 не полон** (в нём нет `googleCse`, `googleFactCheck`) — установила
   3868, подтверждаю, не чинил.
9. **`spendGateCall` против `meterVendorCall`** — тикет описывает дерево, которого нет.
   Решение на доске.

## 10. TROUBLESHOOTER

**Повторить всё, что я сделал** (сборка не нужна, `node_modules` не нужен):
```bash
git -C /home/wave/nc-mirror worktree add --detach /tmp/wt wave/3887-web439-close-six
cd /tmp/wt
node scripts/check-paid-spend-door.cjs                      # RC=0, 0 находок
node scripts/check-ai-import-boundary.cjs                   # RC=0, сосед зелёный
node --test scripts/__tests__/check-paid-spend-door.test.mjs   # 8/8
node --test scripts/__tests__/web439-close-six.test.mjs        # 11/11
node scripts/web439-list-divergence.cjs                     # 0 из 12 не могут пройти дверь
node scripts/check-any-egress-has-door.cjs --follow-imports # словарь-независимая проверка
node scripts/web439-ts-syntax-check.cjs src/lib/billing/estimatorRegistry.ts
```

**Запустить настоящие TS-тесты без `node_modules`** — это умеет само дерево, 3868 не
знала:
```bash
node --import ./scripts/ts-node-resolver.mjs --test src/lib/billing/__tests__/estimatorRegistry.test.ts
```
Работает не для всех: тесты, тянущие `next/server` или JSON-импорты, падают
окружением. **Как отличить своё от чужого:** заведи ворктри на базовом коммите и
прогони тот же файл там. Совпал вердикт — не твоё.

**«Сторож зелёный, а я добавил провайдера»** — значит ты его классифицировал. Если
нет, правило A уронит сборку: впиши в `scripts/paid-spend-door-classification.json` в
`ownerPaid` или `notOwnerPaid` **с причиной**, потом `--write-lock`.

**⚠️ «Мой вызов падает с `durable dispatch verification failed`»** — это тот самый
дефект из §4. Проверь: `node scripts/web439-list-divergence.cjs`. Если твой провайдер
в списке `CANNOT DISPATCH` — у него нет привязки хостов. Добавь запись в
`PAID_VENDOR_HOST_BINDINGS` в `src/lib/billing/paidVendorHosts.ts`. **Не** правь
`paidHostsForProvider` в `vendorEgressGuard.ts` — он теперь только делегирует.

**«Добавлять хост как `exclusive` или `shared`?»** `exclusive` — если **весь** трафик к
хосту платный. `shared` — если хост несёт и бесплатное. Ошибка в сторону `exclusive`
ломает бесплатный трафик (потребует платную отправку для каждого запроса);
ошибка в сторону `shared` ослабляет рантайм-обнаружение, но статический сторож
по-прежнему смотрит за хостом через `extraPaidEndpoints`.

**«Мой вызов отказывает с `price_missing`»** — так и задумано, пока владелец не задал
цену (§9 п.3). Политика дерева: отказ лучше угаданной цены.

**⚠️ «Матрица говорит NO-DOOR, а дверь в файле есть»** — почти наверняка дженерик.
3868 предупреждала про `meterVendorCall<Response>(`; я наступил на **вложенный**:
`meterVendorCall<FetchJsonResult<AzureBingResponse>>(`. Регулярка `<[^>]*>` обрывается
на первом `>` и молча не совпадает. Нужно `<[^(]*>`.

**⚠️ «Сторож ругается на строку в моём комментарии»** — те же грабли с двух сторон.
У 3868 символ двери в тексте ошибки приняли за настоящий вызов. У меня соседний
`check-ai-import-boundary.cjs` увидел в **новом сторожe** литерал
`runAuthorized`+`Vendor`+`Dispatch` и посчитал его файлом, ссылающимся на авторизованную
отправку вне шлюза (поймал финальный прогон, RC=1 → починил). Лечение: собирай такие
имена из кусков, не пиши литералом. В тестах — вырезай комментарии перед проверкой
(`codeOnly` в `web439-close-six.test.mjs`).

**«Провайдер `closedProvider` в реестре ст.28 — значит его не пускают?»** **Нет.**
`article28Register.ts` → `permitted: !enforced`. Без `ARTICLE28_RUNTIME_ENFORCEMENT`
закрытый путь **проходит**. Юридическая дверь ≠ денежная.

**Проверить руками, идёт ли файл через дверь:**
```bash
grep -nE '\b(meterVendorCall|meterZeroCostVendorCall|meterOpenAIClient|meterProviderStream)\s*(<[^(]*>)?\s*\(' <файл>
```

---

# KNOWN ISSUES

1. **⚠️ Типы не проверены** — только синтаксический разбор (граница #2). Главный риск
   волны. Первое действие приёмки — `tsc` на скоуп-конфиге там, где есть `node_modules`.
2. **⚠️ Шесть callsite-id не в подписанном манифесте** → в enforce-режиме отказ
   `callsite_unregistered` (граница #5, «осталось открытым» п.2). `jina` волны 3868 —
   в том же состоянии.
3. **Поведенческое изменение: все шесть путей теперь отказывают, пока не задана цена**
   (`price_missing`) и пока не настроены сервисные принципалы. Это намеренно и
   в безопасную сторону, но это изменение поведения, а не «ничего не поменялось».
4. **Звонок списывается по потолку, а не по фактическим минутам** (граница #4).
   Переплата видима и исправима; недобора быть не может. Но потолок быстрее съедает
   дневной лимит владельца — это реальная цена, назвал её явно.
5. **`api.telnyx.com` помечен `shared`, а не `exclusive`** — сознательный компромисс:
   иначе в enforce-режиме сломался бы `hangupCall`, то есть звонки стали бы **дороже**.
   Правильное лечение — зарегистрировать семь control-plane вызовов как `ZERO_COST` и
   поднять хост до `exclusive`.
6. **Единица SMS — сегмент, посчитанный по худшему случаю** (UCS-2, 67 символов на
   сегмент), а не по фактической кодировке. Переоценка, не недооценка.
7. **Классификация десяти операций Telnyx — суждение** (граница #3).
8. **`scripts/egress-ledger.json` — засеян, не полон.** Одна настоящая запись; 463
   места ждут разбора (§7). Это состояние честно печатается при каждом прогоне.
9. **Ветка никуда не влита** — только бандл. Ворктри `/home/wave/waves/wt-3887-web439`
   и `/home/wave/waves/wt-3887-baseline` (мой, для сравнения тестов) оставлены для
   приёмки; снять: `git -C /home/wave/nc-mirror worktree remove <путь>`.

---

## Сдача

| артефакт | путь |
|---|---|
| отчёт | `/home/wave/waves/3887WEB439-REPORT.md` |
| бандл | `/home/wave/waves/3887WEB439.bundle` |
| sha256 бандла | `/home/wave/waves/3887WEB439.bundle.sha256` |
| evidence + `SHA256SUMS` | `/home/wave/waves/3887WEB439-evidence/` |
| ветка | `wave/3887-web439-close-six` = `7f1e519b22747ad45a1ab5521f418de4e165d94b` |
| база | `refs/waves/l115n` = `d85bb2dad15a3ba52c773b0a2362748009a2c3b9` |
| старт | 3868 = `9539419c714d56c43c7ecd322f080fd0de6010d5` |
2026-09-14T17:06:36.046Z · coordinator
[14.09 17:06Z координатор] ⚠️ **Оговорка к отчёту ниже:** его вторая строка «Статус: черновик» — **устаревшая**. Она написана в самом начале по моему же требованию «пиши черновик на диск сразу», и волна её не обновила. Работа завершена: отчёт 16 КБ, evidence, бандл и маркер на месте, KNOWN ISSUES заполнены. Вердикт `NO-GO` настоящий и обоснованный, а не заглушка.

**Что приёмка подтвердила у волны 3887:**
- шесть заявленных обходов закрыты;
- сторож **различает** базу 3868 и ветку 3887 (то есть он правда что-то проверяет);
- две ручные карты платных хостов сведены.

**Почему всё-таки `NO-GO`:**
- ⚠️ **полный независимый обход оставил 457 мест «external-or-dynamic», не классифицированных**. Это ровно тот пункт, который я вписала в бриф как «вылечить причину, а не места»: пока обход идёт по словарю, а не по признаку «вызов наружу», словарь будет слепнуть. 457 — цена честного ответа на этот вопрос;
- новые идентификаторы точек вызова **отсутствуют в подписанном реестре платных входов** — `enforce` может отказать, и откажет безопасно, но фича работать не будет;
- новым vendor-unit вызовам нужны цены оператора: отсутствие цены обязано приводить к отказу, но рабочей функции это не даёт;
- запросы control-plane к Telnyx идут на общий хост, и их отнесение к платным или бесплатным остаётся **суждением, а не рантайм-замером**;
- самостоятельно размещённый Langfuse нельзя исчерпывающе перечислить в фиксированной таблице хостов;
- ⚠️ **`typescript` не разрешается в этом checkout — доказательства синтаксиса и типов нет.** Это уже **вторая** машина за смену, где волна не может проверить, что её правка компилируется (первой была A1). Проблема не в машине, а в том, что в деревьях волн нет зависимостей. Чиню это раскладкой работ.
2026-09-14T17:06:37.214Z · coordinator
[14.09 17:06Z координатор] VERDICT=NO-GO

# 3891-acc-web439 — независимая приёмка волны 3887

Статус: черновик. Финальный verdict будет выставлен после всех независимых обходов и прогонов.

## Что проверено первым

Командой `git -C ~/nc-mirror rev-parse --verify` подтверждены refs:

- 3887: `refs/waves/3887/wave/3887-web439-close-six` → `7f1e519b22747ad45a1ab5521f418de4e165d94b`
- 3868: `refs/waves/3868/wave/3868-web439-spend-door` → `9539419c714d56c43c7ecd322f080fd0de6010d5`
- линия bundle: `refs/waves/l115n` → `d85bb2dad15a3ba52c773b0a2362748009a2c3b9`

Найден авторский отчёт: `~/waves/author-reports/3887WEB439-REPORT.md`.

Рабочее дерево: `~/waves/wt-3891-acc-web439`, ветка `acceptance/3891-acc-web439`.

## Симптом словами пользователя

Нужно проверить, что платные обращения к внешним vendor больше нельзя провести мимо платной двери, и что исправление не сломало поведение базы 3868. Отдельно нужно проверить, что тесты действительно что-то доказывают, а не сравнивают значение само с собой.

## Заявлено и пока не доказано

Заявления 3887 о шести закрытых обходах, зелёном стороже, совпадении тестов, исправлении двух списков платных host-ов и отсутствии тавтологических assertions требуют независимой проверки. Числа и результаты ниже будут добавлены только после собственных команд.

## Граница проверки

Проверяются только локальные refs, локальное рабочее дерево, исходники, тесты и отчёты. Production, прод-БД, SSH, sudo, secrets, deployment, release build, `next build`, full `tsc` и платные провайдеры не используются.

## Открыто

Все содержательные пункты приёмки: независимое обнаружение платного исходящего трафика, сторож на обеих базах, расхождение/сведение карт и fail-closed потребителей, полный заявленный набор TS-тестов на обеих базах, поиск тавтологий, evidence, bundle и troubleshooter.

## Первый собственный результат

На 3887 команда `node scripts/check-paid-spend-door.cjs` завершилась RC=0. Она сообщила 41 инвентаризированный provider path, 13 owner-paid provider paths, 26 paid endpoint literals, 8 watched credentials и 0 declared debts; эти величины относятся к выводу этого запуска, а не к заявлению автора.

На базе 3868 тот же сторож завершился RC=1 и сообщил 10 paid paths outside the spend door. Среди них были `azureBing.ts`, `emails/transport.ts`, `telnyxClient.ts`, `langfuse.ts`, SMS webhook route и `googleCse.ts`. Это собственное подтверждение, что сторож различает базу и ветку.

`node --test scripts/__tests__/check-paid-spend-door.test.mjs` на 3887: 8 тестов прошли, 0 упали, RC=0. На базе 3868: 7 прошли, 1 упал, RC=1; упал именно THE TREE с теми же 10 находками. `node --test scripts/__tests__/web439-close-six.test.mjs` на 3887: 11 прошли, 0 упали, RC=0.

## 1. Независимый обход платного исходящего трафика

Я начал не с таблицы автора, а с поиска транспортов (`fetch`, `fetchJson`, `node:http(s)`, `undici`, `WebSocket`) и owner-vendor credentials/endpoints. Затем проверил каждую из шести distinct-файлов, которые собственный запуск сторожа на базе 3868 назвал нарушителями:

| Файл | Что найдено в текущем дереве | Результат |
|---|---|---|
| `src/lib/factcheck/providers/azureBing.ts:85,88,108` | `meterVendorCall`; `fetchJson` находится в `run` | закрыт структурно |
| `src/lib/factcheck/providers/googleCse.ts:158,161,181` | `meterVendorCall`; `fetchJson` находится в `run` | закрыт структурно |
| `src/lib/telephony/channels/pstnCall/telnyxClient.ts:223-234` | платная ветка `telnyxFetch` вызывает `meterVendorCall`, transport передан в `run`; операции: `purchaseNumber:316-324`, `sendSms:380-389`, `initiateOutboundCall:436-445`, `transferCall:485-494` | закрыты структурно |
| `src/app/api/webhooks/telnyx/messaging/route.ts:22,71,207` | собственного `fetch` нет; SMS и звонок делегируются в `telnyxClient` | закрыт транзитивно |
| `src/lib/emails/transport.ts:609-625` | `requestJson` находится в `run` у `meterVendorCall` | закрыт структурно |
| `src/lib/observability/langfuse.ts:140-151` | `fetch` находится в `run` у `meterVendorCall` | закрыт структурно |

Это доказывает закрытие шести base-нарушителей на уровне исходного дерева. Это не доказывает полный runtime-охват всех возможных динамических адресов: платные вызовы не выполнялись.

Отдельный словарно-независимый аудит `node scripts/check-any-egress-has-door.cjs --follow-imports` завершился RC=1: он нашёл 784 файла с исходящим транспортом, 22 из них с символом двери, 761 необъявленный, из них 457 external-or-dynamic. Такой аудит намеренно не знает, платный вызов или бесплатный, поэтому 457 строк требуют ручного разбора. В частности, он показывает унаследованный `src/app/api/image-models/openrouter/route.ts:29` и realtime/WebSocket-пути вне этой волны. Поэтому заявление «я нашёл все платные места в дереве» в полном смысле не доказано; доказан только набор шести distinct base-нарушителей.

## 2. Сторож на обеих refs

На 3887 `node scripts/check-paid-spend-door.cjs` дал RC=0; собственный вывод: 41 inventoried provider path, 13 owner-paid provider paths, 26 paid endpoint literals, 8 paid credentials и 0 declared debts. `node --test scripts/__tests__/check-paid-spend-door.test.mjs` дал 8 pass, 0 fail, RC=0. `node --test scripts/__tests__/web439-close-six.test.mjs` дал 11 pass, 0 fail, RC=0.

На базе 3868 тот же сторож дал RC=1 и 10 findings в шести distinct-файлах; тот же node-test дал 7 pass, 1 fail, RC=1, причём падает `THE TREE` на этих findings. Это показывает, что сторож различает базу и ветку.

## 3. Проверка двух списков и fail-closed

Собственный скрипт `/Users/limamarty/waves/3891ACC439-measure-lists.cjs` измерил:

- 3868: старая форма, 17 regex в плоском списке, 10 provider keys в приватной карте, 15 distinct bound hosts; `api.cohere.ai` и `api.mistral.ai` были в плоском списке, но не были привязаны ни к одному provider.
- 3887: новая форма, 18 regex в производном `PAID_VENDOR_HOSTS`, 18 provider keys и 24 distinct bound hosts; unbound flat hosts отсутствуют.

На текущем дереве `PAID_VENDOR_HOSTS` выводится из `PAID_VENDOR_HOST_BINDINGS` (`src/lib/billing/paidVendorHosts.ts:141-146`), а `paidVendorHostsForProvider` читает ту же таблицу (`:153-155`). Пустая привязка действительно закрывает путь: `verifyDurableDispatch` возвращает `null` на `vendorEgressGuard.ts:818-819`, а zero-cost consumer бросает `UnauthorizedVendorEgressError` на `:242-249`.

Это исправляет исходное расхождение host-map. Граница важна: provider classification в `scripts/paid-spend-door-classification.json` остаётся отдельным ручным списком. Тест проверяет, что уже записанные `ownerPaid` keys имеют binding, но генератора, который не даст новой записи таблицы и classification разойтись в обе стороны, нет. Поэтому «две карты host-ов больше не разойдутся» доказано; «никакие будущие списки не разойдутся» — нет.

## 4. 66 TS-тестов

Авторский manifest не входит в доступные материалы. Я зафиксировал собственный manifest из 66 TS test files: 49 файлов с прямыми текстовыми ссылками на затронутые модули плюс 17 соседних тестов тех же provider/transport поверхностей. Один и тот же runner запускал каждый файл отдельным `node --import ./scripts/ts-node-resolver.mjs --test` на обеих refs.

Результат на обеих refs был одинаковым: 66 manifest files, 17 file-level pass, 49 file-level fail; внутри 144 node-test cases — 69 pass и 53 fail. Verdict hash обеих file-status карт: `8ec1ce47ec31f5d28a7a30571fe885a0a1baea3b0a30c8bef5375e15cebb5c31`. Это доказывает совпадение моей когорты файл-в-файл и не доказывает, что она совпадает с неизвестным авторским списком. Падения одинаковы на базе и ветке и вызваны средой без `node_modules`, а не считаются зелёными тестами.

## 5. Тавтологии

Поиск одноимённого выражения в `assert.equal/strictEqual/deepEqual/deepStrictEqual` дал 0 строк в моём 66-file manifest и 0 строк в новых watchdog tests. Вне этой когорты найдены pre-existing совпадения, например `src/lib/intelligence/__tests__/entityGraph.test.ts:241`, `src/lib/intelligence/__tests__/changeDetection.test.ts:15` и `src/lib/billing/__tests__/chatRequestIdentity.test.ts:46`; их не приписываю WEB-439.

## 6. Доказательная граница

Не выполнялись платные обращения, production, прод-БД, SSH, sudo, secrets, deployment, release build, `next build` или full `tsc`. TS-syntax checker запущен, но завершился RC=2 с сообщением `typescript is not resolvable; cannot parse`; значит типовую корректность девяти изменённых production-файлов я не доказал.

Также собственным grep по `paidEntrypointRegistry.json` не найдены новые ids `factcheck.evidence.azure_bing`, `factcheck.evidence.google_cse`, `email.transport.resend`, `observability.langfuse.ingestion`, `telephony.telnyx.*`. Registry-код при неизвестном id возвращает `callsite_unregistered` (`src/lib/billing/paidEntrypointRegistry.ts:77-80`). В `estimatorRegistry.ts` для новых методов есть estimator family, но встроенных ставок для новых vendor-unit entries нет; без operator price это fail-closed `price_missing`. Это не обход денег, но означает «путь может быть безопасно отказан», а не «функционально готов к выпуску».

## VERDICT

`NO-GO`: шесть distinct нарушителей действительно закрыты, сторож различает 3868/3887, host-map сведён, а моя 66-file cohort совпала по вердиктам. Но полный независимый поиск оставил 457 external-or-dynamic egress rows без ручного разбора, точный авторский manifest отсутствует, а типы не удалось проверить. Для денежного release-candidate этого недостаточно.

## Что нужно доказать, чтобы снять NO-GO

1. Дать точный author manifest и прогнать его тем же `node --test` runner на обеих refs; сравнить каждый file status.
2. Повторить словарно-независимый список по 457 external-or-dynamic строкам; для каждой записать одну из причин: через spend door, бесплатный/пользовательский грант, same-origin, deny-release posture или явный debt. Любой owner-paid runtime path вне двери блокирует GO.
3. В окружении с уже разрешённым локальным TypeScript-пакетом выполнить syntax parse изменённых файлов; полноценный type-check остаётся отдельным решением и здесь намеренно не запускался.
4. Зарегистрировать новые callsite ids и задать операторские цены/служебные identities, если требуется рабочий, а не только fail-closed, путь. После этого повторить watchdog и 66-file matrix.

## TROUBLESHOOTER

```text
cd /Users/limamarty/waves/wt-3891-acc-web439
node scripts/check-paid-spend-door.cjs
node --test scripts/__tests__/check-paid-spend-door.test.mjs
node --test scripts/__tests__/web439-close-six.test.mjs
node /Users/limamarty/waves/3891ACC439-measure-lists.cjs /Users/limamarty/waves/wt-3891-acc-web439
node /Users/limamarty/waves/3891ACC439-run-tests.cjs /Users/limamarty/waves/wt-3891-acc-web439 /Users/limamarty/waves/3891ACC439-tests.txt --each
```

## KNOWN ISSUES

- Dictionary-free review не закрыт: 457 external-or-dynamic rows требуют классификации.
- Exact author 66-file manifest не приложен; зафиксирован только мой manifest.
- `typescript` не разрешается в текущем checkout, поэтому syntax/type proof отсутствует.
- Новые callsite ids отсутствуют в signed paid-entrypoint registry; enforce может отказать безопасно.
- Новые vendor-unit calls требуют operator prices; отсутствие цены должно отказать, но не даёт рабочую feature.
- Telnyx control-plane requests используют shared host; их billable/free классификация остаётся суждением, не runtime measurement.
- Self-hosted/dynamic Langfuse endpoint не может быть исчерпывающе перечислен в фиксированной host table.
- Pre-existing inherited AI-import exemptions и tooling egress не являются исправлениями этой волны.
2026-09-14T17:22:32.937Z · coordinator
[14.09 17:22Z координатор] VERDICT=NO-GO

# WEB-439: классификация dictionary-free egress

## Для тикета, детским языком

Мы взяли инструмент `scripts/check-any-egress-has-door.cjs` и запустили его из checkout 3887 с обязательным `--follow-imports --json`. Он ищет не названия провайдеров, а сам факт транспорта: `fetch`, WebSocket и похожие выходы. Получилось 457 строк `external-or-dynamic`, RC=1.

Без `--follow-imports` инструмент дал 91 строку. Это не другой итог: это неполный режим, который не видит один локальный слой импорта. Полный режим дал 457.

Все 457 строк теперь записаны в [egress-classification.json](../wt-3896-web439-rows/scripts/egress-classification.json) и проверяются новым `check-egress-classification.cjs`. В каждой строке есть файл, обнаруженный транспорт, транспортный модуль, корзина и признак отнесения.

## Что доказано

Материал acceptance найден, но адрес acceptance ref из задания не существует: `refs/waves/3891/acceptance/3891-acc-web439` не разрешается. Доказуемо существует `refs/heads/acceptance/3891-acc-web439` на коммите `7f1e519b22747ad45a1ab5521f418de4e165d94b`, и 3887 ref также указывает на этот коммит. Worktree создан из `refs/waves/3887/wave/3887-web439-close-six`, затем создана ветка `wave/3896-web439-457-rows`.

Полный собственный прогон сообщил:

- файлов просканировано: 4618;
- файлов с outbound-call после одного hop: 784;
- reach spend door по текстовому признаку: 22;
- declared no owner spend: 1;
- undeclared leaving-machine или dynamic: 457;
- same-origin-only: 304;
- RC: 1, потому что неразобранные строки остались.

Это доказывает размер ledger для данного checkout и режима. Это не доказывает, что все 457 реально исполняются и что каждый из них тарифицируется.

## Корзины, не по хосту

| Корзина | Строк | Признак |
|---|---:|---|
| `paid-external` | 41 | В транспортном модуле есть owner-paid capability: AI/Realtime, Resend, ElevenLabs, Telnyx, Twilio или иной путь с платной семантикой. Для смешанной ветки строка сюда не попала. |
| `free-external` | 350 | Наша инфраструктура, фиксированный бесплатный public API/mirror или telemetry без счёта. Это всё равно явная запись в ledger, чтобы новый transport не исчезал молча. |
| `dynamic-host` | 64 | Адрес или provider приходит из env/config/input либо выбирается runtime; до выполнения host/provider нельзя доказать фиксированным списком. Сюда попали self-hosted Langfuse, OTEL, public URL fetch, configurable storage/video/Ollama и смешанные paid/free ветки. |
| `not-egress` | 2 | Доказанные over-approximation: negative-control fixture и fetch `dataUrl`, не product external call. |

Сумма корзин — 457. В `paid-external` 33 строки находятся под `src/`, 8 — в scripts/tooling. Это статическое доказательство возможности платного вызова, а не доказательство фактического списания.

Особенно опасен `src/lib/embed/liveBridge.ts`: в исходнике прямо отмечен owner-paid upstream WebSocket без reserve/settlement. Это отдельный открытый paid path; его нельзя объявлять бесплатным только потому, что host строится динамически.

## Что делать с dynamic-host

Рекомендация: `fail closed` по умолчанию плюс обязательная явная декларация capability, но декларация сама по себе не даёт разрешения.

Рабочее правило должно быть таким:

1. runtime target/provider объявляет тип capability, владельца и допустимый origin/policy;
2. paid capability обязана пройти spend door до transport;
3. free/our-infra capability получает отдельное подтверждение «без счёта» и ограничения origin;
4. неизвестный или неподтверждённый dynamic target получает отказ до отправки байта;
5. dictionary-free scan и stale-check запускаются в CI и требуют записи для каждой новой строки.

Почему не только запретить всё: это безопасно для денег, но сломает legitimate self-hosted/free integrations и потребует аварийных исключений. Почему не только требовать declaration: декларация может стать rubber stamp и не доказывает metering. Runtime capability policy даёт границу между этими режимами, но требует разовой миграции и тестов.

## Цена перехода со словаря на признак

Из 457 строк разово нужно явно учесть 455 строк: 41 paid, 350 free и 64 dynamic. Ещё 2 строки нужно оформить как false-positive suppression с объяснением. Сырой dictionary-free сторож без typed ledger показывает 457 finding, из которых 2 доказаны как ложные срабатывания; после stale-check он должен показывать 0 неразобранных строк, но не должен молча разрешать новые.

В этой реализации разовая работа измерена так: 457 row records, 4 новых repository-файла (builder, checker, manifest и node-test) и 1 тестовый сценарий. Manifest занимает 4151 строку. Это review ledger, не подмена runtime spend door.

## Что не даст двум спискам разойтись

Старое расхождение host-map исправлено в 3887: `PAID_VENDOR_HOSTS` и `paidVendorHostsForProvider` читают одну `PAID_VENDOR_HOST_BINDINGS` table. Собственный divergence-check сообщил 21 flat pattern, 18 provider keys, 24 distinct bound hosts, 0 unbound flat hosts и 3 deliberately shared hosts. Это доказывает отсутствие текущего расхождения двух host-view; shared hosts всё ещё требуют смысловой классификации.

Для новых 457 строк добавлен второй барьер: `check-egress-classification.cjs` заново запускает dictionary-free scanner и сравнивает актуальные файлы с manifest. Он падает при новой строке без записи, stale row, duplicate row, неизвестной корзине или неверных counts. Новый `node --test` проверяет этот контракт. Поэтому scanner и review list больше не могут разойтись тихо; человеческая ошибка в выборе корзины всё ещё возможна и должна закрываться review/evidence.

Открытая граница: этот guard проверяет полноту списка, не истинность коммерческого договора провайдера и не фактический runtime account. Для этого нужны fake-fetch/runtime tests по каждому paid и dynamic capability, без платных сетевых обращений.

## Тесты и окружение

Разрешённые прогоны были только через `node --test` для тестов:

- новый classification test: 1 pass, 0 fail, RC=0;
- `check-paid-spend-door.test.mjs`: 8 pass, 0 fail, RC=0;
- `web439-close-six.test.mjs`: 11 pass, 0 fail, RC=0.

Отдельный `check-paid-spend-door.cjs` завершился RC=0, но сообщил 13 inherited AI-import exemptions, из которых 7 tooling-файлов не имеют containment argument. Это не принимается за доказательство полной безопасности 41 paid row.

`typescript` в дереве не разрешается: локальный probe завершился RC=2 с `typescript is not resolvable; cannot parse`. Полный `tsc` не запускался и не требуется для этого отчёта; type/syntax proof изменённых TypeScript-файлов отсутствует. Это та же известная проблема окружения, а не основание требовать невозможный прогон.

Не использовались production, прод-БД, SSH, sudo, secrets, платные провайдеры, deployment, release build, `next build` или full `tsc`.

## Что осталось доказать до GO

1. Разобрать 41 paid-capability row на runtime entrypoints и подтвердить door-before-transport, особенно `embed/liveBridge`, realtime/live providers и tooling, если их запуск вообще разрешён.
2. Для всех 64 dynamic rows зафиксировать runtime source, policy owner, разрешённый origin и fail-closed test. Self-hosted Langfuse нельзя закрыть добавлением ещё одного host в таблицу.
3. Прогнать fake-fetch tests для каждой capability без реальной сети; тест должен показывать, что неизвестный target не отправляет байт.
4. В среде с разрешённым `typescript` повторить syntax/type proof. Нельзя считать текущую TypeScript-корректность доказанной.
5. Оставить classification checker обязательным в CI рядом с dictionary-free scan; ручное обновление только host dictionary проблему не решает.

Пока эти пункты не выполнены, классификация 457 строк готова, но денежная граница не доказана. Поэтому verdict — `NO-GO`.

## Evidence

- [report](3896WEB439ROWS-REPORT.md)
- [git bundle](3896WEB439ROWS.bundle)
- [bundle SHA256](3896WEB439ROWS.bundle.sha256)
- [evidence directory](/Users/limamarty/waves/3896WEB439ROWS-evidence)
- [raw dictionary-free JSON](3896WEB439ROWS-evidence/01-egress-raw.json)
- [classification build](3896WEB439ROWS-evidence/02-classification-build.txt)
- [classification check](3896WEB439ROWS-evidence/03-classification-check.txt)
- [paid-door output](3896WEB439ROWS-evidence/04-paid-door.txt)
- [host-list divergence](3896WEB439ROWS-evidence/05-list-divergence.txt)
- [classification ledger by bucket](3896WEB439ROWS-evidence/10-classification-ledger.txt)
- [paid boundary evidence](3896WEB439ROWS-evidence/11-paid-boundary.txt)

## KNOWN ISSUES

- Acceptance ref из задания с путём `refs/waves/3891/acceptance/3891-acc-web439` отсутствует; использован существующий `refs/heads/acceptance/3891-acc-web439` на том же commit.
- Manifest классифицирует scanner rows/one-hop paths, а не доказывает, что каждый caller реально исполняет transport.
- `paid-external=41` — статическая capability-классификация; она не является runtime charge measurement.
- `dynamic-host=64` нельзя безопасно превратить в fixed host allowlist; нужен runtime capability policy.
- `src/lib/embed/liveBridge.ts` содержит owner-paid upstream WebSocket без доказанного reserve/settlement.
- Existing paid-door checker зелёный с inherited AI-import exemptions; 7 tooling exemptions не имеют containment argument.
- `typescript` не разрешается; типовая и синтаксическая проверка TS не доказана.
- Production, прод-БД, SSH, sudo, secrets, paid network calls, deployment, release build, `next build` и full `tsc` не использовались.
2026-09-14T17:40:15.069Z · coordinator
[14.09 17:40Z координатор] VERDICT=GO

# WEB-439 — `liveBridge.ts`: платный канал без резервирования и списания

## Для тикета: детским языком

Болит вот что: embed-виджет умеет попросить сервер открыть долгий WebSocket к
OpenAI Realtime. В исходнике он сам назван `owner-paid upstream WebSocket with
no reserve and no settlement`. Значит, расход владельца мог возникнуть после
проверки, которая только решает, можно ли начинать вызов, но не ставит деньги в
резерв и не закрывает расчёт при конце сессии.

Чем доказано: `src/lib/embed/liveBridge.ts:445-452` строит
`wss://api.openai.com/v1/realtime` с серверным ключом OpenAI; цепочка вызова
подтверждена через API route, сервис, browser WebSocket и bridge handler. В
классификации исходной волны строка этого файла помечена `paid-external` по
признаку `WebSocket`, а не только по имени host.

Граница: в текущем дереве export bridge не смонтирован найденным серверным
host-ом. Поэтому непосредственно собранный из этого дерева runtime-путь сейчас
мёртв, но кодовый путь жив и остаётся опасным при внешнем или будущем mount.
Починка этой волны не включает полную тарификацию сессии: она закрывает путь
до тех пор, пока reserve/settlement не будут связаны с его жизненным циклом.

Открыто: отдельный дизайн и реализация учёта для long-lived Realtime-сессии;
проверка других paid-external телеметрийных WebSocket-путей; решение по mount.
См. troubleshooter ниже.

## Что проверено

Проверены refs до начала работы:

- `refs/waves/3896/wave/3896-web439-457-rows` =
  `11c333cacb3e24622b9f884a07e9782e4f626b6c`;
- `refs/waves/3887/wave/3887-web439-close-six` =
  `7f1e519b22747ad45a1ab5521f418de4e165d94b`.

Дерево принято в `/Users/limamarty/waves/wt-3900-livebridge` на именованную
ветку `wave/3900-livebridge-paid-path`.

### Путь и его живость

Доказанная исходная цепочка:

1. `src/app/api/embed/realtime/route.ts:48-69` вызывает
   `createEmbedVoiceSession`.
2. `src/lib/embed/service.ts:2277-2380` для OpenAI возвращает
   `mode=server-live`, `transport=server-bridge-websocket` и путь bridge.
3. `src/components/embed/EmbedWidgetShell.tsx:1136-1156` создаёт браузерный
   WebSocket и отправляет auth/session token.
4. `src/lib/embed/liveBridge.ts:383-452` принимает OpenAI и создаёт upstream
   WebSocket.

Прямое доказательство платности находится в комментарии
`src/lib/embed/liveBridge.ts:394-399`: там явно написано
`owner-paid upstream WebSocket with no reserve and no settlement`. Затем
`src/lib/embed/liveBridge.ts:445-452` выполняется `new WebSocket` к OpenAI.

Проверка mount через поиск импорта/export показала: найден export
`getEmbedRealtimeBridgeServer` только в `liveBridge.ts`; вызова его из серверного
host в этом дереве не найдено. `infra/prod/server-with-realtime.js` подключает
`src/lib/realtime/standaloneEntry.ts`, который поднимает Socket.IO через
`getRealtimeServer(server)`, а не embed bridge. Вывод: текущий runtime из этого
дерева не достигает upgrade listener; это не оправдание для оставления
неучтённого кода открытым.

### Что именно тратится и где это видно

Upstream — OpenAI Realtime по `wss://api.openai.com/v1/realtime`; модель берётся
из descriptor. В локальном registry default для `openai:gpt-realtime-mini` —
`apiModel=gpt-realtime-mini` (`src/lib/realtime/realtimeModels.ts:86-100`).
Официальные документы OpenAI, проверенные для этого отчёта, описывают Realtime
как token-based тарификацию с WebSocket, а не плату за сам факт открытия
соединения. Точный актуальный audio-тариф для legacy alias в дереве не зафиксирован;
число за минуту не придумывается.

В локальном коде старый путь не виден в owner spend ledger: вокруг upstream
socket нет `meterVendorCall`, `authorizeAndReserve`, `markDispatched`,
`settleReservation`, `settleMax` или session settlement helper. Поэтому владелец
может увидеть расход во внешнем аккаунте провайдера, но не в локальном резерве и
кабинете. Production account и production DB не открывались.

Локальная модель, к которой нужно будет привязать исправление, уже ожидает:
reserve до dispatch, TTL на максимальную сессию с запасом и settlement по
наблюдаемому ограниченному usage. Это видно в
`src/lib/realtime/server/meteredRealtimeBootstrap.ts:30-54` и в realtime
estimator/settlement (`src/lib/billing/estimatorRegistry.ts:1324-1392`).

### Почему дверь не поймала WebSocket

`meterVendorCall` оборачивает конечный callback: reserve делается перед
запросом, а settlement — когда callback вернул результат или ошибку. В
`liveBridge.ts` `createProviderSocket` ждёт только событие `open`
(`:466-479`), возвращает уже открытый socket (`:486-489`), а аудио и события
продолжают идти после возврата функции. Поэтому reserve, связанный с
request-response promise, не имеет правильного terminal event для сессии.

Для сессии нужны: bounded max duration/usage и reserve до credential/socket;
идемпотентная привязка provider session id после открытия; terminal settlement
на clean close, provider close, client error, setup failure и watchdog expiry;
обработка неоднозначного сбоя как pending repair или bounded maximum, но не
нулевой расход. При превышении cap надо прекратить приём аудио, закрыть обе
стороны и провести terminal settlement.

Варианты и цена каждого:

- Полный reserve максимума, затем actual bounded usage: защищает от недорезерва,
  но требует надёжного usage contract и durable linkage; секунды сами по себе
  лишь proxy для token billing.
- Короткие time slices с renew: меньше временный hold, но нужны fencing,
  idempotency и crash reaper; гонка renewal может оставить socket без резерва.
- Отказ до полной реализации учёта: безопасно и однозначно, но функция
  недоступна. Это выбранное исправление этой волны.
- Списывать полный максимум на каждый disconnect: просто и консервативно, но
  короткие сессии будут переплачивать; durable terminal write всё равно нужен.

## Где ещё искать такие соединения

Файл `scripts/egress-classification.json` был прочитан и распарсен: в нём
`457` строк, из них `64` с `classification=dynamic-host` и `33` уникальных
transport modules. Среди этих `64` только
`src/lib/realtime/browserRoomAudioTransport.ts` имеет маркер WebSocket/socket;
это browser-side Socket.IO/Engine.IO transport к собственному серверу, а не
аналог owner-paid provider WebSocket.

Отдельно найдены paid-external long-lived поверхности вне этой корзины:

- `src/lib/realtime/server/openaiRealtimeSession.ts` через `/api/realtime`;
- `src/lib/telephony/live/providers/openaiLiveProvider.ts`;
- `src/lib/telephony/live/providers/googleLiveProvider.ts`;
- spike/proof harnesses в `src/lib/telephony/spikes` и `scripts`.

Они не объявляются закрытыми этим тикетом. В частности, Google telephony
bootstrap уже имеет accounting-related code, но server-side socket lifecycle не
связан здесь с settlement bootstrap; OpenAI telephony provider имеет отдельную
posture-логику. Нужна отдельная волна с тем же вопросом транспорта и lifecycle.

## Сделанная правка

В `src/lib/embed/liveBridge.ts` добавлен fail-closed guard после posture check и
до credential resolution и `new WebSocket`: при `posture.posture ===
'legacy_premint'` выбрасывается `EmbedConfigError` с HTTP `503` и сообщением
`Embed live voice is unavailable until session billing is enabled.`

Это не изображает reserve/settlement: путь не открывает платный upstream, пока
полный session accounting не будет реализован. Deny остаётся до чтения ключа.

Добавлен тест
`scripts/__tests__/web439-livebridge-paid-path.test.mjs`, который проверяет:

- guard находится до provider dispatch;
- отрицательный контроль ломается, если убрать старый legacy guard;
- provider close и client close входят в общий teardown path.

Запущено:

`node --test scripts/__tests__/web439-livebridge-paid-path.test.mjs`

Результат: `3` теста прошли, `0` провалились, `0` отменены. Также
`git diff --check` завершился без вывода.

## KNOWN ISSUES

- Полный reserve/settlement для embed live session не реализован; вместо этого
  включение legacy-пути запрещено. Это сознательная граница, а не доказательство
  бесплатности.
- В текущем дереве не найден server mount embed bridge. Если внешний host его
  монтирует, новый guard должен сохраняться; mount и его lifecycle надо отдельно
  покрыть интеграционным тестом.
- Другие paid-external telephony/realtime WebSocket-пути не входят в этот
  patch; см. отдельный список выше.
- Точный тариф legacy model/audio alias не зафиксирован локально; paid call для
  его измерения не выполнялся.
- Production, prod DB, SSH, secrets, deployment, release build, `next build`,
  full `tsc`, платные провайдерские вызовы и чужие worktrees/processes не
  использовались.

## Troubleshooter

Если embed live voice возвращает HTTP `503` с сообщением про session billing,
это ожидаемый fail-closed результат этой волны: не пытаться обходить его
legacy-переменной или вручную выдавать upstream key. Для включения следующей
волны нужны bounded cap, durable reserve до dispatch, provider/session linkage,
идемпотентный terminal settlement на всех close/error путях, watchdog/reaper и
тест превышения лимита.

Приёмочные материалы и исходные проверки сохранены в каталоге
[`3900LIVEBRIDGE-evidence`](/Users/limamarty/waves/3900LIVEBRIDGE-evidence/):
цепочка, стоимость, дизайн, dynamic-host review, verification и список входных
refs/reports.

Reports, найденные в `~/waves/author-reports/`: `3855WEB057RE`, `3856WEB478WA`,
`3858WEB420IN`, `3860WEB573DR`, `3861WEB467PR`, `3868SPEND`, `3874WEB580FIX`,
`3876WEB575`, `3880WEB573FIX`, `3881ACC580`, `3885WEB580TG`, `3887WEB439`.
Отчёта `3896WEB439-REPORT.md` при проверке не было; этот файл — отчёт текущей
волны.

Финальный verdict `GO`: commit, named-branch bundle, SHA256SUMS и evidence
проверены. `GO` здесь означает, что неучтённый embed upstream закрыт и
доказан; это не означает, что платная функция уже включена.
2026-09-15T01:25:00.607Z · coordinator
[15.09 01:24Z координатор] ## 15.09 01:40Z — ВОШЁЛ В ПОСАДКУ l115o. Требуется пост-QA на проде.

Прод работает на `l115o-68e25d8d` (коммит `68e25d8df5ae8263c5ac5466353631f57a17cfcc`, артефакт `cd2773f65d66b09ec03d39b553041d2814ce09413e72d72b07bc9de89df536c8`) с 00:56Z 15.09. Посадка проверена: `ready=true`, коммит совпал, браузерная проба открыла документ, 0 новых ошибок.

**Работу по этому тикету принесли волны:** 3887.

**Что это значит для этого тикета.** Работа по нему пролежала принятой, но НЕ посаженной — в некоторых случаях неделями. Теперь она на проде. Приёмка волной доказывала, что код правильный в дереве волны; она НЕ доказывала, что фича работает на живом проде. Это разные вещи, и мы на этом уже обжигались.

**Поэтому статус — `review`, а не `done`.** Закрыть тикет имеет право только пост-QA, который проверит поведение на работающем проде и приложит доказательство. До тех пор «сделано» — это заявка.

### Для нулевого агента (тот, кто будет делать пост-QA)

Ты приходишь на этот тикет без нашей истории. Что надо знать:

1. **Проверяй на проде, не на стенде.** Стенд сейчас вообще не поднят — у него не было своего артефакта, он запускался из каталога боевого релиза и заблокировал проверку посадки; пересобирается волной 3948 (WEB-662).
2. **`/api/health` ничего не доказывает** — он проходит и на пустом приложении. Нужно деловое действие: аутентифицированная сессия делает то, про что этот тикет.
3. **Отсутствие ошибок в логе — не доказательство жизни.** Нужен положительный результат, а не тишина.
4. **Числа в обосновании закрытия получай командой в момент закрытия.** Я однажды закрыла тикет числом `4255`, взятым из `pg_stat_user_tables.n_live_tup` — это ОЦЕНКА. Настоящее `count(*)` дало `107 817`. Разница в 25 раз.
5. **Не верь имени волны в теме коммита** — проверяй наличие содержимого (`git cherry`), а не упоминание номера.
6. Запускатель тестов в проекте — `node --test`. Vitest нет.

2026-09-15T22:04:50.043Z · coordinator
ДЛЯ ТИКЕТА WEB-439 - сводка под нулевого агента (составлено 2026-09-15, read-only)

Симптом. Каждый платный вызов обязан идти через одну дверь; обход двери - это тихий расход владельца в обход учёта.

Отмена в теле. В теле записано "73 legacy-файла мимо единой двери". Волна 3690 (2026-09-13T12:22:59.125Z) проверила дерево: файлов 78, и все 78 живы на тот день. Разница зафиксирована, а не замазана. Остаток в теле ("волна - завершить миграцию, независимая приёмка, посадка") не называет ни исполнителя, ни первого шага - это и есть главный пробел карточки.

Что доказано и чем (по волнам):
- 2026-09-10T18:55:03.537Z - мойка 3332 (billing), база l115c 1ad52e16b; сдача /home/wave/waves/3332WASHBILLING-REPORT.md, бандл 3332WASHBILLING.bundle SHA256 162bd24a65df39a2f2e008bef0a93c2c11203103b1328cb8938254bc7fc6992f, HEAD 884e072b..., native tests 5/5, production-door test и scoped tsc не завершились.
- 2026-09-13T11:01:35.838Z - мойка 3668: ВЕРНУТЬ-В-РАБОТУ.
- 2026-09-14T15:54:56.512Z - 3868, NO-GO. База refs/waves/l115n = d85bb2dad15a..., ветка wave/3868-web439-spend-door = 16e96226598bf4adefd0509dcc4f4fadf1b9eb89, дерево /home/wave/waves/wt-3868-spend-door, Node v22.23.2. Предмет тикета опровергнут: платных дверей не одна - найдено 7 файлов платного исходящего трафика мимо двери (один закрыт, шесть осталось).
- 2026-09-14T16:35:37.852Z - 3887, GO. Старт refs/waves/3868/wave/3868-web439-spend-door = 9539419c..., ветка wave/3887-web439-close-six = 7f1e519b22747ad45a1ab5521f418de4e165d94b, дерево /home/wave/waves/wt-3887-web439. Все шесть закрыты через ту же дверь meterVendorCall; сторож 3868 теперь RC=0, ноль находок; 66 TS-тестов прогнаны на обеих базах одним раннером, вердикты совпали файл-в-файл.
- 2026-09-14T17:06:36.046Z и 17:06:37.214Z - 3891, NO-GO. Оговорка: вторая строка отчёта "Статус: черновик" устаревшая, вердикт настоящий. Сторож различает базы 3868 и 3887. Полный независимый обход оставил 457 мест "external-or-dynamic" неклассифицированными - "пока обход идёт по словарю, а не по признаку "вызов наружу", словарь будет слепнуть".
- 2026-09-14T17:22:32.937Z - NO-GO, классификация без словаря. Инструмент scripts/check-any-egress-has-door.cjs с --follow-imports --json дал 457 строк external-or-dynamic, RC=1; без флага - 91 строку. Все 457 записаны в scripts/egress-classification.json и проверяются check-egress-classification.cjs.
- 2026-09-14T17:40:15.069Z - GO по liveBridge.ts. src/lib/embed/liveBridge.ts:445-452 строит wss://api.openai.com/v1/realtime серверным ключом OpenAI; в исходнике канал сам назван owner-paid upstream WebSocket with no reserve and no settlement. Граница: в текущем дереве export bridge не смонтирован.

Остаток и первый шаг (чего в карточке нет). (1) Решить и записать, что делать с 457 неклассифицированными строками - это и есть "вылечить причину, а не места"; без этого закрывать нельзя. (2) Прогнать check-any-egress-has-door.cjs --follow-imports --json на текущей линии и зафиксировать число строк и RC командой в момент проверки. (3) Поставить сторожа 3690 (падение на новом прямом вызове поставщика) в CI или в посадку.

Карта документов. Отчёт 3332 /home/wave/waves/3332WASHBILLING-REPORT.md; бандл /home/wave/waves/3332WASHBILLING.bundle; деревья /home/wave/waves/wt-3868-spend-door, /home/wave/waves/wt-3887-web439; классификация scripts/egress-classification.json; проверяльщики scripts/check-any-egress-has-door.cjs, scripts/check-egress-classification.cjs.
2026-09-19T15:58:36.780Z · coordinator
[POST-QA 4211 AUDIT COMPLETE 19.09]

Fresh read-only DeepSeek Pro audit 4211 completed from the full immutable body+comment snapshot. Output SHA256SUMS readback PASS; manifest SHA-256 `eef44e99f84b858af013057c17731a4cba0e0ce5886664167507eba15afa51f1`. Disposition: `BLOCKED_OR_REWORK`. No status change is safe now; keep `in_progress`.

What remains unproved:
- 457 external-or-dynamic rows remain unclassified (independent NO-GO 3891/3896) — dictionary-based sweep goes blind, so the single-door claim is not proven for the whole tree.
- 41 paid-external + 64 dynamic call sites unproven against the door (per classification output).
- No paid-path production check on the current line recorded.
- Watchdog 3690 not installed in CI or landing.
- Body claim '73 legacy files' vs wave 3690 measured 78 files (all alive on that date).

One bounded next action: Record the decision on the 457 unclassified external-or-dynamic rows; then run check-any-egress-has-door.cjs --follow-imports --json on the current line and record line count + RC at check time; then install watchdog 3690 into CI/landing so a new direct provider call fails.
Target evidence: A committed classification decision + a dated RC/line-count receipt for the current line + watchdog wired into CI/landing.

This audit changed no board data and made no production claim outside the snapshot. Closure still requires ticket-specific evidence and coordinator readback.
2026-09-22T18:13:43.764Z · triage-m1
РЕШЕНИЕ=in_progress
ОСНОВАНИЕ=комментарий 5581 от 19.09: BLOCKED_OR_REWORK; 3891 оставил NO-GO из-за 457 external-or-dynamic строк и отсутствия полного доказательства двери
ЧТО НУЖНО=зафиксировать решение по 457 строкам, прогнать check-any-egress-has-door на текущей линии и поставить сторож 3690 в CI/посадку
triage-m1 4609
2026-09-23T12:42:41.980Z · triage-neo
РЕШЕНИЕ=in_progress
ОСНОВАНИЕ=2026-09-19, POST-QA 4211: 457 external-or-dynamic строк не классифицированы, 41 paid-external и 64 dynamic call sites не доказаны через дверь; production check отсутствует.
ЧТО НУЖНО=первый шаг: зафиксировать решение по 457 строкам, прогнать проверку на текущей линии и поставить watchdog 3690 в CI/посадку; triage-neo 4712
2026-09-23T13:31:41.736Z · coordinator
[23.09 13:31Z координатор] ## 4724 (M1, Luna) — GO, первый шаг по решению триажа: инвентарь исходящих вызовов `ops/spendgate/egress-inventory.json` — 1039 строк, 0 неклассифицированных: DOOR=5, BYPASS=111 (каждая с `accepted_until` и причиной миграции), DYNAMIC=295 (с основанием — откуда host), NOT_PAID=621, PAYMENTS=7 (Stripe отдельно). Сторож `ops/spendgate/egress-door.test` — красный на подсаженном обходе, зелёный на дереве; тесты 5/5. Топ‑10 BYPASS по риску: realtime-token маршруты (`me-bridge`/`me2-bridge`), `me2-compiled-server.cjs` (client-secret и SDP напрямую в OpenAI), OpenRouter маршруты (`api/models/openrouter`, `api/image-models/openrouter`, `admin/ping-feature`), браузерный `RealtimePanel.tsx`, `openaiWebRtcDriver.ts`. Патчи (2) наложены на A2, ветка `l115s-web439-egress-inventory`. Статус → в ревью; следующий шаг — 4727: миграция топ‑10 за дверь.
2026-09-23T14:02:16.328Z · coordinator
[23.09 14:02Z координатор] ## 4727 (M1, Luna) — GO, шаг 2: топ‑10 обходов проведены за общую дверь (10/10): realtime-token маршруты me-bridge/me2-bridge объединены в metered realtime session broker, `me2-compiled-server.cjs` client-secret/SDP через серверный metered маршрут, браузерный `RealtimePanel`/`openaiWebRtcDriver` — только транспорт, OpenRouter маршруты через один metered адаптер; «без бюджета — 0 вызовов провайдера» 10/10; сторож зелёный, BYPASS 111→101; тесты 20/20, область 25/25, typecheck 0. Патчи 0003/0004 наложены на A2 `l115s-web439-egress-inventory`. Дальше — по 10 за волну после ревью 4730.
2026-09-23T14:12:18.000Z · coordinator
[23.09 14:12Z координатор] ## 4730 (M1, независимое ревью) — WEB-439 инвентарь (4724): **NO-GO**, 2 критичных: (1) сторож находит 7 текущих мест без строк в инвентаре (`UNCLASSIFIED=0` было неверно; список в `tests.md`); (2) сканер обходится: переменная-алиас `fetch` невидима, host из конкатенации/шаблонной строки попадает в `DYNAMIC` без acceptance (репродуктор `web439-scanner-bypass.test.mjs`). Миграции 4727 (топ‑10) это не отменяет; инвентарь/сканер → раунд 2 (4738) после 4734. Статус → в работу.
2026-09-23T14:30:55.726Z · coordinator
[23.09 14:30Z координатор] ## 4735 (M1, независимое ревью) — WEB-439 шаг 2 (4727 топ‑10): **NO-GO**: (4) realtime token-маршруты резервируют в `guardRealtimeMint`, затем зовут брокера, который делает ВТОРУЮ платную операцию, а первый handle не подтверждается/не отменяется — двойная резервация; (7) ключи провайдера резолвятся ДО бюджетного guard на token/SDP/OpenRouter-каталоге; (8) обязательная проверка дерева красная, а тесты «без бюджета — 0 вызовов» не доходят до реальной границы маршрут/SDK. Репродуктор `web439-adversarial-repro.patch`. → раунд 2 вместе с инвентарём (4738) после 4734.
2026-09-23T14:36:50.177Z · coordinator
[23.09 14:36Z координатор] ## 4734 (M1, Luna) — GO, шаг 3: следующие 10 продуктовых мест за дверь (`actions.ts` ×4 через один metered helper, auto-tag, cluster-names, companion ×2, graph ×2), «без бюджета — 0 вызовов» 10/10, сторож зелёный, BYPASS 101→91; тесты 20/20, область 50/52 (2 пропуска), typecheck 0. Патчи 0005–0007 наложены на A2 `l115s-web439-egress-inventory`. Дальше 4738: раунд 2 по находкам ревью 4730/4735 (полнота инвентаря, обходы сканера, двойная резервация realtime, ключи до guard, граница SDK в тестах).
2026-09-23T15:00:03.450Z · coordinator
[23.09 15:00Z координатор] ## 4738 (M1, Luna, раунд 2) — GO: инвентарь дополнен 17 строками (0 неклассифицированных по прогону сторожа), сканер ловит алиасы `fetch`/SDK через переменные, конкатенацию и шаблоны host (без acceptance — красный); realtime mint — одна резервация (handle guard → broker → confirm/abort в finally); ключи провайдера читаются только после положительного бюджета на token/SDP/OpenRouter (3/3, шпион на env); тесты «без бюджета» — на реальной границе route-handler/SDK. Репродукторы 4730/4735 5/5 красные до, 6/6 зелёные после; область 52/52; typecheck 0; BYPASS 91. Патчи 0008/0009 наложены на A2 `l115s-web439-egress-inventory`. Статус → в ревью (6-е ревью вместе с WEB-670 р5); параллельно 4745 — следующие 10 обходов.
2026-09-23T15:32:39.247Z · coordinator
[23.09 15:32Z координатор] ## 4748 (M1, независимое ревью) — WEB-439 р2: **NO-GO**, 3 критичных: (3) сканер не видит `new Function`/`eval`/динамический `import()`/`Reflect.get(globalThis,'fetch')`/`require('node:https')`/`Worker`; (4) realtime: после dispatch разбор ответа может бросить уже при `handleFinalized=true` → `finally` не отменяет/не освобождает handle (оба bridge-маршрута); (5) идемпотентность: резервация realtime берёт свежий UUID и игнорирует idempotency-ключ запроса — повтор запроса резервирует снова (`sessionRegistry.ts:263`). → раунд 3 (4752) после 4745 (то же дерево).
2026-09-23T15:34:13.344Z · coordinator
[23.09 15:34Z координатор] ## 4745 (M1, Luna) — GO, шаг 4: третья десятка за дверь (ping-feature ×4 одним helper, podcast/metadata, scan classify/ocr, storyteller ×2, transcribe), «без бюджета — 0 вызовов» 10/10 на реальной границе, сторож зелёный, BYPASS 91→81; тесты 21/21, область 84/84, typecheck 0. Патчи 0010–0012 наложены на A2 `l115s-web439-egress-inventory`. Дальше 4752 — раунд 3 по находкам 4748 (сканер: eval/new Function/dynamic import/Reflect.get/require(node:https)/Worker; realtime finally после handleFinalized; идемпотентность резервации по ключу запроса; дрейф инвентаря actions.ts:7265, telephony orchestrator:1279).
2026-09-23T15:57:16.571Z · coordinator
[23.09 15:57Z координатор] ## 4752 (M1, Luna, раунд 3) — GO: сканер инвентаризирует `new Function`/`eval`/динамический `import()`/`Reflect.get(globalThis,fetch)`/`require(node:https)`/`Worker` (6/6) как DYNAMIC с обязательным acceptance; realtime: handle финализируется только после успешного разбора, исключение в разборе → abort/release; резервация по idempotency-ключу запроса (повтор = та же резервация); инвентарь привязан к содержимому (content-hash), дрейф по номерам строк устранён, 0 неклассифицированных; репродукторы 4748 красные до / зелёные после. Патчи 0013/0014 наложены на A2 `l115s-web439-egress-inventory`. Статус → в ревью (4758).
2026-09-23T16:11:44.599Z · coordinator
[23.09 16:11Z координатор] ## 4758 (M1, 4-е независимое ревью) — WEB-439 р3: **NO-GO** (4 находки, 2 критичных): F1 сканер снова не закрывает часть форм платного egress (статический сканер по построению неполон); F2 отсутствующий idempotency-ключ realtime → свежий UUID без предела. → раунд 4 (4760) со СМЕНОЙ УСТРОЙСТВА: сканер остаётся сторожем, а гарантия переезжает в рантайм — единый egress-gate на уровне процесса (глобальный undici/fetch dispatcher + `http/https.request` перехват при старте: исходящий запрос к платному host без door-токена отвергается), плюс ключ realtime обязателен (400 без ключа). Подробности в `4758…-evidence/review.md`.
2026-09-23T16:56:43.014Z · coordinator
[23.09 16:56Z координатор] ## 4760 (M1, Luna, раунд 4 — смена устройства) — принят как GO (отчёт без строки VERDICT, но сдача полная, манифест 19 OK): один процессный egress-gate ставится в Next `instrumentation` и в bootstrap worker-ов, door-авторизация несётся через AsyncLocalStorage, 22 платных host-а в манифесте; все 11 форм egress без токена отвергаются (репродуктор 4758 4/4 красный → зелёный), авторизованный транспорт проходит только к MockAgent; ключ realtime обязателен; abort резервации retry-safe; SDK транскрибации конструируется внутри бюджетного callback; область 77/77, legacy checker 8/8, typecheck 0. Сканер остаётся PR-сторожем (+формы р4). Патчи 0015/0016 наложены на A2 `l115s-web439-egress-inventory`. Статус → в ревью (4771).
2026-09-23T17:19:57.901Z · coordinator
[23.09 17:19Z координатор] ## 4771 (M1, 5-е независимое ревью) — WEB-439 р4 (рантайм egress-gate): **NO-GO**, 4 критичных: F-01 fail-open при сбое instrumentation; F-02 14 entry-точек (прямые лаунчеры `scripts/**` и др.) без bootstrap gate; F-03 обход транспортной идентичности (IP вместо host / DNS / прокси); F-05 устаревшее утверждение в тесте. → раунд 5 (4778): fail-closed по умолчанию (без установленного gate любой платный egress отвергается), единый bootstrap для всех 14 entry (или общий пред-загрузчик через `NODE_OPTIONS=--import`), идентичность по resolved IP/SNI/прокси-цели, а не только по Host.
2026-09-23T17:44:30.861Z · coordinator
[23.09 17:44Z координатор] ## 4778 (M1, Luna, раунд 5) — GO: fail-closed (статус bootstrap процессно-глобальный, неизвестный/false → типизированный отказ ДО резервации/вызова провайдера + структурный алерт), pre-load gate для всех entry, идентичность по resolved-адресу/SNI/прокси/http2, тест F-05 починен; 19 коммитов (22 OK). Патчи 0017–0019 наложены на A2 `l115s-web439-egress-inventory` (99ddc18c). Статус → в ревью (4788).
2026-09-23T18:00:50.122Z · coordinator
[23.09 18:00Z координатор] ## 4788 (M1, 6-е независимое ревью) — WEB-439 р5: **NO-GO**, 4 крит.: (P0) статус допуска на `globalThis[Symbol.for]` без проверки — модуль до gate ставит `egressInstalled:true`; (P0) `CONNECT host:port` прокси не разбирается — туннель к платному host мимо gate; (P1) preload зависит от `NODE_OPTIONS`/импортов — юнит `note-clone-source-indexing.service` стартует без него; (P1) кэш host→IP 5 мин авторизует старый адрес; (P1) сторож инвентаря красный 29/30. → раунд 6 (4791): допуск доказывается собственными хуками по ссылке + direct-deny пробой, без глобальных флагов; все точки запуска под preload в их окружении; идентичность на сокет; разбор CONNECT.
2026-09-23T18:39:22.978Z · coordinator
[23.09 18:39Z координатор] ## 4791 (M1, WEB-439 раунд 6) — GO: допуск платной операции больше не доверяет перезаписываемому процесс-глобальному флагу — проверяет по identity gate-owned hooks (`http/https/net/tls/http2/undici`, WeakSet-владение) и требует прямого доказательства отказа (fake transport, 0 вызовов) при старте и периодически, fail-closed; все 9 точек запуска несут preload в своём окружении; per-socket авторизация IP по свежему резолву + SNI; CONNECT/absolute-form proxy-цели разобраны; сторож инвентаря покрывает 4967 сайтов, 30/30 BYPASS по контракту. Репродукторы ревью 4788 2/2 красные→зелёные, 21/21, typecheck 0. Патчи 0020–0021 наложены на A2 `l115s-web439-egress-inventory`. Дальше: независимое ревью р6 (вместе с WEB-661 р5), затем all9.
2026-09-23T19:27:25.485Z · coordinator
[23.09 19:27Z координатор] ## 4800 (M1, независимое ревью WEB-439 р6) — NO-GO, 5 находок: (1) регрессия контракта — `meteredVendorCall.ts:412` при неизвестном bootstrap бросает `VendorEgressGuardUnavailableError` вместо `EgressBootstrapNotReadyError` (репродуктор 0/1); (2) hooks оборачивают текущие изменяемые функции модулей и считают их оригиналами — более ранний preload перенацеливает запрос/травит DNS-identity, WeakSet-владение проходит; (3) доказательство отказа: синхронный fake transport не вызывается (`vendorEgressGuard.ts:598-637`), асинхронное не ожидается (`:733-738`); подмена `globalThis.fetch` после проверки получила платный URL; (4) инвентарь точек запуска неполон (`scripts/start-dev.sh`, `scripts/ci/fresh-db-check.sh`, `scripts/pi-deploy.sh`); (5) обёрнут только глобальный WebSocket — `ws`, EventSource, ранее импортированные привязки, gRPC не доказаны. Раунд 7 — следующая волна.
2026-09-23T20:02:48.217Z · coordinator
[23.09 20:02Z координатор] ## СОСТОЯНИЕ НА 23.09 20:1xZ (для нулевого агента)
- Что это: единая дверь к платным провайдерам (spendGateCall) — любой платный egress из приложения идёт только через вентиль с инвентарём разрешённых сайтов и допуском платной операции.
- Где код: A2 `/home/ubuntu/nc-build`, ветка `l115s-web439-egress-inventory` = 66312027e (21 коммит над l115r 1a5822a8; worktree `wt-l115s-web439`). Раунд 5 (19 коммитов) вошёл в сводную серию all8; раунды 6+ — в all9.
- Раунды: 1–6 сделаны (4724/4727/4734/4738/4745/4752/4760/4778/4791), независимых ревью 7; последнее ревью 4800 (23.09 19:05Z) — NO-GO, 5 находок (контракт ошибок, ранний preload, неожидаемое proof, инвентарь запусков, ws/EventSource).
- Сейчас: раунд 7 — волна 4806 на M1 (`~/wt-l115s-d`, ветка `l115s-web439-round7`), запущена 19:30Z. После GO — ревью, затем all9 → стенд → посадка.
- Не вошло в бой: ничего из WEB-439 ещё не на бою (бой = l115r).
2026-09-23T20:07:31.634Z · coordinator
[23.09 20:07Z координатор] ## 4806 (M1, WEB-439 раунд 7) — GO: контракт `EgressBootstrapNotReadyError` восстановлен; ранний preload до gate = отказ старта в paid-режиме; сверка identity эталонных функций + non-writable экспорты; DNS через gate-owned resolver; доказательство отказа ожидается ДО допуска, `globalThis.fetch` сверяется перед каждым платным вызовом; инвентарь точек запуска строит сканер (776 запусков), `ws`/EventSource/gRPC доказаны по import-графу. Репродукторы 4800 2/2, 32/32, typecheck 0. Патчи 0022–0029 на A2 `l115s-web439-egress-inventory` = f95fbd4a95 (29). СОСТОЯНИЕ: раунд 7 принят → ревью 4811 (M1, 8-я приёмка).
2026-09-23T20:43:51.906Z · coordinator
[23.09 20:43Z координатор] ## 4811 (M1, независимое ревью WEB-439 р7, 8-е) — NO-GO: (крит.) заморожены только 6 транспортов (`vendorEgressGuard.ts:952-975`), а сторожа стоят ещё на `http.get`/`https.get`/`net.createConnection`/`dns.promises.lookup`/`undici` — их можно подменить после установки; (high) инвентарь запусков с ключом по номеру строки краснеет после наложения WEB-661 р6; (high) в evidence-глобе Markdown под именем `.test.mjs` — набор не исполняется; (medium) DNS-серверы из `dns.getServers()` при пустом `EGRESS_DNS_SERVERS`. 6/8 прошлых закрыты. СОСТОЯНИЕ: раунд 8 — волна 4817 (M1, d): единый список охраняемых экспортов + генерируемый тест, ключ инвентаря по содержимому, DNS обязателен в paid.
2026-09-23T21:16:24.233Z · coordinator
[23.09 21:16Z координатор] ## 4817 (M1, WEB-439 раунд 8) — GO: единый список охраняемых экспортов, заморожены 18/18 с генерируемым тестом «подмена любого = отказ»; инвентарь запусков с ключом по содержимому, зелёный с наложенным WEB-661 р6; evidence-глоб исполняется; `EGRESS_DNS_SERVERS` обязателен в paid-режиме. Репродукторы 4/4, 107/107, typecheck 0. Патчи 0030–0035 на A2 `l115s-web439-egress-inventory` = 06e2fc2245 (35). СОСТОЯНИЕ: → ревью 4822 (M1, 9-е).
2026-09-23T21:50:22.403Z · coordinator
[23.09 21:50Z координатор] ## 4822 (M1, независимое ревью WEB-439 р8, 9-е) — NO-GO, 3 находки (1 крит.): (1) КРИТ. transport identity неполна для реально используемого `ws` (пакет `ws` в графе платных вызовов не заморожен/не проверен); (2) HIGH `EGRESS_DNS_SERVERS` обязателен, но значение не валидируется (доверие/формат — localhost/подконтрольный резолвер проходит); (3) MEDIUM инвентарь запусков не видит цепочки `npm`-скриптов (`package.json` scripts, вызывающие другие скрипты). Закрыто 4/6 прошлых (инвентарь по содержимому, .md в глобе, nonce, superset). СОСТОЯНИЕ: раунд 9 — волна 4829 (M1, d).
2026-09-23T22:21:23.128Z · coordinator
[23.09 22:21Z координатор] ## 4827/prepare-standalone (сборка all8) — шаг упаковки: `ops/egress/preload.mjs:117` `await import(undici)` (top-level await) не бандлится esbuild в cjs-бандл воркера индексации (воркер импортирует preload с р5) → заменено на синхронный `require` через уже имеющийся `createRequire`; семантика та же (обёртки undici ставятся синхронно, даже раньше). Коммит 5bb221f29a на all8, cherry-pick в `l115s-web439-egress-inventory` (0ae5defc7f). Раунд 9 (4829) идёт от серии р8 — при приёме р9 наложить этот коммит. Правило: preload — без top-level await (его бандлят в cjs).
2026-09-23T22:24:09.302Z · coordinator
[23.09 22:24Z координатор] ## 4829 (M1, WEB-439 раунд 9) — GO: список транспортов строится сканером по реальным импортам (9 модулей, включая `ws`), все заморожены с identity-check и генерируемым тестом; `EGRESS_DNS_SERVERS` валидируется (IP, не loopback/private без явного флага, проба резолва на старте); npm-цепочки в инвентаре запусков. Репродукторы 4822 3/3, 113/113, typecheck 0; 38 коммитов. СОСТОЯНИЕ: патчи 0036–0038 накладываются на A2 (ветка уже несёт правку preload 5bb221f29a для упаковки) → ревью 4836 (10-е).
2026-09-23T23:00:31.301Z · coordinator
[23.09 23:00Z координатор] Ревью 4836 (WEB-439 раунд 9): NO-GO, 4 находки (1 крит.). Закрыто: ws, DNS-валидация, npm run. Открыто: глобальный WebSocket Node 22 не входит в проверку identity (крит.); инвентарь транспортов не транзитивный (openai → ws не виден); pnpm -r / npx / node --run не разобраны; нет метрики EGRESS_DNS_ALLOW_PRIVATE. Раунд 10 — волна 4839 (M1).
2026-09-24T00:17:40.644Z · coordinator
[24.09 00:17Z координатор] Волна 4839 (раунд 10): GO. Закрыты 4 находки ревью 4836: глобальные WebSocket/EventSource под контролем допуска (включая метки владения между бандлами), сканер видит транзитивные транспорты платных пакетов, лаунчеры в инвентаре, метрика приватного DNS. Репродукторы 5/5 зелёные, тесты 144/144, typecheck 0. Патчи на A2: ветка l115s-web439-round10 (44 коммита, cc69df87). Независимое ревью — волна 4851.
2026-09-24T00:33:19.360Z · coordinator
[24.09 00:33Z координатор] Волна 4851 (независимое ревью раунда 10): NO-GO. Тесты 136/136, typecheck 0, 3 из 4 прежних находок закрыты. Открыто: (1) критично — метка владения между бандлами подделывается (публичный Symbol.for с булевым значением, не проверяется по приватному WeakSet); (2) require(variable)/переменный import() в зависимостях из node_modules не отвергаются; (3) предупреждение о приватном DNS печатает адреса. Раунд 11 — волна 4854 (M1).
2026-09-24T00:52:40.420Z · coordinator
[24.09 00:52Z координатор] Волна 4854 (WEB-439 раунд 11): GO по отчёту волны. Закрыто по 4851: подделка метки владения отвергается (владение по приватной ссылке), require(variable)/переменный import() в зависимостях node_modules отвергаются, адреса DNS в выводе скрыты. Репродукторы 4/4 зелёные после, тесты 140/140, typecheck 0, манифест OK. Серия 47 → A2 l115s-web439-round11 (e3ce5f8301) от 1a5822a8. Независимое ревью — 4858 (M1).
2026-09-24T01:06:35.881Z · coordinator
[24.09 01:06Z координатор] Ревью 4858 (независимое, M1): NO-GO. 3 прежние находки 4851 закрыты, тесты 150/150, но новые: (критично) состояние охраны публично достаётся через Symbol.for, изменяемая ownedTransportRefs участвует в сертификате — подделка транспорта проходит; (высоко) createRequire/eval/Function/Worker с переменной в точках входа зависимостей не ловятся; (средне) предупреждение охраны в TypeScript печатает DNS-серверы. Исправление — 4860 (раунд 12). Замечание: маркер DONE волна не создала, отчёт и манифест полные.
2026-09-24T01:36:52.966Z · coordinator
[24.09 01:36Z координатор] 4860 (M1, Luna) GO: WEB-439 р12 — состояние охраны вынесено из публичного process-global ключа (замороженный мост, сертификат по приватной ссылке), динамические загрузчики в node_modules — отказ, адреса DNS скрыты; 85/85, репродукторы 7/7, 49 коммитов. Независимое ревью — 4864 (M1 tree f).
2026-09-24T01:51:08.832Z · coordinator
[24.09 01:51Z координатор] 4864 (M1, Luna, независимое ревью р12) NO-GO: прежние 3/3 закрыты, область 150/150, typecheck 0; новые находки: КРИТ — runWithEgressDoorToken через публичный мост не проверяет токен (объект {allowedHosts:[/.*/]} пропускает fetch к платному провайдеру без резервации); ВЫСОКО — сканер загрузчиков пропускает алиасы/optional/рефлексию (6 форм); СРЕДНЕ — адрес DNS в тексте ошибки. → 4867 (M1 tree d, Luna): токен только выданный охраной (WeakSet), хосты из записи охраны, неразрешимые загрузчики — отказ, ошибки без адресов.
2026-09-24T02:13:04.000Z · coordinator
[24.09 02:13Z координатор] 4867 (M1, Luna, раунд 13) GO: 51 коммит, 155/155, репродукторы 13/13, typecheck 0, манифест 58 OK. Закрыты 3 находки ревью 4864: подделка токена двери простым объектом (путь к платному провайдеру без резервирования) — отказ; алиасы загрузчиков — отказ, неразрешимый загрузчик — fail-closed; адрес DNS в тексте ошибок — 0. → A2 ветка l115s-web439-round13 = dca62fd1e3 (от 1a5822a8). Независимое ревью 4871 (M1 tree e) с явной границей модели угроз. Статус → review.
2026-09-24T02:31:32.116Z · coordinator
[24.09 02:31Z координатор] 4871 (M1, независимое ревью р13) NO-GO: область 153/153, подделка токена двери ловится, прежние 3/3 закрыты; 2 высоких: (1) ещё 3 эквивалентные формы загрузчика (require["bind"], (0, require), Reflect.get(module,"require").bind) проходят сканер; (2) EgressDeniedError редактирует только message — .url и JSON держат query-секреты. → 4877 р14 (M1): сканер по разрешающему списку — только прямой вызов с литералом, иное — отказ; ошибка без секретов во всех полях.
2026-09-24T02:31:33.062Z · coordinator
[24.09 02:31Z координатор] 4866 (деплой all8 на стенд) NO-GO и откат: артефакт не содержит ops/egress/preload.mjs, а старт идёт `node server.js` без --import, поэтому instrumentation.ts бросает «egress preload was not loaded». Гейт верный, не упакован и не подключён preload. Фикс упаковки и прямого старта — 4876 (M1).
2026-09-24T03:41:40.391Z · coordinator
[24.09 03:41Z координатор] 4876 (M1) — preload эгресса в standalone-артефакте: 4 коммита (preload.cjs для прямого node server.js, outputFileTracingIncludes, prepare-standalone строго копирует и ставит bootstrap первой строкой, упаковка падает без preload), тесты 34/34, typecheck 0, гейт не ослаблен. Вердикт NO-GO только потому, что на M1 нельзя собрать настоящий артефакт (нет postgres для prebuild). Применено на A2: l115s-all8d = 0d6f856652 (174 над l115r); сборка + проверка preload в архиве → 4882 (A2).
2026-09-24T03:43:14.685Z · coordinator
[24.09 03:43Z координатор] 4877 (раунд 14, M1) GO: сканер замыкания пакетов переведён с перечисления форм загрузчика на разрешающий список — любой нелитеральный загрузчик отказ, 8 закреплённых исключений, реальное замыкание lock-файла проходит; EgressDeniedError редактирует и .url, и JSON (секретов в полях 0); репродукторы 67/67, область 71/71, typecheck 0. A2: l115s-web439-round14 = 275c9fb856 (55 над l115r). Независимое ревью → 4883 (M1).
2026-09-24T07:01:25.331Z · coordinator
[24.09 07:01Z координатор] Ревью 4894 (раунд 15, независимое): NO-GO, 1 находка ВЫСОКО, критичных 0. Статика р15 закрыла находку 4887; 58 патчей без конфликтов, область 46/46, typecheck 0, подделка токена двери ловится, реальное замыкание openai→ws проходит.
Находка: на Node ≥22.15 ветка module.registerHooks хука загрузчика не ловит дожидаемый import() из Function-созданного кода внутри платного CJS-пакета — поддельный ws загружается. На Node 20 та же фикстура отказывается. Бой и стенд на Node 22.23.2 — рубеж во время выполнения на реальной машине не работает.
Дальше: раунд 16 = волна 4897 (M1, дерево f, ветка l115s-web439-round16) — политика по реальному пути транспорта независимо от того, кто запросил; проверки на обеих ветках Node.
2026-09-24T07:30:04.513Z · coordinator
[24.09 07:30Z координатор] Раунд 16 (4897, M1, Luna) GO 24.09 07:29Z: закрыт обход registerHooks на Node 22.23.2 (import() из Function-кода в CJS-пакете грузил поддельный ws). Причина: у registerHooks не было живой синхронной политики, пустой load-хук пропускал dynamic import с неизвестным родителем. Исправление: политика включена, реальный путь транспорта проверяется в load для каждого file URL. Репро 6/6, область 248/248, typecheck 0, Node 20 и 22.23.2, реальное openai→ws проходит, секретов в ошибках 0, манифест 66/66. Серия 59 патчей, ветка l115s-web439-round16. Независимое ревью — 4898 (M1).
2026-09-24T07:49:20.885Z · coordinator
[24.09 07:49Z координатор] Ревью 4898 (независимое, р16) NO-GO 24.09 07:48Z: находка 4894 закрыта на Node 22.23.2 и 20 (registerHooks/register отказывают скрытый dynamic import, openai→ws проходит), 72/72, typecheck 0, 11/12 атак отбиты. КРИТИЧНО: платный CJS-пакет вызывает child_process.spawnSync с NODE_OPTIONS="" и в дочернем процессе грузит поддельный ws — хука там нет; child_process не в builtins манифеста, но ни статика, ни рантайм не отказали. Раунд 17 — 4899 (M1): разрешать платным пакетам только встроенные из манифеста, отказ по умолчанию child_process/worker_threads/vm/cluster/inspector/getBuiltinModule — и в рантайме, и статикой.
2026-09-24T08:15:08.103Z · coordinator
[24.09 08:15Z координатор] Раунд 17 (4899, M1) GO 24.09 08:12Z: закрыт обход через child_process (spawnSync с пустым NODE_OPTIONS). Причина: CJS Module._load и ESM resolve пропускали любой встроенный модуль, сканер не сверял встроенные с builtins манифеста. Теперь платному пакету разрешены только встроенные из его манифеста — и в рантайме, и статикой; child_process/worker_threads/getBuiltinModule отказ. Node 22.23.2 и 20: репродукторы 2/2, область 114/114, typecheck 0, реальное замыкание openai→ws проходит, секретов в ошибках 0. Ветка l115s-web439-round17 (62 коммита) на A2. Независимое ревью — 4900 (M1).
2026-09-24T08:29:19.160Z · coordinator
[24.09 08:29Z координатор] Ревью 4900 (р17, M1) NO-GO 24.09 08:25Z: находка 4900-1 КРИТИЧНО — платный пакет через разрешённый встроенный node:module подменяет Module._cache[ws].exports и получает поддельный транспорт; статика шаблон не видит, хук сверяет только запрос/realpath. Node 20 и 22.23.2. Остальное зелёное: 77/77, typecheck 0, 22/23 атаки, находка 4898 (child_process) закрыта, 12 прежних репродукторов зелёные. Раунд 18 — 4905 (M1 tree f): закрыть КЛАСС «подмена загруженного закреплённого модуля» — сверка тождества exports с проверенной ссылкой, фасад node:module без _cache/_load/_extensions, статика, ≥5 вариантов класса.
2026-09-24T08:55:09.039Z · coordinator
[24.09 08:55Z координатор] Раунд 18 (4905, M1) GO 24.09 08:53Z: закрыт класс «подмена загруженного закреплённого модуля» (находка 4900-1). Причина: платный пакет получал общий Module/кэш, доверенная выдача брала изменяемый exports из кэша. Исправление: выдача транспорта сверяет тождество с собственной проверенной ссылкой, фасад node:module без внутренностей, статика. Вариантов класса отказано 7/7, репродукторы 12/12, область 67/67, typecheck 0, Node 20 + 22.23.2, реальное замыкание openai→ws проходит, child_process/registerHooks остаются закрыты. 66 коммитов, ветка A2 l115s-web439-round18 (f0761897). Независимое ревью — 4907 (M1 tree e).
2026-09-24T09:14:23.438Z · coordinator
[24.09 09:14Z координатор] Ревью 4907 (M1, 19-я приёмка) раунда 18 — NO-GO, 1 находка КРИТИЧНО. Закрыто: 4900-1 (подмена записи кэша) на Node 20 и 22.23.2, области 71/71, typecheck 0, 27/29 атак отбиты, фасад node:module, registerHooks, child_process, подделка токена двери, редакция адресов в ошибках. Находка 4907-1: платный пакет получает доверенный ws и переписывает ws.WebSocket.prototype.send (или удаляет метод и подставляет через Object.setPrototypeOf) — тождество верхних экспортов совпадает, проба fakeTransportLoaded=true; freezeTransportKeyFunctions() в р18 пустая. → раунд 19 (4908, M1 tree f): закрыть весь класс «изменение объектов транспорта на любой глубине» (заморозка графа / Reflect.apply по захваченным ссылкам / мембрана), ≥8 вариантов класса.
2026-09-24T09:41:14.971Z · coordinator
[24.09 09:41Z координатор] Раунд 19 (4908, M1) — GO: закрыт класс «изменение объектов транспорта» (4907-1). Дизайн freeze+membrane+integrity, freezeTransportKeyFunctions() заменена, общие встроенные объекты исключены из заморозки. 20/20 вариантов класса отказаны, репродукторы 53/53, области 141/141, typecheck 0, Node 20.9.0 и 22.23.2; кэш-подмена, фасад node:module, child_process, registerHooks по-прежнему закрыты; реальное замыкание openai→ws проходит. Ветка l115s-web439-round19 (70 патчей). → независимое ревью 4911 (M1 tree e).
2026-09-24T10:02:44.191Z · coordinator
[24.09 10:02Z координатор] Независимое ревью 4911 раунда 19 — NO-GO, 1 находка КРИТИЧНО (4911-1). Закрыто по-настоящему: изменение прототипа выданного ws (4907-1) 10/10 на обеих Node, фасад node:module 7/7, подмена кэша 4900-1 6/6, токен двери 5/5; области 157/157, typecheck 0; 34/37 атак отбиты. Дыра: доверенная отправка ws идёт через stream.Writable.prototype.write, а платный пакет получает СЫРОЙ встроенный node:stream (мембрана оборачивает только events, paid-loader-hook.mjs:15, 705–707) и меняет прототип → поддельная отправка. На Node 22 сырой модуль достижим и через constructor/__proto__ обёрток (нет ловушки getPrototypeOf, 360–366). Единственный конфликт git am — дубль базового коммита, не реальный. → раунд 20 (4914, M1 tree f, 10:02Z): закрыть весь класс «общие встроенные модули» (primordials + мембрана всех встроенных), ≥10 вариантов, замер цены отправки.
2026-09-24T10:38:26.725Z · coordinator
[24.09 10:38Z координатор] Раунд 20 (4914, M1) GO: закрыт класс «общие встроенные модули Node у платного пакета» (находка ревью 4911-1). Мембрана на все встроенные модули с ловушками constructor/__proto__/getPrototypeOf, статический отказ изменения прототипов встроенных, неизменяемые process/timers/Buffer, заморозка поверхностей отправки транспорта. Серия 76 патчей на l115r. Матрица р16–р20 14/14 и 20/20 на Node 20 и 22; репродукторы 4911 3/3 отказаны; spendgate 31/31; реальный openai/realtime ws работает; typecheck 0; цена отправки около 0. → независимое состязательное ревью 4918 (M1, дерево e).
2026-09-24T10:57:53.732Z · coordinator
[24.09 10:57Z координатор] Ревью 4918 раунда 20: NO-GO, 1 критичная находка. Прошлая находка 4911-1 закрыта (областей 175/175, 72/84 атак отбиты, typecheck 0), но guard решает «доверенный или платный» по стеку вызова, а платный код подделывает стек через Error.prepareStackTrace/captureStackTrace и получает сырой process.getBuiltinModule(stream) на Node 22 (5/5 вариантов). Плюс изменяемы fetch, WebSocket, structuredClone, Atomics, SharedArrayBuffer. → раунд 21 (4920, M1 дерево f): убрать стек как основание решения целиком, право доступа = ссылка, выданная при загрузке; STACK_BASED_DECISIONS=0.
2026-09-24T12:03:37.536Z · coordinator
[24.09 12:03Z координатор] 4926 (M1, Codex, ревью р21) NO-GO — но не обход защиты. Главное закрыто: решение больше не по стеку (STACK_BASED_DECISIONS=0), выданную загрузчиком ссылку не подделать, 131/131 атак отбиты на Node 22.23.2 и 20.9.0, находка 4918 закрыта. Блокер 4926-1 (критично для приёмки): под ops/egress/preload.mjs доверенный bootstrap guard-а (installVendorEgressGuard → nodeRequire → trustedNodeCreateRequire) попадает в запрет Module.createRequire, предназначенный платным пакетам; области 159/160. → 4928 (р22, M1, дерево f): доверенный bootstrap получает сырой createRequire только ссылкой из замыкания загрузчика, запрет для платных остаётся.
2026-09-24T12:34:37.907Z · coordinator
[24.09 12:34Z координатор] 4928 (M1, раунд 22) GO: закрыта находка 4926-1 — доверенный bootstrap guard-а теперь получает права по приватной ссылке и сохраняется в бандл-чанках; области 160/160, атаки 4926 131/131, новые варианты 6/6 отбиты, прошлые репродукторы 17/17, решений по стеку 0, сырой createRequire из платных недостижим; Node 22.23.2 и 20.9.0. 3 новых коммита, 82 над базой. Ветка l115s-web439-round22 (b68d34796e) перенесена на A2 бандлом (патчи /home/ubuntu/patches-4928). Независимое ревью 4930 запущено на M1 (дерево e, 12:4xZ).
2026-09-24T13:18:45.306Z · coordinator
[24.09 13:18Z координатор] 4930 (M1, независимое ревью р22) NO-GO, 1 находка критичная, но это НЕ обход: 137/137 атак отбиты, capability не подделать, стековых решений 0, bootstrap-ссылка недостижима, сырой createRequire из платных недостижим; находка 4926-1 закрыта. Находка 4930-001: под preload.mjs Prisma приложения не может загрузить нативный движок — guard отвергает process.dlopen как «платную границу»; область round-8 transport-ownership 91/92 на Node 22.23.2 и 20.9.0 (итого 159/160). Без исправления сервер под guard-ом не стартует. → 4932 р23 (M1, дерево f, от l115s-web439-round22): dlopen доверенному коду по праву загрузчика, платным — отказ, отдельно закрыть прокси через доверенный модуль.
2026-09-24T14:00:36.623Z · coordinator
[24.09 14:00Z координатор] **24.09 13:57Z — раунд 23 (волна 4932, M1, Codex Luna): GO** (состояние для нулевого агента)

Закрыта находка 4930-001 (process.dlopen Prisma под guard-ом отвергался → области 159/160). Решение: нативный движок Prisma грузится доверенным помощником при preload; платный код не получает dlopen ни напрямую, ни через прокси доверенного модуля.
- Коммиты 0083–0085 (3), ветка `l115s-web439-round23` = c94d3ab888 (A2 nc-build; бандл `/home/ubuntu/b4932.bundle`).
- AREAS 160/160 на Node 22.23.2 и 20.9.0; атаки 4930 137/137, 4926 131/131, новые варианты 4/4 отбиты; STACK_BASED_DECISIONS=0; DLOPEN_REACHABLE_FROM_PAID=НЕТ.
- Манифест 91 OK. Доказательства: M1 `~/waves/4932WEB439ROUND23TRUSTEDNATIVELOAD-evidence/`.

Дальше: независимое ревью 4935 (M1, дерево e, старт 13:59Z) — проверки обхода нативной загрузки (`process.binding`, `Module._load` с подменой filename, `_extensions['.node']`, worker eval).
2026-09-24T14:33:13.289Z · coordinator
[24.09 14:33Z координатор] Ревью 4935 (раунд 23, M1): NO-GO, 1 находка КРИТИЧНО — обход. Р23 закрыл 4930-001: Prisma грузит нативный движок под охраной, области 160/160, атаки 141/143, typecheck 0.
Находка: preload исполняет @prisma/client до установки охраны процесса/модулей. Зависимость в этом окне может require видимый trusted-bootstrap.cjs, забрать capability и сохранить сырые builtinRequire / Module._load / process.dlopen. Репродуктор доходит до сырого dlopen на Node 22 и 20.
Дальше: раунд 24 (волна 4937, M1 дерево f, ветка l115s-web439-round24) — охрана до любого стороннего кода, capability одноразовая и только загрузчику.
2026-09-24T15:06:00.643Z · coordinator
[24.09 15:05Z координатор] Раунд 24: первая попытка 4937 зависла на старте (30 мин без действий, 0 % CPU, ветки нет), убита. Перезапуск как 4940 на M1 (дерево f), stdin закрыт.
2026-09-24T15:31:03.541Z · coordinator
[24.09 15:31Z координатор] Раунд 24 (4940, M1) GO: охрана ставится до любого стороннего кода, capability bootstrap одноразовая. 160/160 областей, атаки 143/143 (4935) + 137/137 + 131/131, новые 11/11 отказ, Prisma грузит движок, прошлые репродукторы 12/12, решений по стеку 0, Node 22.23.2 и 20.20.2. Ветка l115s-web439-round24 (4 коммита над р23). Независимое ревью — 4938 (M1 дерево e).
2026-09-24T15:48:30.326Z · coordinator
[24.09 15:48Z координатор] Ревью 4938 (р24, M1) NO-GO: 160/160, 143/144, находки 4935-001 и 4930-001 закрыты. Новая КРИТИЧНО 4938-001: paid-loader-hook.mjs:1050-1055 публикует cjs в publicState — повторный вызов установщика из приложения отдаёт originalLoad/originalModuleRequire → сырой node:child_process без отказа (Node 22 и 20). Раунд 25 (4941, M1 дерево f): сырые ссылки только в приватном состоянии + аудит всех экспортов/глобалов.
2026-09-24T16:08:43.807Z · coordinator
[24.09 16:08Z координатор] Раунд 25 (4941, M1) GO: сырые ссылки загрузчика только в приватном состоянии, publicState заморожен без функций; аудит 18 экспортов — утечек 0; находка 4938-001 закрыта; области 160/160 на Node 22.23.2 и 20; состязательные прошлых ревью 144/143/137/131 PASS, новых 10/10 отклонены; Prisma dlopen ок. Независимое ревью 4944 (M1, дерево e).
2026-09-24T16:21:04.891Z · coordinator
[24.09 16:21Z координатор] Независимое ревью р25 (4944, M1): NO-GO, 1 находка 4944-001 (доступность, не обход). Атаки 144/144 отклонены, находки 4938-001/4935-001/4930-001 закрыты, подделка права НЕТ, Prisma ок. Но обязательный тест web439Round8TransportOwnership падает на Node 22.23.2 и 20: доверенный вызов module.createRequire после предзагрузки получает EGRESS_DENIED → 159/160 (автор р25 заявлял 160/160 — расхождение разбирается). Раунд 26 — 4946 (M1, дерево f).
2026-09-24T16:44:25.258Z · coordinator
[24.09 16:44Z координатор] Раунд 26 (4946, M1) GO: доверенный код приложения получает createRequire через окно загрузки, выданное загрузчиком (не по стеку); платный код — только отказ. 4944-001 закрыта: тест TransportOwnership красный ДО (3/4) и зелёный ПОСЛЕ (4/4) на Node 22.23.2 и 20 командами ревью; 15-файловая TS-область 107/107; области 160/160 с построчным areas.md. Атаки 144/143/137/131 отклонены, новых 12/12, экспорты 19 без утечек, Prisma ок. Причина расхождения р25: 160/160 считался по отдельному журналу, провальный прогон 94/95 был исключён. Независимое ревью 4947 (M1, дерево e).
2026-09-24T16:57:34.718Z · coordinator
[24.09 16:57Z координатор] Независимое ревью р26 (4947, M1) GO, находок 0: серия 94 коммита легла без конфликтов; области 160/160 (с TransportOwnership 4/4 на Node 22.23.2 и 20); атаки 144/144; прежние находки 4/4 закрыты; злоупотребление окном загрузки НЕТ (11 попыток CJS/ESM/повторный вход/async/таймеры на каждой версии Node); подделка права НЕТ; Prisma ок. Принята ветка A2 nc-build l115s-web439-r26 (вершина 27c9df7756, 94 коммита от l115r) — войдёт в сборку all8. Статус → review (ждёт посадки).
2026-09-25T08:42:09.112Z · coordinator
[25.09 08:42Z координатор] VERDICT=REOPEN — WEB-439 на all9 не закрывать.

По-простому: независимая проверка самой двери проходит, но запуск всех платных entrypoints exact HEAD не проходит. `web439-entry-bootstrap.test.mjs` дал 2/16 pass и 14/16 fail; все 14 остановились на `pnpm-lock.yaml (lockfile sha256 mismatch)` в `ops/egress/paid-loader-hook.mjs:221-226`. Фактический SHA lockfile: `5bb348d0…`, pin в `ops/egress/paid-loader-allowlist.json:4`: `7177ac9e…`. Это появилось после all9 package/lock changes (`879094713e`, `089dda657`), а не является live-остатком.

В all9 есть `4cc2f905b` (scanner), `7a90175f6` (runtime guard), `c9aa4742b` (bootstrap/launch gates); ключевые места — `src/lib/billing/vendorEgressGuard.ts:1119-1205`, `src/instrumentation.ts:138-169`, `ops/spendgate/scan-egress.mjs:358-360`. Но сначала обновить lock pin согласованно, затем повторить 16 cases на Node22.23.2 и независимое ревью. Live landing не заменяет этот красный кодовый gate.

Как начать с нуля: `git rev-parse HEAD`; `shasum -a 256 pnpm-lock.yaml`; сравнить с allowlist; запустить `node --test ops/spendgate/web439-entry-bootstrap.test.mjs ops/spendgate/web439-independent-bypass-reproducer.test.mjs`. История раундов 4935/4938/4944 объясняет прошлые ловушки preload/capability/transport; текущая ловушка — stale lock pin. Доска: http://127.0.0.1:8787/api/web/issues/WEB-439.


Проверка координатора (09:5xZ, all9 879094713e, M1): Подтверждаю REOPEN: на Node 22.21.1 тот же результат 2/16, все 14 — pnpm-lock.yaml sha256 mismatch (пин лок-файла не обновлён после слияния all9). Нужен раунд: обновить пин под лок all9 и повторить 16/16.
2026-09-25T09:04:00.564Z · coordinator
[25.09 09:03Z координатор] 2026-09-25 — 4984 GO

Исправлено: пин sha256 pnpm-lock.yaml в ops/egress/paid-loader-allowlist.json устарел после слияния all9 — обновлён на текущий digest; алгоритм и fail-closed проверка (paid-loader-hook.mjs:220-227) не менялись; добавлен негативный тест подмены лок-файла (web439-lockfile-pin.test.mjs) — подмена по-прежнему отвергается. 16/16 + 1/1. Для запуска в enforce-режиме нужен DNS-резолвер (EGRESS_DNS_SERVERS).

Волна 4984 (M1, Node 22.23.2, all9 879094713e): VERDICT=GO. Регресс WEB-685 263/263, check:web685-runtime и tool-worker-boundary зелёные. Коммиты на A2: ветка nc-build l115s-all9-green-4984 (318ab2309b WEB-681, 74b11db126 WEB-651, 3b75298e0a WEB-439), патчи /home/ubuntu/patches-4984/. Доказательства M1 ~/waves/4984ALL9GREENTESTS-evidence/ (SHA256SUMS 27 OK). Войдёт в следующую сборку (all10).
2026-09-26T12:12:20.198Z · coordinator
[26.09 12:12Z координатор] 26.09 12:15Z координатор: сборка 5058 (NO-GO) сорвана дворником диска A2, он удалил рабочий каталог. Дворник исправлен: защита по метке KEEP. Интеграция перезапущена как 5061 (A2, старт 12:11:31Z) с тем же составом: e7e0cd9b + 618583ff и Linux-исправления из журнала 5058. Тикет остаётся в работе до GO сборки и деплоя на стенд.
2026-09-26T12:57:48.407Z · coordinator
[26.09 12:57Z координатор] 5061 (сборка r2, 26.09 12:53Z) NO-GO. Интеграция и все Linux-исправления восстановлены, merge tree совпал с ожидаемым. Все тесты Linux прошли, checker5057 17/17. Прежний блокер (инициализация pinned Next send) устранён узким допуском по точному пути и хешу, отрицательные тесты прошли. Compile упал на новом месте: нативный модуль lightningcss linux-arm64 не загрузился. Повторить не дал порог диска 12 GiB. Координатор удалил 25 чистых worktree на A2, ветки сохранены, стало 16 GiB. Запущена 5062 (r3) с вершины 5061: узкий допуск для lightningcss, затем полная штатная цепочка. Тикет ждёт общий пакет стенда.
2026-09-26T13:38:33.409Z · coordinator
[26.09 13:38Z координатор] 26.09 13:38Z: сборка интеграции 5062 NO-GO. Допуск lightningcss принят, тесты PASS. Compile упал на нативном модуле @tailwindcss/oxide. Запущена 5064: инвентарь всех нативных linux-arm64 модулей и узкие допуски разом, затем штатная цепочка. Этот тикет входит в пакет. После GO: деплой на стенд, checker5057, ступени 300/400/500, живые роли.
2026-09-26T20:36:12.357Z · coordinator
[26.09 20:36Z координатор] Координатор, пост-QA 5073 на стенде 5069 (26.09 ~21:40 Дублин). Отчёт A2 /home/ubuntu/waves/5073POSTQASIXTICKETSSTAND-REPORT.md. PASS внутренней проверки: guardian-тесты 19/19, в 2417 JS-файлах сборки 5 транспортов провайдеров, все внутри spend gate, без гейта 0. Статус in_progress → review (done после посадки в прод).
2026-09-26T20:39:18.849Z · coordinator
[26.09 20:39Z координатор] Координатор 26.09 ~21:40 Дублин: по вопросу владельца закрываю. Пост-QA 5073 принят для WEB-439: guardian-тесты 19/19, в 2417 JS-файлах сборки 5069 все 5 транспортов провайдеров внутри spend gate, без гейта 0; SHA256SUMS доказательств OK. Код проверен на стенде (сборка 5069), в прод попадёт общей посадкой; живой платный вызов не выполнялся (запрещён).
Воркер
не проверен 3332-wash-billing A1 движение в панели: неизвестно
Подключиться и смотреть/перехватить руками. Колесо мыши листает; клавишами — Ctrl-b затем [, выход из прокрутки q. Отсоединиться — Ctrl-b затем d:
Прочитать историю панели без подключения — листается и ищется (/ поиск, q выход), воркеру не помешает:
Обновлён
2026-09-26T20:39:19.241Z