11import { db } from '@sim/db'
22import { account } from '@sim/db/schema'
3- import { and , eq } from 'drizzle-orm'
3+ import { createLogger } from '@sim/logger'
4+ import { eq } from 'drizzle-orm'
45import type {
56 ExecutionContext ,
67 ToolCallResult ,
78 ToolCallState ,
89} from '@/lib/copilot/orchestrator/types'
910import { isHosted } from '@/lib/core/config/feature-flags'
1011import { generateRequestId } from '@/lib/core/utils/request'
12+ import { getCredentialActorContext } from '@/lib/credentials/access'
13+ import { getAccessibleOAuthCredentials } from '@/lib/credentials/environment'
1114import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
1215import { getWorkflowById } from '@/lib/workflows/utils'
1316import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -16,6 +19,8 @@ import { executeTool } from '@/tools'
1619import type { ToolConfig } from '@/tools/types'
1720import { resolveToolId } from '@/tools/utils'
1821
22+ const logger = createLogger ( 'CopilotIntegrationTools' )
23+
1924export async function executeIntegrationToolDirect (
2025 toolCall : ToolCallState ,
2126 toolConfig : ToolConfig ,
@@ -34,40 +39,86 @@ export async function executeIntegrationToolDirect(
3439 const decryptedEnvVars =
3540 context . decryptedEnvVars || ( await getEffectiveDecryptedEnv ( userId , workspaceId ) )
3641
37- // Deep resolution walks nested objects to replace {{ENV_VAR}} references.
38- // Safe because tool arguments originate from the LLM (not direct user input)
39- // and env vars belong to the user themselves.
4042 const executionParams = resolveEnvVarReferences ( toolArgs , decryptedEnvVars , {
4143 deep : true ,
4244 } ) as Record < string , unknown >
4345
44- if ( toolConfig . oauth ?. required && toolConfig . oauth . provider ) {
45- const provider = toolConfig . oauth . provider
46- const accounts = await db
47- . select ( )
48- . from ( account )
49- . where ( and ( eq ( account . providerId , provider ) , eq ( account . userId , userId ) ) )
50- . limit ( 1 )
51-
52- if ( ! accounts . length ) {
46+ // If the LLM passed a credential/oauthCredential ID directly, verify the user
47+ // has active credential_member access before proceeding. This prevents
48+ // unauthorized credential usage even if the agent hallucinated or received
49+ // a credential ID the user doesn't have access to.
50+ const suppliedCredentialId = ( executionParams . oauthCredential || executionParams . credential ) as
51+ | string
52+ | undefined
53+ if ( suppliedCredentialId ) {
54+ const actorCtx = await getCredentialActorContext ( suppliedCredentialId , userId )
55+ if ( ! actorCtx . member ) {
56+ logger . warn ( 'Blocked credential use: user lacks credential_member access' , {
57+ credentialId : suppliedCredentialId ,
58+ userId,
59+ toolName,
60+ } )
5361 return {
5462 success : false ,
55- error : `No ${ provider } account connected. Please connect your account first .` ,
63+ error : `You do not have access to credential " ${ suppliedCredentialId } ". Ask the credential admin to add you as a member, or connect your own account .` ,
5664 }
5765 }
66+ }
5867
59- const acc = accounts [ 0 ]
60- const requestId = generateRequestId ( )
61- const { accessToken } = await refreshTokenIfNeeded ( requestId , acc , acc . id )
68+ if ( toolConfig . oauth ?. required && toolConfig . oauth . provider ) {
69+ const provider = toolConfig . oauth . provider
6270
63- if ( ! accessToken ) {
64- return {
65- success : false ,
66- error : `OAuth token not available for ${ provider } . Please reconnect your account.` ,
71+ // If the user already supplied a credential ID that passed the check above,
72+ // skip auto-resolution and let executeTool handle it via the token endpoint.
73+ if ( ! suppliedCredentialId ) {
74+ if ( ! workspaceId ) {
75+ return {
76+ success : false ,
77+ error : `Cannot resolve ${ provider } credential without a workspace context.` ,
78+ }
6779 }
68- }
6980
70- executionParams . accessToken = accessToken
81+ const accessibleCreds = await getAccessibleOAuthCredentials ( workspaceId , userId )
82+ const match = accessibleCreds . find ( ( c ) => c . providerId === provider )
83+
84+ if ( ! match ) {
85+ return {
86+ success : false ,
87+ error : `No accessible ${ provider } account found. You either don't have a ${ provider } account connected in this workspace, or you don't have access to the existing one. Please connect your own account.` ,
88+ }
89+ }
90+
91+ // Resolve the credential to its underlying account for token refresh
92+ const matchCtx = await getCredentialActorContext ( match . id , userId )
93+ const accountId = matchCtx . credential ?. accountId
94+ if ( ! accountId ) {
95+ return {
96+ success : false ,
97+ error : `OAuth account for ${ provider } not found. Please reconnect your account.` ,
98+ }
99+ }
100+
101+ const [ acc ] = await db . select ( ) . from ( account ) . where ( eq ( account . id , accountId ) ) . limit ( 1 )
102+
103+ if ( ! acc ) {
104+ return {
105+ success : false ,
106+ error : `OAuth account for ${ provider } not found. Please reconnect your account.` ,
107+ }
108+ }
109+
110+ const requestId = generateRequestId ( )
111+ const { accessToken } = await refreshTokenIfNeeded ( requestId , acc , acc . id )
112+
113+ if ( ! accessToken ) {
114+ return {
115+ success : false ,
116+ error : `OAuth token not available for ${ provider } . Please reconnect your account.` ,
117+ }
118+ }
119+
120+ executionParams . accessToken = accessToken
121+ }
71122 }
72123
73124 const hasHostedKeySupport = isHosted && ! ! toolConfig . hosting
@@ -82,6 +133,7 @@ export async function executeIntegrationToolDirect(
82133 workflowId,
83134 workspaceId,
84135 userId,
136+ enforceCredentialAccess : true ,
85137 }
86138
87139 if ( toolName === 'function_execute' ) {
0 commit comments