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

View File

@@ -0,0 +1,23 @@
import { type Kysely } from 'kysely';
/**
* Agent identity flag on users (MCP comment/page AI attribution).
*
* Additive boolean marking a service account as an AI agent. When set, the JWT
* strategy derives provenance ('agent') from this SIGNED server-side identity —
* never from a client-supplied field — so every write by the account is
* attributed to AI in a non-spoofable way. Defaults to false; ordinary users
* are unaffected. Kept as a dedicated column (not `role`, which has
* authorization semantics, and not buried in `settings`) for a cheap filter and
* explicitness.
*/
export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.alterTable('users')
.addColumn('is_agent', 'boolean', (col) => col.notNull().defaultTo(false))
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
await db.schema.alterTable('users').dropColumn('is_agent').execute();
}

View File

@@ -36,6 +36,9 @@ export class UserRepo {
'updatedAt',
'deletedAt',
'hasGeneratedPassword',
// AI agent identity flag — needed by the JWT strategy to derive a
// non-spoofable 'agent' provenance from the signed server-side identity.
'isAgent',
];
async findById(

View File

@@ -368,6 +368,7 @@ export interface Users {
emailVerifiedAt: Timestamp | null;
id: Generated<string>;
invitedById: string | null;
isAgent: Generated<boolean>;
lastActiveAt: Timestamp | null;
lastLoginAt: Timestamp | null;
locale: string | null;