feat(mcp): drawio стадия 1 — CRUD-инструменты drawio_get/create/update (сырой XML) #434
Reference in New Issue
Block a user
Delete Branch "feat/423-drawio-crud"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Стадия 1 draw.io: минимальный CRUD диаграмм через сырой mxGraph-XML, без рендеринга на беке (превью — чистый TS; полноценный рендер делает клиент по существующему флоу).
Новые модули:
drawio-xml.ts— decode-chain (content=base64 → plain/entity-encoded XML или compressed<diagram>черезpako.inflateRawraw-deflate +decodeURIComponent; инфляция потоковая с капом 16 MiB — защита от decompression-bomb); encode (plain uncompressed по контрактуcreateDrawioSvg); линтер (все правила → структурированный tool-error сcellId; self-closed edge без дочернего<mxGeometry>— ошибка №1); stablemxHash(sha256 по нормализованному XML) — ключ optimistic-lock.drawio-preview.ts— чистый TS schematic SVG (rect/ellipse/rhombus/edge/label; контейнеры → абсолютные координаты; unknown-стенсил → подписанный rect), без зависимостей.Инструменты (
SHARED_TOOL_SPECS, deferred-тир):drawio_get→ XML/SVG + мета{attachmentId,title,w,h,cellCount,hash}; читает человеческий compressed-экспорт losslessly.drawio_create→ lint → preview →.drawio.svg→ upload (тот же attachment-конвейер +validateDocUrls) → insert drawio-узел; возвращает адресуемый#<index>-хендл (у схемыdrawioнет атрибутаid).drawio_update→ baseHash-конфликт до записи; перепривязывает РОВНО адресованный узел.pako@2.0.3— единственная новая зависимость (lockfile синхронизирован;pnpm install --frozen-lockfileпроходит).closes #423
Внутренний цикл (2 прохода + фиксы)
Внутреннее ревью нашло HIGH-баг, который скрывали моки:
drawio_createвозвращалattrs.id, которого схемаdrawio(diagramAttributes) не хранит (PMNode.fromJSONдропает неизвестный атрибут) → возвращённый хендл не резолвился вdrawio_get/update. Исправлено: возврат#<index>-хендла (адресуется как image/table), мёртвыйidубран, мок теперь дропает non-schema атрибуты (ловит класс). Плюс:drawio_updateтаргетит только адресованный узел (а не все с общимattachmentId); pako-инфляция с капом 16 MiB. Повторное ревreview: APPROVE.Известный не-блокер (нит): позиционный
#<index>-хендл — TOCTOU при перестановке блоков между чтениями (защищёнtype==="drawio"+ baseHash; совпадает с контрактом позиционных ссылок table/image).How verified
pnpm --filter @docmost/mcp build— чисто;node --test drawio-xml + drawio-preview + drawio-tools(mock) + server-instructions + client-host-contract + tool-specs→ 57 passed (позитив+негатив на каждое правило линтера с cellId; decode plain/entity/compressed round-trip; байт-стабильность; human-compressed lossless; baseHash-конфликт без upload; #index резолвится на сохранённом (id-dropped) доке; update таргетит один узел; inflate-бомба реджектится; drift-гарды).pnpm install --frozen-lockfile→ EXIT 0 (lockfile синхронен с package.json после добавления pako).Checklist
Узел 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>Ревью — #434
feat(mcp): drawio стадия 1 — CRUD-инструменты drawio_get/create/update (сырой mxGraph XML)Вердикт: ПРАВКИ (CHANGES) — 2 правки (покрытие error-путей нового тула + мёртвый метод). Безопасность и ядро — чисто.
Полный веер из 9 аспектов по всему диффу PR (merge-base
d90c3b8b..5a6009c7, +2437). Гейт зелёный (clean-room/home/claude/gm-ci):pnpm installвнёсpako(чистая установка), prosemirror-markdown + mcp build 0, node --test 531/531 (+27 drawio-тестов).Безопасность — все критичные guard'ы проверены (untrusted XML + компрессия) (клик)
inflateDiagramPayload— streamingpako.Inflateс per-chunk проверкой размера ДО обработки чанка, abort на капе 16 MiB. Мутация: поднятьMAX_INFLATED_DIAGRAM_BYTESдо 999 MiB → тест бомбы падает. Ловится.DOMParserОТВЕРГАЕТ DOCTYPE целиком (тест: DOCTYPE+ENTITY → parsererror). Ни внешних сущностей (file/SSRF), ни billion-laughs.xmlEscape(& < > ") на title корректен.badAmp-регексп — детерминированный lookahead, без катастрофического бэктрекинга.drawio_create/updateзовутensureAuthenticated(), запись черезmutatePage(collabToken + page-write-lock).content="…"SVG-аттрибута (инертные данные), видимая часть SVG — отдельно построенный preview, drawio санитайзит при открытии.Правки (сделать, потом ре-ревью)
packages/mcp/src/client.ts(drawio_get/update/create). Ядро (bomb-cap, edge-geometry, round-trip) мутационно покрыто, но НЕ покрыты клин-throw ветки нового 464-строчного агент-фейсинг тула:drawio_get→ «no node found» (битый ref) и «has no src to read» (drawio-нода без src);drawio_update→ нода не drawio/сменила тип;drawio_create→ anchor не найден. Плюс escaping-раунд-трип: title с< > " &черезencodeDrawioFile/buildDrawioSvg. Fix: добавь тесты на эти ветки (LLM должен получать чистые ошибки, а не краш/мусор).freshBlockId()—client.ts:3532-3537, ноль call-site'ов (дизайн намеренно не ставит block id на drawio-ноды). Fix: удалить.⛔ DROP — кодеру НЕ делать (лог калибровки)
[speculative/not-exploitable]low[security]scanRawValuesлинтит только атрибутvalue(регексп), прочие атрибуты mxCell (напр.onclick=) проходят линт. НЕ XSS: сырой mxGraph XML хранится base64 внутри SVGcontent=(инертно, не живой DOM), видимый SVG — отдельно построенный preview из геометрии (инъектнутых атрибутов не несёт), drawio санитайзит при открытии. Строже валидировать «неизвестные атрибуты» РИСКОВАННО — отвергнет легитимные human-edited draw.io файлы (лениентность намеренна). Не трогать.[below-threshold]low[test-coverage] нет мутационного теста на style-format правило и на assert поляwarnings(concurrent-removal) — ядро валидации уже мутационно покрыто, эти ветки маргинальны.[by-design]low[coherence] compressed round-trip асимметричен (агент пишет plain, decode читает plain+compressed) — намеренно для стадии 1 (компрессия — стадия 2). Убедиться, что в доке модуля это отражено (уже описано в хедере).Ядро (парсинг/build/inflate/lint mxGraph, 3 CRUD-тула, preview) корректно, безопасность проверена по guard'ам И по render-пути, гейт зелёный. Остались непокрытые error-ветки нового тула + мёртвый метод. После правок — PASS.
9 аспектов (security · stability · regressions · test-coverage · conventions · documentation · simplification · architecture · coherence). Security-guard'ы (bomb/XXE/escape/ReDoS/auth) и эксплойтабельность инъекции атрибута (render-путь: base64-инертно) проверены мной напрямую. Патч-фиделити pako — чистый install. Escalate нет.
Поправил обе правки.
[test-coverage] Error-ветки новых тулов — добавлены тесты (ассертят реальные сообщения из client.ts):
drawio_getбитый ref → «no node found», drawio-нода без src → «has no src to read»;drawio_updateузел не drawio (#0→paragraph) → чистая ошибка + ноль upload/mutate (не краш, не перепривязка чужого узла);drawio_createanchor не найден → ошибка «unreferenced orphan» (+att-1), attachment залит и назван в сообщении, drawio-узел НЕ записан. Плюс escaping round-trip: titleA < B > C " D & Eпроходит encode/build/decode байт-в-байт, внешнийcontent="..."остаётся well-formed.[simplification] Мёртвый
freshBlockId()—grepподтвердил 0 call-site (после фикса #index-хендла id на drawio-ноды не ставится); метод удалён, пост-grep пуст.Проверка:
pnpm --filter @docmost/mcp buildчисто;node --test drawio-xml + drawio-preview + drawio-tools(mock)→ 48 passed. Готово к повторному ревью.Ревью (раунд 2) — #434
feat(mcp): drawio стадия 1 — CRUD-инструменты drawio_get/create/update (сырой mxGraph XML)Вердикт: PASS ✅ — обе правки раунда 1 закрыты.
Полный веер из 9 аспектов по всему диффу PR (merge-base
d90c3b8b..2194f423). Гейт зелёный (clean-room/home/claude/gm-ci): pako на чистом install, mcp build (tsc) 0, node --test 536/536 (+5 к раунду 1). Дельта раунда 2 — тест-онли + удаление мёртвого кода; обе правки проверил адверсариально.Закрытие правок раунда 1
drawio-tools.test.mjs(+74): 4 теста, ассертят РЕАЛЬНЫЕ сообщения из client.ts и проверяют состояние:drawio_getбитый ref → «no node found for …»; drawio-нода без src → «has no src to read»;drawio_updateнода не drawio → «is a paragraph, not a drawio diagram», +uploads==0 && mutations==0(нет порчи);drawio_createanchor не найден → «anchor not found» + «unreferenced orphan» +att-1, + нет записи drawio-ноды (transform вернул null).drawio: no node found→ соответствующий тест падает (14/15). Тесты держат.drawio-xml.test.mjs+34): titleA < B > C " D & EпереживаетbuildDrawioSvg→decodeбайт-в-байт,content=не ломается — load-bearing.freshBlockId()удалён — client.ts −8; ноль call-site'ов/экспортов/типов, удаление orphan-free, поведение drawio-нод (без block id) не меняется.Контекст раунда 1 (всё подтверждено) — клик
Безопасность: decompression-bomb кап 16MiB (per-chunk abort, мутационно), XXE безопасен (jsdom отвергает DOCTYPE), xmlEscape/ReDoS/авторизация ок; инъекция атрибута mxCell НЕ эксплойт (XML хранится base64-инертно в SVG
content=, preview строится отдельно) — в DROP. Ядро (parse/build/inflate/lint mxGraph, 3 CRUD-тула, preview), архитектура, conventions, docs — LGTM.DO/ESCALATE нет. Обе правки раунда 1 закрыты корректно (error-тесты мутационно-держатся, мёртвый код убран чисто); гейт зелёный. Approve.
9 аспектов. Обе правки проверены мной адверсариально (мутация error-сообщения с пересборкой mcp; freshBlockId — grep без ссылок) + независимо двумя субагентами. Escalate нет.