[feat][auth][mcp] API-ключи фаза 3B: /mcp только api_key — эксгумация сервис-аккаунта, Basic-логина и ACCESS-пути #558
Open
opened 2026-07-12 17:51:47 +03:00 by agent_vscode
·
0 comments
No Branch/Tag Specified
main
feat/370-share-approved
develop
feat/568-page-history-redesign
feat/557-apikey-phase3a
feat/561-comments-redesign
feat/530-search-semantic
feat/566-worktime-modal-redesign
feat/370-agent-save-version
recover/506-apikey-ui
fix/520-recovery-escalation
fix/554-literal-br
feat/529-search-lexical
feat/501-mcp-apikey-auth
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
fix/525-insert-unloaded
fix/522-internal-links
fix/507-drawio-cyrillic
fix/541-reconnect-timeout
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#558
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?
Фаза B мета-ишьи #556. Blocked by #501 (merged) и #506. Деплой — только после миграции агентов на ключи (см. «Совместимость»).
Проблема / Цель
/mcpходят только агенты, и агент должен аутентифицироваться исключительно api_key, который ему выпустил человек. Сейчас на/mcpчетыре пути (Basicemail:password, Bearer ACCESS-сессия человека, Bearer api_key, env service-accountMCP_DOCMOST_EMAIL/PASSWORD). Сервис-аккаунт — «адский костыль»: он и inbound-фолбэк, и outbound-identity, под которой встроенный MCP костыльно «ходит обратно» в Docmost, привязывая правки к пользователю. Цель: оставить на/mcpтолько Bearer api_key, а весь остальной путь и сервис-аккаунт выпилить без следа — ни мёртвого кода, ни env, ни доков, ни намёка, что сервис-аккаунт когда-либо был.Контекст и исследование
Inbound-приоритет
resolveMcpSessionConfig(mcp-auth.helpers.ts): 1) Basic, 2) Bearer{ACCESS, API_KEY}, 3) service-account, 4) 401. Обвязка — mcp.service.ts. Разведка дала полный file:line инвентарь (ниже). Ключевые границы:/mcp(loopback/apiчерезgenerateAccessToken/generateCollabToken, ai-chat-tools.service.ts:307-353).{ACCESS, API_KEY}), поэтому переключение конфигов агентов на Bearer до деплоя B не даёт окна 401.packages/mcp(context.tsветка{email,password}+performLogin+ stdio-CLI) — это отдельный транспорт, сервис-аккаунт там легитимен; всю провенанс/collab-машинерию (resolveProvenance,JwtCollabPayload.principal/apiKeyId,authentication.extension,generateCollabToken) — она становится основным путём; ядровые примитивыAuthService.verifyUserCredentials,CREDENTIALS_MISMATCH_MESSAGE,TokenService.verifyJwtOneOf; в helpers —sharedTokenMatches,mapAuthResultToResponse,extractBearer,onModuleDestroy.Дизайн
1.
resolveMcpSessionConfig→ только Bearer api_keybindMcpBearerVerifier(:336-341) — allowlist сузить с{ACCESS, API_KEY}до{API_KEY}.verifyMcpBearer(:371-403) — оставить только ветку API_KEY, удалить ACCESS-ветку (вызовverifyBearerAccess).McpBearerDepsсхлопнуть до{ verifyJwtOneOf, validateApiKey, expectedWorkspaceId }(убратьOmit<BearerVerifyDeps,'verifyJwt'>,findUser,findActiveSession).2. Удалить мёртвый код (эксгумация)
mcp-auth.helpers.ts— DELETE:parseBasicAuth(:19-38),FailedLoginLimiter(класс целиком :51-131),isCredentialsFailure(:222-248),ClientIpRequest+clientIp(:275-305),isInitializeRequestBody(:487-507),BasicGateMfaResult(:568-573),decideBasicGate(:575-612),BearerVerifyDeps(:405-434),verifyBearerAccess(:436-485), ветка{apiUrl;email;password}серверногоDocmostMcpConfig(:137-139), поляemail?/password?/enforceBasicGate?/login/verifyCredentials/limiter/clientIp/isSessionInitвMcpAuthDeps(:174-220). Мёртвый импортCREDENTIALS_MISMATCH_MESSAGE(:10) иisInitializeRequestиз SDK (:8). Комментарии/JSDoc про precedence и service-account (:134, :623-633) — переписать.mcp.service.ts— DELETE:warnedMissingCreds(:88),getEmail/getPassword/credsConfigured(:166-183), warn-блок про сервис-аккаунт (:491-500),failedLogins+sweepIntervalMs+sweepTimer+setIntervalв конструкторе (:93,98-100,116-124) иclearInterval(:128),enforceBasicLoginGate(метод :284-349), вresolveSessionConfig—isSessionInitи depsenforceBasicGate/login/verifyCredentials/limiter/clientIp/isSessionInit/email/password(оставитьapiUrl,findWorkspace,verifyAccessJwt), вverifyMcpBearer-обвязке —findUser/findActiveSession(:229-232). Мёртвые импорты:FailedLoginLimiter(:21),isInitializeRequestBody(:24),clientIp(:26),decideBasicGate(:28),validateSsoEnforcement(:16),Workspace(:18),ModuleRef(:7). Из DI-конструктора убратьuserRepo(:106),userSessionRepo(:107),moduleRef(:108),authService(:104) — разведка подтвердила: после удаления ACCESS/Basic каждый из них используется только в удаляемых путях (workspaceRepo/tokenService/apiKeyService/sandboxStoreостаются). Проверить и почистить провайдеры вmcp.module.ts.3. Наблюдаемость на deny (замена удаляемого лимитера)
Важно (адверсариальный ревью): обещанного в #501 «rate-limited WARN per (apiKeyId, class)» в коде нет —
ApiKeyService.validateкидаетUnauthorizedExceptionбез лога и метрики, аMcpService.handleнамеренно не логируетUnauthorizedException. После удаления Basic-лимитера мёртвый/отозванный ключ, долбящий/mcp, станет полностью невидим. Добавить: rate-limited структурный WARN и/или счётчикapi_key_auth_denied_total{reason}вApiKeyService.validate(или на/mcp-bearer-catch), ключ агрегации —(apiKeyId, reason),apiKeyIdв лог-путь попадает только после верификации подписи (кардинальность ограничена числом выданных ключей). Generic-401 в теле ответа сохраняется (WARN — только в логах оператора).4. Env / доки
.env.example: удалитьMCP_DOCMOST_EMAIL/MCP_DOCMOST_PASSWORD(:116-117) и переписать описание способа аутентификации (:105-110) под «только api_key Bearer». ОставитьMCP_DOCMOST_API_URL(:118, outbound loopback URL) иMCP_TOKEN.AGENTS.md: переписать :453 и :325 (убрать creds-переменные из списка).CHANGELOG— добавить Breaking-запись.5. Тесты
DELETE целиком:
mcp-basic-login-gate.spec.ts,mcp-login-gate-coupling.contract.spec.ts. Вmcp.service.spec.ts— удалить describe-блокиparseBasicAuth,isCredentialsFailure+coupling,FailedLoginLimiter,verifyBearerAccess, Basic-кейсы и service-account-кейс вresolveMcpSessionConfig,isInitializeRequestBody/isSessionInit/non-initialize-side-effects,clientIp,decideBasicGate; переписатьbindMcpBearerVerifier-тест на{API_KEY}, оставить только API_KEY-кейсыverifyMcpBearer, поправитьonModuleDestroy-тест (нетsweepTimer). KEEP:sharedTokenMatches,mapAuthResultToResponse,extractBearer, провенанс/collab-специи (вauthentication.extension.spec.tsпоправить комментарий про «MCP/service-account collab edit» на :235). Добавить тест:/mcpс Basic / с ACCESS-JWT / без заголовка → 401; с api_key → ок.Границы scope
/mcp+ вычистка. Ядро api_key (reveal, kill-switch) — #557. Персона — #PHASE_C.packages/mcpstdio-CLI.Критерии приёмки
/mcpcAuthorization: Basic ...→401; c валидным ACCESS-сессионным JWT →401; без заголовка при выставленныхMCP_DOCMOST_EMAIL/PASSWORD→401(нет service-account фолбэка); c валидным api_key Bearer → работает./mcp).grep -rE 'MCP_DOCMOST_EMAIL|MCP_DOCMOST_PASSWORD|FailedLoginLimiter|parseBasicAuth|enforceBasicLoginGate|verifyBearerAccess|decideBasicGate|service-account' apps/server→ пусто (кроме CHANGELOG)./mcp— единый generic-401 (анти-enumeration)./mcpN раз → в логах оператора rate-limited WARN сapiKeyIdи/или счётчик deny инкрементится (не тишина).Отказы и наблюдаемость
Совместимость и миграции
email:passwordи деплои наMCP_DOCMOST_*. Порядок (обязателен): #506 смержен → выпустить ключи всем агентам (UI/create) и переключить их конфиги наAuthorization: Bearer <key>→ затем деплой B. Приём api_key Bearer работает с #501 → окна 401 нет.Решённые развилки
/mcpACCESS-сессия человека → удалить (только агенты; человек заводит api_key). Разведка подтвердила отделимость от service-account/Basic-вычистки; здесь удаляем вместе (единая цель «только api_key»).Связи
/mcp= агенты-only).