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:
claude code agent 227
2026-06-23 04:25:40 +03:00
parent 7884dc2e1a
commit 989f99abae
14 changed files with 445 additions and 106 deletions
@@ -34,7 +34,9 @@ export class TokenService {
// token carries no actor/aiChatId and is treated as 'user' downstream. The
// internal agent passes { actor:'agent', aiChatId } so REST writes record a
// non-spoofable 'agent' marker off the signed claim (§6.5 / §15 C3 / §14 N2).
provenance?: { actor: 'agent'; aiChatId: string },
// aiChatId is nullable: an external MCP agent has no internal ai_chats row,
// so it stamps 'agent' with a null aiChatId.
provenance?: { actor: 'agent'; aiChatId: string | null },
): Promise<string> {
if (isUserDisabled(user)) {
throw new ForbiddenException();
@@ -58,7 +60,8 @@ export class TokenService {
workspaceId: string,
// Optional agent-edit provenance. When omitted (the human collab path), the
// token carries no actor/aiChatId and is treated as 'user' downstream.
provenance?: { actor: 'agent'; aiChatId: string },
// aiChatId is nullable for an external agent with no internal ai_chats row.
provenance?: { actor: 'agent'; aiChatId: string | null },
): Promise<string> {
if (isUserDisabled(user)) {
throw new ForbiddenException();