mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
improvement(copilot): added session context checks in copilot tool calls (#1466)
* improvement(copilot): added session context checks in copilot tool calls * remove extraneous comments, remove the ability to view copilot API keys after creation * updated skeleton --------- Co-authored-by: waleed <waleed>
This commit is contained in:
@@ -15,6 +15,8 @@ export async function POST(req: NextRequest) {
|
||||
// Move environment variable access inside the function
|
||||
const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT
|
||||
|
||||
await req.json().catch(() => ({}))
|
||||
|
||||
const res = await fetch(`${SIM_AGENT_API_URL}/api/validate-key/generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -31,14 +33,14 @@ export async function POST(req: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
const data = (await res.json().catch(() => null)) as { apiKey?: string } | null
|
||||
const data = (await res.json().catch(() => null)) as { apiKey?: string; id?: string } | null
|
||||
|
||||
if (!data?.apiKey) {
|
||||
return NextResponse.json({ error: 'Invalid response from Sim Agent' }, { status: 500 })
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: true, key: { id: 'new', apiKey: data.apiKey } },
|
||||
{ success: true, key: { id: data?.id || 'new', apiKey: data.apiKey } },
|
||||
{ status: 201 }
|
||||
)
|
||||
} catch (error) {
|
||||
|
||||
@@ -33,7 +33,12 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Invalid response from Sim Agent' }, { status: 500 })
|
||||
}
|
||||
|
||||
const keys = apiKeys
|
||||
const keys = apiKeys.map((k) => {
|
||||
const value = typeof k.apiKey === 'string' ? k.apiKey : ''
|
||||
const last6 = value.slice(-6)
|
||||
const displayKey = `•••••${last6}`
|
||||
return { id: k.id, displayKey }
|
||||
})
|
||||
|
||||
return NextResponse.json({ keys }, { status: 200 })
|
||||
} catch (error) {
|
||||
|
||||
@@ -34,7 +34,7 @@ export async function POST(req: NextRequest) {
|
||||
const { toolName, payload } = ExecuteSchema.parse(body)
|
||||
|
||||
logger.info(`[${tracker.requestId}] Executing server tool`, { toolName })
|
||||
const result = await routeExecution(toolName, payload)
|
||||
const result = await routeExecution(toolName, payload, { userId })
|
||||
|
||||
try {
|
||||
const resultPreview = JSON.stringify(result).slice(0, 300)
|
||||
|
||||
@@ -13,10 +13,8 @@ import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent/constants'
|
||||
|
||||
const logger = createLogger('CopilotMarkToolCompleteAPI')
|
||||
|
||||
// Sim Agent API configuration
|
||||
const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT
|
||||
|
||||
// Schema for mark-complete request
|
||||
const MarkCompleteSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
|
||||
const logger = createLogger('YamlAutoLayoutAPI')
|
||||
|
||||
// Sim Agent API configuration
|
||||
const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT
|
||||
|
||||
const AutoLayoutRequestSchema = z.object({
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
|
||||
const logger = createLogger('YamlDiffCreateAPI')
|
||||
|
||||
// Sim Agent API configuration
|
||||
const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT
|
||||
|
||||
const CreateDiffRequestSchema = z.object({
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
|
||||
const logger = createLogger('YamlDiffMergeAPI')
|
||||
|
||||
// Sim Agent API configuration
|
||||
const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT
|
||||
|
||||
const MergeDiffRequestSchema = z.object({
|
||||
|
||||
@@ -11,7 +11,6 @@ import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/w
|
||||
|
||||
const logger = createLogger('YamlGenerateAPI')
|
||||
|
||||
// Sim Agent API configuration
|
||||
const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT
|
||||
|
||||
const GenerateRequestSchema = z.object({
|
||||
|
||||
@@ -6,7 +6,6 @@ import { generateRequestId } from '@/lib/utils'
|
||||
|
||||
const logger = createLogger('YamlHealthAPI')
|
||||
|
||||
// Sim Agent API configuration
|
||||
const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT
|
||||
|
||||
export async function GET() {
|
||||
|
||||
@@ -11,7 +11,6 @@ import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/w
|
||||
|
||||
const logger = createLogger('YamlParseAPI')
|
||||
|
||||
// Sim Agent API configuration
|
||||
const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT
|
||||
|
||||
const ParseRequestSchema = z.object({
|
||||
|
||||
@@ -11,7 +11,6 @@ import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/w
|
||||
|
||||
const logger = createLogger('YamlToWorkflowAPI')
|
||||
|
||||
// Sim Agent API configuration
|
||||
const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT
|
||||
|
||||
const ConvertRequestSchema = z.object({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Check, Copy, Eye, EyeOff, Plus, Search } from 'lucide-react'
|
||||
import { Check, Copy, Plus, Search } from 'lucide-react'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -13,10 +13,6 @@ import {
|
||||
Input,
|
||||
Label,
|
||||
Skeleton,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
@@ -24,44 +20,36 @@ const logger = createLogger('CopilotSettings')
|
||||
|
||||
interface CopilotKey {
|
||||
id: string
|
||||
apiKey: string
|
||||
displayKey: string
|
||||
}
|
||||
|
||||
export function Copilot() {
|
||||
const [keys, setKeys] = useState<CopilotKey[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [visible, setVisible] = useState<Record<string, boolean>>({})
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
|
||||
// Create flow state
|
||||
const [showNewKeyDialog, setShowNewKeyDialog] = useState(false)
|
||||
const [newKey, setNewKey] = useState<CopilotKey | null>(null)
|
||||
const [copiedKeyIds, setCopiedKeyIds] = useState<Record<string, boolean>>({})
|
||||
const [newKey, setNewKey] = useState<string | null>(null)
|
||||
const [isCreatingKey] = useState(false)
|
||||
const [newKeyCopySuccess, setNewKeyCopySuccess] = useState(false)
|
||||
|
||||
// Delete flow state
|
||||
const [deleteKey, setDeleteKey] = useState<CopilotKey | null>(null)
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
|
||||
// Filter keys based on search term
|
||||
// Filter keys based on search term (by masked display value)
|
||||
const filteredKeys = keys.filter((key) =>
|
||||
key.apiKey.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
key.displayKey.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
|
||||
const maskedValue = useCallback((value: string, show: boolean) => {
|
||||
if (show) return value
|
||||
if (!value) return ''
|
||||
const last6 = value.slice(-6)
|
||||
return `•••••${last6}`
|
||||
}, [])
|
||||
|
||||
const fetchKeys = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const res = await fetch('/api/copilot/api-keys')
|
||||
if (!res.ok) throw new Error(`Failed to fetch: ${res.status}`)
|
||||
const data = await res.json()
|
||||
setKeys(Array.isArray(data.keys) ? data.keys : [])
|
||||
setKeys(Array.isArray(data.keys) ? (data.keys as CopilotKey[]) : [])
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch copilot keys', { error })
|
||||
setKeys([])
|
||||
@@ -83,11 +71,11 @@ export function Copilot() {
|
||||
throw new Error(body.error || 'Failed to generate API key')
|
||||
}
|
||||
const data = await res.json()
|
||||
// Show the new key dialog with the API key (only shown once)
|
||||
if (data?.key) {
|
||||
setNewKey(data.key)
|
||||
if (data?.key?.apiKey) {
|
||||
setNewKey(data.key.apiKey)
|
||||
setShowNewKeyDialog(true)
|
||||
}
|
||||
|
||||
await fetchKeys()
|
||||
} catch (error) {
|
||||
logger.error('Failed to generate copilot API key', { error })
|
||||
@@ -117,12 +105,7 @@ export function Copilot() {
|
||||
const onCopy = async (value: string, keyId?: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value)
|
||||
if (keyId) {
|
||||
setCopiedKeyIds((prev) => ({ ...prev, [keyId]: true }))
|
||||
setTimeout(() => {
|
||||
setCopiedKeyIds((prev) => ({ ...prev, [keyId]: false }))
|
||||
}, 1500)
|
||||
} else {
|
||||
if (!keyId) {
|
||||
setNewKeyCopySuccess(true)
|
||||
setTimeout(() => setNewKeyCopySuccess(false), 1500)
|
||||
}
|
||||
@@ -166,77 +149,32 @@ export function Copilot() {
|
||||
</div>
|
||||
) : (
|
||||
<div className='space-y-2'>
|
||||
{filteredKeys.map((k) => {
|
||||
const isVisible = !!visible[k.id]
|
||||
const value = maskedValue(k.apiKey, isVisible)
|
||||
return (
|
||||
<div key={k.id} className='flex flex-col gap-2'>
|
||||
<Label className='font-normal text-muted-foreground text-xs uppercase'>
|
||||
Copilot API Key
|
||||
</Label>
|
||||
<div className='flex items-center justify-between gap-4'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='flex h-8 items-center rounded-[8px] bg-muted px-3'>
|
||||
<code className='font-mono text-foreground text-xs'>{value}</code>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={() => setVisible((v) => ({ ...v, [k.id]: !isVisible }))}
|
||||
className='h-4 w-4 p-0 text-muted-foreground transition-colors hover:bg-transparent hover:text-foreground'
|
||||
>
|
||||
{isVisible ? (
|
||||
<EyeOff className='!h-3.5 !w-3.5' />
|
||||
) : (
|
||||
<Eye className='!h-3.5 !w-3.5' />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{isVisible ? 'Hide' : 'Reveal'}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={() => onCopy(k.apiKey, k.id)}
|
||||
className='h-4 w-4 p-0 text-muted-foreground transition-colors hover:bg-transparent hover:text-foreground'
|
||||
>
|
||||
{copiedKeyIds[k.id] ? (
|
||||
<Check className='!h-3.5 !w-3.5' />
|
||||
) : (
|
||||
<Copy className='!h-3.5 !w-3.5' />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Copy</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
{filteredKeys.map((k) => (
|
||||
<div key={k.id} className='flex flex-col gap-2'>
|
||||
<Label className='font-normal text-muted-foreground text-xs uppercase'>
|
||||
Copilot API Key
|
||||
</Label>
|
||||
<div className='flex items-center justify-between gap-4'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='flex h-8 items-center rounded-[8px] bg-muted px-3'>
|
||||
<code className='font-mono text-foreground text-xs'>{k.displayKey}</code>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
setDeleteKey(k)
|
||||
setShowDeleteDialog(true)
|
||||
}}
|
||||
className='h-8 text-muted-foreground hover:text-foreground'
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
setDeleteKey(k)
|
||||
setShowDeleteDialog(true)
|
||||
}}
|
||||
className='h-8 text-muted-foreground hover:text-foreground'
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
{/* Show message when search has no results but there are keys */}
|
||||
{searchTerm.trim() && filteredKeys.length === 0 && keys.length > 0 && (
|
||||
<div className='py-8 text-center text-muted-foreground text-sm'>
|
||||
@@ -265,7 +203,7 @@ export function Copilot() {
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Plus className='h-4 w-4 stroke-[2px]' />
|
||||
Generate Key
|
||||
Create Key
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
@@ -285,24 +223,23 @@ export function Copilot() {
|
||||
>
|
||||
<AlertDialogContent className='rounded-[10px] sm:max-w-lg'>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>New Copilot API Key</AlertDialogTitle>
|
||||
<AlertDialogTitle>Your API key has been created</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<span className='font-semibold'>Copy it now</span> and store it securely.
|
||||
This is the only time you will see your API key.{' '}
|
||||
<span className='font-semibold'>Copy it now and store it securely.</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
{newKey && (
|
||||
<div className='relative'>
|
||||
<div className='flex h-9 items-center rounded-[6px] border-none bg-muted px-3 pr-8'>
|
||||
<code className='flex-1 truncate font-mono text-foreground text-sm'>
|
||||
{newKey.apiKey}
|
||||
</code>
|
||||
<code className='flex-1 truncate font-mono text-foreground text-sm'>{newKey}</code>
|
||||
</div>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='-translate-y-1/2 absolute top-1/2 right-2 h-4 w-4 rounded-[4px] p-0 text-muted-foreground transition-colors hover:bg-transparent hover:text-foreground'
|
||||
onClick={() => onCopy(newKey.apiKey)}
|
||||
onClick={() => onCopy(newKey)}
|
||||
>
|
||||
{newKeyCopySuccess ? (
|
||||
<Check className='!h-3.5 !w-3.5' />
|
||||
@@ -320,7 +257,7 @@ export function Copilot() {
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent className='rounded-[10px] sm:max-w-md'>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Copilot API key?</AlertDialogTitle>
|
||||
<AlertDialogTitle>Delete API key?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Deleting this API key will immediately revoke access for any integrations using it.{' '}
|
||||
<span className='text-red-500 dark:text-red-500'>This action cannot be undone.</span>
|
||||
@@ -353,20 +290,16 @@ export function Copilot() {
|
||||
)
|
||||
}
|
||||
|
||||
// Loading skeleton for Copilot API keys
|
||||
function CopilotKeySkeleton() {
|
||||
return (
|
||||
<div className='flex flex-col gap-2'>
|
||||
<Skeleton className='h-4 w-32' /> {/* API key label */}
|
||||
<Skeleton className='h-4 w-32' />
|
||||
<div className='flex items-center justify-between gap-4'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<Skeleton className='h-8 w-40 rounded-[8px]' /> {/* Key preview */}
|
||||
<div className='flex items-center gap-2'>
|
||||
<Skeleton className='h-4 w-4' /> {/* Show/Hide button */}
|
||||
<Skeleton className='h-4 w-4' /> {/* Copy button */}
|
||||
</div>
|
||||
<Skeleton className='h-8 w-20 rounded-[8px]' />
|
||||
<Skeleton className='h-4 w-24' />
|
||||
</div>
|
||||
<Skeleton className='h-8 w-16' /> {/* Delete button */}
|
||||
<Skeleton className='h-8 w-16' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
97
apps/sim/lib/copilot/auth/permissions.ts
Normal file
97
apps/sim/lib/copilot/auth/permissions.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { db } from '@sim/db'
|
||||
import { workflow } from '@sim/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getUserEntityPermissions, type PermissionType } from '@/lib/permissions/utils'
|
||||
|
||||
const logger = createLogger('CopilotPermissions')
|
||||
|
||||
/**
|
||||
* Verifies if a user has access to a workflow for copilot operations
|
||||
*
|
||||
* @param userId - The authenticated user ID
|
||||
* @param workflowId - The workflow ID to check access for
|
||||
* @returns Promise<{ hasAccess: boolean; userPermission: PermissionType | null; workspaceId?: string; isOwner: boolean }>
|
||||
*/
|
||||
export async function verifyWorkflowAccess(
|
||||
userId: string,
|
||||
workflowId: string
|
||||
): Promise<{
|
||||
hasAccess: boolean
|
||||
userPermission: PermissionType | null
|
||||
workspaceId?: string
|
||||
isOwner: boolean
|
||||
}> {
|
||||
try {
|
||||
const workflowData = await db
|
||||
.select({
|
||||
userId: workflow.userId,
|
||||
workspaceId: workflow.workspaceId,
|
||||
})
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, workflowId))
|
||||
.limit(1)
|
||||
|
||||
if (!workflowData.length) {
|
||||
logger.warn('Attempt to access non-existent workflow', {
|
||||
workflowId,
|
||||
userId,
|
||||
})
|
||||
return { hasAccess: false, userPermission: null, isOwner: false }
|
||||
}
|
||||
|
||||
const { userId: workflowOwnerId, workspaceId } = workflowData[0]
|
||||
|
||||
if (workflowOwnerId === userId) {
|
||||
logger.debug('User has direct ownership of workflow', { workflowId, userId })
|
||||
return {
|
||||
hasAccess: true,
|
||||
userPermission: 'admin',
|
||||
workspaceId: workspaceId || undefined,
|
||||
isOwner: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (workspaceId && userId) {
|
||||
const userPermission = await getUserEntityPermissions(userId, 'workspace', workspaceId)
|
||||
|
||||
if (userPermission !== null) {
|
||||
logger.debug('User has workspace permission for workflow', {
|
||||
workflowId,
|
||||
userId,
|
||||
workspaceId,
|
||||
userPermission,
|
||||
})
|
||||
return {
|
||||
hasAccess: true,
|
||||
userPermission,
|
||||
workspaceId: workspaceId || undefined,
|
||||
isOwner: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.warn('User has no access to workflow', {
|
||||
workflowId,
|
||||
userId,
|
||||
workspaceId,
|
||||
workflowOwnerId,
|
||||
})
|
||||
return {
|
||||
hasAccess: false,
|
||||
userPermission: null,
|
||||
workspaceId: workspaceId || undefined,
|
||||
isOwner: false,
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error verifying workflow access', { error, workflowId, userId })
|
||||
return { hasAccess: false, userPermission: null, isOwner: false }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create consistent permission error messages
|
||||
*/
|
||||
export function createPermissionError(operation: string): string {
|
||||
return `Access denied: You do not have permission to ${operation} this workflow`
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface BaseServerTool<TArgs = any, TResult = any> {
|
||||
name: string
|
||||
execute(args: TArgs): Promise<TResult>
|
||||
execute(args: TArgs, context?: { userId: string }): Promise<TResult>
|
||||
}
|
||||
|
||||
@@ -50,8 +50,11 @@ serverToolRegistry[readGDriveFileServerTool.name] = readGDriveFileServerTool
|
||||
serverToolRegistry[getOAuthCredentialsServerTool.name] = getOAuthCredentialsServerTool
|
||||
serverToolRegistry[makeApiRequestServerTool.name] = makeApiRequestServerTool
|
||||
|
||||
// Main router function
|
||||
export async function routeExecution(toolName: string, payload: unknown): Promise<any> {
|
||||
export async function routeExecution(
|
||||
toolName: string,
|
||||
payload: unknown,
|
||||
context?: { userId: string }
|
||||
): Promise<any> {
|
||||
const tool = serverToolRegistry[toolName]
|
||||
if (!tool) {
|
||||
throw new Error(`Unknown server tool: ${toolName}`)
|
||||
@@ -81,7 +84,7 @@ export async function routeExecution(toolName: string, payload: unknown): Promis
|
||||
args = BuildWorkflowInput.parse(args)
|
||||
}
|
||||
|
||||
const result = await tool.execute(args)
|
||||
const result = await tool.execute(args, context)
|
||||
|
||||
if (toolName === 'get_blocks_and_tools') {
|
||||
return GetBlocksAndToolsResult.parse(result)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createPermissionError, verifyWorkflowAccess } from '@/lib/copilot/auth/permissions'
|
||||
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
|
||||
import { getEnvironmentVariableKeys } from '@/lib/environment/utils'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getUserId } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
interface GetEnvironmentVariablesParams {
|
||||
userId?: string
|
||||
@@ -11,22 +11,41 @@ interface GetEnvironmentVariablesParams {
|
||||
export const getEnvironmentVariablesServerTool: BaseServerTool<GetEnvironmentVariablesParams, any> =
|
||||
{
|
||||
name: 'get_environment_variables',
|
||||
async execute(params: GetEnvironmentVariablesParams): Promise<any> {
|
||||
async execute(
|
||||
params: GetEnvironmentVariablesParams,
|
||||
context?: { userId: string }
|
||||
): Promise<any> {
|
||||
const logger = createLogger('GetEnvironmentVariablesServerTool')
|
||||
const { userId: directUserId, workflowId } = params || {}
|
||||
|
||||
logger.info('Getting environment variables (new runtime)', {
|
||||
hasUserId: !!directUserId,
|
||||
hasWorkflowId: !!workflowId,
|
||||
})
|
||||
|
||||
const userId =
|
||||
directUserId || (workflowId ? await getUserId('copilot-env-vars', workflowId) : undefined)
|
||||
if (!userId) {
|
||||
logger.warn('No userId could be determined', { directUserId, workflowId })
|
||||
throw new Error('Either userId or workflowId is required')
|
||||
if (!context?.userId) {
|
||||
logger.error(
|
||||
'Unauthorized attempt to access environment variables - no authenticated user context'
|
||||
)
|
||||
throw new Error('Authentication required')
|
||||
}
|
||||
|
||||
const authenticatedUserId = context.userId
|
||||
|
||||
if (params?.workflowId) {
|
||||
const { hasAccess } = await verifyWorkflowAccess(authenticatedUserId, params.workflowId)
|
||||
|
||||
if (!hasAccess) {
|
||||
const errorMessage = createPermissionError('access environment variables in')
|
||||
logger.error('Unauthorized attempt to access environment variables', {
|
||||
workflowId: params.workflowId,
|
||||
authenticatedUserId,
|
||||
})
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
const userId = authenticatedUserId
|
||||
|
||||
logger.info('Getting environment variables for authenticated user', {
|
||||
userId,
|
||||
hasWorkflowId: !!params?.workflowId,
|
||||
})
|
||||
|
||||
const result = await getEnvironmentVariableKeys(userId)
|
||||
logger.info('Environment variable keys retrieved', { userId, variableCount: result.count })
|
||||
return {
|
||||
|
||||
@@ -2,10 +2,11 @@ import { db } from '@sim/db'
|
||||
import { account, user } from '@sim/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { jwtDecode } from 'jwt-decode'
|
||||
import { createPermissionError, verifyWorkflowAccess } from '@/lib/copilot/auth/permissions'
|
||||
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { generateRequestId } from '@/lib/utils'
|
||||
import { getUserId, refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
interface GetOAuthCredentialsParams {
|
||||
userId?: string
|
||||
@@ -14,18 +15,35 @@ interface GetOAuthCredentialsParams {
|
||||
|
||||
export const getOAuthCredentialsServerTool: BaseServerTool<GetOAuthCredentialsParams, any> = {
|
||||
name: 'get_oauth_credentials',
|
||||
async execute(params: GetOAuthCredentialsParams): Promise<any> {
|
||||
async execute(params: GetOAuthCredentialsParams, context?: { userId: string }): Promise<any> {
|
||||
const logger = createLogger('GetOAuthCredentialsServerTool')
|
||||
const directUserId = params?.userId
|
||||
let userId = directUserId
|
||||
if (!userId && params?.workflowId) {
|
||||
userId = await getUserId('copilot-oauth-creds', params.workflowId)
|
||||
|
||||
if (!context?.userId) {
|
||||
logger.error(
|
||||
'Unauthorized attempt to access OAuth credentials - no authenticated user context'
|
||||
)
|
||||
throw new Error('Authentication required')
|
||||
}
|
||||
if (!userId || typeof userId !== 'string' || userId.trim().length === 0) {
|
||||
throw new Error('userId is required')
|
||||
|
||||
const authenticatedUserId = context.userId
|
||||
|
||||
if (params?.workflowId) {
|
||||
const { hasAccess } = await verifyWorkflowAccess(authenticatedUserId, params.workflowId)
|
||||
|
||||
if (!hasAccess) {
|
||||
const errorMessage = createPermissionError('access credentials in')
|
||||
logger.error('Unauthorized attempt to access OAuth credentials', {
|
||||
workflowId: params.workflowId,
|
||||
authenticatedUserId,
|
||||
})
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
}
|
||||
logger.info('Fetching OAuth credentials for user', {
|
||||
hasDirectUserId: !!directUserId,
|
||||
|
||||
const userId = authenticatedUserId
|
||||
|
||||
logger.info('Fetching OAuth credentials for authenticated user', {
|
||||
userId,
|
||||
hasWorkflowId: !!params?.workflowId,
|
||||
})
|
||||
const accounts = await db.select().from(account).where(eq(account.userId, userId))
|
||||
|
||||
@@ -2,10 +2,10 @@ import { db } from '@sim/db'
|
||||
import { environment } from '@sim/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { z } from 'zod'
|
||||
import { createPermissionError, verifyWorkflowAccess } from '@/lib/copilot/auth/permissions'
|
||||
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { decryptSecret, encryptSecret } from '@/lib/utils'
|
||||
import { getUserId } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
interface SetEnvironmentVariablesParams {
|
||||
variables: Record<string, any> | Array<{ name: string; value: string }>
|
||||
@@ -28,7 +28,6 @@ function normalizeVariables(
|
||||
{} as Record<string, string>
|
||||
)
|
||||
}
|
||||
// Ensure all values are strings
|
||||
return Object.fromEntries(
|
||||
Object.entries(input || {}).map(([k, v]) => [k, String(v ?? '')])
|
||||
) as Record<string, string>
|
||||
@@ -37,19 +36,40 @@ function normalizeVariables(
|
||||
export const setEnvironmentVariablesServerTool: BaseServerTool<SetEnvironmentVariablesParams, any> =
|
||||
{
|
||||
name: 'set_environment_variables',
|
||||
async execute(params: SetEnvironmentVariablesParams): Promise<any> {
|
||||
async execute(
|
||||
params: SetEnvironmentVariablesParams,
|
||||
context?: { userId: string }
|
||||
): Promise<any> {
|
||||
const logger = createLogger('SetEnvironmentVariablesServerTool')
|
||||
|
||||
if (!context?.userId) {
|
||||
logger.error(
|
||||
'Unauthorized attempt to set environment variables - no authenticated user context'
|
||||
)
|
||||
throw new Error('Authentication required')
|
||||
}
|
||||
|
||||
const authenticatedUserId = context.userId
|
||||
const { variables, workflowId } = params || ({} as SetEnvironmentVariablesParams)
|
||||
|
||||
if (workflowId) {
|
||||
const { hasAccess } = await verifyWorkflowAccess(authenticatedUserId, workflowId)
|
||||
|
||||
if (!hasAccess) {
|
||||
const errorMessage = createPermissionError('modify environment variables in')
|
||||
logger.error('Unauthorized attempt to set environment variables', {
|
||||
workflowId,
|
||||
authenticatedUserId,
|
||||
})
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
const userId = authenticatedUserId
|
||||
|
||||
const normalized = normalizeVariables(variables || {})
|
||||
const { variables: validatedVariables } = EnvVarSchema.parse({ variables: normalized })
|
||||
const userId = await getUserId('copilot-set-env-vars', workflowId)
|
||||
if (!userId) {
|
||||
logger.warn('Unauthorized set env vars attempt')
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
|
||||
// Fetch existing
|
||||
const existingData = await db
|
||||
.select()
|
||||
.from(environment)
|
||||
@@ -57,7 +77,6 @@ export const setEnvironmentVariablesServerTool: BaseServerTool<SetEnvironmentVar
|
||||
.limit(1)
|
||||
const existingEncrypted = (existingData[0]?.variables as Record<string, string>) || {}
|
||||
|
||||
// Diff and (re)encrypt
|
||||
const toEncrypt: Record<string, string> = {}
|
||||
const added: string[] = []
|
||||
const updated: string[] = []
|
||||
|
||||
Reference in New Issue
Block a user