diff --git a/AgentCompany-New/src/main/services/chat-service.ts b/AgentCompany-New/src/main/services/chat-service.ts index fd7eabe9..3b9c8c51 100644 --- a/AgentCompany-New/src/main/services/chat-service.ts +++ b/AgentCompany-New/src/main/services/chat-service.ts @@ -5,12 +5,14 @@ import { CHAT_THREAD_BOARD_STATE, FILE_SCAN_STATE, RISK_LEVEL, + ROLE_KEY, type FileScanState, } from "../../shared/domain"; import { createId } from "../../shared/ids"; import { ApprovalService } from "./approval-service"; import { AuditService } from "./audit-service"; import { BoardInterventionService, parseBoardCommand } from "./board-intervention-service"; +import { IdentityAccessService } from "./identity-access-service"; import { buildChatMessageStructure } from "./chat-message-structure"; import { ChatMessageMutationService } from "./chat-message-mutation-service"; import { recordChatMentions, resolveChatMentions, type ResolvedMention } from "./chat-mentions"; @@ -55,6 +57,7 @@ export class ChatService { private readonly boardInterventions: BoardInterventionService; private readonly handoffs: FileHandoffService; private readonly messageMutations: ChatMessageMutationService; + private readonly identity: IdentityAccessService; constructor(private readonly db: DatabaseSync) { this.audit = new AuditService(db); @@ -62,6 +65,7 @@ export class ChatService { this.boardInterventions = new BoardInterventionService(db); this.handoffs = new FileHandoffService(db); this.messageMutations = new ChatMessageMutationService(db); + this.identity = new IdentityAccessService(db); } createSpace(input: CreateChatSpaceInput): string { @@ -400,6 +404,14 @@ export class ChatService { } if (!senderId) throw new Error("Board sender is required."); this.requireOrgEntity("employees", senderId, orgId, "Board sender"); + this.identity.assertEmployeeRole( + orgId, + senderId, + ROLE_KEY.APPROVAL_DECIDER, + "chat_board_intervention", + "organization", + orgId, + ); return { type: "employee", id: senderId }; } diff --git a/AgentCompany-New/src/renderer/src/pages/ChatPage.tsx b/AgentCompany-New/src/renderer/src/pages/ChatPage.tsx index ecfed990..b5893e95 100644 --- a/AgentCompany-New/src/renderer/src/pages/ChatPage.tsx +++ b/AgentCompany-New/src/renderer/src/pages/ChatPage.tsx @@ -10,6 +10,7 @@ import { isRootMessage, replayEventsForThread, } from "./chat/chat-helpers"; +import { ROLE_KEY } from "../../../shared/domain"; import type { PageProps } from "./types"; import { activeOrgId, requireActiveEmployee } from "./types"; @@ -52,6 +53,17 @@ export function ChatPage({ snapshot, actions }: PageProps) { ); const channels = useMemo(() => spaces.filter((space) => space.spaceType !== "direct"), [spaces]); const directs = useMemo(() => spaces.filter((space) => space.spaceType === "direct"), [spaces]); + const canBoardIntervene = useMemo( + () => + snapshot.roleGrants.some( + (grant) => + grant.orgId === orgId && + grant.principalType === "employee" && + grant.principalId === employee.id && + (grant.roleKey === ROLE_KEY.APPROVAL_DECIDER || grant.roleKey === ROLE_KEY.ORG_ADMIN), + ), + [snapshot.roleGrants, orgId, employee.id], + ); const [selectedSpaceId, setSelectedSpaceId] = useState(null); const [openThreadId, setOpenThreadId] = useState(null); @@ -243,6 +255,7 @@ export function ChatPage({ snapshot, actions }: PageProps) { files={files} auditEvents={threadAuditEvents} context={{ employees, agents }} + canIntervene={canBoardIntervene} onClose={() => setOpenThreadId(null)} onSubmit={sendInThread} onBoardSubmit={sendBoardIntervention} diff --git a/AgentCompany-New/src/renderer/src/pages/chat/ChatThreadPanel.tsx b/AgentCompany-New/src/renderer/src/pages/chat/ChatThreadPanel.tsx index c6e5409e..11b65777 100644 --- a/AgentCompany-New/src/renderer/src/pages/chat/ChatThreadPanel.tsx +++ b/AgentCompany-New/src/renderer/src/pages/chat/ChatThreadPanel.tsx @@ -25,6 +25,7 @@ export function ChatThreadPanel({ files, auditEvents, context, + canIntervene, onClose, onSubmit, onBoardSubmit, @@ -39,6 +40,7 @@ export function ChatThreadPanel({ files: FileObjectRecord[]; auditEvents: AuditEventRecord[]; context: MessageContext; + canIntervene: boolean; onClose: () => void; onSubmit: (event: FormEvent) => void; onBoardSubmit: (event: FormEvent) => void; @@ -67,6 +69,7 @@ export function ChatThreadPanel({ thread={thread} interventions={boardInterventions} context={context} + canIntervene={canIntervene} onSubmit={onBoardSubmit} /> {rootMessage && ( @@ -132,20 +135,24 @@ function BoardControl({ thread, interventions, context, + canIntervene, onSubmit, }: { thread: ChatThreadRecord; interventions: BoardInterventionRecord[]; context: MessageContext; + canIntervene: boolean; onSubmit: (event: FormEvent) => void; }) { const [action, setAction] = useState("decision"); + if (!canIntervene && interventions.length === 0) return null; return (
Board
{boardStateLabel(thread.boardState)}
+ {canIntervene && (