feat(ai-chat): agent write tools, provenance wiring, chat panel + provider settings UI" -m "Backend:

- Add reversible write tools to the per-user agent toolset (page create/update/
  move/soft-delete; comment reply + resolve), exposed under the user's JWT and
  enforced by Docmost CASL; no permanent/force delete (D3).
- Non-spoofable agent provenance: sign actor/aiChatId into the access and collab
  tokens (TokenService), propagate via jwt.strategy onto the request, and set
  pages.last_updated_source/last_updated_ai_chat_id on REST create/update/move and
  comments.created_source/resolved_source/ai_chat_id.
- packages/mcp: add an optional getCollabToken provider (content-edit provenance)
  and guard against empty tokens; service-account /mcp path unchanged.

Frontend:
- Admin 'AI / Models' settings section: provider/model/embedding/base URL, a
  write-only API key field, system prompt, and Test connection.
- AI chat panel (useChat + DefaultChatTransport): conversation list, streamed
  messages, tool-call action log and page citations; header entry point gated on
  settings.ai.chat.

Compile-verified (server nest build + client tsc/vite); not yet live-tested.
Known gaps: history 'AI agent' badge (C3), vector RAG (D), external MCP (E);
chat tool-card citation links pending a fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
vvzvlad
2026-06-17 02:39:26 +03:00
parent 683da7a4c5
commit 44b340dc1a
38 changed files with 2384 additions and 21 deletions
@@ -17,6 +17,10 @@ import { ResolveCommentDto } from './dto/resolve-comment.dto';
import { PageIdDto, CommentIdDto } from './dto/comments.input';
import { AuthUser } from '../../common/decorators/auth-user.decorator';
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
import {
AuthProvenance,
AuthProvenanceData,
} from '../../common/decorators/auth-provenance.decorator';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { User, Workspace } from '@docmost/db/types/entity.types';
@@ -54,6 +58,7 @@ export class CommentController {
@Body() createCommentDto: CreateCommentDto,
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
@AuthProvenance() provenance: AuthProvenanceData,
) {
const page = await this.pageRepo.findById(createCommentDto.pageId);
if (!page || page.deletedAt) {
@@ -69,6 +74,7 @@ export class CommentController {
user,
},
createCommentDto,
provenance,
);
this.auditService.log({
@@ -147,6 +153,7 @@ export class CommentController {
@Body() dto: ResolveCommentDto,
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
@AuthProvenance() provenance: AuthProvenanceData,
) {
const comment = await this.commentRepo.findById(dto.commentId, {
includeCreator: true,
@@ -172,6 +179,7 @@ export class CommentController {
comment,
dto.resolved,
user,
provenance,
);
this.auditService.log({
@@ -22,6 +22,7 @@ import {
ICommentResolvedNotificationJob,
} from '../../integrations/queue/constants/queue.interface';
import { WsService } from '../../ws/ws.service';
import { AuthProvenanceData } from '../../common/decorators/auth-provenance.decorator';
@Injectable()
export class CommentService {
@@ -52,9 +53,14 @@ export class CommentService {
async create(
opts: { page: Page; workspaceId: string; user: User },
createCommentDto: CreateCommentDto,
// Optional agent-edit provenance (from the signed access claim). When the
// actor is 'agent', stamp created_source/ai_chat_id so an agent-authored
// comment (incl. a reply) shows the AI marker (§15 C3). Normal user: default.
provenance?: AuthProvenanceData,
) {
const { page, workspaceId, user } = opts;
const commentContent = JSON.parse(createCommentDto.content);
const isAgent = provenance?.actor === 'agent';
if (createCommentDto.parentCommentId) {
const parentComment = await this.commentRepo.findById(
@@ -79,6 +85,11 @@ export class CommentService {
creatorId: user.id,
workspaceId: workspaceId,
spaceId: page.spaceId,
// Agent-edit provenance: the user stays creatorId; this only annotates the
// source. Normal user requests leave the column default ('user').
...(isAgent
? { createdSource: 'agent', aiChatId: provenance.aiChatId }
: {}),
});
if (createCommentDto.yjsSelection) {
@@ -213,12 +224,22 @@ export class CommentService {
comment: Comment,
resolved: boolean,
authUser: User,
// Optional agent-edit provenance (from the signed access claim). When the
// actor is 'agent' and the thread is being resolved, stamp resolved_source
// so the "resolved by" mark shows the AI marker (§15 C3). On unresolve the
// source is cleared alongside resolvedAt/resolvedById.
provenance?: AuthProvenanceData,
): Promise<Comment> {
const resolvedAt = resolved ? new Date() : null;
const resolvedById = resolved ? authUser.id : null;
const isAgent = provenance?.actor === 'agent';
// Set the agent marker only when resolving; on unresolve clear it back to
// null so a reopened thread carries no stale source. A normal user resolve
// leaves resolved_source null (no agent annotation).
const resolvedSource = resolved && isAgent ? 'agent' : null;
await this.commentRepo.updateComment(
{ resolvedAt, resolvedById },
{ resolvedAt, resolvedById, resolvedSource },
comment.id,
);