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:
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user