Skip to content

feat(assistant): integrity core + RAG foundation for the AI assistant#80

Merged
todorkolev merged 91 commits into
midt-bg:mainfrom
nedda76:feat/ai-assistant-impl
Jun 28, 2026
Merged

feat(assistant): integrity core + RAG foundation for the AI assistant#80
todorkolev merged 91 commits into
midt-bg:mainfrom
nedda76:feat/ai-assistant-impl

Conversation

@nedda76

@nedda76 nedda76 commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Какво е това

Backend основата на AI асистента на Sigma — имплементация на docs/spec/ai-assistant.md с хардунирането от §9 (PR #79). За разлика от ранната чернова, бекендът вече е опроводен от край до край: чисти тествани модули → tool registry → agent loop → ресурс route /assistant/chat (BgGPT през Cloudflare AI Gateway). Потребителският слой (dock UI, renderer на справките, глас) и provisioning-ът остават за следващи PR-та.

Какво има (имплементирано и проверено)

  • Интегритет на стойностите (§9.1): моделът не пише числа — emit_report блоковете реферират хендъли към сървърно изпълнени резултати, а bindReport() пре-свързва реалните стойности. Прозата е markdown-санитизирана; guardrail E2 отказва едри числа в прозата (вкл. Unicode цифрови форми).
  • Двуслоен SQL guard (§9.4): структурен read-only guard → AST guard (node-sql-parser) с table-allowlist, забрана на TVF/cross-join/recursion, AST-достоверен LIMIT и scalar-fn denylist.
  • RAG (добавка спрямо спецификацията): Vectorize + Workers AI (bge-m3) — schema grounding + semantic_search.
  • Инструменти: describe_schema, run_sql (+ Denial-of-Wallet rows-read бюджет, [Идея]: Denial-of-Wallet чрез run_sql: D1 таксува прочетени редове, не върнати (§7/§9.4 #122), semantic_search, eop_fetch (валидирана дата, fixed base — без SSRF), source_link и терминалния emit_report.
  • Edge: per-IP rate-limit (fail-closed в прод), cap на тяло/съобщения + abortSignal, drop на client-подадени system/tool съобщения.

Нови зависимости: ai@^6, @ai-sdk/openai@^3, node-sql-parser@^5.
Нови bindings/vars (wrangler.jsonc): AI, VECTORIZE, REPORTS (R2), ASSISTANT_RATE_LIMITER + config vars (BGGPT_*, MAX_STEPS, D1_ROWS_READ_BUDGET).

Проверено: pnpm --filter web typecheck → 0; пълният web test suite минава; pnpm audit --audit-level=high чист; Prettier чист. (CI е авторитетът за актуалния брой тестове.)

⚠️ Provisioning / deploy gate

Това PR добавя bindings към Cloudflare ресурси, които трябва да съществуват преди wrangler deploy. Освен това route-ът изпълнява run_sql в момента, в който BGGPT_API_KEY е наличен, докато D1 binding-ът е read-write — затова не задавайте прод BGGPT_API_KEY, докато (1) run_sql не работи срещу read-only D1 binding/реплика и (2) не е наложен глобален budget/circuit-breaker. Двуслойният SQL guard е defense-in-depth, не единствената бариера. Пълни детайли: apps/web/app/lib/assistant/README.md.

Какво НЕ е тук (следва)

UI слоят (dock, emit_report renderer + /reports/:id, глас), provisioning-ът, и launch-gate hardening: read-only D1 data path, глобален budget/circuit-breaker, freshness wiring, golden-report CI. eop_fetch засега връща само брой редове на ден, не самите данни (probe за наличие/свежест).

Имплементира части от спецификацията в #79.

nedda76 added 8 commits June 19, 2026 17:21
First implementation increment of docs/spec/ai-assistant.md, focused on the
highest-priority §9 hardening items plus the RAG layer. Pure, tested and
deploy-independent — no new deps/bindings — so it can land and be reviewed
before the agent loop / dock UI (which need BGGPT_API_KEY + cloud bindings).

- report-schema.ts: server-owned report values (§9.1) — the model references
  result handles, the server re-binds real numbers; tables take rows wholesale;
  prose is HTML-sanitized (closes stored-XSS on the public /reports/:id).
- sql-guard.ts: read-only structural guard + injected LIMIT + byte cap (§7/§9.4)
  with the AST-parser + read-only-binding primary guards documented as next.
- describe-schema.ts: curated data dictionary encoding the real data traps
  (amount vs amount_eur, value_flag, ocid≠UNP, lots grain) (§9.2).
- rag.ts: Vectorize + Workers-AI (bge-m3) — schema-grounding retrieval and a
  semantic_search tool. Deliberate addition over the SQL-only spec.
- tests for the value-binding and SQL guard (17 new; web suite 75 green).

See apps/web/app/lib/assistant/README.md for the architecture and roadmap.
Continues the foundation with two pure, tested modules that feed the agent loop:

- system-prompt.ts: encodes the runtime policies — emit_report policy (§9.10),
  values-by-reference (§9.1), data-trust / no-instructions-in-data (§7), the
  editorial skeleton (§4) and per-source freshness (§9.7); injects RAG schema
  context with a static-dictionary fallback.
- tool-results.ts: bridges D1 .all() rows to handled, byte-capped QueryResults
  (R1, R2 …) that the report binder re-binds from (§7).

10 new tests; web suite 85 green; typecheck 0; prettier clean.
…ize cap)

Live ЦАИС ЕОП day-query tool, hardened per spec §9.7: takes only a validated
date (never a model-supplied URL → no SSRF), reuses the verified eopSource URL
builder, caps each file before it reaches the model context, and treats the
payload as untrusted. Network call injected for testability.

7 new tests; web suite 92 green; typecheck 0; prettier clean.
CI `pnpm audit --audit-level=high` began failing repo-wide on a newly
published undici advisory (GHSA-vmh5-mc38-953g / -vxpw-j846-p89q /
-hm92-r4w5-c3mj) — DoS via WebSocket fragment-count bypass and SOCKS5 proxy
pool reuse — pulled in transitively via wrangler→miniflare (undici 7.24.8).
Dev/build-time only; never ships to the Worker runtime.

Add an `undici: ^7.28.0` override alongside the existing ws/vite pins. After
the bump `pnpm audit --audit-level=high` exits 0 (1 low remains, below the
gate). Lockfile updated to undici@7.28.0; nothing else changes.
Implements spec §4's two renderer rules as pure helpers the /reports/:id
renderer will consume:
- formatCell(value, hint) → delegates to @sigma/shared money/count/pct/date,
  so reports match native page formatting (no drift).
- entityHref(kind, id) → canonical internal href via @sigma/db hrefForEntity.

4 new tests; web suite 96 green; typecheck 0; prettier clean.
Two more pure helpers:
- source-link.ts: grounded official deep links (ЦАИС ЕОП procedure + open-data
  day files via the verified eopSource helper). Trade Register / АОП links are
  deferred — won't ship an unverified "official source" URL for a gov tool.
- emit-report-schema.ts: validateEmitShape (structural guard that runs before
  bindReport's handle resolution) + the model-facing EMIT_REPORT_JSON_SCHEMA.

10 new tests; web suite 106 green; typecheck 0; prettier clean.
Starts the agent-loop layer with its substance, kept dependency-free and tested:
- tools.ts: describe_schema, run_sql (guard → LIMIT → D1, retains the result
  under a handle), semantic_search, eop_fetch, source_link — each runs
  server-side and returns a compact string; data tools retain full results in
  ctx.results for binding. Plus runTool dispatcher and finalizeReport
  (validateEmitShape → bindReport against THIS turn's results only — §9.1/§9.3).

Remaining: the thin Vercel-AI-SDK wiring (streamText + /assistant/chat + provider
via AI Gateway) — adds the deps/bindings and carries no logic.

9 new tests; web suite 115 green; typecheck 0; prettier clean.
The thin SDK layer that turns the tested foundation into a working chat endpoint:
- agent.ts: BgGPT through @ai-sdk/openai (createOpenAI.chat) routed via the AI
  Gateway (§9.5); maps ASSISTANT_TOOLS → SDK tools (jsonSchema), adds emit_report
  wired to finalizeReport, runs streamText with stopWhen: stepCountIs(MAX_STEPS),
  returns the streamed Response.
- routes/assistant.chat.tsx: stateless resource route — POST UIMessages, RAG-ground
  the prompt (best-effort), run one turn, stream back.
- wrangler.jsonc: AI + VECTORIZE + REPORTS (R2) bindings + config vars; BGGPT_API_KEY
  stays a secret.
- deps: ai ^6, @ai-sdk/openai ^3.

Not runtime-verifiable here (needs BGGPT_API_KEY + the bindings), but typecheck 0,
115 tests, audit clean (no new high/moderate), prettier clean.
@lyubomir-bozhinov

Copy link
Copy Markdown
Collaborator

@nedda76 супер работа — чист, тестван fundament (integrity core + RAG) по §9 от спецификацията, при това deploy-независим (без нови bindings/ключове), така че може да се ревюира преди agent loop-а и облачните части. На практика това може да е Фаза 1 от асистента.

Една бележка по координацията. Искаме да доставим асистента като един общ, дълготраен branch — lyubomir-bozhinov:feat/ai-assistant (#79) — за да влезе целият фийчър в main наведнъж, като един ревюиран блок; иначе споделените файлове (wrangler.jsonc, pnpm-lock, новите пътища) се разминават, ако фазите кацат поотделно. #80 в момента сочи към main.

Предложение: пренасочи #80 към lyubomir-bozhinov:feat/ai-assistant вместо към main (GitHub поддържа PR между двата fork-а) — така PR-ът и кредитът остават твои, а работата става официалната Фаза 1. Ако предпочиташ, мога да cherry-pick-на commit-ите върху branch-а със запазено авторство; branch-ът засега съдържа само спецификацията, така че няма конфликти.

За следващите фази — нека координираме в Discord-а на МИДТ, да си разпределим обхвата и да не дублираме. Благодаря много за усилията!

@nedda76 nedda76 marked this pull request as ready for review June 20, 2026 07:46
@nedda76

nedda76 commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator Author

Благодаря за прегледа и за рамкирането като Фаза 1! 🙏 Превключих #80 на ready for review и обнових README-то да отразява текущото състояние: backend-ът е опроводен от край до край (13 модула + tool registry + agent loop през Vercel AI SDK → AI Gateway → BgGPT + /assistant/chat), 115 теста, typecheck 0, audit чист.

Една уговорка по координацията. Проверих съдържанието на feat/ai-assistant (#79) — то е само документация: 1 commit, 1 файл (docs/spec/ai-assistant.md, §9 към спеца), без код. Тоест цялата имплементация е тук, в #80, а неговата страна е спецификацията.

Понеже #79 е docs-only и не пипа нито един файл, който #80 променя, бих предложила най-простия и чист път: и двете PR-та да влязат директно в main — спецификацията (#79) и имплементацията (#80) — като два независимо ревюируеми блока, вместо да пренасочваме през споделения fork branch. Така няма разминаване по wrangler.jsonc / pnpm-lock (те идват изцяло от това PR), а кредитът и историята остават чисти.

След като и двете са в main, следващите фази стават нови PR-та срещу main — потребителският слой (dock UI, renderer на справките + /reports/:id), глас, и launch gate. За разпределянето на обхвата и архитектурното водене предлагам Тодор да поеме нататък (координация в Discord-а на МИДТ, както спомена), за да не дублираме.

Благодаря отново — радвам се, че foundation-ът е полезен! 🙌

cefothe pushed a commit to lyubomir-bozhinov/sigma that referenced this pull request Jun 20, 2026
Extend the guarantees-vs-limits section with concrete guardrails that
harden the residual data-correctness gaps, building on PR midt-bg#80's
system-prompt/describe-schema foundation: default filters, reconcile-
with-rollup self-check, explicit CPV interpretation, mandatory
methodology callout, Verifier trap-compliance checks, and a golden-
reports CI harness. Honesty (watermark + methodology) stays load-bearing.
@nedda76

nedda76 commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator Author

@Bozhidar-Vangelov @teodorkirkov
По едно око тук моля и ако може да погледнете за бъгове по сайта и предложения в Issues. 🙏

@teodorkirkov

Copy link
Copy Markdown

Сега ще погледна кода и сайта и ще напиша ако забележа нещо

@nedda76

nedda76 commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator Author

@Alben13579 Ако може да прегледаш дизайна на този ПР и като цяло дизайна на проекта. 🙏

@teodorkirkov

Copy link
Copy Markdown

@nedda76 прегледах кода
Примяната изглежда добре с малки коментари
Това е добре структурирана, внимателно обоснована имплементация. Наслояването (чисти модули → регистър на инструменти → цикъл на агенти → маршрут) е ясно, обосновката за сигурност е ясна и проследима до спецификационни секции, а тестовото покритие (115 успешни, 0 грешки при проверка на типа, чист одит) дава истинска увереност. Няколко наблюдения:

  • sql-guard.ts - Проверка само за четене, базирана на регулярни изрази, като основна защита. Настоящата структурна защита улавя очевидните случаи, но съпоставянето на регулярни изрази/низове в SQL е по своята същност непълно. Самият PR документира това („AST защита следва“). Като се има предвид, че това е публично насочен граждански инструмент, където SQL инжектирането може да разкрие чувствителни данни за обществени поръчки, бих предложил да се проследи това като последващ проблем, за да не се пропусне, вместо да се оставя само в README или документация.
  • agent.tsMAX_STEPS от системните параметри като низ. parseInt(env.MAX_STEPS || '5', 10) е добре, но няма валидиране или ограничаване. Неправилно конфигурирана среда може да зададе това на 0 или много голямо число. Струва си да се защити от: Math.max(1, Math.min(parseInt(...), 20)) или подобно, с изрична константа за горната граница.
  • Rate limiting / circuit-breaker отбелязан като Фаза 3. Като се има предвид, че това достига BgGPT при всяка заявка, бих го преместил поне на Фаза 2, а не на 3 - без него, пик на трафика или прекъсване на BgGPT ще доведе до видими за потребителя грешки без коректно влошаване на работата. Един прост флаг за експоненциално отлагане в Worker би бил достатъчен за v1
  • system-prompt.ts — политика за липса на инструкции в данните. Това е правилното място за прилагането ѝ, но би си струвало да се проведе тест, който да упражнява тази граница конкретно — например, резултат от инструмента, съдържащ фалшива инструкция като „Игнорирай предишни инструкции“ и други, за да се провери дали фрагментирането на промптовете действително е валидно. Настоящите тестове обхващат структурата, конкурентен тест би затворил цикъла.
  • wrangler.jsonc — R2, Vectorize, и AI вече са в конфигурацията, което означава, че следващото внедряване ще очаква те да съществуват. Ако средата на Cloudflare все още не е осигурена, това ще прекъсне CI/CD за екипа. Струва си или да се ограничат обвързванията върху env var, или да се добави бележка за deploy-gate в README файла, че осигуряването трябва да се случи преди внедряването на wrangler

@nedda76

nedda76 commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator Author

@experiment-bg Тук сме - един преглед и от теб моля и проверка на сайта. В момента доста ПР-и са се натрупали и може би е добра идея да изчакаме да се мърджнат, иначе голям рибейз ще падне.

@Bozhidar-Vangelov

Copy link
Copy Markdown

@nedda76 разгледах кода подробно

Решението е премислено и се чете леко. Това, че единствено сървърът изписва числа — моделът само посочва хендъли, а bindReport връзва реалните стойности — е същината на защитата срещу измислени данни и е изпипано добре. Харесва ми и че авторовата проза минава през изчистване на HTML преди да стигне публичната справка, и че sql-guard.ts честно си признава докъде стига и какво още липсва. Чистите модули са покрити смислено с тестове. Няколко наблюдения, едно от които изглежда като истински бъг:

  • eop-fetch.ts — таванът за размер на практика не се прилага. Около ред 67 тернарът е разменен:

    const slice = truncated ? body.slice(0, maxBytes) : body;
    const parsed = JSON.parse(truncated ? body : slice); // парсва цялото body

    При голям отговор се разпарсва пълното тяло, а slice изобщо не влиза в употреба. От коментара в catch личи, че е трябвало да е JSON.parse(slice) — тогава отрязаният текст не би се разпарсил и функцията щеше да върне мека грешка без редове. Тестът „caps an oversized response" не хваща това, понеже гледа само флага truncated, без да проверява че редовете реално ги няма — затова минава и при двата варианта. Отгоре на това EOP_MAX_BYTES се сравнява с body.length, т.е. брой знаци, а не байтове (за кирилица излиза близо двойно), докато capRows го прави коректно през TextEncoder. Щетата днес е малка, защото инструментът подава към модела само броя редове, но обещанието от §9.7 не се удържа.

  • routes.ts / tools.ts — въпрос на ред. Съгласен съм с това, което @teodorkirkov вече повдигна за лимитирането и AST проверката; само ще наблегна на последователността: route-ът е вече вкаран, а run_sql работи върху env.DB, който позволява и запис. Единственото, което в момента пази, е липсващият BGGPT_API_KEY — в мига, в който ключът се появи при разгръщане, отваряме публичен, незащитен и без лимит вход към модел + SQL, без точно онези пластове (AST проверка, само-четящ binding, лимит), които спецификацията изисква като задължителни. За чернова е поносимо, но нека е осъзнат избор — аз бих държал route-а зад флаг или изобщо нерегистриран, докато защитите кацнат. Дребно: BGGPT_RATE_LIMIT_RPM стои във vars, но засега нищо не го чете.

  • По-дребни:

    • report-schema.ts — при bar/flows/timeseries празните и нечислови стойности стават 0 (asNumber(...) ?? 0). Ред с value_suspect (NULL amount_eur) излиза като убедителна нула в графиката — за инструмент около достоверността е по-чисто такава точка да отпадне, вместо да се чертае нула.
    • tools.ts — при грешка run_sql подава суровото съобщение от базата право към модела; по-добре нещо обобщено.
    • rag.tssemantic_search търси в ns: 'entity', но в този PR няма кой да напълни този namespace (има само indexSchemaCorpus за схемата), тъй че ще връща празно, докато не се появи индексатор за същностите — струва си да влезе в списъка „предстои".

Иначе посоката е вярна и ядрото за достоверност е силно. За мен единствено спиращо е разменения таван в eop-fetch.ts и синхронизирането на описанието; останалото спокойно може и като отделен follow-up.

nedda76 added 4 commits June 20, 2026 22:00
Clamp MAX_STEPS to [1, 20] so a misconfigured deploy can neither stall
the tool loop (0/negative) nor uncap BgGPT calls (a huge value), and
degrade gracefully on failure: a mid-stream BgGPT outage now surfaces as
a readable message via the stream's onError, and a setup failure returns
a 503 instead of an unhandled 500. Addresses review notes on PR midt-bg#80.
Exercise the boundary teodorkirkov raised on PR midt-bg#80: a tool/EOP/DB value
carrying a fake instruction (e.g. "игнорирай предишните инструкции") must
be treated as DATA, never as a command. Lock the system-prompt data-trust
clause, that forModel serialises a poisoned cell verbatim inside the data
payload, and that bindReport keeps it as a plain table cell. (Model-level
resistance itself stays an eval concern — golden-report CI, §9.9.)
Make explicit that the new AI/Vectorize/R2 bindings reference resources
that must exist before `wrangler deploy` — deploying first fails the
deploy and blocks the team's CD (review note on PR midt-bg#80). Re-stage
rate-limiting + circuit-breaker from the launch gate into Phase 2, note
the v1 graceful degradation now in place, and refresh the test count.
Layer node-sql-parser (SQLite-only build) over the structural read-only
check in run_sql: parse the statement and reject anything that is not a
single read-only SELECT, failing closed on parse errors. Closes the gap
flagged on PR midt-bg#80 that a regex/keyword guard is inherently incomplete.

The structural guard stays the cheap first pass; the parser is the real
gate. Deliberately fails closed — valid-but-unparsed SQLite (e.g. window
functions without PARTITION BY) is refused, steering the model to the
canonical ORDER BY + LIMIT pattern, which all canonical queries use and a
regression test covers. A read-only D1 binding remains the open §9.4 layer.
@nedda76

nedda76 commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator Author

@teodorkirkov Благодаря за внимателния преглед! 🙏 Адресирах бележките в четири commit-а на branch-а:

1. sql-guard.ts — AST guard. Имплементирах го в това PR (sql-ast-guard.ts): над структурния слой run_sql вече минава и през AST guard с node-sql-parser (SQLite build) — парсва заявката и fail-closed отхвърля всичко, което не е единичен read-only SELECT (вкл. при parse грешка). Структурният слой остава евтиният първи филтър, парсерът е същинската врата. Съзнателен компромис: fail-closed значи, че валиден-но-непарснат SQLite (напр. window функции без PARTITION BY) също се отказва — моделът минава към каноничния ORDER BY … LIMIT шаблон; всички канонични заявки са покрити с регресионен тест. Остава като последен §9.4 слой read-only D1 binding (без write права), за да не може дори парсер-пропуск да стане UPDATE/DELETE.

2. agent.tsMAX_STEPS. resolveMaxSteps сега clamp-ва към [1, 20] с явна горна константа; липсваща/невалидна стойност пада към default 6 (+ unit тестове).

3. Rate-limiting / circuit-breaker. Преместих го от launch gate във Фаза 2, както предложи. За v1 добавих базова graceful degradation: mid-stream грешка от BgGPT се показва като четим текст през onError (вместо счупена връзка), а setup грешка връща 503 вместо 500. Пълният лимитер/прекъсвач следва там.

4. system-prompt.ts — adversarial тест. Добавих тестове, които упражняват границата: data-trust клаузата е заключена, forModel сериализира „отровена" клетка дословно вътре в data payload-а, а bindReport я държи като обикновена клетка (не като команда). Самата устойчивост на модела остава eval — golden-report CI (§9.9).

5. wrangler.jsonc — deploy-gate. Добавих изрично предупреждение до bindings-ите и в README: AI/Vectorize/R2 ресурсите трябва да съществуват преди wrangler deploy, иначе deploy-ът пада и блокира CD на екипа. Provisioning-ът трябва да предхожда deploy-а.

Проверено: typecheck 0, 127 теста минават, Prettier чист, pnpm audit --audit-level=high чист. Благодаря отново — много полезен преглед! 🙌

@lyubomir-bozhinov

lyubomir-bozhinov commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

Здравей @nedda76! Направихме задълбочен security ред-тийм на #80, спрямо самата спецификация в #79 (§9 hardening + agent-team addendum-а на @cefothe) — спрямо нашата обща висока летва.

Нa първо място: имплементацията е силна. Проверих какво е изпълнимо (прекарах adversarial SQL през самите guard функции) и write-защитата издържа на всичкоDELETE, SELECT 1; DROP…, WITH x AS (UPDATE…), PRAGMA, ATTACH се отхвърлят; string-literal/comment desync → AST fail-closed. Не намерих parser-differential за запис. Anti-defamation binding-ът, SSRF-safe eop_fetch, чистото боравене с BGGPT_API_KEY, MAX_STEPS clamp-ът — стабилни и покриват „Guaranteed by construction" от addendum-а. typecheck 7/7, 69 unit теста зелени. 👏

Ред-тиймът извади набор от gap-ове — и важното: почти всеки е точно launch-gate изискване, което спецификацията вече описва, просто още не е достигнато в кода. По тежест, с file:line + repro + препратка към спецификацията:

🔴 Launch-gate (спецификацията ги изисква преди публично излагане)

1. /assistant/chat няма rate-limit. routes/assistant.chat.tsx:28 — няма throttle, а всяка заявка пуска embeddings + agent loop (до maxSteps BgGPT извиквания). workers/app.ts лимитира само CSV (:100) и aggregation (:122); BGGPT_RATE_LIMIT_RPM (wrangler.jsonc:43) е деклариран, но не се чете никъде. Спецификацията го има като launch gate („Turnstile + Rate Limiting binding + circuit-breaker"), а agent.ts:107 сам го казва: „A full rate-limit … is the launch gate." Fix: ASSISTANT_RATE_LIMITER (per-IP + глобален таван) преди всяко AI извикване — моделът от #64.

2. run_sql е read-only, но не е ограничен по обхват. Спецификацията (§9, „Read-only SQL — защита в дълбочина"): „run_sql се нуждае от read-only път до данните, не само от AST parser", а без read-only binding — „AST allowlist-ът е носещ". В #80 няма нито едно: describe-schema.ts е речник, не allowlist. Изпълнимо:

SELECT name, sql FROM sqlite_master        → EXECUTES (+ LIMIT 500)
SELECT * FROM <произволна_таблица>          → EXECUTES

→ моделът може да изброи схемата и да чете всяка таблица в sigma D1. Fix: AST allowlist по таблици (+ забрана на sqlite_master/sqlite_schema), и/или §9.4 read-only binding.

3. enforceLimit се заобикаля + липсва query timeout. Спецификацията иска неотменяем per-query timeout, отделно от LIMIT — в #80 няма, а самият LIMIT се bypass-ва. sql-guard.ts:78, repro (изпълнимо):

SELECT * FROM contracts WHERE id IN (SELECT id FROM contracts LIMIT 1)   → без външен LIMIT
SELECT 'LIMIT 1' AS note FROM contracts                                  → без външен LIMIT (string literal!)
WITH RECURSIVE r(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM r)
  SELECT x FROM r WHERE x IN (SELECT 1 LIMIT 1)                          → неограничена рекурсия
SELECT COUNT(*) FROM contracts a, contracts b, contracts c               → cross product (LIMIT 500 не помага)

ctx.db.prepare(sql).all() (tools.ts:66) материализира целия резултат преди capRows. Fix: инжектирай LIMIT по AST на най-външния statement; отхвърляй WITH RECURSIVE/cartesian; per-query timeout.

🟠 Бъгове / hardening

4. eop_fetch byte cap-ът е no-op. eop-fetch.ts:70JSON.parse(truncated ? body : slice) парсва пълното тяло и в двата клона; EOP_MAX_BYTES не ограничава нищо → memory DoS на голям недоверен EOP файл. Fix: JSON.parse(slice) безусловно (soft error при truncation — какъвто вече имаш на :73).

5. Bound data cells не се санитизират. report-schema.ts:7-9 твърди, че stored-XSS векторът е затворен, но sanitizeProse се прилага само на prose; стойностите от резултатните редове (имена на фирми/възложители — повлияеми от подателя) минават през render-format.ts:36 (String(value)) без escaping. (Addendum-ът гарантира link-form и prose, но не markup в data cells.) Дали става XSS зависи от Phase-2 renderer-а, но binding слоят е мястото за гаранцията. Fix: санитизирай и data cells при bind (или коригирай коментара).

6. „Числа в prose" — точно guardrail E2. Binding-ът пази стойностните слотове, но text/callout/label полетата са авторски на модела и не се проверяват за числа. Addendum-ът вече го специфицира: guardrail E2 — „deterministic no-number-in-prose check… не prompt правило". В #80 е още само правило в system prompt-а. Fix: детерминистичен gate (валута + едри/агрегатни числа), с allowlist за години/CPV/ordinals — както пише E2.

7. Без таван на историята/тялото + retry мултипликатор. assistant.chat.tsx:29 не ограничава брой/размер на messages; streamText (agent.ts:96) е без maxOutputTokens/abortSignal, а SDK default-ният maxRetries качва worst-case извикванията над видимия cap. Fix: cap на историята + body size; abortSignal: request.signal; явен maxRetries.

🟡 Low

embed() без vectors.length === chunks.length проверка (rag.ts); без length cap на embed-вани заявки; D1 error се връща към модела (tools.ts:71); entityHref id-та без encodeURIComponent; липсва rag.test.ts.


Накратко: ядрото е добро и покрива „Guaranteed by construction" от addendum-а. Останалото е launch-gate слоят, който спецификацията ми описва (read-only път/allowlist, rate-limit + circuit-breaker, query timeout, E2) + два бъга (4, 5) — и всичко е в backend integrity core-а, който е твоят lane (apps/web/app/lib/assistant/* + route-а). Понеже #80 влиза в main като основата на Фаза 1, това са нещата за затваряне до launch-gate летвата преди ендпойнтът да се provision-не/изложи (спецификацията казва същото).

От наша страна мислим да поемем lane-овете, които консумират схемите ти, без да ги пипат — renderer-ът на /reports/:id, chat dock-а, R2 persist + dedup, voice — така че да няма конфликти по файлове. С радост ще pair-нем по което от хардънинг нещата искаш. Страхотна работа — нека я докараме до launch-grade. 🚀

nedda76 added 3 commits June 21, 2026 08:20
Three integrity-core fixes from the midt-bg#80 security red-team, all at the
binding layer where the guarantee belongs:

- Resolve entity-link ids per row (ResolvedRow.links) and require the
  link idCol to exist. Without this an immutable R2 report could not
  rebuild its /companies/:eik links — the block-spec contract needs it.
- Tag-strip submitter-influenceable string data cells at bind, not just
  prose, so no markup reaches the public /reports/:id even if a renderer
  forgets to escape (defence-in-depth, §7). Fixes the over-claiming
  comment in report-schema.
- Add guardrail E2: a deterministic no-material-number-in-prose gate on
  text/callout (currency, млн/млрд, grouped numbers, 5+ digit integers;
  years/small counts/ordinals pass). The model must put numbers in value
  slots the server binds — closes the unbound-number defamation vector.
Freeze the three contracts the team agreed to lock first so FE and BE can
work in parallel against fixtures: the block-spec (ResolvedReport, incl.
the new per-row link ids), the immutable R2 report object (§5), and the
SSE chat protocol (AI SDK UI message stream + the emit_report tool output
that carries the report). Adds machine-readable JSON/SSE fixtures under
app/lib/assistant/fixtures/. Source of truth stays the BE types.
The EOP_MAX_BYTES cap was a no-op: when the body exceeded the cap the code
still JSON.parsed the FULL body and returned every row (the cap only set a
'truncated' flag). An oversized untrusted EOP file therefore reached the
model in full. Now an over-cap body is refused with a soft error and never
parsed. Strengthens the test to assert rows are withheld. (review midt-bg#80)
@nedda76

nedda76 commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator Author

Чеклист за merge и активиране

Deferred launch-gate / eval елементите от прегледа вече се проследяват като отделни issues:

Launch gate (блокират прод ключа):

Eval (§9.9, преди публичен launch):

Преди merge

  • Осигури Cloudflare ресурсите ПРЕДИ deploy. Новите bindings (VECTORIZEsigma-assistant, REPORTSsigma-reports, AI, rate-limiter) се валидират при wrangler deploy. Ако main авто-деплойва без тях, deploy-ът пада за целия екип, не само за асистента. Командите са в apps/web/app/lib/assistant/README.md (Provisioning gate) — Vectorize индексът трябва да е 1024-dim / cosine (bge-m3), иначе RAG се чупи.

Преди прод BGGPT_API_KEY

Route-ът връща контролирано 503 без ключа, така че merge-ът е безопасен — активирането не е, докато #134 + #135 не са затворени.

Бележка за ревюъра

Чистите модули са изчерпателно unit-тествани; agent.ts (BgGPT loop) и route-ът са само typecheck-нати — end-to-end пътят ще се изпълни за пръв път след provisioning (няма live bindings в CI). Документираните dormant части (semantic_search до entity indexer-а, неносвързан freshness, eop_fetch само-брой) са Фаза 2, не дефекти.

@nedda76

nedda76 commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator Author

📌 TL;DR провизиониране (пълните стъпки: чеклистът по-горе + apps/web/app/lib/assistant/README.md → Provisioning gate):

Преди deploy създай ресурсите (Vectorize sigma-assistant 1024-dim / cosine, R2 sigma-reports) — иначе wrangler deploy пада за целия екип. Merge/deploy е безопасен без ключа (route → 503). Прод BGGPT_API_KEY чака #134 + #135.

@ydimitrof

Copy link
Copy Markdown

Благодаря за това — направих обстоен security преглед (SQL guard-овете, SSRF, интегритет на стойностите/XSS, edge/DoW и supply-chain/malicious-code скан), със сваляне на branch-а и PoC-ове срещу пиннатия node-sql-parser и реален SQLite. Накратко: отлична, съобразена със сигурността работа. Няма зловреден код, няма тайни, зависимостите са чисти, value-binding-ът fail-ва closed, и не успях да прекарам запис или изброяване на схемата през двата слоя на SQL guard-а при доста агресивен fuzzing. Тестовете (181) и typecheck минават локално. Merge-ът е безопасен (route → 503 без ключа); съгласен съм, че gate-ът за активиране е #134 + #135.

Няколко неща, които бих добавил към тракера (нито едно не блокира merge-а):

Струва си преди активиране

  • Fail-closed зависи от build-time флаг. Решението за fail-closed на лимитера ползва import.meta.env.PROD (app.ts:128rate-limit.ts:56), което се определя при build. Preview/staging deploy, билднат в dev режим, би качил лимитера fail-ващ open — нелимитиран платен endpoint, ако там някога има ключ. Предлагам „prod“ да се извежда от runtime сигнал (env променлива или наличие на прод ключа).
  • Относно assistant: run_sql must use a read-only D1 binding before the prod BgGPT key #134: съгласен, че read-only D1 пътят е водещият gate — той е множителят, който превръща всеки пропуск на guard-а в запис, а не в сгрешена справка.

SQL guard — две крехки fail-closed разминавания (безопасни днес само защото SQLite случайно дава грешка):

  • # коментари: структурният guard маха само -- / /* */, но SQLite парсерът третира #… като коментар и отрязва остатъка, така че AST-то не го вижда (sql-guard.ts:49). Струва си # да се маха/отхвърля, за да съвпадне текстовият модел на guard-а с този на изпълнителя.
  • LIMIT 1e9: AST-то го приема, но регексът (\d+) в enforceLimit — не, и се получава LIMIT 1e9 LIMIT 500 (sql-ast-guard.ts:355 / sql-guard.ts:182). Clamp от стойностния възел на AST-то (и отхвърляне на не-целочислени LIMIT литерали) маха разминаването регекс/AST.

Гейт за стойностите / рендиране

  • Gate-ът за числа в прозата декодира числовите entity-та веднъж, така че двойно кодирано 1&#50;000 минава gate-а, но може да се рендира като 12000 (report-schema.ts:256). Декодиране до фикспойнт го затваря.
  • За бъдещия /reports/:id renderer: моля, нека изискването за allowlist на схемите + без raw HTML остане твърд gate — sanitizeProse е denylist и има известни заобикаляния (javascript: с интервал, reference-style data:).

Egress / DoW (дребно)

  • eop_fetch би могъл да ползва redirect:'manual' + timeout + abort сигнала (tools.ts:165), и бюджет за брой извиквания на ход за eop_fetch/semantic_search, аналогично на rows-read бюджета.
  • Тялото се буферира преди проверката за 256 KB (assistant.chat.tsx:41) — предварителна проверка на Content-Length би отрязвала рано.

Много добра основа — с удоволствие ще отворя issue-та за всяко от горните, ако е полезно. 🙌

@lyubomir-bozhinov

lyubomir-bozhinov commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

@nedda76 Изглежда добре — съгласен съм, че merge-ът е безопасен при неактивиран BGGPT_API_KEY (route → 503), а активирането зад #134/#135 е правилната подредба. 👍 Благодаря за provisioning runbook-a.

Едно нещо, което не видях да е повдигано (не блокира merge, по-скоро за #135):

CSRF / липса на same-origin проверка на /assistant/chat. Action-ът не проверява метод, Content-Type или произход, така че cross-site POST с text/plain минава без preflight и стартира платен BgGPT turn от браузъра на жертвата (denial-of-wallet, ползвайки нейното IP). Per-IP лимитът го ограничава, но затварянето е евтино: изисквай POST + Content-Type: application/json + Sec-Fetch-Site: same-origin (или Origin == хоста) преди харченето.

nedda76 added 3 commits June 27, 2026 23:10
Without an origin check, a cross-site page could make a victim's browser POST text/plain (a CORS simple request, no preflight) and start a paid BgGPT turn under the victim's IP. Reject non-POST / non-JSON / explicit cross-site requests before buffering the body — requiring application/json forces a preflight on cross-origin fetch and blocks <form> CSRF. Also pre-check Content-Length to reject an oversized body before buffering. Pure firstPartyRejection helper + tests; error matrix in assistant-contracts.md §3 updated. (review midt-bg#80, lyubomir-bozhinov; Content-Length: ydimitrof)
decodeNumericEntities ran a single pass, so a double-encoded number (1&midt-bg#38;midt-bg#50;000 -> 1&midt-bg#50;000) passed guardrail E2 while a renderer would decode it the rest of the way to a fabricated 12000 — the §9.1 vector the gate exists to close. Loop to a fixpoint (bounded). (review midt-bg#80, ydimitrof)
node-sql-parser accepts LIMIT 1e9 (type 'bigint') and 1.5, but enforceLimit's regex does not, so it appended a second LIMIT (LIMIT 1e9 LIMIT 500) — a SQLite syntax error that only failed closed by accident. Reject any LIMIT/OFFSET that is not a plain non-negative integer so the AST and regex text models agree. (review midt-bg#80, ydimitrof)
@nedda76

nedda76 commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator Author

Благодаря на @lyubomir-bozhinov и @ydimitrof за двата security re-review! Адресирах трите изпълними бележки от 26.06 — head вече е 183efd5 (271 теста зелени, typecheck/Prettier чисти).

Имплементирано (с тестове):

  • CSRF → denial-of-wallet на /assistant/chat (cc690b7, @lyubomir-bozhinov): cross-site POST с text/plain вече се отхвърля преди платения ход. Изисквам POST + Content-Type: application/json (форсира preflight при cross-origin fetch — който не green-light-ваме — и блокира <form> CSRF) + проверка на Sec-Fetch-Site. Чист firstPartyRejection helper + unit тестове; матрицата в assistant-contracts.md §3 е допълнена с 403/405/415. Добавих и Content-Length pre-check преди буфериране на тялото (@ydimitrof).
  • Двойно-кодирани entity-та в gate-а за числа (7d17281, @ydimitrof): decodeNumericEntities вече декодира до фикспойнт1&#38;#50;000 (минаваше E2 като 1&#50;000, но renderer го декодира до 12000) сега се хваща.
  • Не-целочислени LIMIT литерали (183efd5, @ydimitrof): LIMIT 1e9 (AST: bigint) и 1.5 — които regex-ът на enforceLimit не може да clamp-не, та се получаваше LIMIT 1e9 LIMIT 500 (syntax error, fail-closed по случайност) — вече се отхвърлят явно.

Проследено (non-blocking, pre-activation) → #155: fail-closed лимитер от runtime сигнал вместо build-time import.meta.env.PROD; # коментари в структурния guard; egress hardening на eop_fetch (redirect:'manual' + timeout + per-turn call budget).

Активирането остава зад #134 + #135. 🙏

nedda76 added 2 commits June 27, 2026 23:40
Both this PR and midt-bg#118 (/health) independently claimed namespace_id 1004. A duplicate silently disables one limiter (no deploy error) and git auto-merges the two bindings without conflict, so the dup would ship unnoticed. Take 1005 here (1004 stays with midt-bg#118's reviewed health limiter) to deconflict regardless of merge order.
@todorkolev todorkolev merged commit ad23dbd into midt-bg:main Jun 28, 2026
1 check passed
lyubomir-bozhinov added a commit to lyubomir-bozhinov/sigma that referenced this pull request Jun 28, 2026
Mirror the assistant-contract seam (report.ts, stream.ts, fixtures + spec) onto
feat/ai-assistant. It re-exports ResolvedReport from app/lib/assistant/report-schema,
which is present here now that midt-bg#80 has landed in main and feat is caught up. Closes
the seam parity hole between feat/ai-assistant and feat/ai-assistant-contracts.
lyubomir-bozhinov added a commit to lyubomir-bozhinov/sigma that referenced this pull request Jun 28, 2026
Adopt fork-main's canonical app/lib/assistant foundation (upstream midt-bg#80 is strictly
ahead of contracts' midt-bg#80-head base) and deploy layer. wrangler limiter namespace_id
1004->1005 fixes a silent collision with the /health limiter. Keeps contracts' seam
spec and .dev.vars assistant docs. seed-endpoint/reindex (contracts-only) preserved.
lyubomir-bozhinov added a commit to lyubomir-bozhinov/sigma that referenced this pull request Jun 28, 2026
Mirror of #10 (approved) onto feat/ai-assistant-contracts. Identical lane content
— L0-L3 dedup keys + freshness (dedup.ts), in-isolate single-flight coordinator
(single-flight.ts), AI SDK v6 data-dedup/data-progress stream parts
(dedup-stream.ts) — pure, self-contained, not yet wired. The only difference from
#10 is the base: midt-bg#80 head here vs upstream main there.

Part of midt-bg#97 (two identical fixed-period questions must never diverge).
DiyanaDimitrova pushed a commit to lyubomir-bozhinov/sigma that referenced this pull request Jul 3, 2026
…ontract

- format AssistantPanel and AssistantTranscript after rebase conflict resolution
- revert cell() to hard errors for missing column and out-of-range/non-integer row
  (facts/totals precision slots; model must retry with correct reference)
- keep table display columns as warnings (graceful null rendering)
- add hard-error check for missing link idCols in table blocks (structural
  requirement — immutable report cannot reconstruct entity links without them)
DiyanaDimitrova pushed a commit to DiyanaDimitrova/sigma that referenced this pull request Jul 8, 2026
… meta canonical

- contractSlug now encodes %, /, ?, # so any AOP contract number is
  safe as a URL path segment
- entityHref in render-format.ts uses contractSlug directly for
  contracts (removes double-encode risk) and encodeURIComponent on the
  slug part for authorities/companies (closes path-traversal concern
  from review midt-bg#80)
- meta canonical/og:url in contract.tsx re-encodes params.id via
  contractSlug(contractIdFromSlug()) — React Router decodes the param
  before the loader runs, so the raw id was being used previously
- identity.test.ts covers the new ? and # cases and round-trips
B353N added a commit to B353N/sigma that referenced this pull request Jul 9, 2026
Remove the bare '(review ...)' attribution notes I left in contract.json.tsx and
sort-index-sentinel-sync.test.ts; the explanations stay. The pre-existing
'(review midt-bg#80)' issue references elsewhere are an established convention and are
untouched. Comment-only.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Нова функционалност или предложение priority: high Висок приоритет security Сигурност и уязвимости web Област: web

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants