chore(comments): address PR #143 review (operator doc, badge test, dedup, type)

- [warn 1] Document the is_agent operator setup so it survives plan deletion:
  added an AI-agent block to .env.example (use a DEDICATED account, set is_agent
  via SQL, never flag a human/shared account) + a CHANGELOG "Added" entry.
- [warn 2] Test the badge deep-link side effects: ai-agent-badge.test.tsx now
  renders inside an explicit jotai store, clicks the badge, and asserts the
  active chat id, window-open, cleared draft, closed history modal, AND that
  stopPropagation keeps a parent onClick from firing.
- [suggestion 3] Hoist the window.matchMedia stub into vitest.setup.ts and drop
  the duplicated beforeAll block from the three test files (ai-agent-badge,
  comment-list-item, role-cards).
- [suggestion 4] Merge the two near-duplicate "non-clickable" cases via it.each.
- [follow-up 6] Introduce a single ProvenanceSource = 'user' | 'agent' type in
  jwt-payload.ts and reference it from AuthProvenanceData, JwtPayload/
  JwtCollabPayload, and resolveSource() — so a typo can't slip through as a bare
  string. (Server auth chain; client IComment mirroring left as a follow-up.)

Follow-up 5 (shared agentSourceFields write-stamp helper) is deferred as the
review marked it — the 6 REST sites use varied shapes (create-spread vs
resolve-conditional-null vs page move), so it's a separate focused refactor.

Tests: client badge/comment/role-cards suites 11/11 pass; server auth+comment
suites 62 pass; typecheck clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude code agent 227
2026-06-23 23:56:26 +03:00
parent 989f99abae
commit 0647faefcd
9 changed files with 94 additions and 70 deletions

View File

@@ -21,6 +21,7 @@ import { KyselyDB } from '@docmost/db/types/kysely.types';
import { executeTx } from '@docmost/db/utils';
import { InjectQueue } from '@nestjs/bullmq';
import { QueueJob, QueueName } from '../../integrations/queue/constants';
import { ProvenanceSource } from '../../core/auth/dto/jwt-payload';
import { Queue } from 'bullmq';
import {
extractMentions,
@@ -50,7 +51,7 @@ import { TransclusionService } from '../../core/page/transclusion/transclusion.s
export function resolveSource(
stickyTouched: boolean,
contextActor?: string,
): 'agent' | 'user' {
): ProvenanceSource {
return stickyTouched || contextActor === 'agent' ? 'agent' : 'user';
}

View File

@@ -1,4 +1,5 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { ProvenanceSource } from '../../core/auth/dto/jwt-payload';
/**
* The agent-edit provenance carried by the request, read from the SIGNED access
@@ -8,7 +9,7 @@ import { createParamDecorator, ExecutionContext } from '@nestjs/common';
* cannot fake an 'agent' marker.
*/
export interface AuthProvenanceData {
actor: 'user' | 'agent';
actor: ProvenanceSource;
aiChatId: string | null;
}

View File

@@ -1,3 +1,11 @@
/**
* Provenance actor for a write: who the action is attributed to. Derived only
* from the SIGNED token claim (never a request body), so 'agent' is unspoofable.
* Single source of truth so a typo like 'agnet' can't slip through as a bare
* string (#143 review). Distinct from `ActorType` (auth principal kind).
*/
export type ProvenanceSource = 'user' | 'agent';
export enum JwtType {
ACCESS = 'access',
COLLAB = 'collab',
@@ -19,7 +27,7 @@ export type JwtPayload = {
// mints a provenance access token so REST writes (create/rename/move page,
// comment create/resolve) record a non-spoofable 'agent' marker (§6.5 / §15
// C3 / §14 N2).
actor?: 'user' | 'agent';
actor?: ProvenanceSource;
// Nullable: an external MCP agent has no internal ai_chats row, so it carries
// an 'agent' actor with a null aiChatId.
aiChatId?: string | null;
@@ -32,7 +40,7 @@ export type JwtCollabPayload = {
// Optional agent-edit provenance, signed into the collab token. Absent for
// the human collab path (treated as 'user'); set only when the internal agent
// mints a provenance collab token (§6.6 / §15 C2).
actor?: 'user' | 'agent';
actor?: ProvenanceSource;
// Nullable: an external MCP agent has no internal ai_chats row, so it carries
// an 'agent' actor with a null aiChatId.
aiChatId?: string | null;