Document Variant B for showing MCP-created comments (and pages) as AI rather than as the service-account user, reusing the existing agent provenance infrastructure (§15 C3). - Root cause: MCP logs in via a plain service-account token, so provenance.actor stays 'user' and created_source defaults to 'user'; the comment sidebar also renders no AI badge. - B1 (backend): mark the MCP identity as agent via a new users.is_agent flag; jwt.strategy derives req.raw.actor from it (non-spoofable). Relax the provenance aiChatId type to string | null for external MCP. - B2 (frontend): extend IComment with createdSource/aiChatId, extract a shared AiAgentBadge, render it in comment-list-item. - Includes edge cases, tests, scope decisions, and acceptance criteria.
16 KiB
Атрибуция комментариев (и записей) от MCP как «AI», а не как пользователь
Статус: открыто (дизайн). Сейчас комментарии, созданные через MCP-инструмент,
показываются как комментарии обычного пользователя (сервис-аккаунта, под которым
залогинен MCP). Нужно, чтобы они показывались как комментарии от AI. Инфраструктура
agent-провенанса (§15 C3) в проекте уже наполовину построена — задача переиспользует
её, а не строит заново.
Цель
Комментарий, созданный/зарезолвленный через MCP, на фронтенде помечается AI-бейджем (как версии страниц в истории), а не выглядит как комментарий обычного участника. Пометка должна быть неподделываемой (выводиться сервером из идентичности, а не из тела запроса) и аддитивной (человек/сервис-аккаунт-автор остаётся, бейдж добавляется рядом).
Текущее состояние (почему сейчас «от пользователя»)
- Сервер умеет ставить маркер.
apps/server/src/core/comment/comment.service.ts(~стр. 88–92) приprovenance.actor === 'agent'пишет в комментарийcreatedSource: 'agent'+aiChatId; иначе колонка остаётся в дефолте'user'. АналогичноresolveComment(~стр. 235–244) ставитresolved_source = 'agent'. provenance.actorберётся только из подписанного JWT. Декораторapps/server/src/common/decorators/auth-provenance.decorator.tsчитаетrequest.raw.actor, который выставляется вapps/server/src/core/auth/strategies/jwt.strategy.ts(~стр. 80–81) из claimactorтокена. Сделано намеренно, чтобы обычный пользователь не подделал бейдж.- MCP логинится как обычный сервис-аккаунт. stdio-вариант
(
packages/mcp/src/stdio.ts:38-39) создаётDocmostClientпоemail/password(packages/mcp/src/client.ts:99-106) → обычныйPOST /auth/login→ access-токен без claimactor. Ветка API-ключа вjwt.strategy.ts(~стр. 45–47, 86–110) тоже не выставляетactor. Итог:provenance.actor = 'user'→created_source = 'user'→ комментарий выглядит как от пользователя. - В сайдбаре комментариев бейдж не рисуется. Репозиторий уже отдаёт
createdSourceна фронт (selectAll('comments')вapps/server/src/database/repos/comment/comment.repo.ts:34-49), но клиентский типIComment(apps/client/src/features/comment/types/comment.types.ts) его не описывает, аapps/client/src/features/comment/components/comment-list-item.tsx(~стр. 127–162) показывает толькоcomment.creator.name. AI-бейдж сейчас рендерится только в истории страниц —apps/client/src/features/page-history/components/history-item.tsx(компонентAiAgentBadge, иконкаIconSparkles, метка «AI-agent»,lastUpdatedSource === "agent").
Колонки БД для этого уже существуют (миграция
apps/server/src/database/migrations/20260616T130000-agent-provenance.ts:
comments.created_source дефолт 'user', comments.ai_chat_id nullable,
comments.resolved_source nullable). Новых колонок на стороне комментариев не нужно.
Дизайн
Два независимых куска: бэкенд (проставить провенанс для MCP-идентичности) и фронтенд
(отрисовать бейдж). Они стыкуются через уже отдаваемое поле createdSource.
B1. Бэкенд — пометить MCP-идентичность как «agent» (неподделываемо)
Принцип: пометка выводится из идентичности на сервере, а не передаётся клиентом.
Помечаем сам сервис-аккаунт MCP как агентский — тогда все его записи (комментарии,
а также страницы через уже существующий provenance в page.service.ts) автоматически
атрибутируются AI, без правок в теле запроса.
- Флаг агентской идентичности на пользователе. Добавить булеву колонку (например
users.is_agent, дефолтfalse) отдельной аддитивной миграцией. Не переиспользоватьrole(у него семантика авторизации) и не прятать флаг вsettings(нужен дешёвый фильтр и явность). Обновить типUsersвapps/server/src/database/types/db.d.tsи сущностьUser.- Эксплуатация: для MCP завести отдельный сервис-аккаунт и выставить ему
is_agent = true. Не помечать обычных людей.
- Эксплуатация: для MCP завести отдельный сервис-аккаунт и выставить ему
- Проставление
actorв JWT-стратегии. Вapps/server/src/core/auth/strategies/jwt.strategy.tsпосле загрузкиuser(в ACCESS-веткеvalidate, и зеркально вvalidateApiKey, если MCP когда-то перейдёт на API-ключ) выставлять:Внешний MCP не связан с внутренним// Derive provenance from the SIGNED identity, never from a client field: // an account flagged is_agent stamps every write as 'agent'. req.raw.actor = user.isAgent ? 'agent' : ((payload as JwtPayload).actor ?? 'user'); req.raw.aiChatId = (payload as JwtPayload).aiChatId ?? null; // null for external MCPai_chats, поэтомуaiChatIdостаётсяnull— колонкаcomments.ai_chat_idnullable, FKON DELETE SET NULL, это валидно. - Ослабить тип provenance, где он требует
aiChatId: string. Сейчасapps/server/src/core/auth/services/token.service.ts(~стр. 37, 61) и спред вcomment.service.tsисходят из непустогоaiChatId. Для внешнего MCP нуженaiChatId: string | null. Декоратор уже возвращаетaiChatId: ... ?? null, так что правка — это только смягчение типа в цепочкеprovenance(тип-уровень), а не логики. ЗаписьcreatedSource: 'agent', aiChatId: nullв БД корректна.
Почему именно идентичность, а не per-request флаг: (а) неподделываемо «по построению» —
обычный пользователь не сможет получить токен агентской учётки; (б) одной точкой
покрывает и комментарии, и страницы (page.service.ts уже читает provenance для
create/rename/move — стр. ~138/234/446/952), то есть MCP-страницы начнут показывать
AI-бейдж в истории без доп. фронтенд-работы.
Альтернатива (отклонена): заставить MCP чеканить provenance-токены, как это делает
внутренний AI-чат (token.service.generateAccessToken(..., {actor:'agent', aiChatId}),
см. apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts:73). Для внешнего MCP
это тяжелее: он ходит через performLogin, у него нет подписывающего секрета сервера, и
provenance всё равно пришлось бы привязать к идентичности. Идентичность-флаг проще и
покрывает оба транспорта.
B2. Фронтенд — показать AI-бейдж в сайдбаре комментариев
- Расширить тип. Добавить в
IComment(apps/client/src/features/comment/types/comment.types.ts) поляcreatedSource?: string,aiChatId?: string | null,resolvedSource?: string | null(бэкенд их уже отдаёт черезselectAll). - Вынести общий бейдж. Сейчас
AiAgentBadgeлокальный внутриhistory-item.tsx. Вынести его в переиспользуемый компонент (напримерapps/client/src/components/ui/ai-agent-badge.tsx) с опциональнымaiChatId: когдаaiChatIdесть — кликабельный deep-link в чат (поведение истории), когдаnull(внешний MCP) — просто метка. Существующая реализация уже корректно ведёт себя приaiChatId == null(нет курсора/клика). - Отрисовать в
comment-list-item.tsxрядом сcomment.creator.name(~стр. 129–131):{comment.createdSource === "agent" && ( <AiAgentBadge authorName={comment.creator?.name} aiChatId={comment.aiChatId} /> )} - (Опционально, в том же объёме) «Resolved by AI». Поскольку
resolved_sourceуже пишется, аналогичный маркер можно показать у строки «resolved» вresolve-comment.tsx/ шапке треда. Вынести в отдельный подпункт, если объём растёт.
Краевые случаи и тонкие места
aiChatId = nullу внешнего MCP — бейдж некликабелен, FK nullable; проверить, что ни сервер (спред вcomment.service), ни фронт (deep-link) не падают на null.- Неподделываемость — инвариант «
actorтолько из серверной идентичности/подписанного claim, никогда из тела запроса» обязан сохраниться; покрыть тестом, что обычный пользователь не получаетcreated_source='agent'. - Живое обновление — WS-событие
commentCreatedнесёт весь объект комментария (сcreatedSource), значит бейдж появится без перезагрузки. Проверить, что поле не теряется на пути WS → стор. - Уведомления/watchers — автор остаётся сервис-аккаунтом (
creatorId), нотификации работают как раньше; решить, нужно ли вообще слать уведомления о комментариях от AI (по умолчанию — оставить как есть). - Резолв человеком комментария от AI и наоборот —
resolved_sourceнезависим отcreated_source; UI не должен их путать. - Смешанная учётка — если один и тот же аккаунт используется и людьми, и MCP, флаг пометит человеческие действия тоже. Поэтому требование: для MCP — отдельный аккаунт.
Тесты
comment.service(юнит):provenance.actor='agent'→createdSource='agent',aiChatId=nullне ломает вставку;actor='user'→ дефолт.jwt.strategy(юнит/инт):user.isAgent=true→req.raw.actor='agent'; обычный пользователь →'user'; claim из тела не влияет (анти-spoof).- Фронтенд (компонентный):
comment-list-itemрендерит бейдж приcreatedSource==='agent'и не рендерит при'user'; бейдж некликабелен приaiChatId==null. - Регрессия: существующие тесты комментариев (
comment.service.spec,comment.service.behavior.spec) остаются зелёными.
Объём и решения, которые надо зафиксировать перед реализацией
- Охват: помечать как AI только комментарии или все MCP-записи. Рекомендуется все (флаг идентичности это и даёт «бесплатно»; страницы уже поддержаны на бэке и в истории).
- «Resolved by AI»: включать в первый заход или отдельным пунктом.
- Имя/аватар сервис-аккаунта: независимо от бейджа, разумно назвать учётку «AI» и дать аватар-робота — бейдж и имя усиливают друг друга.
Критерии приёмки
- Комментарий, созданный через MCP под агентским сервис-аккаунтом, имеет
created_source = 'agent'в БД. - В сайдбаре комментариев у такого комментария виден AI-бейдж рядом с именем автора; у обычного — нет.
- Обычный пользователь никаким способом (включая поле в теле запроса) не может получить
created_source = 'agent'. - Страницы, созданные через MCP, показывают AI-бейдж в истории (следствие B1, без доп. фронтенд-работы).
- Существующие тесты зелёные; добавлены тесты из раздела «Тесты».
Связанные места (быстрые ссылки)
- Бэкенд-маркер:
apps/server/src/core/comment/comment.service.ts(create ~88–92, resolve ~235–244). - Провенанс из JWT:
apps/server/src/common/decorators/auth-provenance.decorator.ts,apps/server/src/core/auth/strategies/jwt.strategy.ts(~80–81; API-key ~86–110). - Минтинг provenance-токена (образец внутреннего агента):
apps/server/src/core/auth/services/token.service.ts(~30–77),apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts(~53–84). - Колонки БД:
apps/server/src/database/migrations/20260616T130000-agent-provenance.ts. - MCP-аутентификация:
packages/mcp/src/stdio.ts:38-39,packages/mcp/src/client.ts:99-106. - Фронтенд:
apps/client/src/features/comment/types/comment.types.ts,apps/client/src/features/comment/components/comment-list-item.tsx, образец бейджаapps/client/src/features/page-history/components/history-item.tsx(AiAgentBadge).