import { db } from '@sim/db' import { chat, workflow, workflowMcpServer, workflowMcpTool } from '@sim/db/schema' import { toError } from '@sim/utils/errors' import { and, eq, inArray, isNull } from 'drizzle-orm' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' import { performCreateWorkflowMcpServer, performDeleteWorkflowMcpServer, performUpdateWorkflowMcpServer, } from '@/lib/mcp/orchestration' import { generateWorkflowDiffSummary } from '@/lib/workflows/comparison' import { performActivateVersion, performRevertToVersion } from '@/lib/workflows/orchestration' import { listWorkflowVersions, updateDeploymentVersionMetadata, } from '@/lib/workflows/persistence/utils' import { checkNeedsRedeployment } from '@/app/api/workflows/utils' import { ensureWorkflowAccess, ensureWorkspaceAccess } from '../access' import type { CheckDeploymentStatusParams, CreateWorkspaceMcpServerParams, DeleteWorkspaceMcpServerParams, DiffWorkflowsParams, GetDeploymentLogParams, ListWorkspaceMcpServersParams, LoadDeploymentParams, PromoteToLiveParams, UpdateDeploymentVersionParams, UpdateWorkspaceMcpServerParams, } from '../param-types' import { resolveWorkflowStateRef } from './state-refs' export async function executeCheckDeploymentStatus( params: CheckDeploymentStatusParams, context: ExecutionContext ): Promise { try { const workflowId = params.workflowId || context.workflowId if (!workflowId) { return { success: false, error: 'workflowId is required' } } const { workflow: workflowRecord } = await ensureWorkflowAccess(workflowId, context.userId) const workspaceId = workflowRecord.workspaceId const [apiDeploy, chatDeploy] = await Promise.all([ db .select({ isDeployed: workflow.isDeployed, deployedAt: workflow.deployedAt }) .from(workflow) .where(eq(workflow.id, workflowId)) .limit(1), db .select({ id: chat.id, identifier: chat.identifier, title: chat.title, description: chat.description, authType: chat.authType, allowedEmails: chat.allowedEmails, outputConfigs: chat.outputConfigs, password: chat.password, customizations: chat.customizations, }) .from(chat) .where(and(eq(chat.workflowId, workflowId), isNull(chat.archivedAt))) .limit(1), ]) const isApiDeployed = apiDeploy[0]?.isDeployed || false const needsRedeployment = isApiDeployed ? await checkNeedsRedeployment(workflowId) : false const apiDetails = { isDeployed: isApiDeployed, deployedAt: apiDeploy[0]?.deployedAt || null, endpoint: isApiDeployed ? `/api/workflows/${workflowId}/execute` : null, apiKey: workflowRecord.workspaceId ? 'Workspace API keys' : 'Personal API keys', needsRedeployment, } const isChatDeployed = !!chatDeploy[0] const chatCustomizations = (chatDeploy[0]?.customizations as | { welcomeMessage?: string; primaryColor?: string } | undefined) || {} const chatDetails = { isDeployed: isChatDeployed, chatId: chatDeploy[0]?.id || null, identifier: chatDeploy[0]?.identifier || null, chatUrl: isChatDeployed ? `/chat/${chatDeploy[0]?.identifier}` : null, title: chatDeploy[0]?.title || null, description: chatDeploy[0]?.description || null, authType: chatDeploy[0]?.authType || null, allowedEmails: chatDeploy[0]?.allowedEmails || null, outputConfigs: chatDeploy[0]?.outputConfigs || null, welcomeMessage: chatCustomizations.welcomeMessage || null, primaryColor: chatCustomizations.primaryColor || null, hasPassword: Boolean(chatDeploy[0]?.password), } const mcpDetails: { isDeployed: boolean servers: Array<{ serverId: string serverName: string toolName: string toolDescription: string | null parameterSchema: unknown toolId: string }> } = { isDeployed: false, servers: [] } if (workspaceId) { const servers = await db .select({ serverId: workflowMcpServer.id, serverName: workflowMcpServer.name, toolName: workflowMcpTool.toolName, toolDescription: workflowMcpTool.toolDescription, parameterSchema: workflowMcpTool.parameterSchema, toolId: workflowMcpTool.id, }) .from(workflowMcpTool) .innerJoin(workflowMcpServer, eq(workflowMcpTool.serverId, workflowMcpServer.id)) .where(eq(workflowMcpTool.workflowId, workflowId)) if (servers.length > 0) { mcpDetails.isDeployed = true mcpDetails.servers = servers } } const isDeployed = apiDetails.isDeployed || chatDetails.isDeployed || mcpDetails.isDeployed return { success: true, output: { isDeployed, api: apiDetails, chat: chatDetails, mcp: mcpDetails }, } } catch (error) { return { success: false, error: toError(error).message } } } export async function executeListWorkspaceMcpServers( params: ListWorkspaceMcpServersParams, context: ExecutionContext ): Promise { try { let workspaceId = params.workspaceId || context.workspaceId const workflowId = context.workflowId if (!workspaceId && workflowId) { const { workflow: workflowRecord } = await ensureWorkflowAccess(workflowId, context.userId) workspaceId = workflowRecord.workspaceId ?? undefined } if (!workspaceId) { return { success: false, error: 'workspaceId is required' } } await ensureWorkspaceAccess(workspaceId, context.userId, 'read') const servers = await db .select({ id: workflowMcpServer.id, name: workflowMcpServer.name, description: workflowMcpServer.description, }) .from(workflowMcpServer) .where( and(eq(workflowMcpServer.workspaceId, workspaceId), isNull(workflowMcpServer.deletedAt)) ) const serverIds = servers.map((server) => server.id) const tools = serverIds.length > 0 ? await db .select({ serverId: workflowMcpTool.serverId, toolName: workflowMcpTool.toolName, }) .from(workflowMcpTool) .where( and(inArray(workflowMcpTool.serverId, serverIds), isNull(workflowMcpTool.archivedAt)) ) : [] const toolNamesByServer: Record = {} for (const tool of tools) { if (!toolNamesByServer[tool.serverId]) { toolNamesByServer[tool.serverId] = [] } toolNamesByServer[tool.serverId].push(tool.toolName) } const serversWithToolNames = servers.map((server) => ({ ...server, toolCount: toolNamesByServer[server.id]?.length || 0, toolNames: toolNamesByServer[server.id] || [], })) return { success: true, output: { servers: serversWithToolNames, count: servers.length } } } catch (error) { return { success: false, error: toError(error).message } } } export async function executeCreateWorkspaceMcpServer( params: CreateWorkspaceMcpServerParams, context: ExecutionContext ): Promise { try { let workspaceId = params.workspaceId || context.workspaceId const workflowId = context.workflowId if (!workspaceId && workflowId) { const { workflow: workflowRecord } = await ensureWorkflowAccess( workflowId, context.userId, 'write' ) workspaceId = workflowRecord.workspaceId ?? undefined } if (!workspaceId) { return { success: false, error: 'workspaceId is required' } } await ensureWorkspaceAccess(workspaceId, context.userId, 'admin') const name = params.name?.trim() if (!name) { return { success: false, error: 'name is required' } } const result = await performCreateWorkflowMcpServer({ workspaceId, userId: context.userId, name, description: params.description, isPublic: params.isPublic, workflowIds: params.workflowIds, }) if (!result.success) { return { success: false, error: result.error || 'Failed to create MCP server' } } return { success: true, output: { server: result.server, addedTools: result.addedTools || [] } } } catch (error) { return { success: false, error: toError(error).message } } } export async function executeUpdateWorkspaceMcpServer( params: UpdateWorkspaceMcpServerParams, context: ExecutionContext ): Promise { try { const serverId = params.serverId if (!serverId) { return { success: false, error: 'serverId is required' } } const updates: { name?: string; description?: string | null; isPublic?: boolean } = {} if (typeof params.name === 'string') { const name = params.name.trim() if (!name) return { success: false, error: 'name cannot be empty' } updates.name = name } if (typeof params.description === 'string') { updates.description = params.description.trim() || null } if (typeof params.isPublic === 'boolean') { updates.isPublic = params.isPublic } if (Object.keys(updates).length === 0) { return { success: false, error: 'At least one of name, description, or isPublic is required' } } const [existing] = await db .select({ id: workflowMcpServer.id, workspaceId: workflowMcpServer.workspaceId, }) .from(workflowMcpServer) .where(eq(workflowMcpServer.id, serverId)) .limit(1) if (!existing) { return { success: false, error: 'MCP server not found' } } await ensureWorkspaceAccess(existing.workspaceId, context.userId, 'write') const result = await performUpdateWorkflowMcpServer({ serverId, workspaceId: existing.workspaceId, userId: context.userId, ...updates, }) if (!result.success) { return { success: false, error: result.error || 'Failed to update MCP server' } } return { success: true, output: { serverId, ...updates } } } catch (error) { return { success: false, error: toError(error).message } } } export async function executeDeleteWorkspaceMcpServer( params: DeleteWorkspaceMcpServerParams, context: ExecutionContext ): Promise { try { const serverId = params.serverId if (!serverId) { return { success: false, error: 'serverId is required' } } const [existing] = await db .select({ id: workflowMcpServer.id, name: workflowMcpServer.name, workspaceId: workflowMcpServer.workspaceId, }) .from(workflowMcpServer) .where(and(eq(workflowMcpServer.id, serverId), isNull(workflowMcpServer.deletedAt))) .limit(1) if (!existing) { return { success: false, error: 'MCP server not found' } } await ensureWorkspaceAccess(existing.workspaceId, context.userId, 'admin') const result = await performDeleteWorkflowMcpServer({ serverId, workspaceId: existing.workspaceId, userId: context.userId, }) if (!result.success) { return { success: false, error: result.error || 'Failed to delete MCP server' } } return { success: true, output: { serverId, name: existing.name, deleted: true } } } catch (error) { return { success: false, error: toError(error).message } } } export async function executeGetDeploymentLog( params: GetDeploymentLogParams, context: ExecutionContext ): Promise { try { const workflowId = params.workflowId || context.workflowId if (!workflowId) { return { success: false, error: 'workflowId is required' } } await ensureWorkflowAccess(workflowId, context.userId) const { versions: rows } = await listWorkflowVersions(workflowId) const versions = rows.map((r) => ({ id: r.id, version: r.version, name: r.name ?? undefined, description: r.description ?? undefined, isActive: r.isActive, createdAt: r.createdAt.toISOString(), createdBy: r.createdBy ?? undefined, })) return { success: true, output: { workflowId, count: versions.length, versions } } } catch (error) { return { success: false, error: toError(error).message } } } // Cap individual sub-block before/after values so a large diff can't blow the // tool-result budget. Oversized values are replaced with an elision marker. const MAX_DIFF_VALUE_BYTES = 2000 function guardDiffValue(value: unknown): unknown { try { const json = JSON.stringify(value) if (json && json.length > MAX_DIFF_VALUE_BYTES) { return { elided: true, bytes: json.length } } } catch { return { elided: true, reason: 'unserializable' } } return value } export async function executeDiffWorkflows( params: DiffWorkflowsParams, context: ExecutionContext ): Promise { try { const workflowId = params.workflowId || context.workflowId if (!workflowId) { return { success: false, error: 'workflowId is required' } } if (params.ref1 === undefined || params.ref2 === undefined) { return { success: false, error: 'ref1 and ref2 are required' } } // resolveWorkflowStateRef enforces read access on the workflow. const [side1, side2] = await Promise.all([ resolveWorkflowStateRef(workflowId, params.ref1, context.userId), resolveWorkflowStateRef(workflowId, params.ref2, context.userId), ]) // ref1 = base/previous, ref2 = target/current: added = present in ref2 only. const summary = generateWorkflowDiffSummary(side2.state, side1.state) const diff = { ...summary, modifiedBlocks: summary.modifiedBlocks.map((block) => ({ ...block, changes: block.changes.map((change) => ({ field: change.field, oldValue: guardDiffValue(change.oldValue), newValue: guardDiffValue(change.newValue), })), })), } return { success: true, output: { workflowId, ref1: { ref: side1.ref, version: side1.version, isActive: side1.isActive }, ref2: { ref: side2.ref, version: side2.version, isActive: side2.isActive }, diff, }, } } catch (error) { return { success: false, error: toError(error).message } } } function resolveLoadVersion( raw: number | string ): { ok: true; version: number | 'active' } | { ok: false; error: string } { if (typeof raw === 'number' && Number.isFinite(raw)) return { ok: true, version: raw } if (typeof raw === 'string') { const t = raw.trim().toLowerCase() if (t === 'live' || t === 'active') return { ok: true, version: 'active' } if (t === 'draft' || t === 'current') { return { ok: false, error: 'Cannot load "draft" — load_deployment restores a deployed version into the draft', } } if (/^\d+$/.test(t)) return { ok: true, version: Number.parseInt(t, 10) } } return { ok: false, error: `Invalid version "${String(raw)}": expected a version number or "live"`, } } export async function executeLoadDeployment( params: LoadDeploymentParams, context: ExecutionContext ): Promise { try { const workflowId = params.workflowId || context.workflowId if (!workflowId) { return { success: false, error: 'workflowId is required' } } if (params.version === undefined || params.version === null) { return { success: false, error: 'version is required' } } const target = resolveLoadVersion(params.version) if (!target.ok) { return { success: false, error: target.error } } const { workflow: workflowRecord } = await ensureWorkflowAccess( workflowId, context.userId, 'admin' ) const result = await performRevertToVersion({ workflowId, version: target.version, userId: context.userId, workflow: workflowRecord as Record, }) if (!result.success) { return { success: false, error: result.error || 'Failed to load deployment' } } const label = target.version === 'active' ? 'the live deployment' : `version ${target.version}` return { success: true, output: { workflowId, message: `Loaded ${label} into the workflow draft`, lastSaved: result.lastSaved, }, } } catch (error) { return { success: false, error: toError(error).message } } } function normalizePromoteVersion(raw: number | string): number | null { if (typeof raw === 'number' && Number.isFinite(raw)) return raw if (typeof raw === 'string' && /^\d+$/.test(raw.trim())) return Number.parseInt(raw.trim(), 10) return null } export async function executePromoteToLive( params: PromoteToLiveParams, context: ExecutionContext ): Promise { try { const workflowId = params.workflowId || context.workflowId if (!workflowId) { return { success: false, error: 'workflowId is required' } } if (params.version === undefined || params.version === null) { return { success: false, error: 'version is required' } } const version = normalizePromoteVersion(params.version) if (version === null) { return { success: false, error: 'version must be a deployment version number (use load_deployment to change the draft; "live" is already live)', } } const { workflow: workflowRecord } = await ensureWorkflowAccess( workflowId, context.userId, 'admin' ) const result = await performActivateVersion({ workflowId, version, userId: context.userId, workflow: workflowRecord as Record, }) if (!result.success) { return { success: false, error: result.error || 'Failed to promote version' } } return { success: true, output: { workflowId, version, message: `Promoted version ${version} to live`, deployedAt: result.deployedAt ? new Date(result.deployedAt).toISOString() : undefined, warnings: result.warnings, }, } } catch (error) { return { success: false, error: toError(error).message } } } export async function executeUpdateDeploymentVersion( params: UpdateDeploymentVersionParams, context: ExecutionContext ): Promise { try { const workflowId = params.workflowId || context.workflowId if (!workflowId) { return { success: false, error: 'workflowId is required' } } if (params.version === undefined || params.version === null) { return { success: false, error: 'version is required' } } const version = normalizePromoteVersion(params.version) if (version === null) { return { success: false, error: 'version must be a deployment version number (use get_deployment_log to find it)', } } const name = typeof params.name === 'string' ? params.name.trim() : undefined const description = typeof params.description === 'string' ? params.description.trim() : undefined if (name === undefined && description === undefined) { return { success: false, error: 'Provide a name and/or description to update' } } await ensureWorkflowAccess(workflowId, context.userId, 'write') const updated = await updateDeploymentVersionMetadata({ workflowId, version, ...(name !== undefined ? { name: name || null } : {}), ...(description !== undefined ? { description: description || null } : {}), }) if (!updated) { return { success: false, error: `Deployment version ${version} not found` } } return { success: true, output: { workflowId, version, name: updated.name, description: updated.description }, } } catch (error) { return { success: false, error: toError(error).message } } }