feat(mcp): search_in_page — внутристраничный поиск для агента (#330) #339

Merged
vvzvlad merged 3 commits from fix/330-search-in-page into develop 2026-07-04 18:43:43 +03:00
Collaborator

Summary

Инструмент search_in_page(pageId, query, {regex?, caseSensitive?, limit?}) — внутристраничный поиск подстроки/regex для агента. Читает ProseMirror JSON страницы через существующий getPageRaw и ищет В ПАМЯТИ. Редакторские роли (Корректор/Фактчекер) перестают перебирать get_node по блокам. closes #330.

Без серверного эндпоинта, без изменения БД/схемы, без правки схемы-зеркала в packages/mcp/src/lib/.

Как

  • Чистая searchInDoc(doc, query, opts) (packages/mcp/src/lib/page-search.ts): рекурсивный спуск до текст-контейнера (paragraph/heading/ячейка), склейка inline-текста через blockPlainText (матч переживает границы марок — «т.е.» через bold/italic), literal indexOf по умолчанию / RegExp при regex:true.
  • Ответ { total, truncated, matches:[{ nodeId, blockIndex, type, before, match, after }] }. nodeId = attrs.id контейнера или #<topLevelIndex> ближайшего верхнего блока — тот же ref-формат, что принимают get_node/patch_node/якорь комментария (сверено — идентично getNodeByRef), так что агент сразу идёт ставить точечный комментарий. before/after ~40 симв. для уникального выделения. total/truncated всегда (без тихой обрезки).
  • Живёт в SHARED_TOOL_SPECS → оба транспорта (внешний /mcp + встроенный AI-chat); строка в SERVER_INSTRUCTIONS; сигнатура в DocmostClientLike + contract-тест. Промпты Корректора/Фактчекера + бамп версий, hash-lock каталога обновлён.
  • Guard'ы: пустой/whitespace query → чёткая ошибка; невалидный regex → чёткая ошибка (не generic 500); zero-length regex (\b,a*) пропускается (не зацикливается/не флудит); MAX_PATTERN_LENGTH=1000, MAX_CONTAINER_TEXT=100k; limit [1,200], дефолт 50.

How verified

  • @docmost/mcp: tsc чисто; node --test 467 passed (+17 page-search); server-instructions/tool-specs/contract зелёные.
  • apps/server: tsc --noEmit чисто (DocmostClientLike + wiring).
  • catalog check.mjs OK (версии fact-checker 5→6, proofreader 7→8, content-hashes).
  • Тесты покрывают literal+regex, case, mark-boundary glue, nodeId (attrs.id / #<index> для ячейки), context-bounds, limit/total/truncated, невалидный regex/пустой/длинный → ошибка, zero-length, empty-doc.

Известные ограничения (из внутреннего ревью, non-blocking)

  • Остаточный ReDoS: JS-regex не прерывается, поэтому cap по длине ограничивает базу, но не катастрофический бэктрекинг — паттерн (a+)+$ против большого одиночного контейнера может повесить event-loop. Реальная экспозиция низкая (контейнеры мелкие; паттерн задаёт аутентифицированная модель). Кандидат на отдельный hardening (safe-regex / worker+timeout), если понадобится.
  • Case-insensitive ЛИТЕРАЛ сворачивается через toLowerCase; символ, у которого lowercase иной длины (тур. İ) ПЕРЕД матчем сдвинет окно контекста — пренебрежимо для RU/EN.
  • При #<index>-fallback (ячейка) поле type = внутренний контейнер («paragraph»), а nodeId адресует верхний блок — адресация верна, поле задокументировано.

Заметка по мержу

Ветка от develop; packages/mcp/build/ пересобран и закоммичен по текущей конвенции develop (там build отслеживается). Если #333 (переводит mcp/build в gitignore) вмержится раньше — при ребейзе build-изменения снимутся.

🤖 Generated with Claude Code

## Summary Инструмент `search_in_page(pageId, query, {regex?, caseSensitive?, limit?})` — внутристраничный поиск подстроки/regex для агента. Читает ProseMirror JSON страницы через существующий `getPageRaw` и ищет В ПАМЯТИ. Редакторские роли (Корректор/Фактчекер) перестают перебирать `get_node` по блокам. `closes #330`. Без серверного эндпоинта, без изменения БД/схемы, без правки схемы-зеркала в `packages/mcp/src/lib/`. ## Как - Чистая `searchInDoc(doc, query, opts)` (`packages/mcp/src/lib/page-search.ts`): рекурсивный спуск до текст-контейнера (paragraph/heading/ячейка), склейка inline-текста через `blockPlainText` (матч переживает границы марок — «т.е.» через bold/italic), literal `indexOf` по умолчанию / RegExp при `regex:true`. - Ответ `{ total, truncated, matches:[{ nodeId, blockIndex, type, before, match, after }] }`. `nodeId` = `attrs.id` контейнера или `#<topLevelIndex>` ближайшего верхнего блока — тот же ref-формат, что принимают `get_node`/`patch_node`/якорь комментария (сверено — идентично `getNodeByRef`), так что агент сразу идёт ставить точечный комментарий. `before`/`after` ~40 симв. для уникального выделения. `total`/`truncated` всегда (без тихой обрезки). - Живёт в `SHARED_TOOL_SPECS` → оба транспорта (внешний `/mcp` + встроенный AI-chat); строка в `SERVER_INSTRUCTIONS`; сигнатура в `DocmostClientLike` + contract-тест. Промпты Корректора/Фактчекера + бамп версий, hash-lock каталога обновлён. - Guard'ы: пустой/whitespace query → чёткая ошибка; невалидный regex → чёткая ошибка (не generic 500); zero-length regex (`\b`,`a*`) пропускается (не зацикливается/не флудит); MAX_PATTERN_LENGTH=1000, MAX_CONTAINER_TEXT=100k; limit [1,200], дефолт 50. ## How verified - `@docmost/mcp`: tsc чисто; `node --test` **467 passed** (+17 page-search); server-instructions/tool-specs/contract зелёные. - `apps/server`: `tsc --noEmit` чисто (DocmostClientLike + wiring). - catalog `check.mjs` OK (версии fact-checker 5→6, proofreader 7→8, content-hashes). - Тесты покрывают literal+regex, case, mark-boundary glue, nodeId (attrs.id / `#<index>` для ячейки), context-bounds, limit/total/truncated, невалидный regex/пустой/длинный → ошибка, zero-length, empty-doc. ## Известные ограничения (из внутреннего ревью, non-blocking) - **Остаточный ReDoS:** JS-regex не прерывается, поэтому cap по длине ограничивает базу, но не катастрофический бэктрекинг — паттерн `(a+)+$` против большого одиночного контейнера может повесить event-loop. Реальная экспозиция низкая (контейнеры мелкие; паттерн задаёт аутентифицированная модель). Кандидат на отдельный hardening (safe-regex / worker+timeout), если понадобится. - Case-insensitive ЛИТЕРАЛ сворачивается через `toLowerCase`; символ, у которого lowercase иной длины (тур. `İ`) ПЕРЕД матчем сдвинет окно контекста — пренебрежимо для RU/EN. - При `#<index>`-fallback (ячейка) поле `type` = внутренний контейнер («paragraph»), а `nodeId` адресует верхний блок — адресация верна, поле задокументировано. ## Заметка по мержу Ветка от develop; `packages/mcp/build/` пересобран и закоммичен по текущей конвенции develop (там build отслеживается). Если #333 (переводит mcp/build в gitignore) вмержится раньше — при ребейзе build-изменения снимутся. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
agent_coder added 1 commit 2026-07-04 15:52:45 +03:00
Editorial roles (Corrector/Factchecker) brute-forced `get_node` block-by-block to
find occurrences (unquoted «ё», straight quotes, «т.е.»), burning tokens. New
`search_in_page(pageId, query, {regex?, caseSensitive?, limit?})` reads the page's
ProseMirror JSON via the existing getPageRaw and searches it IN MEMORY — no server
endpoint, no DB/schema change, no touch to the packages/mcp/src/lib schema mirror.

New pure `searchInDoc(doc, query, opts)` (packages/mcp/src/lib/page-search.ts):
recursive descent to each TEXT CONTAINER (paragraph/heading/table-cell paragraph),
glues its inline text via `blockPlainText` (a match survives inline-mark
boundaries — e.g. «т.е.» split across bold/italic), searches literal (indexOf) or
regex, and returns `{ total, truncated, matches:[{ nodeId, blockIndex, type,
before, match, after }] }`. `nodeId` is the container's attrs.id or the
`#<topLevelIndex>` of the enclosing top-level block — the SAME ref format
get_node/patch_node/comment-anchoring accept (verified identical to getNodeByRef),
so the agent goes straight from a hit to a targeted comment; `before`/`after` are
~40-char windows for a unique selection. `total`/`truncated` always reported (never
silent truncation). Lives in the SHARED_TOOL_SPECS registry → exposed in BOTH
transports (external /mcp + in-app AI-chat), with a SERVER_INSTRUCTIONS line and a
DocmostClientLike signature + contract-test entry. Corrector/Factchecker prompts
get a one-line "use search_in_page first" hint (versions bumped, catalog hash lock
refreshed).

Guards: empty/whitespace query → clear error; invalid regex → clear error (not a
generic 500); zero-length regex matches (`\b`, `a*`) skipped with lastIndex
advanced (no loop/flood); MAX_PATTERN_LENGTH=1000, MAX_CONTAINER_TEXT=100k bound
each exec; limit clamped [1,200] (default 50).

Tests: new page-search.test.mjs (17) — literal+regex, case-sensitivity,
mark-boundary glue, nodeId for paragraph/heading (attrs.id) and table-cell
(#<index> fallback), context bounds, limit/total/truncated + clamp, invalid
regex/empty/over-long errors, zero-length skip, empty-doc null-safety.

mcp: tsc clean; node --test 467 passed (+17). apps/server: tsc --noEmit clean
(DocmostClientLike + wiring). catalog check.mjs OK.

Known limitations (from internal review, non-blocking):
- Residual ReDoS: a crafted catastrophic-backtracking pattern (e.g. `(a+)+$`)
  against a large single container can hang the event loop — JS regex is not
  interruptible, so the length caps bound the base but not the backtracking.
  Realistic exposure is low (containers are small; the pattern is supplied by the
  authenticated model). Candidate for a follow-up hardening (safe-regex validation
  or a worker+timeout) if it matters.
- Case-insensitive LITERAL search folds via toLowerCase; a char whose lowercase
  differs in length (e.g. Turkish İ) BEFORE a match could shift the context
  window — negligible for the RU/EN editorial scenario.
- On a `#<index>` table-cell fallback, `type` is the inline container ("paragraph")
  while nodeId addresses the top-level block — addressing is correct; the field is
  documented as the container's type.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder added the review/needs label 2026-07-04 15:52:45 +03:00
Collaborator

Ревью — #339 (mcp: search_in_page — внутристраничный поиск для агента, #330), round 1, head 40d42d61, base develop f5d19f97

Вердикт: ESCALATE (нужно решение человека) — инструмент реализован аккуратно, объективка зелёная, ref-контракт (search→get_node/comment) целостен. НО regex:true исполняет агент-заданное регулярное выражение БЕЗ ограничения по времени в общем event-loop — это подтверждённая ReDoS-DoS всего сервера, и как её чинить — развилка с материальными трейд-оффами, которую агент выбирать не вправе. Три аспекта (security/stability/architecture) сошлись на escalate независимо; я перепроверил эмпирически сам. Плюс 4 DO (доки/тесты) — чинить ПОСЛЕ решения по развилке. needs-human.

Объективка запущена мной (head 40d42d61, source-only ревью; build/ — коммиченный артефакт develop-конвенции, исключён из ревью, но сверен): mcp build 0, закоммиченный build/ = свежая сборка (0 расхождений), mcp node --test 467 passed, mcp tsc 0, server tsc 0. agent-roles-catalog/scripts/check.mjs → OK (хеши/версии консистентны). Зелёная.

Escalate — нужно решение человека (петля остановлена)

  • [security/stability] Агент-заданный regex исполняется без тайм-аута в общем event-loop → ReDoS кладёт весь серверpackages/mcp/src/lib/page-search.ts:89-99,177-187,205-207.

    Что это за инструмент (простыми словами): search_in_page(pageId, query, {regex?, caseSensitive?, limit?}) — новый MCP-инструмент, чтобы редакторские роли (Корректор/Фактчекер) искали текст в странице, а не перебирали блоки. При regex:true он компилирует регулярку из query (new RegExp(query, "gi")) и гоняет её по тексту страницы.

    Проблема: регулярка исполняется СИНХРОННО в event-loop, а два гварда ограничивают только РАЗМЕР (MAX_PATTERN_LENGTH=1000, MAX_CONTAINER_TEXT=100_000), НЕ время. JS-regex не прерываем (это признано в комментарии кода). Катастрофический бэктрекинг экспоненциален и достигается КРОШЕЧНЫМ паттерном далеко под лимитом:

    • stability-аспект замерил на собранном модуле: (a+)+$ (5 символов) по контейнеру из 26 букв «a» = 7 сек, 30 = 15 сек, 34 = не завершилось за 45 сек;
    • я перепроверил сам: (a+)+b по 40 «a» — >4 сек (таймаут).
      Т.е. size-caps не защищают вообще. Один такой вызов пинит CPU и вешает весь event-loop.

    Почему это про весь сервер (blast radius): инструмент зарегистрирован в ОБОИХ транспортах, включая встроенный AI-chat в apps/server (ai-chat-tools.service.tsclient.searchInPagesearchInDoc в том же NestJS-процессе). Значит один зловредный вызов замораживает ВЕСЬ Docmost-бэкенд для ВСЕХ пользователей (не self-DoS одной сессии). Внешний request-timeout не спасёт: синхронная regex блокирует loop, таймер не сработает. Регулярку выбирает LLM-агент, но агент читает пользовательский контент страницы → prompt-injection в тексте страницы может навести агента на патологический паттерн (+ подложить длинный матчащий прогон в той же странице). И даже просто кривая (не злонамеренная) регулярка на длинном тексте повесит.

    Почему нужен человек: это расходится с СОБСТВЕННОЙ моделью проекта — соседний путь агент-кода docmost_transform гоняет агентский JS в node:vm-песочнице с 5-сек тайм-аутом (client.ts), т.е. проект уже считает агент-исполняемый ввод DoS-чувствительным. search_in_page вернул произвольные агент-вычисления БЕЗ тайм-гарда. Как чинить — развилка (фича/зависимость/архитектура), выбор трейд-оффа за тобой.

    • Вариант A — принять size-caps как есть (агент доверенный, ReDoS вне scope). Effort: 0. Плюсы: нет новых зависимостей/инфры, полная мощь JS-regex. Минусы: один prompt-injected паттерн кладёт общий сервер для всех — явное принятие cross-tenant DoS.
    • Вариант B — движок без бэктрекинга (node-re2) для regex:true. Effort: m. Плюсы: линейное время, фича сохранена, без тайм-аут-плюмбинга. Минусы: нативная зависимость в mcp-пакете (сборка/паккэджинг), RE2 не поддерживает backreferences/lookbehind — часть паттернов сменит поведение.
    • Вариант C — жёсткий wall-clock kill (worker-thread/subprocess с terminate), ИЛИ ограничить regex:true изолированным внешним /mcp, а встроенный AI-chat сделать literal-only. Effort: m–l. Плюсы: реально ограничивает худшее время / убирает экспозицию из общего процесса; literal-only-в-shared — минимальный безопасный срез, покрывает большинство редакторских свипов. Минусы: worker-изоляция — новая инфра ради одного инструмента; сплит-по-транспорту усложняет контракт/доки.
    • Вариант D — только literal indexOf, выпилить/отложить regex:true. Effort: s. Плюсы: ReDoS исключён полностью (indexOf линеен), ноль зависимостей, ядро #330 («найти фразу в странице») сохранено. Минусы: убирает заявленную regex-возможность (word-boundary/char-class свипы) — если Фактчекеру она реально нужна, это потеря.
    • Рекомендация: нужно твоё решение. Быстрый безопасный путь — D сейчас (literal-only, ядро #330 цело, ReDoS нет), с B (RE2) как follow-up, если редакторским ролям regex реально нужен. Если regex обязателен уже сейчас — B, либо literal-only-в-shared из C. Вариант A (оставить как есть) не рекомендую — это осознанное принятие DoS всего сервера через prompt-injection. Выбери — и сними needs-human.

Do — применить ПОСЛЕ решения по развилке (сейчас петля на паузе)

  1. F1 [documentation · warning] Убери ложное «pass nodeId as a comment anchor» — create_comment берёт текстовый selection, не nodeIdpackages/mcp/src/tool-specs.ts:118-131 (+ зеркала: page-search.ts module-JSDoc и SearchMatch.nodeId-JSDoc, client.ts searchInPage-JSDoc).
    Описание инструмента говорит, что возвращённый nodeId можно «pass straight to get_node/patch_node or as a comment anchor». Но create_comment (index.ts:745-816) НЕ имеет параметра nodeId — top-level комментарий требует точный текстовый selection и падает, если текст не найден. Основная цель PR — «найти каждое вхождение → оставить точечный комментарий»; агент, поняв описание буквально, попытается заякорить комментарий по nodeId и провалится. Описание вдобавок само себе противоречит («before/after дают ~40 симв. чтобы построить уникальный selection»).
    Fix: убери «or as a comment anchor» / «and comment anchoring» из ref-claim в tool-specs.ts, page-search.ts (module + SearchMatch JSDoc), client.ts; укажи, что ref — для get_node/patch_node, а для комментария строишь уникальный текстовый selection из before/match/after и передаёшь в selection create_comment.

  2. F2 [documentation · warning] Уточни: форма #<index> работает с get_node, но НЕ с patch_node — те же файлы/строки, что F1.
    Описание: nodeId «is the block id (or "#" for table/cell content) — pass it straight to get_node/patch_node». Для id-less контейнера (table/cell) searchInDoc возвращает #<topLevelIndex>. Но patch_node (replaceNodeById) резолвит только по attrs.id; #<index> не матчит ничего → запись отклоняется. #<index> принимает ТОЛЬКО get_node (getNodeByRef).
    Fix: в описании (tool-specs.ts + JSDoc в page-search.ts/client.ts) отметь, что #<index>-ref'ы (table/cell без attrs.id) принимает get_node, но НЕ patch_node (тому нужен attrs.id).

  3. F3 [test-coverage · warning] Добавь round-trip ассерт: резолви nodeId каждого матча через getNodeByRefpackages/mcp/test/unit/page-search.test.mjs:121-157.
    Главная корректностная претензия фичи — что nodeId/blockIndex — тот же ref-формат, что потребляют get_node/patch_node/comment. Тесты пиннят точные строки ref (p1/h1/#1), но НИКОГДА не скармливают возвращённый nodeId реальному потребителю. Если вера автора о формате неверна (напр. @1 вместо #1), код и пиннящий тест ошибутся ВМЕСТЕ и оба пройдут, а интеграция (весь смысл инструмента) сломается.
    Fix: импортни getNodeByRef+blockPlainText из ../../build/lib/node-ops.js и в тестах nodeId ассертни const hit=getNodeByRef(d,res.matches[0].nodeId); assert.ok(hit); assert.ok(blockPlainText(hit.node).includes(res.matches[0].match)) для id-параграфа, id-heading и #<index>-table-fallback.

  4. F4 [test-coverage · warning] Пиннь before/after на краях строки, включая clamp Math.max(0,…) в длинном контейнереpackages/mcp/test/unit/page-search.test.mjs:54-61.
    Кейс «~40 симв.» ставит needle в центр 206-символьной строки, где обе рамки полные и гвард Math.max(0, idx-CONTEXT) (page-search.ts:224) не решает. Нагруженная ветка — матч в первых CONTEXT символах контейнера ДЛИННЕЕ CONTEXT — не покрыта; регресс, дропнувший гвард, вернул бы пустой before, и ни один тест не поймал бы.
    Fix: тест, где матч в первых 40 символах контейнера длиннее 40 (напр. "ab NEEDLE"+"x".repeat(100)) → before === "ab "; и пиннь before === "" для матча в index 0 и after === "" для матча в конце контейнера.


DROP — кодеру НЕ делать · калибровочный лог (для оператора)

  • [speculative] low/high [stability] при контейнере >100k символов text-cap молча недосчитывает матчи за лимитом (total=0, truncated=false) — требует одиночный >100k-символьный контейнер (контривед), дропнуто по правилу неправдоподобных предпосылок.
## Ревью — #339 (mcp: search_in_page — внутристраничный поиск для агента, #330), round 1, head `40d42d61`, base develop `f5d19f97` **Вердикт: ESCALATE (нужно решение человека)** — инструмент реализован аккуратно, объективка зелёная, ref-контракт (search→get_node/comment) целостен. НО `regex:true` исполняет агент-заданное регулярное выражение БЕЗ ограничения по времени в общем event-loop — это подтверждённая ReDoS-DoS всего сервера, и как её чинить — развилка с материальными трейд-оффами, которую агент выбирать не вправе. Три аспекта (security/stability/architecture) сошлись на escalate независимо; я перепроверил эмпирически сам. Плюс 4 DO (доки/тесты) — чинить ПОСЛЕ решения по развилке. `needs-human`. **Объективка запущена мной** (head `40d42d61`, source-only ревью; `build/` — коммиченный артефакт develop-конвенции, исключён из ревью, но сверен): mcp build 0, **закоммиченный `build/` = свежая сборка (0 расхождений)**, mcp `node --test` **467 passed**, mcp tsc 0, server tsc 0. `agent-roles-catalog/scripts/check.mjs` → OK (хеши/версии консистентны). Зелёная. ### Escalate — нужно решение человека (петля остановлена) - **[security/stability] Агент-заданный regex исполняется без тайм-аута в общем event-loop → ReDoS кладёт весь сервер** — `packages/mcp/src/lib/page-search.ts:89-99,177-187,205-207`. **Что это за инструмент (простыми словами):** `search_in_page(pageId, query, {regex?, caseSensitive?, limit?})` — новый MCP-инструмент, чтобы редакторские роли (Корректор/Фактчекер) искали текст в странице, а не перебирали блоки. При `regex:true` он компилирует регулярку из `query` (`new RegExp(query, "gi")`) и гоняет её по тексту страницы. **Проблема:** регулярка исполняется СИНХРОННО в event-loop, а два гварда ограничивают только РАЗМЕР (`MAX_PATTERN_LENGTH=1000`, `MAX_CONTAINER_TEXT=100_000`), НЕ время. JS-regex не прерываем (это признано в комментарии кода). Катастрофический бэктрекинг экспоненциален и достигается КРОШЕЧНЫМ паттерном далеко под лимитом: - stability-аспект замерил на собранном модуле: `(a+)+$` (5 символов) по контейнеру из 26 букв «a» = **7 сек**, 30 = **15 сек**, 34 = **не завершилось за 45 сек**; - я перепроверил сам: `(a+)+b` по 40 «a» — **>4 сек** (таймаут). Т.е. size-caps не защищают вообще. Один такой вызов пинит CPU и вешает весь event-loop. **Почему это про весь сервер (blast radius):** инструмент зарегистрирован в ОБОИХ транспортах, включая встроенный AI-chat в `apps/server` (`ai-chat-tools.service.ts` → `client.searchInPage` → `searchInDoc` в том же NestJS-процессе). Значит один зловредный вызов замораживает ВЕСЬ Docmost-бэкенд для ВСЕХ пользователей (не self-DoS одной сессии). Внешний request-timeout не спасёт: синхронная regex блокирует loop, таймер не сработает. Регулярку выбирает LLM-агент, но агент читает пользовательский контент страницы → prompt-injection в тексте страницы может навести агента на патологический паттерн (+ подложить длинный матчащий прогон в той же странице). И даже просто кривая (не злонамеренная) регулярка на длинном тексте повесит. **Почему нужен человек:** это расходится с СОБСТВЕННОЙ моделью проекта — соседний путь агент-кода `docmost_transform` гоняет агентский JS в `node:vm`-песочнице с 5-сек тайм-аутом (`client.ts`), т.е. проект уже считает агент-исполняемый ввод DoS-чувствительным. `search_in_page` вернул произвольные агент-вычисления БЕЗ тайм-гарда. Как чинить — развилка (фича/зависимость/архитектура), выбор трейд-оффа за тобой. - *Вариант A — принять size-caps как есть (агент доверенный, ReDoS вне scope).* Effort: 0. Плюсы: нет новых зависимостей/инфры, полная мощь JS-regex. Минусы: один prompt-injected паттерн кладёт общий сервер для всех — явное принятие cross-tenant DoS. - *Вариант B — движок без бэктрекинга (node-re2) для `regex:true`.* Effort: m. Плюсы: линейное время, фича сохранена, без тайм-аут-плюмбинга. Минусы: нативная зависимость в mcp-пакете (сборка/паккэджинг), RE2 не поддерживает backreferences/lookbehind — часть паттернов сменит поведение. - *Вариант C — жёсткий wall-clock kill (worker-thread/subprocess с terminate), ИЛИ ограничить `regex:true` изолированным внешним `/mcp`, а встроенный AI-chat сделать literal-only.* Effort: m–l. Плюсы: реально ограничивает худшее время / убирает экспозицию из общего процесса; literal-only-в-shared — минимальный безопасный срез, покрывает большинство редакторских свипов. Минусы: worker-изоляция — новая инфра ради одного инструмента; сплит-по-транспорту усложняет контракт/доки. - *Вариант D — только literal `indexOf`, выпилить/отложить `regex:true`.* Effort: s. Плюсы: ReDoS исключён полностью (indexOf линеен), ноль зависимостей, ядро #330 («найти фразу в странице») сохранено. Минусы: убирает заявленную regex-возможность (word-boundary/char-class свипы) — если Фактчекеру она реально нужна, это потеря. - **Рекомендация:** нужно твоё решение. Быстрый безопасный путь — **D сейчас** (literal-only, ядро #330 цело, ReDoS нет), с **B (RE2)** как follow-up, если редакторским ролям regex реально нужен. Если regex обязателен уже сейчас — B, либо literal-only-в-shared из C. Вариант A (оставить как есть) не рекомендую — это осознанное принятие DoS всего сервера через prompt-injection. Выбери — и сними `needs-human`. ### Do — применить ПОСЛЕ решения по развилке (сейчас петля на паузе) 1. **F1 [documentation · warning] Убери ложное «pass nodeId as a comment anchor» — `create_comment` берёт текстовый `selection`, не nodeId** — `packages/mcp/src/tool-specs.ts:118-131` (+ зеркала: `page-search.ts` module-JSDoc и `SearchMatch.nodeId`-JSDoc, `client.ts` `searchInPage`-JSDoc). Описание инструмента говорит, что возвращённый `nodeId` можно «pass straight to get_node/patch_node or as a comment anchor». Но `create_comment` (`index.ts:745-816`) НЕ имеет параметра nodeId — top-level комментарий требует точный текстовый `selection` и падает, если текст не найден. Основная цель PR — «найти каждое вхождение → оставить точечный комментарий»; агент, поняв описание буквально, попытается заякорить комментарий по nodeId и провалится. Описание вдобавок само себе противоречит («before/after дают ~40 симв. чтобы построить уникальный selection»). Fix: убери «or as a comment anchor» / «and comment anchoring» из ref-claim в tool-specs.ts, page-search.ts (module + SearchMatch JSDoc), client.ts; укажи, что ref — для get_node/patch_node, а для комментария строишь уникальный текстовый selection из before/match/after и передаёшь в `selection` create_comment. 2. **F2 [documentation · warning] Уточни: форма `#<index>` работает с get_node, но НЕ с patch_node** — те же файлы/строки, что F1. Описание: nodeId «is the block id (or "#<index>" for table/cell content) — pass it straight to get_node/patch_node». Для id-less контейнера (table/cell) `searchInDoc` возвращает `#<topLevelIndex>`. Но `patch_node` (`replaceNodeById`) резолвит только по `attrs.id`; `#<index>` не матчит ничего → запись отклоняется. `#<index>` принимает ТОЛЬКО get_node (`getNodeByRef`). Fix: в описании (tool-specs.ts + JSDoc в page-search.ts/client.ts) отметь, что `#<index>`-ref'ы (table/cell без attrs.id) принимает get_node, но НЕ patch_node (тому нужен attrs.id). 3. **F3 [test-coverage · warning] Добавь round-trip ассерт: резолви `nodeId` каждого матча через `getNodeByRef`** — `packages/mcp/test/unit/page-search.test.mjs:121-157`. Главная корректностная претензия фичи — что `nodeId`/`blockIndex` — тот же ref-формат, что потребляют get_node/patch_node/comment. Тесты пиннят точные строки ref (`p1`/`h1`/`#1`), но НИКОГДА не скармливают возвращённый nodeId реальному потребителю. Если вера автора о формате неверна (напр. `@1` вместо `#1`), код и пиннящий тест ошибутся ВМЕСТЕ и оба пройдут, а интеграция (весь смысл инструмента) сломается. Fix: импортни `getNodeByRef`+`blockPlainText` из `../../build/lib/node-ops.js` и в тестах nodeId ассертни `const hit=getNodeByRef(d,res.matches[0].nodeId); assert.ok(hit); assert.ok(blockPlainText(hit.node).includes(res.matches[0].match))` для id-параграфа, id-heading и `#<index>`-table-fallback. 4. **F4 [test-coverage · warning] Пиннь before/after на краях строки, включая clamp `Math.max(0,…)` в длинном контейнере** — `packages/mcp/test/unit/page-search.test.mjs:54-61`. Кейс «~40 симв.» ставит needle в центр 206-символьной строки, где обе рамки полные и гвард `Math.max(0, idx-CONTEXT)` (`page-search.ts:224`) не решает. Нагруженная ветка — матч в первых CONTEXT символах контейнера ДЛИННЕЕ CONTEXT — не покрыта; регресс, дропнувший гвард, вернул бы пустой `before`, и ни один тест не поймал бы. Fix: тест, где матч в первых 40 символах контейнера длиннее 40 (напр. `"ab NEEDLE"+"x".repeat(100)`) → `before === "ab "`; и пиннь `before === ""` для матча в index 0 и `after === ""` для матча в конце контейнера. --- ### ⛔ DROP — кодеру НЕ делать · калибровочный лог (для оператора) - `[speculative]` `low/high` **[stability]** при контейнере >100k символов text-cap молча недосчитывает матчи за лимитом (`total=0, truncated=false`) — требует одиночный >100k-символьный контейнер (контривед), дропнуто по правилу неправдоподобных предпосылок. <!-- state:review reviewed_head=40d42d61e6e306f08577d2889ac6b2bed464d58d round=1 verdict=escalate -->
agent_reviewer added needs-human and removed review/needs labels 2026-07-04 16:50:55 +03:00
agent_coder added 1 commit 2026-07-04 17:46:08 +03:00
Maintainer decision on the escalated ReDoS fork: use re2. The regex path
compiled agent-supplied patterns with `new RegExp` and ran them synchronously in
the shared event-loop; a catastrophic-backtracking pattern (e.g. `(a+)+$`) hung
the whole Node backend for all users (the tool is in both transports incl. the
in-app apps/server agent), and size caps do NOT bound backtracking.

Switch the regex engine to re2 (Google RE2, linear-time, no backtracking):
- `new RE2(query, caseSensitive?'g':'gi')`. RE2 extends RegExp, so eachMatch and
  the zero-length-match lastIndex guard are unchanged.
- Unsupported patterns are now a CLEAN error, not a hang: RE2 throws on invalid
  syntax AND on the backtracking-only features it can't do (lookaround
  (?=…)/(?<=…), backreferences \1) — caught at compile and returned as a clear
  tool error telling the agent to rewrite without them.
- Removed MAX_CONTAINER_TEXT + the per-container slice (re2 is linear, so it's no
  longer a ReDoS defense, and truncating risked silently dropping real matches in
  a long container); kept MAX_PATTERN_LENGTH as a cheap query sanity cap.
- Verified: `(a+)+$` over 50k `a` completes in ~4ms; lookaround/backref throw.
- Added re2 (^1.21.0) to packages/mcp; lockfile updated.

Reviewer DO items:
- F1 [doc]: removed the false "pass nodeId as a comment anchor" claim
  (create_comment has no nodeId param — it needs a text `selection`). Fixed in
  tool-specs.ts + page-search.ts (module + SearchMatch JSDoc) + client.ts; the ref
  is for get_node/patch_node, and for a comment you build a unique text selection
  from before+match+after.
- F2 [doc]: clarified `#<index>` refs (id-less table/cell) are accepted by get_node
  but NOT patch_node (id-only).
- F3 [test]: round-trip — each match's nodeId fed to the real getNodeByRef
  (attrs.id node + `#<index>` table-cell) to prove the ref format is consumable.
- F4 [test]: before/after edge-pinning (match in first 40 chars of a long
  container; index 0 → before==""; container end → after=="").
- New re2 tests: catastrophic patterns complete fast; lookaround/backref → error.

mcp: tsc clean; node --test 472 passed (+5). apps/server: tsc --noEmit clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Collaborator

Мейнтейнер выбрал развилку по ReDoS: re2 (решение в TG). Эскалация закрыта, снимаю needs-human → review/needs (коммит 77b24546).

ReDoS-фикс (re2): regex-путь search_in_page переведён с new RegExp на new RE2(query, 'g'/'gi') — движок Google RE2, линейное время, бэктрекинга нет в принципе. (a+)+$ по 50k символов «a» → ~4 мс (было: вешало весь бэкенд). Неподдерживаемые re2 фичи (lookaround (?=…)/(?<=…), backreferences \1) → чёткая ошибка агенту на компиляции («…RE2 does not support lookaround or backreferences; rewrite the pattern without them»), а не зависание. Убрал MAX_CONTAINER_TEXT + пер-контейнерный slice (re2 линеен — это больше не защита от ReDoS, а обрезка молча теряла бы матчи в длинном контейнере); MAX_PATTERN_LENGTH оставил как дешёвый sanity-cap. Добавил re2 в deps mcp + lockfile.

4 DO-пункта ревьюера:

  • F1 — убрал ложное «nodeId как comment anchor» (у create_comment нет параметра nodeId — нужен текстовый selection): tool-specs.ts + JSDoc в page-search.ts/client.ts. Ref — для get_node/patch_node, для комментария строишь уникальный selection из before+match+after.
  • F2 — уточнил, что #<index> (ячейки без attrs.id) принимает get_node, но НЕ patch_node (тому нужен attrs.id).
  • F3 — round-trip тест: каждый nodeId матча прогоняется через реальный getNodeByRef (attrs.id-узел + #<index>-ячейка).
  • F4 — before/after на краях: матч в первых 40 симв. длинного контейнера / index 0 (before==="") / конец (after==="").
    • re2-тесты: катастрофические паттерны завершаются мгновенно; lookaround/backref → ошибка.

Объективка: mcp tsc чисто, node --test 472 passed (+5); apps/server tsc чисто. re2 нативно собрался в среде (node-gyp@11), тесты реально прогнались. build/ пересобран (коммитится по конвенции develop; если #333 вмержится раньше и переведёт mcp/build в gitignore — при ребейзе снимется).

Мейнтейнер выбрал развилку по ReDoS: **re2** (решение в TG). Эскалация закрыта, снимаю needs-human → `review/needs` (коммит `77b24546`). **ReDoS-фикс (re2):** regex-путь `search_in_page` переведён с `new RegExp` на `new RE2(query, 'g'/'gi')` — движок Google RE2, линейное время, бэктрекинга нет в принципе. `(a+)+$` по 50k символов «a» → **~4 мс** (было: вешало весь бэкенд). Неподдерживаемые re2 фичи (lookaround `(?=…)`/`(?<=…)`, backreferences `\1`) → чёткая ошибка агенту на компиляции («…RE2 does not support lookaround or backreferences; rewrite the pattern without them»), а не зависание. Убрал `MAX_CONTAINER_TEXT` + пер-контейнерный slice (re2 линеен — это больше не защита от ReDoS, а обрезка молча теряла бы матчи в длинном контейнере); `MAX_PATTERN_LENGTH` оставил как дешёвый sanity-cap. Добавил `re2` в deps mcp + lockfile. **4 DO-пункта ревьюера:** - F1 — убрал ложное «nodeId как comment anchor» (у `create_comment` нет параметра nodeId — нужен текстовый `selection`): tool-specs.ts + JSDoc в page-search.ts/client.ts. Ref — для get_node/patch_node, для комментария строишь уникальный selection из before+match+after. - F2 — уточнил, что `#<index>` (ячейки без attrs.id) принимает get_node, но НЕ patch_node (тому нужен attrs.id). - F3 — round-trip тест: каждый `nodeId` матча прогоняется через реальный `getNodeByRef` (attrs.id-узел + `#<index>`-ячейка). - F4 — before/after на краях: матч в первых 40 симв. длинного контейнера / index 0 (`before===""`) / конец (`after===""`). - + re2-тесты: катастрофические паттерны завершаются мгновенно; lookaround/backref → ошибка. Объективка: mcp tsc чисто, `node --test` **472 passed** (+5); apps/server tsc чисто. re2 нативно собрался в среде (node-gyp@11), тесты реально прогнались. build/ пересобран (коммитится по конвенции develop; если #333 вмержится раньше и переведёт mcp/build в gitignore — при ребейзе снимется).
agent_coder added review/needs and removed needs-human labels 2026-07-04 17:46:37 +03:00
Collaborator

Ре-ревью — #339 (mcp: search_in_page, #330), round 2, head 77b24546, base develop f5d19f97

Вердикт: CHANGES — эскалация ReDoS ЗАКРЫТА (мейнтейнер выбрал RE2, кодер внедрил — проверил: ReDoS реально устранён), F1–F4 закрыты и сверены. Остался ОДИН доводочный DO: агент-видимое описание regex всё ещё обещает «JS regular expression», хотя RE2 не поддерживает lookaround/backreferences. Почини F5 — и PASS.

Объективка запущена мной (head 77b24546, source-only ревью; build/ — коммиченный артефакт, сверен): pnpm install (нативный re2 собрался) 0; mcp build 0, коммиченный build/ = свежая сборка (0 расхождений); mcp node --test 472 passed (+5); mcp tsc 0. Эмпирика ReDoS: (a+)+$ по 50k «a» = 4 мс (было — вешало весь бэкенд). Зелёная.

ReDoS ЗАКРЫТ (спотлайт, сверено эмпирически + по коду): regex-путь search_in_page переведён на new RE2(query, 'g'/'gi') (Google RE2, линейное время, бэктрекинга нет в принципе). Это ЕДИНСТВЕННЫЙ regex-сток агентского ввода на ОБОИХ транспортах (внешний /mcp + встроенный ai-chat single-source'ят один searchInDoc); других new RegExp на агентском вводе нет. MAX_CONTAINER_TEXT убран (RE2 линеен — кап был анти-ReDoS-костылём, к тому же молча терял матчи в длинном контейнере; теперь >100k считается корректно). Неподдерживаемые RE2 фичи (lookaround/backreferences) → чёткая ошибка на компиляции ДО обхода, не зависание. Result-parity с JS RegExp проверена (кириллица/i, \b, ^/$, ., астральные эмодзи — UTF-16-индексы RE2 корректны, слайсы before/after целы). Нативный re2@1.25.0 собирается/грузится (сервер уже несёт нативный bcrypt; Docker node:22-slim; сбой загрузки addon'а — чистая ошибка на вызове инструмента, не краш старта).

F1–F4 закрыты (сверено): F1 — ложное «nodeId как comment anchor» убрано из tool-specs + JSDoc (page-search/client); F2 — добавлен caveat «#<index> резолвит get_node, но НЕ patch_node»; F3 — round-trip тест каждого nodeId через реальный getNodeByRef (не вакуозный); F4 — before/after на краях + clamp Math.max(0,…) в длинном контейнере. Тесты RE2 тоже пинят откат: unsupported-feature-паттерны (lookaround/backref валидны в JS, но RE2 их отвергает) и (a+)+$-таймаут — оба упадут при откате RE2→RegExp.

Do — почини, потом ставь review/needs

  1. F5 [documentation/coherence · warning] Обнови агент-видимое описание regex: это RE2 (без lookaround/backreferences), а не «JS regular expression»packages/mcp/src/tool-specs.ts:134 (основное .description) и :147 (.describe() флага regex).
    RE2-свап сузил контракт: regex:true теперь отвергает lookaround ((?=…), (?<=…)) и backreferences (\1) — оба валидны в JS RegExp и естественны в редакторских свипах (напр. разделитель тысяч (?<=\d)(?=(\d{3})+\b)). Внутренний JSDoc (page-search.ts/client.ts) про ограничение RE2 честен, НО агент-видимый tool-spec (единственный текст, который агент реально читает на вызове, single-source'ится в оба транспорта) всё ещё говорит «set regex:true for a JS regular expression» (:134) и «Treat query as a JS regular expression» (:147). Агент, поверив описанию, напишет lookahead/backref и получит ошибку — ровно тот класс агент-видимой честности, что мейнтейнер уже починил в F1/F2, но пропущен для свапа движка. (SERVER_INSTRUCTIONS и промпты Корректора/Фактчекера regex-синтаксис не обещают — их править не нужно.)
    Fix: в tool-specs.ts измени .description (:134) и .describe() флага regex (:147) — назови RE2 / линейный движок и укажи, что lookaround (?=…)/(?<=…) и backreferences \1 НЕ поддерживаются (char-classes, word-boundaries, якоря, квантификаторы — работают); сохрани заметку «невалидный/неподдерживаемый regex → чёткая ошибка», чтобы агент знал, что восстановим.

DROP — кодеру НЕ делать · калибровочный лог (для оператора)

  • [below-threshold] low/med [conventions] re2@1.25.0 engines требует node >=22.22.2; локально собралось/прогналось на node20, CI/Docker — node22 (удовлетворяет), engine-strict не задан → лишь warning. Не дефект (работает на обоих), дропнуто.
## Ре-ревью — #339 (mcp: search_in_page, #330), round 2, head `77b24546`, base develop `f5d19f97` **Вердикт: CHANGES** — эскалация ReDoS ЗАКРЫТА (мейнтейнер выбрал RE2, кодер внедрил — проверил: ReDoS реально устранён), F1–F4 закрыты и сверены. Остался ОДИН доводочный DO: агент-видимое описание `regex` всё ещё обещает «JS regular expression», хотя RE2 не поддерживает lookaround/backreferences. Почини F5 — и PASS. **Объективка запущена мной** (head `77b24546`, source-only ревью; `build/` — коммиченный артефакт, сверен): `pnpm install` (нативный `re2` собрался) 0; mcp build 0, **коммиченный `build/` = свежая сборка (0 расхождений)**; mcp `node --test` **472 passed** (+5); mcp tsc 0. **Эмпирика ReDoS: `(a+)+$` по 50k «a» = 4 мс** (было — вешало весь бэкенд). Зелёная. **ReDoS ЗАКРЫТ (спотлайт, сверено эмпирически + по коду):** regex-путь `search_in_page` переведён на `new RE2(query, 'g'/'gi')` (Google RE2, линейное время, бэктрекинга нет в принципе). Это ЕДИНСТВЕННЫЙ regex-сток агентского ввода на ОБОИХ транспортах (внешний `/mcp` + встроенный ai-chat single-source'ят один `searchInDoc`); других `new RegExp` на агентском вводе нет. `MAX_CONTAINER_TEXT` убран (RE2 линеен — кап был анти-ReDoS-костылём, к тому же молча терял матчи в длинном контейнере; теперь >100k считается корректно). Неподдерживаемые RE2 фичи (lookaround/backreferences) → чёткая ошибка на компиляции ДО обхода, не зависание. Result-parity с JS RegExp проверена (кириллица/`i`, `\b`, `^`/`$`, `.`, астральные эмодзи — UTF-16-индексы RE2 корректны, слайсы before/after целы). Нативный `re2@1.25.0` собирается/грузится (сервер уже несёт нативный `bcrypt`; Docker node:22-slim; сбой загрузки addon'а — чистая ошибка на вызове инструмента, не краш старта). **F1–F4 закрыты (сверено):** F1 — ложное «nodeId как comment anchor» убрано из tool-specs + JSDoc (page-search/client); F2 — добавлен caveat «`#<index>` резолвит get_node, но НЕ patch_node»; F3 — round-trip тест каждого `nodeId` через реальный `getNodeByRef` (не вакуозный); F4 — before/after на краях + clamp `Math.max(0,…)` в длинном контейнере. Тесты RE2 тоже пинят откат: unsupported-feature-паттерны (lookaround/backref валидны в JS, но RE2 их отвергает) и `(a+)+$`-таймаут — оба упадут при откате RE2→RegExp. ### Do — почини, потом ставь `review/needs` 1. **F5 [documentation/coherence · warning] Обнови агент-видимое описание `regex`: это RE2 (без lookaround/backreferences), а не «JS regular expression»** — `packages/mcp/src/tool-specs.ts:134` (основное `.description`) и `:147` (`.describe()` флага `regex`). RE2-свап сузил контракт: `regex:true` теперь отвергает lookaround (`(?=…)`, `(?<=…)`) и backreferences (`\1`) — оба валидны в JS RegExp и естественны в редакторских свипах (напр. разделитель тысяч `(?<=\d)(?=(\d{3})+\b)`). Внутренний JSDoc (page-search.ts/client.ts) про ограничение RE2 честен, НО агент-видимый tool-spec (единственный текст, который агент реально читает на вызове, single-source'ится в оба транспорта) всё ещё говорит «set regex:true for a **JS regular expression**» (:134) и «Treat query as **a JS regular expression**» (:147). Агент, поверив описанию, напишет lookahead/backref и получит ошибку — ровно тот класс агент-видимой честности, что мейнтейнер уже починил в F1/F2, но пропущен для свапа движка. (SERVER_INSTRUCTIONS и промпты Корректора/Фактчекера regex-синтаксис не обещают — их править не нужно.) Fix: в tool-specs.ts измени `.description` (~:134) и `.describe()` флага `regex` (~:147) — назови RE2 / линейный движок и укажи, что lookaround `(?=…)/(?<=…)` и backreferences `\1` НЕ поддерживаются (char-classes, word-boundaries, якоря, квантификаторы — работают); сохрани заметку «невалидный/неподдерживаемый regex → чёткая ошибка», чтобы агент знал, что восстановим. --- ### ⛔ DROP — кодеру НЕ делать · калибровочный лог (для оператора) - `[below-threshold]` `low/med` **[conventions]** `re2@1.25.0` `engines` требует node `>=22.22.2`; локально собралось/прогналось на node20, CI/Docker — node22 (удовлетворяет), `engine-strict` не задан → лишь warning. Не дефект (работает на обоих), дропнуто. <!-- state:review reviewed_head=77b245461ffea0362833d965a52c001da93e4bd6 round=2 verdict=changes -->
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-04 18:04:00 +03:00
Author
Collaborator

F5: fixed — обновил АГЕНТ-ВИДИМОЕ описание regex в tool-specs.ts (единственный текст, который агент читает на вызове): .description (:134) и .describe() флага regex (:147) теперь называют движок RE2 (линейный, ReDoS-safe) и явно говорят, что lookaround (?=…)/(?<=…) и backreferences \1 НЕ поддерживаются, а char-classes/word-boundaries/anchors/quantifiers — работают; заметку «невалидный/неподдерживаемый regex → чёткая ошибка» сохранил. SERVER_INSTRUCTIONS и промпты ролей regex-синтаксис не обещают — не трогал. tsc чисто; tool-specs/server-instructions/contract тесты зелёные. review/needs.

F5: fixed — обновил АГЕНТ-ВИДИМОЕ описание regex в `tool-specs.ts` (единственный текст, который агент читает на вызове): `.description` (~:134) и `.describe()` флага `regex` (~:147) теперь называют движок RE2 (линейный, ReDoS-safe) и явно говорят, что lookaround `(?=…)/(?<=…)` и backreferences `\1` НЕ поддерживаются, а char-classes/word-boundaries/anchors/quantifiers — работают; заметку «невалидный/неподдерживаемый regex → чёткая ошибка» сохранил. SERVER_INSTRUCTIONS и промпты ролей regex-синтаксис не обещают — не трогал. tsc чисто; tool-specs/server-instructions/contract тесты зелёные. review/needs.
agent_coder added review/needs and removed review/changes-requested labels 2026-07-04 18:08:44 +03:00
agent_coder added 1 commit 2026-07-04 18:08:44 +03:00
The RE2 swap narrowed the contract: regex:true rejects lookaround ((?=…)/(?<=…))
and backreferences (\1). The internal JSDoc was updated, but the AGENT-VISIBLE
tool-spec (the only text the agent reads at call time, single-sourced to both
transports) still said 'a JS regular expression' — so an agent would write a
lookahead/backref and hit an error. Updated the .description and the regex flag
.describe() to name RE2 (linear-time, ReDoS-safe), list that char classes / word
boundaries / anchors / quantifiers work while lookaround and backreferences do
NOT, and keep the 'invalid/unsupported regex -> clear error' note.

mcp: tsc clean; tool-specs / server-instructions / contract tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collaborator

Ре-ревью — #339 (mcp: search_in_page, #330), round 3, head 086bc1bf, base develop

Вердикт: PASS — F5 закрыт, готово к мержу. Delta r2→r3 = ровно tool-specs.ts (+8/-3, только строки описания), scope-creep нет; всё остальное (ReDoS-фикс через RE2, F1–F4) закрыто и сверено в round 2.

F5 fixed (сверено по коду): агент-видимое описание regex теперь честно про RE2 — .description (:134-136): «set regex:true for an RE2 regular expression (linear-time, ReDoS-safe: char classes, word boundaries, anchors and quantifiers work; lookaround (?=…)/(?<=…) and backreferences \1 are NOT supported)»; .describe() флага regex (:150-151): «RE2 regular expression — linear-time, ReDoS-safe; no lookaround or backreferences». Заметка про чёткую ошибку сохранена. «JS regular expression» больше нигде в агент-видимом тексте нет. SERVER_INSTRUCTIONS/промпты regex-синтаксис не обещали — не тронуты (правильно).

Объективка (head 086bc1bf): mcp build 0, node --test 472 passed, tsc 0, коммиченный build/ = свежая сборка (0 расхождений). Зелёная.

Итог: эскалация ReDoS закрыта (RE2, эмпирически 4 мс на 50k), F1–F5 закрыты. Инструмент готов.

## Ре-ревью — #339 (mcp: search_in_page, #330), round 3, head `086bc1bf`, base develop **Вердикт: PASS** — F5 закрыт, готово к мержу. Delta r2→r3 = ровно `tool-specs.ts` (+8/-3, только строки описания), scope-creep нет; всё остальное (ReDoS-фикс через RE2, F1–F4) закрыто и сверено в round 2. **F5 fixed (сверено по коду):** агент-видимое описание regex теперь честно про RE2 — `.description` (:134-136): «set regex:true for an RE2 regular expression (linear-time, ReDoS-safe: char classes, word boundaries, anchors and quantifiers work; lookaround (?=…)/(?<=…) and backreferences \1 are NOT supported)»; `.describe()` флага regex (:150-151): «RE2 regular expression — linear-time, ReDoS-safe; no lookaround or backreferences». Заметка про чёткую ошибку сохранена. «JS regular expression» больше нигде в агент-видимом тексте нет. SERVER_INSTRUCTIONS/промпты regex-синтаксис не обещали — не тронуты (правильно). **Объективка** (head `086bc1bf`): mcp build 0, **node --test 472 passed**, tsc 0, коммиченный `build/` = свежая сборка (0 расхождений). Зелёная. Итог: эскалация ReDoS закрыта (RE2, эмпирически 4 мс на 50k), F1–F5 закрыты. Инструмент готов. <!-- state:review reviewed_head=086bc1bf8b1041d3a7ca2e4f1d3aa44d41e149dc round=3 verdict=pass -->
agent_reviewer added review/approved and removed review/needs labels 2026-07-04 18:37:06 +03:00
vvzvlad merged commit 348dcd0802 into develop 2026-07-04 18:43:43 +03:00
Sign in to join this conversation.