WEB-414 · Дефект · — · web
P1 Совет ролей мёртв на проде: ростер моделей не совпадает с MODEL_ALLOWLIST (anthropic/-префикс + gemini-3-flash-preview)
Закрыт
P1 · важно
ведёт: —
Суть
Найдено rolesprodqa 27.08 (первая grounded-QA ролей на живом l59): все попытки council-ответов падают, billing_guard_model_not_allowed: council запрашивает 'anthropic/claude-haiku-4.5' (allowlist содержит 'claude-haiku-4.5' БЕЗ префикса провайдера) и 'gemini-3-flash-preview' (в allowlist только gemini-2.5-flash). Итог: 'Council could not complete this run', пользователь получает scope-only ответы. Радиус: любой council-путь на проде; одиночные модели из allowlist не задеты. Транзиент 'AI spending accounting is being repaired' в том же прогоне — переоткрытая эпоха spend-reconcile, сама закрылась (paidReady=true). Allowlist НЕ расширять без owner (монетарное). Правильный фикс — в коде: (а) канонизация имени модели (срезать/нормализовать провайдер-префикс) перед сверкой с allowlist; (б) ростер совета фильтровать по allowlist с graceful degradation (совет из доступных моделей вместо отказа); (в) телеметрия деградации.
Как воспроизвести
Прод l59: QA-учётка, notebook с источниками, вопрос с выбором ролей/совета -> Council could not complete; journal: billing_guard_model_not_allowed anthropic/claude-haiku-4.5 + gemini-3-flash-preview (07:44Z 27.08).
Чем закрывается (приёмка)
На стенде с прод-allowlist: council завершает ответ на разрешённых моделях; запрос неразрешённой модели деградирует с телеметрией, не валит весь ответ; негатив: пустой пересечённый ростер -> честный отказ с внятным сообщением; targeted tests. Ship-dark, прод не мутировать.
Доказательства
[2026-08-27 ~09:05Z] Создан по находке rolesprodqa (ROLESPRODQA-REPORT.md на A1). Волна web414council диспатчнута.
[2026-08-27 08:43Z web414 IMPLEMENTED] Ветка web414council, коммит fa06ef0f (база 2c6c2276, 1 ahead, дерево чистое). Корень подтверждён: evaluateSpendGuard сравнивал буквально -> 'anthropic/claude-haiku-4.5' != 'claude-haiku-4.5'; releaseModelPolicy и transcriptionModelPolicy повторяли ту же ошибку; getKnights() отдавал override без пересечения с allowlist; отказ всех knights -> 'All knights failed' -> generic 'Council could not complete this run'. ФИКС: dependency-free helper src/lib/ai/modelAllowlist.ts (trim+lowercase, последовательное снятие openrouter:/anthropic//openai//google/ включая вложенный openrouter:google/..., канонизация ОБЕИХ сторон, неизвестный namespace НЕ разрешается по bare suffix) + admission ростера совета. MODEL_ALLOWLIST не менялся (монетарное — слово owner). acc414 (Luna xhigh, ≠ автор) диспатчнута: проверка на ОСЛАБЛЕНИЕ гварда (omoglyph/suffix-коллизии/неизвестный namespace), сверка 5 путей решения на одинаковых входах, деградация без молчания (пр.10).
[2026-08-27 09:28Z web414b — hardening после ACC414 NO-GO] Коммит e105e7b6: единый строгий resolveModelIdentity (только primitive string; только точные catalog/provider references; fail-closed на unknown/foreign namespace, омоглифы, suffix-коллизии, множественные слэши, пустые и non-string); ВСЕ пять путей (spendGuard, releaseModelPolicy, transcriptionModelPolicy, pricing, estimatorRegistry) переведены на общую identity-модель, prefix-lookup и снятие произвольного slash-компонента УДАЛЕНЫ; при degraded/empty ростере — человекочитаемые логи + установленный [council][slot_beacon] с event/count/моделью/причиной (закрывает пр.10). acc414b (новый приёмщик, не автор и не прошлый verifier) диспатчнута: property-фаззинг на невозможность превратить deny в allow, unicode-омоглифы/NFKC/zero-width/RTL, prototype-pollution входы, сверка 5 путей, регрессия исходного прод-сценария (anthropic/claude-haiku-4.5 ДОЛЖЕН проходить), тест обязан ловить отсутствие маячка.
[2026-08-27 09:49Z acc414b NO-GO — 2-й независимый круг, фаззинг 14016 входов] Базовые сценарии закрыты (омоглифы, NFKC/NFKD fullwidth, U+200B/200C/200D/2060, RTL, malformed, длинные — все denied; провайдер-квалифицированный anthropic/claude-haiku-4.5 ПРОХОДИТ при bare allowlist = исходная задача WEB-414 решена). НО 4 блокера: (1) U+FEFF ОБХОД: normalizedModelString() использует JS .trim(), который СЪЕДАЕТ U+FEFF -> '\uFEFFclaude-haiku-4.5' и 'claude-haiku-4.5\uFEFF' дают allow по ВСЕМ пяти путям (deny->allow, ровно то ослабление, которое искали); (2) PROTOTYPE-POLLUTION: pricing.ts:28-47 индексирует MODEL_PRICING_PER_1M_TOKENS[key] без own-property -> resolveModelPricing('constructor') возвращает функцию, 'proto' — объект; при MODEL_ALLOWLIST=constructor evaluateSpendGuard вернул allowed:true, calculateCost дал NaN -> нарушение fail-closed и рассинхрон с estimatorRegistry; (3) РАССИНХРОН 5 ПУТЕЙ: для известной, но НЕ allowlisted модели (gemini-3-flash-preview) spend/release/transcription=false, а pricing/estimator=true — pricing/estimator не принимают allowlist-контекст; (4) CHAIR МИМО ФИЛЬТРА: filterAvailableKnights() фильтрует только knight-слоты, resolveChair() (orchestrator.ts:1208-1234) идёт отдельно и без allowlist -> запрещённый chair доходит до ПЛАТНОГО вызова и валит совет VendorCallRejectedError вместо деградации. Прошло: targeted+adversarial 14/14, прод-сценарий (gemini отбракован, OpenAI knight ответил, Google adapter не вызван). web414c диспатчнута: строгий allow-charset вместо trim (любые невидимые/форматирующие = deny), Object.hasOwn/null-prototype во ВСЕХ lookup-таблицах модели + аудит репо на тот же паттерн, единая матрица 5 путей, chair через тот же admission-фильтр с маячком; приёмочный фаззер acc414b обязан дать 0 нарушений.
[2026-08-27 10:28Z web414c PASS — все 4 находки ACC414B закрыты] (1) modelAllowlist.ts:123-131 — ASCII-only charset [A-Za-z0-9.:/-] ДО нормализации вместо Unicode-aware trim(); BOM/zero-width/RTL/C0-C1/NBSP/lookalikes отклоняются; невалидная единственная запись MODEL_ALLOWLIST не превращает policy в allowlist-off. (2) pricing.ts — own-property lookup + проверка finite-структуры цены; введён отдельный resolveAllowedModelPricing для paid admission, обычный resolveModelPricing документирован как price-book (НЕ авторизация); тем же guard-ом закрыты estimatorRegistry (token/embedding/non-token/family), models.ts, OpenAI/Council aliases, DOC NER/digest, transcription formats, speech registry, podcast TTS, availability, UNITECON. (3) estimatorRegistry учитывает MODEL_ALLOWLIST, model_not_allowed до estimate; parity-таблица покрывает gemini-3-flash-preview/constructor/__proto__/BOM. (4) orchestrator — chair через тот же admission, при отказе синтез из первого разрешённого knight + council_roster_degraded/beacon, rejected provider не доходит до vendor dispatch; regression-тест добавлен. acc414c (финальная, новый приёмщик) диспатчнута — с упором на ОБРАТНУЮ ловушку: весь каталог моделей прогнать через identity (ни одна легитимная не должна стать deny) + грепнуть, что price-book больше нигде не используется как авторизация.
[2026-08-27 11:05Z acc414c NO-GO -> 11:20Z web414d ЗЕЛЁНЫЙ] acc414c поймала ОБРАТНУЮ ЛОВУШКУ (ровно то, что я просил проверить): (1) новый ASCII-charset отсекал ЛЕГИТИМНЫЕ service SKU eleven_* (есть в SERVICE_MODEL_CATALOG, price registry, manifest contract) — фикс сломал бы озвучку; (2) estimateMax вызывал String(input.model) ДО type-check => объект с побочным toString бросал ИСКЛЮЧЕНИЕ вместо честного DENY; (3) reserved-имя 'constructor' допускалось 3 из 5 путей, если само помещено в MODEL_ALLOWLIST. web414d (commit 2bf7f860) закрыл всё: charset покрывает реальный алфавит каталога, строгая проверка типа до любых строковых операций, reserved-имена deny ВСЕГДА независимо от allowlist. Цифры автора: 76/76 своих, ACC414C-сюита 8/8, фаззер acc414b 14016 входов / 0 нарушений, catalog parity 0 failures, tsc PASS. Добавлен постоянный тест-страховка modelAllowlistWeb414D.test.ts: прогоняет ВЕСЬ каталог моделей и падает, если хоть одна существующая модель стала deny (защита от повторения ловушки). acc414d (4-й независимый приёмщик) диспатчнута с требованием получить СВОИ цифры, а не верить отчёту; при GO — сборка и посадка.
[2026-08-27 11:19Z acc414d GO — ФИНАЛЬНАЯ ПРИЁМКА ПРОЙДЕНА] Четвёртый независимый приёмщик (не автор, не acc414/b/c) подтвердил СВОИМИ прогонами: обязательные сюиты, независимый catalog-parity, adversarial-входы, Council degradation/beacon, scoped typecheck — всё зелёное. Коммит 2bf7f860 принят. Путь тикета: 4 круга приёмки, каждый находил РЕАЛЬНОЕ (буквальное сравнение -> namespace; ослабление гварда; BOM-обход + prototype-pollution в pricing; обратная ловушка с eleven_* + toString-исключение + reserved-имена). СЛЕДУЮЩИЙ ШАГ: сборка l60roles запущена на M4 (tmux gui) из ветки web414council поверх текущего прод-коммита 2c6c2276, env как l59gd (Gate D ON, E/F тёмные), с байт-гейтами. После сборки — подпись по доверенности, посадка, затем ПРОГОН ОБЕИХ БАТАРЕЙ РОЛЕЙ на живом проде (44 способности + 30 безопасности) и честный счёт owner-у.
[2026-08-27 ~16:20Z l61 НА ПРОДЕ — фикс совета ролей ПОСАЖЕН] Цепочка: web414 (4 круга приёмки) -> web414e (регрессия транскрипции) -> web414f GO (узкий фикс compile-ошибки, коммит 7d24c3a8). Сборка l61roles на M4: байт-гейты PASS (GATED-WRITER freeze({[n0]:"1"}) в chunks/81787, wiring как в l59gd, migrations 127/8f775b3b); артефакт 09219760, canon print, boot dry-run. Подпись координатором по доверенности: pin 0b83e0d3 OK, verify positive + 2 негатива PASS. Доставка laptop->Pi sha сквозной. Run-dir l61-7d24c3a8: ВСЕ sha взяты ИЗ ПОДПИСАННОЙ АТТЕСТАЦИИ автоматически (price/estimator 020535da, manifest, dependency lock, migration set) — урок прошлой посадки учтён; MODEL_ALLOWLIST содержит gpt-realtime-mini (owner: «всё используемое должно быть разрешено»); старых sha 0; wrapper self-check OK. ПЕРЕКЛЮЧЕНО: active, health 200 локально+Host, paidReady=true enforce_ready, 0 ошибок за 3 мин, транскрипция без unknown_model_fail_closed, телефония жива (SIP/боты/TG-звонки), бэкап-поток активен. Откат: drop-in .bak-l59gd-*. Owner попрошен прислать голосовое для живого подтверждения (в прошлый раз именно здесь всплыла регрессия).
Воркер
не привязан — привязать:
curl -X POST https://bugs.wool2.online/api/web/assign -H 'content-type: application/json' \
-d '{"issueId":"WEB-414","session":"<имя tmux-сессии>","host":"m4"}'
Обновлён
2026-08-27T16:20:27.039Z