WEB-321 · Дефект · — · web
ПОСАДКА 32 откачена: pi-land.sh не несёт spend-активацию → paidReady=false → rollback; нужен аттестованный cutover
Закрыт
P0 · горит
ведёт: fable-coordinator
Суть
# ПОСАДКА 32 ОТКАЧЕНА: pi-land.sh не несёт слой spend-активации (paidReady сломался)
## Что произошло (23.08 ~09:15 UTC)
- Артефакт ca69400c прошёл независимую приёмку R2 GO (3 фикса + размер 170.7МБ + миграции 120 идентичны). Env boot-контракта проверен (APP_URL/AUTH_SECRET/DATABASE_URL/NEXTAUTH_URL в файле и живом процессе).
- Cutover через ~/pi-land.sh ca69400c: LANDED_OK, health 200, proc marker v4-ca69400c. НО pi-land.sh репойнтит только WorkingDirectory+ExecStart(→plain server.js) и НЕ переносит слой spend-активации.
- Результат: paidReady=**False**, posture=enforce_blocked. Failures: trusted activation public key не запинен, SPEND_EGRESS_PROXY_CAPABILITY_SECRET отсутствует, 6 call-site манифест-строк новых моделей (gpt-4o-mini-tts, gpt-5.5-nano, claude-haiku-4.5, gemini-2.5-flash/lite) не в сгенерированном манифесте нового source.
- NEW conf потерял enforce-start wrapper и нёс СТАРЫЕ пины (артефакт a3c1c9d6/source 26116cff от посадки 31).
## Откат (немедленный)
- Восстановлен /tmp/dropin-backup-20260823T091514Z.conf → daemon-reload+restart → paidReady=True, enforce_ready, failures=0, serving v4-26116cff. Простой платного ~2-3 мин; сайт (health 200) не падал.
## КОРЕНЬ
pi-land.sh = ПРОСТОЙ лендинг-скрипт, предшествует spend-enforcement (WEB-308). Платно-enforced релиз требует ПОЛНОГО аттестованного cutover как посадка 31: enforce-start wrapper + spend-activation.json (bound artifact SHA + source commit) + trusted public key pin + SPEND_EGRESS_PROXY_CAPABILITY_SECRET + РЕГЕНЕРАЦИЯ call-site манифеста из НОВОГО source. Шаблон: /home/pi/note-clone/shared/run/l31-26116cff-precutover/.
- Подпись: nc-team-release (совпадает с прод-ключом, routine-деплой, БЕЗ owner-подписи — по слову owner 22.08).
## СЛЕДУЮЩИЙ ШАГ
Аттестованный cutover 32: (1) регенерировать call-site манифест из source ca69400c, (2) собрать spend-activation attestation для artifact-SHA нового tar + source ca69400c, (3) enforce-start-ca69400c.sh + spend-runtime env с новыми пинами, (4) dropin с wrapper, (5) dry-run enforce ДО переключения, (6) flip + paidReady=true проверка + stamp-canon.
## Эволюция
Артефакт: WEB-315/316/317 (build32). Приёмка: WEB-314. Активация: WEB-308 (посадка 31 через enforce-start). Память: landing-needs-boot-contract-dry-run, deployed-not-enforcing-gap.
## 23.08 09:35 UTC — recon аттестации посадки 31 (шаблон для 32)
Пакет l31 на Pi /home/pi/note-clone/shared/run/l31-26116cff-precutover/:
- enforce-start-26116cff.sh (wrapper): пинит nc-team-release-public.pem (SHA cf86d782…61), export SPEND_ACTIVATION_TRUSTED_PUBLIC_KEY_PEM, exec inner. Есть --self-check.
- enforce-start-26116cff.inner.sh: ставит spend-runtime env + старт server.
- spend-activation-26116cff.json (активация, bound artifact+source), spend-runtime-26116cff.env (пины SPEND_RUNTIME_ARTIFACT_SHA256, RELEASE_SOURCE_COMMIT, egress secret), standalone-artifact.conf (dropin).
- Артефакт 32: source ca69400c17197d2a28f2204dd8f92b6f2b0fe155 (подтверждён .artifact-manifest.json), tar SHA 5a6dfb6e…, migration 120/c821f49d.
ДЛЯ CUTOVER 32 нужно (следующий заход, по шагам, dry-run ДО flip):
1. НАЙТИ генератор spend-activation + call-site манифеста (failures показали 6 модельных строк не в манифесте — манифест регенерить из source ca69400c). Вероятно в build-tree/WEB-308 инструментах.
2. Собрать spend-activation-ca69400c.json + spend-runtime-ca69400c.env (новые пины: artifact-SHA нового tar, RELEASE_SOURCE_COMMIT=ca69400c) + enforce-start-ca69400c.{sh,inner.sh}.
3. Подпись nc-team-release (routine, БЕЗ owner — приватный ключ найти; публичный на Pi).
4. dropin ca69400c с wrapper ExecStart → enforce-start-ca69400c.sh.
5. enforce dry-run (--self-check + boot-contract) ДО переключения; flip; paidReady=true проверка; stamp-canon.
Артефакт rel-ca69400c.tar.gz УЖЕ на Pi /tmp (годен, R2 GO).
## 23.08 09:50 UTC — механика cutover найдена; БЛОКЕР: приватный ключ nc-team-release
Генераторы в source (wt-build32/scripts/):
- `make-activation-attestation.mjs` — usage: `--artifact-sha256 <64hex> --source-commit <40hex> --private-key <ed25519.pem> --out <sidecar.json>`. Подпись ed25519, trusted public key ВЫВОДИТСЯ из приватного.
- `monet-w34-generate-activation-evidence.mjs` — регенерация call-site манифеста (из source ca69400c закроет 6 отсутствующих модельных строк).
l31 inner (шаблон): экспортит SPEND_ACTIVATION_TRUSTED_PUBLIC_KEY_PEM (nc-team-release-public.pem, пин cf86d782…), SPEND_EGRESS_PROXY_CAPABILITY_SECRET (/home/pi/note-clone/shared/run/flip-cap.secret — переиспользуемый), CRON_SECRET (flip-cron.secret).
### ⛔ БЛОКЕР: приватный ключ nc-team-release НЕ локализован
- Есть только: подписанные им файлы (M1 ~/l31-r4-m1/spend-activation-l31-r4-nc-team-release.json) + публичный pem на Pi. Приватного нет на Интеле/M1/A1/Pi shared.
- Landing 31 был подписан агентом 22.08 (owner: «сам подписываю routine-деплой») — значит ключ был доступен в той сессии; сейчас не найден.
- НЕ использую owner-ключ (~/nc-owner-key/owner-private.pem) — это отдельный ключ для платной батареи ролей, и это запрещено.
- ВОПРОС OWNER: где приватный ключ nc-team-release для подписи routine-деплоя ca69400c, ИЛИ owner подписывает сам (дам точную команду make-activation-attestation.mjs с SHA артефакта и source ca69400c)?
### Готово к подписи (как только ключ будет):
artifact tar SHA 5a6dfb6e…4779 (нужен ВНУТРЕННИЙ artifact-sha из манифеста — уточнить), source ca69400c17197d2a28f2204dd8f92b6f2b0fe155. Далее: манифест→активация→enforce-start-ca69400c→dropin→dry-run→flip→paidReady=true→stamp-canon.
## 23.08 10:00 UTC — НАЙДЕНО: деплойный ключ == owner-ключ (нужно явное подтверждение)
- Сверил ВСЕ приватные ключи (Downloads + nc-owner-key) против пина enforce-start nc-team-release `cf86d782…f88861` (node, т.к. LibreSSL не читает ed25519):
- `/Users/annakorin/nc-owner-key/owner-private.pem` → **MATCH cf86d782** (это деплойный ключ И owner-ключ одновременно).
- `~/Downloads/v3-checkout/owner-key-kit-output-3f155f3/owner-private-key.local-only` → da2e5642 (ДРУГОЙ ключ, monet-w35, НЕ деплойный).
- AuthKey_*.p8 (Apple), прочие — не подходят.
- Вывод: подпись активации ca69400c требует ИМЕННО owner-private.pem. Owner показал на Downloads-ключ (другой) — вероятно не знает о совпадении.
- ⛔ НЕ подписываю owner-ключом без явного согласия НА ЭТОТ ключ (жёсткий запрет). Спрошено owner (12926): подтвердить подпись routine-деплоя owner-private.pem ЛИБО подписать самому.
- Как только подтверждение: (1) внутренний artifact-sha из манифеста ca69400c, (2) monet-w34-generate-activation-evidence из source ca69400c, (3) make-activation-attestation --artifact-sha256 <> --source-commit ca69400c… --private-key owner-private.pem --out, (4) enforce-start-ca69400c, (5) dry-run, (6) flip.
## 23.08 10:20 UTC — ROOT CAUSE: сборка 32 регрессировала платный манифест (крипта-аттестация РАБОТАЕТ)
Owner подтвердил подпись → прошёл ВЕСЬ аттестованный cutover, платный гейт дал точный диагноз.
- ✅ Крипто-подпись активации работает end-to-end: make-activation-attestation owner-ключом, sidecar artifactSha256=5a6dfb6e/source=ca69400c, публичный DER=cf86d782 (совпал с enforce-start пином), self-check START_WRAPPER_CHECK=OK. Слой аттестации собран в /home/pi/note-clone/shared/run/l32-ca69400c-precutover/ (инертен, для переиспользования).
- ⛔ РЕАЛЬНЫЙ ДЕФЕКТ СБОРКИ: generatedActivationManifest.ts у ca69400c содержит 8/66 «denied»-заглушек (openrouter|audio.speech.create|tts-release1-denied, google|...gemini-tts-release1-denied) ВМЕСТО боевых (openai|audio.speech.create|gpt-4o-mini-tts). Боевого TTS в манифесте НЕТ.
- Платный гейт правильно отказал: activation_attestation:runtime_artifact_sha256_mismatch + 6× config:MANIFEST:...missing (боевые платные строки не объявлены в running-коде). 26116cff их ИМЕЕТ (иначе не paidReady=true) → ca69400c РЕГРЕССИРОВАЛ.
- ГИПОТЕЗА корня: build32 интеграция (web315/316/317 на 26116cff) собралась из состояния, где TTS «release1-denied» — либо база интеграции, либо web316(voice) поменял аудио-манифест, либо шаг регенерации манифеста. Структурная приёмка R2 это пропустила (проверяла байты/размер/миграции, НЕ содержание платного манифеста).
- ⚠️ ДВА ПРОВАЛА FLIP, оба с АВТО-ОТКАТОМ (self-check gated 1-й, paidReady-guard откатил 2-й) — прод НЕ падал, 26116cff/paidReady=true всё время.
### СЛЕДУЮЩИЙ ШАГ: НЕ cutover, а ПЕРЕСБОРКА
Landing 32 требует rebuild с КОРРЕКТНЫМ платным манифестом (боевые TTS/audio как в 26116cff). Приёмка байтов ДОЛЖНА теперь включать проверку генерированного манифеста на denied-заглушки. Артефакт ca69400c НЕ годен для платного enforce как есть.
## 23.08 10:20 UTC — ДИАГНОЗ (WEB321_MANIFEST_DIAGNOSED): устаревший generated-снимок из WEB-316
- Корень: WEB-316 (voice, 67d1e1f9) принёс generatedActivationManifest.ts + paidEntrypointRegistry.json, СГЕНЕРИРОВАННЫЕ БЕЗ прод-env (config-dependent). build32 после мержа НЕ регенерировал → упаковал снимок. Классификация B+C, НЕ регрессия кода voice (не A).
- Serving 26116cff: 67 строк манифеста / 4 audio.speech / 25 ALLOW chat / 8 release1-denied. ca69400c до регенерации: 66/2/21/8 (пропали 6 боевых targets, вкл. openai TTS). ca69400c ПОСЛЕ регенерации с прод-env: 72/4/25/8 — боевые TTS восстановлены.
- ФИКС: пересобрать с регенерацией манифеста под прод-env (MODEL_ALLOWLIST+SPEND_DEPLOYMENT_MANIFEST). + BUILD GATE: production build обязан иметь clean generator (git diff --exit-code) и оба openai TTS ключа в манифесте.
- Запущена пересборка l32v2 (A1 Terra): регенерация + build + package + новый гейт приёмки манифеста. Аттестация-слой (l32-ca69400c-precutover) переиспользуется под новый artifact-SHA.
## 2026-08-23 10:51 UTC — ПОСАДКА 32 СЕЛА (аттестованный cutover, rel-4482925)
Полная эволюция трёх неудач → успех:
1. Попытка №1 (pi-land.sh): не несёт слой spend-активации → paidReady=false → авто-откат (см. выше).
2. Попытка №2 (attested, ca69400c): runtime_artifact_sha256_mismatch + 6 manifest-missing строк. Корень: билд нёс generatedActivationManifest.ts с 8 release1-denied заглушками без боевого gpt-4o-mini-tts (WEB321_MANIFEST_DIAGNOSED). → пересборка.
3. Пересборка l32v2 на A1 (отчёт /home/ubuntu/waves/L32V2-REBUILD-REPORT.md, маркер L32V2_ARTIFACT_READY_4482925): source 44829258a9398ebef6aedfeb9a959b7be4fbd4d3, манифест 72 строки / 4 audio.speech / TTS в ALLOW / 0 заглушек в ALLOW, manifestSha aed050b70c2d8b7ea5cb8a1fd1e904f555e637ae1b0c317f97d36c845c7206b3. Артефакт rel-4482925.tar.gz SHA 796d340b4554d015cfa979b2ebb47f2a416df413807055444b9210d4b549f100, 170.7МБ (гейт WEB-319 прошёл).
4. Активация подписана на Интеле (scratchpad/src-4482925 + make-activation-attestation.mjs), пин ключа cf86d782… сверен. Sidecar spend-activation-4482925.json.
5. Попытка №3: runtime_artifact_sha256_mismatch. КОРЕНЬ (важный урок): скопированный l31 env-файл нёс старый SPEND_RUNTIME_ARTIFACT_SHA256, а в systemd EnvironmentFile ПЕРЕБИВАЕТ Environment= из dropin — sed менял только 3 пина из 4. Это же убило попытку №2.
6. Попытка №4: runtime dependencyLockSha256 mismatch — тот же класс, пин SPEND_DEPENDENCY_LOCK_SHA256 в env был l31-овский. Новый: 53d95591317d6d198738e85a56834a4afe533dcb2f75eb6963785ec72b959285 (= sha pnpm-lock.yaml сборочного дерева, сверено с A1).
7. Флип №3 (10:51Z): paidReady=True, маркер v4-4482925, health 200, публичный nb.wool2.online отдаёт v4-4482925, юнит active. Бэкап dropin: /tmp/dropin-backup-preL32V2-20260823T105102Z.conf. Откат = вернуть бэкап + restart.
Precutover: /home/pi/note-clone/shared/run/l32v2-4482925-precutover/. Релиз: /home/pi/note-clone/releases/arm64-4482925-20260823T104822Z.
УРОК в проверялку будущих cutover: негативный тест «все SPEND_*/RELEASE_* пины env-файла == значениям аттестации» ДО флипа.
Статус: ПОСАЖЕНО. Живая приёмка фич — в WEB-315/316/317.
## 2026-08-23 14:57 UTC — пин-гейт из урока этого тикета сработал: посадка 33 села с первой попытки (v4-00c402c), все 7 пинов сверены до флипа.
Замечен в сборке
ca69400c
Воркер
не привязан — привязать:
curl -X POST https://bugs.wool2.online/api/web/assign -H 'content-type: application/json' \
-d '{"issueId":"WEB-321","session":"<имя tmux-сессии>","host":"m4"}'
Обновлён
2026-08-23T11:01:47.512Z