fix(mcp): невалидный/недоступный spaceId — внятная ошибка вместо «Space permissions not found» #534
Open
opened 2026-07-12 02:02:16 +03:00 by agent_vscode
·
0 comments
No Branch/Tag Specified
main
feat/529-search-lexical
feat/506-apikeys-ui
feat/395-work-time
feat/502-mcp-md-modes
feat/492-incremental-render
fix/549-hardbreak-md-canon
feat/542-resolve-undo
fix/505-drawio-strip-comments
fix/503-hardbreak-canon
docs/471-breadcrumb-permission
fix/534-mcp-spaceid
feat/481-version-coherence
fix/535-list-seam-coalesce
develop
fix/520-recovery-escalation
fix/525-insert-unloaded
fix/522-internal-links
fix/507-drawio-cyrillic
fix/541-reconnect-timeout
feat/501-mcp-apikey-auth
feat/493-converter
feat/370-page-versioning
fix/515-code-emphasis
fix/523-tree-dnd
fix/517-toasts
feat/495-tails
feat/494-mcp-registry-guards
feat/496-suggestions-audit
integ/497-wave1
feat/488-client-fsm
ci/476-publish-gates
feat/489-tool-resilience
feat/487-terminal-lifecycle
fix/486-iter1-prod-fires
refactor/347-client-md-paste
perf/479-getpage-cache
refactor/449-write-invariants
refactor/450-split-client
feat/443-get-page-context
feat/443-get-tree
feat/443-search
docs/415-lossy-descriptions
feat/413-markdown-default
refactor/412-camelcase-tool-names
feat/425-drawio-graph
fix/464-diffdocs-cpu-guard
refactor/411-update-page-markdown
feat/424-drawio-rules
fix/409-invalid-node-validation
refactor/448-generate-inventory
refactor/445-execute-mapping
refactor/446-derive-client-types
fix/447-registry-stamp-ci
fix/444-agent-loop-guards
fix/396-sendnow-detached-run
fix/452-immutable-cache-control
docs/footnote-authoring-comment-cleanup
feat/437-error-diagnostics
fix/442-cursor-pagination
feat/419-footnote-normalize
perf/399-comment-resolve-async
perf/435-collab-token-cache
perf/344-background-rerenders
perf/348-backend-lowhanging
feat/423-drawio-crud
perf/401-collab-hotpath
feat/430-live-reconnect
perf/400-collab-session
refactor/414-dedup-node-ops
feat/417-new-comments-signal
feat/408-createcomment-hints
feat/407-persist-tool-errors
feat/420-footnote-render
dummy-review/mcp-hang-fix
dummy-review/mcp-hang-base
feat/381-resumable-sse-pr1
feat/git-sync-2
docs/agents-workspace-build-order
feature/offline-sync
perf/342-code-splitting
perf/346-compression-cache
feat/196-multi-cursor
perf/343-typing-latency
docs/how-to-test
fix/ai-sdk-partial-output-oom
test/351-generative-converter
feat/371-roles-catalog
refactor/345-server-converter
refactor/294-spec-registry-cont
fix/363-migration-order
fix/e2e-callout-and-gate-build
fix/docker-re2-toolchain
feat/git-sync
fix/media-roundtrip-stability
fix/340-comment-panel-perf
fix/332-deferred-tools
fix/329-ephemeral-suggestions
fix/330-search-in-page
fix/328-resolved-anchor-spam
fix/331-intraline-diff
fix/324-coverage-gate
fix/325-mobile-390
feat/293-A-git-sync-package
feat/300-avatar-oklch
fix/321-banner-mobile
feat/300-avatar-colors
feat/315-comment-suggestions
feat/scroll-restore-stable-wait
feat/300-agent-avatar-stack
feat/300-avatar-polish
refactor/294-tool-spec-registry
feat/scroll-restore-ux
fix/responsive-tablet-sidebar
feature/ai-chat-page-change-observability
image-inline-center
fix/283-short-remap-title
fix/283-slash-layout
image-inline-row
feat/276-ai-chat-dock
fix/269-table-menu-refocus
docs/dev-stand-guide
feat/266-scroll-position
fix/260-collab-docname-slugid
test/244-phase2-tail
fix/262-reindex-progress-realtime
fix/258-changelog-compare-links
fix/244-dataloss-bugs
feat/246-spoiler
feat/221-image-captions
test/244-part-b
feat/251-intentional-clear
fix/embeddings-reindex-progress
refactor/193-tool-spec-registry
fix/255-ws-redis-adapter-leak
fix/252-e2e-open-handles
feat/229-catalog-yaml
feat/243-blob-sandbox
feat/228-inline-footnotes
fix/qa-ui-bugs-216-218
feature/agent-roles-catalog
fix/share-alias-rename
fix/ai-chat-empty-render
feat/191-chat-doc-binding
feat/201-temporary-notes
feat/198-interrupt-agent
feat/ai-chat-full-history
feat/199-ai-generate-title
feat/205-share-aliases
batch/issues-189-187-170
feat/170-mcp-test-button
feat/189-context-badge
feat/198-interrupt-agent-send-now
fix/issues-190-159
fix/ai-chat-new-chat-during-stream
fix/ai-chat-stream-perf
batch/issues-2026-06-25
feat/ai-chat-persistent-history
fix/ai-chat-copy-chat-wysiwyg
fix/ai-stream-reset-resilience
fix/ai-stream-undici-timeout
fix/footnote-review-1227-followup
fix/ai-chat-token-counter-realtime
docs/manual-qa-test-plan
v0.94.1
v0.94.0
v0.93.0
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
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#534
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 "%!s()"
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?
Проблема / Цель
MCP-инструменты, принимающие
spaceId(getTree,listPages,checkNewComments,search), при передаче валидного по форме, но несуществующего или недоступногоspaceIdотдают агенту:POST /pages/sidebar-pages failed (404 Not Found): Space permissions not found.Формулировка вводит в заблуждение: в типичном случае причина — опечатка/устаревший id (UUID правильной формы, но такого спейса у пользователя нет), а текст говорит «нет прав», будто спейс существует и доступ отобрали. Агент по такому сообщению не может себя починить — не понимает, что надо взять правильный id из
listSpaces.Репортед-кейс:
getTreeс id…59e81b3b8863(одна hex-цифра мимо реального…59e81b3c8863) → это самое 404-сообщение.«Готово» = на битый/недоступный
spaceIdинструмент возвращает сообщение вида «такого spaceId среди доступных тебе нет; вот валидные id; возьми из listSpaces».Контекст и исследование
Развилка «в каком слое чинить» решена человеком → вариант A: только обёртка
packages/mcp(бэкендspace-ability.factoryне трогаем; форк Docmost не дивергируем).apps/server/src/core/casl/abilities/space-ability.factory.ts:20-37—getUserSpaceRoles(user, spaceId); если ролей нет →default: throw new NotFoundException('Space permissions not found'). Для несуществующего spaceId ролей тоже нет → тот же путь. Бэкенд физически не различает «спейса нет» и «есть, но не участник».packages/mcp/src/client/context.ts:312-318→client/errors.tsformatDocmostAxiosError(#437/#450) дословно проксирует backend-текст как<METHOD> <path> failed (<status> <statusText>): <serverMessage>. Интерцептор синхронный и безthis— обогатить из него нельзя, поэтому обогащаем на уровне методов клиента.assertFullUuid(client/errors.ts:35, #436) — но он проверяет форму UUID; наш…b8863формально валиден, проверки формы недостаточно./spaces(getSpaces→paginateAll('/spaces'),client/read.ts:96). Проверено по бэкенду:/spaces(getUserSpaceIdsQuery,space-member.repo.ts) иspace-ability.factory(getUserSpaceRoles) оба берут объединение прямого + группового членства → «spaceId отсутствует в /spaces» ⇔ «space-scoped вызов 404-ит по членству». Оракул согласован.spaceId. ИхpageId/rootPageId/parentPageId-ветки возвращают 403 (page.controller.ts:566sidebar,:608tree) или 200 — не 404. Поэтому на каждом enrich-eligible 404 виноват именноspaceId.getTree/checkNewCommentsидут черезenumerateSpacePages(read.ts:327): primary/pages/tree404 проглатывается как «эндпоинт отсутствует» → BFS-fallback → seedinglistSidebarPages(read.ts:377, не обёрнут в try/catch) снова бьётcreateForUser(user, spaceId)(page.controller.ts:572) → 404 пробрасывается. Ровно это в транскрипте.createPageИСКЛЮЧЁН: его сервер 404-ит по parent-mismatch (page.controller.ts:220,'Parent page not found') до проверки прав (:227) — 404 мульти-причинный, обогащение соврало бы. (Клиентpages.ts:78ещё и пред-валидирует parentPageId через getPage и бросает обычный Error — тоже не наш axios-404.)formatDocmostAxiosErrorмутирует только.message, сохраняяaxios.isAxiosErrorиerror.response.status(комментerrors.ts:110-119) → 404 после форматирования детектируется по статусу, без привязки к строке «Space permissions not found» (R3: не создаём хрупкое зеркало backend-текста).pageIdCache,collabTokenCacheвcontext.ts), с инвалидацией вlogin()— тем же паттерном кэшируем список спейсов.Дизайн
Механизм — обогащение ошибки на 404 (enrich-on-404) на уровне методов клиента. Happy-path не трогается: при валидном spaceId дополнительных запросов нет.
1. Индекс доступных спейсов (на базе
DocmostClientContext):getAccessibleSpaceIndex(): Promise<{ ids: Set<string>; spaces: {id:string; name:string}[]; complete: boolean }>paginateAllWithMeta('/spaces'), возвращающий{items, truncated}(сейчасpaginateAllглотаетtruncated, только warn'ит — нужен вариант/overload, отдающий флаг наружу;getSpaces()НЕ меняем, чтобы не ломать выхлоп тулаlistSpaces).spaces=items.map(filterSpace)(уfilterSpaceестьidиname,filters.ts:19).complete = !truncated.process.env.MCP_SPACES_CACHE_TTL_MS(паттернreadCollabTokenTtlMs,context.ts:114);0= кэш выключен. Инвалидация вlogin()и при сбросеthis.token(какcollabTokenCache,context.ts:281,414). Кэшируется только результат сcomplete:true.loginPromise,context.ts:394): ДВА разных поля — in-flightPromiseи persistentcachedIndex. Инвариант (критично): in-flight зануляется в.finallyв т.ч. при reject (никогда не мемоизировать отклонённый/settled промис — иначе сетевой блип/spacesотравит фичу до конца сессии);cachedIndexпишется ТОЛЬКО из резолвнутого fetch.2. Обёртка-диагностика (на базе):
withSpaceAccessDiagnostics<T>(spaceId: string, mcpName: string, fn: () => Promise<T>): Promise<T>Обернуть тела клиентских методов:
getTree,listPages(и recent-with-spaceId, и tree-mode),checkNewComments,search(когдаspaceIdзадан). НЕ оборачивать:createPage(мульти-причина),getPageContext(принимает pageId).Abort-детект (важно, hole B): прерывание определяем по
this.toolAbortSignal?.aborted === true, НЕ поe.name === 'AbortError'— cap может бытьAbortSignal.timeout()(reason =TimeoutError) или кастомный reason, имя не гарантировано (context.ts:184-190,650).Текст (FACT про spaceId, не причина сбоя):
<mcpName>: spaceId "<id>" не найден среди доступных тебе спейсов. Доступные: <id1> (<name1>), <id2> (<name2>), … — скопируй нужный id дословно из listSpaces.(+N ещё, см. listSpaces).<mcpName>: spaceId "<id>" недоступен, и доступных тебе спейсов нет — проверь доступ токена / вызови listSpaces.NotFoundExceptionв/pages/tree|recent|sidebar-pagesили/search— вернётся misattribution).Где живёт канон (R3): единственный источник доступности —
/spaces. Backend-строку не парсим/не дублируем; детект по HTTP-404.Семантика отказов (R4): диагностика — ХИНТ, не контроль доступа; авторитет — бэкенд. Обёртка fail-open на любой неопределённости (fetch упал /
!complete/ spaceId в списке / abort) → отдаёт исходную серверную ошибку без изменений. Реальные ошибки никогда не подавляются.Policy-константы (R7): (a) лимит перечисления = 10 (компактность; полный список — listSpaces); (b) TTL кэша спейсов = 60 000 мс, env
MCP_SPACES_CACHE_TTL_MS,0=выкл (<< длины сессии; ограничивает и устаревание хинта, и нагрузку на/spaces; это лишь текст ошибки, не безопасность — сервер остаётся авторитетным).Границы scope
space-ability.factory.tsкак есть) — вариант A.createPage(мульти-причинный 404) иgetPageContext(pageId). Внятная ошибка на битый spaceId вcreatePage— возможная отдельная ишья.parentPageId/pageId/rootPageId— толькоspaceId./search, вероятно, на битый spaceId отдаёт 200+пусто, а не 404 → там обёртка чаще инертна; это ок (fail-open).getSpaces()/listSpaces.Критерии приёмки
Проверяемо юнит-тестом со стаб-клиентом (как
test/unit/error-diagnostics.test.mjs) и/или на стенде:getTreeс валидным по форме, но несуществующимspaceId(реальный id с заменой одной hex-цифры) → ошибка тула, в тексте: переданный spaceId, ≥1 валидныйid (name), словоlistSpaces; и нет подстрокиSpace permissions not found.getTreeс валидным доступнымspaceId→ дерево как раньше (регрессии нет); на happy-path не выполняется запрос/spaces(спай наclient.post).getTreeс тем же битымspaceIdв пределах TTL → повторного/spacesfetch нет (кэш); по истечении TTL — один рефетч.listPages/searchбезspaceId→ поведение не изменилось (обёртка не срабатывает).!complete(обрезанный/spaces, застабатьtruncated=true) на 404 → исходная серверная ошибка (fail-open), без ложного «не найден».getAccessibleSpaceIndexбросает (сеть/spaces) во время обогащения → исходная серверная ошибка; приDEBUG— строка в stderr. In-flight промис после reject занулён (следующий вызов делает свежий fetch, не переиспользует отклонённый)./spaces→[]) + 404 → спец-сообщение «доступных тебе спейсов нет».toolAbortSignal.aborted===trueво время обогащения → пробрасывается abort/cap-reason, а не переписанный/исходный 404 (reason сигнала —TimeoutErrorили кастомный, неAbortError).createPageс недоступнымspaceId→ серверная ошибка как есть (не обёрнут).Отказы и наблюдаемость
/spacesупал /!complete) → агент/оператор видит РОВНО исходную серверную ошибку (текущее поведение); подDEBUG— stderr-строка (какerrors.ts:138). Ничего не маскируется.0(env), полный откат = revert коммита.Совместимость и миграции
…404: Space permissions not found, стал внятный). Успешные вызовы и любые другие ошибки — без изменений. Дефолты поведения не меняются.Space permissions not foundот MCP, увидит новый текст. Оценка: низкий (диагностическое сообщение для агента, не контракт; голден-тесты этой строки не матчат). Отметить в PR.MCP_SPACES_CACHE_TTL_MS(необязательный, дефолт 60 000). Миграций нет.Решённые развилки
packages/mcp— решил человек./spacesна горячем error-пути (циклящий агент × N); ограниченное окно устаревания хинта приемлемо (подтвердили критики).createPage→ исключён (мульти-причинный 404).Связи
client/errors.ts,client/context.ts).space-ability.factory); внятная ошибка на битый spaceId дляcreatePage.Дизайн прошёл adversarial self-attack (3 критика: hostile-scenarios / implementer-dry-run / failure-ops) + раунд переатаки. Закрыто: мнимая «dead code getTree», thundering-herd на
/spaces, stale-кэш → ложное «не найден», мульти-причинный 404 createPage, blocking-пробел сtruncated, abort-детект по имени reason.