docs: add review adjustments and blocker resolutions to plan

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.
This commit is contained in:
vvzvlad
2026-06-17 00:25:47 +03:00
parent 504fc3db81
commit fe05828271

View File

@@ -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) — реализовать первым сквозным
вертикальным срезом «правка агентом → бейдж в истории», чтобы снять интеграционный риск рано.