[meta][auth][mcp] API-ключи — фаза 3: api_key как единственная авторизация агентов + копируемый ключ + персона «MCP внешний» #556
Open
opened 2026-07-12 17:49:16 +03:00 by agent_vscode
·
0 comments
No Branch/Tag Specified
main
feat/557-apikey-phase3a
develop
feat/568-page-history-redesign
feat/561-comments-redesign
feat/530-search-semantic
feat/566-worktime-modal-redesign
feat/370-agent-save-version
recover/506-apikey-ui
feat/370-share-approved
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#556
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?
Проблема / Цель
После фаз 1 (#501, сервер, замержено) и 2 (#506, UI, открыто) api_key стал одним из способов авторизации агентов на
/mcp, но рядом остались костыли, ради устранения которых всё и затевалось:API_KEYS_ENABLED— лишняя сложность (строгий парс, boot-лог, 404-ветки). Ключи должны быть включены безусловно.MCP_DOCMOST_EMAIL/PASSWORD— «адский костыль», просочившийся в код: он и inbound-фолбэк на/mcp, и одновременно outbound-identity, под которой встроенный MCP «ходит обратно» в Docmost, костыльно привязывая правки к нужному пользователю. Плюс Basic-логин (bcrypt + анти-брутфорс-лимитер) и приём ACCESS-сессии человека на/mcp.Цель фазы 3:
/mcp— только для агентов, агент аутентифицируется исключительно api_key, который ему выпускает человек. Никаких Basic/сервис-аккаунтов/ACCESS на/mcp, ни следа сервис-аккаунта в коде. Ключ копируемый (детерминированный re-mint под step-up). Правки и комменты и внешних, и встроенных агентов идут через настоящую identity, а внешний MCP показывается отдельной персоной «MCP внешний» — по аналогии с тем, как встроенные агенты показывают имя своей роли.Контекст: единственный человек, homelab, single-workspace, агенты живут годами. Владелец: @vvzvlad.
Дочерние фазы (порядок)
API_KEYS_ENABLED+ копируемый ключ (детерминированный re-mint +revealпод step-up). Правит UI #506./mcpтолько api_key — полное удаление сервис-аккаунта, Basic-логина и ACCESS-пути + наблюдаемость на deny. Деплой только после миграции агентов на ключи.*_api_key_id).Технически A/B/C независимы (все опираются на уже смерженный #501), но все blocked by #506. Рекомендуемый порядок выката: сначала #557 и/или #559, затем #558 (breaking-удаление — только после того, как все агенты переведены на Bearer-ключи).
Контекст и исследование
Проверено по
develop(коммит фазы 1 —74d212c, PR #526):/mcp—resolveMcpSessionConfigв mcp-auth.helpers.ts, приоритет: 1) Basicemail:password, 2) Bearer (allowlist{ACCESS, API_KEY}), 3) env service-account, 4) 401. Обвязка — mcp.service.ts.{email,password}вDocmostMcpConfig, клиент делаетperformLogin)./mcp: он строит свой loopback-DocmostClientчерезgenerateAccessToken/generateCollabTokenпротив/api(ai-chat-tools.service.ts:307-353), не касается ни сервис-аккаунта, ни ACCESS-на-/mcp— удаление их его не ломает (подтверждено разведкой).expчерез выделенныйapiKeyJwtService(token.service.ts:150-177);ApiKeyService.validate(api-key.service.ts) — единый валидатор для REST и/mcp, ревокация немедленная (row-checkdeletedAtна каждом запросе).resolveAgentProvenance(agent-provenance.ts:62-93) уже имеет внешнюю-MCP ветку;AgentAvatarStack(agent-avatar-stack.tsx) умеет режим внешнего MCP; collab несётprincipal:'api_key'+apiKeyId. Единственное недостающее звено:resolveProvenanceметитactor='agent'только приuser.isAgent(auth-provenance.decorator.ts:30-38), поэтому api_key-правка обычного пользователя проваливается в'user'.DatabaseAuditService(audit.module.ts) — событияAPI_KEY_*персистятся.Дизайн (целиком; реализация фазируется)
Три PR-размерных изменения + этот мета-тикет. Полный жизненный цикл (выдача → использование → копирование → отзыв → атрибуция → удаление владельца) спроектирован здесь до старта любой фазы.
Фаза A (#557) — ядро api_key: снятие kill-switch + копируемый ключ. Убрать
API_KEYS_ENABLEDцеликом (без остаточных проверок и boot-WARN — торчащий=falseпросто игнорируется). Сделать api_key-токен детерминированным (noTimestamp:true→ нетiat→ токен = чистая функция(apiKeyId, sub, workspaceId, APP_SECRET)). НовыйPOST /api-keys/revealпод step-up (повторный ввод пароля), только владелец,rejectApiKeyPrincipal, throttle, аудит. UI #506: вместо «показать один раз» — кнопка «скопировать».Фаза B (#558) —
/mcpтолько api_key + удаление сервис-аккаунта/Basic/ACCESS.resolveMcpSessionConfig: оставить только Bearer, allowlist сузить до{API_KEY}. Вычистить весь мёртвый код (parseBasicAuth, FailedLoginLimiter, decideBasicGate, verifyBearerAccess, enforceBasicLoginGate, getEmail/getPassword и т.д.), env-переменные, доки, тесты — ни следа сервис-аккаунта. Оставить: пакетpackages/mcp{email,password}+ stdio-CLI (отдельный транспорт), всю провенанс/collab-машинерию (становится основным путём). Добавить наблюдаемость на deny (rate-limited WARN / счётчик) взамен удаляемого лимитера.Фаза C (#559) — персона «MCP внешний». Метить api_key-правки (
principal='api_key') как внешне-MCP-агента, протащитьapiKeyIdдо места записи (REST и collab), персистить новые колонкиcreated_api_key_id/last_updated_api_key_id, резолвить имя через joinapi_keys.name(фолбэк «MCP внешний»).Модель состояний ключа (дополнение к #501)
deletedAt IS NULL, не истёкexpiresAt < nowdeletedAt)api_keys.creator_id onDelete cascade)Границы scope
APP_SECRETкак процесс; правка stdio-CLI пакетаpackages/mcp(отдельный транспорт, сервис-аккаунт там легитимен — не трогаем); новые продуктовые метрики сверх счётчика deny.Критерии приёмки (высокоуровнево; детальные — в фазах)
/mcpc Basic / ACCESS-сессией / без заголовка (в т.ч. при выставленныхMCP_DOCMOST_*) →401; c валидным api_key Bearer → работает.grepпоMCP_DOCMOST_EMAIL|MCP_DOCMOST_PASSWORD|API_KEYS_ENABLED|FailedLoginLimiter|parseBasicAuth|enforceBasicLoginGate|verifyBearerAccessвapps/server→ пусто (кроме исторического CHANGELOG).exp, ниiat; дваrevealодного ключа → байт-в-байт одинаковый токен.revealтребует step-up; api_key-принципал наreveal→403; чужой/revoked/expired ключ → uniform404.Отказы и наблюдаемость
/mcp— единый generic-401 (анти-enumeration инвариант #501 сохраняется).(apiKeyId, reason)вvalidate()или на/mcp-bearer-catch (иначе мёртвый ключ, долбящий/mcp, невидим). Детали в #558.reveal— durable audit (DatabaseAuditService, реальный с #496) + throttle; step-up отсекает угон через компрометированную сессию.revokeключей + workspace-тогглai.mcpгасит/mcp. Глобального рубильника для REST-api_key нет — принято владельцем (homelab, ротацияAPP_SECRET/рестарт — крайняя мера).Совместимость и миграции
/mcpломает существующие MCP-сетапы наemail:passwordи наMCP_DOCMOST_*. Порядок выката (обязателен): (1) #506 смержен (UI выдачи ключей) → (2) выпустить ключи всем агентам и переключить их конфиги наAuthorization: Bearer <key>→ (3) выкатывать фазу B (#558). Приём api_key Bearer уже работает с #501, поэтому окна 401 при переключении конфигов до деплоя B нет.created_api_key_id(comments),last_updated_api_key_id(pages, page_history) сonDelete('set null'). Миграция аддитивная, без блокировок горячих таблиц (nullable-колонки без бэкофилла).API_KEYS_ENABLEDудаляется целиком; деплой на инстанс, где стоял=false, молча включит ключи — приемлемо (homelab, дефолт всегда был ON).Решённые развилки
ai.mcp-тоггл уже гасит/mcp)./mcpACCESS-сессия человека → удалить (/mcpтолько для агентов; человек при желании заводит себе api_key).noTimestamp), токен-материал в БД не хранится; revoke по-прежнему soft-delete (владелец).ProvenanceSourcevs переиспользовать'agent'→ переиспользовать'agent'+ дискриминатор поapi_key_id(внешняя ветка резолвера уже существует; клиент уже ветвится наsource==='agent'). Деталь — в #559.Связи