From fe058282718721e3752b0ce5378bf7d5be55818c Mon Sep 17 00:00:00 2001 From: vvzvlad Date: Wed, 17 Jun 2026 00:25:47 +0300 Subject: [PATCH] docs: add review adjustments and blocker resolutions to plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added sections 14 and 15 to the AI‑agent chat plan documenting review findings, identified blockers (C1‑C3) and their resolutions, high/medium issues, and verification steps. This provides clear guidance before starting implementation. --- docs/ai-agent-chat-plan.md | 145 +++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/docs/ai-agent-chat-plan.md b/docs/ai-agent-chat-plan.md index 3215257f..ac0382fe 100644 --- a/docs/ai-agent-chat-plan.md +++ b/docs/ai-agent-chat-plan.md @@ -526,3 +526,148 @@ namespacing (префикс именем сервера, в пределах о - [ ] E2 `McpClientsService`: `@ai-sdk/mcp` подключение/namespacing/мердж/lifecycle/таймауты - [ ] E3 CRUD + Test внешних MCP в UI + SSRF-защита URL - [ ] E4 защита от prompt-injection из веб-контента (инструкция в system-каркасе) + +--- + +## 14. Корректировки по ревью (дельта, сверено с кодом) + +Ревью сверило план с исходниками и нашло реальные дыры. Ниже — принятые правки; +этот раздел имеет приоритет над более ранними, если расходится. + +### Блокеры (доделать ДО кодинга) +- **[C1] `sessionId` для минта токена.** `TokenService.generateAccessToken(user, sessionId)` + требует **реальную активную сессию**: `jwt.strategy` валидирует `sessionId` через + `userSessionRepo.findActiveById` (`apps/server/src/core/auth/strategies/jwt.strategy.ts:65-72`). + Источник — `req.raw.sessionId` (стратегия кладёт туда, НЕ в `req.user`). Правка: адаптер + инструментов принимает `(user, sessionId)` = `(req.user, req.raw.sessionId)`. Кейсы Bearer/ + API-key без сессии (`payload.sessionId` пуст) — решить отдельно: либо запрещать агента, + либо минтить сессионный токен от имени системной сессии. Без этого этап B не взлетит. +- **[C2] Провенанс-маркер не доедет через реальный путь записи.** Контент-правки идут через + collab-WS, а collab-токен агент берёт из `POST /auth/collab-token` → + `generateCollabToken(user, workspaceId)` **без** провенанса (`auth.controller.ts:187`, + `token.service.ts:45`). Правка §6.6: внутренний путь **минтит provenance-collab-токен сам** + (минуя REST-эндпоинт) и отдаёт его провайдеру collab; `onAuthenticate` должен **возвращать** + `{ user, actor, aiChatId }` (сейчас возвращает только `{ user }`). Плюс: `verifyJwt(.., COLLAB)` + проверяет лишь `type`, так что доп. claim'ы переживут верификацию — это ок. +- **[C3] `delete_comment` — необратим (hard delete).** `comment.repo.ts:94` — + `deleteFrom('comments')`, без корзины/истории. Нарушает инвариант D3. **Решение (дефолт):** + `delete_comment` агенту **не экспонируем** до появления мягкого удаления комментариев. + Снято из «открытых вопросов». +- **[H3/M4/M5] Подтвердить API AI SDK v6 (через context7 уже частично сверено).** Доки AI SDK + показывают `createMCPClient` из `@ai-sdk/mcp` с `transport {type:'http'|'sse', url, headers, + redirect:'error'}` и `client.tools()` — это не «по памяти». НО: (а) **запинить версию** + `@ai-sdk/mcp`/`@ai-sdk/react` под мажор `ai@6` (в lockfile их пока нет); (б) `toUIMessageStreamResponse()` + возвращает **Web `Response`**, а `res.hijack()` даёт Node-`res` — нужен мост: + `Readable.fromWeb(response.body).pipe(res.raw)` + SSE-заголовки, либо `pipeUIMessageStreamToResponse` + (проверить наличие в v6); (в) `useChat` v6 — через `DefaultChatTransport({ api: '/api/ai-chat/stream' })` + с cookie-credentials, протокол UI-message-stream должен совпасть с серверным. + +### High/Medium (учесть в соответствующих этапах) +- **[H1] Аудит в форке — no-op (EE вырезан).** `audit.service.ts` экспортит только + `NoopAuditService`; `ActorType = 'user'|'system'|'api_key'` (нет `'agent'`), + `AuditLogPayload` без `source`/`aiChatId`. **Решение (дефолт):** аудит **убираем из + контролей безопасности** (в т.ч. из митигации prompt-injection §8.12); трассировка действий + агента — через `ai_chat_messages.tool_calls` + маркер «правка агентом». Рабочий аудит с + `'agent'`-актором — опциональное будущее, не v1. +- **[H2] Коалесцинг может скрыть вклад агента.** В смешанном окне (агент→человек или наоборот) + один снапшот пометится по последнему писавшему. **Решение:** «sticky»-маркер — если агент + коснулся страницы в окне коалесцинга, снапшот помечаем `agent` независимо от последнего + писавшего (хранить «agent-touched» флажок в `collabHistory` рядом с contributors). Поблочную + атрибуцию не делаем в v1. +- **[H4] Хрупкость guardrail перманентного удаления.** Факт верен (`deletePage` не шлёт + `permanentlyDelete`, `page.controller.ts:322`), но добавить **тест**, что адаптер физически + не может выставить `permanentlyDelete`. +- **[M1] У `AI_QUEUE` нет консьюмера.** Есть только продюсер (`persistence.extension`), + процессора `@Processor(AI_QUEUE)` нет. На этапе D писать **и сам процессор**, а не только + индексатор «поверх готового хука». Уточнение к §3.1/§6.7. +- **[M2] Новые колонки `pages` не попадут в выборку.** `pageRepo.baseFields` — + фиксированный список; добавить туда `lastUpdatedSource`/`lastUpdatedAiChatId` (+ типы + `UpdatablePage`), иначе `saveHistory` получит `undefined`. Уточнение к §5.2/§6.6. +- **[M3] Удаление AI_*-геттеров — после аудита потребителей.** Их больше, чем в выноске + (`getAiEmbeddingModel/Completion/Dimension/SupportsMrl`, `getOpenAiApiUrl`, Google/Ollama). + Удалять только реально неиспользуемые (grep по потребителям). +- **[M6] Postgres-образ без pgvector.** `docker-compose.yml:19` — `postgres:18`. D1: сменить на + `pgvector/pgvector:pg18` (или ставить расширение в свой образ) + CI. +- **[M7] Тип `PageEmbeddings` богаче, чем §5.5.** Требует `spaceId`, `attachmentId`, `modelName`, + `modelDimensions`, `chunkIndex/Start/Length`. Миграция D — со всеми колонками; для + `embedding` использовать уже установленный npm `pgvector` (см. L1). + +### Low/факт-чек +- **[L1]** `pgvector` (npm) **уже в зависимостях** — нет именно расширения Postgres и таблицы. +- **[L2]** `packages/mcp` = `@docmost/mcp`, **ESM-only** — адаптер под NestJS (commonjs) грузить + индирект-импортом, как в `mcp.service.ts` (`new Function('return import(...)')`). +- **[L4]** Нумерация этапов — **A…E** (не A…D); в чеклисте `A8` перенести в блок A. +- **[N2]** `createPage` идёт через REST `/pages/import` (CASL `Edit Page`), не через collab — + маркер «agent» на свежесозданной странице collab-claim'ом не проставится; для create + проставлять провенанс **на REST-пути** (как для rename/move в §6.6). +- **[N1]** Правки агента через collab триггерят mention-нотификации и добавление в + contributors/watchers — учесть, чтобы агент не спамил уведомлениями. + +### Вердикт ревью +План архитектурно зрелый и ~80% точен по фактам; ключевые риски осознаны. Но к старту +**не готов** из-за блокеров C1/C2/C3 и непроверенных швов H3/M4/M5. Pre-flight перед кодингом: +закрыть C1 (sessionId), C2 (provenance-путь collab), C3 (убрать `delete_comment`), подтвердить +API AI SDK v6 + мост стрима (H3/M4/M5), снять аудит как контроль (H1). + +--- + +## 15. Решения по находкам (закрыто, сверено с кодом) + +### Блокеры — закрыты +- **C1 (auth loopback) → forward токена юзера.** Чат-запрос приходит с cookie-JWT юзера; + `jwt.strategy` валидирует и кладёт `req.raw.sessionId` (`jwt.strategy.ts:70`). Внутренний + тулсет аутентифицирует loopback-REST, **переиспользуя живой access-токен юзера** (тот, что + уже в запросе) как Bearer `DocmostClient` — без минта (turn короче TTL токена). Рефреш в + длинных turn'ах — `getToken()` минтит заново через `generateAccessToken(user, req.raw.sessionId)`. + Кейс Bearer/API-key без сессии: чат для v1 требует интерактивную сессию → иначе 400. +- **C2 (provenance через collab) → инъекция provenance-collab-токена.** Точка найдена: + контент-правки идут через `mutatePageContent(pageId, collabToken, …)` / + `new HocuspocusProvider({ token: collabToken })` (`collaboration.ts:382,476`) — collab-токен + уже параметр. Решение: `DocmostClient` получает **провайдер collab-токена**; для внутреннего + агента он отдаёт `generateCollabToken(user, workspaceId, { actor:'agent', aiChatId })` + (расширить сигнатуру), минуя `/auth/collab-token`. `onAuthenticate` теперь **возвращает** + `{ user, actor, aiChatId }` (доп. claim переживает `verifyJwt(COLLAB)` — он чекает только + `type`). `onStoreDocument` пишет `actor/aiChatId` в `pages`. +- **C3 (комментарии) → агенту `create` (ответы) + `resolve`; без `update`/`delete`.** + - **`create`/reply — даём**: агент отвечает на комментарии (`/comments/create`, + `parentCommentId`). Ограничение бэка: только **1 уровень** — «нельзя отвечать на ответ» + (`parentComment.parentCommentId` должен быть null, `comment.service.ts` `create`). + - **`resolve` — даём** (обратим, `resolved: true/false`, `comment.service.ts:212`; + `POST /comments/resolve`, только top-level). В `packages/mcp` его нет — добавить tool. + - **`update` — НЕ даём**: редактирование **контента** коммента (overwrite + `editedAt`, + **без истории → необратимо**), и только своих (`creatorId === authUser.id`, иначе Forbidden, + `comment.service.ts` `update`). Низкая ценность + необратимо → исключаем. + - **`delete` — НЕ даём** (hard delete, `comment.repo.ts:94`). + - **Маркер «агент» на комментах** (как на страницах): новая миграция — `comments.created_source` + ('user'|'agent'), `comments.ai_chat_id` (nullable FK), и `comments.resolved_source` для + резолва. Ставится на **REST-пути** (create/resolve) при `actor='agent'`. UI: бейдж «AI-агент» + на комменте и на отметке «resolved by». + +### Проверки — подтверждены +- **H3 → пакеты есть, пинним.** `@ai-sdk/mcp@^1.0.51` (`createMCPClient` реален), + `@ai-sdk/react@^3.0.208` (мажор совместим с `ai@6`). Опция `redirect:'error'` подтверждена докой. +- **M4 → моста писать не надо.** AI SDK v6 пишет прямо в Node-`res`: + `result.pipeUIMessageStreamToResponse(res.raw)` (или `pipeAgentUIStreamToResponse({ response, + agent, uiMessages, abortSignal })`). Под Fastify: `res.hijack()` → `pipeUIMessageStreamToResponse(res.raw)`. + Отмена — `abortSignal: req.signal` + `onAbort` (персист частичного ответа). Самостоятельный + Web→Node мост не нужен (снимает замечание M4 из §14). +- **M5 → useChat.** Клиент: `useChat({ transport: new DefaultChatTransport({ api: + '/api/ai-chat/stream', credentials: 'include' }) })` — протокол совпадает с серверным. + +### Остальное — действия зафиксированы +- **H1** аудит убран из контролей (no-op в форке); трассировка = `ai_chat_messages.tool_calls` + маркер. Реальный аудит (`'agent'`-актор) — опционально потом. +- **H2** sticky-маркер: «агент коснулся в окне коалесцинга» → снапшот помечается `agent`. +- **H4** тест: адаптер физически не может выставить `permanentlyDelete`. +- **M1** на этапе D пишем сам `@Processor(AI_QUEUE)`. +- **M2** новые колонки → в `pageRepo.baseFields` + `UpdatablePage`. +- **M3** удаляем только реально неиспользуемые `getAi*`/`getOpenAi*` (после grep потребителей). +- **M6** D1: образ `pgvector/pgvector:pg18` (compose + CI). +- **M7** миграция `page_embeddings` со всеми колонками типа; `embedding` через npm `pgvector` (уже установлен — L1). +- **N2** для `createPage` (REST `/pages/import`) провенанс ставим на REST-пути. +- **L2** адаптер тулсета грузит ESM `@docmost/mcp` индирект-импортом (как `mcp.service.ts`). +- **L4** нумерация этапов A…E; `A8` в блок A. + +### Статус +Все блокеры имеют конкретный механизм; непроверенные швы подтверждены. План **готов к старту +этапа A**. Самый рискованный кусок — C2 (provenance-collab) — реализовать первым сквозным +вертикальным срезом «правка агентом → бейдж в истории», чтобы снять интеграционный риск рано.