WEB-629 · Задача · — · web
Enterprise-2 / CAP-02: Изолированный стенд, egress и preflight
Закрыт
P2
ведёт: —
эпик: WEB-626
Суть
ENTERPRISE2-R1:CAP-02
Правила обогащения: WEB-449. Эпик: Enterprise-2.
Цель и сдача: Повторяемый isolated deployment kit и проверенная расписка готовности среды.
Работа: Переиспользовать WEB-489 после compatibility review. DB/Redis/storage/auth/provider identities отделены; sanitize clone до workers. Посчитать storage growth + WAL + evidence. Fail-closed target allowlist, TTL, dry-run cleanup, kill только тестовых units. B→A gate через 2 часа выделенного окна.
Зависимости: CAP-00, CAP-01.
Исполнение: coordinator+infra.
КАРТА ДОКУМЕНТОВ
Intel: /Users/annakorin/nc-ops-scripts/enterprise2-20260910/ENTERPRISE-2.md, tickets.json, board-created.json.
Вход: /Users/annakorin/Downloads/NC-CAPACITY-SCALE-TEST-BRIEF-R1.md.
Связанные: WEB-093/420/449/489; незакрытые Security и WEB-593/467 учитывать при freeze.
ЭВОЛЮЦИЯ
10.09.2026: создана задача из запроса владельца; capacity ещё не измерена. Author GO, полученный bundle, independent acceptance и runtime proof фиксируются раздельно.
DISPATCH-20260910-3395
На 2026-09-10T21:38:23.260126+00:00: active; A2, волна 3395 cap-isolation-preflight. Exact BASE 1ad52e16b784d3ae80a148fddae4d5a3ab4a89c4. Бриф прошёл brief-guard; входы SHA256 и наличие BASE проверены до постановки. Входы /home/ubuntu/waves/inputs/3395-cap-isolation-preflight. Отчёт ожидается /home/ubuntu/waves/3395CAPISOLATIONPREFLIGHT-REPORT.md. Живой процесс модели подтверждён; возраст журнала 142 с. Source/подготовка и независимая проверка — в объёме брифа; это не посадка, не live/capacity GO и не окончательное закрытие security.
REFILL-20260910-2155-3410
Снимок 2026-09-10T21:54:52.066911+00:00. Волна 3410 / acc-cap-isolation на A1: blocked. Не начала проверку: A1 OAuth expired; Claude ограничен лимитами по уточнению владельца. Бриф сохранён в hold-auth-20260910. Финальная приёмка не выполнена. Новых посадок и измерений capacity нет; source QA не означает окончательное закрытие Security.
CODEX-RESUMED-M4-20260910-3410
2026-09-10T22:01:27.591835+00:00: A1, волна 3410, Codex gpt-5.6-terra, queued. Бриф в очереди A1, выполнение ещё не началось. Exact BASE 8a0849c6cc8541a54628f31ac1bf37a7d03278a2; входы /home/wave/waves/inputs/3410-acc-cap-isolation-codex; guard, SHA256 и bundle проверены до постановки. Владелец прямо разрешил Codex для security-приёмок; прежний блокер Claude снят. Нового verdict или посадки этим запуском нет.
REFILL-2221-RESULTS-WEB-629
Снимок 2026-09-10T22:27:17.204978+00:00
3410 NO-GO: collision baseline/current DB id, одинаковый DB/Redis endpoint, потеря точности disk arithmetic. Доработка3429, измерений capacity нет.
Волна 3429 a1nc gpt-5.6-terra: active (дочерний процесс подтверждён, журнал 38 с); exact BASE eed78fc01d2bf70c278b05c5d273e680f8f9e8ab; inputs /home/wave/waves/inputs/3429-cap-isolation-repair
Новой посадки этим тиком нет. Evidence: nc-ops-scripts/refill-20260910-2221/.
WORKER-PROGRESS-RECOVERY-3441
2026-09-10T22:35:58.616092+00:00 3429 source checker исправлен; независимая проверка трёх исходных RED, live isolation UNKNOWN. Волна 3441 a1nc gpt-5.6-luna active; BASE bd2082ced9c1635ac74d23dd29c159e9e7cd32d2; guard/SHA/bundle до постановки проверены. Новых посадок нет.
## HANDOFF-20260912T1721:WEB-629 — точка входа для нового агента
Правила обогащения: [[WEB-449]]. Наблюдение: 12.09.2026 18:21:36 Europe/Dublin / 17:21:36 UTC. Автор: координатор. История выше сохранена; эту запись читать как текущий handoff на указанное время.
**Задача.** Enterprise-2 / CAP-02: Изолированный стенд, egress и preflight
**Что подтверждено и что остаётся.** Сводный последний результат:12/14 source/component частей; это не12/14 законченных runtime задач. r8c SAVE_READBACK_PASS: реальный Auth.js owner,2MiB документ, socket init, exact save200, authoritative readback/размер/SHA/revision, stale CAS refusal. Shared editor/k6 application T0/load/soak ещё не подтверждены. Ёмкость UNKNOWN.
**Как устроено / почему остановилось.** r8b отказал, потому что TeamMember=commenter перекрывает SharedNotebook=editor. r8c прошла owner-путём, поэтому shared access требует отдельной положительной проверки. Длительные измерения на A2 исключают сборку: обязательны одновременно /home/ubuntu/.coordinator-release-build.lock и /home/ubuntu/capacity-3516/runtime-t0.lock. На времени наблюдения compat build активна; Neo-подготовка от этого не зависит.
**Следующая операция.** После свободных обоих locks выполнить штатный isolated preflight: отдельные PG/Redis/storage/порты/identities, запрет prod egress, source/artifact binding, cleanup. Не копировать prod .env.
**Исполнитель и приёмка.** Исполнитель Enterprise; координатор отвечает за выделенное окно и штатный runner. 3532 на Neo — назначение, живой старт пока не подтверждён. Независимый приёмщик получает frozen manifest и raw evidence.
**Условие закрытия.** Негативные проверки запрещают prod destinations/secrets; T0 стенда и cleanup пройдены. Источник и установленный artifact совпадают, запуск не влияет на пользователей. Общий итог и зависимости: [[WEB-626]].
**КАРТА ДОКУМЕНТОВ**
- Intel: /Users/annakorin/nc-ops-scripts/enterprise2-20260910/ENTERPRISE-2.md
- Intel: /Users/annakorin/nc-ops-scripts/tick-20260912-recovery/capacity-r8-coordinator/collected-r8c/collection-receipt.json
- Intel: /Users/annakorin/nc-ops-scripts/tick-20260912-recovery/3522/3522ACCCAPACITYAUTHRUNTIME-REPORT.md
- A2: /home/ubuntu/capacity-3516/; runtime-t0-r3/; собственные run-owned evidence
- Intel: /Users/annakorin/nc-ops-scripts/tick-20260912-recovery/capacity-r8-coordinator/launch-receipt.json
SHIFT-STOP-20260912:FINAL:WEB-629
Enterprise-2 / CAP-02: Изолированный стенд, egress и preflight
Срез перед остановкой 12.09.2026. По поручению владельца 18:13:55 UTC новая работа остановлена.
Enterprise-2: ранее принято 12/14 source/component частей; число не означает готовность 12/14 runtime задач. Ёмкость UNKNOWN. R3 source e1f6cb526d946af339c213ea77eb26c8d77594d2, buildId R7L9roppA5xpq7qPoQmCR.
r8c SAVE_READBACK_PASS: настоящий Auth.js owner, 2MiB документ, Socket.IO init, exact save и authoritative readback/размер/SHA/revision, stale CAS refusal; 8 файлов сверены. Собственная PG остановлена, unit inactive/MainPID0. Shared editor остаётся OPEN из-за effective TeamMember=commenter.
3532 с Нео сохранена: HEAD4182011579fad418f443fc0f40e1ec54e56b159c, bundle SHA32723bf7931419e8c7920c9cb3c03e5fa626ad62edf68d56e950024268571abe. Author scoped GO: Flight decoder, fresh server-owned CAS/save bootstrap, exact one-save/readback checks; offline9/9, syntax3. k6 execution/applicationT0/load/spike/soak NOT_RUN. Новая независимая приёмка3532 не присваивалась.
Следующий запуск требует двух locks: /home/ubuntu/.coordinator-release-build.lock и /home/ubuntu/capacity-3516/runtime-t0.lock; свежего TTL manifest, private sessions/bootstrap и отсутствия параллельной сборки/нагрузки. Реальные credentials не публиковать.
Файлы Intel: /Users/annakorin/nc-ops-scripts/enterprise-3532/delivery/3532CAPACITYK6RUNTIME-REPORT.md (полная инструкция bootstrap/generator/k6); bundle и evidence рядом. /Users/annakorin/nc-ops-scripts/tick-20260912-recovery/capacity-r8-coordinator/collected-r8c/collection-receipt.json. Исторические отчёты/отказы сохранены.
ИНДИВИДУАЛЬНЫЙ ОСТАТОК
Новый run-owned TTL manifest, независимые DB/Redis/storage/auth/stub, egress negative и ready/cleanup evidence. Старый manifest не продлевать ручной правкой.
Следующая смена начинает только после возобновления владельцем. Исторический status сохранён; STOPPED означает отсутствие выполняемой работы, не закрытие задачи.
Чем закрывается (приёмка)
Негативные проверки запрещают prod destinations/secrets; T0 стенда и cleanup пройдены. Источник и установленный artifact совпадают, запуск не влияет на пользователей.
Доказательства
DISPATCH-20260910-3395
На 2026-09-10T21:38:23.260126+00:00: active; A2, волна 3395 cap-isolation-preflight. Exact BASE 1ad52e16b784d3ae80a148fddae4d5a3ab4a89c4. Бриф прошёл brief-guard; входы SHA256 и наличие BASE проверены до постановки. Входы /home/ubuntu/waves/inputs/3395-cap-isolation-preflight. Отчёт ожидается /home/ubuntu/waves/3395CAPISOLATIONPREFLIGHT-REPORT.md. Живой процесс модели подтверждён; возраст журнала 142 с. Source/подготовка и независимая проверка — в объёме брифа; это не посадка, не live/capacity GO и не окончательное закрытие security.
REFILL-20260910-2155-3410
Снимок 2026-09-10T21:54:52.066911+00:00. Волна 3410 / acc-cap-isolation на A1: blocked. Не начала проверку: A1 OAuth expired; Claude ограничен лимитами по уточнению владельца. Бриф сохранён в hold-auth-20260910. Финальная приёмка не выполнена. Новых посадок и измерений capacity нет; source QA не означает окончательное закрытие Security.
CODEX-RESUMED-M4-20260910-3410
2026-09-10T22:01:27.591835+00:00: A1, волна 3410, Codex gpt-5.6-terra, queued. Бриф в очереди A1, выполнение ещё не началось. Exact BASE 8a0849c6cc8541a54628f31ac1bf37a7d03278a2; входы /home/wave/waves/inputs/3410-acc-cap-isolation-codex; guard, SHA256 и bundle проверены до постановки. Владелец прямо разрешил Codex для security-приёмок; прежний блокер Claude снят. Нового verdict или посадки этим запуском нет.
REFILL-2221-RESULTS-WEB-629
Снимок 2026-09-10T22:27:17.204978+00:00
3410 NO-GO: collision baseline/current DB id, одинаковый DB/Redis endpoint, потеря точности disk arithmetic. Доработка3429, измерений capacity нет.
Волна 3429 a1nc gpt-5.6-terra: active (дочерний процесс подтверждён, журнал 38 с); exact BASE eed78fc01d2bf70c278b05c5d273e680f8f9e8ab; inputs /home/wave/waves/inputs/3429-cap-isolation-repair
Новой посадки этим тиком нет. Evidence: nc-ops-scripts/refill-20260910-2221/.
WORKER-PROGRESS-RECOVERY-3441
2026-09-10T22:35:58.616092+00:00 3429 source checker исправлен; независимая проверка трёх исходных RED, live isolation UNKNOWN. Волна 3441 a1nc gpt-5.6-luna active; BASE bd2082ced9c1635ac74d23dd29c159e9e7cd32d2; guard/SHA/bundle до постановки проверены. Новых посадок нет.
THREAD-STATUS-2240 3441 independent checker source GO. Реального изолированного стенда/измерений это не доказывает.
HANDOFF-20260912T1721:WEB-629 — автономный handoff добавлен в body: причина, механизм, подтверждённое/OPEN, следующая операция, ответственный, критерий закрытия и карта документов. Срез фактов 2026-09-12T17:21:36.454887+00:00. Исторические отказы и статусы сохранены. Полный аудит записи: Intel /Users/annakorin/nc-ops-scripts/release-l115f/handoff-tickets/.
SHIFT-STOP-20260912:FINAL:WEB-629
Enterprise-2 / CAP-02: Изолированный стенд, egress и preflight
Срез перед остановкой 12.09.2026. По поручению владельца 18:13:55 UTC новая работа остановлена.
Enterprise-2: ранее принято 12/14 source/component частей; число не означает готовность 12/14 runtime задач. Ёмкость UNKNOWN. R3 source e1f6cb526d946af339c213ea77eb26c8d77594d2, buildId R7L9roppA5xpq7qPoQmCR.
r8c SAVE_READBACK_PASS: настоящий Auth.js owner, 2MiB документ, Socket.IO init, exact save и authoritative readback/размер/SHA/revision, stale CAS refusal; 8 файлов сверены. Собственная PG остановлена, unit inactive/MainPID0. Shared editor остаётся OPEN из-за effective TeamMember=commenter.
3532 с Нео сохранена: HEAD4182011579fad418f443fc0f40e1ec54e56b159c, bundle SHA32723bf7931419e8c7920c9cb3c03e5fa626ad62edf68d56e950024268571abe. Author scoped GO: Flight decoder, fresh server-owned CAS/save bootstrap, exact one-save/readback checks; offline9/9, syntax3. k6 execution/applicationT0/load/spike/soak NOT_RUN. Новая независимая приёмка3532 не присваивалась.
Следующий запуск требует двух locks: /home/ubuntu/.coordinator-release-build.lock и /home/ubuntu/capacity-3516/runtime-t0.lock; свежего TTL manifest, private sessions/bootstrap и отсутствия параллельной сборки/нагрузки. Реальные credentials не публиковать.
Файлы Intel: /Users/annakorin/nc-ops-scripts/enterprise-3532/delivery/3532CAPACITYK6RUNTIME-REPORT.md (полная инструкция bootstrap/generator/k6); bundle и evidence рядом. /Users/annakorin/nc-ops-scripts/tick-20260912-recovery/capacity-r8-coordinator/collected-r8c/collection-receipt.json. Исторические отчёты/отказы сохранены.
ИНДИВИДУАЛЬНЫЙ ОСТАТОК
Новый run-owned TTL manifest, независимые DB/Redis/storage/auth/stub, egress negative и ready/cleanup evidence. Старый manifest не продлевать ручной правкой.
Следующая смена начинает только после возобновления владельцем. Исторический status сохранён; STOPPED означает отсутствие выполняемой работы, не закрытие задачи.
Лента
2026-09-10T19:40:36.719Z · coordinatorENTERPRISE2-LINKS:CAP-02
Parent: WEB-626
Зависимости: WEB-627 (CAP-00), WEB-628 (CAP-01)
Спецификация целиком в WEB-626. Порядок: подготовка → изолированный стенд/T0 → выделенные измерения → независимый итог. CAP-13 опциональный.
2026-09-10T21:39:01.203Z · coordinatorDISPATCH-20260910-3395
На 2026-09-10T21:38:23.260126+00:00: active; A2, волна 3395 cap-isolation-preflight. Exact BASE 1ad52e16b784d3ae80a148fddae4d5a3ab4a89c4. Бриф прошёл brief-guard; входы SHA256 и наличие BASE проверены до постановки. Входы /home/ubuntu/waves/inputs/3395-cap-isolation-preflight. Отчёт ожидается /home/ubuntu/waves/3395CAPISOLATIONPREFLIGHT-REPORT.md. Живой процесс модели подтверждён; возраст журнала 142 с. Source/подготовка и независимая проверка — в объёме брифа; это не посадка, не live/capacity GO и не окончательное закрытие security.
2026-09-10T21:55:31.013Z · coordinatorREFILL-20260910-2155-3410
Снимок 2026-09-10T21:54:52.066911+00:00. Волна 3410 / acc-cap-isolation на A1: blocked. Не начала проверку: A1 OAuth expired; Claude ограничен лимитами по уточнению владельца. Бриф сохранён в hold-auth-20260910. Финальная приёмка не выполнена. Новых посадок и измерений capacity нет; source QA не означает окончательное закрытие Security.
2026-09-10T22:02:11.053Z · coordinatorCODEX-RESUMED-M4-20260910-3410
2026-09-10T22:01:27.591835+00:00: A1, волна 3410, Codex gpt-5.6-terra, queued. Бриф в очереди A1, выполнение ещё не началось. Exact BASE 8a0849c6cc8541a54628f31ac1bf37a7d03278a2; входы /home/wave/waves/inputs/3410-acc-cap-isolation-codex; guard, SHA256 и bundle проверены до постановки. Владелец прямо разрешил Codex для security-приёмок; прежний блокер Claude снят. Нового verdict или посадки этим запуском нет.
2026-09-10T22:27:17.749Z · coordinatorREFILL-2221-RESULTS-WEB-629
Снимок 2026-09-10T22:27:17.204978+00:00
3410 NO-GO: collision baseline/current DB id, одинаковый DB/Redis endpoint, потеря точности disk arithmetic. Доработка3429, измерений capacity нет.
Волна 3429 a1nc gpt-5.6-terra: active (дочерний процесс подтверждён, журнал 38 с); exact BASE eed78fc01d2bf70c278b05c5d273e680f8f9e8ab; inputs /home/wave/waves/inputs/3429-cap-isolation-repair
Новой посадки этим тиком нет. Evidence: nc-ops-scripts/refill-20260910-2221/.
2026-09-10T22:36:39.413Z · coordinatorWORKER-PROGRESS-RECOVERY-3441
2026-09-10T22:35:58.616092+00:00 3429 source checker исправлен; независимая проверка трёх исходных RED, live isolation UNKNOWN. Волна 3441 a1nc gpt-5.6-luna active; BASE bd2082ced9c1635ac74d23dd29c159e9e7cd32d2; guard/SHA/bundle до постановки проверены. Новых посадок нет.
2026-09-10T22:53:37.901Z · coordinatorTHREAD-STATUS-2240 3441 independent checker source GO. Реального изолированного стенда/измерений это не доказывает.
2026-09-12T10:16:35.623Z · coordinatorCAP3516-PROVIDER-ISOLATION-R1:WEB-629
Проверен запуск тестового provider emulator на A2 в отдельном systemd PrivateNetwork, exact source e1f6cb526d946af339c213ea77eb26c8d77594d2. В пространстве сети только loopback; исходящие IPv4 и IPv6 соединения отвергнуты ENETUNREACH. Provider readiness=true, activeRequests=0; процесс штатно остановлен exit0, порт закрыт, unit inactive/MainPID0. Шесть файлов доказательств скопированы, SHA256 совпали. Это проверка окружения и жизненного цикла provider, не вызов через приложение и не T0: applicationT0=NOT_RUN, capacity=UNKNOWN. При запуске приложения его собственная изоляция должна быть проверена заново. Evidence: nc-ops-scripts/capacity-3516-linux/provider-isolation-probe-r1/receipt.json; provider-isolation-collection.json.
2026-09-12T18:27:04.838Z · coordinatorSHIFT-STOP-20260912:FINAL:WEB-629
Enterprise-2 / CAP-02: Изолированный стенд, egress и preflight
Срез перед остановкой 12.09.2026. По поручению владельца 18:13:55 UTC новая работа остановлена.
Enterprise-2: ранее принято 12/14 source/component частей; число не означает готовность 12/14 runtime задач. Ёмкость UNKNOWN. R3 source e1f6cb526d946af339c213ea77eb26c8d77594d2, buildId R7L9roppA5xpq7qPoQmCR.
r8c SAVE_READBACK_PASS: настоящий Auth.js owner, 2MiB документ, Socket.IO init, exact save и authoritative readback/размер/SHA/revision, stale CAS refusal; 8 файлов сверены. Собственная PG остановлена, unit inactive/MainPID0. Shared editor остаётся OPEN из-за effective TeamMember=commenter.
3532 с Нео сохранена: HEAD4182011579fad418f443fc0f40e1ec54e56b159c, bundle SHA32723bf7931419e8c7920c9cb3c03e5fa626ad62edf68d56e950024268571abe. Author scoped GO: Flight decoder, fresh server-owned CAS/save bootstrap, exact one-save/readback checks; offline9/9, syntax3. k6 execution/applicationT0/load/spike/soak NOT_RUN. Новая независимая приёмка3532 не присваивалась.
Следующий запуск требует двух locks: /home/ubuntu/.coordinator-release-build.lock и /home/ubuntu/capacity-3516/runtime-t0.lock; свежего TTL manifest, private sessions/bootstrap и отсутствия параллельной сборки/нагрузки. Реальные credentials не публиковать.
Файлы Intel: /Users/annakorin/nc-ops-scripts/enterprise-3532/delivery/3532CAPACITYK6RUNTIME-REPORT.md (полная инструкция bootstrap/generator/k6); bundle и evidence рядом. /Users/annakorin/nc-ops-scripts/tick-20260912-recovery/capacity-r8-coordinator/collected-r8c/collection-receipt.json. Исторические отчёты/отказы сохранены.
ИНДИВИДУАЛЬНЫЙ ОСТАТОК
Новый run-owned TTL manifest, независимые DB/Redis/storage/auth/stub, egress negative и ready/cleanup evidence. Старый manifest не продлевать ручной правкой.
Следующая смена начинает только после возобновления владельцем. Исторический status сохранён; STOPPED означает отсутствие выполняемой работы, не закрытие задачи.
2026-09-12T22:25:57.427Z · coordinator[12.09 22:25Z координатор] ОБОГАЩЕНИЕ 12.09 (3571-enrich-web626):
Сделано: source checker после NO-GO 3410/доработки 3429 принят (3441 GO); проба provider emulator в PrivateNetwork — egress отвергнут ENETUNREACH.
На проде: НЕТ — изоляция проверялась только на A2, к прод-стенду отношения не имеет.
Доказано: provider-isolation-probe-r1/receipt.json (6 файлов SHA сверены).
Осталось: T0 самого приложения + его собственная изоляция, cleanup, совпадение source/artifact; свежий TTL manifest.
Кто следующий: координатор после подъёма юнитов nc-cap3516-*.
Ссылки: /home/ubuntu/waves/3395CAPISOLATIONPREFLIGHT-REPORT.md; /home/wave/waves/inputs/3410-acc-cap-isolation-codex; /home/wave/waves/inputs/3429-cap-isolation-repair; /Users/annakorin/nc-ops-scripts/capacity-3516-linux/provider-isolation-probe-r1/{receipt.json,provider-isolation-collection.json}; commits 8a0849c6cc8541a54628f31ac1bf37a7d03278a2, eed78fc01d2bf70c278b05c5d273e680f8f9e8ab, bd2082ced9c1635ac74d23dd29c159e9e7cd32d2
Отчёт волны: /Users/milamarty/waves/3571ENRICH*-REPORT.md (M4), копия nc-ops-scripts/shift-20260912-resume/wash-20260912/enrich-collected/. 2026-09-14T17:40:46.124Z · coordinator[14.09 17:40Z координатор] # WEB-629 — Enterprise-2 / CAP-02: Изолированный стенд, egress и preflight
**Блок подготовлен волной 3898 (2026-09-14) для публикации координатором.**
Статус не двигать. Волна 3898 комментарии не публиковала.
**Метки источника:** `[задание]` — дословно из задания 3898; `[3898]` — проверено
самой волной 3898 на `a1-payg-01`; `[3875-отчёт]` — прочитано из файла на A1.
**Это тикет, который держит достоверность ВСЕХ чисел эпика.** Ниже — два факта,
делающих целые окна замеров недействительными.
---
## 1. СНЯТО — НЕ ИСПОЛЬЗОВАТЬ КАК ФАКТ
| снятое утверждение | чем опровергнуто |
|---|---|
| «между A1 и A2 нет сети» (как характеристика изоляции стенда) | мерились **внутренние** адреса; по внешним ping **0.821 мс** `[задание]` |
| «стенд изолирован, значит числа сравнимы» (неявная посылка всех прогонов 14.09) | в окне **14:56Z–15:21:38Z** Postgres **не был прижат** к ядрам 0,1 вопреки `taskset` — числа того окна **недействительны** `[задание]` |
### Эволюция: почему изоляция считалась данностью и что её опровергло
`taskset` стоял в `run-rung-r4.sh`, его видно в скрипте, и этого казалось достаточно.
Волна 3875 проверила аффинность **не по тому, что просил скрипт, а по тому, что выдал
ядру**, прочитав живые pid: `k6` → 2,3; `room-audio` → 2,3; приложение :3610 (pid
261374) → 0,1; приложение :3611 → 0,1; postmaster PG (134 pid в дереве) → 0,1
`[3875-отчёт §2.4]`. В её окне 15:24:40–15:26:41Z `taskset` **действовал**.
А в окне **14:56Z–15:21:38Z** — нет: Postgres оказался не прижат `[задание]`. Из этого
следует жёсткое правило: **аффинность — это измерение, а не настройка.** Её надо
снимать на живых pid в каждом прогоне и класть в манифест прогона, иначе «изоляция»
остаётся намерением.
Найденная попутно дырка в том же приборе: в `run-rung-r4.sh` `k6` и демон получают
ядра 2,3, а ребёнок `sampler.sh` получает **0-3** — то есть **сэмплер крутится на тех
самых ядрах, которые измеряет** `[3875-отчёт §2.4]`.
---
## 2. ДОКАЗАНО ЗАМЕРАМИ
1. **Аффинность проверяема и в окне 15:24:40–15:26:41Z была верной** — список pid и
масок выше `[3875-отчёт §2.4]`.
2. **Окно 14:56Z–15:21:38Z недействительно** `[задание]`.
3. **Кластер PG подменён внутри смены.** Волна 3864 подняла `max_connections`
**100 → 400** и **перезапустила** кластер в **14:56Z**; любая выборка
`pg_stat_activity` до этого момента относится к **другому экземпляру кластера**
`[3875-отчёт §11.6]`.
4. **Четыре волны на одном стенде.** 14.09 на одной 4-ядерной машине работали 3864,
3867, 3877 и 3875. Две ступени испортили друг друга расхождением старта в
**8 секунд**: демон волны 3877 занял фиксированный порт **18099**
(`room-audio-daemon.mjs:94`, `CAP06_ROOM_AUDIO_REOPEN_PORT || 18099` — жёсткий
дефолт), и ступень 3864 умерла с **EADDRINUSE** `[3875-отчёт §2.4, §3]`.
5. **Граница стенда на A1 — проверено волной 3898 изнутри:** слушателей на 3610/3611
**ноль**; наружу смотрят только `22, 80, 443, 5060, 5061, 8088, 4080, 4084, 4573`
`[3898: evidence/02-a1-listeners.txt]`.
6. **Egress с A2 на A1** — отвечают ровно `22, 80, 443`; `3030/3610/3611/55432/6379`
closed/filtered `[3875-отчёт §1.2]`.
---
## 3. ОТКРЫТО
1. **Сеть под замер открыта 14.09** — правило Oracle, порты 3610-3611 с A2,
**временное** `[задание]`. Отсюда **не проверено**; и без слушателя на A1 оно не
даёт плеча (см. WEB-636).
2. **Preflight, который бы это ловил, не существует.** Нужен набор проверок, который
до старта прогона отвечает: одна ли волна на машине; какая аффинность реально у
приложения, PG, k6 и демона; тот ли это экземпляр кластера; свободен ли порт демона;
валиден ли манифест цели по `expiresAt`.
3. **Фиксированный порт 18099 не исправлен** `[3875-отчёт §11]`.
4. **Процессное правило «одна волна на стенде» не зафиксировано.**
---
## 4. С ЧЕГО НАЧАТЬ НУЛЕВОМУ АГЕНТУ
**Куда смотреть:** `run-rung-r4.sh` (где стоит `taskset` и где его нет — у `sampler.sh`);
`room-audio-daemon.mjs:94`; `3875CAP09-evidence/step2-rig-vs-app.log`;
`tools-3882/rung-3882.sh` — там уже есть проверка чужого k6 и запись аффинности до и
после `[3882-README]`.
**Первый шаг (секунды):** `pgrep -af 'claude -p # [0-9]'` — если на машине не только ты,
preflight провален, и дальше идти нельзя.
Затем — прочитать реальные маски: `taskset -pc <pid>` для приложения, postmaster, k6 и
демона, и положить их в манифест прогона.
**Что считается готовым:** preflight-скрипт, который **fail-closed** останавливает
прогон при: чужой волне на машине; несовпадении реальной аффинности с заявленной;
занятом порте демона; истёкшем манифесте цели; неизвестном времени старта кластера PG.
И запись всех этих значений в манифест каждого прогона — чтобы недействительное окно
было видно **из файла**, а не из чужой памяти.
**Чего делать нельзя:**
- считать `taskset` в скрипте доказательством аффинности;
- брать числа из окна 14:56Z–15:21:38Z 14.09;
- сравнивать выборки `pg_stat_activity` через границу 14:56Z — это разные кластеры;
- запускать ступень параллельно чужой волне;
- пересевать `cap3516build` под живыми соседями;
- трогать production, прод-БД, sudo, secrets.
---
## 5. Граница этого блока
Волна 3898 проверила сама только границу A1 изнутри (слушатели, нелокальные привязки).
Аффинность, окно 14:56Z–15:21:38Z, подмена кластера и конфликт четырёх волн — числа
волн 3875 / 3864 / 3877, взятые дословно из задания или прочитанные из отчёта 3875 на
этой машине. Правило Oracle и состояние стенда на A2 **отсюда не проверены**.
2026-09-14T19:07:38.391Z · coordinator[14.09 19:07Z координатор] VERDICT=NO-GO
# 3911 / CAP-02 (WEB-629) — комплект изолированного развёртывания и его отрицательные проверки
**Вывод одной строкой.** Изоляция стенда на сегодня **не доказана и не обеспечена кодом**.
Строгий проверяльщик изоляции в дереве есть и работает правильно — но **он не подключён
ни к одному шагу реального прогона**. То, что реально стоит на пути прогона, пропускает
прод-адрес, платного провайдера, секрет в манифесте и бессрочный срок годности. Поэтому
все числа ёмкости, снятые на этом стенде, сняты в условиях, про которые нельзя сказать
«изолировано».
Отдельно, и это важнее всего для владельца: **по умолчанию у стенда вообще нет денежной
границы.** Проверено запуском (см. `04-paid-vendor-egress-probe.txt`).
---
## Оглавление
1. [Что сделано и чем доказано](#1-что-сделано-и-чем-доказано)
2. [Механизм: что сейчас обеспечивает изоляцию](#2-механизм-что-сейчас-обеспечивает-изоляцию)
3. [Отрицательные проверки и их результат](#3-отрицательные-проверки-и-их-результат)
4. [Источник == артефакт](#4-источник--артефакт)
5. [TTL](#5-ttl)
6. [Сухой прогон очистки](#6-сухой-прогон-очистки)
7. [ДЛЯ ТИКЕТА — детским языком](#7-для-тикета--детским-языком)
8. [Чего НЕЛЬЗЯ сделать волной](#8-чего-нельзя-сделать-волной)
9. [KNOWN ISSUES](#9-known-issues)
10. [Сдача](#10-сдача)
---
## 1. Что сделано и чем доказано
Построены и выполнены две группы проверок, обе — только `node --test`, без сборок,
без сети, без базы:
| группа | файлов | проверок | результат |
|---|---|---|---|
| `waves/3911-cap02/tests/observed/` — что комплект делает **сегодня** | 8 | **55** | **55 зелёных** |
| `waves/3911-cap02/tests/required/` — что изоляция **обязана** гарантировать | 6 | **24** | **24 красных** |
Числа получены прогонами, логи — `3911CAP02-evidence/01-observed-suite.txt` и
`02-required-suite.txt`. Запуск:
```bash
cd <дерево> # ветка wave/3911-cap02-isolated-kit
./waves/3911-cap02/run-checks.sh observed # должно быть 55/55 зелёных
./waves/3911-cap02/run-checks.sh required # сегодня 24/24 красных — это и есть дыры
```
Две группы намеренно дублируют друг друга с разных сторон. Каждая красная
`REQUIRED`-проверка имеет зелёную `OBSERVED`-пару, которая утверждает **то же самое
поведение** как факт. Поэтому «красный тест просто сломан» — не объяснение.
**Как удалось выполнить настоящий код.** k6 на этой машине нет (`00-environment.txt`),
`node_modules` нет, править TypeScript запрещено. Поэтому:
- `tools/k6-gate-loader.mjs` исполняет **исходный текст** `k6-script.js` в `node:vm`:
пять импортов `k6/*` заменены заглушками, четыре относительных импорта загружены
по-настоящему, ключевое слово `export` снято. Тело самих ворот не тронуто; состав
замен печатается в `loaded.transform`, так что изменение формы файла ломает загрузчик
громко, а не молча.
- `tools/ts-resolver.mjs` разрешает `@/x` и импорты без расширения, чтобы штатное
«снятие типов» Node 22 загрузило настоящие `src/lib/billing/*.ts` без сборки.
Единственная внешняя зависимость на этом пути, `@/lib/prisma`, — заглушка, которая
бросает исключение при любом обращении к базе.
- `tools/egress-probe.mjs` форкает по процессу на режим (состояние стража —
процессно-глобальное). Счётчик подставляется в `globalThis.fetch` **до** установки
стража, поэтому `transportCalls=1` означает «байты ушли бы», а не «байты ушли».
- `tests/observed/t0-gate.test.mjs` запускает `3501-t0-gate.mjs` настоящим дочерним
процессом, подставив на `K6_BIN` заглушку.
---
## 2. Механизм: что сейчас обеспечивает изоляцию
Доказано: `03-mechanism-inventory.txt`, `10-strict-checker-cli.txt`.
Механизмов **три**, и они не связаны друг с другом.
### 2.1. Строгий проверяльщик манифеста — `scripts/capacity/isolation/checker.mjs`
678 строк, схема `schemaVersion: 1`. Делает ровно то, что просит CAP-02, и делает хорошо.
Проверено запуском его собственного CLI (`10-strict-checker-cli.txt`):
| подсунуто | код отказа | выход процесса |
|---|---|---|
| эталонный манифест | — | `0` |
| публичный IP `203.0.113.10` в списке разрешённого | `TARGET_ADDRESS_NOT_LITERAL_PRIVATE` | `1` |
| имя `app.production.example.com` | `LIVE_TARGET_ALIAS_FORBIDDEN` | `1` |
| **пустой** список разрешённого | `TARGET_ALLOWLIST_REQUIRED` | `1` |
| нет секции `targets` вовсе | `TARGET_ALLOWLIST_REQUIRED` | `1` |
| поле `connectionString` | `SECRET_FIELD_FORBIDDEN` | `1` |
| истёкший срок | `TTL_EXPIRED` | `1` |
| срок в год | `TTL_OUT_OF_RANGE` | `1` |
| путь `/etc` вне корня прогона | `PATH_OUTSIDE_RUN_ROOT` | `1` |
| `destructiveExecutor: true` | `DESTRUCTIVE_EXECUTOR_FORBIDDEN` | `1` |
Список разрешённого **падает закрыто**: пустой список и отсутствующая секция дают одну и
ту же ошибку, пустой документ `{}` тоже отвергается.
> ⚠️ **И это всё бесполезно на прогоне.** Единственное упоминание `checker.mjs` во всём
> дереве — строка `'scripts/capacity/isolation/checker.mjs'` в списке
> файлов-которые-должны-существовать внутри `3487-coordinator-checklist.mjs:15`.
> Ни одного `import`, ни одного `spawn`. Прогон `run-rung-r4.sh` его не зовёт.
### 2.2. Ворота на реальном пути прогона — `k6-script.js:372-382`
Это то, что действительно исполняется. Схема **другая**: `kind: 'capacity-isolated-target'`.
Полный текст ворот — в `03-mechanism-inventory.txt`. Что они проверяют:
- `kind`, `manifestVersion`, `runId` непустой;
- `targetUrl` **дословно равен** `TARGET_URL`;
- `approved === true`, `providerMode === 'stub'`, `externalEgressBlocked === true`;
- `sourceCommit === APPROVED_APPLICATION_SOURCE_COMMIT` (константа в самом файле,
`1628b697d9b20a4446b9bdba20ada0a6c2820d11`);
- `expiresAt` в будущем;
- в URL нет логина/пароля/якоря.
Чего они **не** проверяют: **где находится адрес**. Нет ни списка разрешённого, ни
понятия приватной сети.
### 2.3. Среда выполнения приложения — `src/lib/billing/vendorEgressGuard.ts`
Настоящая денежная граница: оборачивает `globalThis.fetch`, `node:http/https`, `undici` и
`WebSocket`, отказывает запросу к платному вендору вне авторизованной диспетчеризации.
Устанавливается в `src/instrumentation.ts` при старте сервера.
**Но включается только переменной `SPEND_CONTAINMENT_MODE`.** Измерено
(`04-paid-vendor-egress-probe.txt`, по процессу на строку):
| `SPEND_CONTAINMENT_MODE` | страж установлен | режим | вызовов транспорта | итог |
|---|---|---|---|---|
| *не задана* | **нет** | `off` | **1** | **REACHED_TRANSPORT** |
| `off` | **нет** | `off` | **1** | **REACHED_TRANSPORT** |
| `shadow` | да | `shadow` | **1** | **REACHED_TRANSPORT** (только запись в лог) |
| `enforce` | да | `enforce` | 0 | DENIED |
| мусорная строка | да | `enforce` | 0 | DENIED *(верное направление)* |
И: **ни один файл под `scripts/capacity/` не задаёт и не читает `SPEND_CONTAINMENT_MODE`**
(`grep` в `03-mechanism-inventory.txt` → пусто). То есть изоляция стенда от платных
провайдеров сегодня — это привычка оператора (`OPENAI_BASE_URL` в мёртвый порт,
подставной ключ), а не свойство комплекта. Заплатка на A1, упомянутая в задании,
подтверждается кодом: подставлять руками приходится именно потому, что подставлять
больше нечему.
### 2.4. Где комплект вообще лежит
| ref | коммит | `scripts/capacity/` |
|---|---|---|
| `refs/waves/l115n` (продуктовая линия) | `d85bb2dad1` | **0 файлов** |
| `refs/heads/wave/3818-triple-utf8-encode` (дерево стенда) | `3675752255` | есть |
| `refs/heads/wave/3497-capacity-runtime-seed` | `08f510afab` | есть + `docs/capacity/` |
Комплект изоляции **не входит в линию**. Линия может уехать в релиз без него, и ничто не
заметит.
---
## 3. Отрицательные проверки и их результат
### 3.1. Прод-адрес — ОТКАЗА НЕТ
Доказано: `05-target-gate-matrix.txt` (настоящие ворота, настоящий исходник).
Манифест во всех строках безупречен: `approved=true`, `providerMode=stub`,
`externalEgressBlocked=true`, правильный `sourceCommit`, срок в будущем.
| класс | `TARGET_URL` | вердикт ворот |
|---|---|---|
| петля | `http://127.0.0.1:3610` | ACCEPTED |
| приватная сеть | `http://10.0.0.5:3000` | ACCEPTED |
| **публичный IP** | `http://8.8.8.8:80` | **ACCEPTED** |
| **публичный IP** | `https://203.0.113.10` | **ACCEPTED** |
| **прод по имени** | `https://app.production.example.com` | **ACCEPTED** |
| **прод по имени** | `https://prod-web.internal.example.net` | **ACCEPTED** |
| логин/пароль в URL | `http://u:p@127.0.0.1:3610` | refused |
Единственное, что вообще отвергается по адресу — учётные данные внутри URL.
### 3.2. Платный провайдер — ОТКАЗА НЕТ, на двух уровнях
**Уровень ворот прогона** (`05-target-gate-matrix.txt`):
`https://api.openai.com`, `https://api.anthropic.com`, `https://openrouter.ai` — **ACCEPTED**.
**Уровень T0-ворот** (`06-t0-gate-paid-provider.txt`) — запущено по-настоящему:
```
node scripts/capacity/integration/3501-t0-gate.mjs \
--target-url https://api.openai.com --isolated-manifest ... --runtime-input ...
→ gate exit RC=0
→ шим записал: SHIM_K6_RUN_INVOKED args=run scripts/capacity/harness/k6-script.js
SHIM_TARGET_URL=https://api.openai.com
```
T0-ворота **вышли с нулём и запустили прогон против платного провайдера**, причём
манифест в этом запуске имел `expiresAt: 2019-01-01` — срок годности T0-ворота не смотрят
вообще.
**Уровень приложения**: см. таблицу 2.3 — по умолчанию стража нет.
И даже в `enforce` граница — это **список запрещённого из 17 хостов**, а не список
разрешённого: `api.deepseek.com`, `api.together.xyz`, любой новый вендор — проходят
(`04-paid-vendor-egress-probe.txt`).
### 3.3. Пустой список разрешённого — падает закрыто ТАМ, где список есть
- Строгий проверяльщик: **падает закрыто**. Пустой список → `TARGET_ALLOWLIST_REQUIRED`;
отсутствие секции → та же ошибка; пустой документ → набор ошибок. Зелёные проверки
4, 5, 6 в `01-observed-suite.txt`.
- Путь прогона: **списка разрешённого не существует**. Манифест
`kind=capacity-isolated-target` такого поля не имеет; добавленный вручную
`targets.allowlist: []` просто проезжает как неизвестное поле
(`05-target-gate-matrix.txt`, последняя секция). Падать закрыто нечему.
### 3.4. Секреты — ОТКАЗА НЕТ на пути прогона
- Строгий проверяльщик: любой ключ, похожий на секрет, отвергается
(`SECRET_FIELD_FORBIDDEN`), проверено на `apiKey`, `databaseUrl`, `authHeader`,
`password`, `bearerToken`, `connectionString`.
- Ворота прогона: неизвестные поля не отвергаются и не вычищаются. `apiKey` и
`databaseUrl` **доезжают до прогона** (`05-target-gate-matrix.txt`).
- Плюс: весь манифест едет в k6 **переменной окружения**
(`run-rung-r4.sh:123`), тогда как соседний `runtime-input-*.json` получает `chmod 600`
(`run-rung-r4.sh:47`). Окружение процесса читается из `/proc` любым процессом того же
пользователя.
### 3.5. Живой стенд: заявленная петля против фактического сокета
Снято на этой машине (она и есть `a1-payg-01`) **только чтением** `ss -ltn` —
`11-live-stand-sockets.txt`. Порты 3610/3611 не тронуты: ни подключения, ни сигнала,
ни записи.
```
LISTEN 0 511 0.0.0.0:3610 0.0.0.0:*
LISTEN 0 511 0.0.0.0:3611 0.0.0.0:*
```
Манифест заявляет `targetUrl=http://127.0.0.1:3610`, строгая схема заявила бы
`networkClass: 'loopback'`. Фактический сокет открыт **на всех интерфейсах**. Для
контраста, почти всё остальное на машине (5432, 6379, 55432, 3010, 3012, 3031, 25, 587)
связано с `127.0.0.1`. Ничто в комплекте не сравнивает заявленный класс сети с реальным
адресом привязки.
> Граница: это **одно** чтение в момент, указанный в файле. Чей это процесс — отсюда не
> установлено (колонка процесса требует прав, которых у волны нет). Дошёл бы до порта
> чужой SYN — отсюда тоже не установлено: перед машиной может стоять сетевое правило.
> См. §8.
---
## 4. Источник == артефакт
**Ничто не мешает** запустить стенд из одного дерева, а расписку выдать про другое.
Это дефект, и он обесценивает замеры. Доказано в `08-source-equals-artifact.txt` и
`07-readiness-receipt.txt` — обоими запусками настоящих скриптов.
### 4.1. Цепочка — это четыре копирования строки
```
--source-commit <40 hex> ← напечатано человеком в командной строке
└→ generated-action-manifest.json { sourceCommit }
└→ runtime-input.json { sourceCommit }
isolated-target.json { sourceCommit } ← напечатано человеком в файле
└→ k6-script.js: manifest.sourceCommit === APPROVED_APPLICATION_SOURCE_COMMIT
```
Запущено по-настоящему: в `/tmp` собран «артефакт» из двух файлов, не соответствующий
никакому коммиту. `3501-extract-action-manifest.mjs` проштамповал его **утверждённым**
коммитом `1628b697d9…` и вышел с нулём. Затем тот же артефакт проштампован
несуществующим `c0ffee00…` — тоже ноль. В экстракторе **ноль** упоминаний
`createHash|sha256|rev-parse|spawn|execFile`; слепка артефакта нигде не появляется.
`k6-script.js` не спрашивает у цели, что она такое: в файле нет ни `artifactSha`, ни
`buildId`, ни обращения к версии. Ворота приняли манифест с правильным коммитом, указывая
на произвольный порт (`08-source-equals-artifact.txt`, зелёная проверка 48 в
`01-observed-suite.txt`).
### 4.2. Утверждённый коммит — не то дерево, с которого гоняют
| что | коммит |
|---|---|
| `APPROVED_APPLICATION_SOURCE_COMMIT` в `k6-script.js` | `1628b697d9…` = `refs/heads/acceptance/3755-postqa-l115l`, 2026-09-13 |
| дерево, с которого идёт прогон (`wt-instrument-3818`) | `3675752255` = `refs/heads/wave/3818-triple-utf8-encode`, 2026-09-14 |
Это разные деревья. Коммит **измерительного прибора** не записывается никуда: в отчёте
печатается только продуктовый `sourceCommit`. Две ступени, снятые разными версиями
харнесса, в доказательствах неразличимы.
### 4.3. Расписка готовности проверяет текст, а не стенд
`3487-coordinator-checklist.mjs --check` печатает вердикт
`READY_FOR_OWNER_APPROVED_T0` / `BLOCKED_RUNTIME_INPUTS`. Запущено (`07-readiness-receipt.txt`):
- **А.** как лежит в дереве `3497`: `runtimeReady: false`, все девять проверок `false`.
- **Б.** тот же скрипт, то же дерево, отредактирован **один JSON-файл**
(`docs/capacity/integration/3487-capacity-integration-addons-manifest.json`,
все `status` → `AVAILABLE`): `runtimeReady: **true**`, все девять → `true`, включая
`isolatedTarget: true` и `egress: true`. Единственная проверка, которая вообще трогает
диск, `serverReferenceManifest`, удовлетворена файлом `{}`.
- **В.** тот же скрипт на дереве, с которого реально гоняют стенд: **падает с `ENOENT`**
на первой строке — `docs/capacity/` на `wave/3818-triple-utf8-encode` нет. Выход `1`,
вердикта нет вообще.
То есть: «изолированная цель есть» и «egress доказан» — это строки, скопированные из
файла в репозитории. А любая расписка, которая где-то лежит в доказательствах, была
выпущена **не тем деревом, которое крутило ступени**, потому что на том дереве она не
запускается.
---
## 5. TTL
Срок годности **есть в строгом проверяльщике** и **почти отсутствует на пути прогона**.
| | строгий `checker.mjs` | путь прогона (`isolated-target.json` + k6) | T0-ворота |
|---|---|---|---|
| поле срока | `expiresAtUtc` + `ttlHours` | `expiresAt` | — |
| истёк → отказ | **да** (`TTL_EXPIRED`) | **да** | **нет** |
| нет поля → отказ | **да** (`INVALID_UTC_TIMESTAMP`) | **да** | **нет** |
| потолок срока | **да, 168 часов** (`TTL_OUT_OF_RANGE`) | **нет** | **нет** |
| согласованность `created+ttl == expires` | **да** (`TTL_TIMESTAMP_CONTRADICTION`) | нет поля `ttlHours` | — |
Что происходит по истечении: на пути прогона — k6 бросает `isolated manifest expired` и
ступень не стартует (зелёная проверка 33). Это работает. Чего нет: **потолка**. Ворота
принимают `expiresAt: 2099-01-01` и `2199-01-01` (`05-target-gate-matrix.txt`). Разрешение
на 73 года — это, как сказано в задании, не разрешение.
T0-ворота срок не читают вообще: их единственная защита — то, что дальше запустится k6.
А какой именно двоичный файл запустится, решает `K6_BIN` (по умолчанию `k6` из `PATH`), и
проверяется он только текстом ответа на `k6 version`; контрольной суммы против
`k6-version.json` нет. Шим из четырёх строк прошёл эту проверку (`06-…`).
---
## 6. Сухой прогон очистки
Доказано: `09-cleanup-dry-run.txt` — на настоящем дереве каталогов, со сверкой sha256
каждого файла до и после.
**Что хорошо — и это проверено байтами:**
- `planCleanup()` вернул `ok=true, executed=false, mode='dry-run-only'`, все восемь
действий с `executed:false` и областью `run-owned-only` / `manifest-owned-only`;
- инвентарь из шести файлов (пять «своих» + один соседский) **побайтово идентичен** до и
после; соседский `someone-elses-run/their.dat` цел;
- на невалидном манифесте (пустой список разрешённого) план — `ok=false, actions: []`.
Очистка **падает закрыто**: не смогли проверить — не показываем, что удалять.
**Что не работает — «ровно то, что создала» проверить нельзя:**
- ни одно действие не называет путь. В плане: `{"kind":"remove-run-owned-data",
"resource":"database.data","scope":"manifest-owned-only"}`. Какой каталог — не сказано;
- план **не упоминает ни корень прогона, ни его идентификатор** (проверено подстрокой);
- план для **другого** прогона **побайтово тот же самый**. План нельзя отнести к прогону,
и план для прогона A неотличим от плана, который снёс бы прогон B;
- четыре управляемых пути Postgres описаны в манифесте и проверены на вложенность в корень
прогона — но **в план не попадают**;
- `planCleanup` во всём `scripts/` упоминается только в файле, который его определяет.
**Исполнителя нет.** Уборка на стенде — это то, что оператор наберёт руками.
Итог: сухой прогон **честно сухой**, но описывает уборку, которую никто не делает и
которую не с чем сверить.
---
## 7. ДЛЯ ТИКЕТА — детским языком
### Что проверяли
Мы хотели понять простую вещь: **если стенд, на котором мы меряем скорость, вдруг
попробует дозвониться до настоящего сайта или до платного робота — ему откажут?**
Потому что если не откажут, то любой наш замер может либо помешать живым людям, либо
потратить деньги владельца.
### Что нашли
**1. В доме есть хороший замок — но он лежит в ящике.**
В коде есть проверяльщик (`checker.mjs`), который умеет всё правильно: не пускает
публичные адреса, не пускает имена вроде «production», ругается на пустой список
разрешённого, ругается на просроченный пропуск, ругается на пароль в бумажке. Мы его
запустили десять раз с разными плохими бумажками — он десять раз сказал «нет».
**Но когда стенд на самом деле запускают, этот проверяльщик никто не зовёт.** Во всём коде
есть ровно одно место, где он упомянут, — и это просто список файлов, которые должны
лежать на диске.
**2. То, что на самом деле стоит на двери, не смотрит на адрес.**
Настоящая дверь — это несколько строк внутри `k6-script.js`. Они проверяют, что в бумажке
написаны правильные слова: «одобрено», «провайдер поддельный», «наружу не ходим». Слова —
проверяют. **А куда именно идёт стенд — нет.** Мы подставили `https://api.openai.com`,
`https://app.production.example.com`, `8.8.8.8` — дверь открылась во всех трёх случаях.
**3. Ворота «пуск» тоже пустили.**
Есть отдельный скрипт `3501-t0-gate.mjs`, который решает, можно ли начинать замер. Мы его
запустили по-настоящему, дав ему адрес платного робота и пропуск, который **просрочен с
2019 года**. Скрипт сказал «всё в порядке» и запустил прогон.
**4. Денежной защиты по умолчанию просто нет.**
У продукта есть настоящая защита от платных вызовов (`vendorEgressGuard`), и она хорошая.
Но она включается переменной `SPEND_CONTAINMENT_MODE`, и **если её не задать — защита не
устанавливается вообще**. Мы проверили каждый режим отдельным процессом: «не задана»,
«off», «shadow» — вызов к `api.openai.com` доходит до отправки. Только `enforce`
отказывает. И **ни один файл в хозяйстве стенда эту переменную не задаёт**. Поэтому на A1
и пришлось затыкать дыру руками — затыкать было больше нечем.
**5. Бумажка про «какой это код» — просто строчка, которую печатают руками.**
Номер версии кода (`--source-commit`) — это текст, который человек набирает в команде.
Мы собрали в `/tmp` фальшивый «артефакт» из двух пустых файлов и проштамповали его
утверждённым номером версии. Программа согласилась. Потом проштамповали тем же артефактом
несуществующий номер — тоже согласилась. Никто не считает контрольную сумму, никто не
спрашивает у работающего сервера, кто он такой.
**6. Расписка «всё готово» проверяет текст, а не стенд.**
Есть скрипт, который печатает «ГОТОВО» или «ЗАБЛОКИРОВАНО». Мы отредактировали **один
JSON-файл** в репозитории — и все девять проверок готовности, включая «изолированная цель
есть» и «выход наружу закрыт», стали «правда». Стенда при этом не существовало.
А на том дереве, с которого стенд реально гоняют, этот скрипт **вообще не запускается** —
падает с «файл не найден».
**7. Живой стенд слушает на всех интерфейсах.**
Мы посмотрели (только посмотрели, `ss -ltn`, ничего не трогали): порты 3610 и 3611 открыты
на `0.0.0.0` — на всех сетевых картах. В бумажке написано `127.0.0.1`. Почти всё остальное
на машине честно сидит на `127.0.0.1`. Никто эти две вещи между собой не сверяет.
**8. Уборка действительно ничего не трогает — но и сказать не может, что уберёт.**
Сухой прогон мы проверили побайтово: до и после — те же самые файлы, соседский каталог
цел. Это хорошо. Плохо другое: в плане уборки **не написано ни одного пути**. Написано
«database.data», а какой это каталог — нигде. План для одного прогона и для другого —
буква в букву одинаковый. И исполнителя у этого плана в коде нет.
### Чем доказано
Всё вышеперечисленное — не чтение кода, а **запуски**:
- `3911CAP02-evidence/01-observed-suite.txt` — 55 проверок, все зелёные;
- `02-required-suite.txt` — 24 проверки, все красные (каждая красная = дыра);
- `04-paid-vendor-egress-probe.txt` — таблица по режимам, по процессу на строку;
- `05-target-gate-matrix.txt` — настоящие ворота против 11 адресов;
- `06-t0-gate-paid-provider.txt` — T0-ворота запущены против платного провайдера;
- `07-readiness-receipt.txt` — расписка запущена трижды;
- `08-source-equals-artifact.txt` — фальшивый артефакт проштампован дважды;
- `09-cleanup-dry-run.txt` — сверка sha256 до и после;
- `10-strict-checker-cli.txt` — CLI проверяльщика на десяти плохих манифестах;
- `11-live-stand-sockets.txt` — чтение `ss -ltn` на этой машине.
### ГДЕ ГРАНИЦА
Отсюда **проверено**:
- исходники изоляции на `refs/waves/l115n` и `refs/heads/wave/3818-triple-utf8-encode`;
- поведение `checker.mjs`, `k6-script.js`-ворот, `3501-t0-gate.mjs`,
`3501-extract-action-manifest.mjs`, `3487-coordinator-checklist.mjs`,
`planCleanup` — запусками;
- поведение `vendorEgressGuard.ts` / `containmentMode.ts` / `paidVendorHosts.ts` —
запусками с подставным транспортом;
- один снимок слушающих сокетов этой машины.
Отсюда **НЕ проверено** и в отчёте как факт не используется:
- реальная конфигурация процессов стенда на A1: какие переменные окружения у живого
приложения на 3610/3611, задан ли им `SPEND_CONTAINMENT_MODE`, куда указывает
`OPENAI_BASE_URL`. Чтение чужого `/proc/<pid>/environ` — за границей волны;
- дошёл бы до 3610/3611 пакет снаружи машины. Сокет на `0.0.0.0` — необходимое условие,
но не достаточное: перед машиной может стоять сетевое правило облака;
- состояние A2 и правило Oracle, открытое 14.09;
- содержимое настоящего `$STAND/inputs/isolated-target.json` на A1: единственный
доступный отсюда экземпляр `run-rung-r4.sh` — **копия «как поставлено»** в
`3859REPLAY2-evidence`, а не живой файл на стенде;
- никакой k6 не запускался: k6 на этой машине нет. Ворота исполнены как исходный текст в
`node:vm`, а T0-ворота — с заглушкой на `K6_BIN`;
- ни одного числа ёмкости эта волна не снимала и не перепроверяла.
### Что осталось открытым
1. Подключить `checker.mjs` к пути прогона — или признать, что списка разрешённого нет.
2. Свести два манифеста в один документ.
3. Сделать `externalEgressBlocked` **измерением**: стенд сам пробует внешний адрес и
записывает отказ в манифест прогона.
4. Связать `sourceCommit` с байтами артефакта (слепок) и с работающим сервером.
5. Записывать коммит самого харнесса.
6. Потолок TTL на пути прогона.
7. Пути в плане очистки + исполнитель.
8. Решение владельца: включать ли `SPEND_CONTAINMENT_MODE=enforce` на стендах, и
переводить ли границу вендоров из списка запрещённого в список разрешённого.
### Troubleshooter
| симптом | что это | что делать |
|---|---|---|
| `explicit approved isolated manifest … live target refused` | манифест не совпал с воротами по одному из: `kind`, `manifestVersion`, `targetUrl` ≠ `TARGET_URL`, `approved`, `providerMode`, `externalEgressBlocked`, `runId`, `sourceCommit` | сверить `isolated-target.json` с `TARGET_URL` дословно; `sourceCommit` обязан быть ровно `1628b697d9b20a4446b9bdba20ada0a6c2820d11` |
| `isolated manifest expired` | `expiresAt` в прошлом или не разбирается | выписать новый манифест; потолка TTL на этом пути нет, но ставить больше 168 ч — против правила строгой схемы |
| `target URL contains unsafe origin components` | логин/пароль/якорь в `TARGET_URL` | убрать учётные данные из URL |
| `CAP-06 T0 gate: pinned Grafana k6 v0.54.0 is required` | `K6_BIN`/`PATH` указывают не на тот двоичный файл | проверить `K6_BIN`; **помнить, что проверяется только текст `k6 version`** |
| расписка падает с `ENOENT … 3487-capacity-integration-addons-manifest.json` | скрипт запущен на дереве без `docs/capacity/` (например, `wave/3818-…`) | запускать только на дереве, где лежит и скрипт, и его манифест; если расписка нужна про дерево стенда — её сегодня выпустить нечем |
| `checker.mjs` печатает `PATH_SYMLINK_TRAVERSAL` | в пути прогона есть символьная ссылка (часто сам `/tmp`) | использовать `realpath` для корня прогона |
| `checker.mjs` печатает `MANIFEST_PATH_REQUIRED` | путь к манифесту не абсолютный | передавать абсолютный путь |
| `[billing][unauthorized_vendor_egress_observed]` в логе приложения | режим `shadow`: нарушение записано, **запрос ушёл** | для стенда это не защита; нужен `enforce` |
| в логе нет ни `egress_guard_installed`, ни `unauthorized_vendor_egress_*` | `SPEND_CONTAINMENT_MODE` не задана → стража нет вовсе | задать `enforce`, иначе денежной границы нет |
| наши проверки не находят харнесс | `scripts/capacity/` нет в дереве | задать `CAP02_HARNESS_TREE=<чекаут wave/3818-triple-utf8-encode>`; иначе они сами достанут его из `/home/wave/nc-mirror` |
| `ERR_MODULE_NOT_FOUND` при запуске проверок | забыт `--import tools/register-resolver.mjs` | пользоваться `./waves/3911-cap02/run-checks.sh` |
---
## 8. Чего НЕЛЬЗЯ сделать волной
Отдельным списком, как просило задание.
**Нужен root / права на машине**
1. Установить, какие переменные окружения у живых процессов стенда на 3610/3611
(`/proc/<pid>/environ` чужого процесса) — а именно это ответило бы на главный вопрос:
задан ли там `SPEND_CONTAINMENT_MODE`.
2. Узнать владельца сокетов `0.0.0.0:3610/3611` (колонка процесса в `ss -ltnp`).
3. Поставить сетевое правило, которое реально закрывает исходящий трафик стенда
(`iptables`/`nftables`/network namespace). Без этого «egress заблокирован» остаётся
утверждением в файле.
4. Перезапустить приложение стенда с `SPEND_CONTAINMENT_MODE=enforce`.
**Нужен доступ к другим машинам**
5. Проверить egress с A2 на A1 и обратно; подтвердить или снять временное правило Oracle,
открытое 14.09 на 3610-3611.
6. Проверить, дойдёт ли внешний SYN до `0.0.0.0:3610` — это требует второй машины и
разрешения на такую попытку.
7. Прочитать настоящий `$STAND/inputs/isolated-target.json` и настоящий
`run-rung-r4.sh` на A1 (здесь доступна только копия «как поставлено» из чужих
доказательств).
**Нужно решение владельца**
8. Перевести денежную границу из списка запрещённого (17 хостов) в список разрешённого.
Это меняет поведение продукта, не только стенда.
9. Решить, обязателен ли `SPEND_CONTAINMENT_MODE=enforce` на стендах, и что делать с
`shadow` (сегодня он выглядит как защита и ею не является).
10. Решить судьбу `checker.mjs`: подключать к пути прогона или объявить мёртвым кодом.
Сегодня его наличие вводит в заблуждение — выглядит как доказательство изоляции,
им не являясь.
11. Признать недействительными или переснять замеры, снятые на стенде, про который
изоляция не доказана. Это решение об уже потраченной работе, не техническое.
**Запрещено этой волной прямо (и не делалось)**
12. Правки TypeScript — на этой машине нечем проверить компиляцию. Ни одного `.ts` файла
не изменено (см. список файлов в `00-environment.txt`).
13. Любые сборки, `next build`, полный `tsc`.
14. Прод, прод-БД, SSH, sudo, секреты, платные провайдеры, деплой.
15. Что-либо со стендами на портах 3610/3611 — там идёт замер CAP-10. Использовано
только `ss -ltn` (чтение).
16. Подагенты — не использовались.
---
## 9. KNOWN ISSUES
**KI-1. Ворота прогона не ограничивают класс адреса.**
`k6-script.js:372-382` проверяет слова в манифесте и равенство `targetUrl` с
`TARGET_URL`, но не проверяет, что адрес — петля или приватная сеть. Доказано:
`05-target-gate-matrix.txt`. Последствие: стенд, настроенный на прод или на платного
вендора, запустится штатно.
**KI-2. По умолчанию у стенда нет денежной границы.**
`installVendorEgressGuard()` — no-op при незаданном `SPEND_CONTAINMENT_MODE`; ни один файл
хозяйства стенда её не задаёт. Доказано: `04-paid-vendor-egress-probe.txt` +
`03-mechanism-inventory.txt`. Ручная заплатка на A1 (`OPENAI_BASE_URL` в мёртвый порт) —
следствие, а не причина.
**KI-3. `shadow` выглядит защитой и ею не является.**
Страж установлен, нарушение записано в лог, **запрос отправлен**. Для стенда это худший
из режимов: лог создаёт ложную уверенность.
**KI-4. Граница вендоров — список запрещённого из 17 хостов.**
`api.deepseek.com`, `api.together.xyz` и любой новый вендор проходят даже в `enforce`.
Доказано запуском.
**KI-5. Строгий проверяльщик изоляции не подключён.**
Единственное упоминание `checker.mjs` — строка в списке файлов
(`3487-coordinator-checklist.mjs:15`). Ни импорта, ни запуска.
**KI-6. Два несовместимых манифеста.**
`schemaVersion: 1` (проверяется, не используется) и `kind: capacity-isolated-target`
(используется, почти не проверяется). Прохождение первого ничего не говорит про прогон.
**KI-7. `sourceCommit` ничем не связан с артефактом.**
Аргумент командной строки, скопированный в два файла и сравнённый с константой.
Фальшивый артефакт из `/tmp` принял и утверждённый коммит, и несуществующий.
**KI-8. Коммит харнесса не фиксируется нигде.**
Печатается только продуктовый `sourceCommit`. Прибор, которым мерили, в доказательствах
не назван. (Совпадает с тем, что 3852 записала как «harness.k6ScriptGitCommit в receipt» —
не сделано.)
**KI-9. Расписка готовности проверяет текст.**
Девять проверок готовности времени выполнения — сравнения строк с закоммиченным JSON.
Редактирование файла делает расписку зелёной без стенда.
**KI-10. Расписку нельзя выпустить с дерева стенда.**
На `wave/3818-triple-utf8-encode` нет `docs/capacity/`; скрипт падает с `ENOENT`.
Любая существующая расписка выпущена не тем деревом, которое крутило ступени.
**KI-11. У пропуска нет потолка.**
На пути прогона принимается `expiresAt: 2199-01-01`. Потолок 168 ч есть только в
неподключённом `checker.mjs`.
**KI-12. T0-ворота не смотрят срок годности и проверяют k6 по тексту.**
Манифест с `expiresAt: 2019-01-01` прошёл; четырёхстрочный шим сошёл за k6 v0.54.0.
Контрольной суммы двоичного файла нет.
**KI-13. План очистки не называет путей и не привязан к прогону.**
Побайтово одинаков для разных прогонов; корень и id прогона в нём не встречаются;
исполнителя в дереве нет.
**KI-14. Секрет в манифесте прогона проезжает и едет в окружении.**
Неизвестные поля не отвергаются и не вычищаются; весь манифест передаётся k6 переменной
окружения, тогда как соседний runtime-input получает `chmod 600`.
**KI-15. Комплект изоляции не входит в продуктовую линию.**
`refs/waves/l115n` не содержит `scripts/capacity/` вовсе.
**KI-16. Заявленный класс сети не сверяется с реальной привязкой.**
Наблюдение на этой машине: 3610/3611 слушают `0.0.0.0`, манифест заявляет `127.0.0.1`.
Граница наблюдения — §3.5 и §7.
**KI-17 (про сами проверки). Загрузчик ворот зависит от формы `k6-script.js`.**
`tools/k6-gate-loader.mjs` снимает пять строк `import 'k6/*'` и ключевые слова `export`
регулярными выражениями. Если файл поменяет форму, загрузчик бросит исключение
(«no k6 import lines found», «an export survived the rewrite», «targetAndManifest not
found») — то есть сломается **громко**. Но это всё же не k6: настоящий goja-рантайм
здесь не запускался.
**KI-18 (про сами проверки). `REQUIRED`-набор красный по замыслу.**
`./waves/3911-cap02/run-checks.sh required` выходит с кодом 1. Это не сбой: 24 красных —
и есть результат волны. В CI его нельзя включать как обычный тест, пока дыры не закрыты.
---
## 10. Сдача
| артефакт | путь |
|---|---|
| отчёт | `/home/wave/waves/3911CAP02-REPORT.md` |
| бандл | `/home/wave/waves/3911CAP02.bundle` |
| контрольная сумма | `/home/wave/waves/3911CAP02.bundle.sha256` |
| доказательства | `/home/wave/waves/3911CAP02-evidence/` (+ `SHA256SUMS`) |
| ветка | `wave/3911-cap02-isolated-kit` @ `f034f88cff` |
| база | `refs/waves/l115n` @ `d85bb2dad15a3ba52c773b0a2362748009a2c3b9` |
Бандл собран именованной веткой и от линии:
`git bundle create … wave/3911-cap02-isolated-kit --not refs/waves/l115n`;
`git bundle verify` — `is okay`, одна ветка, требует `d85bb2dad1`.
Изменённых файлов TypeScript: **0**. Сборок: **0**. Тесты — только `node --test`.
2026-09-14T19:35:35.121Z · coordinator[14.09 19:35Z координатор] VERDICT=GO
# 3916 CAP-02: подключить строгий проверяльщик изоляции к реальному прогону
Отчёт по волне. Все факты ниже, помеченные «доказано», я получил собственным
запуском и указываю файл:строка. «Заявлено» — из отчёта 3911 или документации,
я это не перепроверял. «Оценка» — моё суждение, не измерение.
---
## Итог (коротко)
Строгий проверяльщик изоляции теперь стоит на пути прогона: `3501-t0-gate.mjs`
вызывает новый `checkIsolatedTarget()` из `checker.mjs` и **отказывается стартовать**
(exit 1, k6 не запускается), если манифест прогона не прошёл строгую проверку
«куда / насколько / без секретов». Денежная граница по умолчанию закрыта: при
незаданном `SPEND_CONTAINMENT_MODE` режим теперь `enforce`, а не `off` — сторож
vendor-egress установлен и платный провайдер недостижим, даже если заглушку
клиента забыли подставить.
Четыре отрицательные проверки краснели на старом коде и позеленели на новом
(см. таблицу ниже). Обычный прогон не сломан: хороший loopback-манифест проходит.
---
## Что именно изменено (доказано, файл:строка)
### 1. Строгий проверяльщик добавлен и подключён
- `scripts/capacity/isolation/checker.mjs` — строгий проверяльщик (schemaVersion:1)
уже существовал на ветке 3818; я перенёс его на l115n и добавил
`checkIsolatedTarget()` (`checker.mjs:671`) — строгую проверку для манифеста
**вида прогона** (`kind: 'capacity-isolated-target'`). Правила:
- `targetUrl` обязан быть буквальным частным адресом (loopback/private-isolated),
иначе `TARGET_ADDRESS_NOT_LITERAL_PRIVATE` (`checker.mjs:690`);
- запрещён live-алиас в адресе/runId (`LIVE_TARGET_ALIAS_FORBIDDEN`,
`checker.mjs:689`, `checker.mjs:701`);
- `expiresAt` не в прошлом (`TTL_EXPIRED`, `checker.mjs:696`) и не дальше 168ч
(`TTL_OUT_OF_RANGE`, `checker.mjs:697`);
- ни одного поля с именем-секретом (`SECRET_FIELD_FORBIDDEN`, `checker.mjs:679`,
`checker.mjs:19`).
- `scripts/capacity/integration/3501-t0-gate.mjs` — T0-гейт (node-уровень, запускается
перед k6) теперь вызывает `checkIsolatedTarget` и при провале
`fail(...)` → exit 1 **до** `spawn(k6)` (`3501-t0-gate.mjs:62-65`). Раньше гейт
проверял только слова оператора (`approved/stub/externalEgressBlocked`,
`3501-t0-gate.mjs:61`).
### 2. Денежная граница по умолчанию
- `src/lib/billing/containmentMode.ts:86` — незаданный режим теперь
`{ kind: 'valid', mode: 'enforce' }` (было `'off'`). Цепочка:
`installVendorEgressGuard()` (`vendorEgressGuard.ts:421`) читает
`resolveContainmentMode(env)` (`vendorEgressGuard.ts:426`) → `readContainmentMode`
(`containmentMode.ts:82`). `off` остаётся явным осознанным выключением.
- Точка установки сторожа: `src/instrumentation.ts:118` (`installVendorEgressGuard()`),
в `try/catch` (`instrumentation.ts:113-139`), поэтому неустановка сторожа не роняет
загрузку — платные поверхности закрываются по fail-closed, свободные продолжают жить.
### 3. Почему раньше это была дыра (доказано)
- Слабый гейт k6 (`k6-script.js:372-382`, из ветки 3818) проверяет: `kind`,
`manifestVersion`, `targetUrl === target`, `approved`, `providerMode==='stub'`,
`externalEgressBlocked`, непустой `runId`, `sourceCommit === <зашитый хэш>`,
`expiresAt > now`, и у URL — только протокол + отсутствие userinfo/hash.
**Не проверяет**: класс адреса (публичный/прод/платный), TTL-потолок, секретные
поля, live-алиасы. Поэтому `https://nb.wool2.online` и
`https://api.openai.com/...` проходили.
- T0-гейт (node) до моей правки проверял только 3 слова и не проверял ни адрес,
ни TTL, ни секреты.
- Money: при незаданном режиме `installVendorEgressGuard` был no-op — сторожа не
было вовсе.
---
## Четыре отрицательные проверки: красный до / зелёный после
Все — собственным запуском. «Красный до» = старый гейт/старый дефолт пропускает;
«зелёный после» = новый код отказывает. Сырые логи в `3916CAP02GATE-evidence/`.
| Случай | Красный ДО (доказано) | Зелёный ПОСЛЕ (доказано) |
|---|---|---|
| прод-адрес `https://nb.wool2.online` | старый гейт `exit=0, gate_passed=YES` (`gate-RED-before.txt`) | `exit=1`, `LIVE_TARGET_ALIAS_FORBIDDEN` + `TARGET_ADDRESS_NOT_LITERAL_PRIVATE` (`gate-GREEN-after.txt`) |
| платный провайдер `https://api.openai.com/v1/chat/completions` | старый гейт `exit=0` | `exit=1`, `TARGET_ADDRESS_NOT_LITERAL_PRIVATE` |
| секрет в манифесте (`apiKey`) | старый гейт `exit=0` | `exit=1`, `SECRET_FIELD_FORBIDDEN` |
| просроченный `expiresAt` | старый гейт `exit=0` | `exit=1`, `TTL_EXPIRED` |
| (бонус) TTL > 168ч | старый гейт `exit=0` (проверял только `> now`) | `exit=1`, `TTL_OUT_OF_RANGE` |
Пояснение к «просроченному expiry»: k6-гейт (`k6-script.js:378`) уже отбрасывал
просроченный манифест — но **после** старта k6. Мой гейт отказывает **до** старта
k6, т.е. «отказ стартовать» достигается на первой node-линии. Для прод/платный/секрет
дыра была и в k6-гейте — там она закрывается уже моим T0-гейтом раньше по цепочке.
## Денежная граница: красный до / зелёный после (доказано)
Проба `tools/egress-probe.mjs` форкает процесс, ставит **настоящий**
`vendorEgressGuard.ts`, подменяет `globalThis.fetch` счётчиком и дёргает платный URL.
**Реального сетевого вызова нет** — счётчик показывает, ушли бы байты или нет.
| Режим | ДО (`containmentMode.ts:75` → `off`) | ПОСЛЕ (`:86` → `enforce`) |
|---|---|---|
| `__unset__` | `install.installed=false`, `mode=off`, `transportCalls=1`, `outcome=REACHED_TRANSPORT` (`money-RED-before.txt`) | `install.installed=true`, `mode=enforce`, `transportCalls=0`, `outcome=DENIED`, `UnauthorizedVendorEgressError` (`money-GREEN-after.txt`) |
`transportCalls: 1 → 0` — это и есть граница: до фикса байты ушли бы к провайдеру,
после — нет.
## Обычный прогон не сломан (доказано)
- `checkIsolatedTarget` принимает хороший loopback-манифест (`tests/isolation-gate.test.mjs`,
тест «accepts a good loopback stub manifest»).
- Полный гейт с хорошим манифестом проходит слово-проверки + строгую проверку и
доходит до k6 (тест «t0 gate accepts ... reaches k6»; `gate-GREEN-after.txt`
строка `good-loopback exit=0 gate_passed=YES`).
- Канонический target рантайма — `http://127.0.0.1:<port>/v1`
(`runtime-provider/create-manifest.mjs:21`, `runtime-provider/common.mjs:5`); мой
гейт его принимает (loopback). Загрузка `instrumentation.ts` вся в `try/catch`
(`:113-139`, `:145-163`), так что `enforce`-по-умолчанию не роняет загрузку
стенда, использующего заглушки.
- Все проверки — только `node --test` (`run-checks.sh`), без сборок/tsc/next. Итог:
**12 pass, 0 fail** (`tests-GREEN-after.txt`).
---
## ДЛЯ ТИКЕТА детским языком
У стенда было два слабых места, из-за которых он «на словах изолированный», а на
деле мог дотянуться до настоящего мира.
1. **Пропуск на въезд был фальшивым.** При прогоне входной вахтёр проверял, что
в бумажке написано «я изолированный, я одобренный, я заглушка». Он не смотрел,
*куда* реально едет трафик. Поэтому в бумажке можно было написать адрес боевого
прода или платного провайдера — вахтёр кивал и пропускал. Я поставил настоящую
проверку: адрес обязан быть частным (127.0.0.1 или соседний изолированный),
срок годности не просрочен и не длиннее недели, а в бумажке не должно быть
секретов. Теперь при неправильной бумажке вахтёр **не пускает** — прогон даже не
стартует.
2. **Кошелёк был открыт по умолчанию.** Защита от платных провайдеров включалась
только когда кто-то явно нажимал кнопку «включить». Если забыли подставить
заглушку и ничего не нажали — стенд мог пойти к платному провайдеру. Я сделал
наоборот: по умолчанию защита **включена**, платный адрес недостижим; чтобы
выключить, надо явно написать «off».
Проверки: четыре плохих случая (прод-адрес, платный провайдер, секрет в манифесте,
просроченный срок) на старом коде проходили, на новом — отказ. Хороший стенд
продолжает стартовать.
---
## Troubleshooter
- **Гейт отказал с `TARGET_ADDRESS_NOT_LITERAL_PRIVATE` на легитимном стенде.**
Строгий гейт требует буквальный частный IP, а не hostname. `localhost` не
проходит (он не «буквальный адрес» — его можно переписать в `/etc/hosts`).
Замените `http://localhost:<port>` на `http://127.0.0.1:<port>` (канонический
рантайм так и делает — `create-manifest.mjs:21`).
- **Гейт отказал с `TTL_EXPIRED`/`TTL_OUT_OF_RANGE`.** Манифест пересоздайте с
`expiresAt` в будущем и не дальше 168 часов.
- **Гейт отказал с `SECRET_FIELD_FORBIDDEN`.** В манифесте лежит поле, имя которого
выглядит как секрет (`apiKey`, `token`, `password`, …). Уберите его из манифеста —
секреты и так должны ходить отдельными env-входами, а не внутри манифеста.
- **`installVendorEgressGuard` теперь устанавливается на стенде, а я хочу как было.**
Явно задайте `SPEND_CONTAINMENT_MODE=off` — это осознанный opt-out.
- **Проверки не запускаются / `ERR_MODULE_NOT_FOUND` на `waves/...`.** Передавайте
абсолютные пути к `--import`-скриптам и файлам тестов (см. `run-checks.sh`).
- **`Cannot find package 'waves'`.** Относительный `--import waves/...` трактуется
как пакет; используйте `./waves/...` или абсолютный путь.
---
## KNOWN ISSUES
1. **k6-гейт остаётся слабым как вторая линия.** `k6-script.js:372-382` не умеет
импортировать `checker.mjs` (рантайм goja без node-встроенных), поэтому строгая
проверка стоит только на node-уровне (`3501-t0-gate.mjs`). Если кто-то запустит
k6 в обход T0-гейта, адресная/TTL/секретная проверка не сработает. Это
defence-in-depth-разрыв, а не регресс: дыра в k6-гейте теперь перекрыта раньше.
2. **`localhost`-hostname не проходит строгий гейт.** Осознанно: «буквальный частный
адрес» — только IP. См. Troubleshooter.
3. **Гейт допускает private LAN (10.x/192.168.x/172.16-31.x), а runtime-provider —
только loopback.** Это не новая дыра (private-isolated — разрешённый класс
строгого проверяльщика), а upstream `runtime-provider/common.mjs` всё равно
режет до loopback. Класс «публичный/прод/платный» закрыт и там, и там.
4. **Authorized vendor dispatch не закрывается этой границей.** Сторож пропускает
вызов внутри `runAuthorizedVendorDispatch` (`vendorEgressGuard.ts:207`), как и
задумано для продакшена. На стенде «забыли заглушку» с прямым `fetch` к платному
адресу граница ловит; если же код стенда штатно оборачивает платный вызов в
authorized dispatch — это вопрос конфигурации стенда, а не этой волны.
5. **`enforce`-по-умолчанию включает и бухгалтерский гейт на загрузке.**
`instrumentation.ts:147` теперь `resolveContainmentMode() !== 'off'` истинно при
незаданном режиме → `reconstructAccountingGate()` читает БД на загрузке. Он в
`try/catch` (`:156-162`), стенд не упадёт, но это лишнее чтение БД на стендах с
заглушками (заглушка `@/lib/prisma` кидает — ловится как `ok:false`).
---
## Артефакты
- Код: ветка `wave/3916-cap02-wire-the-gate`, коммит `5d2127a774`.
- Бандл: `/Users/milamarty/waves/3916CAP02GATE.bundle` +
`/Users/milamarty/waves/3916CAP02GATE.bundle.sha256` (проверен `git bundle verify`:
«okay», база `d85bb2dad1` = `refs/waves/l115n`).
- Доказательства: `/Users/milamarty/waves/3916CAP02GATE-evidence/` + `SHA256SUMS`.
- Тесты: `waves/3916-cap02/tests/`, запуск `waves/3916-cap02/run-checks.sh`.
## Ограничения проверки
- Запускалось только `node --test` + `node --experimental-strip-types`; никаких
сборок, tsc, next, деплоев, прод-БД, sudo, SSH, платных провайдеров (проба
платного адреса — счётчик-транспорт, без сети).
- Стенды A1/A2 не трогал.
- Секретов в вывод не попало: во всех манифестах/тестах только фейковые значения
(`sk-cap02-...`, `not-a-real-password`).
2026-09-14T19:50:39.595Z · coordinator[14.09 19:50Z координатор] VERDICT=GO
# 3918 ACC-CAP02-GATE — независимая приёмка волны 3916 (денежная граница стенда)
**Вывод одной строкой.** Все три заявления волны 3916 держатся своим прогоном: ворота
`3501-t0-gate.mjs` теперь зовут строгий `checkIsolatedTarget()` и **отказываются стартовать**
(exit 1, k6 не запускается) на каждой из четырёх дыр 3911; при незаданном
`SPEND_CONTAINMENT_MODE` сторож vendor-egress **установлен** и платный провайдер **недостижим**
даже без заглушки; заведомо годный манифест **проходит как раньше**. Правила проверки —
константы в `checker.mjs`, из манифеста не читаются; падение самого проверяльщика роняет
прогон **закрыто**, а не пропускает.
---
## 1. Что проверял и чем
Всё — только `node --test` / `node` своими прогонами, без сборок, без k6 (на машине его нет —
заглушка на `K6_BIN`), без сети, без базы. Дерево: worktree
`~/waves/wt-3918-acc-gate` @ `5d2127a774` (ветка `acceptance/3918-acc-cap02-gate`), база
`refs/waves/l115n` @ `d85bb2dad1`.
Свои пробы (лежат в `3918ACCGATE-evidence/`, каждая — исходник + вывод):
| проба | файл | что делает |
|---|---|---|
| ворота | `probe-gate-matrix.mjs` → `01-gate-matrix.txt` | гоняет настоящий `3501-t0-gate.mjs` дочерним процессом; заглушка k6 оставляет маркер-файл, если её реально запустили — «k6Started» берётся из маркера, а не со слов ворот |
| деньги | `probe-money-boundary.mjs` → `02-money-matrix.txt` | загружает настоящий `vendorEgressGuard.ts`, снимает все заглушки (`OPENAI_BASE_URL`, ключи) из окружения, счётчик `transportCalls` на месте `globalThis.fetch` |
| падение | `probe-checker-failclosed.mjs` → `03-checker-failclosed.txt` | копирует ворота в `/tmp` и портит/удаляет `checker.mjs` четырьмя способами |
| перекрёст | `04-wave-suite.txt` | прогон собственного набора волны явными путями (12/12) |
Замечание к перекрёстному прогону: `waves/3916-cap02/run-checks.sh` под Node 26 **не работает**
(см. KI-1), тесты зелёные только при запуске явными путями файлов — так и гонял.
## 2. П1 — четыре дыры 3911 останавливают прогон (доказано)
`01-gate-matrix.txt`, 22 строки, каждая — свой дочерний запуск настоящих ворот:
| класс | манифест | exit | k6 запущен | ошибка |
|---|---|---|---|---|
| годный петлевой | `http://127.0.0.1:4317` | **0** | **да** | — |
| годная приватная сеть | `http://10.0.0.5:3000` | **0** | **да** | — |
| прод-адрес (публичный IP) | `http://8.8.8.8:80` | **1** | **нет** | `TARGET_ADDRESS_NOT_LITERAL_PRIVATE` |
| прод-адрес (публичный IP) | `https://203.0.113.10` | **1** | **нет** | `TARGET_ADDRESS_NOT_LITERAL_PRIVATE` |
| прод по имени | `https://app.production.example.com` | **1** | **нет** | `LIVE_TARGET_ALIAS_FORBIDDEN` + `TARGET_ADDRESS_NOT_LITERAL_PRIVATE` |
| живое имя wool2 | `https://nb.wool2.online` | **1** | **нет** | `LIVE_TARGET_ALIAS_FORBIDDEN` |
| платный провайдер | `https://api.openai.com/v1/chat/completions` | **1** | **нет** | `TARGET_ADDRESS_NOT_LITERAL_PRIVATE` |
| платный провайдер | `https://api.anthropic.com` | **1** | **нет** | `TARGET_ADDRESS_NOT_LITERAL_PRIVATE` |
| секрет на верхнем уровне | `apiKey: …` | **1** | **нет** | `SECRET_FIELD_FORBIDDEN@$.apiKey` |
| секрет вложенный | `config.db.connectionString` | **1** | **нет** | `SECRET_FIELD_FORBIDDEN@$.config.db.connectionString` |
| секрет bearer | `auth.bearerToken` | **1** | **нет** | `SECRET_FIELD_FORBIDDEN@$.auth.bearerToken` |
| истёкший срок | `expiresAt` в прошлом | **1** | **нет** | `TTL_EXPIRED` |
| бессрочный/сверх потолка | `expiresAt` +200 ч | **1** | **нет** | `TTL_OUT_OF_RANGE` |
| бессрочный | `expiresAt: 2099-01-01` | **1** | **нет** | `TTL_OUT_OF_RANGE` |
| срок отсутствует | нет `expiresAt` | **1** | **нет** | `INVALID_UTC_TIMESTAMP` |
Каждая дыра **останавливает прогон** (exit 1, `k6Started=false`), а не предупреждает. Порядок в
коде верный: строгая проверка (`3501-t0-gate.mjs:62-65`) стоит **до** проверки k6 и **до** `spawn`
(там же `:66-82`), то есть плохой манифест не доезжает даже до `k6 version`.
## 3. П2 — граница расходов без заглушки (доказано)
`02-money-matrix.txt`, по процессу на режим (состояние стража — процессно-глобальное), все
заглушки сняты из окружения (`OPENAI_BASE_URL`, `OPENAI_API_KEY` и т.п. удалены до вызова):
| `SPEND_CONTAINMENT_MODE` | страж установлен | режим | `transportCalls` | итог |
|---|---|---|---|---|
| **не задана** | **да** | **enforce** | **0** | **DENIED (`UnauthorizedVendorEgressError`)** |
| `off` | нет | off | 1 | REACHED_TRANSPORT (явный отказ) |
| `shadow` | да | shadow | 1 | REACHED_TRANSPORT (только лог) |
| `enforce` | да | enforce | 0 | DENIED |
| мусорная строка | да | enforce | 0 | DENIED |
Главное утверждение волны держится: **без подставного ключа и мёртвого адреса** обращение к
`api.openai.com` не доходит до транспорта. Источник — одна строка
`src/lib/billing/containmentMode.ts:86` (`if (!trimmed) return { kind: 'valid', mode: 'enforce' }`);
стража ставит `src/instrumentation.ts:118` (`installVendorEgressGuard()`), который читает режим из
этой же функции. `SPEND_CONTAINMENT_MODE=off` в хозяйстве стенда никто не подставляет
(проверено grep'ом по `scripts/`).
## 4. П3 — годный манифест проходит, ложного отказа нет (доказано)
Тот же `01-gate-matrix.txt`: `good-loopback` (петля) и `good-private-10` (RFC1918) — **exit 0,
k6 запущен**. Строгая проверка не мешает штатному прогону стенда на `127.0.0.1`. Ворота по-прежнему
проверяют и старые «слова» (`approved`/`providerMode=stub`/`externalEgressBlocked`,
`3501-t0-gate.mjs:60-61`), так что путь для нормального манифеста не изменился.
Граница ложного отказа (задокументирована, не блокер): `localhost` как hostname отвергается —
см. KI-3.
## 5. П4 — правила не читаются из манифеста (доказано чтением + прогоном)
- Ворота импортируют проверяльщик статически, по буквальному пути
`import { checkIsolatedTarget } from '../isolation/checker.mjs'` (`3501-t0-gate.mjs:5`) — путь
не берётся ни из манифеста, ни из окружения.
- `checkIsolatedTarget()` читает из манифеста только `targetUrl`, `expiresAt`, `runId` и сканирует
**имена ключей** на секреты (`checker.mjs:671-704`). Никакого поля `allowlist`/`rules`/`mode`
из проверяемого объекта не читается.
- Все правила — литералы в модуле: `SECRET_KEY` (`checker.mjs:19`), `LIVE_ALIAS` (`:20`),
диапазоны приватных сетей в `isPrivateIpv4`/`isPrivateAddress` (`:197`, `:203`),
потолок `MAX_ISOLATED_TTL_MS = 168ч` (`:630`). Ни `eval`, ни `new Function`, ни чтения правил
из файла манифеста.
- Прогоном: манифест, пытающийся сам себя благословить (`networkClass: 'loopback'` или
`targets.allowlist: ['api.openai.com']` при `targetUrl: api.openai.com`), **всё равно отвергнут**
(`01-gate-matrix.txt`, `self-blessing-*`). Пакет не ручается за себя.
## 6. П5 — падение проверяльщика падает закрыто (доказано)
`03-checker-failclosed.txt`, четыре запуска ворот из `/tmp`-копий:
| состояние `checker.mjs` | exit | k6 запущен | что случилось |
|---|---|---|---|
| на месте | 0 | да | — |
| **файла нет** | **1** | **нет** | `ERR_MODULE_NOT_FOUND` на этапе импорта |
| **ошибка разбора** | **1** | **нет** | `SyntaxError` на этапе импорта |
| **нет экспорта `checkIsolatedTarget`** | **1** | **нет** | ошибка связывания именованного импорта |
Статический импорт означает: сломанный проверяльщик валит ворота целиком ещё до `main()`. Прогон
**не пропускается**. Дополнительно: `manifest: null` / `[]` / `{}` тоже дают exit 1 без запуска k6
(`01-gate-matrix.txt`, три последние строки) — правда, `null` падает некрасиво (сырой `TypeError`),
см. KI-6.
## 7. ДЛЯ ТИКЕТА — детским языком
**Что проверяли.** В прошлый раз (волна 3911) выяснилось, что у стенда есть хороший замок
(`checker.mjs`), но его никто не вставлял в дверь, а денежная защита по умолчанию была выключена.
Волна 3916 говорит: «теперь замок вставлен, а денежная защита по умолчанию включена». Мы это
перепроверили сами, запуская настоящие программы, а не читая код.
**Чем проверяли.** Запускали `3501-t0-gate.mjs` с подложным k6 (заглушкой), который оставляет
файл-след, если его правда запустили. И запускали настоящую денежную защиту
`vendorEgressGuard.ts`, подсунув вместо сети счётчик — «ушли байты или нет».
**Что устояло.**
1. Дверь теперь смотрит, **куда** идёт стенд. Прод-адрес, платный робот (`api.openai.com`,
`api.anthropic.com`), прод по имени — всё это больше не пускает: программа говорит «нет» и
**вообще не запускает замер**.
2. Бумажка с секретом (поле, названное как секрет — `apiKey`, `connectionString`, `bearerToken` и
т.п.) больше не проходит.
3. Просроченный пропуск и пропуск «на 73 года вперёд» больше не проходят; пропуск обязан быть
свежим и не дольше недели (168 часов).
4. Денежная защита **включена по умолчанию**. Раньше, если забывали включить переменную — защиты
не было вообще, и вызов к платному роботу уходил. Теперь забыл — всё равно защита стоит, и
вызов к `api.openai.com` не уходит, даже если убрать все подставные ключи и адреса.
5. Правила, по которым дверь решает «да/нет», **зашиты в саму программу**, а не написаны в
бумажке, которую проверяют. Бумажка не может сама себя объявить «я хорошая».
6. Если сам замок сломался (файл пропал или в нём ошибка) — замер **не запускается**, а не
проезжает без проверки.
**Что не устояло / осталось как было (не поломалось, но и не чинилось).**
- Денежная защита по-прежнему — список **17 запрещённых** адресов, а не список разрешённого.
Новый платный робот, которого нет в списке (например, `api.deepseek.com`), пройдёт даже при
включённой защите. Волна 3916 это не обещала чинить — она чинила «по умолчанию выключено →
по умолчанию включено».
- Проверка «тот ли это k6» осталась по тексту ответа на `k6 version`, а не по контрольной сумме.
Поддельный четырёхстрочный скрипт её обойдёт. Но это теперь стоит **за** строгой проверкой
манифеста, так что дыра по адресу/секрету/сроку до k6 не доезжает.
**ГДЕ ГРАНИЦА (что доказано и что нет).**
- **Доказано** (своим прогоном, файл:строка в отчёте): ворота останавливают прогон на четырёх
дырах; unset-режим = enforce и платный провайдер недостижим; годный манифест проходит; правила
не из манифеста; падение проверяльщика — закрыто.
- **Не доказано** (и в отчёте как факт не используется): настоящий k6 не запускался (на машине его
нет — заглушка); стенды A1/A2 не трогались вовсе (там идёт замер); реальное окружение живого
приложения стенда (`SPEND_CONTAINMENT_MODE` у процессов на 3610/3611) не читалось — это за
границей волны; не проверялось, что `scripts/capacity/harness/k6-script.js` есть на дереве, с
которого ворота реально запустят (на l115n его нет — см. KI-4).
- **Заявлено волной** (сверено с её `evidence/`, совпадает с моим прогоном): «RED-before /
GREEN-after» — четыре дыры до и после, и денежная граница до и после.
**Что осталось открытым.** См. KNOWN ISSUES ниже; главное — перевести денежную границу из
списка запрещённого в список разрешённого, довезти harness до дерева запуска, и решить, как быть
с `localhost`-адресами и строгим форматом срока.
## 8. KNOWN ISSUES
**KI-1. `waves/3916-cap02/run-checks.sh` не запускается под Node 26.**
`exec node --test waves/3916-cap02/tests/` (каталог с хвостовым `/`) даёт `MODULE_NOT_FOUND`
(`04-wave-suite.txt`, первый прогон). Тесты зелёные только явными путями файлов. Тулинг-дефект,
на продукт не влияет.
**KI-2. Денежная граница — список запрещённого из 17 хостов, не список разрешённого.**
`paidVendorHosts.ts` — 17 regex. `api.deepseek.com`, `api.together.xyz`, любой новый вендор проходят
даже в `enforce`. Это KI-4 из 3911, волной 3916 не чинилось и не заявлялось. Граница денег — ровно
эти 17 хостов, не «все платные».
**KI-3. `localhost` (hostname) отвергается.**
`checkIsolatedTarget` требует **буквальный** приватный IP (`isPrivateAddress` использует `isIP`),
поэтому `http://localhost:4317` даёт `TARGET_ADDRESS_NOT_LITERAL_PRIVATE` (`01-gate-matrix.txt`,
`hostname-localhost`). Стенд на `127.0.0.1` работает; но привычка писать `localhost` даст ложный
отказ. Намеренная консервативность (не резолвить hostname), но это реальная граница «годный не
прошёл».
**KI-4. Ворота зовут `scripts/capacity/harness/k6-script.js`, которого нет на l115n.**
`3501-t0-gate.mjs:81` жёстко пишет `'scripts/capacity/harness/k6-script.js'`. На l115n в
`scripts/capacity/` только ворота и проверяльщик; harness (`k6-script.js`, `run-rung-r4.sh`) не
довезён. «Годный манифест стартует» я доказал с заглушкой k6 (ворота доходят до spawn и выходят
0); реальный k6-прогон на l115n упрётся в отсутствующий скрипт уже после ворот. Это не дыра
безопасности (ворота отказали до этого), но интеграционный хвост: ворота на линии, а их груз —
нет.
**KI-5. Проверка k6 — по тексту `version`, не по контрольной сумме.**
`3501-t0-gate.mjs:69` матчит `^k6 v0.54.0`. Четырёхстрочный шим проходит (KI-12 из 3911). Стоит за
строгой проверкой манифеста, но «какой именно двоичный файл» остаётся недоказанным.
**KI-6. `manifest: null` падает некрасиво.**
Сырой `TypeError: Cannot read properties of null (reading 'targetUrl')` вместо внятного кода
изоляции. Всё равно exit 1 и без k6 (fail-closed), но диагностика слабая.
**KI-7. Секреты ловятся по имени ключа, не по значению.**
`SECRET_KEY` проверяет имена полей. Секрет под нейтральным именем (например `x: "sk-…"`) пройдёт
проверку и, как и прежде, уедет к k6 переменной окружения (`ISOLATED_MANIFEST_JSON`,
`3501-t0-gate.mjs:76-79`). 3916 закрыла «секрет с секретным именем»; «секрет под чужим именем» —
открыт.
**KI-8. Строгий формат `expiresAt`.**
`parseUtc` требует ровно `YYYY-MM-DDTHH:MM:SS(.mmm)?Z` (`checker.mjs:189`). Манифест со сдвигом
часового пояса (`+03:00`) или шестизначными миллисекундами даст `INVALID_UTC_TIMESTAMP` — ложный
отказ для старых манифестов с другим форматом. Потолок 168 ч теперь действует и на пути прогона:
оператор обязан выписывать свежие пропуска ≤ недели.
## 9. Сдача
| артефакт | путь |
|---|---|
| отчёт | `/Users/milamarty/waves/3918ACCGATE-REPORT.md` |
| бандл | `/Users/milamarty/waves/3918ACCGATE.bundle` (+ `.bundle.sha256`) |
| доказательства | `/Users/milamarty/waves/3918ACCGATE-evidence/` (+ `SHA256SUMS`) |
| ветка | `acceptance/3918-acc-cap02-gate` @ `5d2127a774` |
| база | `refs/waves/l115n` @ `d85bb2dad1` |
Изменённых TypeScript-файлов мной: **0**. Сборок: **0**. Стенды A1/A2 не тронуты. Подагенты: не
использовались.
2026-09-14T21:33:39.126Z · coordinator[14.09 21:33Z координатор] **НЕ закрываю, и поправляю себя: сегодня утром я сказала владельцу, что CAP-02 можно закрыть. Это была ошибка — я смотрела на то, что сделано, и не проверила, что из этого работает.**
**Что действительно сделано:**
- изолированный стенд поднят (`cap3610stand`, артефакт линии, порт 3610);
- строгий контроль мишени вшит в `3501-t0-gate.mjs` через `checkIsolatedTarget()` — при несовпадении выход `1`, запуск не начинается (волна 3916, принята 3918);
- `SPEND_CONTAINMENT_MODE` по умолчанию переведён с `off` на `enforce`.
**Почему этого недостаточно — доказано волной 3910, которая трижды перепроверила мишень своими руками:**
| # | дефект | чем доказан |
|---|---|---|
| A | **база стенда пуста**: `User=0`, `Notebook=0`, `Source=0` при 211 таблицах | `psql` на A1 с `DATABASE_URL`, прочитанным из `/proc/<pid>/environ` самого приложения |
| B | **харнесс отвергает манифест стенда**: `k6-script.js:24` фиксирует `APPROVED_APPLICATION_SOURCE_COMMIT = 1628b697d…`, строка 376 бросает, а манифест несёт `d85bb2dad…` | чтение обоих файлов |
| C | **сессии стенда не аутентифицируются**: один и тот же cookie даёт полный объект пользователя на 3611 и литеральный `null` на 3610 | два `curl`, рядом, с одним cookie |
«Проверена запросами» в задании волны оказалось верно только для `/api/health` — а он неаутентифицирован и проходит на совершенно пустом стенде. **211 таблиц — это схема, а не корпус.**
Следствие: `ISOLATED_MANIFEST_FILE=…/isolated-target-a1stand.json` не может выполнить ни одного делового действия, и все замеры шли против стенда на **3611**, а не против выданной мишени.
**Отдельно, и это стоит человеческого подтверждения:** манифест мишени `isolated-target-3902.json` подписан **волной, а не владельцем**. Существо (одноразовый стенд, заглушенные провайдеры, отрезанный egress, не прод) проверено; подпись — не владельца.
**Что осталось по остатку тикета:** run-owned TTL manifest, независимые DB/Redis/storage/auth/stub, негативная проверка egress и evidence по ready/cleanup. Ни один из трёх дефектов выше не устранён.
Статус остаётся `review`. Закрывать нечего — тикет описывает изоляцию, а изоляция в выданном виде **не исполнима**.
2026-09-14T22:10:06.872Z · coordinator[14.09 22:10Z координатор] **Крупный сдвиг по этому тикету: проверяльщик изоляции МОЛЧА пропускал все три дефекта, из-за которых тикет открыт. Теперь ловит каждый — волна 3933, `VERDICT=GO`.**
Напомню, почему тикет открыт: волна 3910 трижды доказала, что выданная мишень A1:3610 негодна — база пуста (`User=0` при 211 таблицах), харнесс отвергает её `sourceCommit`, сессии не аутентифицируются. Замеры шли на 3611.
**Теперь выяснилось худшее: проверяльщик, который должен был это поймать, пропускал всё три.** Волна 3933 доказала прогоном red-before/green-after:
```
BEFORE (старый чекер): ok=true [(none)]
AFTER (исправленный): ok=false [SOURCE_COMMIT_NOT_APPROVED, REQUIRED_FIELD, REQUIRED_FIELD]
```
| дефект | что проходило раньше | код отказа теперь |
|---|---|---|
| чужой `sourceCommit` | манифест со `sourceCommit`, отличным от `APPROVED_APPLICATION_SOURCE_COMMIT` (`scripts/capacity/approved-source.mjs`), проходил — мерили не тот исходник | `SOURCE_COMMIT_NOT_APPROVED` |
| пустой стенд | `seededUserCount` отсутствует или `0` — проходило, мерили пустую базу | `REQUIRED_FIELD` / `DATABASE_SEED_USER_COUNT_INVALID` |
| прод-сессии | `sessionAuthMode` не `run-owned-credentials` или отсутствует — проходило, стенд мог работать на прод-сессиях | `REQUIRED_FIELD` / `SESSION_AUTH_MODE_INVALID` |
Каждый ловится и изолированно (`D1/D2/D3`), положительный контроль сохранён: исправный манифест остаётся `ok=true`. Тест: `tests/capacity/isolation/checker.wave-3910-defects.test.mjs`, 4 из 4 зелёных.
**Первопричина девяти падений — одна:** `pathContainsSymlink(value)` шёл от корня `/` и проверял на симлинк **каждый** сегмент пути. Десятое падение — устаревший тест `coverageOrder` (класс «тест устарел», не дефект кода).
**Что это меняет для тикета.** Раньше «изоляция не исполнима» было констатацией без инструмента: негодную мишень ловили руками, задним числом и случайно. Теперь есть проверка, которая отказывает **до** запуска и называет причину кодом. Это не закрывает тикет — мишень 3610 по-прежнему негодна, run-owned TTL manifest и негативная проверка egress не сделаны, — но **убирает главный риск: следующая негодная мишень не пройдёт молча**.
**Выдана независимая приёмка — волна 3937 (Neo, автор 3933 к ней отношения не имеет).** Её задача не согласиться, а искать, что проверяльщик **всё ещё** пропускает: частично засеянная база (`seededUserCount: 1`), `sourceCommit` правильной формы но другого дерева, верный `sessionAuthMode` при прод-сессиях, пустые строки вместо отсутствующих полей. И отдельно — ложные срабатывания на законном пути через симлинк.
Ветка: `refs/waves/3933/wave/3933-isolation-checker-fix`.
2026-09-14T22:23:26.131Z · coordinator[14.09 22:23Z координатор] **Приёмка проверяльщика изоляции — `NO-GO` (волна 3937, Neo). Три старые дыры закрыты и это подтверждено, но найдены две новые. Тикет остаётся открытым обоснованно.**
**Что приёмка подтвердила своим прогоном:** red-before/green-after воспроизведён — `BEFORE ok=true [(none)]` → `AFTER ok=false [SOURCE_COMMIT_NOT_APPROVED, REQUIRED_FIELD, REQUIRED_FIELD]`. Отдельно подтверждено: чужой commit правильного hex-формата отвергается (`checker.mjs:623-630`), отсутствующий и нулевой `seededUserCount` отвергаются (`:448-451`), неверный и пустой `sessionAuthMode` отвергаются (`:500-503`), лишнее поле — `UNKNOWN_FIELD` (`:125-138`), положительный контроль сохранён.
**Что всё ещё пропускается — две дыры, обе доказаны её прогоном:**
1. **Частично засеянная база проходит.** `seededUserCount: 1` → `ok=true [(none)]`. Проверка требует лишь **положительное число**, а не полноту стенда. Стенд с одним пользователем так же негоден, как пустой: на нём не выполнить ни один многопользовательский сценарий — а именно на этом встала волна 3910.
2. **`sessionAuthMode` проверяется только как декларация.** Манифест с `sessionAuthMode: run-owned-credentials` проходит **при смоделированном продовом backend сессий снаружи манифеста**. Проверяльщик читает заявление, а не факт. Это ровно третий дефект мишени 3610 — «сессии не аутентифицируются» — и он закрыт лишь по форме.
**Выдан круг второй — волна 3938 (M4).** По первой дыре требование: порог обязан быть **обоснован кодом сценариев** (сколько личностей требует самый требовательный), а не выбран наугад. По второй — либо починить, либо **доказать, что внутри контракта манифеста это невозможно**, и предложить, куда вынести проверку (например, в раннер, который видит реальный ответ на пробный запрос). Молчать нельзя.
**Почему это хорошая новость, а не плохая.** До сегодня негодную мишень ловили руками, задним числом и случайно — так и застрял этот тикет. Теперь у нас есть инструмент, который отказывает до запуска, и **независимая проверка, которая знает, чего он ещё не умеет**. Дыры названы поимённо и имеют владельца.
Ветки: `refs/waves/3933/wave/3933-isolation-checker-fix`, `refs/waves/3937/acceptance/3937-acc-isofix`.
2026-09-14T23:06:21.702Z · coordinator[14.09 23:06Z координатор] **🔴 Аудит живого стенда (волна 3940, `VERDICT=NO-GO`) поймал в этом тикете ЧЕТЫРЕ ошибки. Тикет я писала по памяти и по чужим отчётам — ровно тот способ, за который ругаю волны. Владелец справедливо потребовал проверку; она нашла всё.**
## Что было неверно
**1. «Порт 3611 — мишень для замеров» — УЖЕ НЕТ, и это срочно.**
Сегодня за 3611 сидит **распределитель очереди от совсем другой работы**. Настоящие копии стенда переехали на **3621 и 3622**. Кто придёт мерить на 3611 — **померит не продукт**. Любая инструкция, называющая 3611, теперь вредна.
**2. «В корпусе 48 личностей» — НЕВЕРНО: их 2185.**
Аудит пересчитал сам. **48 — это сколько можно вести одновременно** (два пула по 24), а не размер корпуса. ⚠️ Предыдущая волна (3935) это уже объясняла, **а тикет повторил старую ошибку**.
**Последствие: моё решение по CAP-10/CAP-13 стояло на неверном основании.** Я унесла ступени 50/100/1000 в CAP-13 с формулировкой «корпус 48, ступень 50 нечем населить». **Корпус 2185 — ступень 50 возможна.** Решение подлежит пересмотру: в CAP-13 остаётся «1000», но «50 и 100» надо вернуть в область достижимого и сказать, что мешает на самом деле.
**3. «Egress отрезан» — НЕВЕРНО.**
Запрет на машине есть, но выписан **на другого жильца**: правило на `uid 999`, а стенд работает под **`uid 1001`**. Стенду наружу **никто ничего не запрещал**. Это прямая дыра в изоляции и отдельная причина, по которой **CAP-02 (WEB-629) не закрывается**.
**4. «Стенд — один экземпляр на двух портах» — НЕВЕРНО.**
Экземпляров **два**, из **разных сборок**, с **разными хранилищами**: у одного своя отдельная база и свой склад, у другого — в общем. А `cap3610stand` — это **имя базы**, а не имя экземпляра.
## Что аудит добавил сверх тикета
**Стенд никем не сторожится.** Он поднят руками в ssh-сессии: пока окно открыто — работает. Закроется окно или перезагрузится машина — **стенд исчезнет, и поднять его будет некому**. Настоящий сайт рядом под присмотром systemd, стенд — нет.
**Тетрадка с секретами (`app.env`) заперта** и аудиту недоступна; он нашёл рядом открытую копию такой же и переписал из неё.
**Сторож замерялки работает:** проверяет, что манифест приложен к той сборке, из которой собрана копия; не та сборка — замер не начнётся.
## Что это меняет
1. **Тикет переписывается по фактам аудита**, а не по моей памяти — порты, число экземпляров, размер корпуса, состояние egress.
2. **Решение по ступеням 50/100 пересматривается** — основание было ложным.
3. **Дыра в egress (`uid 999` вместо `uid 1001`) идёт в CAP-02** как отдельный пункт: «изоляция объявлена, но правило выписано не на того пользователя».
4. **Отсутствие присмотра за стендом** — первое требование к воспроизводимой сборке: стенд обязан подниматься как служба, а не из ssh-окна.
**Урок, который стоит записать явно:** я потребовала от волны ловить мои ошибки — и она поймала четыре, включая одну, на которой стояло принятое решение. Описание инфраструктуры **по памяти** недопустимо; только командой на живой машине.
2026-09-15T01:17:09.577Z · coordinator[15.09 01:17Z координатор] ## 15.09 01:30Z — остаток сформулирован точно: egress НЕ отрезан, и причина известна.
Правило ufw выписано на **`uid 999`**, а стенд работает под **`uid 1001`**. То есть правило есть, выглядит как защита и ничего не режет. Это ровно тот класс, что и мёртвый `failClosed` в телефонии (WEB-665): объявленная защита, которая не участвует в решении.
Проверка «есть ли правило» такой дефект не ловит — ловит только проверка «уходит ли трафик у ТОГО пользователя, под которым реально работает стенд».
Работа передана волне **3948** (WEB-662): настоящее отрезание egress для реального пользователя стенда входит в сценарий воспроизводимой сборки. Разводить это на две волны бессмысленно — егress отрезается там же, где стенд создаётся.
**CAP-02 закрывается вместе с CAP-14**, когда 3948 сдаст и пройдёт приёмку. До тех пор статус review держим сознательно: работа сделана не полностью, и закрывать её нельзя.
2026-09-15T01:25:15.070Z · coordinator[15.09 01:25Z координатор] ## 15.09 01:40Z — ВОШЁЛ В ПОСАДКУ l115o. Требуется пост-QA на проде.
Прод работает на `l115o-68e25d8d` (коммит `68e25d8df5ae8263c5ac5466353631f57a17cfcc`, артефакт `cd2773f65d66b09ec03d39b553041d2814ce09413e72d72b07bc9de89df536c8`) с 00:56Z 15.09. Посадка проверена: `ready=true`, коммит совпал, браузерная проба открыла документ, 0 новых ошибок.
**Работу по этому тикету принесли волны:** 3429, 3441.
**Что это значит для этого тикета.** Работа по нему пролежала принятой, но НЕ посаженной — в некоторых случаях неделями. Теперь она на проде. Приёмка волной доказывала, что код правильный в дереве волны; она НЕ доказывала, что фича работает на живом проде. Это разные вещи, и мы на этом уже обжигались.
**Поэтому статус — `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-15T10:46:41.136Z · coordinator[15.09 10:46Z координатор] Post-QA 3973: FAIL на текущем стенде. Egress-правило выписано для uid 999, а фактический стенд работает под uid 1001; объявленная защита не участвует в решении. Следующий шаг: привязать egress к реальному uid и повторить isolated T0 с отрицательным same-uid сетевым контролем. Карточку оставить review.
2026-09-15T13:21:37.596Z · coordinator[15.09 13:21Z координатор] [15.09 13:20Z координатор] Post-QA 3973 воспроизвёл current-line дефект: egress-защита относится к uid 999, а фактический app-host работает под uid 1001, поэтому объявленное правило не участвует в решении. Статус `review → in_progress`. A2/Luna 4019 чинит источник UID fail-closed и обязана показать BASE RED → candidate GREEN в одноразовом сетевом sandbox: forbidden listener недостижим для фактического UID, loopback DB/Redis остаётся доступен, mismatch/unknown/production destination дают nonzero отказ, T0 и cleanup подтверждены. Живой standby, host-wide firewall и production не трогать. Авторский GO потребует независимой приёмки до интеграции.
2026-09-15T13:54:11.929Z · coordinator[15.09 13:54Z координатор] [15.09 13:50Z координатор] Corrective 4019: VERDICT=GO, ready for independent acceptance only. Candidate 9ca6dc2c808c17c3b06a77662378904fcb4fbac5 derives the egress owner from the paused live app PID, validates /proc real/effective/saved/fs UID/GID against the named non-root account, installs the owner rule before resume, and revalidates the identity at the proxy. Exact BASE RED: actual uid 1001 bypassed the static uid 999 rule. Candidate GREEN: forbidden synthetic destination blocked, 3/3 loopback links preserved; missing/stale/root/mismatch cases fail closed. Focused tests 46/46 plus own 4/4, T0 cleanup PASS, evidence 15/15 and bundle sidecar verified. No host-wide firewall, live standby, production or provider touched. Independent acceptance dispatched separately; ticket stays in_progress.
2026-09-15T14:10:47.742Z · coordinator[15.09 14:10Z координатор] [15.09 14:09Z координатор] Независимая приёмка 4031: VERDICT=NO-GO, evidence 9/9 SHA256. Bundle 9ca6dc2c… подлинный, 53/53 focused tests зелёные, но firewall verify/teardown перечитывают числовые UID/GID из identity-файла без строгого сравнения с live /proc preflight; дубликаты/лишние поля могут быть приняты. Darwin не дал независимо повторить Linux namespace RED→GREEN, поэтому supplied proof не заменён собственным. WEB-629 остаётся in_progress; требуется fail-closed canonical identity parsing + Linux independent acceptance.
2026-09-15T14:35:06.021Z · coordinator[15.09 14:35Z координатор] [15.09 14:34Z координатор] A1/4040: VERDICT=INCOMPLETE, не GO.
Cumulative candidate `96e7230658e756338571ab94c266e9fef579fc7c` от exact l115o сохраняет live-PID design 4019 и исправляет конкретный NO-GO 4031: identity record теперь строгий, однозначный и сравнивается со свежими real/effective/saved/fs UID/GID из `/proc/<pid>/status` перед verify, proxy use и teardown. Source/CAP-02 проверки: 53/53, syntax и diff-check зелёные; bundle prerequisite ровно l115o, evidence 10/10 SHA256.
Обязательный namespace RED→GREEN на A1 не выполнен: sandbox имеет `no_new_privs=1`, поэтому private namespace не создался. Это единственный остаток, а не PASS. Пакет передан A2/4049, где 4019 уже выполняла такой root-created disposable namespace stand. WEB-629 остаётся `in_progress` до независимого runtime GO; production/firewall/standby не менялись.
2026-09-15T15:01:12.289Z · coordinator[15.09 15:01Z координатор] [15.09 15:00Z координатор] A2/4049 independent cumulative acceptance: VERDICT=NO-GO, evidence 8/8 SHA256. Exact candidate 96e7230658e756338571ab94c266e9fef579fc7c reproduces the intended namespace firewall: BASE bypass is RED, candidate apply/verify and 10 fail-closed identity cases pass, UID 1001 loopback 3/3 allowed and forbidden destination rejected, teardown residue 0. Two source defects block acceptance: tests/capacity/egress-uid-repair-stand.sh expands label under set -u before assignment, so the full WEB-629 suite is 3/4; and gate0-step2/egress-fence/proxy.js rejects a valid strict 12-line live identity record with HTTP 403 instead of 200. WEB-629 remains in_progress. Next step: narrow cumulative repair of those two defects, then repeat independent namespace/proxy acceptance.
2026-09-15T15:17:53.154Z · coordinator[15.09 15:17Z координатор] 4058 author repair = GO, отправляется на свежую независимую приёмку. Candidate b875406a248aa8f00e8399abcfce9a2b49437e07 исправляет set -u helper и strict proxy absolute-form health request без ослабления identity. WEB-629 full 4/4, CAP-02 53/53, disposable namespace: BASE RED, candidate GREEN, proxy valid=200, legacy/tampered=403, residue=0. SHA manifest цел; production не менялся.
2026-09-15T15:40:17.441Z · coordinator[15.09 15:40Z координатор] Финальная A2/4065: INCOMPLETE, не NO-GO кандидата. Bundle/head/prerequisite/ancestry и точный трёхфайловый diff подтверждены, SHA evidence 6/6. Исполнитель повторил unprivileged `unshare`, который AppArmor запрещает, поэтому решающий namespace-прогон не состоялся. На хосте есть `sudo NOPASSWD`; перевыпущена A2/Terra 4067 с явным `sudo -n` только внутри одноразового namespace. WEB-629 остаётся in_progress до независимого BASE RED/candidate GREEN и нулевого residue.
2026-09-15T15:47:14.887Z · coordinator[15.09 15:47Z координатор] Финальная независимая A2/Terra 4067: GO для source-входа следующей линии, не production closure. BASE `68e25d8df5ae8263c5ac5466353631f57a17cfcc`, candidate `b875406a248aa8f00e8399abcfce9a2b49437e07`; bundle, ancestry, clean three-file diff и SHA manifest 10/10 подтверждены. Свежий `sudo -n unshare --net` прогон: BASE legacy UID-999 bypass RED; candidate UID-1001 GREEN, loopback 3/3, non-loopback denied, strict 12-line identity agreement на apply/verify/proxy/teardown, origin/absolute health 200/200, восемь классов поддельных/stale записей 403, IPv4/IPv6 правила точны, residue=0. WEB-629 portable 3/3, full 4/4, CAP-02 exact seven-file 53/53. Карточка переводится в review до сборки, посадки и post-QA на проде.
2026-09-15T16:43:21.492Z · coordinator[15.09 16:43Z координатор] [15.09 16:42Z координатор] M4/Terra 4087 собрала cumulative tip `f69b6fbe720bb281d94562f11db5f23afb63e898`: first parent — repaired next-line `4f305668…`, second parent — independently accepted WEB-629 `b875406a…`; пересечение путей пустое, merge автоматический, bundle SHA `4770e796…`, evidence 5/5. Вердикт честно INCOMPLETE: macOS не имеет `/proc/net/tcp`, `sudo -n` namespace недоступен и нет доверенного dependency tree, поэтому Linux/root и parent type/lint гейты не выданы за PASS. Неизменный tip передан на A1 как 4091 для свежей Linux/root приёмки; WEB-629 остаётся review.
2026-09-15T17:01:41.149Z · coordinator[15.09 17:01Z координатор] [15.09 17:02Z координатор] A1/Luna 4091 завершила Linux acceptance cumulative tip `f69b6fbe…`: VERDICT=INCOMPLETE, report+evidence SHA 7/7 OK, marker не создан. Provenance/parents/bundle и все supplied manifests зелёные; portable WEB-629 3/3, CAP-02 53/53, activation/release 9/9, WEB-635 8/8+4/4, migration/projection/syntax/diff gates PASS.
A1 `NoNewPrivs` снова не даёт войти в `sudo -n unshare`, поэтому privileged namespace proof отсюда не повторён (он уже был GO на A2/4067). Но 4091 нашла отдельный candidate-only lint defect: cumulative tip добавляет `@typescript-eslint/no-require-imports` в `proxy.js` из-за `require('node:child_process')`. Billing comparison одинаково 20 pass / 12 fail на PARENT и CANDIDATE из-за общего mocked Prisma preflight; scoped tsc OOM 2 GiB и не даёт вердикта. Следующий шаг — M4/Terra 4098 чинит только lint/source integration gate, затем новый tip принимает A2 с реальным namespace. WEB-629 остаётся review, общей линии GO ещё нет.
2026-09-15T22:04:49.931Z · coordinatorDRAFT-DELTA-20260915-WEB-629 (append-only; правила обогащения: WEB-449).
ЧЕРНОВИК волны 4129 (M1/DeepSeek Flash). Опубликовано координатором после проверки SHA пакета.
УЖЕ ПОКРЫТО историей тикета (не дублируется):
- точная формулировка остатка (egress не отрезан, причина известна) — 2026-09-15T01:17:09.577Z;
- FAIL на текущем стенде — 2026-09-15T10:46:41.136Z и воспроизведение — 13:21:37.596Z;
- цепочка кандидатов — 4019 GO 13:54:11.929Z, 4031 NO-GO 14:10:47.742Z, 4040 INCOMPLETE
14:35:06.021Z, 4049 NO-GO 15:01:12.289Z, 4058 repair GO 15:17:53.154Z (candidate
`b875406a248aa8f00e8399abcfce9a2b49…`), 4065 INCOMPLETE 15:40:17.441Z,
4067 GO 15:47:14.887Z (BASE `68e25d8df…`);
- текущий tip — 2026-09-15T16:43:21.492Z (4087, `f69b6fbe720bb281d94562f11db5f23afb63e898`)
и 17:01:41.149Z (4091 A1/Luna, `VERDICT=INCOMPLETE`, SHA 7/7).
ЧЕГО НЕ ХВАТАЕТ ПО КАНОНУ WEB-449:
1) НЕТ блока KNOWN ISSUES в формате канона. Главный known issue тикета —
ДЫРА В ИЗОЛЯЦИИ, и у неё нет ни «как проверить за 2 минуты», ни «причина файл:строка»:
егресс-правило выписано на `uid 999`, а фактический стенд работает под `uid 1001`
(2026-09-15T10:46:41.136Z, 13:21:37.596Z). Проверка за 2 минуты:
`ss -ltnp | grep -E ':(3621|3622)'` — взять pid стенда и сверить
`ps -o uid= -p <pid>`; если uid ≠ 999, объявленная изоляция не действует.
Причина — правило писала одна волна, а поднимала стенд другая; точный файл правила
в комментариях 15.09 НЕ назван, и это надо назвать явно (в WEB-640
2026-09-14T23:06:22.962Z он описан как «правило на uid 999»).
2) Кандидат 4091 нашёл ОТДЕЛЬНЫЙ candidate-only lint-дефект, который не оформлен как
known issue с проверкой: cumulative tip добавляет `@typescript-eslint/no-require-imports`
в `proxy.js` из-за `require('node:child_process')` (2026-09-15T17:01:41.149Z).
Проверка за 2 минуты: `npx eslint proxy.js` — падение по этому правилу.
3) В 17:01:41.149Z нет МАШИННЫХ ПУТЕЙ к report и evidence 4091 (сказано только
«report+evidence SHA 7/7 OK, marker не создан»). По правилу 15 это заявка без ссылки
на факт: нужен путь вида `<host>:/<dir>/4091*-REPORT.md` и путь к `SHA256SUMS`.
То же для 4087 (16:43:21.492Z — названы только SHA bundle/evidence 5/5, без путей).
4) Тело заканчивается блоком 12.09 (`SHIFT-STOP-20260912:FINAL`, STOPPED,
`ИНДИВИДУАЛЬНЫЙ ОСТАТОК`) — противоречит `review` на 15.09.
5) Не зафиксирована граница: 4091 не повторила privileged namespace proof, потому что
A1 `NoNewPrivs` не пускает в `sudo -n unshare`; сам proof был GO только на A2/4067
(2026-09-15T17:01:41.149Z). Без этой пометки нулевой агент прочитает 4091 как провал.
ОСТАТОК на 15.09 17:01Z: M4/Terra 4098 чинит только lint/source integration gate,
затем новый tip принимает A2 с реальным namespace (2026-09-15T17:01:41.149Z).
WEB-629 остаётся `review`; общей линии GO ещё нет.
ПЕРВЫЙ ШАГ НУЛЕВОГО АГЕНТА (2 минуты, ничего не запускает): прочитать
2026-09-15T17:01:41.149Z и проверить, существует ли уже отчёт 4098; если нет —
считать линию незакрытой и НЕ повторять старый QA на l115o (прямое указание
2026-09-15T16:04:09.442Z: «повторять QA на l115o до новой линии бессмысленно»).
2026-09-15T23:42:37.957Z · coordinatorL115Q-LANDING-20260915-WEB-629
[15.09 23:38Z координатор] Новая исполняемая линия после предыдущего WAIT/REVIEW действительно посажена.
- Прод: `l115q`, exact source `cc278e1810e1e03ac277b8e3a5fe06a2aebfba30`, artifact `e9cdd5b041c29d058ef3c06e11ac39862e4de57d4ea307e79a7902d2d6535d86`.
- Machine verify: 3010/3012 `ready=true`, `paidReady=true`, `posture=enforce_ready`; background fresh; 4 unit checks success; old-process count 0; оба worker timer active.
- Миграции: live ledger 211, pending 0, inherited no-op receipt; rollback compatibility old l115o → новая схема: `dangerousCount=0`.
- Общий post-landing: BF-08 PASS по 107 changed paths / 105 operational paths / 8 классам. Browser/editor вошёл, увидел 58 документов, открыл документ; новых ошибок после клика 0.
- Платный smoke: один HTTP 200; одна бронь `SETTLED_ACTUAL` 40 724→1 013 микро; один terminal event `APPLIED`; artifact SHA совпадает.
- Evidence: `nc-ops-scripts/release-l115q/postlanding-evidence/`, SHA-манифест проверен.
Важно: эта общая расписка снимает только ожидание новой линии и общий landing/browser/paid smoke. Статус этой карточки не меняется автоматически: `done` допустим только после её собственного узкого served-boundary/post-QA критерия из последнего комментария. Следующий шаг нулевого агента — выполнить именно этот узкий остаток на l115q и записать отдельную машинную расписку; старые l115o FAIL/WAIT не считать текущим состоянием линии.
2026-09-16T01:57:56.686Z · coordinatorENRICH-4140-WEB-629
Аудит-источник: `WEB-626-ENTERPRISE2-REMAINDER-20260916.md` SHA256 `0eb55a41341b010b8005cb16b653504e408dec4dcbeb5c3f166cd5f4eacad2fe` (проверен 4140).
Живая доска: http://127.0.0.1:8787/api/web (localhost, read-only GET), read-only readback.
append-only; статус доски этой записью не двигается.
# WEB-629 — CAP-02: изолированный стенд, egress и preflight
## Самое сильное принятое доказательство
- Независимая A2/4067 `GO`: BASE RED / candidate GREEN в privileged namespace,
WEB-629 4/4, CAP-02 53/53; соответствующий код вошёл в carrier `l115q`.
- `l115q` несёт %s.
- Живой readback: статус `review`, последний комментарий `2026-09-15T23:42:37.957Z`.
## Чего НЕ ХВАТАЕТ на карточке
Общая расписка `l115q` (`23:42:37.957Z`) снимает только ожидание новой линии.
Точный узкий остаток карточки на ней не выписан: сказано «выполнить узкий остаток
из последнего комментария», но последним комментарием теперь является сама эта
общая расписка — ссылка замкнулась.
Историческая находка (`uid 999` в правиле против фактического `1001`) на карточке
есть, а полной приёмки изоляции — нет.
## Точная недостающая расписка
Один source/artifact/target-bound Linux receipt на одноразовом стенде:
- UID/GID из фактического app PID (`ss -ltnp` → `ps -o uid= -p <pid>`), а не из конфига;
- forbidden egress заблокирован;
- три разрешённых loopback-связи работают;
- run-owned auth принимается, production-shaped auth отвергается;
- T0 и cleanup PASS.
Ожидаемая форма перечислена в `worker-results/4103POSTLANDINGPLAN-REPORT.md`, раздел WEB-629.
## Первый шаг нулевого агента
За 2 минуты, ничего не запуская: поднять PID слушателя стенда и сравнить uid
с объявленным в правиле —
`ss -ltnp | grep -E ':(3621|3622)'` → `ps -o uid= -p <pid>`.
Расхождение (`1001` против `999`) означает, что объявленная изоляция не действует,
и receipt писать не на чем. Затем прочитать
`worker-results/4103POSTLANDINGPLAN-REPORT.md`, раздел WEB-629.
## Явные незамены
- `l115q ready=true`, общий browser/editor smoke и один платный запрос — НЕ заменяют
узкую расписку этой карточки.
- Fixture/package `GO` не равен post-landing PASS.
- Source ancestry (`git merge-base --is-ancestor`) не доказывает исполняемый runtime path.
- Более поздний NO-GO/INCOMPLETE или отозванный источник числа сильнее старого `done`/`GO`.
- `23.5/s`, «42 ms на действие», лучший из двух повторов и success-от-стартовавших
не возвращаются в итог без нового первичного происхождения.
2026-09-16T02:28:26.181Z · coordinatorPOSTQA-L115Q-4139-WEB629-AUTHOR-GO
Авторская A2/Terra 4139 сдала GO на exact l115q cc278e1810e1e03ac277b8e3a5fe06a2aebfba30 / artifact e9cdd5b041c29d058ef3c06e11ac39862e4de57d4ea307e79a7902d2d6535d86. Исторический uid=999 BASE RED воспроизведён; actual listener-owning UID/GID candidate GREEN дважды; run-owned auth accepted, production-shaped/duplicate/stale/foreign identities rejected. Evidence SHA256SUMS 29/29 PASS, оба teardown PASS, residue нет. Report SHA256 5741727280eee2480ba3f51e291dcb271d7e254f99ce5d9d6d4cbe83cb5d83ce. Это авторский GO, не независимая приёмка: M4/Luna 4153 уже проверяет пакет. Статус остаётся review до её вердикта.
2026-09-19T11:18:22.119Z · coordinatorPOSTQA-4153-REJECTED-4196-REWORK
После разморозки найден фактический итог независимой 4153: NOT ACCEPTED — NOT_RERUN. Exit=0 означал завершение модели, не GO. 28/28 входных SHA прошли, но отсутствовали artifact bytes/manifest; teardown не наблюдал исчезновение unit/PID/socket; raw P3009 и negative logs не сохранены; readiness содержал красные background checks; заявленный mode 0444 фактически был 0644. Terminal package 4153 не создан. 4196/DeepSeek Flash на M4 сейчас строит fail-closed r2 package по этим пяти блокерам. WEB-629 остаётся review.
2026-09-19T11:29:28.435Z · coordinatorREWORK-4196-READY-FOR-A2-RERUN
DeepSeek Flash 4196 завершила исправленный пакет после rejection 4153: VERDICT=GO_FOR_A2_RERUN, но это НЕ live GO. Координатор независимо проверил все 100 записей SHA256SUMS; terminal marker status=VERIFIED, mode policy scripts=0555/other=0444, liveStandBooted=false. Пять блокеров 4153 воспроизведены; 43/43 static controls и 7/7 mutation controls PASS. Следующий обязательный шаг — реальный A2 rerun на exact l115q artifact с raw P3009/negative logs и измеренным teardown. Статус review сохраняется.
2026-09-19T11:45:23.730Z · coordinator[19.09 11:45Z координатор] LIVE-A2-RERUN-4196-STARTED
Пакет 4196 после DeepSeek-ремонта повторно проверен координатором: полный SHA256SUMS зелёный, exact source cc278e1810e1e03ac277b8e3a5fe06a2aebfba30 доступен, exact artifact SHA-256 e9cdd5b041c29d058ef3c06e11ac39862e4de57d4ea307e79a7902d2d6535d86 совпал, старого evidence не было. Первая попытка честно NOT_STARTED: systemd/root получил Git dubious ownership до boot; отказ сохранён отдельно, глобальный git config не менялся. R2 запущена с одноразовым process-scoped safe.directory, unit nc4196-live-r2 active. Выполняются два независимых cold start, P3009, egress/auth controls и измеренный teardown. Background workers явно declared out of scope и не называются зелёными. До terminal overall.json, полного SHA readback и независимой проверки статус остаётся review.
2026-09-19T11:55:36.603Z · coordinator[19.09 11:55Z координатор] LIVE-RERUN-4196-R4-STARTED
4196 r2 and r3 both stopped before app/readiness, so neither is a ticket result. r2: disposable state root was inaccessible to intended UID 1001. r3: PostgreSQL/Redis and P3009 recovery ran, then the evidence emitter refused on one jq concatenation syntax error before app/firewall. Both failures are preserved with SHA; no NC_PROOF_EGRESS residue remains. Minimal r4 contains only intended state ownership (0750 ubuntu:ubuntu) plus parenthesized jq concatenation; bash syntax, isolated jq compile and ownership smoke passed. Live r4 is active on A2 as nc4196-live-r4. WEB-629/662 stay review until terminal report + complete SHA readback.
2026-09-19T11:58:40.712Z · coordinator[19.09 11:58Z координатор] LIVE-RERUN-4196-R5-STARTED
r4 is NOT_STARTED as a ticket verdict: the first cold start reached a live next-server and passed DB/P3009/fence preflight, but the outer collector exited before readiness capture because `local ... out="$3" rawdir="$out/..."` expanded `out` under set -u in the same declaration. The live unit was authenticated and torn down; firewall teardown PASS, unit stopped, state removed, no NC_PROOF_EGRESS residue. r4 failure is preserved with SHA. r5 splits both capture/teardown local declarations, bash syntax passes, and is now active on A2. Status remains review until both cold starts, teardown receipts and complete SHA readback finish.
2026-09-19T14:21:31.232Z · coordinator[4196 r7 ORCHESTRATION_REFUSAL 19.09 14:20Z]
Live rerun 4196 r7 on A2 completed two disposable cold-start phases, but issued no product verdict.
Verified phase evidence:
- both /api/ready receipts: ready=true, status=ok, failures=[], sourceCommit cc278e1810e1e03ac277b8e3a5fe06a2aebfba30;
- release ids are the expected distinct identities 4196-l115q-first and 4196-l115q-second;
- application scope GREEN in both phases;
- background scope RED_DECLARED_OUT_OF_SCOPE in both phases, yielding GO_WITH_DECLARED_EXCLUSIONS rather than an unconditional PASS;
- both measured teardowns PASS: unit/app/PostgreSQL/Redis/sockets/identity absent, firewall baseline identical, residue=[].
The run then failed in the coordinator cross-phase gate before overall.json and the original final manifest. The accepted r7 runner invoked jq -e with slurpfiles but without -n and without an input document. Reproduction on the sealed receipts: old invocation exit=4; identical expression with jq -n -e exit=0 and confirms both phases served the same source under distinct expected ids.
Classification: ORCHESTRATION_REFUSAL; productVerdict=NOT_ISSUED. Do not close WEB-629/WEB-662 from r7.
Durable A2 evidence: /home/ubuntu/waves/4196STAND-evidence-r7, 90 files. Post-run SHA256SUMS readback PASS; manifest SHA-256 674d5bcaa6ac2cbb56b18de6aab05ebeabeb96a3b15963c4fff77617aa38ac1a. Cleanup readback: state residue 0, NC_PROOF_EGRESS IPv4/IPv6 0.
Minimal r8 changes only VERSION and jq -n -e; r8 requires a fresh live run and its own readback before any ticket closure.
2026-09-19T14:25:32.034Z · coordinator[19.09 14:25Z координатор] [4196 r8 ORCHESTRATION_REFUSAL + RESCUE 19.09 14:24Z]
r8 used the minimal verified diff from r7: VERSION=4196-r8 and jq -n -e in the cross-phase gate; runner SHA-256 9ec703afeb3a506d4b94cb293da7eb2bc64f9c6f8584f28dfad1c4e8c6cb3ef0.
The first disposable phase completed and tore down. During the second phase the outer runner's start wait expired at 30 seconds and exited with "phase second failed to start", but the inner transient unit continued booting and subsequently served /api/ready successfully inside its namespace:
- ready=true, status=ok, failures=[];
- release.id=4196-l115q-second;
- sourceCommit=cc278e1810e1e03ac277b8e3a5fe06a2aebfba30.
This is an orchestration timeout, not a product verdict. The outer failure left the second inner unit active, so the coordinator performed a bounded rescue: captured state inventory, journal, live readiness, listeners and raw logs; invoked the accepted authenticated fence teardown inside the namespace; stopped the exact transient unit; copied final logs; removed only the exact disposable state root; verified no 4196 state roots and no NC_PROOF_EGRESS IPv4/IPv6 residue.
Durable rescue evidence: /home/ubuntu/waves/4196STAND-evidence-r8-rescue-20260919T1423Z, 2019 files; SHA256SUMS readback PASS; manifest SHA-256 f7dcbc04e66ffaaea1bc14acca94b170966771d81707fea56dc2afde1e02547e.
Classification: ORCHESTRATION_REFUSAL; productVerdict=NOT_ISSUED. WEB-629/WEB-662 remain review. A future rerun must replace the fixed 30-second start wait with readiness-aware terminal handling and independently verify the resulting package before launch.
2026-09-19T15:27:58.672Z · coordinator[4196 r9 STARTED AFTER 4205 GO 19.09 15:27Z]
Independent DeepSeek Pro acceptance 4205 returned GO_FOR_A2_RERUN for exact runner SHA-256 817994b0e31250a34968e898abf9518cda469972b9bdc53de557b07bb181eec9. Coordinator readback: acceptance SHA256SUMS PASS, manifest SHA-256 220a4b41d50de2ee7bb524f5867753748c5afce3121a483f738bc7dbaf972851, independent controls 11/11, mutations 6/6.
The first coordinator launch wrapper refused before runner entry because nested shell quoting produced empty cwd/log paths. Unit nc4196-live-r9.service exited status 2; state roots 0, firewall residue 0/0, inner units 0, evidence/log/launch receipt absent. This was an orchestration refusal, not a product result.
The immutable base package was restored from /home/ubuntu/waves/4196-delivery.tar.gz SHA-256 8a813a3b87a3ff85a5c1302ff32d2595277cec0a2db6f55d5924b2732df1276e, original SHA256SUMS passed, accepted r9 inserted, full R9 manifest readback passed with digest 25eca0f404f8659cae3727d879ef9ffb5435a699f0e2ffde5b76047fdf2ebc87. One corrected live rerun is now active as nc4196-live-r9a.service, PID 2752661, invocation 0aadb1b0984045b393dc48d7400ee608. This is not yet a product verdict. Keep both tickets in review until two cold starts, evidence checksums, teardown and terminal residue readback complete.
2026-09-19T15:35:44.492Z · coordinator[4196 r9 ORCHESTRATION_REFUSAL 19.09 15:28Z]
Exact independently accepted r9 runner SHA-256 817994b0e31250a34968e898abf9518cda469972b9bdc53de557b07bb181eec9 completed both cold starts on A2. Distinct release ids were served (`4196-l115q-first`, `4196-l115q-second`) with the same exact source cc278e1810e1e03ac277b8e3a5fe06a2aebfba30. Runner wrote overall claim GO_WITH_DECLARED_EXCLUSIONS, application readiness GREEN, and WEB-629/WEB-662 PASS under the declared background-heartbeat exclusion.
This is NOT a product verdict because the final receipt verifier correctly returned REFUSE: six inner receipts (`fence-auth`, `migration-tree`, `p3009`, both phases) carried fallback runId `4196-unset` instead of outer run id `4196-l115q-20260919T152702Z-2752661`. Root cause is executable: outer assigns RUN_ID/UNIT_NAME, but systemd-run does not pass them to the inner shell. Outer exited 1.
Terminal cleanup readback PASS: state roots 0, NC_PROOF_EGRESS IPv4/IPv6 residue 0. Evidence sealed in `/home/ubuntu/waves/4196STAND-evidence-r9`: 94 files plus SHA256SUMS, fresh readback PASS, manifest SHA-256 f2d67a36eac9a1b7ad838eefb8955c6fe53ec46e33ce0a7c8c84e67332c90afd.
A narrow static author repair 4208 is active on the owner laptop. It may only propagate and validate exact RUN_ID/UNIT_NAME and must preserve all r9 gates. No new A2 rerun until 4208 terminal package, coordinator checksum readback, and a fresh independent acceptance. Keep ticket in review.
2026-09-19T16:02:45.651Z · coordinator[4196 R10 AUTHOR GO; 4214 INDEPENDENT STARTED 19.09]
Author DeepSeek Pro package 4208 completed `GO_FOR_INDEPENDENT_ACCEPTANCE`. Output checksum readback PASS; manifest SHA-256 `be27cb04d54c51d1c2e030ee3041682389c236b71ce03df44a425f8e5c9b7604`; r10 runner SHA-256 `53086d1ba0756bd81f172e317bf0d70aad54e351fd381fe8ff7ec5dac71f37a8`. Author controls 12/12 and mutations 4/4.
Repair scope: propagate exact outer `RUN_ID` and `UNIT_NAME` into both inner systemd phases and fail closed before live receipts if identity, phase, state/unit marker, or run.json disagree. This is author evidence only — no A2 run or product verdict.
Fresh independent DeepSeek Pro acceptance 4214 is active on M1 from exact r9+r10 immutable inputs; outer input manifest `84ef77db82ee633b31c2ac6b4b29336ebb782f5e4bd95c794670eedcc38792bf`. No A2 rerun is authorized before terminal verdict, coordinator checksum/readback, and explicit `GO_FOR_ONE_A2_RERUN`. Status remains `review`.
2026-09-19T16:24:02.705Z · coordinator[4196 R10 A2 POST-QA COMPLETE 19.09]
The single independently-authorized A2 r10 rerun completed `GO_WITH_DECLARED_EXCLUSIONS` on exact production source/artifact identity: source `cc278e1810e1e03ac277b8e3a5fe06a2aebfba30`, artifact `e9cdd5b041c29d058ef3c06e11ac39862e4de57d4ea307e79a7902d2d6535d86`. Evidence `SHA256SUMS` full readback PASS; manifest SHA-256 `541c1093bc11d0688383c7d0e71502ffc75d331934f069807378a354a7efc419` (94 files).
Bound live results:
- two cold starts served distinct release ids `4196-l115q-first` / `4196-l115q-second` with the same exact source; application boot/database/release GREEN in both;
- WEB-629 egress fence: old uid-999 rule did not protect actual uid 1001; candidate blocked non-loopback for the actual run-owned identity; production-shaped, duplicate, stale, foreign, and cleanup-failure identities all refused in both phases;
- WEB-662 artifact/source migration trees byte-identical: 207 files, digest `e368b9f0d3ec3d39a0b064701b5d7a759c7ecb930618c4807ac53e2cdfb7c99b`; P3009 observed then recovered; 206 migrations applied in both phases;
- receipt verification PASS: 19 checked, all live, no synthetic-as-live, no bypass flag, no errors;
- teardown PASS twice and coordinator post-check: state roots 0, active 4196 units 0, firewall residue IPv4/IPv6 0.
Declared boundary: background worker heartbeats were RED and explicitly out of scope on this disposable stand; `unconditionalPass=false`. This is A2 post-QA evidence, not direct production execution. A1 remains healthy on the same exact source, but no ticket-specific destructive prod rerun was performed. Therefore status stays `review`; no closure is claimed.
2026-09-19T21:07:25.653Z · coordinator[COORDINATOR CLOSURE 2026-09-19 — r10 isolated post-QA criteria satisfied]
Coordinator independently re-read the sealed A2 r10 package after the earlier conservative no-closure comment. `/home/ubuntu/waves/4196STAND-evidence-r10/SHA256SUMS` passes 93/93, SHA-256 `541c1093bc11d0688383c7d0e71502ffc75d331934f069807378a354a7efc419`, self-entry 0. Both cold starts served distinct release ids from exact current production source `cc278e1810e1e03ac277b8e3a5fe06a2aebfba30` and artifact `e9cdd5b041c29d058ef3c06e11ac39862e4de57d4ea307e79a7902d2d6535d86`. WEB-629: actual uid 1001 non-loopback egress blocked; production-shaped/duplicate/stale/foreign/cleanup-failure identities refused in both phases. WEB-662: source/artifact migration trees byte-identical 207/207, digest `e368b9f0d3ec3d39a0b064701b5d7a759c7ecb930618c4807ac53e2cdfb7c99b`; P3009 observed and recovered; 206 migrations applied in each disposable phase. T0 PASS twice; receipt verifier PASS 19/19, all live, no synthetic-as-live or bypass; teardown PASS twice with no unit/app/Postgres/Redis/socket/identity/firewall residue. Background worker heartbeats remain RED_DECLARED_OUT_OF_SCOPE and `unconditionalPass=false`; they are not relabelled PASS and are outside these two tickets’ isolated application-stand closure criteria. This is isolated A2 post-QA bound to the exact production artifact, not a destructive production rerun. Both tickets’ stated closure conditions are satisfied.
Воркер
не проверен
4196-r10:a2-postqa-pass; coordinator-closed-20260919
coordinator
движение в панели: неизвестно
Подключиться и смотреть/перехватить руками. Колесо мыши листает; клавишами — Ctrl-b затем [, выход из прокрутки q. Отсоединиться — Ctrl-b затем d:
Прочитать историю панели без подключения — листается и ищется (/ поиск, q выход), воркеру не помешает:
Обновлён
2026-09-19T21:07:25.656Z