fix(mcp): внятная ошибка на недоступный spaceId вместо «Space permissions not found» (#534) #536
Open
agent_coder
wants to merge 4 commits from
fix/534-mcp-spaceid into develop
pull from: fix/534-mcp-spaceid
merge into: vvzvlad:develop
vvzvlad:main
vvzvlad:feat/529-search-lexical
vvzvlad:feat/506-apikeys-ui
vvzvlad:feat/395-work-time
vvzvlad:feat/502-mcp-md-modes
vvzvlad:feat/492-incremental-render
vvzvlad:fix/549-hardbreak-md-canon
vvzvlad:feat/542-resolve-undo
vvzvlad:fix/505-drawio-strip-comments
vvzvlad:fix/503-hardbreak-canon
vvzvlad:docs/471-breadcrumb-permission
vvzvlad:feat/481-version-coherence
vvzvlad:fix/535-list-seam-coalesce
vvzvlad:develop
vvzvlad:fix/520-recovery-escalation
vvzvlad:fix/525-insert-unloaded
vvzvlad:fix/522-internal-links
vvzvlad:fix/507-drawio-cyrillic
vvzvlad:fix/541-reconnect-timeout
vvzvlad:feat/501-mcp-apikey-auth
vvzvlad:feat/493-converter
vvzvlad:feat/370-page-versioning
vvzvlad:fix/515-code-emphasis
vvzvlad:fix/523-tree-dnd
vvzvlad:fix/517-toasts
vvzvlad:feat/495-tails
vvzvlad:feat/494-mcp-registry-guards
vvzvlad:feat/496-suggestions-audit
vvzvlad:integ/497-wave1
vvzvlad:feat/488-client-fsm
vvzvlad:ci/476-publish-gates
vvzvlad:feat/489-tool-resilience
vvzvlad:feat/487-terminal-lifecycle
vvzvlad:fix/486-iter1-prod-fires
vvzvlad:refactor/347-client-md-paste
vvzvlad:perf/479-getpage-cache
vvzvlad:refactor/449-write-invariants
vvzvlad:refactor/450-split-client
vvzvlad:feat/443-get-page-context
vvzvlad:feat/443-get-tree
vvzvlad:feat/443-search
vvzvlad:docs/415-lossy-descriptions
vvzvlad:feat/413-markdown-default
vvzvlad:refactor/412-camelcase-tool-names
vvzvlad:feat/425-drawio-graph
vvzvlad:fix/464-diffdocs-cpu-guard
vvzvlad:refactor/411-update-page-markdown
vvzvlad:feat/424-drawio-rules
vvzvlad:fix/409-invalid-node-validation
vvzvlad:refactor/448-generate-inventory
vvzvlad:refactor/445-execute-mapping
vvzvlad:refactor/446-derive-client-types
vvzvlad:fix/447-registry-stamp-ci
vvzvlad:fix/444-agent-loop-guards
vvzvlad:fix/396-sendnow-detached-run
vvzvlad:fix/452-immutable-cache-control
vvzvlad:docs/footnote-authoring-comment-cleanup
vvzvlad:feat/437-error-diagnostics
vvzvlad:fix/442-cursor-pagination
vvzvlad:feat/419-footnote-normalize
vvzvlad:perf/399-comment-resolve-async
vvzvlad:perf/435-collab-token-cache
vvzvlad:perf/344-background-rerenders
vvzvlad:perf/348-backend-lowhanging
vvzvlad:feat/423-drawio-crud
vvzvlad:perf/401-collab-hotpath
vvzvlad:feat/430-live-reconnect
vvzvlad:perf/400-collab-session
vvzvlad:refactor/414-dedup-node-ops
vvzvlad:feat/417-new-comments-signal
vvzvlad:feat/408-createcomment-hints
vvzvlad:feat/407-persist-tool-errors
vvzvlad:feat/420-footnote-render
vvzvlad:dummy-review/mcp-hang-fix
vvzvlad:dummy-review/mcp-hang-base
vvzvlad:feat/381-resumable-sse-pr1
vvzvlad:feat/git-sync-2
vvzvlad:docs/agents-workspace-build-order
vvzvlad:feature/offline-sync
vvzvlad:perf/342-code-splitting
vvzvlad:perf/346-compression-cache
vvzvlad:feat/196-multi-cursor
vvzvlad:perf/343-typing-latency
vvzvlad:docs/how-to-test
vvzvlad:fix/ai-sdk-partial-output-oom
vvzvlad:test/351-generative-converter
vvzvlad:feat/371-roles-catalog
vvzvlad:refactor/345-server-converter
vvzvlad:refactor/294-spec-registry-cont
vvzvlad:fix/363-migration-order
vvzvlad:fix/e2e-callout-and-gate-build
vvzvlad:fix/docker-re2-toolchain
vvzvlad:feat/git-sync
vvzvlad:fix/media-roundtrip-stability
vvzvlad:fix/340-comment-panel-perf
vvzvlad:fix/332-deferred-tools
vvzvlad:fix/329-ephemeral-suggestions
vvzvlad:fix/330-search-in-page
vvzvlad:fix/328-resolved-anchor-spam
vvzvlad:fix/331-intraline-diff
vvzvlad:fix/324-coverage-gate
vvzvlad:fix/325-mobile-390
vvzvlad:feat/293-A-git-sync-package
vvzvlad:feat/300-avatar-oklch
vvzvlad:fix/321-banner-mobile
vvzvlad:feat/300-avatar-colors
vvzvlad:feat/315-comment-suggestions
vvzvlad:feat/scroll-restore-stable-wait
vvzvlad:feat/300-agent-avatar-stack
vvzvlad:feat/300-avatar-polish
vvzvlad:refactor/294-tool-spec-registry
vvzvlad:feat/scroll-restore-ux
vvzvlad:fix/responsive-tablet-sidebar
vvzvlad:feature/ai-chat-page-change-observability
vvzvlad:image-inline-center
vvzvlad:fix/283-short-remap-title
vvzvlad:fix/283-slash-layout
vvzvlad:image-inline-row
vvzvlad:feat/276-ai-chat-dock
vvzvlad:fix/269-table-menu-refocus
vvzvlad:docs/dev-stand-guide
vvzvlad:feat/266-scroll-position
vvzvlad:fix/260-collab-docname-slugid
vvzvlad:test/244-phase2-tail
vvzvlad:fix/262-reindex-progress-realtime
vvzvlad:fix/258-changelog-compare-links
vvzvlad:fix/244-dataloss-bugs
vvzvlad:feat/246-spoiler
vvzvlad:feat/221-image-captions
vvzvlad:test/244-part-b
vvzvlad:feat/251-intentional-clear
vvzvlad:fix/embeddings-reindex-progress
vvzvlad:refactor/193-tool-spec-registry
vvzvlad:fix/255-ws-redis-adapter-leak
vvzvlad:fix/252-e2e-open-handles
vvzvlad:feat/229-catalog-yaml
vvzvlad:feat/243-blob-sandbox
vvzvlad:feat/228-inline-footnotes
vvzvlad:fix/qa-ui-bugs-216-218
vvzvlad:feature/agent-roles-catalog
vvzvlad:fix/share-alias-rename
vvzvlad:fix/ai-chat-empty-render
vvzvlad:feat/191-chat-doc-binding
vvzvlad:feat/201-temporary-notes
vvzvlad:feat/198-interrupt-agent
vvzvlad:feat/ai-chat-full-history
vvzvlad:feat/199-ai-generate-title
vvzvlad:feat/205-share-aliases
vvzvlad:batch/issues-189-187-170
vvzvlad:feat/170-mcp-test-button
vvzvlad:feat/189-context-badge
vvzvlad:feat/198-interrupt-agent-send-now
vvzvlad:fix/issues-190-159
vvzvlad:fix/ai-chat-new-chat-during-stream
vvzvlad:fix/ai-chat-stream-perf
vvzvlad:batch/issues-2026-06-25
vvzvlad:feat/ai-chat-persistent-history
vvzvlad:fix/ai-chat-copy-chat-wysiwyg
vvzvlad:fix/ai-stream-reset-resilience
vvzvlad:fix/ai-stream-undici-timeout
vvzvlad:fix/footnote-review-1227-followup
vvzvlad:fix/ai-chat-token-counter-realtime
vvzvlad:docs/manual-qa-test-plan
No Reviewers
Labels
Clear labels
epic
needs-human
review/approved
review/changes-requested
review/needs
возможно, баг
Large multi-phase effort spanning many changes
эскалация: нужно решение человека
в последнем ревью нет открытых blocking-находок
последнее ревью оставило открытые blocking-находки
head не ревьюился (head != reviewed_head)
Похоже на баг, но требует подтверждения/воспроизведения; чиним, если повторится
No Label
review/approved
Milestone
No items
No Milestone
Projects
Clear projects
No project
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: vvzvlad/gitmost#536
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "fix/534-mcp-spaceid"
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
MCP-инструменты с
spaceId(getTree,listPages,checkNewComments,search) на валидный по форме, но несуществующий/недоступный spaceId отдавали агенту невнятноеPOST /pages/sidebar-pages failed (404): Space permissions not found— будто спейс есть, а прав нет. Типичная причина — опечатка в id; агент не мог себя починить. Теперь на таком 404 инструмент возвращает: «spaceId "" не найден среди доступных тебе спейсов. Доступные: (), … — скопируй нужный id из listSpaces». closes #534.Вариант A — только обёртка
packages/mcp, бэкенд не тронут (форк Docmost не дивергируем;space-ability.factoryфизически не различает «нет спейса» vs «нет прав» — схлопываем).Механизм (enrich-on-404, нулевая цена happy-path)
getAccessibleSpaceIndex()— индекс доступных спейсов из/spaces({ids, spaces:{id,name}[], complete}); новыйpaginateAllWithMetaотдаёт флагtruncated(контрактpaginateAll/getSpaces/listSpacesне изменён).withSpaceAccessDiagnostics(spaceId, mcpName, fn)оборачивает тела методов; на 404 сверяет spaceId с индексом и заменяет текст. Fail-open на любой неопределённости (non-404 / fetch/spacesупал / список неполон / spaceId доступен / abort) → рефроусит ОРИГИНАЛЬНУЮ серверную ошибку. Реальные ошибки не подавляются.MCP_SPACES_CACHE_TTL_MS,0=выкл); single-flight; инвалидация в тех же двух точках, что иcollabTokenCache(reauth послеtoken=null+ login-success).Ключевые решения (по резолюции владельца)
toolAbortSignal?.aborted, НЕ поe.name(cap может бытьTimeoutError/кастомный) — Stop/cap не переписывается в spaceId-сообщение..finallyна resolve И reject);cachedIndexпишется только из resolved+complete— сетевой блип/spacesне отравляет фичу до конца сессии.Space permissions not found).createPage(мульти-причинный 404: parent-mismatch до проверки прав) иgetPageContext(pageId). Инвариант «среди обёрнутых 404 приходит ТОЛЬКО от проверки spaceId» запинен комментом у allowlist.How verified (на стенде)
space-access-diagnostics.test.mjs: 11/11, покрывает все 9 критериев приёмки (rewrite на битый id; happy-path без запроса/spaces— спай наclient.post; TTL-кэш + рефетч; без spaceId обёртка инертна;!completefail-open; fetch-fail fail-open + DEBUG stderr + не-мемоизация отклонённого промиса; 0 спейсов; abort-reason проброс; createPage не обёрнут).tscчисто.Совместимость
Space permissions not foundот MCP, увидит новый текст (диагностика для агента, не контракт; голден-тесты её не матчат).MCP_SPACES_CACHE_TTL_MS(дефолт 60000). Миграций нет.Checklist
A well-formed but non-existent/inaccessible spaceId made the space-permissions check answer with an opaque 404 ("Space permissions not found"), which the agent could not self-correct from. Add an enrich-on-404 wrapper at the client-method level (Variant A — no backend change) that, ONLY on that 404, replaces the server text with a factual message naming the bad spaceId and the spaces the token can actually see, pointing at listSpaces. Mechanism (context.ts): - getAccessibleSpaceIndex(): the token's accessible spaces from the single source of truth (/spaces), with a per-instance short-TTL cache (MCP_SPACES_CACHE_TTL_MS, default 60s; 0 disables) and single-flight dedup. Only a COMPLETE (untruncated) result is cached and only a complete result may drive the rewrite. The in-flight promise is nulled on BOTH resolve and reject so a transient /spaces blip is never memoized. Cache invalidated on every identity change, mirroring collabTokenCache (login() + the 401/403 reauth interceptor). - paginateAllWithMeta(): surfaces the `truncated` flag paginateAll swallows; paginateAll now delegates to it (unchanged contract, getSpaces untouched). - withSpaceAccessDiagnostics(spaceId, mcpName, fn): abort/cap wins FIRST (by the toolAbortSignal flag, NOT e.name — a cap may be TimeoutError/custom); only a 404 is enrichable; FAILS OPEN (rethrows the original server error) on every source of uncertainty (fetch failed / !complete / spaceId present / aborted). Wrapped only the paths where the sole 404 cause is the space membership check: getTree, listPages (tree + recent-with-spaceId), search (with spaceId), checkNewComments. createPage/getPageContext are deliberately NOT wrapped. formatSpaceNotAccessible (errors.ts) composes the message (<=10 spaces + "(+N ещё)" tail; distinct zero-spaces variant). Tests: 11 new unit tests (stub client) covering all 9 acceptance criteria + message shape; full mcp unit suite green (701 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Ревью — MCP enrich-on-404 для битого spaceId (#534). Вердикт: changes-requested
Механизм отличный: enrich — fail-open HINT-слой, на ЛЮБОЙ неопределённости отдаёт ОРИГИНАЛЬНУЮ 404 (abort / non-404 / фейл
/spaces/ truncated / id-present), happy-path 0 доп.запросов, пагинация ограничена (MAX_PAGES=50, курсор строго от сервера, break на missing/repeat), сообщение «не найден среди доступных ТЕБЕ спейсов» корректно и для несуществующего, и для нет-прав (Variant A не врёт), утечки чужих спейсов нет. Гейт mcp 843/843 + новый сьют 11/11 (мутация rewrite→passthrough роняет 3 теста — не вакуумно). Security/regressions/stability/coherence/architecture/simplification/documentation — LGTM. Эскалаций нет.Открыто (оба мелкие, код корректен):
ERROR_MESSAGE_CAP.📋 F1, F2 — детали
F1 [warning · test-coverage] два fail-open гарда
withSpaceAccessDiagnosticsне покрыты —packages/mcp/src/client/context.ts:878,897Обе ветки выживают мутацию на чистой сборке (сьют остаётся 11/11):
:878if(!(isAxiosError && status===404)) throw e): заменить условие наfalse(500/403/network пошёл бы в/spaces-sweep + возможный rewrite) — не краснеет НИЧЕГО: ни один тест не подаёт non-404 ошибку в обёрнутый вызов С непустым spaceId. Это ровно регрессия, которую PR НЕ должен внести (проглотить/переформатировать реальный 500/403) — и она не заперта.:897if(idx.ids.has(spaceId)) throw e): убрать → не краснеет ничего, т.к. все enrich-тесты используют BAD (никогда не в SPACES),has()всегда false. Ветка, гасящая ложное «не найден среди доступных», когда спейс НА САМОМ ДЕЛЕ доступен, не прогоняется.Fix: 2 теста в
space-access-diagnostics.test.mjs: (a)getTree(BAD), где enumerate кидает non-404 AxiosError (status 500) при непустом spaceId → ассертить, что оригинальный 500 проходит без rewrite иc._spacesFetches===0; (b)getTree(SPACES[0].id), где enumerate кидаетmake404()а индекс СОДЕРЖИТ этот id → ассертить, что оригинальная 404 проходит (не/не найден среди/).F2 [suggestion · conventions] сообщение не соблюдает
ERROR_MESSAGE_CAP—packages/mcp/src/client/errors.ts:64-81Файл документирует и enforce'ит
ERROR_MESSAGE_CAP=300(errors.ts:16,193-194) с явной целью «model context stays compact и whitelisted серверная строка не раздует текст» — применяет вformatDocmostAxiosError. НоformatSpaceNotAccessibleинтерполирует до 10 (SPACE_LIST_CAP) серверныхnameбез лимита по ДЛИНЕ → ~10 длинноимённых спейсов дают строку в несколько раз больше 300ch. Единственное новое model-facing сообщение, игнорирующее собственный инвариант файла. Блэст-радиус мал (имена — свои же доступные спейсы, не атакер-инпут), потому suggestion.Fix: применить кап к собранной строке перед возвратом (переиспользовать slice-and-ellipsis из
formatDocmostAxiosError, либо усечь каждое имя).9 аспектов (свёрнуты в 3 агента + мой разбор крюкса). Гейт: mcp build чист, node:test 843/843 + новый 11/11 (мутация rewrite→3 краснеют). База develop, own delta 6 файлов/+661. Дерево после мутаций чисто. ⛔ кодеру НЕ делать: коммент
read.ts:190(парентеза о том, какой эндпоинт отдаёт 404) неточен, но функционально безвреден — below-threshold, не трогай.Обе находки закрыты (
46bb55db+ мерж develop). Внутренний цикл: 1 фикс-проход, оба guard'а подтверждены хирургичными мутациями.F1: fixed— два fail-open guard'аwithSpaceAccessDiagnosticsпокрыты. Добавил 2 теста вspace-access-diagnostics.test.mjs: (a) non-404 passthrough —getTree(BAD), enumerate кидает 500 при непустом spaceId → ассерт: оригинальная 500 проходит без rewrite И_spacesFetches===0(никакого /spaces-свипа — та самая регрессия, которую PR не должен внести); (b) id-present edge —getTree(SPACES[0].id), id ЕСТЬ в индексе, wrapped-вызов кидает 404 → ассерт: оригинальная 404 проходит (не «не найден среди»),_spacesFetches===1. Мутации хирургичны:status===404||true→ краснеет РОВНО non-404 тест; снятьif(idx.ids.has(spaceId))→ краснеет РОВНО id-present тест.F2: fixed—formatSpaceNotAccessibleтеперь соблюдаетERROR_MESSAGE_CAP=300: список спейсов режется по бюджетуCAP - prefix - suffix(prefix с spaceId и suffix с listSpaces всегда целы), 10 длинных имён → ≤300 с spaceId+listSpaces. Плюс (по ре-ревью) добавил безусловный финальный backstop-срез как вformatDocmostAxiosError— на патологически длинный агент-переданный spaceId (не валидируется по длине, живёт в prefix): без backstop'а он обошёл бы cap (5000-символьный spaceId → 5117), теперь всё сообщение ≤300 при любом входе.📋 Проверка
space-access-diagnostics.test.mjs14/14; полный mcp unit 704, mock 142. Backstop проверен: spaceId 5000 символов → сообщение ровно 300; нормальный вход (36-символьный UUID + 10 спейсов) → 300, spaceId и listSpaces целы.context.ts(сам механизм enrich) НЕ тронут — F1 test-only. node_modules 0. Не трогал комментread.ts:190(below-threshold). Влил develop.Ре-ревью — MCP enrich-404 (#534), раунд 2. Вердикт: approved ✅
Обе находки закрыты, проверил адверсариально:
space-access-diagnostics.test.mjs:172) — мутацияif(false)throw eна non-404-гарде → тест краснеет; (b) тест «404 when spaceId IS in the accessible index fails open» (:194) — мутацияif(false)на id-present-гарде → тест краснеет. Оба не вакуумны, стерегут ровно те ветки.formatSpaceNotAccessibleтеперь применяетERROR_MESSAGE_CAP(budget = CAP − prefix − suffix, усечениеlisted+ финальный кап всего сообщения,errors.ts:80-99). Эмпирика: 10 длинноимённых спейсов → сообщение ровно 300 (≤300 ✓, хвост усечён); короткое (1 спейс) → 135, имя цело.Гейт: mcp node:test 880/880 (было 843 + новые тесты). Дерево после мутаций чисто. Готово к мержу.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.