WEB-409 · Задача · — · web
[WEB370-K] Плановый cutover и rollback runbook
Закрыт
P1 · важно
ведёт: —
эпик: WEB-395
Суть
Источник: WEB370PLAN2. Зависимости: WEB370-B,G,H,I,J. Оценка: 1.5 дня. Это только runbook; запуск cutover требует отдельного owner GO.
Чем закрывается (приёмка)
Пом минутный runbook с commands/owners/timestamps; rollback branches tabletop-confirmed ≤15m; final delta мал; в каждом шаге ровно один host/provider route.
Доказательства
[2026-08-27 14:10Z] web409k в очереди A1: полный cutover+rollback runbook (только документ, запуск = отдельный owner GO), SIP/мессенджеры-непрерывность по директиве owner, DR-вариант.
[2026-08-27 15:10Z] WEB409K DONE: runbook 886 строк (cutover+rollback+DR, GO/ABORT критерии на каждом шаге, SIP/мессенджеры-непрерывность, 60-мин наблюдение). Честный blocker-список до исполнения (M1 lease delivery, Pi fence proof, Signal/TG production доказательства, N-репетиция, owner GO). Вердикт: готов для review/tabletop, исполнение = ABORT до закрытия блокеров. -> review.
[2026-08-27 20:29:19Z tickacc5] ВЕРДИКТ: частично доказано
Что проверил: открыл WEB409K-REPORT.md и WEB409K-RUNBOOK.md; wc -l дал 886; проверил owner/IC/witness, 15-minute clock, ABORT/rollback, provider/final-delta gates и execution receipts.
Что увидел: runbook детальный и запрещает запуск без owner GO; фиксирует отсутствующие WEB400B evidence/provider commands и NO-GO. Execution receipts, tabletop rollback <=15 min и final delta не предъявлены.
Отрицательный тест: поиск tabletop/owner-GO receipt не подтвердил исполнение; cutover/provider/rollback не выполнялись.
Чего не хватает: подписанный owner/IC GO, hash-addressed WEB400B snapshot/restore receipts, isolated tabletop с rollback <=15 min и final-delta/canary evidence.
[2026-08-27 22:05 UTC EVID409410] Read-only executability audit completed; no cutover, restore, fencing, provider action, systemd start/stop, relink, reboot, Pi mutation, M1 mutation, or git action performed.
Report: /home/ubuntu/waves/EVID409410-REPORT.md; sha256=80b56a8c95421d380ef8f183ce838ddbb87ce4988575b7bdaf06b50e07a43f2b.
Current A1 snapshot: avail=81893122048 B, required=85899345920 B, shortage=4006223872 B; /opt/nc-landing/releases contains web401c-a1-arm64-20260827 and clean-verify copy, but the runbook-required /opt/nc-landing/releases/web370-a1 is absent. A1 ACTIVE, lease, accepted-epoch and M1 pubkey are absent; production units remain inactive/disabled and app :3010 is HTTP 000.
Positive evidence: 14 installed unit/target files match local web404f-systemd sources; systemd-analyze verify rc=0; checker/guard/ports hashes match expected runbook values; fail-closed DENY is present. These do not prove release, rollback, Pi or M1 readiness.
Gaps: T+00 App controller, T+04 independent fence, T+05 WEB400B snapshotctl, T+07 restore command, T+11 provider controller and observation probe are absent or placeholders. PI_EXTERNAL_FENCE_PROOF, COMBINED_PI_PROOF_M1, COMBINED_A1_PROOF, COMBINED_A1_PROOF_M1 and A1_INBOX are not produced/reachable. Rollback backups/manifests are absent on A1; Pi/M1 read-only checks were blocked by timeout/auth failure. §6.1 and M1 status are not actually read-only because they write evidence/state.
Verdict: NO-GO; the runbook is not executable now, and l61 then l62 are not mapped to an existing immutable release path/hash with verifiable entry/exit and rollback evidence.
---
RUNBOOKFIX-2026-08-27
Обновлены локальные артефакты:
- WEB409K-RUNBOOK.md: каноническая таблица l62/l61 release→commit→artifact SHA→serving label; актуальные Pi runtime paths и note-clone-3010/3010; rollback на l61 и существующий l61 drop-in backup.
- В §6.3a записана systemd-ловушка: ExecStart ровно двумя строками, первая пустая reset; EnvironmentFile перебивает Environment=; добавлен exhaustive 40/64-hex cross-check launch directory against signed attestation.
- Добавлена post-switch identity/readiness gate: service active → paid readiness status=ok/paidReady=true/failures=[] → DB status=ok → process label=.disk label → public label; для R0/R1/R1b/R2/R3 и других отмеченных проверяющим шагов добавлены concrete exit proofs.
- RUNBOOKFIX-REPORT.md содержит таблицу «что было → что стало → на основании чего».
Проверка исполнимости: Pi read-only SSH недоступен (timeout/publickey), M1 read-only SSH недоступен (authentication failure), поэтому фактическое existence/effective-state подтверждение и недостающие controller/receipt schemas оставлены как «требуется подтверждение». A1 exact target, l61 serving label/runtime mapping, verifier/key path, WEB400B/provider/canary/observation и harmless M1 status command не выдуманы. Production cutover/rollback/restart/reboot/provider action и команды runbook не выполнялись; git и секреты не затрагивались. Результат: документ обновлён, GO остаётся NO-GO до receipts.
## 2026-08-28 ~12:15Z — взято в работу (координатор Фабл)
Волна `web409` — плановый переход и откат (это и есть «переключатель»). Строится на карте WEB-399, переписи WEB-401 и снимке состояния WEB-400. Обязательно: **точка невозврата названа явно**; проверка после переключения — «продукт отвечает по делу», а не «служба запустилась» (отказ вида 401/403 успехом не считается — на этом уже обожглись в ACC400); отдельно перечислить, что переживёт переключение плохо: звонки в процессе, открытые комнаты, очереди.
[28.08 ~13:35Z Фабл] web409 сдана: плановый переход и откат (сам переключатель); заявлено — невидимых переключений нет (звонки, Realtime, lease, таймеры, Telegram identity, DNS названы), непроверяемое честно помечено НЕ ПРОВЕРЕНО. Заряжена приёмка **acc409** (очередь 258): точка невозврата явно, успех = «продукт отвечает по делу» (не 401/403), согласованность трёх документов 409↔410↔400B, запрет на SIP-ветки. Отчёты: WEB409-REPORT.md, будет ACC409-REPORT.md.
[28.08 14:10Z Фабл] ACC409 = NO-GO: первый реальный switch запрещён до закрытия блокеров; приёмщик выдал чек-лист координатора (свежий baseline A1, owner-approved scope с запретом SIP-веток, M1/ACTIVE/lease доказательство, FREEZE_ID/READY квитанции по WEB400B, substantive probe на реальном маршруте, dry-run calls/rooms/queues/timers/Telegram/DNS, один утверждённый pre-NPR rollback). Заряжен web409b (очередь 259): блокеры + чек-лист координатора с готовыми командами. Отчёты: ACC409-REPORT.md, будет WEB409B-REPORT.md.
[28.08 19:10Z Фабл] ACC409C = NO-GO: принятое живо (fsync/fencing/freshness), но ожидаемые live-wrappers отсутствуют в ops/web409/bin (только coordinator). Заряжен web409d (очередь 270). Отчёты: ACC409C-REPORT.md.
[28.08 19:45Z Фабл] ⚠️ TODO: web409d (live-wrappers в ops/web409/bin) СДАН, приёмка acc409e НЕ ЗАРЯЖЕНА (потерялась при перегрузе A1) — зарядить! ЭВОЛЮЦИЯ ТУМБЛЕРА: web409 (план+откат) → ACC409 NO-GO (перепись критериев: NPR явно, успех=«по делу», чек-лист координатора) → web409b → ACC409B NO-GO (state без fsync/atomic — crash-window мешает старое с новым; flock не тот; stale проходит) → web409c (fsync+rename+recovery, per-run dir+fencing, schema+freshness) → ACC409C NO-GO (wrappers отсутствовали) → web409d СДАН. Смежные: аварийный WEB-410 (сдан+квитанция §1.4 WAL из сейфа), учение WEB-400 (круг web400g). Отчёты: A1:WEB409*-REPORT.md, ACC409*-REPORT.md. После GO: чек-лист первого переключения владельцу на подпись.
[28.08 ~18:38Z ACC409E ЗАРЯЖЕНА И БЕЖИТ — TODO из хендоффа №2 закрыт]
Приёмка потерялась при перегрузе A1 (я запустил 10 опус-волн на 4 ядрах, машину увело в load 89-100, часть волн погибла, брифы в /tmp пропали при перезагрузке). Теперь бриф лежит устойчиво: /home/ubuntu/waves/queue/281-acc409e-brief.md.
Проверяется работа web409d (дерево /home/ubuntu/waves/wt-web409, ветка wk-web409, HEAD 11c57182, отчёт /home/ubuntu/waves/WEB409D-REPORT.md): P0-окно ACC409C закрыто (advance сверяет previous_receipt_sha256 == state.last_receipt_sha256 ДО записи), 17 acceptance-wrapper-ов + 2 supporting на общем wrapper_lib.py, recovery отвергает symlink/nested, подтверждённый business commit блокирует pre-NPR откат, init делает runs/ приватным 0700.
Бриф приёмки требует: воспроизвести гонку/крэш-окно самому (чужой предшественник, обрыв между receipt и state, два одновременных advance), три вопроса, матрицу мусора, негатив по каждому из 17 wrapper-ов (отсутствие owner-receipt = явный WEB409_REFUSAL, не молчаливый PASS), проверку что health/login/пустой/чужой JSON/401/403/5xx не считаются PASS, обязательный негативный тест.
ОТДЕЛЬНАЯ СТРОКА ВЕРДИКТА: готов ли тумблер к ПЕРВОМУ РЕАЛЬНОМУ переключению под подписью owner-а + чек-лист шагов (что owner делает руками, что проверяет, как откатывается).
[28.08 ~18:53Z ACC409E ВЕРДИКТ: NO-GO — и это сигнал менять УСТРОЙСТВО]
Отчёт: /home/ubuntu/waves/ACC409E-REPORT.md (30 КБ, дерево приёмки несло состояние автора, sha обязательных файлов совпали).
ЗАКРЫТО прошлым кругом (подтверждено пробами приёмщика): P0 predecessor до записи (RC=75, durable state цел), crash между receipt и state (RC=97 → recovery RC=76), рестарт на durable disk, два advance/lock/CAS (второй RC=73), takeover/fencing (старый токен RC=74), missing receipts/nested corruption (RC=76), все 17 wrapper-ов при отсутствии proof дают явный WEB409_REFUSAL, post-NPR переходы, негативный тест PASS.
НОВЫЕ БЛОКЕРЫ (9 находок, 5×P0). ТРИ ОДНОГО КЛАССА — «доказательство принимается по форме, а не по подлинности»:
• F-01 P0 — substantive validator считает ответ деловым, если в JSON есть поле id (wrapper_lib.py:1129) → health-id/login/foreign дают rc=0 PASS. Требование брифа «health/login/пустой/чужой не PASS» нарушено.
• F-02 P0 — commit-npr принимает ПРОИЗВОЛЬНОЕ evidence: приёмщик подсунул {"attacker":"not-owner","result":"PASS"} и получил npr_state=COMMITTED, phase=NPR_COMMITTED без всякого внешнего business write (coordinator.py:441,854).
• F-03 P0 — route wrapper принимает НЕПОДПИСАННОЕ provider acknowledgement.
Смежные (тот же корень): F-04 P0 symlink escape за границы evidence/state root; F-06 P0 wrappers mode 775 и вне pinned integrity (исполняемое доказательство подменяемо); F-05/F-09 P1 recovery fail-open на повреждённом marker и глушение отказа публикации FAIL-evidence; F-07/F-08 P1 loopback через альтернативные hostname-формы и ложное «independent client».
ВЕРДИКТ ПРИЁМКИ ДОСЛОВНО: первое реальное переключение ЗАПРЕЩЕНО до изменения trust architecture; чек-лист первого переключения не применяется.
РЕШЕНИЕ КООРДИНАТОРА (правило «третья находка одного класса = менять устройство»): круг 6 — НЕ заплатки, а переустройство доверия. Запущена волна WEB409E на опусе (/home/ubuntu/waves/briefs-opus/web409e-brief.md): единая точка проверки вместо самостоятельных решений wrapper-ов; подпись владельца как ЕДИНСТВЕННЫЙ источник доверия через УЖЕ работающий механизм релизов (ed25519 + канонический JSON + pin публичного ключа, тот же, которым прод проверяет активацию); явный контракт делового объекта вместо «есть id»; realpath-границы путей; манифест хешей исполняемых гейтов; fail-closed везде. Приватного ключа волне не даём — она делает ПРОВЕРКУ и выдаёт точную команду подписи для owner-а.
[28.08 ~19:35Z WEB409E СДАН — устройство доверия переделано, приёмка ACC409F заряжена]
Коммит e1241cc9 в /home/ubuntu/waves/wt-web409e, отчёт /home/ubuntu/waves/WEB409E-REPORT.md. 66 тестов, повторный прогон оставляет дерево чистым.
Заявлено по трём вопросам: перезапуск — recovery ЗАНОВО проверяет каждую подпись; два одновременных контекста — проходит ровно один, чужой ключ даёт OWNER_KEY_ROTATED; повреждённый вход отличим и больше не мягче валидного.
★ ГЛАВНОЕ ДЛЯ ВЛАДЕЛЬЦА (слова автора): тумблер теперь ЧЕСТНО ОТКАЗЫВАЕТСЯ РАБОТАТЬ, пока владелец не выпустит ключ и не даст его отпечаток. Это состояние нового устройства, а не дефект — но именно оно, а не код, теперь блокирует первое переключение.
Приёмка ACC409F (очередь 296, база l65) обязана: воспроизвести все шесть находок прошлого круга на новом устройстве (подделанное evidence, health/login/foreign в substantive, неподписанный route-ack, symlink escape, подмена wrapper-а, fail-open на повреждённом marker); и отдельно искать дыры САМОГО нового устройства — обход единой точки прямым вызовом wrapper-а, правильно подписанное доказательство от ЧУЖОГО прогона/фазы (подпись без привязки — не доказательство), изменение подписанного объекта на байт, отсутствие приватного/тестового ключа в дереве. Плюс пошаговая процедура для владельца.
[28.08 ~20:30Z СОСТОЯНИЕ ТУМБЛЕРА ПЕРЕД КОМПАКТОМ]
Круг 5 (ACC409E) дал NO-GO с девятью находками, из них ТРИ одного класса — «доказательство принимается по форме, а не по подлинности»: подделанный файл {"attacker":"not-owner","result":"PASS"} проводил переход в COMMITTED; substantive validator принимал health/login/чужой объект по наличию поля id; route wrapper принимал неподписанное подтверждение провайдера. Плюс symlink escape, wrappers 775 вне pinned integrity, fail-open на повреждённом marker, глушение отказа публикации FAIL-evidence.
По правилу владельца (третья находка класса = менять устройство) запущена НЕ заплатка, а переустройство: WEB409E, коммит e1241cc9 «one verifier, owner signature as the only source of trust». Единая точка проверки; подпись владельца как единственный источник доверия через тот же механизм, которым прод проверяет активацию (ed25519 + канонический JSON + пин ключа); контракт делового объекта вместо «есть id»; realpath-границы; манифест хешей исполняемых гейтов; fail-closed везде. 66 тестов, повторный прогон оставляет дерево чистым.
★ ГЛАВНОЕ ДЛЯ ВЛАДЕЛЬЦА: тумблер теперь ЧЕСТНО ОТКАЗЫВАЕТСЯ РАБОТАТЬ, пока владелец не выпустит ключ и не даст его отпечаток. Это состояние нового устройства, а не дефект — но именно оно, а не код, теперь блокирует первое переключение.
ИДЁТ приёмка ACC409F (дерево создано ПРЯМО НА КОММИТЕ e1241cc9 — первый заход пришлось остановить: базовый патч вышел пустым, и перенос состояния поверх линии стёр 37 файлов l65). Приёмка обязана воспроизвести все шесть находок на новом устройстве И искать дыры самого устройства: обход единой точки прямым вызовом wrapper-а, правильно подписанное доказательство от ЧУЖОГО прогона/фазы (подпись без привязки — не доказательство), изменение подписанного объекта на байт, отсутствие приватного/тестового ключа в дереве. Плюс пошаговая процедура для владельца.
ПОСЛЕ GO: отдать владельцу процедуру выпуска ключа и чек-лист первого реального переключения. Тумблер — один из элементов миграции (WEB-409/WEB-410), см. также WEB-400 (SQL-страж миграции, круг WEB400I идёт).
[28.08 ~20:35Z ACC409F: NO-GO — подпись ОБЪЯВЛЕНА, но фактически НЕ ПРОВЕРЯЕТСЯ. Четвёртое появление класса]
Отчёт: /home/ubuntu/waves/ACC409F-REPORT.md. Дерево приёмки создано ПРАВИЛЬНО (прямо на коммите автора e1241cc9, HEAD и чистота проверены сразу — новое правило работает).
ТРИ P0 ОДНОГО КОРНЯ:
• N-01: wrapper_lib.py:778-834 проверяет наличие result=PASS, scope_sha256 и форму scope, но НЕ вызывает trust anchor / Ed25519 verifier. С самостоятельно посчитанным self-hash и detached JSON {"result":"PASS","scope_sha256":"…","signer":"not-owner"} команда bin/validate-config дала PASS rc=0 БЕЗ ключа владельца. Приёмщик отдельно ответил на вопрос про обход единой точки: ДА, wrapper вызывается напрямую и выдаёт доверенный PASS в обход owner-authentication координатора.
• N-02: wrapper_lib.py:1198-1215 — pre-NPR rollback approval проверяет только approved=true, имя команды и hash файла, но не owner attestation; без ключа verify-pre-npr-rollback-approval дал rc=0 и опубликовал PASS.
• N-03: verify-receipt не проверяет attestation receipt и принимает внешний путь.
ДИАГНОЗ: круги 3, 4, 5 и 6 находят ОДНО И ТО ЖЕ — доказательство принимается по ФОРМЕ (правильные поля, правильный hash), а не по ПОДЛИННОСТИ (криптографически проверенная подпись владельца + привязка к прогону/фазе/последовательности). Проблема не в отдельных wrapper-ах: проверка подписи не является обязательным необходимым шагом на пути к PASS.
Круг 7 запущен на опусе: WEB409G — сделать Ed25519-проверку против ПИНОВАННОГО ключа владельца единственным способом получить PASS на ЛЮБОМ пути (включая прямой вызов wrapper_lib и bin/*), проверять не только подпись но и привязку, закрыть N-03 с realpath-границами, убрать любые режимы без ключа. Доказательства: дословный повтор трёх проб приёмщика + пять своих обходов (подпись от другого прогона, объект изменён на байт, пустая подпись, посторонний ключ, отсутствие trust anchor) + негативный тест с отключением проверки.
[29.08 КРУГ 9: 970e6fa478ae4a5adbce0935c2d24659be8ffbac, приёмка ACC409J заряжена]
Блокер круга 8 (ACC409I NO-GO): default-sidecar давали PASS мимо печати — sidecars в GATE_INPUTS были optional (ops/web409/wrapper_lib.py:96) и читались до publish() (:1316-1322), а их пути вообще не попадали в подписанные входы, поэтому InputSeal о них не знал.
Круг 9 отвечает не заплаткой, а разбором устройства: для каждого непустого path-input gate_inputs() делает ровно ОДНО чтение до подписи владельца, байты кладутся в InputSeal, дальнейшие читатели получают запечатанную копию; перед publish()/announce() дисковый backstop даёт GATE_INPUT_SUBSTITUTED и FAIL при подмене; после закрытия печати путь вне подписанных входов даёт UNSEALED_GATE_INPUT; новый файл внутри подписанного дерева не становится читаемым. Приведена полная таблица: 19 gate keys, 44 entries.
Оговорка автора (слабое место, отдано приёмке): optional-входы могут отсутствовать без ошибки там, где код осознанно выбирает optional-режим.
Приёмке ACC409J поставлена задача СЛОМАТЬ гарантию: гонка между чтением и publish, симлинк и подмена его цели, каталог вместо файла, пустой файл против отсутствующего, пути с .. и другим регистром, жёсткие ссылки, заход через optional-вход, подмена входа «не в signed inputs».
[29.08 ACC409J_VERDICT=NO-GO — воспроизводимая ГОНКА, круг 10 заряжен (очередь 453)]
Приёмка подменила вход МЕЖДУ возвратом assert_inputs_unaltered() внутри publish() и атомарной записью PASS. Результат: assert_calls=2, exit_code=80, stderr=GATE_INPUT_SUBSTITUTED; EVIDENCE_PUBLICATION_FAILED, но evidence_result=PASS, scope_after=https://attacker.example.net, authority_claimed=true. Гейт успевает записать настоящий PASS; вторая проверка в main() замену видит, но publish_failure() не может заменить уже существующий evidence (OUTPUT_EXISTS) — отказ НЕ оставляет FAIL, PASS остаётся на диске, полномочие потрачено.
Вторая проба ещё прямее: краткая подмена с восстановлением сразу после записи PASS — events=replaced-after-publish-backstop,restored-after-PASS-write, exit_code=0, GATE_INPUT_SUBSTITUTED не возник вовсе, evidence_result=PASS. Гейт не заметил ничего.
Точки кода: ops/web409/wrapper_lib.py:607 publish(), :888 assert_inputs_unaltered(), :2360 main().
Заявка круга 9 «подмена перед publish даёт GATE_INPUT_SUBSTITUTED и FAIL» опровергнута: sealed copy защищает вычисление на подписанных байтах, но атомарного backstop не даёт.
КРУГ 10: устранить окно, а не сузить (публикация во временный файл + rename только после финальной проверки; либо хеш входов внутри самого evidence; либо удержание дескрипторов с проверкой st_dev/st_ino). Плюс починить OUTPUT_EXISTS — отказ обязан оставлять FAIL. Обе пробы приёмки — обязательными тестами.
[29.08 КРУГ 10: b82381bbcef217ff2c3d2e5c5c834629cf3350dd, приёмка ACC409K заряжена (очередь 463)]
Решение окна: publish() строит evidence во ВРЕМЕННОМ файле; финальная проверка входов идёт callback-ом непосредственно перед эксклюзивным атомарным link в тот же каталог — пока проверка идёт, PASS-пути на диске НЕТ, а параллельный writer не может заменить уже существующий output.
OUTPUT_EXISTS починен: обычная PASS-публикация — create-if-absent commit; если backstop обнаружил мутацию после видимости PASS, publish_failure() заменяет вывод атомарным FAIL с refuse_existing=False, поэтому OUTPUT_EXISTS больше не сохраняет PASS. Для гейтов с каталогом evidence запоминается фактический args._pass_output — заменяется именно опубликованный файл, а не случайный result.json.
Добавлен harness ops/web409/test_web409k.py, инжектирующий замену В САМ commit syscall (обе пробы приёмки круга 9).
⚠️ НОВЫЙ РИСК, отдан приёмке: refuse_existing=False означает, что опубликованный результат теперь МОЖНО перезаписать — приёмка обязана проверить, кто имеет право звать эту замену и нельзя ли подменить валидный PASS на FAIL или обратно.
[29.08 ACC409K_VERDICT=NO-GO — круг 10 закрыл вход, но не ВЫХОД; круг 11 заряжен (очередь 467)]
ПРИНЯТО: assert_inputs_unaltered() действительно стоит непосредственно перед os.link() (wrapper_lib.py:496-547), подмена ВХОДА в этом окне закрыта, OUTPUT_EXISTS больше не сохраняет PASS.
БЛОКЕР K-01: приёмка поставила hook в сам os.link(src,dst) — прочитала ВРЕМЕННЫЙ evidence-файл, заменила в нём поле решения details.pi_sip_scope на INCLUDED и передала управление настоящему os.link. Входной scope.json НЕ менялся. Результат: TEMP_FORGE_RC=0, TEMP_FORGE_RESULT=PASS, DETAIL_SCOPE=INCLUDED, TEMP_FORGE_READER=PASS. То есть защищены входы, а не выход: InputSeal и verify_evidence_input_bindings() на стороне читателя проверяют вход и не смотрят на закоммиченный payload.
БЛОКЕР K-02: claim полномочия создаётся уже ПОСЛЕ видимости PASS (wrapper_lib.py:798-805) — порядок обратный безопасному.
КРУГ 11: сделать публикуемый payload неподделываемым (хеш итогового evidence в подписанной части + сверка читателем, либо подпись целиком, либо публикация из того же дескриптора с fsync и link того же inode); расширить проверку читателя на сам результат; полномочие фиксировать ДО видимости PASS. Плюс затребован перечень ВСЕХ байтов, участвующих в решении, с отметкой «защищён/не защищён».
[29.08 ⭐ ПРИНЯТО — ACC409L_VERDICT=GO, коммит c6b8bde3c83ad66c0d7b600497132c00c13c5d69, ОДИННАДЦАТЫЙ круг]
Гейт тумблера наконец герметичен. Устройство: hash строится из неизменяемого канонического payload в памяти и независимо сверяется с байтами удерживаемого inode — подмена staging не превращается в PASS. after_commit помечает вывод владельцем текущей транзакции: проигравшая конкурентная публикация больше не может затереть чужой уже опубликованный PASS своим FAIL.
АТАКА, СЛОМАВШАЯ КРУГ 10, ОТБИТА: hook в os.link(src,dst) с подменой только details.pi_sip_scope (scope.json не трогали) даёт rc=79, evidence FAIL, EVIDENCE_PAYLOAD_CHANGED, claim удалён, scope.json побайтово прежний.
Замена источника хеша вместо файла — тоже не проходит.
ОТРИЦАТЕЛЬНЫЙ КОНТРОЛЬ harness-а: тот же тест K-01 с ожиданиями круга L против старого b82381bb даёт FAIL (старый producer оставил temporary-evidence-forged и не создал claim-before-link); старый checkout воспроизводит исторический ложный PASS DETAIL_SCOPE=INCLUDED, а круг L выдаёт FAIL/EVIDENCE_PAYLOAD_CHANGED. То есть проверка краснеет на старом состоянии — она настоящая.
K-02 (полномочие после видимости PASS) закрыт: порядок изменён.
Путь линии: 11 кругов, каждый заворачивала приёмка, находя новую щель семейства «между формированием и фиксацией» — сначала подмена входа, потом default-sidecar, потом гонка проверка/запись, потом подмена самого публикуемого файла. Кандидат в линию после l69. Дальше по эпику: репетиция WEB-412 в окне без голоса, затем переключение WEB-413 по решению владельца.
[BOARDTRIAGE] Приёмщик: migration/release coordinator. ACC409L GO, commit c6b8bde3; gate готов как prerequisite WEB-412, но сам cutover не запускался.
Лента
2026-09-02T00:18:47.180Z · Fable[02.09 00:25Z Фабл, закрытие хвостов эпиков] ACC409L GO (c6b8bde3): коммит НЕ найден в A2/nc-build (история l85) — либо это документ-линия (runbook) на A1/M4, либо потерян с деревьями 01.09. Не закрываю вслепую; проверю наличие артефакта runbook на A1 (waves/WEB409*-REPORT.md) при следующем проходе.
2026-09-02T00:20:37.032Z · Fable[02.09 00:30Z Фабл] Коммит c6b8bde3 НАЙДЕН в A1 mirrors/rescued/wt-web409l-c6b8bde3.bundle (7 коммитов, 32 файла, только документы/runbook +9319 строк). Слит в l86-candidate на A2 (fast-forward, без конфликтов) — сядет с l86; тогда закрываю.
2026-09-02T02:19:32.840Z · Fable[02.09 02:25Z Фабл] 🛬 ПОСАЖЕН В l86 (A1, commit 3d23ac475f30a19719c42aefea4b656a65eceffc, артефакт me2-standalone-linux-arm64-3d23ac475-20260902T021427Z sha 0885c670…, аттестация owner-ключом fable-a2-l86, сухой прогон :3011 paidReady TRUE, флип nc-a1 + nc-a1-indexing, paid 200 публично). Журнал посадки — WEB-320. runbook cutover/rollback (c6b8bde3, документы). Закрываю.
Воркер
не привязан — привязать:
curl -X POST https://bugs.wool2.online/api/web/assign -H 'content-type: application/json' \
-d '{"issueId":"WEB-409","session":"<имя tmux-сессии>","host":"m4"}'
Обновлён
2026-09-02T02:19:32.968Z