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:
@@ -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();
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
1
apps/server/src/database/types/db.d.ts
vendored
1
apps/server/src/database/types/db.d.ts
vendored
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user