11/**
22 * @vitest -environment node
33 */
4- import type { WorkflowExecutionDelegatedPrincipal } from '@sim/auth/principal'
4+ import type {
5+ BoundWorkflowExecutionDelegatedPrincipal ,
6+ SubjectDelegatedPrincipal ,
7+ } from '@sim/auth/principal'
58import { beforeEach , describe , expect , it , vi } from 'vitest'
69
710const mocks = vi . hoisted ( ( ) => ( {
@@ -50,7 +53,7 @@ const SERVER = {
5053 workspaceId : WORKSPACE . workspaceId ,
5154 enabled : true ,
5255}
53- const PRINCIPAL : WorkflowExecutionDelegatedPrincipal = {
56+ const PRINCIPAL : BoundWorkflowExecutionDelegatedPrincipal = {
5457 kind : 'delegated' ,
5558 serviceId : 'executor' ,
5659 subjectUserId : 'user-1' ,
@@ -61,7 +64,7 @@ const PRINCIPAL: WorkflowExecutionDelegatedPrincipal = {
6164 expiresAt : new Date ( '2099-08-27T00:05:00.000Z' ) ,
6265 delegationContext : { kind : 'workflow_execution' , workflowId : 'workflow-1' } ,
6366}
64- const ACTORLESS_PRINCIPAL : WorkflowExecutionDelegatedPrincipal = {
67+ const ACTORLESS_PRINCIPAL : BoundWorkflowExecutionDelegatedPrincipal = {
6568 kind : 'delegated' ,
6669 serviceId : 'executor' ,
6770 workspaceId : WORKSPACE . workspaceId ,
@@ -85,7 +88,18 @@ const ACTORLESS_PRINCIPAL: WorkflowExecutionDelegatedPrincipal = {
8588 } ,
8689 } ,
8790}
88- const COMPATIBILITY_ACTOR_PRINCIPAL : WorkflowExecutionDelegatedPrincipal = {
91+ const COPILOT_PRINCIPAL : SubjectDelegatedPrincipal = {
92+ kind : 'delegated' ,
93+ serviceId : 'copilot' ,
94+ subjectUserId : 'chat-user' ,
95+ workspaceId : WORKSPACE . workspaceId ,
96+ delegationId : 'copilot-tool:call-1' ,
97+ audience : 'sim:mcp-servers' ,
98+ issuedAt : new Date ( '2026-08-27T00:00:00.000Z' ) ,
99+ expiresAt : new Date ( '2099-08-27T00:05:00.000Z' ) ,
100+ resourceScope : { chatId : 'chat-1' } ,
101+ }
102+ const COMPATIBILITY_ACTOR_PRINCIPAL : BoundWorkflowExecutionDelegatedPrincipal = {
89103 ...ACTORLESS_PRINCIPAL ,
90104 delegationContext : {
91105 ...ACTORLESS_PRINCIPAL . delegationContext ,
@@ -119,6 +133,80 @@ describe('executeMcpToolUseCase', () => {
119133 mocks . executeTool . mockResolvedValue ( { content : [ { type : 'text' , text : 'done' } ] } )
120134 } )
121135
136+ it . each ( [ 'read' , 'write' , 'admin' ] ) (
137+ 'executes as the current Copilot subject with %s workspace permission' ,
138+ async ( permission ) => {
139+ mocks . resolvePermission . mockResolvedValue ( permission )
140+ await executeMcpToolUseCase . execute ( {
141+ principal : COPILOT_PRINCIPAL ,
142+ input : {
143+ workspaceId : WORKSPACE . workspaceId ,
144+ serverId : SERVER . id ,
145+ toolName : 'lookup' ,
146+ arguments : { count : 1 } ,
147+ } ,
148+ } )
149+ expect ( mocks . resolvePermission ) . toHaveBeenCalledWith (
150+ 'chat-user' ,
151+ WORKSPACE . workspaceId ,
152+ null ,
153+ undefined ,
154+ { forUpdate : undefined }
155+ )
156+ expect ( mocks . assertPermissionsAllowed ) . toHaveBeenCalledWith ( {
157+ userId : 'chat-user' ,
158+ workspaceId : WORKSPACE . workspaceId ,
159+ toolKind : 'mcp' ,
160+ } )
161+ expect ( mocks . executeTool ) . toHaveBeenCalledWith (
162+ 'chat-user' ,
163+ SERVER . id ,
164+ { name : 'lookup' , arguments : { count : 1 } } ,
165+ WORKSPACE . workspaceId ,
166+ undefined ,
167+ undefined ,
168+ { signal : undefined , timeoutMs : undefined }
169+ )
170+ }
171+ )
172+
173+ it . each ( [
174+ { audience : 'sim:other' } ,
175+ { workspaceId : 'foreign-workspace' } ,
176+ { expiresAt : new Date ( 0 ) } ,
177+ ] ) ( 'rejects invalid Copilot delegation %j before discovery or execution' , async ( override ) => {
178+ await expect (
179+ executeMcpToolUseCase . execute ( {
180+ principal : { ...COPILOT_PRINCIPAL , ...override } ,
181+ input : {
182+ workspaceId : WORKSPACE . workspaceId ,
183+ serverId : SERVER . id ,
184+ toolName : 'lookup' ,
185+ arguments : { count : 1 } ,
186+ } ,
187+ } )
188+ ) . rejects . toMatchObject ( { code : 'forbidden' } )
189+ expect ( mocks . discoverServerTools ) . not . toHaveBeenCalled ( )
190+ expect ( mocks . executeTool ) . not . toHaveBeenCalled ( )
191+ } )
192+
193+ it ( 'rechecks the Copilot subject after workspace membership is revoked' , async ( ) => {
194+ mocks . resolvePermission . mockResolvedValue ( null )
195+ await expect (
196+ executeMcpToolUseCase . execute ( {
197+ principal : COPILOT_PRINCIPAL ,
198+ input : {
199+ workspaceId : WORKSPACE . workspaceId ,
200+ serverId : SERVER . id ,
201+ toolName : 'lookup' ,
202+ arguments : { count : 1 } ,
203+ } ,
204+ } )
205+ ) . rejects . toMatchObject ( { code : 'forbidden' } )
206+ expect ( mocks . discoverServerTools ) . not . toHaveBeenCalled ( )
207+ expect ( mocks . executeTool ) . not . toHaveBeenCalled ( )
208+ } )
209+
122210 it ( 'authorizes, coerces the discovered schema, and preserves execution context' , async ( ) => {
123211 const provenance = vi . fn ( )
124212 const signal = new AbortController ( ) . signal
0 commit comments