feat(comments): attribute MCP agent comments as AI (unspoofable provenance)
Mark comments (and, via existing page provenance, pages) created under an is_agent service account as authored by AI, derived from the SIGNED server identity rather than any client field, and render the existing AI badge in the comments sidebar. Backend (B1): - Add additive users.is_agent boolean (default false) migration; reflect in the Users Kysely type, the user repo baseFields, and (via Selectable) the User entity. - jwt.strategy: derive req.raw.actor from user.isAgent (an is_agent account stamps every write 'agent'); external MCP has no internal ai_chats row so aiChatId stays null. Non-spoofable: a plain user cannot obtain created_source='agent'. - Loosen the provenance aiChatId type to string|null across token.service and the JwtPayload/JwtCollabPayload claims (type-level only; the internal AI-chat path still passes a real aiChatId). Frontend (B2): - Extend IComment with createdSource/aiChatId/resolvedSource (backend already returns them via selectAll). - Extract the local AiAgentBadge from history-item into a shared components/ui/ai-agent-badge.tsx (clickable deep-link when aiChatId present, plain label when null/absent); reuse it in history-item and render it in comment-list-item next to the author name when createdSource==='agent'. Tests: comment.service agent/null-aiChatId provenance, jwt.strategy provenance derivation + anti-spoof, AiAgentBadge clickable/non-clickable branches, and comment-list-item badge render/no-render. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -147,6 +147,24 @@ describe('CommentService — behavior', () => {
|
||||
expect(insertArg.creatorId).toBe('user-1');
|
||||
});
|
||||
|
||||
it('stamps createdSource:"agent" with a null aiChatId (external MCP agent) without breaking insert', async () => {
|
||||
const { service, commentRepo } = makeService();
|
||||
|
||||
// An external MCP agent is flagged is_agent server-side but has no
|
||||
// internal ai_chats row, so provenance carries actor='agent' + a null
|
||||
// aiChatId. The insert must still record the agent marker.
|
||||
await service.create(
|
||||
{ page: page(), workspaceId: 'ws-1', user: user() },
|
||||
{ content: JSON.stringify(docMentioning()) } as any,
|
||||
{ actor: 'agent', aiChatId: null },
|
||||
);
|
||||
|
||||
const insertArg = commentRepo.insertComment.mock.calls[0][0];
|
||||
expect(insertArg.createdSource).toBe('agent');
|
||||
expect(insertArg.aiChatId).toBeNull();
|
||||
expect(insertArg.creatorId).toBe('user-1');
|
||||
});
|
||||
|
||||
it('leaves source default (no agent stamp) for a normal user', async () => {
|
||||
const { service, commentRepo } = makeService();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user