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:
Waleed
2025-09-26 19:04:49 -07:00
committed by GitHub
parent 3ff6509028
commit a63f3a3d8d
18 changed files with 249 additions and 162 deletions

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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(),

View File

@@ -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({

View File

@@ -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({

View File

@@ -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({

View File

@@ -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({

View File

@@ -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() {

View File

@@ -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({

View File

@@ -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({

View File

@@ -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>
)

View 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`
}

View File

@@ -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>
}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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))

View File

@@ -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[] = []