bc433d12e6db2c065011ea4089a8ee6feffcbe05
37 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bc433d12e6 |
fix(mcp): ревью #513 — гонка LRU-эвикции + доводки контракта/доков
WARNING [stability]: эвикция могла убить ЧУЖУЮ ещё-connecting сессию как idle-жертву — isBusy()=false во время open(), сессия вставлена в мапу до await open(); параллельный acquire на ДРУГУЮ страницу при насыщении выселял её → спурьёзный reject не-начатой записи / старвейшн новых записей. Фикс: idle-victim скан теперь — connecting падает в last-resort oldestBusy, честный idle по-прежнему предпочитается, кап держится, коалесинг того же ключа (per-page lock) не задет. Тест на интерливинг (connecting не выселяется под насыщением), mutation-verified. Доводки (проза, логику не меняют): drawioCreate description (nodeId:null на nested-вставке); forward-коммент у writeWithCollabAuthRetry (ретрай только auth; при расширении — проверять isCollabIndeterminateError, #435); хедер comment-anchor (все 4 точки делегируют в resolveAnchorSelection); CHANGELOG. Ребейзнут на develop (волна 1 смержена): только 4 коммита #494. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e56a05926d |
fix(mcp): registration-time assert — каждый не-inline спек регистрируем (#494)
Коммит 1. Спек без execute падал по-разному на двух хостах: MCP-хост (index.ts) в цикле регистрации делает `mcpExecute` иначе `spec.execute!` — без execute это TypeError в момент ВЫЗОВА тула (то есть в проде, только когда модель выберет именно этот тул); in-app-хост (ai-chat-tools.service.ts) делает `inAppExecute ?? execute`, затем `if (!run) continue` — то есть МОЛЧА роняет тул, он просто исчезает у агента без единой ошибки. Комментарий «mirror this» гардом не считается: закрываем зеркало настоящим структурным assert'ом. `assertEverySpecIsRegisterable()` гоняется при загрузке модуля tool-specs на ОБОИХ хостах (оба его импортируют) и кидает исключение, если не-inline спек, который хост регистрирует, не несёт исполнителя для этого хоста — латентный рантайм-TypeError / тихий дроп превращается в громкий отказ на старте. `inlineBothHosts` освобождён (оба хоста регистрируют его inline, execute у него намеренно нет); `inAppOnly`/`mcpOnly` проверяются только для своего хоста. Тест по реестру с мутационной проверкой: синтетические плохие реестры (без execute; inAppOnly без inAppExecute) обязаны кидать, а mcpExecute-only / inAppExecute-only / inlineBothHosts — проходить. Часть (б) — assert объявления write-класса — уже приземлилась в #489. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1b05224b27 |
fix(ai-chat): MCP-кэш — in-run восстановление, ретраи только для readOnly (#489)
Кэш внешних MCP-клиентов не делал health-check/reconnect-on-error: после
разрыва SSE-транспорта (bodyTimeout режет при тишине МЕЖДУ вызовами) отдавал
труп до конца TTL, а модель жгла шаги на ретраях ВНУТРИ текущего рана.
- Новое поле writeClass ('readOnly'|'write') на КАЖДОМ SHARED_TOOL_SPEC +
registration-time assert (и compile-time через satisfies). Все 48 спеков
расклассифицированы: чтения → readOnly, любая мутация страницы/коммента/шэра/
диаграммы → write. Экспортированы SHARED_TOOL_WRITE_CLASS + isRetryableWriteClass.
- Per-run обёртка восстановления транспорта: при транспортной ошибке readOnly-тул
реконнектит свой сервер и ретраит РОВНО 1 раз ВНУТРИ рана; write-тул НЕ
авторетраится (indeterminate — «могло примениться, проверь», класс инцидента
#435). CAS-своп байндинга по identity (проигравший конкурентный вызов ретраит
на текущем клиенте, не минтит второй). Лизы не освобождаются mid-run — ран
копит set (старая+новая) и релизит на turn-end.
- Проверка abortSignal ПЕРЕД ретраем И ПЕРЕД чеканкой свежего клиента; per-call
cap покрывает оба attempt'а + connect.
- Классификация транспортной ошибки по РЕАЛЬНЫМ шейпам undici (SocketError/
BodyTimeoutError, cause-цепочка), не по мок-ошибкам.
- Отдельный, поднятый bodyTimeout для SSE-транспорта MCP (тишина между вызовами
легальна) — DEFAULT 10 мин, AI_MCP_SSE_BODY_TIMEOUT_MS.
write-class map грузится в mcp-clients лениво через dynamic import (пакет ESM),
type-only импорт — без static require ESM из commonjs.
Тесты на РЕАЛЬНЫХ error-шейпах: «повтор после обрыва ВНУТРИ рана получает живой
клиент», «write-тул не авторетраится», «ретрай после Stop не происходит»,
+ writeClass-контракт в mcp node --test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
fdc37de3e8 |
fix(ai-chat): ревью-раунд #500 — красный серверный сьют + регрессия Redis-health
B1: переименование строки ошибки (#394) не обновило 4 предсуществующих share-ассерта → полный серверный сьют был красный. Обновлены ассерты в public-share-chat.spec.ts и public-share-chat-tools.service.spec.ts под новую классифицированную строку. B2: /health первая проба после старта врала DOWN при живом Redis (lazyConnect + enableOfflineQueue:false + maxRetriesPerRequest:1 → первый ping до открытия сокета). Добавлен ensureConnected() с bounded-таймаутом перед первым ping; покрывает и путь пересоздания после onModuleDestroy. Тест UP-с-первой-пробы против реального ioredis (mutation-verified). Устранён open-handle leak в redis.health.spec (drain ioredis force-destroy-таймера в teardown; без forceExit) и окно ложного DOWN при конкурентных пробах (мемоизированный connectingPromise). B3: комментарий про orphan-чат при провале beginRun (insert до begin). B4: описание listPages упоминает поле truncated в tree-режиме. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2a951df096 |
feat(mcp): drawio стадия 3 — семантические тулы drawioFromGraph/FromMermaid/EditCells (#425)
Сырой XML остаётся escape-hatch, но для 90% случаев модель не видит ни координат, ни style-строк — весь класс ошибок лейаута и иконок уходит by construction: эти решения принимает сервер, а не LLM. Стоит на #443, переиспользует конвейер #423 и shape-index/elkjs/линтер #424. - drawioFromGraph(pageId, where, graph, direction?, preset?, layout?): граф узлов/групп/связей → резолв иконок (shape-index #424; неизвестная → generic по kind с подписью, не пустой квадрат), стили из пресета (kind→палитра), elkjs-layered с compound-группами, ассемблер XML (линтер-чистый by construction: зазоры >=150, прозрачные контейнеры, относительные координаты детей, cross-container рёбра parent=1, эскейп меток). Хинты pinned/sameLayerAs/layer и layout none/full/incremental — детерминированным post-pass'ом (ELK-констрейнты оказались ненадёжны). Пресеты default/dark/colorblind-safe (Okabe-Ito) — данные. - drawioFromMermaid(pageId, where, mermaid): чистый парсер flowchart (без браузера/CLI) → graph → тот же конвейер. Формы/направление/пунктир=async/ subgraph→группы/цепочки; не-flowchart отвергает внятно. - drawioEditCells(pageId, node, operations, baseHash): ID-based add/update/delete, delete каскадит на детей контейнера и связанные рёбра; baseHash обязателен (optimistic lock как drawioUpdate); сентинелы 0/1 от delete защищены. DoS-границы (LLM-вход): MAX_GRAPH_EDGES=1000/GROUPS=500 в validateGraph (узлы уже 500), mermaid MAX_CHARS=200k/LINES=20k/GROUPS=500, chain 500 — все с быстрым throw ДО лейаута/ассемблера (ассемблер и маппер вне ELK-таймаута, иначе OOM воркера). incremental сохраняет неперечисленные существующие ячейки (mergeExisting Cells) — «добавь узел» не стирает ручную расстановку. sameLayerAs/layer после снапа раскладываются по перпендикулярной оси с зазором >=150 → 0 quality-warnings; pinned — точные пользовательские координаты (clamp>=0, могут дать warning, гарантия «0 by construction» относится к авто-лейауту). Регистрация: 3 shared-spec на оба хоста (camelCase, execute-in-spec, inlineBoth Hosts не понадобился), DocmostClientLike/Method += 3, contract, routing-проза (fromGraph→архитектуры/облака, fromMermaid→стандартные flowchart, raw xml→экзотика). Тесты: mcp node --test 782/782 (57 новых) — приёмка (15+ узлов/2 вложенные группы/ AWS-иконки→0 lint/0 warnings/иконки резолвятся, hints, incremental без сдвига, edit_cells update/cascade/stale-baseHash, снапшоты пресетов + colorblind-safe, mermaid ветвление+subgraph) + регрессии на DoS-границы. tsc чисто; server jest (contract + ai-chat) 290/290. closes #425. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5dc7a2703f |
feat(mcp): getPageContext — «где я / что вокруг» по pageId одним вызовом (#443, часть 3/3)
Финальная из трёх частей #443. Есть pageId (из поиска, из ссылки) — нужно понять местоположение и окружение. Раньше это get_page + полное дерево. Только чтение, только метаданные (без контента). closes #443. - client.ts getPageContext(pageId): resolvePageId (slugId→uuid, для uuid без round-trip) → POST /pages/breadcrumbs + listSidebarPages(spaceId, uuid). Два запроса для uuid-входа (spaceId берётся из ответа breadcrumbs), +1 для slugId. Выход {page:{pageId,title,spaceId}, breadcrumbs:[{pageId,title}] (root→parent; [] для корня), children:[{pageId,title,hasChildren}]}. - Порядок breadcrumbs выверен по серверному источнику (getPageBreadCrumbs: CTE сидится на childPageId, идёт вверх, .reverse() → root→page; страница — ПОСЛЕДНИЙ элемент). page = chain[last], breadcrumbs = chain.slice(0,-1); корень → []. Проекция явная — наружу только pageId (UUID), slugId/icon/position не утекают. Пустой chain / 404 / 403 → явная ошибка, не пустой объект. - children — прямые дети через listSidebarPages (cursor-пагинация #442/#451, страница с >20 детьми отдаёт всех без дублей, порядок по position, hasChildren). - Общий реестр: getPageContext на ОБОИХ хостах через цикл спеков (mcpName=== inAppKey==="getPageContext", camelCase); DocmostClientLike/DocmostClientMethod += it; compile-time contract; TOOL_FAMILY READ + routing-проза. Тесты: get-page-context.test.mjs +6 (3-й уровень split, no-slugId-leak + 2 запроса, корень→[], slugId-resolve, >20 детей без cap/дублей, плохой id→ошибка, пустой chain→throw), tool-specs +1. mcp node --test 725/725, tsc чисто; server jest (contract + ai-chat) 265/265. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
bfb4c8d8d0 |
feat(mcp): getTree — иерархия пространства/поддерева одним вызовом (#443, часть 2/3)
Раньше единственный способ получить дерево — listPages tree:true: BFS по sidebar-эндпоинту, десятки-сотни HTTP-вызовов и молчаливая потеря страниц (#442). Бэкенд форка уже отдаёт всё одним POST /pages/tree (getSidebarPagesTree, merged #442/#451), так что это MCP-сторона + общий реестр. Только чтение. - lib/tree.ts: buildPageTree(nodes) аддитивно расширен до buildPageTree(nodes, options?) с {shape?: "lean"|"getTree"; maxDepth?}. Дефолт/{} — байт-идентичный lean {id,slugId,title,children?} (существующие вызыватели listPages tree:true и subtree-BFS не тронуты, есть регрессионный тест на точный набор полей). shape:"getTree" проецирует {pageId, title, children?, hasChildren?} (id→pageId; slugId/icon/position/parentPageId не утекают ни на одной глубине). - client.ts: getTree(spaceId, rootPageId?, maxDepth?) — тот же код-путь, что listPages tree:true: один enumerateSpacePages(spaceId, rootPageId) (единичный /pages/tree + cursor-BFS фолбэк для stock upstream) → buildPageTree(pages, {shape:"getTree", maxDepth}). listPages tree:true помечен DEPRECATED в JSDoc (BFS-фолбэк на месте, поведение не тронуто). - maxDepth/hasChildren: полное дерево строится, потом обрезается. Корни = глубина 1; maxDepth:1 → только корни; hasChildren:true ТОЛЬКО на срезанном узле с серверным hasChildren (source of truth), опущен у листьев и раскрытых узлов. Схема тула min(1) отбраковывает 0/отрицательные. - tool-specs.ts: shared-спек getTree (оба хоста через цикл реестра), DocmostClientLike Pick += getTree, listPages desc/catalogLine — про депрекацию. server-instructions.ts: getTree:"READ" + routing-проза. Loader DocmostClientMethod += getTree, compile-time client-call contract += getTree. Циклы/self-ref безопасны: project рекурсирует только от корней, циклический компонент недостижим из корней (нет бесконечной рекурсии). Orphan (родитель отфильтрован пермишенами) всплывает как корень. Тесты: tree.test.mjs +9 (nesting+порядок+no-leak, maxDepth:1/2 + hasChildren, orphan→root, seeded rootPageId, ≤0/нефинитный = без среза, lean-байт-идентичность), tool-specs.test.mjs (схема getTree + депрекация listPages). mcp node --test 718/718, tsc чисто; server jest (shared-tool-specs.contract + ai-chat) 263/263. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4be4a75fa3 |
docs(mcp): точные описания потерь getPage/exportPageMarkdown — конвертер канонический (#415)
Описания двух тулов устарели после #345 (канонический конвертер, #293/#351): «LOSSY…approximated» у getPage и «lossless» у exportPageMarkdown искажали роутинг агентов и молчаливо скрывали реальную потерю данных. Четвёртый линк breaking-окна, стоит на #413. - tool-specs.ts: getPage — вместо «LOSSY…approximated» точный закрытый список потерь (canonical for text; теряет лишь id блоков, resolved-якоря комментариев и фиксированный набор ACCEPTED-атрибутов без markdown- представления: спаны/colwidth/фон ячеек таблиц, indent, callout.icon, orderedList.type, link internal/target/rel/class). exportPageMarkdown — убрано «lossless»: round-trip перегенерирует id блоков и молча отбрасывает тот же набор (в первую очередь merge-спаны ячеек); держать в page-JSON, если нужны. Список выведен из источника истины (ATTR_VALUE_FUZZ_ALLOWLIST + MARK_ATTR_ALLOWLIST); opaque carried-verbatim токены (attachmentId, mime, slugId и пр.) намеренно НЕ в списке потерь — они round-trip'ятся. - server-instructions.ts: READ-строка ROUTING_PROSE приведена к тому же точному списку, без противоречия markdown-default роутингу #413. - Исправлена вторая латентная неточность: getPage описывался как сохраняющий resolved-якоря — но client.ts передаёт dropResolvedCommentAnchors: true, resolved-якоря скрыты (getNode, наоборот, их сохраняет — другой тул). - README пакета (EN + RU-зеркало) приведены в соответствие passage-for-passage: убраны безоговорочные «lossless/lossy» для Markdown round-trip; genuinely- lossless ссылки на raw-JSON (getPageJson/getNode) оставлены. Логику не трогает. server-instructions.test.mjs удалён и заменён tool-inventory.test.mjs (без пинов на старую формулировку). mcp tsc чисто, node --test 702/702, tool-inventory 5/5. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d269bd9efe |
feat(mcp): markdown — формат по умолчанию для блочных getNode/patchNode/insertNode (#413)
Канонический конвертер (#293/#345/#351) зрел для блочного уровня: markdown становится дефолтом чтения/записи ОДНОГО блока, PM JSON — опция «для тонких работ». Новых тулов нет, поверхность не растёт. Имена уже camelCase (стоит на #412). - getNode(pageId, nodeId, format='markdown'): дефолт markdown — обёртка {type:doc,content:[node]} → convertProseMirrorToMarkdown, comment-якоря (ВКЛЮЧАЯ resolved) сохранены (это чтение под редактирование, не getPage). format:'json' — сырой сабтри. Авто-фолбэк не-топ-левел типов (tableRow/Cell/ Header по #<index>) в JSON через canBeDocChild = docmostSchema.nodes.doc. contentMatch.matchType (не рукописный список); поле format на каждом ответе. - patchNode/insertNode: XOR-вход {markdown?|node?} (оба optional в схеме, XOR на рантайме). markdown → импорт фрагмента → 1→N сплайс: первый блок наследует id цели, остальные свежие; dry replaceNodeById для #159-ambiguity ДО сплайса; соседние блоки byte-identical. Guard findUnrepresentableTableAttrs: цель со span/colwidth/backgroundColor → отказ с указанием на table-тулы/node-JSON. insertNode — insertNodesRelative (N блоков по порядку); голый tableRow/Cell JSON-only. - Сноски: ^[...] во фрагменте → канон-импортёр; importMarkdownFragment делит блоки от footnotesList, РЕМАПИТ id сносок фрагмента в свежие uuid (fn-1 фрагмента не коллизит с fn-1 страницы), mergeFootnoteDefinitions добавляет через ту же машинерию appendDefinition→normalizeAndMergeFootnotes→ canonicalizeFootnotes, что insertFootnote. Сырые JSON-пути не тронуты. - node-ops: replaceNodeByIdWithMany/insertNodesRelative (сплайс массива) в prosemirror-markdown/node-ops (канон после #414) + barrel. - ROUTING_PROSE (READ/EDIT), compile-time client-call contract, CHANGELOG (getNode default→markdown, breaking для внешних клиентов, в окне #411/#412). Тесты: сходимость (patchNode(markdown) блок docsCanonicallyEqual полному импорту — нет «второго канона»); id-нить 1→N (первый наследует, остальные свежие, соседи byte-identical); XOR; getNode markdown/json/non-top-level-fallback; сохранение comment-якорей (active+resolved); ^[...]→хвостовой список+перенумерация; guard. mcp node --test 697/697; pmd vitest 736; tsc чисто; server jest 273. Третий линк breaking-окна, стоит на #412 (#411→#412→ЭТОТ→#415). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7cb3199d09 |
refactor(mcp)!: BREAKING — все имена MCP-тулов snake_case → camelCase (унификация с in-app) (#412)
Один логический тул жил под двумя именами: внешний MCP snake_case (edit_page_text), in-app camelCase (editPageText) — дублирование доков, путаница при переносе промптов/скиллов, помеха шарингу спек (#294). Решение владельца: единый camelCase везде, включая внешний MCP. После этого mcpName === inAppKey. - tool-specs.ts: mcpName ВЫВЕДЕН из ключа спеки (mcpName == inAppKey) для всех 43 shared-спек — раньше divergent snake, теперь равен ключу (проверено: mcpName читается только структурно — цикл регистрации, генератор <tool_inventory>, TOOL_FAMILY). +5 inline-регистраций (tableGet/updateComment/deleteComment/ docmostTransform; search без изменений). Рантайм: 47 тулов, все camelCase, ноль подчёркиваний. - Контракт-конвенция ИНВЕРТИРОВАНА: shared-tool-specs.contract.spec `mcpName === toSnake(inAppKey)` → `mcpName === inAppKey`; tool-specs.test и tool-inventory.test обновлены. - ROUTING_PROSE/TOOL_FAMILY/INLINE_MCP_INVENTORY (server-instructions.ts) → camelCase (105 замен). ai-chat.prompt/guard уже на in-app camelCase-ключах — без изменений (guard прошёл). comment-signal EXCLUDED_TOOLS схлопнут с дублей snake+camel до camelCase. - Некоторое неочевидное: assertUnambiguousMatch(op: "patch_node"|"delete_node") в prosemirror-markdown/node-ops — op интерполируется в model-facing ошибку; литерал-юнион + call-sites → "patchNode"|"deleteNode". - Все snake-имена в описаниях/error-строках/комментах/тестах/доках → camelCase (whole-token, longest-match-first). CHANGELOG: BREAKING-таблица 46 строк + миграция (allowlists mcp__gitmost-*__get_node→__getNode, промпты/скиллы, .mcp.json, метрики по tool-label); релизится вместе с #411. Внутренние имена методов (PageService.updatePageContent и т.п.) НЕ тронуты — переименованы только ИМЕНА ТУЛОВ. Гейт: mcp node --test 677/677; tsc -p apps/server чисто; jest ai-chat-tools. service + shared-tool-specs.contract + tool-tiers + ai-chat.prompt + comment-signal-inapp → 323. Второй линк breaking-окна (#411→ЭТОТ→#413→#415). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3a521ada4d |
refactor(tools): updatePageContent → updatePageMarkdown; внешний MCP: +updatePageMarkdown, −import_page_markdown (#411)
Поверхности записи «целым телом» были несимметричны: у in-app агента полная
замена тела markdown называлась updatePageContent (имя не про формат, тогда как
парный updatePageJson — про JSON), а у внешнего MCP голого plain-body-replace
не было вовсе (только import_page_markdown — на деле парсер round-trip к
export_page_markdown, не plain-replace). Пара должна быть updatePageMarkdown /
updatePageJson.
Пост-Фаза-1б архитектура (реестр + циклы по обоим хостам):
- новая shared-спека updatePageMarkdown (mcpName update_page_markdown, inAppKey
updatePageMarkdown, tier как у updatePageJson) с execute (client, {pageId,
content, title}) => client.updatePage(...) — тот же путь updatePageContentRealtime
→ markdownToProseMirrorCanonical, ^[...]-сноски парсятся. Реестровый цикл
регистрирует её на ОБОИХ хостах автоматически. Добавлен 'updatePage' в
Pick DocmostClientLike.
- import_page_markdown убран с внешнего MCP через inAppOnly:true у спеки
importPageMarkdown — MCP-цикл и генератор инвентаря её пропускают, in-app
агент сохраняет importPageMarkdown; спека и client-метод НЕ удалены.
- удалён inline in-app updatePageContent tool (теперь из реестра под inAppKey
updatePageMarkdown) + его INLINE_TOOL_TIERS-энтри.
- ROUTING_PROSE: bulk-rewrite ссылается на update_page_markdown|update_page_json;
убрано упоминание import_page_markdown; инвентарь генерируется из catalogLine.
- лейбл-мапы chat-markdown.util (en/ru), человекочитаемые метки не тронуты.
- НЕ тронуты одноимённые внутренности: PageService.updatePageContent,
updatePageContentRealtime, collaboration.handler — переименовано только имя тула.
Тесты: updatePageMarkdown на обеих поверхностях с идентичной схемой, forward в
client.updatePage; import_page_markdown ОТСУТСТВУЕТ на MCP, присутствует in-app;
^[...]→сноски покрыт через collaboration.test. CHANGELOG BREAKING + миграция;
README/README.ru пакета обновлены. Гейт: mcp node --test 646/646, server jest
259, tsc чисто. Первый линк breaking-окна #416 (#411→#412→#413→#415).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
ddb37376a4 |
refactor(mcp): вписать drawio-тулы в реестровый цикл (Фаза 1б смержена) (#440)
Фаза 1б (#445/#446/#447/#448) влилась ПЕРЕД этим PR, поэтому явная проводка drawio через registerShared/sharedTool конфликтовала с реестровыми циклами. Фолд: - drawioGet/Create/Update → канонический execute в спеке (это client-методы): execute возвращает сырой результат, цикл оборачивает jsonContent на MCP и отдаёт как есть in-app — байт-в-байт как старые тела, overrides не нужны. layout сохранён: добавлен в buildShape create/update + 5-м аргументом (layout as 'elk'|undefined) в обоих execute. - drawioShapes/drawioGuide → остаются inline на ОБОИХ хостах. Гайд архитектора «execute в спеке с импортом searchShapes/getGuideSection в tool-specs.ts» оказался невозможен: drawio-shapes.ts использует import.meta.url, а tool-specs.ts тайпчекается из исходника под module:commonjs (contract-спека) → TS1343 на статический value-import, а import.meta не индиректится. Введён флаг спеки inlineBothHosts: спеки остаются в SHARED_TOOL_SPECS (contract пинит имя/описание/схему), но без execute; ОБА цикла их пропускают (добавлен симметричный guard в MCP-цикл), каждый хост регистрирует их inline через чистые хелперы — поведение байт-в-байт как до ребейза. - docmost-client.loader: взята develop-форма DocmostClientLike = Pick< DocmostClient> (#446); ручное зеркало убрано, паритет layout наследуется из реальной сигнатуры client. - ROUTING_PROSE: drawio intent-подсказки (shapes-first, guide, layout:elk); инвентарные строки убраны (генерируются из catalogLine). Гейт: mcp node --test 674/674; server jest ai-chat-tools.service + contract (211, все 5 drawio на обоих хостах, идентичная схема) + tool-tiers → 264; tsc чисто; layout-passthrough тест 3/3. ELK DoS-кап и layout-фикс из round 3/4 сохранены. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e454fe189c |
feat(mcp): drawio стадия 2 — правила качества, каталог фигур, guide, ELK-лейаут, warnings (#424)
Надстройка над стадией-1 (сырой mxGraph XML) — помогает агенту рисовать
корректные диаграммы без бэкенд-рендеринга:
- hard-rules в описаниях drawio_create/drawio_update (геометрия, parent-
relative координаты, стили);
- drawio_guide (5 секций: skeleton / layout / containers / icons-aws /
icons-azure, каждая ≤4KB) — по требованию, не раздувает контекст;
- drawio_shapes — реальный jgraph shape-index (10446 фигур, gzip 437KB,
ленивый node:zlib gunzip) + курируемый оверлей (service-level паттерны
AWS/Azure, note-подсказки на пустые resIcon, палитра категорий);
ранжирование aws4>aws3; escapeRe в score (не ReDoS);
- layout:"elk" через elkjs (чистый JS, dependencies:{}) — compound-nesting,
best-effort (на сбое ELK возвращает нормализованный вход), 73→0 warnings
на 12-узловом графе;
- 6 типов quality-warnings в линтере (overlap, out-of-bounds, edge-cross,
и т.п.), геометрия Liang-Barsky; warnings НИКОГДА не блокируют write.
Оба новых инструмента в SHARED_TOOL_SPECS (tier:deferred) + SERVER_
INSTRUCTIONS; drift-guards зелёные. elkjs ^0.11.1 — единственный новый
рантайм-деп; lockfile синхронизирован (--frozen-lockfile --offline EXIT 0).
data/ едет с воркспейсом (.gitignore-негация !packages/mcp/data/).
Внутренний цикл: 1 проход внутреннего ревью (APPROVE WITH SUGGESTIONS);
все 59 профильных тестов зелёные.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
93d244478e |
Merge pull request 'refactor(tools): генерировать инвентарь SERVER_INSTRUCTIONS из реестра + guard имён тулов (#448)' (#460) from refactor/448-generate-inventory into develop
Reviewed-on: #460 |
||
|
|
791f709c18 |
Merge pull request 'refactor(tools): execute-маппинг в SHARED_TOOL_SPECS + автопроводка обоих хостов (#445)' (#459) from refactor/445-execute-mapping into develop
Reviewed-on: #459 |
||
|
|
0f5f048ca2 |
fix(mcp): pre-validate node JSON против схемы + путь битого узла (#409, остаток Фазы 1)
Структурные редакторы (patchNode/insertNode/updatePageJson/transformPage) кидали
опаковый Yjs-крах на агентском JSON с вложенным узлом без/с неизвестным `type`:
«Failed to encode document to Yjs (fromJSON): Unknown node type: undefined» —
ГЛУБОКО в энкодере, уже ПОСЛЕ открытия collab-сессии, а хинт мислейблил это как
проблему атрибута. Агент ретраил вслепую (~34 краха в истории 06-17…07-07).
- findInvalidNode(doc) в prosemirror-markdown/node-ops.ts: DFS по content,
возвращает {path, summary} первого узла с отсутствующим/не-строковым `type`
или типом/маркой вне схемы. Множество имён — из getSchema(docmostExtensions),
ТОГО ЖЕ, из которого энкод-путь строит docmostSchema → «известный тип»
обходчика ровно то, что примет PMNode.fromJSON/toYdoc (сверено на 45 узлах +
12 марках, ни ложных положительных, ни пропуска краш-типа).
- unstorableYjsError: findInvalidNode ПЕРВЫМ (node-shape крах больше не
мислейблится как атрибут), затем findUnstorableAttr, generic-фраза последней.
- assertValidNodeShape(op, node) ДО getCollabTokenWithReauth/mutatePageContent
в patchNode/insertNode/updatePageJson: fail-fast — collab-сессия не
открывается, page-lock не берётся, сообщение детерминировано (mock-тест
ассертит collabTokenFetched===false на битом пути). tableUpdateCell не тронут
(строит абзац из plain text через makeCellParagraph, агентский JSON не глотает).
- Описания patch_node/insert_node/update_page_json: каждый узел, включая
вложенные, несёт строковый `type` из схемы; текст-листы {"type":"text",...}.
sanitizeForYjs (стрип undefined-атрибутов) сохранён — другой класс отказа.
Внутреннее ревью: APPROVE WITH SUGGESTIONS — schema-fidelity/fail-fast/
no-false-positive/precedence подтверждены; замечания необязательны (тест
перечисления схемы, depth-guard безобиден т.к. энкодер падает раньше).
prosemirror-markdown vitest 726/726, mcp node --test 613/613.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
d0f99052cf |
refactor(tools): генерировать инвентарь SERVER_INSTRUCTIONS из реестра + guard имён тулов в промпте (#448)
Финальный линк Фазы 1б. Инвентарь тулов жил в 4 рукописных прозаических копиях (SERVER_INSTRUCTIONS под regex-тестом; <tool_catalog>; имена в ai-chat.prompt.ts без гарда; README) — роадмап #416 планировал 4 последовательных ручных правки этого текста (#411/#412/#413/#415). - SERVER_INSTRUCTIONS разбит (новый модуль server-instructions.ts): ROUTING_ PROSE (рукописные intent-подсказки «когда что» — осмысленно ручные, перенесены ДОСЛОВНО со всеми предостережениями: <=250 у create_comment, soft-delete у delete_page, baseHash у drawio_update, PUBLIC у share_page) + buildToolInventory() — генерирует <tool_inventory> из реестра (mcpName + purpose из catalogLine, группировка по TOOL_FAMILY, бакет OTHER ловит незамаппленное → тул нельзя тихо потерять) + 5 inline MCP-only (INLINE_MCP_INVENTORY). Детерминирован (семейства FAMILY_ORDER, имена localeCompare). regex-тест server-instructions удалён; структурные гарантии — в новом tool-inventory.test.mjs (точное членство множества сильнее старого \b-скрейпа). - Имена тулов в ai-chat.prompt.ts → через экспорт PROMPT_TOOL_NAMES; новый гард ai-chat.prompt.tool-names.spec.ts: каждое имя — реальный тул реестра, скан guidance-нот на camelCase-токены падает на несуществующем (escape- нейтрализация против ложных nThe-токенов). - INLINE_TOOL_TIERS уже содержал ровно 8 genuinely-inline тулов (после #445) — сжатие не потребовалось. Критерий: добавление/переименование спека меняет инвентарь БЕЗ правки прозы. Внутреннее ревью: APPROVE — фактическим прогоном подтверждено, что НИ ОДИН тул из старого SERVER_INSTRUCTIONS не выпал (диф старый-vs-новый пуст; добавился get_workspace, раньше прятавшийся в EXCEPTIONS); проза дословна; инвентарь полон/детерминирован/без фантомов; гард краснеет на обеих ветках провала. 613 node + 289 jest зелёные. Стоит на #445 — мержить последним в стопке 1б. README-каталоги вне обязательного скоупа (docs-скрипт) — в чек-лист #412. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8c74659d91 |
refactor(tools): execute-маппинг в SHARED_TOOL_SPECS + автопроводка обоих хостов (#445)
Ядро Фазы 1б. Реестр (#294) шарил только метаданные (имя/схема/описание/tier), но НЕ execute-логику — у каждого shared-тула было ДВА рукописных execute-тела с копией маппинга аргументов (MCP registerShared в index.ts; in-app sharedTool в ai-chat-tools.service, зеркалящий MCP-транспорт вручную). Корень повторяющихся parity-багов ( |
||
|
|
ee33a293b9 |
Merge pull request 'feat(mcp): drawio стадия 1 — CRUD-инструменты drawio_get/create/update (сырой XML)' (#434) from feat/423-drawio-crud into develop
Reviewed-on: #434 |
||
|
|
5a6009c750 |
feat(mcp): drawio стадия 1 — CRUD-инструменты drawio_get/create/update (сырой mxGraph XML)
Узел drawio для агента был непрозрачен: round-trip хранит узел, но содержимое
диаграммы недоступно. Стадия 1 даёт минимальный CRUD без рендеринга на беке.
Новые модули:
- drawio-xml.ts: decode-chain (content= base64 -> plain/entity-encoded XML или
compressed <diagram> через pako.inflateRaw raw-deflate + decodeURIComponent;
инфляция потоковая с капом 16 MiB — защита от decompression-bomb); encode
(plain uncompressed по контракту createDrawioSvg); линтер (все правила ->
структурированный tool-error с cellId; edge без дочернего mxGeometry — ошибка
№1); stable mxHash (sha256 по нормализованному XML) — ключ optimistic-lock.
- drawio-preview.ts: чистый TS schematic SVG (rect/ellipse/rhombus/edge/label,
контейнеры -> абсолютные координаты, unknown-стенсил -> подписанный rect),
без зависимостей и без бэкенд-рендера.
Инструменты (SHARED_TOOL_SPECS, deferred-тир):
- drawio_get(pageId, node, format?) -> XML/SVG + мета {attachmentId,title,w,h,
cellCount,hash}; читает человеческий compressed-экспорт losslessly.
- drawio_create(pageId, where, xml, title?) -> lint -> preview -> .drawio.svg ->
upload (тот же attachment-конвейер + validateDocUrls) -> insert drawio-узел;
возвращает адресуемый '#<index>'-хендл (у схемы drawio нет атрибута id).
- drawio_update(pageId, node, xml, baseHash) -> baseHash-конфликт до записи;
перепривязывает РОВНО адресованный узел (не все с общим attachmentId).
pako@2.0.3 — единственная новая зависимость (lockfile синхронизирован).
closes #423
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
c9293e316b |
feat(mcp): createComment — подсказки самокоррекции якоря (closest-block, markdown-strip, multi-block)
createComment — топ-хотспот ошибок агента (промахи по якорю, слепые ретраи). Портированы аффордансы самокоррекции из editPageText: - Closest-block hint: общий хелпер closestBlockHint вынесен в text-normalize.ts (json-edit.ts теперь тоже его зовёт), подключён во все 3 throw-а createComment. - Markdown-strip fallback в comment-anchor.ts согласованно по всем 4 функциям (can/count/apply/get) через единый resolveAnchorSelection: exact-verbatim wins глобально, stripped — только если raw не якорится нигде; soft warning как в editPageText. Инвариант уникальности suggestion (0/1/>=2) сохранён: raw-unique никогда не запускает fallback -> не может стать ambiguous. Хранимый selection остаётся СЫРОЙ подстрокой документа (strip только для поиска). - Multi-block detection: явное сообщение 'selection spans multiple blocks' когда per-block поиск провалился, но выделение есть в объединённом тексте блоков. - tool-spec createComment: копировать selection дословно из getPage/searchInPage. Известное мелкое ограничение (нит внутреннего ревью): детектор multi-block использует raw selection, поэтому markdown-стилизованное выделение через границу блоков получит generic-подсказку вместо spans-multiple-blocks (редко, guidance всё равно корректный). closes #408 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
327737b701 |
feat(ai-chat): give the in-app agent insertFootnote/insertImage/replaceImage (#410)
The Researcher role wrote 40 literal `^[...]` and zero real footnotes: its
incremental write path (insertNode/editPageText) doesn't parse markdown, and
the footnote-capable tool was MCP-only. Promote three tools from inline
MCP-only to the shared registry so the in-app agent gets them too.
- tool-specs.ts: insertFootnote/insertImage/replaceImage added to
SHARED_TOOL_SPECS (mcpName/schema/description moved VERBATIM from the inline
registrations — MCP names + behaviour unchanged for external clients).
- index.ts: the 3 inline registerTool calls become registerShared; drop the
"MCP-only by design" comments.
- ai-chat-tools.service.ts: register the 3 in-app via sharedTool ->
client.insertFootnote/insertImage/replaceImage (imageUrl->url,
attachmentId->oldAttachmentId mapping).
- tool-tiers.ts: insertFootnote -> core (else the original asymmetry recurs —
footnote tool hidden while editPageText is core); images -> deferred.
- research/{en,ru}.yaml FOOTNOTES: `^[...]` parses ONLY on a whole-markdown
write (create/update/import); for a pinpoint citation to existing text use
insertFootnote; via editPageText/insertNode it stays literal.
- json-edit.ts guardrail: an edit_page_text `replace` containing a `^[...]`
token is refused into failed[] with an insert_footnote hint, mirroring the
existing formatting-marker refusal. (Slightly broader net than that mirror —
a literal `^[a-z]` regex class in a replace is also refused; accepted
defense-in-depth, has a no-false-positive test.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
36b940fdb8 |
fix(#294 review F1-F2): test the changed execute wirings + transport-neutral descriptions
- F1: added in-app execute tests for the two wirings that ACTUALLY changed in the migration (the contract-parity test only checks advertised schema keys, not execute bodies): movePage forwards the newly-added optional `position` to client.movePage (and passes undefined position + null parent when omitted); the table trio (insert/delete/updateCell) forwards the unified `table` param positionally. A field destructured under the wrong name would have silently passed undefined to the client (execute is any-cast, tsc won't catch it). - F2: rewrote the three migrated descriptions that hardcoded snake_case sibling tool names (which the in-app camelCase layer exposes under different ids, violating the registry's own transport-neutral-prose convention) into neutral prose: getPage "use get_page_json" -> "use the lossless page-JSON read tool"; updatePageJson "get_page_json -> ... -> update_page_json" -> "read the page-JSON view -> modify -> write it back", "prefer rename_page" -> "prefer the rename-page tool"; exportPageMarkdown "import_page_markdown round-trip" -> "page-Markdown import round-trip" (the last was a direct regress — the in-app base said the camelCase importPageMarkdown). (stashPage's pre-existing get_page_json mention is out of scope, per the reviewer.) Gate: mcp build 0; ai-chat-tools.service + tool-tiers (catalog-partition) pass, incl. the 5 new execute-wiring tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ce70fab1df |
refactor(ai-chat): unify share_page into SHARED_TOOL_SPECS (#294, misc family)
Migrates share_page / sharePage into the transport-agnostic spec registry (schema + description declared once; each transport keeps only its execute/auth): - sharePage (deferred) -> SHARED_TOOL_SPECS; index.ts uses registerShared(), ai-chat uses sharedTool(); removed from INLINE_TOOL_TIERS. Drift reconciled (documented inline): both inline copies already carried the "only share when the user explicitly asked" security framing, so the old "per-transport divergence" note in BOTH layers was STALE — there was no real behavioral divergence, only wording drift. The canonical description merges the MCP copy's URL-format + idempotency detail with the in-app copy's reversibility note and keeps the shared security framing. pageId keeps the MCP copy's stricter .min(1). The MCP execute keeps its own `searchIndexing ?? true` default (per-layer, not part of the shared schema). Intentionally NOT migrated (kept inline — genuinely divergent, as their existing notes state): - search / searchPages: the in-app tool is a semantic+keyword hybrid (RRF) with in-process access control and a tuned schema (limit 1-20); the MCP `search` is a plain REST full-text search (limit up to 100). Different behavior AND schema. - docmost_transform / transformPage: the in-app tool deliberately omits the `deleteComments` schema field (a comment-deletion guardrail) and carries a shorter description. Different schema. Gate: mcp build 0 + node --test 458/458 (page-search excluded — hangs only under the local re2->RegExp type-shim, its source untouched), server jest 775 incl. tool-tiers catalog-partition + shared-spec contract parity, server tsc 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
39735afd73 |
refactor(ai-chat): unify page tools into SHARED_TOOL_SPECS (#294, pages family)
Migrates the three-layer page tools into the transport-agnostic spec registry
(schema + description declared once; each transport keeps only its execute/auth):
- getPage, listPages (core), createPage, movePage, renamePage, deletePage,
updatePageJson, exportPageMarkdown (deferred) -> SHARED_TOOL_SPECS; index.ts
uses registerShared(), ai-chat uses sharedTool(); removed from
INLINE_TOOL_TIERS. Tiers preserved from CORE_TOOL_KEYS (getPage/listPages =
core, the rest deferred).
delete_page is genuinely three-layer (in-app deletePage exists), so it IS
migrated — not MCP-only. Its H4 guardrail is preserved: the shared schema
exposes ONLY pageId, so no permanentlyDelete/forceDelete flag can reach the
client (still asserted by ai-chat-tools.service.spec.ts).
Descriptions merged (documented inline): each canonical text takes the MCP
copy's richer structural notes plus the in-app copy's reversibility framing.
Schema DRIFT reconciled (documented inline):
- createPage.content: MCP pinned .min(1) but the in-app copy left it unbounded
and DOCUMENTS an empty body as valid ("may be empty" — creating an empty page
to fill later is a real use). Kept the looser no-min form: create_page now also
accepts an empty body (harmless) and no previously-valid in-app input is
rejected. title/spaceId keep the MCP .min(1) (empty is never valid).
- movePage: MCP exposed an optional `position` (fractional-index) field the
in-app copy lacked. Unified by KEEPING position — the in-app client already
accepts an optional position arg, so the in-app execute now forwards it;
optional, so no previously-valid call is rejected. `parentPageId` is nullable
on both (real JSON null -> root); the MCP execute keeps its 'null'/'' string
coercion as a per-layer robustness fallback.
- getPage/renamePage/updatePageJson/exportPageMarkdown/listPages: kept the MCP
copy's stricter .min(1) on ids where the in-app copy was unbounded.
Per-transport execute logic preserved: getPage's {title,markdown} projection,
updatePageJson's JSON-string normalization, list_pages' default limit/tree, and
move_page's cycle guard + positive-confirmation check all stay in their execute
bodies.
Intentionally NOT touched: updatePageContent (Markdown-based body update; no MCP
equivalent) and getTable (name-convention divergence, see tables family) stay
inline.
Gate: mcp build 0 + node --test 458/458 (page-search excluded — hangs only under
the local re2->RegExp type-shim, its source untouched), server jest 770 incl.
tool-tiers catalog-partition + shared-spec contract parity + deletePage H4
guardrail, server tsc 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
eebbe6717c |
refactor(ai-chat): unify table row/cell tools into SHARED_TOOL_SPECS (#294, tables family)
Migrates the three-layer table WRITE tools into the transport-agnostic spec registry (schema + description declared once; each transport keeps only its execute/auth): - tableInsertRow, tableDeleteRow, tableUpdateCell -> SHARED_TOOL_SPECS; index.ts uses registerShared(), ai-chat uses sharedTool(); removed from INLINE_TOOL_TIERS (all three are deferred; not in CORE_TOOL_KEYS). Drift reconciled (documented inline): the four table tools previously carried a "NOT shared" note in both layers over a single parameter-NAME drift — the MCP layer named the table reference `table`, the in-app layer `tableRef`. Unified on the MCP name `table` (renaming the public MCP parameter would break external MCP clients; the in-app parameter is model-facing/prompt-only and safe to rename). The in-app execute bodies now destructure `table`. Descriptions took the MCP copy's richer wording (documents `#<index>`, padding, header-row behavior) plus the in-app copy's "Reversible via page history" note; both fields keep the MCP copy's stricter .min(1) (in-app left them unbounded); sibling tool references phrased transport-neutrally. Intentionally NOT migrated (kept inline): table_get / getTable. Its MCP tool name is noun-first (`table_get`) while the in-app key is verb-first (`getTable`), which breaks the snake_case(inAppKey) naming convention the registry enforces (shared-tool-specs.contract.spec.ts). Renaming the public MCP tool would break external clients, so it stays per-transport — but its in-app reference param was still aligned to `table` (was `tableRef`) for consistency with the migrated trio. Gate: mcp tsc 0 + node --test 458/458 (page-search excluded — hangs only under the local re2->RegExp type-shim, its source is untouched), server jest 730 incl. tool-tiers catalog-partition + shared-spec contract parity, server tsc 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e348433a39 |
refactor(ai-chat): unify comment tools into SHARED_TOOL_SPECS (#294, comments family)
Migrates the three-layer comment tools into the single transport-agnostic spec registry (schema + model-facing description declared once; each transport keeps only its execute/auth): - createComment, listComments, resolveComment, checkNewComments — moved to SHARED_TOOL_SPECS; index.ts uses registerShared(), ai-chat uses sharedTool(); removed from INLINE_TOOL_TIERS (tier/catalogLine now on the spec). Tiers preserved from CORE_TOOL_KEYS (create/list/resolve = core, check = deferred). Intentionally NOT migrated (kept MCP-inline): update_comment / delete_comment — they are MCP-only by design; the in-app AI-chat layer deliberately has no updateComment/deleteComment (comment edits are irreversible / not version-tracked), asserted by ai-chat-tools.service.spec.ts. A registry spec's tier/catalogLine are in-app metadata and the catalog-partition test forbids a deferred spec without a live in-app tool, so these stay per-transport. Drift reconciled (documented inline): createComment/listComments/checkNewComments took the more-maintained/superset description + stricter .min(1) guards. resolveComment: `resolved` drifted (MCP optional+default(true) vs in-app required) — kept the MCP superset, so in-app resolveComment now accepts an omitted `resolved` (defaults to resolve) — a deliberate, backward-compatible unification (never rejects a previously-valid input). Gate: mcp build 0 + node --test 480/480, ai-chat 654, tool-tiers (incl. F3 catalog-partition) 16/16, server tsc 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e431b33bb1 |
feat(ai-chat): deferred tool loading (tiers + loadTools meta-tool) (#332)
The in-app AI agent shipped all ~41 tool schemas on every model step. This adds a two-tier catalog: core tools (frequent or one-line) stay always-active; the rest are advertised as a compact catalog and their full schema is fetched on demand via the loadTools meta-tool, wired through ai@6 prepareStep's per-step activeTools. - tools/tool-tiers.ts: CORE_TOOL_KEYS, INLINE_TOOL_TIERS, applyLoadTools, catalog builders (+ tool-tiers.spec.ts, 13 cases). - ai-chat.service.ts prepareAgentStep: returns activeTools = [...CORE_TOOL_KEYS, loadTools, ...activatedTools]; per-turn activated Set. - ai-chat.prompt.ts: buildToolCatalogBlock renders the deferred catalog. - mcp/tool-specs.ts: tier + catalogLine metadata (external snake_case /mcp transport unchanged). - EnvironmentService.isAiChatDeferredToolsEnabled(): AI_CHAT_DEFERRED_TOOLS, default ON per issue intent (kill-switch =false restores old behavior). Gate: server ai-chat 631/631, tool-tiers 13/13, mcp 472/472, tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
086bc1bf8b |
docs(mcp): search_in_page regex desc names RE2, not JS regex (#330 review F5)
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> |
||
|
|
77b245461f |
fix(mcp): search_in_page regex via re2 (ReDoS-safe) + review DO F1-F4 (#330 review)
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> |
||
|
|
40d42d61e6 |
feat(mcp): search_in_page tool — in-page substring/regex search for the agent (#330)
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>
|
||
|
|
351615e5bc |
prompt(mcp): fix inaccurate and misleading tool descriptions
Audit of all 41 tool descriptions against the actual implementation found factually wrong or misleading texts: - list_comments claimed '(paginated)' — it takes only pageId and returns ALL comments in one call (internal pagination); now also states that RESOLVED threads are included and how to filter them. In-app twin synced. - search claimed the limit default is 'applied by the client' — the client deliberately omits it so the SERVER applies its default. - create_page's '(automatically moves it to the correct hierarchy)' said nothing useful — now documents parentPageId nesting semantics; move_page drops the stale 'essential for organizing pages created via create_page'. - share_page now warns the page becomes accessible to ANYONE with the URL. - get_page (both transports) now explains inline <span data-comment-id> tags are comment anchors (incl. resolved) — markup, not page text. - patch_node/delete_node/insert_node pointed only at the expensive page-JSON view for block ids — now route through the cheap page outline first. - docmost_transform marks 'Примечания переводчика' as the DEFAULT notesHeading, overridable for non-Russian pages. Checks: @docmost/mcp tests 450/450 (incl. the server-instructions guard); server ai-chat-tools spec 20/20; mcp build/ artifacts rebuilt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1fda0ec8b0 |
prompt(mcp): rewrite SERVER_INSTRUCTIONS to cover all tools + guard test
The intent-routing guide had rotted: 17 of 41 registered tools were absent (get_outline, get_node, the whole table_* family, search, stash_page, sharing, page lifecycle), and two tips were actively harmful — 'read block ids via get_page_json' told agents to pull the whole ~100KB document when get_outline exists precisely to grab ids cheaply, and 'table cell -> patch_node by attrs.id' dead-ends because table nodes carry no attrs.id. - Rewrite SERVER_INSTRUCTIONS as intent clusters (READ / EDIT / PAGES / COMMENTS / HISTORY) covering every tool except get_workspace; add safety notes (share_page = PUBLIC, delete_page = soft) and a comment-anchor markup warning for get_page. - delete_page tool description: state SOFT delete / restorable explicitly. - MAINTENANCE RULE comments at both registration sites (index.ts, tool-specs.ts) + an AGENTS.md convention bullet: adding/renaming/removing a tool REQUIRES updating the guide. - New guard test (test/unit/server-instructions.test.mjs): extracts every registered tool name from source and fails when one is not mentioned in the shipped SERVER_INSTRUCTIONS (word-boundary match, so get_page can't hide behind get_page_json); EXCEPTIONS list is itself validated against the registry. SERVER_INSTRUCTIONS exported for the test. Tests: @docmost/mcp 450/450 (448 + 2 new). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f720151c63 |
refactor(ai-chat): move patch_node/insert_node metadata into the shared tool-spec registry (#294)
The same tool metadata (zod schema + model-facing description) was hand-duplicated between the standalone MCP server and the in-app AI-chat agent, so every tweak had to land in two places and copies drifted (a materialized parity bug). The shared transport-agnostic registry (packages/mcp/src/tool-specs.ts) already de-duplicates 14 tools; this migrates two more genuinely-identical ones — patch_node/patchNode and insert_node/insertNode. The canonical description is a strict SUPERSET of both originals (keeps MCP's "without resending the whole document" + table-structure/anchor guidance AND the in-app "reversible via page history" / "exactly one of anchorNodeId or anchorText" framing — no model-facing guidance dropped); the schema is identical (the in-app side just gains MCP's .min(1) on ids, a safe tightening). Each transport keeps its own execute/auth wrapper, and the in-app parseNodeArg node-arg normalization is unchanged. The three table tools are intentionally NOT merged (a real param-name divergence: table vs tableRef) — documented on both sides. Other per-transport divergences (search/share/create_comment/transform/list_pages) are left separate with a short comment explaining why (the issue asked to flag these as intentional). DocmostClientLike stays a hand-mirror (the ESM/CJS boundary blocks a compile-time type import; a runtime drift-guard already pins it). Also fixes a latent contract-spec bug: derive `required` from `instanceof z.ZodOptional` (matches the emitted JSON schema) instead of `isOptional()`, which wrongly reported z.any() fields as optional. Partially addresses #294. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6eb335d5e3 |
fix(sandbox): address PR #250 review — SSRF guard, eviction safety, cleanup (#243)
Security: - stash_page: reject path-traversal / percent-encoded srcs before the authed loopback fetch (resolveInternalFilePath), closing an SSRF/exfiltration hole where a crafted node.attrs.src could read an arbitrary internal GET endpoint into the anonymous sandbox. Stability: - stash_page: revert + recount mirrors FIFO-evicted by a later put in the same stash (no dangling sandbox refs, honest images.mirrored/failed); free image blobs if the final document put throws. - Reject/clamp non-positive SANDBOX_TTL_MS to the 1h default (warn once). - Log mirror failures unconditionally (console.warn, no blob bodies). Cleanup / architecture: - Remove dead expiresAt from SandboxPutResult. - Centralize the /api/sb route in SANDBOX_ROUTE_SEGMENT/SANDBOX_API_PATH and move URL composition into SandboxStore.putAndLink; drop the duplicated sink closures and the now-unused EnvironmentService injection from McpService and AiChatToolsService. - Un-export isInternalFileUrl; document the process-local (instance-bound) sandbox limitation in the tool description and .env.example. Docs/tests: - README/README.ru: 38 -> 39 tools + stash_page entry. - Add traversal/normalize/recursion unit tests, stash self-eviction + doc-put-throw + empty/octet-stream mock tests, controller If-None-Match (wildcard/weak/list) + Cache-Control tests, and SANDBOX_TTL_MS validation tests. Regenerate packages/mcp/build. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
2fe4ca8537 |
feat(sandbox): in-RAM blob sandbox for out-of-band page transfer (#243)
Add an ephemeral, process-local blob store so the in-app agent (and the
embedded MCP) can hand a large page document and its images to an external
consumer WITHOUT routing the bytes through the model context or Docmost auth.
- SandboxStore (@Injectable singleton): Map<uuid,{buf,mime,sha256,expiresAt}>
in RAM only. put() picks a per-blob cap by mime (image vs doc), enforces a
total-bytes RAM guard with oldest-first eviction, and stamps a TTL; get()
lazily expires. sha256 computed at put() doubles as the strong ETag. An
unref'd sweep interval clears expired entries and is cleared on destroy.
- GET /api/sb/:uuid anonymous controller: serves raw bytes with Content-Type,
Content-Length and ETag=sha256; 404 on missing/expired/non-UUID (anti-
traversal), 304 on a matching If-None-Match. No tokens, no 401 — the
capability is the unguessable UUID + short TTL + TLS. Auth-exempt the same
way as /api/files/public (no JwtAuthGuard) plus an /api/sb entry in main.ts's
workspace-resolution preHandler so a remote consumer with no workspace host
is not rejected.
- stash_page tool in both layers (MCP resource_link + in-app {uri,size,sha256,
images}). client.stashPage serializes the get_page_json shape, mirrors every
INTERNAL file/image src (type-agnostic, covers drawio/excalidraw/video/file)
into the sandbox under Docmost auth and rewrites src to the sandbox URL;
external http(s) srcs are left untouched; dedup by src; a failed image fetch
is counted, never aborts the doc.
- SANDBOX_PUBLIC_URL / SANDBOX_TTL_MS / SANDBOX_MAX_BYTES /
SANDBOX_MAX_IMAGE_BYTES / SANDBOX_MAX_TOTAL_BYTES wired through the
environment service + validation + .env.example.
- SandboxModule (@Global) provides the shared store to the controller,
McpService and AiChatToolsService (same instance for put and get).
Tests: SandboxStore (round-trip, sha256, TTL lazy + sweep, caps, eviction),
SandboxController (200+ETag+CT+CL, 404 missing/expired/non-UUID, 304), and a
mock-HTTP stashPage test (mirror+rewrite internal, keep external, dedup, failed
image counted, returns only a link). Interoperates with the vvzvlad/habr-mcp
consumer's anonymous-GET + sha256-ETag + resource_link contract.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
f3fa15e746 |
refactor(ai-chat): shared tool-spec registry for identical tools; formalize integration db factory
Implements two architecture follow-ups from the multi-aspect review.
1. Shared, zod-agnostic tool-spec registry (packages/mcp/src/tool-specs.ts)
for the 14 AI tools whose name + schema + model-facing description are
genuinely identical across the standalone MCP server and the in-app
AI-SDK chat. Both layers consume it (registerShared in index.ts;
sharedTool in ai-chat-tools.service.ts) and keep their own execute/auth.
- Zod-agnostic builders (z) => ZodRawShape bridge the zod v3 (mcp) vs
zod v4 (server) split; the registry imports no zod.
- Folds in the documented edit_page_text drift-bug fix: the stale
"strip-and-retry tolerated" claim is gone; canonical wording states a
formatting-only change is refused into failed[].
- Sibling-tool references in shared descriptions are transport-neutral so
one description is correct for both snake_case (MCP) and camelCase
(in-app) tool names.
- Loader fail-fast guard for a stale @docmost/mcp build.
- The ~17 intentionally-divergent tools (security guardrails, tuned UX)
stay per-layer, untouched.
- Rebuilt committed mcp artifacts (also regenerates a previously stale
build/lib/docmost-schema.js to match its already-committed source).
2. Formalize apps/server/test/integration/db.ts as the canonical
integration-test seed factory (module doc + a shortId helper); the
hand-written minimal seeders are kept on purpose, decoupled from the
app service-layer side effects.
Verified: server tsc + lint clean, mcp build clean; mcp unit tests 261 pass,
ai-chat-tools.service 16 pass, public-share-chat-tools 8 pass, ai-chat suite
224 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|