fix(assistant): integrity, grounding & guard fixes from the PR #79 review - #223
fix(assistant): integrity, grounding & guard fixes from the PR #79 review#223nedda76 wants to merge 17 commits into
Conversation
ydimitrof
left a comment
There was a problem hiding this comment.
Преглед на PR: fix(assistant): integrity, grounding & guard fixes
Обобщение
PR-ът е фокусиран follow-up от прегледа на #79 и затяга няколко защитни слоя в асистента без разширяване на обхвата. Промените са атомарни, добре мотивирани в коментарите и всяка е придружена от смислени тестове (не тривиални „cheater" тестове).
Фаза 0 — Сигурност (ЗАДЪЛЖИТЕЛНА проверка): ✅ ЧИСТО
- Няма хардкоднати тайни, ключове, пароли или токени.
- Няма нови/променени URL адреси, нито промени в зависимостите.
- Няма злонамерени шаблони (backdoor, code injection, обфускация).
- Всички промени всъщност засилват защитата: разширен guard за SQL агрегати, capping на масивите от модела, whitelist за
align, floor за релевантност при RAG.
По измерения
Сигурност (agent-level): 1.0/1.0
sql-guard.ts: добавениgroup_concat/json_group_array/json_group_objectкъм блокираните функции — коректно затваря клас memory-amplification (една клетка от пълно сканиране предиcapRows). Регексът изисква\s*\(, така че колона с такова име не се блокира по грешка.report-schema.ts: явното изграждане на колоните вместо{ ...c }премахва passthrough на непознати свойства към рендера — добра defensive промяна.emit-report-schema.ts: whitelistisAlign(самоleft|right|undefined) предотвратява попадане на out-of-enum стойност в атрибут/style.
Тестове: 3.0/3.0
- Всяка нова пътека има целеви тест: capping на масиви,
alignwhitelist, floor при RAG (над/под прага и празен резултат), spelled трилион/билион, string-building агрегати, безусловни hard-traps под RAG. Тестовете са проектирани да разкриват дефекти, не да минават формално.
Качество на кода: 2.0/2.0
- Извеждането на
renderTraps()премахва дублиране междуdescribeSchemaиsystem-prompt(single source of truth) — точно според „NO CODE DUPLICATION". - Стилът е консистентен с останалата част на модула.
Производителност: 2.0/2.0
- Няма регресии; caps-овете (
MAX_BLOCKS/ITEMS/COLUMNS) са безопасни горни граници далеч над реален отчет.
Документация: 2.0/2.0
- Коментарите обясняват „защо" на всяка граница; актуализирани доки за таблиците
amendments/partiesи коригиран grain наdata_freshness(view→таблица).
Съответствие с CLAUDE.md
Без частични имплементации, без TODO/dead code, без смесени концерни, без ресурсни течове. Промените са атомарни и в рамките на обхвата.
Заключение
Висококачествен, добре тестван защитен PR. Нямам блокиращи забележки. Оставям два незадължителни инлайн коментара за проверка (grounding към home_totals.suspect и робъстност при липсваща score), които не блокират сливането.
Композитна оценка: 9.5/10 — препоръка: одобрение с малки уточнения.
ydimitrof
left a comment
There was a problem hiding this comment.
Преглед на PR: fix(assistant): integrity, grounding & guard fixes (follow-up на PR #79)
ВЕРДИКТ: COMMENT — одобрявам по същество; няма блокиращи проблеми. Не публикувам автоматично, изчаквам вашата проверка преди изпращане.
Фаза 0 — Скенер за сигурност (задължителен, изпълнен пръв)
- Твърдо кодирани тайни: няма (0 API ключа/пароли/токени). ✅
- Промени по URL адреси: няма нови/променени URL адреси. ✅
- Зловредни модели (backdoor / инжекция на код / обфускация): няма. ✅
- Промени по зависимости: няма нови пакети. ✅
- Заключение на Фаза 0: ЧИСТО → продължавам към същинския преглед.
Важно: този PR е серия от защитни (hardening) поправки — той затяга SQL guard-а, ограничава размерите на масивите от модела, въвежда праг за релевантност при RAG и разширява детектора на изписани числа. Т.е. промените намаляват риска, а не го увеличават.
Анализ по файлове
sql-guard.ts — Добавени са group_concat / json_group_array / json_group_object към черния списък. Правилно: това са агрегати, които колабират цял table scan в една огромна клетка, преди capRows да я измери (memory amplification, същият клас като printf). Регулярният израз е коректен (\b…\s*\(); коментарите се премахват преди проверката, така че group_concat/*x*/( няма да заобиколи гарда. Fail-closed — правилен избор. (Виж инлайн бележка за остатъчен вектор с рекурсивни CTE.)
emit-report-schema.ts — Въведени горни граници MAX_BLOCKS=100, MAX_ITEMS=50, MAX_COLUMNS=50 и whitelist валидатор isAlign (само undefined|left|right). Затваря DoS през неограничена дължина на масив и предотвратява out-of-enum стойност за align да достигне рендерер, който я интерполира в атрибут/стил (защита срещу attribute/HTML injection — OWASP A03). Тестовете покриват center, "><b> и препълнените масиви. ✅
report-schema.ts — Добавени трилион|билион|квадрилион към стема за изписани величини. Коректно затваря пропуска „3 трилиона лева“ (дефамационен вектор един порядък над „12 млрд.“). Преминаването към явно построяване на колоните (без spread { ...c }) е важно подобрение: спира пренасянето на непознати, подадени от модела свойства към рендерера, тъй като validateEmitShape не отхвърля непознати ключове. Много добра защита в дълбочина. ✅
rag.ts — Въведен праг за релевантност MIN_SCHEMA_SCORE=0.35 с (m.score ?? 0). Логиката е правилна: под прага се връщат по-малко или нула чънкове, а нула кара buildSystemPrompt да падне обратно към пълния статичен речник — безопасният изход. ?? 0 защитава срещу backend, който пропуска score (не се инжектира като „контекст“ без ранг). ✅
system-prompt.ts / describe-schema.ts — Твърдите MUST/NEVER капани (SUM само amount_eur, ocid ≠ УНП, …) вече се инжектират безусловно чрез споделения renderTraps(), дори при RAG. Това коригира реалния дефект, при който извличането е заменяло капаните и е оставяло RAG-хода с по-малко ограничения от no-RAG отстъплението (пропускът, довел до SUM(amount)). Споделеният рендерер премахва риска от дрейф между двата пътя. Актуализирани са и enum-ите (value_flag + value_low) и документацията на таблиците amendments/parties. ✅
OWASP съответствие
- A03 (Injection): SQL guard е fail-closed и SELECT-only; изписаните числа и подаваните от модела полета се санитизират/валидират. Няма конкатенация на непроверен вход в SQL в този diff.
- A04 (Insecure Design) / DoS: новите горни граници и блокирането на string-агрегатите адресират точно ресурсното изчерпване.
- Няма нови вектори за A01/A02/A05/A08.
Тестове и качество
Всяка промяна е придружена от смислен, целенасочен тест (align whitelist, лимити на масивите, RAG праг, трилион/билион, SQL агрегати, безусловни капани). Тестовете са проектирани да разкриват дефекти, не да минават тривиално. Именуването и стилът са консистентни с останалата кодова база. Няма частична имплементация, TODO-та, дублиран или мъртъв код.
Незадължителни препоръки (не блокират сливането)
- sql-guard.ts — Остатъчен вектор: рекурсивен CTE, който строи низ чрез
||(напр.WITH RECURSIVE r(s) AS (SELECT 'x' UNION ALL SELECT s||'x' FROM r LIMIT 1e6) SELECT s), може да материализира огромен низ, без нито една от блокираните функции. Извън обхвата на този PR (съществуващ пропуск), но си струва отделен follow-up. - report-schema.ts — Стемата покрива до
квадрилион, но не иквинтилион/секстилион. Отворен край с ниска вероятност; евентуално по-общ шаблон в бъдеще. - describe-schema.ts — Grain на
data_freshnessе сменен от „view“ на „таблица“. Моля потвърдете, че обектът реално е таблица, а не изглед — това е документация, която моделът чете при писане на SQL.
Обща оценка: висококачествен, добре тестван защитен PR. Препоръчвам одобрение след вашата ръчна проверка.
ydimitrof
left a comment
There was a problem hiding this comment.
Ревю на PR: fix(assistant): integrity, grounding & guard fixes from the PR #79 review
Здравей, Явор! Прегледах промените изключително подробно, с фокус върху сигурност и цялост на данните (SQL инжекции, обход на guard-а, XSS през рендер, изтичане на ресурси). Ето обобщението.
ВЕРДИКТ: COMMENT — не блокиращо. Одобрявам след потвърждение по единствената бележка по-долу (string_agg). Не съм публикувал коментари — това е чернова за твоя преглед.
Фаза 0 — Сканиране за критична сигурност (по дифа)
- Твърдо кодирани тайни (API ключове/пароли/токени): няма.
- Промени по URL/крайни точки: няма.
- Нови/променени зависимости: няма.
- Зловредни шаблони (бекдор, инжекция на код, обфускация): няма. Всяка промяна затяга съществуваща защита, а не я отслабва.
- Резултат: CLEAN — продължавам към прегледа по същество.
Оценка по файлове
- sql-guard.ts — Добавени
group_concat/json_group_array/json_group_objectкъм забранените функции. Правилно — това е същият клас усилване на паметта катоprintf/format(цял скан се събира в една клетка предиcapRows). Виж единствената бележка заstring_agg. - report-schema.ts — (1) Регексът за изписани величини е сменен от точен списък (
милиард|милион|хиляд) на суфиксите (илион|илиард|хиляд), което затваря дупката „3 квинтилиона лева" нагоре без регресия — тестовете го потвърждават, старите величини минават през суфиксите. (2) Колоните вече се строят с явни полета вместо spread — това спира пренасянето на непознати, подадени от модела свойства към рендера (validateEmitShapeне отхвърля непознати ключове). Много добра защита. - emit-report-schema.ts — Тавани
MAX_BLOCKS/ITEMS/COLUMNSи whitelist заalign(left|right).alignвалидирането спира атрибут/style инжекция при рендер (тестът с'"><b>'го покрива). Капацитетите провалят валидацията, така че скъпият път вbindReportне се изпълнява — коректно. - rag.ts — Праг за релевантност
MIN_SCHEMA_SCORE = 0.35с?? 0защита за липсващscore. Правилна посока: под прага → падане към пълния речник (по-силна заземеност от частичен RAG). Тестовете покриват и трите случая. - system-prompt.ts / describe-schema.ts — Твърдите MUST/NEVER капани се инжектират безусловно и под RAG чрез споделен
renderTraps(), така че двата пътя не могат да се разминат. Затваря първопричината (RAG turn с по-малко ограничения от no-RAG, който пуснаSUM(amount)). Обновените описания наamendments/parties/data_freshnessи разграничениетоamount_eur IS NULL≠value_suspectса коректни пояснения (само документация в промпта).
OWASP / CLAUDE.md
- Няма инжекция (SQL guard е allowlist за SELECT + денилист за опасни функции; рендерът получава само whitelisted
align). Валидацията на вход е засилена. Няма изтичане на ресурси. Няма мъртъв код, дублиране или частична имплементация. Наименованията са консистентни. Тестовете са смислени (проверяват отхвърляне, не тривиално минаване).
Единствена бележка (не блокираща)
Виж inline коментара в sql-guard.ts за string_agg. Понеже подходът е денилист, той е по природа непълен — заслужава да се обмисли allowlist на агрегатните функции в бъдеще.
Много добра, дисциплинирана работа по затягане на цялостта и заземеността. 👍
ydimitrof
left a comment
There was a problem hiding this comment.
Ревю на PR: fix(assistant): integrity, grounding & guard fixes from the PR #79 review
ВЕРДИКТ: COMMENT — одобрим по същество, без установени блокери; финално одобрение след зелена CI (тестове + покритие). Не съм публикувал inline коментари по твое изрично искане.
Обобщение
PR-ът е серия защитни (security-hardening) корекции по AI асистента като follow-up на ревюто на PR #79. Промените са атомарни, добре мотивирани в коментарите и придружени с целеви тестове. Не открих зловреден код, задни вратички (backdoors), обфускация, инжекции или изтичане на тайни. Промяната стеснява повърхността на атака — не я разширява.
Фаза 0 — Сканиране за критична сигурност (ЧИСТО)
- Твърдо кодирани тайни (secrets/API ключове/пароли): няма.
- Промени в URL/домейни: няма.
- Зловредни шаблони (backdoor/eval/обфускация/code injection): няма.
- Нови зависимости: няма промени в
package.json/lockfile — нулев риск от dependency-supply-chain. - SQL експлойти: промяната всъщност засилва SQL пазача (виж по-долу).
Преглед по файлове
sql-guard.ts — Добавени в denylist-а group_concat, string_agg (SQLite ≥3.44 синоним, релевантен за D1), json_group_array, json_group_object. Това затваря клас „memory-amplification" — string-building агрегати, които сгъват цял table scan в една огромна клетка, материализирана преди capRows да я измери (потенциален OOM на изолата). Проверих локално: пазачът блокира изброените функции и коректно пропуска легитимни sum(amount_eur) / concat(...). Забележка: това остава denylist (игра на догонване); коментарът честно признава, че устойчивото решение е позитивен allowlist — приемам го като tracked-separately.
report-schema.ts — (1) Регексът за „изписани с думи" величини мина от изричен списък (милиард|милион|хиляд) към суфиксите илион|илиард|хиляд, което затваря скалата нагоре (трилион/квадрилион/квинтилион/секстилион…). Потвърдих локално, че „3 трилиона лева", „три квинтилиона" и старите случаи се флагват; „Илион" (Троя) се over-флагва — приемливо, тъй като гейтът трябва да fail-toward-flagging. (2) Изричното изграждане на колоните (без { ...c } spread) е правилно — пречи неизвестни, подадени от модела полета да достигнат до renderer-а. Добра защита в дълбочина.
emit-report-schema.ts — Горни граници MAX_BLOCKS=100 / MAX_ITEMS=50 / MAX_COLUMNS=50 върху дължините на масивите (преди беше ограничен само размерът в байтове на редовете, не броят елементи) + isAlign whitelist (undefined|left|right). Проверих renderer-а (ReportBlockRenderer.tsx:180): align се сравнява с 'right' и се мапва към контролиран клас 'num', т.е. не се интерполира сурово в атрибут/стил — whitelist-ът е коректна защита в дълбочина. Границите са доста над реален отчет — без функционален риск.
rag.ts — Въведен праг MIN_SCHEMA_SCORE=0.35 за релевантност. Логиката е правилна: под прага се връщат по-малко/нула чънкове, а нула кара buildSystemPrompt да падне обратно към пълния статичен речник — т.е. по-безопасният изход. (m.score ?? 0) е коректна защита срещу backend без score (чете се като „под прага", отпада). Покрито с тестове (включително случая без score).
system-prompt.ts / describe-schema.ts — Твърдите MUST/NEVER капани (SUM само amount_eur, ocid≠УНП, …) вече се инжектират безусловно, дори при RAG. Това коригира реален integrity-дефект: RAG turn можеше да остане с по-малко ограничения от no-RAG fallback-а (пътят, който пропускаше SUM(amount)). renderTraps() е споделен между двата пътя — премахва дрифт. Актуализациите на речника (value_low в enum-а, колони на amendments/parties, data_freshness като таблица) са документационни и консистентни.
Тестове
Всеки промяна носи целеви тестове (align whitelist, cap-ове на масиви, relevance floor вкл. missing-score, суфиксите на величините, string-building агрегати, безусловни hard-traps). Тестовете изглеждат смислени (не тривиални). Не мога да потвърдя точен процент покритие/100% pass от diff-а — това остава за CI.
OWASP
- A03 Injection — SQL пазачът е усилен; prose-gate-ът и sanitizeProse ограничават материални числа в публичния отчет. ОК.
- A04 Insecure Design / DoS — cap-овете на масивите и блокирането на amplification-агрегати адресират ресурсно изчерпване. ОК.
- A05 Misconfiguration — без промени в конфигурация/тайни. ОК.
- Няма разширяване на повърхността на атака.
Незадължителни препоръки (не блокират)
- sql-guard: приоритизирайте позитивния function-allowlist (вече споменат в коментара) — denylist-ите ще изостават от нови алиаси.
- prose-regex: over-flag на „Илион"/„милиони" контексти може да увеличи retry-та на модела; приемливо, но си струва да се следи като UX сигнал.
- Уверете се, че
validateEmitShapeвинаги предхождаbindReportв реалния път (иначеalignразчита само на renderer-а — който все пак е безопасен).
Заключение: качествен, добре тестван защитен PR. Препоръчвам сливане след зелена CI.
|
Благодаря за подробното ревю! Кратко по трите незадължителни препоръки:
Локално тестовете ( |
lyubomir-bozhinov
left a comment
There was a problem hiding this comment.
Прегледах стриктно на връх 038d4e3 — силна работа, guard-овете издържаха на доста red-team. Проверих (и всички отхвърлени): stacked ; вътре в '…', CTE кръстен като blocked table, recursive CTE без RECURSIVE, pragma_table_info(...) TVF, json_each/generate_series, tautological JOIN ON 1=1, LIMIT offset,count / отрицателен / 1e9, load_extension/group_concat/string_agg, numeric-entity double-encode, markdown-split число, homoglyph цифри, prompt injection през RAG контекста. Двуслойният guard (L1 текст + L2 AST) е издържан; bindReport explicit-field reconstruction и безусловният hardTraps() (fix за grounding-регресията) са точно правилните решения.
Едно дребно (ново в PR-а, hardening):
apps/web/app/lib/assistant/emit-report-schema.ts(~ред 68) —validateEmitShapeне прави early-returnследMAX_BLOCKSпроверката, тъй че обхожда целия (model-emitted) масив.finalizeReportgate-ваbindReportзадshape.ok, тъй че няма downstream щета — но добаветеreturn { ok:false, errors }веднага след push-а (както за!Array.isArrayпо-горе), също и заMAX_ITEMS.
Прерогатив за прод (не са scope на този PR, вече tracked — само маркирам): || string-concat memory-amplification (#227) и cost-ът на първата run_sql заявка (D1 таксува scanned rows, #122). Струва си да са hard-prerequisite за provision, не просто roadmap.
Одобрявам.
An over-cap blocks/items/columns array is exactly the unbounded structure the ceilings guard against, yet validateEmitShape recorded the length error and then walked the whole array anyway — doing the very scan the cap exists to refuse. Return before the per-block scan on oversized blocks, and skip the per-element scan on oversized items/columns. Behaviour is unchanged for valid reports (ok:false either way); this only stops the wasted walk. Test asserts a single cap error with no per-element errors, proving the array is not scanned. Addresses lyubomir-bozhinov's review note on PR midt-bg#223.
|
Благодаря за red-team прегледа! По бележката за Клонът е обновен и с актуалния |
|
@ydimitrof @lyubomir-bozhinov @todorkolev един бърз преглед моля. |
An over-cap blocks/items/columns array is exactly the unbounded structure the ceilings guard against, yet validateEmitShape recorded the length error and then walked the whole array anyway — doing the very scan the cap exists to refuse. Return before the per-block scan on oversized blocks, and skip the per-element scan on oversized items/columns. Behaviour is unchanged for valid reports (ok:false either way); this only stops the wasted walk. Test asserts a single cap error with no per-element errors, proving the array is not scanned. Addresses lyubomir-bozhinov's review note on PR midt-bg#223.
f016dcd to
dcb7f60
Compare
An over-cap blocks/items/columns array is exactly the unbounded structure the ceilings guard against, yet validateEmitShape recorded the length error and then walked the whole array anyway — doing the very scan the cap exists to refuse. Return before the per-block scan on oversized blocks, and skip the per-element scan on oversized items/columns. Behaviour is unchanged for valid reports (ok:false either way); this only stops the wasted walk. Test asserts a single cap error with no per-element errors, proving the array is not scanned. Addresses lyubomir-bozhinov's review note on PR midt-bg#223.
dcb7f60 to
7c31fa9
Compare
ydimitrof
left a comment
There was a problem hiding this comment.
Преглед на PR: „fix(assistant): integrity, grounding & guard fixes from the PR #79 review“
Фаза 0 — Скенер за критична сигурност: ЧИСТО ✅
- Твърдо кодирани тайни: няма (нито ключове, пароли или токени).
- Промени по URL адреси: няма нови/променени URL.
- Злонамерени модели (backdoor, инжекция на код, обфускация): няма. Обратно — промените в
sql-guard.tsзатягат защитата, като добавятgroup_concat/string_agg/json_group_array/json_group_objectкъм denylist-а срещу амплификация на паметта. - Зависимости: единствената промяна е нов
IgnoredVulnsзапис вosv-scanner.tomlзаsharp(транзитивна, само за dev, презminiflare) — добре обоснован, с дата за повторно преразглеждане. Не влиза в деплойнатия Worker.
Резултат: няма блокиране, продължавам към преглед по измерения.
Обобщение по измерения
Сигурност (код-ниво) — отлично. Разширяването на denylist-а е коректно; регулярният израз с \b…\s*\( не дава очевидни фалшиви положителни (напр. my_group_concat( не се хваща заради _ като част от думата). Явното изграждане на колоните в bindReport (вместо { ...c }) е правилна защита срещу пренасяне на непознати, подадени от модела свойства към рендерера, тъй като validateEmitShape не отхвърля непознати ключове. Whitelist-ът isAlign (left|right) затваря дупка, при която стойност извън enum-а би достигнала атрибут/стил.
Тестове — 3/3. Всяка промяна е придружена от смислени тестове, които разкриват дефекти, а не минават тривиално: капове на масивите (къс път при over-cap), floor на релевантността в RAG (вкл. защитния случай на липсващ score), суфиксите -илион/-илиард при всички магнитуди, новите SQL агрегати. Тестовете за short-circuit проверяват точното поведение (само cap грешка, без обхождане).
Качество на кода — отлично. renderTraps() премахва риска от дрейф между describeSchema и RAG hard-traps блока — единствен източник на истина. Няма дублиране, мъртъв код или смесени отговорности.
Производителност — подобрение. Капове на масивите (MAX_BLOCKS/ITEMS/COLUMNS) и SQL агрегатните забрани реално намаляват риска от OOM в изолата. Няма регресии.
Документация — пълна. Коментарите обясняват „защо“, а не „какво“; актуализирани са речниковите описания (amendments, parties, data_freshness като таблица), и е добавено важното пояснение, че amount_eur IS NULL НЕ Е синоним на value_suspect.
Архитектура / grounding — стабилно. Безусловното инжектиране на hard-traps (hardTraps()) в RAG пътя коректно гарантира, че извличането никога не оставя хода с по-малко ограничения от no-RAG fallback-а — това е ключова поправка за интегритета. Заглавието # в RAG пътя е контекстуално правилно (там няма обгръщащ # Речник на данните).
Забележки (незадължителни)
- Дребна консистентност: новият
ignoreUntil = 2026-10-22използва формат само-дата, докато съществуващият запис по-горе е пълен RFC3339 (2026-10-01T00:00:00Z). Виж inline коментар. - Възможна лека редундантност: ако trap-текст попадне и в извлечените RAG чънкове, той може да се появи два пъти (веднъж през
hardTraps()). Безвредно, но си струва да се провери дали корпусът индексира trap-редовете.
Композитна оценка: 9.6/10
Няма открити дефекти. Промените са атомарни, добре тествани и подобряват сигурността и grounding-а. Препоръка: APPROVE.
ydimitrof
left a comment
There was a problem hiding this comment.
Ревю на PR: fix(assistant): integrity, grounding & guard fixes
Фаза 0 — Сигурност (задължителен сканиращ гейт): ЧИСТО ✅
- Твърдо кодирани тайни: няма.
BGGPT_API_KEYсе задава презwrangler secret put(интерактивно, некомитнато) — правилен подход. - URL промени: няма нови външни URL-и; само
wrangler/Vectorize/R2 ресурсни имена. - Злонамерени шаблони: няма backdoor/eval/обфускация. Всъщност PR-ът затяга атакувателната повърхност (sql-guard denylist, cap-ове на масиви, floor за релевантност).
- Зависимости: няма нови пакети.
osv-scanner.tomlдобавя два обосновани ignore-а (sharptransitive dev-only през miniflare,react-routerRSC unreachable) сignoreUntilдати и ясни „remove when" условия. Приемливо.
Резултат: PR не се блокира от Фаза 0.
По измерения
Сигурност (agent-level): силно. sql-guard.ts разширява denylist-а с group_concat/string_agg/json_group_array/json_group_object — реален клас за memory-amplification (агрегиране на цял scan в една клетка преди capRows). string_agg като SQLite ≥3.44 синоним на D1 е коректно уловен. Коментарът честно отбелязва, че denylist е „catch-up" игра и че positive allowlist е трайното решение — добра прозрачност. isAlign whitelist в emit-report-schema.ts затваря injection през атрибут/стил дори когато типът твърди, че стойността е невъзможна — правилен defense-in-depth.
Коректност/архитектура: солидно. Изнасянето на trap-овете от RAG корпуса и безусловното им инжектиране през hardTraps() е правилната поправка на реалния дефект (RAG turn с по-малко ограничения от no-RAG fallback-а). renderTraps() премахва дублирането между describeSchema() и hard-traps блока — няма drift. Версионираният native namespace (SCHEMA_NS='schema-v2') с версия и в id-тата коректно поддържа rollback на Worker-а срещу стар кохорт. MIN_SCHEMA_SCORE floor-ът с ?? 0 защита срещу липсващ score е разумен.
report-schema.ts: експлицитното изграждане на колоните (вместо { ...c }) е важна поправка — spread би пренесъл непроверени model-supplied ключове към рендера. Типизирано като EmitTableColumn[], така че tsc гарантира пълнота — без регресия на познати полета.
Тестове: отлични. Всяка промяна има прицелен тест: cap short-circuit (проверява, че over-cap масив НЕ се обхожда), score-floor fallback вкл. scoreless match, композиционен тест през реалния write→read seam (indexSchemaCorpus→retrieveSchemaContext→buildSystemPrompt) с exactly-once проверка на всеки trap, суфикс-магнитуди на всяка скала, и string-агрегатите в sql-guard. Тестовете са смислени, не тривиални.
Документация: пълна. README покрива re-indexing семантиката, „WHEN TO BUMP" правилото и rollback прозореца. Коментарите в кода са изчерпателни.
Забележки (ниска важност)
report-schema.tsregex/илион|илиард|хиляд/— over-flagging е умишлено и безопасно, но виж inline коментара за един реален false-positive извън „Илион".rag.ts: структурните типове са умишлено стеснена гледка към реалните bindings и не се проверяват срещу тях (route-ът кастира сas unknown as) — вече документирано, но drift срещуworker-configuration.d.tsняма да бъде уловен от tsc. Дръжте под око при промяна на Vectorize API.
Заключение
PR-ът е фокусиран, атомарен (integrity/grounding/guard follow-up), без scope creep, с висококачествени тестове и без открити уязвимости. Не мога да изпълня pnpm --filter web test/coverage в тази среда, затова оставям вердикт COMMENT вместо автоматично APPROVE — при зелен CI (typecheck 0, целият пакет минава, pnpm audit чист, както твърди README) PR-ът покрива критериите за одобрение.
fd3138f to
c67a011
Compare
ydimitrof
left a comment
There was a problem hiding this comment.
Преглед на PR: fix(assistant): integrity, grounding & guard fixes
Фаза 0 — Скан за критична сигурност: ЧИСТ ✅
- Твърдо кодирани тайни: няма.
BGGPT_API_KEYсе задава презwrangler secret putи изрично се коментира „никога не се комитва". - URL промени: няма нови/променени външни URL-и. Промените в
osv-scanner.tomlса само игнориране на CVE с обосновка иignoreUntilдата. - Злонамерени шаблони: няма backdoor/обфускация/инжекция. Промените в
sql-guard.tsвсъщност разширяват защитата (блокират string-building агрегати, които могат да OOM-нат isolate-а). - Зависимости: няма нови пакети; само документиран, dev-only, transitive
sharpCVE ignore.
Обобщение по измерения
- Сигурност (код): Силна.
sql-guard.tsдобавяgroup_concat/string_agg/json_group_array/json_group_objectкъм denylist-а със стабилна обосновка (памет-амплификация предиcapRows).emit-report-schema.tsдобавя горни граници на масивите (MAX_BLOCKS/ITEMS/COLUMNS) и whitelist заalign— коректна защита срещу неограничени структури и срещу out-of-enum стойност, стигаща до renderer, който я интерполира в атрибут/стил.report-schema.tsстрои колоните ЯВНО вместо със spread — правилно затваря пропускането на непознати model-подадени полета. - Grounding/RAG: Добра инженерна работа. Trap-правилата вече влизат безусловно през
hardTraps(), а RAG корпусът съдържа само таблици/примерни заявки — премахва дублирането и гарантира, че RAG turn никога не е с по-малко ограничения от no-RAG fallback-а. Версионираният native namespace (schema-v2) + релевантен праг (MIN_SCHEMA_SCORE) са коректно обосновани, вкл. rollback семантиката. - Тестове: Много добро покритие. Новите тестове са смислени (не тривиални): проверяват capping short-circuit, floor-drop, scoreless-match защита, exactly-once рендиране на trap-овете през реалния write→read seam, и разширените prose-magnitude/агрегат случаи.
- Документация: README и inline коментарите са изчерпателни, вкл. „WHEN TO BUMP" правило и ре-индексиране/rollback процедура.
- Производителност: Няма регресии; промените намаляват risk-а от OOM и не добавят горещи пътища.
Забележки (незадължителни, defense-in-depth)
Една минорна бележка по sql-guard.ts denylist-а (виж inline коментара) — самите автори вече отбелязват, че denylist е „catch-up game" и че positive allowlist е трайното решение (проследено отделно). Приемам това като известно ограничение, не като блокер.
Заключение
Промените са атомарни, добре тествани и подобряват integrity/сигурността. Няма блокиращи проблеми. Оставям COMMENT с една незадължителна бележка; препоръчвам мърдж след потвърждение, че пълният тестов пакет на apps/web минава (не мога да го изпълня в тази среда).
ydimitrof
left a comment
There was a problem hiding this comment.
Ревю на PR: fix(assistant): integrity, grounding & guard fixes
Обща оценка: ~9.2/10 — COMMENT (мержваем след потвърждаване на deploy-предпоставката).
Phase 0 — Сигурност (критичен скан): ЧИСТО ✅
- Няма hardcoded secrets —
BGGPT_API_KEYминава само презwrangler secret put, изрично документирано „никога не се комитва". - Няма нови/съмнителни URL-и и без обфускация/backdoor патерни.
- Зависимости: промяната в
osv-scanner.toml(sharp <0.35.0 libvips CVE) е ignore, а не нов пакет — обосновката е коректна (транзитивна dev-only зависимост на miniflare, не влиза в деплойнатия Worker, имаignoreUntilдата за ре-евалуация). - Промените в
sql-guard.tsподобряват позата по сигурност (blokира string-building агрегати и quoted-identifier bypass).
Силни страни
- Отлично тестово покритие — всяка поправка идва със смислен, целеви тест (не тривиален): capове на масиви,
alignwhitelist, quoted-identifier bypass,string_aggсиноним, релевантен floor + fallback, exactly-once рендер на trap-овете през реалния write→read seam. emit-report-schema.ts: caповете (MAX_BLOCKS/ITEMS/COLUMNS) с ранен изход преди сканиране са коректни; short-circuit семантиката е покрита от тест.report-schema.ts: явното построяване на колоните (вместо{ ...c }) затваря реален път за пропускане на непроверени model-полета към рендера — добра корекция.rag.ts: разделянето на trap-ове (безусловно презhardTraps()) от RAG корпуса маха дублирането и гарантира, че RAG turn никога не е по-слабо ограничен от no-RAG fallback-а. Версионираният native namespace +MIN_SCHEMA_SCOREfloor +score ?? 0защитата са добре обосновани.- Консистентно именуване, споделеният
renderTraps()премахва дрейфа между двата пътя.
CLAUDE.md съответствие
Без частични имплементации, без TODO-та, без дублиран код (renderTraps е извлечен точно за да няма дублиране), без dead code. Разделението на отговорности е спазено.
Наблюдения (незадължителни, не блокират)
- Deploy-предпоставка (главен операционен риск): след този деплой заявките ползват native namespace
schema-v2, който е ПРАЗЕН докатоindexSchemaCorpusне се пусне отново. Поведението е безопасно (fallback към пълния речник), но означава асистент без RAG grounding, докато ре-индексът не мине. Уверете се, че CD/runbook-ът гарантира ре-индекса — иначе тихо RAG-off. Документирано в README, но заслужава явна стъпка в деплой процедурата. sql-guard.tsdenylist остава по същество „догонваща игра" срещу нови алиаси — самите автори го признават и трекват positive allowlist отделно. Одобрявам посоката; препоръчвам да се приоритизира allowlist-ът като трайно решение.
Няма намерени блокиращи дефекти. Препоръка: APPROVE след потвърждение, че деплой процедурата включва ре-индекса.
An over-cap blocks/items/columns array is exactly the unbounded structure the ceilings guard against, yet validateEmitShape recorded the length error and then walked the whole array anyway — doing the very scan the cap exists to refuse. Return before the per-block scan on oversized blocks, and skip the per-element scan on oversized items/columns. Behaviour is unchanged for valid reports (ok:false either way); this only stops the wasted walk. Test asserts a single cap error with no per-element errors, proving the array is not scanned. Addresses lyubomir-bozhinov's review note on PR midt-bg#223.
1dacbdc to
40eaeea
Compare
An over-cap blocks/items/columns array is exactly the unbounded structure the ceilings guard against, yet validateEmitShape recorded the length error and then walked the whole array anyway — doing the very scan the cap exists to refuse. Return before the per-block scan on oversized blocks, and skip the per-element scan on oversized items/columns. Behaviour is unchanged for valid reports (ok:false either way); this only stops the wasted walk. Test asserts a single cap error with no per-element errors, proving the array is not scanned. Addresses lyubomir-bozhinov's review note on PR midt-bg#223.
40eaeea to
7f3376d
Compare
An over-cap blocks/items/columns array is exactly the unbounded structure the ceilings guard against, yet validateEmitShape recorded the length error and then walked the whole array anyway — doing the very scan the cap exists to refuse. Return before the per-block scan on oversized blocks, and skip the per-element scan on oversized items/columns. Behaviour is unchanged for valid reports (ok:false either way); this only stops the wasted walk. Test asserts a single cap error with no per-element errors, proving the array is not scanned. Addresses lyubomir-bozhinov's review note on PR midt-bg#223.
7f3376d to
1252f16
Compare
The curated dictionary the model treats as hard fact had drifted from packages/db/migrations/0000_init.sql: - amendments: no contract_id column — it links via unp/contract_number - parties: no role column — real cols are party_key, eik, ocid, party_id, name… - value_flag enum was missing value_low - amount_eur IS NULL was described as meaning value_suspect; it actually has several causes (FX-rateless foreign / value_suspect w/o estimate / no signing+current), and the unconfirmed count is value_flag='value_suspect' (home_totals.suspect), not NULL-amount rows - data_freshness is a table, not a view Drift here misleads a weak model into wrong joins or a wrong integrity KPI.
…e retrieval Two grounding gaps that could leave a RAG turn LESS constrained than the no-RAG fallback: - buildSystemPrompt used the retrieved chunks INSTEAD of the dictionary, so a retrieval that missed the money-sum trap dropped the SUM(amount_eur) rule entirely. Inject the short imperative DATA_TRAPS unconditionally; RAG now only selects the extra tables/example-queries for the question. - retrieveSchemaContext had no relevance floor — top-K returned its K least-distant chunks even when all were off-topic. Add MIN_SCHEMA_SCORE; below it we return fewer/zero chunks, and zero falls back to the full dictionary (the safe outcome).
group_concat / json_group_array / json_group_object collapse an entire full-table scan into one huge cell that materialises in Worker memory before capRows can measure it (and capRows keeps the first row whole) — the same memory-amplification class already blocked for printf/format/randomblob, one level up. Add them to the scalar blocklist.
- Prose number-gate missed трилион/билион/квадрилион: '3 трилиона лева' slipped the whole gate (the digit can't reach 'лева' across the Cyrillic word), an unbound order-up figure on a public report — the '12 млрд.' vector one magnitude higher. Add them to the spelled-magnitude stem. - Validate the optional column align against a left|right whitelist, and build resolved table columns explicitly instead of spreading the model object, so no unknown/unvalidated property reaches the renderer. - Cap model-emitted array lengths (blocks, items, columns) in validateEmitShape.
…h prompt paths
renderTraps() now owns the numbered-list rendering that describeSchema (full
dictionary) and the RAG hard-traps block duplicated, so the two paths cannot
drift, and the full-dictionary heading is harmonised to match the RAG block
("Задължителни правила за данните"). No behaviour change — string assembly only.
retrieveSchemaContext relied on `m.score` always being numeric. If an index backend ever returns a match without a `score`, the comparison was falsy and the chunk was dropped — the correct, safe outcome, but only incidentally. Make it explicit with `(m.score ?? 0) >= minScore` and a comment so a future refactor can't strip the guard, and cover it with a test. Addresses the review note on rag.ts robustness (ydimitrof).
…vers квинтилион+) The prose-number gate listed magnitudes explicitly and stopped at квадрилион, so "3 квинтилиона лева" slipped. Match the shared suffixes instead — милион⊃"илион", милиард⊃"илиард" — which covers the whole family (милион…секстилион…, милиард…) and closes the row upward for good rather than chasing an endless list. Addresses the review note on report-schema.ts (ydimitrof).
… the SQL guard string_agg(X, sep) is the official SQLite 3.44 synonym of group_concat and reaches the same code path on D1's modern SQLite, so it bypassed the scalar/aggregate denylist and achieved the same memory amplification (whole scan into one cell before capRows) the guard just closed for group_concat. Add it to the regex and the adversarial test. Addresses the review note on sql-guard.ts (ydimitrof).
An over-cap blocks/items/columns array is exactly the unbounded structure the ceilings guard against, yet validateEmitShape recorded the length error and then walked the whole array anyway — doing the very scan the cap exists to refuse. Return before the per-block scan on oversized blocks, and skip the per-element scan on oversized items/columns. Behaviour is unchanged for valid reports (ok:false either way); this only stops the wasted walk. Test asserts a single cap error with no per-element errors, proving the array is not scanned. Addresses lyubomir-bozhinov's review note on PR midt-bg#223.
The Dependency-audit step (osv-scanner) fails on ANY known vuln and, per its own comment, expects intentional exceptions in osv-scanner.toml — which did not exist yet. sharp@0.34.5 (High, GHSA-f88m-g3jw-g9cj: inherited libvips decoder CVEs) has no in-range upstream fix: miniflare pins sharp ^0.34.5 and its latest release still ships 0.34.5, so 0.35.0 is unreachable without a forced override. sharp is a transitive dev-only dep (miniflare dev server / test runtime), absent from the deployed Worker, and the vuln needs decoding an untrusted image the toolchain never handles. Record a dated (ignoreUntil 2026-10-22) exception so the audit gate goes green and the entry auto-resurfaces for revisit. Verified locally with osv-scanner 2.4.0: fails without the config, passes with.
…d RAG retrieval DATA_TRAPS are injected into the system prompt unconditionally (hardTraps), so indexing them in the schema corpus let retrieval hand the same rule back as "context" and render it twice. Traps are no longer indexed, and retrieveSchemaContext drops kind:'trap' matches a previously deployed index may still hold. Retrieval's job stays selecting relevant tables/queries. (review note, ydimitrof)
The sharp entry used a TOML local-date (2026-10-22) while the react-router entry above uses full RFC3339; align on the latter so parser versions cannot read the file inconsistently. (review note, ydimitrof)
…ace instead of a runtime trap filter Self-review of the previous commit found the client-side kind:'trap' filter ran AFTER Vectorize's server-side topK cut, so legacy trap vectors (12 of ~37 in a pre-change index, and the most money-question-similar text in the corpus) could eat up to all six retrieval slots — leaving the turn with fewer tables/queries than the no-RAG fallback, silently and permanently, since upsert never deletes the stale ids. Replaced with a versioned NATIVE namespace (SCHEMA_NS = 'schema-v2') on both the upserted vectors and the query: native namespaces need no metadata index and exclude every stale cohort at the source, so no topK slot is ever spent on a discarded match and the filter is gone. The version is in the vector ids too, so re-indexing writes a new cohort and a Worker rollback keeps working against the old one. An un-reindexed environment gets zero matches → the documented full-dictionary fallback. Also from the self-review: the stale module header still said trap-rules are embedded; system-prompt tests fed trap strings retrieval can no longer produce; and no test entered through the composed seam — added a retrieveSchemaContext → buildSystemPrompt test seeded with the real corpus asserting every DATA_TRAP renders exactly once (negative-controlled: re-adding traps under a disguised id/kind fails it and the new corpus-length assertion). README provisioning now documents the re-index-on-bump requirement.
Gap-sweep on the namespace fix found the composed exactly-once test was weaker than advertised: it hand-mirrored the write mapping instead of running indexSchemaCorpus, sliced only the first topK chunks (so a trap appended at the corpus tail escaped it), and hard-coded a 0.9 score silently coupled to MIN_SCHEMA_SCORE. It now routes through the real write path into a recording fake, retrieves the WHOLE corpus, and derives its score from the floor — so the write→read metadata contract (text key, ids, namespace) is under test and a trap re-added at any position under any id/kind fails it (negative-controlled again with a tail-appended, table-kind trap). Also: remaining fixtures moved off pre-v2 unversioned ids; the semanticSearch test title no longer claims a namespace it does not use (it pins the entity METADATA filter); the module header no longer claims the bindings satisfy the structural types (the route casts — drift is not tsc-checked); the new-cohort rollback guarantee is now correctly stated as bump-only, with an explicit WHEN TO BUMP rule (in-place upserts, positional query ids, orphan risk); the README no longer suggests purging a cohort inside its rollback window and notes delete-vectors needs an explicit id list; dropped the stale '150 теста' verification claim.
… величините
Единствените near-collisions на суфиксния шаблон са думи на -лион
(напр. „Илион") — приети съзнателно: gate-ът нарочно флагва в повече,
а в регистъра на поръчките такива думи почти не се срещат. Записан е
изходът при евентуални фалшиви отхвърляния: \p{L} lookaround граница
(JS \b е ASCII-only), а не списък с изключения. Изброяването на
-илиард величините е сведено до реалните форми на „милиард"
(бележка от ревюто).
… gate-а „Дванадесет млрд. лева" нямаше нито цифра (за \d…млрд шаблона), нито пълнословен суфикс — изписано числително + абревиатура се промъкваше покрай целия gate. млрд/млн влизат в стем шаблона (флагват и без цифра; негативен контрол: тестът пада без промяната). Остатъкът „хил." без цифра остава приет — хилядите не са defamation-мащабният вектор (бележка от ревюто на midt-bg#320).
…enylist SQLite (D1) резолва "group_concat"(x), [group_concat](x) и `group_concat`(x) до същия built-in, а регексът изискваше голо име непосредствено пред скобата — цитиран идентификатор минаваше L1. Опционален quote клас след името затваря и трите форми (adversarial тестове; негативен контрол: падат без промяната). Идентификатор с padding в кавичките е РАЗЛИЧЕН за SQLite и не резолва built-in — не изисква обработка (бележка от ревюто).
1252f16 to
d344a8a
Compare
Отстранява набор от конкретни дефекти в асистента, открити при ревю на PR #79 (насочено към петте области, които екипът поиска: интегритет на стойностите, SQL guard-а, RAG/grounding-а, hardening-а и provisioning-а). Всяка находка е проверена срещу кода/миграцията и покрита с тест.
Какво влиза (по комит)
1. Речник на данните ↔ миграция (
describe-schema.ts) — dictionary-ът, който моделът третира като твърд факт, беше се разминал сpackages/db/migrations/0000_init.sql:amendmentsнямаcontract_id— връзката е презunp/contract_number;partiesнямаrole— реалните колони саparty_key, eik, ocid, party_id, name…;value_flagenum-ът беше безvalue_low;amount_eur IS NULLбеше описано като „= value_suspect“, а има няколко причини (чужда валута без курс / value_suspect без оценка / липсва подписана+текуща); броят „непотвърдени“ еvalue_flag='value_suspect'(home_totals.suspect), не редовете с NULL;data_freshnessе таблица, не view.2. RAG grounding (
rag.ts,system-prompt.ts) — затваря случая, в който RAG ход остава по-слабо ограничен от no-RAG fallback-а:DATA_TRAPSвече се инжектират безусловно (RAG само добавя релевантните таблици/примерни заявки), така че пропуск в retrieval-а не може да изхвърли правилотоSUM(amount_eur);MIN_SCHEMA_SCORE) — под него връщаме по-малко/нула чънкове, а нула връща пълния речник (безопасният изход).3. SQL scalar guard (
sql-guard.ts) — блокираgroup_concat/json_group_array/json_group_object: колабират цял table scan в една огромна клетка, която се материализира предиcapRows— същият клас memory-amplification, който вече е блокиран заprintf/randomblob, едно ниво по-нагоре.4. Интегритет на справката (
report-schema.ts,emit-report-schema.ts):трилион/билион/квадрилион— „3 трилиона лева“ минаваше целия gate (цифрата не стига до „лева“ през кирилската дума), необвързана стойност с порядък по-висока от „12 млрд.“; добавени към шаблона на изписаните величини;alignна колона вече се валидира срещуleft|right, а resolved колоните се строят изрично (без spread), за да не стигне неизвестно свойство до renderer-а;blocks/items/columns) въвvalidateEmitShape.Съзнателно ИЗВЪН обхвата (за отделно обсъждане/PR)
Това са архитектурни решения, не еднолинейни поправки — държа ги настрана, за да остане PR-ът ревюируем, и ги описвам в коментара към PR #79:
run_sql— днешните два слоя стоят пред read-write binding;link.kindспрямо префикса на id-то и съгласуваностformat↔стойност.Проверка
pnpm test(apps/web): 342 passedpnpm typecheck: чистоprettier --check: чистоБазирано на
main, защото кодът на асистента вече е там; ако предпочитате да влезе презfeat/ai-assistant, лесно пренасочвам.Свързани issue-та
capRows: трайното решение срещу memory-amplification класа, чийто евтин L1 backstop е разширеният тук денилист (string-агрегати,string_aggсинонимът, quoted-identifier формите). Този PR съзнателно НЕ затваря run_sql: durable memory-amplification защита (allowlist на функции + cell-size cap преди capRows) #227 — денилистът остава догонваща игра до влизането на allowlist-а.